Sync with official PuTTY 0.81 source code
[TortoiseGit.git] / src / TortoiseProc / GitLogListAction.cpp
blob1691bbf1ac1e5cd65cf4e9468753af6933527293
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2023 - 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
22 #include "stdafx.h"
23 #include "TortoiseProc.h"
24 #include "GitLogList.h"
25 #include "GitRev.h"
26 #include "IconMenu.h"
27 #include "cursor.h"
28 #include "GitProgressDlg.h"
29 #include "ProgressDlg.h"
30 #include "SysProgressDlg.h"
31 #include "LogDlg.h"
32 #include "MessageBox.h"
33 #include "registry.h"
34 #include "AppUtils.h"
35 #include "StringUtils.h"
36 #include "UnicodeUtils.h"
37 #include "TempFile.h"
38 #include "CommitDlg.h"
39 #include "RebaseDlg.h"
40 #include "CommitIsOnRefsDlg.h"
41 #include "GitDiff.h"
42 #include "../TGitCache/CacheInterface.h"
44 IMPLEMENT_DYNAMIC(CGitLogList, CHintCtrl<CListCtrl>)
46 static bool GetFirstEntryStartingWith(const 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())
50 return false;
51 result = *it;
52 return true;
55 int CGitLogList::RevertSelectedCommits(int parent)
57 CSysProgressDlg progress;
58 int ret = -1;
60 if (CMessageBox::Show(GetParentHWND(), IDS_REVERTCOMMITS, IDS_APPNAME, MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2) == IDNO)
61 return -1;
63 #if 0
64 if(!g_Git.CheckCleanWorkTree())
65 CMessageBox::Show(nullptr, IDS_PROC_NOCLEAN, IDS_APPNAME, MB_OK);
66 #endif
68 if (this->GetSelectedCount() > 1)
70 progress.SetTitle(CString(MAKEINTRESOURCE(IDS_PROGS_TITLE_REVERTCOMMIT)));
71 progress.SetTime(true);
72 progress.ShowModeless(this);
75 CBlockCacheForPath cacheBlock(g_Git.m_CurrentDir);
77 POSITION pos = GetFirstSelectedItemPosition();
78 int i=0;
79 CString mergeMsg;
80 CString dotGitPath;
81 GitAdminDir::GetWorktreeAdminDirPath(g_Git.m_CurrentDir, dotGitPath);
82 while(pos)
84 const int index = GetNextSelectedItem(pos);
85 GitRev* r1 = m_arShownList.SafeGetAt(index);
87 if (progress.IsVisible())
89 progress.FormatNonPathLine(1, IDS_PROC_REVERTCOMMIT, static_cast<LPCWSTR>(r1->m_CommitHash.ToString()));
90 progress.FormatNonPathLine(2, L"%s", static_cast<LPCWSTR>(r1->GetSubject()));
91 progress.SetProgress(i, this->GetSelectedCount());
93 ++i;
95 if(r1->m_CommitHash.IsEmpty())
96 continue;
98 if (g_Git.GitRevert(parent, r1->m_CommitHash))
100 CString str;
101 str.LoadString(IDS_SVNACTION_FAILEDREVERT);
102 str = g_Git.GetGitLastErr(str, CGit::GIT_CMD_REVERT);
103 if( GetSelectedCount() == 1)
104 CMessageBox::Show(GetParentHWND(), str, L"TortoiseGit", MB_OK | MB_ICONERROR);
105 else if (CMessageBox::Show(GetParentHWND(), str, L"TortoiseGit", 2, IDI_ERROR, CString(MAKEINTRESOURCE(IDS_SKIPBUTTON)), CString(MAKEINTRESOURCE(IDS_ABORTBUTTON))) == 2)
106 return ret;
108 else
110 if (!mergeMsg.IsEmpty())
111 mergeMsg += "\r\n";
112 CGit::LoadTextFile(dotGitPath + L"MERGE_MSG", mergeMsg);
113 ret = 0;
116 if (progress.HasUserCancelled())
117 break;
119 if (!mergeMsg.IsEmpty())
120 CStringUtils::WriteStringToTextFile(dotGitPath + L"MERGE_MSG", mergeMsg);
121 return ret;
123 int CGitLogList::CherryPickFrom(CString from, CString to)
125 CLogDataVector logs(&m_LogCache);
126 CString range;
127 range.Format(L"%s..%s", static_cast<LPCWSTR>(from), static_cast<LPCWSTR>(to));
128 if (logs.ParserFromLog(nullptr, 0, 0, &range))
129 return -1;
131 if (logs.empty())
132 return 0;
134 CSysProgressDlg progress;
135 progress.SetTitle(CString(MAKEINTRESOURCE(IDS_PROGS_TITLE_CHERRYPICK)));
136 progress.SetTime(true);
137 progress.ShowModeless(this);
139 CBlockCacheForPath cacheBlock(g_Git.m_CurrentDir);
141 for (int i = static_cast<int>(logs.size()) - 1; i >= 0; --i)
143 if (progress.IsVisible())
145 progress.FormatNonPathLine(1, IDS_PROC_PICK, static_cast<LPCWSTR>(logs.GetGitRevAt(i).m_CommitHash.ToString()));
146 progress.FormatNonPathLine(2, L"%s", static_cast<LPCWSTR>(logs.GetGitRevAt(i).GetSubject()));
147 progress.SetProgress64(logs.size() - i, logs.size());
149 if (progress.HasUserCancelled())
150 throw std::exception(CUnicodeUtils::GetUTF8(CString(MAKEINTRESOURCE(IDS_USERCANCELLED))));
151 CString cmd,out;
152 cmd.Format(L"git.exe cherry-pick %s", static_cast<LPCWSTR>(logs.GetGitRevAt(i).m_CommitHash.ToString()));
153 if(g_Git.Run(cmd,&out,CP_UTF8))
154 throw std::exception(CUnicodeUtils::GetUTF8(CString(MAKEINTRESOURCE(IDS_PROC_CHERRYPICKFAILED)) + L":\r\n\r\n" + out));
157 return 0;
160 void CGitLogList::ContextMenuAction(int cmd, int FirstSelect, int LastSelect, CMenu* popmenu, const MAP_HASH_NAME& hashMap)
162 POSITION pos = GetFirstSelectedItemPosition();
163 const int indexNext = GetNextSelectedItem(pos);
164 if (indexNext < 0)
165 return;
167 GitRevLoglist* pSelLogEntry = m_arShownList.SafeGetAt(indexNext);
169 bool bShiftPressed = !!(GetAsyncKeyState(VK_SHIFT) & 0x8000);
171 theApp.DoWaitCursor(1);
172 switch (cmd&0xFFFF)
174 case ID_COMMIT:
176 CTGitPathList pathlist;
177 pathlist.AddPath(this->m_Path);
178 bool bSelectFilesForCommit = !!DWORD(CRegStdDWORD(L"Software\\TortoiseGit\\SelectFilesForCommit", TRUE));
179 CString str;
180 CAppUtils::Commit(GetParentHWND(), CString(), false, str,
181 pathlist, bSelectFilesForCommit);
182 //this->Refresh();
183 this->GetParent()->PostMessage(WM_COMMAND,ID_LOGDLG_REFRESH,0);
185 break;
186 case ID_MERGE_ABORT:
188 if (CAppUtils::MergeAbort(GetParentHWND()))
189 this->GetParent()->PostMessage(WM_COMMAND,ID_LOGDLG_REFRESH, 0);
191 break;
192 case ID_GNUDIFF1: // compare with WC, unified
194 GitRev* r1 = m_arShownList.SafeGetAt(FirstSelect);
195 if(!r1->m_CommitHash.IsEmpty())
197 CString hash2;
198 bool bMerge = false, bCombine = false;
199 CString merge;
200 cmd >>= 16;
201 if( (cmd&0xFFFF) == 0xFFFF)
202 bMerge = true;
203 else if((cmd&0xFFFF) == 0xFFFE)
204 bCombine = true;
205 else if ((cmd & 0xFFFF) == 0xFFFD)
207 CString tempfile = GetTempFile();
208 if (tempfile.IsEmpty())
210 MessageBox(L"Could not create temp file.", L"TortoiseGit", MB_OK | MB_ICONERROR);
211 break;
213 CString gitcmd = L"git.exe diff-tree --cc " + r1->m_CommitHash.ToString();
214 CString lastErr;
215 if (g_Git.RunLogFile(gitcmd, tempfile, &lastErr))
217 MessageBox(lastErr, L"TortoiseGit", MB_ICONERROR);
218 break;
223 CStdioFile file(tempfile, CFile::typeText | CFile::modeRead | CFile::shareDenyWrite);
224 CString strLine;
225 bool isHash = file.ReadString(strLine) && r1->m_CommitHash.ToString() == strLine;
226 bool more = isHash && file.ReadString(strLine) && !strLine.IsEmpty();
227 if (!more)
229 CMessageBox::Show(GetParentHWND(), IDS_NOCHANGEAFTERMERGE, IDS_APPNAME, MB_OK);
230 break;
233 catch (CFileException* e)
235 e->Delete();
238 CAppUtils::StartUnifiedDiffViewer(tempfile, r1->m_CommitHash.ToString());
239 break;
241 else
243 if (cmd == 0)
244 cmd = 1;
245 if (static_cast<size_t>(cmd) > r1->m_ParentHash.size())
247 CString str;
248 str.Format(IDS_PROC_NOPARENT, cmd);
249 MessageBox(str, L"TortoiseGit", MB_OK | MB_ICONERROR);
250 return;
252 else
254 if(cmd>0)
255 hash2 = r1->m_ParentHash[cmd-1].ToString();
258 if (m_Path.IsDirectory() || !(m_ShowMask & CGit::LOG_INFO_FOLLOW))
259 CAppUtils::StartShowUnifiedDiff(nullptr, m_Path, hash2, m_Path, r1->m_CommitHash.ToString(), bShiftPressed, false, false, bMerge, bCombine);
260 else
262 CString path = m_Path.GetGitPathString();
263 // start with 1 (0 = working copy changes)
264 for (int i = m_bShowWC ? 1 : 0; i < FirstSelect; ++i)
266 GitRevLoglist* first = m_arShownList.SafeGetAt(i);
267 auto filesWrapper = first->GetFiles(nullptr);
268 auto& list = filesWrapper.m_files;
269 const CTGitPath* file = list.LookForGitPath(path);
270 if (file && !file->GetGitOldPathString().IsEmpty())
271 path = file->GetGitOldPathString();
273 CAppUtils::StartShowUnifiedDiff(nullptr, CTGitPath(path), hash2, CTGitPath(path), r1->m_CommitHash.ToString(), bShiftPressed, false, false, bMerge, bCombine);
276 else
277 CAppUtils::StartShowUnifiedDiff(nullptr, m_Path, L"HEAD", m_Path, GitRev::GetWorkingCopy(), bShiftPressed);
279 break;
281 case ID_GNUDIFF2: // compare two revisions, unified
283 GitRev* r1 = m_arShownList.SafeGetAt(FirstSelect);
284 GitRev* r2 = m_arShownList.SafeGetAt(LastSelect);
285 if (m_Path.IsDirectory() || !(m_ShowMask & CGit::LOG_INFO_FOLLOW))
286 CAppUtils::StartShowUnifiedDiff(nullptr, m_Path, r2->m_CommitHash.ToString(), m_Path, r1->m_CommitHash.ToString(), bShiftPressed);
287 else
289 CString path = m_Path.GetGitPathString();
290 // start with 1 (0 = working copy changes)
291 for (int i = m_bShowWC ? 1 : 0; i < FirstSelect; ++i)
293 GitRevLoglist* first = m_arShownList.SafeGetAt(i);
294 auto filesWrapper = first->GetFiles(nullptr);
295 auto& list = filesWrapper.m_files;
296 const CTGitPath* file = list.LookForGitPath(path);
297 if (file && !file->GetGitOldPathString().IsEmpty())
298 path = file->GetGitOldPathString();
300 CAppUtils::StartShowUnifiedDiff(nullptr, CTGitPath(path), r2->m_CommitHash.ToString(), CTGitPath(path), r1->m_CommitHash.ToString(), bShiftPressed);
303 break;
305 case ID_COMPARETWO: // compare two revisions
307 GitRev* r1 = m_arShownList.SafeGetAt(FirstSelect);
308 GitRev* r2 = m_arShownList.SafeGetAt(LastSelect);
309 if (m_Path.IsDirectory() || !(m_ShowMask & CGit::LOG_INFO_FOLLOW))
310 CGitDiff::DiffCommit(GetParentHWND(), m_Path, r1, r2, bShiftPressed);
311 else
313 CString path1 = m_Path.GetGitPathString();
314 // start with 1 (0 = working copy changes)
315 for (int i = m_bShowWC ? 1 : 0; i < FirstSelect; ++i)
317 GitRevLoglist* first = m_arShownList.SafeGetAt(i);
318 auto filesWrapper = first->GetFiles(nullptr);
319 auto& list = filesWrapper.m_files;
320 const CTGitPath* file = list.LookForGitPath(path1);
321 if (file && !file->GetGitOldPathString().IsEmpty())
322 path1 = file->GetGitOldPathString();
324 CString path2 = path1;
325 for (int i = FirstSelect; i < LastSelect; ++i)
327 GitRevLoglist* first = m_arShownList.SafeGetAt(i);
328 auto filesWrapper = first->GetFiles(nullptr);
329 auto& list = filesWrapper.m_files;
330 const CTGitPath* file = list.LookForGitPath(path2);
331 if (file && !file->GetGitOldPathString().IsEmpty())
332 path2 = file->GetGitOldPathString();
334 CGitDiff::DiffCommit(GetParentHWND(), CTGitPath(path1), CTGitPath(path2), r1, r2, bShiftPressed);
338 break;
340 case ID_COMPARE: // compare revision with WC
342 GitRevLoglist* r1 = &m_wcRev;
343 GitRevLoglist* r2 = pSelLogEntry;
345 if (m_Path.IsDirectory() || !(m_ShowMask & CGit::LOG_INFO_FOLLOW))
346 CGitDiff::DiffCommit(GetParentHWND(), m_Path, r1, r2, bShiftPressed);
347 else
349 CString path1 = m_Path.GetGitPathString();
350 // start with 1 (0 = working copy changes)
351 for (int i = m_bShowWC ? 1 : 0; i < FirstSelect; ++i)
353 GitRevLoglist* first = m_arShownList.SafeGetAt(i);
354 auto filesWrapper = first->GetFiles(nullptr);
355 auto& list = filesWrapper.m_files;
356 const CTGitPath* file = list.LookForGitPath(path1);
357 if (file && !file->GetGitOldPathString().IsEmpty())
358 path1 = file->GetGitOldPathString();
360 CGitDiff::DiffCommit(GetParentHWND(), m_Path, CTGitPath(path1), r1, r2, bShiftPressed);
363 //user clicked on the menu item "compare with working copy"
364 //if (PromptShown())
366 // GitDiff diff(this, m_hWnd, true);
367 // diff.SetAlternativeTool(!!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
368 // diff.SetHEADPeg(m_LogRevision);
369 // diff.ShowCompare(m_path, GitRev::REV_WC, m_path, revSelected);
371 //else
372 // CAppUtils::StartShowCompare(m_hWnd, m_path, GitRev::REV_WC, m_path, revSelected, GitRev(), m_LogRevision, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
374 break;
376 case ID_COMPAREWITHPREVIOUS:
378 if (pSelLogEntry->m_ParentHash.empty())
380 if (pSelLogEntry->GetParentFromHash(pSelLogEntry->m_CommitHash))
381 MessageBox(pSelLogEntry->GetLastErr(), L"TortoiseGit", MB_ICONERROR);
384 if (!pSelLogEntry->m_ParentHash.empty())
385 //if(m_logEntries.m_HashMap[pSelLogEntry->m_ParentHash[0]]>=0)
387 cmd>>=16;
388 cmd&=0xFFFF;
390 if(cmd == 0)
391 cmd=1;
393 if (m_Path.IsDirectory() || !(m_ShowMask & CGit::LOG_INFO_FOLLOW))
394 CGitDiff::DiffCommit(GetParentHWND(), m_Path, pSelLogEntry->m_CommitHash.ToString(), pSelLogEntry->m_ParentHash[cmd - 1].ToString(), bShiftPressed);
395 else
397 CString path1 = m_Path.GetGitPathString();
398 // start with 1 (0 = working copy changes)
399 for (int i = m_bShowWC ? 1 : 0; i < indexNext; ++i)
401 GitRevLoglist* first = m_arShownList.SafeGetAt(i);
402 auto filesWrapper = first->GetFiles(nullptr);
403 auto& list = filesWrapper.m_files;
404 const CTGitPath* file = list.LookForGitPath(path1);
405 if (file && !file->GetGitOldPathString().IsEmpty())
406 path1 = file->GetGitOldPathString();
408 CString path2 = path1;
409 GitRevLoglist* first = m_arShownList.SafeGetAt(indexNext);
410 auto filesWrapper = first->GetFiles(nullptr);
411 auto& list = filesWrapper.m_files;
412 const CTGitPath* file = list.LookForGitPath(path2);
413 if (file && !file->GetGitOldPathString().IsEmpty())
414 path2 = file->GetGitOldPathString();
416 CGitDiff::DiffCommit(GetParentHWND(), CTGitPath(path1), CTGitPath(path2), pSelLogEntry->m_CommitHash.ToString(), pSelLogEntry->m_ParentHash[cmd - 1].ToString(), bShiftPressed);
419 else
421 CMessageBox::Show(GetParentHWND(), IDS_PROC_NOPREVIOUSVERSION, IDS_APPNAME, MB_OK);
423 //if (PromptShown())
425 // GitDiff diff(this, m_hWnd, true);
426 // diff.SetAlternativeTool(!!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
427 // diff.SetHEADPeg(m_LogRevision);
428 // diff.ShowCompare(CTGitPath(pathURL), revPrevious, CTGitPath(pathURL), revSelected);
430 //else
431 // CAppUtils::StartShowCompare(m_hWnd, CTGitPath(pathURL), revPrevious, CTGitPath(pathURL), revSelected, GitRev(), m_LogRevision, !!(GetAsyncKeyState(VK_SHIFT) & 0x8000));
433 break;
434 case ID_COMPARETWOCOMMITCHANGES:
436 auto pFirstEntry = m_arShownList.SafeGetAt(FirstSelect);
437 auto pLastEntry = m_arShownList.SafeGetAt(LastSelect);
438 CString patch1 = GetTempFile();
439 CString patch2 = GetTempFile();
440 if (patch1.IsEmpty() || patch2.IsEmpty())
442 MessageBox(L"Could not create temp file.", L"TortoiseGit", MB_OK | MB_ICONERROR);
443 break;
445 if (CString err; g_Git.RunLogFile(L"git.exe format-patch --stdout " + pFirstEntry->m_CommitHash.ToString() + L"~1.." + pFirstEntry->m_CommitHash.ToString() + L"", patch1, &err))
447 MessageBox(L"Could not generate patch for commit " + pFirstEntry->m_CommitHash.ToString() + L".\n" + err, L"TortoiseGit", MB_ICONERROR);
448 break;
450 if (CString err; g_Git.RunLogFile(L"git.exe format-patch --stdout " + pLastEntry->m_CommitHash.ToString() + L"~1.." + pLastEntry->m_CommitHash.ToString() + L"", patch2, &err))
452 MessageBox(L"Could not generate patch for commit " + pLastEntry->m_CommitHash.ToString() + L".\n" + err, L"TortoiseGit", MB_ICONERROR);
453 break;
455 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, pLastEntry->m_CommitHash, CAppUtils::DiffFlags());
457 break;
458 case ID_LOG_VIEWRANGE:
459 case ID_LOG_VIEWRANGE_REVERSE:
460 case ID_LOG_VIEWRANGE_REACHABLEFROMONLYONE:
462 GitRev* pLastEntry = m_arShownList.SafeGetAt(LastSelect);
464 CString sep = L"..";
465 if ((cmd & 0xFFFF) == ID_LOG_VIEWRANGE_REACHABLEFROMONLYONE)
466 sep = L"...";
468 CString cmdline;
469 if ((cmd & 0xFFFF) == ID_LOG_VIEWRANGE_REVERSE)
470 cmdline.Format(L"/command:log /path:\"%s\" /range:\"%s%s%s\"", static_cast<LPCWSTR>(g_Git.CombinePath(m_Path)), static_cast<LPCWSTR>(pSelLogEntry->m_CommitHash.ToString()), static_cast<LPCWSTR>(sep), static_cast<LPCWSTR>(pLastEntry->m_CommitHash.ToString()));
471 else
472 cmdline.Format(L"/command:log /path:\"%s\" /range:\"%s%s%s\"", static_cast<LPCWSTR>(g_Git.CombinePath(m_Path)), static_cast<LPCWSTR>(pLastEntry->m_CommitHash.ToString()), static_cast<LPCWSTR>(sep), static_cast<LPCWSTR>(pSelLogEntry->m_CommitHash.ToString()));
473 CAppUtils::RunTortoiseGitProc(cmdline);
475 break;
476 case ID_COPYCLIPBOARDFULL:
477 case ID_COPYCLIPBOARDFULLNOPATHS:
478 case ID_COPYCLIPBOARDHASH:
479 case ID_COPYCLIPBOARDAUTHORSFULL:
480 case ID_COPYCLIPBOARDAUTHORSNAME:
481 case ID_COPYCLIPBOARDAUTHORSEMAIL:
482 case ID_COPYCLIPBOARDSUBJECTS:
483 case ID_COPYCLIPBOARDMESSAGES:
485 CopySelectionToClipBoard(cmd & 0xFFFF);
487 break;
488 case ID_COPYCLIPBOARDBRANCHTAG:
490 if (!popmenu)
491 break;
492 auto selectedBranch = reinterpret_cast<const CString*>(reinterpret_cast<CIconMenu*>(popmenu)->GetMenuItemData(cmd));
493 CString sClipboard;
494 if (selectedBranch)
496 if (CStringUtils::StartsWith(*selectedBranch, L"refs/tags/"))
498 if (CStringUtils::EndsWith(*selectedBranch, L"^{}"))
499 sClipboard = selectedBranch->Mid(static_cast<int>(wcslen(L"refs/tags/")), selectedBranch->GetLength() - static_cast<int>(wcslen(L"refs/tags/")) - static_cast<int>(wcslen(L"^{}")));
500 else
501 sClipboard = selectedBranch->Mid(static_cast<int>(wcslen(L"refs/tags/")));
503 else
504 sClipboard = CGit::StripRefName(*selectedBranch);
506 else if (auto refList = hashMap.find(pSelLogEntry->m_CommitHash); refList != hashMap.cend())
508 for (const auto& ref : refList->second)
510 if (CStringUtils::StartsWith(ref, L"refs/tags/") && CStringUtils::EndsWith(ref, L"^{}"))
511 sClipboard += ref.Left(ref.GetLength() - static_cast<int>(wcslen(L"^{}")));
512 else
513 sClipboard += ref;
514 sClipboard += L"\r\n";
517 CStringUtils::WriteAsciiStringToClipboard(sClipboard, GetSafeHwnd());
519 break;
520 case ID_EXPORT:
522 CString str=pSelLogEntry->m_CommitHash.ToString();
523 // try to get the tag
524 if (auto refList = hashMap.find(pSelLogEntry->m_CommitHash); refList != hashMap.cend())
525 GetFirstEntryStartingWith(refList->second, L"refs/tags/", str);
526 CAppUtils::Export(GetParentHWND(), &str, &m_Path);
528 break;
529 case ID_CREATE_BRANCH:
530 case ID_CREATE_TAG:
532 auto branch = popmenu ? reinterpret_cast<const CString*>(static_cast<CIconMenu*>(popmenu)->GetMenuItemData(cmd & 0xFFFF)) : nullptr;
533 CString str = pSelLogEntry->m_CommitHash.ToString();
534 if (branch)
535 str = *branch;
536 else if (auto refList = hashMap.find(pSelLogEntry->m_CommitHash); refList != hashMap.cend()) // try to guess remote branch in order to enable tracking
537 GetFirstEntryStartingWith(refList->second, L"refs/remotes/", str);
539 CAppUtils::CreateBranchTag(GetParentHWND(), (cmd & 0xFFFF) == ID_CREATE_TAG, &str);
540 ReloadHashMap();
541 if (m_pFindDialog)
542 m_pFindDialog->RefreshList();
543 Invalidate();
544 ::PostMessage(this->GetParent()->m_hWnd,MSG_REFLOG_CHANGED,0,0);
546 break;
547 case ID_SWITCHTOREV:
549 CString str = pSelLogEntry->m_CommitHash.ToString();
550 auto branch = popmenu ? reinterpret_cast<const CString*>(static_cast<CIconMenu*>(popmenu)->GetMenuItemData(cmd & 0xFFFF)) : nullptr;
551 if (branch)
552 str = *branch;
553 else if (auto refList = hashMap.find(pSelLogEntry->m_CommitHash); refList != hashMap.cend()) // try to guess remote branch in order to recommend good branch name and tracking
554 GetFirstEntryStartingWith(refList->second, L"refs/remotes/", str);
556 CAppUtils::Switch(GetParentHWND(), str);
558 ReloadHashMap();
559 Invalidate();
560 ::PostMessage(this->GetParent()->m_hWnd,MSG_REFLOG_CHANGED,0,0);
561 break;
562 case ID_SWITCHBRANCH:
563 if(popmenu)
565 auto branch = popmenu ? reinterpret_cast<const CString*>(static_cast<CIconMenu*>(popmenu)->GetMenuItemData(cmd)) : nullptr;
566 if(branch)
568 CString name = *branch;
569 CGit::GetShortName(*branch, name, L"refs/heads/");
570 CAppUtils::PerformSwitch(GetParentHWND(), name);
572 ReloadHashMap();
573 Invalidate();
574 ::PostMessage(this->GetParent()->m_hWnd,MSG_REFLOG_CHANGED,0,0);
576 break;
577 case ID_RESET:
579 if (CAppUtils::GitReset(GetParentHWND(), pSelLogEntry->m_CommitHash.ToString()))
581 ResetWcRev(true);
582 ReloadHashMap();
583 Invalidate();
586 break;
587 case ID_REBASE_PICK:
588 SetSelectedRebaseAction(LOGACTIONS_REBASE_PICK);
589 break;
590 case ID_REBASE_EDIT:
591 SetSelectedRebaseAction(LOGACTIONS_REBASE_EDIT);
592 break;
593 case ID_REBASE_SQUASH:
594 SetSelectedRebaseAction(LOGACTIONS_REBASE_SQUASH);
595 break;
596 case ID_REBASE_SKIP:
597 SetSelectedRebaseAction(LOGACTIONS_REBASE_SKIP);
598 break;
599 case ID_COMBINE_COMMIT:
601 CString head;
602 CGitHash headhash;
603 CGitHash hashFirst,hashLast;
605 int headindex=GetHeadIndex();
606 if(headindex>=0) //incase show all branch, head is not the first commits.
608 head.Format(L"HEAD~%d", FirstSelect - headindex);
609 if (g_Git.GetHash(hashFirst, head))
611 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of first selected revision."), L"TortoiseGit", MB_ICONERROR);
612 break;
615 head.Format(L"HEAD~%d", LastSelect - headindex);
616 if (g_Git.GetHash(hashLast, head))
618 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of last selected revision."), L"TortoiseGit", MB_ICONERROR);
619 break;
623 GitRev* pFirstEntry = m_arShownList.SafeGetAt(FirstSelect);
624 GitRev* pLastEntry = m_arShownList.SafeGetAt(LastSelect);
625 if(pFirstEntry->m_CommitHash != hashFirst || pLastEntry->m_CommitHash != hashLast)
627 CMessageBox::Show(GetParentHWND(), IDS_PROC_CANNOTCOMBINE, IDS_APPNAME, MB_OK | MB_ICONEXCLAMATION);
628 break;
631 GitRev lastRevision;
632 if (lastRevision.GetParentFromHash(hashLast))
634 MessageBox(lastRevision.GetLastErr(), L"TortoiseGit", MB_ICONERROR);
635 break;
638 if (g_Git.GetHash(headhash, L"HEAD"))
640 MessageBox(g_Git.GetGitLastErr(L"Could not get HEAD hash."), L"TortoiseGit", MB_ICONERROR);
641 break;
644 if(!g_Git.CheckCleanWorkTree())
646 CMessageBox::Show(GetParentHWND(), IDS_PROC_NOCLEAN, IDS_APPNAME, MB_OK | MB_ICONEXCLAMATION);
647 break;
649 CString sCmd, out;
651 //Use throw to abort this process (reset back to original HEAD)
654 sCmd.Format(L"git.exe reset --hard %s --", static_cast<LPCWSTR>(pFirstEntry->m_CommitHash.ToString()));
655 if(g_Git.Run(sCmd, &out, CP_UTF8))
657 MessageBox(out, L"TortoiseGit", MB_OK | MB_ICONERROR);
658 throw std::exception(CUnicodeUtils::GetUTF8(CString(MAKEINTRESOURCE(IDS_PROC_COMBINE_ERRORSTEP1)) + L"\r\n\r\n" + out));
660 sCmd.Format(L"git.exe reset --soft %s --", static_cast<LPCWSTR>(hashLast.ToString()));
661 if(g_Git.Run(sCmd, &out, CP_UTF8))
663 MessageBox(out, L"TortoiseGit", MB_OK | MB_ICONERROR);
664 throw std::exception(CUnicodeUtils::GetUTF8(CString(MAKEINTRESOURCE(IDS_PROC_COMBINE_ERRORSTEP2)) + L"\r\n\r\n"+out));
667 CCommitDlg dlg;
668 for (int i = FirstSelect; i <= LastSelect; ++i)
670 GitRev* pRev = m_arShownList.SafeGetAt(i);
671 dlg.m_sLogMessage += pRev->GetSubject() + L'\n' + pRev->GetBody();
672 dlg.m_sLogMessage += L'\n';
674 dlg.m_bWholeProject=true;
675 dlg.m_bSelectFilesForCommit = true;
676 dlg.m_bForceCommitAmend=true;
677 dlg.m_bCommitAmend = TRUE;
678 const int squashDate = CRegDWORD(L"Software\\TortoiseGit\\SquashDate", 0);
679 if (squashDate == 1)
680 dlg.SetTime(m_arShownList.SafeGetAt(FirstSelect)->GetAuthorDate());
681 else if (squashDate == 2)
682 dlg.SetTime(CTime::GetCurrentTime());
683 else
684 dlg.SetTime(m_arShownList.SafeGetAt(LastSelect)->GetAuthorDate());
685 CTGitPathList gpl;
686 gpl.AddPath(CTGitPath(g_Git.m_CurrentDir));
687 dlg.m_pathList = gpl;
688 if (lastRevision.ParentsCount() != 1)
690 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);
691 dlg.m_bAmendDiffToLastCommit = TRUE;
693 else
694 dlg.m_bAmendDiffToLastCommit = FALSE;
695 dlg.m_bNoPostActions=true;
696 dlg.m_AmendStr=dlg.m_sLogMessage;
698 if (dlg.DoModal() == IDOK)
700 if(pFirstEntry->m_CommitHash!=headhash)
702 if (CherryPickFrom(pFirstEntry->m_CommitHash.ToString(), headhash.ToString()))
704 CString msg;
705 msg.Format(L"Error while cherry pick commits on top of combined commits. Aborting.\r\n\r\n");
706 throw std::exception(CUnicodeUtils::GetUTF8(msg));
710 else
711 throw std::exception(CUnicodeUtils::GetUTF8(CString(MAKEINTRESOURCE(IDS_USERCANCELLED))));
713 catch(std::exception& e)
715 CMessageBox::Show(GetParentHWND(), CUnicodeUtils::GetUnicode(e.what()), L"TortoiseGit", MB_OK | MB_ICONERROR);
716 sCmd.Format(L"git.exe reset --hard %s --", static_cast<LPCWSTR>(headhash.ToString()));
717 out.Empty();
718 if(g_Git.Run(sCmd, &out, CP_UTF8))
719 MessageBox(CString(MAKEINTRESOURCE(IDS_PROC_COMBINE_ERRORRESETHEAD)) + L"\r\n\r\n" + out, L"TortoiseGit", MB_OK | MB_ICONERROR);
721 Refresh();
723 break;
725 case ID_CHERRY_PICK:
727 if (s_bThreadRunning)
729 CMessageBox::Show(GetParentHWND(), IDS_PROC_LOG_ONLYONCE, IDS_APPNAME, MB_ICONEXCLAMATION);
730 break;
732 CRebaseDlg dlg;
733 dlg.m_IsCherryPick = TRUE;
734 dlg.m_Upstream = this->m_CurrentBranch;
735 POSITION pos2 = GetFirstSelectedItemPosition();
736 while(pos2)
738 const int indexNext2 = GetNextSelectedItem(pos2);
739 dlg.m_CommitList.m_logEntries.push_back(m_arShownList.SafeGetAt(indexNext2)->m_CommitHash);
740 dlg.m_CommitList.m_LogCache.m_HashMap[m_arShownList.SafeGetAt(indexNext2)->m_CommitHash] = *m_arShownList.SafeGetAt(indexNext2);
741 dlg.m_CommitList.m_logEntries.GetGitRevAt(dlg.m_CommitList.m_logEntries.size() - 1).GetRebaseAction() |= LOGACTIONS_REBASE_PICK;
744 SafeTerminateAsyncDiffThread();
745 if(dlg.DoModal() == IDOK)
747 Refresh();
749 else
750 StartAsyncDiffThread();
752 break;
753 case ID_REBASE_TO_VERSION:
755 if (s_bThreadRunning)
757 CMessageBox::Show(GetParentHWND(), IDS_PROC_LOG_ONLYONCE, IDS_APPNAME, MB_ICONEXCLAMATION);
758 break;
760 CRebaseDlg dlg;
761 dlg.m_Upstream = pSelLogEntry->m_CommitHash.ToString();
762 // try to guess a branch, optimally a local branch
763 if (auto refList = hashMap.find(pSelLogEntry->m_CommitHash); refList != hashMap.cend())
765 if (!refList->second.empty())
766 dlg.m_Upstream = refList->second.front();
767 for (const auto& ref : refList->second)
769 if (CGit::GetShortName(ref, dlg.m_Upstream, L"refs/heads/"))
770 break;
774 SafeTerminateAsyncDiffThread();
775 if(dlg.DoModal() == IDOK)
777 Refresh();
779 else
780 StartAsyncDiffThread();
783 break;
785 case ID_STASH_SAVE:
786 if (CAppUtils::StashSave(GetParentHWND()))
787 Refresh();
788 break;
790 case ID_STASH_POP:
791 if (CAppUtils::StashPop(GetParentHWND()))
792 Refresh();
793 break;
795 case ID_STASH_LIST:
796 CAppUtils::RunTortoiseGitProc(L"/command:reflog /ref:refs/stash");
797 break;
799 case ID_REFLOG_STASH_APPLY:
800 CAppUtils::StashApply(GetParentHWND(), pSelLogEntry->m_Ref);
801 break;
803 case ID_REFLOG_DEL:
805 // security measure, make sure the view is still up2date before deleting any refs, see issue #3782
806 std::vector<GitRevLoglist> cache;
807 if (CString err; GitRevLoglist::GetRefLog(m_CurrentBranch, cache, err))
809 MessageBox(L"Error while loading reflog.\n" + err, L"TortoiseGit", MB_ICONERROR);
810 break;
812 if (cache.size() != m_arShownList.size() || !cache.empty() && (cache.at(0).GetCommitterDate() != m_arShownList.SafeGetAt(0)->GetCommitterDate() || cache.at(0).m_CommitHash != m_arShownList.SafeGetAt(0)->m_CommitHash || cache.at(cache.size() - 1).m_CommitHash != m_arShownList.SafeGetAt(m_arShownList.size() - 1)->m_CommitHash))
814 MessageBox(L"The current view seems to be out of date. Please refresh, e.g. by pressing F5 and recheck the selection.", L"TortoiseGit", MB_ICONERROR);
815 break;
818 CString str;
819 if (GetSelectedCount() > 1)
820 str.Format(IDS_PROC_DELETENREFS, GetSelectedCount());
821 else
822 str.Format(IDS_PROC_DELETEREF, static_cast<LPCWSTR>(pSelLogEntry->m_Ref));
824 if (CMessageBox::Show(GetParentHWND(), str, L"TortoiseGit", 1, IDI_QUESTION, CString(MAKEINTRESOURCE(IDS_DELETEBUTTON)), CString(MAKEINTRESOURCE(IDS_ABORTBUTTON))) == 2)
825 return;
827 std::vector<CString> refsToDelete;
828 POSITION pos2 = GetFirstSelectedItemPosition();
829 while (pos2)
831 CString ref = m_arShownList.SafeGetAt(GetNextSelectedItem(pos2))->m_Ref;
832 if (CStringUtils::StartsWith(ref, L"refs/"))
833 ref = ref.Mid(static_cast<int>(wcslen(L"refs/")));
834 const int refpos = ref.ReverseFind(L'{');
835 if (refpos > 0 && ref.Mid(refpos - 1, 2) != L"@{")
836 ref = ref.Left(refpos) + L'@'+ ref.Mid(refpos);
837 refsToDelete.push_back(ref);
840 for (auto revIt = refsToDelete.crbegin(); revIt != refsToDelete.crend(); ++revIt)
842 CString ref = *revIt;
843 CString sCmd, out;
844 if (CStringUtils::StartsWith(ref, L"stash"))
845 sCmd.Format(L"git.exe stash drop %s", static_cast<LPCWSTR>(ref));
846 else
847 sCmd.Format(L"git.exe reflog delete %s", static_cast<LPCWSTR>(ref));
849 if (g_Git.Run(sCmd, &out, CP_UTF8))
850 MessageBox(out, L"TortoiseGit", MB_OK | MB_ICONERROR);
852 ::PostMessage(this->GetParent()->m_hWnd,MSG_REFLOG_CHANGED,0,0);
855 break;
856 case ID_LOG:
858 CString sCmd = L"/command:log";
859 sCmd += L" /path:\"" + g_Git.m_CurrentDir + L"\" ";
860 GitRev* r1 = m_arShownList.SafeGetAt(FirstSelect);
861 sCmd += L" /endrev:" + r1->m_CommitHash.ToString();
862 CAppUtils::RunTortoiseGitProc(sCmd);
864 break;
865 case ID_CREATE_PATCH:
867 const int select = this->GetSelectedCount();
868 CString sCmd = L"/command:formatpatch";
869 sCmd += L" /path:\"" + g_Git.m_CurrentDir + L"\" ";
871 GitRev* r1 = m_arShownList.SafeGetAt(FirstSelect);
872 GitRev* r2 = nullptr;
873 if(select == 1)
875 sCmd += L" /startrev:" + r1->m_CommitHash.ToString();
877 else
879 r2 = m_arShownList.SafeGetAt(LastSelect);
880 if( this->m_IsOldFirst )
882 sCmd += L" /startrev:" + r1->m_CommitHash.ToString() + L"~1";
883 sCmd += L" /endrev:" + r2->m_CommitHash.ToString();
886 else
888 sCmd += L" /startrev:" + r2->m_CommitHash.ToString() + L"~1";
889 sCmd += L" /endrev:" + r1->m_CommitHash.ToString();
894 CAppUtils::RunTortoiseGitProc(sCmd);
896 break;
897 case ID_BISECTSTART:
899 GitRev* first = m_arShownList.SafeGetAt(FirstSelect);
900 GitRev* last = m_arShownList.SafeGetAt(LastSelect);
901 ASSERT(first && last);
903 CString firstBad = first->m_CommitHash.ToString();
904 if (auto refList = hashMap.find(first->m_CommitHash); refList != hashMap.cend() && !refList->second.empty())
905 firstBad = refList->second.at(0);
906 CString lastGood = last->m_CommitHash.ToString();
907 if (auto refList = hashMap.find(last->m_CommitHash); refList != hashMap.cend() && !refList->second.empty())
908 lastGood = refList->second.at(0);
910 if (CAppUtils::BisectStart(GetParentHWND(), lastGood, firstBad))
911 Refresh();
913 break;
914 case ID_BISECTGOOD:
916 GitRev* first = m_arShownList.SafeGetAt(FirstSelect);
917 if (CAppUtils::BisectOperation(GetParentHWND(), L"good", !first->m_CommitHash.IsEmpty() ? first->m_CommitHash.ToString() : CString()))
918 Refresh();
920 break;
921 case ID_BISECTBAD:
923 GitRev* first = m_arShownList.SafeGetAt(FirstSelect);
924 if (CAppUtils::BisectOperation(GetParentHWND(), L"bad", !first->m_CommitHash.IsEmpty() ? first->m_CommitHash.ToString() : CString()))
925 Refresh();
927 break;
928 case ID_BISECTSKIP:
930 CString refs;
931 POSITION pos2 = GetFirstSelectedItemPosition();
932 while (pos2)
934 const int indexNext2 = GetNextSelectedItem(pos2);
935 auto rev = m_arShownList.SafeGetAt(indexNext2);
936 if (!rev->m_CommitHash.IsEmpty())
937 refs.AppendFormat(L" %s", static_cast<LPCWSTR>(rev->m_CommitHash.ToString()));
939 if (CAppUtils::BisectOperation(GetParentHWND(), L"skip", refs))
940 Refresh();
942 break;
943 case ID_BISECTRESET:
945 if (CAppUtils::BisectOperation(GetParentHWND(), L"reset"))
946 Refresh();
948 break;
949 case ID_REPOBROWSE:
951 CString sCmd;
952 sCmd.Format(L"/command:repobrowser /path:\"%s\" /rev:%s", static_cast<LPCWSTR>(g_Git.m_CurrentDir), static_cast<LPCWSTR>(pSelLogEntry->m_CommitHash.ToString()));
953 CAppUtils::RunTortoiseGitProc(sCmd);
955 break;
956 case ID_PUSH:
958 CString workingDir = g_Git.m_CurrentDir;
959 workingDir.Replace(L':', L'_');
960 bool pushAllBranches = CRegDWORD(L"Software\\TortoiseGit\\TortoiseProc\\Push\\" + workingDir + L"\\AllBranches", FALSE) == TRUE;
962 CString guessAssociatedBranch = pSelLogEntry->m_CommitHash.ToString();
963 auto branch = popmenu ? reinterpret_cast<const CString*>(static_cast<CIconMenu*>(popmenu)->GetMenuItemData(cmd)) : nullptr;
964 if (branch && !CStringUtils::StartsWith(*branch, L"refs/remotes/"))
966 guessAssociatedBranch = *branch;
967 pushAllBranches = false;
969 else if (auto refList = hashMap.find(pSelLogEntry->m_CommitHash); refList != hashMap.cend())
971 if (!GetFirstEntryStartingWith(refList->second, L"refs/heads/", guessAssociatedBranch))
972 GetFirstEntryStartingWith(refList->second, L"refs/tags/", guessAssociatedBranch);
975 if (CStringUtils::EndsWith(guessAssociatedBranch, L"^{}"))
976 guessAssociatedBranch.Truncate(guessAssociatedBranch.GetLength() - static_cast<int>(wcslen(L"^{}")));
978 if (CAppUtils::Push(GetParentHWND(), guessAssociatedBranch, pushAllBranches))
979 Refresh();
981 break;
982 case ID_PULL:
984 if (CAppUtils::Pull(GetParentHWND()))
985 Refresh();
987 break;
988 case ID_FETCH:
990 if (CAppUtils::Fetch(GetParentHWND()))
991 Refresh();
993 break;
994 case ID_SVNDCOMMIT:
996 if (CAppUtils::SVNDCommit(GetParentHWND()))
997 Refresh();
999 break;
1000 case ID_CLEANUP:
1002 CString sCmd;
1003 sCmd.Format(L"/command:cleanup /path:\"%s\"", static_cast<LPCWSTR>(g_Git.m_CurrentDir));
1004 CAppUtils::RunTortoiseGitProc(sCmd);
1006 break;
1007 case ID_SUBMODULE_UPDATE:
1009 CString sCmd;
1010 sCmd.Format(L"/command:subupdate /bkpath:\"%s\"", static_cast<LPCWSTR>(g_Git.m_CurrentDir));
1011 CAppUtils::RunTortoiseGitProc(sCmd);
1013 break;
1014 case ID_SHOWBRANCHES:
1016 CCommitIsOnRefsDlg* dlg = new CCommitIsOnRefsDlg(this);
1017 dlg->m_Rev = static_cast<LPCWSTR>(pSelLogEntry->m_CommitHash.ToString());
1018 dlg->Create(this);
1019 // pointer won't leak as it is destroyed within PostNcDestroy()
1021 break;
1022 case ID_DELETE:
1024 auto branch = popmenu ? reinterpret_cast<const CString*>(static_cast<CIconMenu*>(popmenu)->GetMenuItemData(cmd)) : nullptr;
1025 if (!branch)
1027 CMessageBox::Show(GetParentHWND(), IDS_ERROR_NOREF, IDS_APPNAME, MB_OK | MB_ICONERROR);
1028 return;
1030 CString shortname;
1031 if (branch == reinterpret_cast<CString*>(MAKEINTRESOURCE(IDS_ALL)))
1033 auto refList = hashMap.find(pSelLogEntry->m_CommitHash);
1034 if (refList == hashMap.cend())
1035 return;
1036 CString currentBranch = L"refs/heads/" + m_CurrentBranch;
1037 bool nothingDeleted = true;
1038 for (const auto& ref : refList->second)
1040 if (ref == currentBranch)
1041 continue;
1042 if (!CAppUtils::DeleteRef(this, ref))
1043 break;
1044 nothingDeleted = false;
1046 if (nothingDeleted)
1047 return;
1049 else if (!CAppUtils::DeleteRef(this, *branch))
1050 return;
1051 this->ReloadHashMap();
1052 if (m_pFindDialog)
1053 m_pFindDialog->RefreshList();
1054 CRect rect;
1055 this->GetItemRect(FirstSelect,&rect,LVIR_BOUNDS);
1056 this->InvalidateRect(rect);
1058 break;
1060 case ID_TOGGLE_ROLLUP:
1062 auto newRollUpStates = std::make_shared<RollUpStateMap>(*m_RollUpStates.load());
1063 RollUpState newNodeState = pSelLogEntry->m_RolledUp ? RollUpState::Expand : RollUpState::Collapse;
1064 if (auto it = newRollUpStates->find(pSelLogEntry->m_CommitHash); it != newRollUpStates->end())
1066 if (pSelLogEntry->m_RolledUpIsForced)
1067 newRollUpStates->erase(it);
1068 else
1069 it->second = newNodeState;
1071 else
1072 newRollUpStates->emplace(pSelLogEntry->m_CommitHash, newNodeState);
1073 m_RollUpStates.store(newRollUpStates);
1074 this->Refresh();
1076 break;
1078 case ID_FINDENTRY:
1080 m_nSearchIndex = GetSelectionMark();
1081 if (m_nSearchIndex < 0)
1082 m_nSearchIndex = 0;
1083 if (m_pFindDialog)
1085 m_pFindDialog->SetFocus();
1086 break;
1088 else
1090 m_pFindDialog = new CFindDlg();
1091 m_pFindDialog->Create(this);
1094 break;
1095 case ID_MERGEREV:
1097 CString str = pSelLogEntry->m_CommitHash.ToString();
1098 auto branch = popmenu ? reinterpret_cast<const CString*>(static_cast<CIconMenu*>(popmenu)->GetMenuItemData(cmd & 0xFFFF)) : nullptr;
1099 if (branch)
1100 str = *branch;
1101 else if (auto refList = hashMap.find(pSelLogEntry->m_CommitHash); refList != hashMap.cend() && !refList->second.empty())
1102 str = refList->second.at(0);
1103 // we need an URL to complete this command, so error out if we can't get an URL
1104 if (CAppUtils::Merge(GetParentHWND(), &str))
1106 this->Refresh();
1109 break;
1110 case ID_REVERTREV:
1112 int parent = 0;
1113 if (GetSelectedCount() == 1)
1115 parent = cmd >> 16;
1116 if (static_cast<size_t>(parent) > pSelLogEntry->m_ParentHash.size())
1118 CString str;
1119 str.Format(IDS_PROC_NOPARENT, parent);
1120 MessageBox(str, L"TortoiseGit", MB_OK | MB_ICONERROR);
1121 return;
1125 if (!this->RevertSelectedCommits(parent))
1127 if (CMessageBox::Show(m_hWnd, IDS_REVREVERTED, IDS_APPNAME, 1, IDI_QUESTION, IDS_OKBUTTON, IDS_COMMITBUTTON) == 2)
1129 CTGitPathList pathlist;
1130 pathlist.AddPath(this->m_Path);
1131 bool bSelectFilesForCommit = !!DWORD(CRegStdDWORD(L"Software\\TortoiseGit\\SelectFilesForCommit", TRUE));
1132 CString str;
1133 CAppUtils::Commit(GetParentHWND(), CString(), false, str, pathlist, bSelectFilesForCommit);
1135 this->Refresh();
1138 break;
1139 case ID_EDITNOTE:
1141 CAppUtils::EditNote(GetParentHWND(), pSelLogEntry, &m_ProjectProperties);
1142 this->SetItemState(FirstSelect, 0, LVIS_SELECTED);
1143 this->SetItemState(FirstSelect, LVIS_SELECTED, LVIS_SELECTED);
1145 break;
1146 default:
1147 //CMessageBox::Show(nullptr, L"Have not implemented", L"TortoiseGit", MB_OK);
1148 break;
1149 #if 0
1151 case ID_BLAMECOMPARE:
1153 //user clicked on the menu item "compare with working copy"
1154 //now first get the revision which is selected
1155 if (PromptShown())
1157 GitDiff diff(this, this->m_hWnd, true);
1158 diff.SetHEADPeg(m_LogRevision);
1159 diff.ShowCompare(m_path, GitRev::REV_BASE, m_path, revSelected, GitRev(), false, true);
1161 else
1162 CAppUtils::StartShowCompare(m_hWnd, m_path, GitRev::REV_BASE, m_path, revSelected, GitRev(), m_LogRevision, false, false, true);
1164 break;
1165 case ID_BLAMEWITHPREVIOUS:
1167 //user clicked on the menu item "Compare and Blame with previous revision"
1168 if (PromptShown())
1170 GitDiff diff(this, this->m_hWnd, true);
1171 diff.SetHEADPeg(m_LogRevision);
1172 diff.ShowCompare(CTGitPath(pathURL), revPrevious, CTGitPath(pathURL), revSelected, GitRev(), false, true);
1174 else
1175 CAppUtils::StartShowCompare(m_hWnd, CTGitPath(pathURL), revPrevious, CTGitPath(pathURL), revSelected, GitRev(), m_LogRevision, false, false, true);
1177 break;
1179 case ID_OPENWITH:
1180 bOpenWith = true;
1181 [[fallthrough]];
1182 case ID_OPEN:
1184 CProgressDlg progDlg;
1185 progDlg.SetTitle(IDS_APPNAME);
1186 progDlg.SetAnimation(IDR_DOWNLOAD);
1187 CString sInfoLine;
1188 sInfoLine.Format(IDS_PROGRESSGETFILEREVISION, m_path.GetWinPath(), static_cast<LPCWSTR>(revSelected.ToString()));
1189 progDlg.SetLine(1, sInfoLine, true);
1190 SetAndClearProgressInfo(&progDlg);
1191 progDlg.ShowModeless(m_hWnd);
1192 CTGitPath tempfile = CTempFiles::Instance().GetTempFilePath(false, m_path, revSelected);
1193 bool bSuccess = true;
1194 if (!Cat(m_path, GitRev(GitRev::REV_HEAD), revSelected, tempfile))
1196 bSuccess = false;
1197 // try again, but with the selected revision as the peg revision
1198 if (!Cat(m_path, revSelected, revSelected, tempfile))
1200 progDlg.Stop();
1201 SetAndClearProgressInfo(nullptr);
1202 CMessageBox::Show(GetSafeHwnd(), GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
1203 EnableOKButton();
1204 break;
1206 bSuccess = true;
1208 if (bSuccess)
1210 progDlg.Stop();
1211 SetAndClearProgressInfo(nullptr);
1212 SetFileAttributes(tempfile.GetWinPath(), FILE_ATTRIBUTE_READONLY);
1213 if (!bOpenWith)
1214 CAppUtils::ShellOpen(tempfile.GetWinPath(), GetSafeHwnd());
1215 else
1216 CAppUtils::ShowOpenWithDialog(tempfile.GetWinPathString(), GetSafeHwnd());
1219 break;
1220 case ID_BLAME:
1222 CBlameDlg dlg;
1223 dlg.EndRev = revSelected;
1224 if (dlg.DoModal() == IDOK)
1226 CBlame blame;
1227 CString tempfile;
1228 CString logfile;
1229 tempfile = blame.BlameToTempFile(m_path, dlg.StartRev, dlg.EndRev, dlg.EndRev, logfile, L"", dlg.m_bIncludeMerge, TRUE, TRUE);
1230 if (!tempfile.IsEmpty())
1232 if (dlg.m_bTextView)
1234 //open the default text editor for the result file
1235 CAppUtils::StartTextViewer(tempfile);
1237 else
1239 CString sParams = L"/path:\"" + m_path.GetGitPathString() + L"\" ";
1240 if(!CAppUtils::LaunchTortoiseBlame(tempfile, logfile, CPathUtils::GetFileNameFromPath(m_path.GetFileOrDirectoryName()),sParams))
1242 break;
1246 else
1248 CMessageBox::Show(GetSafeHwnd(), blame.GetLastErrorMessage(), L"TortoiseGit", MB_ICONERROR);
1252 break;
1253 case ID_EXPORT:
1255 CString sCmd;
1256 sCmd.Format(L"%s /command:export /path:\"%s\" /revision:%ld",
1257 static_cast<LPCWSTR>(CPathUtils::GetAppDirectory() + L"TortoiseGitProc.exe"),
1258 static_cast<LPCWSTR>(pathURL), static_cast<LONG>(revSelected));
1259 CAppUtils::LaunchApplication(sCmd, nullptr, false);
1261 break;
1262 case ID_VIEWREV:
1264 CString url = m_ProjectProperties.sWebViewerRev;
1265 url = GetAbsoluteUrlFromRelativeUrl(url);
1266 url.Replace(L"%REVISION%", revSelected.ToString());
1267 if (!url.IsEmpty())
1268 ShellExecute(GetSafeHwnd(), L"open", url, nullptr, nullptr, SW_SHOWDEFAULT);
1270 break;
1271 case ID_VIEWPATHREV:
1273 CString relurl = pathURL;
1274 CString sRoot = GetRepositoryRoot(CTGitPath(relurl));
1275 relurl = relurl.Mid(sRoot.GetLength());
1276 CString url = m_ProjectProperties.sWebViewerPathRev;
1277 url = GetAbsoluteUrlFromRelativeUrl(url);
1278 url.Replace(L"%REVISION%", revSelected.ToString());
1279 url.Replace(L"%PATH%", relurl);
1280 if (!url.IsEmpty())
1281 ShellExecute(GetSafeHwnd(), L"open", url, nullptr, nullptr, SW_SHOWDEFAULT);
1283 break;
1284 #endif
1286 } // switch (cmd)
1288 theApp.DoWaitCursor(-1);
1291 void CGitLogList::SetSelectedRebaseAction(int action)
1293 POSITION pos = GetFirstSelectedItemPosition();
1294 if (!pos) return;
1295 while(pos)
1297 auto index = GetNextSelectedItem(pos);
1298 if (m_arShownList.SafeGetAt(index)->GetRebaseAction() & (LOGACTIONS_REBASE_CURRENT | LOGACTIONS_REBASE_DONE) || (index == GetItemCount() - 1 && action == LOGACTIONS_REBASE_SQUASH))
1299 continue;
1300 if (!m_bIsCherryPick && m_arShownList.SafeGetAt(index)->ParentsCount() > 1 && action == LOGACTIONS_REBASE_SQUASH)
1301 continue;
1302 m_arShownList.SafeGetAt(index)->GetRebaseAction() = action;
1303 CRect rect;
1304 this->GetItemRect(index,&rect,LVIR_BOUNDS);
1305 this->InvalidateRect(rect);
1308 GetParent()->PostMessage(CGitLogListBase::m_RebaseActionMessage);
1311 void CGitLogList::SetUnselectedRebaseAction(int action)
1313 POSITION pos = GetFirstSelectedItemPosition();
1314 int index = pos ? GetNextSelectedItem(pos) : -1;
1315 for (int i = 0; i < GetItemCount(); i++)
1317 if (i == index)
1319 index = pos ? GetNextSelectedItem(pos) : -1;
1320 continue;
1323 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))
1324 continue;
1325 m_arShownList.SafeGetAt(i)->GetRebaseAction() = action;
1326 CRect rect;
1327 this->GetItemRect(i, &rect, LVIR_BOUNDS);
1328 this->InvalidateRect(rect);
1331 GetParent()->PostMessage(CGitLogListBase::m_RebaseActionMessage);
1334 void CGitLogList::ShiftSelectedRebaseAction()
1336 POSITION pos = GetFirstSelectedItemPosition();
1337 while(pos)
1339 auto index = GetNextSelectedItem(pos);
1340 int* action = &(m_arShownList.SafeGetAt(index))->GetRebaseAction();
1341 switch (*action)
1343 case LOGACTIONS_REBASE_PICK:
1344 *action = LOGACTIONS_REBASE_SKIP;
1345 break;
1346 case LOGACTIONS_REBASE_SKIP:
1347 *action= LOGACTIONS_REBASE_EDIT;
1348 break;
1349 case LOGACTIONS_REBASE_EDIT:
1350 *action = LOGACTIONS_REBASE_SQUASH;
1351 if (index == GetItemCount() - 1 && (m_bIsCherryPick || m_arShownList.SafeGetAt(index)->m_ParentHash.size() == 1))
1352 *action = LOGACTIONS_REBASE_PICK;
1353 break;
1354 case LOGACTIONS_REBASE_SQUASH:
1355 *action= LOGACTIONS_REBASE_PICK;
1356 break;
1358 CRect rect;
1359 this->GetItemRect(index, &rect, LVIR_BOUNDS);
1360 this->InvalidateRect(rect);