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.
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
;
44 if (rev1
!= GIT_REV_ZERO
)
45 cmd
.Format(_T("git.exe ls-tree \"%s\" -- \"%s\""), (LPCTSTR
)rev1
, (LPCTSTR
)pPath
->GetGitPathString());
47 cmd
.Format(_T("git.exe ls-files -s -- \"%s\""), (LPCTSTR
)pPath
->GetGitPathString());
50 if (g_Git
.Run(cmd
, &output
, &err
, CP_UTF8
))
52 CMessageBox::Show(nullptr, output
+ L
"\n" + err
, _T("TortoiseGit"), MB_OK
| MB_ICONERROR
);
57 start
=output
.Find(_T(' '),start
);
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);
63 newhash
=output
.Mid(start
+1, 40);
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 --"), (LPCTSTR
)newhash
);
70 bool toOK
= !subgit
.Run(cmd
,&newsub
,encode
);
73 if (rev1
== GIT_REV_ZERO
)
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())
89 if (rev1
!= GIT_REV_ZERO
)
90 CMessageBox::Show(nullptr, _T("ls-tree output format error"), _T("TortoiseGit"), MB_OK
| MB_ICONERROR
);
92 CMessageBox::Show(nullptr, _T("ls-files output format error"), _T("TortoiseGit"), MB_OK
| MB_ICONERROR
);
96 int CGitDiff::DiffNull(const CTGitPath
*pPath
, git_revnum_t rev1
, bool bIsAdd
, int jumpToLine
, bool bAlternative
)
99 GetTempPath(temppath
);
100 if (rev1
!= GIT_REV_ZERO
)
103 if (g_Git
.GetHash(rev1Hash
, rev1
)) // make sure we have a HASH here, otherwise filenames might be invalid
105 MessageBox(nullptr, g_Git
.GetGitLastErr(_T("Could not get hash of \"") + rev1
+ _T("\".")), _T("TortoiseGit"), MB_ICONERROR
);
108 rev1
= rev1Hash
.ToString();
114 if(pPath
->IsDirectory())
117 // refresh if result = 1
118 CTGitPath path
= *pPath
;
119 while ((result
= SubmoduleDiffNull(&path
, rev1
)) == 1)
120 path
.SetFromGit(pPath
->GetGitPathString());
124 if(rev1
!= GIT_REV_ZERO
)
126 TCHAR szTempName
[MAX_PATH
] = {0};
127 GetTempFileName(temppath
, pPath
->GetBaseFilename(), 0, szTempName
);
128 CString
temp(szTempName
);
129 DeleteFile(szTempName
);
130 CreateDirectory(szTempName
, nullptr);
131 file1
.Format(_T("%s\\%s-%s%s"),
133 (LPCTSTR
)pPath
->GetBaseFilename(),
134 (LPCTSTR
)rev1
.Left(g_Git
.GetShortHASHLength()),
135 (LPCTSTR
)pPath
->GetFileExtension());
137 if (g_Git
.GetOneFile(rev1
, *pPath
, file1
))
140 out
.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED
, (LPCTSTR
)pPath
->GetGitPathString(), (LPCTSTR
)rev1
, (LPCTSTR
)file1
);
141 CMessageBox::Show(nullptr, g_Git
.GetGitLastErr(out
, CGit::GIT_CMD_GETONEFILE
), _T("TortoiseGit"), MB_OK
);
146 file1
= g_Git
.CombinePath(pPath
);
148 // preserve FileExtension, needed especially for diffing deleted images (detection on new filename extension)
149 CString tempfile
=::GetTempFile() + pPath
->GetFileExtension();
150 CStdioFile
file(tempfile
,CFile::modeReadWrite
|CFile::modeCreate
);
151 //file.WriteString();
153 ::SetFileAttributes(tempfile
, FILE_ATTRIBUTE_READONLY
);
155 CAppUtils::DiffFlags flags
;
156 flags
.bAlternativeTool
= bAlternative
;
158 CAppUtils::StartExtDiff(tempfile
,file1
,
159 pPath
->GetGitPathString(),
160 pPath
->GetGitPathString() + _T(":") + rev1
.Left(g_Git
.GetShortHASHLength()),
161 g_Git
.CombinePath(pPath
), g_Git
.CombinePath(pPath
),
162 git_revnum_t(GIT_REV_ZERO
), rev1
163 , flags
, jumpToLine
);
165 CAppUtils::StartExtDiff(file1
,tempfile
,
166 pPath
->GetGitPathString() + _T(":") + rev1
.Left(g_Git
.GetShortHASHLength()),
167 pPath
->GetGitPathString(),
168 g_Git
.CombinePath(pPath
), g_Git
.CombinePath(pPath
),
169 rev1
, git_revnum_t(GIT_REV_ZERO
)
170 , flags
, jumpToLine
);
175 int CGitDiff::SubmoduleDiff(const CTGitPath
* pPath
, const CTGitPath
* /*pPath2*/, const git_revnum_t
&rev1
, const git_revnum_t
&rev2
, bool /*blame*/, bool /*unified*/)
181 bool isWorkingCopy
= false;
182 if( rev2
== GIT_REV_ZERO
|| rev1
== GIT_REV_ZERO
)
184 oldhash
= GIT_REV_ZERO
;
185 newhash
= GIT_REV_ZERO
;
188 if( rev2
!= GIT_REV_ZERO
)
190 if( rev1
!= GIT_REV_ZERO
)
193 isWorkingCopy
= true;
195 cmd
.Format(_T("git.exe diff %s -- \"%s\""),
196 (LPCTSTR
)rev
, (LPCTSTR
)pPath
->GetGitPathString());
199 if (g_Git
.Run(cmd
, &output
, &err
, CP_UTF8
))
201 CMessageBox::Show(nullptr, output
+ L
"\n" + err
, _T("TortoiseGit"), MB_OK
| MB_ICONERROR
);
205 if (output
.IsEmpty())
209 // also compare against index
210 cmd
.Format(_T("git.exe diff -- \"%s\""), (LPCTSTR
)pPath
->GetGitPathString());
211 if (g_Git
.Run(cmd
, &output
, &err
, CP_UTF8
))
213 CMessageBox::Show(nullptr, output
+ _T("\n") + err
, _T("TortoiseGit"), MB_OK
| MB_ICONERROR
);
217 if (output
.IsEmpty())
219 CMessageBox::Show(nullptr, IDS_ERR_EMPTYDIFF
, IDS_APPNAME
, MB_OK
| MB_ICONERROR
);
222 else if (CMessageBox::Show(nullptr, IDS_SUBMODULE_EMPTYDIFF
, IDS_APPNAME
, 1, IDI_QUESTION
, IDS_MSGBOX_YES
, IDS_MSGBOX_NO
) == 1)
225 sCmd
.Format(_T("/command:subupdate /bkpath:\"%s\""), (LPCTSTR
)g_Git
.m_CurrentDir
);
226 CAppUtils::RunTortoiseGitProc(sCmd
);
232 int oldstart
= output
.Find(_T("-Subproject commit"),start
);
235 CMessageBox::Show(nullptr, _T("Subproject Diff Format error"), _T("TortoiseGit"), MB_OK
| MB_ICONERROR
);
238 oldhash
= output
.Mid(oldstart
+ CString(_T("-Subproject commit")).GetLength()+1,40);
240 int newstart
= output
.Find(_T("+Subproject commit"),start
);
243 CMessageBox::Show(nullptr, _T("Subproject Diff Format error"), _T("TortoiseGit"), MB_OK
| MB_ICONERROR
);
246 newhash
= output
.Mid(newstart
+ CString(_T("+Subproject commit")).GetLength()+1,40);
247 dirty
= output
.Mid(newstart
+ CString(_T("+Subproject commit")).GetLength() + 41) == _T("-dirty\n");
251 cmd
.Format(_T("git.exe diff-tree -r -z %s %s -- \"%s\""),
252 (LPCTSTR
)rev2
, (LPCTSTR
)rev1
, (LPCTSTR
)pPath
->GetGitPathString());
254 BYTE_VECTOR bytes
, errBytes
;
255 if(g_Git
.Run(cmd
, &bytes
, &errBytes
))
258 CGit::StringAppend(&err
, &errBytes
[0], CP_UTF8
);
259 CMessageBox::Show(nullptr, err
, _T("TortoiseGit"), MB_OK
| MB_ICONERROR
);
263 if (bytes
.size() < 15 + 2 * GIT_HASH_SIZE
+ 1 + 2 * GIT_HASH_SIZE
)
265 CMessageBox::Show(nullptr, _T("git diff-tree gives invalid output"), _T("TortoiseGit"), MB_OK
| MB_ICONERROR
);
268 CGit::StringAppend(&oldhash
, &bytes
[15], CP_UTF8
, 2 * GIT_HASH_SIZE
);
269 CGit::StringAppend(&newhash
, &bytes
[15 + 2 * GIT_HASH_SIZE
+ 1], CP_UTF8
, 2 * GIT_HASH_SIZE
);
275 bool oldOK
= false, newOK
= false;
278 subgit
.m_CurrentDir
= g_Git
.CombinePath(pPath
);
279 ChangeType changeType
= Unknown
;
281 if (pPath
->HasAdminDir())
282 GetSubmoduleChangeType(subgit
, oldhash
, newhash
, oldOK
, newOK
, changeType
, oldsub
, newsub
);
284 CSubmoduleDiffDlg submoduleDiffDlg
;
285 submoduleDiffDlg
.SetDiff(pPath
->GetWinPath(), isWorkingCopy
, oldhash
, oldsub
, oldOK
, newhash
, newsub
, newOK
, dirty
, changeType
);
286 submoduleDiffDlg
.DoModal();
287 if (submoduleDiffDlg
.IsRefresh())
293 void CGitDiff::GetSubmoduleChangeType(CGit
& subgit
, const CString
& oldhash
, const CString
& newhash
, bool& oldOK
, bool& newOK
, ChangeType
& changeType
, CString
& oldsub
, CString
& newsub
)
296 int encode
= CAppUtils::GetLogOutputEncode(&subgit
);
297 int oldTime
= 0, newTime
= 0;
299 if (oldhash
!= GIT_REV_ZERO
)
301 CString cmdout
, cmderr
;
302 cmd
.Format(_T("git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --"), (LPCTSTR
)oldhash
);
303 oldOK
= !subgit
.Run(cmd
, &cmdout
, &cmderr
, encode
);
306 int pos
= cmdout
.Find(_T(" "));
307 oldTime
= _ttoi(cmdout
.Left(pos
));
308 oldsub
= cmdout
.Mid(pos
+ 1);
313 if (newhash
!= GIT_REV_ZERO
)
315 CString cmdout
, cmderr
;
316 cmd
.Format(_T("git.exe log -n1 --pretty=format:\"%%ct %%s\" %s --"), (LPCTSTR
)newhash
);
317 newOK
= !subgit
.Run(cmd
, &cmdout
, &cmderr
, encode
);
320 int pos
= cmdout
.Find(_T(" "));
321 newTime
= _ttoi(cmdout
.Left(pos
));
322 newsub
= cmdout
.Mid(pos
+ 1);
328 if (oldhash
== GIT_REV_ZERO
)
331 changeType
= NewSubmodule
;
333 else if (newhash
== GIT_REV_ZERO
)
336 changeType
= DeleteSubmodule
;
338 else if (oldhash
!= newhash
)
340 bool ffNewer
= false, ffOlder
= false;
341 ffNewer
= subgit
.IsFastForward(oldhash
, newhash
);
344 ffOlder
= subgit
.IsFastForward(newhash
, oldhash
);
347 if (newTime
> oldTime
)
348 changeType
= NewerTime
;
349 else if (newTime
< oldTime
)
350 changeType
= OlderTime
;
352 changeType
= SameTime
;
358 changeType
= FastForward
;
360 else if (oldhash
== newhash
)
361 changeType
= Identical
;
363 if (!oldOK
|| !newOK
)
364 changeType
= Unknown
;
367 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
)
370 GetTempPath(temppath
);
372 // make sure we have HASHes here, otherwise filenames might be invalid
373 if (rev1
!= GIT_REV_ZERO
)
376 if (g_Git
.GetHash(rev1Hash
, rev1
))
378 MessageBox(nullptr, g_Git
.GetGitLastErr(_T("Could not get hash of \"") + rev1
+ _T("\".")), _T("TortoiseGit"), MB_ICONERROR
);
381 rev1
= rev1Hash
.ToString();
383 if (rev2
!= GIT_REV_ZERO
)
386 if (g_Git
.GetHash(rev2Hash
, rev2
))
388 MessageBox(nullptr, g_Git
.GetGitLastErr(_T("Could not get hash of \"") + rev2
+ _T("\".")), _T("TortoiseGit"), MB_ICONERROR
);
391 rev2
= rev2Hash
.ToString();
398 if(pPath
->IsDirectory() || pPath2
->IsDirectory())
401 // refresh if result = 1
402 CTGitPath path
= *pPath
;
403 CTGitPath path2
= *pPath2
;
404 while ((result
= SubmoduleDiff(&path
, &path2
, rev1
, rev2
)) == 1)
406 path
.SetFromGit(pPath
->GetGitPathString());
407 path2
.SetFromGit(pPath2
->GetGitPathString());
412 if(rev1
!= GIT_REV_ZERO
)
414 TCHAR szTempName
[MAX_PATH
] = {0};
415 GetTempFileName(temppath
, pPath
->GetBaseFilename(), 0, szTempName
);
416 CString
temp(szTempName
);
417 DeleteFile(szTempName
);
418 CreateDirectory(szTempName
, nullptr);
419 // use original file extension, an external diff tool might need it
420 file1
.Format(_T("%s\\%s-%s-right%s"),
422 (LPCTSTR
)pPath
->GetBaseFilename(),
423 (LPCTSTR
)rev1
.Left(g_Git
.GetShortHASHLength()),
424 (LPCTSTR
)pPath
->GetFileExtension());
425 title1
= pPath
->GetGitPathString() + L
": " + rev1
.Left(g_Git
.GetShortHASHLength());
426 if (g_Git
.GetOneFile(rev1
, *pPath
, file1
))
429 out
.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED
, (LPCTSTR
)pPath
->GetGitPathString(), (LPCTSTR
)rev1
, (LPCTSTR
)file1
);
430 CMessageBox::Show(nullptr, g_Git
.GetGitLastErr(out
, CGit::GIT_CMD_GETONEFILE
), _T("TortoiseGit"), MB_OK
);
433 ::SetFileAttributes(file1
, FILE_ATTRIBUTE_READONLY
);
437 file1
= g_Git
.CombinePath(pPath
);
438 title1
.Format(IDS_DIFF_WCNAME
, (LPCTSTR
)pPath
->GetGitPathString());
439 if (!PathFileExists(file1
))
442 sMsg
.Format(IDS_PROC_DIFFERROR_FILENOTINWORKINGTREE
, (LPCTSTR
)file1
);
443 if (MessageBox(nullptr, sMsg
, _T("TortoiseGit"), MB_ICONEXCLAMATION
| MB_YESNO
) != IDYES
)
445 if (!CCommonAppUtils::FileOpenSave(file1
, nullptr, IDS_SELECTFILE
, IDS_COMMONFILEFILTER
, true))
453 if(rev2
!= GIT_REV_ZERO
)
455 TCHAR szTempName
[MAX_PATH
] = {0};
456 GetTempFileName(temppath
, pPath2
->GetBaseFilename(), 0, szTempName
);
457 CString
temp(szTempName
);
458 DeleteFile(szTempName
);
459 CreateDirectory(szTempName
, nullptr);
460 CTGitPath fileName
= *pPath2
;
461 if (pPath2
->m_Action
& CTGitPath::LOGACTIONS_REPLACED
)
462 fileName
= CTGitPath(pPath2
->GetGitOldPathString());
464 // use original file extension, an external diff tool might need it
465 file2
.Format(_T("%s\\%s-%s-left%s"),
467 (LPCTSTR
)fileName
.GetBaseFilename(),
468 (LPCTSTR
)rev2
.Left(g_Git
.GetShortHASHLength()),
469 (LPCTSTR
)fileName
.GetFileExtension());
470 title2
= fileName
.GetGitPathString() + L
": " + rev2
.Left(g_Git
.GetShortHASHLength());
471 if (g_Git
.GetOneFile(rev2
, fileName
, file2
))
474 out
.Format(IDS_STATUSLIST_CHECKOUTFILEFAILED
, (LPCTSTR
)pPath2
->GetGitPathString(), (LPCTSTR
)rev2
, (LPCTSTR
)file2
);
475 CMessageBox::Show(nullptr, g_Git
.GetGitLastErr(out
, CGit::GIT_CMD_GETONEFILE
), _T("TortoiseGit"), MB_OK
);
478 ::SetFileAttributes(file2
, FILE_ATTRIBUTE_READONLY
);
482 file2
= g_Git
.CombinePath(pPath2
);
483 title2
.Format(IDS_DIFF_WCNAME
, pPath2
->GetGitPathString());
486 CAppUtils::DiffFlags flags
;
487 flags
.bAlternativeTool
= bAlternativeTool
;
488 CAppUtils::StartExtDiff(file2
,file1
,
491 g_Git
.CombinePath(pPath2
),
492 g_Git
.CombinePath(pPath
),
500 int CGitDiff::DiffCommit(const CTGitPath
& path
, const GitRev
* r1
, const GitRev
* r2
, bool bAlternative
)
502 return DiffCommit(path
, path
, r1
, r2
, bAlternative
);
505 int CGitDiff::DiffCommit(const CTGitPath
& path1
, const CTGitPath
& path2
, const GitRev
* r1
, const GitRev
* r2
, bool bAlternative
)
507 if (path1
.GetWinPathString().IsEmpty())
510 dlg
.SetDiff(nullptr, *r2
, *r1
);
513 else if (path1
.IsDirectory())
516 dlg
.SetDiff(&path1
, *r2
, *r1
);
520 Diff(&path1
, &path2
, r1
->m_CommitHash
.ToString(), r2
->m_CommitHash
.ToString(), false, false, 0, bAlternative
);
524 int CGitDiff::DiffCommit(const CTGitPath
& path
, const CString
& r1
, const CString
& r2
, bool bAlternative
)
526 return DiffCommit(path
, path
, r1
, r2
, bAlternative
);
529 int CGitDiff::DiffCommit(const CTGitPath
& path1
, const CTGitPath
& path2
, const CString
& r1
, const CString
& r2
, bool bAlternative
)
531 if (path1
.GetWinPathString().IsEmpty())
534 dlg
.SetDiff(nullptr, r2
, r1
);
537 else if (path1
.IsDirectory())
540 dlg
.SetDiff(&path1
, r2
, r1
);
544 Diff(&path1
, &path2
, r1
, r2
, false, false, 0, bAlternative
);