1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2016 - TortoiseGit
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License
7 // as published by the Free Software Foundation; either version 2
8 // of the License, or (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software Foundation,
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 #include "..\TortoiseShell\resource.h"
23 #include "GitStatus.h"
24 #include "UnicodeUtils.h"
27 #include "ShellCache.h"
29 #include "SmartHandle.h"
31 extern CGitAdminDirMap g_AdminDirMap
;
32 extern CGitIndexFileMap g_IndexFileMap
;
33 CGitHeadFileMap g_HeadFileMap
;
34 CGitIgnoreList g_IgnoreList
;
36 GitStatus::GitStatus()
39 m_status
.assumeValid
= m_status
.skipWorktree
= false;
40 m_status
.prop_status
= m_status
.text_status
= git_wc_status_none
;
45 git_wc_status_kind
GitStatus::GetAllStatus(const CTGitPath
& path
, git_depth_t depth
, bool * assumeValid
, bool * skipWorktree
)
47 git_wc_status_kind statuskind
;
52 isDir
= path
.IsDirectory();
53 if (!path
.HasAdminDir(&sProjectRoot
))
54 return git_wc_status_none
;
56 // rev.kind = git_opt_revision_unspecified;
57 statuskind
= git_wc_status_none
;
59 const BOOL bIsRecursive
= (depth
== git_depth_infinity
|| depth
== git_depth_unknown
); // taken from SVN source
62 CString s
= path
.GetWinPathString();
63 if (s
.GetLength() > sProjectRoot
.GetLength())
65 if (sProjectRoot
.GetLength() == 3 && sProjectRoot
[1] == L
':')
66 sSubPath
= s
.Right(s
.GetLength() - sProjectRoot
.GetLength());
68 sSubPath
= s
.Right(s
.GetLength() - sProjectRoot
.GetLength() - 1/*otherwise it gets initial slash*/);
71 bool isfull
= ((DWORD
)CRegStdDWORD(L
"Software\\TortoiseGit\\CacheType",
72 GetSystemMetrics(SM_REMOTESESSION
) ? ShellCache::dll
: ShellCache::exe
) == ShellCache::dllFull
);
76 err
= GetDirStatus(sProjectRoot
, sSubPath
, &statuskind
, isfull
, bIsRecursive
, isfull
);
77 // folders must not be displayed as added or deleted only as modified (this is for Shell Overlay-Modes)
78 if (statuskind
== git_wc_status_unversioned
&& sSubPath
.IsEmpty())
79 statuskind
= git_wc_status_normal
;
80 else if (statuskind
== git_wc_status_deleted
|| statuskind
== git_wc_status_added
)
81 statuskind
= git_wc_status_modified
;
84 err
= GetFileStatus(sProjectRoot
, sSubPath
, &statuskind
, isfull
, false, isfull
, nullptr, nullptr, assumeValid
, skipWorktree
);
91 git_wc_status_kind
GitStatus::GetMoreImportant(git_wc_status_kind status1
, git_wc_status_kind status2
)
93 if (GetStatusRanking(status1
) >= GetStatusRanking(status2
))
97 // static private method
98 int GitStatus::GetStatusRanking(git_wc_status_kind status
)
102 case git_wc_status_none
:
104 case git_wc_status_unversioned
:
106 case git_wc_status_ignored
:
108 case git_wc_status_incomplete
:
110 case git_wc_status_normal
:
111 case git_wc_status_external
:
113 case git_wc_status_added
:
115 case git_wc_status_missing
:
117 case git_wc_status_deleted
:
119 case git_wc_status_replaced
:
121 case git_wc_status_modified
:
123 case git_wc_status_merged
:
125 case git_wc_status_conflicted
:
127 case git_wc_status_obstructed
:
134 void GitStatus::GetStatus(const CTGitPath
& path
, bool /*update*/ /* = false */, bool noignore
/* = false */, bool /*noexternals*/ /* = false */)
136 // NOTE: unlike the SVN version this one does not cache the enumerated files, because in practice no code in all of
137 // Tortoise uses this, all places that call GetStatus create a temp GitStatus object which gets destroyed right
138 // after the call again
140 CString sProjectRoot
;
141 if ( !path
.HasAdminDir(&sProjectRoot
) )
144 bool isfull
= ((DWORD
)CRegStdDWORD(L
"Software\\TortoiseGit\\CacheType",
145 GetSystemMetrics(SM_REMOTESESSION
) ? ShellCache::dll
: ShellCache::exe
) == ShellCache::dllFull
);
149 LPCTSTR lpszSubPath
= nullptr;
151 CString s
= path
.GetWinPathString();
152 if (s
.GetLength() > sProjectRoot
.GetLength())
154 sSubPath
= s
.Right(s
.GetLength() - sProjectRoot
.GetLength());
155 lpszSubPath
= sSubPath
;
156 // skip initial slash if necessary
157 if (*lpszSubPath
== L
'\\')
161 m_status
.prop_status
= m_status
.text_status
= git_wc_status_none
;
162 m_status
.assumeValid
= false;
163 m_status
.skipWorktree
= false;
165 if (path
.IsDirectory())
167 err
= GetDirStatus(sProjectRoot
, lpszSubPath
, &m_status
.text_status
, isfull
, false, !noignore
);
168 if (m_status
.text_status
== git_wc_status_added
|| m_status
.text_status
== git_wc_status_deleted
) // fix for issue #1769; a folder is either modified, conflicted or normal
169 m_status
.text_status
= git_wc_status_modified
;
172 err
= GetFileStatus(sProjectRoot
, lpszSubPath
, &m_status
.text_status
, isfull
, false, !noignore
, nullptr, nullptr, &m_status
.assumeValid
, &m_status
.skipWorktree
);
174 // Error present if function is not under version control
185 typedef CComCritSecLock
<CComCriticalSection
> CAutoLocker
;
187 int GitStatus::GetFileStatus(const CString
& gitdir
, CString path
, git_wc_status_kind
* status
, BOOL IsFull
, BOOL
/*IsRecursive*/, BOOL IsIgnore
, FILL_STATUS_CALLBACK callback
, void* pData
, bool* assumeValid
, bool* skipWorktree
)
192 path
.Replace(L
'\\', L
'/');
194 CString lowcasepath
= path
;
195 lowcasepath
.MakeLower();
197 git_wc_status_kind st
= git_wc_status_none
;
200 g_IndexFileMap
.GetFileStatus(gitdir
, path
, &st
, IsFull
, false, callback
, pData
, &hash
, true, assumeValid
, skipWorktree
);
202 if (st
== git_wc_status_conflicted
)
205 if (callback
&& assumeValid
&& skipWorktree
)
206 callback(CombinePath(gitdir
, path
), st
, false, pData
, *assumeValid
, *skipWorktree
);
210 if (st
== git_wc_status_unversioned
)
214 *status
= git_wc_status_unversioned
;
215 if (callback
&& assumeValid
&& skipWorktree
)
216 callback(CombinePath(gitdir
, path
), *status
, false, pData
, *assumeValid
, *skipWorktree
);
220 g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, path
, false);
221 if (g_IgnoreList
.IsIgnore(path
, gitdir
, false))
222 st
= git_wc_status_ignored
;
225 if (callback
&& assumeValid
&& skipWorktree
)
226 callback(CombinePath(gitdir
, path
), st
, false, pData
, *assumeValid
, *skipWorktree
);
231 if ((st
== git_wc_status_normal
|| st
== git_wc_status_modified
) && IsFull
)
233 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
235 // Check Head Tree Hash
236 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
239 size_t start
= SearchInSortVector(*treeptr
, lowcasepath
, -1);
242 *status
= st
= git_wc_status_added
;
243 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": File miss in head tree %s", (LPCTSTR
)path
);
244 if (callback
&& assumeValid
&& skipWorktree
)
245 callback(CombinePath(gitdir
, path
), st
, false, pData
, *assumeValid
, *skipWorktree
);
249 // staged and not commit
250 if ((*treeptr
)[start
].m_Hash
!= hash
)
252 *status
= st
= git_wc_status_modified
;
253 if (callback
&& assumeValid
&& skipWorktree
)
254 callback(CombinePath(gitdir
, path
), st
, false, pData
, *assumeValid
, *skipWorktree
);
259 if (callback
&& assumeValid
&& skipWorktree
)
260 callback(CombinePath(gitdir
, path
), st
, false, pData
, *assumeValid
, *skipWorktree
);
265 bool GitStatus::CheckAndUpdateIgnoreFiles(const CString
& gitdir
, const CString
& subpaths
, bool isDir
)
267 return g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, subpaths
, isDir
);
269 int GitStatus::IsUnderVersionControl(const CString
&gitdir
, const CString
&path
, bool isDir
,bool *isVersion
)
271 if (g_IndexFileMap
.IsUnderVersionControl(gitdir
, path
, isDir
, isVersion
))
274 return g_HeadFileMap
.IsUnderVersionControl(gitdir
, path
, isDir
, isVersion
);
278 bool GitStatus::IsIgnored(const CString
& gitdir
, const CString
& path
, bool isDir
)
280 return g_IgnoreList
.IsIgnore(path
, gitdir
, isDir
);
283 int GitStatus::GetFileList(CString path
, std::vector
<CGitFileName
> &list
)
286 WIN32_FIND_DATA data
;
287 CAutoFindFile handle
= ::FindFirstFileEx(path
, SysInfo::Instance().IsWin7OrLater() ? FindExInfoBasic
: FindExInfoStandard
, &data
, FindExSearchNameMatch
, nullptr, SysInfo::Instance().IsWin7OrLater() ? FIND_FIRST_EX_LARGE_FETCH
: 0);
292 if (wcscmp(data
.cFileName
, L
".git") == 0)
295 if (wcscmp(data
.cFileName
, L
".") == 0)
298 if (wcscmp(data
.cFileName
, L
"..") == 0)
301 CGitFileName filename
;
303 filename
.m_CaseFileName
= filename
.m_FileName
= data
.cFileName
;
304 filename
.m_FileName
.MakeLower();
306 if(data
.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
)
307 filename
.m_FileName
+= L
'/';
309 list
.push_back(filename
);
311 }while(::FindNextFile(handle
, &data
));
313 handle
.CloseHandle(); // manually close handle here in order to keep handles open as short as possible
315 std::sort(list
.begin(), list
.end(), SortCGitFileName
);
319 int GitStatus::EnumDirStatus(const CString
&gitdir
, const CString
&subpath
, git_wc_status_kind
* status
,BOOL IsFul
, BOOL IsRecursive
, BOOL IsIgnore
, FILL_STATUS_CALLBACK callback
, void *pData
)
324 CString path
= subpath
;
326 path
.Replace(L
'\\', L
'/');
327 if (!path
.IsEmpty() && path
[path
.GetLength() - 1] != L
'/')
328 path
+= L
'/'; // Add trail / to show it is directory, not file name.
330 std::vector
<CGitFileName
> filelist
;
331 GetFileList(CombinePath(gitdir
, subpath
), filelist
);
333 g_IndexFileMap
.CheckAndUpdate(gitdir
,true);
335 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
337 SHARED_INDEX_PTR indexptr
= g_IndexFileMap
.SafeGet(gitdir
);
338 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
340 // new git working tree has no index file
343 for (auto it
= filelist
.cbegin(); it
!= filelist
.cend(); ++it
)
345 CString casepath
= path
;
346 casepath
+= it
->m_CaseFileName
;
349 if (!it
->m_FileName
.IsEmpty() && it
->m_FileName
[it
->m_FileName
.GetLength() - 1] == L
'/')
354 g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, casepath
, bIsDir
);
355 if (g_IgnoreList
.IsIgnore(casepath
, gitdir
, bIsDir
))
356 *status
= git_wc_status_ignored
;
360 *status
= git_wc_status_unversioned
;
365 *status
= git_wc_status_unversioned
;
368 callback(CombinePath(gitdir
, casepath
), *status
, bIsDir
, pData
, false, false);
373 CString lowcasepath
= path
;
374 lowcasepath
.MakeLower();
376 for (auto it
= filelist
.cbegin(), itend
= filelist
.cend(); it
!= itend
; ++it
)
378 CString
onepath(lowcasepath
);
379 onepath
+= it
->m_FileName
;
380 CString
casepath(path
);
381 casepath
+= it
->m_CaseFileName
;
384 if (!onepath
.IsEmpty() && onepath
[onepath
.GetLength() - 1] == L
'/')
387 int matchLength
= -1;
389 matchLength
= onepath
.GetLength();
390 size_t pos
= SearchInSortVector(*indexptr
, onepath
, matchLength
);
391 size_t posintree
= SearchInSortVector(*treeptr
, onepath
, matchLength
);
393 if (pos
== NPOS
&& posintree
== NPOS
)
395 if (onepath
.IsEmpty())
400 *status
= git_wc_status_unversioned
;
402 callback(CombinePath(gitdir
, casepath
), *status
, bIsDir
, pData
, false, false);
406 g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, casepath
, bIsDir
);
407 if (g_IgnoreList
.IsIgnore(casepath
, gitdir
, bIsDir
))
408 *status
= git_wc_status_ignored
;
410 *status
= git_wc_status_unversioned
;
413 callback(CombinePath(gitdir
, casepath
), *status
, bIsDir
, pData
, false, false);
415 else if (pos
== NPOS
&& posintree
!= NPOS
) /* check if file delete in index */
417 *status
= git_wc_status_deleted
;
419 callback(CombinePath(gitdir
, casepath
), *status
, bIsDir
, pData
, false, false);
421 else if (pos
!= NPOS
&& posintree
== NPOS
) /* Check if file added */
423 *status
= git_wc_status_added
;
424 if ((*indexptr
)[pos
].m_Flags
& GIT_IDXENTRY_STAGEMASK
)
425 *status
= git_wc_status_conflicted
;
427 callback(CombinePath(gitdir
, casepath
), *status
, bIsDir
, pData
, false, false);
431 if (onepath
.IsEmpty())
436 *status
= git_wc_status_normal
;
438 callback(CombinePath(gitdir
, casepath
), *status
, bIsDir
, pData
, false, false);
442 bool assumeValid
= false;
443 bool skipWorktree
= false;
444 git_wc_status_kind filestatus
;
445 GetFileStatus(gitdir
, casepath
, &filestatus
, IsFul
, IsRecursive
, IsIgnore
, callback
, pData
, &assumeValid
, &skipWorktree
);
450 /* Check deleted file in system */
451 size_t start
= 0, end
= 0;
452 size_t pos
= SearchInSortVector(*indexptr
, lowcasepath
, lowcasepath
.GetLength()); // match path prefix, (sub)folders end with slash
453 std::set
<CString
> skipWorktreeSet
;
455 if (GetRangeInSortVector(*indexptr
, lowcasepath
, lowcasepath
.GetLength(), &start
, &end
, pos
) == 0)
458 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; it
<= itlast
; ++it
)
461 int commonPrefixLength
= lowcasepath
.GetLength();
462 int index
= entry
.m_FileName
.Find(L
'/', commonPrefixLength
);
464 index
= entry
.m_FileName
.GetLength();
466 ++index
; // include slash at the end for subfolders, so that we do not match files by mistake
468 CString filename
= entry
.m_FileName
.Mid(commonPrefixLength
, index
- commonPrefixLength
);
469 if (oldstring
!= filename
)
471 oldstring
= filename
;
472 if (SearchInSortVector(filelist
, filename
, filename
.GetLength()) == NPOS
)
474 bool skipWorktree
= false;
475 *status
= git_wc_status_deleted
;
476 if ((entry
.m_FlagsExtended
& GIT_IDXENTRY_SKIP_WORKTREE
) != 0)
478 skipWorktreeSet
.insert(filename
);
480 *status
= git_wc_status_normal
;
483 callback(CombinePath(gitdir
, entry
.m_FileName
), *status
, false, pData
, false, skipWorktree
);
490 pos
= SearchInSortVector(*treeptr
, lowcasepath
, lowcasepath
.GetLength()); // match path prefix, (sub)folders end with slash
491 if (GetRangeInSortVector(*treeptr
, lowcasepath
, lowcasepath
.GetLength(), &start
, &end
, pos
) == 0)
494 for (auto it
= treeptr
->cbegin() + start
, itlast
= treeptr
->cbegin() + end
; it
<= itlast
; ++it
)
497 int commonPrefixLength
= lowcasepath
.GetLength();
498 int index
= entry
.m_FileName
.Find(L
'/', commonPrefixLength
);
500 index
= entry
.m_FileName
.GetLength();
502 ++index
; // include slash at the end for subfolders, so that we do not match files by mistake
504 CString filename
= entry
.m_FileName
.Mid(commonPrefixLength
, index
- commonPrefixLength
);
505 if (oldstring
!= filename
&& skipWorktreeSet
.find(filename
) == skipWorktreeSet
.cend())
507 oldstring
= filename
;
508 if (SearchInSortVector(filelist
, filename
, filename
.GetLength()) == NPOS
)
510 *status
= git_wc_status_deleted
;
512 callback(CombinePath(gitdir
, entry
.m_FileName
), *status
, false, pData
, false, false);
522 int GitStatus::GetDirStatus(const CString
& gitdir
, const CString
& subpath
, git_wc_status_kind
* status
, BOOL IsFul
, BOOL IsRecursive
, BOOL IsIgnore
)
527 CString path
= subpath
;
529 path
.Replace(L
'\\', L
'/');
530 if (!path
.IsEmpty() && path
[path
.GetLength() - 1] != L
'/')
531 path
+= L
'/'; //Add trail / to show it is directory, not file name.
533 g_IndexFileMap
.CheckAndUpdate(gitdir
, true);
535 SHARED_INDEX_PTR indexptr
= g_IndexFileMap
.SafeGet(gitdir
);
539 *status
= git_wc_status_unversioned
;
543 CString lowcasepath
= path
;
544 lowcasepath
.MakeLower();
546 size_t pos
= SearchInSortVector(*indexptr
, lowcasepath
, lowcasepath
.GetLength());
548 // Not In Version Contorl
553 *status
= git_wc_status_unversioned
;
557 // Check ignore always.
558 g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, path
, true);
559 if (g_IgnoreList
.IsIgnore(path
, gitdir
, true))
560 *status
= git_wc_status_ignored
;
562 *status
= git_wc_status_unversioned
;
564 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
566 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
567 // Check init repository
568 if (treeptr
->HeadIsEmpty() && path
.IsEmpty())
569 *status
= git_wc_status_normal
;
570 // check if only one file in repository is deleted in index
571 else if (path
.IsEmpty() && !treeptr
->empty())
572 *status
= git_wc_status_deleted
;
577 // In version control
578 *status
= git_wc_status_normal
;
583 GetRangeInSortVector(*indexptr
, lowcasepath
, lowcasepath
.GetLength(), &start
, &end
, pos
);
586 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; indexptr
->m_bHasConflicts
&& it
<= itlast
; ++it
)
588 if (((*it
).m_Flags
& GIT_IDXENTRY_STAGEMASK
) != 0)
590 *status
= git_wc_status_conflicted
;
595 if (IsFul
&& (*status
!= git_wc_status_conflicted
))
597 *status
= git_wc_status_normal
;
599 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
603 // Check if new init repository
604 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
606 if (!treeptr
->empty() || treeptr
->HeadIsEmpty())
608 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; it
<= itlast
; ++it
)
610 pos
= SearchInSortVector(*treeptr
, (*it
).m_FileName
, -1);
614 *status
= max(git_wc_status_added
, *status
); // added file found
618 if ((*treeptr
)[pos
].m_Hash
!= (*it
).m_IndexHash
)
620 *status
= max(git_wc_status_modified
, *status
); // modified file found
626 if (*status
== git_wc_status_normal
)
628 pos
= SearchInSortVector(*treeptr
, lowcasepath
, lowcasepath
.GetLength());
630 *status
= max(git_wc_status_added
, *status
); // added file found
634 // we know that pos exists in treeptr
635 GetRangeInSortVector(*treeptr
, lowcasepath
, lowcasepath
.GetLength(), &hstart
, &hend
, pos
);
636 for (auto hit
= treeptr
->cbegin() + hstart
, lastElement
= treeptr
->cbegin() + hend
; hit
<= lastElement
; ++hit
)
638 if (SearchInSortVector(*indexptr
, (*hit
).m_FileName
, -1) == NPOS
)
640 *status
= max(git_wc_status_deleted
, *status
); // deleted file found
650 // When status == git_wc_status_conflicted, needn't check each file status
651 // because git_wc_status_conflicted is highest.s
652 if (*status
== git_wc_status_conflicted
)
655 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; it
<= itlast
; ++it
)
657 //skip child directory
658 if (!IsRecursive
&& (*it
).m_FileName
.Find(L
'/', path
.GetLength()) > 0)
661 git_wc_status_kind filestatus
= git_wc_status_none
;
662 bool assumeValid
= false;
663 bool skipWorktree
= false;
664 GetFileStatus(gitdir
, (*it
).m_FileName
, &filestatus
, IsFul
, IsRecursive
, IsIgnore
, nullptr, nullptr, &assumeValid
, &skipWorktree
);
667 case git_wc_status_added
:
668 case git_wc_status_modified
:
669 case git_wc_status_deleted
:
670 case git_wc_status_conflicted
:
671 *status
= GetMoreImportant(filestatus
, *status
);
680 bool GitStatus::IsExistIndexLockFile(CString sDirName
)
682 if (!PathIsDirectory(sDirName
))
684 int x
= sDirName
.ReverseFind(L
'\\');
688 sDirName
.Truncate(x
);
693 if (PathFileExists(CombinePath(sDirName
, L
".git")))
695 if (PathFileExists(g_AdminDirMap
.GetAdminDirConcat(sDirName
, L
"index.lock")))
701 int x
= sDirName
.ReverseFind(L
'\\');
705 sDirName
.Truncate(x
);
710 bool GitStatus::ReleasePath(const CString
&gitdir
)
712 g_IndexFileMap
.SafeClear(gitdir
);
713 g_HeadFileMap
.SafeClear(gitdir
);
717 bool GitStatus::ReleasePathsRecursively(const CString
&rootpath
)
719 g_IndexFileMap
.SafeClearRecursively(rootpath
);
720 g_HeadFileMap
.SafeClearRecursively(rootpath
);