1
// TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2017-2021 - TortoiseGit
4 // Copyright (C) 2003-2016 - TortoiseSVN
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 #include "StringUtils.h"
25 #include "UnicodeUtils.h"
28 #include "git2/sys/repository.h"
29 #include "scope_exit_noexcept.h"
31 void LoadIgnorePatterns(const char* wc
, GitWCRev_t
* GitStat
)
33 std::string path
= wc
;
34 std::string ignorepath
= path
+ "/.GitWCRevignore";
37 infile
.open(ignorepath
);
42 while (std::getline(infile
, line
))
47 line
.insert(line
.begin(), '!');
48 GitStat
->ignorepatterns
.emplace(line
);
52 static std::wstring
GetHomePath()
55 if ((tmp
= _wgetenv(L
"HOME")) != nullptr && *tmp
)
58 if ((tmp
= _wgetenv(L
"HOMEDRIVE")) != nullptr)
60 std::wstring
home(tmp
);
61 if ((tmp
= _wgetenv(L
"HOMEPATH")) != nullptr)
64 if (PathIsDirectory(home
.c_str()))
69 if ((tmp
= _wgetenv(L
"USERPROFILE")) != nullptr && *tmp
)
75 static int is_cygwin_msys2_hack_active()
78 DWORD dwType
= REG_DWORD
;
80 DWORD dwSize
= sizeof(dwValue
);
81 if (RegOpenKeyExW(HKEY_CURRENT_USER
, L
"Software\\TortoiseGit", 0, KEY_ALL_ACCESS
, &hKey
) == ERROR_SUCCESS
)
83 RegQueryValueExW(hKey
, L
"CygwinHack", nullptr, &dwType
, reinterpret_cast<LPBYTE
>(&dwValue
), &dwSize
);
85 RegQueryValueExW(hKey
, L
"Msys2Hack", nullptr, &dwType
, reinterpret_cast<LPBYTE
>(&dwValue
), &dwSize
);
91 static std::wstring
GetProgramDataConfig()
93 // do not use shared windows-wide system config when cygwin hack is active
94 if (is_cygwin_msys2_hack_active())
97 // Git >= 2.24 doesn't use ProgramData any more
98 if (CRegStdDWORD(L
"Software\\TortoiseGit\\git_cached_version", (2 << 24 | 24 << 16)) >= (2 << 24 | 24 << 16))
102 if (SHGetKnownFolderPath(FOLDERID_ProgramData
, 0, nullptr, &pszPath
) != S_OK
)
105 SCOPE_EXIT
{ CoTaskMemFree(pszPath
); };
107 if (wcslen(pszPath
) >= MAX_PATH
- wcslen(L
"\\Git\\config"))
110 return std::wstring(pszPath
) + L
"\\Git\\config";
113 static std::wstring
GetSystemGitConfig()
116 DWORD dwType
= REG_SZ
;
117 wchar_t path
[MAX_PATH
] = { 0 };
118 DWORD dwSize
= _countof(path
) - 1;
119 if (RegOpenKeyExW(HKEY_CURRENT_USER
, L
"Software\\TortoiseGit", 0, KEY_ALL_ACCESS
, &hKey
) == ERROR_SUCCESS
)
121 RegQueryValueExW(hKey
, L
"SystemConfig", nullptr, &dwType
, reinterpret_cast<LPBYTE
>(&path
), &dwSize
);
127 static int RepoStatus(const wchar_t* path
, std::string pathA
, git_repository
* repo
, GitWCRev_t
& GitStat
)
129 git_status_options git_status_options
= GIT_STATUS_OPTIONS_INIT
;
130 git_status_options
.flags
= GIT_STATUS_OPT_INCLUDE_UNTRACKED
| GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS
;
131 if (GitStat
.bNoSubmodules
)
132 git_status_options
.flags
|= GIT_STATUS_OPT_EXCLUDE_SUBMODULES
;
134 std::string
workdir(git_repository_workdir(repo
));
135 std::transform(pathA
.begin(), pathA
.end(), pathA
.begin(), [](char c
) { return (c
== '\\') ? '/' : c
; });
136 pathA
.erase(pathA
.begin(), pathA
.begin() + min(workdir
.length(), pathA
.length())); // workdir always ends with a slash, however, wcA is not guaranteed to
137 LoadIgnorePatterns(workdir
.c_str(), &GitStat
);
139 std::vector
<const char*> pathspec
;
142 pathspec
.emplace_back(pathA
.c_str());
143 git_status_options
.pathspec
.count
= 1;
145 if (!GitStat
.ignorepatterns
.empty())
147 std::transform(GitStat
.ignorepatterns
.cbegin(), GitStat
.ignorepatterns
.cend(), std::back_inserter(pathspec
), [](auto& pattern
) { return pattern
.c_str(); });
148 git_status_options
.pathspec
.count
+= GitStat
.ignorepatterns
.size();
151 pathspec
.push_back("*");
152 git_status_options
.pathspec
.count
+= 1;
155 if (git_status_options
.pathspec
.count
> 0)
156 git_status_options
.pathspec
.strings
= const_cast<char**>(pathspec
.data());
158 CAutoStatusList status
;
159 if (git_status_list_new(status
.GetPointer(), repo
, &git_status_options
) < 0)
162 for (size_t i
= 0, maxi
= git_status_list_entrycount(status
); i
< maxi
; ++i
)
164 const git_status_entry
* s
= git_status_byindex(status
, i
);
165 if (s
->index_to_workdir
&& s
->index_to_workdir
->new_file
.mode
== GIT_FILEMODE_COMMIT
)
167 GitStat
.bHasSubmodule
= TRUE
;
168 unsigned int smstatus
= 0;
169 if (!git_submodule_status(&smstatus
, repo
, s
->index_to_workdir
->new_file
.path
, GIT_SUBMODULE_IGNORE_UNSPECIFIED
))
171 if (smstatus
& GIT_SUBMODULE_STATUS_WD_MODIFIED
) // HEAD of submodule not matching
172 GitStat
.bHasSubmoduleNewCommits
= TRUE
;
173 else if ((smstatus
& GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED
) || (smstatus
& GIT_SUBMODULE_STATUS_WD_WD_MODIFIED
))
174 GitStat
.bHasSubmoduleMods
= TRUE
;
175 else if (smstatus
& GIT_SUBMODULE_STATUS_WD_UNTRACKED
)
176 GitStat
.bHasSubmoduleUnversioned
= TRUE
;
180 if (s
->status
== GIT_STATUS_CURRENT
)
182 if (s
->status
== GIT_STATUS_WT_NEW
|| s
->status
== GIT_STATUS_INDEX_NEW
)
183 GitStat
.HasUnversioned
= TRUE
;
185 GitStat
.HasMods
= TRUE
;
188 if (pathA
.empty()) // working tree root is always versioned
190 GitStat
.bIsGitItem
= TRUE
;
193 else if (PathIsDirectory(path
)) // directories are unversioned in Git
195 GitStat
.bIsGitItem
= FALSE
;
198 unsigned int status_flags
= 0;
199 int ret
= git_status_file(&status_flags
, repo
, pathA
.c_str());
200 GitStat
.bIsGitItem
= (ret
== GIT_OK
&& !(status_flags
& (GIT_STATUS_WT_NEW
| GIT_STATUS_IGNORED
| GIT_STATUS_INDEX_NEW
)));
204 int GetStatusUnCleanPath(const wchar_t* wcPath
, GitWCRev_t
& GitStat
)
206 DWORD reqLen
= GetFullPathName(wcPath
, 0, nullptr, nullptr);
207 auto wcfullPath
= std::make_unique
<wchar_t[]>(reqLen
+ 1);
208 GetFullPathName(wcPath
, reqLen
, wcfullPath
.get(), nullptr);
209 // GetFullPathName() sometimes returns the full path with the wrong
210 // case. This is not a problem on Windows since its filesystem is
211 // case-insensitive. But for Git that's a problem if the wrong case
212 // is inside a working copy: the git index is case sensitive.
213 // To fix the casing of the path, we use a trick:
214 // convert the path to its short form, then back to its long form.
215 // That will fix the wrong casing of the path.
216 int shortlen
= GetShortPathName(wcfullPath
.get(), nullptr, 0);
219 auto shortPath
= std::make_unique
<wchar_t[]>(shortlen
+ 1);
220 if (GetShortPathName(wcfullPath
.get(), shortPath
.get(), shortlen
+ 1))
222 reqLen
= GetLongPathName(shortPath
.get(), nullptr, 0);
223 wcfullPath
= std::make_unique
<wchar_t[]>(reqLen
+ 1);
224 GetLongPathName(shortPath
.get(), wcfullPath
.get(), reqLen
);
227 return GetStatus(wcfullPath
.get(), GitStat
);
230 int GetStatus(const wchar_t* path
, GitWCRev_t
& GitStat
)
232 // Configure libgit2 search paths
233 std::wstring systemConfig
= GetSystemGitConfig();
234 if (!systemConfig
.empty())
235 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH
, GIT_CONFIG_LEVEL_SYSTEM
, CUnicodeUtils::StdGetUTF8(systemConfig
).c_str());
237 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH
, GIT_CONFIG_LEVEL_SYSTEM
, "");
238 std::string
home(CUnicodeUtils::StdGetUTF8(GetHomePath()));
239 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH
, GIT_CONFIG_LEVEL_GLOBAL
, (home
+ "\\.gitconfig").c_str());
240 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH
, GIT_CONFIG_LEVEL_XDG
, (home
+ "\\.config\\git\\config").c_str());
241 std::wstring programDataConfig
= GetProgramDataConfig();
242 if (!programDataConfig
.empty())
243 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH
, GIT_CONFIG_LEVEL_PROGRAMDATA
, CUnicodeUtils::StdGetUTF8(programDataConfig
).c_str());
245 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH
, GIT_CONFIG_LEVEL_PROGRAMDATA
, "");
247 std::string pathA
= CUnicodeUtils::StdGetUTF8(path
);
249 if (git_repository_discover(dotgitdir
, pathA
.c_str(), 0, nullptr) < 0)
252 CAutoRepository repo
;
253 if (git_repository_open(repo
.GetPointer(), dotgitdir
->ptr
))
256 if (git_repository_is_bare(repo
))
259 if (git_repository_head_unborn(repo
))
261 memset(GitStat
.HeadHash
, 0, sizeof(GitStat
.HeadHash
));
262 strncpy_s(GitStat
.HeadHashReadable
, GIT_OID_HEX_ZERO
, strlen(GIT_OID_HEX_ZERO
));
263 GitStat
.bIsUnborn
= TRUE
;
265 CAutoReference symbolicHead
;
266 if (git_reference_lookup(symbolicHead
.GetPointer(), repo
, "HEAD"))
268 auto branchName
= git_reference_symbolic_target(symbolicHead
);
269 if (CStringUtils::StartsWith(branchName
, "refs/heads/"))
270 branchName
+= strlen("refs/heads/");
271 else if (CStringUtils::StartsWith(branchName
, "refs/"))
272 branchName
+= strlen("refs/");
273 GitStat
.CurrentBranch
= branchName
;
275 return RepoStatus(path
, pathA
, repo
, GitStat
);
279 if (git_repository_head(head
.GetPointer(), repo
) < 0)
281 GitStat
.CurrentBranch
= git_reference_shorthand(head
);
284 if (git_reference_peel(object
.GetPointer(), head
, GIT_OBJECT_COMMIT
) < 0)
287 const git_oid
* oid
= git_object_id(object
);
288 git_oid_cpy(reinterpret_cast<git_oid
*>(GitStat
.HeadHash
), oid
);
289 git_oid_tostr(GitStat
.HeadHashReadable
, sizeof(GitStat
.HeadHashReadable
), oid
);
292 if (git_commit_lookup(commit
.GetPointer(), repo
, oid
) < 0)
295 const git_signature
* sig
= git_commit_author(commit
);
296 GitStat
.HeadTime
= sig
->when
.time
;
297 if (CRegStdDWORD(L
"Software\\TortoiseGit\\UseMailmap", TRUE
) == TRUE
)
299 CAutoMailmap mailmap
;
300 if (git_mailmap_from_repository(mailmap
.GetPointer(), repo
))
302 CAutoSignature resolvedSignature
;
303 if (git_mailmap_resolve_signature(resolvedSignature
.GetPointer(), mailmap
, sig
))
305 GitStat
.HeadAuthor
= (*resolvedSignature
).name
;
306 GitStat
.HeadEmail
= (*resolvedSignature
).email
;
310 GitStat
.HeadAuthor
= sig
->name
;
311 GitStat
.HeadEmail
= sig
->email
;
314 struct TagPayload
{ git_repository
* repo
; GitWCRev_t
& GitStat
; } tagpayload
= { repo
, GitStat
};
316 if (git_tag_foreach(repo
, [](const char*, git_oid
* tagoid
, void* payload
)
318 auto pl
= reinterpret_cast<struct TagPayload
*>(payload
);
319 if (git_oid_cmp(tagoid
, reinterpret_cast<git_oid
*>(pl
->GitStat
.HeadHash
)) == 0)
321 pl
->GitStat
.bIsTagged
= TRUE
;
326 if (git_tag_lookup(tag
.GetPointer(), pl
->repo
, tagoid
))
327 return 0; // not an annotated tag
328 CAutoObject tagObject
;
329 if (git_tag_peel(tagObject
.GetPointer(), tag
))
331 if (git_oid_cmp(git_object_id(tagObject
), reinterpret_cast<git_oid
*>(pl
->GitStat
.HeadHash
)) == 0)
332 pl
->GitStat
.bIsTagged
= TRUE
;
338 // count the first-parent revisions from HEAD to the first commit
340 if (git_revwalk_new(walker
.GetPointer(), repo
) < 0)
342 git_revwalk_simplify_first_parent(walker
);
343 if (git_revwalk_push_head(walker
) < 0)
346 while (!git_revwalk_next(&oidlog
, walker
))
347 ++GitStat
.NumCommits
;
349 return RepoStatus(path
, pathA
, repo
, GitStat
);