Fixed issue #4133: libgit2 returned: failed to parse revision specifier (ref ending...
[TortoiseGit.git] / src / Git / gitindex.h
blob5ce619bb1006ac46b556b71f70ee32596a12c494
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.
20 #include "GitHash.h"
21 #include "gitdll.h"
22 #include "GitStatus.h"
23 #include "UnicodeUtils.h"
24 #include "ReaderWriterLock.h"
25 #include "GitAdminDir.h"
26 #include "StringUtils.h"
27 #include "PathUtils.h"
29 #ifndef S_IFLNK
30 #define S_IFLNK 0120000
31 #undef _S_IFLNK
32 #define _S_IFLNK S_IFLNK
33 #endif
34 #ifndef S_ISLNK
35 #define S_ISLNK(m) (((m) & _S_IFMT) == _S_IFLNK)
36 #endif
38 struct CGitIndex
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 */
41 CString m_FileName;
42 mutable int32_t m_ModifyTime;
43 mutable uint32_t m_ModifyTimeNanos;
44 uint16_t m_Flags;
45 uint16_t m_FlagsExtended;
46 CGitHash m_IndexHash;
47 uint32_t m_Size;
48 uint32_t m_Mode;
50 int Print();
53 class CGitIndexList : private std::vector<CGitIndex>
55 public:
56 BOOL m_bHasConflicts = FALSE;
57 CString m_branch;
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; }
63 CGitIndexList();
64 ~CGitIndexList();
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);
82 #endif
83 private:
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;
90 CAutoConfig config;
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>
100 public:
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())
107 return {};
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())
117 return false;
118 this->erase(lookup);
119 return true;
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)
133 this->erase(*it);
134 return !toRemove.empty();
137 protected:
138 void SafeSet(const CString& path, SharedPtr ptr)
140 CString thePath(CPathUtils::NormalizePath(path));
141 CAutoLocker lock(m_critSec);
142 (*this)[thePath] = ptr;
145 private:
146 CComAutoCriticalSection m_critSec;
149 class CGitIndexFileMap : protected SharedPtrMapTmpl<SHARED_INDEX_PTR>
151 public:
152 [[nodiscard]] SHARED_INDEX_PTR CheckAndUpdate(const CString& gitdir)
154 if (auto pIndex = SafeGet(gitdir); pIndex && !pIndex->HasIndexChangedOnDisk(gitdir))
155 return pIndex;
157 auto newIndex = std::make_shared<CGitIndexList>();
158 if (newIndex->ReadIndex(gitdir))
160 SafeClear(gitdir);
161 return {};
164 SafeSet(gitdir, newIndex);
166 return newIndex;
169 using SharedPtrMapTmpl<SHARED_INDEX_PTR>::SafeClear;
170 using SharedPtrMapTmpl<SHARED_INDEX_PTR>::SafeClearRecursively;
171 using SharedPtrMapTmpl<SHARED_INDEX_PTR>::SafeGet;
174 struct CGitTreeItem
176 CString m_FileName;
177 CGitHash m_Hash;
178 int m_Flags;
181 /* After object create, never change field against
182 * that needn't lock to get field
184 class CGitHeadFileList : private std::vector<CGitTreeItem>
186 private:
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;
197 CGitHash m_Head;
198 CString m_HeadFile;
199 CString m_Gitdir;
200 CString m_PackRefFile;
201 bool m_bRefFromPackRefFile = false;
203 std::map<CString,CGitHash> m_PackRefMap;
205 public:
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[];
220 private:
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>
227 public:
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;
235 struct CGitFileName
237 CGitFileName() = default;
238 CGitFileName(LPCWSTR filename, __int64 size, __int64 lastmodified)
239 : m_FileName(filename)
240 , m_Size(size)
241 , m_LastModified(lastmodified)
244 CString m_FileName;
245 __int64 m_Size = -1;
246 __int64 m_LastModified = 0;
247 bool m_bSymlink = false;
250 class CGitIgnoreItem
252 public:
253 ~CGitIgnoreItem()
255 if(m_pExcludeList)
256 git_free_exclude_list(m_pExcludeList);
259 __time64_t m_LastModifyTime = 0;
260 __int64 m_LastFileSize = -1;
261 CStringA m_BaseDir;
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);
276 #endif
279 class CGitIgnoreList
281 private:
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();
301 public:
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);
312 template<class T>
313 inline void DoSortFilenametSortVector(T& vector, bool ignoreCase)
315 if (ignoreCase)
316 std::sort(vector.begin(), vector.end(), [](const auto& e1, const auto& e2) { return e1.m_FileName.CompareNoCase(e2.m_FileName) < 0; });
317 else
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");
325 template<class T>
326 inline int GetRangeInSortVector(const T& vector, LPCWSTR pstr, size_t len, bool ignoreCase, size_t* start, size_t* end, size_t pos)
328 if (ignoreCase)
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");
338 if (pos == NPOS)
339 return -1;
340 if (!start || !end)
341 return -1;
343 *start = *end = NPOS;
345 if (vector.empty())
346 return -1;
348 if (pos >= vector.size())
349 return -1;
351 if (compare(vector[pos].m_FileName, pstr, len) != 0)
352 return -1;
354 *start = 0;
355 *end = vector.size() - 1;
357 // shortcut, if all entries are going match
358 if (!len)
359 return 0;
361 for (size_t i = pos; i < vector.size(); ++i)
363 if (compare(vector[i].m_FileName, pstr, len) != 0)
364 break;
366 *end = i;
368 for (size_t i = pos + 1; i-- > 0;)
370 if (compare(vector[i].m_FileName, pstr, len) != 0)
371 break;
373 *start = i;
376 return 0;
379 template<class T>
380 inline size_t SearchInSortVector(const T& vector, LPCWSTR pstr, int len, bool ignoreCase)
382 if (ignoreCase)
384 if (len < 0)
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); });
390 if (len < 0)
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;
401 size_t start = 0;
402 size_t mid = (start + end) / 2;
404 if (vector.empty())
405 return NPOS;
407 while(!( start == end && start==mid))
409 const int cmp = compare(vector[mid].m_FileName, pstr);
410 if (cmp == 0)
411 return mid;
412 else if (cmp < 0)
413 start = mid + 1;
414 else // (cmp > 0)
415 end = mid;
417 mid=(start +end ) /2;
421 if (compare(vector[mid].m_FileName, pstr) == 0)
422 return mid;
424 return NPOS;
427 class CGitAdminDirMap : private std::map<CString, CString>
429 public:
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())
441 CString adminDir;
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];
450 ATLASSERT(false);
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));
460 result += subpath;
461 return result;
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())
471 CString wtadmindir;
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];
478 ATLASSERT(false);
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));
487 result += subpath;
488 return result;
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())
497 return gitDir;
498 return lookup->second;
501 #ifdef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
502 using std::map<CString, CString>::clear;
503 #endif