1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2008 - TortoiseSVN
4 // Copyright (C) 2008-2018 - 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 CString
& rev1
)
37 if (rev1
!= GIT_REV_ZERO
)
38 cmd
.Format(L
"git.exe ls-tree \"%s\" -- \"%s\"", (LPCTSTR
)rev1
, (LPCTSTR
)pPath
->GetGitPathString());
40 cmd
.Format(L
"git.exe ls-files -s -- \"%s\"", (LPCTSTR
)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 (rev1
!= GIT_REV_ZERO
) // 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
=output
.Mid(start
+ 1, GIT_HASH_SIZE
* 2);
58 subgit
.m_CurrentDir
= g_Git
.CombinePath(pPath
);
59 int encode
=CAppUtils::GetLogOutputEncode(&subgit
);
61 cmd
.Format(L
"git.exe log -n1 --pretty=format:\"%%s\" %s --", (LPCTSTR
)newhash
);
62 bool toOK
= !subgit
.Run(cmd
,&newsub
,encode
);
65 if (rev1
== GIT_REV_ZERO
&& !(pPath
->m_Action
& CTGitPath::LOGACTIONS_DELETED
))
68 subgit
.Run(L
"git.exe status --porcelain", &dirtyList
, encode
);
69 dirty
= !dirtyList
.IsEmpty();
72 CSubmoduleDiffDlg
submoduleDiffDlg(GetExplorerHWND() == hWnd
? nullptr : CWnd::FromHandle(hWnd
));
73 if (GetExplorerHWND() == hWnd
)
74 theApp
.m_pMainWnd
= &submoduleDiffDlg
;
75 if (pPath
->m_Action
& CTGitPath::LOGACTIONS_DELETED
)
76 submoduleDiffDlg
.SetDiff(pPath
->GetWinPath(), false, newhash
, newsub
, toOK
, GIT_REV_ZERO
, L
"", false, dirty
, DeleteSubmodule
);
78 submoduleDiffDlg
.SetDiff(pPath
->GetWinPath(), false, GIT_REV_ZERO
, L
"", true, newhash
, newsub
, toOK
, dirty
, NewSubmodule
);
79 submoduleDiffDlg
.DoModal();
80 if (submoduleDiffDlg
.IsRefresh())
86 if (rev1
!= GIT_REV_ZERO
)
87 CMessageBox::Show(hWnd
, L
"ls-tree output format error", L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
89 CMessageBox::Show(hWnd
, L
"ls-files output format error", L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
93 int CGitDiff::DiffNull(HWND hWnd
, const CTGitPath
* pPath
, CString rev1
, bool bIsAdd
, int jumpToLine
, bool bAlternative
)
95 if (rev1
!= GIT_REV_ZERO
)
98 if (g_Git
.GetHash(rev1Hash
, rev1
)) // make sure we have a HASH here, otherwise filenames might be invalid
100 MessageBox(hWnd
, g_Git
.GetGitLastErr(L
"Could not get hash of \"" + rev1
+ L
"\"."), L
"TortoiseGit", MB_ICONERROR
);
103 rev1
= rev1Hash
.ToString();
109 if(pPath
->IsDirectory())
112 // refresh if result = 1
113 CTGitPath path
= *pPath
;
114 while ((result
= SubmoduleDiffNull(hWnd
, &path
, rev1
)) == 1)
115 path
.SetFromGit(pPath
->GetGitPathString());
119 if(rev1
!= GIT_REV_ZERO
)
121 file1
= CTempFiles::Instance().GetTempFilePath(false, *pPath
, rev1
).GetWinPathString();
122 if (g_Git
.GetOneFile(rev1
, *pPath
, file1
))
125 out
.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED
, (LPCTSTR
)pPath
->GetGitPathString(), (LPCTSTR
)rev1
, (LPCTSTR
)file1
);
126 CMessageBox::Show(hWnd
, g_Git
.GetGitLastErr(out
, CGit::GIT_CMD_GETONEFILE
), L
"TortoiseGit", MB_OK
);
129 ::SetFileAttributes(file1
, FILE_ATTRIBUTE_READONLY
);
132 file1
= g_Git
.CombinePath(pPath
);
134 CString tempfile
= CTempFiles::Instance().GetTempFilePath(false, *pPath
, rev1
).GetWinPathString();
135 ::SetFileAttributes(tempfile
, FILE_ATTRIBUTE_READONLY
);
137 CAppUtils::DiffFlags flags
;
138 flags
.bAlternativeTool
= bAlternative
;
140 CAppUtils::StartExtDiff(tempfile
,file1
,
141 pPath
->GetGitPathString(),
142 pPath
->GetGitPathString() + L
':' + rev1
.Left(g_Git
.GetShortHASHLength()),
143 g_Git
.CombinePath(pPath
), g_Git
.CombinePath(pPath
),
145 , flags
, jumpToLine
);
147 CAppUtils::StartExtDiff(file1
,tempfile
,
148 pPath
->GetGitPathString() + L
':' + rev1
.Left(g_Git
.GetShortHASHLength()),
149 pPath
->GetGitPathString(),
150 g_Git
.CombinePath(pPath
), g_Git
.CombinePath(pPath
),
152 , flags
, jumpToLine
);
157 int CGitDiff::SubmoduleDiff(HWND hWnd
, const CTGitPath
* pPath
, const CTGitPath
* /*pPath2*/, const CString
& rev1
, const CString
& rev2
, bool /*blame*/, bool /*unified*/)
163 bool isWorkingCopy
= false;
164 if( rev2
== GIT_REV_ZERO
|| rev1
== GIT_REV_ZERO
)
166 oldhash
= GIT_REV_ZERO
;
167 newhash
= GIT_REV_ZERO
;
170 if( rev2
!= GIT_REV_ZERO
)
172 if( rev1
!= GIT_REV_ZERO
)
175 isWorkingCopy
= true;
177 cmd
.Format(L
"git.exe diff %s -- \"%s\"",
178 (LPCTSTR
)rev
, (LPCTSTR
)pPath
->GetGitPathString());
181 if (g_Git
.Run(cmd
, &output
, &err
, CP_UTF8
))
183 CMessageBox::Show(hWnd
, output
+ L
'\n' + err
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
187 if (output
.IsEmpty())
191 // also compare against index
192 cmd
.Format(L
"git.exe diff -- \"%s\"", (LPCTSTR
)pPath
->GetGitPathString());
193 if (g_Git
.Run(cmd
, &output
, &err
, CP_UTF8
))
195 CMessageBox::Show(hWnd
, output
+ L
'\n' + err
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
199 if (output
.IsEmpty())
201 CMessageBox::Show(hWnd
, IDS_ERR_EMPTYDIFF
, IDS_APPNAME
, MB_OK
| MB_ICONERROR
);
204 else if (CMessageBox::Show(hWnd
, IDS_SUBMODULE_EMPTYDIFF
, IDS_APPNAME
, 1, IDI_QUESTION
, IDS_MSGBOX_YES
, IDS_MSGBOX_NO
) == 1)
207 sCmd
.Format(L
"/command:subupdate /bkpath:\"%s\"", (LPCTSTR
)g_Git
.m_CurrentDir
);
208 CAppUtils::RunTortoiseGitProc(sCmd
);
214 int oldstart
= output
.Find(L
"-Subproject commit", start
);
217 CMessageBox::Show(hWnd
, L
"Subproject Diff Format error", L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
220 oldhash
= output
.Mid(oldstart
+ (int)wcslen(L
"-Subproject commit") + 1, GIT_HASH_SIZE
* 2);
222 int newstart
= output
.Find(L
"+Subproject commit",start
);
225 CMessageBox::Show(hWnd
, L
"Subproject Diff Format error", L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
228 newhash
= output
.Mid(newstart
+ (int)wcslen(L
"+Subproject commit") + 1, GIT_HASH_SIZE
* 2);
229 dirty
= output
.Mid(newstart
+ (int)wcslen(L
"+Subproject commit") + GIT_HASH_SIZE
* 2 + 1) == L
"-dirty\n";
233 cmd
.Format(L
"git.exe diff-tree -r -z %s %s -- \"%s\"",
234 (LPCTSTR
)rev2
, (LPCTSTR
)rev1
, (LPCTSTR
)pPath
->GetGitPathString());
236 BYTE_VECTOR bytes
, errBytes
;
237 if(g_Git
.Run(cmd
, &bytes
, &errBytes
))
240 CGit::StringAppend(&err
, &errBytes
[0], CP_UTF8
);
241 CMessageBox::Show(hWnd
, err
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
245 if (bytes
.size() < 15 + 2 * GIT_HASH_SIZE
+ 1 + 2 * GIT_HASH_SIZE
)
247 CMessageBox::Show(hWnd
, L
"git diff-tree gives invalid output", L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
250 CGit::StringAppend(&oldhash
, &bytes
[15], CP_UTF8
, 2 * GIT_HASH_SIZE
);
251 CGit::StringAppend(&newhash
, &bytes
[15 + 2 * GIT_HASH_SIZE
+ 1], CP_UTF8
, 2 * GIT_HASH_SIZE
);
257 bool oldOK
= false, newOK
= false;
260 subgit
.m_CurrentDir
= g_Git
.CombinePath(pPath
);
261 ChangeType changeType
= Unknown
;
263 if (pPath
->HasAdminDir())
264 GetSubmoduleChangeType(subgit
, oldhash
, newhash
, oldOK
, newOK
, changeType
, oldsub
, newsub
);
266 CSubmoduleDiffDlg
submoduleDiffDlg(GetExplorerHWND() == hWnd
? nullptr : CWnd::FromHandle(hWnd
));
267 if (GetExplorerHWND() == hWnd
)
268 theApp
.m_pMainWnd
= &submoduleDiffDlg
;
269 submoduleDiffDlg
.SetDiff(pPath
->GetWinPath(), isWorkingCopy
, oldhash
, oldsub
, oldOK
, newhash
, newsub
, newOK
, dirty
, changeType
);
270 submoduleDiffDlg
.DoModal();
271 if (submoduleDiffDlg
.IsRefresh())
277 void CGitDiff::GetSubmoduleChangeType(CGit
& subgit
, const CGitHash
& oldhash
, const CGitHash
& newhash
, bool& oldOK
, bool& newOK
, ChangeType
& changeType
, CString
& oldsub
, CString
& newsub
)
280 int encode
= CAppUtils::GetLogOutputEncode(&subgit
);
281 int oldTime
= 0, newTime
= 0;
283 if (!oldhash
.IsEmpty())
285 CString cmdout
, cmderr
;
286 cmd
.Format(L
"git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --", (LPCTSTR
)oldhash
.ToString());
287 oldOK
= !subgit
.Run(cmd
, &cmdout
, &cmderr
, encode
);
290 int pos
= cmdout
.Find(L
' ');
291 oldTime
= _wtoi(cmdout
.Left(pos
));
292 oldsub
= cmdout
.Mid(pos
+ 1);
297 if (!newhash
.IsEmpty())
299 CString cmdout
, cmderr
;
300 cmd
.Format(L
"git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --", (LPCTSTR
)newhash
.ToString());
301 newOK
= !subgit
.Run(cmd
, &cmdout
, &cmderr
, encode
);
304 int pos
= cmdout
.Find(L
' ');
305 newTime
= _wtoi(cmdout
.Left(pos
));
306 newsub
= cmdout
.Mid(pos
+ 1);
312 if (oldhash
.IsEmpty())
315 changeType
= NewSubmodule
;
317 else if (newhash
.IsEmpty())
320 changeType
= DeleteSubmodule
;
322 else if (oldhash
!= newhash
)
324 bool ffNewer
= subgit
.IsFastForward(oldhash
, newhash
);
327 bool ffOlder
= subgit
.IsFastForward(newhash
, oldhash
);
330 if (newTime
> oldTime
)
331 changeType
= NewerTime
;
332 else if (newTime
< oldTime
)
333 changeType
= OlderTime
;
335 changeType
= SameTime
;
341 changeType
= FastForward
;
343 else if (oldhash
== newhash
)
344 changeType
= Identical
;
346 if (!oldOK
|| !newOK
)
347 changeType
= Unknown
;
350 int CGitDiff::Diff(HWND hWnd
, const CTGitPath
* pPath
, const CTGitPath
* pPath2
, CString rev1
, CString rev2
, bool /*blame*/, bool /*unified*/, int jumpToLine
, bool bAlternativeTool
, bool mustExist
)
352 // make sure we have HASHes here, otherwise filenames might be invalid
353 if (rev1
!= GIT_REV_ZERO
)
356 if (g_Git
.GetHash(rev1Hash
, rev1
))
358 MessageBox(hWnd
, g_Git
.GetGitLastErr(L
"Could not get hash of \"" + rev1
+ L
"\"."), L
"TortoiseGit", MB_ICONERROR
);
361 rev1
= rev1Hash
.ToString();
363 if (rev2
!= GIT_REV_ZERO
)
366 if (g_Git
.GetHash(rev2Hash
, rev2
))
368 MessageBox(hWnd
, g_Git
.GetGitLastErr(L
"Could not get hash of \"" + rev2
+ L
"\"."), L
"TortoiseGit", MB_ICONERROR
);
371 rev2
= rev2Hash
.ToString();
378 if(pPath
->IsDirectory() || pPath2
->IsDirectory())
381 // refresh if result = 1
382 CTGitPath path
= *pPath
;
383 CTGitPath path2
= *pPath2
;
384 while ((result
= SubmoduleDiff(hWnd
, &path
, &path2
, rev1
, rev2
)) == 1)
386 path
.SetFromGit(pPath
->GetGitPathString());
387 path2
.SetFromGit(pPath2
->GetGitPathString());
392 if(rev1
!= GIT_REV_ZERO
)
394 // use original file extension, an external diff tool might need it
395 file1
= CTempFiles::Instance().GetTempFilePath(false, *pPath
, rev1
).GetWinPathString();
396 title1
= pPath
->GetGitPathString() + L
": " + rev1
.Left(g_Git
.GetShortHASHLength());
397 auto ret
= g_Git
.GetOneFile(rev1
, *pPath
, file1
);
398 if (ret
&& !(!mustExist
&& ret
== GIT_ENOTFOUND
))
401 out
.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED
, (LPCTSTR
)pPath
->GetGitPathString(), (LPCTSTR
)rev1
, (LPCTSTR
)file1
);
402 CMessageBox::Show(hWnd
, g_Git
.GetGitLastErr(out
, CGit::GIT_CMD_GETONEFILE
), L
"TortoiseGit", MB_OK
);
405 ::SetFileAttributes(file1
, FILE_ATTRIBUTE_READONLY
);
409 file1
= g_Git
.CombinePath(pPath
);
410 title1
.Format(IDS_DIFF_WCNAME
, (LPCTSTR
)pPath
->GetGitPathString());
411 if (!PathFileExists(file1
))
414 sMsg
.Format(IDS_PROC_DIFFERROR_FILENOTINWORKINGTREE
, (LPCTSTR
)file1
);
415 if (MessageBox(hWnd
, sMsg
, L
"TortoiseGit", MB_ICONEXCLAMATION
| MB_YESNO
) != IDYES
)
417 if (!CCommonAppUtils::FileOpenSave(file1
, nullptr, IDS_SELECTFILE
, IDS_COMMONFILEFILTER
, true))
425 if(rev2
!= GIT_REV_ZERO
)
427 CTGitPath fileName
= *pPath2
;
428 if (pPath2
->m_Action
& CTGitPath::LOGACTIONS_REPLACED
)
429 fileName
= CTGitPath(pPath2
->GetGitOldPathString());
431 file2
= CTempFiles::Instance().GetTempFilePath(false, fileName
, rev2
).GetWinPathString();
432 title2
= fileName
.GetGitPathString() + L
": " + rev2
.Left(g_Git
.GetShortHASHLength());
433 auto ret
= g_Git
.GetOneFile(rev2
, fileName
, file2
);
434 if (ret
&& !(!mustExist
&& ret
== GIT_ENOTFOUND
))
437 out
.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED
, (LPCTSTR
)pPath2
->GetGitPathString(), (LPCTSTR
)rev2
, (LPCTSTR
)file2
);
438 CMessageBox::Show(hWnd
, g_Git
.GetGitLastErr(out
, CGit::GIT_CMD_GETONEFILE
), L
"TortoiseGit", MB_OK
);
441 ::SetFileAttributes(file2
, FILE_ATTRIBUTE_READONLY
);
445 file2
= g_Git
.CombinePath(pPath2
);
446 title2
.Format(IDS_DIFF_WCNAME
, (LPCTSTR
)pPath2
->GetGitPathString());
449 CAppUtils::DiffFlags flags
;
450 flags
.bAlternativeTool
= bAlternativeTool
;
451 CAppUtils::StartExtDiff(file2
,file1
,
454 g_Git
.CombinePath(pPath2
),
455 g_Git
.CombinePath(pPath
),
463 int CGitDiff::DiffCommit(HWND hWnd
, const CTGitPath
& path
, const GitRev
* r1
, const GitRev
* r2
, bool bAlternative
)
465 return DiffCommit(hWnd
, path
, path
, r1
, r2
, bAlternative
);
468 int CGitDiff::DiffCommit(HWND hWnd
, const CTGitPath
& path1
, const CTGitPath
& path2
, const GitRev
* r1
, const GitRev
* r2
, bool bAlternative
)
470 if (path1
.GetWinPathString().IsEmpty())
472 CFileDiffDlg
dlg(GetExplorerHWND() == hWnd
? nullptr : CWnd::FromHandle(hWnd
));
473 if (GetExplorerHWND() == hWnd
)
474 theApp
.m_pMainWnd
= &dlg
;
475 dlg
.SetDiff(nullptr, *r2
, *r1
);
478 else if (path1
.IsDirectory())
480 CFileDiffDlg
dlg(GetExplorerHWND() == hWnd
? nullptr : CWnd::FromHandle(hWnd
));
481 if (GetExplorerHWND() == hWnd
)
482 theApp
.m_pMainWnd
= &dlg
;
483 dlg
.SetDiff(&path1
, *r2
, *r1
);
487 Diff(hWnd
, &path1
, &path2
, r1
->m_CommitHash
.ToString(), r2
->m_CommitHash
.ToString(), false, false, 0, bAlternative
);
491 int CGitDiff::DiffCommit(HWND hWnd
, const CTGitPath
& path
, const CString
& r1
, const CString
& r2
, bool bAlternative
)
493 return DiffCommit(hWnd
, path
, path
, r1
, r2
, bAlternative
);
496 int CGitDiff::DiffCommit(HWND hWnd
, const CTGitPath
& path1
, const CTGitPath
& path2
, const CString
& r1
, const CString
& r2
, bool bAlternative
)
498 if (path1
.GetWinPathString().IsEmpty())
500 CFileDiffDlg
dlg(GetExplorerHWND() == hWnd
? nullptr : CWnd::FromHandle(hWnd
));
501 if (GetExplorerHWND() == hWnd
)
502 theApp
.m_pMainWnd
= &dlg
;
503 dlg
.SetDiff(nullptr, r2
, r1
);
506 else if (path1
.IsDirectory())
508 CFileDiffDlg
dlg(GetExplorerHWND() == hWnd
? nullptr : CWnd::FromHandle(hWnd
));
509 if (GetExplorerHWND() == hWnd
)
510 theApp
.m_pMainWnd
= &dlg
;
511 dlg
.SetDiff(&path1
, r2
, r1
);
515 Diff(hWnd
, &path1
, &path2
, r1
, r2
, false, false, 0, bAlternative
);