Fetch+Rebase: Don't error out if no remote tracked branch is configured for current...
[TortoiseGit.git] / src / TortoiseProc / GitDiff.cpp
blob79e02843080472392cccdbb7e6588a4017b9cbf7
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2008 - TortoiseSVN
4 // Copyright (C) 2008-2016 - 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 "AppUtils.h"
23 #include "gittype.h"
24 #include "resource.h"
25 #include "MessageBox.h"
26 #include "FileDiffDlg.h"
27 #include "SubmoduleDiffDlg.h"
28 #include "TempFile.h"
30 int CGitDiff::SubmoduleDiffNull(const CTGitPath * pPath, const git_revnum_t &rev1)
32 CString oldhash = GIT_REV_ZERO;
33 CString oldsub ;
34 CString newsub;
35 CString newhash;
37 CString cmd;
38 if (rev1 != GIT_REV_ZERO)
39 cmd.Format(L"git.exe ls-tree \"%s\" -- \"%s\"", (LPCTSTR)rev1, (LPCTSTR)pPath->GetGitPathString());
40 else
41 cmd.Format(L"git.exe ls-files -s -- \"%s\"", (LPCTSTR)pPath->GetGitPathString());
43 CString output, err;
44 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
46 CMessageBox::Show(nullptr, output + L'\n' + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
47 return -1;
50 int start = output.Find(L' ');
51 if(start>0)
53 if (rev1 != GIT_REV_ZERO) // in ls-files the hash is in the second column; in ls-tree it's in the third one
54 start = output.Find(L' ', start + 1);
55 if(start>0)
56 newhash=output.Mid(start+1, 40);
58 CGit subgit;
59 subgit.m_CurrentDir = g_Git.CombinePath(pPath);
60 int encode=CAppUtils::GetLogOutputEncode(&subgit);
62 cmd.Format(L"git.exe log -n1 --pretty=format:\"%%s\" %s --", (LPCTSTR)newhash);
63 bool toOK = !subgit.Run(cmd,&newsub,encode);
65 bool dirty = false;
66 if (rev1 == GIT_REV_ZERO && !(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;
74 if (pPath->m_Action & CTGitPath::LOGACTIONS_DELETED)
75 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), false, newhash, newsub, toOK, oldhash, oldsub, false, dirty, DeleteSubmodule);
76 else
77 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), false, oldhash, oldsub, true, newhash, newsub, toOK, dirty, NewSubmodule);
78 submoduleDiffDlg.DoModal();
79 if (submoduleDiffDlg.IsRefresh())
80 return 1;
82 return 0;
85 if (rev1 != GIT_REV_ZERO)
86 CMessageBox::Show(nullptr, L"ls-tree output format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
87 else
88 CMessageBox::Show(nullptr, L"ls-files output format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
89 return -1;
92 int CGitDiff::DiffNull(const CTGitPath *pPath, git_revnum_t rev1, bool bIsAdd, int jumpToLine, bool bAlternative)
94 if (rev1 != GIT_REV_ZERO)
96 CGitHash rev1Hash;
97 if (g_Git.GetHash(rev1Hash, rev1)) // make sure we have a HASH here, otherwise filenames might be invalid
99 MessageBox(nullptr, g_Git.GetGitLastErr(L"Could not get hash of \"" + rev1 + L"\"."), L"TortoiseGit", MB_ICONERROR);
100 return -1;
102 rev1 = rev1Hash.ToString();
104 CString file1;
105 CString nullfile;
106 CString cmd;
108 if(pPath->IsDirectory())
110 int result;
111 // refresh if result = 1
112 CTGitPath path = *pPath;
113 while ((result = SubmoduleDiffNull(&path, rev1)) == 1)
114 path.SetFromGit(pPath->GetGitPathString());
115 return result;
118 if(rev1 != GIT_REV_ZERO )
120 file1 = CTempFiles::Instance().GetTempFilePath(false, *pPath, rev1).GetWinPathString();
121 if (g_Git.GetOneFile(rev1, *pPath, file1))
123 CString out;
124 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)pPath->GetGitPathString(), (LPCTSTR)rev1, (LPCTSTR)file1);
125 CMessageBox::Show(nullptr, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
126 return -1;
128 ::SetFileAttributes(file1, FILE_ATTRIBUTE_READONLY);
130 else
131 file1 = g_Git.CombinePath(pPath);
133 CString tempfile = CTempFiles::Instance().GetTempFilePath(false, *pPath, rev1).GetWinPathString();
134 ::SetFileAttributes(tempfile, FILE_ATTRIBUTE_READONLY);
136 CAppUtils::DiffFlags flags;
137 flags.bAlternativeTool = bAlternative;
138 if(bIsAdd)
139 CAppUtils::StartExtDiff(tempfile,file1,
140 pPath->GetGitPathString(),
141 pPath->GetGitPathString() + L':' + rev1.Left(g_Git.GetShortHASHLength()),
142 g_Git.CombinePath(pPath), g_Git.CombinePath(pPath),
143 git_revnum_t(GIT_REV_ZERO), rev1
144 , flags, jumpToLine);
145 else
146 CAppUtils::StartExtDiff(file1,tempfile,
147 pPath->GetGitPathString() + L':' + rev1.Left(g_Git.GetShortHASHLength()),
148 pPath->GetGitPathString(),
149 g_Git.CombinePath(pPath), g_Git.CombinePath(pPath),
150 rev1, git_revnum_t(GIT_REV_ZERO)
151 , flags, jumpToLine);
153 return 0;
156 int CGitDiff::SubmoduleDiff(const CTGitPath * pPath, const CTGitPath * /*pPath2*/, const git_revnum_t &rev1, const git_revnum_t &rev2, bool /*blame*/, bool /*unified*/)
158 CString oldhash;
159 CString newhash;
160 bool dirty = false;
161 CString cmd;
162 bool isWorkingCopy = false;
163 if( rev2 == GIT_REV_ZERO || rev1 == GIT_REV_ZERO )
165 oldhash = GIT_REV_ZERO;
166 newhash = GIT_REV_ZERO;
168 CString rev;
169 if( rev2 != GIT_REV_ZERO )
170 rev = rev2;
171 if( rev1 != GIT_REV_ZERO )
172 rev = rev1;
174 isWorkingCopy = true;
176 cmd.Format(L"git.exe diff %s -- \"%s\"",
177 (LPCTSTR)rev, (LPCTSTR)pPath->GetGitPathString());
179 CString output, err;
180 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
182 CMessageBox::Show(nullptr, output + L'\n' + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
183 return -1;
186 if (output.IsEmpty())
188 output.Empty();
189 err.Empty();
190 // also compare against index
191 cmd.Format(L"git.exe diff -- \"%s\"", (LPCTSTR)pPath->GetGitPathString());
192 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
194 CMessageBox::Show(nullptr, output + L'\n' + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
195 return -1;
198 if (output.IsEmpty())
200 CMessageBox::Show(nullptr, IDS_ERR_EMPTYDIFF, IDS_APPNAME, MB_OK | MB_ICONERROR);
201 return -1;
203 else if (CMessageBox::Show(nullptr, IDS_SUBMODULE_EMPTYDIFF, IDS_APPNAME, 1, IDI_QUESTION, IDS_MSGBOX_YES, IDS_MSGBOX_NO) == 1)
205 CString sCmd;
206 sCmd.Format(L"/command:subupdate /bkpath:\"%s\"", (LPCTSTR)g_Git.m_CurrentDir);
207 CAppUtils::RunTortoiseGitProc(sCmd);
209 return -1;
212 int start =0;
213 int oldstart = output.Find(L"-Subproject commit", start);
214 if(oldstart<0)
216 CMessageBox::Show(nullptr, L"Subproject Diff Format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
217 return -1;
219 oldhash = output.Mid(oldstart + CString(L"-Subproject commit").GetLength() + 1, 40);
220 start = 0;
221 int newstart = output.Find(L"+Subproject commit",start);
222 if (newstart < 0)
224 CMessageBox::Show(nullptr, L"Subproject Diff Format error", L"TortoiseGit", MB_OK | MB_ICONERROR);
225 return -1;
227 newhash = output.Mid(newstart+ CString(L"+Subproject commit").GetLength() + 1, 40);
228 dirty = output.Mid(newstart + CString(L"+Subproject commit").GetLength() + 41) == L"-dirty\n";
230 else
232 cmd.Format(L"git.exe diff-tree -r -z %s %s -- \"%s\"",
233 (LPCTSTR)rev2, (LPCTSTR)rev1, (LPCTSTR)pPath->GetGitPathString());
235 BYTE_VECTOR bytes, errBytes;
236 if(g_Git.Run(cmd, &bytes, &errBytes))
238 CString err;
239 CGit::StringAppend(&err, &errBytes[0], CP_UTF8);
240 CMessageBox::Show(nullptr, err, L"TortoiseGit", MB_OK | MB_ICONERROR);
241 return -1;
244 if (bytes.size() < 15 + 2 * GIT_HASH_SIZE + 1 + 2 * GIT_HASH_SIZE)
246 CMessageBox::Show(nullptr, L"git diff-tree gives invalid output", L"TortoiseGit", MB_OK | MB_ICONERROR);
247 return -1;
249 CGit::StringAppend(&oldhash, &bytes[15], CP_UTF8, 2 * GIT_HASH_SIZE);
250 CGit::StringAppend(&newhash, &bytes[15 + 2 * GIT_HASH_SIZE + 1], CP_UTF8, 2 * GIT_HASH_SIZE);
254 CString oldsub;
255 CString newsub;
256 bool oldOK = false, newOK = false;
258 CGit subgit;
259 subgit.m_CurrentDir = g_Git.CombinePath(pPath);
260 ChangeType changeType = Unknown;
262 if (pPath->HasAdminDir())
263 GetSubmoduleChangeType(subgit, oldhash, newhash, oldOK, newOK, changeType, oldsub, newsub);
265 CSubmoduleDiffDlg submoduleDiffDlg;
266 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), isWorkingCopy, oldhash, oldsub, oldOK, newhash, newsub, newOK, dirty, changeType);
267 submoduleDiffDlg.DoModal();
268 if (submoduleDiffDlg.IsRefresh())
269 return 1;
271 return 0;
274 void CGitDiff::GetSubmoduleChangeType(CGit& subgit, const CString& oldhash, const CString& newhash, bool& oldOK, bool& newOK, ChangeType& changeType, CString& oldsub, CString& newsub)
276 CString cmd;
277 int encode = CAppUtils::GetLogOutputEncode(&subgit);
278 int oldTime = 0, newTime = 0;
280 if (oldhash != GIT_REV_ZERO)
282 CString cmdout, cmderr;
283 cmd.Format(L"git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --", (LPCTSTR)oldhash);
284 oldOK = !subgit.Run(cmd, &cmdout, &cmderr, encode);
285 if (oldOK)
287 int pos = cmdout.Find(L' ');
288 oldTime = _wtoi(cmdout.Left(pos));
289 oldsub = cmdout.Mid(pos + 1);
291 else
292 oldsub = cmderr;
294 if (newhash != GIT_REV_ZERO)
296 CString cmdout, cmderr;
297 cmd.Format(L"git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --", (LPCTSTR)newhash);
298 newOK = !subgit.Run(cmd, &cmdout, &cmderr, encode);
299 if (newOK)
301 int pos = cmdout.Find(L' ');
302 newTime = _wtoi(cmdout.Left(pos));
303 newsub = cmdout.Mid(pos + 1);
305 else
306 newsub = cmderr;
309 if (oldhash == GIT_REV_ZERO)
311 oldOK = true;
312 changeType = NewSubmodule;
314 else if (newhash == GIT_REV_ZERO)
316 newOK = true;
317 changeType = DeleteSubmodule;
319 else if (oldhash != newhash)
321 bool ffNewer = subgit.IsFastForward(oldhash, newhash);
322 if (!ffNewer)
324 bool ffOlder = subgit.IsFastForward(newhash, oldhash);
325 if (!ffOlder)
327 if (newTime > oldTime)
328 changeType = NewerTime;
329 else if (newTime < oldTime)
330 changeType = OlderTime;
331 else
332 changeType = SameTime;
334 else
335 changeType = Rewind;
337 else
338 changeType = FastForward;
340 else if (oldhash == newhash)
341 changeType = Identical;
343 if (!oldOK || !newOK)
344 changeType = Unknown;
347 int CGitDiff::Diff(const CTGitPath* pPath, const CTGitPath* pPath2, git_revnum_t rev1, git_revnum_t rev2, bool /*blame*/, bool /*unified*/, int jumpToLine, bool bAlternativeTool)
349 // make sure we have HASHes here, otherwise filenames might be invalid
350 if (rev1 != GIT_REV_ZERO)
352 CGitHash rev1Hash;
353 if (g_Git.GetHash(rev1Hash, rev1))
355 MessageBox(nullptr, g_Git.GetGitLastErr(L"Could not get hash of \"" + rev1 + L"\"."), L"TortoiseGit", MB_ICONERROR);
356 return -1;
358 rev1 = rev1Hash.ToString();
360 if (rev2 != GIT_REV_ZERO)
362 CGitHash rev2Hash;
363 if (g_Git.GetHash(rev2Hash, rev2))
365 MessageBox(nullptr, g_Git.GetGitLastErr(L"Could not get hash of \"" + rev2 + L"\"."), L"TortoiseGit", MB_ICONERROR);
366 return -1;
368 rev2 = rev2Hash.ToString();
371 CString file1;
372 CString title1;
373 CString cmd;
375 if(pPath->IsDirectory() || pPath2->IsDirectory())
377 int result;
378 // refresh if result = 1
379 CTGitPath path = *pPath;
380 CTGitPath path2 = *pPath2;
381 while ((result = SubmoduleDiff(&path, &path2, rev1, rev2)) == 1)
383 path.SetFromGit(pPath->GetGitPathString());
384 path2.SetFromGit(pPath2->GetGitPathString());
386 return result;
389 if(rev1 != GIT_REV_ZERO )
391 // use original file extension, an external diff tool might need it
392 file1 = CTempFiles::Instance().GetTempFilePath(false, *pPath, rev1).GetWinPathString();
393 title1 = pPath->GetGitPathString() + L": " + rev1.Left(g_Git.GetShortHASHLength());
394 if (g_Git.GetOneFile(rev1, *pPath, file1))
396 CString out;
397 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)pPath->GetGitPathString(), (LPCTSTR)rev1, (LPCTSTR)file1);
398 CMessageBox::Show(nullptr, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
399 return -1;
401 ::SetFileAttributes(file1, FILE_ATTRIBUTE_READONLY);
403 else
405 file1 = g_Git.CombinePath(pPath);
406 title1.Format(IDS_DIFF_WCNAME, (LPCTSTR)pPath->GetGitPathString());
407 if (!PathFileExists(file1))
409 CString sMsg;
410 sMsg.Format(IDS_PROC_DIFFERROR_FILENOTINWORKINGTREE, (LPCTSTR)file1);
411 if (MessageBox(nullptr, sMsg, L"TortoiseGit", MB_ICONEXCLAMATION | MB_YESNO) != IDYES)
412 return 1;
413 if (!CCommonAppUtils::FileOpenSave(file1, nullptr, IDS_SELECTFILE, IDS_COMMONFILEFILTER, true))
414 return 1;
415 title1 = file1;
419 CString file2;
420 CString title2;
421 if(rev2 != GIT_REV_ZERO)
423 CTGitPath fileName = *pPath2;
424 if (pPath2->m_Action & CTGitPath::LOGACTIONS_REPLACED)
425 fileName = CTGitPath(pPath2->GetGitOldPathString());
427 file2 = CTempFiles::Instance().GetTempFilePath(false, fileName, rev2).GetWinPathString();
428 title2 = fileName.GetGitPathString() + L": " + rev2.Left(g_Git.GetShortHASHLength());
429 if (g_Git.GetOneFile(rev2, fileName, file2))
431 CString out;
432 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, (LPCTSTR)pPath2->GetGitPathString(), (LPCTSTR)rev2, (LPCTSTR)file2);
433 CMessageBox::Show(nullptr, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), L"TortoiseGit", MB_OK);
434 return -1;
436 ::SetFileAttributes(file2, FILE_ATTRIBUTE_READONLY);
438 else
440 file2 = g_Git.CombinePath(pPath2);
441 title2.Format(IDS_DIFF_WCNAME, pPath2->GetGitPathString());
444 CAppUtils::DiffFlags flags;
445 flags.bAlternativeTool = bAlternativeTool;
446 CAppUtils::StartExtDiff(file2,file1,
447 title2,
448 title1,
449 g_Git.CombinePath(pPath2),
450 g_Git.CombinePath(pPath),
451 rev2,
452 rev1,
453 flags, jumpToLine);
455 return 0;
458 int CGitDiff::DiffCommit(const CTGitPath& path, const GitRev* r1, const GitRev* r2, bool bAlternative)
460 return DiffCommit(path, path, r1, r2, bAlternative);
463 int CGitDiff::DiffCommit(const CTGitPath& path1, const CTGitPath& path2, const GitRev* r1, const GitRev* r2, bool bAlternative)
465 if (path1.GetWinPathString().IsEmpty())
467 CFileDiffDlg dlg;
468 dlg.SetDiff(nullptr, *r2, *r1);
469 dlg.DoModal();
471 else if (path1.IsDirectory())
473 CFileDiffDlg dlg;
474 dlg.SetDiff(&path1, *r2, *r1);
475 dlg.DoModal();
477 else
478 Diff(&path1, &path2, r1->m_CommitHash.ToString(), r2->m_CommitHash.ToString(), false, false, 0, bAlternative);
479 return 0;
482 int CGitDiff::DiffCommit(const CTGitPath& path, const CString& r1, const CString& r2, bool bAlternative)
484 return DiffCommit(path, path, r1, r2, bAlternative);
487 int CGitDiff::DiffCommit(const CTGitPath& path1, const CTGitPath& path2, const CString& r1, const CString& r2, bool bAlternative)
489 if (path1.GetWinPathString().IsEmpty())
491 CFileDiffDlg dlg;
492 dlg.SetDiff(nullptr, r2, r1);
493 dlg.DoModal();
495 else if (path1.IsDirectory())
497 CFileDiffDlg dlg;
498 dlg.SetDiff(&path1, r2, r1);
499 dlg.DoModal();
501 else
502 Diff(&path1, &path2, r1, r2, false, false, 0, bAlternative);
503 return 0;