Includes cleanup
[TortoiseGit.git] / src / TortoiseProc / GitDiff.cpp
blob9905a986c2b6ba8fd63e3e20ff57cfc96a76a781
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2008 - TortoiseSVN
4 // Copyright (C) 2008-2015 - 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"
29 CGitDiff::CGitDiff(void)
33 CGitDiff::~CGitDiff(void)
36 int CGitDiff::SubmoduleDiffNull(const CTGitPath * pPath, const git_revnum_t &rev1)
38 CString oldhash = GIT_REV_ZERO;
39 CString oldsub ;
40 CString newsub;
41 CString newhash;
43 CString cmd;
44 if (rev1 != GIT_REV_ZERO)
45 cmd.Format(_T("git.exe ls-tree \"%s\" -- \"%s\""), rev1, pPath->GetGitPathString());
46 else
47 cmd.Format(_T("git.exe ls-files -s -- \"%s\""), pPath->GetGitPathString());
49 CString output, err;
50 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
52 CMessageBox::Show(NULL, output + L"\n" + err, _T("TortoiseGit"), MB_OK|MB_ICONERROR);
53 return -1;
56 int start=0;
57 start=output.Find(_T(' '),start);
58 if(start>0)
60 if (rev1 != GIT_REV_ZERO) // in ls-files the hash is in the second column; in ls-tree it's in the third one
61 start = output.Find(_T(' '), start + 1);
62 if(start>0)
63 newhash=output.Mid(start+1, 40);
65 CGit subgit;
66 subgit.m_CurrentDir = g_Git.CombinePath(pPath);
67 int encode=CAppUtils::GetLogOutputEncode(&subgit);
69 cmd.Format(_T("git.exe log -n1 --pretty=format:\"%%s\" %s --"), newhash);
70 bool toOK = !subgit.Run(cmd,&newsub,encode);
72 bool dirty = false;
73 if (rev1 == GIT_REV_ZERO)
75 CString dirtyList;
76 subgit.Run(_T("git.exe status --porcelain"), &dirtyList, encode);
77 dirty = !dirtyList.IsEmpty();
80 CSubmoduleDiffDlg submoduleDiffDlg;
81 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), false, oldhash, oldsub, true, newhash, newsub, toOK, dirty, NewSubmodule);
82 submoduleDiffDlg.DoModal();
83 if (submoduleDiffDlg.IsRefresh())
84 return 1;
86 return 0;
89 if (rev1 != GIT_REV_ZERO)
90 CMessageBox::Show(NULL, _T("ls-tree output format error"), _T("TortoiseGit"), MB_OK | MB_ICONERROR);
91 else
92 CMessageBox::Show(NULL, _T("ls-files output format error"), _T("TortoiseGit"), MB_OK | MB_ICONERROR);
93 return -1;
96 int CGitDiff::DiffNull(const CTGitPath *pPath, git_revnum_t rev1, bool bIsAdd, int jumpToLine)
98 CString temppath;
99 GetTempPath(temppath);
100 if (rev1 != GIT_REV_ZERO)
102 CGitHash rev1Hash;
103 if (g_Git.GetHash(rev1Hash, rev1)) // make sure we have a HASH here, otherwise filenames might be invalid
105 MessageBox(NULL, g_Git.GetGitLastErr(_T("Could not get hash of \"") + rev1 + _T("\".")), _T("TortoiseGit"), MB_ICONERROR);
106 return -1;
108 rev1 = rev1Hash.ToString();
110 CString file1;
111 CString nullfile;
112 CString cmd;
114 if(pPath->IsDirectory())
116 int result;
117 // refresh if result = 1
118 CTGitPath path = *pPath;
119 while ((result = SubmoduleDiffNull(&path, rev1)) == 1)
121 path.SetFromGit(pPath->GetGitPathString());
123 return result;
126 if(rev1 != GIT_REV_ZERO )
128 TCHAR szTempName[MAX_PATH] = {0};
129 GetTempFileName(temppath, pPath->GetBaseFilename(), 0, szTempName);
130 CString temp(szTempName);
131 DeleteFile(szTempName);
132 CreateDirectory(szTempName, NULL);
133 file1.Format(_T("%s\\%s-%s%s"),
134 temp,
135 pPath->GetBaseFilename(),
136 rev1.Left(g_Git.GetShortHASHLength()),
137 pPath->GetFileExtension());
139 if (g_Git.GetOneFile(rev1, *pPath, file1))
141 CString out;
142 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, pPath->GetGitPathString(), rev1, file1);
143 CMessageBox::Show(nullptr, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), _T("TortoiseGit"), MB_OK);
144 return -1;
147 else
149 file1 = g_Git.CombinePath(pPath);
152 // preserve FileExtension, needed especially for diffing deleted images (detection on new filename extension)
153 CString tempfile=::GetTempFile() + pPath->GetFileExtension();
154 CStdioFile file(tempfile,CFile::modeReadWrite|CFile::modeCreate );
155 //file.WriteString();
156 file.Close();
157 ::SetFileAttributes(tempfile, FILE_ATTRIBUTE_READONLY);
159 CAppUtils::DiffFlags flags;
161 if(bIsAdd)
162 CAppUtils::StartExtDiff(tempfile,file1,
163 pPath->GetGitPathString(),
164 pPath->GetGitPathString() + _T(":") + rev1.Left(g_Git.GetShortHASHLength()),
165 g_Git.CombinePath(pPath), g_Git.CombinePath(pPath),
166 git_revnum_t(GIT_REV_ZERO), rev1
167 , flags, jumpToLine);
168 else
169 CAppUtils::StartExtDiff(file1,tempfile,
170 pPath->GetGitPathString() + _T(":") + rev1.Left(g_Git.GetShortHASHLength()),
171 pPath->GetGitPathString(),
172 g_Git.CombinePath(pPath), g_Git.CombinePath(pPath),
173 rev1, git_revnum_t(GIT_REV_ZERO)
174 , flags, jumpToLine);
176 return 0;
179 int CGitDiff::SubmoduleDiff(const CTGitPath * pPath, const CTGitPath * /*pPath2*/, const git_revnum_t &rev1, const git_revnum_t &rev2, bool /*blame*/, bool /*unified*/)
181 CString oldhash;
182 CString newhash;
183 bool dirty = false;
184 CString cmd;
185 bool isWorkingCopy = false;
186 if( rev2 == GIT_REV_ZERO || rev1 == GIT_REV_ZERO )
188 oldhash = GIT_REV_ZERO;
189 newhash = GIT_REV_ZERO;
191 CString rev;
192 if( rev2 != GIT_REV_ZERO )
193 rev = rev2;
194 if( rev1 != GIT_REV_ZERO )
195 rev = rev1;
197 isWorkingCopy = true;
199 cmd.Format(_T("git.exe diff %s -- \"%s\""),
200 rev,pPath->GetGitPathString());
202 CString output, err;
203 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
205 CMessageBox::Show(NULL, output + L"\n" + err, _T("TortoiseGit"), MB_OK|MB_ICONERROR);
206 return -1;
209 if (output.IsEmpty())
211 output.Empty();
212 err.Empty();
213 // also compare against index
214 cmd.Format(_T("git.exe diff -- \"%s\""), pPath->GetGitPathString());
215 if (g_Git.Run(cmd, &output, &err, CP_UTF8))
217 CMessageBox::Show(NULL, output + _T("\n") + err, _T("TortoiseGit"), MB_OK | MB_ICONERROR);
218 return -1;
221 if (output.IsEmpty())
223 CMessageBox::Show(NULL, CString(MAKEINTRESOURCE(IDS_ERR_EMPTYDIFF)), _T("TortoiseGit"), MB_OK | MB_ICONERROR);
224 return -1;
226 else if (CMessageBox::Show(NULL, CString(MAKEINTRESOURCE(IDS_SUBMODULE_EMPTYDIFF)), _T("TortoiseGit"), 1, IDI_QUESTION, CString(MAKEINTRESOURCE(IDS_MSGBOX_YES)), CString(MAKEINTRESOURCE(IDS_MSGBOX_NO))) == 1)
228 CString sCmd;
229 sCmd.Format(_T("/command:subupdate /bkpath:\"%s\""), g_Git.m_CurrentDir);
230 CAppUtils::RunTortoiseGitProc(sCmd);
232 return -1;
235 int start =0;
236 int oldstart = output.Find(_T("-Subproject commit"),start);
237 if(oldstart<0)
239 CMessageBox::Show(NULL,_T("Subproject Diff Format error") ,_T("TortoiseGit"),MB_OK|MB_ICONERROR);
240 return -1;
242 oldhash = output.Mid(oldstart+ CString(_T("-Subproject commit")).GetLength()+1,40);
243 start = 0;
244 int newstart = output.Find(_T("+Subproject commit"),start);
245 if (newstart < 0)
247 CMessageBox::Show(NULL,_T("Subproject Diff Format error") ,_T("TortoiseGit"),MB_OK|MB_ICONERROR);
248 return -1;
250 newhash = output.Mid(newstart+ CString(_T("+Subproject commit")).GetLength()+1,40);
251 dirty = output.Mid(newstart + CString(_T("+Subproject commit")).GetLength() + 41) == _T("-dirty\n");
253 else
255 cmd.Format(_T("git.exe diff-tree -r -z %s %s -- \"%s\""),
256 rev2,rev1,pPath->GetGitPathString());
258 BYTE_VECTOR bytes, errBytes;
259 if(g_Git.Run(cmd, &bytes, &errBytes))
261 CString err;
262 CGit::StringAppend(&err, &errBytes[0], CP_UTF8);
263 CMessageBox::Show(NULL,err,_T("TortoiseGit"),MB_OK|MB_ICONERROR);
264 return -1;
267 if (bytes.size() < 15 + 2 * GIT_HASH_SIZE + 1 + 2 * GIT_HASH_SIZE)
269 CMessageBox::Show(NULL, _T("git diff-tree gives invalid output"), _T("TortoiseGit"), MB_OK | MB_ICONERROR);
270 return -1;
272 CGit::StringAppend(&oldhash, &bytes[15], CP_UTF8, 2 * GIT_HASH_SIZE);
273 CGit::StringAppend(&newhash, &bytes[15 + 2 * GIT_HASH_SIZE + 1], CP_UTF8, 2 * GIT_HASH_SIZE);
277 CString oldsub;
278 CString newsub;
279 bool oldOK = false, newOK = false;
281 CGit subgit;
282 subgit.m_CurrentDir = g_Git.CombinePath(pPath);
283 ChangeType changeType = Unknown;
285 if (pPath->HasAdminDir())
286 GetSubmoduleChangeType(subgit, oldhash, newhash, oldOK, newOK, changeType, oldsub, newsub);
288 CSubmoduleDiffDlg submoduleDiffDlg;
289 submoduleDiffDlg.SetDiff(pPath->GetWinPath(), isWorkingCopy, oldhash, oldsub, oldOK, newhash, newsub, newOK, dirty, changeType);
290 submoduleDiffDlg.DoModal();
291 if (submoduleDiffDlg.IsRefresh())
292 return 1;
294 return 0;
297 void CGitDiff::GetSubmoduleChangeType(CGit& subgit, const CString& oldhash, const CString& newhash, bool& oldOK, bool& newOK, ChangeType& changeType, CString& oldsub, CString& newsub)
299 CString cmd;
300 int encode = CAppUtils::GetLogOutputEncode(&subgit);
301 int oldTime = 0, newTime = 0;
303 if (oldhash != GIT_REV_ZERO)
305 CString cmdout, cmderr;
306 cmd.Format(_T("git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --"), oldhash);
307 oldOK = !subgit.Run(cmd, &cmdout, &cmderr, encode);
308 if (oldOK)
310 int pos = cmdout.Find(_T(" "));
311 oldTime = _ttoi(cmdout.Left(pos));
312 oldsub = cmdout.Mid(pos + 1);
314 else
315 oldsub = cmderr;
317 if (newhash != GIT_REV_ZERO)
319 CString cmdout, cmderr;
320 cmd.Format(_T("git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --"), newhash);
321 newOK = !subgit.Run(cmd, &cmdout, &cmderr, encode);
322 if (newOK)
324 int pos = cmdout.Find(_T(" "));
325 newTime = _ttoi(cmdout.Left(pos));
326 newsub = cmdout.Mid(pos + 1);
328 else
329 newsub = cmderr;
332 if (oldhash == GIT_REV_ZERO)
334 oldOK = true;
335 changeType = NewSubmodule;
337 else if (newhash == GIT_REV_ZERO)
339 newOK = true;
340 changeType = DeleteSubmodule;
342 else if (oldhash != newhash)
344 bool ffNewer = false, ffOlder = false;
345 ffNewer = subgit.IsFastForward(oldhash, newhash);
346 if (!ffNewer)
348 ffOlder = subgit.IsFastForward(newhash, oldhash);
349 if (!ffOlder)
351 if (newTime > oldTime)
352 changeType = NewerTime;
353 else if (newTime < oldTime)
354 changeType = OlderTime;
355 else
356 changeType = SameTime;
358 else
359 changeType = Rewind;
361 else
362 changeType = FastForward;
364 else if (oldhash == newhash)
365 changeType = Identical;
367 if (!oldOK || !newOK)
368 changeType = Unknown;
371 int CGitDiff::Diff(const CTGitPath * pPath, const CTGitPath * pPath2, git_revnum_t rev1, git_revnum_t rev2, bool /*blame*/, bool /*unified*/, int jumpToLine)
373 CString temppath;
374 GetTempPath(temppath);
376 // make sure we have HASHes here, otherwise filenames might be invalid
377 if (rev1 != GIT_REV_ZERO)
379 CGitHash rev1Hash;
380 if (g_Git.GetHash(rev1Hash, rev1))
382 MessageBox(NULL, g_Git.GetGitLastErr(_T("Could not get hash of \"") + rev1 + _T("\".")), _T("TortoiseGit"), MB_ICONERROR);
383 return -1;
385 rev1 = rev1Hash.ToString();
387 if (rev2 != GIT_REV_ZERO)
389 CGitHash rev2Hash;
390 if (g_Git.GetHash(rev2Hash, rev2))
392 MessageBox(NULL, g_Git.GetGitLastErr(_T("Could not get hash of \"") + rev2 + _T("\".")), _T("TortoiseGit"), MB_ICONERROR);
393 return -1;
395 rev2 = rev2Hash.ToString();
398 CString file1;
399 CString title1;
400 CString cmd;
402 if(pPath->IsDirectory() || pPath2->IsDirectory())
404 int result;
405 // refresh if result = 1
406 CTGitPath path = *pPath;
407 CTGitPath path2 = *pPath2;
408 while ((result = SubmoduleDiff(&path, &path2, rev1, rev2)) == 1)
410 path.SetFromGit(pPath->GetGitPathString());
411 path2.SetFromGit(pPath2->GetGitPathString());
413 return result;
416 if(rev1 != GIT_REV_ZERO )
418 TCHAR szTempName[MAX_PATH] = {0};
419 GetTempFileName(temppath, pPath->GetBaseFilename(), 0, szTempName);
420 CString temp(szTempName);
421 DeleteFile(szTempName);
422 CreateDirectory(szTempName, NULL);
423 // use original file extension, an external diff tool might need it
424 file1.Format(_T("%s\\%s-%s-right%s"),
425 temp,
426 pPath->GetBaseFilename(),
427 rev1.Left(g_Git.GetShortHASHLength()),
428 pPath->GetFileExtension());
429 title1 = pPath->GetFileOrDirectoryName() + _T(":") + rev1.Left(g_Git.GetShortHASHLength());
430 if (g_Git.GetOneFile(rev1, *pPath, file1))
432 CString out;
433 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, pPath->GetGitPathString(), rev1, file1);
434 CMessageBox::Show(nullptr, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), _T("TortoiseGit"), MB_OK);
435 return -1;
437 ::SetFileAttributes(file1, FILE_ATTRIBUTE_READONLY);
439 else
441 file1 = g_Git.CombinePath(pPath);
442 title1.Format( IDS_DIFF_WCNAME, pPath->GetFileOrDirectoryName() );
443 if (!PathFileExists(file1))
445 CString sMsg;
446 sMsg.Format(IDS_PROC_DIFFERROR_FILENOTINWORKINGTREE, file1);
447 if (MessageBox(NULL, sMsg, _T("TortoiseGit"), MB_ICONEXCLAMATION | MB_YESNO) != IDYES)
448 return 1;
449 if (!CCommonAppUtils::FileOpenSave(file1, NULL, IDS_SELECTFILE, IDS_COMMONFILEFILTER, true))
450 return 1;
451 title1.Format(IDS_DIFF_WCNAME, CTGitPath(file1).GetUIFileOrDirectoryName());
455 CString file2;
456 CString title2;
457 if(rev2 != GIT_REV_ZERO)
459 TCHAR szTempName[MAX_PATH] = {0};
460 GetTempFileName(temppath, pPath2->GetBaseFilename(), 0, szTempName);
461 CString temp(szTempName);
462 DeleteFile(szTempName);
463 CreateDirectory(szTempName, NULL);
464 CTGitPath fileName = *pPath2;
465 if (pPath2->m_Action & CTGitPath::LOGACTIONS_REPLACED)
466 fileName = CTGitPath(pPath2->GetGitOldPathString());
468 // use original file extension, an external diff tool might need it
469 file2.Format(_T("%s\\%s-%s-left%s"),
470 temp,
471 fileName.GetBaseFilename(),
472 rev2.Left(g_Git.GetShortHASHLength()),
473 fileName.GetFileExtension());
474 title2 = fileName.GetFileOrDirectoryName() + _T(":") + rev2.Left(g_Git.GetShortHASHLength());
475 if (g_Git.GetOneFile(rev2, fileName, file2))
477 CString out;
478 out.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED, pPath->GetGitPathString(), rev2, file2);
479 CMessageBox::Show(nullptr, g_Git.GetGitLastErr(out, CGit::GIT_CMD_GETONEFILE), _T("TortoiseGit"), MB_OK);
480 return -1;
482 ::SetFileAttributes(file2, FILE_ATTRIBUTE_READONLY);
484 else
486 file2 = g_Git.CombinePath(pPath2);
487 title2.Format( IDS_DIFF_WCNAME, pPath2->GetFileOrDirectoryName() );
490 if (pPath->m_Action == pPath->LOGACTIONS_ADDED)
492 CGitDiff::DiffNull(pPath, rev1, true, jumpToLine);
494 else if (pPath->m_Action == pPath->LOGACTIONS_DELETED)
496 CGitDiff::DiffNull(pPath, rev2, false, jumpToLine);
498 else
500 CAppUtils::DiffFlags flags;
501 CAppUtils::StartExtDiff(file2,file1,
502 title2,
503 title1,
504 g_Git.CombinePath(pPath2),
505 g_Git.CombinePath(pPath),
506 rev2,
507 rev1,
508 flags, jumpToLine);
510 return 0;
513 int CGitDiff::DiffCommit(const CTGitPath &path, const GitRev *r1, const GitRev *r2)
515 return DiffCommit(path, path, r1, r2);
518 int CGitDiff::DiffCommit(const CTGitPath &path1, const CTGitPath &path2, const GitRev *r1, const GitRev *r2)
520 if (path1.GetWinPathString().IsEmpty())
522 CFileDiffDlg dlg;
523 dlg.SetDiff(NULL, *r1, *r2);
524 dlg.DoModal();
526 else if (path1.IsDirectory())
528 CFileDiffDlg dlg;
529 dlg.SetDiff(&path1, *r1, *r2);
530 dlg.DoModal();
532 else
534 Diff(&path1, &path2, r1->m_CommitHash.ToString(), r2->m_CommitHash.ToString());
536 return 0;
539 int CGitDiff::DiffCommit(const CTGitPath &path, const CString &r1, const CString &r2)
541 return DiffCommit(path, path, r1, r2);
545 int CGitDiff::DiffCommit(const CTGitPath &path1, const CTGitPath &path2, const CString &r1, const CString &r2)
547 if (path1.GetWinPathString().IsEmpty())
549 CFileDiffDlg dlg;
550 dlg.SetDiff(NULL, r1, r2);
551 dlg.DoModal();
553 else if (path1.IsDirectory())
555 CFileDiffDlg dlg;
556 dlg.SetDiff(&path1, r1, r2);
557 dlg.DoModal();
559 else
561 Diff(&path1, &path2, r1, r2);
563 return 0;