Use higher resolution timestamp
[TortoiseGit.git] / src / Git / GitIndex.cpp
blobccd8b7f46e6f6694b190a542665503d17e770dd8
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)
58 , m_LastFileSize(-1)
60 m_iMaxCheckSize = (__int64)CRegDWORD(L"Software\\TortoiseGit\\TGitCacheCheckContentMaxSize", 10 * 1024) * 1024; // stored in KiB
63 CGitIndexList::~CGitIndexList()
67 static bool SortIndex(const CGitIndex &Item1, const CGitIndex &Item2)
69 return Item1.m_FileName.Compare(Item2.m_FileName) < 0;
72 static bool SortTree(const CGitTreeItem &Item1, const CGitTreeItem &Item2)
74 return Item1.m_FileName.Compare(Item2.m_FileName) < 0;
77 int CGitIndexList::ReadIndex(CString dgitdir)
79 #ifdef GTEST_INCLUDE_GTEST_GTEST_H_
80 clear(); // HACK to make tests work, until we use CGitIndexList
81 #endif
82 ATLASSERT(empty());
84 CAutoRepository repository(dgitdir);
85 if (!repository)
87 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not open git repository in %s: %s\n", (LPCTSTR)dgitdir, (LPCTSTR)CGit::GetLibGit2LastErr());
88 return -1;
91 // add config files
92 config.New();
94 CString projectConfig = g_AdminDirMap.GetAdminDir(dgitdir) + L"config";
95 CString globalConfig = g_Git.GetGitGlobalConfig();
96 CString globalXDGConfig = g_Git.GetGitGlobalXDGConfig();
97 CString systemConfig(CRegString(REG_SYSTEM_GITCONFIGPATH, L"", FALSE));
98 CString programDataConfig(GetProgramDataGitConfig());
100 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(projectConfig), GIT_CONFIG_LEVEL_LOCAL, FALSE);
101 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalConfig), GIT_CONFIG_LEVEL_GLOBAL, FALSE);
102 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalXDGConfig), GIT_CONFIG_LEVEL_XDG, FALSE);
103 if (!systemConfig.IsEmpty())
104 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(systemConfig), GIT_CONFIG_LEVEL_SYSTEM, FALSE);
105 if (!programDataConfig.IsEmpty())
106 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(programDataConfig), GIT_CONFIG_LEVEL_PROGRAMDATA, FALSE);
108 git_repository_set_config(repository, config);
110 CGit::GetFileModifyTime(g_AdminDirMap.GetWorktreeAdminDir(dgitdir) + L"index", &m_LastModifyTime, nullptr, &m_LastFileSize);
112 CAutoIndex index;
113 // load index in order to enumerate files
114 if (git_repository_index(index.GetPointer(), repository))
116 config.Free();
117 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not get index of git repository in %s: %s\n", (LPCTSTR)dgitdir, (LPCTSTR)CGit::GetLibGit2LastErr());
118 return -1;
121 m_bHasConflicts = FALSE;
123 size_t ecount = git_index_entrycount(index);
126 resize(ecount);
128 catch (const std::bad_alloc& ex)
130 config.Free();
131 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Could not resize index-vector: %s\n", ex.what());
132 return -1;
134 for (size_t i = 0; i < ecount; ++i)
136 const git_index_entry *e = git_index_get_byindex(index, i);
138 auto& item = (*this)[i];
139 item.m_FileName = CUnicodeUtils::GetUnicode(e->path);
140 if (e->mode & S_IFDIR)
141 item.m_FileName += L'/';
142 item.m_ModifyTime = e->mtime.seconds;
143 item.m_Flags = e->flags;
144 item.m_FlagsExtended = e->flags_extended;
145 item.m_IndexHash = e->id.id;
146 item.m_Size = e->file_size;
147 m_bHasConflicts |= GIT_IDXENTRY_STAGE(e);
150 std::sort(this->begin(), this->end(), SortIndex);
152 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Reloaded index for repo: %s\n", (LPCTSTR)dgitdir);
154 return 0;
157 int CGitIndexList::GetFileStatus(const CString& gitdir, const CString& pathorg, git_wc_status2_t& status, __int64 time, __int64 filesize, CGitHash* pHash)
159 size_t index = SearchInSortVector(*this, pathorg, -1);
161 if (index == NPOS)
163 status.status = git_wc_status_unversioned;
164 if (pHash)
165 pHash->Empty();
167 return 0;
170 auto& entry = (*this)[index];
171 if (pHash)
172 *pHash = entry.m_IndexHash;
173 ATLASSERT(pathorg == entry.m_FileName);
174 CAutoRepository repository;
175 return GetFileStatus(repository, gitdir, entry, status, time, filesize);
178 int CGitIndexList::GetFileStatus(CAutoRepository& repository, const CString& gitdir, CGitIndex& entry, git_wc_status2_t& status, __int64 time, __int64 filesize)
180 ATLASSERT(!status.assumeValid && !status.skipWorktree);
182 // skip-worktree has higher priority than assume-valid
183 if (entry.m_FlagsExtended & GIT_IDXENTRY_SKIP_WORKTREE)
185 status.status = git_wc_status_normal;
186 status.skipWorktree = true;
188 else if (entry.m_Flags & GIT_IDXENTRY_VALID)
190 status.status = git_wc_status_normal;
191 status.assumeValid = true;
193 else if (filesize == -1)
194 status.status = git_wc_status_deleted;
195 else if (filesize != entry.m_Size)
196 status.status = git_wc_status_modified;
197 else if (CGit::filetime_to_time_t(time) == entry.m_ModifyTime)
198 status.status = git_wc_status_normal;
199 else if (config && filesize < m_iMaxCheckSize)
202 * Opening a new repository each time is not yet optimal, however, there is no API to clear the pack-cache
203 * When a shared repository is used, we might need a mutex to prevent concurrent access to repository instance and especially filter-lists
205 if (!repository)
207 if (repository.Open(gitdir))
209 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Could not open git repository in %s for checking file: %s\n", (LPCTSTR)gitdir, (LPCTSTR)CGit::GetLibGit2LastErr());
210 return -1;
212 git_repository_set_config(repository, config);
215 git_oid actual;
216 CStringA fileA = CUnicodeUtils::GetMulti(entry.m_FileName, CP_UTF8);
217 if (!git_repository_hashfile(&actual, repository, fileA, GIT_OBJ_BLOB, nullptr) && !git_oid_cmp(&actual, (const git_oid*)entry.m_IndexHash.m_hash))
219 entry.m_ModifyTime = time;
220 status.status = git_wc_status_normal;
222 else
223 status.status = git_wc_status_modified;
225 else
226 status.status = git_wc_status_modified;
228 if (entry.m_Flags & GIT_IDXENTRY_STAGEMASK)
229 status.status = git_wc_status_conflicted;
230 else if (entry.m_FlagsExtended & GIT_IDXENTRY_INTENT_TO_ADD)
231 status.status = git_wc_status_added;
233 return 0;
236 int CGitIndexList::GetFileStatus(const CString& gitdir, const CString& path, git_wc_status2_t& status, CGitHash* pHash)
238 ATLASSERT(!status.assumeValid && !status.skipWorktree);
240 __int64 time, filesize = 0;
241 bool isDir = false;
243 int result;
244 if (path.IsEmpty())
245 result = CGit::GetFileModifyTime(gitdir, &time, &isDir);
246 else
247 result = CGit::GetFileModifyTime(CombinePath(gitdir, path), &time, &isDir, &filesize);
249 if (result)
250 filesize = -1;
252 if (!isDir)
253 return GetFileStatus(gitdir, path, status, time, filesize, pHash);
255 if (CStringUtils::EndsWith(path, L'/'))
257 size_t index = SearchInSortVector(*this, path, -1);
258 if (index == NPOS)
260 status.status = git_wc_status_unversioned;
261 if (pHash)
262 pHash->Empty();
264 return 0;
267 if (pHash)
268 *pHash = (*this)[index].m_IndexHash;
270 if (!result)
271 status.status = git_wc_status_normal;
272 else
273 status.status = git_wc_status_deleted;
274 return 0;
277 // we should never get here
278 status.status = git_wc_status_unversioned;
280 return -1;
283 bool CGitIndexFileMap::HasIndexChangedOnDisk(const CString& gitdir)
285 __int64 time = -1, size = -1;
287 auto pIndex = SafeGet(gitdir);
289 if (!pIndex)
290 return true;
292 CString IndexFile = g_AdminDirMap.GetWorktreeAdminDirConcat(gitdir, L"index");
293 // no need to refresh if there is no index right now and the current index is empty, but otherwise or lastmodified time differs
294 return (CGit::GetFileModifyTime(IndexFile, &time, nullptr, &size) && !pIndex->empty()) || pIndex->m_LastModifyTime != time || pIndex->m_LastFileSize != size;
297 int CGitIndexFileMap::LoadIndex(const CString &gitdir)
299 SHARED_INDEX_PTR pIndex = std::make_shared<CGitIndexList>();
301 if (pIndex->ReadIndex(gitdir))
303 SafeClear(gitdir);
304 return -1;
307 this->SafeSet(gitdir, pIndex);
309 return 0;
312 // This method is assumed to be called with m_SharedMutex locked.
313 int CGitHeadFileList::GetPackRef(const CString &gitdir)
315 CString PackRef = g_AdminDirMap.GetAdminDirConcat(gitdir, L"packed-refs");
317 __int64 mtime = 0, packsize = -1;
318 if (CGit::GetFileModifyTime(PackRef, &mtime, nullptr, &packsize))
320 //packed refs is not existed
321 this->m_PackRefFile.Empty();
322 this->m_PackRefMap.clear();
323 return 0;
325 else if (mtime == m_LastModifyTimePackRef && packsize == m_LastFileSizePackRef)
326 return 0;
327 else
329 this->m_PackRefFile = PackRef;
330 this->m_LastModifyTimePackRef = mtime;
331 this->m_LastFileSizePackRef = packsize;
334 m_PackRefMap.clear();
336 CAutoFile hfile = CreateFile(PackRef,
337 GENERIC_READ,
338 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
339 nullptr,
340 OPEN_EXISTING,
341 FILE_ATTRIBUTE_NORMAL,
342 nullptr);
344 if (!hfile)
345 return -1;
347 DWORD filesize = GetFileSize(hfile, nullptr);
348 if (filesize == 0 || filesize == INVALID_FILE_SIZE)
349 return -1;
351 DWORD size = 0;
352 auto buff = std::make_unique<char[]>(filesize);
353 ReadFile(hfile, buff.get(), filesize, &size, nullptr);
355 if (size != filesize)
356 return -1;
358 for (DWORD i = 0; i < filesize;)
360 CString hash;
361 CString ref;
362 if (buff[i] == '#' || buff[i] == '^')
364 while (buff[i] != '\n')
366 ++i;
367 if (i == filesize)
368 break;
370 ++i;
373 if (i >= filesize)
374 break;
376 while (buff[i] != ' ')
378 hash.AppendChar(buff[i]);
379 ++i;
380 if (i == filesize)
381 break;
384 ++i;
385 if (i >= filesize)
386 break;
388 while (buff[i] != '\n')
390 ref.AppendChar(buff[i]);
391 ++i;
392 if (i == filesize)
393 break;
396 if (!ref.IsEmpty())
397 m_PackRefMap[ref] = hash;
399 while (buff[i] == '\n')
401 ++i;
402 if (i == filesize)
403 break;
406 return 0;
408 int CGitHeadFileList::ReadHeadHash(const CString& gitdir)
410 ATLASSERT(m_Gitdir.IsEmpty() && m_HeadFile.IsEmpty() && m_Head.IsEmpty());
412 m_Gitdir = g_AdminDirMap.GetWorktreeAdminDir(gitdir);
414 m_HeadFile = m_Gitdir;
415 m_HeadFile += L"HEAD";
417 if (CGit::GetFileModifyTime(m_HeadFile, &m_LastModifyTimeHead, nullptr, &m_LastFileSizeHead))
418 return -1;
420 CAutoFile hfile = CreateFile(m_HeadFile,
421 GENERIC_READ,
422 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
423 nullptr,
424 OPEN_EXISTING,
425 FILE_ATTRIBUTE_NORMAL,
426 nullptr);
428 if (!hfile)
429 return -1;
431 DWORD size = 0;
432 unsigned char buffer[2 * GIT_HASH_SIZE];
433 ReadFile(hfile, buffer, (DWORD)strlen("ref:"), &size, nullptr);
434 if (size != strlen("ref:"))
435 return -1;
436 buffer[4] = '\0';
437 if (strcmp((const char*)buffer, "ref:") == 0)
439 m_HeadRefFile.Empty();
440 DWORD filesize = GetFileSize(hfile, nullptr);
441 if (filesize < 5 || filesize == INVALID_FILE_SIZE)
442 return -1;
444 unsigned char *p = (unsigned char*)malloc(filesize - strlen("ref:"));
445 if (!p)
446 return -1;
448 ReadFile(hfile, p, filesize - (DWORD)strlen("ref:"), &size, nullptr);
449 CGit::StringAppend(&m_HeadRefFile, p, CP_UTF8, filesize - (int)strlen("ref:"));
450 free(p);
452 CString ref = m_HeadRefFile.Trim();
453 int start = 0;
454 ref = ref.Tokenize(L"\n", start);
455 m_HeadRefFile = g_AdminDirMap.GetAdminDir(gitdir) + m_HeadRefFile;
456 m_HeadRefFile.Replace(L'/', L'\\');
458 __int64 time;
459 if (CGit::GetFileModifyTime(m_HeadRefFile, &time, nullptr))
461 m_HeadRefFile.Empty();
462 if (GetPackRef(gitdir))
463 return -1;
464 if (m_PackRefMap.find(ref) != m_PackRefMap.end())
466 m_Head = m_PackRefMap[ref];
467 return 0;
470 // unborn branch
471 m_Head.Empty();
473 return 0;
476 CAutoFile href = CreateFile(m_HeadRefFile,
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 (!href)
486 m_HeadRefFile.Empty();
488 if (GetPackRef(gitdir))
489 return -1;
491 if (m_PackRefMap.find(ref) == m_PackRefMap.end())
492 return -1;
494 m_Head = m_PackRefMap[ref];
495 return 0;
498 ReadFile(href, buffer, 2 * GIT_HASH_SIZE, &size, nullptr);
499 if (size != 2 * GIT_HASH_SIZE)
500 return -1;
502 m_Head.ConvertFromStrA((char*)buffer);
504 m_LastModifyTimeRef = time;
506 return 0;
509 ReadFile(hfile, buffer + (DWORD)strlen("ref:"), 2 * GIT_HASH_SIZE - (DWORD)strlen("ref:"), &size, nullptr);
510 if (size != 2 * GIT_HASH_SIZE - (DWORD)strlen("ref:"))
511 return -1;
513 m_HeadRefFile.Empty();
515 m_Head.ConvertFromStrA((char*)buffer);
517 return 0;
520 bool CGitHeadFileList::CheckHeadUpdate()
522 if (this->m_HeadFile.IsEmpty())
523 return true;
525 __int64 mtime = 0, size = -1;
527 if (CGit::GetFileModifyTime(m_HeadFile, &mtime, nullptr, &size))
528 return true;
530 if (mtime != m_LastModifyTimeHead || size != m_LastFileSizeHead)
531 return true;
533 if (!this->m_HeadRefFile.IsEmpty())
535 if (CGit::GetFileModifyTime(m_HeadRefFile, &mtime))
536 return true;
538 if (mtime != this->m_LastModifyTimeRef)
539 return true;
542 if(!this->m_PackRefFile.IsEmpty())
544 size = -1;
545 if (CGit::GetFileModifyTime(m_PackRefFile, &mtime, nullptr, &size))
546 return true;
548 if (mtime != m_LastModifyTimePackRef || size != m_LastFileSizePackRef)
549 return true;
552 // in an empty repo HEAD points to refs/heads/master, but this ref doesn't exist.
553 // So we need to retry again and again until the ref exists - otherwise we will never notice
554 if (this->m_Head.IsEmpty() && this->m_HeadRefFile.IsEmpty() && this->m_PackRefFile.IsEmpty())
555 return true;
557 return false;
560 int CGitHeadFileList::CallBack(const unsigned char *sha1, const char *base, int baselen,
561 const char *pathname, unsigned mode, int /*stage*/, void *context)
563 #define S_IFGITLINK 0160000
565 CGitHeadFileList* p = reinterpret_cast<CGitHeadFileList*>(context);
567 if ((mode & S_IFDIR) && (mode & S_IFMT) != S_IFGITLINK)
568 return READ_TREE_RECURSIVE;
570 CGitTreeItem item;
571 item.m_Hash = sha1;
572 CGit::StringAppend(&item.m_FileName, (BYTE*)base, CP_UTF8, baselen);
573 CGit::StringAppend(&item.m_FileName, (BYTE*)pathname, CP_UTF8);
574 if ((mode & S_IFMT) == S_IFGITLINK)
575 item.m_FileName += L'/';
577 p->push_back(item);
579 if( (mode&S_IFMT) == S_IFGITLINK)
580 return 0;
582 return READ_TREE_RECURSIVE;
585 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)
587 size_t count = git_tree_entrycount(tree);
588 for (size_t i = 0; i < count; ++i)
590 const git_tree_entry *entry = git_tree_entry_byindex(tree, i);
591 if (!entry)
592 continue;
593 int mode = git_tree_entry_filemode(entry);
594 if( CallBack(git_tree_entry_id(entry)->id,
595 base,
596 base.GetLength(),
597 git_tree_entry_name(entry),
598 mode,
600 data) == READ_TREE_RECURSIVE
603 if(mode&S_IFDIR)
605 git_object* object = nullptr;
606 git_tree_entry_to_object(&object, &repo, entry);
607 if (!object)
608 continue;
609 CStringA parent = base;
610 parent += git_tree_entry_name(entry);
611 parent += "/";
612 ReadTreeRecursive(repo, (git_tree*)object, parent, CallBack, data);
613 git_object_free(object);
619 return 0;
622 // ReadTree is/must only be executed on an empty list
623 int CGitHeadFileList::ReadTree()
625 ATLASSERT(empty());
627 // unborn branch
628 if (m_Head.IsEmpty())
629 return 0;
631 CAutoRepository repository(m_Gitdir);
632 CAutoCommit commit;
633 CAutoTree tree;
634 bool ret = repository;
635 ret = ret && !git_commit_lookup(commit.GetPointer(), repository, (const git_oid*)m_Head.m_hash);
636 ret = ret && !git_commit_tree(tree.GetPointer(), commit);
639 ret = ret && !ReadTreeRecursive(*repository, tree, "", CGitHeadFileList::CallBack, this);
641 catch (const std::bad_alloc& ex)
643 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": Catched exception inside ReadTreeRecursive: %s\n", ex.what());
644 return -1;
646 if (!ret)
648 clear();
649 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());
650 m_LastModifyTimeHead = 0;
651 m_LastFileSizeHead = -1;
652 return -1;
655 std::sort(this->begin(), this->end(), SortTree);
657 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Reloaded HEAD tree (commit is %s) for repo: %s\n", (LPCTSTR)m_Head.ToString(), (LPCTSTR)m_Gitdir);
659 return 0;
661 int CGitIgnoreItem::FetchIgnoreList(const CString& projectroot, const CString& file, bool isGlobal, int* ignoreCase)
663 if (this->m_pExcludeList)
665 git_free_exclude_list(m_pExcludeList);
666 m_pExcludeList = nullptr;
668 free(m_buffer);
669 m_buffer = nullptr;
671 this->m_BaseDir.Empty();
672 if (!isGlobal)
674 CString base = file.Mid(projectroot.GetLength() + 1);
675 base.Replace(L'\\', L'/');
677 int start = base.ReverseFind(L'/');
678 if(start > 0)
680 base.Truncate(start);
681 this->m_BaseDir = CUnicodeUtils::GetMulti(base, CP_UTF8) + "/";
685 if (CGit::GetFileModifyTime(file, &m_LastModifyTime, nullptr, &m_LastFileSize))
686 return -1;
688 CAutoFile hfile = CreateFile(file,
689 GENERIC_READ,
690 FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
691 nullptr,
692 OPEN_EXISTING,
693 FILE_ATTRIBUTE_NORMAL,
694 nullptr);
696 if (!hfile)
697 return -1 ;
699 DWORD filesize = GetFileSize(hfile, nullptr);
700 if (filesize == INVALID_FILE_SIZE)
701 return -1;
703 m_buffer = new BYTE[filesize + 1];
704 if (!m_buffer)
705 return -1;
707 DWORD size = 0;
708 if (!ReadFile(hfile, m_buffer, filesize, &size, nullptr))
710 free(m_buffer);
711 m_buffer = nullptr;
712 return -1;
714 m_buffer[size] = '\0';
716 if (git_create_exclude_list(&m_pExcludeList))
718 free(m_buffer);
719 m_buffer = nullptr;
720 return -1;
723 m_iIgnoreCase = ignoreCase;
725 BYTE *p = m_buffer;
726 int line = 0;
727 for (DWORD i = 0; i < size; ++i)
729 if (m_buffer[i] == '\n' || m_buffer[i] == '\r' || i == (size - 1))
731 if (m_buffer[i] == '\n' || m_buffer[i] == '\r')
732 m_buffer[i] = '\0';
734 if (p[0] != '#' && p[0])
735 git_add_exclude((const char*)p, this->m_BaseDir, m_BaseDir.GetLength(), this->m_pExcludeList, ++line);
737 p = m_buffer + i + 1;
741 if (!line)
743 git_free_exclude_list(m_pExcludeList);
744 m_pExcludeList = nullptr;
745 free(m_buffer);
746 m_buffer = nullptr;
749 return 0;
752 #ifdef GTEST_INCLUDE_GTEST_GTEST_H_
753 int CGitIgnoreItem::IsPathIgnored(const CStringA& patha, int& type)
755 int pos = patha.ReverseFind('/');
756 const char* base = (pos >= 0) ? ((const char*)patha + pos + 1) : patha;
758 return IsPathIgnored(patha, base, type);
760 #endif
762 int CGitIgnoreItem::IsPathIgnored(const CStringA& patha, const char* base, int& type)
764 if (!m_pExcludeList)
765 return -1; // error or undecided
767 return git_check_excluded_1(patha, patha.GetLength(), base, &type, m_pExcludeList, m_iIgnoreCase ? *m_iIgnoreCase : 1);
770 bool CGitIgnoreList::CheckFileChanged(const CString &path)
772 __int64 time = 0, size = -1;
774 int ret = CGit::GetFileModifyTime(path, &time, nullptr, &size);
776 bool cacheExist;
778 CAutoReadLock lock(m_SharedMutex);
779 cacheExist = (m_Map.find(path) != m_Map.end());
782 if (!cacheExist && ret == 0)
784 CAutoWriteLock lock(m_SharedMutex);
785 m_Map[path].m_LastModifyTime = 0;
786 m_Map[path].m_LastFileSize = -1;
788 // both cache and file is not exist
789 if ((ret != 0) && (!cacheExist))
790 return false;
792 // file exist but cache miss
793 if ((ret == 0) && (!cacheExist))
794 return true;
796 // file not exist but cache exist
797 if ((ret != 0) && (cacheExist))
798 return true;
799 // file exist and cache exist
802 CAutoReadLock lock(m_SharedMutex);
803 if (m_Map[path].m_LastModifyTime == time && m_Map[path].m_LastFileSize == size)
804 return false;
806 return true;
809 int CGitIgnoreList::FetchIgnoreFile(const CString &gitdir, const CString &gitignore, bool isGlobal)
811 if (CGit::GitPathFileExists(gitignore)) //if .gitignore remove, we need remote cache
813 CAutoWriteLock lock(m_SharedMutex);
814 m_Map[gitignore].FetchIgnoreList(gitdir, gitignore, isGlobal, &m_IgnoreCase[g_AdminDirMap.GetAdminDir(gitdir)]);
816 else
818 CAutoWriteLock lock(m_SharedMutex);
819 m_Map.erase(gitignore);
821 return 0;
824 bool CGitIgnoreList::CheckAndUpdateIgnoreFiles(const CString& gitdir, const CString& path, bool isDir)
826 CString temp(gitdir);
827 temp += L'\\';
828 temp += path;
830 temp.Replace(L'/', L'\\');
832 if (!isDir)
834 int x = temp.ReverseFind(L'\\');
835 if (x >= 2)
836 temp.Truncate(x);
839 bool updated = false;
840 while (!temp.IsEmpty())
842 temp += L"\\.gitignore";
844 if (CheckFileChanged(temp))
846 FetchIgnoreFile(gitdir, temp, false);
847 updated = true;
850 temp.Truncate(temp.GetLength() - (int)wcslen(L"\\.gitignore"));
851 if (CPathUtils::ArePathStringsEqual(temp, gitdir))
853 CString adminDir = g_AdminDirMap.GetAdminDir(temp);
854 CString wcglobalgitignore = adminDir + L"info\\exclude";
855 if (CheckFileChanged(wcglobalgitignore))
857 FetchIgnoreFile(gitdir, wcglobalgitignore, true);
858 updated = true;
861 if (CheckAndUpdateCoreExcludefile(adminDir))
863 CString excludesFile;
865 CAutoReadLock lock(m_SharedMutex);
866 excludesFile = m_CoreExcludesfiles[adminDir];
868 if (!excludesFile.IsEmpty())
870 FetchIgnoreFile(gitdir, excludesFile, true);
871 updated = true;
875 return updated;
878 int i = temp.ReverseFind(L'\\');
879 temp.Truncate(max(0, i));
881 return updated;
884 bool CGitIgnoreList::CheckAndUpdateGitSystemConfigPath(bool force)
886 if (force)
887 m_sGitProgramDataConfigPath = GetProgramDataGitConfig();
888 // recheck every 30 seconds
889 if (GetTickCount64() - m_dGitSystemConfigPathLastChecked > 30000UL || force)
891 m_dGitSystemConfigPathLastChecked = GetTickCount64();
892 CString gitSystemConfigPath(CRegString(REG_SYSTEM_GITCONFIGPATH, L"", FALSE));
893 if (gitSystemConfigPath != m_sGitSystemConfigPath)
895 m_sGitSystemConfigPath = gitSystemConfigPath;
896 return true;
899 return false;
901 bool CGitIgnoreList::CheckAndUpdateCoreExcludefile(const CString &adminDir)
903 CString projectConfig(adminDir);
904 projectConfig += L"config";
905 CString globalConfig = g_Git.GetGitGlobalConfig();
906 CString globalXDGConfig = g_Git.GetGitGlobalXDGConfig();
908 CAutoWriteLock lock(m_coreExcludefilesSharedMutex);
909 bool hasChanged = CheckAndUpdateGitSystemConfigPath();
910 hasChanged = hasChanged || CheckFileChanged(projectConfig);
911 hasChanged = hasChanged || CheckFileChanged(globalConfig);
912 hasChanged = hasChanged || CheckFileChanged(globalXDGConfig);
913 if (!m_sGitProgramDataConfigPath.IsEmpty())
914 hasChanged = hasChanged || CheckFileChanged(m_sGitProgramDataConfigPath);
915 if (!m_sGitSystemConfigPath.IsEmpty())
916 hasChanged = hasChanged || CheckFileChanged(m_sGitSystemConfigPath);
918 CString excludesFile;
920 CAutoReadLock lock2(m_SharedMutex);
921 excludesFile = m_CoreExcludesfiles[adminDir];
923 if (!excludesFile.IsEmpty())
924 hasChanged = hasChanged || CheckFileChanged(excludesFile);
926 if (!hasChanged)
927 return false;
929 CAutoConfig config(true);
930 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(projectConfig), GIT_CONFIG_LEVEL_LOCAL, FALSE);
931 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalConfig), GIT_CONFIG_LEVEL_GLOBAL, FALSE);
932 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(globalXDGConfig), GIT_CONFIG_LEVEL_XDG, FALSE);
933 if (!m_sGitSystemConfigPath.IsEmpty())
934 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(m_sGitSystemConfigPath), GIT_CONFIG_LEVEL_SYSTEM, FALSE);
935 if (!m_sGitProgramDataConfigPath.IsEmpty())
936 git_config_add_file_ondisk(config, CGit::GetGitPathStringA(m_sGitProgramDataConfigPath), GIT_CONFIG_LEVEL_PROGRAMDATA, FALSE);
938 config.GetString(L"core.excludesfile", excludesFile);
939 if (excludesFile.IsEmpty())
940 excludesFile = GetWindowsHome() + L"\\.config\\git\\ignore";
941 else if (CStringUtils::StartsWith(excludesFile, L"~/"))
942 excludesFile = GetWindowsHome() + excludesFile.Mid(1);
944 CAutoWriteLock lockMap(m_SharedMutex);
945 m_IgnoreCase[adminDir] = 1;
946 config.GetBOOL(L"core.ignorecase", m_IgnoreCase[adminDir]);
947 CGit::GetFileModifyTime(projectConfig, &m_Map[projectConfig].m_LastModifyTime);
948 CGit::GetFileModifyTime(globalXDGConfig, &m_Map[globalXDGConfig].m_LastModifyTime);
949 if (m_Map[globalXDGConfig].m_LastModifyTime == 0)
950 m_Map.erase(globalXDGConfig);
951 CGit::GetFileModifyTime(globalConfig, &m_Map[globalConfig].m_LastModifyTime);
952 if (m_Map[globalConfig].m_LastModifyTime == 0)
953 m_Map.erase(globalConfig);
954 if (!m_sGitSystemConfigPath.IsEmpty())
955 CGit::GetFileModifyTime(m_sGitSystemConfigPath, &m_Map[m_sGitSystemConfigPath].m_LastModifyTime);
956 if (m_Map[m_sGitSystemConfigPath].m_LastModifyTime == 0 || m_sGitSystemConfigPath.IsEmpty())
957 m_Map.erase(m_sGitSystemConfigPath);
958 if (!m_sGitProgramDataConfigPath.IsEmpty())
959 CGit::GetFileModifyTime(m_sGitProgramDataConfigPath, &m_Map[m_sGitProgramDataConfigPath].m_LastModifyTime);
960 if (m_Map[m_sGitProgramDataConfigPath].m_LastModifyTime == 0 || m_sGitProgramDataConfigPath.IsEmpty())
961 m_Map.erase(m_sGitProgramDataConfigPath);
962 m_CoreExcludesfiles[adminDir] = excludesFile;
964 return true;
966 const CString CGitIgnoreList::GetWindowsHome()
968 static CString sWindowsHome(g_Git.GetHomeDirectory());
969 return sWindowsHome;
971 bool CGitIgnoreList::IsIgnore(CString str, const CString& projectroot, bool isDir)
973 str.Replace(L'\\', L'/');
975 if (!str.IsEmpty() && str[str.GetLength() - 1] == L'/')
976 str.Truncate(str.GetLength() - 1);
978 int ret;
979 ret = CheckIgnore(str, projectroot, isDir);
980 while (ret < 0)
982 int start = str.ReverseFind(L'/');
983 if(start < 0)
984 return (ret == 1);
986 str.Truncate(start);
987 ret = CheckIgnore(str, projectroot, isDir);
990 return (ret == 1);
992 int CGitIgnoreList::CheckFileAgainstIgnoreList(const CString &ignorefile, const CStringA &patha, const char * base, int &type)
994 if (m_Map.find(ignorefile) == m_Map.end())
995 return -1; // error or undecided
997 return (m_Map[ignorefile].IsPathIgnored(patha, base, type));
999 int CGitIgnoreList::CheckIgnore(const CString &path, const CString &projectroot, bool isDir)
1001 CString temp = CombinePath(projectroot, path);
1002 temp.Replace(L'/', L'\\');
1004 CStringA patha = CUnicodeUtils::GetMulti(path, CP_UTF8);
1005 patha.Replace('\\', '/');
1007 int type = 0;
1008 if (isDir)
1010 type = DT_DIR;
1012 // strip directory name
1013 // we do not need to check for a .ignore file inside a directory we might ignore
1014 int i = temp.ReverseFind(L'\\');
1015 if (i >= 0)
1016 temp.Truncate(i);
1018 else
1020 type = DT_REG;
1022 int x = temp.ReverseFind(L'\\');
1023 if (x >= 2)
1024 temp.Truncate(x);
1027 int pos = patha.ReverseFind('/');
1028 const char * base = (pos >= 0) ? ((const char*)patha + pos + 1) : patha;
1030 int ret = -1;
1032 CAutoReadLock lock(m_SharedMutex);
1033 while (!temp.IsEmpty())
1035 temp += L"\\.gitignore";
1037 if ((ret = CheckFileAgainstIgnoreList(temp, patha, base, type)) != -1)
1038 return ret;
1040 temp.Truncate(temp.GetLength() - (int)wcslen(L"\\.gitignore"));
1042 if (CPathUtils::ArePathStringsEqual(temp, projectroot))
1044 CString adminDir = g_AdminDirMap.GetAdminDir(temp);
1045 CString wcglobalgitignore = adminDir;
1046 wcglobalgitignore += L"info\\exclude";
1047 if ((ret = CheckFileAgainstIgnoreList(wcglobalgitignore, patha, base, type)) != -1)
1048 return ret;
1050 CString excludesFile = m_CoreExcludesfiles[adminDir];
1051 if (!excludesFile.IsEmpty())
1052 return CheckFileAgainstIgnoreList(excludesFile, patha, base, type);
1054 return -1;
1057 int i = temp.ReverseFind(L'\\');
1058 temp.Truncate(max(0, i));
1061 return -1;
1064 void CGitHeadFileMap::CheckHeadAndUpdate(const CString &gitdir)
1066 SHARED_TREE_PTR ptr = this->SafeGet(gitdir);
1068 if (ptr.get() && !ptr->CheckHeadUpdate())
1069 return;
1071 ptr = std::make_shared<CGitHeadFileList>();
1072 if (ptr->ReadHeadHash(gitdir) || ptr->ReadTree())
1074 SafeClear(gitdir);
1075 return;
1078 this->SafeSet(gitdir, ptr);
1080 return;