Fix typos
[TortoiseGit.git] / src / TortoiseProc / GitDiff.cpp
blob0079f67dff7ed4c2064fbda8a9929aaacfdc7992
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2008 - TortoiseSVN
4 // Copyright (C) 2008-2020, 2023 - TortoiseGit
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "stdafx.h"
21 #include "GitDiff.h"
22 #include "TortoiseProc.h"
23 #include "AppUtils.h"
24 #include "gittype.h"
25 #include "resource.h"
26 #include "MessageBox.h"
27 #include "FileDiffDlg.h"
28 #include "SubmoduleDiffDlg.h"
29 #include "TempFile.h"
31 int CGitDiff::SubmoduleDiffNull(HWND hWnd, const CTGitPath* pPath, const CGitHash& hash)
33 CString newsub;
34 CGitHash newhash;
36 CString cmd;
37 if (!hash.IsEmpty())
38 cmd.Format(L"git.exe ls-tree \"%s\" -- \"%s\"", static_cast<LPCWSTR>(hash.ToString()), static_cast<LPCWSTR>(pPath->GetGitPathString()));
39 else
40 cmd.Format(L"git.exe ls-files -s -- \"%s\"", static_cast<LPCWSTR>(pPath->GetGitPathString()));
42 CString output, err;
43 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
45 CMessageBox::Show(hWnd, output + L'\n' + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
46 return -1;
49 int start = output.Find(L' ');
50 if(start>0)
52 if (!hash.IsEmpty()) // in ls-files the hash is in the second column; in ls-tree it's in the third one
53 start = output.Find(L' ', start + 1);
54 if(start>0)
55 newhash = CGitHash::FromHexStrTry(output.Mid(start + 1, GIT_HASH_SIZE * 2));
57 CGit subgit;
58 subgit.m_IsUseGitDLL = false;
59 subgit.m_CurrentDir = g_Git.CombinePath(pPath);
60 const int encode = CAppUtils::GetLogOutputEncode(&subgit);
62 cmd.Format(L"git.exe log -n1 --pretty=format:\"%%s\" %s --", static_cast<LPCWSTR>(newhash.ToString()));
63 const bool toOK = !subgit.Run(cmd, &newsub, encode);
65 bool dirty = false;
66 if (hash.IsEmpty() && !(pPath->m_Action & CTGitPath::LOGACTIONS_DELETED))
68 CString dirtyList;
69 subgit.Run(L"git.exe status --porcelain", &dirtyList, encode);
70 dirty = !dirtyList.IsEmpty();
73 CSubmoduleDiffDlg submoduleDiffDlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
74 if (pPath->m_Action & CTGitPath::LOGACTIONS_DELETED)
75 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), false, newhash, newsub, toOK, CGitHash(), L"", false, dirty, ChangeType::DeleteSubmodule);
76 else
77 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), false, CGitHash(), L"", true, newhash, newsub, toOK, dirty, ChangeType::NewSubmodule);
78 submoduleDiffDlg.DoModal();
79 if (submoduleDiffDlg.IsRefresh())
80 return 1;
82 return 0;
85 if (!hash.IsEmpty())
86 CMessageBox::Show(hWnd, L"ls-tree output format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
87 else
88 CMessageBox::Show(hWnd, L"ls-files output format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
89 return -1;
92 int CGitDiff::DiffNull(HWND hWnd, const CTGitPath* pPath, const CString& rev1, bool bIsAdd, int jumpToLine, bool bAlternative)
94 CGitHash rev1Hash;
95 if (rev1 != GIT_REV_ZERO)
97 if (g_Git.GetHash(rev1Hash, rev1 + L"^{}")) // make sure we have a HASH here, otherwise filenames might be invalid, also add ^{} in order to dereference signed tags
99 MessageBox(hWnd, g_Git.GetGitLastErr(L"Could not get hash of \"" + rev1 + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
100 return -1;
103 CString file1;
104 CString nullfile;
105 CString cmd;
107 if(pPath->IsDirectory())
109 int result;
110 // refresh if result = 1
111 CTGitPath path = *pPath;
112 while ((result = SubmoduleDiffNull(hWnd, &path, rev1Hash)) == 1)
113 path.SetFromGit(pPath->GetGitPathString());
114 return result;
117 if (!rev1Hash.IsEmpty())
119 file1 = CTempFiles::Instance().GetTempFilePath(false, *pPath, rev1Hash).GetWinPathString();
120 if (g_Git.GetOneFile(rev1Hash.ToString(), *pPath, file1))
122 CString out;
123 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, static_cast<LPCWSTR>(pPath->GetGitPathString()), static_cast<LPCWSTR>(rev1Hash.ToString()), static_cast<LPCWSTR>(file1));
124 CMessageBox::Show(hWnd, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
125 return -1;
127 ::SetFileAttributes(file1, FILE_ATTRIBUTE_READONLY);
129 else
130 file1 = g_Git.CombinePath(pPath);
132 CString tempfile = CTempFiles::Instance().GetTempFilePath(false, *pPath, rev1Hash).GetWinPathString();
133 ::SetFileAttributes(tempfile, FILE_ATTRIBUTE_READONLY);
135 const auto flags = CAppUtils::DiffFlags().AlternativeTool(bAlternative);
136 if(bIsAdd)
137 CAppUtils::StartExtDiff(tempfile,file1,
138 pPath->GetGitPathString(),
139 pPath->GetGitPathString() + L':' + rev1Hash.ToString(g_Git.GetShortHASHLength()),
140 g_Git.CombinePath(pPath), g_Git.CombinePath(pPath),
141 CGitHash(), rev1Hash
142 , flags, jumpToLine);
143 else
144 CAppUtils::StartExtDiff(file1,tempfile,
145 pPath->GetGitPathString() + L':' + rev1Hash.ToString(g_Git.GetShortHASHLength()),
146 pPath->GetGitPathString(),
147 g_Git.CombinePath(pPath), g_Git.CombinePath(pPath),
148 rev1Hash, CGitHash()
149 , flags, jumpToLine);
151 return 0;
154 int CGitDiff::SubmoduleDiff(HWND hWnd, const CTGitPath* pPath, const CTGitPath* /*pPath2*/, const CGitHash& rev1, const CGitHash& rev2, bool /*blame*/, bool /*unified*/)
156 CGitHash oldhash;
157 CGitHash newhash;
158 bool dirty = false;
159 CString cmd;
160 bool isWorkingCopy = false;
161 if (rev2.IsEmpty() || rev1.IsEmpty())
163 CGitHash rev;
164 if (!rev2.IsEmpty())
165 rev = rev2;
166 if (!rev1.IsEmpty())
167 rev = rev1;
169 isWorkingCopy = true;
171 cmd.Format(L"git.exe diff --submodule=short %s -- \"%s\"",
172 static_cast<LPCWSTR>(rev.ToString()), static_cast<LPCWSTR>(pPath->GetGitPathString()));
174 CString output, err;
175 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
177 CMessageBox::Show(hWnd, output + L'\n' + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
178 return -1;
181 if (output.IsEmpty())
183 output.Empty();
184 err.Empty();
185 // also compare against index
186 cmd.Format(L"git.exe diff --submodule=short -- \"%s\"", static_cast<LPCWSTR>(pPath->GetGitPathString()));
187 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
189 CMessageBox::Show(hWnd, output + L'\n' + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
190 return -1;
193 if (output.IsEmpty())
195 CMessageBox::Show(hWnd, IDS_ERR_EMPTYDIFF, IDS_APPNAME, MB_OK | MB_ICONERROR);
196 return -1;
198 else if (CMessageBox::Show(hWnd, IDS_SUBMODULE_EMPTYDIFF, IDS_APPNAME, 1, IDI_QUESTION, IDS_MSGBOX_YES, IDS_MSGBOX_NO) == 1)
200 CString sCmd;
201 sCmd.Format(L"/command:subupdate /bkpath:\"%s\"", static_cast<LPCWSTR>(g_Git.m_CurrentDir));
202 CAppUtils::RunTortoiseGitProc(sCmd);
204 return -1;
207 int start =0;
208 const int oldstart = output.Find(L"-Subproject commit", start);
209 if(oldstart<0)
211 CMessageBox::Show(hWnd, L"Subproject Diff Format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
212 return -1;
214 oldhash = CGitHash::FromHexStrTry(output.Mid(oldstart + static_cast<int>(wcslen(L"-Subproject commit")) + 1, GIT_HASH_SIZE * 2));
215 start = 0;
216 const int newstart = output.Find(L"+Subproject commit", start);
217 if (newstart < 0)
219 CMessageBox::Show(hWnd, L"Subproject Diff Format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
220 return -1;
222 newhash = CGitHash::FromHexStrTry(output.Mid(newstart + static_cast<int>(wcslen(L"+Subproject commit")) + 1, GIT_HASH_SIZE * 2));
223 dirty = output.Mid(newstart + static_cast<int>(wcslen(L"+Subproject commit")) + GIT_HASH_SIZE * 2 + 1) == L"-dirty\n";
225 else
227 cmd.Format(L"git.exe diff-tree -r -z %s %s -- \"%s\"",
228 static_cast<LPCWSTR>(rev2.ToString()), static_cast<LPCWSTR>(rev1.ToString()), static_cast<LPCWSTR>(pPath->GetGitPathString()));
230 BYTE_VECTOR bytes, errBytes;
231 if(g_Git.Run(cmd, &bytes, &errBytes))
233 CString err;
234 CGit::StringAppend(err, errBytes.data(), CP_UTF8);
235 CMessageBox::Show(hWnd, err, L"TortoiseGit", MB_OK | MB_ICONERROR);
236 return -1;
239 if (bytes.size() < 15 + 2 * GIT_HASH_SIZE + 1 + 2 * GIT_HASH_SIZE)
241 CMessageBox::Show(hWnd, L"git diff-tree gives invalid output", L"TortoiseGit", MB_OK | MB_ICONERROR);
242 return -1;
244 CString temp;
245 CGit::StringAppend(temp, &bytes[15], CP_UTF8, 2 * GIT_HASH_SIZE);
246 oldhash = CGitHash::FromHexStrTry(temp);
247 temp.Empty();
248 CGit::StringAppend(temp, &bytes[15 + 2 * GIT_HASH_SIZE + 1], CP_UTF8, 2 * GIT_HASH_SIZE);
249 newhash = CGitHash::FromHexStrTry(temp);
252 CString oldsub;
253 CString newsub;
254 bool oldOK = false, newOK = false;
256 CGit subgit;
257 subgit.m_IsUseGitDLL = false;
258 subgit.m_CurrentDir = g_Git.CombinePath(pPath);
259 ChangeType changeType = ChangeType::Unknown;
261 if (CTGitPath(subgit.m_CurrentDir).HasAdminDir())
262 GetSubmoduleChangeType(subgit, oldhash, newhash, oldOK, newOK, changeType, oldsub, newsub);
264 CSubmoduleDiffDlg submoduleDiffDlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
265 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), isWorkingCopy, oldhash, oldsub, oldOK, newhash, newsub, newOK, dirty, changeType);
266 submoduleDiffDlg.DoModal();
267 if (submoduleDiffDlg.IsRefresh())
268 return 1;
270 return 0;
273 void CGitDiff::GetSubmoduleChangeType(CGit& subgit, const CGitHash& oldhash, const CGitHash& newhash, bool& oldOK, bool& newOK, ChangeType& changeType, CString& oldsub, CString& newsub)
275 CString cmd;
276 const int encode = CAppUtils::GetLogOutputEncode(&subgit);
277 int oldTime = 0, newTime = 0;
279 if (!oldhash.IsEmpty())
281 CString cmdout, cmderr;
282 cmd.Format(L"git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --", static_cast<LPCWSTR>(oldhash.ToString()));
283 oldOK = !subgit.Run(cmd, &cmdout, &cmderr, encode);
284 if (oldOK)
286 int pos = cmdout.Find(L' ');
287 oldTime = _wtoi(cmdout.Left(pos));
288 oldsub = cmdout.Mid(pos + 1);
290 else
291 oldsub = cmderr;
293 if (!newhash.IsEmpty())
295 CString cmdout, cmderr;
296 cmd.Format(L"git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --", static_cast<LPCWSTR>(newhash.ToString()));
297 newOK = !subgit.Run(cmd, &cmdout, &cmderr, encode);
298 if (newOK)
300 int pos = cmdout.Find(L' ');
301 newTime = _wtoi(cmdout.Left(pos));
302 newsub = cmdout.Mid(pos + 1);
304 else
305 newsub = cmderr;
308 if (oldhash.IsEmpty())
310 oldOK = true;
311 changeType = ChangeType::NewSubmodule;
313 else if (newhash.IsEmpty())
315 newOK = true;
316 changeType = ChangeType::DeleteSubmodule;
318 else if (oldhash != newhash)
320 bool ffNewer = subgit.IsFastForward(oldhash.ToString(), newhash.ToString());
321 if (!ffNewer)
323 bool ffOlder = subgit.IsFastForward(newhash.ToString(), oldhash.ToString());
324 if (!ffOlder)
326 if (newTime > oldTime)
327 changeType = ChangeType::NewerTime;
328 else if (newTime < oldTime)
329 changeType = ChangeType::OlderTime;
330 else
331 changeType = ChangeType::SameTime;
333 else
334 changeType = ChangeType::Rewind;
336 else
337 changeType = ChangeType::FastForward;
339 else if (oldhash == newhash)
340 changeType = ChangeType::Identical;
342 if (!oldOK || !newOK)
343 changeType = ChangeType::Unknown;
346 int CGitDiff::Diff(HWND hWnd, const CTGitPath* pPath, const CTGitPath* pPath2, const CString& rev1, const CString& rev2, bool /*blame*/, bool /*unified*/, int jumpToLine, bool bAlternativeTool, bool mustExist)
348 // make sure we have HASHes here, otherwise filenames might be invalid
349 CGitHash rev1Hash;
350 CGitHash rev2Hash;
351 if (rev1 != GIT_REV_ZERO)
353 if (g_Git.GetHash(rev1Hash, rev1 + L"^{}")) // add ^{} in order to dereference signed tags
355 MessageBox(hWnd, g_Git.GetGitLastErr(L"Could not get hash of \"" + rev1 + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
356 return -1;
359 if (rev2 != GIT_REV_ZERO)
361 if (g_Git.GetHash(rev2Hash, rev2 + L"^{}")) // add ^{} in order to dereference signed tags
363 MessageBox(hWnd, g_Git.GetGitLastErr(L"Could not get hash of \"" + rev2 + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
364 return -1;
368 CString file1;
369 CString title1;
370 CString cmd;
372 if(pPath->IsDirectory() || pPath2->IsDirectory())
374 int result;
375 // refresh if result = 1
376 CTGitPath path = *pPath;
377 CTGitPath path2 = *pPath2;
378 while ((result = SubmoduleDiff(hWnd, &path, &path2, rev1Hash, rev2Hash)) == 1)
380 path.SetFromGit(pPath->GetGitPathString());
381 path2.SetFromGit(pPath2->GetGitPathString());
383 return result;
386 if (!rev1Hash.IsEmpty())
388 // use original file extension, an external diff tool might need it
389 file1 = CTempFiles::Instance().GetTempFilePath(false, *pPath, rev1Hash).GetWinPathString();
390 title1 = pPath->GetGitPathString() + L": " + rev1Hash.ToString(g_Git.GetShortHASHLength());
391 auto ret = g_Git.GetOneFile(rev1Hash.ToString(), *pPath, file1);
392 if (ret && !(!mustExist && ret == GIT_ENOTFOUND))
394 CString out;
395 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, static_cast<LPCWSTR>(pPath->GetGitPathString()), static_cast<LPCWSTR>(rev1Hash.ToString()), static_cast<LPCWSTR>(file1));
396 CMessageBox::Show(hWnd, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
397 return -1;
399 ::SetFileAttributes(file1, FILE_ATTRIBUTE_READONLY);
401 else
403 if (PathIsRelative(pPath->GetWinPath()))
405 file1 = g_Git.CombinePath(pPath);
406 title1.Format(IDS_DIFF_WCNAME, static_cast<LPCWSTR>(pPath->GetGitPathString()));
408 else
410 file1 = pPath->GetWinPath();
411 title1 = pPath->GetWinPathString();
413 if (!PathFileExists(file1))
415 CString sMsg;
416 sMsg.Format(IDS_PROC_DIFFERROR_FILENOTINWORKINGTREE, static_cast<LPCWSTR>(file1));
417 if (MessageBox(hWnd, sMsg, L"TortoiseGit", MB_ICONEXCLAMATION | MB_YESNO) != IDYES)
418 return 1;
419 if (!CCommonAppUtils::FileOpenSave(file1, nullptr, IDS_SELECTFILE, IDS_COMMONFILEFILTER, true))
420 return 1;
421 title1 = file1;
425 CString file2;
426 CString title2;
427 if (!rev2Hash.IsEmpty())
429 CTGitPath fileName = *pPath2;
430 if (pPath2->m_Action & CTGitPath::LOGACTIONS_REPLACED)
431 fileName = CTGitPath(pPath2->GetGitOldPathString());
433 file2 = CTempFiles::Instance().GetTempFilePath(false, fileName, rev2Hash).GetWinPathString();
434 title2 = fileName.GetGitPathString() + L": " + rev2Hash.ToString(g_Git.GetShortHASHLength());
435 const auto ret = g_Git.GetOneFile(rev2Hash.ToString(), fileName, file2);
436 if (ret && !(!mustExist && ret == GIT_ENOTFOUND))
438 CString out;
439 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, static_cast<LPCWSTR>(pPath2->GetGitPathString()), static_cast<LPCWSTR>(rev2Hash.ToString()), static_cast<LPCWSTR>(file2));
440 CMessageBox::Show(hWnd, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
441 return -1;
443 ::SetFileAttributes(file2, FILE_ATTRIBUTE_READONLY);
445 else
447 if (PathIsRelative(pPath2->GetWinPath()))
449 file2 = g_Git.CombinePath(pPath2);
450 title2.Format(IDS_DIFF_WCNAME, static_cast<LPCWSTR>(pPath2->GetGitPathString()));
452 else
454 file2 = pPath2->GetWinPath();
455 title2 = pPath2->GetWinPathString();
459 CAppUtils::StartExtDiff(file2,file1,
460 title2,
461 title1,
462 g_Git.CombinePath(pPath2),
463 g_Git.CombinePath(pPath),
464 rev2Hash,
465 rev1Hash,
466 CAppUtils::DiffFlags().AlternativeTool(bAlternativeTool), jumpToLine);
468 return 0;
471 int CGitDiff::DiffCommit(HWND hWnd, const CTGitPath& path, const GitRev* r1, const GitRev* r2, bool bAlternative)
473 return DiffCommit(hWnd, path, path, r1, r2, bAlternative);
476 int CGitDiff::DiffCommit(HWND hWnd, const CTGitPath& path1, const CTGitPath& path2, const GitRev* r1, const GitRev* r2, bool bAlternative)
478 if (path1.GetWinPathString().IsEmpty())
480 CFileDiffDlg dlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
481 dlg.SetDiff(nullptr, *r2, *r1);
482 dlg.DoModal();
484 else if (path1.IsDirectory())
486 CFileDiffDlg dlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
487 dlg.SetDiff(&path1, *r2, *r1);
488 dlg.DoModal();
490 else
491 Diff(hWnd, &path1, &path2, r1->m_CommitHash.ToString(), r2->m_CommitHash.ToString(), false, false, 0, bAlternative);
492 return 0;
495 int CGitDiff::DiffCommit(HWND hWnd, const CTGitPath& path, const CString& r1, const CString& r2, bool bAlternative)
497 return DiffCommit(hWnd, path, path, r1, r2, bAlternative);
500 int CGitDiff::DiffCommit(HWND hWnd, const CTGitPath& path1, const CTGitPath& path2, const CString& r1, const CString& r2, bool bAlternative)
502 if (path1.GetWinPathString().IsEmpty())
504 CFileDiffDlg dlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
505 dlg.SetDiff(nullptr, r2, r1);
506 dlg.DoModal();
508 else if (path1.IsDirectory())
510 CFileDiffDlg dlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
511 dlg.SetDiff(&path1, r2, r1);
512 dlg.DoModal();
514 else
515 Diff(hWnd, &path1, &path2, r1, r2, false, false, 0, bAlternative);
516 return 0;