1
// TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2024 - TortoiseGit
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License
7 // as published by the Free Software Foundation; either version 2
8 // of the License, or (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software Foundation,
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 #include "UnicodeUtils.h"
24 #include "PathUtils.h"
26 #include <sys/types.h>
28 #include "SmartHandle.h"
29 #include "git2/sys/repository.h"
32 CGitAdminDirMap g_AdminDirMap
;
34 int CGitIndex::Print()
36 wprintf(L
"0x%08X 0x%08X %s %s\n",
37 static_cast<int>(this->m_ModifyTime
),
39 static_cast<LPCWSTR
>(this->m_IndexHash
.ToString()),
40 static_cast<LPCWSTR
>(this->m_FileName
));
45 CGitIndexList::CGitIndexList()
47 #ifndef TGIT_TESTS_ONLY
48 m_iMaxCheckSize
= static_cast<__int64
>(CRegDWORD(L
"Software\\TortoiseGit\\TGitCacheCheckContentMaxSize", 10 * 1024)) * 1024; // stored in KiB
49 m_bCalculateIncomingOutgoing
= (CRegStdDWORD(L
"Software\\TortoiseGit\\ModifyExplorerTitle", TRUE
) != FALSE
);
53 CGitIndexList::~CGitIndexList()
57 bool CGitIndexList::HasIndexChangedOnDisk(const CString
& gitdir
) const
59 __int64 time
= -1, size
= -1;
61 CString indexFile
= g_AdminDirMap
.GetWorktreeAdminDirConcat(gitdir
, L
"index");
62 // no need to refresh if there is no index right now and the current index is empty, but otherwise lastFileSize or lastmodifiedTime differ
63 return (CGit::GetFileModifyTime(indexFile
, &time
, nullptr, &size
) && !empty()) || m_LastModifyTime
!= time
|| m_LastFileSize
!= size
;
66 int CGitIndexList::ReadIndex(const CString
& dgitdir
)
68 #ifdef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
69 clear(); // HACK to make tests work, until we use CGitIndexList
73 CString repodir
= dgitdir
;
74 if (dgitdir
.GetLength() == 2 && dgitdir
[1] == L
':')
75 repodir
+= L
'\\'; // libgit2 requires a drive root to end with a (back)slash
77 CAutoRepository
repository(repodir
);
80 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Could not open git repository in %s: %s\n", static_cast<LPCWSTR
>(dgitdir
), static_cast<LPCWSTR
>(CGit::GetLibGit2LastErr()));
84 CString projectConfig
= g_AdminDirMap
.GetAdminDir(dgitdir
) + L
"config";
85 CString globalConfig
= g_Git
.GetGitGlobalConfig();
86 CString globalXDGConfig
= g_Git
.GetGitGlobalXDGConfig();
87 CString
systemConfig(CRegString(REG_SYSTEM_GITCONFIGPATH
, L
"", FALSE
));
89 CAutoConfig temp
{ true };
90 git_config_add_file_ondisk(temp
, CGit::GetGitPathStringA(projectConfig
), GIT_CONFIG_LEVEL_LOCAL
, repository
, FALSE
);
91 git_config_add_file_ondisk(temp
, CGit::GetGitPathStringA(globalConfig
), GIT_CONFIG_LEVEL_GLOBAL
, repository
, FALSE
);
92 git_config_add_file_ondisk(temp
, CGit::GetGitPathStringA(globalXDGConfig
), GIT_CONFIG_LEVEL_XDG
, repository
, FALSE
);
93 if (!systemConfig
.IsEmpty())
94 git_config_add_file_ondisk(temp
, CGit::GetGitPathStringA(systemConfig
), GIT_CONFIG_LEVEL_SYSTEM
, repository
, FALSE
);
96 git_config_snapshot(config
.GetPointer(), temp
);
98 git_repository_set_config(repository
, config
);
100 CGit::GetFileModifyTime(g_AdminDirMap
.GetWorktreeAdminDir(dgitdir
) + L
"index", &m_LastModifyTime
, nullptr, &m_LastFileSize
);
103 // load index in order to enumerate files
104 if (git_repository_index(index
.GetPointer(), repository
))
107 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Could not get index of git repository in %s: %s\n", static_cast<LPCWSTR
>(dgitdir
), static_cast<LPCWSTR
>(CGit::GetLibGit2LastErr()));
111 m_bHasConflicts
= FALSE
;
112 m_iIndexCaps
= git_index_caps(index
);
113 if (CRegDWORD(L
"Software\\TortoiseGit\\OverlaysCaseSensitive", TRUE
) != FALSE
)
114 m_iIndexCaps
&= ~GIT_INDEX_CAPABILITY_IGNORE_CASE
;
116 const size_t ecount
= git_index_entrycount(index
);
121 catch (const std::bad_alloc
& ex
)
124 CTraceToOutputDebugString::Instance()(__FUNCTION__
": Could not resize index-vector: %s\n", ex
.what());
127 catch (const std::length_error
& ex
)
130 CTraceToOutputDebugString::Instance()(__FUNCTION__
": Could not resize index-vector, length_error: %s\n", ex
.what());
133 for (size_t i
= 0; i
< ecount
; ++i
)
135 const git_index_entry
*e
= git_index_get_byindex(index
, i
);
137 auto& item
= (*this)[i
];
138 item
.m_FileName
= CUnicodeUtils::GetUnicode(e
->path
);
139 if (e
->mode
& S_IFDIR
)
140 item
.m_FileName
+= L
'/';
141 static_assert(std::is_same
<decltype(item
.m_ModifyTime
), decltype(e
->mtime
.seconds
)>::value
);
142 item
.m_ModifyTime
= e
->mtime
.seconds
;
143 static_assert(std::is_same
<decltype(item
.m_ModifyTimeNanos
), decltype(e
->mtime
.nanoseconds
)>::value
);
144 item
.m_ModifyTimeNanos
= e
->mtime
.nanoseconds
;
145 item
.m_Flags
= e
->flags
;
146 item
.m_FlagsExtended
= e
->flags_extended
;
147 item
.m_IndexHash
= e
->id
;
148 static_assert(std::is_same
<decltype(item
.m_Size
), decltype(e
->file_size
)>::value
);
149 item
.m_Size
= e
->file_size
;
150 item
.m_Mode
= e
->mode
;
151 m_bHasConflicts
|= GIT_INDEX_ENTRY_STAGE(e
);
154 DoSortFilenametSortVector(*this, IsIgnoreCase());
156 ReadIncomingOutgoing(repository
);
158 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Reloaded index for repo: %s\n", static_cast<LPCWSTR
>(dgitdir
));
163 int CGitIndexList::ReadIncomingOutgoing(git_repository
* repository
)
165 ATLASSERT(m_stashCount
== 0 && m_outgoing
== static_cast<size_t>(-1) && m_incoming
== static_cast<size_t>(-1) && m_branch
.IsEmpty());
167 if (!m_bCalculateIncomingOutgoing
)
170 if (git_stash_foreach(repository
, [](size_t, const char*, const git_oid
*, void* payload
) -> int {
171 auto stashCount
= static_cast<size_t*>(payload
);
174 }, &m_stashCount
) < 0)
177 if (const int detachedhead
= git_repository_head_detached(repository
); detachedhead
== 1)
179 m_branch
= L
"detached HEAD";
182 else if (detachedhead
< 0)
186 if (const int unborn
= git_repository_head_unborn(repository
); unborn
< 0)
188 else if (unborn
== 1)
190 if (git_reference_lookup(head
.GetPointer(), repository
, "HEAD") < 0)
193 m_branch
= CGit::StripRefName(CUnicodeUtils::GetUnicode(git_reference_symbolic_target(head
)));
197 if (git_repository_head(head
.GetPointer(), repository
) < 0)
200 m_branch
= CUnicodeUtils::GetUnicode(git_reference_shorthand(head
));
202 CAutoBuf upstreambranchname
;
204 // check whether there is an upstream branch
205 if (git_branch_upstream_name(upstreambranchname
, repository
, git_reference_name(head
)) != 0 || git_reference_name_to_id(&upstream
, repository
, upstreambranchname
->ptr
) != 0)
206 return 0; // we don't have an upstream branch
208 if (git_graph_ahead_behind(&m_outgoing
, &m_incoming
, repository
, git_reference_target(head
), &upstream
) < 0)
214 int CGitIndexList::GetFileStatus(const CString
& gitdir
, const CString
& pathorg
, git_wc_status2_t
& status
, __int64 time
, __int64 filesize
, bool isSymlink
, CGitHash
* pHash
) const
216 size_t index
= SearchInSortVector(*this, pathorg
, -1, IsIgnoreCase());
220 status
.status
= git_wc_status_unversioned
;
227 auto& entry
= (*this)[index
];
229 *pHash
= entry
.m_IndexHash
;
230 ATLASSERT(IsIgnoreCase() ? pathorg
.CompareNoCase(entry
.m_FileName
) == 0 : pathorg
.Compare(entry
.m_FileName
) == 0);
231 CAutoRepository repository
;
232 return GetFileStatus(repository
, gitdir
, entry
, status
, time
, filesize
, isSymlink
);
235 int CGitIndexList::GetFileStatus(CAutoRepository
& repository
, const CString
& gitdir
, const CGitIndex
& entry
, git_wc_status2_t
& status
, __int64 time
, __int64 filesize
, bool isSymlink
) const
237 ATLASSERT(!status
.assumeValid
&& !status
.skipWorktree
);
239 // skip-worktree has higher priority than assume-valid
240 if (entry
.m_FlagsExtended
& GIT_INDEX_ENTRY_SKIP_WORKTREE
)
242 status
.status
= git_wc_status_normal
;
243 status
.skipWorktree
= true;
245 else if (entry
.m_Flags
& GIT_INDEX_ENTRY_VALID
)
247 status
.status
= git_wc_status_normal
;
248 status
.assumeValid
= true;
250 else if (filesize
== -1)
251 status
.status
= git_wc_status_deleted
;
252 else if ((isSymlink
&& !S_ISLNK(entry
.m_Mode
)) || ((m_iIndexCaps
& GIT_INDEX_CAPABILITY_NO_SYMLINKS
) != GIT_INDEX_CAPABILITY_NO_SYMLINKS
&& isSymlink
!= S_ISLNK(entry
.m_Mode
)))
253 status
.status
= git_wc_status_modified
;
254 else if (!isSymlink
&& static_cast<uint32_t>(filesize
) != entry
.m_Size
)
255 status
.status
= git_wc_status_modified
;
256 else if (static_cast<int32_t>(CGit::filetime_to_time_t(time
)) == entry
.m_ModifyTime
&& entry
.m_ModifyTimeNanos
== (time
% 10000000) * 100)
257 status
.status
= git_wc_status_normal
;
258 else if (config
&& filesize
< m_iMaxCheckSize
)
261 * Opening a new repository each time is not yet optimal, however, there is no API to clear the pack-cache
262 * When a shared repository is used, we might need a mutex to prevent concurrent access to repository instance and especially filter-lists
266 CString repodir
= gitdir
;
267 if (gitdir
.GetLength() == 2 && gitdir
[1] == L
':')
268 repodir
+= L
'\\'; // libgit2 requires a drive root to end with a (back)slash
270 if (repository
.Open(repodir
))
272 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Could not open git repository in %s for checking file: %s\n", static_cast<LPCWSTR
>(gitdir
), static_cast<LPCWSTR
>(CGit::GetLibGit2LastErr()));
275 git_repository_set_config(repository
, config
);
279 CStringA fileA
= CUnicodeUtils::GetUTF8(entry
.m_FileName
);
280 if (isSymlink
&& S_ISLNK(entry
.m_Mode
))
282 CStringA linkDestination
;
283 if (!CPathUtils::ReadLink(CombinePath(gitdir
, entry
.m_FileName
), &linkDestination
) && !git_odb_hash(&actual
, static_cast<LPCSTR
>(linkDestination
), linkDestination
.GetLength(), GIT_OBJECT_BLOB
) && !git_oid_cmp(&actual
, entry
.m_IndexHash
))
285 entry
.m_ModifyTime
= static_cast<int32_t>(CGit::filetime_to_time_t(time
));
286 entry
.m_ModifyTimeNanos
= (time
% 10000000) * 100;
287 status
.status
= git_wc_status_normal
;
290 status
.status
= git_wc_status_modified
;
292 else if (!git_repository_hashfile(&actual
, repository
, fileA
, GIT_OBJECT_BLOB
, nullptr) && !git_oid_cmp(&actual
, entry
.m_IndexHash
))
294 entry
.m_ModifyTime
= static_cast<int32_t>(CGit::filetime_to_time_t(time
));
295 entry
.m_ModifyTimeNanos
= (time
% 10000000) * 100;
296 status
.status
= git_wc_status_normal
;
299 status
.status
= git_wc_status_modified
;
302 status
.status
= git_wc_status_modified
;
304 if (entry
.m_Flags
& GIT_INDEX_ENTRY_STAGEMASK
)
305 status
.status
= git_wc_status_conflicted
;
306 else if (entry
.m_FlagsExtended
& GIT_INDEX_ENTRY_INTENT_TO_ADD
)
307 status
.status
= git_wc_status_added
;
312 int CGitIndexList::GetFileStatus(const CString
& gitdir
, const CString
& path
, git_wc_status2_t
& status
, CGitHash
* pHash
) const
314 ATLASSERT(!status
.assumeValid
&& !status
.skipWorktree
);
316 __int64 time
, filesize
= 0;
318 bool isSymlink
= false;
322 result
= CGit::GetFileModifyTime(gitdir
, &time
, &isDir
);
324 result
= CGit::GetFileModifyTime(CombinePath(gitdir
, path
), &time
, &isDir
, &filesize
, &isSymlink
);
329 if (!isDir
|| (isSymlink
&& (m_iIndexCaps
& GIT_INDEX_CAPABILITY_NO_SYMLINKS
) != GIT_INDEX_CAPABILITY_NO_SYMLINKS
))
330 return GetFileStatus(gitdir
, path
, status
, time
, filesize
, isSymlink
, pHash
);
332 if (CStringUtils::EndsWith(path
, L
'/'))
334 size_t index
= SearchInSortVector(*this, path
, -1, IsIgnoreCase());
337 status
.status
= git_wc_status_unversioned
;
345 *pHash
= (*this)[index
].m_IndexHash
;
348 status
.status
= git_wc_status_normal
;
350 status
.status
= git_wc_status_deleted
;
354 // we get here for symlinks which are handled as files inside the git index
355 if ((m_iIndexCaps
& GIT_INDEX_CAPABILITY_NO_SYMLINKS
) != GIT_INDEX_CAPABILITY_NO_SYMLINKS
)
356 return GetFileStatus(gitdir
, path
, status
, time
, filesize
, isSymlink
, pHash
);
358 // we should never get here
359 status
.status
= git_wc_status_unversioned
;
364 // This method is assumed to be called with m_SharedMutex locked.
365 int CGitHeadFileList::GetPackRef(const CString
&gitdir
)
367 CString PackRef
= g_AdminDirMap
.GetAdminDirConcat(gitdir
, L
"packed-refs");
369 __int64 mtime
= 0, packsize
= -1;
370 if (CGit::GetFileModifyTime(PackRef
, &mtime
, nullptr, &packsize
))
372 //packed refs is not existed
373 this->m_PackRefFile
.Empty();
374 this->m_PackRefMap
.clear();
377 else if (mtime
== m_LastModifyTimePackRef
&& packsize
== m_LastFileSizePackRef
)
381 this->m_PackRefFile
= PackRef
;
382 this->m_LastModifyTimePackRef
= mtime
;
383 this->m_LastFileSizePackRef
= packsize
;
386 m_PackRefMap
.clear();
388 CAutoFile hfile
= CreateFile(PackRef
,
390 FILE_SHARE_READ
| FILE_SHARE_DELETE
| FILE_SHARE_WRITE
,
393 FILE_ATTRIBUTE_NORMAL
,
399 LARGE_INTEGER fileSize
;
400 if (!::GetFileSizeEx(hfile
, &fileSize
) || fileSize
.QuadPart
>= INT_MAX
)
404 auto buff
= std::unique_ptr
<char[]>(new (std::nothrow
) char[fileSize
.LowPart
]); // prevent default initialization and throwing on allocation error
408 if (!ReadFile(hfile
, buff
.get(), fileSize
.LowPart
, &size
, nullptr))
411 if (size
!= fileSize
.LowPart
)
414 for (DWORD i
= 0; i
< fileSize
.LowPart
;)
418 if (buff
[i
] == '#' || buff
[i
] == '^')
420 while (buff
[i
] != '\n')
423 if (i
== fileSize
.LowPart
)
429 if (i
>= fileSize
.LowPart
)
432 while (buff
[i
] != ' ')
434 hash
.AppendChar(buff
[i
]);
436 if (i
== fileSize
.LowPart
)
441 if (i
>= fileSize
.LowPart
)
444 while (buff
[i
] != '\n')
446 ref
.AppendChar(buff
[i
]);
448 if (i
== fileSize
.LowPart
)
453 m_PackRefMap
[ref
] = CGitHash::FromHexStrTry(hash
);
455 while (buff
[i
] == '\n')
458 if (i
== fileSize
.LowPart
)
464 int CGitHeadFileList::ReadHeadHash(const CString
& gitdir
)
466 ATLASSERT(m_Gitdir
.IsEmpty() && m_HeadFile
.IsEmpty() && m_Head
.IsEmpty());
468 m_Gitdir
= g_AdminDirMap
.GetWorktreeAdminDir(gitdir
);
470 m_HeadFile
= m_Gitdir
;
471 m_HeadFile
+= L
"HEAD";
473 if (CGit::GetFileModifyTime(m_HeadFile
, &m_LastModifyTimeHead
, nullptr, &m_LastFileSizeHead
))
476 CAutoFile hfile
= CreateFile(m_HeadFile
,
478 FILE_SHARE_READ
| FILE_SHARE_DELETE
| FILE_SHARE_WRITE
,
481 FILE_ATTRIBUTE_NORMAL
,
488 unsigned char buffer
[2 * GIT_HASH_SIZE
];
489 ReadFile(hfile
, buffer
, static_cast<DWORD
>(strlen("ref:")), &size
, nullptr);
490 if (size
!= strlen("ref:"))
492 buffer
[strlen("ref:")] = '\0';
493 if (strcmp(reinterpret_cast<const char*>(buffer
), "ref:") == 0)
495 m_HeadRefFile
.Empty();
496 LARGE_INTEGER fileSize
;
497 if (!::GetFileSizeEx(hfile
, &fileSize
) || fileSize
.QuadPart
< static_cast<int>(strlen("ref:") + 1) || fileSize
.QuadPart
>= 100 * 1024 * 1024)
501 auto p
= std::unique_ptr
<char[]>(new (std::nothrow
) char[fileSize
.LowPart
- strlen("ref:")]); // prevent default initialization and throwing on allocation error
505 if (!ReadFile(hfile
, p
.get(), fileSize
.LowPart
- static_cast<DWORD
>(strlen("ref:")), &size
, nullptr))
507 CGit::StringAppend(m_HeadRefFile
, p
.get(), CP_UTF8
, fileSize
.LowPart
- static_cast<int>(strlen("ref:")));
509 CString ref
= m_HeadRefFile
.Trim();
511 ref
= ref
.Tokenize(L
"\n", start
);
512 m_HeadRefFile
= g_AdminDirMap
.GetAdminDir(gitdir
) + m_HeadRefFile
;
513 m_HeadRefFile
.Replace(L
'/', L
'\\');
516 if (CGit::GetFileModifyTime(m_HeadRefFile
, &time
, nullptr))
518 if (GetPackRef(gitdir
))
520 if (m_PackRefMap
.find(ref
) != m_PackRefMap
.end())
522 m_bRefFromPackRefFile
= true;
523 m_Head
= m_PackRefMap
[ref
];
533 CAutoFile href
= CreateFile(m_HeadRefFile
,
535 FILE_SHARE_READ
| FILE_SHARE_DELETE
| FILE_SHARE_WRITE
,
538 FILE_ATTRIBUTE_NORMAL
,
543 if (GetPackRef(gitdir
))
546 if (m_PackRefMap
.find(ref
) == m_PackRefMap
.end())
549 m_bRefFromPackRefFile
= true;
550 m_Head
= m_PackRefMap
[ref
];
554 ReadFile(href
, buffer
, 2 * GIT_HASH_SIZE
, &size
, nullptr);
555 if (size
!= 2 * GIT_HASH_SIZE
)
558 m_Head
= CGitHash::FromHexStr(reinterpret_cast<const char*>(buffer
));
560 m_LastModifyTimeRef
= time
;
565 ReadFile(hfile
, buffer
+ static_cast<DWORD
>(strlen("ref:")), 2 * GIT_HASH_SIZE
- static_cast<DWORD
>(strlen("ref:")), &size
, nullptr);
566 if (size
!= 2 * GIT_HASH_SIZE
- static_cast<DWORD
>(strlen("ref:")))
569 m_HeadRefFile
.Empty();
571 m_Head
= CGitHash::FromHexStr(reinterpret_cast<const char*>(buffer
));
576 bool CGitHeadFileList::CheckHeadUpdate() const
578 if (this->m_HeadFile
.IsEmpty())
581 __int64 mtime
= 0, size
= -1;
583 if (CGit::GetFileModifyTime(m_HeadFile
, &mtime
, nullptr, &size
))
586 if (mtime
!= m_LastModifyTimeHead
|| size
!= m_LastFileSizeHead
)
589 if (!this->m_HeadRefFile
.IsEmpty())
591 // we need to check for the HEAD ref file here, because the original ref might have come from packedrefs and now is a ref-file
592 if (CGit::GetFileModifyTime(m_HeadRefFile
, &mtime
))
594 if (!m_bRefFromPackRefFile
)
597 } else if (mtime
!= this->m_LastModifyTimeRef
)
601 if (m_bRefFromPackRefFile
&& !m_PackRefFile
.IsEmpty())
604 if (CGit::GetFileModifyTime(m_PackRefFile
, &mtime
, nullptr, &size
))
607 if (mtime
!= m_LastModifyTimePackRef
|| size
!= m_LastFileSizePackRef
)
611 // in an empty repo HEAD points to refs/heads/master, but this ref doesn't exist.
612 // So we need to retry again and again until the ref exists - otherwise we will never notice
613 if (this->m_Head
.IsEmpty() && this->m_HeadRefFile
.IsEmpty() && this->m_PackRefFile
.IsEmpty())
619 int CGitHeadFileList::ReadTreeRecursive(git_repository
& repo
, const git_tree
* tree
, const CString
& base
)
621 #define S_IFGITLINK 0160000
622 size_t count
= git_tree_entrycount(tree
);
623 for (size_t i
= 0; i
< count
; ++i
)
625 const git_tree_entry
*entry
= git_tree_entry_byindex(tree
, i
);
628 const int mode
= git_tree_entry_filemode(entry
);
629 const bool isDir
= (mode
& S_IFDIR
) == S_IFDIR
;
630 const bool isSubmodule
= (mode
& S_IFMT
) == S_IFGITLINK
;
631 if (!isDir
|| isSubmodule
)
634 item
.m_Hash
= git_tree_entry_id(entry
);
635 item
.m_FileName
= base
;
636 CGit::StringAppend(item
.m_FileName
, git_tree_entry_name(entry
), CP_UTF8
);
638 item
.m_FileName
+= L
'/';
644 git_tree_entry_to_object(object
.GetPointer(), &repo
, entry
);
647 CString parent
= base
;
648 CGit::StringAppend(parent
, git_tree_entry_name(entry
));
650 ReadTreeRecursive(repo
, reinterpret_cast<git_tree
*>(static_cast<git_object
*>(object
)), parent
);
656 // ReadTree is/must only be executed on an empty list
657 int CGitHeadFileList::ReadTree(bool ignoreCase
)
662 if (m_Head
.IsEmpty())
665 CAutoRepository
repository(m_Gitdir
);
668 bool ret
= repository
;
669 ret
= ret
&& !git_commit_lookup(commit
.GetPointer(), repository
, m_Head
);
670 ret
= ret
&& !git_commit_tree(tree
.GetPointer(), commit
);
673 ret
= ret
&& !ReadTreeRecursive(*repository
, tree
, L
"");
675 catch (const std::bad_alloc
& ex
)
677 CTraceToOutputDebugString::Instance()(__FUNCTION__
": Catched exception inside ReadTreeRecursive: %s\n", ex
.what());
680 catch (const std::length_error
& ex
)
682 CTraceToOutputDebugString::Instance()(__FUNCTION__
": Catched exception inside ReadTreeRecursive, length_error: %s\n", ex
.what());
688 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Could not open git repository in %s and read HEAD commit %s: %s\n", static_cast<LPCWSTR
>(m_Gitdir
), static_cast<LPCWSTR
>(m_Head
.ToString()), static_cast<LPCWSTR
>(CGit::GetLibGit2LastErr()));
689 m_LastModifyTimeHead
= 0;
690 m_LastFileSizeHead
= -1;
694 DoSortFilenametSortVector(*this, ignoreCase
);
696 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Reloaded HEAD tree (commit is %s) for repo: %s\n", static_cast<LPCWSTR
>(m_Head
.ToString()), static_cast<LPCWSTR
>(m_Gitdir
));
700 int CGitIgnoreItem::FetchIgnoreList(const CString
& projectroot
, const CString
& file
, bool isGlobal
, int* ignoreCase
)
702 if (this->m_pExcludeList
)
704 git_free_exclude_list(m_pExcludeList
);
705 m_pExcludeList
= nullptr;
709 this->m_BaseDir
.Empty();
712 CString base
= file
.Mid(projectroot
.GetLength() + 1);
713 base
.Replace(L
'\\', L
'/');
715 int start
= base
.ReverseFind(L
'/');
718 base
.Truncate(start
);
719 this->m_BaseDir
= CUnicodeUtils::GetUTF8(base
) + "/";
723 if (CGit::GetFileModifyTime(file
, &m_LastModifyTime
, nullptr, &m_LastFileSize
))
726 CAutoFile hfile
= CreateFile(file
,
728 FILE_SHARE_READ
| FILE_SHARE_DELETE
| FILE_SHARE_WRITE
,
731 FILE_ATTRIBUTE_NORMAL
,
737 LARGE_INTEGER fileSize
;
738 if (!::GetFileSizeEx(hfile
, &fileSize
) || fileSize
.QuadPart
>= INT_MAX
)
741 m_buffer
= std::unique_ptr
<char[]>(new (std::nothrow
) char[fileSize
.LowPart
+ 1]); // prevent default initialization and throwing on allocation error
746 if (!ReadFile(hfile
, m_buffer
.get(), fileSize
.LowPart
, &size
, nullptr))
751 m_buffer
[size
] = '\0';
753 if (git_create_exclude_list(&m_pExcludeList
))
759 m_iIgnoreCase
= ignoreCase
;
761 const char *p
= m_buffer
.get();
763 for (DWORD i
= 0; i
< size
; ++i
)
765 if (m_buffer
[i
] == '\n' || m_buffer
[i
] == '\r' || i
== (size
- 1))
767 if (m_buffer
[i
] == '\n' || m_buffer
[i
] == '\r')
770 if (p
[0] != '#' && p
[0])
771 git_add_exclude(p
, m_BaseDir
, m_BaseDir
.GetLength(), m_pExcludeList
, ++line
);
773 p
= m_buffer
.get() + i
+ 1;
779 git_free_exclude_list(m_pExcludeList
);
780 m_pExcludeList
= nullptr;
787 #ifdef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
788 int CGitIgnoreItem::IsPathIgnored(const CStringA
& patha
, int& type
)
790 int pos
= patha
.ReverseFind('/');
791 const char* base
= (pos
>= 0) ? (static_cast<const char*>(patha
) + pos
+ 1) : static_cast<const char*>(patha
);
793 return IsPathIgnored(patha
, base
, type
);
797 int CGitIgnoreItem::IsPathIgnored(const CStringA
& patha
, const char* base
, int& type
)
800 return -1; // error or undecided
802 return git_check_excluded_1(patha
, patha
.GetLength(), base
, &type
, m_pExcludeList
, m_iIgnoreCase
? *m_iIgnoreCase
: 1);
805 bool CGitIgnoreList::CheckFileChanged(const CString
&path
)
807 __int64 time
= 0, size
= -1;
809 const int ret
= CGit::GetFileModifyTime(path
, &time
, nullptr, &size
);
813 CAutoReadLock
lock(m_SharedMutex
);
814 cacheExist
= (m_Map
.find(path
) != m_Map
.end());
817 if (!cacheExist
&& ret
== 0)
819 CAutoWriteLock
lock(m_SharedMutex
);
820 m_Map
[path
].m_LastModifyTime
= 0;
821 m_Map
[path
].m_LastFileSize
= -1;
823 // both cache and file is not exist
824 if ((ret
!= 0) && (!cacheExist
))
827 // file exist but cache miss
828 if ((ret
== 0) && (!cacheExist
))
831 // file not exist but cache exist
832 if ((ret
!= 0) && (cacheExist
))
834 // file exist and cache exist
837 CAutoReadLock
lock(m_SharedMutex
);
838 if (m_Map
[path
].m_LastModifyTime
== time
&& m_Map
[path
].m_LastFileSize
== size
)
844 int CGitIgnoreList::FetchIgnoreFile(const CString
&gitdir
, const CString
&gitignore
, bool isGlobal
)
846 if (CGit::GitPathFileExists(gitignore
)) //if .gitignore remove, we need remote cache
848 CAutoWriteLock
lock(m_SharedMutex
);
849 m_Map
[gitignore
].FetchIgnoreList(gitdir
, gitignore
, isGlobal
, &m_IgnoreCase
[g_AdminDirMap
.GetAdminDir(gitdir
)]);
853 CAutoWriteLock
lock(m_SharedMutex
);
854 m_Map
.erase(gitignore
);
859 bool CGitIgnoreList::CheckAndUpdateIgnoreFiles(const CString
& gitdir
, const CString
& path
, bool isDir
, std::set
<CString
>* lastChecked
)
861 CString
temp(gitdir
);
865 temp
.Replace(L
'/', L
'\\');
869 const int x
= temp
.ReverseFind(L
'\\');
874 bool updated
= false;
875 while (!temp
.IsEmpty())
879 if (lastChecked
->find(temp
) != lastChecked
->end())
881 lastChecked
->insert(temp
);
884 temp
+= L
"\\.gitignore";
886 if (CheckFileChanged(temp
))
888 FetchIgnoreFile(gitdir
, temp
, false);
892 temp
.Truncate(temp
.GetLength() - static_cast<int>(wcslen(L
"\\.gitignore")));
893 if (CPathUtils::ArePathStringsEqual(temp
, gitdir
))
895 CString adminDir
= g_AdminDirMap
.GetAdminDir(temp
);
896 CString wcglobalgitignore
= adminDir
+ L
"info\\exclude";
897 if (CheckFileChanged(wcglobalgitignore
))
899 FetchIgnoreFile(gitdir
, wcglobalgitignore
, true);
903 if (CheckAndUpdateCoreExcludefile(adminDir
))
905 CString excludesFile
;
907 CAutoReadLock
lock(m_SharedMutex
);
908 excludesFile
= m_CoreExcludesfiles
[adminDir
];
910 if (!excludesFile
.IsEmpty())
912 FetchIgnoreFile(gitdir
, excludesFile
, true);
920 const int i
= temp
.ReverseFind(L
'\\');
921 temp
.Truncate(max(0, i
));
926 bool CGitIgnoreList::CheckAndUpdateGitSystemConfigPath(bool force
)
928 // recheck every 30 seconds
929 if (GetTickCount64() - m_dGitSystemConfigPathLastChecked
> 30000UL || force
)
931 m_dGitSystemConfigPathLastChecked
= GetTickCount64();
932 CString
gitSystemConfigPath(CRegString(REG_SYSTEM_GITCONFIGPATH
, L
"", FALSE
));
933 if (gitSystemConfigPath
!= m_sGitSystemConfigPath
)
935 m_sGitSystemConfigPath
= gitSystemConfigPath
;
941 bool CGitIgnoreList::CheckAndUpdateCoreExcludefile(const CString
&adminDir
)
943 CString
projectConfig(adminDir
);
944 projectConfig
+= L
"config";
945 CString globalConfig
= g_Git
.GetGitGlobalConfig();
946 CString globalXDGConfig
= g_Git
.GetGitGlobalXDGConfig();
948 CAutoWriteLock
lock(m_coreExcludefilesSharedMutex
);
949 bool hasChanged
= CheckAndUpdateGitSystemConfigPath();
950 hasChanged
= hasChanged
|| CheckFileChanged(projectConfig
);
951 hasChanged
= hasChanged
|| CheckFileChanged(globalConfig
);
952 hasChanged
= hasChanged
|| CheckFileChanged(globalXDGConfig
);
953 if (!m_sGitSystemConfigPath
.IsEmpty())
954 hasChanged
= hasChanged
|| CheckFileChanged(m_sGitSystemConfigPath
);
956 CString excludesFile
;
958 CAutoReadLock
lock2(m_SharedMutex
);
959 excludesFile
= m_CoreExcludesfiles
[adminDir
];
961 if (!excludesFile
.IsEmpty())
962 hasChanged
= hasChanged
|| CheckFileChanged(excludesFile
);
967 CAutoConfig
config(true);
968 CAutoRepository
repo(adminDir
);
969 git_config_add_file_ondisk(config
, CGit::GetGitPathStringA(projectConfig
), GIT_CONFIG_LEVEL_LOCAL
, repo
, FALSE
);
970 git_config_add_file_ondisk(config
, CGit::GetGitPathStringA(globalConfig
), GIT_CONFIG_LEVEL_GLOBAL
, repo
, FALSE
);
971 git_config_add_file_ondisk(config
, CGit::GetGitPathStringA(globalXDGConfig
), GIT_CONFIG_LEVEL_XDG
, repo
, FALSE
);
972 if (!m_sGitSystemConfigPath
.IsEmpty())
973 git_config_add_file_ondisk(config
, CGit::GetGitPathStringA(m_sGitSystemConfigPath
), GIT_CONFIG_LEVEL_SYSTEM
, repo
, FALSE
);
975 config
.GetString(L
"core.excludesfile", excludesFile
);
976 if (excludesFile
.IsEmpty())
977 excludesFile
= GetWindowsHome() + L
"\\.config\\git\\ignore";
978 else if (CStringUtils::StartsWith(excludesFile
, L
"~/"))
979 excludesFile
= GetWindowsHome() + excludesFile
.Mid(static_cast<int>(wcslen(L
"~")));
981 CAutoWriteLock
lockMap(m_SharedMutex
);
982 m_IgnoreCase
[adminDir
] = 1;
983 config
.GetBOOL(L
"core.ignorecase", m_IgnoreCase
[adminDir
]);
984 CGit::GetFileModifyTime(projectConfig
, &m_Map
[projectConfig
].m_LastModifyTime
, nullptr, &m_Map
[projectConfig
].m_LastFileSize
);
985 CGit::GetFileModifyTime(globalXDGConfig
, &m_Map
[globalXDGConfig
].m_LastModifyTime
, nullptr, &m_Map
[globalXDGConfig
].m_LastFileSize
);
986 if (m_Map
[globalXDGConfig
].m_LastModifyTime
== 0)
987 m_Map
.erase(globalXDGConfig
);
988 CGit::GetFileModifyTime(globalConfig
, &m_Map
[globalConfig
].m_LastModifyTime
, nullptr, &m_Map
[globalConfig
].m_LastFileSize
);
989 if (m_Map
[globalConfig
].m_LastModifyTime
== 0)
990 m_Map
.erase(globalConfig
);
991 if (!m_sGitSystemConfigPath
.IsEmpty())
992 CGit::GetFileModifyTime(m_sGitSystemConfigPath
, &m_Map
[m_sGitSystemConfigPath
].m_LastModifyTime
, nullptr, &m_Map
[m_sGitSystemConfigPath
].m_LastFileSize
);
993 if (m_Map
[m_sGitSystemConfigPath
].m_LastModifyTime
== 0 || m_sGitSystemConfigPath
.IsEmpty())
994 m_Map
.erase(m_sGitSystemConfigPath
);
995 m_CoreExcludesfiles
[adminDir
] = excludesFile
;
999 const CString
CGitIgnoreList::GetWindowsHome()
1001 static CString
sWindowsHome(g_Git
.GetHomeDirectory());
1002 return sWindowsHome
;
1004 bool CGitIgnoreList::IsIgnore(CString str
, const CString
& projectroot
, bool isDir
, const CString
& adminDir
)
1006 str
.Replace(L
'\\', L
'/');
1008 if (!str
.IsEmpty() && str
[str
.GetLength() - 1] == L
'/')
1009 str
.Truncate(str
.GetLength() - 1);
1011 int ret
= CheckIgnore(str
, projectroot
, isDir
, adminDir
);
1014 int start
= str
.ReverseFind(L
'/');
1018 str
.Truncate(start
);
1019 ret
= CheckIgnore(str
, projectroot
, TRUE
, adminDir
);
1024 int CGitIgnoreList::CheckFileAgainstIgnoreList(const CString
&ignorefile
, const CStringA
&patha
, const char * base
, int &type
)
1026 if (m_Map
.find(ignorefile
) == m_Map
.end())
1027 return -1; // error or undecided
1029 return (m_Map
[ignorefile
].IsPathIgnored(patha
, base
, type
));
1031 int CGitIgnoreList::CheckIgnore(const CString
&path
, const CString
&projectroot
, bool isDir
, const CString
& adminDir
)
1033 CString temp
= CombinePath(projectroot
, path
);
1034 temp
.Replace(L
'/', L
'\\');
1036 CStringA patha
= CUnicodeUtils::GetUTF8(path
);
1037 patha
.Replace('\\', '/');
1044 // strip directory name
1045 // we do not need to check for a .ignore file inside a directory we might ignore
1046 const int i
= temp
.ReverseFind(L
'\\');
1054 int x
= temp
.ReverseFind(L
'\\');
1059 int pos
= patha
.ReverseFind('/');
1060 const char* base
= (pos
>= 0) ? (static_cast<const char*>(patha
) + pos
+ 1) : static_cast<const char*>(patha
);
1063 CAutoReadLock
lock(m_SharedMutex
);
1064 while (!temp
.IsEmpty())
1066 temp
+= L
"\\.gitignore";
1068 if (auto ret
= CheckFileAgainstIgnoreList(temp
, patha
, base
, type
); ret
!= -1)
1071 temp
.Truncate(temp
.GetLength() - static_cast<int>(wcslen(L
"\\.gitignore")));
1073 if (CPathUtils::ArePathStringsEqual(temp
, projectroot
))
1075 CString wcglobalgitignore
= adminDir
;
1076 wcglobalgitignore
+= L
"info\\exclude";
1077 if (auto ret
= CheckFileAgainstIgnoreList(wcglobalgitignore
, patha
, base
, type
); ret
!= -1)
1080 CString excludesFile
= m_CoreExcludesfiles
[adminDir
];
1081 if (!excludesFile
.IsEmpty())
1082 return CheckFileAgainstIgnoreList(excludesFile
, patha
, base
, type
);
1087 const int i
= temp
.ReverseFind(L
'\\');
1088 temp
.Truncate(max(0, i
));
1094 SHARED_TREE_PTR
CGitHeadFileMap::CheckHeadAndUpdate(const CString
& gitdir
, bool ignoreCase
)
1096 if (auto ptr
= this->SafeGet(gitdir
); ptr
.get() && !ptr
->CheckHeadUpdate())
1099 auto newPtr
= std::make_shared
<CGitHeadFileList
>();
1100 if (newPtr
->ReadHeadHash(gitdir
) || newPtr
->ReadTree(ignoreCase
))
1106 this->SafeSet(gitdir
, newPtr
);