Honor renames in patch views
[TortoiseGit.git] / src / Git / Git.cpp
blob8323727b7436f5acebefa2e81d1ac1a709b58e92
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.
20 #include "stdafx.h"
21 #include "Git.h"
22 #include "GitRev.h"
23 #include "registry.h"
24 #include "GitForWindows.h"
25 #include "UnicodeUtils.h"
26 #include "gitdll.h"
27 #include <fstream>
28 #include <iterator>
29 #include "FormatMessageWrapper.h"
30 #include "SmartHandle.h"
31 #include "MassiveGitTaskBase.h"
32 #include "git2/sys/filter.h"
33 #include "git2/sys/transport.h"
34 #include "git2/sys/errors.h"
35 #include "../libgit2/filter-filter.h"
36 #include "../libgit2/ssh-wintunnel.h"
38 constexpr static int CalculateDiffSimilarityIndexThreshold(DWORD index) noexcept
40 if (index < 0 || index > 100)
41 return 50;
42 return index;
45 bool CGit::ms_bCygwinGit = (CRegDWORD(L"Software\\TortoiseGit\\CygwinHack", FALSE) == TRUE);
46 bool CGit::ms_bMsys2Git = (CRegDWORD(L"Software\\TortoiseGit\\Msys2Hack", FALSE) == TRUE);
47 int CGit::ms_iSimilarityIndexThreshold = CalculateDiffSimilarityIndexThreshold(CRegDWORD(L"Software\\TortoiseGit\\DiffSimilarityIndexThreshold", 50));
48 int CGit::m_LogEncode=CP_UTF8;
50 static LPCWSTR nextpath(const wchar_t* path, wchar_t* buf, size_t buflen)
52 if (!path || !buf || buflen == 0)
53 return nullptr;
55 const wchar_t* base = path;
56 const wchar_t term = (*path == L'"') ? *path++ : L';';
58 for (buflen--; *path && *path != term && buflen; buflen--)
59 *buf++ = *path++;
61 *buf = L'\0'; /* reserved a byte via initial subtract */
63 while (*path == term || *path == L';')
64 ++path;
66 return (path != base) ? path : nullptr;
69 static CString FindFileOnPath(const CString& filename, LPCWSTR env, bool wantDirectory = false)
71 wchar_t buf[MAX_PATH] = { 0 };
73 // search in all paths defined in PATH
74 while ((env = nextpath(env, buf, _countof(buf) - 1)) != nullptr && *buf)
76 wchar_t* pfin = buf + wcslen(buf) - 1;
78 // ensure trailing slash
79 if (*pfin != L'/' && *pfin != L'\\')
80 wcscpy_s(++pfin, 2, L"\\"); // we have enough space left, MAX_PATH-1 is used in nextpath above
82 const size_t len = wcslen(buf);
84 if ((len + filename.GetLength()) < _countof(buf))
85 wcscpy_s(pfin + 1, _countof(buf) - len, filename);
86 else
87 break;
89 if (PathFileExists(buf))
91 if (wantDirectory)
92 pfin[1] = L'\0';
93 return CString(buf);
97 return L"";
100 static BOOL FindGitPath()
102 size_t size;
103 _wgetenv_s(&size, nullptr, 0, L"PATH");
104 if (!size)
105 return FALSE;
107 auto env = std::make_unique<wchar_t[]>(size);
108 if (!env)
109 return FALSE;
110 _wgetenv_s(&size, env.get(), size, L"PATH");
112 CString gitExeDirectory = FindFileOnPath(L"git.exe", env.get(), true);
113 if (!gitExeDirectory.IsEmpty())
115 CGit::ms_LastMsysGitDir = gitExeDirectory;
116 CGit::ms_LastMsysGitDir.TrimRight(L'\\');
118 // check for (broken) scoop shim, cf. issue #4033
119 if (CString contents; CGit::LoadTextFile(CGit::ms_LastMsysGitDir + L"\\git.shim", contents))
121 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": found git.shim");
122 int start = 0 ;
123 while (start >= 0)
125 CString line = contents.Tokenize(L"\n", start);
126 if (auto separatorPos = line.Find(L'='); separatorPos > 0 && line.Mid(0, separatorPos).Trim() == L"path")
128 CString path = line.Mid(separatorPos + 1).Trim();
129 if (path.GetLength() >= 2 && path[0] == L'"' && path[path.GetLength() - 1] == L'"')
130 path = path.Mid(1, path.GetLength() - 2);
131 if (CStringUtils::EndsWithI(path, L"\\git.exe") && PathFileExists(path))
132 CGit::ms_LastMsysGitDir = path.Mid(0, path.GetLength() - static_cast<int>(wcslen(L"\\git.exe")));
133 break;
138 if (CStringUtils::EndsWith(CGit::ms_LastMsysGitDir, L"\\mingw32\\bin") || CStringUtils::EndsWith(CGit::ms_LastMsysGitDir, L"\\mingw64\\bin"))
140 // prefer cmd directory as early Git for Windows 2.x releases only had this
141 CString installRoot = CGit::ms_LastMsysGitDir.Left(CGit::ms_LastMsysGitDir.GetLength() - static_cast<int>(wcslen(L"\\mingw64\\bin"))) + L"\\cmd\\git.exe";
142 if (PathFileExists(installRoot))
143 CGit::ms_LastMsysGitDir = CGit::ms_LastMsysGitDir.Left(CGit::ms_LastMsysGitDir.GetLength() - static_cast<int>(wcslen(L"\\mingw64\\bin"))) + L"\\cmd";
145 if (CStringUtils::EndsWith(CGit::ms_LastMsysGitDir, L"\\cmd"))
147 // often the msysgit\cmd folder is on the %PATH%, but
148 // that git.exe does not work, so try to guess the bin folder
149 if (PathFileExists(CGit::ms_LastMsysGitDir.Left(CGit::ms_LastMsysGitDir.GetLength() - static_cast<int>(wcslen(L"\\cmd"))) + L"\\bin\\git.exe"))
150 CGit::ms_LastMsysGitDir = CGit::ms_LastMsysGitDir.Left(CGit::ms_LastMsysGitDir.GetLength() - static_cast<int>(wcslen(L"\\cmd"))) + L"\\bin";
152 return TRUE;
155 return FALSE;
158 static CString FindExecutableOnPath(const CString& executable, LPCWSTR env)
160 CString filename = executable;
162 if (!CStringUtils::EndsWith(executable, L".exe"))
163 filename += L".exe";
165 if (PathFileExists(filename))
166 return filename;
168 filename = FindFileOnPath(filename, env);
169 if (!filename.IsEmpty())
170 return filename;
172 return executable;
175 static bool g_bSortLogical;
176 static bool g_bSortLocalBranchesFirst;
177 static bool g_bSortTagsReversed;
178 static git_credential_acquire_cb g_Git2CredCallback;
179 static git_transport_certificate_check_cb g_Git2CheckCertificateCallback;
181 static void GetSortOptions()
183 #ifdef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
184 g_bSortLogical = true;
185 g_bSortLocalBranchesFirst = true;
186 g_bSortTagsReversed = false;
187 #else
188 g_bSortLogical = !CRegDWORD(L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\NoStrCmpLogical", 0, false, HKEY_CURRENT_USER);
189 if (g_bSortLogical)
190 g_bSortLogical = !CRegDWORD(L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\NoStrCmpLogical", 0, false, HKEY_LOCAL_MACHINE);
191 g_bSortLocalBranchesFirst = !CRegDWORD(L"Software\\TortoiseGit\\NoSortLocalBranchesFirst", 0, false, HKEY_CURRENT_USER);
192 if (g_bSortLocalBranchesFirst)
193 g_bSortLocalBranchesFirst = !CRegDWORD(L"Software\\TortoiseGit\\NoSortLocalBranchesFirst", 0, false, HKEY_LOCAL_MACHINE);
194 g_bSortTagsReversed = !!CRegDWORD(L"Software\\TortoiseGit\\SortTagsReversed", 0, false, HKEY_LOCAL_MACHINE);
195 if (!g_bSortTagsReversed)
196 g_bSortTagsReversed = !!CRegDWORD(L"Software\\TortoiseGit\\SortTagsReversed", 0, false, HKEY_CURRENT_USER);
197 #endif
200 static int LogicalComparePredicate(const CString &left, const CString &right)
202 if (g_bSortLogical)
203 return StrCmpLogicalW(left, right) < 0;
204 return StrCmpI(left, right) < 0;
207 static int LogicalCompareReversedPredicate(const CString &left, const CString &right)
209 return LogicalComparePredicate(right, left);
212 static int LogicalCompareBranchesPredicate(const CString &left, const CString &right)
214 if (g_bSortLocalBranchesFirst)
216 const bool leftIsRemote = CStringUtils::StartsWith(left, L"remotes/");
217 const bool rightIsRemote = CStringUtils::StartsWith(right, L"remotes/");
219 if (leftIsRemote && !rightIsRemote)
220 return false;
221 else if (!leftIsRemote && rightIsRemote)
222 return true;
224 return LogicalComparePredicate(left, right);
227 #define CALL_OUTPUT_READ_CHUNK_SIZE 1024
229 CString CGit::ms_LastMsysGitDir;
230 CString CGit::ms_MsysGitRootDir;
231 int CGit::ms_LastMsysGitVersion = 0;
232 CGit g_Git;
235 CGit::CGit()
237 git_libgit2_init();
238 GetCurrentDirectory(MAX_PATH, CStrBuf(m_CurrentDir, MAX_PATH));
239 m_IsUseGitDLL = !!CRegDWORD(L"Software\\TortoiseGit\\UsingGitDLL",1);
240 m_IsUseLibGit2 = !!CRegDWORD(L"Software\\TortoiseGit\\UseLibgit2", TRUE);
241 m_IsUseLibGit2_mask = CRegDWORD(L"Software\\TortoiseGit\\UseLibgit2_mask", DEFAULT_USE_LIBGIT2_MASK);
243 GetSortOptions();
244 CheckMsysGitDir();
247 CGit::~CGit()
249 if(this->m_GitDiff)
251 git_close_diff(m_GitDiff);
252 m_GitDiff=0;
254 if(this->m_GitSimpleListDiff)
256 git_close_diff(m_GitSimpleListDiff);
257 m_GitSimpleListDiff=0;
259 git_libgit2_shutdown();
262 bool CGit::IsBranchNameValid(const CString& branchname)
264 if (branchname.FindOneOf(L"\"|<>") >= 0) // not valid on Windows
265 return false;
266 int valid = 0;
267 git_branch_name_is_valid(&valid, CUnicodeUtils::GetUTF8(branchname));
268 return valid == 1;
271 int CGit::RunAsync(CString cmd, PROCESS_INFORMATION& piOut, HANDLE* hReadOut, HANDLE* hErrReadOut, const CString* StdioFile)
273 CAutoGeneralHandle hRead, hWrite, hReadErr, hWriteErr, hWriteIn, hReadIn;
274 CAutoFile hStdioFile;
276 SECURITY_ATTRIBUTES sa = { 0 };
277 sa.nLength = sizeof(SECURITY_ATTRIBUTES);
278 sa.bInheritHandle=TRUE;
279 if (!CreatePipe(hReadIn.GetPointer(), hWriteIn.GetPointer(), &sa, 0))
281 CString err { static_cast<LPCWSTR>(CFormatMessageWrapper()) };
282 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": could not open stdin pipe: %s\n", static_cast<LPCWSTR>(err.Trim()));
283 return TGIT_GIT_ERROR_OPEN_PIP;
285 if (!CreatePipe(hRead.GetPointer(), hWrite.GetPointer(), &sa, 0))
287 CString err { static_cast<LPCWSTR>(CFormatMessageWrapper()) };
288 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": could not open stdout pipe: %s\n", static_cast<LPCWSTR>(err.Trim()));
289 return TGIT_GIT_ERROR_OPEN_PIP;
291 if (hErrReadOut && !CreatePipe(hReadErr.GetPointer(), hWriteErr.GetPointer(), &sa, 0))
293 CString err { static_cast<LPCWSTR>(CFormatMessageWrapper()) };
294 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": could not open stderr pipe: %s\n", static_cast<LPCWSTR>(err.Trim()));
295 return TGIT_GIT_ERROR_OPEN_PIP;
298 if(StdioFile)
299 hStdioFile = CreateFile(*StdioFile, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
301 STARTUPINFO si = { 0 };
302 PROCESS_INFORMATION pi = { 0 };
303 si.cb=sizeof(STARTUPINFO);
304 si.hStdInput = hReadIn;
305 if (hErrReadOut)
306 si.hStdError = hWriteErr;
307 else
308 si.hStdError = hWrite;
309 if(StdioFile)
310 si.hStdOutput=hStdioFile;
311 else
312 si.hStdOutput=hWrite;
314 si.wShowWindow=SW_HIDE;
315 si.dwFlags=STARTF_USESTDHANDLES|STARTF_USESHOWWINDOW;
317 LPWSTR pEnv = m_Environment;
318 const DWORD dwFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS;
320 memset(&this->m_CurrentGitPi,0,sizeof(PROCESS_INFORMATION));
322 bool startsWithGit = CStringUtils::StartsWith(cmd, L"git");
323 if ((ms_bMsys2Git || ms_bCygwinGit) && startsWithGit)
325 if (!CStringUtils::StartsWith(cmd, L"git.exe config "))
326 cmd.Replace(L'\\', L'/');
327 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": executing via bash: %s\n", static_cast<LPCWSTR>(cmd));
328 CString tmpFile = GetTempFile();
329 if (ms_bMsys2Git)
330 cmd = L"/usr/bin/" + cmd;
331 else if (ms_bCygwinGit)
332 cmd = L"/bin/" + cmd;
333 CStringUtils::WriteStringToTextFile(tmpFile, cmd);
334 tmpFile.Replace(L'\\', L'/');
335 cmd = L'"' + CGit::ms_LastMsysGitDir + L"\\bash.exe\" \"" + tmpFile + L'"';
337 else if (startsWithGit || CStringUtils::StartsWith(cmd, L"bash"))
339 const int firstSpace = cmd.Find(L' ');
340 if (firstSpace > 0)
341 cmd = L'"' + CGit::ms_LastMsysGitDir + L'\\' + cmd.Left(firstSpace) + L'"' + cmd.Mid(firstSpace);
342 else
343 cmd = L'"' + CGit::ms_LastMsysGitDir + L'\\' + cmd + L'"';
346 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": executing %s\n", static_cast<LPCWSTR>(cmd));
347 if(!CreateProcess(nullptr, cmd.GetBuffer(), nullptr, nullptr, TRUE, dwFlags, pEnv, m_CurrentDir.GetBuffer(), &si, &pi))
349 CString err { static_cast<LPCWSTR>(CFormatMessageWrapper()) };
350 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": error while executing command: %s\n", static_cast<LPCWSTR>(err.Trim()));
351 return TGIT_GIT_ERROR_CREATE_PROCESS;
354 // Close the pipe handle so the child process stops reading.
355 hWriteIn.CloseHandle();
357 m_CurrentGitPi = pi;
358 piOut = pi;
359 if(hReadOut)
360 *hReadOut = hRead.Detach();
361 if(hErrReadOut)
362 *hErrReadOut = hReadErr.Detach();
363 return 0;
365 //Must use sperate function to convert ANSI str to union code string
366 //Because A2W use stack as internal convert buffer.
367 void CGit::StringAppend(CString& str, const char* p, int code, int length)
369 const int len = [length, p]() {
370 if (length < 0)
371 return SafeSizeToInt(strlen(p));
372 else
373 return length;
374 }();
375 if (len == 0)
376 return;
377 const int currentContentLen = str.GetLength();
378 if (len >= INT_MAX / 2)
379 throw std::overflow_error("CString would become too long");
380 auto* buf = str.GetBuffer(SafeSizeToInt(static_cast<size_t>(len) * 2 + currentContentLen)) + currentContentLen;
381 const int appendedLen = MultiByteToWideChar(code, 0, p, len, buf, len * 2);
382 str.ReleaseBuffer(currentContentLen + appendedLen); // no - 1 because MultiByteToWideChar is called with a fixed length (thus no nul char included)
385 // This method was originally used to check for orphaned branches
386 BOOL CGit::CanParseRev(CString ref)
388 if (ref.IsEmpty())
389 ref = L"HEAD";
391 // --end-of-options does not work without --verify, but we cannot use --verify becasue we also want to check for valid ranges
392 CString cmdout;
393 if (Run(L"git.exe rev-parse --revs-only " + ref, &cmdout, CP_UTF8))
394 return FALSE;
395 if(cmdout.IsEmpty())
396 return FALSE;
398 return TRUE;
401 // Checks if we have an orphaned HEAD
402 BOOL CGit::IsInitRepos()
404 CGitHash hash;
405 if (GetHash(hash, L"HEAD") != 0)
406 return FALSE;
407 return hash.IsEmpty() ? TRUE : FALSE;
410 DWORD WINAPI CGit::AsyncReadStdErrThread(LPVOID lpParam)
412 auto pDataArray = static_cast<ASYNCREADSTDERRTHREADARGS*>(lpParam);
414 DWORD readnumber;
415 char data[CALL_OUTPUT_READ_CHUNK_SIZE];
416 while (ReadFile(pDataArray->fileHandle, data, CALL_OUTPUT_READ_CHUNK_SIZE, &readnumber, nullptr))
418 if (pDataArray->pcall->OnOutputErrData(data,readnumber))
419 break;
422 return 0;
425 #ifdef _MFC_VER
426 void CGit::KillRelatedThreads(CWinThread* thread)
428 CAutoLocker lock(m_critSecThreadMap);
429 auto it = m_AsyncReadStdErrThreadMap.find(thread->m_nThreadID);
430 if (it != m_AsyncReadStdErrThreadMap.cend())
432 TerminateThread(it->second, static_cast<DWORD>(-1));
433 m_AsyncReadStdErrThreadMap.erase(it);
435 TerminateThread(thread->m_hThread, static_cast<DWORD>(-1));
437 #endif
439 int CGit::Run(CGitCall& pcall)
441 PROCESS_INFORMATION pi;
442 CAutoGeneralHandle hRead, hReadErr;
443 if (RunAsync(pcall.GetCmd(), pi, hRead.GetPointer(), hReadErr.GetPointer()))
444 return TGIT_GIT_ERROR_CREATE_PROCESS;
446 CAutoGeneralHandle piThread(std::move(pi.hThread));
447 CAutoGeneralHandle piProcess(std::move(pi.hProcess));
449 ASYNCREADSTDERRTHREADARGS threadArguments;
450 threadArguments.fileHandle = hReadErr;
451 threadArguments.pcall = &pcall;
452 CAutoGeneralHandle thread = CreateThread(nullptr, 0, AsyncReadStdErrThread, &threadArguments, 0, nullptr);
454 if (thread)
456 CAutoLocker lock(m_critSecThreadMap);
457 m_AsyncReadStdErrThreadMap[GetCurrentThreadId()] = thread;
460 DWORD readnumber;
461 char data[CALL_OUTPUT_READ_CHUNK_SIZE];
462 bool bAborted=false;
463 while (ReadFile(hRead, data, CALL_OUTPUT_READ_CHUNK_SIZE, &readnumber, nullptr))
465 // TODO: when OnOutputData() returns 'true', abort git-command. Send CTRL-C signal?
466 if(!bAborted)//For now, flush output when command aborted.
467 if(pcall.OnOutputData(data,readnumber))
468 bAborted=true;
470 if(!bAborted)
471 pcall.OnEnd();
473 if (thread)
475 WaitForSingleObject(thread, INFINITE);
477 CAutoLocker lock(m_critSecThreadMap);
478 m_AsyncReadStdErrThreadMap.erase(GetCurrentThreadId());
481 WaitForSingleObject(pi.hProcess, INFINITE);
482 DWORD exitcode =0;
484 if(!GetExitCodeProcess(pi.hProcess,&exitcode))
486 CString err { static_cast<LPCWSTR>(CFormatMessageWrapper()) };
487 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": could not get exit code: %s\n", static_cast<LPCWSTR>(err.Trim()));
488 return TGIT_GIT_ERROR_GET_EXIT_CODE;
490 else
491 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": process exited: %d\n", exitcode);
493 return exitcode;
495 class CGitCall_ByteVector : public CGitCall
497 public:
498 CGitCall_ByteVector(CString cmd,BYTE_VECTOR* pvector, BYTE_VECTOR* pvectorErr = nullptr) : CGitCall(cmd),m_pvector(pvector), m_pvectorErr(pvectorErr) {}
499 bool OnOutputData(const char* data, size_t size) override
501 ASSERT(data);
502 if (!m_pvector || size == 0)
503 return false;
504 const size_t oldsize = m_pvector->size();
505 size_t newLength;
506 if (SizeTAdd(oldsize, size, &newLength) != S_OK)
507 return false;
508 m_pvector->resize(newLength);
509 memcpy(&*(m_pvector->begin()+oldsize),data,size);
510 return false;
512 bool OnOutputErrData(const char* data, size_t size) override
514 ASSERT(data);
515 if (!m_pvectorErr || size == 0)
516 return false;
517 const size_t oldsize = m_pvectorErr->size();
518 size_t newLength;
519 if (SizeTAdd(oldsize, size, &newLength) != S_OK)
520 return false;
521 m_pvectorErr->resize(newLength);
522 memcpy(&*(m_pvectorErr->begin() + oldsize), data, size);
523 return false;
525 BYTE_VECTOR* m_pvector;
526 BYTE_VECTOR* m_pvectorErr;
528 int CGit::Run(CString cmd,BYTE_VECTOR *vector, BYTE_VECTOR *vectorErr)
530 CGitCall_ByteVector call(cmd, vector, vectorErr);
531 return Run(call);
533 int CGit::Run(CString cmd, CString* output, int code)
535 CString err;
536 const int ret = Run(cmd, output, &err, code);
538 if (output && !err.IsEmpty())
540 if (!output->IsEmpty())
541 *output += L'\n';
542 *output += err;
545 return ret;
547 int CGit::Run(CString cmd, CString* output, CString* outputErr, int code)
549 BYTE_VECTOR vector, vectorErr;
550 int ret;
551 if (outputErr)
552 ret = Run(cmd, &vector, &vectorErr);
553 else
554 ret = Run(cmd, &vector);
556 vector.push_back(0);
557 if (output)
558 StringAppend(*output, vector.data(), code);
560 if (outputErr)
562 vectorErr.push_back(0);
563 StringAppend(*outputErr, vectorErr.data(), code);
566 return ret;
569 CString CGit::GetUserName()
571 CEnvironment env;
572 env.CopyProcessEnvironment();
573 CString envname = env.GetEnv(L"GIT_AUTHOR_NAME");
574 if (!envname.IsEmpty())
575 return envname;
577 if (ms_LastMsysGitVersion >= ConvertVersionToInt(2, 22, 0))
578 if (auto authorname = GetConfigValue(L"author.name"); !authorname.IsEmpty())
579 return authorname;
581 return GetConfigValue(L"user.name");
583 CString CGit::GetUserEmail()
585 CEnvironment env;
586 env.CopyProcessEnvironment();
587 CString envmail = env.GetEnv(L"GIT_AUTHOR_EMAIL");
588 if (!envmail.IsEmpty())
589 return envmail;
591 if (ms_LastMsysGitVersion >= ConvertVersionToInt(2, 22, 0))
592 if (auto authormail = GetConfigValue(L"author.email"); !authormail.IsEmpty())
593 return authormail;
595 return GetConfigValue(L"user.email");
598 CString CGit::GetCommitterName()
600 CEnvironment env;
601 env.CopyProcessEnvironment();
602 CString envname = env.GetEnv(L"GIT_COMMITTER_NAME");
603 if (!envname.IsEmpty())
604 return envname;
606 if (ms_LastMsysGitVersion >= ConvertVersionToInt(2, 22, 0))
607 if (auto committername = GetConfigValue(L"committer.name"); !committername.IsEmpty())
608 return committername;
610 return GetConfigValue(L"user.name");
613 CString CGit::GetCommitterEmail()
615 CEnvironment env;
616 env.CopyProcessEnvironment();
617 CString envmail = env.GetEnv(L"GIT_AUTHOR_EMAIL");
618 if (!envmail.IsEmpty())
619 return envmail;
621 if (ms_LastMsysGitVersion >= ConvertVersionToInt(2, 22, 0))
622 if (auto committermail = GetConfigValue(L"committer.email"); !committermail.IsEmpty())
623 return committermail;
625 return GetConfigValue(L"user.email");
628 CString CGit::GetConfigValue(const CString& name, const CString& def, bool wantBool)
630 if(this->m_IsUseGitDLL)
632 CAutoLocker lock(g_Git.m_critGitDllSec);
636 CheckAndInitDll();
637 }catch(...)
640 CStringA key, value;
641 key = CUnicodeUtils::GetUTF8(name);
645 if (git_get_config(key, CStrBufA(value, 4096), 4096))
646 return def;
648 catch (const char *msg)
650 ::MessageBox(nullptr, L"Could not get config.\nlibgit reports:\n" + CUnicodeUtils::GetUnicode(msg), L"TortoiseGit", MB_OK | MB_ICONERROR);
651 return def;
654 return CUnicodeUtils::GetUnicode(value);
656 else
658 CString cmd;
659 cmd.Format(L"git.exe config%s %s", wantBool ? L" --bool" : L"", static_cast<LPCWSTR>(name));
660 CString configValue;
661 if (Run(cmd, &configValue, nullptr, CP_UTF8))
662 return def;
663 if (configValue.IsEmpty())
664 return configValue;
665 return configValue.Left(configValue.GetLength() - 1); // strip last newline character
669 bool CGit::GetConfigValueBool(const CString& name, const bool def)
671 CString configValue = GetConfigValue(name, def ? L"true" : L"false", true);
672 configValue.MakeLower();
673 configValue.Trim();
674 if (configValue == L"true" || configValue == L"on" || configValue == L"yes" || StrToInt(configValue) != 0)
675 return true;
676 else
677 return false;
680 int CGit::GetConfigValueInt32(const CString& name, const int def)
682 CString configValue = GetConfigValue(name);
683 int value = def;
684 if (!git_config_parse_int32(&value, CUnicodeUtils::GetUTF8(configValue)))
685 return value;
686 return def;
689 int CGit::SetConfigValue(const CString& key, const CString& value, CONFIG_TYPE type)
691 if(this->m_IsUseGitDLL)
693 CAutoLocker lock(g_Git.m_critGitDllSec);
697 CheckAndInitDll();
698 }catch(...)
701 CStringA keya, valuea;
702 keya = CUnicodeUtils::GetUTF8(key);
703 valuea = CUnicodeUtils::GetUTF8(value);
707 return git_set_config(keya, valuea, type);
709 catch (const char *msg)
711 ::MessageBox(nullptr, L"Could not set config.\nlibgit reports:\n" + CUnicodeUtils::GetUnicode(msg), L"TortoiseGit", MB_OK | MB_ICONERROR);
712 return -1;
715 else
717 CString cmd;
718 CString option;
719 switch(type)
721 case CONFIG_GLOBAL:
722 option = L"--global";
723 break;
724 case CONFIG_SYSTEM:
725 option = L"--system";
726 break;
727 default:
728 break;
730 CString mangledValue = value;
731 mangledValue.Replace(L"\\\"", L"\\\\\"");
732 mangledValue.Replace(L"\"", L"\\\"");
733 cmd.Format(L"git.exe config %s %s \"%s\"", static_cast<LPCWSTR>(option), static_cast<LPCWSTR>(key), static_cast<LPCWSTR>(mangledValue));
734 CString out;
735 if (Run(cmd, &out, nullptr, CP_UTF8))
736 return -1;
738 return 0;
741 int CGit::UnsetConfigValue(const CString& key, CONFIG_TYPE type)
743 if(this->m_IsUseGitDLL)
745 CAutoLocker lock(g_Git.m_critGitDllSec);
749 CheckAndInitDll();
750 }catch(...)
753 CStringA keya;
754 keya = CUnicodeUtils::GetUTF8(key);
758 return git_set_config(keya, nullptr, type);
760 catch (const char *msg)
762 ::MessageBox(nullptr, L"Could not unset config.\nlibgit reports:\n" + CUnicodeUtils::GetUnicode(msg), L"TortoiseGit", MB_OK | MB_ICONERROR);
763 return -1;
766 else
768 CString cmd;
769 CString option;
770 switch(type)
772 case CONFIG_GLOBAL:
773 option = L"--global";
774 break;
775 case CONFIG_SYSTEM:
776 option = L"--system";
777 break;
778 default:
779 break;
781 cmd.Format(L"git.exe config %s --unset %s", static_cast<LPCWSTR>(option), static_cast<LPCWSTR>(key));
782 CString out;
783 if (Run(cmd, &out, nullptr, CP_UTF8))
784 return -1;
786 return 0;
789 int CGit::ApplyPatchToIndex(const CString& patchPath, CString* out)
791 CString cmd;
792 cmd.Format(L"git.exe apply --cached -- \"%s\"", static_cast<LPCWSTR>(patchPath));
793 return Run(cmd, out, CP_UTF8);
796 int CGit::ApplyPatchToIndexReverse(const CString& patchPath, CString* out)
798 CString cmd;
799 cmd.Format(L"git.exe apply --cached -R -- \"%s\"", static_cast<LPCWSTR>(patchPath));
800 return Run(cmd, out, CP_UTF8);
803 CString CGit::GetCurrentBranch(bool fallback)
805 CString output;
806 //Run(L"git.exe branch", &branch);
808 const int result = GetCurrentBranchFromFile(m_CurrentDir, output, fallback);
809 if (result != 0 && ((result == 1 && !fallback) || result != 1))
810 return L"(no branch)";
811 else
812 return output;
815 void CGit::GetRemoteTrackedBranch(const CString& localBranch, CString& pullRemote, CString& pullBranch)
817 if (localBranch.IsEmpty())
818 return;
820 CString configName;
821 configName.Format(L"branch.%s.remote", static_cast<LPCWSTR>(localBranch));
822 pullRemote = GetConfigValue(configName);
824 //Select pull-branch from current branch
825 configName.Format(L"branch.%s.merge", static_cast<LPCWSTR>(localBranch));
826 pullBranch = StripRefName(GetConfigValue(configName));
829 void CGit::GetRemoteTrackedBranchForHEAD(CString& remote, CString& branch)
831 CString refName;
832 if (GetCurrentBranchFromFile(m_CurrentDir, refName))
833 return;
834 GetRemoteTrackedBranch(StripRefName(refName), remote, branch);
837 void CGit::GetRemotePushBranch(const CString& localBranch, CString& pushRemote, CString& pushBranch)
839 if (localBranch.IsEmpty())
840 return;
842 CString configName;
844 configName.Format(L"branch.%s.pushremote", static_cast<LPCWSTR>(localBranch));
845 pushRemote = g_Git.GetConfigValue(configName);
846 if (pushRemote.IsEmpty())
848 pushRemote = g_Git.GetConfigValue(L"remote.pushdefault");
849 if (pushRemote.IsEmpty())
851 configName.Format(L"branch.%s.remote", static_cast<LPCWSTR>(localBranch));
852 pushRemote = g_Git.GetConfigValue(configName);
856 configName.Format(L"branch.%s.pushbranch", static_cast<LPCWSTR>(localBranch));
857 pushBranch = g_Git.GetConfigValue(configName); // do not strip branch name (for gerrit), see issue #1609)
858 if (pushBranch.IsEmpty())
860 configName.Format(L"branch.%s.merge", static_cast<LPCWSTR>(localBranch));
861 pushBranch = CGit::StripRefName(g_Git.GetConfigValue(configName));
865 CString CGit::GetFullRefName(const CString& shortRefName)
867 CString refName;
868 CString cmd;
869 cmd.Format(L"git.exe rev-parse --symbolic-full-name --verify --end-of-options %s", static_cast<LPCWSTR>(shortRefName));
870 if (Run(cmd, &refName, nullptr, CP_UTF8) != 0)
871 return CString();//Error
872 return refName.TrimRight();
875 CString CGit::StripRefName(CString refName)
877 if (CStringUtils::StartsWith(refName, L"refs/heads/"))
878 refName = refName.Mid(static_cast<int>(wcslen(L"refs/heads/")));
879 else if (CStringUtils::StartsWith(refName, L"refs/"))
880 refName = refName.Mid(static_cast<int>(wcslen(L"refs/")));
881 return refName.TrimRight();
884 int CGit::GetCurrentBranchFromFile(const CString &sProjectRoot, CString &sBranchOut, bool fallback)
886 // read current branch name like git-gui does, by parsing the .git/HEAD file directly
888 if ( sProjectRoot.IsEmpty() )
889 return -1;
891 CString sDotGitPath;
892 if (!GitAdminDir::GetWorktreeAdminDirPath(sProjectRoot, sDotGitPath))
893 return -1;
895 CString sHeadFile = sDotGitPath + L"HEAD";
897 CAutoFILE pFile = _wfsopen(sHeadFile.GetString(), L"r", SH_DENYWR);
898 if (!pFile)
899 return -1;
901 char s[MAX_PATH] = {0};
902 fgets(s, sizeof(s), pFile);
904 const char *pfx = "ref: refs/heads/";
905 const size_t len = strlen(pfx);
907 if ( !strncmp(s, pfx, len) )
909 //# We're on a branch. It might not exist. But
910 //# HEAD looks good enough to be a branch.
911 CStringA utf8Branch(s + len);
912 sBranchOut = CUnicodeUtils::GetUnicode(utf8Branch);
913 sBranchOut.TrimRight(L" \r\n\t");
915 if ( sBranchOut.IsEmpty() )
916 return -1;
918 else if (fallback)
920 CStringA utf8Hash(s);
921 CString unicodeHash = CUnicodeUtils::GetUnicode(utf8Hash);
922 unicodeHash.TrimRight(L" \r\n\t");
923 if (CGitHash::IsValidSHA1(unicodeHash))
924 sBranchOut = unicodeHash;
925 else
926 //# Assume this is a detached head.
927 sBranchOut = L"HEAD";
928 return 1;
930 else
932 //# Assume this is a detached head.
933 sBranchOut = "HEAD";
935 return 1;
938 return 0;
941 CString CGit::GetLogCmd(CString range, const CTGitPath* path, int mask, CFilterData* Filter, int logOrderBy)
943 CString param;
945 if(mask& LOG_INFO_STAT )
946 param += L" --numstat";
947 if(mask& LOG_INFO_FILESTATE)
948 param += L" --raw";
950 if(mask& LOG_INFO_BOUNDARY)
951 param += L" --left-right --boundary";
953 if(mask& CGit::LOG_INFO_ALL_BRANCH)
955 param += L" --all";
956 if ((mask & LOG_INFO_ALWAYS_APPLY_RANGE) == 0)
957 range.Empty();
960 if (mask& CGit::LOG_INFO_BASIC_REFS)
962 param += L" --branches";
963 param += L" --tags";
964 param += L" --remotes";
965 param += L" --glob=stas[h]"; // require at least one glob operator
966 param += L" --glob=bisect";
967 if ((mask & LOG_INFO_ALWAYS_APPLY_RANGE) == 0)
968 range.Empty();
971 if(mask & CGit::LOG_INFO_LOCAL_BRANCHES)
973 param += L" --branches";
974 if ((mask & LOG_INFO_ALWAYS_APPLY_RANGE) == 0)
975 range.Empty();
978 if(mask& CGit::LOG_INFO_DETECT_COPYRENAME)
979 param.AppendFormat(L" -C%d%%", ms_iSimilarityIndexThreshold);
981 if(mask& CGit::LOG_INFO_DETECT_RENAME )
982 param.AppendFormat(L" -M%d%%", ms_iSimilarityIndexThreshold);
984 if(mask& CGit::LOG_INFO_FIRST_PARENT )
985 param += L" --first-parent";
987 if(mask& CGit::LOG_INFO_NO_MERGE )
988 param += L" --no-merges";
990 if (mask & CGit::LOG_INFO_FULL_HISTORY)
991 param += " --full-history";
992 else
993 param += " --parents"; // cf. issue #3728
995 if(mask& CGit::LOG_INFO_FOLLOW)
996 param += L" --follow";
998 if(mask& CGit::LOG_INFO_SHOW_MERGEDFILE)
999 param += L" -c";
1001 if(mask& CGit::LOG_INFO_FULL_DIFF)
1002 param += L" --full-diff";
1004 if(mask& CGit::LOG_INFO_SIMPILFY_BY_DECORATION)
1005 param += L" --simplify-by-decoration";
1007 if (mask & CGit::LOG_INFO_SPARSE)
1008 param += L" --sparse";
1010 if (Filter)
1012 if (Filter->m_NumberOfLogsScale >= CFilterData::SHOW_LAST_N_YEARS)
1014 CTime now = CTime::GetCurrentTime();
1015 CTime time = CTime(now.GetYear(), now.GetMonth(), now.GetDay(), 0, 0, 0);
1016 __time64_t substract = 86400;
1017 CString scale;
1018 switch (Filter->m_NumberOfLogsScale)
1020 case CFilterData::SHOW_LAST_N_YEARS:
1021 substract *= 365;
1022 break;
1023 case CFilterData::SHOW_LAST_N_MONTHS:
1024 substract *= 30;
1025 break;
1026 case CFilterData::SHOW_LAST_N_WEEKS:
1027 substract *= 7;
1028 break;
1030 Filter->m_From = static_cast<DWORD>(time.GetTime()) - (Filter->m_NumberOfLogs * substract);
1032 if (Filter->m_NumberOfLogsScale == CFilterData::SHOW_LAST_N_COMMITS)
1033 param.AppendFormat(L" -n%ld", Filter->m_NumberOfLogs);
1034 else if (Filter->m_NumberOfLogsScale >= CFilterData::SHOW_LAST_SEL_DATE && Filter->m_From > 0)
1035 param.AppendFormat(L" --max-age=%I64u", Filter->m_From);
1038 if( Filter && (Filter->m_To != -1))
1039 param.AppendFormat(L" --min-age=%I64u", Filter->m_To);
1041 if (logOrderBy == LOG_ORDER_TOPOORDER || (mask & CGit::LOG_ORDER_TOPOORDER))
1042 param += L" --topo-order";
1043 else if (logOrderBy == LOG_ORDER_DATEORDER)
1044 param += L" --date-order";
1046 CString cmd;
1047 CString file;
1048 if (path)
1049 file.Format(L" \"%s\"", static_cast<LPCWSTR>(path->GetGitPathString()));
1050 // gitdll.dll:setup_revisions() only looks at args[1] and greater. To account for this, pass a dummy parameter in the 0th place
1051 cmd.Format(L"-z%s %s --%s", static_cast<LPCWSTR>(param), static_cast<LPCWSTR>(range), static_cast<LPCWSTR>(file));
1053 return cmd;
1055 #define BUFSIZE 512
1056 void GetTempPath(CString &path)
1058 wchar_t lpPathBuffer[BUFSIZE] = { 0 };
1059 const DWORD dwBufSize = BUFSIZE;
1060 const DWORD dwRetVal = GetTortoiseGitTempPath(dwBufSize, lpPathBuffer);
1061 if (dwRetVal > dwBufSize || (dwRetVal == 0))
1062 path.Empty();
1063 path.Format(L"%s", lpPathBuffer);
1065 CString GetTempFile()
1067 wchar_t lpPathBuffer[BUFSIZE] = { 0 };
1068 const DWORD dwBufSize = BUFSIZE;
1069 wchar_t szTempName[BUFSIZE] = { 0 };
1071 auto dwRetVal = GetTortoiseGitTempPath(dwBufSize, lpPathBuffer);
1072 if (dwRetVal > dwBufSize || (dwRetVal == 0))
1073 return L"";
1075 // Create a temporary file.
1076 if (!GetTempFileName(lpPathBuffer, // directory for tmp files
1077 TEXT("Patch"), // temp file name prefix
1078 0, // create unique name
1079 szTempName)) // buffer for name
1080 return L"";
1082 return CString(szTempName);
1085 DWORD GetTortoiseGitTempPath(DWORD nBufferLength, LPWSTR lpBuffer)
1087 const DWORD result = ::GetTempPath(nBufferLength, lpBuffer);
1088 if (result == 0) return 0;
1089 if (!lpBuffer || (result + 13 > nBufferLength))
1091 if (lpBuffer)
1092 lpBuffer[0] = '\0';
1093 return result + 13;
1096 wcscat_s(lpBuffer, nBufferLength, L"TortoiseGit\\");
1097 CreateDirectory(lpBuffer, nullptr);
1099 return result + 13;
1102 int CGit::RunLogFile(CString cmd, const CString &filename, CString *stdErr)
1104 PROCESS_INFORMATION pi;
1105 CAutoGeneralHandle hReadErr;
1106 if (RunAsync(cmd, pi, nullptr, hReadErr.GetPointer(), &filename))
1107 return TGIT_GIT_ERROR_CREATE_PROCESS;
1109 CAutoGeneralHandle piThread(std::move(pi.hThread));
1110 CAutoGeneralHandle piProcess(std::move(pi.hProcess));
1112 BYTE_VECTOR stderrVector;
1113 CGitCall_ByteVector pcall(L"", nullptr, &stderrVector);
1114 ASYNCREADSTDERRTHREADARGS threadArguments;
1115 threadArguments.fileHandle = hReadErr;
1116 threadArguments.pcall = &pcall;
1117 CAutoGeneralHandle thread = CreateThread(nullptr, 0, AsyncReadStdErrThread, &threadArguments, 0, nullptr);
1119 if (thread)
1121 CAutoLocker lock(m_critSecThreadMap);
1122 m_AsyncReadStdErrThreadMap[GetCurrentThreadId()] = thread;
1125 WaitForSingleObject(pi.hProcess,INFINITE);
1127 if (thread)
1129 WaitForSingleObject(thread, INFINITE);
1131 CAutoLocker lock(m_critSecThreadMap);
1132 m_AsyncReadStdErrThreadMap.erase(GetCurrentThreadId());
1135 stderrVector.push_back(0);
1136 if (stdErr)
1137 StringAppend(*stdErr, stderrVector.data(), CP_UTF8);
1139 DWORD exitcode = 0;
1140 if (!GetExitCodeProcess(pi.hProcess, &exitcode))
1142 CString err { static_cast<LPCWSTR>(CFormatMessageWrapper()) };
1143 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": could not get exit code: %s\n", static_cast<LPCWSTR>(err.Trim()));
1144 return TGIT_GIT_ERROR_GET_EXIT_CODE;
1146 else
1147 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": process exited: %d\n", exitcode);
1149 return exitcode;
1152 CAutoRepository CGit::GetGitRepository() const
1154 return CAutoRepository(GetGitPathStringA(m_CurrentDir));
1157 int CGit::GetHash(git_repository * repo, CGitHash &hash, const CString& friendname, bool skipFastCheck /* = false */)
1159 ATLASSERT(repo);
1161 // no need to parse a ref if it's already a 40-byte hash
1162 if (!skipFastCheck && CGitHash::IsValidSHA1(friendname))
1164 hash = CGitHash::FromHexStr(friendname);
1165 return 0;
1168 const int isHeadOrphan = git_repository_head_unborn(repo);
1169 if (isHeadOrphan != 0)
1171 hash.Empty();
1172 if (isHeadOrphan == 1)
1174 if (friendname == GitRev::GetHead()) // special check for unborn branch: if not requesting HEAD, do normal commit lookup
1175 return 0;
1177 else
1178 return -1;
1181 CAutoObject gitObject;
1182 if (git_revparse_single(gitObject.GetPointer(), repo, CUnicodeUtils::GetUTF8(friendname)))
1183 return -1;
1185 hash = git_object_id(gitObject);
1187 return 0;
1190 int CGit::GetHash(CGitHash &hash, const CString& friendname)
1192 // no need to parse a ref if it's already a 40-byte hash
1193 if (CGitHash::IsValidSHA1(friendname))
1195 hash = CGitHash::FromHexStr(friendname);
1196 return 0;
1199 if (m_IsUseLibGit2)
1201 CAutoRepository repo(GetGitRepository());
1202 if (!repo)
1203 return -1;
1205 return GetHash(repo, hash, friendname, true);
1207 else
1209 if (friendname.IsEmpty())
1211 gitLastErr.Empty();
1212 return -1;
1214 CString branch = FixBranchName(friendname);
1215 if (friendname == L"FETCH_HEAD" && branch.IsEmpty())
1216 branch = friendname;
1217 CString cmd;
1218 cmd.Format(L"git.exe rev-parse --verify --end-of-options %s", static_cast<LPCWSTR>(branch));
1219 gitLastErr.Empty();
1220 const int ret = Run(cmd, &gitLastErr, nullptr, CP_UTF8);
1221 hash = CGitHash::FromHexStrTry(gitLastErr.Trim());
1222 if (ret == 0)
1223 gitLastErr.Empty();
1224 else if (friendname == L"HEAD") // special check for unborn branch
1226 CString currentbranch;
1227 if (GetCurrentBranchFromFile(m_CurrentDir, currentbranch))
1228 return -1;
1229 gitLastErr.Empty();
1230 return 0;
1232 return ret;
1236 int CGit::GetInitAddList(CTGitPathList& outputlist, bool getStagingStatus)
1238 BYTE_VECTOR cmdout;
1240 outputlist.Clear();
1241 if (Run(L"git.exe ls-files -s -t -z", &cmdout))
1242 return -1;
1244 if (outputlist.ParserFromLsFile(cmdout))
1245 return -1;
1246 for(int i = 0; i < outputlist.GetCount(); ++i)
1247 const_cast<CTGitPath&>(outputlist[i]).m_Action = CTGitPath::LOGACTIONS_ADDED;
1249 if (getStagingStatus)
1251 BYTE_VECTOR cmdunstagedout;
1252 for (int i = 0; i < outputlist.GetCount(); ++i)
1253 const_cast<CTGitPath&>(outputlist[i]).m_stagingStatus = CTGitPath::StagingStatus::TotallyStaged;
1254 if (Run(L"git.exe diff-files --raw --numstat -C -M -z --", &cmdunstagedout))
1255 return -1;
1257 CTGitPathList unstaged;
1258 unstaged.ParserFromLog(cmdunstagedout);
1259 // File shows up both in the output of ls-files and diff-files: partially staged (typically modified after being added)
1260 for (int j = 0; j < unstaged.GetCount(); ++j)
1262 CString path = unstaged[j].GetGitPathString();
1263 outputlist.UpdateStagingStatusFromPath(path, CTGitPath::StagingStatus::PartiallyStaged); // TODO: This is inefficient
1266 return 0;
1268 int CGit::GetCommitDiffList(const CString &rev1, const CString &rev2, CTGitPathList &outputlist, bool ignoreSpaceAtEol, bool ignoreSpaceChange, bool ignoreAllSpace , bool ignoreBlankLines)
1270 CString cmd;
1271 CString ignore;
1272 if (ignoreSpaceAtEol)
1273 ignore += L" --ignore-space-at-eol";
1274 if (ignoreSpaceChange)
1275 ignore += L" --ignore-space-change";
1276 if (ignoreAllSpace)
1277 ignore += L" --ignore-all-space";
1278 if (ignoreBlankLines)
1279 ignore += L" --ignore-blank-lines";
1281 if(rev1 == GIT_REV_ZERO || rev2 == GIT_REV_ZERO)
1283 if(rev1 == GIT_REV_ZERO)
1284 cmd.Format(L"git.exe diff -r --raw -C%d%% -M%d%% --numstat -z %s --end-of-options %s --", ms_iSimilarityIndexThreshold, ms_iSimilarityIndexThreshold, static_cast<LPCWSTR>(ignore), static_cast<LPCWSTR>(rev2));
1285 else
1286 cmd.Format(L"git.exe diff -r -R --raw -C%d%% -M%d%% --numstat -z %s --end-of-options %s --", ms_iSimilarityIndexThreshold, ms_iSimilarityIndexThreshold, static_cast<LPCWSTR>(ignore), static_cast<LPCWSTR>(rev1));
1288 else
1289 cmd.Format(L"git.exe diff-tree -r --raw -C%d%% -M%d%% --numstat -z %s --end-of-options %s %s --", ms_iSimilarityIndexThreshold, ms_iSimilarityIndexThreshold, static_cast<LPCWSTR>(ignore), static_cast<LPCWSTR>(rev2), static_cast<LPCWSTR>(rev1));
1291 BYTE_VECTOR out;
1292 if (Run(cmd, &out))
1293 return -1;
1295 return outputlist.ParserFromLog(out);
1298 int CGit::GetTagList(STRING_VECTOR &list)
1300 const size_t prevCount = list.size();
1301 if (this->m_IsUseLibGit2)
1303 CAutoRepository repo(GetGitRepository());
1304 if (!repo)
1305 return -1;
1307 CAutoStrArray tag_names;
1309 if (git_tag_list(tag_names, repo))
1310 return -1;
1312 for (size_t i = 0; i < tag_names->count; ++i)
1314 list.push_back(CUnicodeUtils::GetUnicode(tag_names->strings[i]));
1317 std::sort(list.begin() + prevCount, list.end(), g_bSortTagsReversed ? LogicalCompareReversedPredicate : LogicalComparePredicate);
1319 return 0;
1321 else
1323 gitLastErr.Empty();
1324 const int ret = Run(L"git.exe tag -l", [&](const CStringA& lineA)
1326 if (lineA.IsEmpty())
1327 return;
1328 list.push_back(CUnicodeUtils::GetUnicode(lineA));
1329 }, &gitLastErr);
1330 if (!ret)
1331 std::sort(list.begin() + prevCount, list.end(), g_bSortTagsReversed ? LogicalCompareReversedPredicate : LogicalComparePredicate);
1332 else if (ret == 1 && IsInitRepos())
1333 return 0;
1334 return ret;
1338 CString CGit::GetGitLastErr(const CString& msg)
1340 if (this->m_IsUseLibGit2)
1341 return GetLibGit2LastErr(msg);
1342 else if (gitLastErr.IsEmpty())
1343 return msg + L"\nUnknown git.exe error.";
1344 else
1346 CString lastError = gitLastErr;
1347 gitLastErr.Empty();
1348 return msg + L'\n' + lastError;
1352 CString CGit::GetGitLastErr(const CString& msg, LIBGIT2_CMD cmd)
1354 if (UsingLibGit2(cmd))
1355 return GetLibGit2LastErr(msg);
1356 else if (gitLastErr.IsEmpty())
1357 return msg + L"\nUnknown git.exe error.";
1358 else
1360 CString lastError = gitLastErr;
1361 gitLastErr.Empty();
1362 return msg + L'\n' + lastError;
1366 CString CGit::GetLibGit2LastErr()
1368 const git_error *libgit2err = git_error_last();
1369 if (libgit2err)
1371 CString lastError = CUnicodeUtils::GetUnicode(libgit2err->message);
1372 git_error_clear();
1373 return L"libgit2 returned: " + lastError;
1375 else
1376 return L"An error occoured in libgit2, but no message is available.";
1379 CString CGit::GetLibGit2LastErr(const CString& msg)
1381 if (!msg.IsEmpty())
1382 return msg + L'\n' + GetLibGit2LastErr();
1383 return GetLibGit2LastErr();
1386 CString CGit::FixBranchName_Mod(CString& branchName)
1388 if (branchName == L"FETCH_HEAD")
1389 branchName = DerefFetchHead();
1390 return branchName;
1393 CString CGit::FixBranchName(const CString& branchName)
1395 CString tempBranchName = branchName;
1396 FixBranchName_Mod(tempBranchName);
1397 return tempBranchName;
1400 bool CGit::IsBranchTagNameUnique(const CString& name)
1402 if (m_IsUseLibGit2)
1404 CAutoRepository repo(GetGitRepository());
1405 if (!repo)
1406 return true; // TODO: optimize error reporting
1408 CAutoReference tagRef;
1409 if (git_reference_lookup(tagRef.GetPointer(), repo, CUnicodeUtils::GetUTF8(L"refs/tags/" + name)))
1410 return true;
1412 CAutoReference branchRef;
1413 if (git_reference_lookup(branchRef.GetPointer(), repo, CUnicodeUtils::GetUTF8(L"refs/heads/" + name)))
1414 return true;
1416 return false;
1418 // else
1419 CString cmd;
1420 cmd.Format(L"git.exe show-ref --tags --heads -- refs/heads/%s refs/tags/%s", static_cast<LPCWSTR>(name), static_cast<LPCWSTR>(name));
1422 int refCnt = 0;
1423 Run(cmd, [&](const CStringA& lineA)
1425 if (lineA.IsEmpty())
1426 return;
1427 ++refCnt;
1430 return (refCnt <= 1);
1433 bool CGit::IsLocalBranch(const CString& shortName)
1435 STRING_VECTOR list;
1436 GetBranchList(list, nullptr, CGit::BRANCH_LOCAL);
1437 return std::find(list.cbegin(), list.cend(), shortName) != list.cend();
1440 bool CGit::BranchTagExists(const CString& name, bool isBranch /*= true*/)
1442 if (m_IsUseLibGit2)
1444 CAutoRepository repo(GetGitRepository());
1445 if (!repo)
1446 return false; // TODO: optimize error reporting
1448 CString prefix;
1449 if (isBranch)
1450 prefix = L"refs/heads/";
1451 else
1452 prefix = L"refs/tags/";
1454 CAutoReference ref;
1455 if (git_reference_lookup(ref.GetPointer(), repo, CUnicodeUtils::GetUTF8(prefix + name)))
1456 return false;
1458 return true;
1460 // else
1461 CString cmd, output;
1463 cmd = L"git.exe show-ref ";
1464 if (isBranch)
1465 cmd += L"--heads ";
1466 else
1467 cmd += L"--tags ";
1469 cmd += L"-- refs/heads/" + name;
1470 cmd += L" refs/tags/" + name;
1472 if (!Run(cmd, &output, nullptr, CP_UTF8))
1474 if (!output.IsEmpty())
1475 return true;
1478 return false;
1481 CString CGit::DerefFetchHead()
1483 CString dotGitPath;
1484 GitAdminDir::GetWorktreeAdminDirPath(m_CurrentDir, dotGitPath);
1485 std::ifstream fetchHeadFile((dotGitPath + L"FETCH_HEAD").GetString(), std::ios::in | std::ios::binary);
1486 int forMergeLineCount = 0;
1487 std::string line;
1488 std::string hashToReturn;
1489 while(getline(fetchHeadFile, line))
1491 //Tokenize this line
1492 std::string::size_type prevPos = 0;
1493 std::string::size_type pos = line.find('\t');
1494 if(pos == std::string::npos) continue; //invalid line
1496 std::string hash = line.substr(0, pos);
1497 ++pos; prevPos = pos; pos = line.find('\t', pos); if(pos == std::string::npos) continue;
1499 bool forMerge = pos == prevPos;
1500 ++pos; prevPos = pos; pos = line.size(); if(pos == std::string::npos) continue;
1502 //std::string remoteBranch = line.substr(prevPos, pos - prevPos);
1504 //Process this line
1505 if(forMerge)
1507 hashToReturn = hash;
1508 ++forMergeLineCount;
1509 if(forMergeLineCount > 1)
1510 return L""; //More then 1 branch to merge found. Octopus merge needed. Cannot pick single ref from FETCH_HEAD
1514 return CUnicodeUtils::GetUnicode(hashToReturn.c_str());
1517 int CGit::GetBranchList(STRING_VECTOR &list, int *current, BRANCH_TYPE type, bool skipCurrent /*false*/)
1519 const size_t prevCount = list.size();
1520 int ret = 0;
1521 CString cur;
1522 bool headIsDetached = false;
1523 if (m_IsUseLibGit2)
1525 CAutoRepository repo(GetGitRepository());
1526 if (!repo)
1527 return -1;
1529 if (git_repository_head_detached(repo) == 1)
1530 headIsDetached = true;
1532 if ((type & (BRANCH_LOCAL | BRANCH_REMOTE)) != 0)
1534 git_branch_t flags = GIT_BRANCH_LOCAL;
1535 if ((type & BRANCH_LOCAL) && (type & BRANCH_REMOTE))
1536 flags = GIT_BRANCH_ALL;
1537 else if (type & BRANCH_REMOTE)
1538 flags = GIT_BRANCH_REMOTE;
1540 CAutoBranchIterator it;
1541 if (git_branch_iterator_new(it.GetPointer(), repo, flags))
1542 return -1;
1544 CAutoReference ref;
1545 git_branch_t branchType;
1546 while (git_branch_next(ref.GetPointer(), &branchType, it) == 0)
1548 const char * name = nullptr;
1549 if (git_branch_name(&name, ref))
1550 continue;
1552 CString branchname = CUnicodeUtils::GetUnicode(name);
1553 if (branchType & GIT_BRANCH_REMOTE)
1554 list.push_back(L"remotes/" + branchname);
1555 else
1557 if (git_branch_is_head(ref))
1559 if (skipCurrent)
1560 continue;
1561 cur = branchname;
1563 list.push_back(branchname);
1568 else
1570 CString cmd = L"git.exe branch --no-color";
1572 if ((type & BRANCH_ALL) == BRANCH_ALL)
1573 cmd += L" -a";
1574 else if (type & BRANCH_REMOTE)
1575 cmd += L" -r";
1577 ret = Run(cmd, [&](CStringA lineA)
1579 lineA.Trim(" \r\n\t");
1580 if (lineA.IsEmpty())
1581 return;
1582 if (lineA.Find(" -> ") >= 0)
1583 return; // skip something like: refs/origin/HEAD -> refs/origin/master
1585 CString branch = CUnicodeUtils::GetUnicode(lineA);
1586 if (lineA[0] == '*')
1588 if (skipCurrent)
1589 return;
1590 branch = branch.Mid(static_cast<int>(wcslen(L"* ")));
1591 cur = branch;
1593 // check whether HEAD is detached
1594 CString currentHead;
1595 if (branch[0] == L'(' && GetCurrentBranchFromFile(m_CurrentDir, currentHead) == 1)
1597 headIsDetached = true;
1598 return;
1601 else if (lineA[0] == '+') // since Git 2.23 branches that are checked out in other worktrees connected to the same repository prefixed with '+'
1602 branch = branch.Mid(static_cast<int>(wcslen(L"+ ")));
1603 if ((type & BRANCH_REMOTE) != 0 && (type & BRANCH_LOCAL) == 0)
1604 branch = L"remotes/" + branch;
1605 list.push_back(branch);
1607 if (ret == 1 && IsInitRepos())
1608 return 0;
1611 if(type & BRANCH_FETCH_HEAD && !DerefFetchHead().IsEmpty())
1612 list.push_back(L"FETCH_HEAD");
1614 std::sort(list.begin() + prevCount, list.end(), LogicalCompareBranchesPredicate);
1616 if (current && !headIsDetached && !skipCurrent)
1618 for (unsigned int i = 0; i < list.size(); ++i)
1620 if (list[i] == cur)
1622 *current = i;
1623 break;
1628 return ret;
1631 int CGit::GetRefsCommitIsOn(STRING_VECTOR& list, const CGitHash& hash, bool includeTags, bool includeBranches, BRANCH_TYPE type)
1633 if (!includeTags && !includeBranches || hash.IsEmpty())
1634 return 0;
1636 const size_t prevCount = list.size();
1637 if (UsingLibGit2(GIT_CMD_BRANCH_CONTAINS))
1639 CAutoRepository repo(GetGitRepository());
1640 if (!repo)
1641 return -1;
1643 CAutoReferenceIterator it;
1644 if (git_reference_iterator_new(it.GetPointer(), repo))
1645 return -1;
1647 auto checkDescendent = [&list, &hash, &repo](const git_oid* oid, const git_reference* ref) {
1648 if (!oid)
1649 return;
1650 if (git_oid_equal(oid, hash) || git_graph_descendant_of(repo, oid, hash) == 1)
1652 const char* name = git_reference_name(ref);
1653 if (!name)
1654 return;
1656 list.push_back(CUnicodeUtils::GetUnicode(name));
1660 CAutoReference ref;
1661 while (git_reference_next(ref.GetPointer(), it) == 0)
1663 if (git_reference_is_tag(ref))
1665 if (!includeTags)
1666 continue;
1668 CAutoTag tag;
1669 if (git_tag_lookup(tag.GetPointer(), repo, git_reference_target(ref)) == 0)
1671 CAutoObject obj;
1672 if (git_tag_peel(obj.GetPointer(), tag) < 0)
1673 continue;
1674 checkDescendent(git_object_id(obj), ref);
1675 continue;
1678 else if (git_reference_is_remote(ref))
1680 if (!includeBranches || !(type & BRANCH_REMOTE))
1681 continue;
1683 else if (git_reference_is_branch(ref))
1685 if (!includeBranches || !(type & BRANCH_LOCAL))
1686 continue;
1688 else
1689 continue;
1691 if (git_reference_type(ref) == GIT_REFERENCE_SYMBOLIC)
1693 CAutoReference peeledRef;
1694 if (git_reference_resolve(peeledRef.GetPointer(), ref) < 0)
1695 continue;
1697 checkDescendent(git_reference_target(peeledRef), ref);
1698 continue;
1701 checkDescendent(git_reference_target(ref), ref);
1704 else
1706 if (includeBranches)
1708 CString cmd = L"git.exe branch --no-color";
1709 if ((type & BRANCH_ALL) == BRANCH_ALL)
1710 cmd += L" -a";
1711 else if (type & BRANCH_REMOTE)
1712 cmd += L" -r";
1713 cmd += L" --contains " + hash.ToString();
1715 if (Run(cmd, [&](CStringA lineA)
1717 lineA.Trim(" \r\n\t");
1718 if (lineA.IsEmpty())
1719 return;
1720 if (lineA.Find(" -> ") >= 0) // normalize symbolic refs: "refs/origin/HEAD -> refs/origin/master" to "refs/origin/HEAD"
1721 lineA.Truncate(lineA.Find(" -> "));
1723 CString branch = CUnicodeUtils::GetUnicode(lineA);
1724 if (lineA[0] == '*')
1726 branch = branch.Mid(static_cast<int>(wcslen(L"* ")));
1727 CString currentHead;
1728 if (branch[0] == L'(' && GetCurrentBranchFromFile(m_CurrentDir, currentHead) == 1)
1729 return;
1732 if ((type & BRANCH_REMOTE) != 0 && (type & BRANCH_LOCAL) == 0)
1733 branch = L"refs/remotes/" + branch;
1734 else if (CStringUtils::StartsWith(branch, L"remotes/"))
1735 branch = L"refs/" + branch;
1736 else
1737 branch = L"refs/heads/" + branch;
1738 list.push_back(branch);
1740 return -1;
1743 if (includeTags)
1745 CString cmd = L"git.exe tag --contains " + hash.ToString();
1746 if (Run(cmd, [&list](CStringA lineA)
1748 if (lineA.IsEmpty())
1749 return;
1750 list.push_back(L"refs/tags/" + CUnicodeUtils::GetUnicode(lineA));
1752 return -1;
1756 std::sort(list.begin() + prevCount, list.end(), LogicalCompareBranchesPredicate);
1757 list.erase(std::unique(list.begin() + prevCount, list.end()), list.end());
1759 return 0;
1762 int CGit::GetRemoteList(STRING_VECTOR &list)
1764 const size_t prevCount = list.size();
1765 if (this->m_IsUseLibGit2)
1767 CAutoRepository repo(GetGitRepository());
1768 if (!repo)
1769 return -1;
1771 CAutoStrArray remotes;
1772 if (git_remote_list(remotes, repo))
1773 return -1;
1775 for (size_t i = 0; i < remotes->count; ++i)
1777 list.push_back(CUnicodeUtils::GetUnicode(remotes->strings[i]));
1780 std::sort(list.begin() + prevCount, list.end(), LogicalComparePredicate);
1782 return 0;
1785 gitLastErr.Empty();
1786 return Run(L"git.exe remote", [&](const CStringA& lineA)
1788 if (lineA.IsEmpty())
1789 return;
1790 list.push_back(CUnicodeUtils::GetUnicode(lineA));
1791 }, &gitLastErr);
1794 int CGit::GetRemoteRefs(const CString& remote, REF_VECTOR& list, bool includeTags, bool includeBranches)
1796 if (!includeTags && !includeBranches)
1797 return 0;
1799 const size_t prevCount = list.size();
1800 if (UsingLibGit2(GIT_CMD_FETCH))
1802 CAutoRepository repo(GetGitRepository());
1803 if (!repo)
1804 return -1;
1806 CStringA remoteA = CUnicodeUtils::GetUTF8(remote);
1807 CAutoRemote gitremote;
1808 // first try with a named remote (e.g. "origin")
1809 if (git_remote_lookup(gitremote.GetPointer(), repo, remoteA) < 0)
1811 // retry with repository located at a specific url
1812 if (git_remote_create_anonymous(gitremote.GetPointer(), repo, remoteA) < 0)
1813 return -1;
1816 git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT;
1817 callbacks.credentials = g_Git2CredCallback;
1818 callbacks.certificate_check = g_Git2CheckCertificateCallback;
1819 git_proxy_options proxy = GIT_PROXY_OPTIONS_INIT;
1820 proxy.type = GIT_PROXY_AUTO;
1821 if (git_remote_connect(gitremote, GIT_DIRECTION_FETCH, &callbacks, &proxy, nullptr) < 0)
1822 return -1;
1824 const git_remote_head** heads = nullptr;
1825 size_t size = 0;
1826 if (git_remote_ls(&heads, &size, gitremote) < 0)
1827 return -1;
1829 for (size_t i = 0; i < size; i++)
1831 CString ref = CUnicodeUtils::GetUnicode(heads[i]->name);
1832 CString shortname;
1833 if (GetShortName(ref, shortname, L"refs/tags/"))
1835 if (!includeTags)
1836 continue;
1838 else
1840 if (!includeBranches)
1841 continue;
1842 if (!GetShortName(ref, shortname, L"refs/heads/"))
1843 shortname = ref;
1845 if (includeTags && includeBranches)
1846 list.emplace_back(TGitRef{ ref, &heads[i]->oid });
1847 else
1848 list.emplace_back(TGitRef{ shortname, &heads[i]->oid });
1850 std::sort(list.begin() + prevCount, list.end(), g_bSortTagsReversed && includeTags && !includeBranches ? LogicalCompareReversedPredicate : LogicalComparePredicate);
1851 return 0;
1854 CString cmd;
1855 cmd.Format(L"git.exe ls-remote%s -- \"%s\"", (includeTags && !includeBranches) ? L" -t" : L" --refs", static_cast<LPCWSTR>(remote));
1856 gitLastErr = cmd + L'\n';
1857 if (Run(
1858 cmd, [&](CStringA lineA) {
1859 CGitHash hash = CGitHash::FromHexStr(lineA.Left(GIT_HASH_SIZE * 2));
1860 lineA = lineA.Mid(GIT_HASH_SIZE * 2 + static_cast<int>(wcslen(L"\t"))); // sha1, tab
1861 if (lineA.IsEmpty())
1862 return;
1863 CString ref = CUnicodeUtils::GetUnicode(lineA);
1864 CString shortname;
1865 if (GetShortName(ref, shortname, L"refs/tags/"))
1867 if (!includeTags)
1868 return;
1870 else
1872 if (!includeBranches)
1873 return;
1874 if (!GetShortName(ref, shortname, L"refs/heads/"))
1875 shortname = ref;
1877 if (includeTags && includeBranches)
1878 list.emplace_back(TGitRef{ ref, hash });
1879 else if (includeTags && CStringUtils::EndsWith(ref, L"^{}"))
1880 list.emplace_back(TGitRef{ shortname + L"^{}", hash });
1881 else
1882 list.emplace_back(TGitRef{ shortname, hash });
1884 &gitLastErr))
1885 return -1;
1886 std::sort(list.begin() + prevCount, list.end(), g_bSortTagsReversed && includeTags && !includeBranches ? LogicalCompareReversedPredicate : LogicalComparePredicate);
1887 return 0;
1890 int CGit::DeleteRemoteRefs(const CString& sRemote, const STRING_VECTOR& list)
1892 if (UsingLibGit2(GIT_CMD_PUSH))
1894 CAutoRepository repo(GetGitRepository());
1895 if (!repo)
1896 return -1;
1898 CStringA remoteA = CUnicodeUtils::GetUTF8(sRemote);
1899 CAutoRemote remote;
1900 if (git_remote_lookup(remote.GetPointer(), repo, remoteA) < 0)
1901 return -1;
1903 git_push_options pushOpts = GIT_PUSH_OPTIONS_INIT;
1904 git_remote_callbacks& callbacks = pushOpts.callbacks;
1905 callbacks.credentials = g_Git2CredCallback;
1906 callbacks.certificate_check = g_Git2CheckCertificateCallback;
1907 std::vector<CStringA> refspecs;
1908 refspecs.reserve(list.size());
1909 std::transform(list.cbegin(), list.cend(), std::back_inserter(refspecs), [](auto& ref) { return CUnicodeUtils::GetUTF8(L":" + ref); });
1911 std::vector<char*> vc;
1912 vc.reserve(refspecs.size());
1913 std::transform(refspecs.begin(), refspecs.end(), std::back_inserter(vc), [](CStringA& s) -> char* { return s.GetBuffer(); });
1914 git_strarray specs = { vc.data(), vc.size() };
1916 if (git_remote_push(remote, &specs, &pushOpts) < 0)
1917 return -1;
1919 else
1921 CMassiveGitTaskBase mgtPush(L"push " + sRemote, FALSE);
1922 for (const auto& ref : list)
1923 mgtPush.AddFile(L':' + ref);
1925 BOOL cancel = FALSE;
1926 mgtPush.Execute(cancel);
1929 return 0;
1932 int libgit2_addto_list_each_ref_fn(git_reference* rawref, void* payload)
1934 CAutoReference ref{ std::move(rawref) };
1935 auto list = static_cast<STRING_VECTOR*>(payload);
1936 list->push_back(CUnicodeUtils::GetUnicode(git_reference_name(ref)));
1937 return 0;
1940 int CGit::GetRefList(STRING_VECTOR &list)
1942 const size_t prevCount = list.size();
1943 if (this->m_IsUseLibGit2)
1945 CAutoRepository repo(GetGitRepository());
1946 if (!repo)
1947 return -1;
1949 if (git_reference_foreach(repo, libgit2_addto_list_each_ref_fn, &list))
1950 return -1;
1952 std::sort(list.begin() + prevCount, list.end(), LogicalComparePredicate);
1954 return 0;
1957 gitLastErr.Empty();
1958 const int ret = Run(L"git.exe show-ref -d", [&](const CStringA& lineA)
1960 const int start = lineA.Find(L' ');
1961 ASSERT(start == 2 * GIT_HASH_SIZE);
1962 if (start <= 0)
1963 return;
1965 CString name = CUnicodeUtils::GetUnicode(lineA.Mid(start + 1));
1966 if (list.empty() || name != *list.crbegin() + L"^{}")
1967 list.push_back(name);
1968 }, &gitLastErr);
1969 if (!ret)
1970 std::sort(list.begin() + prevCount, list.end(), LogicalComparePredicate);
1971 else if (ret == 1 && IsInitRepos())
1972 return 0;
1973 return ret;
1976 struct map_each_ref_payload {
1977 git_repository* repo = nullptr;
1978 MAP_HASH_NAME* map = nullptr;
1981 int libgit2_addto_map_each_ref_fn(git_reference* rawref, void* payload)
1983 CAutoReference ref{ std::move(rawref) };
1984 auto payloadContent = static_cast<map_each_ref_payload*>(payload);
1986 CString str = CUnicodeUtils::GetUnicode(git_reference_name(ref));
1988 CAutoObject gitObject;
1989 if (git_revparse_single(gitObject.GetPointer(), payloadContent->repo, git_reference_name(ref)))
1990 return (git_reference_is_remote(ref) && git_reference_type(ref) == GIT_REFERENCE_SYMBOLIC) ? 0 : 1; // don't bail out for symbolic remote references ("git.exe show-ref -d" also doesn't complain), cf. issue #2926
1992 if (git_object_type(gitObject) == GIT_OBJECT_TAG)
1994 str += L"^{}"; // deref tag
1995 CAutoObject derefedTag;
1996 if (git_tag_target(derefedTag.GetPointer(), reinterpret_cast<git_tag*>(static_cast<git_object*>(gitObject))))
1997 return 1;
1998 gitObject.Swap(derefedTag);
2001 (*payloadContent->map)[git_object_id(gitObject)].push_back(str);
2003 return 0;
2005 int CGit::GetMapHashToFriendName(git_repository* repo, MAP_HASH_NAME &map)
2007 ATLASSERT(repo);
2009 map_each_ref_payload payloadContent = { repo, &map };
2011 if (git_reference_foreach(repo, libgit2_addto_map_each_ref_fn, &payloadContent))
2012 return -1;
2014 for (auto it = map.begin(); it != map.end(); ++it)
2016 std::sort(it->second.begin(), it->second.end());
2019 return 0;
2022 int CGit::GetMapHashToFriendName(MAP_HASH_NAME &map)
2024 if (this->m_IsUseLibGit2)
2026 CAutoRepository repo(GetGitRepository());
2027 if (!repo)
2028 return -1;
2030 return GetMapHashToFriendName(repo, map);
2033 gitLastErr.Empty();
2034 const int ret = Run(L"git.exe show-ref -d", [&](const CStringA& lineA)
2036 const int start = lineA.Find(L' ');
2037 ASSERT(start == 2 * GIT_HASH_SIZE);
2038 if (start <= 0)
2039 return;
2041 CGitHash hash = CGitHash::FromHexStr(lineA);
2042 map[hash].push_back(CUnicodeUtils::GetUnicode(lineA.Mid(start + 1)));
2043 }, &gitLastErr);
2045 if (ret == 1 && IsInitRepos())
2046 return 0;
2047 return ret;
2050 int CGit::GuessRefForHash(CString& ref, const CGitHash& hash)
2052 MAP_HASH_NAME map;
2053 if (GetMapHashToFriendName(map))
2054 return -1;
2056 auto it = map.find(hash);
2057 if (it == map.cend())
2059 ref = hash.ToString(GetShortHASHLength());
2060 return 1;
2063 const auto& reflist = it->second;
2064 for (const auto& reftype : { L"refs/heads/", L"refs/remotes/", L"refs/tags/" })
2066 auto found = std::find_if(reflist.cbegin(), reflist.cend(), [&reftype](const auto& ref) { return CStringUtils::StartsWith(ref, reftype); });
2067 if (found == reflist.cend())
2068 continue;
2070 GetShortName(*found, ref, reftype);
2071 return 0;
2074 ref = hash.ToString(GetShortHASHLength());
2075 return 1;
2078 int CGit::GetBranchDescriptions(MAP_STRING_STRING& map)
2080 CAutoConfig config(true);
2081 if (git_config_add_file_ondisk(config, CGit::GetGitPathStringA(GetGitLocalConfig()), GIT_CONFIG_LEVEL_LOCAL, nullptr, FALSE) < 0)
2082 return -1;
2083 return git_config_foreach_match(config, "^branch\\..*\\.description$", [](const git_config_entry* entry, void* data)
2085 auto descriptions = static_cast<MAP_STRING_STRING*>(data);
2086 CString key = CUnicodeUtils::GetUnicode(entry->name);
2087 // extract branch name from config key
2088 key = key.Mid(static_cast<int>(wcslen(L"branch.")), key.GetLength() - static_cast<int>(wcslen(L"branch.")) - static_cast<int>(wcslen(L".description")));
2089 descriptions->insert(std::make_pair(key, CUnicodeUtils::GetUnicode(entry->value)));
2090 return 0;
2091 }, &map);
2094 static void SetLibGit2SearchPath(int level, const CString &value)
2096 CStringA valueA = CUnicodeUtils::GetUTF8(value);
2097 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, level, static_cast<LPCSTR>(valueA));
2100 static void SetLibGit2TemplatePath(const CString &value)
2102 CStringA valueA = CUnicodeUtils::GetUTF8(value);
2103 git_libgit2_opts(GIT_OPT_SET_TEMPLATE_PATH, static_cast<LPCSTR>(valueA));
2106 int CGit::FindAndSetGitExePath(BOOL bFallback)
2108 CRegString msysdir = CRegString(REG_MSYSGIT_PATH, L"", FALSE);
2109 CString str = msysdir;
2110 if (!str.IsEmpty() && PathFileExists(str + L"\\git.exe"))
2112 CGit::ms_LastMsysGitDir = str;
2113 return TRUE;
2116 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": git.exe not exists: %s\n", static_cast<LPCWSTR>(CGit::ms_LastMsysGitDir));
2117 if (!bFallback)
2118 return FALSE;
2120 // first, search PATH if git/bin directory is already present
2121 if (FindGitPath())
2123 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": FindGitPath() => %s\n", static_cast<LPCWSTR>(CGit::ms_LastMsysGitDir));
2124 msysdir = CGit::ms_LastMsysGitDir;
2125 msysdir.write();
2126 return TRUE;
2129 if (FindGitForWindows(str))
2131 msysdir = str;
2132 CGit::ms_LastMsysGitDir = str;
2133 msysdir.write();
2134 return TRUE;
2137 return FALSE;
2140 BOOL CGit::CheckMsysGitDir(BOOL bFallback)
2142 if (m_bInitialized)
2143 return TRUE;
2145 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": CheckMsysGitDir(%d)\n", bFallback);
2146 this->m_Environment.clear();
2147 m_Environment.CopyProcessEnvironment();
2149 m_Environment.SetEnv(L"TGIT_INITIATED_CALL", L"1");
2151 // Sanitize GIT_* environment variables, cf. https://github.com/git-for-windows/build-extra/pull/529 and git/environment.h
2152 m_Environment.SetEnv(L"GIT_INDEX_FILE", nullptr);
2153 m_Environment.SetEnv(L"GIT_INDEX_VERSION", nullptr);
2154 m_Environment.SetEnv(L"GIT_OBJECT_DIRECTORY", nullptr);
2155 m_Environment.SetEnv(L"GIT_ALTERNATE_OBJECT_DIRECTORIES", nullptr);
2156 m_Environment.SetEnv(L"GIT_DIR", nullptr);
2157 m_Environment.SetEnv(L"GIT_WORK_TREE", nullptr);
2158 m_Environment.SetEnv(L"GIT_NAMESPACE", nullptr);
2159 m_Environment.SetEnv(L"GIT_CEILING_DIRECTORIES", nullptr);
2160 m_Environment.SetEnv(L"GIT_DISCOVERY_ACROSS_FILESYSTEM", nullptr);
2161 m_Environment.SetEnv(L"GIT_COMMON_DIR", nullptr);
2162 m_Environment.SetEnv(L"GIT_DEFAULT_HASH", nullptr);
2163 m_Environment.SetEnv(L"GIT_CONFIG", nullptr);
2164 m_Environment.SetEnv(L"GIT_CONFIG_GLOBAL", nullptr);
2165 m_Environment.SetEnv(L"GIT_CONFIG_SYSTEM", nullptr);
2166 m_Environment.SetEnv(L"GIT_CONFIG_NOSYSTEM", nullptr);
2167 m_Environment.SetEnv(L"GIT_CONFIG_COUNT", nullptr);
2168 m_Environment.SetEnv(L"GIT_ATTR_NOSYSTEM", nullptr);
2169 m_Environment.SetEnv(L"GIT_ATTR_SOURCE", nullptr);
2170 m_Environment.SetEnv(L"GIT_SHALLOW_FILE", nullptr);
2171 m_Environment.SetEnv(L"GIT_GRAFT_FILE", nullptr);
2173 // Git for Windows 2.10.1 and 2.10.2 require LC_ALL to be set, see https://tortoisegit.org/issue/2859 and https://github.com/git-for-windows/git/issues/945,
2174 // because MSys2 changed the default to "ASCII". SO, make sure we have a proper default set
2175 if (m_Environment.GetEnv(L"LC_ALL").IsEmpty())
2176 m_Environment.SetEnv(L"LC_ALL", L"C");
2178 // set HOME if not set already
2179 size_t homesize;
2180 _wgetenv_s(&homesize, nullptr, 0, L"HOME");
2181 if (!homesize)
2182 m_Environment.SetEnv(L"HOME", GetHomeDirectory());
2184 //setup ssh client
2185 CString sshclient = CRegString(L"Software\\TortoiseGit\\SSH");
2186 if (sshclient.IsEmpty())
2187 sshclient = CRegString(L"Software\\TortoiseGit\\SSH", L"", FALSE, HKEY_LOCAL_MACHINE);
2189 if(!sshclient.IsEmpty())
2191 if (ms_bCygwinGit)
2192 sshclient.Replace(L'\\', L'/');
2193 m_Environment.SetEnv(L"GIT_SSH", sshclient);
2194 if (CStringUtils::EndsWithI(sshclient, L"tortoisegitplink") || CStringUtils::EndsWithI(sshclient, L"tortoisegitplink.exe"))
2195 m_Environment.SetEnv(L"GIT_SSH_VARIANT", L"ssh");
2196 m_Environment.SetEnv(L"SVN_SSH", sshclient);
2198 else
2200 wchar_t sPlink[MAX_PATH] = { 0 };
2201 GetModuleFileName(nullptr, sPlink, _countof(sPlink));
2202 LPWSTR ptr = wcsrchr(sPlink, L'\\');
2203 if (ptr) {
2204 wcscpy_s(ptr + 1, _countof(sPlink) - (ptr - sPlink + 1), L"TortoiseGitPlink.exe");
2205 if (ms_bCygwinGit)
2206 CPathUtils::ConvertToSlash(sPlink);
2207 m_Environment.SetEnv(L"GIT_SSH", sPlink);
2208 m_Environment.SetEnv(L"GIT_SSH_VARIANT", L"ssh");
2209 m_Environment.SetEnv(L"SVN_SSH", sPlink);
2214 wchar_t sAskPass[MAX_PATH] = { 0 };
2215 GetModuleFileName(nullptr, sAskPass, _countof(sAskPass));
2216 LPWSTR ptr = wcsrchr(sAskPass, L'\\');
2217 if (ptr)
2219 wcscpy_s(ptr + 1, _countof(sAskPass) - (ptr - sAskPass + 1), L"SshAskPass.exe");
2220 if (ms_bCygwinGit)
2221 CPathUtils::ConvertToSlash(sAskPass);
2222 m_Environment.SetEnv(L"DISPLAY",L":9999");
2223 m_Environment.SetEnv(L"SSH_ASKPASS",sAskPass);
2224 m_Environment.SetEnv(L"GIT_ASKPASS",sAskPass);
2225 m_Environment.SetEnv(L"GIT_ASK_YESNO", sAskPass);
2226 m_Environment.SetEnv(L"SSH_ASKPASS_REQUIRE", L"force"); // improve compatibility with Win32-OpenSSH, see issue #3996
2230 if (!FindAndSetGitExePath(bFallback))
2231 return FALSE;
2233 CString msysGitDir;
2234 PathCanonicalize(CStrBuf(msysGitDir, MAX_PATH), CGit::ms_LastMsysGitDir + L"\\..\\");
2235 static const CString prefixes[] = { L"mingw64\\etc", L"mingw32\\etc", L"etc" };
2236 static const int prefixes_len[] = { 8, 8, 0 };
2237 for (int i = 0; i < _countof(prefixes); ++i)
2239 #ifndef _WIN64
2240 if (i == 0)
2241 continue;
2242 #endif
2243 if (PathIsDirectory(msysGitDir + prefixes[i])) {
2244 msysGitDir += prefixes[i].Left(prefixes_len[i]);
2245 break;
2248 if (ms_bMsys2Git) // in Msys2 git.exe is in usr\bin; this also need to be after the check for etc folder, as Msys2 also has mingw64\etc, but uses etc
2249 PathCanonicalize(CStrBuf(msysGitDir, MAX_PATH), CGit::ms_LastMsysGitDir + L"\\..\\..");
2250 CGit::ms_MsysGitRootDir = msysGitDir;
2252 if (static_cast<CString>(CRegString(REG_SYSTEM_GITCONFIGPATH, L"", FALSE)) != g_Git.GetGitSystemConfig())
2253 CRegString(REG_SYSTEM_GITCONFIGPATH, L"", FALSE) = g_Git.GetGitSystemConfig();
2255 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": ms_LastMsysGitDir = %s\n", static_cast<LPCWSTR>(CGit::ms_LastMsysGitDir));
2256 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": ms_MsysGitRootDir = %s\n", static_cast<LPCWSTR>(CGit::ms_MsysGitRootDir));
2257 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": System config = %s\n", static_cast<LPCWSTR>(g_Git.GetGitSystemConfig()));
2258 SetLibGit2SearchPath(GIT_CONFIG_LEVEL_PROGRAMDATA, L"");
2260 if (ms_bCygwinGit)
2261 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": CygwinHack: true\n");
2262 if (ms_bMsys2Git)
2263 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Msys2Hack: true\n");
2265 // Configure libgit2 search paths
2266 SetLibGit2SearchPath(GIT_CONFIG_LEVEL_SYSTEM, CTGitPath(g_Git.GetGitSystemConfig()).GetContainingDirectory().GetWinPathString());
2267 SetLibGit2SearchPath(GIT_CONFIG_LEVEL_GLOBAL, g_Git.GetHomeDirectory());
2268 SetLibGit2SearchPath(GIT_CONFIG_LEVEL_XDG, g_Git.GetGitGlobalXDGConfigPath());
2269 static git_smart_subtransport_definition ssh_wintunnel_subtransport_definition = { [](git_smart_subtransport **out, git_transport* owner, void*) -> int { return git_smart_subtransport_ssh_wintunnel(out, owner, FindExecutableOnPath(g_Git.m_Environment.GetEnv(L"GIT_SSH"), g_Git.m_Environment.GetEnv(L"PATH")), g_Git.m_Environment); }, 0 };
2270 git_transport_register("ssh", git_transport_smart, &ssh_wintunnel_subtransport_definition);
2271 git_transport_register("ssh+git", git_transport_smart, &ssh_wintunnel_subtransport_definition);
2272 git_transport_register("git+ssh", git_transport_smart, &ssh_wintunnel_subtransport_definition);
2273 git_libgit2_opts(GIT_OPT_SET_USER_AGENT, "TortoiseGit libgit2");
2274 if (!(ms_bCygwinGit || ms_bMsys2Git))
2275 SetLibGit2TemplatePath(CGit::ms_MsysGitRootDir + L"share\\git-core\\templates");
2276 else
2277 SetLibGit2TemplatePath(CGit::ms_MsysGitRootDir + L"usr\\share\\git-core\\templates");
2279 m_Environment.AddToPath(CGit::ms_LastMsysGitDir);
2280 m_Environment.AddToPath(static_cast<CString>(CRegString(REG_MSYSGIT_EXTRA_PATH, L"", FALSE)));
2282 #if !defined(TGITCACHE) && !defined(TORTOISESHELL)
2283 // register filter only once
2284 if (!git_filter_lookup("filter"))
2286 CString sh;
2287 for (const CString& binDirPrefix : { L"\\..\\usr\\bin", L"\\..\\bin", L"" })
2289 CString possibleShExe = CGit::ms_LastMsysGitDir + binDirPrefix + L"\\sh.exe";
2290 if (PathFileExists(possibleShExe))
2292 CString temp;
2293 PathCanonicalize(CStrBuf(temp, MAX_PATH), possibleShExe);
2294 sh.Format(L"\"%s\"", static_cast<LPCWSTR>(temp));
2295 // we need to put the usr\bin folder on the path for Git for Windows based on msys2
2296 m_Environment.AddToPath(temp.Left(temp.GetLength() - static_cast<int>(wcslen(L"\\sh.exe"))));
2297 break;
2301 // Add %GIT_EXEC_PATH% to %PATH% when launching libgit2 filter executable
2302 // It is possible that the filter points to a git subcommand, that is located at libexec\git-core
2303 CString gitExecPath = CGit::ms_MsysGitRootDir;
2304 gitExecPath.Append(L"libexec\\git-core");
2305 m_Environment.AddToPath(gitExecPath);
2307 if (git_filter_register("filter", git_filter_filter_new(sh, m_Environment), GIT_FILTER_DRIVER_PRIORITY))
2308 return FALSE;
2310 #endif
2312 m_bInitialized = true;
2313 return true;
2316 CString CGit::GetHomeDirectory() const
2318 static CString homeDirectory;
2320 if (!homeDirectory.IsEmpty())
2321 return homeDirectory;
2323 homeDirectory = m_Environment.GetEnv(L"HOME");
2324 if (!homeDirectory.IsEmpty())
2325 return homeDirectory;
2327 if (CString tmp = m_Environment.GetEnv(L"HOMEDRIVE"); !tmp.IsEmpty())
2329 if (CString tmp2 = m_Environment.GetEnv(L"HOMEPATH"); !tmp2.IsEmpty())
2331 tmp += tmp2;
2332 if (CString windowsSysDirectory; GetSystemDirectory(CStrBuf(windowsSysDirectory, 4096), 4096) != FALSE && windowsSysDirectory != tmp && PathIsDirectory(tmp))
2334 homeDirectory = tmp;
2335 return homeDirectory;
2340 if (CString tmp = m_Environment.GetEnv(L"USERPROFILE"); !tmp.IsEmpty())
2342 homeDirectory = tmp;
2343 return homeDirectory;
2346 return homeDirectory;
2349 CString CGit::GetGitLocalConfig() const
2351 CString path;
2352 GitAdminDir::GetAdminDirPath(m_CurrentDir, path);
2353 path += L"config";
2354 return path;
2357 CStringA CGit::GetGitPathStringA(const CString &path)
2359 return CUnicodeUtils::GetUTF8(CTGitPath(path).GetGitPathString());
2362 CString CGit::GetGitGlobalConfig() const
2364 return g_Git.GetHomeDirectory() + L"\\.gitconfig";
2367 CString CGit::GetGitGlobalXDGConfigPath() const
2369 if (CString xdgPath = m_Environment.GetEnv(L"XDG_CONFIG_HOME"); !xdgPath.IsEmpty())
2370 return xdgPath + L"\\git";
2371 return g_Git.GetHomeDirectory() + L"\\.config\\git";
2374 CString CGit::GetGitGlobalXDGConfig() const
2376 return g_Git.GetGitGlobalXDGConfigPath() + L"\\config";
2379 CString CGit::GetGitSystemConfig() const
2381 return wget_msysgit_etc(m_Environment);
2384 CString CGit::GetNotesRef() const
2386 return CUnicodeUtils::GetUnicode(git_default_notes_ref());
2389 BOOL CGit::CheckCleanWorkTree(bool stagedOk /* false */)
2391 if (UsingLibGit2(GIT_CMD_CHECK_CLEAN_WT))
2393 CAutoRepository repo(GetGitRepository());
2394 if (!repo)
2395 return FALSE;
2397 if (git_repository_head_unborn(repo))
2398 return FALSE;
2400 git_status_options statusopt = GIT_STATUS_OPTIONS_INIT;
2401 statusopt.show = stagedOk ? GIT_STATUS_SHOW_WORKDIR_ONLY : GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
2402 statusopt.flags = GIT_STATUS_OPT_UPDATE_INDEX | GIT_STATUS_OPT_EXCLUDE_SUBMODULES;
2404 CAutoStatusList status;
2405 if (git_status_list_new(status.GetPointer(), repo, &statusopt))
2406 return FALSE;
2408 return (0 == git_status_list_entrycount(status));
2411 if (Run(L"git.exe rev-parse --verify HEAD", nullptr, nullptr, CP_UTF8))
2412 return FALSE;
2414 if (Run(L"git.exe update-index --ignore-submodules --refresh", nullptr, nullptr, CP_UTF8))
2415 return FALSE;
2417 if (Run(L"git.exe diff-files --quiet --ignore-submodules", nullptr, nullptr, CP_UTF8))
2418 return FALSE;
2420 if (!stagedOk && Run(L"git.exe diff-index --cached --quiet HEAD --ignore-submodules --", nullptr, nullptr, CP_UTF8))
2421 return FALSE;
2423 return TRUE;
2426 BOOL CGit::IsResultingCommitBecomeEmpty(bool amend /* = false */)
2428 CString cmd;
2429 cmd.Format(L"git.exe diff-index --cached --quiet HEAD%s --", amend ? L"~1" : L"");
2430 return Run(cmd, nullptr, nullptr, CP_UTF8) == 0;
2433 int CGit::HasWorkingTreeConflicts(git_repository* repo)
2435 ATLASSERT(repo);
2437 CAutoIndex index;
2438 if (git_repository_index(index.GetPointer(), repo))
2439 return -1;
2441 return git_index_has_conflicts(index);
2444 int CGit::HasWorkingTreeConflicts()
2446 if (UsingLibGit2(GIT_CMD_CHECKCONFLICTS))
2448 CAutoRepository repo(GetGitRepository());
2449 if (!repo)
2450 return -1;
2452 return HasWorkingTreeConflicts(repo);
2455 CString output;
2456 gitLastErr.Empty();
2457 if (Run(L"git.exe ls-files -u -t -z", &output, &gitLastErr, CP_UTF8))
2458 return -1;
2460 return output.IsEmpty() ? 0 : 1;
2463 bool CGit::IsFastForward(const CString &from, const CString &to, CGitHash * commonAncestor)
2465 if (UsingLibGit2(GIT_CMD_MERGE_BASE))
2467 CAutoRepository repo(GetGitRepository());
2468 if (!repo)
2469 return false;
2471 CGitHash fromHash, toHash, baseHash;
2472 if (GetHash(repo, toHash, FixBranchName(to)))
2473 return false;
2475 if (GetHash(repo, fromHash, FixBranchName(from)))
2476 return false;
2478 git_oid baseOid;
2479 if (git_merge_base(&baseOid, repo, toHash, fromHash))
2480 return false;
2482 baseHash = baseOid;
2484 if (commonAncestor)
2485 *commonAncestor = baseHash;
2487 return fromHash == baseHash;
2489 // else
2490 CString base;
2491 CGitHash basehash,hash;
2492 CString cmd;
2493 cmd.Format(L"git.exe merge-base %s %s", static_cast<LPCWSTR>(FixBranchName(to)), static_cast<LPCWSTR>(FixBranchName(from)));
2495 gitLastErr.Empty();
2496 if (Run(cmd, &base, &gitLastErr, CP_UTF8))
2497 return false;
2498 basehash = CGitHash::FromHexStrTry(base.Trim());
2500 GetHash(hash, from);
2502 if (commonAncestor)
2503 *commonAncestor = basehash;
2505 return hash == basehash;
2508 unsigned int CGit::Hash2int(const CGitHash &hash)
2510 int ret=0;
2511 for (int i = 0; i < 4; ++i)
2513 ret = ret << 8;
2514 ret |= hash.ToRaw()[i];
2516 return ret;
2519 int CGit::RefreshGitIndex()
2521 // HACK: don't use internal update-index if we have a git-lfs enabled repository as the libgit version fails when executing the filter, issue #3220
2522 if (g_Git.m_IsUseGitDLL && !CTGitPath(g_Git.m_CurrentDir).HasLFS())
2524 CAutoLocker lock(g_Git.m_critGitDllSec);
2527 g_Git.CheckAndInitDll();
2529 int result = git_update_index();
2530 git_exit_cleanup();
2531 return result;
2533 }catch(...)
2535 git_exit_cleanup();
2536 return -1;
2540 else
2541 return Run(L"git.exe update-index --refresh", nullptr, nullptr, CP_UTF8);
2544 int CGit::GetOneFile(const CString &Refname, const CTGitPath &path, const CString &outputfile)
2546 if (UsingLibGit2(GIT_CMD_GETONEFILE))
2548 CAutoRepository repo(GetGitRepository());
2549 if (!repo)
2550 return -1;
2552 CGitHash hash;
2553 if (GetHash(repo, hash, Refname + L"^{}")) // add ^{} in order to dereference signed tags
2554 return -1;
2556 CAutoCommit commit;
2557 if (git_commit_lookup(commit.GetPointer(), repo, hash))
2558 return -1;
2560 CAutoTree tree;
2561 if (git_commit_tree(tree.GetPointer(), commit))
2562 return -1;
2564 CAutoTreeEntry entry;
2565 if (auto ret = git_tree_entry_bypath(entry.GetPointer(), tree, CUnicodeUtils::GetUTF8(path.GetGitPathString())); ret)
2566 return ret;
2568 if (git_tree_entry_filemode(entry) == GIT_FILEMODE_COMMIT)
2570 git_error_set_str(GIT_ERROR_NONE, "The requested object is a submodule and not a file.");
2571 return -1;
2574 CAutoBlob blob;
2575 if (git_tree_entry_to_object(reinterpret_cast<git_object**>(blob.GetPointer()), repo, entry))
2576 return -1;
2578 CAutoFILE file = _wfsopen(outputfile, L"wb", SH_DENYWR);
2579 if (file == nullptr)
2581 git_error_set_str(GIT_ERROR_NONE, "Could not create file.");
2582 return -1;
2584 CAutoBuf buf;
2585 git_blob_filter_options opts = GIT_BLOB_FILTER_OPTIONS_INIT;
2586 opts.flags &= ~static_cast<uint32_t>(GIT_BLOB_FILTER_CHECK_FOR_BINARY);
2587 if (git_blob_filter(buf, blob, CUnicodeUtils::GetUTF8(path.GetGitPathString()), &opts))
2588 return -1;
2589 if (fwrite(buf->ptr, sizeof(char), buf->size, file) != buf->size)
2591 git_error_set_str(GIT_ERROR_OS, "Could not write to file.");
2592 return -1;
2595 return 0;
2597 else if (g_Git.m_IsUseGitDLL)
2599 CAutoLocker lock(g_Git.m_critGitDllSec);
2602 g_Git.CheckAndInitDll();
2603 CStringA ref, patha, outa;
2604 ref = CUnicodeUtils::GetUTF8(Refname);
2605 patha = CUnicodeUtils::GetUTF8(path.GetGitPathString());
2606 outa = CUnicodeUtils::GetUTF8(outputfile);
2607 ::DeleteFile(outputfile);
2608 return git_checkout_file(ref, patha, CStrBufA(outa));
2610 catch (const char * msg)
2612 gitLastErr = L"gitdll.dll reports: " + CUnicodeUtils::GetUnicode(msg);
2613 return -1;
2615 catch (...)
2617 gitLastErr = L"An unknown gitdll.dll error occurred.";
2618 return -1;
2621 else
2623 CString cmd;
2624 cmd.Format(L"git.exe cat-file -p %s:\"%s\"", static_cast<LPCWSTR>(Refname), static_cast<LPCWSTR>(path.GetGitPathString()));
2625 gitLastErr.Empty();
2626 return RunLogFile(cmd, outputfile, &gitLastErr);
2630 void CEnvironment::clear()
2632 __super::clear();
2633 baseptr = nullptr;
2636 bool CEnvironment::empty() const
2638 return size() < 3; // three is minimum for an empty environment with an empty key and empty value: "=\0\0"
2641 CEnvironment::operator LPWSTR()
2643 if (empty())
2644 return nullptr;
2645 return data();
2648 CEnvironment::operator const LPWSTR*() const
2650 return &baseptr;
2653 void CEnvironment::CopyProcessEnvironment()
2655 if (!empty())
2656 pop_back();
2657 wchar_t* porig = GetEnvironmentStrings();
2658 const wchar_t* p = porig;
2659 while(*p !=0 || *(p+1) !=0)
2660 this->push_back(*p++);
2662 push_back(L'\0');
2663 push_back(L'\0');
2664 baseptr = data();
2666 FreeEnvironmentStrings(porig);
2669 CString CEnvironment::GetEnv(const wchar_t* name) const
2671 ASSERT(name);
2672 CString str;
2673 for (size_t i = 0; i < size(); ++i)
2675 str = &(*this)[i];
2676 int start =0;
2677 CString sname = str.Tokenize(L"=", start);
2678 if(sname.CompareNoCase(name) == 0)
2679 return &(*this)[i+start];
2680 i+=str.GetLength();
2682 return L"";
2685 void CEnvironment::SetEnv(const wchar_t* name, const wchar_t* value)
2687 ASSERT(name);
2688 unsigned int i;
2689 for (i = 0; i < size(); ++i)
2691 CString str = &(*this)[i];
2692 int start =0;
2693 CString sname = str.Tokenize(L"=", start);
2694 if(sname.CompareNoCase(name) == 0)
2695 break;
2696 i+=str.GetLength();
2699 if(i == size())
2701 if (!value) // as we haven't found the variable we want to remove, just return
2702 return;
2703 if (i == 0) // make inserting into an empty environment work
2705 this->push_back(L'\0');
2706 ++i;
2708 i -= 1; // roll back terminate \0\0
2709 this->push_back(L'\0');
2712 CEnvironment::iterator it;
2713 it=this->begin();
2714 it += i;
2716 while(*it && i<size())
2718 this->erase(it);
2719 it=this->begin();
2720 it += i;
2723 if (value == nullptr) // remove the variable
2725 this->erase(it);
2726 if (empty())
2727 baseptr = nullptr;
2728 else
2729 baseptr = data();
2730 return;
2733 while(*name)
2735 this->insert(it,*name++);
2736 ++i;
2737 it= begin()+i;
2740 this->insert(it, L'=');
2741 ++i;
2742 it= begin()+i;
2744 while(*value)
2746 this->insert(it,*value++);
2747 ++i;
2748 it= begin()+i;
2750 baseptr = data();
2753 void CEnvironment::AddToPath(CString value)
2755 value.TrimRight(L'\\');
2756 if (value.IsEmpty())
2757 return;
2759 CString path = GetEnv(L"PATH").TrimRight(L';') + L';';
2761 // do not double add paths to %PATH%
2762 if (path.Find(value + L';') >= 0 || path.Find(value + L"\\;") >= 0)
2763 return;
2765 path += value;
2767 SetEnv(L"PATH", path);
2770 int CGit::GetShortHASHLength() const
2772 return 8;
2775 CString CGit::GetShortName(const CString& ref, REF_TYPE *out_type)
2777 CString str=ref;
2778 CString shortname;
2779 REF_TYPE type = CGit::UNKNOWN;
2781 if (CGit::GetShortName(str, shortname, L"refs/heads/"))
2782 type = CGit::LOCAL_BRANCH;
2783 else if (CGit::GetShortName(str, shortname, L"refs/remotes/"))
2784 type = CGit::REMOTE_BRANCH;
2785 else if (CStringUtils::EndsWith(str, L"^{}") && CGit::GetShortName(str, shortname, L"refs/tags/"))
2786 type = CGit::ANNOTATED_TAG;
2787 else if (CGit::GetShortName(str, shortname, L"refs/tags/"))
2788 type = CGit::TAG;
2789 else if (CGit::GetShortName(str, shortname, L"refs/stash"))
2791 type = CGit::STASH;
2792 shortname = L"stash";
2794 else if (CGit::GetShortName(str, shortname, L"refs/bisect/"))
2796 CString bisectGood;
2797 CString bisectBad;
2798 g_Git.GetBisectTerms(&bisectGood, &bisectBad);
2799 wchar_t c;
2800 if (CStringUtils::StartsWith(shortname, bisectGood) && ((c = shortname.GetAt(bisectGood.GetLength())) == '-' || c == '\0'))
2802 type = CGit::BISECT_GOOD;
2803 shortname = bisectGood;
2806 if (CStringUtils::StartsWith(shortname, bisectBad) && ((c = shortname.GetAt(bisectBad.GetLength())) == '-' || c == '\0'))
2808 type = CGit::BISECT_BAD;
2809 shortname = bisectBad;
2812 if (CStringUtils::StartsWith(shortname, L"skip") && ((c = shortname.GetAt(4)) == '-' || c == '\0'))
2814 type = CGit::BISECT_SKIP;
2815 shortname = L"skip";
2818 else if (CGit::GetShortName(str, shortname, L"refs/notes/"))
2819 type = CGit::NOTES;
2820 else if (CGit::GetShortName(str, shortname, L"refs/"))
2821 type = CGit::UNKNOWN;
2822 else
2824 type = CGit::UNKNOWN;
2825 shortname = ref;
2828 if(out_type)
2829 *out_type = type;
2831 return shortname;
2834 bool CGit::UsingLibGit2(LIBGIT2_CMD cmd) const
2836 return m_IsUseLibGit2 && ((1 << cmd) & m_IsUseLibGit2_mask) ? true : false;
2839 void CGit::SetGit2CredentialCallback(void* callback)
2841 g_Git2CredCallback = static_cast<git_credential_acquire_cb>(callback);
2844 void CGit::SetGit2CertificateCheckCertificate(void* callback)
2846 g_Git2CheckCertificateCallback = static_cast<git_transport_certificate_check_cb>(callback);
2849 CString CGit::GetUnifiedDiffCmd(const CTGitPath& path, const CString& rev1, const CString& rev2, bool bMerge, bool bCombine, int diffContext, bool bNoPrefix)
2851 CString cmd;
2852 if (rev2 == GitRev::GetWorkingCopy())
2853 cmd.Format(L"git.exe diff --stat%s -p --end-of-options %s --", bNoPrefix ? L" --no-prefix" : L"", static_cast<LPCWSTR>(rev1));
2854 else if (rev1 == GitRev::GetWorkingCopy())
2855 cmd.Format(L"git.exe diff -R --stat%s -p --end-of-options %s --", bNoPrefix ? L" --no-prefix" : L"", static_cast<LPCWSTR>(rev2));
2856 else
2858 CString merge;
2859 if (bMerge)
2860 merge += L" -m";
2862 if (bCombine)
2863 merge += L" -c";
2865 CString unified;
2866 if (diffContext >= 0)
2867 unified.Format(L" --unified=%d", diffContext);
2868 cmd.Format(L"git.exe diff-tree -r -p%s%s --stat%s --end-of-options %s %s --", static_cast<LPCWSTR>(merge), static_cast<LPCWSTR>(unified), bNoPrefix ? L" --no-prefix" : L"", static_cast<LPCWSTR>(rev1), static_cast<LPCWSTR>(rev2));
2871 if (!path.IsEmpty())
2873 cmd += L" \"";
2874 cmd += path.GetGitPathString();
2875 cmd += L'"';
2878 return cmd;
2881 static void UnifiedDiffStatToFile(const git_buf* text, void* payload)
2883 ATLASSERT(payload && text);
2884 auto file = static_cast<FILE*>(payload);
2885 fwrite(text->ptr, 1, text->size, file);
2886 fwrite("\n", 1, 1, file);
2889 static int UnifiedDiffToFile(const git_diff_delta * /* delta */, const git_diff_hunk * /* hunk */, const git_diff_line * line, void *payload)
2891 ATLASSERT(payload && line);
2892 auto file = static_cast<FILE*>(payload);
2893 if (line->origin == GIT_DIFF_LINE_CONTEXT || line->origin == GIT_DIFF_LINE_ADDITION || line->origin == GIT_DIFF_LINE_DELETION)
2894 fwrite(&line->origin, 1, 1, file);
2895 fwrite(line->content, 1, line->content_len, file);
2896 return 0;
2899 static int resolve_to_tree(git_repository *repo, const char *identifier, git_tree **tree)
2901 ATLASSERT(repo && identifier && tree);
2903 /* try to resolve identifier */
2904 CAutoObject obj;
2905 if (git_revparse_single(obj.GetPointer(), repo, identifier))
2906 return -1;
2908 if (obj == nullptr)
2909 return GIT_ENOTFOUND;
2911 int err = 0;
2912 switch (git_object_type(obj))
2914 case GIT_OBJECT_TREE:
2915 *tree = reinterpret_cast<git_tree*>(obj.Detach());
2916 break;
2917 case GIT_OBJECT_COMMIT:
2918 err = git_commit_tree(tree, reinterpret_cast<git_commit*>(static_cast<git_object*>(obj)));
2919 break;
2920 default:
2921 err = GIT_ENOTFOUND;
2924 return err;
2927 /* use libgit2 get unified diff */
2928 static int GetUnifiedDiffLibGit2(const CTGitPath& path, const CString& revOld, const CString& revNew, std::function<void(const git_buf*, void*)> statCallback, git_diff_line_cb callback, void* data, bool /* bMerge */, bool bNoPrefix)
2930 CStringA tree1 = CUnicodeUtils::GetUTF8(revNew);
2931 CStringA tree2 = CUnicodeUtils::GetUTF8(revOld);
2933 CAutoRepository repo(g_Git.GetGitRepository());
2934 if (!repo)
2935 return -1;
2937 int isHeadOrphan = git_repository_head_unborn(repo);
2938 if (isHeadOrphan == 1)
2939 return 0;
2940 else if (isHeadOrphan != 0)
2941 return -1;
2943 git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
2944 CStringA pathA = CUnicodeUtils::GetUTF8(path.GetGitPathString());
2945 char *buf = pathA.GetBuffer();
2946 if (!pathA.IsEmpty())
2948 opts.pathspec.strings = &buf;
2949 opts.pathspec.count = 1;
2951 if (bNoPrefix)
2953 opts.new_prefix = "";
2954 opts.old_prefix = "";
2956 CAutoDiff diff;
2958 if (revNew == GitRev::GetWorkingCopy() || revOld == GitRev::GetWorkingCopy())
2960 CAutoTree t1;
2961 CAutoDiff diff2;
2963 if (revNew != GitRev::GetWorkingCopy() && resolve_to_tree(repo, tree1, t1.GetPointer()))
2964 return -1;
2966 if (revOld != GitRev::GetWorkingCopy() && resolve_to_tree(repo, tree2, t1.GetPointer()))
2967 return -1;
2969 if (git_diff_tree_to_index(diff.GetPointer(), repo, t1, nullptr, &opts))
2970 return -1;
2972 if (git_diff_index_to_workdir(diff2.GetPointer(), repo, nullptr, &opts))
2973 return -1;
2975 if (git_diff_merge(diff, diff2))
2976 return -1;
2978 else
2980 if (tree1.IsEmpty() && tree2.IsEmpty())
2981 return -1;
2983 if (tree1.IsEmpty())
2985 tree1 = tree2;
2986 tree2.Empty();
2989 CAutoTree t1;
2990 CAutoTree t2;
2991 if (!tree1.IsEmpty() && resolve_to_tree(repo, tree1, t1.GetPointer()))
2992 return -1;
2994 if (tree2.IsEmpty())
2996 /* don't check return value, there are not parent commit at first commit*/
2997 resolve_to_tree(repo, tree1 + "~1", t2.GetPointer());
2999 else if (resolve_to_tree(repo, tree2, t2.GetPointer()))
3000 return -1;
3001 if (git_diff_tree_to_tree(diff.GetPointer(), repo, t2, t1, &opts))
3002 return -1;
3005 CAutoDiffStats stats;
3006 if (git_diff_get_stats(stats.GetPointer(), diff))
3007 return -1;
3008 CAutoBuf statBuf;
3009 if (git_diff_stats_to_buf(statBuf, stats, GIT_DIFF_STATS_FULL, 0))
3010 return -1;
3011 statCallback(statBuf, data);
3013 for (size_t i = 0; i < git_diff_num_deltas(diff); ++i)
3015 CAutoPatch patch;
3016 if (git_patch_from_diff(patch.GetPointer(), diff, i))
3017 return -1;
3019 if (git_patch_print(patch, callback, data))
3020 return -1;
3023 pathA.ReleaseBuffer();
3025 return 0;
3028 int CGit::GetUnifiedDiff(const CTGitPath& path, const CString& rev1, const CString& rev2, CString patchfile, bool bMerge, bool bCombine, int diffContext, bool bNoPrefix)
3030 if (UsingLibGit2(GIT_CMD_DIFF))
3032 CAutoFILE file = _wfsopen(patchfile, L"wb", SH_DENYRW);
3033 if (!file)
3034 return -1;
3035 return GetUnifiedDiffLibGit2(path, rev1, rev2, UnifiedDiffStatToFile, UnifiedDiffToFile, file, bMerge, bNoPrefix);
3037 else
3039 CString cmd;
3040 cmd = GetUnifiedDiffCmd(path, rev1, rev2, bMerge, bCombine, diffContext, bNoPrefix);
3041 gitLastErr.Empty();
3042 return RunLogFile(cmd, patchfile, &gitLastErr);
3046 static void UnifiedDiffStatToStringA(const git_buf* text, void* payload)
3048 ATLASSERT(payload && text);
3049 auto str = static_cast<CStringA*>(payload);
3050 str->Append(text->ptr, SafeSizeToInt(text->size));
3051 str->AppendChar('\n');
3054 static int UnifiedDiffToStringA(const git_diff_delta * /*delta*/, const git_diff_hunk * /*hunk*/, const git_diff_line *line, void *payload)
3056 ATLASSERT(payload && line);
3057 auto str = static_cast<CStringA*>(payload);
3058 if (line->origin == GIT_DIFF_LINE_CONTEXT || line->origin == GIT_DIFF_LINE_ADDITION || line->origin == GIT_DIFF_LINE_DELETION)
3059 str->Append(&line->origin, 1);
3060 str->Append(line->content, SafeSizeToInt(line->content_len));
3061 return 0;
3064 int CGit::GetUnifiedDiff(const CTGitPath& path, const CString& rev1, const CString& rev2, CStringA& buffer, bool bMerge, bool bCombine, int diffContext)
3066 if (UsingLibGit2(GIT_CMD_DIFF))
3067 return GetUnifiedDiffLibGit2(path, rev1, rev2, UnifiedDiffStatToStringA, UnifiedDiffToStringA, &buffer, bMerge, false);
3068 else
3070 CString cmd;
3071 cmd = GetUnifiedDiffCmd(path, rev1, rev2, bMerge, bCombine, diffContext);
3072 BYTE_VECTOR vector;
3073 const int ret = Run(cmd, &vector);
3074 if (!vector.empty())
3076 vector.push_back(0); // vector is not NUL terminated
3077 buffer.Append(vector.data());
3079 return ret;
3083 int CGit::GitRevert(int parent, const CGitHash &hash)
3085 if (UsingLibGit2(GIT_CMD_REVERT))
3087 CAutoRepository repo(GetGitRepository());
3088 if (!repo)
3089 return -1;
3091 CAutoCommit commit;
3092 if (git_commit_lookup(commit.GetPointer(), repo, hash))
3093 return -1;
3095 git_revert_options revert_opts = GIT_REVERT_OPTIONS_INIT;
3096 revert_opts.mainline = parent;
3097 return !git_revert(repo, commit, &revert_opts) ? 0 : -1;
3099 else
3101 CString cmd, merge;
3102 if (parent)
3103 merge.Format(L"-m %d ", parent);
3104 cmd.Format(L"git.exe revert --no-edit --no-commit %s%s", static_cast<LPCWSTR>(merge), static_cast<LPCWSTR>(hash.ToString()));
3105 gitLastErr = cmd + L'\n';
3106 if (Run(cmd, &gitLastErr, CP_UTF8))
3107 return -1;
3108 else
3110 gitLastErr.Empty();
3111 return 0;
3116 int CGit::DeleteRef(const CString& reference)
3118 if (UsingLibGit2(GIT_CMD_DELETETAGBRANCH))
3120 CAutoRepository repo(GetGitRepository());
3121 if (!repo)
3122 return -1;
3124 CStringA refA;
3125 if (CStringUtils::EndsWith(reference, L"^{}"))
3126 refA = CUnicodeUtils::GetUTF8(reference.Left(reference.GetLength() - static_cast<int>(wcslen(L"^{}"))));
3127 else
3128 refA = CUnicodeUtils::GetUTF8(reference);
3130 CAutoReference ref;
3131 if (git_reference_lookup(ref.GetPointer(), repo, refA))
3132 return -1;
3134 int result = -1;
3135 if (git_reference_is_tag(ref))
3136 result = git_tag_delete(repo, git_reference_shorthand(ref));
3137 else if (git_reference_is_branch(ref))
3138 result = git_branch_delete(ref);
3139 else if (git_reference_is_remote(ref))
3140 result = git_branch_delete(ref);
3141 else
3142 result = git_reference_delete(ref);
3144 return result;
3146 else
3148 CString cmd, shortname;
3149 if (GetShortName(reference, shortname, L"refs/heads/"))
3150 cmd.Format(L"git.exe branch -D -- %s", static_cast<LPCWSTR>(shortname));
3151 else if (GetShortName(reference, shortname, L"refs/tags/"))
3152 cmd.Format(L"git.exe tag -d -- %s", static_cast<LPCWSTR>(shortname));
3153 else if (GetShortName(reference, shortname, L"refs/remotes/"))
3154 cmd.Format(L"git.exe branch -r -D -- %s", static_cast<LPCWSTR>(shortname));
3155 else
3157 gitLastErr = L"unsupported reference type: " + reference;
3158 return -1;
3161 gitLastErr.Empty();
3162 if (Run(cmd, &gitLastErr, CP_UTF8))
3163 return -1;
3165 gitLastErr.Empty();
3166 return 0;
3170 bool CGit::LoadTextFile(const CString &filename, CString &msg)
3172 if (!PathFileExists(filename))
3173 return false;
3175 CAutoFILE pFile = _wfsopen(filename, L"rb", SH_DENYWR);
3176 if (!pFile)
3178 ::MessageBox(nullptr, L"Could not open " + filename, L"TortoiseGit", MB_ICONERROR);
3179 return true; // load no further files
3182 CStringA str;
3185 char s[8196] = { 0 };
3186 int read = static_cast<int>(fread(s, sizeof(char), sizeof(s), pFile));
3187 if (read == 0)
3188 break;
3189 str += CStringA(s, read);
3190 } while (true);
3191 msg += CUnicodeUtils::GetUnicode(str);
3192 msg.Replace(L"\r\n", L"\n");
3193 msg.TrimRight(L'\n');
3194 msg += L'\n';
3196 return true; // load no further files
3199 int CGit::GetWorkingTreeChanges(CTGitPathList& result, bool amend, const CTGitPathList* filterlist, bool includedStaged /* = false */, bool getStagingStatus /* = false */)
3201 if (IsInitRepos())
3202 return GetInitAddList(result, getStagingStatus);
3204 BYTE_VECTOR out;
3206 int count = 1;
3207 if (filterlist)
3208 count = filterlist->GetCount();
3209 ATLASSERT(count > 0);
3211 CString head = L"HEAD";
3212 if (amend)
3213 head = L"HEAD~1";
3215 CString gitStatusParams;
3216 if (ms_LastMsysGitVersion >= ConvertVersionToInt(2, 17, 0))
3217 gitStatusParams = L" --no-ahead-behind";
3219 for (int i = 0; i < count; ++i)
3221 ATLASSERT(!filterlist || !(*filterlist)[i].GetGitPathString().IsEmpty()); // pathspec must not be empty, be compatible with Git >= 2.16.0
3222 BYTE_VECTOR cmdout;
3223 CString cmd;
3224 if (ms_bCygwinGit || ms_bMsys2Git)
3226 // Prevent showing all files as modified when using cygwin's git
3227 if (!filterlist)
3228 cmd.Format(L"git.exe status%s --", static_cast<LPCWSTR>(gitStatusParams));
3229 else
3230 cmd.Format(L"git.exe status%s -- \"%s\"", static_cast<LPCWSTR>(gitStatusParams), static_cast<LPCWSTR>((*filterlist)[i].GetGitPathString()));
3231 Run(cmd, &cmdout);
3232 cmdout.clear();
3235 // also list staged files which will be in the commit
3236 if (includedStaged || !filterlist)
3237 Run(L"git.exe diff-index --cached --raw " + head + L" --numstat -C -M -z --", &cmdout);
3238 else
3240 cmd.Format(L"git.exe diff-index --cached --raw %s --numstat -C -M -z -- \"%s\"", static_cast<LPCWSTR>(head), static_cast<LPCWSTR>((*filterlist)[i].GetGitPathString()));
3241 Run(cmd, &cmdout);
3244 if (!filterlist)
3245 cmd.Format(L"git.exe diff-index --raw %s --numstat -C%d%% -M%d%% -z --", static_cast<LPCWSTR>(head), ms_iSimilarityIndexThreshold, ms_iSimilarityIndexThreshold);
3246 else
3247 cmd.Format(L"git.exe diff-index --raw %s --numstat -C%d%% -M%d%% -z -- \"%s\"", static_cast<LPCWSTR>(head), ms_iSimilarityIndexThreshold, ms_iSimilarityIndexThreshold, static_cast<LPCWSTR>((*filterlist)[i].GetGitPathString()));
3249 BYTE_VECTOR cmdErr;
3250 if (Run(cmd, &cmdout, &cmdErr))
3252 CString str{ cmdErr };
3253 if (str.IsEmpty())
3254 str.Format(L"\"%s\" exited with an error code, but did not output any error message", static_cast<LPCWSTR>(cmd));
3255 MessageBox(nullptr, str, L"TortoiseGit", MB_OK | MB_ICONERROR);
3258 out.append(cmdout, 0);
3260 result.ParserFromLog(out);
3262 if (getStagingStatus)
3264 // This will show staged files regardless of any filterlist, so that it has the same behavior that the commit window has when staging support is disabled
3265 BYTE_VECTOR cmdStagedUnfilteredOut, cmdUnstagedUnfilteredOut;
3266 CString cmd = L"git.exe diff-index --cached --raw " + head + L" --numstat -C -M -z --";
3267 Run(cmd, &cmdStagedUnfilteredOut);
3269 CTGitPathList stagedUnfiltered;
3270 stagedUnfiltered.ParserFromLog(cmdStagedUnfilteredOut);
3272 cmd = L"git.exe diff-files --raw --numstat -C -M -z --";
3273 Run(cmd, &cmdUnstagedUnfilteredOut);
3275 CTGitPathList unstagedUnfiltered;
3276 unstagedUnfiltered.ParserFromLog(cmdUnstagedUnfilteredOut); // Necessary to detect partially staged files outside the filterlist
3278 // File shows up both in the output of diff-index --cached and diff-files: partially staged
3279 // File shows up only in the output of diff-index --cached: totally staged
3280 // File shows up only in the output of diff-files: totally unstaged
3281 // TODO: This is inefficient. It would be better if ParserFromLog received the output of each command
3282 // separately and did this processing there, dropping the new function UpdateStagingStatusFromPath entirely.
3283 for (int j = 0; j < stagedUnfiltered.GetCount(); ++j)
3285 CString path = stagedUnfiltered[j].GetGitPathString();
3286 if (unstagedUnfiltered.LookForGitPath(path))
3287 result.UpdateStagingStatusFromPath(path, CTGitPath::StagingStatus::PartiallyStaged);
3288 else
3289 result.UpdateStagingStatusFromPath(path, CTGitPath::StagingStatus::TotallyStaged);
3291 for (int j = 0; j < unstagedUnfiltered.GetCount(); ++j)
3293 CString path = unstagedUnfiltered[j].GetGitPathString();
3294 if (!stagedUnfiltered.LookForGitPath(path))
3295 result.UpdateStagingStatusFromPath(path, CTGitPath::StagingStatus::TotallyUnstaged);
3297 // make sure conflicted files show up as unstaged instead of partially staged
3298 for (int j = 0; j < result.GetCount(); ++j)
3300 if (result[j].m_Action & CTGitPath::LOGACTIONS_UNMERGED)
3301 const_cast<CTGitPath&>(result[j]).m_stagingStatus = CTGitPath::StagingStatus::TotallyUnstaged;
3305 std::map<CString, int> duplicateMap;
3306 for (int i = 0; i < result.GetCount(); ++i)
3307 duplicateMap.insert(std::pair<CString, int>(result[i].GetGitPathString(), i));
3309 // handle delete conflict case, when remote : modified, local : deleted.
3310 for (int i = 0; i < count; ++i)
3312 BYTE_VECTOR cmdout;
3313 CString cmd;
3315 if (!filterlist)
3316 cmd = L"git.exe ls-files -u -t -z";
3317 else
3318 cmd.Format(L"git.exe ls-files -u -t -z -- \"%s\"", static_cast<LPCWSTR>((*filterlist)[i].GetGitPathString()));
3320 Run(cmd, &cmdout);
3322 CTGitPathList conflictlist;
3323 conflictlist.ParserFromLsFile(cmdout);
3324 for (int j = 0; j < conflictlist.GetCount(); ++j)
3326 auto existing = duplicateMap.find(conflictlist[j].GetGitPathString());
3327 if (existing != duplicateMap.end())
3329 CTGitPath& p = const_cast<CTGitPath&>(result[existing->second]);
3330 p.m_Action |= CTGitPath::LOGACTIONS_UNMERGED;
3332 else
3334 // should we ever get here?
3335 ASSERT(false);
3336 result.AddPath(conflictlist[j]);
3337 duplicateMap.insert(std::pair<CString, int>(result[i].GetGitPathString(), result.GetCount() - 1));
3342 static bool useOldLSFilesDBehaviorKS = CRegDWORD(L"Software\\TortoiseGit\\OldLSFilesDBehaviorKS", FALSE, false, HKEY_LOCAL_MACHINE) == TRUE; // TODO: remove kill-switch
3343 // handle source files of file renames/moves (issue #860)
3344 // if a file gets renamed and the new file "git add"ed, diff-index doesn't list the source file anymore
3345 for (int i = 0; i < count; ++i)
3347 BYTE_VECTOR cmdout;
3348 CString cmd;
3350 if (!filterlist)
3352 if (!useOldLSFilesDBehaviorKS)
3353 cmd = L"git.exe diff --name-only --diff-filter=D -z";
3354 else
3355 cmd = L"git.exe ls-files -d -z";
3357 else
3359 if (!useOldLSFilesDBehaviorKS)
3360 cmd.Format(L"git.exe diff --name-only --diff-filter=D -z -- \"%s\"", static_cast<LPCWSTR>((*filterlist)[i].GetGitPathString()));
3361 else
3362 cmd.Format(L"git.exe ls-files -d -z -- \"%s\"", static_cast<LPCWSTR>((*filterlist)[i].GetGitPathString()));
3365 Run(cmd, &cmdout);
3367 CTGitPathList deletelist;
3368 deletelist.ParserFromLsFileSimple(cmdout, CTGitPath::LOGACTIONS_DELETED | CTGitPath::LOGACTIONS_MISSING);
3369 for (int j = 0; j < deletelist.GetCount(); ++j)
3371 auto existing = duplicateMap.find(deletelist[j].GetGitPathString());
3372 if (existing == duplicateMap.end())
3374 result.AddPath(deletelist[j]);
3375 duplicateMap.insert(std::pair<CString, int>(result[i].GetGitPathString(), result.GetCount() - 1));
3377 else
3379 CTGitPath& p = const_cast<CTGitPath&>(result[existing->second]);
3380 p.m_Action |= CTGitPath::LOGACTIONS_MISSING;
3381 result.m_Action |= CTGitPath::LOGACTIONS_MISSING;
3386 return 0;
3389 void CGit::GetBisectTerms(CString* good, CString* bad)
3391 static CString lastGood;
3392 static CString lastBad;
3393 static ULONGLONG lastRead = 0;
3395 SCOPE_EXIT
3397 if (bad)
3398 *bad = lastBad;
3399 if (good)
3400 *good = lastGood;
3403 #ifndef GOOGLETEST_INCLUDE_GTEST_GTEST_H_
3404 // add some caching here, because this method might be called multiple times in a short time from LogDlg and RevisionGraph
3405 // as we only read a small file the performance effects should be negligible
3406 if (lastRead + 5000 > GetTickCount64())
3407 return;
3408 #endif
3410 lastGood = L"good";
3411 lastBad = L"bad";
3413 CString adminDir;
3414 if (!GitAdminDir::GetAdminDirPath(m_CurrentDir, adminDir))
3415 return;
3417 CString termsFile = adminDir + L"BISECT_TERMS";
3418 CAutoFILE fp;
3419 _wfopen_s(fp.GetPointer(), termsFile, L"rb");
3420 if (!fp)
3421 return;
3422 char badA[MAX_PATH] = { 0 };
3423 fgets(badA, sizeof(badA), fp);
3424 size_t len = strlen(badA);
3425 if (len > 0 && badA[len - 1] == '\n')
3426 badA[len - 1] = '\0';
3427 char goodA[MAX_PATH] = { 0 };
3428 fgets(goodA, sizeof(goodA), fp);
3429 len = strlen(goodA);
3430 if (len > 0 && goodA[len - 1] == '\n')
3431 goodA[len - 1] = '\0';
3432 lastGood = CUnicodeUtils::GetUnicode(goodA);
3433 lastBad = CUnicodeUtils::GetUnicode(badA);
3434 lastRead = GetTickCount64();
3437 int CGit::GetGitVersion(CString* versiondebug, CString* errStr)
3439 CString version, err;
3440 if (Run(L"git.exe --version", &version, &err, CP_UTF8))
3442 if (errStr)
3443 *errStr = err;
3444 return -1;
3447 int ver = 0;
3448 if (versiondebug)
3449 *versiondebug = version;
3453 int start = 0;
3454 CString str = version.Tokenize(L".", start);
3455 int space = str.ReverseFind(L' ');
3456 str = str.Mid(space + 1, start);
3457 ver = _wtol(str);
3458 ver <<= 24;
3460 version = version.Mid(start);
3461 start = 0;
3463 str = version.Tokenize(L".", start);
3464 ver |= (_wtol(str) & 0xFF) << 16;
3466 str = version.Tokenize(L".", start);
3467 ver |= (_wtol(str) & 0xFF) << 8;
3469 str = version.Tokenize(L".", start);
3470 ver |= (_wtol(str) & 0xFF);
3472 catch (...)
3474 if (!ver)
3475 return -1;
3478 return ver;
3481 int CGit::GetGitNotes(const CGitHash& hash, CString& notes)
3483 CAutoRepository repo(GetGitRepository());
3484 if (!repo)
3485 return -1;
3487 CAutoNote note;
3488 const int ret = git_note_read(note.GetPointer(), repo, nullptr, hash);
3489 if (ret == GIT_ENOTFOUND)
3491 notes.Empty();
3492 return 0;
3494 else if (ret)
3495 return -1;
3496 notes = CUnicodeUtils::GetUnicode(git_note_message(note));
3497 return 0;
3500 int CGit::SetGitNotes(const CGitHash& hash, const CString& notes)
3502 CAutoRepository repo(GetGitRepository());
3503 if (!repo)
3504 return -1;
3506 CAutoSignature signature;
3507 if (git_signature_default(signature.GetPointer(), repo) < 0)
3508 return -1;
3510 git_oid oid;
3511 if (git_note_create(&oid, repo, nullptr, signature, signature, hash, CUnicodeUtils::GetUTF8(notes), 1) < 0)
3512 return -1;
3514 return 0;
3517 int CGit::GetSubmodulePointer(SubmoduleInfo& submoduleinfo) const
3519 submoduleinfo.Empty();
3521 if (CRegDWORD(L"Software\\TortoiseGit\\LogShowSuperProjectSubmodulePointer", TRUE) != TRUE)
3522 return 0;
3524 if (GitAdminDir::IsBareRepo(g_Git.m_CurrentDir))
3525 return -1;
3526 CString superprojectRoot;
3527 GitAdminDir::HasAdminDir(g_Git.m_CurrentDir, false, &superprojectRoot);
3528 if (superprojectRoot.IsEmpty())
3529 return -1;
3531 CAutoRepository repo(superprojectRoot);
3532 if (!repo)
3533 return -1;
3534 CAutoIndex index;
3535 if (git_repository_index(index.GetPointer(), repo))
3536 return -1;
3538 CString submodulePath;
3539 if (superprojectRoot[superprojectRoot.GetLength() - 1] == L'\\')
3540 submodulePath = g_Git.m_CurrentDir.Right(g_Git.m_CurrentDir.GetLength() - superprojectRoot.GetLength());
3541 else
3542 submodulePath = g_Git.m_CurrentDir.Right(g_Git.m_CurrentDir.GetLength() - superprojectRoot.GetLength() - 1);
3543 submodulePath.Replace(L'\\', L'/');
3545 // if current submodule is in conflict state, return the relevant hashes
3546 const git_index_entry* ancestor{};
3547 const git_index_entry* our{};
3548 const git_index_entry* their{};
3549 if (!git_index_conflict_get(&ancestor, &our, &their, index, CUnicodeUtils::GetUTF8(submodulePath)))
3551 if (our)
3552 submoduleinfo.mergeconflictMineHash = our->id;
3553 if (their)
3554 submoduleinfo.mergeconflictTheirsHash = their->id;
3556 CTGitPath superProject{superprojectRoot};
3557 if (superProject.IsRebaseActive())
3559 submoduleinfo.mineLabel = L"super-project-rebase-head";
3560 submoduleinfo.theirsLabel = L"super-project-head";
3562 else
3564 submoduleinfo.mineLabel = L"super-project-head";
3565 submoduleinfo.theirsLabel = L"super-project-merge-head";
3567 return 0;
3570 // determine hash of submodule
3571 const git_index_entry* entry = git_index_get_bypath(index, CUnicodeUtils::GetUTF8(submodulePath), 0);
3572 if (!entry)
3573 return -1;
3575 submoduleinfo.superProjectHash = entry->id;
3576 return 0;
3579 // similar code in CTGitPath::ParserFromLsFile
3580 int CGit::ParseConflictHashesFromLsFile(const BYTE_VECTOR& out, CGitHash& baseHash, bool& baseIsFile, CGitHash& mineHash, bool& mineIsFile, CGitHash& remoteHash, bool& remoteIsFile)
3582 size_t pos = 0;
3583 const size_t end = out.size();
3584 if (end == 0)
3585 return 1;
3586 while (pos < end)
3588 const size_t lineStart = pos;
3590 if (out[pos] != 'M')
3592 ASSERT(false && "this should never happen as this method should only be called for output of git ls-files -u -t -z");
3593 return -1;
3596 pos = out.find(' ', pos); // advance to mode
3597 if (pos == CGitByteArray::npos)
3598 return -1;
3600 const size_t modeStart = pos + 1;
3601 pos = out.find(' ', modeStart); // advance to hash
3602 if (pos == CGitByteArray::npos)
3603 return -1;
3605 const size_t hashStart = pos + 1;
3606 pos = out.find(' ', hashStart); // advance to Stage
3607 if (pos == CGitByteArray::npos)
3608 return -1;
3610 const size_t stageStart = pos + 1;
3611 pos = out.find('\t', stageStart); // advance to filename
3612 if (pos == CGitByteArray::npos || stageStart - 1 - hashStart >= INT_MAX)
3613 return -1;
3615 ++pos;
3616 const size_t fileNameEnd = out.find(0, pos);
3617 if (fileNameEnd == CGitByteArray::npos || fileNameEnd == pos || pos - lineStart != 52)
3618 return -1;
3620 CString hash;
3621 CGit::StringAppend(hash, &out[hashStart], CP_UTF8, static_cast<int>(stageStart - 1 - hashStart));
3622 long mode = strtol(&out[modeStart], nullptr, 10);
3623 const int stage = strtol(&out[stageStart], nullptr, 10);
3624 if (stage == 0)
3625 return -1;
3626 else if (stage == 1)
3628 baseHash = CGitHash::FromHexStrTry(hash);
3629 baseIsFile = mode != 160000;
3631 else if (stage == 2)
3633 mineHash = CGitHash::FromHexStrTry(hash);
3634 mineIsFile = mode != 160000;
3636 else if (stage == 3)
3638 remoteHash = CGitHash::FromHexStrTry(hash);
3639 remoteIsFile = mode != 160000;
3642 pos = out.findNextString(pos);
3645 return 0;