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 (!status
.assumeValid
&& !status
.skipWorktree
&& (*treeptr
)[start
].m_Hash
!= hash
)
245 status
.status
= git_wc_status_modified
;
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(CString path
, std::vector
<CGitFileName
>& list
, bool& isRepoRoot
)
285 WIN32_FIND_DATA data
;
286 CAutoFindFile handle
= ::FindFirstFileEx(path
, FindExInfoBasic
, &data
, FindExSearchNameMatch
, nullptr, FIND_FIRST_EX_LARGE_FETCH
);
291 if (wcscmp(data
.cFileName
, L
".git") == 0)
297 if (wcscmp(data
.cFileName
, L
".") == 0)
300 if (wcscmp(data
.cFileName
, L
"..") == 0)
303 CGitFileName
filename(data
.cFileName
, ((__int64
)data
.nFileSizeHigh
<< 32) + data
.nFileSizeLow
, ((__int64
)data
.ftLastWriteTime
.dwHighDateTime
<< 32) + data
.ftLastWriteTime
.dwLowDateTime
);
304 if(data
.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
)
305 filename
.m_FileName
+= L
'/';
307 list
.push_back(filename
);
309 }while(::FindNextFile(handle
, &data
));
311 handle
.CloseHandle(); // manually close handle here in order to keep handles open as short as possible
313 std::sort(list
.begin(), list
.end(), SortCGitFileName
);
317 int GitStatus::EnumDirStatus(const CString
& gitdir
, const CString
& subpath
, git_wc_status_kind
* dirstatus
, FILL_STATUS_CALLBACK callback
, void* pData
)
319 CString path
= subpath
;
321 path
.Replace(L
'\\', L
'/');
322 if (!path
.IsEmpty() && path
[path
.GetLength() - 1] != L
'/')
323 path
+= L
'/'; // Add trail / to show it is directory, not file name.
325 std::vector
<CGitFileName
> filelist
;
326 bool isRepoRoot
= false;
327 GetFileList(CombinePath(gitdir
, subpath
), filelist
, isRepoRoot
);
328 *dirstatus
= git_wc_status_unknown
;
330 *dirstatus
= git_wc_status_normal
;
332 g_IndexFileMap
.CheckAndUpdate(gitdir
);
334 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
336 SHARED_INDEX_PTR indexptr
= g_IndexFileMap
.SafeGet(gitdir
);
337 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
339 // there was an error loading the index or the HEAD commit/tree
340 if (!indexptr
|| !treeptr
)
343 CAutoRepository repository
;
344 for (auto it
= filelist
.cbegin(), itend
= filelist
.cend(); it
!= itend
; ++it
)
346 auto& fileentry
= *it
;
348 CString
onepath(path
);
349 onepath
+= fileentry
.m_FileName
;
352 if (!onepath
.IsEmpty() && onepath
[onepath
.GetLength() - 1] == L
'/')
355 int matchLength
= -1;
357 matchLength
= onepath
.GetLength();
358 size_t pos
= SearchInSortVector(*indexptr
, onepath
, matchLength
);
359 size_t posintree
= SearchInSortVector(*treeptr
, onepath
, matchLength
);
361 git_wc_status2_t status
= { git_wc_status_none
, false, false };
363 if (pos
== NPOS
&& posintree
== NPOS
)
365 status
.status
= git_wc_status_unversioned
;
367 g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, onepath
, bIsDir
);
368 if (g_IgnoreList
.IsIgnore(onepath
, gitdir
, bIsDir
))
369 status
.status
= git_wc_status_ignored
;
371 callback(CombinePath(gitdir
, onepath
), &status
, bIsDir
, fileentry
.m_LastModified
, pData
);
373 else if (pos
== NPOS
&& posintree
!= NPOS
) /* check if file delete in index */
375 status
.status
= git_wc_status_deleted
;
376 callback(CombinePath(gitdir
, onepath
), &status
, bIsDir
, fileentry
.m_LastModified
, pData
);
378 else if (pos
!= NPOS
&& posintree
== NPOS
) /* Check if file added */
380 status
.status
= git_wc_status_added
;
381 if ((*indexptr
)[pos
].m_Flags
& GIT_IDXENTRY_STAGEMASK
)
382 status
.status
= git_wc_status_conflicted
;
383 callback(CombinePath(gitdir
, onepath
), &status
, bIsDir
, fileentry
.m_LastModified
, pData
);
389 status
.status
= git_wc_status_normal
;
390 callback(CombinePath(gitdir
, onepath
), &status
, bIsDir
, fileentry
.m_LastModified
, pData
);
394 auto& indexentry
= (*indexptr
)[pos
];
395 if (indexentry
.m_Flags
& GIT_IDXENTRY_STAGEMASK
)
397 status
.status
= git_wc_status_conflicted
;
398 callback(CombinePath(gitdir
, onepath
), &status
, false, fileentry
.m_LastModified
, pData
);
401 if ((*indexptr
).GetFileStatus(repository
, gitdir
, indexentry
, status
, CGit::filetime_to_time_t(fileentry
.m_LastModified
), fileentry
.m_Size
))
403 if (status
.status
== git_wc_status_normal
&& !status
.assumeValid
&& !status
.skipWorktree
&& (*treeptr
)[posintree
].m_Hash
!= indexentry
.m_IndexHash
)
404 status
.status
= git_wc_status_modified
;
405 callback(CombinePath(gitdir
, onepath
), &status
, false, fileentry
.m_LastModified
, pData
);
409 repository
.Free(); // explicitly free the handle here in order to keep an open repository as short as possible
411 /* Check deleted file in system */
412 size_t start
= 0, end
= 0;
413 size_t pos
= SearchInSortVector(*indexptr
, path
, path
.GetLength()); // match path prefix, (sub)folders end with slash
414 std::set
<CString
> alreadyReported
;
416 if (GetRangeInSortVector(*indexptr
, path
, path
.GetLength(), &start
, &end
, pos
) == 0)
418 *dirstatus
= git_wc_status_normal
; // here we know that this folder has versioned entries
420 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; it
<= itlast
; ++it
)
423 int commonPrefixLength
= path
.GetLength();
424 int index
= entry
.m_FileName
.Find(L
'/', commonPrefixLength
);
426 index
= entry
.m_FileName
.GetLength();
428 ++index
; // include slash at the end for subfolders, so that we do not match files by mistake
430 CString filename
= entry
.m_FileName
.Mid(commonPrefixLength
, index
- commonPrefixLength
);
431 if (oldstring
!= filename
)
433 oldstring
= filename
;
434 int length
= filename
.GetLength();
435 bool isDir
= filename
[length
- 1] == L
'/';
436 if (SearchInSortVector(filelist
, filename
, isDir
? length
: -1) == NPOS
) // do full match for filenames and only prefix-match ending with "/" for folders
438 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
439 if ((entry
.m_FlagsExtended
& GIT_IDXENTRY_SKIP_WORKTREE
) != 0)
441 status
.skipWorktree
= true;
442 status
.status
= git_wc_status_normal
;
443 oldstring
.Empty(); // without this a deleted folder which has two versioned files and only the first is skipwoktree flagged gets reported as normal
444 if (alreadyReported
.find(filename
) != alreadyReported
.cend())
447 alreadyReported
.insert(filename
);
448 callback(CombinePath(gitdir
, subpath
, filename
), &status
, isDir
, 0, pData
);
455 pos
= SearchInSortVector(*treeptr
, path
, path
.GetLength()); // match path prefix, (sub)folders end with slash
456 if (GetRangeInSortVector(*treeptr
, path
, path
.GetLength(), &start
, &end
, pos
) == 0)
458 *dirstatus
= git_wc_status_normal
; // here we know that this folder has versioned entries
460 for (auto it
= treeptr
->cbegin() + start
, itlast
= treeptr
->cbegin() + end
; it
<= itlast
; ++it
)
463 int commonPrefixLength
= path
.GetLength();
464 int index
= entry
.m_FileName
.Find(L
'/', commonPrefixLength
);
466 index
= entry
.m_FileName
.GetLength();
468 ++index
; // include slash at the end for subfolders, so that we do not match files by mistake
470 CString filename
= entry
.m_FileName
.Mid(commonPrefixLength
, index
- commonPrefixLength
);
471 if (oldstring
!= filename
&& alreadyReported
.find(filename
) == alreadyReported
.cend())
473 oldstring
= filename
;
474 int length
= filename
.GetLength();
475 bool isDir
= filename
[length
- 1] == L
'/';
476 if (SearchInSortVector(filelist
, filename
, isDir
? length
: -1) == NPOS
) // do full match for filenames and only prefix-match ending with "/" for folders
478 git_wc_status2_t status
= { (!isDir
|| IsDirectSubmodule(entry
.m_FileName
, commonPrefixLength
)) ? git_wc_status_deleted
: git_wc_status_modified
, false, false };
479 callback(CombinePath(gitdir
, subpath
, filename
), &status
, isDir
, 0, pData
);
489 int GitStatus::GetDirStatus(const CString
& gitdir
, const CString
& subpath
, git_wc_status_kind
* status
, BOOL IsFul
, BOOL IsRecursive
, BOOL IsIgnore
)
493 CString path
= subpath
;
495 path
.Replace(L
'\\', L
'/');
496 if (!path
.IsEmpty() && path
[path
.GetLength() - 1] != L
'/')
497 path
+= L
'/'; //Add trail / to show it is directory, not file name.
499 g_IndexFileMap
.CheckAndUpdate(gitdir
);
501 SHARED_INDEX_PTR indexptr
= g_IndexFileMap
.SafeGet(gitdir
);
506 *status
= git_wc_status_none
;
510 size_t pos
= SearchInSortVector(*indexptr
, path
, path
.GetLength());
512 // Not In Version Contorl
517 // WC root is at least normal if there are no files added/deleted
518 if (subpath
.IsEmpty())
520 *status
= git_wc_status_normal
;
523 *status
= git_wc_status_unversioned
;
527 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
529 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
533 *status
= git_wc_status_none
;
537 // check whether there files in head with are not in index
538 pos
= SearchInSortVector(*treeptr
, path
, path
.GetLength());
541 *status
= git_wc_status_deleted
;
545 // WC root is at least normal if there are no files added/deleted
548 *status
= git_wc_status_normal
;
553 g_IgnoreList
.CheckAndUpdateIgnoreFiles(gitdir
, path
, true);
554 if (g_IgnoreList
.IsIgnore(path
, gitdir
, true))
555 *status
= git_wc_status_ignored
;
557 *status
= git_wc_status_unversioned
;
562 // In version control
563 *status
= git_wc_status_normal
;
568 GetRangeInSortVector(*indexptr
, path
, path
.GetLength(), &start
, &end
, pos
);
571 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; indexptr
->m_bHasConflicts
&& it
<= itlast
; ++it
)
573 if (((*it
).m_Flags
& GIT_IDXENTRY_STAGEMASK
) != 0)
575 *status
= git_wc_status_conflicted
;
576 // When status == git_wc_status_conflicted, we don't need to check each file status
577 // because git_wc_status_conflicted is the highest.
584 g_HeadFileMap
.CheckHeadAndUpdate(gitdir
);
588 // Check if new init repository
589 SHARED_TREE_PTR treeptr
= g_HeadFileMap
.SafeGet(gitdir
);
593 *status
= git_wc_status_none
;
598 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; it
<= itlast
; ++it
)
600 auto& indexentry
= *it
;
601 pos
= SearchInSortVector(*treeptr
, indexentry
.m_FileName
, -1);
605 *status
= GetMoreImportant(git_wc_status_added
, *status
); // added file found
606 AdjustFolderStatus(*status
);
607 if (GetMoreImportant(*status
, git_wc_status_modified
) == *status
) // the only potential higher status which me might get in this loop
612 if ((indexentry
.m_Flags
& GIT_IDXENTRY_VALID
) == 0 && (indexentry
.m_FlagsExtended
& GIT_IDXENTRY_SKIP_WORKTREE
) == 0 && (*treeptr
)[pos
].m_Hash
!= indexentry
.m_IndexHash
)
614 *status
= GetMoreImportant(git_wc_status_modified
, *status
); // modified file found
620 if (*status
== git_wc_status_normal
)
622 pos
= SearchInSortVector(*treeptr
, path
, path
.GetLength());
624 *status
= GetMoreImportant(git_wc_status_added
, *status
); // added file found
628 // we know that pos exists in treeptr
629 GetRangeInSortVector(*treeptr
, path
, path
.GetLength(), &hstart
, &hend
, pos
);
630 for (auto hit
= treeptr
->cbegin() + hstart
, lastElement
= treeptr
->cbegin() + hend
; hit
<= lastElement
; ++hit
)
632 if (SearchInSortVector(*indexptr
, (*hit
).m_FileName
, -1) == NPOS
)
634 *status
= GetMoreImportant(git_wc_status_deleted
, *status
); // deleted file found
644 auto mostImportantPossibleFolderStatus
= GetMoreImportant(git_wc_status_added
, GetMoreImportant(git_wc_status_modified
, git_wc_status_deleted
));
645 AdjustFolderStatus(mostImportantPossibleFolderStatus
);
646 // we can skip here when we already have the highest possible status
647 if (mostImportantPossibleFolderStatus
== *status
)
650 for (auto it
= indexptr
->cbegin() + start
, itlast
= indexptr
->cbegin() + end
; it
<= itlast
; ++it
)
652 auto& indexentry
= *it
;
653 // skip child directory, but handle submodules
654 if (!IsRecursive
&& indexentry
.m_FileName
.Find(L
'/', path
.GetLength()) > 0 && !IsDirectSubmodule(indexentry
.m_FileName
, path
.GetLength()))
657 git_wc_status2_t filestatus
= { git_wc_status_none
, false, false };
658 GetFileStatus(gitdir
, indexentry
.m_FileName
, filestatus
, IsFul
, IsIgnore
, false);
659 switch (filestatus
.status
)
661 case git_wc_status_added
:
662 case git_wc_status_modified
:
663 case git_wc_status_deleted
:
664 //case git_wc_status_conflicted: cannot happen, we exit as soon we found a conflict in subpath
665 *status
= GetMoreImportant(filestatus
.status
, *status
);
666 AdjustFolderStatus(*status
);
667 if (mostImportantPossibleFolderStatus
== *status
)
677 bool GitStatus::IsExistIndexLockFile(CString sDirName
)
679 if (!PathIsDirectory(sDirName
))
681 int x
= sDirName
.ReverseFind(L
'\\');
685 sDirName
.Truncate(x
);
690 if (PathFileExists(CombinePath(sDirName
, L
".git")))
692 if (PathFileExists(g_AdminDirMap
.GetWorktreeAdminDirConcat(sDirName
, L
"index.lock")))
698 int x
= sDirName
.ReverseFind(L
'\\');
702 sDirName
.Truncate(x
);
707 bool GitStatus::ReleasePath(const CString
&gitdir
)
709 g_IndexFileMap
.SafeClear(gitdir
);
710 g_HeadFileMap
.SafeClear(gitdir
);
714 bool GitStatus::ReleasePathsRecursively(const CString
&rootpath
)
716 g_IndexFileMap
.SafeClearRecursively(rootpath
);
717 g_HeadFileMap
.SafeClearRecursively(rootpath
);
721 void GitStatus::AdjustFolderStatus(git_wc_status_kind
& status
)
723 if (status
== git_wc_status_deleted
|| status
== git_wc_status_added
)
724 status
= git_wc_status_modified
;