Fixed issue #3307: Abort Merge on a single file always results in a parameter error...
[TortoiseGit.git] / src / TortoiseProc / GitDiff.cpp
blob07ba46f4688437be2f5f5bf987693aab614ac4fd
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.
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 CString& rev1)
33 CString newsub;
34 CString newhash;
36 CString cmd;
37 if (rev1 != GIT_REV_ZERO)
38 cmd.Format(L"git.exe ls-tree \"%s\" -- \"%s\"", (LPCTSTR)rev1, (LPCTSTR)pPath->GetGitPathString());
39 else
40 cmd.Format(L"git.exe ls-files -s -- \"%s\"", (LPCTSTR)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 (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);
54 if(start>0)
55 newhash=output.Mid(start + 1, GIT_HASH_SIZE * 2);
57 CGit subgit;
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);
64 bool dirty = false;
65 if (rev1 == GIT_REV_ZERO && !(pPath->m_Action & CTGitPath::LOGACTIONS_DELETED))
67 CString dirtyList;
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 (pPath->m_Action & CTGitPath::LOGACTIONS_DELETED)
74 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), false, newhash, newsub, toOK, GIT_REV_ZERO, L"", false, dirty, DeleteSubmodule);
75 else
76 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), false, GIT_REV_ZERO, L"", true, newhash, newsub, toOK, dirty, NewSubmodule);
77 submoduleDiffDlg.DoModal();
78 if (submoduleDiffDlg.IsRefresh())
79 return 1;
81 return 0;
84 if (rev1 != GIT_REV_ZERO)
85 CMessageBox::Show(hWnd, L"ls-tree output format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
86 else
87 CMessageBox::Show(hWnd, L"ls-files output format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
88 return -1;
91 int CGitDiff::DiffNull(HWND hWnd, const CTGitPath* pPath, const CString& rev1, bool bIsAdd, int jumpToLine, bool bAlternative)
93 CGitHash rev1Hash;
94 if (rev1 != GIT_REV_ZERO)
96 if (g_Git.GetHash(rev1Hash, rev1)) // make sure we have a HASH here, otherwise filenames might be invalid
98 MessageBox(hWnd, g_Git.GetGitLastErr(L"Could not get hash of \"" + rev1 + L"\"."), L"TortoiseGit", MB_ICONERROR);
99 return -1;
102 CString file1;
103 CString nullfile;
104 CString cmd;
106 if(pPath->IsDirectory())
108 int result;
109 // refresh if result = 1
110 CTGitPath path = *pPath;
111 while ((result = SubmoduleDiffNull(hWnd, &path, rev1Hash.ToString())) == 1)
112 path.SetFromGit(pPath->GetGitPathString());
113 return result;
116 if (!rev1Hash.IsEmpty())
118 file1 = CTempFiles::Instance().GetTempFilePath(false, *pPath, rev1Hash).GetWinPathString();
119 if (g_Git.GetOneFile(rev1Hash.ToString(), *pPath, file1))
121 CString out;
122 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)pPath->GetGitPathString(), (LPCTSTR)rev1Hash.ToString(), (LPCTSTR)file1);
123 CMessageBox::Show(hWnd, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
124 return -1;
126 ::SetFileAttributes(file1, FILE_ATTRIBUTE_READONLY);
128 else
129 file1 = g_Git.CombinePath(pPath);
131 CString tempfile = CTempFiles::Instance().GetTempFilePath(false, *pPath, rev1Hash).GetWinPathString();
132 ::SetFileAttributes(tempfile, FILE_ATTRIBUTE_READONLY);
134 CAppUtils::DiffFlags flags;
135 flags.bAlternativeTool = bAlternative;
136 if(bIsAdd)
137 CAppUtils::StartExtDiff(tempfile,file1,
138 pPath->GetGitPathString(),
139 pPath->GetGitPathString() + L':' + rev1Hash.ToString().Left(g_Git.GetShortHASHLength()),
140 g_Git.CombinePath(pPath), g_Git.CombinePath(pPath),
141 GIT_REV_ZERO, rev1Hash.ToString()
142 , flags, jumpToLine);
143 else
144 CAppUtils::StartExtDiff(file1,tempfile,
145 pPath->GetGitPathString() + L':' + rev1Hash.ToString().Left(g_Git.GetShortHASHLength()),
146 pPath->GetGitPathString(),
147 g_Git.CombinePath(pPath), g_Git.CombinePath(pPath),
148 rev1Hash.ToString(), GIT_REV_ZERO
149 , flags, jumpToLine);
151 return 0;
154 int CGitDiff::SubmoduleDiff(HWND hWnd, const CTGitPath* pPath, const CTGitPath* /*pPath2*/, const CString& rev1, const CString& rev2, bool /*blame*/, bool /*unified*/)
156 CString oldhash;
157 CString newhash;
158 bool dirty = false;
159 CString cmd;
160 bool isWorkingCopy = false;
161 if( rev2 == GIT_REV_ZERO || rev1 == GIT_REV_ZERO )
163 oldhash = GIT_REV_ZERO;
164 newhash = GIT_REV_ZERO;
166 CString rev;
167 if( rev2 != GIT_REV_ZERO )
168 rev = rev2;
169 if( rev1 != GIT_REV_ZERO )
170 rev = rev1;
172 isWorkingCopy = true;
174 cmd.Format(L"git.exe diff --submodule=short %s -- \"%s\"",
175 (LPCTSTR)rev, (LPCTSTR)pPath->GetGitPathString());
177 CString output, err;
178 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
180 CMessageBox::Show(hWnd, output + L'\n' + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
181 return -1;
184 if (output.IsEmpty())
186 output.Empty();
187 err.Empty();
188 // also compare against index
189 cmd.Format(L"git.exe diff --submodule=short -- \"%s\"", (LPCTSTR)pPath->GetGitPathString());
190 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
192 CMessageBox::Show(hWnd, output + L'\n' + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
193 return -1;
196 if (output.IsEmpty())
198 CMessageBox::Show(hWnd, IDS_ERR_EMPTYDIFF, IDS_APPNAME, MB_OK | MB_ICONERROR);
199 return -1;
201 else if (CMessageBox::Show(hWnd, IDS_SUBMODULE_EMPTYDIFF, IDS_APPNAME, 1, IDI_QUESTION, IDS_MSGBOX_YES, IDS_MSGBOX_NO) == 1)
203 CString sCmd;
204 sCmd.Format(L"/command:subupdate /bkpath:\"%s\"", (LPCTSTR)g_Git.m_CurrentDir);
205 CAppUtils::RunTortoiseGitProc(sCmd);
207 return -1;
210 int start =0;
211 int oldstart = output.Find(L"-Subproject commit", start);
212 if(oldstart<0)
214 CMessageBox::Show(hWnd, L"Subproject Diff Format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
215 return -1;
217 oldhash = output.Mid(oldstart + (int)wcslen(L"-Subproject commit") + 1, GIT_HASH_SIZE * 2);
218 start = 0;
219 int newstart = output.Find(L"+Subproject commit",start);
220 if (newstart < 0)
222 CMessageBox::Show(hWnd, L"Subproject Diff Format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
223 return -1;
225 newhash = output.Mid(newstart + (int)wcslen(L"+Subproject commit") + 1, GIT_HASH_SIZE * 2);
226 dirty = output.Mid(newstart + (int)wcslen(L"+Subproject commit") + GIT_HASH_SIZE * 2 + 1) == L"-dirty\n";
228 else
230 cmd.Format(L"git.exe diff-tree -r -z %s %s -- \"%s\"",
231 (LPCTSTR)rev2, (LPCTSTR)rev1, (LPCTSTR)pPath->GetGitPathString());
233 BYTE_VECTOR bytes, errBytes;
234 if(g_Git.Run(cmd, &bytes, &errBytes))
236 CString err;
237 CGit::StringAppend(&err, &errBytes[0], CP_UTF8);
238 CMessageBox::Show(hWnd, err, L"TortoiseGit", MB_OK | MB_ICONERROR);
239 return -1;
242 if (bytes.size() < 15 + 2 * GIT_HASH_SIZE + 1 + 2 * GIT_HASH_SIZE)
244 CMessageBox::Show(hWnd, L"git diff-tree gives invalid output", L"TortoiseGit", MB_OK | MB_ICONERROR);
245 return -1;
247 CGit::StringAppend(&oldhash, &bytes[15], CP_UTF8, 2 * GIT_HASH_SIZE);
248 CGit::StringAppend(&newhash, &bytes[15 + 2 * GIT_HASH_SIZE + 1], CP_UTF8, 2 * GIT_HASH_SIZE);
252 CString oldsub;
253 CString newsub;
254 bool oldOK = false, newOK = false;
256 CGit subgit;
257 subgit.m_CurrentDir = g_Git.CombinePath(pPath);
258 ChangeType changeType = Unknown;
260 if (pPath->HasAdminDir())
261 GetSubmoduleChangeType(subgit, oldhash, newhash, oldOK, newOK, changeType, oldsub, newsub);
263 CSubmoduleDiffDlg submoduleDiffDlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
264 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), isWorkingCopy, oldhash, oldsub, oldOK, newhash, newsub, newOK, dirty, changeType);
265 submoduleDiffDlg.DoModal();
266 if (submoduleDiffDlg.IsRefresh())
267 return 1;
269 return 0;
272 void CGitDiff::GetSubmoduleChangeType(CGit& subgit, const CGitHash& oldhash, const CGitHash& newhash, bool& oldOK, bool& newOK, ChangeType& changeType, CString& oldsub, CString& newsub)
274 CString cmd;
275 int encode = CAppUtils::GetLogOutputEncode(&subgit);
276 int oldTime = 0, newTime = 0;
278 if (!oldhash.IsEmpty())
280 CString cmdout, cmderr;
281 cmd.Format(L"git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --", (LPCTSTR)oldhash.ToString());
282 oldOK = !subgit.Run(cmd, &cmdout, &cmderr, encode);
283 if (oldOK)
285 int pos = cmdout.Find(L' ');
286 oldTime = _wtoi(cmdout.Left(pos));
287 oldsub = cmdout.Mid(pos + 1);
289 else
290 oldsub = cmderr;
292 if (!newhash.IsEmpty())
294 CString cmdout, cmderr;
295 cmd.Format(L"git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --", (LPCTSTR)newhash.ToString());
296 newOK = !subgit.Run(cmd, &cmdout, &cmderr, encode);
297 if (newOK)
299 int pos = cmdout.Find(L' ');
300 newTime = _wtoi(cmdout.Left(pos));
301 newsub = cmdout.Mid(pos + 1);
303 else
304 newsub = cmderr;
307 if (oldhash.IsEmpty())
309 oldOK = true;
310 changeType = NewSubmodule;
312 else if (newhash.IsEmpty())
314 newOK = true;
315 changeType = DeleteSubmodule;
317 else if (oldhash != newhash)
319 bool ffNewer = subgit.IsFastForward(oldhash.ToString(), newhash.ToString());
320 if (!ffNewer)
322 bool ffOlder = subgit.IsFastForward(newhash.ToString(), oldhash.ToString());
323 if (!ffOlder)
325 if (newTime > oldTime)
326 changeType = NewerTime;
327 else if (newTime < oldTime)
328 changeType = OlderTime;
329 else
330 changeType = SameTime;
332 else
333 changeType = Rewind;
335 else
336 changeType = FastForward;
338 else if (oldhash == newhash)
339 changeType = Identical;
341 if (!oldOK || !newOK)
342 changeType = Unknown;
345 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)
347 // make sure we have HASHes here, otherwise filenames might be invalid
348 CGitHash rev1Hash;
349 CGitHash rev2Hash;
350 if (rev1 != GIT_REV_ZERO)
352 if (g_Git.GetHash(rev1Hash, rev1))
354 MessageBox(hWnd, g_Git.GetGitLastErr(L"Could not get hash of \"" + rev1 + L"\"."), L"TortoiseGit", MB_ICONERROR);
355 return -1;
358 if (rev2 != GIT_REV_ZERO)
360 if (g_Git.GetHash(rev2Hash, rev2))
362 MessageBox(hWnd, g_Git.GetGitLastErr(L"Could not get hash of \"" + rev2 + L"\"."), L"TortoiseGit", MB_ICONERROR);
363 return -1;
367 CString file1;
368 CString title1;
369 CString cmd;
371 if(pPath->IsDirectory() || pPath2->IsDirectory())
373 int result;
374 // refresh if result = 1
375 CTGitPath path = *pPath;
376 CTGitPath path2 = *pPath2;
377 while ((result = SubmoduleDiff(hWnd, &path, &path2, rev1Hash.ToString(), rev2Hash.ToString())) == 1)
379 path.SetFromGit(pPath->GetGitPathString());
380 path2.SetFromGit(pPath2->GetGitPathString());
382 return result;
385 if (!rev1Hash.IsEmpty())
387 // use original file extension, an external diff tool might need it
388 file1 = CTempFiles::Instance().GetTempFilePath(false, *pPath, rev1Hash).GetWinPathString();
389 title1 = pPath->GetGitPathString() + L": " + rev1Hash.ToString().Left(g_Git.GetShortHASHLength());
390 auto ret = g_Git.GetOneFile(rev1Hash.ToString(), *pPath, file1);
391 if (ret && !(!mustExist && ret == GIT_ENOTFOUND))
393 CString out;
394 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)pPath->GetGitPathString(), (LPCTSTR)rev1Hash.ToString(), (LPCTSTR)file1);
395 CMessageBox::Show(hWnd, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
396 return -1;
398 ::SetFileAttributes(file1, FILE_ATTRIBUTE_READONLY);
400 else
402 file1 = g_Git.CombinePath(pPath);
403 title1.Format(IDS_DIFF_WCNAME, (LPCTSTR)pPath->GetGitPathString());
404 if (!PathFileExists(file1))
406 CString sMsg;
407 sMsg.Format(IDS_PROC_DIFFERROR_FILENOTINWORKINGTREE, (LPCTSTR)file1);
408 if (MessageBox(hWnd, sMsg, L"TortoiseGit", MB_ICONEXCLAMATION | MB_YESNO) != IDYES)
409 return 1;
410 if (!CCommonAppUtils::FileOpenSave(file1, nullptr, IDS_SELECTFILE, IDS_COMMONFILEFILTER, true))
411 return 1;
412 title1 = file1;
416 CString file2;
417 CString title2;
418 if (!rev2Hash.IsEmpty())
420 CTGitPath fileName = *pPath2;
421 if (pPath2->m_Action & CTGitPath::LOGACTIONS_REPLACED)
422 fileName = CTGitPath(pPath2->GetGitOldPathString());
424 file2 = CTempFiles::Instance().GetTempFilePath(false, fileName, rev2Hash).GetWinPathString();
425 title2 = fileName.GetGitPathString() + L": " + rev2Hash.ToString().Left(g_Git.GetShortHASHLength());
426 auto ret = g_Git.GetOneFile(rev2Hash.ToString(), fileName, file2);
427 if (ret && !(!mustExist && ret == GIT_ENOTFOUND))
429 CString out;
430 out.FormatMessage(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)pPath2->GetGitPathString(), (LPCTSTR)rev2Hash.ToString(), (LPCTSTR)file2);
431 CMessageBox::Show(hWnd, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
432 return -1;
434 ::SetFileAttributes(file2, FILE_ATTRIBUTE_READONLY);
436 else
438 file2 = g_Git.CombinePath(pPath2);
439 title2.Format(IDS_DIFF_WCNAME, (LPCTSTR)pPath2->GetGitPathString());
442 CAppUtils::DiffFlags flags;
443 flags.bAlternativeTool = bAlternativeTool;
444 CAppUtils::StartExtDiff(file2,file1,
445 title2,
446 title1,
447 g_Git.CombinePath(pPath2),
448 g_Git.CombinePath(pPath),
449 rev2Hash.ToString(),
450 rev1Hash.ToString(),
451 flags, jumpToLine);
453 return 0;
456 int CGitDiff::DiffCommit(HWND hWnd, const CTGitPath& path, const GitRev* r1, const GitRev* r2, bool bAlternative)
458 return DiffCommit(hWnd, path, path, r1, r2, bAlternative);
461 int CGitDiff::DiffCommit(HWND hWnd, const CTGitPath& path1, const CTGitPath& path2, const GitRev* r1, const GitRev* r2, bool bAlternative)
463 if (path1.GetWinPathString().IsEmpty())
465 CFileDiffDlg dlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
466 dlg.SetDiff(nullptr, *r2, *r1);
467 dlg.DoModal();
469 else if (path1.IsDirectory())
471 CFileDiffDlg dlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
472 dlg.SetDiff(&path1, *r2, *r1);
473 dlg.DoModal();
475 else
476 Diff(hWnd, &path1, &path2, r1->m_CommitHash.ToString(), r2->m_CommitHash.ToString(), false, false, 0, bAlternative);
477 return 0;
480 int CGitDiff::DiffCommit(HWND hWnd, const CTGitPath& path, const CString& r1, const CString& r2, bool bAlternative)
482 return DiffCommit(hWnd, path, path, r1, r2, bAlternative);
485 int CGitDiff::DiffCommit(HWND hWnd, const CTGitPath& path1, const CTGitPath& path2, const CString& r1, const CString& r2, bool bAlternative)
487 if (path1.GetWinPathString().IsEmpty())
489 CFileDiffDlg dlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
490 dlg.SetDiff(nullptr, r2, r1);
491 dlg.DoModal();
493 else if (path1.IsDirectory())
495 CFileDiffDlg dlg(GetExplorerHWND() == hWnd ? nullptr : CWnd::FromHandle(hWnd));
496 dlg.SetDiff(&path1, r2, r1);
497 dlg.DoModal();
499 else
500 Diff(hWnd, &path1, &path2, r1, r2, false, false, 0, bAlternative);
501 return 0;