1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2017 - 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 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
.status
= git_wc_status_none
;
45 int GitStatus::GetAllStatus(const CTGitPath
& path
, bool bIsRecursive
, git_wc_status2_t
& status
)
50 isDir
= path
.IsDirectory();
51 if (!path
.HasAdminDir(&sProjectRoot
))
52 return git_wc_status_none
;
55 CString s
= path
.GetWinPathString();
56 if (s
.GetLength() > sProjectRoot
.GetLength())
58 if (sProjectRoot
.GetLength() == 3 && sProjectRoot
[1] == L
':')
59 sSubPath
= s
.Right(s
.GetLength() - sProjectRoot
.GetLength());
61 sSubPath
= s
.Right(s
.GetLength() - sProjectRoot
.GetLength() - 1/*otherwise it gets initial slash*/);
64 bool isfull
= ((DWORD
)CRegStdDWORD(L
"Software\\TortoiseGit\\CacheType",
65 GetSystemMetrics(SM_REMOTESESSION
) ? ShellCache::dll
: ShellCache::exe
) == ShellCache::dllFull
);
69 auto err
= GetDirStatus(sProjectRoot
, sSubPath
, &status
.status
, isfull
, bIsRecursive
, isfull
);
70 AdjustFolderStatus(status
.status
);
74 return GetFileStatus(sProjectRoot
, sSubPath
, status
, isfull
, isfull
);
79 git_wc_status_kind
GitStatus::GetMoreImportant(git_wc_status_kind status1
, git_wc_status_kind status2
)
81 if (GetStatusRanking(status1
) >= GetStatusRanking(status2
))
85 // static private method
86 int GitStatus::GetStatusRanking(git_wc_status_kind status
)
90 case git_wc_status_none
:
92 case git_wc_status_unversioned
:
94 case git_wc_status_ignored
:
96 case git_wc_status_normal
:
97 case git_wc_status_added
:
99 case git_wc_status_deleted
:
101 case git_wc_status_modified
:
103 case git_wc_status_conflicted
:
110 void GitStatus::GetStatus(const CTGitPath
& path
, bool /*update*/ /* = false */, bool noignore
/* = false */, bool /*noexternals*/ /* = false */)
112 // NOTE: unlike the SVN version this one does not cache the enumerated files, because in practice no code in all of
113 // Tortoise uses this, all places that call GetStatus create a temp GitStatus object which gets destroyed right
114 // after the call again
116 CString sProjectRoot
;
117 if ( !path
.HasAdminDir(&sProjectRoot
) )
120 bool isfull
= ((DWORD
)CRegStdDWORD(L
"Software\\TortoiseGit\\CacheType",
121 GetSystemMetrics(SM_REMOTESESSION
) ? ShellCache::dll
: ShellCache::exe
) == ShellCache::dllFull
);
125 LPCTSTR lpszSubPath
= nullptr;
127 CString s
= path
.GetWinPathString();
128 if (s
.GetLength() > sProjectRoot
.GetLength())
130 sSubPath
= s
.Right(s
.GetLength() - sProjectRoot
.GetLength());
131 lpszSubPath
= sSubPath
;
132 // skip initial slash if necessary
133 if (*lpszSubPath
== L
'\\')
137 m_status
.status
= git_wc_status_none
;
138 m_status
.assumeValid
= false;
139 m_status
.skipWorktree
= false;
141 if (path
.IsDirectory())
143 err
= GetDirStatus(sProjectRoot
, lpszSubPath
, &m_status
.status
, isfull
, false, !noignore
);
144 AdjustFolderStatus(m_status
.status
);
147 err
= GetFileStatus(sProjectRoot
, lpszSubPath
, m_status
, isfull
, !noignore
);
149 // Error present if function is not under version control
160 typedef CComCritSecLock
<CComCriticalSection
> CAutoLocker
;
162 int GitStatus::GetFileStatus(const CString
& gitdir
, CString path
, git_wc_status2_t
& status
, BOOL IsFull
, BOOL IsIgnore
, bool update
)
164 ATLASSERT(!status
.assumeValid
&& !status
.skipWorktree
);
166 path
.Replace(L
'\\', L
'/');
169 g_IndexFileMap
.CheckAndUpdate(gitdir
);
170 auto pIndex
= g_IndexFileMap
.SafeGet(gitdir
);
172 if (!pIndex
|| pIndex
->GetFileStatus(gitdir
, path
, status
, &hash
))
174 // git working tree has broken index or an error occurred in GetFileStatus
175 status
.status
= git_wc_status_none
;
179 if (status
.status
== git_wc_status_conflicted
)
182 if (status
.status
== git_wc_status_unversioned
)
187 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
189 // Check Head Tree Hash
190 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
194 status
.status
= git_wc_status_none
;
198 // deleted only in index item?
199 if (SearchInSortVector(*treeptr
, path
, -1) != NPOS
)
201 status
.status
= git_wc_status_deleted
;
208 status
.status
= git_wc_status_unversioned
;
212 g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, path
, false);
213 if (g_IgnoreList
.IsIgnore(path
, gitdir
, false))
214 status
.status
= git_wc_status_ignored
;
219 if ((status
.status
== git_wc_status_normal
|| status
.status
== git_wc_status_modified
) && IsFull
)
222 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
224 // Check Head Tree Hash
225 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
229 status
.status
= git_wc_status_none
;
234 size_t start
= SearchInSortVector(*treeptr
, path
, -1);
237 status
.status
= git_wc_status_added
;
238 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": File miss in head tree %s", (LPCTSTR
)path
);
242 // staged and not commit
243 if ((*treeptr
)[start
].m_Hash
!= hash
)
245 status
= { git_wc_status_modified
, false, false };
253 // checks whether indexPath is a direct submodule and not one in a subfolder
254 static bool IsDirectSubmodule(const CString
& indexPath
, int prefix
)
256 if (!CStringUtils::EndsWith(indexPath
, L
'/'))
259 auto ptr
= indexPath
.GetString() + prefix
;
268 return folderdepth
== 1;
272 bool GitStatus::CheckAndUpdateIgnoreFiles(const CString
& gitdir
, const CString
& subpaths
, bool isDir
)
274 return g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, subpaths
, isDir
);
277 bool GitStatus::IsIgnored(const CString
& gitdir
, const CString
& path
, bool isDir
)
279 return g_IgnoreList
.IsIgnore(path
, gitdir
, isDir
);
282 int GitStatus::GetFileList(const CString
& path
, std::vector
<CGitFileName
>& list
, bool& isRepoRoot
)
284 WIN32_FIND_DATA data
;
285 CAutoFindFile handle
= ::FindFirstFileEx(CombinePath(path
, L
"*.*"), FindExInfoBasic
, &data
, FindExSearchNameMatch
, nullptr, FIND_FIRST_EX_LARGE_FETCH
);
290 if (wcscmp(data
.cFileName
, L
".git") == 0)
296 if (wcscmp(data
.cFileName
, L
".") == 0)
299 if (wcscmp(data
.cFileName
, L
"..") == 0)
302 CGitFileName
filename(data
.cFileName
, ((__int64
)data
.nFileSizeHigh
<< 32) + data
.nFileSizeLow
, ((__int64
)data
.ftLastWriteTime
.dwHighDateTime
<< 32) + data
.ftLastWriteTime
.dwLowDateTime
);
303 if ((data
.dwFileAttributes
& FILE_ATTRIBUTE_REPARSE_POINT
) && !CPathUtils::ReadLink(CombinePath(path
, filename
.m_FileName
)))
304 filename
.m_bSymlink
= true;
305 else if (data
.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
)
306 filename
.m_FileName
+= L
'/';
308 list
.push_back(filename
);
310 }while(::FindNextFile(handle
, &data
));
312 handle
.CloseHandle(); // manually close handle here in order to keep handles open as short as possible
314 std::sort(list
.begin(), list
.end(), SortCGitFileName
);
318 int GitStatus::EnumDirStatus(const CString
& gitdir
, const CString
& subpath
, git_wc_status_kind
* dirstatus
, FILL_STATUS_CALLBACK callback
, void* pData
)
320 CString path
= subpath
;
322 path
.Replace(L
'\\', L
'/');
323 if (!path
.IsEmpty() && path
[path
.GetLength() - 1] != L
'/')
324 path
+= L
'/'; // Add trail / to show it is directory, not file name.
326 std::vector
<CGitFileName
> filelist
;
327 bool isRepoRoot
= false;
328 GetFileList(CombinePath(gitdir
, subpath
), filelist
, isRepoRoot
);
329 *dirstatus
= git_wc_status_unknown
;
331 *dirstatus
= git_wc_status_normal
;
333 g_IndexFileMap
.CheckAndUpdate(gitdir
);
335 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
337 SHARED_INDEX_PTR indexptr
= g_IndexFileMap
.SafeGet(gitdir
);
338 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
340 // there was an error loading the index or the HEAD commit/tree
341 if (!indexptr
|| !treeptr
)
344 CAutoRepository repository
;
345 for (auto it
= filelist
.cbegin(), itend
= filelist
.cend(); it
!= itend
; ++it
)
347 auto& fileentry
= *it
;
349 CString
onepath(path
);
350 onepath
+= fileentry
.m_FileName
;
353 if (!onepath
.IsEmpty() && onepath
[onepath
.GetLength() - 1] == L
'/')
356 int matchLength
= -1;
358 matchLength
= onepath
.GetLength();
359 size_t pos
= SearchInSortVector(*indexptr
, onepath
, matchLength
);
360 size_t posintree
= SearchInSortVector(*treeptr
, onepath
, matchLength
);
362 git_wc_status2_t status
= { git_wc_status_none
, false, false };
364 if (pos
== NPOS
&& posintree
== NPOS
)
366 status
.status
= git_wc_status_unversioned
;
368 g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, onepath
, bIsDir
);
369 if (g_IgnoreList
.IsIgnore(onepath
, gitdir
, bIsDir
))
370 status
.status
= git_wc_status_ignored
;
372 callback(CombinePath(gitdir
, onepath
), &status
, bIsDir
, fileentry
.m_LastModified
, pData
);
374 else if (pos
== NPOS
&& posintree
!= NPOS
) /* check if file delete in index */
376 status
.status
= git_wc_status_deleted
;
377 callback(CombinePath(gitdir
, onepath
), &status
, bIsDir
, fileentry
.m_LastModified
, pData
);
379 else if (pos
!= NPOS
&& posintree
== NPOS
) /* Check if file added */
381 status
.status
= git_wc_status_added
;
382 if ((*indexptr
)[pos
].m_Flags
& GIT_IDXENTRY_STAGEMASK
)
383 status
.status
= git_wc_status_conflicted
;
384 callback(CombinePath(gitdir
, onepath
), &status
, bIsDir
, fileentry
.m_LastModified
, pData
);
390 status
.status
= git_wc_status_normal
;
391 callback(CombinePath(gitdir
, onepath
), &status
, bIsDir
, fileentry
.m_LastModified
, pData
);
395 auto& indexentry
= (*indexptr
)[pos
];
396 if (indexentry
.m_Flags
& GIT_IDXENTRY_STAGEMASK
)
398 status
.status
= git_wc_status_conflicted
;
399 callback(CombinePath(gitdir
, onepath
), &status
, false, fileentry
.m_LastModified
, pData
);
402 if ((*indexptr
).GetFileStatus(repository
, gitdir
, indexentry
, status
, fileentry
.m_LastModified
, fileentry
.m_Size
, fileentry
.m_bSymlink
))
404 if (status
.status
== git_wc_status_normal
&& (*treeptr
)[posintree
].m_Hash
!= indexentry
.m_IndexHash
)
405 status
= { git_wc_status_modified
, false, false };
406 callback(CombinePath(gitdir
, onepath
), &status
, false, fileentry
.m_LastModified
, pData
);
410 repository
.Free(); // explicitly free the handle here in order to keep an open repository as short as possible
412 /* Check deleted file in system */
413 size_t start
= 0, end
= 0;
414 size_t pos
= SearchInSortVector(*indexptr
, path
, path
.GetLength()); // match path prefix, (sub)folders end with slash
415 std::set
<CString
> alreadyReported
;
417 if (GetRangeInSortVector(*indexptr
, path
, path
.GetLength(), &start
, &end
, pos
) == 0)
419 *dirstatus
= git_wc_status_normal
; // here we know that this folder has versioned entries
421 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; it
<= itlast
; ++it
)
424 int commonPrefixLength
= path
.GetLength();
425 int index
= entry
.m_FileName
.Find(L
'/', commonPrefixLength
);
427 index
= entry
.m_FileName
.GetLength();
429 ++index
; // include slash at the end for subfolders, so that we do not match files by mistake
431 CString filename
= entry
.m_FileName
.Mid(commonPrefixLength
, index
- commonPrefixLength
);
432 if (oldstring
!= filename
)
434 oldstring
= filename
;
435 int length
= filename
.GetLength();
436 bool isDir
= filename
[length
- 1] == L
'/';
437 if (SearchInSortVector(filelist
, filename
, isDir
? length
: -1) == NPOS
) // do full match for filenames and only prefix-match ending with "/" for folders
439 git_wc_status2_t status
= { (!isDir
|| IsDirectSubmodule(entry
.m_FileName
, commonPrefixLength
)) ? git_wc_status_deleted
: git_wc_status_modified
, false, false }; // only report deleted submodules and files as deletedy
440 if ((entry
.m_FlagsExtended
& GIT_IDXENTRY_SKIP_WORKTREE
) != 0)
442 status
.skipWorktree
= true;
443 status
.status
= git_wc_status_normal
;
444 oldstring
.Empty(); // without this a deleted folder which has two versioned files and only the first is skipwoktree flagged gets reported as normal
445 if (alreadyReported
.find(filename
) != alreadyReported
.cend())
448 alreadyReported
.insert(filename
);
449 callback(CombinePath(gitdir
, subpath
, filename
), &status
, isDir
, 0, pData
);
452 // folder might be replaced by symlink
453 filename
.TrimRight(L
'/');
454 auto filepos
= SearchInSortVector(filelist
, filename
, -1);
455 if (filepos
== NPOS
|| !filelist
[filepos
].m_bSymlink
)
457 status
.status
= git_wc_status_deleted
;
458 callback(CombinePath(gitdir
, subpath
, filename
), &status
, false, 0, pData
);
466 pos
= SearchInSortVector(*treeptr
, path
, path
.GetLength()); // match path prefix, (sub)folders end with slash
467 if (GetRangeInSortVector(*treeptr
, path
, path
.GetLength(), &start
, &end
, pos
) == 0)
469 *dirstatus
= git_wc_status_normal
; // here we know that this folder has versioned entries
471 for (auto it
= treeptr
->cbegin() + start
, itlast
= treeptr
->cbegin() + end
; it
<= itlast
; ++it
)
474 int commonPrefixLength
= path
.GetLength();
475 int index
= entry
.m_FileName
.Find(L
'/', commonPrefixLength
);
477 index
= entry
.m_FileName
.GetLength();
479 ++index
; // include slash at the end for subfolders, so that we do not match files by mistake
481 CString filename
= entry
.m_FileName
.Mid(commonPrefixLength
, index
- commonPrefixLength
);
482 if (oldstring
!= filename
&& alreadyReported
.find(filename
) == alreadyReported
.cend())
484 oldstring
= filename
;
485 int length
= filename
.GetLength();
486 bool isDir
= filename
[length
- 1] == L
'/';
487 if (SearchInSortVector(filelist
, filename
, isDir
? length
: -1) == NPOS
) // do full match for filenames and only prefix-match ending with "/" for folders
489 git_wc_status2_t status
= { (!isDir
|| IsDirectSubmodule(entry
.m_FileName
, commonPrefixLength
)) ? git_wc_status_deleted
: git_wc_status_modified
, false, false };
490 callback(CombinePath(gitdir
, subpath
, filename
), &status
, isDir
, 0, pData
);
500 int GitStatus::GetDirStatus(const CString
& gitdir
, const CString
& subpath
, git_wc_status_kind
* status
, BOOL IsFul
, BOOL IsRecursive
, BOOL IsIgnore
)
504 CString path
= subpath
;
506 path
.Replace(L
'\\', L
'/');
507 if (!path
.IsEmpty() && path
[path
.GetLength() - 1] != L
'/')
508 path
+= L
'/'; //Add trail / to show it is directory, not file name.
510 g_IndexFileMap
.CheckAndUpdate(gitdir
);
512 SHARED_INDEX_PTR indexptr
= g_IndexFileMap
.SafeGet(gitdir
);
517 *status
= git_wc_status_none
;
521 size_t pos
= SearchInSortVector(*indexptr
, path
, path
.GetLength());
523 // Not In Version Contorl
528 // WC root is at least normal if there are no files added/deleted
529 if (subpath
.IsEmpty())
531 *status
= git_wc_status_normal
;
534 *status
= git_wc_status_unversioned
;
538 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
540 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
544 *status
= git_wc_status_none
;
548 // check whether there files in head with are not in index
549 pos
= SearchInSortVector(*treeptr
, path
, path
.GetLength());
552 *status
= git_wc_status_deleted
;
556 // WC root is at least normal if there are no files added/deleted
559 *status
= git_wc_status_normal
;
564 g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, path
, true);
565 if (g_IgnoreList
.IsIgnore(path
, gitdir
, true))
566 *status
= git_wc_status_ignored
;
568 *status
= git_wc_status_unversioned
;
573 // In version control
574 *status
= git_wc_status_normal
;
579 GetRangeInSortVector(*indexptr
, path
, path
.GetLength(), &start
, &end
, pos
);
582 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; indexptr
->m_bHasConflicts
&& it
<= itlast
; ++it
)
584 if (((*it
).m_Flags
& GIT_IDXENTRY_STAGEMASK
) != 0)
586 *status
= git_wc_status_conflicted
;
587 // When status == git_wc_status_conflicted, we don't need to check each file status
588 // because git_wc_status_conflicted is the highest.
595 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
599 // Check if new init repository
600 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
604 *status
= git_wc_status_none
;
609 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; it
<= itlast
; ++it
)
611 auto& indexentry
= *it
;
612 pos
= SearchInSortVector(*treeptr
, indexentry
.m_FileName
, -1);
616 *status
= GetMoreImportant(git_wc_status_added
, *status
); // added file found
617 AdjustFolderStatus(*status
);
618 if (GetMoreImportant(*status
, git_wc_status_modified
) == *status
) // the only potential higher status which me might get in this loop
623 if ((*treeptr
)[pos
].m_Hash
!= indexentry
.m_IndexHash
)
625 *status
= GetMoreImportant(git_wc_status_modified
, *status
); // modified file found
631 if (*status
== git_wc_status_normal
)
633 pos
= SearchInSortVector(*treeptr
, path
, path
.GetLength());
635 *status
= GetMoreImportant(git_wc_status_added
, *status
); // added file found
639 // we know that pos exists in treeptr
640 GetRangeInSortVector(*treeptr
, path
, path
.GetLength(), &hstart
, &hend
, pos
);
641 for (auto hit
= treeptr
->cbegin() + hstart
, lastElement
= treeptr
->cbegin() + hend
; hit
<= lastElement
; ++hit
)
643 if (SearchInSortVector(*indexptr
, (*hit
).m_FileName
, -1) == NPOS
)
645 *status
= GetMoreImportant(git_wc_status_deleted
, *status
); // deleted file found
655 auto mostImportantPossibleFolderStatus
= GetMoreImportant(git_wc_status_added
, GetMoreImportant(git_wc_status_modified
, git_wc_status_deleted
));
656 AdjustFolderStatus(mostImportantPossibleFolderStatus
);
657 // we can skip here when we already have the highest possible status
658 if (mostImportantPossibleFolderStatus
== *status
)
661 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; it
<= itlast
; ++it
)
663 auto& indexentry
= *it
;
664 // skip child directory, but handle submodules
665 if (!IsRecursive
&& indexentry
.m_FileName
.Find(L
'/', path
.GetLength()) > 0 && !IsDirectSubmodule(indexentry
.m_FileName
, path
.GetLength()))
668 git_wc_status2_t filestatus
= { git_wc_status_none
, false, false };
669 GetFileStatus(gitdir
, indexentry
.m_FileName
, filestatus
, IsFul
, IsIgnore
, false);
670 switch (filestatus
.status
)
672 case git_wc_status_added
:
673 case git_wc_status_modified
:
674 case git_wc_status_deleted
:
675 //case git_wc_status_conflicted: cannot happen, we exit as soon we found a conflict in subpath
676 *status
= GetMoreImportant(filestatus
.status
, *status
);
677 AdjustFolderStatus(*status
);
678 if (mostImportantPossibleFolderStatus
== *status
)
688 bool GitStatus::IsExistIndexLockFile(CString sDirName
)
690 if (!PathIsDirectory(sDirName
))
692 int x
= sDirName
.ReverseFind(L
'\\');
696 sDirName
.Truncate(x
);
701 if (PathFileExists(CombinePath(sDirName
, L
".git")))
703 if (PathFileExists(g_AdminDirMap
.GetWorktreeAdminDirConcat(sDirName
, L
"index.lock")))
709 int x
= sDirName
.ReverseFind(L
'\\');
713 sDirName
.Truncate(x
);
718 bool GitStatus::ReleasePath(const CString
&gitdir
)
720 g_IndexFileMap
.SafeClear(gitdir
);
721 g_HeadFileMap
.SafeClear(gitdir
);
725 bool GitStatus::ReleasePathsRecursively(const CString
&rootpath
)
727 g_IndexFileMap
.SafeClearRecursively(rootpath
);
728 g_HeadFileMap
.SafeClearRecursively(rootpath
);
732 void GitStatus::AdjustFolderStatus(git_wc_status_kind
& status
)
734 if (status
== git_wc_status_deleted
|| status
== git_wc_status_added
)
735 status
= git_wc_status_modified
;