1
// TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2024 - 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 "GitStatus.h"
23 #include "UnicodeUtils.h"
24 #include "ReaderWriterLock.h"
25 #include "GitAdminDir.h"
26 #include "StringUtils.h"
27 #include "PathUtils.h"
30 #define S_IFLNK 0120000
32 #define _S_IFLNK S_IFLNK
35 #define S_ISLNK(m) (((m) & _S_IFMT) == _S_IFLNK)
40 /* m_Size and m_ModifyTime are only uint32_t in libgit2, cf. https://github.com/libgit2/libgit2/blob/8535fdb9cbad8fcd15ee4022ed29c4138547e22d/include/git2/index.h#L48-L51 and https://tortoisegit.org/issue/4108 */
42 mutable int32_t m_ModifyTime
;
43 mutable uint32_t m_ModifyTimeNanos
;
45 uint16_t m_FlagsExtended
;
53 class CGitIndexList
: private std::vector
<CGitIndex
>
56 BOOL m_bHasConflicts
= FALSE
;
58 size_t m_incoming
= static_cast<size_t>(-1);
59 size_t m_outgoing
= static_cast<size_t>(-1);
60 size_t m_stashCount
= 0;
61 inline bool IsIgnoreCase() const { return m_iIndexCaps
& GIT_INDEX_CAPABILITY_IGNORE_CASE
; }
66 bool HasIndexChangedOnDisk(const CString
& gitdir
) const;
67 int ReadIndex(const CString
& dotgitdir
);
68 int ReadIncomingOutgoing(git_repository
* repo
);
69 int GetFileStatus(const CString
& gitdir
, const CString
& path
, git_wc_status2_t
& status
, CGitHash
* pHash
= nullptr) const;
70 int GetFileStatus(CAutoRepository
& repository
, const CString
& gitdir
, const CGitIndex
& entry
, git_wc_status2_t
& status
, __int64 time
, __int64 filesize
, bool isSymlink
) const;
72 using std::vector
<CGitIndex
>::begin
;
73 using std::vector
<CGitIndex
>::end
;
74 using std::vector
<CGitIndex
>::cbegin
;
75 using std::vector
<CGitIndex
>::cend
;
76 using std::vector
<CGitIndex
>::empty
;
77 using std::vector
<CGitIndex
>::size
;
78 using std::vector
<CGitIndex
>::operator[];
80 #ifdef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
81 FRIEND_TEST(GitIndexCBasicGitWithTestRepoFixture
, GetFileStatus
);
84 __time64_t m_LastModifyTime
= 0;
85 __int64 m_LastFileSize
= -1;
87 int m_iIndexCaps
= GIT_INDEX_CAPABILITY_IGNORE_CASE
| GIT_INDEX_CAPABILITY_NO_SYMLINKS
;
88 __int64 m_iMaxCheckSize
= 10 * 1024 * 1024;
89 bool m_bCalculateIncomingOutgoing
= true;
91 int GetFileStatus(const CString
& gitdir
, const CString
& path
, git_wc_status2_t
& status
, __int64 time
, __int64 filesize
, bool isSymlink
, CGitHash
* pHash
= nullptr) const;
94 using SHARED_INDEX_PTR
= std::shared_ptr
<const CGitIndexList
>;
95 using CAutoLocker
= CComCritSecLock
<CComCriticalSection
>;
97 template<typename SharedPtr
>
98 class SharedPtrMapTmpl
: private std::map
<CString
, SharedPtr
>
101 [[nodiscard
]] SharedPtr
SafeGet(const CString
& path
)
103 CString
thePath(CPathUtils::NormalizePath(path
));
104 CAutoLocker
lock(m_critSec
);
105 auto lookup
= this->find(thePath
);
106 if (lookup
== this->cend())
108 return lookup
->second
;
111 bool SafeClear(const CString
& path
)
113 CString
thePath(CPathUtils::NormalizePath(path
));
114 CAutoLocker
lock(m_critSec
);
115 auto lookup
= this->find(thePath
);
116 if (lookup
== this->cend())
122 bool SafeClearRecursively(const CString
& path
)
124 CString
thePath(CPathUtils::NormalizePath(path
));
125 CAutoLocker
lock(m_critSec
);
126 std::vector
<CString
> toRemove
;
127 for (auto it
= this->cbegin(); it
!= this->cend(); ++it
)
129 if (CStringUtils::StartsWith((*it
).first
, thePath
))
130 toRemove
.push_back((*it
).first
);
132 for (auto it
= toRemove
.cbegin(); it
!= toRemove
.cend(); ++it
)
134 return !toRemove
.empty();
138 void SafeSet(const CString
& path
, SharedPtr ptr
)
140 CString
thePath(CPathUtils::NormalizePath(path
));
141 CAutoLocker
lock(m_critSec
);
142 (*this)[thePath
] = ptr
;
146 CComAutoCriticalSection m_critSec
;
149 class CGitIndexFileMap
: protected SharedPtrMapTmpl
<SHARED_INDEX_PTR
>
152 [[nodiscard
]] SHARED_INDEX_PTR
CheckAndUpdate(const CString
& gitdir
)
154 if (auto pIndex
= SafeGet(gitdir
); pIndex
&& !pIndex
->HasIndexChangedOnDisk(gitdir
))
157 auto newIndex
= std::make_shared
<CGitIndexList
>();
158 if (newIndex
->ReadIndex(gitdir
))
164 SafeSet(gitdir
, newIndex
);
169 using SharedPtrMapTmpl
<SHARED_INDEX_PTR
>::SafeClear
;
170 using SharedPtrMapTmpl
<SHARED_INDEX_PTR
>::SafeClearRecursively
;
171 using SharedPtrMapTmpl
<SHARED_INDEX_PTR
>::SafeGet
;
181 /* After object create, never change field against
182 * that needn't lock to get field
184 class CGitHeadFileList
: private std::vector
<CGitTreeItem
>
187 int GetPackRef(const CString
&gitdir
);
189 __time64_t m_LastModifyTimeHead
= 0;
190 __time64_t m_LastModifyTimeRef
= 0;
191 __time64_t m_LastModifyTimePackRef
= 0;
193 __int64 m_LastFileSizeHead
= -1;
194 __int64 m_LastFileSizePackRef
= -1;
196 CString m_HeadRefFile
;
200 CString m_PackRefFile
;
201 bool m_bRefFromPackRefFile
= false;
203 std::map
<CString
,CGitHash
> m_PackRefMap
;
206 CGitHeadFileList() = default;
208 int ReadTree(bool ignoreCase
);
209 int ReadHeadHash(const CString
& gitdir
);
210 bool CheckHeadUpdate() const;
212 using std::vector
<CGitTreeItem
>::begin
;
213 using std::vector
<CGitTreeItem
>::end
;
214 using std::vector
<CGitTreeItem
>::cbegin
;
215 using std::vector
<CGitTreeItem
>::cend
;
216 using std::vector
<CGitTreeItem
>::empty
;
217 using std::vector
<CGitTreeItem
>::size
;
218 using std::vector
<CGitTreeItem
>::operator[];
221 int ReadTreeRecursive(git_repository
& repo
, const git_tree
* tree
, const CString
& base
);
224 using SHARED_TREE_PTR
= std::shared_ptr
<const CGitHeadFileList
>;
225 class CGitHeadFileMap
: protected SharedPtrMapTmpl
<SHARED_TREE_PTR
>
228 [[nodiscard
]] SHARED_TREE_PTR
CheckHeadAndUpdate(const CString
& gitdir
, bool ignoreCase
);
230 using SharedPtrMapTmpl
<SHARED_TREE_PTR
>::SafeClear
;
231 using SharedPtrMapTmpl
<SHARED_TREE_PTR
>::SafeClearRecursively
;
232 using SharedPtrMapTmpl
<SHARED_TREE_PTR
>::SafeGet
;
237 CGitFileName() = default;
238 CGitFileName(LPCWSTR filename
, __int64 size
, __int64 lastmodified
)
239 : m_FileName(filename
)
241 , m_LastModified(lastmodified
)
246 __int64 m_LastModified
= 0;
247 bool m_bSymlink
= false;
256 git_free_exclude_list(m_pExcludeList
);
259 __time64_t m_LastModifyTime
= 0;
260 __int64 m_LastFileSize
= -1;
262 std::unique_ptr
<char[]> m_buffer
;
263 EXCLUDE_LIST m_pExcludeList
= nullptr;
264 int* m_iIgnoreCase
= nullptr;
266 int FetchIgnoreList(const CString
& projectroot
, const CString
& file
, bool isGlobal
, int* ignoreCase
);
269 * patha: the filename to be checked whether is is ignored or not
270 * base: must be a pointer to the beginning of the base filename WITHIN patha
271 * type: DT_DIR or DT_REG
273 int IsPathIgnored(const CStringA
& patha
, const char* base
, int& type
);
274 #ifdef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
275 int IsPathIgnored(const CStringA
& patha
, int& type
);
282 bool CheckFileChanged(const CString
&path
);
283 int FetchIgnoreFile(const CString
&gitdir
, const CString
&gitignore
, bool isGlobal
);
285 int CheckIgnore(const CString
& path
, const CString
& root
, bool isDir
, const CString
& adminDir
);
286 int CheckFileAgainstIgnoreList(const CString
&ignorefile
, const CStringA
&patha
, const char * base
, int &type
);
288 // core.excludesfile stuff
289 std::map
<CString
, CString
> m_CoreExcludesfiles
;
290 std::map
<CString
, int> m_IgnoreCase
;
291 CString m_sGitSystemConfigPath
;
292 ULONGLONG m_dGitSystemConfigPathLastChecked
= 0LL;
293 CReaderWriterLock m_coreExcludefilesSharedMutex
;
294 // checks if the msysgit path has changed and return true/false
295 // if the path changed, the cache is update
296 // force is only ised in constructor
297 bool CheckAndUpdateGitSystemConfigPath(bool force
= true);
298 bool CheckAndUpdateCoreExcludefile(const CString
&adminDir
);
299 const CString
GetWindowsHome();
302 CReaderWriterLock m_SharedMutex
;
304 CGitIgnoreList(){ CheckAndUpdateGitSystemConfigPath(true); }
306 std::map
<CString
, CGitIgnoreItem
> m_Map
;
308 bool CheckAndUpdateIgnoreFiles(const CString
& gitdir
, const CString
& path
, bool isDir
, std::set
<CString
>* lastChecked
= nullptr);
309 bool IsIgnore(CString path
, const CString
& root
, bool isDir
, const CString
& adminDir
);
313 inline void DoSortFilenametSortVector(T
& vector
, bool ignoreCase
)
316 std::sort(vector
.begin(), vector
.end(), [](const auto& e1
, const auto& e2
) { return e1
.m_FileName
.CompareNoCase(e2
.m_FileName
) < 0; });
318 std::sort(vector
.begin(), vector
.end(), [](const auto& e1
, const auto& e2
) { return e1
.m_FileName
.Compare(e2
.m_FileName
) < 0; });
321 static const size_t NPOS
= static_cast<size_t>(-1); // bad/missing length/position
322 static_assert(MAXSIZE_T
== NPOS
, "NPOS must equal MAXSIZE_T");
323 static_assert(-1 == static_cast<int>(NPOS
), "NPOS must equal -1");
326 inline int GetRangeInSortVector(const T
& vector
, LPCWSTR pstr
, size_t len
, bool ignoreCase
, size_t* start
, size_t* end
, size_t pos
)
329 return GetRangeInSortVector_int(vector
, pstr
, len
, _wcsnicmp
, start
, end
, pos
);
331 return GetRangeInSortVector_int(vector
, pstr
, len
, wcsncmp
, start
, end
, pos
);
334 template<class T
, class V
>
335 int GetRangeInSortVector_int(const T
& vector
, LPCWSTR pstr
, size_t len
, V compare
, size_t* start
, size_t* end
, size_t pos
)
337 static_assert(std::is_convertible_v
<V
, std::function
<int(const wchar_t*, const wchar_t*, size_t)>>, "Wrong signature for the compare method, needs to be a wcsncmp equivalent");
343 *start
= *end
= NPOS
;
348 if (pos
>= vector
.size())
351 if (compare(vector
[pos
].m_FileName
, pstr
, len
) != 0)
355 *end
= vector
.size() - 1;
357 // shortcut, if all entries are going match
361 for (size_t i
= pos
; i
< vector
.size(); ++i
)
363 if (compare(vector
[i
].m_FileName
, pstr
, len
) != 0)
368 for (size_t i
= pos
+ 1; i
-- > 0;)
370 if (compare(vector
[i
].m_FileName
, pstr
, len
) != 0)
380 inline size_t SearchInSortVector(const T
& vector
, LPCWSTR pstr
, int len
, bool ignoreCase
)
385 return SearchInSortVector_int(vector
, pstr
, _wcsicmp
);
387 return SearchInSortVector_int(vector
, pstr
, [len
](const auto& s1
, const auto& s2
) { return _wcsnicmp(s1
, s2
, len
); });
391 return SearchInSortVector_int(vector
, pstr
, wcscmp
);
393 return SearchInSortVector_int(vector
, pstr
, [len
](const auto& s1
, const auto& s2
) { return wcsncmp(s1
, s2
, len
); });
396 template<class T
, class V
>
397 size_t SearchInSortVector_int(const T
& vector
, LPCWSTR pstr
, V compare
)
399 static_assert(std::is_convertible_v
<V
, std::function
<int(const wchar_t*, const wchar_t*)>>, "Wrong signature for the compare method, needs to be a wcscmp equivalent");
400 size_t end
= vector
.size() - 1;
402 size_t mid
= (start
+ end
) / 2;
407 while(!( start
== end
&& start
==mid
))
409 const int cmp
= compare(vector
[mid
].m_FileName
, pstr
);
417 mid
=(start
+end
) /2;
421 if (compare(vector
[mid
].m_FileName
, pstr
) == 0)
427 class CGitAdminDirMap
: private std::map
<CString
, CString
>
430 CComAutoCriticalSection m_critIndexSec
;
431 std::map
<CString
, CString
> m_reverseLookup
;
432 std::map
<CString
, CString
> m_WorktreeAdminDirLookup
;
434 CString
GetAdminDir(const CString
&path
)
436 CString
thePath(CPathUtils::NormalizePath(path
));
437 CAutoLocker
lock(m_critIndexSec
);
438 auto lookup
= find(thePath
);
439 if (lookup
== cend())
442 bool isWorktree
= false;
443 if (GitAdminDir::GetAdminDirPath(path
, adminDir
, &isWorktree
) && PathIsDirectory(adminDir
))
445 (*this)[thePath
] = adminDir
;
446 if (!isWorktree
) // GitAdminDir::GetAdminDirPath returns the commongit dir ("parent/.git") and this would override the lookup path for the main repo
447 m_reverseLookup
[CPathUtils::BuildPathWithPathDelimiter(CPathUtils::NormalizePath(adminDir
))] = path
;
448 return (*this)[thePath
];
451 return CPathUtils::BuildPathWithPathDelimiter(path
) + L
".git\\"; // in case of an error stick to old behavior
454 return lookup
->second
;
457 CString
GetAdminDirConcat(const CString
& path
, const CString
& subpath
)
459 CString
result(GetAdminDir(path
));
464 CString
GetWorktreeAdminDir(const CString
& path
)
466 CString
thePath(CPathUtils::NormalizePath(path
));
467 CAutoLocker
lock(m_critIndexSec
);
468 auto lookup
= m_WorktreeAdminDirLookup
.find(thePath
);
469 if (lookup
== m_WorktreeAdminDirLookup
.cend())
472 if (GitAdminDir::GetWorktreeAdminDirPath(path
, wtadmindir
) && PathIsDirectory(wtadmindir
))
474 m_WorktreeAdminDirLookup
[thePath
] = wtadmindir
;
475 m_reverseLookup
[CPathUtils::BuildPathWithPathDelimiter(CPathUtils::NormalizePath(wtadmindir
))] = path
;
476 return m_WorktreeAdminDirLookup
[thePath
];
479 return CPathUtils::BuildPathWithPathDelimiter(path
) + L
".git\\"; // we should never get here
481 return lookup
->second
;
484 CString
GetWorktreeAdminDirConcat(const CString
& path
, const CString
& subpath
)
486 CString
result(GetWorktreeAdminDir(path
));
491 CString
GetWorkingCopy(const CString
&gitDir
)
493 CString
path(CPathUtils::BuildPathWithPathDelimiter(CPathUtils::NormalizePath(gitDir
)));
494 CAutoLocker
lock(m_critIndexSec
);
495 auto lookup
= m_reverseLookup
.find(path
);
496 if (lookup
== m_reverseLookup
.cend())
498 return lookup
->second
;
501 #ifdef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
502 using std::map
<CString
, CString
>::clear
;