Fixed issue #4143: BrowseRefsDlg: Order the branches by commit date instead of date...
[TortoiseGit.git] / src / GitWCRev / status.cpp
blob07cac89d2882705b45638cdef641e627673e25a0
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2017-2023 - 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.
20 #include "stdafx.h"
21 #include "GitWCRev.h"
22 #include "status.h"
23 #include "registry.h"
24 #include "StringUtils.h"
25 #include "UnicodeUtils.h"
26 #include <fstream>
27 #include <ShlObj.h>
28 #include "git2/sys/repository.h"
29 #include <atlbase.h>
31 void LoadIgnorePatterns(const char* wc, GitWCRev_t* GitStat)
33 std::string path = wc;
34 std::string ignorepath = path + "/.GitWCRevignore";
36 std::ifstream infile;
37 infile.open(ignorepath);
38 if (!infile.good())
39 return;
41 std::string line;
42 while (std::getline(infile, line))
44 if (line.empty())
45 continue;
47 line.insert(line.begin(), '!');
48 GitStat->ignorepatterns.emplace(line);
52 static std::wstring GetHomePath()
54 wchar_t* tmp;
55 if ((tmp = _wgetenv(L"HOME")) != nullptr && *tmp)
56 return tmp;
58 if ((tmp = _wgetenv(L"HOMEDRIVE")) != nullptr)
60 std::wstring home(tmp);
61 if ((tmp = _wgetenv(L"HOMEPATH")) != nullptr)
63 home.append(tmp);
64 if (PathIsDirectory(home.c_str()))
65 return home;
69 if ((tmp = _wgetenv(L"USERPROFILE")) != nullptr && *tmp)
70 return tmp;
72 return {};
75 static int is_cygwin_msys2_hack_active()
77 HKEY hKey;
78 DWORD dwType = REG_DWORD;
79 DWORD dwValue = 0;
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);
84 if (dwValue != 1)
85 RegQueryValueExW(hKey, L"Msys2Hack", nullptr, &dwType, reinterpret_cast<LPBYTE>(&dwValue), &dwSize);
86 RegCloseKey(hKey);
88 return dwValue == 1;
91 static std::wstring GetSystemGitConfig()
93 HKEY hKey;
94 DWORD dwType = REG_SZ;
95 wchar_t path[MAX_PATH] = { 0 };
96 DWORD dwSize = _countof(path) - 1;
97 if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\TortoiseGit", 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS)
99 RegQueryValueExW(hKey, L"SystemConfig", nullptr, &dwType, reinterpret_cast<LPBYTE>(&path), &dwSize);
100 RegCloseKey(hKey);
101 std::wstring readPath{ path };
102 if (readPath.size() > wcslen(L"\\gitconfig"))
103 return readPath.substr(0, readPath.size() - wcslen(L"\\gitconfig"));
105 return path;
108 static int RepoStatus(const wchar_t* path, std::string pathA, git_repository* repo, GitWCRev_t& GitStat)
110 git_status_options git_status_options = GIT_STATUS_OPTIONS_INIT;
111 git_status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
112 if (GitStat.bNoSubmodules)
113 git_status_options.flags |= GIT_STATUS_OPT_EXCLUDE_SUBMODULES;
115 std::string workdir(git_repository_workdir(repo));
116 std::transform(pathA.begin(), pathA.end(), pathA.begin(), [](char c) { return (c == '\\') ? '/' : c; });
117 pathA.erase(pathA.begin(), pathA.begin() + min(workdir.length(), pathA.length())); // workdir always ends with a slash, however, wcA is not guaranteed to
118 LoadIgnorePatterns(workdir.c_str(), &GitStat);
120 std::vector<const char*> pathspec;
121 if (!pathA.empty())
123 pathspec.emplace_back(pathA.c_str());
124 git_status_options.pathspec.count = 1;
126 if (!GitStat.ignorepatterns.empty())
128 std::transform(GitStat.ignorepatterns.cbegin(), GitStat.ignorepatterns.cend(), std::back_inserter(pathspec), [](auto& pattern) { return pattern.c_str(); });
129 git_status_options.pathspec.count += GitStat.ignorepatterns.size();
130 if (pathA.empty())
132 pathspec.push_back("*");
133 git_status_options.pathspec.count += 1;
136 if (git_status_options.pathspec.count > 0)
137 git_status_options.pathspec.strings = const_cast<char**>(pathspec.data());
139 CAutoStatusList status;
140 if (git_status_list_new(status.GetPointer(), repo, &git_status_options) < 0)
141 return ERR_GIT_ERR;
143 for (size_t i = 0, maxi = git_status_list_entrycount(status); i < maxi; ++i)
145 const git_status_entry* s = git_status_byindex(status, i);
146 if (s->index_to_workdir && s->index_to_workdir->new_file.mode == GIT_FILEMODE_COMMIT)
148 GitStat.bHasSubmodule = TRUE;
149 unsigned int smstatus = 0;
150 if (!git_submodule_status(&smstatus, repo, s->index_to_workdir->new_file.path, GIT_SUBMODULE_IGNORE_UNSPECIFIED))
152 if (smstatus & GIT_SUBMODULE_STATUS_WD_MODIFIED) // HEAD of submodule not matching
153 GitStat.bHasSubmoduleNewCommits = TRUE;
154 else if ((smstatus & GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED) || (smstatus & GIT_SUBMODULE_STATUS_WD_WD_MODIFIED))
155 GitStat.bHasSubmoduleMods = TRUE;
156 else if (smstatus & GIT_SUBMODULE_STATUS_WD_UNTRACKED)
157 GitStat.bHasSubmoduleUnversioned = TRUE;
159 continue;
161 if (s->status == GIT_STATUS_CURRENT)
162 continue;
163 if (s->status == GIT_STATUS_WT_NEW || s->status == GIT_STATUS_INDEX_NEW)
164 GitStat.HasUnversioned = TRUE;
165 else
166 GitStat.HasMods = TRUE;
169 if (pathA.empty()) // working tree root is always versioned
171 GitStat.bIsGitItem = TRUE;
172 return 0;
174 else if (PathIsDirectory(path)) // directories are unversioned in Git
176 GitStat.bIsGitItem = FALSE;
177 return 0;
179 unsigned int status_flags = 0;
180 int ret = git_status_file(&status_flags, repo, pathA.c_str());
181 GitStat.bIsGitItem = (ret == GIT_OK && !(status_flags & (GIT_STATUS_WT_NEW | GIT_STATUS_IGNORED | GIT_STATUS_INDEX_NEW)));
182 return 0;
185 int GetStatusUnCleanPath(const wchar_t* wcPath, GitWCRev_t& GitStat)
187 DWORD reqLen = GetFullPathName(wcPath, 0, nullptr, nullptr);
188 auto wcfullPath = std::make_unique<wchar_t[]>(reqLen + 1);
189 GetFullPathName(wcPath, reqLen, wcfullPath.get(), nullptr);
190 // GetFullPathName() sometimes returns the full path with the wrong
191 // case. This is not a problem on Windows since its filesystem is
192 // case-insensitive. But for Git that's a problem if the wrong case
193 // is inside a working copy: the git index is case sensitive.
194 // To fix the casing of the path, we use a trick:
195 // convert the path to its short form, then back to its long form.
196 // That will fix the wrong casing of the path.
197 int shortlen = GetShortPathName(wcfullPath.get(), nullptr, 0);
198 if (shortlen)
200 auto shortPath = std::make_unique<wchar_t[]>(shortlen + 1);
201 if (GetShortPathName(wcfullPath.get(), shortPath.get(), shortlen + 1))
203 reqLen = GetLongPathName(shortPath.get(), nullptr, 0);
204 wcfullPath = std::make_unique<wchar_t[]>(reqLen + 1);
205 GetLongPathName(shortPath.get(), wcfullPath.get(), reqLen);
208 return GetStatus(wcfullPath.get(), GitStat);
211 int GetStatus(const wchar_t* path, GitWCRev_t& GitStat)
213 // Configure libgit2 search paths
214 std::wstring systemConfig = GetSystemGitConfig();
215 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_SYSTEM, CUnicodeUtils::StdGetUTF8(systemConfig).c_str());
216 std::string home(CUnicodeUtils::StdGetUTF8(GetHomePath()));
217 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, home.c_str());
218 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_XDG, (home + "\\.config\\git").c_str());
219 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_PROGRAMDATA, L"");
221 std::string pathA = CUnicodeUtils::StdGetUTF8(path);
222 CAutoBuf dotgitdir;
223 if (git_repository_discover(dotgitdir, pathA.c_str(), 0, nullptr) < 0)
224 return ERR_NOWC;
226 CAutoRepository repo;
227 if (git_repository_open(repo.GetPointer(), dotgitdir->ptr))
228 return ERR_NOWC;
230 if (git_repository_is_bare(repo))
231 return ERR_NOWC;
233 if (int ret = git_repository_head_unborn(repo); ret < 0)
234 return ERR_GIT_ERR;
235 else if (ret == 1)
237 memset(GitStat.HeadHash, 0, sizeof(GitStat.HeadHash));
238 memset(GitStat.HeadHashReadable, '0', sizeof(GitStat.HeadHashReadable));
239 GitStat.HeadHashReadable[sizeof(GitStat.HeadHashReadable) - 1] = '\0';
240 GitStat.bIsUnborn = TRUE;
242 CAutoReference symbolicHead;
243 if (git_reference_lookup(symbolicHead.GetPointer(), repo, "HEAD"))
244 return ERR_GIT_ERR;
245 auto branchName = git_reference_symbolic_target(symbolicHead);
246 if (CStringUtils::StartsWith(branchName, "refs/heads/"))
247 branchName += strlen("refs/heads/");
248 else if (CStringUtils::StartsWith(branchName, "refs/"))
249 branchName += strlen("refs/");
250 GitStat.CurrentBranch = branchName;
252 return RepoStatus(path, pathA, repo, GitStat);
255 CAutoReference head;
256 if (git_repository_head(head.GetPointer(), repo) < 0)
257 return ERR_GIT_ERR;
258 GitStat.CurrentBranch = git_reference_shorthand(head);
260 CAutoObject object;
261 if (git_reference_peel(object.GetPointer(), head, GIT_OBJECT_COMMIT) < 0)
262 return ERR_GIT_ERR;
264 const git_oid* oid = git_object_id(object);
265 git_oid_cpy(reinterpret_cast<git_oid*>(GitStat.HeadHash), oid);
266 git_oid_tostr(GitStat.HeadHashReadable, sizeof(GitStat.HeadHashReadable), oid);
268 CAutoCommit commit;
269 if (git_commit_lookup(commit.GetPointer(), repo, oid) < 0)
270 return ERR_GIT_ERR;
272 const git_signature* sig = git_commit_author(commit);
273 GitStat.HeadTime = sig->when.time;
274 if (CRegStdDWORD(L"Software\\TortoiseGit\\UseMailmap", TRUE) == TRUE)
276 CAutoMailmap mailmap;
277 if (git_mailmap_from_repository(mailmap.GetPointer(), repo))
278 return ERR_GIT_ERR;
279 CAutoSignature resolvedSignature;
280 if (git_mailmap_resolve_signature(resolvedSignature.GetPointer(), mailmap, sig))
281 return ERR_GIT_ERR;
282 GitStat.HeadAuthor = (*resolvedSignature).name;
283 GitStat.HeadEmail = (*resolvedSignature).email;
285 else
287 GitStat.HeadAuthor = sig->name;
288 GitStat.HeadEmail = sig->email;
291 struct TagPayload { git_repository* repo; GitWCRev_t& GitStat; } tagpayload = { repo, GitStat };
293 if (git_tag_foreach(repo, [](const char*, git_oid* tagoid, void* payload)
295 auto pl = reinterpret_cast<struct TagPayload*>(payload);
296 if (git_oid_cmp(tagoid, reinterpret_cast<git_oid*>(pl->GitStat.HeadHash)) == 0)
298 pl->GitStat.bIsTagged = TRUE;
299 return 0;
302 CAutoTag tag;
303 if (git_tag_lookup(tag.GetPointer(), pl->repo, tagoid))
304 return 0; // not an annotated tag
305 CAutoObject tagObject;
306 if (git_tag_peel(tagObject.GetPointer(), tag))
307 return -1;
308 if (git_oid_cmp(git_object_id(tagObject), reinterpret_cast<git_oid*>(pl->GitStat.HeadHash)) == 0)
309 pl->GitStat.bIsTagged = TRUE;
311 return 0;
312 }, &tagpayload))
313 return ERR_GIT_ERR;
315 // count the first-parent revisions from HEAD to the first commit
316 CAutoRevwalk walker;
317 if (git_revwalk_new(walker.GetPointer(), repo) < 0)
318 return ERR_GIT_ERR;
319 git_revwalk_simplify_first_parent(walker);
320 if (git_revwalk_push_head(walker) < 0)
321 return ERR_GIT_ERR;
322 git_oid oidlog;
323 while (!git_revwalk_next(&oidlog, walker))
324 ++GitStat.NumCommits;
326 return RepoStatus(path, pathA, repo, GitStat);