Fixed issue #4126: Capitalize the first letter in the Push dialog
[TortoiseGit.git] / src / Git / GitIndex.cpp
blob6bc93ef42f43b492329323ace16cbaca65926412
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 "stdafx.h"
21 #include "Git.h"
22 #include "registry.h"
23 #include "UnicodeUtils.h"
24 #include "PathUtils.h"
25 #include "gitindex.h"
26 #include <sys/types.h>
27 #include <sys/stat.h>
28 #include "SmartHandle.h"
29 #include "git2/sys/repository.h"
30 #include <stdexcept>
32 CGitAdminDirMap g_AdminDirMap;
34 int CGitIndex::Print()
36 wprintf(L"0x%08X 0x%08X %s %s\n",
37 static_cast<int>(this->m_ModifyTime),
38 this->m_Flags,
39 static_cast<LPCWSTR>(this->m_IndexHash.ToString()),
40 static_cast<LPCWSTR>(this->m_FileName));
42 return 0;
45 CGitIndexList::CGitIndexList()
47 #ifndef TGIT_TESTS_ONLY
48 m_iMaxCheckSize = static_cast<__int64>(CRegDWORD(L"Software\\TortoiseGit\\TGitCacheCheckContentMaxSize", 10 * 1024)) * 1024; // stored in KiB
49 m_bCalculateIncomingOutgoing = (CRegStdDWORD(L"Software\\TortoiseGit\\ModifyExplorerTitle", TRUE) != FALSE);
50 #endif
53 CGitIndexList::~CGitIndexList()
57 bool CGitIndexList::HasIndexChangedOnDisk(const CString& gitdir) const
59 __int64 time = -1, size = -1;
61 CString indexFile = g_AdminDirMap.GetWorktreeAdminDirConcat(gitdir, L"index");
62 // no need to refresh if there is no index right now and the current index is empty, but otherwise lastFileSize or lastmodifiedTime differ
63 return (CGit::GetFileModifyTime(indexFile, &time, nullptr, &size) && !empty()) || m_LastModifyTime != time || m_LastFileSize != size;
66 int CGitIndexList::ReadIndex(const CString& dgitdir)
68 #ifdef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
69 clear(); // HACK to make tests work, until we use CGitIndexList
70 #endif
71 ATLASSERT(empty());
73 CString repodir = dgitdir;
74 if (dgitdir.GetLength() == 2 && dgitdir[1] == L':')
75 repodir += L'\\'; // libgit2 requires a drive root to end with a (back)slash
77 CAutoRepository repository(repodir);
78 if (!repository)
80 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not open git repository in %s: %s\n", static_cast<LPCWSTR>(dgitdir), static_cast<LPCWSTR>(CGit::GetLibGit2LastErr()));
81 return -1;
84 CString projectConfig = g_AdminDirMap.GetAdminDir(dgitdir) + L"config";
85 CString globalConfig = g_Git.GetGitGlobalConfig();
86 CString globalXDGConfig = g_Git.GetGitGlobalXDGConfig();
87 CString systemConfig(CRegString(REG_SYSTEM_GITCONFIGPATH, L"", FALSE));
89 CAutoConfig temp { true };
90 git_config_add_file_ondisk(temp, CGit::GetGitPathStringA(projectConfig), GIT_CONFIG_LEVEL_LOCAL, repository, FALSE);
91 git_config_add_file_ondisk(temp, CGit::GetGitPathStringA(globalConfig), GIT_CONFIG_LEVEL_GLOBAL, repository, FALSE);
92 git_config_add_file_ondisk(temp, CGit::GetGitPathStringA(globalXDGConfig), GIT_CONFIG_LEVEL_XDG, repository, FALSE);
93 if (!systemConfig.IsEmpty())
94 git_config_add_file_ondisk(temp, CGit::GetGitPathStringA(systemConfig), GIT_CONFIG_LEVEL_SYSTEM, repository, FALSE);
96 git_config_snapshot(config.GetPointer(), temp);
97 temp.Free();
98 git_repository_set_config(repository, config);
100 CGit::GetFileModifyTime(g_AdminDirMap.GetWorktreeAdminDir(dgitdir) + L"index", &m_LastModifyTime, nullptr, &m_LastFileSize);
102 CAutoIndex index;
103 // load index in order to enumerate files
104 if (git_repository_index(index.GetPointer(), repository))
106 config.Free();
107 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not get index of git repository in %s: %s\n", static_cast<LPCWSTR>(dgitdir), static_cast<LPCWSTR>(CGit::GetLibGit2LastErr()));
108 return -1;
111 m_bHasConflicts = FALSE;
112 m_iIndexCaps = git_index_caps(index);
113 if (CRegDWORD(L"Software\\TortoiseGit\\OverlaysCaseSensitive", TRUE) != FALSE)
114 m_iIndexCaps &= ~GIT_INDEX_CAPABILITY_IGNORE_CASE;
116 const size_t ecount = git_index_entrycount(index);
119 resize(ecount);
121 catch (const std::bad_alloc& ex)
123 config.Free();
124 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Could not resize index-vector: %s\n", ex.what());
125 return -1;
127 catch (const std::length_error& ex)
129 config.Free();
130 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Could not resize index-vector, length_error: %s\n", ex.what());
131 return -1;
133 for (size_t i = 0; i < ecount; ++i)
135 const git_index_entry *e = git_index_get_byindex(index, i);
137 auto& item = (*this)[i];
138 item.m_FileName = CUnicodeUtils::GetUnicode(e->path);
139 if (e->mode & S_IFDIR)
140 item.m_FileName += L'/';
141 static_assert(std::is_same<decltype(item.m_ModifyTime), decltype(e->mtime.seconds)>::value);
142 item.m_ModifyTime = e->mtime.seconds;
143 static_assert(std::is_same<decltype(item.m_ModifyTimeNanos), decltype(e->mtime.nanoseconds)>::value);
144 item.m_ModifyTimeNanos = e->mtime.nanoseconds;
145 item.m_Flags = e->flags;
146 item.m_FlagsExtended = e->flags_extended;
147 item.m_IndexHash = e->id;
148 static_assert(std::is_same<decltype(item.m_Size), decltype(e->file_size)>::value);
149 item.m_Size = e->file_size;
150 item.m_Mode = e->mode;
151 m_bHasConflicts |= GIT_INDEX_ENTRY_STAGE(e);
154 DoSortFilenametSortVector(*this, IsIgnoreCase());
156 ReadIncomingOutgoing(repository);
158 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Reloaded index for repo: %s\n", static_cast<LPCWSTR>(dgitdir));
160 return 0;
163 int CGitIndexList::ReadIncomingOutgoing(git_repository* repository)
165 ATLASSERT(m_stashCount == 0 && m_outgoing == static_cast<size_t>(-1) && m_incoming == static_cast<size_t>(-1) && m_branch.IsEmpty());
167 if (!m_bCalculateIncomingOutgoing)
168 return 0;
170 if (git_stash_foreach(repository, [](size_t, const char*, const git_oid*, void* payload) -> int {
171 auto stashCount = static_cast<size_t*>(payload);
172 ++(*stashCount);
173 return 0;
174 }, &m_stashCount) < 0)
175 return -1;
177 if (const int detachedhead = git_repository_head_detached(repository); detachedhead == 1)
179 m_branch = L"detached HEAD";
180 return 0;
182 else if (detachedhead < 0)
183 return -1;
185 CAutoReference head;
186 if (const int unborn = git_repository_head_unborn(repository); unborn < 0)
187 return -1;
188 else if (unborn == 1)
190 if (git_reference_lookup(head.GetPointer(), repository, "HEAD") < 0)
191 return -1;
193 m_branch = CGit::StripRefName(CUnicodeUtils::GetUnicode(git_reference_symbolic_target(head)));
194 return 0;
197 if (git_repository_head(head.GetPointer(), repository) < 0)
198 return -1;
200 m_branch = CUnicodeUtils::GetUnicode(git_reference_shorthand(head));
202 CAutoBuf upstreambranchname;
203 git_oid upstream{};
204 // check whether there is an upstream branch
205 if (git_branch_upstream_name(upstreambranchname, repository, git_reference_name(head)) != 0 || git_reference_name_to_id(&upstream, repository, upstreambranchname->ptr) != 0)
206 return 0; // we don't have an upstream branch
208 if (git_graph_ahead_behind(&m_outgoing, &m_incoming, repository, git_reference_target(head), &upstream) < 0)
209 return -1;
211 return 0;
214 int CGitIndexList::GetFileStatus(const CString& gitdir, const CString& pathorg, git_wc_status2_t& status, __int64 time, __int64 filesize, bool isSymlink, CGitHash* pHash) const
216 size_t index = SearchInSortVector(*this, pathorg, -1, IsIgnoreCase());
218 if (index == NPOS)
220 status.status = git_wc_status_unversioned;
221 if (pHash)
222 pHash->Empty();
224 return 0;
227 auto& entry = (*this)[index];
228 if (pHash)
229 *pHash = entry.m_IndexHash;
230 ATLASSERT(IsIgnoreCase() ? pathorg.CompareNoCase(entry.m_FileName) == 0 : pathorg.Compare(entry.m_FileName) == 0);
231 CAutoRepository repository;
232 return GetFileStatus(repository, gitdir, entry, status, time, filesize, isSymlink);
235 int CGitIndexList::GetFileStatus(CAutoRepository& repository, const CString& gitdir, const CGitIndex& entry, git_wc_status2_t& status, __int64 time, __int64 filesize, bool isSymlink) const
237 ATLASSERT(!status.assumeValid && !status.skipWorktree);
239 // skip-worktree has higher priority than assume-valid
240 if (entry.m_FlagsExtended & GIT_INDEX_ENTRY_SKIP_WORKTREE)
242 status.status = git_wc_status_normal;
243 status.skipWorktree = true;
245 else if (entry.m_Flags & GIT_INDEX_ENTRY_VALID)
247 status.status = git_wc_status_normal;
248 status.assumeValid = true;
250 else if (filesize == -1)
251 status.status = git_wc_status_deleted;
252 else if ((isSymlink && !S_ISLNK(entry.m_Mode)) || ((m_iIndexCaps & GIT_INDEX_CAPABILITY_NO_SYMLINKS) != GIT_INDEX_CAPABILITY_NO_SYMLINKS && isSymlink != S_ISLNK(entry.m_Mode)))
253 status.status = git_wc_status_modified;
254 else if (!isSymlink && static_cast<uint32_t>(filesize) != entry.m_Size)
255 status.status = git_wc_status_modified;
256 else if (static_cast<int32_t>(CGit::filetime_to_time_t(time)) == entry.m_ModifyTime && entry.m_ModifyTimeNanos == (time % 10000000) * 100)
257 status.status = git_wc_status_normal;
258 else if (config && filesize < m_iMaxCheckSize)
261 * Opening a new repository each time is not yet optimal, however, there is no API to clear the pack-cache
262 * When a shared repository is used, we might need a mutex to prevent concurrent access to repository instance and especially filter-lists
264 if (!repository)
266 CString repodir = gitdir;
267 if (gitdir.GetLength() == 2 && gitdir[1] == L':')
268 repodir += L'\\'; // libgit2 requires a drive root to end with a (back)slash
270 if (repository.Open(repodir))
272 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not open git repository in %s for checking file: %s\n", static_cast<LPCWSTR>(gitdir), static_cast<LPCWSTR>(CGit::GetLibGit2LastErr()));
273 return -1;
275 git_repository_set_config(repository, config);
278 git_oid actual;
279 CStringA fileA = CUnicodeUtils::GetUTF8(entry.m_FileName);
280 if (isSymlink && S_ISLNK(entry.m_Mode))
282 CStringA linkDestination;
283 if (!CPathUtils::ReadLink(CombinePath(gitdir, entry.m_FileName), &linkDestination) && !git_odb_hash(&actual, static_cast<LPCSTR>(linkDestination), linkDestination.GetLength(), GIT_OBJECT_BLOB) && !git_oid_cmp(&actual, entry.m_IndexHash))
285 entry.m_ModifyTime = static_cast<int32_t>(CGit::filetime_to_time_t(time));
286 entry.m_ModifyTimeNanos = (time % 10000000) * 100;
287 status.status = git_wc_status_normal;
289 else
290 status.status = git_wc_status_modified;
292 else if (!git_repository_hashfile(&actual, repository, fileA, GIT_OBJECT_BLOB, nullptr) && !git_oid_cmp(&actual, entry.m_IndexHash))
294 entry.m_ModifyTime = static_cast<int32_t>(CGit::filetime_to_time_t(time));
295 entry.m_ModifyTimeNanos = (time % 10000000) * 100;
296 status.status = git_wc_status_normal;
298 else
299 status.status = git_wc_status_modified;
301 else
302 status.status = git_wc_status_modified;
304 if (entry.m_Flags & GIT_INDEX_ENTRY_STAGEMASK)
305 status.status = git_wc_status_conflicted;
306 else if (entry.m_FlagsExtended & GIT_INDEX_ENTRY_INTENT_TO_ADD)
307 status.status = git_wc_status_added;
309 return 0;
312 int CGitIndexList::GetFileStatus(const CString& gitdir, const CString& path, git_wc_status2_t& status, CGitHash* pHash) const
314 ATLASSERT(!status.assumeValid && !status.skipWorktree);
316 __int64 time, filesize = 0;
317 bool isDir = false;
318 bool isSymlink = false;
320 int result;
321 if (path.IsEmpty())
322 result = CGit::GetFileModifyTime(gitdir, &time, &isDir);
323 else
324 result = CGit::GetFileModifyTime(CombinePath(gitdir, path), &time, &isDir, &filesize, &isSymlink);
326 if (result)
327 filesize = -1;
329 if (!isDir || (isSymlink && (m_iIndexCaps & GIT_INDEX_CAPABILITY_NO_SYMLINKS) != GIT_INDEX_CAPABILITY_NO_SYMLINKS))
330 return GetFileStatus(gitdir, path, status, time, filesize, isSymlink, pHash);
332 if (CStringUtils::EndsWith(path, L'/'))
334 size_t index = SearchInSortVector(*this, path, -1, IsIgnoreCase());
335 if (index == NPOS)
337 status.status = git_wc_status_unversioned;
338 if (pHash)
339 pHash->Empty();
341 return 0;
344 if (pHash)
345 *pHash = (*this)[index].m_IndexHash;
347 if (!result)
348 status.status = git_wc_status_normal;
349 else
350 status.status = git_wc_status_deleted;
351 return 0;
354 // we get here for symlinks which are handled as files inside the git index
355 if ((m_iIndexCaps & GIT_INDEX_CAPABILITY_NO_SYMLINKS) != GIT_INDEX_CAPABILITY_NO_SYMLINKS)
356 return GetFileStatus(gitdir, path, status, time, filesize, isSymlink, pHash);
358 // we should never get here
359 status.status = git_wc_status_unversioned;
361 return -1;
364 // This method is assumed to be called with m_SharedMutex locked.
365 int CGitHeadFileList::GetPackRef(const CString &gitdir)
367 CString PackRef = g_AdminDirMap.GetAdminDirConcat(gitdir, L"packed-refs");
369 __int64 mtime = 0, packsize = -1;
370 if (CGit::GetFileModifyTime(PackRef, &mtime, nullptr, &packsize))
372 //packed refs is not existed
373 this->m_PackRefFile.Empty();
374 this->m_PackRefMap.clear();
375 return 0;
377 else if (mtime == m_LastModifyTimePackRef && packsize == m_LastFileSizePackRef)
378 return 0;
379 else
381 this->m_PackRefFile = PackRef;
382 this->m_LastModifyTimePackRef = mtime;
383 this->m_LastFileSizePackRef = packsize;
386 m_PackRefMap.clear();
388 CAutoFile hfile = CreateFile(PackRef,
389 GENERIC_READ,
390 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
391 nullptr,
392 OPEN_EXISTING,
393 FILE_ATTRIBUTE_NORMAL,
394 nullptr);
396 if (!hfile)
397 return -1;
399 LARGE_INTEGER fileSize;
400 if (!::GetFileSizeEx(hfile, &fileSize) || fileSize.QuadPart >= INT_MAX)
401 return -1;
403 DWORD size = 0;
404 auto buff = std::unique_ptr<char[]>(new (std::nothrow) char[fileSize.LowPart]); // prevent default initialization and throwing on allocation error
405 if (!buff)
406 return -1;
408 if (!ReadFile(hfile, buff.get(), fileSize.LowPart, &size, nullptr))
409 return -1;
411 if (size != fileSize.LowPart)
412 return -1;
414 for (DWORD i = 0; i < fileSize.LowPart;)
416 CString hash;
417 CString ref;
418 if (buff[i] == '#' || buff[i] == '^')
420 while (buff[i] != '\n')
422 ++i;
423 if (i == fileSize.LowPart)
424 break;
426 ++i;
429 if (i >= fileSize.LowPart)
430 break;
432 while (buff[i] != ' ')
434 hash.AppendChar(buff[i]);
435 ++i;
436 if (i == fileSize.LowPart)
437 break;
440 ++i;
441 if (i >= fileSize.LowPart)
442 break;
444 while (buff[i] != '\n')
446 ref.AppendChar(buff[i]);
447 ++i;
448 if (i == fileSize.LowPart)
449 break;
452 if (!ref.IsEmpty())
453 m_PackRefMap[ref] = CGitHash::FromHexStrTry(hash);
455 while (buff[i] == '\n')
457 ++i;
458 if (i == fileSize.LowPart)
459 break;
462 return 0;
464 int CGitHeadFileList::ReadHeadHash(const CString& gitdir)
466 ATLASSERT(m_Gitdir.IsEmpty() && m_HeadFile.IsEmpty() && m_Head.IsEmpty());
468 m_Gitdir = g_AdminDirMap.GetWorktreeAdminDir(gitdir);
470 m_HeadFile = m_Gitdir;
471 m_HeadFile += L"HEAD";
473 if (CGit::GetFileModifyTime(m_HeadFile, &m_LastModifyTimeHead, nullptr, &m_LastFileSizeHead))
474 return -1;
476 CAutoFile hfile = CreateFile(m_HeadFile,
477 GENERIC_READ,
478 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
479 nullptr,
480 OPEN_EXISTING,
481 FILE_ATTRIBUTE_NORMAL,
482 nullptr);
484 if (!hfile)
485 return -1;
487 DWORD size = 0;
488 unsigned char buffer[2 * GIT_HASH_SIZE];
489 ReadFile(hfile, buffer, static_cast<DWORD>(strlen("ref:")), &size, nullptr);
490 if (size != strlen("ref:"))
491 return -1;
492 buffer[strlen("ref:")] = '\0';
493 if (strcmp(reinterpret_cast<const char*>(buffer), "ref:") == 0)
495 m_HeadRefFile.Empty();
496 LARGE_INTEGER fileSize;
497 if (!::GetFileSizeEx(hfile, &fileSize) || fileSize.QuadPart < static_cast<int>(strlen("ref:") + 1) || fileSize.QuadPart >= 100 * 1024 * 1024)
498 return -1;
501 auto p = std::unique_ptr<char[]>(new (std::nothrow) char[fileSize.LowPart - strlen("ref:")]); // prevent default initialization and throwing on allocation error
502 if (!p)
503 return -1;
505 if (!ReadFile(hfile, p.get(), fileSize.LowPart - static_cast<DWORD>(strlen("ref:")), &size, nullptr))
506 return -1;
507 CGit::StringAppend(m_HeadRefFile, p.get(), CP_UTF8, fileSize.LowPart - static_cast<int>(strlen("ref:")));
509 CString ref = m_HeadRefFile.Trim();
510 int start = 0;
511 ref = ref.Tokenize(L"\n", start);
512 m_HeadRefFile = g_AdminDirMap.GetAdminDir(gitdir) + m_HeadRefFile;
513 m_HeadRefFile.Replace(L'/', L'\\');
515 __int64 time;
516 if (CGit::GetFileModifyTime(m_HeadRefFile, &time, nullptr))
518 if (GetPackRef(gitdir))
519 return -1;
520 if (m_PackRefMap.find(ref) != m_PackRefMap.end())
522 m_bRefFromPackRefFile = true;
523 m_Head = m_PackRefMap[ref];
524 return 0;
527 // unborn branch
528 m_Head.Empty();
530 return 0;
533 CAutoFile href = CreateFile(m_HeadRefFile,
534 GENERIC_READ,
535 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
536 nullptr,
537 OPEN_EXISTING,
538 FILE_ATTRIBUTE_NORMAL,
539 nullptr);
541 if (!href)
543 if (GetPackRef(gitdir))
544 return -1;
546 if (m_PackRefMap.find(ref) == m_PackRefMap.end())
547 return -1;
549 m_bRefFromPackRefFile = true;
550 m_Head = m_PackRefMap[ref];
551 return 0;
554 ReadFile(href, buffer, 2 * GIT_HASH_SIZE, &size, nullptr);
555 if (size != 2 * GIT_HASH_SIZE)
556 return -1;
558 m_Head = CGitHash::FromHexStr(reinterpret_cast<const char*>(buffer));
560 m_LastModifyTimeRef = time;
562 return 0;
565 ReadFile(hfile, buffer + static_cast<DWORD>(strlen("ref:")), 2 * GIT_HASH_SIZE - static_cast<DWORD>(strlen("ref:")), &size, nullptr);
566 if (size != 2 * GIT_HASH_SIZE - static_cast<DWORD>(strlen("ref:")))
567 return -1;
569 m_HeadRefFile.Empty();
571 m_Head = CGitHash::FromHexStr(reinterpret_cast<const char*>(buffer));
573 return 0;
576 bool CGitHeadFileList::CheckHeadUpdate() const
578 if (this->m_HeadFile.IsEmpty())
579 return true;
581 __int64 mtime = 0, size = -1;
583 if (CGit::GetFileModifyTime(m_HeadFile, &mtime, nullptr, &size))
584 return true;
586 if (mtime != m_LastModifyTimeHead || size != m_LastFileSizeHead)
587 return true;
589 if (!this->m_HeadRefFile.IsEmpty())
591 // we need to check for the HEAD ref file here, because the original ref might have come from packedrefs and now is a ref-file
592 if (CGit::GetFileModifyTime(m_HeadRefFile, &mtime))
594 if (!m_bRefFromPackRefFile)
595 return true;
597 } else if (mtime != this->m_LastModifyTimeRef)
598 return true;
601 if (m_bRefFromPackRefFile && !m_PackRefFile.IsEmpty())
603 size = -1;
604 if (CGit::GetFileModifyTime(m_PackRefFile, &mtime, nullptr, &size))
605 return true;
607 if (mtime != m_LastModifyTimePackRef || size != m_LastFileSizePackRef)
608 return true;
611 // in an empty repo HEAD points to refs/heads/master, but this ref doesn't exist.
612 // So we need to retry again and again until the ref exists - otherwise we will never notice
613 if (this->m_Head.IsEmpty() && this->m_HeadRefFile.IsEmpty() && this->m_PackRefFile.IsEmpty())
614 return true;
616 return false;
619 int CGitHeadFileList::ReadTreeRecursive(git_repository& repo, const git_tree* tree, const CString& base)
621 #define S_IFGITLINK 0160000
622 size_t count = git_tree_entrycount(tree);
623 for (size_t i = 0; i < count; ++i)
625 const git_tree_entry *entry = git_tree_entry_byindex(tree, i);
626 if (!entry)
627 continue;
628 const int mode = git_tree_entry_filemode(entry);
629 const bool isDir = (mode & S_IFDIR) == S_IFDIR;
630 const bool isSubmodule = (mode & S_IFMT) == S_IFGITLINK;
631 if (!isDir || isSubmodule)
633 CGitTreeItem item;
634 item.m_Hash = git_tree_entry_id(entry);
635 item.m_FileName = base;
636 CGit::StringAppend(item.m_FileName, git_tree_entry_name(entry), CP_UTF8);
637 if (isSubmodule)
638 item.m_FileName += L'/';
639 push_back(item);
640 continue;
643 CAutoObject object;
644 git_tree_entry_to_object(object.GetPointer(), &repo, entry);
645 if (!object)
646 continue;
647 CString parent = base;
648 CGit::StringAppend(parent, git_tree_entry_name(entry));
649 parent += L'/';
650 ReadTreeRecursive(repo, reinterpret_cast<git_tree*>(static_cast<git_object*>(object)), parent);
653 return 0;
656 // ReadTree is/must only be executed on an empty list
657 int CGitHeadFileList::ReadTree(bool ignoreCase)
659 ATLASSERT(empty());
661 // unborn branch
662 if (m_Head.IsEmpty())
663 return 0;
665 CAutoRepository repository(m_Gitdir);
666 CAutoCommit commit;
667 CAutoTree tree;
668 bool ret = repository;
669 ret = ret && !git_commit_lookup(commit.GetPointer(), repository, m_Head);
670 ret = ret && !git_commit_tree(tree.GetPointer(), commit);
673 ret = ret && !ReadTreeRecursive(*repository, tree, L"");
675 catch (const std::bad_alloc& ex)
677 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Catched exception inside ReadTreeRecursive: %s\n", ex.what());
678 return -1;
680 catch (const std::length_error& ex)
682 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Catched exception inside ReadTreeRecursive, length_error: %s\n", ex.what());
683 return -1;
685 if (!ret)
687 clear();
688 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not open git repository in %s and read HEAD commit %s: %s\n", static_cast<LPCWSTR>(m_Gitdir), static_cast<LPCWSTR>(m_Head.ToString()), static_cast<LPCWSTR>(CGit::GetLibGit2LastErr()));
689 m_LastModifyTimeHead = 0;
690 m_LastFileSizeHead = -1;
691 return -1;
694 DoSortFilenametSortVector(*this, ignoreCase);
696 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Reloaded HEAD tree (commit is %s) for repo: %s\n", static_cast<LPCWSTR>(m_Head.ToString()), static_cast<LPCWSTR>(m_Gitdir));
698 return 0;
700 int CGitIgnoreItem::FetchIgnoreList(const CString& projectroot, const CString& file, bool isGlobal, int* ignoreCase)
702 if (this->m_pExcludeList)
704 git_free_exclude_list(m_pExcludeList);
705 m_pExcludeList = nullptr;
707 m_buffer = nullptr;
709 this->m_BaseDir.Empty();
710 if (!isGlobal)
712 CString base = file.Mid(projectroot.GetLength() + 1);
713 base.Replace(L'\\', L'/');
715 int start = base.ReverseFind(L'/');
716 if(start > 0)
718 base.Truncate(start);
719 this->m_BaseDir = CUnicodeUtils::GetUTF8(base) + "/";
723 if (CGit::GetFileModifyTime(file, &m_LastModifyTime, nullptr, &m_LastFileSize))
724 return -1;
726 CAutoFile hfile = CreateFile(file,
727 GENERIC_READ,
728 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
729 nullptr,
730 OPEN_EXISTING,
731 FILE_ATTRIBUTE_NORMAL,
732 nullptr);
734 if (!hfile)
735 return -1 ;
737 LARGE_INTEGER fileSize;
738 if (!::GetFileSizeEx(hfile, &fileSize) || fileSize.QuadPart >= INT_MAX)
739 return -1;
741 m_buffer = std::unique_ptr<char[]>(new (std::nothrow) char[fileSize.LowPart + 1]); // prevent default initialization and throwing on allocation error
742 if (!m_buffer)
743 return -1;
745 DWORD size = 0;
746 if (!ReadFile(hfile, m_buffer.get(), fileSize.LowPart, &size, nullptr))
748 m_buffer = nullptr;
749 return -1;
751 m_buffer[size] = '\0';
753 if (git_create_exclude_list(&m_pExcludeList))
755 m_buffer = nullptr;
756 return -1;
759 m_iIgnoreCase = ignoreCase;
761 const char *p = m_buffer.get();
762 int line = 0;
763 for (DWORD i = 0; i < size; ++i)
765 if (m_buffer[i] == '\n' || m_buffer[i] == '\r' || i == (size - 1))
767 if (m_buffer[i] == '\n' || m_buffer[i] == '\r')
768 m_buffer[i] = '\0';
770 if (p[0] != '#' && p[0])
771 git_add_exclude(p, m_BaseDir, m_BaseDir.GetLength(), m_pExcludeList, ++line);
773 p = m_buffer.get() + i + 1;
777 if (!line)
779 git_free_exclude_list(m_pExcludeList);
780 m_pExcludeList = nullptr;
781 m_buffer = nullptr;
784 return 0;
787 #ifdef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
788 int CGitIgnoreItem::IsPathIgnored(const CStringA& patha, int& type)
790 int pos = patha.ReverseFind('/');
791 const char* base = (pos >= 0) ? (static_cast<const char*>(patha) + pos + 1) : static_cast<const char*>(patha);
793 return IsPathIgnored(patha, base, type);
795 #endif
797 int CGitIgnoreItem::IsPathIgnored(const CStringA& patha, const char* base, int& type)
799 if (!m_pExcludeList)
800 return -1; // error or undecided
802 return git_check_excluded_1(patha, patha.GetLength(), base, &type, m_pExcludeList, m_iIgnoreCase ? *m_iIgnoreCase : 1);
805 bool CGitIgnoreList::CheckFileChanged(const CString &path)
807 __int64 time = 0, size = -1;
809 const int ret = CGit::GetFileModifyTime(path, &time, nullptr, &size);
811 bool cacheExist;
813 CAutoReadLock lock(m_SharedMutex);
814 cacheExist = (m_Map.find(path) != m_Map.end());
817 if (!cacheExist && ret == 0)
819 CAutoWriteLock lock(m_SharedMutex);
820 m_Map[path].m_LastModifyTime = 0;
821 m_Map[path].m_LastFileSize = -1;
823 // both cache and file is not exist
824 if ((ret != 0) && (!cacheExist))
825 return false;
827 // file exist but cache miss
828 if ((ret == 0) && (!cacheExist))
829 return true;
831 // file not exist but cache exist
832 if ((ret != 0) && (cacheExist))
833 return true;
834 // file exist and cache exist
837 CAutoReadLock lock(m_SharedMutex);
838 if (m_Map[path].m_LastModifyTime == time && m_Map[path].m_LastFileSize == size)
839 return false;
841 return true;
844 int CGitIgnoreList::FetchIgnoreFile(const CString &gitdir, const CString &gitignore, bool isGlobal)
846 if (CGit::GitPathFileExists(gitignore)) //if .gitignore remove, we need remote cache
848 CAutoWriteLock lock(m_SharedMutex);
849 m_Map[gitignore].FetchIgnoreList(gitdir, gitignore, isGlobal, &m_IgnoreCase[g_AdminDirMap.GetAdminDir(gitdir)]);
851 else
853 CAutoWriteLock lock(m_SharedMutex);
854 m_Map.erase(gitignore);
856 return 0;
859 bool CGitIgnoreList::CheckAndUpdateIgnoreFiles(const CString& gitdir, const CString& path, bool isDir, std::set<CString>* lastChecked)
861 CString temp(gitdir);
862 temp += L'\\';
863 temp += path;
865 temp.Replace(L'/', L'\\');
867 if (!isDir)
869 const int x = temp.ReverseFind(L'\\');
870 if (x >= 2)
871 temp.Truncate(x);
874 bool updated = false;
875 while (!temp.IsEmpty())
877 if (lastChecked)
879 if (lastChecked->find(temp) != lastChecked->end())
880 return updated;
881 lastChecked->insert(temp);
884 temp += L"\\.gitignore";
886 if (CheckFileChanged(temp))
888 FetchIgnoreFile(gitdir, temp, false);
889 updated = true;
892 temp.Truncate(temp.GetLength() - static_cast<int>(wcslen(L"\\.gitignore")));
893 if (CPathUtils::ArePathStringsEqual(temp, gitdir))
895 CString adminDir = g_AdminDirMap.GetAdminDir(temp);
896 CString wcglobalgitignore = adminDir + L"info\\exclude";
897 if (CheckFileChanged(wcglobalgitignore))
899 FetchIgnoreFile(gitdir, wcglobalgitignore, true);
900 updated = true;
903 if (CheckAndUpdateCoreExcludefile(adminDir))
905 CString excludesFile;
907 CAutoReadLock lock(m_SharedMutex);
908 excludesFile = m_CoreExcludesfiles[adminDir];
910 if (!excludesFile.IsEmpty())
912 FetchIgnoreFile(gitdir, excludesFile, true);
913 updated = true;
917 return updated;
920 const int i = temp.ReverseFind(L'\\');
921 temp.Truncate(max(0, i));
923 return updated;
926 bool CGitIgnoreList::CheckAndUpdateGitSystemConfigPath(bool force)
928 // recheck every 30 seconds
929 if (GetTickCount64() - m_dGitSystemConfigPathLastChecked > 30000UL || force)
931 m_dGitSystemConfigPathLastChecked = GetTickCount64();
932 CString gitSystemConfigPath(CRegString(REG_SYSTEM_GITCONFIGPATH, L"", FALSE));
933 if (gitSystemConfigPath != m_sGitSystemConfigPath)
935 m_sGitSystemConfigPath = gitSystemConfigPath;
936 return true;
939 return false;
941 bool CGitIgnoreList::CheckAndUpdateCoreExcludefile(const CString &adminDir)
943 CString projectConfig(adminDir);
944 projectConfig += L"config";
945 CString globalConfig = g_Git.GetGitGlobalConfig();
946 CString globalXDGConfig = g_Git.GetGitGlobalXDGConfig();
948 CAutoWriteLock lock(m_coreExcludefilesSharedMutex);
949 bool hasChanged = CheckAndUpdateGitSystemConfigPath();
950 hasChanged = hasChanged || CheckFileChanged(projectConfig);
951 hasChanged = hasChanged || CheckFileChanged(globalConfig);
952 hasChanged = hasChanged || CheckFileChanged(globalXDGConfig);
953 if (!m_sGitSystemConfigPath.IsEmpty())
954 hasChanged = hasChanged || CheckFileChanged(m_sGitSystemConfigPath);
956 CString excludesFile;
958 CAutoReadLock lock2(m_SharedMutex);
959 excludesFile = m_CoreExcludesfiles[adminDir];
961 if (!excludesFile.IsEmpty())
962 hasChanged = hasChanged || CheckFileChanged(excludesFile);
964 if (!hasChanged)
965 return false;
967 CAutoConfig config(true);
968 CAutoRepository repo(adminDir);
969 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(projectConfig), GIT_CONFIG_LEVEL_LOCAL, repo, FALSE);
970 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalConfig), GIT_CONFIG_LEVEL_GLOBAL, repo, FALSE);
971 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalXDGConfig), GIT_CONFIG_LEVEL_XDG, repo, FALSE);
972 if (!m_sGitSystemConfigPath.IsEmpty())
973 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(m_sGitSystemConfigPath), GIT_CONFIG_LEVEL_SYSTEM, repo, FALSE);
975 config.GetString(L"core.excludesfile", excludesFile);
976 if (excludesFile.IsEmpty())
977 excludesFile = GetWindowsHome() + L"\\.config\\git\\ignore";
978 else if (CStringUtils::StartsWith(excludesFile, L"~/"))
979 excludesFile = GetWindowsHome() + excludesFile.Mid(static_cast<int>(wcslen(L"~")));
981 CAutoWriteLock lockMap(m_SharedMutex);
982 m_IgnoreCase[adminDir] = 1;
983 config.GetBOOL(L"core.ignorecase", m_IgnoreCase[adminDir]);
984 CGit::GetFileModifyTime(projectConfig, &m_Map[projectConfig].m_LastModifyTime, nullptr, &m_Map[projectConfig].m_LastFileSize);
985 CGit::GetFileModifyTime(globalXDGConfig, &m_Map[globalXDGConfig].m_LastModifyTime, nullptr, &m_Map[globalXDGConfig].m_LastFileSize);
986 if (m_Map[globalXDGConfig].m_LastModifyTime == 0)
987 m_Map.erase(globalXDGConfig);
988 CGit::GetFileModifyTime(globalConfig, &m_Map[globalConfig].m_LastModifyTime, nullptr, &m_Map[globalConfig].m_LastFileSize);
989 if (m_Map[globalConfig].m_LastModifyTime == 0)
990 m_Map.erase(globalConfig);
991 if (!m_sGitSystemConfigPath.IsEmpty())
992 CGit::GetFileModifyTime(m_sGitSystemConfigPath, &m_Map[m_sGitSystemConfigPath].m_LastModifyTime, nullptr, &m_Map[m_sGitSystemConfigPath].m_LastFileSize);
993 if (m_Map[m_sGitSystemConfigPath].m_LastModifyTime == 0 || m_sGitSystemConfigPath.IsEmpty())
994 m_Map.erase(m_sGitSystemConfigPath);
995 m_CoreExcludesfiles[adminDir] = excludesFile;
997 return true;
999 const CString CGitIgnoreList::GetWindowsHome()
1001 static CString sWindowsHome(g_Git.GetHomeDirectory());
1002 return sWindowsHome;
1004 bool CGitIgnoreList::IsIgnore(CString str, const CString& projectroot, bool isDir, const CString& adminDir)
1006 str.Replace(L'\\', L'/');
1008 if (!str.IsEmpty() && str[str.GetLength() - 1] == L'/')
1009 str.Truncate(str.GetLength() - 1);
1011 int ret = CheckIgnore(str, projectroot, isDir, adminDir);
1012 while (ret < 0)
1014 int start = str.ReverseFind(L'/');
1015 if(start < 0)
1016 return (ret == 1);
1018 str.Truncate(start);
1019 ret = CheckIgnore(str, projectroot, TRUE, adminDir);
1022 return (ret == 1);
1024 int CGitIgnoreList::CheckFileAgainstIgnoreList(const CString &ignorefile, const CStringA &patha, const char * base, int &type)
1026 if (m_Map.find(ignorefile) == m_Map.end())
1027 return -1; // error or undecided
1029 return (m_Map[ignorefile].IsPathIgnored(patha, base, type));
1031 int CGitIgnoreList::CheckIgnore(const CString &path, const CString &projectroot, bool isDir, const CString& adminDir)
1033 CString temp = CombinePath(projectroot, path);
1034 temp.Replace(L'/', L'\\');
1036 CStringA patha = CUnicodeUtils::GetUTF8(path);
1037 patha.Replace('\\', '/');
1039 int type = 0;
1040 if (isDir)
1042 type = DT_DIR;
1044 // strip directory name
1045 // we do not need to check for a .ignore file inside a directory we might ignore
1046 const int i = temp.ReverseFind(L'\\');
1047 if (i >= 0)
1048 temp.Truncate(i);
1050 else
1052 type = DT_REG;
1054 int x = temp.ReverseFind(L'\\');
1055 if (x >= 2)
1056 temp.Truncate(x);
1059 int pos = patha.ReverseFind('/');
1060 const char* base = (pos >= 0) ? (static_cast<const char*>(patha) + pos + 1) : static_cast<const char*>(patha);
1063 CAutoReadLock lock(m_SharedMutex);
1064 while (!temp.IsEmpty())
1066 temp += L"\\.gitignore";
1068 if (auto ret = CheckFileAgainstIgnoreList(temp, patha, base, type); ret != -1)
1069 return ret;
1071 temp.Truncate(temp.GetLength() - static_cast<int>(wcslen(L"\\.gitignore")));
1073 if (CPathUtils::ArePathStringsEqual(temp, projectroot))
1075 CString wcglobalgitignore = adminDir;
1076 wcglobalgitignore += L"info\\exclude";
1077 if (auto ret = CheckFileAgainstIgnoreList(wcglobalgitignore, patha, base, type); ret != -1)
1078 return ret;
1080 CString excludesFile = m_CoreExcludesfiles[adminDir];
1081 if (!excludesFile.IsEmpty())
1082 return CheckFileAgainstIgnoreList(excludesFile, patha, base, type);
1084 return -1;
1087 const int i = temp.ReverseFind(L'\\');
1088 temp.Truncate(max(0, i));
1091 return -1;
1094 SHARED_TREE_PTR CGitHeadFileMap::CheckHeadAndUpdate(const CString& gitdir, bool ignoreCase)
1096 if (auto ptr = this->SafeGet(gitdir); ptr.get() && !ptr->CheckHeadUpdate())
1097 return ptr;
1099 auto newPtr = std::make_shared<CGitHeadFileList>();
1100 if (newPtr->ReadHeadHash(gitdir) || newPtr->ReadTree(ignoreCase))
1102 SafeClear(gitdir);
1103 return {};
1106 this->SafeSet(gitdir, newPtr);
1108 return newPtr;