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.
22 #include "TortoiseProc.h"
26 #include "MessageBox.h"
27 #include "FileDiffDlg.h"
28 #include "SubmoduleDiffDlg.h"
31 int CGitDiff::SubmoduleDiffNull(HWND hWnd
, const CTGitPath
* pPath
, const CGitHash
& hash
)
38 cmd
.Format(L
"git.exe ls-tree \"%s\" -- \"%s\"", static_cast<LPCWSTR
>(hash
.ToString()), static_cast<LPCWSTR
>(pPath
->GetGitPathString()));
40 cmd
.Format(L
"git.exe ls-files -s -- \"%s\"", static_cast<LPCWSTR
>(pPath
->GetGitPathString()));
43 if (g_Git
.Run(cmd
, &output
, &err
, CP_UTF8
))
45 CMessageBox::Show(hWnd
, output
+ L
'\n' + err
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
49 int start
= output
.Find(L
' ');
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);
55 newhash
= CGitHash::FromHexStrTry(output
.Mid(start
+ 1, GIT_HASH_SIZE
* 2));
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
);
66 if (hash
.IsEmpty() && !(pPath
->m_Action
& CTGitPath::LOGACTIONS_DELETED
))
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
);
77 submoduleDiffDlg
.SetDiff(pPath
->GetWinPath(), false, CGitHash(), L
"", true, newhash
, newsub
, toOK
, dirty
, ChangeType::NewSubmodule
);
78 submoduleDiffDlg
.DoModal();
79 if (submoduleDiffDlg
.IsRefresh())
86 CMessageBox::Show(hWnd
, L
"ls-tree output format error", L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
88 CMessageBox::Show(hWnd
, L
"ls-files output format error", L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
92 int CGitDiff::DiffNull(HWND hWnd
, const CTGitPath
* pPath
, const CString
& rev1
, bool bIsAdd
, int jumpToLine
, bool bAlternative
)
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
);
107 if(pPath
->IsDirectory())
110 // refresh if result = 1
111 CTGitPath path
= *pPath
;
112 while ((result
= SubmoduleDiffNull(hWnd
, &path
, rev1Hash
)) == 1)
113 path
.SetFromGit(pPath
->GetGitPathString());
117 if (!rev1Hash
.IsEmpty())
119 file1
= CTempFiles::Instance().GetTempFilePath(false, *pPath
, rev1Hash
).GetWinPathString();
120 if (g_Git
.GetOneFile(rev1Hash
.ToString(), *pPath
, file1
))
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
);
127 ::SetFileAttributes(file1
, FILE_ATTRIBUTE_READONLY
);
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
);
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
),
142 , flags
, jumpToLine
);
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
),
149 , flags
, jumpToLine
);
154 int CGitDiff::SubmoduleDiff(HWND hWnd
, const CTGitPath
* pPath
, const CTGitPath
* /*pPath2*/, const CGitHash
& rev1
, const CGitHash
& rev2
, bool /*blame*/, bool /*unified*/)
160 bool isWorkingCopy
= false;
161 if (rev2
.IsEmpty() || rev1
.IsEmpty())
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()));
175 if (g_Git
.Run(cmd
, &output
, &err
, CP_UTF8
))
177 CMessageBox::Show(hWnd
, output
+ L
'\n' + err
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
181 if (output
.IsEmpty())
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
);
193 if (output
.IsEmpty())
195 CMessageBox::Show(hWnd
, IDS_ERR_EMPTYDIFF
, IDS_APPNAME
, MB_OK
| MB_ICONERROR
);
198 else if (CMessageBox::Show(hWnd
, IDS_SUBMODULE_EMPTYDIFF
, IDS_APPNAME
, 1, IDI_QUESTION
, IDS_MSGBOX_YES
, IDS_MSGBOX_NO
) == 1)
201 sCmd
.Format(L
"/command:subupdate /bkpath:\"%s\"", static_cast<LPCWSTR
>(g_Git
.m_CurrentDir
));
202 CAppUtils::RunTortoiseGitProc(sCmd
);
208 const int oldstart
= output
.Find(L
"-Subproject commit", start
);
211 CMessageBox::Show(hWnd
, L
"Subproject Diff Format error", L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
214 oldhash
= CGitHash::FromHexStrTry(output
.Mid(oldstart
+ static_cast<int>(wcslen(L
"-Subproject commit")) + 1, GIT_HASH_SIZE
* 2));
216 const int newstart
= output
.Find(L
"+Subproject commit", start
);
219 CMessageBox::Show(hWnd
, L
"Subproject Diff Format error", L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
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";
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
))
234 CGit::StringAppend(err
, errBytes
.data(), CP_UTF8
);
235 CMessageBox::Show(hWnd
, err
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
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
);
245 CGit::StringAppend(temp
, &bytes
[15], CP_UTF8
, 2 * GIT_HASH_SIZE
);
246 oldhash
= CGitHash::FromHexStrTry(temp
);
248 CGit::StringAppend(temp
, &bytes
[15 + 2 * GIT_HASH_SIZE
+ 1], CP_UTF8
, 2 * GIT_HASH_SIZE
);
249 newhash
= CGitHash::FromHexStrTry(temp
);
254 bool oldOK
= false, newOK
= false;
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())
273 void CGitDiff::GetSubmoduleChangeType(CGit
& subgit
, const CGitHash
& oldhash
, const CGitHash
& newhash
, bool& oldOK
, bool& newOK
, ChangeType
& changeType
, CString
& oldsub
, CString
& newsub
)
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
);
286 int pos
= cmdout
.Find(L
' ');
287 oldTime
= _wtoi(cmdout
.Left(pos
));
288 oldsub
= cmdout
.Mid(pos
+ 1);
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
);
300 int pos
= cmdout
.Find(L
' ');
301 newTime
= _wtoi(cmdout
.Left(pos
));
302 newsub
= cmdout
.Mid(pos
+ 1);
308 if (oldhash
.IsEmpty())
311 changeType
= ChangeType::NewSubmodule
;
313 else if (newhash
.IsEmpty())
316 changeType
= ChangeType::DeleteSubmodule
;
318 else if (oldhash
!= newhash
)
320 bool ffNewer
= subgit
.IsFastForward(oldhash
.ToString(), newhash
.ToString());
323 bool ffOlder
= subgit
.IsFastForward(newhash
.ToString(), oldhash
.ToString());
326 if (newTime
> oldTime
)
327 changeType
= ChangeType::NewerTime
;
328 else if (newTime
< oldTime
)
329 changeType
= ChangeType::OlderTime
;
331 changeType
= ChangeType::SameTime
;
334 changeType
= ChangeType::Rewind
;
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
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
);
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
);
372 if(pPath
->IsDirectory() || pPath2
->IsDirectory())
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());
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
))
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
);
399 ::SetFileAttributes(file1
, FILE_ATTRIBUTE_READONLY
);
403 if (PathIsRelative(pPath
->GetWinPath()))
405 file1
= g_Git
.CombinePath(pPath
);
406 title1
.Format(IDS_DIFF_WCNAME
, static_cast<LPCWSTR
>(pPath
->GetGitPathString()));
410 file1
= pPath
->GetWinPath();
411 title1
= pPath
->GetWinPathString();
413 if (!PathFileExists(file1
))
416 sMsg
.Format(IDS_PROC_DIFFERROR_FILENOTINWORKINGTREE
, static_cast<LPCWSTR
>(file1
));
417 if (MessageBox(hWnd
, sMsg
, L
"TortoiseGit", MB_ICONEXCLAMATION
| MB_YESNO
) != IDYES
)
419 if (!CCommonAppUtils::FileOpenSave(file1
, nullptr, IDS_SELECTFILE
, IDS_COMMONFILEFILTER
, true))
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
))
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
);
443 ::SetFileAttributes(file2
, FILE_ATTRIBUTE_READONLY
);
447 if (PathIsRelative(pPath2
->GetWinPath()))
449 file2
= g_Git
.CombinePath(pPath2
);
450 title2
.Format(IDS_DIFF_WCNAME
, static_cast<LPCWSTR
>(pPath2
->GetGitPathString()));
454 file2
= pPath2
->GetWinPath();
455 title2
= pPath2
->GetWinPathString();
459 CAppUtils::StartExtDiff(file2
,file1
,
462 g_Git
.CombinePath(pPath2
),
463 g_Git
.CombinePath(pPath
),
466 CAppUtils::DiffFlags().AlternativeTool(bAlternativeTool
), jumpToLine
);
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
);
484 else if (path1
.IsDirectory())
486 CFileDiffDlg
dlg(GetExplorerHWND() == hWnd
? nullptr : CWnd::FromHandle(hWnd
));
487 dlg
.SetDiff(&path1
, *r2
, *r1
);
491 Diff(hWnd
, &path1
, &path2
, r1
->m_CommitHash
.ToString(), r2
->m_CommitHash
.ToString(), false, false, 0, bAlternative
);
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
);
508 else if (path1
.IsDirectory())
510 CFileDiffDlg
dlg(GetExplorerHWND() == hWnd
? nullptr : CWnd::FromHandle(hWnd
));
511 dlg
.SetDiff(&path1
, r2
, r1
);
515 Diff(hWnd
, &path1
, &path2
, r1
, r2
, false, false, 0, bAlternative
);