Fixed issue #2979: gitignore patterns are always treated case sensitively for overlay...
[TortoiseGit.git] / src / Git / GitIndex.cpp
blob1f91ae69256e4eeefd648942616b2aadbf2cfac9
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2017 - TortoiseGit
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License
7 // as published by the Free Software Foundation; either version 2
8 // of the License, or (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software Foundation,
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
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)
59 m_iMaxCheckSize = (__int64)CRegDWORD(L"Software\\TortoiseGit\\TGitCacheCheckContentMaxSize", 10 * 1024) * 1024; // stored in KiB
62 CGitIndexList::~CGitIndexList()
66 static bool SortIndex(const CGitIndex &Item1, const CGitIndex &Item2)
68 return Item1.m_FileName.Compare(Item2.m_FileName) < 0;
71 static bool SortTree(const CGitTreeItem &Item1, const CGitTreeItem &Item2)
73 return Item1.m_FileName.Compare(Item2.m_FileName) < 0;
76 int CGitIndexList::ReadIndex(CString dgitdir)
78 #ifdef GTEST_INCLUDE_GTEST_GTEST_H_
79 clear(); // HACK to make tests work, until we use CGitIndexList
80 #endif
81 ATLASSERT(empty());
83 CAutoRepository repository(dgitdir);
84 if (!repository)
85 return -1;
87 // add config files
88 config.New();
90 CString projectConfig = g_AdminDirMap.GetAdminDir(dgitdir) + L"config";
91 CString globalConfig = g_Git.GetGitGlobalConfig();
92 CString globalXDGConfig = g_Git.GetGitGlobalXDGConfig();
93 CString systemConfig(CRegString(REG_SYSTEM_GITCONFIGPATH, L"", FALSE));
94 CString programDataConfig(GetProgramDataGitConfig());
96 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(projectConfig), GIT_CONFIG_LEVEL_LOCAL, FALSE);
97 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalConfig), GIT_CONFIG_LEVEL_GLOBAL, FALSE);
98 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalXDGConfig), GIT_CONFIG_LEVEL_XDG, FALSE);
99 if (!systemConfig.IsEmpty())
100 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(systemConfig), GIT_CONFIG_LEVEL_SYSTEM, FALSE);
101 if (!programDataConfig.IsEmpty())
102 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(programDataConfig), GIT_CONFIG_LEVEL_PROGRAMDATA, FALSE);
104 git_repository_set_config(repository, config);
106 CAutoIndex index;
107 // load index in order to enumerate files
108 if (git_repository_index(index.GetPointer(), repository))
110 config.Free();
111 return -1;
114 m_bHasConflicts = FALSE;
116 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 for (size_t i = 0; i < ecount; ++i)
129 const git_index_entry *e = git_index_get_byindex(index, i);
131 auto& item = (*this)[i];
132 item.m_FileName = CUnicodeUtils::GetUnicode(e->path);
133 if (e->mode & S_IFDIR)
134 item.m_FileName += L'/';
135 item.m_ModifyTime = e->mtime.seconds;
136 item.m_Flags = e->flags;
137 item.m_FlagsExtended = e->flags_extended;
138 item.m_IndexHash = e->id.id;
139 item.m_Size = e->file_size;
140 m_bHasConflicts |= GIT_IDXENTRY_STAGE(e);
143 CGit::GetFileModifyTime(g_AdminDirMap.GetWorktreeAdminDir(dgitdir) + L"index", &m_LastModifyTime);
144 std::sort(this->begin(), this->end(), SortIndex);
146 return 0;
149 int CGitIndexList::GetFileStatus(const CString &gitdir, const CString &pathorg, git_wc_status_kind *status, __int64 time, __int64 filesize, FILL_STATUS_CALLBACK callback, void *pData, CGitHash *pHash, bool * assumeValid, bool * skipWorktree)
151 if (!status)
152 return 0;
154 CString path = pathorg;
156 size_t index = SearchInSortVector(*this, path, -1);
158 if (index == NPOS)
160 *status = git_wc_status_unversioned;
161 if (pHash)
162 pHash->Empty();
164 if (callback && assumeValid && skipWorktree)
165 callback(CombinePath(gitdir, pathorg), *status, false, pData, *assumeValid, *skipWorktree);
167 return 0;
170 auto& entry = (*this)[index];
171 // skip-worktree has higher priority than assume-valid
172 if (entry.m_FlagsExtended & GIT_IDXENTRY_SKIP_WORKTREE)
174 *status = git_wc_status_normal;
175 if (skipWorktree)
176 *skipWorktree = true;
178 else if (entry.m_Flags & GIT_IDXENTRY_VALID)
180 *status = git_wc_status_normal;
181 if (assumeValid)
182 *assumeValid = true;
184 else if (filesize != entry.m_Size)
185 *status = git_wc_status_modified;
186 else if (time == entry.m_ModifyTime)
187 *status = git_wc_status_normal;
188 else if (config && filesize < m_iMaxCheckSize)
191 * Opening a new repository each time is not yet optimal, however, there is no API to clear the pack-cache
192 * When a shared repository is used, we might need a mutex to prevent concurrent access to repository instance and especially filter-lists
194 CAutoRepository repository(gitdir);
195 if (!repository)
196 return -1;
198 git_oid actual;
199 CStringA fileA = CUnicodeUtils::GetMulti(pathorg, CP_UTF8);
200 git_repository_set_config(repository, config);
201 if (!git_repository_hashfile(&actual, repository, fileA, GIT_OBJ_BLOB, nullptr) && !git_oid_cmp(&actual, (const git_oid*)entry.m_IndexHash.m_hash))
203 entry.m_ModifyTime = time;
204 *status = git_wc_status_normal;
206 else
207 *status = git_wc_status_modified;
209 else
210 *status = git_wc_status_modified;
212 if (entry.m_Flags & GIT_IDXENTRY_STAGEMASK)
213 *status = git_wc_status_conflicted;
214 else if (entry.m_FlagsExtended & GIT_IDXENTRY_INTENT_TO_ADD)
215 *status = git_wc_status_added;
217 if (pHash)
218 *pHash = entry.m_IndexHash;
220 if (callback && assumeValid && skipWorktree)
221 callback(CombinePath(gitdir, pathorg), *status, false, pData, *assumeValid, *skipWorktree);
223 return 0;
226 int CGitIndexList::GetStatus(const CString& gitdir, CString path, git_wc_status_kind* status,
227 BOOL IsFull, BOOL /*IsRecursive*/,
228 FILL_STATUS_CALLBACK callback, void *pData,
229 CGitHash *pHash, bool * assumeValid, bool * skipWorktree)
231 __int64 time, filesize = 0;
232 bool isDir = false;
234 if (!status)
235 return 0;
237 git_wc_status_kind dirstatus = git_wc_status_none;
238 int result;
239 if (path.IsEmpty())
240 result = CGit::GetFileModifyTime(gitdir, &time, &isDir);
241 else
242 result = CGit::GetFileModifyTime(CombinePath(gitdir, path), &time, &isDir, &filesize);
244 if (result)
246 *status = git_wc_status_deleted;
247 if (callback && assumeValid && skipWorktree)
248 callback(CombinePath(gitdir, path), git_wc_status_deleted, false, pData, *assumeValid, *skipWorktree);
250 return 0;
253 if (!isDir)
255 GetFileStatus(gitdir, path, status, time, filesize, callback, pData, pHash, assumeValid, skipWorktree);
256 return 0;
259 if (!path.IsEmpty() && !CStringUtils::EndsWith(path, L'\\'))
260 path += L'\\';
262 int len = path.GetLength();
264 for (auto it = cbegin(), itend = cend(); it != itend; ++it)
266 auto& entry = *it;
267 if (!(entry.m_FileName.GetLength() > len && wcsncmp(entry.m_FileName, path, len) == 0))
268 continue;
270 if (!IsFull)
272 *status = git_wc_status_normal;
273 if (callback)
274 callback(CombinePath(gitdir, path), *status, false, pData, (entry.m_Flags & GIT_IDXENTRY_VALID) && !(entry.m_FlagsExtended & GIT_IDXENTRY_SKIP_WORKTREE), (entry.m_FlagsExtended & GIT_IDXENTRY_SKIP_WORKTREE) != 0);
276 return 0;
279 result = CGit::GetFileModifyTime(CombinePath(gitdir, entry.m_FileName), &time, nullptr, &filesize);
280 if (result)
281 continue;
283 *status = git_wc_status_none;
284 if (assumeValid)
285 *assumeValid = false;
286 if (skipWorktree)
287 *skipWorktree = false;
289 GetFileStatus(gitdir, entry.m_FileName, status, time, filesize, callback, pData, nullptr, assumeValid, skipWorktree);
291 // if a file is assumed valid, we need to inform the caller, otherwise the assumevalid flag might not get to the explorer on first open of a repository
292 if (callback && assumeValid && skipWorktree && (*assumeValid || *skipWorktree))
293 callback(CombinePath(gitdir, path), *status, false, pData, *assumeValid, *skipWorktree);
294 if (*status != git_wc_status_none)
296 if (dirstatus == git_wc_status_none)
297 dirstatus = git_wc_status_normal;
298 if (*status != git_wc_status_normal)
299 dirstatus = git_wc_status_modified;
301 } /* End For */
303 if (dirstatus != git_wc_status_none)
304 *status = dirstatus;
305 else
306 *status = git_wc_status_unversioned;
308 if (callback)
309 callback(CombinePath(gitdir, path), *status, false, pData, false, false);
311 return 0;
314 int CGitIndexFileMap::Check(const CString &gitdir, bool *isChanged)
316 __int64 time;
318 CString IndexFile = g_AdminDirMap.GetWorktreeAdminDirConcat(gitdir, L"index");
320 if (CGit::GetFileModifyTime(IndexFile, &time))
322 g_AdminDirMap.ResetAdminDirCache(gitdir);
323 return -1;
326 SHARED_INDEX_PTR pIndex;
327 pIndex = this->SafeGet(gitdir);
329 if (!pIndex)
331 if(isChanged)
332 *isChanged = true;
333 return 0;
336 if (pIndex->m_LastModifyTime == time)
338 if (isChanged)
339 *isChanged = false;
341 else
343 if (isChanged)
344 *isChanged = true;
346 return 0;
349 int CGitIndexFileMap::LoadIndex(const CString &gitdir)
351 SHARED_INDEX_PTR pIndex = std::make_shared<CGitIndexList>();
353 if (pIndex->ReadIndex(gitdir))
354 return -1;
356 this->SafeSet(gitdir, pIndex);
358 return 0;
361 int CGitIndexFileMap::GetFileStatus(const CString &gitdir, const CString &path, git_wc_status_kind *status,BOOL IsFull, BOOL IsRecursive,
362 FILL_STATUS_CALLBACK callback, void *pData,
363 CGitHash *pHash,
364 bool* assumeValid, bool* skipWorktree)
366 CheckAndUpdate(gitdir);
368 SHARED_INDEX_PTR pIndex = this->SafeGet(gitdir);
369 if (pIndex)
370 pIndex->GetStatus(gitdir, path, status, IsFull, IsRecursive, callback, pData, pHash, assumeValid, skipWorktree);
371 else
373 // git working tree has not index
374 *status = git_wc_status_unversioned;
377 return 0;
380 int CGitIndexFileMap::IsUnderVersionControl(const CString& gitdir, CString subpath, bool isDir, bool* isVersion)
382 if (subpath.IsEmpty())
384 *isVersion = true;
385 return 0;
388 subpath.Replace(L'\\', L'/');
389 if (isDir)
390 subpath += L'/';
392 CheckAndUpdate(gitdir);
394 SHARED_INDEX_PTR pIndex = this->SafeGet(gitdir);
396 if (pIndex)
398 if (isDir)
399 *isVersion = (SearchInSortVector(*pIndex, subpath, subpath.GetLength()) != NPOS);
400 else
401 *isVersion = (SearchInSortVector(*pIndex, subpath, -1) != NPOS);
404 return 0;
407 // This method is assumed to be called with m_SharedMutex locked.
408 int CGitHeadFileList::GetPackRef(const CString &gitdir)
410 CString PackRef = g_AdminDirMap.GetAdminDirConcat(gitdir, L"packed-refs");
412 __int64 mtime;
413 if (CGit::GetFileModifyTime(PackRef, &mtime))
415 //packed refs is not existed
416 this->m_PackRefFile.Empty();
417 this->m_PackRefMap.clear();
418 return 0;
420 else if(mtime == m_LastModifyTimePackRef)
421 return 0;
422 else
424 this->m_PackRefFile = PackRef;
425 this->m_LastModifyTimePackRef = mtime;
428 m_PackRefMap.clear();
430 CAutoFile hfile = CreateFile(PackRef,
431 GENERIC_READ,
432 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
433 nullptr,
434 OPEN_EXISTING,
435 FILE_ATTRIBUTE_NORMAL,
436 nullptr);
438 if (!hfile)
439 return -1;
441 DWORD filesize = GetFileSize(hfile, nullptr);
442 if (filesize == 0 || filesize == INVALID_FILE_SIZE)
443 return -1;
445 DWORD size = 0;
446 auto buff = std::make_unique<char[]>(filesize);
447 ReadFile(hfile, buff.get(), filesize, &size, nullptr);
449 if (size != filesize)
450 return -1;
452 for (DWORD i = 0; i < filesize;)
454 CString hash;
455 CString ref;
456 if (buff[i] == '#' || buff[i] == '^')
458 while (buff[i] != '\n')
460 ++i;
461 if (i == filesize)
462 break;
464 ++i;
467 if (i >= filesize)
468 break;
470 while (buff[i] != ' ')
472 hash.AppendChar(buff[i]);
473 ++i;
474 if (i == filesize)
475 break;
478 ++i;
479 if (i >= filesize)
480 break;
482 while (buff[i] != '\n')
484 ref.AppendChar(buff[i]);
485 ++i;
486 if (i == filesize)
487 break;
490 if (!ref.IsEmpty())
491 m_PackRefMap[ref] = hash;
493 while (buff[i] == '\n')
495 ++i;
496 if (i == filesize)
497 break;
500 return 0;
502 int CGitHeadFileList::ReadHeadHash(const CString& gitdir)
504 ATLASSERT(m_Gitdir.IsEmpty() && m_HeadFile.IsEmpty());
506 m_Gitdir = g_AdminDirMap.GetWorktreeAdminDir(gitdir);
508 m_HeadFile = m_Gitdir;
509 m_HeadFile += L"HEAD";
511 if( CGit::GetFileModifyTime(m_HeadFile, &m_LastModifyTimeHead))
512 return -1;
514 CAutoFile hfile = CreateFile(m_HeadFile,
515 GENERIC_READ,
516 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
517 nullptr,
518 OPEN_EXISTING,
519 FILE_ATTRIBUTE_NORMAL,
520 nullptr);
522 if (!hfile)
523 return -1;
525 DWORD size = 0;
526 unsigned char buffer[2 * GIT_HASH_SIZE];
527 ReadFile(hfile, buffer, (DWORD)strlen("ref:"), &size, nullptr);
528 if (size != strlen("ref:"))
529 return -1;
530 buffer[4] = '\0';
531 if (strcmp((const char*)buffer, "ref:") == 0)
533 m_HeadRefFile.Empty();
534 DWORD filesize = GetFileSize(hfile, nullptr);
535 if (filesize < 5 || filesize == INVALID_FILE_SIZE)
536 return -1;
538 unsigned char *p = (unsigned char*)malloc(filesize - strlen("ref:"));
539 if (!p)
540 return -1;
542 ReadFile(hfile, p, filesize - (DWORD)strlen("ref:"), &size, nullptr);
543 CGit::StringAppend(&m_HeadRefFile, p, CP_UTF8, filesize - (int)strlen("ref:"));
544 free(p);
546 CString ref = m_HeadRefFile.Trim();
547 int start = 0;
548 ref = ref.Tokenize(L"\n", start);
549 m_HeadRefFile = g_AdminDirMap.GetAdminDir(gitdir) + m_HeadRefFile;
550 m_HeadRefFile.Replace(L'/', L'\\');
552 __int64 time;
553 if (CGit::GetFileModifyTime(m_HeadRefFile, &time, nullptr))
555 m_HeadRefFile.Empty();
556 if (GetPackRef(gitdir))
557 return -1;
558 if (m_PackRefMap.find(ref) == m_PackRefMap.end())
559 return -1;
561 m_Head = m_PackRefMap[ref];
562 return 0;
565 CAutoFile href = CreateFile(m_HeadRefFile,
566 GENERIC_READ,
567 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
568 nullptr,
569 OPEN_EXISTING,
570 FILE_ATTRIBUTE_NORMAL,
571 nullptr);
573 if (!href)
575 m_HeadRefFile.Empty();
577 if (GetPackRef(gitdir))
578 return -1;
580 if (m_PackRefMap.find(ref) == m_PackRefMap.end())
581 return -1;
583 m_Head = m_PackRefMap[ref];
584 return 0;
587 ReadFile(href, buffer, 2 * GIT_HASH_SIZE, &size, nullptr);
588 if (size != 2 * GIT_HASH_SIZE)
589 return -1;
591 m_Head.ConvertFromStrA((char*)buffer);
593 m_LastModifyTimeRef = time;
595 return 0;
598 ReadFile(hfile, buffer + (DWORD)strlen("ref:"), 2 * GIT_HASH_SIZE - (DWORD)strlen("ref:"), &size, nullptr);
599 if (size != 2 * GIT_HASH_SIZE - (DWORD)strlen("ref:"))
600 return -1;
602 m_HeadRefFile.Empty();
604 m_Head.ConvertFromStrA((char*)buffer);
606 return 0;
609 bool CGitHeadFileList::CheckHeadUpdate()
611 if (this->m_HeadFile.IsEmpty())
612 return true;
614 __int64 mtime=0;
616 if (CGit::GetFileModifyTime(m_HeadFile, &mtime))
617 return true;
619 if (mtime != this->m_LastModifyTimeHead)
620 return true;
622 if (!this->m_HeadRefFile.IsEmpty())
624 if (CGit::GetFileModifyTime(m_HeadRefFile, &mtime))
625 return true;
627 if (mtime != this->m_LastModifyTimeRef)
628 return true;
631 if(!this->m_PackRefFile.IsEmpty())
633 if (CGit::GetFileModifyTime(m_PackRefFile, &mtime))
634 return true;
636 if (mtime != this->m_LastModifyTimePackRef)
637 return true;
640 // in an empty repo HEAD points to refs/heads/master, but this ref doesn't exist.
641 // So we need to retry again and again until the ref exists - otherwise we will never notice
642 if (this->m_Head.IsEmpty() && this->m_HeadRefFile.IsEmpty() && this->m_PackRefFile.IsEmpty())
643 return true;
645 return false;
648 bool CGitHeadFileList::HeadHashEqualsTreeHash()
650 return (m_Head == m_TreeHash);
653 bool CGitHeadFileList::HeadFileIsEmpty()
655 return m_HeadFile.IsEmpty();
658 bool CGitHeadFileList::HeadIsEmpty()
660 return m_Head.IsEmpty();
663 int CGitHeadFileList::CallBack(const unsigned char *sha1, const char *base, int baselen,
664 const char *pathname, unsigned mode, int /*stage*/, void *context)
666 #define S_IFGITLINK 0160000
668 CGitHeadFileList* p = reinterpret_cast<CGitHeadFileList*>(context);
670 if ((mode & S_IFDIR) && (mode & S_IFMT) != S_IFGITLINK)
671 return READ_TREE_RECURSIVE;
673 CGitTreeItem item;
674 item.m_Hash = sha1;
675 CGit::StringAppend(&item.m_FileName, (BYTE*)base, CP_UTF8, baselen);
676 CGit::StringAppend(&item.m_FileName, (BYTE*)pathname, CP_UTF8);
677 if ((mode & S_IFMT) == S_IFGITLINK)
678 item.m_FileName += L'/';
680 p->push_back(item);
682 if( (mode&S_IFMT) == S_IFGITLINK)
683 return 0;
685 return READ_TREE_RECURSIVE;
688 int ReadTreeRecursive(git_repository &repo, const git_tree * tree, const CStringA& base, int (*CallBack) (const unsigned char *, const char *, int, const char *, unsigned int, int, void *), void *data)
690 size_t count = git_tree_entrycount(tree);
691 for (size_t i = 0; i < count; ++i)
693 const git_tree_entry *entry = git_tree_entry_byindex(tree, i);
694 if (!entry)
695 continue;
696 int mode = git_tree_entry_filemode(entry);
697 if( CallBack(git_tree_entry_id(entry)->id,
698 base,
699 base.GetLength(),
700 git_tree_entry_name(entry),
701 mode,
703 data) == READ_TREE_RECURSIVE
706 if(mode&S_IFDIR)
708 git_object* object = nullptr;
709 git_tree_entry_to_object(&object, &repo, entry);
710 if (!object)
711 continue;
712 CStringA parent = base;
713 parent += git_tree_entry_name(entry);
714 parent += "/";
715 ReadTreeRecursive(repo, (git_tree*)object, parent, CallBack, data);
716 git_object_free(object);
722 return 0;
725 // ReadTree is/must only be executed on an empty list
726 int CGitHeadFileList::ReadTree()
728 ATLASSERT(empty());
730 CAutoRepository repository(m_Gitdir);
731 CAutoCommit commit;
732 CAutoTree tree;
733 bool ret = repository;
734 ret = ret && !git_commit_lookup(commit.GetPointer(), repository, (const git_oid*)m_Head.m_hash);
735 ret = ret && !git_commit_tree(tree.GetPointer(), commit);
738 ret = ret && !ReadTreeRecursive(*repository, tree, "", CGitHeadFileList::CallBack, this);
740 catch (const std::bad_alloc& ex)
742 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Catched exception inside ReadTreeRecursive: %s\n", ex.what());
743 return -1;
745 if (!ret)
747 clear();
748 m_LastModifyTimeHead = 0;
749 return -1;
752 std::sort(this->begin(), this->end(), SortTree);
753 m_TreeHash = git_commit_id(commit)->id;
755 return 0;
757 int CGitIgnoreItem::FetchIgnoreList(const CString& projectroot, const CString& file, bool isGlobal, int* ignoreCase)
759 if (this->m_pExcludeList)
761 git_free_exclude_list(m_pExcludeList);
762 m_pExcludeList = nullptr;
764 free(m_buffer);
765 m_buffer = nullptr;
767 this->m_BaseDir.Empty();
768 if (!isGlobal)
770 CString base = file.Mid(projectroot.GetLength() + 1);
771 base.Replace(L'\\', L'/');
773 int start = base.ReverseFind(L'/');
774 if(start > 0)
776 base.Truncate(start);
777 this->m_BaseDir = CUnicodeUtils::GetMulti(base, CP_UTF8) + "/";
781 if (CGit::GetFileModifyTime(file, &m_LastModifyTime))
782 return -1;
784 CAutoFile hfile = CreateFile(file,
785 GENERIC_READ,
786 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
787 nullptr,
788 OPEN_EXISTING,
789 FILE_ATTRIBUTE_NORMAL,
790 nullptr);
792 if (!hfile)
793 return -1 ;
795 DWORD filesize = GetFileSize(hfile, nullptr);
796 if (filesize == INVALID_FILE_SIZE)
797 return -1;
799 m_buffer = new BYTE[filesize + 1];
800 if (!m_buffer)
801 return -1;
803 DWORD size = 0;
804 if (!ReadFile(hfile, m_buffer, filesize, &size, nullptr))
806 free(m_buffer);
807 m_buffer = nullptr;
808 return -1;
810 m_buffer[size] = '\0';
812 if (git_create_exclude_list(&m_pExcludeList))
814 free(m_buffer);
815 m_buffer = nullptr;
816 return -1;
819 m_iIgnoreCase = ignoreCase;
821 BYTE *p = m_buffer;
822 int line = 0;
823 for (DWORD i = 0; i < size; ++i)
825 if (m_buffer[i] == '\n' || m_buffer[i] == '\r' || i == (size - 1))
827 if (m_buffer[i] == '\n' || m_buffer[i] == '\r')
828 m_buffer[i] = '\0';
830 if (p[0] != '#' && p[0])
831 git_add_exclude((const char*)p, this->m_BaseDir, m_BaseDir.GetLength(), this->m_pExcludeList, ++line);
833 p = m_buffer + i + 1;
837 if (!line)
839 git_free_exclude_list(m_pExcludeList);
840 m_pExcludeList = nullptr;
841 free(m_buffer);
842 m_buffer = nullptr;
845 return 0;
848 #ifdef GTEST_INCLUDE_GTEST_GTEST_H_
849 int CGitIgnoreItem::IsPathIgnored(const CStringA& patha, int& type)
851 int pos = patha.ReverseFind('/');
852 const char* base = (pos >= 0) ? ((const char*)patha + pos + 1) : patha;
854 return IsPathIgnored(patha, base, type);
856 #endif
858 int CGitIgnoreItem::IsPathIgnored(const CStringA& patha, const char* base, int& type)
860 if (!m_pExcludeList)
861 return -1; // error or undecided
863 return git_check_excluded_1(patha, patha.GetLength(), base, &type, m_pExcludeList, m_iIgnoreCase ? *m_iIgnoreCase : 1);
866 bool CGitIgnoreList::CheckFileChanged(const CString &path)
868 __int64 time = 0;
870 int ret = CGit::GetFileModifyTime(path, &time);
872 bool cacheExist;
874 CAutoReadLock lock(m_SharedMutex);
875 cacheExist = (m_Map.find(path) != m_Map.end());
878 if (!cacheExist && ret == 0)
880 CAutoWriteLock lock(m_SharedMutex);
881 m_Map[path].m_LastModifyTime = 0;
883 // both cache and file is not exist
884 if ((ret != 0) && (!cacheExist))
885 return false;
887 // file exist but cache miss
888 if ((ret == 0) && (!cacheExist))
889 return true;
891 // file not exist but cache exist
892 if ((ret != 0) && (cacheExist))
893 return true;
894 // file exist and cache exist
897 CAutoReadLock lock(m_SharedMutex);
898 if (m_Map[path].m_LastModifyTime == time)
899 return false;
901 return true;
904 int CGitIgnoreList::FetchIgnoreFile(const CString &gitdir, const CString &gitignore, bool isGlobal)
906 if (CGit::GitPathFileExists(gitignore)) //if .gitignore remove, we need remote cache
908 CAutoWriteLock lock(m_SharedMutex);
909 m_Map[gitignore].FetchIgnoreList(gitdir, gitignore, isGlobal, &m_IgnoreCase[g_AdminDirMap.GetAdminDir(gitdir)]);
911 else
913 CAutoWriteLock lock(m_SharedMutex);
914 m_Map.erase(gitignore);
916 return 0;
919 bool CGitIgnoreList::CheckAndUpdateIgnoreFiles(const CString& gitdir, const CString& path, bool isDir)
921 CString temp(gitdir);
922 temp += L'\\';
923 temp += path;
925 temp.Replace(L'/', L'\\');
927 if (!isDir)
929 int x = temp.ReverseFind(L'\\');
930 if (x >= 2)
931 temp.Truncate(x);
934 bool updated = false;
935 while (!temp.IsEmpty())
937 temp += L"\\.gitignore";
939 if (CheckFileChanged(temp))
941 FetchIgnoreFile(gitdir, temp, false);
942 updated = true;
945 temp.Truncate(temp.GetLength() - (int)wcslen(L"\\.gitignore"));
946 if (CPathUtils::ArePathStringsEqual(temp, gitdir))
948 CString adminDir = g_AdminDirMap.GetAdminDir(temp);
949 CString wcglobalgitignore = adminDir + L"info\\exclude";
950 if (CheckFileChanged(wcglobalgitignore))
952 FetchIgnoreFile(gitdir, wcglobalgitignore, true);
953 updated = true;
956 if (CheckAndUpdateCoreExcludefile(adminDir))
958 CString excludesFile;
960 CAutoReadLock lock(m_SharedMutex);
961 excludesFile = m_CoreExcludesfiles[adminDir];
963 if (!excludesFile.IsEmpty())
965 FetchIgnoreFile(gitdir, excludesFile, true);
966 updated = true;
970 return updated;
973 int i = temp.ReverseFind(L'\\');
974 temp.Truncate(max(0, i));
976 return updated;
979 bool CGitIgnoreList::CheckAndUpdateGitSystemConfigPath(bool force)
981 if (force)
982 m_sGitProgramDataConfigPath = GetProgramDataGitConfig();
983 // recheck every 30 seconds
984 if (GetTickCount64() - m_dGitSystemConfigPathLastChecked > 30000UL || force)
986 m_dGitSystemConfigPathLastChecked = GetTickCount64();
987 CString gitSystemConfigPath(CRegString(REG_SYSTEM_GITCONFIGPATH, L"", FALSE));
988 if (gitSystemConfigPath != m_sGitSystemConfigPath)
990 m_sGitSystemConfigPath = gitSystemConfigPath;
991 return true;
994 return false;
996 bool CGitIgnoreList::CheckAndUpdateCoreExcludefile(const CString &adminDir)
998 CString projectConfig(adminDir);
999 projectConfig += L"config";
1000 CString globalConfig = g_Git.GetGitGlobalConfig();
1001 CString globalXDGConfig = g_Git.GetGitGlobalXDGConfig();
1003 CAutoWriteLock lock(m_coreExcludefilesSharedMutex);
1004 bool hasChanged = CheckAndUpdateGitSystemConfigPath();
1005 hasChanged = hasChanged || CheckFileChanged(projectConfig);
1006 hasChanged = hasChanged || CheckFileChanged(globalConfig);
1007 hasChanged = hasChanged || CheckFileChanged(globalXDGConfig);
1008 if (!m_sGitProgramDataConfigPath.IsEmpty())
1009 hasChanged = hasChanged || CheckFileChanged(m_sGitProgramDataConfigPath);
1010 if (!m_sGitSystemConfigPath.IsEmpty())
1011 hasChanged = hasChanged || CheckFileChanged(m_sGitSystemConfigPath);
1013 CString excludesFile;
1015 CAutoReadLock lock2(m_SharedMutex);
1016 excludesFile = m_CoreExcludesfiles[adminDir];
1018 if (!excludesFile.IsEmpty())
1019 hasChanged = hasChanged || CheckFileChanged(excludesFile);
1021 if (!hasChanged)
1022 return false;
1024 CAutoConfig config(true);
1025 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(projectConfig), GIT_CONFIG_LEVEL_LOCAL, FALSE);
1026 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalConfig), GIT_CONFIG_LEVEL_GLOBAL, FALSE);
1027 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalXDGConfig), GIT_CONFIG_LEVEL_XDG, FALSE);
1028 if (!m_sGitSystemConfigPath.IsEmpty())
1029 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(m_sGitSystemConfigPath), GIT_CONFIG_LEVEL_SYSTEM, FALSE);
1030 if (!m_sGitProgramDataConfigPath.IsEmpty())
1031 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(m_sGitProgramDataConfigPath), GIT_CONFIG_LEVEL_PROGRAMDATA, FALSE);
1033 config.GetString(L"core.excludesfile", excludesFile);
1034 if (excludesFile.IsEmpty())
1035 excludesFile = GetWindowsHome() + L"\\.config\\git\\ignore";
1036 else if (CStringUtils::StartsWith(excludesFile, L"~/"))
1037 excludesFile = GetWindowsHome() + excludesFile.Mid(1);
1039 CAutoWriteLock lockMap(m_SharedMutex);
1040 m_IgnoreCase[adminDir] = 1;
1041 config.GetBOOL(L"core.ignorecase", m_IgnoreCase[adminDir]);
1042 CGit::GetFileModifyTime(projectConfig, &m_Map[projectConfig].m_LastModifyTime);
1043 CGit::GetFileModifyTime(globalXDGConfig, &m_Map[globalXDGConfig].m_LastModifyTime);
1044 if (m_Map[globalXDGConfig].m_LastModifyTime == 0)
1045 m_Map.erase(globalXDGConfig);
1046 CGit::GetFileModifyTime(globalConfig, &m_Map[globalConfig].m_LastModifyTime);
1047 if (m_Map[globalConfig].m_LastModifyTime == 0)
1048 m_Map.erase(globalConfig);
1049 if (!m_sGitSystemConfigPath.IsEmpty())
1050 CGit::GetFileModifyTime(m_sGitSystemConfigPath, &m_Map[m_sGitSystemConfigPath].m_LastModifyTime);
1051 if (m_Map[m_sGitSystemConfigPath].m_LastModifyTime == 0 || m_sGitSystemConfigPath.IsEmpty())
1052 m_Map.erase(m_sGitSystemConfigPath);
1053 if (!m_sGitProgramDataConfigPath.IsEmpty())
1054 CGit::GetFileModifyTime(m_sGitProgramDataConfigPath, &m_Map[m_sGitProgramDataConfigPath].m_LastModifyTime);
1055 if (m_Map[m_sGitProgramDataConfigPath].m_LastModifyTime == 0 || m_sGitProgramDataConfigPath.IsEmpty())
1056 m_Map.erase(m_sGitProgramDataConfigPath);
1057 m_CoreExcludesfiles[adminDir] = excludesFile;
1059 return true;
1061 const CString CGitIgnoreList::GetWindowsHome()
1063 static CString sWindowsHome(g_Git.GetHomeDirectory());
1064 return sWindowsHome;
1066 bool CGitIgnoreList::IsIgnore(CString str, const CString& projectroot, bool isDir)
1068 str.Replace(L'\\', L'/');
1070 if (!str.IsEmpty() && str[str.GetLength() - 1] == L'/')
1071 str.Truncate(str.GetLength() - 1);
1073 int ret;
1074 ret = CheckIgnore(str, projectroot, isDir);
1075 while (ret < 0)
1077 int start = str.ReverseFind(L'/');
1078 if(start < 0)
1079 return (ret == 1);
1081 str.Truncate(start);
1082 ret = CheckIgnore(str, projectroot, isDir);
1085 return (ret == 1);
1087 int CGitIgnoreList::CheckFileAgainstIgnoreList(const CString &ignorefile, const CStringA &patha, const char * base, int &type)
1089 if (m_Map.find(ignorefile) == m_Map.end())
1090 return -1; // error or undecided
1092 return (m_Map[ignorefile].IsPathIgnored(patha, base, type));
1094 int CGitIgnoreList::CheckIgnore(const CString &path, const CString &projectroot, bool isDir)
1096 CString temp = CombinePath(projectroot, path);
1097 temp.Replace(L'/', L'\\');
1099 CStringA patha = CUnicodeUtils::GetMulti(path, CP_UTF8);
1100 patha.Replace('\\', '/');
1102 int type = 0;
1103 if (isDir)
1105 type = DT_DIR;
1107 // strip directory name
1108 // we do not need to check for a .ignore file inside a directory we might ignore
1109 int i = temp.ReverseFind(L'\\');
1110 if (i >= 0)
1111 temp.Truncate(i);
1113 else
1115 type = DT_REG;
1117 int x = temp.ReverseFind(L'\\');
1118 if (x >= 2)
1119 temp.Truncate(x);
1122 int pos = patha.ReverseFind('/');
1123 const char * base = (pos >= 0) ? ((const char*)patha + pos + 1) : patha;
1125 int ret = -1;
1127 CAutoReadLock lock(m_SharedMutex);
1128 while (!temp.IsEmpty())
1130 temp += L"\\.gitignore";
1132 if ((ret = CheckFileAgainstIgnoreList(temp, patha, base, type)) != -1)
1133 return ret;
1135 temp.Truncate(temp.GetLength() - (int)wcslen(L"\\.gitignore"));
1137 if (CPathUtils::ArePathStringsEqual(temp, projectroot))
1139 CString adminDir = g_AdminDirMap.GetAdminDir(temp);
1140 CString wcglobalgitignore = adminDir;
1141 wcglobalgitignore += L"info\\exclude";
1142 if ((ret = CheckFileAgainstIgnoreList(wcglobalgitignore, patha, base, type)) != -1)
1143 return ret;
1145 CString excludesFile = m_CoreExcludesfiles[adminDir];
1146 if (!excludesFile.IsEmpty())
1147 return CheckFileAgainstIgnoreList(excludesFile, patha, base, type);
1149 return -1;
1152 int i = temp.ReverseFind(L'\\');
1153 temp.Truncate(max(0, i));
1156 return -1;
1159 bool CGitHeadFileMap::CheckHeadAndUpdate(const CString &gitdir)
1161 SHARED_TREE_PTR ptr = this->SafeGet(gitdir, true);
1163 if (ptr.get() && !ptr->CheckHeadUpdate() && ptr->HeadHashEqualsTreeHash())
1164 return false;
1166 ptr = std::make_shared<CGitHeadFileList>();
1167 ptr->ReadHeadHash(gitdir);
1168 ptr->ReadTree();
1170 this->SafeSet(gitdir, ptr);
1172 return true;
1175 int CGitHeadFileMap::IsUnderVersionControl(const CString& gitdir, CString subpath, bool isDir, bool* isVersion)
1177 if (subpath.IsEmpty())
1179 *isVersion = true;
1180 return 0;
1183 subpath.Replace(L'\\', L'/');
1184 if (isDir)
1185 subpath += L'/';
1187 CheckHeadAndUpdate(gitdir);
1189 SHARED_TREE_PTR treeptr = SafeGet(gitdir);
1191 // Init Repository
1192 if (treeptr->HeadFileIsEmpty())
1194 *isVersion = false;
1195 return 0;
1197 if (treeptr->empty())
1199 *isVersion = false;
1200 return 1;
1203 if (isDir)
1204 *isVersion = (SearchInSortVector(*treeptr, subpath, subpath.GetLength()) != NPOS);
1205 else
1206 *isVersion = (SearchInSortVector(*treeptr, subpath, -1) != NPOS);
1208 return 0;