Update libgit2
[TortoiseGit.git] / src / Git / GitIndex.cpp
blob2b53f52c025b0f9f56f730c01d9263f044dfe6b0
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2019 - 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"
31 CGitAdminDirMap g_AdminDirMap;
33 static CString GetProgramDataGitConfig()
35 if (!((CRegDWORD(L"Software\\TortoiseGit\\CygwinHack", FALSE) == TRUE) || (CRegDWORD(L"Software\\TortoiseGit\\Msys2Hack", FALSE) == TRUE)))
37 CString programdataConfig;
38 if (SHGetFolderPath(nullptr, CSIDL_COMMON_APPDATA, NULL, SHGFP_TYPE_CURRENT, CStrBuf(programdataConfig, MAX_PATH)) == S_OK && programdataConfig.GetLength() < MAX_PATH - (int)wcslen(L"\\Git\\config"))
39 return programdataConfig + L"\\Git\\config";
41 return L"";
44 int CGitIndex::Print()
46 wprintf(L"0x%08X 0x%08X %s %s\n",
47 (int)this->m_ModifyTime,
48 this->m_Flags,
49 (LPCTSTR)this->m_IndexHash.ToString(),
50 (LPCTSTR)this->m_FileName);
52 return 0;
55 CGitIndexList::CGitIndexList()
56 : m_bHasConflicts(FALSE)
57 , m_LastModifyTime(0)
58 , m_LastFileSize(-1)
59 , m_iIndexCaps(GIT_INDEXCAP_IGNORE_CASE | GIT_INDEXCAP_NO_SYMLINKS)
61 m_iMaxCheckSize = (__int64)CRegDWORD(L"Software\\TortoiseGit\\TGitCacheCheckContentMaxSize", 10 * 1024) * 1024; // stored in KiB
64 CGitIndexList::~CGitIndexList()
68 int CGitIndexList::ReadIndex(CString dgitdir)
70 #ifdef GTEST_INCLUDE_GTEST_GTEST_H_
71 clear(); // HACK to make tests work, until we use CGitIndexList
72 #endif
73 ATLASSERT(empty());
75 CAutoRepository repository(dgitdir);
76 if (!repository)
78 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not open git repository in %s: %s\n", (LPCTSTR)dgitdir, (LPCTSTR)CGit::GetLibGit2LastErr());
79 return -1;
82 // add config files
83 config.New();
85 CString projectConfig = g_AdminDirMap.GetAdminDir(dgitdir) + L"config";
86 CString globalConfig = g_Git.GetGitGlobalConfig();
87 CString globalXDGConfig = g_Git.GetGitGlobalXDGConfig();
88 CString systemConfig(CRegString(REG_SYSTEM_GITCONFIGPATH, L"", FALSE));
89 CString programDataConfig(GetProgramDataGitConfig());
91 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(projectConfig), GIT_CONFIG_LEVEL_LOCAL, repository, FALSE);
92 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalConfig), GIT_CONFIG_LEVEL_GLOBAL, repository, FALSE);
93 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalXDGConfig), GIT_CONFIG_LEVEL_XDG, repository, FALSE);
94 if (!systemConfig.IsEmpty())
95 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(systemConfig), GIT_CONFIG_LEVEL_SYSTEM, repository, FALSE);
96 if (!programDataConfig.IsEmpty())
97 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(programDataConfig), GIT_CONFIG_LEVEL_PROGRAMDATA, repository, FALSE);
99 git_repository_set_config(repository, config);
101 CGit::GetFileModifyTime(g_AdminDirMap.GetWorktreeAdminDir(dgitdir) + L"index", &m_LastModifyTime, nullptr, &m_LastFileSize);
103 CAutoIndex index;
104 // load index in order to enumerate files
105 if (git_repository_index(index.GetPointer(), repository))
107 config.Free();
108 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not get index of git repository in %s: %s\n", (LPCTSTR)dgitdir, (LPCTSTR)CGit::GetLibGit2LastErr());
109 return -1;
112 m_bHasConflicts = FALSE;
113 m_iIndexCaps = git_index_caps(index);
114 if (CRegDWORD(L"Software\\TortoiseGit\\OverlaysCaseSensitive", TRUE) != FALSE)
115 m_iIndexCaps &= ~GIT_INDEXCAP_IGNORE_CASE;
117 size_t ecount = git_index_entrycount(index);
120 resize(ecount);
122 catch (const std::bad_alloc& ex)
124 config.Free();
125 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Could not resize index-vector: %s\n", ex.what());
126 return -1;
128 for (size_t i = 0; i < ecount; ++i)
130 const git_index_entry *e = git_index_get_byindex(index, i);
132 auto& item = (*this)[i];
133 item.m_FileName = CUnicodeUtils::GetUnicode(e->path);
134 if (e->mode & S_IFDIR)
135 item.m_FileName += L'/';
136 item.m_ModifyTime = e->mtime.seconds;
137 item.m_Flags = e->flags;
138 item.m_FlagsExtended = e->flags_extended;
139 item.m_IndexHash = e->id;
140 item.m_Size = e->file_size;
141 item.m_Mode = e->mode;
142 m_bHasConflicts |= GIT_IDXENTRY_STAGE(e);
145 DoSortFilenametSortVector(*this, IsIgnoreCase());
147 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Reloaded index for repo: %s\n", (LPCTSTR)dgitdir);
149 return 0;
152 int CGitIndexList::GetFileStatus(const CString& gitdir, const CString& pathorg, git_wc_status2_t& status, __int64 time, __int64 filesize, bool isSymlink, CGitHash* pHash)
154 size_t index = SearchInSortVector(*this, pathorg, -1, IsIgnoreCase());
156 if (index == NPOS)
158 status.status = git_wc_status_unversioned;
159 if (pHash)
160 pHash->Empty();
162 return 0;
165 auto& entry = (*this)[index];
166 if (pHash)
167 *pHash = entry.m_IndexHash;
168 ATLASSERT(IsIgnoreCase() ? pathorg.CompareNoCase(entry.m_FileName) == 0 : pathorg.Compare(entry.m_FileName) == 0);
169 CAutoRepository repository;
170 return GetFileStatus(repository, gitdir, entry, status, time, filesize, isSymlink);
173 int CGitIndexList::GetFileStatus(CAutoRepository& repository, const CString& gitdir, CGitIndex& entry, git_wc_status2_t& status, __int64 time, __int64 filesize, bool isSymlink)
175 ATLASSERT(!status.assumeValid && !status.skipWorktree);
177 // skip-worktree has higher priority than assume-valid
178 if (entry.m_FlagsExtended & GIT_IDXENTRY_SKIP_WORKTREE)
180 status.status = git_wc_status_normal;
181 status.skipWorktree = true;
183 else if (entry.m_Flags & GIT_IDXENTRY_VALID)
185 status.status = git_wc_status_normal;
186 status.assumeValid = true;
188 else if (filesize == -1)
189 status.status = git_wc_status_deleted;
190 else if ((isSymlink && !S_ISLNK(entry.m_Mode)) || ((m_iIndexCaps & GIT_INDEXCAP_NO_SYMLINKS) != GIT_INDEXCAP_NO_SYMLINKS && isSymlink != S_ISLNK(entry.m_Mode)))
191 status.status = git_wc_status_modified;
192 else if (!isSymlink && filesize != entry.m_Size)
193 status.status = git_wc_status_modified;
194 else if (CGit::filetime_to_time_t(time) == entry.m_ModifyTime)
195 status.status = git_wc_status_normal;
196 else if (config && filesize < m_iMaxCheckSize)
199 * Opening a new repository each time is not yet optimal, however, there is no API to clear the pack-cache
200 * When a shared repository is used, we might need a mutex to prevent concurrent access to repository instance and especially filter-lists
202 if (!repository)
204 if (repository.Open(gitdir))
206 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not open git repository in %s for checking file: %s\n", (LPCTSTR)gitdir, (LPCTSTR)CGit::GetLibGit2LastErr());
207 return -1;
209 git_repository_set_config(repository, config);
212 git_oid actual;
213 CStringA fileA = CUnicodeUtils::GetMulti(entry.m_FileName, CP_UTF8);
214 if (isSymlink && S_ISLNK(entry.m_Mode))
216 CStringA linkDestination;
217 if (!CPathUtils::ReadLink(CombinePath(gitdir, entry.m_FileName), &linkDestination) && !git_odb_hash(&actual, (void*)(LPCSTR)linkDestination, linkDestination.GetLength(), GIT_OBJECT_BLOB) && !git_oid_cmp(&actual, entry.m_IndexHash))
219 entry.m_ModifyTime = time;
220 status.status = git_wc_status_normal;
222 else
223 status.status = git_wc_status_modified;
225 else if (!git_repository_hashfile(&actual, repository, fileA, GIT_OBJECT_BLOB, nullptr) && !git_oid_cmp(&actual, entry.m_IndexHash))
227 entry.m_ModifyTime = time;
228 status.status = git_wc_status_normal;
230 else
231 status.status = git_wc_status_modified;
233 else
234 status.status = git_wc_status_modified;
236 if (entry.m_Flags & GIT_IDXENTRY_STAGEMASK)
237 status.status = git_wc_status_conflicted;
238 else if (entry.m_FlagsExtended & GIT_IDXENTRY_INTENT_TO_ADD)
239 status.status = git_wc_status_added;
241 return 0;
244 int CGitIndexList::GetFileStatus(const CString& gitdir, const CString& path, git_wc_status2_t& status, CGitHash* pHash)
246 ATLASSERT(!status.assumeValid && !status.skipWorktree);
248 __int64 time, filesize = 0;
249 bool isDir = false;
250 bool isSymlink = false;
252 int result;
253 if (path.IsEmpty())
254 result = CGit::GetFileModifyTime(gitdir, &time, &isDir);
255 else
256 result = CGit::GetFileModifyTime(CombinePath(gitdir, path), &time, &isDir, &filesize, &isSymlink);
258 if (result)
259 filesize = -1;
261 if (!isDir || (isSymlink && (m_iIndexCaps & GIT_INDEXCAP_NO_SYMLINKS) != GIT_INDEXCAP_NO_SYMLINKS))
262 return GetFileStatus(gitdir, path, status, time, filesize, isSymlink, pHash);
264 if (CStringUtils::EndsWith(path, L'/'))
266 size_t index = SearchInSortVector(*this, path, -1, IsIgnoreCase());
267 if (index == NPOS)
269 status.status = git_wc_status_unversioned;
270 if (pHash)
271 pHash->Empty();
273 return 0;
276 if (pHash)
277 *pHash = (*this)[index].m_IndexHash;
279 if (!result)
280 status.status = git_wc_status_normal;
281 else
282 status.status = git_wc_status_deleted;
283 return 0;
286 // we get here for symlinks which are handled as files inside the git index
287 if ((m_iIndexCaps & GIT_INDEXCAP_NO_SYMLINKS) != GIT_INDEXCAP_NO_SYMLINKS)
288 return GetFileStatus(gitdir, path, status, time, filesize, isSymlink, pHash);
290 // we should never get here
291 status.status = git_wc_status_unversioned;
293 return -1;
296 bool CGitIndexFileMap::HasIndexChangedOnDisk(const CString& gitdir)
298 __int64 time = -1, size = -1;
300 auto pIndex = SafeGet(gitdir);
302 if (!pIndex)
303 return true;
305 CString IndexFile = g_AdminDirMap.GetWorktreeAdminDirConcat(gitdir, L"index");
306 // no need to refresh if there is no index right now and the current index is empty, but otherwise or lastmodified time differs
307 return (CGit::GetFileModifyTime(IndexFile, &time, nullptr, &size) && !pIndex->empty()) || pIndex->m_LastModifyTime != time || pIndex->m_LastFileSize != size;
310 int CGitIndexFileMap::LoadIndex(const CString &gitdir)
312 SHARED_INDEX_PTR pIndex = std::make_shared<CGitIndexList>();
314 if (pIndex->ReadIndex(gitdir))
316 SafeClear(gitdir);
317 return -1;
320 this->SafeSet(gitdir, pIndex);
322 return 0;
325 // This method is assumed to be called with m_SharedMutex locked.
326 int CGitHeadFileList::GetPackRef(const CString &gitdir)
328 CString PackRef = g_AdminDirMap.GetAdminDirConcat(gitdir, L"packed-refs");
330 __int64 mtime = 0, packsize = -1;
331 if (CGit::GetFileModifyTime(PackRef, &mtime, nullptr, &packsize))
333 //packed refs is not existed
334 this->m_PackRefFile.Empty();
335 this->m_PackRefMap.clear();
336 return 0;
338 else if (mtime == m_LastModifyTimePackRef && packsize == m_LastFileSizePackRef)
339 return 0;
340 else
342 this->m_PackRefFile = PackRef;
343 this->m_LastModifyTimePackRef = mtime;
344 this->m_LastFileSizePackRef = packsize;
347 m_PackRefMap.clear();
349 CAutoFile hfile = CreateFile(PackRef,
350 GENERIC_READ,
351 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
352 nullptr,
353 OPEN_EXISTING,
354 FILE_ATTRIBUTE_NORMAL,
355 nullptr);
357 if (!hfile)
358 return -1;
360 DWORD filesize = GetFileSize(hfile, nullptr);
361 if (filesize == 0 || filesize == INVALID_FILE_SIZE)
362 return -1;
364 DWORD size = 0;
365 auto buff = std::make_unique<char[]>(filesize);
366 ReadFile(hfile, buff.get(), filesize, &size, nullptr);
368 if (size != filesize)
369 return -1;
371 for (DWORD i = 0; i < filesize;)
373 CString hash;
374 CString ref;
375 if (buff[i] == '#' || buff[i] == '^')
377 while (buff[i] != '\n')
379 ++i;
380 if (i == filesize)
381 break;
383 ++i;
386 if (i >= filesize)
387 break;
389 while (buff[i] != ' ')
391 hash.AppendChar(buff[i]);
392 ++i;
393 if (i == filesize)
394 break;
397 ++i;
398 if (i >= filesize)
399 break;
401 while (buff[i] != '\n')
403 ref.AppendChar(buff[i]);
404 ++i;
405 if (i == filesize)
406 break;
409 if (!ref.IsEmpty())
410 m_PackRefMap[ref] = hash;
412 while (buff[i] == '\n')
414 ++i;
415 if (i == filesize)
416 break;
419 return 0;
421 int CGitHeadFileList::ReadHeadHash(const CString& gitdir)
423 ATLASSERT(m_Gitdir.IsEmpty() && m_HeadFile.IsEmpty() && m_Head.IsEmpty());
425 m_Gitdir = g_AdminDirMap.GetWorktreeAdminDir(gitdir);
427 m_HeadFile = m_Gitdir;
428 m_HeadFile += L"HEAD";
430 if (CGit::GetFileModifyTime(m_HeadFile, &m_LastModifyTimeHead, nullptr, &m_LastFileSizeHead))
431 return -1;
433 CAutoFile hfile = CreateFile(m_HeadFile,
434 GENERIC_READ,
435 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
436 nullptr,
437 OPEN_EXISTING,
438 FILE_ATTRIBUTE_NORMAL,
439 nullptr);
441 if (!hfile)
442 return -1;
444 DWORD size = 0;
445 unsigned char buffer[2 * GIT_HASH_SIZE];
446 ReadFile(hfile, buffer, (DWORD)strlen("ref:"), &size, nullptr);
447 if (size != strlen("ref:"))
448 return -1;
449 buffer[4] = '\0';
450 if (strcmp((const char*)buffer, "ref:") == 0)
452 m_HeadRefFile.Empty();
453 DWORD filesize = GetFileSize(hfile, nullptr);
454 if (filesize < 5 || filesize == INVALID_FILE_SIZE)
455 return -1;
457 unsigned char *p = (unsigned char*)malloc(filesize - strlen("ref:"));
458 if (!p)
459 return -1;
461 ReadFile(hfile, p, filesize - (DWORD)strlen("ref:"), &size, nullptr);
462 CGit::StringAppend(&m_HeadRefFile, p, CP_UTF8, filesize - (int)strlen("ref:"));
463 free(p);
465 CString ref = m_HeadRefFile.Trim();
466 int start = 0;
467 ref = ref.Tokenize(L"\n", start);
468 m_HeadRefFile = g_AdminDirMap.GetAdminDir(gitdir) + m_HeadRefFile;
469 m_HeadRefFile.Replace(L'/', L'\\');
471 __int64 time;
472 if (CGit::GetFileModifyTime(m_HeadRefFile, &time, nullptr))
474 if (GetPackRef(gitdir))
475 return -1;
476 if (m_PackRefMap.find(ref) != m_PackRefMap.end())
478 m_bRefFromPackRefFile = true;
479 m_Head = m_PackRefMap[ref];
480 return 0;
483 // unborn branch
484 m_Head.Empty();
486 return 0;
489 CAutoFile href = CreateFile(m_HeadRefFile,
490 GENERIC_READ,
491 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
492 nullptr,
493 OPEN_EXISTING,
494 FILE_ATTRIBUTE_NORMAL,
495 nullptr);
497 if (!href)
499 if (GetPackRef(gitdir))
500 return -1;
502 if (m_PackRefMap.find(ref) == m_PackRefMap.end())
503 return -1;
505 m_bRefFromPackRefFile = true;
506 m_Head = m_PackRefMap[ref];
507 return 0;
510 ReadFile(href, buffer, 2 * GIT_HASH_SIZE, &size, nullptr);
511 if (size != 2 * GIT_HASH_SIZE)
512 return -1;
514 m_Head.ConvertFromStrA((char*)buffer);
516 m_LastModifyTimeRef = time;
518 return 0;
521 ReadFile(hfile, buffer + (DWORD)strlen("ref:"), 2 * GIT_HASH_SIZE - (DWORD)strlen("ref:"), &size, nullptr);
522 if (size != 2 * GIT_HASH_SIZE - (DWORD)strlen("ref:"))
523 return -1;
525 m_HeadRefFile.Empty();
527 m_Head.ConvertFromStrA((char*)buffer);
529 return 0;
532 bool CGitHeadFileList::CheckHeadUpdate()
534 if (this->m_HeadFile.IsEmpty())
535 return true;
537 __int64 mtime = 0, size = -1;
539 if (CGit::GetFileModifyTime(m_HeadFile, &mtime, nullptr, &size))
540 return true;
542 if (mtime != m_LastModifyTimeHead || size != m_LastFileSizeHead)
543 return true;
545 if (!this->m_HeadRefFile.IsEmpty())
547 // 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
548 if (CGit::GetFileModifyTime(m_HeadRefFile, &mtime))
550 if (!m_bRefFromPackRefFile)
551 return true;
553 } else if (mtime != this->m_LastModifyTimeRef)
554 return true;
557 if (m_bRefFromPackRefFile && !m_PackRefFile.IsEmpty())
559 size = -1;
560 if (CGit::GetFileModifyTime(m_PackRefFile, &mtime, nullptr, &size))
561 return true;
563 if (mtime != m_LastModifyTimePackRef || size != m_LastFileSizePackRef)
564 return true;
567 // in an empty repo HEAD points to refs/heads/master, but this ref doesn't exist.
568 // So we need to retry again and again until the ref exists - otherwise we will never notice
569 if (this->m_Head.IsEmpty() && this->m_HeadRefFile.IsEmpty() && this->m_PackRefFile.IsEmpty())
570 return true;
572 return false;
575 int CGitHeadFileList::ReadTreeRecursive(git_repository& repo, const git_tree* tree, const CStringA& base)
577 #define S_IFGITLINK 0160000
578 size_t count = git_tree_entrycount(tree);
579 for (size_t i = 0; i < count; ++i)
581 const git_tree_entry *entry = git_tree_entry_byindex(tree, i);
582 if (!entry)
583 continue;
584 int mode = git_tree_entry_filemode(entry);
585 bool isDir = (mode & S_IFDIR) == S_IFDIR;
586 bool isSubmodule = (mode & S_IFMT) == S_IFGITLINK;
587 if (!isDir || isSubmodule)
589 CGitTreeItem item;
590 item.m_Hash = git_tree_entry_id(entry);
591 CGit::StringAppend(&item.m_FileName, (BYTE*)(LPCSTR)base, CP_UTF8, base.GetLength());
592 CGit::StringAppend(&item.m_FileName, (BYTE*)git_tree_entry_name(entry), CP_UTF8);
593 if (isSubmodule)
594 item.m_FileName += L'/';
595 push_back(item);
596 continue;
599 CAutoObject object;
600 git_tree_entry_to_object(object.GetPointer(), &repo, entry);
601 if (!object)
602 continue;
603 CStringA parent = base;
604 parent += git_tree_entry_name(entry);
605 parent += "/";
606 ReadTreeRecursive(repo, (git_tree*)(git_object*)object, parent);
609 return 0;
612 // ReadTree is/must only be executed on an empty list
613 int CGitHeadFileList::ReadTree(bool ignoreCase)
615 ATLASSERT(empty());
617 // unborn branch
618 if (m_Head.IsEmpty())
619 return 0;
621 CAutoRepository repository(m_Gitdir);
622 CAutoCommit commit;
623 CAutoTree tree;
624 bool ret = repository;
625 ret = ret && !git_commit_lookup(commit.GetPointer(), repository, m_Head);
626 ret = ret && !git_commit_tree(tree.GetPointer(), commit);
629 ret = ret && !ReadTreeRecursive(*repository, tree, "");
631 catch (const std::bad_alloc& ex)
633 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Catched exception inside ReadTreeRecursive: %s\n", ex.what());
634 return -1;
636 if (!ret)
638 clear();
639 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not open git repository in %s and read HEAD commit %s: %s\n", (LPCTSTR)m_Gitdir, (LPCTSTR)m_Head.ToString(), (LPCTSTR)CGit::GetLibGit2LastErr());
640 m_LastModifyTimeHead = 0;
641 m_LastFileSizeHead = -1;
642 return -1;
645 DoSortFilenametSortVector(*this, ignoreCase);
647 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Reloaded HEAD tree (commit is %s) for repo: %s\n", (LPCTSTR)m_Head.ToString(), (LPCTSTR)m_Gitdir);
649 return 0;
651 int CGitIgnoreItem::FetchIgnoreList(const CString& projectroot, const CString& file, bool isGlobal, int* ignoreCase)
653 if (this->m_pExcludeList)
655 git_free_exclude_list(m_pExcludeList);
656 m_pExcludeList = nullptr;
658 free(m_buffer);
659 m_buffer = nullptr;
661 this->m_BaseDir.Empty();
662 if (!isGlobal)
664 CString base = file.Mid(projectroot.GetLength() + 1);
665 base.Replace(L'\\', L'/');
667 int start = base.ReverseFind(L'/');
668 if(start > 0)
670 base.Truncate(start);
671 this->m_BaseDir = CUnicodeUtils::GetMulti(base, CP_UTF8) + "/";
675 if (CGit::GetFileModifyTime(file, &m_LastModifyTime, nullptr, &m_LastFileSize))
676 return -1;
678 CAutoFile hfile = CreateFile(file,
679 GENERIC_READ,
680 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
681 nullptr,
682 OPEN_EXISTING,
683 FILE_ATTRIBUTE_NORMAL,
684 nullptr);
686 if (!hfile)
687 return -1 ;
689 DWORD filesize = GetFileSize(hfile, nullptr);
690 if (filesize == INVALID_FILE_SIZE)
691 return -1;
693 m_buffer = new BYTE[filesize + 1];
694 if (!m_buffer)
695 return -1;
697 DWORD size = 0;
698 if (!ReadFile(hfile, m_buffer, filesize, &size, nullptr))
700 free(m_buffer);
701 m_buffer = nullptr;
702 return -1;
704 m_buffer[size] = '\0';
706 if (git_create_exclude_list(&m_pExcludeList))
708 free(m_buffer);
709 m_buffer = nullptr;
710 return -1;
713 m_iIgnoreCase = ignoreCase;
715 BYTE *p = m_buffer;
716 int line = 0;
717 for (DWORD i = 0; i < size; ++i)
719 if (m_buffer[i] == '\n' || m_buffer[i] == '\r' || i == (size - 1))
721 if (m_buffer[i] == '\n' || m_buffer[i] == '\r')
722 m_buffer[i] = '\0';
724 if (p[0] != '#' && p[0])
725 git_add_exclude((const char*)p, this->m_BaseDir, m_BaseDir.GetLength(), this->m_pExcludeList, ++line);
727 p = m_buffer + i + 1;
731 if (!line)
733 git_free_exclude_list(m_pExcludeList);
734 m_pExcludeList = nullptr;
735 free(m_buffer);
736 m_buffer = nullptr;
739 return 0;
742 #ifdef GTEST_INCLUDE_GTEST_GTEST_H_
743 int CGitIgnoreItem::IsPathIgnored(const CStringA& patha, int& type)
745 int pos = patha.ReverseFind('/');
746 const char* base = (pos >= 0) ? ((const char*)patha + pos + 1) : patha;
748 return IsPathIgnored(patha, base, type);
750 #endif
752 int CGitIgnoreItem::IsPathIgnored(const CStringA& patha, const char* base, int& type)
754 if (!m_pExcludeList)
755 return -1; // error or undecided
757 return git_check_excluded_1(patha, patha.GetLength(), base, &type, m_pExcludeList, m_iIgnoreCase ? *m_iIgnoreCase : 1);
760 bool CGitIgnoreList::CheckFileChanged(const CString &path)
762 __int64 time = 0, size = -1;
764 int ret = CGit::GetFileModifyTime(path, &time, nullptr, &size);
766 bool cacheExist;
768 CAutoReadLock lock(m_SharedMutex);
769 cacheExist = (m_Map.find(path) != m_Map.end());
772 if (!cacheExist && ret == 0)
774 CAutoWriteLock lock(m_SharedMutex);
775 m_Map[path].m_LastModifyTime = 0;
776 m_Map[path].m_LastFileSize = -1;
778 // both cache and file is not exist
779 if ((ret != 0) && (!cacheExist))
780 return false;
782 // file exist but cache miss
783 if ((ret == 0) && (!cacheExist))
784 return true;
786 // file not exist but cache exist
787 if ((ret != 0) && (cacheExist))
788 return true;
789 // file exist and cache exist
792 CAutoReadLock lock(m_SharedMutex);
793 if (m_Map[path].m_LastModifyTime == time && m_Map[path].m_LastFileSize == size)
794 return false;
796 return true;
799 int CGitIgnoreList::FetchIgnoreFile(const CString &gitdir, const CString &gitignore, bool isGlobal)
801 if (CGit::GitPathFileExists(gitignore)) //if .gitignore remove, we need remote cache
803 CAutoWriteLock lock(m_SharedMutex);
804 m_Map[gitignore].FetchIgnoreList(gitdir, gitignore, isGlobal, &m_IgnoreCase[g_AdminDirMap.GetAdminDir(gitdir)]);
806 else
808 CAutoWriteLock lock(m_SharedMutex);
809 m_Map.erase(gitignore);
811 return 0;
814 bool CGitIgnoreList::CheckAndUpdateIgnoreFiles(const CString& gitdir, const CString& path, bool isDir, std::set<CString>* lastChecked)
816 CString temp(gitdir);
817 temp += L'\\';
818 temp += path;
820 temp.Replace(L'/', L'\\');
822 if (!isDir)
824 int x = temp.ReverseFind(L'\\');
825 if (x >= 2)
826 temp.Truncate(x);
829 bool updated = false;
830 while (!temp.IsEmpty())
832 if (lastChecked)
834 if (lastChecked->find(temp) != lastChecked->end())
835 return updated;
836 lastChecked->insert(temp);
839 temp += L"\\.gitignore";
841 if (CheckFileChanged(temp))
843 FetchIgnoreFile(gitdir, temp, false);
844 updated = true;
847 temp.Truncate(temp.GetLength() - (int)wcslen(L"\\.gitignore"));
848 if (CPathUtils::ArePathStringsEqual(temp, gitdir))
850 CString adminDir = g_AdminDirMap.GetAdminDir(temp);
851 CString wcglobalgitignore = adminDir + L"info\\exclude";
852 if (CheckFileChanged(wcglobalgitignore))
854 FetchIgnoreFile(gitdir, wcglobalgitignore, true);
855 updated = true;
858 if (CheckAndUpdateCoreExcludefile(adminDir))
860 CString excludesFile;
862 CAutoReadLock lock(m_SharedMutex);
863 excludesFile = m_CoreExcludesfiles[adminDir];
865 if (!excludesFile.IsEmpty())
867 FetchIgnoreFile(gitdir, excludesFile, true);
868 updated = true;
872 return updated;
875 int i = temp.ReverseFind(L'\\');
876 temp.Truncate(max(0, i));
878 return updated;
881 bool CGitIgnoreList::CheckAndUpdateGitSystemConfigPath(bool force)
883 if (force)
884 m_sGitProgramDataConfigPath = GetProgramDataGitConfig();
885 // recheck every 30 seconds
886 if (GetTickCount64() - m_dGitSystemConfigPathLastChecked > 30000UL || force)
888 m_dGitSystemConfigPathLastChecked = GetTickCount64();
889 CString gitSystemConfigPath(CRegString(REG_SYSTEM_GITCONFIGPATH, L"", FALSE));
890 if (gitSystemConfigPath != m_sGitSystemConfigPath)
892 m_sGitSystemConfigPath = gitSystemConfigPath;
893 return true;
896 return false;
898 bool CGitIgnoreList::CheckAndUpdateCoreExcludefile(const CString &adminDir)
900 CString projectConfig(adminDir);
901 projectConfig += L"config";
902 CString globalConfig = g_Git.GetGitGlobalConfig();
903 CString globalXDGConfig = g_Git.GetGitGlobalXDGConfig();
905 CAutoWriteLock lock(m_coreExcludefilesSharedMutex);
906 bool hasChanged = CheckAndUpdateGitSystemConfigPath();
907 hasChanged = hasChanged || CheckFileChanged(projectConfig);
908 hasChanged = hasChanged || CheckFileChanged(globalConfig);
909 hasChanged = hasChanged || CheckFileChanged(globalXDGConfig);
910 if (!m_sGitProgramDataConfigPath.IsEmpty())
911 hasChanged = hasChanged || CheckFileChanged(m_sGitProgramDataConfigPath);
912 if (!m_sGitSystemConfigPath.IsEmpty())
913 hasChanged = hasChanged || CheckFileChanged(m_sGitSystemConfigPath);
915 CString excludesFile;
917 CAutoReadLock lock2(m_SharedMutex);
918 excludesFile = m_CoreExcludesfiles[adminDir];
920 if (!excludesFile.IsEmpty())
921 hasChanged = hasChanged || CheckFileChanged(excludesFile);
923 if (!hasChanged)
924 return false;
926 CAutoConfig config(true);
927 CAutoRepository repo(adminDir);
928 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(projectConfig), GIT_CONFIG_LEVEL_LOCAL, repo, FALSE);
929 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalConfig), GIT_CONFIG_LEVEL_GLOBAL, repo, FALSE);
930 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalXDGConfig), GIT_CONFIG_LEVEL_XDG, repo, FALSE);
931 if (!m_sGitSystemConfigPath.IsEmpty())
932 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(m_sGitSystemConfigPath), GIT_CONFIG_LEVEL_SYSTEM, repo, FALSE);
933 if (!m_sGitProgramDataConfigPath.IsEmpty())
934 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(m_sGitProgramDataConfigPath), GIT_CONFIG_LEVEL_PROGRAMDATA, repo, FALSE);
936 config.GetString(L"core.excludesfile", excludesFile);
937 if (excludesFile.IsEmpty())
938 excludesFile = GetWindowsHome() + L"\\.config\\git\\ignore";
939 else if (CStringUtils::StartsWith(excludesFile, L"~/"))
940 excludesFile = GetWindowsHome() + excludesFile.Mid((int)wcslen(L"~"));
942 CAutoWriteLock lockMap(m_SharedMutex);
943 m_IgnoreCase[adminDir] = 1;
944 config.GetBOOL(L"core.ignorecase", m_IgnoreCase[adminDir]);
945 CGit::GetFileModifyTime(projectConfig, &m_Map[projectConfig].m_LastModifyTime, nullptr, &m_Map[projectConfig].m_LastFileSize);
946 CGit::GetFileModifyTime(globalXDGConfig, &m_Map[globalXDGConfig].m_LastModifyTime, nullptr, &m_Map[globalXDGConfig].m_LastFileSize);
947 if (m_Map[globalXDGConfig].m_LastModifyTime == 0)
948 m_Map.erase(globalXDGConfig);
949 CGit::GetFileModifyTime(globalConfig, &m_Map[globalConfig].m_LastModifyTime, nullptr, &m_Map[globalConfig].m_LastFileSize);
950 if (m_Map[globalConfig].m_LastModifyTime == 0)
951 m_Map.erase(globalConfig);
952 if (!m_sGitSystemConfigPath.IsEmpty())
953 CGit::GetFileModifyTime(m_sGitSystemConfigPath, &m_Map[m_sGitSystemConfigPath].m_LastModifyTime, nullptr, &m_Map[m_sGitSystemConfigPath].m_LastFileSize);
954 if (m_Map[m_sGitSystemConfigPath].m_LastModifyTime == 0 || m_sGitSystemConfigPath.IsEmpty())
955 m_Map.erase(m_sGitSystemConfigPath);
956 if (!m_sGitProgramDataConfigPath.IsEmpty())
957 CGit::GetFileModifyTime(m_sGitProgramDataConfigPath, &m_Map[m_sGitProgramDataConfigPath].m_LastModifyTime, nullptr, &m_Map[m_sGitProgramDataConfigPath].m_LastFileSize);
958 if (m_Map[m_sGitProgramDataConfigPath].m_LastModifyTime == 0 || m_sGitProgramDataConfigPath.IsEmpty())
959 m_Map.erase(m_sGitProgramDataConfigPath);
960 m_CoreExcludesfiles[adminDir] = excludesFile;
962 return true;
964 const CString CGitIgnoreList::GetWindowsHome()
966 static CString sWindowsHome(g_Git.GetHomeDirectory());
967 return sWindowsHome;
969 bool CGitIgnoreList::IsIgnore(CString str, const CString& projectroot, bool isDir, const CString& adminDir)
971 str.Replace(L'\\', L'/');
973 if (!str.IsEmpty() && str[str.GetLength() - 1] == L'/')
974 str.Truncate(str.GetLength() - 1);
976 int ret;
977 ret = CheckIgnore(str, projectroot, isDir, adminDir);
978 while (ret < 0)
980 int start = str.ReverseFind(L'/');
981 if(start < 0)
982 return (ret == 1);
984 str.Truncate(start);
985 ret = CheckIgnore(str, projectroot, TRUE, adminDir);
988 return (ret == 1);
990 int CGitIgnoreList::CheckFileAgainstIgnoreList(const CString &ignorefile, const CStringA &patha, const char * base, int &type)
992 if (m_Map.find(ignorefile) == m_Map.end())
993 return -1; // error or undecided
995 return (m_Map[ignorefile].IsPathIgnored(patha, base, type));
997 int CGitIgnoreList::CheckIgnore(const CString &path, const CString &projectroot, bool isDir, const CString& adminDir)
999 CString temp = CombinePath(projectroot, path);
1000 temp.Replace(L'/', L'\\');
1002 CStringA patha = CUnicodeUtils::GetMulti(path, CP_UTF8);
1003 patha.Replace('\\', '/');
1005 int type = 0;
1006 if (isDir)
1008 type = DT_DIR;
1010 // strip directory name
1011 // we do not need to check for a .ignore file inside a directory we might ignore
1012 int i = temp.ReverseFind(L'\\');
1013 if (i >= 0)
1014 temp.Truncate(i);
1016 else
1018 type = DT_REG;
1020 int x = temp.ReverseFind(L'\\');
1021 if (x >= 2)
1022 temp.Truncate(x);
1025 int pos = patha.ReverseFind('/');
1026 const char * base = (pos >= 0) ? ((const char*)patha + pos + 1) : patha;
1029 CAutoReadLock lock(m_SharedMutex);
1030 while (!temp.IsEmpty())
1032 temp += L"\\.gitignore";
1034 int ret;
1035 if ((ret = CheckFileAgainstIgnoreList(temp, patha, base, type)) != -1)
1036 return ret;
1038 temp.Truncate(temp.GetLength() - (int)wcslen(L"\\.gitignore"));
1040 if (CPathUtils::ArePathStringsEqual(temp, projectroot))
1042 CString wcglobalgitignore = adminDir;
1043 wcglobalgitignore += L"info\\exclude";
1044 if ((ret = CheckFileAgainstIgnoreList(wcglobalgitignore, patha, base, type)) != -1)
1045 return ret;
1047 CString excludesFile = m_CoreExcludesfiles[adminDir];
1048 if (!excludesFile.IsEmpty())
1049 return CheckFileAgainstIgnoreList(excludesFile, patha, base, type);
1051 return -1;
1054 int i = temp.ReverseFind(L'\\');
1055 temp.Truncate(max(0, i));
1058 return -1;
1061 void CGitHeadFileMap::CheckHeadAndUpdate(const CString& gitdir, bool ignoreCase)
1063 SHARED_TREE_PTR ptr = this->SafeGet(gitdir);
1065 if (ptr.get() && !ptr->CheckHeadUpdate())
1066 return;
1068 ptr = std::make_shared<CGitHeadFileList>();
1069 if (ptr->ReadHeadHash(gitdir) || ptr->ReadTree(ignoreCase))
1071 SafeClear(gitdir);
1072 return;
1075 this->SafeSet(gitdir, ptr);
1077 return;