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.
23 #include "UnicodeUtils.h"
24 #include "PathUtils.h"
26 #include <sys/types.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";
44 int CGitIndex::Print()
46 wprintf(L
"0x%08X 0x%08X %s %s\n",
47 (int)this->m_ModifyTime
,
49 (LPCTSTR
)this->m_IndexHash
.ToString(),
50 (LPCTSTR
)this->m_FileName
);
55 CGitIndexList::CGitIndexList()
56 : m_bHasConflicts(FALSE
)
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
75 CAutoRepository
repository(dgitdir
);
78 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Could not open git repository in %s: %s\n", (LPCTSTR
)dgitdir
, (LPCTSTR
)CGit::GetLibGit2LastErr());
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
);
104 // load index in order to enumerate files
105 if (git_repository_index(index
.GetPointer(), repository
))
108 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Could not get index of git repository in %s: %s\n", (LPCTSTR
)dgitdir
, (LPCTSTR
)CGit::GetLibGit2LastErr());
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
);
122 catch (const std::bad_alloc
& ex
)
125 CTraceToOutputDebugString::Instance()(__FUNCTION__
": Could not resize index-vector: %s\n", ex
.what());
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
);
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());
158 status
.status
= git_wc_status_unversioned
;
165 auto& entry
= (*this)[index
];
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
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());
209 git_repository_set_config(repository
, config
);
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
;
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
;
231 status
.status
= git_wc_status_modified
;
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
;
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;
250 bool isSymlink
= false;
254 result
= CGit::GetFileModifyTime(gitdir
, &time
, &isDir
);
256 result
= CGit::GetFileModifyTime(CombinePath(gitdir
, path
), &time
, &isDir
, &filesize
, &isSymlink
);
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());
269 status
.status
= git_wc_status_unversioned
;
277 *pHash
= (*this)[index
].m_IndexHash
;
280 status
.status
= git_wc_status_normal
;
282 status
.status
= git_wc_status_deleted
;
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
;
296 bool CGitIndexFileMap::HasIndexChangedOnDisk(const CString
& gitdir
)
298 __int64 time
= -1, size
= -1;
300 auto pIndex
= SafeGet(gitdir
);
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
))
320 this->SafeSet(gitdir
, pIndex
);
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();
338 else if (mtime
== m_LastModifyTimePackRef
&& packsize
== m_LastFileSizePackRef
)
342 this->m_PackRefFile
= PackRef
;
343 this->m_LastModifyTimePackRef
= mtime
;
344 this->m_LastFileSizePackRef
= packsize
;
347 m_PackRefMap
.clear();
349 CAutoFile hfile
= CreateFile(PackRef
,
351 FILE_SHARE_READ
| FILE_SHARE_DELETE
| FILE_SHARE_WRITE
,
354 FILE_ATTRIBUTE_NORMAL
,
360 DWORD filesize
= GetFileSize(hfile
, nullptr);
361 if (filesize
== 0 || filesize
== INVALID_FILE_SIZE
)
365 auto buff
= std::make_unique
<char[]>(filesize
);
366 ReadFile(hfile
, buff
.get(), filesize
, &size
, nullptr);
368 if (size
!= filesize
)
371 for (DWORD i
= 0; i
< filesize
;)
375 if (buff
[i
] == '#' || buff
[i
] == '^')
377 while (buff
[i
] != '\n')
389 while (buff
[i
] != ' ')
391 hash
.AppendChar(buff
[i
]);
401 while (buff
[i
] != '\n')
403 ref
.AppendChar(buff
[i
]);
410 m_PackRefMap
[ref
] = hash
;
412 while (buff
[i
] == '\n')
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
))
433 CAutoFile hfile
= CreateFile(m_HeadFile
,
435 FILE_SHARE_READ
| FILE_SHARE_DELETE
| FILE_SHARE_WRITE
,
438 FILE_ATTRIBUTE_NORMAL
,
445 unsigned char buffer
[2 * GIT_HASH_SIZE
];
446 ReadFile(hfile
, buffer
, (DWORD
)strlen("ref:"), &size
, nullptr);
447 if (size
!= strlen("ref:"))
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
)
457 unsigned char *p
= (unsigned char*)malloc(filesize
- strlen("ref:"));
461 ReadFile(hfile
, p
, filesize
- (DWORD
)strlen("ref:"), &size
, nullptr);
462 CGit::StringAppend(&m_HeadRefFile
, p
, CP_UTF8
, filesize
- (int)strlen("ref:"));
465 CString ref
= m_HeadRefFile
.Trim();
467 ref
= ref
.Tokenize(L
"\n", start
);
468 m_HeadRefFile
= g_AdminDirMap
.GetAdminDir(gitdir
) + m_HeadRefFile
;
469 m_HeadRefFile
.Replace(L
'/', L
'\\');
472 if (CGit::GetFileModifyTime(m_HeadRefFile
, &time
, nullptr))
474 if (GetPackRef(gitdir
))
476 if (m_PackRefMap
.find(ref
) != m_PackRefMap
.end())
478 m_bRefFromPackRefFile
= true;
479 m_Head
= m_PackRefMap
[ref
];
489 CAutoFile href
= CreateFile(m_HeadRefFile
,
491 FILE_SHARE_READ
| FILE_SHARE_DELETE
| FILE_SHARE_WRITE
,
494 FILE_ATTRIBUTE_NORMAL
,
499 if (GetPackRef(gitdir
))
502 if (m_PackRefMap
.find(ref
) == m_PackRefMap
.end())
505 m_bRefFromPackRefFile
= true;
506 m_Head
= m_PackRefMap
[ref
];
510 ReadFile(href
, buffer
, 2 * GIT_HASH_SIZE
, &size
, nullptr);
511 if (size
!= 2 * GIT_HASH_SIZE
)
514 m_Head
.ConvertFromStrA((char*)buffer
);
516 m_LastModifyTimeRef
= time
;
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:"))
525 m_HeadRefFile
.Empty();
527 m_Head
.ConvertFromStrA((char*)buffer
);
532 bool CGitHeadFileList::CheckHeadUpdate()
534 if (this->m_HeadFile
.IsEmpty())
537 __int64 mtime
= 0, size
= -1;
539 if (CGit::GetFileModifyTime(m_HeadFile
, &mtime
, nullptr, &size
))
542 if (mtime
!= m_LastModifyTimeHead
|| size
!= m_LastFileSizeHead
)
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
)
553 } else if (mtime
!= this->m_LastModifyTimeRef
)
557 if (m_bRefFromPackRefFile
&& !m_PackRefFile
.IsEmpty())
560 if (CGit::GetFileModifyTime(m_PackRefFile
, &mtime
, nullptr, &size
))
563 if (mtime
!= m_LastModifyTimePackRef
|| size
!= m_LastFileSizePackRef
)
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())
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
);
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
)
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
);
594 item
.m_FileName
+= L
'/';
600 git_tree_entry_to_object(object
.GetPointer(), &repo
, entry
);
603 CStringA parent
= base
;
604 parent
+= git_tree_entry_name(entry
);
606 ReadTreeRecursive(repo
, (git_tree
*)(git_object
*)object
, parent
);
612 // ReadTree is/must only be executed on an empty list
613 int CGitHeadFileList::ReadTree(bool ignoreCase
)
618 if (m_Head
.IsEmpty())
621 CAutoRepository
repository(m_Gitdir
);
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());
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;
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
);
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;
661 this->m_BaseDir
.Empty();
664 CString base
= file
.Mid(projectroot
.GetLength() + 1);
665 base
.Replace(L
'\\', L
'/');
667 int start
= base
.ReverseFind(L
'/');
670 base
.Truncate(start
);
671 this->m_BaseDir
= CUnicodeUtils::GetMulti(base
, CP_UTF8
) + "/";
675 if (CGit::GetFileModifyTime(file
, &m_LastModifyTime
, nullptr, &m_LastFileSize
))
678 CAutoFile hfile
= CreateFile(file
,
680 FILE_SHARE_READ
| FILE_SHARE_DELETE
| FILE_SHARE_WRITE
,
683 FILE_ATTRIBUTE_NORMAL
,
689 DWORD filesize
= GetFileSize(hfile
, nullptr);
690 if (filesize
== INVALID_FILE_SIZE
)
693 m_buffer
= new BYTE
[filesize
+ 1];
698 if (!ReadFile(hfile
, m_buffer
, filesize
, &size
, nullptr))
704 m_buffer
[size
] = '\0';
706 if (git_create_exclude_list(&m_pExcludeList
))
713 m_iIgnoreCase
= ignoreCase
;
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')
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;
733 git_free_exclude_list(m_pExcludeList
);
734 m_pExcludeList
= nullptr;
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
);
752 int CGitIgnoreItem::IsPathIgnored(const CStringA
& patha
, const char* base
, int& type
)
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
);
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
))
782 // file exist but cache miss
783 if ((ret
== 0) && (!cacheExist
))
786 // file not exist but cache exist
787 if ((ret
!= 0) && (cacheExist
))
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
)
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
)]);
808 CAutoWriteLock
lock(m_SharedMutex
);
809 m_Map
.erase(gitignore
);
814 bool CGitIgnoreList::CheckAndUpdateIgnoreFiles(const CString
& gitdir
, const CString
& path
, bool isDir
, std::set
<CString
>* lastChecked
)
816 CString
temp(gitdir
);
820 temp
.Replace(L
'/', L
'\\');
824 int x
= temp
.ReverseFind(L
'\\');
829 bool updated
= false;
830 while (!temp
.IsEmpty())
834 if (lastChecked
->find(temp
) != lastChecked
->end())
836 lastChecked
->insert(temp
);
839 temp
+= L
"\\.gitignore";
841 if (CheckFileChanged(temp
))
843 FetchIgnoreFile(gitdir
, temp
, false);
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);
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);
875 int i
= temp
.ReverseFind(L
'\\');
876 temp
.Truncate(max(0, i
));
881 bool CGitIgnoreList::CheckAndUpdateGitSystemConfigPath(bool 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
;
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
);
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
;
964 const CString
CGitIgnoreList::GetWindowsHome()
966 static CString
sWindowsHome(g_Git
.GetHomeDirectory());
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);
977 ret
= CheckIgnore(str
, projectroot
, isDir
, adminDir
);
980 int start
= str
.ReverseFind(L
'/');
985 ret
= CheckIgnore(str
, projectroot
, TRUE
, adminDir
);
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('\\', '/');
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
'\\');
1020 int x
= temp
.ReverseFind(L
'\\');
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";
1035 if ((ret
= CheckFileAgainstIgnoreList(temp
, patha
, base
, type
)) != -1)
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)
1047 CString excludesFile
= m_CoreExcludesfiles
[adminDir
];
1048 if (!excludesFile
.IsEmpty())
1049 return CheckFileAgainstIgnoreList(excludesFile
, patha
, base
, type
);
1054 int i
= temp
.ReverseFind(L
'\\');
1055 temp
.Truncate(max(0, i
));
1061 void CGitHeadFileMap::CheckHeadAndUpdate(const CString
& gitdir
, bool ignoreCase
)
1063 SHARED_TREE_PTR ptr
= this->SafeGet(gitdir
);
1065 if (ptr
.get() && !ptr
->CheckHeadUpdate())
1068 ptr
= std::make_shared
<CGitHeadFileList
>();
1069 if (ptr
->ReadHeadHash(gitdir
) || ptr
->ReadTree(ignoreCase
))
1075 this->SafeSet(gitdir
, ptr
);