Use STL algorithms more often
[TortoiseGit.git] / src / GitWCRev / status.cpp
blobe5e3611bb3fac162a8b077b01611266d7e2e20af
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2017-2018 - 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"
30 void LoadIgnorePatterns(const char* wc, GitWCRev_t* GitStat)
32 std::string path = wc;
33 std::string ignorepath = path + "/.GitWCRevignore";
35 std::ifstream infile;
36 infile.open(ignorepath);
37 if (!infile.good())
38 return;
40 std::string line;
41 while (std::getline(infile, line))
43 if (line.empty())
44 continue;
46 line.insert(line.begin(), '!');
47 GitStat->ignorepatterns.emplace(line);
51 static std::wstring GetHomePath()
53 wchar_t* tmp;
54 if ((tmp = _wgetenv(L"HOME")) != nullptr && *tmp)
55 return tmp;
57 if ((tmp = _wgetenv(L"HOMEDRIVE")) != nullptr)
59 std::wstring home(tmp);
60 if ((tmp = _wgetenv(L"HOMEPATH")) != nullptr)
62 home.append(tmp);
63 if (PathIsDirectory(home.c_str()))
64 return home;
68 if ((tmp = _wgetenv(L"USERPROFILE")) != nullptr && *tmp)
69 return tmp;
71 return {};
74 static int is_cygwin_msys2_hack_active()
76 HKEY hKey;
77 DWORD dwType = REG_DWORD;
78 DWORD dwValue = 0;
79 DWORD dwSize = sizeof(dwValue);
80 if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\TortoiseGit", 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS)
82 RegQueryValueExW(hKey, L"CygwinHack", nullptr, &dwType, (LPBYTE)&dwValue, &dwSize);
83 if (dwValue != 1)
84 RegQueryValueExW(hKey, L"Msys2Hack", nullptr, &dwType, (LPBYTE)&dwValue, &dwSize);
85 RegCloseKey(hKey);
87 return dwValue == 1;
90 static std::wstring GetProgramDataConfig()
92 wchar_t wbuffer[MAX_PATH];
94 // do not use shared windows-wide system config when cygwin hack is active
95 if (is_cygwin_msys2_hack_active())
96 return {};
98 if (SHGetFolderPathW(nullptr, CSIDL_COMMON_APPDATA, nullptr, SHGFP_TYPE_CURRENT, wbuffer) != S_OK || wcslen(wbuffer) >= MAX_PATH - wcslen(L"\\Git\\config"))
99 return{};
101 wcscat(wbuffer, L"\\Git\\config");
103 return wbuffer;
106 static std::wstring GetSystemGitConfig()
108 HKEY hKey;
109 DWORD dwType = REG_SZ;
110 TCHAR path[MAX_PATH] = { 0 };
111 DWORD dwSize = _countof(path) - 1;
112 if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\TortoiseGit", 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS)
114 RegQueryValueExW(hKey, L"SystemConfig", nullptr, &dwType, (LPBYTE)&path, &dwSize);
115 RegCloseKey(hKey);
117 return path;
120 static int RepoStatus(const TCHAR* path, std::string pathA, git_repository* repo, GitWCRev_t& GitStat)
122 git_status_options git_status_options = GIT_STATUS_OPTIONS_INIT;
123 git_status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
124 if (GitStat.bNoSubmodules)
125 git_status_options.flags |= GIT_STATUS_OPT_EXCLUDE_SUBMODULES;
127 std::string workdir(git_repository_workdir(repo));
128 std::transform(pathA.begin(), pathA.end(), pathA.begin(), [](char c) { return (c == '\\') ? '/' : c; });
129 pathA.erase(pathA.begin(), pathA.begin() + min(workdir.length(), pathA.length())); // workdir always ends with a slash, however, wcA is not guaranteed to
130 LoadIgnorePatterns(workdir.c_str(), &GitStat);
132 std::vector<const char*> pathspec;
133 if (!pathA.empty())
135 pathspec.emplace_back(pathA.c_str());
136 git_status_options.pathspec.count = 1;
138 if (!GitStat.ignorepatterns.empty())
140 std::transform(GitStat.ignorepatterns.cbegin(), GitStat.ignorepatterns.cend(), std::back_inserter(pathspec), [](auto& pattern) { return pattern.c_str(); });
141 git_status_options.pathspec.count += GitStat.ignorepatterns.size();
142 if (pathA.empty())
144 pathspec.push_back("*");
145 git_status_options.pathspec.count += 1;
148 if (git_status_options.pathspec.count > 0)
149 git_status_options.pathspec.strings = const_cast<char**>(pathspec.data());
151 CAutoStatusList status;
152 if (git_status_list_new(status.GetPointer(), repo, &git_status_options) < 0)
153 return ERR_GIT_ERR;
155 for (size_t i = 0, maxi = git_status_list_entrycount(status); i < maxi; ++i)
157 const git_status_entry* s = git_status_byindex(status, i);
158 if (s->index_to_workdir && s->index_to_workdir->new_file.mode == GIT_FILEMODE_COMMIT)
160 GitStat.bHasSubmodule = TRUE;
161 unsigned int smstatus = 0;
162 if (!git_submodule_status(&smstatus, repo, s->index_to_workdir->new_file.path, GIT_SUBMODULE_IGNORE_UNSPECIFIED))
164 if (smstatus & GIT_SUBMODULE_STATUS_WD_MODIFIED) // HEAD of submodule not matching
165 GitStat.bHasSubmoduleNewCommits = TRUE;
166 else if ((smstatus & GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED) || (smstatus & GIT_SUBMODULE_STATUS_WD_WD_MODIFIED))
167 GitStat.bHasSubmoduleMods = TRUE;
168 else if (smstatus & GIT_SUBMODULE_STATUS_WD_UNTRACKED)
169 GitStat.bHasSubmoduleUnversioned = TRUE;
171 continue;
173 if (s->status == GIT_STATUS_CURRENT)
174 continue;
175 if (s->status == GIT_STATUS_WT_NEW || s->status == GIT_STATUS_INDEX_NEW)
176 GitStat.HasUnversioned = TRUE;
177 else
178 GitStat.HasMods = TRUE;
181 if (pathA.empty()) // working tree root is always versioned
183 GitStat.bIsGitItem = TRUE;
184 return 0;
186 else if (PathIsDirectory(path)) // directories are unversioned in Git
188 GitStat.bIsGitItem = FALSE;
189 return 0;
191 unsigned int status_flags = 0;
192 int ret = git_status_file(&status_flags, repo, pathA.c_str());
193 GitStat.bIsGitItem = (ret == GIT_OK && !(status_flags & (GIT_STATUS_WT_NEW | GIT_STATUS_IGNORED | GIT_STATUS_INDEX_NEW)));
194 return 0;
197 int GetStatusUnCleanPath(const TCHAR* wcPath, GitWCRev_t& GitStat)
199 DWORD reqLen = GetFullPathName(wcPath, 0, nullptr, nullptr);
200 auto wcfullPath = std::make_unique<TCHAR[]>(reqLen + 1);
201 GetFullPathName(wcPath, reqLen, wcfullPath.get(), nullptr);
202 // GetFullPathName() sometimes returns the full path with the wrong
203 // case. This is not a problem on Windows since its filesystem is
204 // case-insensitive. But for Git that's a problem if the wrong case
205 // is inside a working copy: the git index is case sensitive.
206 // To fix the casing of the path, we use a trick:
207 // convert the path to its short form, then back to its long form.
208 // That will fix the wrong casing of the path.
209 int shortlen = GetShortPathName(wcfullPath.get(), nullptr, 0);
210 if (shortlen)
212 auto shortPath = std::make_unique<TCHAR[]>(shortlen + 1);
213 if (GetShortPathName(wcfullPath.get(), shortPath.get(), shortlen + 1))
215 reqLen = GetLongPathName(shortPath.get(), nullptr, 0);
216 wcfullPath = std::make_unique<TCHAR[]>(reqLen + 1);
217 GetLongPathName(shortPath.get(), wcfullPath.get(), reqLen);
220 return GetStatus(wcfullPath.get(), GitStat);
223 int GetStatus(const TCHAR* path, GitWCRev_t& GitStat)
225 std::string pathA = CUnicodeUtils::StdGetUTF8(path);
226 CAutoBuf dotgitdir;
227 if (git_repository_discover(dotgitdir, pathA.c_str(), 0, nullptr) < 0)
228 return ERR_NOWC;
230 CAutoRepository repo;
231 if (git_repository_open(repo.GetPointer(), dotgitdir->ptr))
232 return ERR_NOWC;
234 if (git_repository_is_bare(repo))
235 return ERR_NOWC;
237 CAutoConfig config(true);
238 std::string gitdir(dotgitdir->ptr, dotgitdir->size);
239 git_config_add_file_ondisk(config, (gitdir + "config").c_str(), GIT_CONFIG_LEVEL_LOCAL, repo, FALSE);
240 std::string home(CUnicodeUtils::StdGetUTF8(GetHomePath()));
241 git_config_add_file_ondisk(config, (home + "\\.gitconfig").c_str(), GIT_CONFIG_LEVEL_GLOBAL, repo, FALSE);
242 git_config_add_file_ondisk(config, (home + "\\.config\\git\\config").c_str(), GIT_CONFIG_LEVEL_XDG, repo, FALSE);
243 std::wstring systemConfig = GetSystemGitConfig();
244 if (!systemConfig.empty())
245 git_config_add_file_ondisk(config, CUnicodeUtils::StdGetUTF8(systemConfig).c_str(), GIT_CONFIG_LEVEL_SYSTEM, repo, FALSE);
246 std::wstring programDataConfig = GetProgramDataConfig();
247 if (!programDataConfig.empty())
248 git_config_add_file_ondisk(config, CUnicodeUtils::StdGetUTF8(programDataConfig).c_str(), GIT_CONFIG_LEVEL_PROGRAMDATA, repo, FALSE);
249 git_repository_set_config(repo, config);
251 if (git_repository_head_unborn(repo))
253 memset(GitStat.HeadHash, 0, sizeof(GitStat.HeadHash));
254 strncpy_s(GitStat.HeadHashReadable, GIT_OID_HEX_ZERO, strlen(GIT_OID_HEX_ZERO));
255 GitStat.bIsUnborn = TRUE;
257 CAutoReference symbolicHead;
258 if (git_reference_lookup(symbolicHead.GetPointer(), repo, "HEAD"))
259 return ERR_GIT_ERR;
260 auto branchName = git_reference_symbolic_target(symbolicHead);
261 if (CStringUtils::StartsWith(branchName, "refs/heads/"))
262 branchName += strlen("refs/heads/");
263 else if (CStringUtils::StartsWith(branchName, "refs/"))
264 branchName += strlen("refs/");
265 GitStat.CurrentBranch = branchName;
267 return RepoStatus(path, pathA, repo, GitStat);
270 CAutoReference head;
271 if (git_repository_head(head.GetPointer(), repo) < 0)
272 return ERR_GIT_ERR;
273 GitStat.CurrentBranch = git_reference_shorthand(head);
275 CAutoObject object;
276 if (git_reference_peel(object.GetPointer(), head, GIT_OBJ_COMMIT) < 0)
277 return ERR_GIT_ERR;
279 const git_oid* oid = git_object_id(object);
280 git_oid_cpy((git_oid*)GitStat.HeadHash, oid);
281 git_oid_tostr(GitStat.HeadHashReadable, sizeof(GitStat.HeadHashReadable), oid);
283 CAutoCommit commit;
284 if (git_commit_lookup(commit.GetPointer(), repo, oid) < 0)
285 return ERR_GIT_ERR;
287 const git_signature* sig = git_commit_author(commit);
288 GitStat.HeadAuthor = sig->name;
289 GitStat.HeadEmail = sig->email;
290 GitStat.HeadTime = sig->when.time;
292 #pragma warning(push)
293 #pragma warning(disable: 4510 4512 4610)
294 struct TagPayload { git_repository* repo; GitWCRev_t& GitStat; } tagpayload = { repo, GitStat };
295 #pragma warning(pop)
297 if (git_tag_foreach(repo, [](const char*, git_oid* tagoid, void* payload)
299 auto pl = reinterpret_cast<struct TagPayload*>(payload);
300 if (git_oid_cmp(tagoid, (git_oid*)pl->GitStat.HeadHash) == 0)
302 pl->GitStat.bIsTagged = TRUE;
303 return 0;
306 CAutoTag tag;
307 if (git_tag_lookup(tag.GetPointer(), pl->repo, tagoid))
308 return 0; // not an annotated tag
309 CAutoObject tagObject;
310 if (git_tag_peel(tagObject.GetPointer(), tag))
311 return -1;
312 if (git_oid_cmp(git_object_id(tagObject), (git_oid*)pl->GitStat.HeadHash) == 0)
313 pl->GitStat.bIsTagged = TRUE;
315 return 0;
316 }, &tagpayload))
317 return ERR_GIT_ERR;
319 // count the first-parent revisions from HEAD to the first commit
320 CAutoRevwalk walker;
321 if (git_revwalk_new(walker.GetPointer(), repo) < 0)
322 return ERR_GIT_ERR;
323 git_revwalk_simplify_first_parent(walker);
324 if (git_revwalk_push_head(walker) < 0)
325 return ERR_GIT_ERR;
326 git_oid oidlog;
327 while (!git_revwalk_next(&oidlog, walker))
328 ++GitStat.NumCommits;
330 return RepoStatus(path, pathA, repo, GitStat);