1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2017 - TortoiseGit
4 // Copyright (C) 2005-2007 Marco Costalba
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 // GitLogList.cpp : implementation file
23 #include "TortoiseProc.h"
24 #include "GitLogList.h"
28 #include "GitProgressDlg.h"
29 #include "ProgressDlg.h"
30 #include "SysProgressDlg.h"
32 #include "MessageBox.h"
35 #include "StringUtils.h"
36 #include "UnicodeUtils.h"
38 #include "CommitDlg.h"
39 #include "RebaseDlg.h"
40 #include "CommitIsOnRefsDlg.h"
42 #include "../TGitCache/CacheInterface.h"
44 IMPLEMENT_DYNAMIC(CGitLogList
, CHintCtrl
<CListCtrl
>)
46 static bool GetFirstEntryStartingWith(STRING_VECTOR
& heystack
, const CString
& needle
, CString
& result
)
48 auto it
= std::find_if(heystack
.cbegin(), heystack
.cend(), [&needle
](const CString
& entry
) { return CStringUtils::StartsWith(entry
, needle
); });
49 if (it
== heystack
.cend())
55 int CGitLogList::RevertSelectedCommits(int parent
)
57 CSysProgressDlg progress
;
61 if(!g_Git
.CheckCleanWorkTree())
62 CMessageBox::Show(nullptr, IDS_PROC_NOCLEAN
, IDS_APPNAME
, MB_OK
);
65 if (this->GetSelectedCount() > 1)
67 progress
.SetTitle(CString(MAKEINTRESOURCE(IDS_PROGS_TITLE_REVERTCOMMIT
)));
68 progress
.SetTime(true);
69 progress
.ShowModeless(this);
72 CBlockCacheForPath
cacheBlock(g_Git
.m_CurrentDir
);
74 POSITION pos
= GetFirstSelectedItemPosition();
78 int index
= GetNextSelectedItem(pos
);
79 GitRev
* r1
= m_arShownList
.SafeGetAt(index
);
81 if (progress
.IsVisible())
83 progress
.FormatNonPathLine(1, IDS_PROC_REVERTCOMMIT
, (LPCTSTR
)r1
->m_CommitHash
.ToString());
84 progress
.FormatNonPathLine(2, L
"%s", (LPCTSTR
)r1
->GetSubject());
85 progress
.SetProgress(i
, this->GetSelectedCount());
89 if(r1
->m_CommitHash
.IsEmpty())
92 if (g_Git
.GitRevert(parent
, r1
->m_CommitHash
))
95 str
.LoadString(IDS_SVNACTION_FAILEDREVERT
);
96 str
= g_Git
.GetGitLastErr(str
, CGit::GIT_CMD_REVERT
);
97 if( GetSelectedCount() == 1)
98 CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), str
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
99 else if (CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), str
, L
"TortoiseGit", 2, IDI_ERROR
, CString(MAKEINTRESOURCE(IDS_SKIPBUTTON
)), CString(MAKEINTRESOURCE(IDS_ABORTBUTTON
))) == 2)
105 if (progress
.HasUserCancelled())
110 int CGitLogList::CherryPickFrom(CString from
, CString to
)
112 CLogDataVector
logs(&m_LogCache
);
114 range
.Format(L
"%s..%s", (LPCTSTR
)from
, (LPCTSTR
)to
);
115 if (logs
.ParserFromLog(nullptr, 0, 0, &range
))
121 CSysProgressDlg progress
;
122 progress
.SetTitle(CString(MAKEINTRESOURCE(IDS_PROGS_TITLE_CHERRYPICK
)));
123 progress
.SetTime(true);
124 progress
.ShowModeless(this);
126 CBlockCacheForPath
cacheBlock(g_Git
.m_CurrentDir
);
128 for (int i
= (int)logs
.size() - 1; i
>= 0; i
--)
130 if (progress
.IsVisible())
132 progress
.FormatNonPathLine(1, IDS_PROC_PICK
, (LPCTSTR
)logs
.GetGitRevAt(i
).m_CommitHash
.ToString());
133 progress
.FormatNonPathLine(2, L
"%s", (LPCTSTR
)logs
.GetGitRevAt(i
).GetSubject());
134 progress
.SetProgress64(logs
.size() - i
, logs
.size());
136 if (progress
.HasUserCancelled())
137 throw std::exception(CUnicodeUtils::GetUTF8(CString(MAKEINTRESOURCE(IDS_USERCANCELLED
))));
139 cmd
.Format(L
"git.exe cherry-pick %s", (LPCTSTR
)logs
.GetGitRevAt(i
).m_CommitHash
.ToString());
140 if(g_Git
.Run(cmd
,&out
,CP_UTF8
))
141 throw std::exception(CUnicodeUtils::GetUTF8(CString(MAKEINTRESOURCE(IDS_PROC_CHERRYPICKFAILED
)) + L
":\r\n\r\n" + out
));
147 void CGitLogList::ContextMenuAction(int cmd
,int FirstSelect
, int LastSelect
, CMenu
*popmenu
)
149 POSITION pos
= GetFirstSelectedItemPosition();
150 int indexNext
= GetNextSelectedItem(pos
);
154 GitRevLoglist
* pSelLogEntry
= m_arShownList
.SafeGetAt(indexNext
);
156 bool bShiftPressed
= !!(GetAsyncKeyState(VK_SHIFT
) & 0x8000);
158 theApp
.DoWaitCursor(1);
163 CTGitPathList pathlist
;
164 CTGitPathList selectedlist
;
165 pathlist
.AddPath(this->m_Path
);
166 bool bSelectFilesForCommit
= !!DWORD(CRegStdDWORD(L
"Software\\TortoiseGit\\SelectFilesForCommit", TRUE
));
168 CAppUtils::Commit(CString(),false,str
,
169 pathlist
,selectedlist
,bSelectFilesForCommit
);
171 this->GetParent()->PostMessage(WM_COMMAND
,ID_LOGDLG_REFRESH
,0);
176 if (CAppUtils::MergeAbort())
177 this->GetParent()->PostMessage(WM_COMMAND
,ID_LOGDLG_REFRESH
, 0);
180 case ID_GNUDIFF1
: // compare with WC, unified
182 GitRev
* r1
= m_arShownList
.SafeGetAt(FirstSelect
);
183 bool bMerge
= false, bCombine
= false;
185 if(!r1
->m_CommitHash
.IsEmpty())
189 if( (cmd
&0xFFFF) == 0xFFFF)
191 else if((cmd
&0xFFFF) == 0xFFFE)
193 else if ((cmd
& 0xFFFF) == 0xFFFD)
195 CString tempfile
= GetTempFile();
196 CString gitcmd
= L
"git.exe diff-tree --cc " + r1
->m_CommitHash
.ToString();
198 if (g_Git
.RunLogFile(gitcmd
, tempfile
, &lastErr
))
200 MessageBox(lastErr
, L
"TortoiseGit", MB_ICONERROR
);
206 CStdioFile
file(tempfile
, CFile::typeText
| CFile::modeRead
| CFile::shareDenyWrite
);
208 bool isHash
= file
.ReadString(strLine
) && r1
->m_CommitHash
.ToString() == strLine
;
209 bool more
= isHash
&& file
.ReadString(strLine
) && !strLine
.IsEmpty();
212 CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), IDS_NOCHANGEAFTERMERGE
, IDS_APPNAME
, MB_OK
);
216 catch (CFileException
* e
)
221 CAppUtils::StartUnifiedDiffViewer(tempfile
, r1
->m_CommitHash
.ToString());
228 if ((size_t)cmd
> r1
->m_ParentHash
.size())
231 str
.Format(IDS_PROC_NOPARENT
, cmd
);
232 MessageBox(str
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
238 hash2
= r1
->m_ParentHash
[cmd
-1].ToString();
241 CAppUtils::StartShowUnifiedDiff(nullptr, CTGitPath(), hash2
, CTGitPath(), r1
->m_CommitHash
.ToString(), bShiftPressed
, false, false, bMerge
, bCombine
);
244 CAppUtils::StartShowUnifiedDiff(nullptr, CTGitPath(), L
"HEAD", CTGitPath(), GitRev::GetWorkingCopy(), bShiftPressed
, false, false, bMerge
, bCombine
);
248 case ID_GNUDIFF2
: // compare two revisions, unified
250 GitRev
* r1
= m_arShownList
.SafeGetAt(FirstSelect
);
251 GitRev
* r2
= m_arShownList
.SafeGetAt(LastSelect
);
252 CAppUtils::StartShowUnifiedDiff(nullptr, CTGitPath(), r2
->m_CommitHash
.ToString(), CTGitPath(), r1
->m_CommitHash
.ToString(), bShiftPressed
);
256 case ID_COMPARETWO
: // compare two revisions
258 GitRev
* r1
= m_arShownList
.SafeGetAt(FirstSelect
);
259 GitRev
* r2
= m_arShownList
.SafeGetAt(LastSelect
);
260 if (m_Path
.IsDirectory() || !(m_ShowMask
& CGit::LOG_INFO_FOLLOW
))
261 CGitDiff::DiffCommit(m_Path
, r1
, r2
, bShiftPressed
);
264 CString path1
= m_Path
.GetGitPathString();
265 // start with 1 (0 = working copy changes)
266 for (int i
= m_bShowWC
? 1 : 0; i
< FirstSelect
; ++i
)
268 GitRevLoglist
* first
= m_arShownList
.SafeGetAt(i
);
269 CTGitPathList list
= first
->GetFiles(nullptr);
270 const CTGitPath
* file
= list
.LookForGitPath(path1
);
271 if (file
&& !file
->GetGitOldPathString().IsEmpty())
272 path1
= file
->GetGitOldPathString();
274 CString path2
= path1
;
275 for (int i
= FirstSelect
; i
< LastSelect
; ++i
)
277 GitRevLoglist
* first
= m_arShownList
.SafeGetAt(i
);
278 CTGitPathList list
= first
->GetFiles(nullptr);
279 const CTGitPath
* file
= list
.LookForGitPath(path2
);
280 if (file
&& !file
->GetGitOldPathString().IsEmpty())
281 path2
= file
->GetGitOldPathString();
283 CGitDiff::DiffCommit(CTGitPath(path1
), CTGitPath(path2
), r1
, r2
, bShiftPressed
);
289 case ID_COMPARE
: // compare revision with WC
291 GitRevLoglist
* r1
= &m_wcRev
;
292 GitRevLoglist
* r2
= pSelLogEntry
;
294 if (m_Path
.IsDirectory() || !(m_ShowMask
& CGit::LOG_INFO_FOLLOW
))
295 CGitDiff::DiffCommit(m_Path
, r1
, r2
, bShiftPressed
);
298 CString path1
= m_Path
.GetGitPathString();
299 // start with 1 (0 = working copy changes)
300 for (int i
= m_bShowWC
? 1 : 0; i
< FirstSelect
; ++i
)
302 GitRevLoglist
* first
= m_arShownList
.SafeGetAt(i
);
303 CTGitPathList list
= first
->GetFiles(nullptr);
304 const CTGitPath
* file
= list
.LookForGitPath(path1
);
305 if (file
&& !file
->GetGitOldPathString().IsEmpty())
306 path1
= file
->GetGitOldPathString();
308 CGitDiff::DiffCommit(m_Path
, CTGitPath(path1
), r1
, r2
, bShiftPressed
);
311 //user clicked on the menu item "compare with working copy"
314 // GitDiff diff(this, m_hWnd, true);
315 // diff.SetAlternativeTool(!!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
316 // diff.SetHEADPeg(m_LogRevision);
317 // diff.ShowCompare(m_path, GitRev::REV_WC, m_path, revSelected);
320 // CAppUtils::StartShowCompare(m_hWnd, m_path, GitRev::REV_WC, m_path, revSelected, GitRev(), m_LogRevision, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
324 case ID_COMPAREWITHPREVIOUS
:
326 if (pSelLogEntry
->m_ParentHash
.empty())
328 if (pSelLogEntry
->GetParentFromHash(pSelLogEntry
->m_CommitHash
))
329 MessageBox(pSelLogEntry
->GetLastErr(), L
"TortoiseGit", MB_ICONERROR
);
332 if (!pSelLogEntry
->m_ParentHash
.empty())
333 //if(m_logEntries.m_HashMap[pSelLogEntry->m_ParentHash[0]]>=0)
341 if (m_Path
.IsDirectory() || !(m_ShowMask
& CGit::LOG_INFO_FOLLOW
))
342 CGitDiff::DiffCommit(m_Path
, pSelLogEntry
->m_CommitHash
.ToString(), pSelLogEntry
->m_ParentHash
[cmd
- 1].ToString(), bShiftPressed
);
345 CString path1
= m_Path
.GetGitPathString();
346 // start with 1 (0 = working copy changes)
347 for (int i
= m_bShowWC
? 1 : 0; i
< indexNext
; ++i
)
349 GitRevLoglist
* first
= m_arShownList
.SafeGetAt(i
);
350 CTGitPathList list
= first
->GetFiles(nullptr);
351 const CTGitPath
* file
= list
.LookForGitPath(path1
);
352 if (file
&& !file
->GetGitOldPathString().IsEmpty())
353 path1
= file
->GetGitOldPathString();
355 CString path2
= path1
;
356 GitRevLoglist
* first
= m_arShownList
.SafeGetAt(indexNext
);
357 CTGitPathList list
= first
->GetFiles(nullptr);
358 const CTGitPath
* file
= list
.LookForGitPath(path2
);
359 if (file
&& !file
->GetGitOldPathString().IsEmpty())
360 path2
= file
->GetGitOldPathString();
362 CGitDiff::DiffCommit(CTGitPath(path1
), CTGitPath(path2
), pSelLogEntry
->m_CommitHash
.ToString(), pSelLogEntry
->m_ParentHash
[cmd
- 1].ToString(), bShiftPressed
);
367 CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), IDS_PROC_NOPREVIOUSVERSION
, IDS_APPNAME
, MB_OK
);
371 // GitDiff diff(this, m_hWnd, true);
372 // diff.SetAlternativeTool(!!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
373 // diff.SetHEADPeg(m_LogRevision);
374 // diff.ShowCompare(CTGitPath(pathURL), revPrevious, CTGitPath(pathURL), revSelected);
377 // CAppUtils::StartShowCompare(m_hWnd, CTGitPath(pathURL), revPrevious, CTGitPath(pathURL), revSelected, GitRev(), m_LogRevision, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
380 case ID_COMPARETWOCOMMITCHANGES
:
382 auto pFirstEntry
= m_arShownList
.SafeGetAt(FirstSelect
);
383 auto pLastEntry
= m_arShownList
.SafeGetAt(LastSelect
);
384 CString patch1
= GetTempFile();
385 CString patch2
= GetTempFile();
387 if (g_Git
.RunLogFile(L
"git.exe format-patch --stdout " + pFirstEntry
->m_CommitHash
.ToString() + L
"~1.." + pFirstEntry
->m_CommitHash
.ToString() + L
"", patch1
, &err
))
389 MessageBox(L
"Could not generate patch for commit " + pFirstEntry
->m_CommitHash
.ToString() + L
".\n" + err
, L
"TortoiseGit", MB_ICONERROR
);
392 if (g_Git
.RunLogFile(L
"git.exe format-patch --stdout " + pLastEntry
->m_CommitHash
.ToString() + L
"~1.." + pLastEntry
->m_CommitHash
.ToString() + L
"", patch2
, &err
))
394 MessageBox(L
"Could not generate patch for commit " + pLastEntry
->m_CommitHash
.ToString() + L
".\n" + err
, L
"TortoiseGit", MB_ICONERROR
);
397 CAppUtils::DiffFlags flags
;
398 CAppUtils::StartExtDiff(patch1
, patch2
, pFirstEntry
->m_CommitHash
.ToString(), pLastEntry
->m_CommitHash
.ToString(), pFirstEntry
->m_CommitHash
.ToString() + L
".patch", pLastEntry
->m_CommitHash
.ToString() + L
".patch", pFirstEntry
->m_CommitHash
.ToString(), pLastEntry
->m_CommitHash
.ToString(), flags
);
401 case ID_LOG_VIEWRANGE
:
402 case ID_LOG_VIEWRANGE_REACHABLEFROMONLYONE
:
404 GitRev
* pLastEntry
= m_arShownList
.SafeGetAt(LastSelect
);
407 if ((cmd
& 0xFFFF) == ID_LOG_VIEWRANGE_REACHABLEFROMONLYONE
)
411 cmdline
.Format(L
"/command:log /path:\"%s\" /range:\"%s%s%s\"",
412 (LPCTSTR
)g_Git
.CombinePath(m_Path
), (LPCTSTR
)pLastEntry
->m_CommitHash
.ToString(), (LPCTSTR
)sep
, (LPCTSTR
)pSelLogEntry
->m_CommitHash
.ToString());
413 CAppUtils::RunTortoiseGitProc(cmdline
);
416 case ID_COPYCLIPBOARDFULL
:
417 case ID_COPYCLIPBOARDFULLNOPATHS
:
418 case ID_COPYCLIPBOARDHASH
:
419 case ID_COPYCLIPBOARDAUTHORSFULL
:
420 case ID_COPYCLIPBOARDAUTHORSNAME
:
421 case ID_COPYCLIPBOARDAUTHORSEMAIL
:
422 case ID_COPYCLIPBOARDSUBJECTS
:
423 case ID_COPYCLIPBOARDMESSAGES
:
425 CopySelectionToClipBoard(cmd
& 0xFFFF);
428 case ID_COPYCLIPBOARDBRANCHTAG
:
432 auto selectedBranch
= reinterpret_cast<const CString
*>(reinterpret_cast<CIconMenu
*>(popmenu
)->GetMenuItemData(cmd
));
436 if (CStringUtils::StartsWith(*selectedBranch
, L
"refs/tags/"))
437 sClipboard
= selectedBranch
->Mid((int)wcslen(L
"refs/tags/")).TrimRight(L
"^{}");
439 sClipboard
= CGit::StripRefName(*selectedBranch
);
443 for (const auto& ref
: m_HashMap
[pSelLogEntry
->m_CommitHash
])
446 sClipboard
+= L
"\r\n";
449 CStringUtils::WriteAsciiStringToClipboard(sClipboard
, GetSafeHwnd());
454 CString str
=pSelLogEntry
->m_CommitHash
.ToString();
455 // try to get the tag
456 GetFirstEntryStartingWith(m_HashMap
[pSelLogEntry
->m_CommitHash
], L
"refs/tags/", str
);
457 CAppUtils::Export(&str
, &m_Path
);
460 case ID_CREATE_BRANCH
:
463 const CString
* branch
= popmenu
? (const CString
*)((CIconMenu
*)popmenu
)->GetMenuItemData(cmd
& 0xFFFF) : nullptr;
464 CString str
= pSelLogEntry
->m_CommitHash
.ToString();
467 else // try to guess remote branch in order to enable tracking
468 GetFirstEntryStartingWith(m_HashMap
[pSelLogEntry
->m_CommitHash
], L
"refs/remotes/", str
);
470 CAppUtils::CreateBranchTag((cmd
&0xFFFF) == ID_CREATE_TAG
, &str
);
473 m_pFindDialog
->RefreshList();
475 ::PostMessage(this->GetParent()->m_hWnd
,MSG_REFLOG_CHANGED
,0,0);
480 CString str
= pSelLogEntry
->m_CommitHash
.ToString();
481 const CString
* branch
= popmenu
? (const CString
*)((CIconMenu
*)popmenu
)->GetMenuItemData(cmd
& 0xFFFF) : nullptr;
484 else // try to guess remote branch in order to recommend good branch name and tracking
485 GetFirstEntryStartingWith(m_HashMap
[pSelLogEntry
->m_CommitHash
], L
"refs/remotes/", str
);
487 CAppUtils::Switch(str
);
491 ::PostMessage(this->GetParent()->m_hWnd
,MSG_REFLOG_CHANGED
,0,0);
493 case ID_SWITCHBRANCH
:
496 const CString
* branch
= (const CString
*)((CIconMenu
*)popmenu
)->GetMenuItemData(cmd
);
499 CString name
= *branch
;
500 CGit::GetShortName(*branch
, name
, L
"refs/heads/");
501 CAppUtils::PerformSwitch(name
);
505 ::PostMessage(this->GetParent()->m_hWnd
,MSG_REFLOG_CHANGED
,0,0);
510 CString str
= pSelLogEntry
->m_CommitHash
.ToString();
511 if (CAppUtils::GitReset(&str
))
520 SetSelectedRebaseAction(LOGACTIONS_REBASE_PICK
);
523 SetSelectedRebaseAction(LOGACTIONS_REBASE_EDIT
);
525 case ID_REBASE_SQUASH
:
526 SetSelectedRebaseAction(LOGACTIONS_REBASE_SQUASH
);
529 SetSelectedRebaseAction(LOGACTIONS_REBASE_SKIP
);
531 case ID_COMBINE_COMMIT
:
535 CGitHash hashFirst
,hashLast
;
537 int headindex
=GetHeadIndex();
538 if(headindex
>=0) //incase show all branch, head is not the first commits.
540 head
.Format(L
"HEAD~%d", FirstSelect
- headindex
);
541 if (g_Git
.GetHash(hashFirst
, head
))
543 MessageBox(g_Git
.GetGitLastErr(L
"Could not get hash of first selected revision."), L
"TortoiseGit", MB_ICONERROR
);
547 head
.Format(L
"HEAD~%d", LastSelect
- headindex
);
548 if (g_Git
.GetHash(hashLast
, head
))
550 MessageBox(g_Git
.GetGitLastErr(L
"Could not get hash of last selected revision."), L
"TortoiseGit", MB_ICONERROR
);
555 GitRev
* pFirstEntry
= m_arShownList
.SafeGetAt(FirstSelect
);
556 GitRev
* pLastEntry
= m_arShownList
.SafeGetAt(LastSelect
);
557 if(pFirstEntry
->m_CommitHash
!= hashFirst
|| pLastEntry
->m_CommitHash
!= hashLast
)
559 CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), IDS_PROC_CANNOTCOMBINE
, IDS_APPNAME
, MB_OK
| MB_ICONEXCLAMATION
);
564 if (lastRevision
.GetParentFromHash(hashLast
))
566 MessageBox(lastRevision
.GetLastErr(), L
"TortoiseGit", MB_ICONERROR
);
570 if (g_Git
.GetHash(headhash
, L
"HEAD"))
572 MessageBox(g_Git
.GetGitLastErr(L
"Could not get HEAD hash."), L
"TortoiseGit", MB_ICONERROR
);
576 if(!g_Git
.CheckCleanWorkTree())
578 CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), IDS_PROC_NOCLEAN
, IDS_APPNAME
, MB_OK
| MB_ICONEXCLAMATION
);
583 //Use throw to abort this process (reset back to original HEAD)
586 sCmd
.Format(L
"git.exe reset --hard %s --", (LPCTSTR
)pFirstEntry
->m_CommitHash
.ToString());
587 if(g_Git
.Run(sCmd
, &out
, CP_UTF8
))
589 MessageBox(out
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
590 throw std::exception(CUnicodeUtils::GetUTF8(CString(MAKEINTRESOURCE(IDS_PROC_COMBINE_ERRORSTEP1
)) + L
"\r\n\r\n" + out
));
592 sCmd
.Format(L
"git.exe reset --soft %s --", (LPCTSTR
)hashLast
.ToString());
593 if(g_Git
.Run(sCmd
, &out
, CP_UTF8
))
595 MessageBox(out
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
596 throw std::exception(CUnicodeUtils::GetUTF8(CString(MAKEINTRESOURCE(IDS_PROC_COMBINE_ERRORSTEP2
)) + L
"\r\n\r\n"+out
));
600 for (int i
= FirstSelect
; i
<= LastSelect
; ++i
)
602 GitRev
* pRev
= m_arShownList
.SafeGetAt(i
);
603 dlg
.m_sLogMessage
+= pRev
->GetSubject() + L
'\n' + pRev
->GetBody();
604 dlg
.m_sLogMessage
+= L
'\n';
606 dlg
.m_bWholeProject
=true;
607 dlg
.m_bSelectFilesForCommit
= true;
608 dlg
.m_bForceCommitAmend
=true;
609 int squashDate
= (int)CRegDWORD(L
"Software\\TortoiseGit\\SquashDate", 0);
611 dlg
.SetTime(m_arShownList
.SafeGetAt(FirstSelect
)->GetAuthorDate());
612 else if (squashDate
== 2)
613 dlg
.SetTime(CTime::GetCurrentTime());
615 dlg
.SetTime(m_arShownList
.SafeGetAt(LastSelect
)->GetAuthorDate());
617 gpl
.AddPath(CTGitPath(g_Git
.m_CurrentDir
));
618 dlg
.m_pathList
= gpl
;
619 if (lastRevision
.ParentsCount() != 1)
621 MessageBox(L
"The following commit dialog can only show changes of oldest commit if it has exactly one parent. This is not the case right now.", L
"TortoiseGit", MB_OK
| MB_ICONINFORMATION
);
622 dlg
.m_bAmendDiffToLastCommit
= TRUE
;
625 dlg
.m_bAmendDiffToLastCommit
= FALSE
;
626 dlg
.m_bNoPostActions
=true;
627 dlg
.m_AmendStr
=dlg
.m_sLogMessage
;
629 if (dlg
.DoModal() == IDOK
)
631 if(pFirstEntry
->m_CommitHash
!=headhash
)
633 if(CherryPickFrom(pFirstEntry
->m_CommitHash
.ToString(),headhash
))
636 msg
.Format(L
"Error while cherry pick commits on top of combined commits. Aborting.\r\n\r\n");
637 throw std::exception(CUnicodeUtils::GetUTF8(msg
));
642 throw std::exception(CUnicodeUtils::GetUTF8(CString(MAKEINTRESOURCE(IDS_USERCANCELLED
))));
644 catch(std::exception
& e
)
646 CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), CUnicodeUtils::GetUnicode(CStringA(e
.what())), L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
647 sCmd
.Format(L
"git.exe reset --hard %s --", (LPCTSTR
)headhash
.ToString());
649 if(g_Git
.Run(sCmd
, &out
, CP_UTF8
))
650 MessageBox(CString(MAKEINTRESOURCE(IDS_PROC_COMBINE_ERRORRESETHEAD
)) + L
"\r\n\r\n" + out
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
658 if (m_bThreadRunning
)
660 CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), IDS_PROC_LOG_ONLYONCE
, IDS_APPNAME
, MB_ICONEXCLAMATION
);
664 dlg
.m_IsCherryPick
= TRUE
;
665 dlg
.m_Upstream
= this->m_CurrentBranch
;
666 POSITION pos2
= GetFirstSelectedItemPosition();
669 int indexNext2
= GetNextSelectedItem(pos2
);
670 dlg
.m_CommitList
.m_logEntries
.push_back(m_arShownList
.SafeGetAt(indexNext2
)->m_CommitHash
);
671 dlg
.m_CommitList
.m_LogCache
.m_HashMap
[m_arShownList
.SafeGetAt(indexNext2
)->m_CommitHash
] = *m_arShownList
.SafeGetAt(indexNext2
);
672 dlg
.m_CommitList
.m_logEntries
.GetGitRevAt(dlg
.m_CommitList
.m_logEntries
.size() - 1).GetRebaseAction() |= LOGACTIONS_REBASE_PICK
;
675 if(dlg
.DoModal() == IDOK
)
681 case ID_REBASE_TO_VERSION
:
683 if (m_bThreadRunning
)
685 CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), IDS_PROC_LOG_ONLYONCE
, IDS_APPNAME
, MB_ICONEXCLAMATION
);
689 auto refList
= m_HashMap
[pSelLogEntry
->m_CommitHash
];
690 dlg
.m_Upstream
= refList
.empty() ? pSelLogEntry
->m_CommitHash
.ToString() : refList
.front();
691 for (const auto& ref
: refList
)
692 if (CGit::GetShortName(ref
, dlg
.m_Upstream
, L
"refs/heads/"))
695 if(dlg
.DoModal() == IDOK
)
704 if (CAppUtils::StashSave())
709 if (CAppUtils::StashPop())
714 CAppUtils::RunTortoiseGitProc(L
"/command:reflog /ref:refs/stash");
717 case ID_REFLOG_STASH_APPLY
:
718 CAppUtils::StashApply(pSelLogEntry
->m_Ref
);
724 if (GetSelectedCount() > 1)
725 str
.Format(IDS_PROC_DELETENREFS
, GetSelectedCount());
727 str
.Format(IDS_PROC_DELETEREF
, (LPCTSTR
)pSelLogEntry
->m_Ref
);
729 if (CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), str
, L
"TortoiseGit", 1, IDI_QUESTION
, CString(MAKEINTRESOURCE(IDS_DELETEBUTTON
)), CString(MAKEINTRESOURCE(IDS_ABORTBUTTON
))) == 2)
732 std::vector
<CString
> refsToDelete
;
733 POSITION pos2
= GetFirstSelectedItemPosition();
736 CString ref
= m_arShownList
.SafeGetAt(GetNextSelectedItem(pos2
))->m_Ref
;
737 if (CStringUtils::StartsWith(ref
, L
"refs/"))
739 int refpos
= ref
.ReverseFind(L
'{');
740 if (refpos
> 0 && ref
.Mid(refpos
- 1, 2) != L
"@{")
741 ref
= ref
.Left(refpos
) + L
'@'+ ref
.Mid(refpos
);
742 refsToDelete
.push_back(ref
);
745 for (auto revIt
= refsToDelete
.crbegin(); revIt
!= refsToDelete
.crend(); ++revIt
)
747 CString ref
= *revIt
;
749 if (CStringUtils::StartsWith(ref
, L
"stash"))
750 sCmd
.Format(L
"git.exe stash drop %s", (LPCTSTR
)ref
);
752 sCmd
.Format(L
"git.exe reflog delete %s", (LPCTSTR
)ref
);
754 if (g_Git
.Run(sCmd
, &out
, CP_UTF8
))
755 MessageBox(out
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
757 ::PostMessage(this->GetParent()->m_hWnd
,MSG_REFLOG_CHANGED
,0,0);
763 CString sCmd
= L
"/command:log";
764 sCmd
+= L
" /path:\"" + g_Git
.m_CurrentDir
+ L
"\" ";
765 GitRev
* r1
= m_arShownList
.SafeGetAt(FirstSelect
);
766 sCmd
+= L
" /endrev:" + r1
->m_CommitHash
.ToString();
767 CAppUtils::RunTortoiseGitProc(sCmd
);
770 case ID_CREATE_PATCH
:
772 int select
=this->GetSelectedCount();
773 CString sCmd
= L
"/command:formatpatch";
774 sCmd
+= L
" /path:\"" + g_Git
.m_CurrentDir
+ L
"\" ";
776 GitRev
* r1
= m_arShownList
.SafeGetAt(FirstSelect
);
777 GitRev
* r2
= nullptr;
780 sCmd
+= L
" /startrev:" + r1
->m_CommitHash
.ToString();
784 r2
= m_arShownList
.SafeGetAt(LastSelect
);
785 if( this->m_IsOldFirst
)
787 sCmd
+= L
" /startrev:" + r1
->m_CommitHash
.ToString() + L
"~1";
788 sCmd
+= L
" /endrev:" + r2
->m_CommitHash
.ToString();
793 sCmd
+= L
" /startrev:" + r2
->m_CommitHash
.ToString() + L
"~1";
794 sCmd
+= L
" /endrev:" + r1
->m_CommitHash
.ToString();
799 CAppUtils::RunTortoiseGitProc(sCmd
);
804 GitRev
* first
= m_arShownList
.SafeGetAt(FirstSelect
);
805 GitRev
* last
= m_arShownList
.SafeGetAt(LastSelect
);
806 ASSERT(first
&& last
);
808 CString firstBad
= first
->m_CommitHash
.ToString();
809 if (!m_HashMap
[first
->m_CommitHash
].empty())
810 firstBad
= m_HashMap
[first
->m_CommitHash
].at(0);
811 CString lastGood
= last
->m_CommitHash
.ToString();
812 if (!m_HashMap
[last
->m_CommitHash
].empty())
813 lastGood
= m_HashMap
[last
->m_CommitHash
].at(0);
815 if (CAppUtils::BisectStart(lastGood
, firstBad
))
821 GitRev
* first
= m_arShownList
.SafeGetAt(FirstSelect
);
822 if (CAppUtils::BisectOperation(L
"good", !first
->m_CommitHash
.IsEmpty() ? first
->m_CommitHash
.ToString() : L
""))
828 GitRev
* first
= m_arShownList
.SafeGetAt(FirstSelect
);
829 if (CAppUtils::BisectOperation(L
"bad", !first
->m_CommitHash
.IsEmpty() ? first
->m_CommitHash
.ToString() : L
""))
836 POSITION pos2
= GetFirstSelectedItemPosition();
839 int indexNext2
= GetNextSelectedItem(pos2
);
840 auto rev
= m_arShownList
.SafeGetAt(indexNext2
);
841 if (!rev
->m_CommitHash
.IsEmpty())
842 refs
.AppendFormat(L
" %s", (LPCTSTR
)rev
->m_CommitHash
.ToString());
844 if (CAppUtils::BisectOperation(L
"skip", refs
))
850 if (CAppUtils::BisectOperation(L
"reset"))
857 sCmd
.Format(L
"/command:repobrowser /path:\"%s\" /rev:%s", (LPCTSTR
)g_Git
.m_CurrentDir
, (LPCTSTR
)pSelLogEntry
->m_CommitHash
.ToString());
858 CAppUtils::RunTortoiseGitProc(sCmd
);
863 CString guessAssociatedBranch
= pSelLogEntry
->m_CommitHash
;
864 const CString
* branch
= popmenu
? (const CString
*)((CIconMenu
*)popmenu
)->GetMenuItemData(cmd
) : nullptr;
865 if (branch
&& !CStringUtils::StartsWith(*branch
, L
"refs/remotes/"))
866 guessAssociatedBranch
= *branch
;
867 else if (!GetFirstEntryStartingWith(m_HashMap
[pSelLogEntry
->m_CommitHash
], L
"refs/heads/", guessAssociatedBranch
))
868 GetFirstEntryStartingWith(m_HashMap
[pSelLogEntry
->m_CommitHash
], L
"refs/tags/", guessAssociatedBranch
);
870 guessAssociatedBranch
.Replace(L
"^{}", L
"");
872 if (CAppUtils::Push(guessAssociatedBranch
))
878 if (CAppUtils::Pull())
884 if (CAppUtils::Fetch())
890 if (CAppUtils::SVNDCommit())
897 sCmd
.Format(L
"/command:cleanup /path:\"%s\"", (LPCTSTR
)g_Git
.m_CurrentDir
);
898 CAppUtils::RunTortoiseGitProc(sCmd
);
901 case ID_SUBMODULE_UPDATE
:
904 sCmd
.Format(L
"/command:subupdate /bkpath:\"%s\"", (LPCTSTR
)g_Git
.m_CurrentDir
);
905 CAppUtils::RunTortoiseGitProc(sCmd
);
908 case ID_SHOWBRANCHES
:
910 CCommitIsOnRefsDlg
* dlg
= new CCommitIsOnRefsDlg(this);
911 dlg
->m_Rev
= (LPCTSTR
)pSelLogEntry
->m_CommitHash
.ToString();
913 // pointer won't leak as it is destroyed within PostNcDestroy()
918 const CString
* branch
= popmenu
? (const CString
*)((CIconMenu
*)popmenu
)->GetMenuItemData(cmd
) : nullptr;
921 CMessageBox::Show(GetSafeOwner()->GetSafeHwnd(), IDS_ERROR_NOREF
, IDS_APPNAME
, MB_OK
| MB_ICONERROR
);
925 if (branch
== (CString
*)MAKEINTRESOURCE(IDS_ALL
))
927 CString currentBranch
= L
"refs/heads/" + m_CurrentBranch
;
928 bool nothingDeleted
= true;
929 for (const auto& ref
: m_HashMap
[pSelLogEntry
->m_CommitHash
])
931 if (ref
== currentBranch
)
933 if (!CAppUtils::DeleteRef(this, ref
))
935 nothingDeleted
= false;
940 else if (!CAppUtils::DeleteRef(this, *branch
))
942 this->ReloadHashMap();
944 m_pFindDialog
->RefreshList();
946 this->GetItemRect(FirstSelect
,&rect
,LVIR_BOUNDS
);
947 this->InvalidateRect(rect
);
953 m_nSearchIndex
= GetSelectionMark();
954 if (m_nSearchIndex
< 0)
960 m_pFindDialog
= new CFindDlg();
961 m_pFindDialog
->Create(this);
967 CString str
= pSelLogEntry
->m_CommitHash
.ToString();
968 const CString
* branch
= popmenu
? (const CString
*)((CIconMenu
*)popmenu
)->GetMenuItemData(cmd
& 0xFFFF) : nullptr;
971 else if (!m_HashMap
[pSelLogEntry
->m_CommitHash
].empty())
972 str
= m_HashMap
[pSelLogEntry
->m_CommitHash
].at(0);
973 // we need an URL to complete this command, so error out if we can't get an URL
974 if(CAppUtils::Merge(&str
))
983 if (GetSelectedCount() == 1)
986 if ((size_t)parent
> pSelLogEntry
->m_ParentHash
.size())
989 str
.Format(IDS_PROC_NOPARENT
, parent
);
990 MessageBox(str
, L
"TortoiseGit", MB_OK
| MB_ICONERROR
);
995 if (!this->RevertSelectedCommits(parent
))
997 if (CMessageBox::Show(m_hWnd
, IDS_REVREVERTED
, IDS_APPNAME
, 1, IDI_QUESTION
, IDS_OKBUTTON
, IDS_COMMITBUTTON
) == 2)
999 CTGitPathList pathlist
;
1000 CTGitPathList selectedlist
;
1001 pathlist
.AddPath(this->m_Path
);
1002 bool bSelectFilesForCommit
= !!DWORD(CRegStdDWORD(L
"Software\\TortoiseGit\\SelectFilesForCommit", TRUE
));
1004 CAppUtils::Commit(CString(), false, str
, pathlist
, selectedlist
, bSelectFilesForCommit
);
1012 CAppUtils::EditNote(pSelLogEntry
, &m_ProjectProperties
);
1013 this->SetItemState(FirstSelect
, 0, LVIS_SELECTED
);
1014 this->SetItemState(FirstSelect
, LVIS_SELECTED
, LVIS_SELECTED
);
1018 //CMessageBox::Show(nullptr, L"Have not implemented", L"TortoiseGit", MB_OK);
1022 case ID_BLAMECOMPARE
:
1024 //user clicked on the menu item "compare with working copy"
1025 //now first get the revision which is selected
1028 GitDiff
diff(this, this->m_hWnd
, true);
1029 diff
.SetHEADPeg(m_LogRevision
);
1030 diff
.ShowCompare(m_path
, GitRev::REV_BASE
, m_path
, revSelected
, GitRev(), false, true);
1033 CAppUtils::StartShowCompare(m_hWnd
, m_path
, GitRev::REV_BASE
, m_path
, revSelected
, GitRev(), m_LogRevision
, false, false, true);
1036 case ID_BLAMEWITHPREVIOUS
:
1038 //user clicked on the menu item "Compare and Blame with previous revision"
1041 GitDiff
diff(this, this->m_hWnd
, true);
1042 diff
.SetHEADPeg(m_LogRevision
);
1043 diff
.ShowCompare(CTGitPath(pathURL
), revPrevious
, CTGitPath(pathURL
), revSelected
, GitRev(), false, true);
1046 CAppUtils::StartShowCompare(m_hWnd
, CTGitPath(pathURL
), revPrevious
, CTGitPath(pathURL
), revSelected
, GitRev(), m_LogRevision
, false, false, true);
1054 CProgressDlg progDlg
;
1055 progDlg
.SetTitle(IDS_APPNAME
);
1056 progDlg
.SetAnimation(IDR_DOWNLOAD
);
1058 sInfoLine
.Format(IDS_PROGRESSGETFILEREVISION
, m_path
.GetWinPath(), (LPCTSTR
)revSelected
.ToString());
1059 progDlg
.SetLine(1, sInfoLine
, true);
1060 SetAndClearProgressInfo(&progDlg
);
1061 progDlg
.ShowModeless(m_hWnd
);
1062 CTGitPath tempfile
= CTempFiles::Instance().GetTempFilePath(false, m_path
, revSelected
);
1063 bool bSuccess
= true;
1064 if (!Cat(m_path
, GitRev(GitRev::REV_HEAD
), revSelected
, tempfile
))
1067 // try again, but with the selected revision as the peg revision
1068 if (!Cat(m_path
, revSelected
, revSelected
, tempfile
))
1071 SetAndClearProgressInfo(nullptr);
1072 CMessageBox::Show(GetSafeHwnd(), GetLastErrorMessage(), L
"TortoiseGit", MB_ICONERROR
);
1081 SetAndClearProgressInfo(nullptr);
1082 SetFileAttributes(tempfile
.GetWinPath(), FILE_ATTRIBUTE_READONLY
);
1084 CAppUtils::ShellOpen(tempfile
.GetWinPath(), GetSafeHwnd());
1086 CAppUtils::ShowOpenWithDialog(tempfile
.GetWinPathString(), GetSafeHwnd());
1093 dlg
.EndRev
= revSelected
;
1094 if (dlg
.DoModal() == IDOK
)
1099 tempfile
= blame
.BlameToTempFile(m_path
, dlg
.StartRev
, dlg
.EndRev
, dlg
.EndRev
, logfile
, L
"", dlg
.m_bIncludeMerge
, TRUE
, TRUE
);
1100 if (!tempfile
.IsEmpty())
1102 if (dlg
.m_bTextView
)
1104 //open the default text editor for the result file
1105 CAppUtils::StartTextViewer(tempfile
);
1109 CString sParams
= L
"/path:\"" + m_path
.GetGitPathString() + L
"\" ";
1110 if(!CAppUtils::LaunchTortoiseBlame(tempfile
, logfile
, CPathUtils::GetFileNameFromPath(m_path
.GetFileOrDirectoryName()),sParams
))
1118 CMessageBox::Show(GetSafeHwnd(), blame
.GetLastErrorMessage(), L
"TortoiseGit", MB_ICONERROR
);
1126 sCmd
.Format(L
"%s /command:export /path:\"%s\" /revision:%ld",
1127 (LPCTSTR
)(CPathUtils::GetAppDirectory() + L
"TortoiseGitProc.exe"),
1128 (LPCTSTR
)pathURL
, (LONG
)revSelected
);
1129 CAppUtils::LaunchApplication(sCmd
, nullptr, false);
1134 CString url
= m_ProjectProperties
.sWebViewerRev
;
1135 url
= GetAbsoluteUrlFromRelativeUrl(url
);
1136 url
.Replace(L
"%REVISION%", revSelected
.ToString());
1138 ShellExecute(GetSafeHwnd(), L
"open", url
, nullptr, nullptr, SW_SHOWDEFAULT
);
1141 case ID_VIEWPATHREV
:
1143 CString relurl
= pathURL
;
1144 CString sRoot
= GetRepositoryRoot(CTGitPath(relurl
));
1145 relurl
= relurl
.Mid(sRoot
.GetLength());
1146 CString url
= m_ProjectProperties
.sWebViewerPathRev
;
1147 url
= GetAbsoluteUrlFromRelativeUrl(url
);
1148 url
.Replace(L
"%REVISION%", revSelected
.ToString());
1149 url
.Replace(L
"%PATH%", relurl
);
1151 ShellExecute(GetSafeHwnd(), L
"open", url
, nullptr, nullptr, SW_SHOWDEFAULT
);
1158 theApp
.DoWaitCursor(-1);
1161 void CGitLogList::SetSelectedRebaseAction(int action
)
1163 POSITION pos
= GetFirstSelectedItemPosition();
1168 index
= GetNextSelectedItem(pos
);
1169 if (m_arShownList
.SafeGetAt(index
)->GetRebaseAction() & (LOGACTIONS_REBASE_CURRENT
| LOGACTIONS_REBASE_DONE
) || (index
== GetItemCount() - 1 && action
== LOGACTIONS_REBASE_SQUASH
))
1171 if (!m_bIsCherryPick
&& m_arShownList
.SafeGetAt(index
)->ParentsCount() > 1 && action
== LOGACTIONS_REBASE_SQUASH
)
1173 m_arShownList
.SafeGetAt(index
)->GetRebaseAction() = action
;
1175 this->GetItemRect(index
,&rect
,LVIR_BOUNDS
);
1176 this->InvalidateRect(rect
);
1179 GetParent()->PostMessage(CGitLogListBase::m_RebaseActionMessage
);
1182 void CGitLogList::SetUnselectedRebaseAction(int action
)
1184 POSITION pos
= GetFirstSelectedItemPosition();
1185 int index
= pos
? GetNextSelectedItem(pos
) : -1;
1186 for (int i
= 0; i
< GetItemCount(); i
++)
1190 index
= pos
? GetNextSelectedItem(pos
) : -1;
1194 if (m_arShownList
.SafeGetAt(i
)->GetRebaseAction() & (LOGACTIONS_REBASE_CURRENT
| LOGACTIONS_REBASE_DONE
) || (i
== GetItemCount() - 1 && action
== LOGACTIONS_REBASE_SQUASH
) || (!m_bIsCherryPick
&& action
== LOGACTIONS_REBASE_SQUASH
&& m_arShownList
.SafeGetAt(i
)->ParentsCount() != 1))
1196 m_arShownList
.SafeGetAt(i
)->GetRebaseAction() = action
;
1198 this->GetItemRect(i
, &rect
, LVIR_BOUNDS
);
1199 this->InvalidateRect(rect
);
1202 GetParent()->PostMessage(CGitLogListBase::m_RebaseActionMessage
);
1205 void CGitLogList::ShiftSelectedRebaseAction()
1207 POSITION pos
= GetFirstSelectedItemPosition();
1211 index
= GetNextSelectedItem(pos
);
1212 int* action
= &(m_arShownList
.SafeGetAt(index
))->GetRebaseAction();
1215 case LOGACTIONS_REBASE_PICK
:
1216 *action
= LOGACTIONS_REBASE_SKIP
;
1218 case LOGACTIONS_REBASE_SKIP
:
1219 *action
= LOGACTIONS_REBASE_EDIT
;
1221 case LOGACTIONS_REBASE_EDIT
:
1222 *action
= LOGACTIONS_REBASE_SQUASH
;
1223 if (index
== GetItemCount() - 1 && (m_bIsCherryPick
|| m_arShownList
.SafeGetAt(index
)->m_ParentHash
.size() == 1))
1224 *action
= LOGACTIONS_REBASE_PICK
;
1226 case LOGACTIONS_REBASE_SQUASH
:
1227 *action
= LOGACTIONS_REBASE_PICK
;
1231 this->GetItemRect(index
, &rect
, LVIR_BOUNDS
);
1232 this->InvalidateRect(rect
);