Fixed issue #4132: Error "Could not get next commit. libgit returns: -4" in Log Messa...
[TortoiseGit.git] / src / TortoiseProc / GitLogListBase.cpp
blob9f943ff2c5e072b32dc824b33f29dcd100065016
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2008-2024 - 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 "GitLogListBase.h"
24 #include "IconMenu.h"
25 #include "GitProgressDlg.h"
26 #include "ProgressDlg.h"
27 #include "MessageBox.h"
28 #include "LoglistUtils.h"
29 #include "StringUtils.h"
30 #include "UnicodeUtils.h"
31 #include "../TortoiseShell/Resource.h"
32 #include "CommonAppUtils.h"
33 #include "DPIAware.h"
35 const UINT CGitLogListBase::m_FindDialogMessage = RegisterWindowMessage(FINDMSGSTRING);
36 const UINT CGitLogListBase::m_ScrollToMessage = RegisterWindowMessage(L"TORTOISEGIT_LOG_SCROLLTO");
37 const UINT CGitLogListBase::m_ScrollToRef = RegisterWindowMessage(L"TORTOISEGIT_LOG_SCROLLTOREF");
38 const UINT CGitLogListBase::m_RebaseActionMessage = RegisterWindowMessage(L"TORTOISEGIT_LOG_REBASEACTION");
39 const UINT CGitLogListBase::LOGLIST_RESET_WCREV = RegisterWindowMessage(L"TORTOISEGIT_LOG_RESET_WCREV");
41 long volatile CGitLogListBase::s_bThreadRunning = FALSE;
43 IMPLEMENT_DYNAMIC(CGitLogListBase, CHintCtrl<CResizableColumnsListCtrl<CListCtrl>>)
45 CGitLogListBase::CGitLogListBase() : CHintCtrl<CResizableColumnsListCtrl<CListCtrl>>()
46 ,m_regMaxBugIDColWidth(L"Software\\TortoiseGit\\MaxBugIDColWidth", 200)
47 , m_logEntries(&m_LogCache)
48 , m_arShownList(&m_critSec)
50 // use the default GUI font, create a copy of it and
51 // change the copy to BOLD (leave the rest of the font
52 // the same)
54 ResetWcRev(false);
56 const int cx = GetSystemMetrics(SM_CXSMICON);
57 const int cy = GetSystemMetrics(SM_CYSMICON);
58 m_hModifiedIcon = CCommonAppUtils::LoadIconEx(IDI_ACTIONMODIFIED, cx, cy);
59 m_hReplacedIcon = CCommonAppUtils::LoadIconEx(IDI_ACTIONREPLACED, cx, cy);
60 m_hConflictedIcon = CCommonAppUtils::LoadIconEx(IDI_ACTIONCONFLICTED, cx, cy);
61 m_hAddedIcon = CCommonAppUtils::LoadIconEx(IDI_ACTIONADDED, cx, cy);
62 m_hDeletedIcon = CCommonAppUtils::LoadIconEx(IDI_ACTIONDELETED, cx, cy);
63 m_hFetchIcon = CCommonAppUtils::LoadIconEx(IDI_ACTIONFETCHING, cx, cy);
64 m_hErrorIcon = CCommonAppUtils::LoadIconEx(IDI_ACTIONERROR, cx, cy);
66 m_Filter.m_NumberOfLogsScale = CRegDWORD(L"Software\\TortoiseGit\\LogDialog\\NumberOfLogsScale", CFilterData::SHOW_NO_LIMIT);
67 if (m_Filter.m_NumberOfLogsScale == CFilterData::SHOW_LAST_SEL_DATE)
69 CString key;
70 key.Format(L"Software\\TortoiseGit\\History\\LogDlg_Limits\\%s\\FromDate", static_cast<LPCWSTR>(g_Git.m_CurrentDir));
71 key.Replace(L':', L'_');
72 CString lastSelFromDate = CRegString(key);
73 if (lastSelFromDate.GetLength() == 10)
75 CTime time = CTime(_wtoi(static_cast<LPCWSTR>(lastSelFromDate.Left(4))), _wtoi(static_cast<LPCWSTR>(lastSelFromDate.Mid(5, 2))), _wtoi(static_cast<LPCWSTR>(lastSelFromDate.Mid(8, 2))), 0, 0, 0);
76 m_Filter.m_From = static_cast<DWORD>(time.GetTime());
79 m_Filter.m_NumberOfLogs = CRegDWORD(L"Software\\TortoiseGit\\LogDialog\\NumberOfLogs", 1);
81 // get short/long datetime setting from registry
82 DWORD RegUseShortDateFormat = CRegDWORD(L"Software\\TortoiseGit\\LogDateFormat", TRUE);
83 if ( RegUseShortDateFormat )
85 m_DateFormat = DATE_SHORTDATE;
87 else
89 m_DateFormat = DATE_LONGDATE;
91 // get relative time display setting from registry
92 DWORD regRelativeTimes = CRegDWORD(L"Software\\TortoiseGit\\RelativeTimes", FALSE);
93 m_bRelativeTimes = (regRelativeTimes != 0);
95 m_ContextMenuMask &= ~GetContextMenuBit(ID_REBASE_PICK);
96 m_ContextMenuMask &= ~GetContextMenuBit(ID_REBASE_SQUASH);
97 m_ContextMenuMask &= ~GetContextMenuBit(ID_REBASE_EDIT);
98 m_ContextMenuMask &= ~GetContextMenuBit(ID_REBASE_SKIP);
99 m_ContextMenuMask &= ~GetContextMenuBit(ID_LOG);
100 m_ContextMenuMask &= ~GetContextMenuBit(ID_BLAME);
101 m_ContextMenuMask &= ~GetContextMenuBit(ID_BLAMEPREVIOUS);
103 m_ColumnRegKey = L"log";
105 m_bTagsBranchesOnRightSide = !!CRegDWORD(L"Software\\TortoiseGit\\DrawTagsBranchesOnRightSide", FALSE);
106 m_bSymbolizeRefNames = !!CRegDWORD(L"Software\\TortoiseGit\\SymbolizeRefNames", FALSE);
107 m_bIncludeBoundaryCommits = !!CRegDWORD(L"Software\\TortoiseGit\\LogIncludeBoundaryCommits", FALSE);
108 m_bFullCommitMessageOnLogLine = !!CRegDWORD(L"Software\\TortoiseGit\\FullCommitMessageOnLogLine", FALSE);
110 m_LineWidth = max(1, static_cast<int>(CRegDWORD(L"Software\\TortoiseGit\\TortoiseProc\\Graph\\LogLineWidth", 2)));
111 m_NodeSize = max(1, static_cast<int>(CRegDWORD(L"Software\\TortoiseGit\\TortoiseProc\\Graph\\LogNodeSize", 10)));
113 m_AsyncDiffEvent = ::CreateEvent(nullptr, FALSE, TRUE, nullptr);
114 StartAsyncDiffThread();
117 HWND CGitLogListBase::GetParentHWND()
119 auto owner = GetSafeOwner();
120 if (!owner)
121 return GetSafeHwnd();
122 return owner->GetSafeHwnd();
125 int CGitLogListBase::AsyncDiffThread()
127 while(!m_AsyncThreadExit)
129 ::WaitForSingleObject(m_AsyncDiffEvent, INFINITE);
131 GitRevLoglist* pRev = nullptr;
132 while(!m_AsyncThreadExit && !m_AsynDiffList.empty())
134 m_AsynDiffListLock.Lock();
135 pRev = m_AsynDiffList.back();
136 m_AsynDiffList.pop_back();
137 m_AsynDiffListLock.Unlock();
139 if( pRev->m_CommitHash.IsEmpty() )
141 if(pRev->m_IsDiffFiles)
142 continue;
144 auto filesWrapper = pRev->GetFilesWriter();
145 auto& files = filesWrapper.m_files;
146 files.Clear();
147 pRev->m_ParentHash.clear();
148 pRev->m_ParentHash.push_back(m_HeadHash);
149 g_Git.RefreshGitIndex();
150 g_Git.GetWorkingTreeChanges(files, false, nullptr, true); // filtering is done in LogDlg.cpp
151 auto& action = pRev->GetAction(this);
152 action = 0;
153 for (int j = 0; j < files.GetCount(); ++j)
154 action |= files[j].m_Action;
156 if (CString err; pRev->GetUnRevFiles().FillUnRev(CTGitPath::LOGACTIONS_UNVER, nullptr, &err))
158 MessageBox(L"Failed to get UnRev file list\n" + err, L"TortoiseGit", MB_OK | MB_ICONERROR);
159 InterlockedExchange(&m_AsyncThreadRunning, FALSE);
160 return -1;
163 InterlockedExchange(&pRev->m_IsDiffFiles, TRUE);
165 CString body = L"\n";
166 body.AppendFormat(IDS_FILESCHANGES, files.GetCount());
167 pRev->GetBody() = body;
168 ::PostMessage(m_hWnd, MSG_LOADED, 0, 0);
169 this->GetParent()->PostMessage(WM_COMMAND, MSG_FETCHED_DIFF, 0);
170 continue;
173 pRev->CheckAndDiff();
174 { // fetch change file list
175 for (int i = GetTopIndex(); !m_AsyncThreadExit && i <= GetTopIndex() + GetCountPerPage(); ++i)
177 if (i < static_cast<int>(m_arShownList.size()))
179 GitRevLoglist* data = m_arShownList.SafeGetAt(i);
180 if(data->m_CommitHash == pRev->m_CommitHash)
182 ::PostMessage(m_hWnd, MSG_LOADED, i, 0);
183 break;
188 if(!m_AsyncThreadExit && GetSelectedCount() == 1)
190 POSITION pos = GetFirstSelectedItemPosition();
191 const int nItem = GetNextSelectedItem(pos);
193 if(nItem>=0)
195 GitRevLoglist* data = m_arShownList.SafeGetAt(nItem);
196 if(data)
197 if(data->m_CommitHash == pRev->m_CommitHash)
198 this->GetParent()->PostMessage(WM_COMMAND, MSG_FETCHED_DIFF, 0);
204 InterlockedExchange(&m_AsyncThreadRunning, FALSE);
205 return 0;
207 void CGitLogListBase::hideFromContextMenu(unsigned __int64 hideMask, bool exclusivelyShow)
209 if (exclusivelyShow)
210 m_ContextMenuMask &= hideMask;
211 else
212 m_ContextMenuMask &= ~hideMask;
215 CGitLogListBase::~CGitLogListBase()
217 InterlockedExchange(&m_bNoDispUpdates, TRUE);
218 this->m_arShownList.SafeRemoveAll();
220 m_logEntries.ClearAll();
222 SafeTerminateThread();
223 SafeTerminateAsyncDiffThread();
225 if(m_AsyncDiffEvent)
226 CloseHandle(m_AsyncDiffEvent);
230 BEGIN_MESSAGE_MAP(CGitLogListBase, CHintCtrl<CResizableColumnsListCtrl<CListCtrl>>)
231 ON_REGISTERED_MESSAGE(m_FindDialogMessage, OnFindDialogMessage)
232 ON_REGISTERED_MESSAGE(m_ScrollToMessage, OnScrollToMessage)
233 ON_REGISTERED_MESSAGE(m_ScrollToRef, OnScrollToRef)
234 ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnNMCustomdrawLoglist)
235 ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnLvnGetdispinfoLoglist)
236 ON_WM_CONTEXTMENU()
237 ON_NOTIFY_REFLECT(NM_DBLCLK, OnNMDblclkLoglist)
238 ON_NOTIFY_REFLECT(LVN_ODFINDITEM,OnLvnOdfinditemLoglist)
239 ON_WM_CREATE()
240 ON_WM_DESTROY()
241 ON_MESSAGE(MSG_LOADED,OnLoad)
242 ON_WM_MEASUREITEM()
243 ON_WM_MEASUREITEM_REFLECT()
244 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, &OnToolTipText)
245 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, &OnToolTipText)
246 ON_WM_MOUSEMOVE()
247 ON_WM_LBUTTONUP()
248 ON_NOTIFY_REFLECT(LVN_BEGINDRAG, OnBeginDrag)
249 END_MESSAGE_MAP()
251 void CGitLogListBase::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
253 //if (m_nRowHeight>0)
254 lpMeasureItemStruct->itemHeight = CDPIAware::Instance().ScaleY(GetSafeHwnd(), 50);
257 int CGitLogListBase:: OnCreate(LPCREATESTRUCT lpCreateStruct)
259 PreSubclassWindow();
260 return __super::OnCreate(lpCreateStruct);
263 void CGitLogListBase::SetStyle()
265 SetExtendedStyle(LVS_EX_INFOTIP | LVS_EX_DOUBLEBUFFER | LVS_EX_SUBITEMIMAGES | LVS_EX_FULLROWSELECT);
268 void CGitLogListBase::PreSubclassWindow()
270 SetStyle();
271 // load the icons for the action columns
272 // m_Theme.Open(m_hWnd, L"ListView");
273 SetWindowTheme(m_hWnd, L"Explorer", nullptr);
274 __super::PreSubclassWindow();
277 CString CGitLogListBase::GetRebaseActionName(int action)
279 if (action & LOGACTIONS_REBASE_EDIT)
280 return MAKEINTRESOURCE(IDS_PATHACTIONS_EDIT);
281 if (action & LOGACTIONS_REBASE_SQUASH)
282 return MAKEINTRESOURCE(IDS_PATHACTIONS_SQUASH);
283 if (action & LOGACTIONS_REBASE_PICK)
284 return MAKEINTRESOURCE(IDS_PATHACTIONS_PICK);
285 if (action & LOGACTIONS_REBASE_SKIP)
286 return MAKEINTRESOURCE(IDS_PATHACTIONS_SKIP);
288 return MAKEINTRESOURCE(IDS_PATHACTIONS_UNKNOWN);
291 void CGitLogListBase::InsertGitColumn()
293 CString temp;
295 Init();
296 if (CRegDWORD(L"Software\\TortoiseGit\\LogFontForLogCtrl", FALSE))
298 CCommonAppUtils::CreateFontForLogs(GetSafeHwnd(), m_Font);
299 SetFont(&m_Font);
302 // use the default font, create a copy of it and
303 // change the copy to BOLD (leave the rest of the font
304 // the same)
305 LOGFONT lf = { 0 };
306 GetFont()->GetLogFont(&lf);
307 lf.lfWeight = FW_BOLD;
308 m_boldFont.CreateFontIndirect(&lf);
309 lf.lfWeight = FW_DONTCARE;
310 lf.lfItalic = TRUE;
311 m_FontItalics.CreateFontIndirect(&lf);
312 lf.lfWeight = FW_BOLD;
313 m_boldItalicsFont.CreateFontIndirect(&lf);
315 // only load properties if we have a repository
316 if (GitAdminDir::IsWorkingTreeOrBareRepo(g_Git.m_CurrentDir))
317 UpdateProjectProperties();
319 static UINT normal[] =
321 IDS_LOG_GRAPH,
322 IDS_LOG_REBASE,
323 IDS_LOG_ID,
324 IDS_LOG_HASH,
325 IDS_LOG_ACTIONS,
326 IDS_LOG_MESSAGE,
327 IDS_LOG_AUTHOR,
328 IDS_LOG_DATE,
329 IDS_LOG_EMAIL,
330 IDS_LOG_COMMIT_NAME,
331 IDS_LOG_COMMIT_EMAIL,
332 IDS_LOG_COMMIT_DATE,
333 IDS_LOG_BUGIDS,
334 IDS_LOG_SVNREV,
337 auto iconItemBorder = CDPIAware::Instance().ScaleX(GetSafeHwnd(), ICONITEMBORDER);
338 auto columnWidth = CDPIAware::Instance().ScaleX(GetSafeHwnd(), ICONITEMBORDER + 16 * 4);
339 static int with[] =
341 columnWidth,
342 columnWidth,
343 columnWidth,
344 columnWidth,
345 2 * iconItemBorder + GetSystemMetrics(SM_CXSMICON) * 5,
346 CDPIAware::Instance().ScaleX(GetSafeHwnd(), LOGLIST_MESSAGE_MIN),
347 columnWidth,
348 columnWidth,
349 columnWidth,
350 columnWidth,
351 columnWidth,
352 columnWidth,
353 columnWidth,
354 columnWidth,
356 m_dwDefaultColumns = GIT_LOG_GRAPH|GIT_LOG_ACTIONS|GIT_LOG_MESSAGE|GIT_LOG_AUTHOR|GIT_LOG_DATE;
358 DWORD hideColumns = 0;
359 if(this->m_IsRebaseReplaceGraph)
361 hideColumns |= GIT_LOG_GRAPH;
362 m_dwDefaultColumns |= GIT_LOG_REBASE;
364 else
365 hideColumns |= GIT_LOG_REBASE;
367 if(this->m_IsIDReplaceAction)
369 hideColumns |= GIT_LOG_ACTIONS;
370 m_dwDefaultColumns |= GIT_LOG_ID;
371 m_dwDefaultColumns |= GIT_LOG_HASH;
373 else
374 hideColumns |= GIT_LOG_ID;
375 if(this->m_bShowBugtraqColumn)
376 m_dwDefaultColumns |= GIT_LOGLIST_BUG;
377 else
378 hideColumns |= GIT_LOGLIST_BUG;
379 if (CTGitPath(g_Git.m_CurrentDir).HasGitSVNDir())
380 m_dwDefaultColumns |= GIT_LOGLIST_SVNREV;
381 else
382 hideColumns |= GIT_LOGLIST_SVNREV;
383 SetRedraw(false);
385 m_ColumnManager.SetNames(normal, _countof(normal));
386 constexpr int columnVersion = 6; // adjust when changing number/names/etc. of columns
387 m_ColumnManager.ReadSettings(m_dwDefaultColumns, hideColumns, m_ColumnRegKey + L"loglist", columnVersion, _countof(normal), with);
388 m_ColumnManager.SetRightAlign(LOGLIST_ID);
390 if (!(hideColumns & GIT_LOG_ACTIONS))
392 // Configure fake a imagelist for LogList with 1px width and height = GetSystemMetrics(SM_CYSMICON)
393 // to set the minimum item height: we draw icons in the actions column, but on High-DPI the
394 // display's font height may be less than small icon height.
395 ASSERT((GetStyle() & LVS_SHAREIMAGELISTS) == 0);
396 HIMAGELIST hImageList = ImageList_Create(1, GetSystemMetrics(SM_CYSMICON), 0, 1, 0);
397 ListView_SetImageList(GetSafeHwnd(), hImageList, LVSIL_SMALL);
400 SetRedraw(true);
403 void CGitLogListBase::FillBackGround(HDC hdc, DWORD_PTR Index, CRect &rect)
405 LVITEM rItem = { 0 };
406 rItem.mask = LVIF_STATE;
407 rItem.iItem = static_cast<int>(Index);
408 rItem.stateMask = LVIS_SELECTED | LVIS_FOCUSED;
409 GetItem(&rItem);
411 GitRevLoglist* pLogEntry = m_arShownList.SafeGetAt(Index);
412 HBRUSH brush = nullptr;
414 if (pLogEntry && !(rItem.state & LVIS_SELECTED))
416 const int action = pLogEntry->GetRebaseAction();
417 if (action & LOGACTIONS_REBASE_SQUASH)
418 brush = ::CreateSolidBrush(RGB(156,156,156));
419 else if (action & LOGACTIONS_REBASE_EDIT)
420 brush = ::CreateSolidBrush(RGB(200,200,128));
422 else if (!IsAppThemed())
424 if (rItem.state & LVIS_SELECTED)
426 if (::GetFocus() == m_hWnd)
427 brush = ::CreateSolidBrush(::GetSysColor(COLOR_HIGHLIGHT));
428 else
429 brush = ::CreateSolidBrush(::GetSysColor(COLOR_BTNFACE));
432 if (brush)
434 ::FillRect(hdc, &rect, brush);
435 ::DeleteObject(brush);
439 void DrawTrackingRoundRect(HDC hdc, CRect rect, HBRUSH brush, COLORREF darkColor)
441 POINT point = { 4, 4 };
442 CRect rt2 = rect;
443 rt2.DeflateRect(1, 1);
444 rt2.OffsetRect(2, 2);
446 HPEN nullPen = ::CreatePen(PS_NULL, 0, 0);
447 HPEN oldpen = static_cast<HPEN>(::SelectObject(hdc, nullPen));
448 HBRUSH darkBrush = static_cast<HBRUSH>(::CreateSolidBrush(darkColor));
449 HBRUSH oldbrush = static_cast<HBRUSH>(::SelectObject(hdc, darkBrush));
450 ::RoundRect(hdc, rt2.left, rt2.top, rt2.right, rt2.bottom, point.x, point.y);
452 ::SelectObject(hdc, brush);
453 rt2.OffsetRect(-2, -2);
454 ::RoundRect(hdc, rt2.left, rt2.top, rt2.right, rt2.bottom, point.x, point.y);
455 ::SelectObject(hdc, oldbrush);
456 ::SelectObject(hdc, oldpen);
457 ::DeleteObject(nullPen);
458 ::DeleteObject(darkBrush);
461 void DrawUpstream(HDC hdc, CRect rect, COLORREF color, int bold)
463 HPEN pen = ::CreatePen(PS_SOLID, bold, color);
464 HPEN oldpen = static_cast<HPEN>(::SelectObject(hdc, pen));
465 ::MoveToEx(hdc, rect.left + 2 + bold, rect.top + 2 - bold, nullptr);
466 ::LineTo(hdc, rect.left + 2 + bold, rect.bottom + 1 - bold);
467 ::MoveToEx(hdc, rect.left + 3, rect.top + 1, nullptr);
468 ::LineTo(hdc, rect.left, rect.top + 4);
469 ::MoveToEx(hdc, rect.left + 2 + bold, rect.top + 1, nullptr);
470 ::LineTo(hdc, rect.right + 1 + bold, rect.top + 4);
471 ::MoveToEx(hdc, rect.left + 1, rect.top + 2 + bold, nullptr);
472 ::LineTo(hdc, rect.right + 1 + bold, rect.top + 2 + bold);
473 ::SelectObject(hdc, oldpen);
474 ::DeleteObject(pen);
477 void CGitLogListBase::DrawTagBranchMessage(NMLVCUSTOMDRAW* pLVCD, const CRect& rect, INT_PTR index, const std::vector<REFLABEL>& refList)
479 GitRevLoglist* data = m_arShownList.SafeGetAt(index);
480 CRect rt=rect;
481 LVITEM rItem = { 0 };
482 rItem.mask = LVIF_STATE;
483 rItem.iItem = static_cast<int>(index);
484 rItem.stateMask = LVIS_SELECTED | LVIS_FOCUSED;
485 GetItem(&rItem);
487 CDC W_Dc;
488 W_Dc.Attach(pLVCD->nmcd.hdc);
490 CAutoThemeData hTheme;
491 if (IsAppThemed())
492 hTheme = OpenThemeData(m_hWnd, L"Explorer::ListView;ListView");
494 if (m_bTagsBranchesOnRightSide)
496 HFONT oldFont = static_cast<HFONT>(SelectObject(pLVCD->nmcd.hdc, GetStockObject(DEFAULT_GUI_FONT)));
497 SIZE oneSpaceSize;
498 GetTextExtentPoint32(pLVCD->nmcd.hdc, L" ", 1, &oneSpaceSize);
499 int borderWidth = 0;
500 if (hTheme)
501 GetThemeMetric(hTheme, pLVCD->nmcd.hdc, LVP_LISTITEM, LISS_NORMAL, TMT_BORDERSIZE, &borderWidth);
502 SelectObject(pLVCD->nmcd.hdc, oldFont);
503 rt.left += borderWidth + oneSpaceSize.cx;
505 else
507 SIZE oneSpaceSize;
508 GetTextExtentPoint32(pLVCD->nmcd.hdc, L" ", 1, &oneSpaceSize);
509 DrawTagBranch(pLVCD->nmcd.hdc, W_Dc, hTheme, rect, rt, rItem, data, refList);
510 rt.left += oneSpaceSize.cx;
513 CString msg = MessageDisplayStr(data);
514 const int action = data->GetRebaseAction();
515 const bool skip = !!(action & (LOGACTIONS_REBASE_DONE | LOGACTIONS_REBASE_SKIP));
516 std::vector<CHARRANGE> ranges;
517 auto filter{ m_LogFilter.load() };
518 if ((filter->GetSelectedFilters() & (LOGFILTER_SUBJECT | (m_bFullCommitMessageOnLogLine ? LOGFILTER_MESSAGES : 0))) && filter->IsFilterActive())
519 filter->GetMatchRanges(ranges, msg, 0);
520 if (hTheme)
522 int txtState = LISS_NORMAL;
523 if (rItem.state & LVIS_SELECTED)
524 txtState = LISS_SELECTED;
526 if (!ranges.empty())
527 DrawListItemWithMatchesRect(pLVCD, ranges, rt, msg, m_Colors, hTheme, txtState);
528 else
530 DTTOPTS opts = { 0 };
531 opts.dwSize = sizeof(opts);
532 opts.crText = skip ? RGB(128, 128, 128) : CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor : GetSysColor(COLOR_WINDOWTEXT);
533 opts.dwFlags = DTT_TEXTCOLOR;
534 DrawThemeTextEx(hTheme, pLVCD->nmcd.hdc, LVP_LISTITEM, txtState, msg, -1, DT_NOPREFIX | DT_LEFT | DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS, &rt, &opts);
537 else
539 if ((rItem.state & LVIS_SELECTED) && ::GetFocus() == m_hWnd)
540 pLVCD->clrText = skip ? RGB(128, 128, 128) : ::GetSysColor(COLOR_HIGHLIGHTTEXT);
541 else
542 pLVCD->clrText = skip ? RGB(128, 128, 128) : CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor : GetSysColor(COLOR_WINDOWTEXT);
543 if (!ranges.empty())
544 DrawListItemWithMatchesRect(pLVCD, ranges, rt, msg, m_Colors);
545 else
547 COLORREF clrOld = ::SetTextColor(pLVCD->nmcd.hdc, pLVCD->clrText);
548 ::DrawText(pLVCD->nmcd.hdc, msg, msg.GetLength(), &rt, DT_NOPREFIX | DT_LEFT | DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS);
549 ::SetTextColor(pLVCD->nmcd.hdc, clrOld);
553 if (m_bTagsBranchesOnRightSide)
555 SIZE oneSpaceSize;
556 GetTextExtentPoint32(pLVCD->nmcd.hdc, L" ", 1, &oneSpaceSize);
558 SIZE size;
559 GetTextExtentPoint32(pLVCD->nmcd.hdc, msg, msg.GetLength(), &size);
561 rt.left += oneSpaceSize.cx + size.cx;
563 DrawTagBranch(pLVCD->nmcd.hdc, W_Dc, hTheme, rect, rt, rItem, data, refList);
566 W_Dc.Detach();
569 void CGitLogListBase::DrawTagBranch(HDC hdc, CDC& W_Dc, HTHEME hTheme, const CRect& rect, CRect& rt, LVITEM& rItem, GitRevLoglist* data, const std::vector<REFLABEL>& refList)
571 for (unsigned int i = 0; i < refList.size(); ++i)
573 CString shortname = !refList[i].simplifiedName.IsEmpty() ? refList[i].simplifiedName : refList[i].name;
574 HBRUSH brush = 0;
575 COLORREF colRef = refList[i].color;
576 const bool singleRemote = refList[i].singleRemote;
577 const bool hasTracking = refList[i].hasTracking;
578 CGit::REF_TYPE refType = refList[i].refType;
580 //When row selected, ajust label color
581 if (!IsAppThemed())
583 if (rItem.state & LVIS_SELECTED)
584 colRef = CColors::MixColors(colRef, ::GetSysColor(COLOR_HIGHLIGHT), 150);
587 brush = ::CreateSolidBrush(colRef);
589 if (!shortname.IsEmpty() && (rt.left < rect.right))
591 SIZE size = { 0 };
592 GetTextExtentPoint32(hdc, shortname, shortname.GetLength(), &size);
594 rt.SetRect(rt.left, rt.top, rt.left + size.cx, rt.bottom);
595 rt.right += 8;
597 int textpos = DT_CENTER;
599 if (rt.right > rect.right)
601 rt.right = rect.right;
602 textpos = 0;
605 CRect textRect = rt;
607 if (singleRemote)
609 rt.right += 5;
610 textRect.OffsetRect(5, 0);
613 if (hasTracking)
614 DrawTrackingRoundRect(hdc, rt, brush, m_Colors.Darken(colRef, 100));
615 else
617 //Fill interior of ref label
618 ::FillRect(hdc, &rt, brush);
621 //Draw edge of label
622 CRect rectEdge = rt;
624 if (!hasTracking)
626 W_Dc.Draw3dRect(rectEdge, m_Colors.Lighten(colRef, 100), m_Colors.Darken(colRef, 100));
627 rectEdge.DeflateRect(1, 1);
628 W_Dc.Draw3dRect(rectEdge, m_Colors.Lighten(colRef, 50), m_Colors.Darken(colRef, 50));
631 if (refType == CGit::REF_TYPE::ANNOTATED_TAG)
633 rt.right += 8;
634 POINT trianglept[3] = { { rt.right - 8, rt.top }, { rt.right, (rt.top + rt.bottom) / 2 }, { rt.right - 8, rt.bottom } };
635 HRGN hrgn = ::CreatePolygonRgn(trianglept, 3, ALTERNATE);
636 ::FillRgn(hdc, hrgn, brush);
637 ::DeleteObject(hrgn);
638 ::MoveToEx(hdc, trianglept[0].x - 1, trianglept[0].y, nullptr);
639 HPEN pen;
640 HPEN oldpen = static_cast<HPEN>(SelectObject(hdc, pen = ::CreatePen(PS_SOLID, 2, m_Colors.Lighten(colRef, 50))));
641 ::LineTo(hdc, trianglept[1].x - 1, trianglept[1].y - 1);
642 ::DeleteObject(pen);
643 SelectObject(hdc, pen = ::CreatePen(PS_SOLID, 2, m_Colors.Darken(colRef, 50)));
644 ::LineTo(hdc, trianglept[2].x - 1, trianglept[2].y - 1);
645 ::DeleteObject(pen);
646 SelectObject(hdc, pen = ::CreatePen(PS_SOLID, 2, colRef));
647 ::MoveToEx(hdc, trianglept[0].x - 1, trianglept[2].y - 3, nullptr);
648 ::LineTo(hdc, trianglept[0].x - 1, trianglept[0].y);
649 ::DeleteObject(pen);
650 SelectObject(hdc, oldpen);
653 //Draw text inside label
654 const bool customColor = (colRef & 0xff) * 30 + ((colRef >> 8) & 0xff) * 59 + ((colRef >> 16) & 0xff) * 11 <= 12800; // check if dark background
655 if (!customColor && IsAppThemed())
657 int txtState = LISS_NORMAL;
658 if (rItem.state & LVIS_SELECTED)
659 txtState = LISS_SELECTED;
661 DTTOPTS opts = { 0 };
662 opts.dwSize = sizeof(opts);
663 opts.crText = GetSysColor(COLOR_WINDOWTEXT);
664 opts.dwFlags = DTT_TEXTCOLOR;
665 DrawThemeTextEx(hTheme, hdc, LVP_LISTITEM, txtState, shortname, -1, textpos | DT_NOPREFIX | DT_SINGLELINE | DT_VCENTER, &textRect, &opts);
667 else
669 W_Dc.SetBkMode(TRANSPARENT);
670 if (customColor || (rItem.state & LVIS_SELECTED))
672 COLORREF clrNew = ::GetSysColor(COLOR_HIGHLIGHTTEXT);
673 COLORREF clrOld = ::SetTextColor(hdc,clrNew);
674 ::DrawText(hdc, shortname, shortname.GetLength(), &textRect, textpos | DT_NOPREFIX | DT_SINGLELINE | DT_VCENTER);
675 ::SetTextColor(hdc,clrOld);
677 else
679 COLORREF clrOld = ::SetTextColor(hdc, CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor : GetSysColor(COLOR_WINDOWTEXT));
680 ::DrawText(hdc, shortname, shortname.GetLength(), &textRect, textpos | DT_NOPREFIX | DT_SINGLELINE | DT_VCENTER);
681 ::SetTextColor(hdc, clrOld);
685 if (singleRemote)
687 COLORREF color = ::GetSysColor(customColor ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT);
688 const int bold = data->m_CommitHash == m_HeadHash ? 2 : 1;
689 CRect newRect;
690 newRect.SetRect(rt.left + 2, rt.top + 4, rt.left + 6, rt.bottom - 4);
691 DrawUpstream(hdc, newRect, color, bold);
694 if (!refList[i].fullName.IsEmpty())
695 m_RefLabelPosMap[refList[i].fullName] = rt;
697 rt.left = rt.right + 1;
699 if (brush)
700 ::DeleteObject(brush);
702 rt.right = rect.right;
705 Gdiplus::Color GetGdiColor(COLORREF col)
707 return Gdiplus::Color(GetRValue(col),GetGValue(col),GetBValue(col));
709 void CGitLogListBase::paintGraphLane(HDC hdc, int laneHeight, Lanes::LaneType type, bool rolledUp, int x1, int x2,
710 const COLORREF& col,const COLORREF& activeColor, int top
713 const int h = laneHeight / 2;
714 const int m = (x1 + x2) / 2;
715 const int r = (x2 - x1) * m_NodeSize / 30;
716 const int d = 2 * r;
718 #define P_CENTER m , h+top
719 #define P_CENTER_0 m+r, h+top
720 #define P_CENTER_90 m, h-r+top
721 #define P_CENTER_180 m-r, h+top
722 #define P_CENTER_270 m, h+r+top
723 #define P_0 x2, h+top
724 #define P_90 m , 0+top-1
725 #define P_180 x1, h+top
726 #define P_270 m , 2 * h+top +1
727 #define R_CENTER m - r, h - r+top, d, d
730 #define DELTA_UR_B 2*(x1 - m), 2*h +top
731 #define DELTA_UR_E 0*16, 90*16 +top // -,
733 #define DELTA_DR_B 2*(x1 - m), 2*-h +top
734 #define DELTA_DR_E 270*16, 90*16 +top // -'
736 #define DELTA_UL_B 2*(x2 - m), 2*h +top
737 #define DELTA_UL_E 90*16, 90*16 +top // ,-
739 #define DELTA_DL_B 2*(x2 - m),2*-h +top
740 #define DELTA_DL_E 180*16, 90*16 // '-
742 #define CENTER_UR x1, 2*h, 225
743 #define CENTER_DR x1, 0 , 135
744 #define CENTER_UL x2, 2*h, 315
745 #define CENTER_DL x2, 0 , 45
748 Gdiplus::Graphics graphics( hdc );
750 graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
752 // arc
753 switch (type) {
754 case Lanes::LaneType::JOIN:
755 case Lanes::LaneType::JOIN_R:
756 case Lanes::LaneType::HEAD:
757 case Lanes::LaneType::HEAD_R:
759 Gdiplus::LinearGradientBrush gradient(
760 Gdiplus::Point(x1-2, h+top-2),
761 Gdiplus::Point(P_270),
762 GetGdiColor(activeColor),GetGdiColor(col));
765 Gdiplus::Pen mypen(&gradient, static_cast<Gdiplus::REAL>(m_LineWidth));
766 //Gdiplus::Pen mypen(Gdiplus::Color(0,0,0),2);
768 //graphics.DrawRectangle(&mypen,x1-(x2-x1)/2,top+h, x2-x1,laneHeight);
769 graphics.DrawArc(&mypen,x1-(x2-x1)/2-1,top+h-1, x2-x1,laneHeight,270,90);
770 //graphics.DrawLine(&mypen,x1-1,h+top,P_270);
772 break;
774 case Lanes::LaneType::JOIN_L:
776 Gdiplus::LinearGradientBrush gradient(
777 Gdiplus::Point(P_270),
778 Gdiplus::Point(x2+1, h+top-1),
779 GetGdiColor(col),GetGdiColor(activeColor));
782 Gdiplus::Pen mypen(&gradient, static_cast<Gdiplus::REAL>(m_LineWidth));
783 //Gdiplus::Pen mypen(Gdiplus::Color(0,0,0),2);
785 //graphics.DrawRectangle(&mypen,x1-(x2-x1)/2,top+h, x2-x1,laneHeight);
786 graphics.DrawArc(&mypen,x1+(x2-x1)/2,top+h-1, x2-x1,laneHeight,180,90);
787 //graphics.DrawLine(&mypen,x1-1,h+top,P_270);
790 break;
792 case Lanes::LaneType::TAIL:
793 case Lanes::LaneType::TAIL_R:
795 Gdiplus::LinearGradientBrush gradient(
796 Gdiplus::Point(x1-2, h+top-2),
797 Gdiplus::Point(P_90),
798 GetGdiColor(activeColor),GetGdiColor(col));
800 Gdiplus::Pen mypen(&gradient, static_cast<Gdiplus::REAL>(m_LineWidth));
802 graphics.DrawArc(&mypen,x1-(x2-x1)/2-1,top-h-1, x2-x1,laneHeight,0,90);
804 #if 0
805 QConicalGradient gradient(CENTER_DR);
806 gradient.setColorAt(0.375, activeCol);
807 gradient.setColorAt(0.625, col);
808 myPen.setBrush(gradient);
809 p->setPen(myPen);
810 p->drawArc(P_CENTER, DELTA_DR);
811 #endif
812 break;
814 default:
815 break;
819 //static QPen myPen(Qt::black, 2); // fast path here
820 CPen pen;
821 pen.CreatePen(PS_SOLID,2,col);
822 //myPen.setColor(col);
823 HPEN oldpen = static_cast<HPEN>(::SelectObject(hdc, pen));
825 Gdiplus::Pen myPen(GetGdiColor(col), static_cast<Gdiplus::REAL>(m_LineWidth));
827 graphics.SetSmoothingMode(Gdiplus::SmoothingModeNone);
829 //p->setPen(myPen);
831 // vertical line
832 switch (type) {
833 case Lanes::LaneType::ACTIVE:
834 case Lanes::LaneType::MERGE_FORK:
835 case Lanes::LaneType::MERGE_FORK_R:
836 case Lanes::LaneType::MERGE_FORK_L:
837 if (rolledUp)
839 graphics.DrawLine(&myPen, P_90, P_CENTER_90);
840 graphics.DrawLine(&myPen, P_CENTER_270, P_270);
841 break;
843 [[fallthrough]];
844 case Lanes::LaneType::NOT_ACTIVE:
845 case Lanes::LaneType::JOIN:
846 case Lanes::LaneType::JOIN_R:
847 case Lanes::LaneType::JOIN_L:
848 case Lanes::LaneType::CROSS:
849 //DrawLine(hdc,P_90,P_270);
850 graphics.DrawLine(&myPen,P_90,P_270);
851 //p->drawLine(P_90, P_270);
852 break;
853 case Lanes::LaneType::BRANCH:
854 if (rolledUp)
856 graphics.DrawLine(&myPen, P_CENTER_270, P_270);
857 break;
859 [[fallthrough]];
860 case Lanes::LaneType::HEAD_L:
861 //DrawLine(hdc,P_CENTER,P_270);
862 graphics.DrawLine(&myPen,P_CENTER,P_270);
863 //p->drawLine(P_CENTER, P_270);
864 break;
865 case Lanes::LaneType::INITIAL:
866 case Lanes::LaneType::MERGE_FORK_L_INITIAL:
867 case Lanes::LaneType::BOUNDARY:
868 case Lanes::LaneType::BOUNDARY_C:
869 case Lanes::LaneType::BOUNDARY_R:
870 case Lanes::LaneType::BOUNDARY_L:
871 if (rolledUp)
873 graphics.DrawLine(&myPen, P_90, P_CENTER_90);
874 break;
876 [[fallthrough]];
877 case Lanes::LaneType::TAIL_L:
878 //DrawLine(hdc,P_90, P_CENTER);
879 graphics.DrawLine(&myPen,P_90,P_CENTER);
880 //p->drawLine(P_90, P_CENTER);
881 break;
882 default:
883 break;
886 myPen.SetColor(GetGdiColor(activeColor));
888 // horizontal line
889 switch (type) {
890 case Lanes::LaneType::MERGE_FORK:
891 case Lanes::LaneType::BOUNDARY_C:
892 if (rolledUp)
894 graphics.DrawLine(&myPen, P_180, P_CENTER_180);
895 graphics.DrawLine(&myPen, P_CENTER_0, P_0);
896 break;
898 [[fallthrough]];
899 case Lanes::LaneType::JOIN:
900 case Lanes::LaneType::HEAD:
901 case Lanes::LaneType::TAIL:
902 case Lanes::LaneType::CROSS:
903 case Lanes::LaneType::CROSS_EMPTY:
904 //DrawLine(hdc,P_180,P_0);
905 graphics.DrawLine(&myPen,P_180,P_0);
906 //p->drawLine(P_180, P_0);
907 break;
908 case Lanes::LaneType::MERGE_FORK_R:
909 case Lanes::LaneType::BOUNDARY_R:
910 //DrawLine(hdc,P_180,P_CENTER);
911 if (rolledUp)
912 graphics.DrawLine(&myPen, P_180, P_CENTER_180);
913 else
914 graphics.DrawLine(&myPen, P_180, P_CENTER);
915 //p->drawLine(P_180, P_CENTER);
916 break;
917 case Lanes::LaneType::MERGE_FORK_L:
918 case Lanes::LaneType::MERGE_FORK_L_INITIAL:
919 case Lanes::LaneType::BOUNDARY_L:
920 if (rolledUp)
922 graphics.DrawLine(&myPen, P_CENTER_0, P_0);
923 break;
925 [[fallthrough]];
926 case Lanes::LaneType::HEAD_L:
927 case Lanes::LaneType::TAIL_L:
928 //DrawLine(hdc,P_CENTER,P_0);
929 graphics.DrawLine(&myPen,P_CENTER,P_0);
930 //p->drawLine(P_CENTER, P_0);
931 break;
932 default:
933 break;
936 graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
938 CBrush brush;
939 brush.CreateSolidBrush(col);
940 HBRUSH oldbrush = static_cast<HBRUSH>(::SelectObject(hdc, brush));
942 Gdiplus::SolidBrush myBrush(GetGdiColor(col));
943 Gdiplus::Pen myPen1(GetGdiColor(col), 1);
944 // center symbol, e.g. rect or ellipse
945 switch (type) {
946 case Lanes::LaneType::ACTIVE:
947 case Lanes::LaneType::INITIAL:
948 case Lanes::LaneType::BRANCH:
950 //p->setPen(Qt::NoPen);
951 //p->setBrush(col);
952 graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
953 if (rolledUp)
954 graphics.DrawEllipse(&myPen1, R_CENTER);
955 else
956 graphics.FillEllipse(&myBrush, R_CENTER);
957 //p->drawEllipse(R_CENTER);
958 break;
959 case Lanes::LaneType::MERGE_FORK:
960 case Lanes::LaneType::MERGE_FORK_R:
961 case Lanes::LaneType::MERGE_FORK_L:
962 case Lanes::LaneType::MERGE_FORK_L_INITIAL:
963 //p->setPen(Qt::NoPen);
964 //p->setBrush(col);
965 //p->drawRect(R_CENTER);
966 graphics.SetSmoothingMode(Gdiplus::SmoothingModeNone);
967 if (rolledUp)
968 graphics.DrawRectangle(&myPen1, R_CENTER);
969 else
970 graphics.FillRectangle(&myBrush, R_CENTER);
971 break;
972 case Lanes::LaneType::UNAPPLIED:
973 // Red minus sign
974 //p->setPen(Qt::NoPen);
975 //p->setBrush(Qt::red);
976 //p->drawRect(m - r, h - 1, d, 2);
977 graphics.SetSmoothingMode(Gdiplus::SmoothingModeNone);
978 graphics.FillRectangle(&myBrush,m-r,h-1,d,2);
979 break;
980 case Lanes::LaneType::APPLIED:
981 // Green plus sign
982 //p->setPen(Qt::NoPen);
983 //p->setBrush(DARK_GREEN);
984 //p->drawRect(m - r, h - 1, d, 2);
985 //p->drawRect(m - 1, h - r, 2, d);
986 graphics.SetSmoothingMode(Gdiplus::SmoothingModeNone);
987 graphics.FillRectangle(&myBrush,m-r,h-1,d,2);
988 graphics.FillRectangle(&myBrush,m-1,h-r,2,d);
989 break;
990 case Lanes::LaneType::BOUNDARY:
991 //p->setBrush(back);
992 //p->drawEllipse(R_CENTER);
993 graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
994 graphics.DrawEllipse(&myPen, R_CENTER);
995 break;
996 case Lanes::LaneType::BOUNDARY_C:
997 case Lanes::LaneType::BOUNDARY_R:
998 case Lanes::LaneType::BOUNDARY_L:
999 //p->setBrush(back);
1000 //p->drawRect(R_CENTER);
1001 graphics.SetSmoothingMode(Gdiplus::SmoothingModeNone);
1002 if (rolledUp)
1003 graphics.DrawRectangle(&myPen1, R_CENTER);
1004 else
1005 graphics.FillRectangle(&myBrush, R_CENTER);
1006 break;
1007 default:
1008 break;
1011 ::SelectObject(hdc,oldpen);
1012 ::SelectObject(hdc,oldbrush);
1013 #undef P_CENTER
1014 #undef P_CENTER_0
1015 #undef P_CENTER_90
1016 #undef P_CENTER_180
1017 #undef P_CENTER_270
1018 #undef P_0
1019 #undef P_90
1020 #undef P_180
1021 #undef P_270
1022 #undef R_CENTER
1025 void CGitLogListBase::DrawGraph(HDC hdc,CRect &rect,INT_PTR index)
1027 // TODO: unfinished
1028 // return;
1029 GitRevLoglist* data = m_arShownList.SafeGetAt(index);
1030 if(data->m_CommitHash.IsEmpty())
1031 return;
1033 CRect rt=rect;
1034 LVITEM rItem = { 0 };
1035 rItem.mask = LVIF_STATE;
1036 rItem.iItem = static_cast<int>(index);
1037 rItem.stateMask = LVIS_SELECTED | LVIS_FOCUSED;
1038 GetItem(&rItem);
1040 // p->translate(QPoint(opt.rect.left(), opt.rect.top()));
1042 if (data->m_Lanes.empty())
1043 m_logEntries.setLane(data->m_CommitHash, m_ShowMask & CGit::LOG_INFO_FIRST_PARENT);
1045 std::vector<Lanes::LaneType>& lanes = data->m_Lanes;
1046 const size_t laneNum = lanes.size();
1047 UINT activeLane = 0;
1048 for (UINT i = 0; i < laneNum; ++i)
1049 if (Lanes::isMerge(lanes[i])) {
1050 activeLane = i;
1051 break;
1054 int x2 = 0;
1055 const int maxWidth = rect.Width();
1056 const int lw = 3 * rect.Height() / 4; // laneWidth()
1058 COLORREF activeColor = CTheme::Instance().GetThemeColor(m_Colors.GetColor((CColors::Colors)(CColors::BranchLine1 + (activeLane % Lanes::COLORS_NUM))), true);
1060 for (unsigned int i = 0; i < laneNum && x2 < maxWidth; ++i)
1062 int x1 = x2;
1063 x2 += lw;
1065 Lanes::LaneType ln = lanes[i];
1066 if (ln == Lanes::LaneType::EMPTY)
1067 continue;
1069 COLORREF color = i == activeLane ? activeColor : CTheme::Instance().GetThemeColor(m_Colors.GetColor((CColors::Colors)(CColors::BranchLine1 + (i % Lanes::COLORS_NUM))), true);
1070 paintGraphLane(hdc, rect.Height(), ln, data->m_RolledUp, x1 + rect.left, x2 + rect.left, color,activeColor, rect.top);
1073 #if 0
1074 for (UINT i = 0; i < laneNum && x2 < maxWidth; ++i) {
1075 int x1 = x2;
1076 x2 += lw;
1078 int ln = lanes[i];
1079 if (ln == Lanes::EMPTY)
1080 continue;
1082 UINT col = ( Lanes:: isHead(ln) ||Lanes:: isTail(ln) || Lanes::isJoin(ln)
1083 || ln ==Lanes:: CROSS_EMPTY) ? activeLane : i;
1085 if (ln == Lanes::CROSS)
1087 paintGraphLane(hdc, rect.Height(),Lanes::NOT_ACTIVE, x1, x2, m_LineColors[col % Lanes::COLORS_NUM],rect.top);
1088 paintGraphLane(hdc, rect.Height(),Lanes::CROSS, x1, x2, m_LineColors[activeLane % Lanes::COLORS_NUM],rect.top);
1090 else
1091 paintGraphLane(hdc, rect.Height(),ln, x1, x2, m_LineColors[col % Lanes::COLORS_NUM],rect.top);
1093 #endif
1096 void CGitLogListBase::OnNMCustomdrawLoglist(NMHDR *pNMHDR, LRESULT *pResult)
1098 NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
1099 // Take the default processing unless we set this to something else below.
1100 *pResult = CDRF_DODEFAULT;
1102 if (m_bNoDispUpdates)
1103 return;
1105 switch (pLVCD->nmcd.dwDrawStage)
1107 case CDDS_PREPAINT:
1109 *pResult = CDRF_NOTIFYITEMDRAW;
1110 return;
1112 break;
1113 case CDDS_ITEMPREPAINT:
1115 // This is the prepaint stage for an item. Here's where we set the
1116 // item's text color.
1118 // Tell Windows to send draw notifications for each subitem.
1119 *pResult = CDRF_NOTIFYSUBITEMDRAW;
1121 COLORREF crText = CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor : GetSysColor(COLOR_WINDOWTEXT);
1123 if (m_arShownList.size() > pLVCD->nmcd.dwItemSpec)
1125 GitRevLoglist* data = m_arShownList.SafeGetAt(pLVCD->nmcd.dwItemSpec);
1126 if (data)
1128 HGDIOBJ hGdiObj = nullptr;
1129 const int action = data->GetRebaseAction();
1130 if (action & (LOGACTIONS_REBASE_DONE | LOGACTIONS_REBASE_SKIP))
1131 crText = RGB(128, 128, 128);
1133 if (action & LOGACTIONS_REBASE_SQUASH)
1134 pLVCD->clrTextBk = RGB(156, 156, 156);
1135 else if (action & LOGACTIONS_REBASE_EDIT)
1136 pLVCD->clrTextBk = CTheme::Instance().GetThemeColor(RGB(200, 200, 128));
1137 else
1138 pLVCD->clrTextBk = CTheme::Instance().IsDarkTheme() ? CTheme::darkBkColor : GetSysColor(COLOR_WINDOW);
1140 if (action & LOGACTIONS_REBASE_CURRENT)
1141 hGdiObj = m_boldFont.GetSafeHandle();
1143 const BOOL isHeadHash = data->m_CommitHash == m_HeadHash && m_bNoHightlightHead == FALSE;
1144 const BOOL isHighlight = data->m_CommitHash == m_highlight && !m_highlight.IsEmpty();
1145 if (isHeadHash && isHighlight)
1146 hGdiObj = m_boldItalicsFont.GetSafeHandle();
1147 else if (isHeadHash)
1148 hGdiObj = m_boldFont.GetSafeHandle();
1149 else if (isHighlight)
1150 hGdiObj = m_FontItalics.GetSafeHandle();
1152 if (hGdiObj)
1154 SelectObject(pLVCD->nmcd.hdc, hGdiObj);
1155 *pResult = CDRF_NOTIFYSUBITEMDRAW | CDRF_NEWFONT;
1158 // if ((data->childStackDepth)||(m_mergedRevs.find(data->Rev) != m_mergedRevs.end()))
1159 // crText = GetSysColor(COLOR_GRAYTEXT);
1161 if (data->m_CommitHash.IsEmpty())
1163 //crText = GetSysColor(RGB(200,200,0));
1164 //SelectObject(pLVCD->nmcd.hdc, m_boldFont);
1165 // We changed the font, so we're returning CDRF_NEWFONT. This
1166 // tells the control to recalculate the extent of the text.
1167 *pResult = CDRF_NOTIFYSUBITEMDRAW | CDRF_NEWFONT;
1171 // Store the color back in the NMLVCUSTOMDRAW struct.
1172 pLVCD->clrText = crText;
1173 return;
1175 break;
1177 case CDDS_ITEMPREPAINT | CDDS_ITEM | CDDS_SUBITEM:
1179 switch (pLVCD->iSubItem)
1181 case LOGLIST_GRAPH:
1182 if ((m_ShowFilter & FILTERSHOW_MERGEPOINTS) && !m_LogFilter.load()->IsFilterActive())
1184 if (m_arShownList.size() > pLVCD->nmcd.dwItemSpec && !this->m_IsRebaseReplaceGraph)
1186 CRect rect;
1187 GetSubItemRect(static_cast<int>(pLVCD->nmcd.dwItemSpec), pLVCD->iSubItem, LVIR_LABEL, rect);
1189 //TRACE(L"A Graphic left %d right %d\r\n", rect.left, rect.right);
1190 FillBackGround(pLVCD->nmcd.hdc, pLVCD->nmcd.dwItemSpec,rect);
1192 GitRevLoglist* data = m_arShownList.SafeGetAt(pLVCD->nmcd.dwItemSpec);
1193 if( !data ->m_CommitHash.IsEmpty())
1194 DrawGraph(pLVCD->nmcd.hdc,rect,pLVCD->nmcd.dwItemSpec);
1196 *pResult = CDRF_SKIPDEFAULT;
1197 return;
1200 break;
1202 case LOGLIST_MESSAGE:
1203 // If the top index of list is changed, the position map of reference label is outdated.
1204 if (m_OldTopIndex != GetTopIndex())
1206 m_OldTopIndex = GetTopIndex();
1207 m_RefLabelPosMap.clear();
1210 if (m_arShownList.size() > pLVCD->nmcd.dwItemSpec)
1212 GitRevLoglist* data = m_arShownList.SafeGetAt(pLVCD->nmcd.dwItemSpec);
1214 auto hashMapSharedPtr{ m_HashMap.load() };
1215 const auto& hashMap = *hashMapSharedPtr;
1216 if ((hashMap.find(data->m_CommitHash) != hashMap.cend() || m_submoduleInfo.AnyMatches(data->m_CommitHash)) && !(data->GetRebaseAction() & LOGACTIONS_REBASE_DONE))
1218 CRect rect;
1219 GetSubItemRect(static_cast<int>(pLVCD->nmcd.dwItemSpec), pLVCD->iSubItem, LVIR_BOUNDS, rect);
1221 FillBackGround(pLVCD->nmcd.hdc, pLVCD->nmcd.dwItemSpec, rect);
1223 std::vector<REFLABEL> refsToShow;
1224 STRING_VECTOR remoteTrackingList;
1225 std::vector<CString>::const_iterator refListIt;
1226 std::vector<CString>::const_iterator refListItEnd;
1227 auto commitRefsIt = hashMap.find(data->m_CommitHash);
1228 if (commitRefsIt != hashMap.cend())
1230 refListIt = (*commitRefsIt).second.cbegin();
1231 refListItEnd = (*commitRefsIt).second.cend();
1233 for (; refListIt != refListItEnd; ++refListIt)
1235 REFLABEL refLabel;
1236 refLabel.color = RGB(255, 255, 255);
1237 refLabel.singleRemote = false;
1238 refLabel.hasTracking = false;
1239 refLabel.sameName = false;
1240 refLabel.name = CGit::GetShortName(*refListIt, &refLabel.refType);
1241 refLabel.fullName = *refListIt;
1243 switch (refLabel.refType)
1245 case CGit::REF_TYPE::LOCAL_BRANCH:
1247 if (!(m_ShowRefMask & LOGLIST_SHOWLOCALBRANCHES))
1248 continue;
1249 if (refLabel.name == m_CurrentBranch)
1250 refLabel.color = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::CurrentBranch), true);
1251 else
1252 refLabel.color = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::LocalBranch), true);
1254 std::pair<CString, CString> trackingEntry = m_TrackingMap[refLabel.name];
1255 CString pullRemote = trackingEntry.first;
1256 CString pullBranch = trackingEntry.second;
1257 if (!pullRemote.IsEmpty() && !pullBranch.IsEmpty())
1259 CString defaultUpstream;
1260 defaultUpstream.Format(L"refs/remotes/%s/%s", static_cast<LPCWSTR>(pullRemote), static_cast<LPCWSTR>(pullBranch));
1261 refLabel.hasTracking = true;
1262 if (m_ShowRefMask & LOGLIST_SHOWREMOTEBRANCHES)
1264 bool found = false;
1265 for (auto it2 = refListIt + 1; it2 != refListItEnd; ++it2)
1267 if (*it2 == defaultUpstream)
1269 found = true;
1270 break;
1274 if (found)
1276 const bool sameName = pullBranch == refLabel.name;
1277 refsToShow.push_back(refLabel);
1278 CGit::GetShortName(defaultUpstream, refLabel.name, L"refs/remotes/");
1279 refLabel.color = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::RemoteBranch), true);
1280 if (m_bSymbolizeRefNames)
1282 if (!m_SingleRemote.IsEmpty() && m_SingleRemote == pullRemote)
1284 refLabel.simplifiedName = L'/';
1285 if (sameName)
1286 refLabel.simplifiedName += L'≡';
1287 else
1288 refLabel.simplifiedName += pullBranch;
1289 refLabel.singleRemote = true;
1291 else if (sameName)
1292 refLabel.simplifiedName = pullRemote + L"/≡";
1293 refLabel.sameName = sameName;
1295 refLabel.fullName = defaultUpstream;
1296 refsToShow.push_back(refLabel);
1297 remoteTrackingList.push_back(defaultUpstream);
1298 continue;
1302 break;
1304 case CGit::REF_TYPE::REMOTE_BRANCH:
1306 if (!(m_ShowRefMask & LOGLIST_SHOWREMOTEBRANCHES))
1307 continue;
1308 bool found = false;
1309 for (size_t j = 0; j < remoteTrackingList.size(); ++j)
1311 if (remoteTrackingList[j] == *refListIt)
1313 found = true;
1314 break;
1317 if (found)
1318 continue;
1320 refLabel.color = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::RemoteBranch), true);
1321 if (m_bSymbolizeRefNames)
1323 if (!m_SingleRemote.IsEmpty() && CStringUtils::StartsWith(refLabel.name, m_SingleRemote + L"/"))
1325 refLabel.simplifiedName = L'/' + refLabel.name.Mid(m_SingleRemote.GetLength() + 1);
1326 refLabel.singleRemote = true;
1329 break;
1331 case CGit::REF_TYPE::ANNOTATED_TAG:
1332 [[fallthrough]];
1333 case CGit::REF_TYPE::TAG:
1334 if (!(m_ShowRefMask & LOGLIST_SHOWTAGS))
1335 continue;
1336 refLabel.color = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::Tag), true);
1337 break;
1339 case CGit::REF_TYPE::STASH:
1340 if (!(m_ShowRefMask & LOGLIST_SHOWSTASH))
1341 continue;
1342 refLabel.color = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::Stash), true);
1343 break;
1345 case CGit::REF_TYPE::BISECT_GOOD:
1346 [[fallthrough]];
1347 case CGit::REF_TYPE::BISECT_BAD:
1348 [[fallthrough]];
1349 case CGit::REF_TYPE::BISECT_SKIP:
1350 if (!(m_ShowRefMask & LOGLIST_SHOWBISECT))
1351 continue;
1352 refLabel.color = CTheme::Instance().GetThemeColor((refLabel.refType == CGit::REF_TYPE::BISECT_GOOD) ? m_Colors.GetColor(CColors::BisectGood) : ((refLabel.refType == CGit::REF_TYPE::BISECT_SKIP) ? m_Colors.GetColor(CColors::BisectSkip) : m_Colors.GetColor(CColors::BisectBad)), true);
1353 break;
1355 case CGit::REF_TYPE::NOTES:
1356 if (!(m_ShowRefMask & LOGLIST_SHOWOTHERREFS))
1357 continue;
1358 refLabel.color = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::NoteNode), true);
1359 break;
1361 default:
1362 if (!(m_ShowRefMask & LOGLIST_SHOWOTHERREFS))
1363 continue;
1364 refLabel.color = CTheme::Instance().GetThemeColor(m_Colors.GetColor(CColors::OtherRef), true);
1365 break;
1367 refsToShow.push_back(refLabel);
1370 const auto fnAddSuperProjectHash = [&](const CGitHash& hash, const CString& label) {
1371 if (hash.IsEmpty() || data->m_CommitHash != hash)
1372 return;
1374 REFLABEL refLabel;
1375 refLabel.color = CTheme::Instance().GetThemeColor(RGB(246, 153, 253), true);
1376 refLabel.singleRemote = false;
1377 refLabel.hasTracking = false;
1378 refLabel.sameName = false;
1379 refLabel.name = label;
1380 refLabel.fullName = "";
1381 refsToShow.push_back(refLabel);
1383 fnAddSuperProjectHash(m_submoduleInfo.superProjectHash, L"super-project-pointer");
1384 fnAddSuperProjectHash(m_submoduleInfo.mergeconflictMineHash, m_submoduleInfo.mineLabel);
1385 fnAddSuperProjectHash(m_submoduleInfo.mergeconflictTheirsHash, m_submoduleInfo.theirsLabel);
1387 if (refsToShow.empty())
1389 *pResult = CDRF_DODEFAULT;
1390 return;
1393 DrawTagBranchMessage(pLVCD, rect, pLVCD->nmcd.dwItemSpec, refsToShow);
1395 *pResult = CDRF_SKIPDEFAULT;
1396 return;
1398 else if (DrawListItemWithMatchesIfEnabled(m_LogFilter, LOGFILTER_SUBJECT | LOGFILTER_MESSAGES, pLVCD, pResult))//from here
1399 return;
1401 break;
1403 case LOGLIST_ACTION:
1405 if (m_IsIDReplaceAction || !m_ColumnManager.IsVisible(LOGLIST_ACTION))
1407 *pResult = CDRF_DODEFAULT;
1408 return;
1410 *pResult = CDRF_DODEFAULT;
1412 if (m_arShownList.size() <= pLVCD->nmcd.dwItemSpec)
1413 return;
1415 int nIcons = 0;
1416 const int iconwidth = ::GetSystemMetrics(SM_CXSMICON);
1417 const int iconheight = ::GetSystemMetrics(SM_CYSMICON);
1419 GitRevLoglist* pLogEntry = m_arShownList.SafeGetAt(pLVCD->nmcd.dwItemSpec);
1420 CRect rect;
1421 GetSubItemRect(static_cast<int>(pLVCD->nmcd.dwItemSpec), pLVCD->iSubItem, LVIR_BOUNDS, rect);
1422 //TRACE(L"Action left %d right %d\r\n", rect.left, rect.right);
1423 // Get the selected state of the
1424 // item being drawn.
1426 CMemDC myDC(*CDC::FromHandle(pLVCD->nmcd.hdc), rect);
1427 BitBlt(myDC.GetDC(), rect.left, rect.top, rect.Width(), rect.Height(), pLVCD->nmcd.hdc, rect.left, rect.top, SRCCOPY);
1429 // Fill the background if necessary
1430 FillBackGround(myDC.GetDC(), pLVCD->nmcd.dwItemSpec, rect);
1432 // Draw the icon(s) into the compatible DC
1433 const int action = pLogEntry->GetAction(this);
1434 auto iconItemBorder = CDPIAware::Instance().ScaleX(GetSafeHwnd(), ICONITEMBORDER);
1435 if (!pLogEntry->m_IsDiffFiles)
1437 ::DrawIconEx(myDC.GetDC(), rect.left + iconItemBorder, rect.top, m_hFetchIcon, iconwidth, iconheight, 0, nullptr, DI_NORMAL);
1438 *pResult = CDRF_SKIPDEFAULT;
1439 return;
1441 else if (pLogEntry->m_IsDiffFiles == 2)
1443 ::DrawIconEx(myDC.GetDC(), rect.left + iconItemBorder, rect.top, m_hErrorIcon, iconwidth, iconheight, 0, nullptr, DI_NORMAL);
1444 *pResult = CDRF_SKIPDEFAULT;
1445 return;
1448 if (action & CTGitPath::LOGACTIONS_MODIFIED)
1449 ::DrawIconEx(myDC.GetDC(), rect.left + iconItemBorder, rect.top, m_hModifiedIcon, iconwidth, iconheight, 0, nullptr, DI_NORMAL);
1450 ++nIcons;
1452 if (action & (CTGitPath::LOGACTIONS_ADDED | CTGitPath::LOGACTIONS_COPY))
1453 ::DrawIconEx(myDC.GetDC(), rect.left + nIcons * iconwidth + iconItemBorder, rect.top, m_hAddedIcon, iconwidth, iconheight, 0, nullptr, DI_NORMAL);
1454 ++nIcons;
1456 if (action & CTGitPath::LOGACTIONS_DELETED)
1457 ::DrawIconEx(myDC.GetDC(), rect.left + nIcons * iconwidth + iconItemBorder, rect.top, m_hDeletedIcon, iconwidth, iconheight, 0, nullptr, DI_NORMAL);
1458 ++nIcons;
1460 if (action & CTGitPath::LOGACTIONS_REPLACED)
1461 ::DrawIconEx(myDC.GetDC(), rect.left + nIcons * iconwidth + iconItemBorder, rect.top, m_hReplacedIcon, iconwidth, iconheight, 0, nullptr, DI_NORMAL);
1462 ++nIcons;
1464 if (action & CTGitPath::LOGACTIONS_UNMERGED)
1465 ::DrawIconEx(myDC.GetDC(), rect.left + nIcons * iconwidth + iconItemBorder, rect.top, m_hConflictedIcon, iconwidth, iconheight, 0, nullptr, DI_NORMAL);
1466 ++nIcons;
1468 *pResult = CDRF_SKIPDEFAULT;
1469 return;
1472 case LOGLIST_HASH:
1473 if (DrawListItemWithMatchesIfEnabled(m_LogFilter, LOGFILTER_REVS, pLVCD, pResult))
1474 return;
1475 break;
1477 case LOGLIST_AUTHOR:
1478 case LOGLIST_COMMIT_NAME:
1479 if (DrawListItemWithMatchesIfEnabled(m_LogFilter, LOGFILTER_AUTHORS, pLVCD, pResult))
1480 return;
1481 break;
1483 case LOGLIST_EMAIL:
1484 case LOGLIST_COMMIT_EMAIL:
1485 if (DrawListItemWithMatchesIfEnabled(m_LogFilter, LOGFILTER_EMAILS, pLVCD, pResult))
1486 return;
1487 break;
1489 case LOGLIST_BUG:
1490 if (DrawListItemWithMatchesIfEnabled(m_LogFilter, LOGFILTER_BUGID, pLVCD, pResult))
1491 return;
1492 break;
1495 break;
1497 *pResult = CDRF_DODEFAULT;
1500 CString FindSVNRev(const CString& msg)
1504 const std::wsregex_iterator end;
1505 std::wstring s { static_cast<LPCWSTR>(msg) };
1506 std::wregex regex1(L"^\\s*git-svn-id:\\s+(.*)\\@(\\d+)\\s([a-f\\d\\-]+)$");
1507 for (std::wsregex_iterator it(s.cbegin(), s.cend(), regex1); it != end; ++it)
1509 const std::wsmatch match = *it;
1510 if (match.size() == 4)
1512 ATLTRACE(L"matched rev: %s\n", std::wstring(match[2]).c_str());
1513 return std::wstring(match[2]).c_str();
1516 std::wregex regex2(L"^\\s*git-svn-id:\\s(\\d+)\\@([a-f\\d\\-]+)$");
1517 for (std::wsregex_iterator it(s.cbegin(), s.cend(), regex2); it != end; ++it)
1519 const std::wsmatch match = *it;
1520 if (match.size() == 3)
1522 ATLTRACE(L"matched rev: %s\n", std::wstring(match[1]).c_str());
1523 return std::wstring(match[1]).c_str();
1527 catch (std::exception&) {}
1529 return L"";
1532 CString CGitLogListBase::MessageDisplayStr(GitRev* pLogEntry)
1534 if (!m_bFullCommitMessageOnLogLine || pLogEntry->GetBody().IsEmpty())
1535 return pLogEntry->GetSubject();
1537 CString txt(pLogEntry->GetSubject());
1538 txt += L' ';
1539 txt += pLogEntry->GetBody();
1541 // Deal with CRLF
1542 txt.Replace(L'\n', L' ');
1543 txt.Replace(L'\r', L' ');
1545 return txt;
1548 // CGitLogListBase message handlers
1550 void CGitLogListBase::OnLvnGetdispinfoLoglist(NMHDR *pNMHDR, LRESULT *pResult)
1552 NMLVDISPINFO *pDispInfo = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);
1554 // Create a pointer to the item
1555 LV_ITEM* pItem = &(pDispInfo)->item;
1557 // Do the list need text information?
1558 if (!(pItem->mask & LVIF_TEXT))
1559 return;
1561 // By default, clear text buffer.
1562 lstrcpyn(pItem->pszText, L"", pItem->cchTextMax - 1);
1564 const bool bOutOfRange = pItem->iItem >= static_cast<int>(m_arShownList.size());
1566 *pResult = 0;
1567 if (m_bNoDispUpdates || bOutOfRange)
1568 return;
1570 // Which item number?
1571 GitRevLoglist* pLogEntry = m_arShownList.SafeGetAt(pItem->iItem);
1573 CString temp;
1574 if(m_IsOldFirst)
1575 temp.Format(L"%d", pItem->iItem + 1);
1576 else
1577 temp.Format(L"%zu", m_arShownList.size() - pItem->iItem);
1579 if (!pLogEntry)
1580 return;
1582 // Which column?
1583 switch (pItem->iSubItem)
1585 case LOGLIST_GRAPH: //Graphic
1586 break;
1587 case LOGLIST_REBASE:
1588 if (m_IsRebaseReplaceGraph)
1589 lstrcpyn(pItem->pszText, GetRebaseActionName(pLogEntry->GetRebaseAction() & LOGACTIONS_REBASE_MODE_MASK), pItem->cchTextMax - 1);
1590 break;
1591 case LOGLIST_ACTION: //action -- no text in the column
1592 break;
1593 case LOGLIST_HASH:
1594 lstrcpyn(pItem->pszText, pLogEntry->m_CommitHash.ToString(), pItem->cchTextMax - 1);
1595 break;
1596 case LOGLIST_ID:
1597 if (this->m_IsIDReplaceAction)
1598 lstrcpyn(pItem->pszText, temp, pItem->cchTextMax - 1);
1599 break;
1600 case LOGLIST_MESSAGE: //Message
1601 lstrcpyn(pItem->pszText, static_cast<LPCWSTR>(MessageDisplayStr(pLogEntry)), pItem->cchTextMax - 1);
1602 break;
1603 case LOGLIST_AUTHOR: //Author
1604 lstrcpyn(pItem->pszText, static_cast<LPCWSTR>(pLogEntry->GetAuthorName()), pItem->cchTextMax - 1);
1605 break;
1606 case LOGLIST_DATE: //Date
1607 if (!pLogEntry->m_CommitHash.IsEmpty())
1608 lstrcpyn(pItem->pszText,
1609 CLoglistUtils::FormatDateAndTime(pLogEntry->GetAuthorDate(), m_DateFormat, true, m_bRelativeTimes),
1610 pItem->cchTextMax - 1);
1611 break;
1613 case LOGLIST_EMAIL:
1614 lstrcpyn(pItem->pszText, static_cast<LPCWSTR>(pLogEntry->GetAuthorEmail()), pItem->cchTextMax - 1);
1615 break;
1617 case LOGLIST_COMMIT_NAME: //Commit
1618 lstrcpyn(pItem->pszText, static_cast<LPCWSTR>(pLogEntry->GetCommitterName()), pItem->cchTextMax - 1);
1619 break;
1621 case LOGLIST_COMMIT_EMAIL: //Commit Email
1622 lstrcpyn(pItem->pszText, static_cast<LPCWSTR>(pLogEntry->GetCommitterEmail()), pItem->cchTextMax - 1);
1623 break;
1625 case LOGLIST_COMMIT_DATE: //Commit Date
1626 if (!pLogEntry->m_CommitHash.IsEmpty())
1627 lstrcpyn(pItem->pszText,
1628 CLoglistUtils::FormatDateAndTime(pLogEntry->GetCommitterDate(), m_DateFormat, true, m_bRelativeTimes),
1629 pItem->cchTextMax - 1);
1630 break;
1631 case LOGLIST_BUG: //Bug ID
1632 lstrcpyn(pItem->pszText, static_cast<LPCWSTR>(this->m_ProjectProperties.FindBugID(pLogEntry->GetSubjectBody())), pItem->cchTextMax - 1);
1633 break;
1634 case LOGLIST_SVNREV: //SVN revision
1635 lstrcpyn(pItem->pszText, static_cast<LPCWSTR>(FindSVNRev(pLogEntry->GetSubjectBody())), pItem->cchTextMax - 1);
1636 break;
1638 default:
1639 ASSERT(false);
1643 bool CGitLogListBase::IsOnStash(int index)
1645 GitRevLoglist* rev = m_arShownList.SafeGetAt(index);
1646 if (IsStash(rev))
1647 return true;
1648 if (index > 0)
1650 GitRevLoglist* preRev = m_arShownList.SafeGetAt(index - 1);
1651 if (IsStash(preRev))
1652 return preRev->m_ParentHash.size() == 2 && preRev->m_ParentHash[1] == rev->m_CommitHash;
1654 return false;
1657 bool CGitLogListBase::IsStash(const GitRev * pSelLogEntry)
1659 auto hashMap{ m_HashMap.load() };
1660 const auto refList = hashMap.get()->find(pSelLogEntry->m_CommitHash);
1661 if (refList == hashMap.get()->cend())
1662 return false;
1663 return any_of((*refList).second, [](const auto& ref) { return ref == L"refs/stash"; });
1666 bool CGitLogListBase::IsBisect(const GitRev * pSelLogEntry)
1668 auto hashMap{ m_HashMap.load() };
1669 const auto refList = hashMap->find(pSelLogEntry->m_CommitHash);
1670 if (refList == hashMap->cend())
1671 return false;
1672 return any_of((*refList).second, [](const auto& ref) { return CStringUtils::StartsWith(ref, L"refs/bisect/"); });
1675 void CGitLogListBase::GetParentHashes(GitRev *pRev, GIT_REV_LIST &parentHash)
1677 if (pRev->m_ParentHash.empty() && !pRev->m_CommitHash.IsEmpty())
1679 if (pRev->GetParentFromHash(pRev->m_CommitHash))
1680 MessageBox(pRev->GetLastErr(), L"TortoiseGit", MB_ICONERROR);
1682 parentHash = pRev->m_ParentHash;
1685 void CGitLogListBase::OnContextMenu(CWnd* pWnd, CPoint point)
1687 __super::OnContextMenu(pWnd, point);
1689 if (pWnd != this)
1690 return;
1692 const int selIndex = GetSelectionMark();
1693 if (selIndex < 0)
1694 return; // nothing selected, nothing to do with a context menu
1696 // if the context menu is invoked through the keyboard, we have to use
1697 // a calculated position on where to anchor the menu on
1698 if ((point.x == -1) && (point.y == -1))
1700 CRect rect;
1701 GetItemRect(selIndex, &rect, LVIR_LABEL);
1702 ClientToScreen(&rect);
1703 point = rect.CenterPoint();
1705 m_nSearchIndex = selIndex;
1707 const bool showExtendedMenu = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
1709 POSITION pos = GetFirstSelectedItemPosition();
1710 const int FirstSelect = GetNextSelectedItem(pos);
1711 if (FirstSelect < 0)
1712 return;
1714 GitRevLoglist* pSelLogEntry = m_arShownList.SafeGetAt(FirstSelect);
1715 if (pSelLogEntry == nullptr)
1716 return;
1718 int LastSelect = -1;
1719 UINT selectedCount = 1;
1720 while (pos)
1722 LastSelect = GetNextSelectedItem(pos);
1723 ++selectedCount;
1726 ASSERT(GetSelectedCount() == selectedCount);
1728 //entry is selected, now show the popup menu
1729 CIconMenu popup;
1730 CIconMenu subbranchmenu, submenu, gnudiffmenu, diffmenu, blamemenu, revertmenu;
1732 if (popup.CreatePopupMenu())
1734 CGitHash headHash;
1735 if (g_Git.GetHash(headHash, L"HEAD"))
1737 MessageBox(g_Git.GetGitLastErr(L"Could not get HEAD hash."), L"TortoiseGit", MB_ICONERROR);
1738 return;
1740 auto hashMapSharedPtr{ m_HashMap.load() };
1741 const auto& hashMap = *hashMapSharedPtr;
1742 const bool isHeadCommit = (pSelLogEntry->m_CommitHash == headHash);
1743 CString currentBranch = L"refs/heads/" + g_Git.GetCurrentBranch();
1744 CTGitPath workingTree(g_Git.m_CurrentDir);
1745 const bool isMergeActive = workingTree.IsMergeActive();
1746 const bool isBisectActive = workingTree.IsBisectActive();
1747 const bool isStash = IsOnStash(FirstSelect);
1748 GIT_REV_LIST parentHash;
1749 GetParentHashes(pSelLogEntry, parentHash);
1750 STRING_VECTOR parentInfo;
1751 for (size_t i = 0; i < parentHash.size(); ++i)
1753 CString str;
1754 str.Format(IDS_PARENT, i + 1);
1755 GitRev rev;
1756 if (rev.GetCommit(parentHash[i].ToString()) == 0)
1758 CString commitTitle = rev.GetSubject();
1759 if (commitTitle.GetLength() > 20)
1761 commitTitle.Truncate(20);
1762 commitTitle += L"...";
1764 str.AppendFormat(L": \"%s\" (%s)", static_cast<LPCWSTR>(CStringUtils::EscapeAccellerators(commitTitle)), static_cast<LPCWSTR>(parentHash[i].ToString(g_Git.GetShortHASHLength())));
1766 else
1767 str.AppendFormat(L" (%s)", static_cast<LPCWSTR>(parentHash[i].ToString(g_Git.GetShortHASHLength())));
1768 parentInfo.push_back(str);
1771 if (m_ContextMenuMask & GetContextMenuBit(ID_REBASE_PICK) && !(pSelLogEntry->GetRebaseAction() & (LOGACTIONS_REBASE_CURRENT | LOGACTIONS_REBASE_DONE)))
1772 popup.AppendMenuIcon(ID_REBASE_PICK, IDS_REBASE_PICK, IDI_PICK);
1774 if (m_ContextMenuMask & GetContextMenuBit(ID_REBASE_SQUASH) && !(pSelLogEntry->GetRebaseAction() & (LOGACTIONS_REBASE_CURRENT | LOGACTIONS_REBASE_DONE)) && FirstSelect != GetItemCount() - 1 && LastSelect != GetItemCount() - 1 && (m_bIsCherryPick || pSelLogEntry->ParentsCount() == 1))
1775 popup.AppendMenuIcon(ID_REBASE_SQUASH, IDS_REBASE_SQUASH, IDI_SQUASH);
1777 if (m_ContextMenuMask & GetContextMenuBit(ID_REBASE_EDIT) && !(pSelLogEntry->GetRebaseAction() & (LOGACTIONS_REBASE_CURRENT | LOGACTIONS_REBASE_DONE)))
1778 popup.AppendMenuIcon(ID_REBASE_EDIT, IDS_REBASE_EDIT, IDI_EDIT);
1780 if (m_ContextMenuMask & GetContextMenuBit(ID_REBASE_SKIP) && !(pSelLogEntry->GetRebaseAction() & (LOGACTIONS_REBASE_CURRENT | LOGACTIONS_REBASE_DONE)))
1781 popup.AppendMenuIcon(ID_REBASE_SKIP, IDS_REBASE_SKIP, IDI_SKIP);
1783 if (m_ContextMenuMask & (GetContextMenuBit(ID_REBASE_SKIP) | GetContextMenuBit(ID_REBASE_EDIT) | GetContextMenuBit(ID_REBASE_SQUASH) | GetContextMenuBit(ID_REBASE_PICK)) && !(pSelLogEntry->GetRebaseAction() & (LOGACTIONS_REBASE_CURRENT | LOGACTIONS_REBASE_DONE)))
1784 popup.AppendMenu(MF_SEPARATOR, NULL);
1786 if (selectedCount == 1)
1789 bool requiresSeparator = false;
1790 if( !pSelLogEntry->m_CommitHash.IsEmpty())
1792 if(m_ContextMenuMask&GetContextMenuBit(ID_COMPARE) && m_hasWC) // compare revision with WC
1794 popup.AppendMenuIcon(ID_COMPARE, IDS_LOG_POPUP_COMPARE, IDI_DIFF);
1795 requiresSeparator = true;
1798 else
1800 if(m_ContextMenuMask&GetContextMenuBit(ID_COMMIT))
1802 popup.AppendMenuIcon(ID_COMMIT, IDS_LOG_POPUP_COMMIT, IDI_COMMIT);
1803 requiresSeparator = true;
1805 if (isMergeActive && (m_ContextMenuMask & GetContextMenuBit(ID_MERGE_ABORT)))
1807 popup.AppendMenuIcon(ID_MERGE_ABORT, IDS_MENUMERGEABORT, IDI_MERGEABORT);
1808 requiresSeparator = true;
1812 if (m_ContextMenuMask & GetContextMenuBit(ID_BLAMEPREVIOUS))
1814 if (parentHash.size() == 1)
1816 popup.AppendMenuIcon(ID_BLAMEPREVIOUS, IDS_LOG_POPUP_BLAMEPREVIOUS, IDI_BLAME);
1817 requiresSeparator = true;
1819 else if (parentHash.size() > 1)
1821 blamemenu.CreatePopupMenu();
1822 popup.AppendMenuIcon(ID_BLAMEPREVIOUS, IDS_LOG_POPUP_BLAMEPREVIOUS, IDI_BLAME, blamemenu.m_hMenu);
1823 for (size_t i = 0; i < parentInfo.size(); ++i)
1825 blamemenu.AppendMenuIcon(ID_BLAMEPREVIOUS + ((i + 1) << 16), parentInfo[i]);
1827 requiresSeparator = true;
1831 if(m_ContextMenuMask&GetContextMenuBit(ID_GNUDIFF1) && m_hasWC) // compare with WC, unified
1833 if (parentHash.size() == 1)
1835 popup.AppendMenuIcon(ID_GNUDIFF1, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
1836 requiresSeparator = true;
1838 else if (parentHash.size() > 1)
1840 gnudiffmenu.CreatePopupMenu();
1841 popup.AppendMenuIcon(ID_GNUDIFF1,IDS_LOG_POPUP_GNUDIFF_PARENT, IDI_DIFF, gnudiffmenu.m_hMenu);
1843 gnudiffmenu.AppendMenuIcon(static_cast<UINT_PTR>(ID_GNUDIFF1 + (0xFFFF << 16)), CString(MAKEINTRESOURCE(IDS_ALLPARENTS)));
1844 gnudiffmenu.AppendMenuIcon(static_cast<UINT_PTR>(ID_GNUDIFF1 + (0xFFFE << 16)), CString(MAKEINTRESOURCE(IDS_ONLYMERGEDFILES)));
1845 gnudiffmenu.AppendMenuIcon(static_cast<UINT_PTR>(ID_GNUDIFF1 + (0xFFFD << 16)), CString(MAKEINTRESOURCE(IDS_DIFFWITHMERGE)));
1847 for (size_t i = 0; i < parentInfo.size(); ++i)
1849 gnudiffmenu.AppendMenuIcon(ID_GNUDIFF1 + ((i + 1) << 16), parentInfo[i]);
1851 requiresSeparator = true;
1855 if(m_ContextMenuMask&GetContextMenuBit(ID_COMPAREWITHPREVIOUS))
1857 if (parentHash.size() == 1)
1859 popup.AppendMenuIcon(ID_COMPAREWITHPREVIOUS, IDS_LOG_POPUP_COMPAREWITHPREVIOUS, IDI_DIFF);
1860 if (CRegDWORD(L"Software\\TortoiseGit\\DiffByDoubleClickInLog", FALSE) && m_ColumnRegKey != L"reflog")
1861 popup.SetDefaultItem(ID_COMPAREWITHPREVIOUS, FALSE);
1862 requiresSeparator = true;
1864 else if (parentHash.size() > 1)
1866 diffmenu.CreatePopupMenu();
1867 popup.AppendMenuIcon(ID_COMPAREWITHPREVIOUS, IDS_LOG_POPUP_COMPAREWITHPREVIOUS, IDI_DIFF, diffmenu.m_hMenu);
1868 for (size_t i = 0; i < parentInfo.size(); ++i)
1870 diffmenu.AppendMenuIcon(ID_COMPAREWITHPREVIOUS + ((i + 1) << 16), parentInfo[i]);
1871 if (i == 0 && CRegDWORD(L"Software\\TortoiseGit\\DiffByDoubleClickInLog", FALSE) && m_ColumnRegKey != L"reflog")
1873 popup.SetDefaultItem(ID_COMPAREWITHPREVIOUS, FALSE);
1874 diffmenu.SetDefaultItem(static_cast<UINT>(ID_COMPAREWITHPREVIOUS + ((i + 1) << 16)), FALSE);
1877 requiresSeparator = true;
1881 if(m_ContextMenuMask&GetContextMenuBit(ID_BLAME))
1883 popup.AppendMenuIcon(ID_BLAME, IDS_LOG_POPUP_BLAME, IDI_BLAME);
1884 requiresSeparator = true;
1887 if (requiresSeparator)
1889 popup.AppendMenu(MF_SEPARATOR, NULL);
1890 requiresSeparator = false;
1893 if (pSelLogEntry->m_CommitHash.IsEmpty() && !isMergeActive)
1895 if(m_ContextMenuMask&GetContextMenuBit(ID_STASH_SAVE))
1897 popup.AppendMenuIcon(ID_STASH_SAVE, IDS_MENUSTASHSAVE, IDI_SHELVE);
1898 requiresSeparator = true;
1902 if ((pSelLogEntry->m_CommitHash.IsEmpty() || isStash) && workingTree.HasStashDir())
1904 if (m_ContextMenuMask&GetContextMenuBit(ID_STASH_POP))
1906 popup.AppendMenuIcon(ID_STASH_POP, IDS_MENUSTASHPOP, IDI_UNSHELVE);
1907 requiresSeparator = true;
1910 if (m_ContextMenuMask&GetContextMenuBit(ID_STASH_LIST))
1912 popup.AppendMenuIcon(ID_STASH_LIST, IDS_MENUSTASHLIST, IDI_LOG);
1913 requiresSeparator = true;
1917 if (requiresSeparator)
1919 popup.AppendMenu(MF_SEPARATOR, NULL);
1920 requiresSeparator = false;
1923 if (isBisectActive)
1925 GitRevLoglist* pFirstEntry = m_arShownList.SafeGetAt(FirstSelect);
1926 if (m_ContextMenuMask&GetContextMenuBit(ID_BISECTGOOD) && !IsBisect(pFirstEntry))
1928 popup.AppendMenuIcon(ID_BISECTGOOD, IDS_MENUBISECTGOOD, IDI_THUMB_UP);
1929 requiresSeparator = true;
1932 if (m_ContextMenuMask&GetContextMenuBit(ID_BISECTBAD) && !IsBisect(pFirstEntry))
1934 popup.AppendMenuIcon(ID_BISECTBAD, IDS_MENUBISECTBAD, IDI_THUMB_DOWN);
1935 requiresSeparator = true;
1937 if (m_ContextMenuMask&GetContextMenuBit(ID_BISECTSKIP) && !IsBisect(pFirstEntry))
1939 popup.AppendMenuIcon(ID_BISECTSKIP, IDS_MENUBISECTSKIP, IDI_BISECT);
1940 requiresSeparator = true;
1944 if (pSelLogEntry->m_CommitHash.IsEmpty() && isBisectActive)
1946 if (m_ContextMenuMask&GetContextMenuBit(ID_BISECTRESET))
1948 popup.AppendMenuIcon(ID_BISECTRESET, IDS_MENUBISECTRESET, IDI_BISECT_RESET);
1949 requiresSeparator = true;
1953 if (requiresSeparator)
1955 popup.AppendMenu(MF_SEPARATOR, NULL);
1956 requiresSeparator = false;
1959 if (pSelLogEntry->m_CommitHash.IsEmpty())
1961 if (m_ContextMenuMask & GetContextMenuBit(ID_PULL) && !isMergeActive)
1962 popup.AppendMenuIcon(ID_PULL, IDS_MENUPULL, IDI_PULL);
1964 if(m_ContextMenuMask&GetContextMenuBit(ID_FETCH))
1965 popup.AppendMenuIcon(ID_FETCH, IDS_MENUFETCH, IDI_UPDATE);
1967 if ((m_ContextMenuMask & GetContextMenuBit(ID_SUBMODULE_UPDATE)) && workingTree.HasSubmodules())
1968 popup.AppendMenuIcon(ID_SUBMODULE_UPDATE, IDS_PROC_SYNC_SUBKODULEUPDATE, IDI_UPDATE);
1970 popup.AppendMenu(MF_SEPARATOR, NULL);
1972 if (m_ContextMenuMask & GetContextMenuBit(ID_CLEANUP))
1973 popup.AppendMenuIcon(ID_CLEANUP, IDS_MENUCLEANUP, IDI_CLEANUP);
1975 popup.AppendMenu(MF_SEPARATOR, NULL);
1979 // if (!m_ProjectProperties.sWebViewerRev.IsEmpty())
1980 // {
1981 // popup.AppendMenuIcon(ID_VIEWREV, IDS_LOG_POPUP_VIEWREV);
1982 // }
1983 // if (!m_ProjectProperties.sWebViewerPathRev.IsEmpty())
1984 // {
1985 // popup.AppendMenuIcon(ID_VIEWPATHREV, IDS_LOG_POPUP_VIEWPATHREV);
1986 // }
1987 // if ((!m_ProjectProperties.sWebViewerPathRev.IsEmpty())||
1988 // (!m_ProjectProperties.sWebViewerRev.IsEmpty()))
1989 // {
1990 // popup.AppendMenu(MF_SEPARATOR, NULL);
1991 // }
1993 CString str;
1994 //if (m_hasWC)
1995 // popup.AppendMenuIcon(ID_REVERTTOREV, IDS_LOG_POPUP_REVERTTOREV, IDI_REVERT);
1997 if(!pSelLogEntry->m_CommitHash.IsEmpty())
1999 if (m_ContextMenuMask & GetContextMenuBit(ID_LOG) || ((!isStash && hashMap.find(pSelLogEntry->m_CommitHash) != hashMap.cend()) || showExtendedMenu))
2001 popup.AppendMenuIcon(ID_LOG, IDS_LOG_POPUP_LOG, IDI_LOG);
2002 if (m_ColumnRegKey == L"reflog")
2003 popup.SetDefaultItem(ID_LOG, FALSE);
2006 if (m_ContextMenuMask&GetContextMenuBit(ID_REPOBROWSE))
2007 popup.AppendMenuIcon(ID_REPOBROWSE, IDS_LOG_BROWSEREPO, IDI_REPOBROWSE);
2009 str.Format(IDS_LOG_POPUP_MERGEREV, static_cast<LPCWSTR>(g_Git.GetCurrentBranch()));
2011 if (m_ContextMenuMask&GetContextMenuBit(ID_MERGEREV) && !isHeadCommit && m_hasWC && !isMergeActive && !isStash)
2013 popup.AppendMenuIcon(ID_MERGEREV, str, IDI_MERGE);
2015 size_t index = static_cast<size_t>(-1);
2016 CGit::REF_TYPE type = CGit::REF_TYPE::UNKNOWN;
2017 if (IsMouseOnRefLabelFromPopupMenu(pSelLogEntry, point, type, hashMap, nullptr, &index))
2018 popup.SetMenuItemData(ID_MERGEREV, reinterpret_cast<LONG_PTR>(&hashMap.find(pSelLogEntry->m_CommitHash)->second[index]));
2021 str.Format(IDS_RESET_TO_THIS_FORMAT, static_cast<LPCWSTR>(g_Git.GetCurrentBranch()));
2023 if (m_ContextMenuMask&GetContextMenuBit(ID_RESET) && m_hasWC && !isStash)
2024 popup.AppendMenuIcon(ID_RESET, str, IDI_RESET);
2027 // Add Switch Branch express Menu
2028 if (hashMap.find(pSelLogEntry->m_CommitHash) != hashMap.end()
2029 && (m_ContextMenuMask&GetContextMenuBit(ID_SWITCHBRANCH) && m_hasWC && !isStash)
2032 std::vector<const CString*> branchs;
2033 auto addCheck = [&](const CString& ref)
2035 if (!CStringUtils::StartsWith(ref, L"refs/heads/") || ref == currentBranch)
2036 return;
2037 branchs.push_back(&ref);
2039 size_t index = static_cast<size_t>(-1);
2040 CGit::REF_TYPE type = CGit::REF_TYPE::UNKNOWN;
2041 if (IsMouseOnRefLabelFromPopupMenu(pSelLogEntry, point, type, hashMap, nullptr, &index))
2042 addCheck(hashMap.find(pSelLogEntry->m_CommitHash)->second[index]);
2043 else
2044 for_each(hashMap.find(pSelLogEntry->m_CommitHash)->second, addCheck);
2046 CString str2;
2047 str2.LoadString(IDS_SWITCH_BRANCH);
2049 if(branchs.size() == 1)
2051 str2 += L' ';
2052 str2 += L'"' + branchs[0]->Mid(static_cast<int>(wcslen(L"refs/heads/"))) + L'"';
2053 popup.AppendMenuIcon(ID_SWITCHBRANCH, str2, IDI_SWITCH);
2055 popup.SetMenuItemData(ID_SWITCHBRANCH, reinterpret_cast<LONG_PTR>(branchs[0]));
2058 else if(branchs.size() > 1)
2060 subbranchmenu.CreatePopupMenu();
2061 for (size_t i = 0 ; i < branchs.size(); ++i)
2063 if (*branchs[i] != currentBranch)
2065 subbranchmenu.AppendMenuIcon(ID_SWITCHBRANCH + (i << 16), branchs[i]->Mid(static_cast<int>(wcslen(L"refs/heads/"))));
2066 subbranchmenu.SetMenuItemData(ID_SWITCHBRANCH+(i<<16), reinterpret_cast<LONG_PTR>(branchs[i]));
2070 popup.AppendMenuIcon(ID_SWITCHBRANCH, str2, IDI_SWITCH, subbranchmenu.m_hMenu);
2074 if (m_ContextMenuMask&GetContextMenuBit(ID_SWITCHTOREV) && !isHeadCommit && m_hasWC && !isStash)
2076 popup.AppendMenuIcon(ID_SWITCHTOREV, IDS_SWITCH_TO_THIS, IDI_SWITCH);
2077 size_t index = static_cast<size_t>(-1);
2078 CGit::REF_TYPE type = CGit::REF_TYPE::UNKNOWN;
2079 if (IsMouseOnRefLabelFromPopupMenu(pSelLogEntry, point, type, hashMap, nullptr, &index))
2080 popup.SetMenuItemData(ID_SWITCHTOREV, reinterpret_cast<LONG_PTR>(&hashMap.find(pSelLogEntry->m_CommitHash)->second[index]));
2083 if (m_ContextMenuMask&GetContextMenuBit(ID_CREATE_BRANCH) && !isStash)
2085 popup.AppendMenuIcon(ID_CREATE_BRANCH, IDS_CREATE_BRANCH_AT_THIS, IDI_COPY);
2087 size_t index = static_cast<size_t>(-1);
2088 CGit::REF_TYPE type = CGit::REF_TYPE::REMOTE_BRANCH;
2089 if (IsMouseOnRefLabelFromPopupMenu(pSelLogEntry, point, type, hashMap, nullptr, &index))
2090 popup.SetMenuItemData(ID_CREATE_BRANCH, reinterpret_cast<LONG_PTR>(&hashMap.find(pSelLogEntry->m_CommitHash)->second[index]));
2093 if (m_ContextMenuMask&GetContextMenuBit(ID_CREATE_TAG) && !isStash)
2094 popup.AppendMenuIcon(ID_CREATE_TAG,IDS_CREATE_TAG_AT_THIS , IDI_TAG);
2096 str.Format(IDS_REBASE_THIS_FORMAT, static_cast<LPCWSTR>(g_Git.GetCurrentBranch()));
2098 if (pSelLogEntry->m_CommitHash != headHash && m_hasWC && !isMergeActive && !isStash)
2099 if(m_ContextMenuMask&GetContextMenuBit(ID_REBASE_TO_VERSION))
2100 popup.AppendMenuIcon(ID_REBASE_TO_VERSION, str , IDI_REBASE);
2102 if(m_ContextMenuMask&GetContextMenuBit(ID_EXPORT))
2103 popup.AppendMenuIcon(ID_EXPORT,IDS_EXPORT_TO_THIS, IDI_EXPORT);
2105 if (m_ContextMenuMask&GetContextMenuBit(ID_REVERTREV) && m_hasWC && !isMergeActive && !isStash)
2107 if (parentHash.size() == 1)
2108 popup.AppendMenuIcon(ID_REVERTREV, IDS_LOG_POPUP_REVERTREV, IDI_REVERT);
2109 else if (parentHash.size() > 1)
2111 revertmenu.CreatePopupMenu();
2112 popup.AppendMenuIcon(ID_REVERTREV, IDS_LOG_POPUP_REVERTREV, IDI_REVERT, revertmenu.m_hMenu);
2114 for (size_t i = 0; i < parentInfo.size(); ++i)
2116 revertmenu.AppendMenuIcon(ID_REVERTREV + ((i + 1) << 16), parentInfo[i]);
2121 if (m_ContextMenuMask&GetContextMenuBit(ID_EDITNOTE) && !isStash)
2122 popup.AppendMenuIcon(ID_EDITNOTE, IDS_EDIT_NOTES, IDI_EDIT);
2124 popup.AppendMenu(MF_SEPARATOR, NULL);
2128 if(!pSelLogEntry->m_Ref.IsEmpty())
2130 popup.AppendMenuIcon(ID_REFLOG_DEL, IDS_REFLOG_DEL, IDI_DELETE);
2131 if (selectedCount == 1 && CStringUtils::StartsWith(pSelLogEntry->m_Ref, L"refs/stash"))
2132 popup.AppendMenuIcon(ID_REFLOG_STASH_APPLY, IDS_MENUSTASHAPPLY, IDI_UNSHELVE);
2133 if (selectedCount <= 2)
2134 popup.AppendMenu(MF_SEPARATOR, NULL);
2137 if (selectedCount >= 2)
2139 bool bAddSeparator = false;
2140 if ((selectedCount == 2) || IsSelectionContinuous())
2142 if (m_ContextMenuMask&GetContextMenuBit(ID_COMPARETWO)) // compare two revisions
2144 popup.AppendMenuIcon(ID_COMPARETWO, IDS_LOG_POPUP_COMPARETWO, IDI_DIFF);
2145 bAddSeparator = true;
2149 if (selectedCount == 2)
2151 if (m_ContextMenuMask&GetContextMenuBit(ID_GNUDIFF2) && m_hasWC) // compare two revisions, unified
2153 popup.AppendMenuIcon(ID_GNUDIFF2, IDS_LOG_POPUP_GNUDIFF, IDI_DIFF);
2154 bAddSeparator = true;
2157 if (!pSelLogEntry->m_CommitHash.IsEmpty())
2159 CString firstSelHash = pSelLogEntry->m_CommitHash.ToString(g_Git.GetShortHASHLength());
2160 GitRevLoglist* pLastEntry = m_arShownList.SafeGetAt(LastSelect);
2161 CString lastSelHash = pLastEntry->m_CommitHash.ToString(g_Git.GetShortHASHLength());
2162 CString menu;
2163 menu.Format(IDS_SHOWLOG_OF, static_cast<LPCWSTR>(lastSelHash + L".." + firstSelHash));
2164 popup.AppendMenuIcon(ID_LOG_VIEWRANGE, menu, IDI_LOG);
2165 menu.Format(IDS_SHOWLOG_OF, static_cast<LPCWSTR>(firstSelHash + L".." + lastSelHash));
2166 popup.AppendMenuIcon(ID_LOG_VIEWRANGE_REVERSE, menu, IDI_LOG);
2167 menu.Format(IDS_SHOWLOG_OF, static_cast<LPCWSTR>(lastSelHash + L"..." + firstSelHash));
2168 popup.AppendMenuIcon(ID_LOG_VIEWRANGE_REACHABLEFROMONLYONE, menu, IDI_LOG);
2169 bAddSeparator = true;
2173 if ((m_ContextMenuMask & GetContextMenuBit(ID_COMPARETWOCOMMITCHANGES)) && selectedCount == 2)
2175 bAddSeparator = true;
2176 popup.AppendMenuIcon(ID_COMPARETWOCOMMITCHANGES, IDS_LOG_POPUP_COMPARECHANGESET, IDI_DIFF);
2179 if (bAddSeparator)
2181 popup.AppendMenu(MF_SEPARATOR, NULL);
2182 bAddSeparator = false;
2185 if (m_ContextMenuMask&GetContextMenuBit(ID_REVERTREV) && m_hasWC && !isMergeActive)
2186 popup.AppendMenuIcon(ID_REVERTREV, IDS_LOG_POPUP_REVERTREVS, IDI_REVERT);
2188 if (bAddSeparator)
2189 popup.AppendMenu(MF_SEPARATOR, NULL);
2192 if (selectedCount > 1 && isBisectActive && (m_ContextMenuMask & GetContextMenuBit(ID_BISECTSKIP)) && !IsBisect(pSelLogEntry))
2194 popup.AppendMenuIcon(ID_BISECTSKIP, IDS_MENUBISECTSKIP, IDI_BISECT);
2195 popup.AppendMenu(MF_SEPARATOR, NULL);
2198 if (!pSelLogEntry->m_CommitHash.IsEmpty())
2200 bool bAddSeparator = false;
2201 if (selectedCount >= 2 && IsSelectionContinuous())
2203 if (m_ContextMenuMask&GetContextMenuBit(ID_COMBINE_COMMIT) && m_hasWC && !isMergeActive)
2205 const int headindex = this->GetHeadIndex();
2206 if(headindex>=0 && LastSelect >= headindex && FirstSelect >= headindex)
2208 CString head;
2209 head.Format(L"HEAD~%d", FirstSelect - headindex);
2210 CGitHash hashFirst;
2211 int ret = g_Git.GetHash(hashFirst, head);
2212 head.Format(L"HEAD~%d",LastSelect-headindex);
2213 CGitHash hash;
2214 ret = ret || g_Git.GetHash(hash, head);
2215 GitRevLoglist* pFirstEntry = m_arShownList.SafeGetAt(FirstSelect);
2216 GitRevLoglist* pLastEntry = m_arShownList.SafeGetAt(LastSelect);
2217 if (!ret && pFirstEntry->m_CommitHash == hashFirst && pLastEntry->m_CommitHash == hash)
2219 popup.AppendMenuIcon(ID_COMBINE_COMMIT,IDS_COMBINE_TO_ONE,IDI_COMBINE);
2220 bAddSeparator = true;
2225 if (m_ContextMenuMask&GetContextMenuBit(ID_CHERRY_PICK) && !isHeadCommit && m_hasWC && !isMergeActive) {
2226 if (selectedCount >= 2)
2227 popup.AppendMenuIcon(ID_CHERRY_PICK, IDS_CHERRY_PICK_VERSIONS, IDI_CHERRYPICK);
2228 else
2229 popup.AppendMenuIcon(ID_CHERRY_PICK, IDS_CHERRY_PICK_VERSION, IDI_CHERRYPICK);
2230 bAddSeparator = true;
2233 if (!isStash && (selectedCount <= 2 || IsSelectionContinuous()))
2234 if(m_ContextMenuMask&GetContextMenuBit(ID_CREATE_PATCH)) {
2235 popup.AppendMenuIcon(ID_CREATE_PATCH, IDS_CREATE_PATCH, IDI_PATCH);
2236 bAddSeparator = true;
2239 if (bAddSeparator)
2240 popup.AppendMenu(MF_SEPARATOR, NULL);
2243 if (m_hasWC && !isMergeActive && !isStash && (m_ContextMenuMask & GetContextMenuBit(ID_BISECTSTART)) && selectedCount == 2 && !m_arShownList.SafeGetAt(FirstSelect)->m_CommitHash.IsEmpty() && !isBisectActive)
2245 popup.AppendMenuIcon(ID_BISECTSTART, IDS_MENUBISECTSTART, IDI_BISECT);
2246 popup.AppendMenu(MF_SEPARATOR, NULL);
2249 if (selectedCount == 1)
2251 bool bAddSeparator = false;
2252 if ((m_ContextMenuMask & GetContextMenuBit(ID_PUSH)) && ((!isStash && hashMap.find(pSelLogEntry->m_CommitHash) != hashMap.cend()) || showExtendedMenu))
2254 // show the push-option only if the log entry has an associated local branch
2255 const bool isLocal = hashMap.find(pSelLogEntry->m_CommitHash) != hashMap.cend() && any_of(hashMap.find(pSelLogEntry->m_CommitHash)->second, [](const CString& ref) { return CStringUtils::StartsWith(ref, L"refs/heads/") || CStringUtils::StartsWith(ref, L"refs/tags/"); });
2256 if (isLocal || showExtendedMenu)
2258 CString str;
2259 str.LoadString(IDS_MENUPUSH);
2261 CString branch;
2262 size_t index = static_cast<size_t>(-1);
2263 CGit::REF_TYPE type = CGit::REF_TYPE::UNKNOWN;
2264 if (IsMouseOnRefLabelFromPopupMenu(pSelLogEntry, point, type, hashMap, &branch, &index))
2265 if (type == CGit::REF_TYPE::LOCAL_BRANCH || type == CGit::REF_TYPE::ANNOTATED_TAG || type == CGit::REF_TYPE::TAG)
2266 str.Insert(str.Find(L'.'), L" \"" + branch + L'"');
2268 popup.AppendMenuIcon(ID_PUSH, str, IDI_PUSH);
2270 if (auto refList = hashMap.find(pSelLogEntry->m_CommitHash); index != static_cast<size_t>(-1) && index < refList->second.size())
2271 popup.SetMenuItemData(ID_PUSH, reinterpret_cast<LONG_PTR>(&refList->second[index]));
2273 if (m_ContextMenuMask & GetContextMenuBit(ID_SVNDCOMMIT) && workingTree.HasGitSVNDir())
2274 popup.AppendMenuIcon(ID_SVNDCOMMIT, IDS_MENUSVNDCOMMIT, IDI_COMMIT);
2276 bAddSeparator = true;
2279 if (m_ContextMenuMask & GetContextMenuBit(ID_PULL) && isHeadCommit && !isMergeActive && m_hasWC)
2281 popup.AppendMenuIcon(ID_PULL, IDS_MENUPULL, IDI_PULL);
2282 bAddSeparator = true;
2286 if(m_ContextMenuMask &GetContextMenuBit(ID_DELETE))
2288 if (auto refList = hashMap.find(pSelLogEntry->m_CommitHash); refList != hashMap.end() )
2290 std::vector<const CString*> branchs;
2291 auto addCheck = [&](const CString& ref)
2293 if (ref == currentBranch)
2294 return;
2295 branchs.push_back(&ref);
2297 size_t index = static_cast<size_t>(-1);
2298 CGit::REF_TYPE type = CGit::REF_TYPE::UNKNOWN;
2299 if (IsMouseOnRefLabelFromPopupMenu(pSelLogEntry, point, type, hashMap, nullptr, &index))
2300 addCheck(refList->second[index]);
2301 else
2302 for_each(refList->second, addCheck);
2304 CString str;
2305 if (branchs.size() == 1)
2307 str.LoadString(IDS_DELETE_BRANCHTAG_SHORT);
2308 str += L' ';
2309 str += *branchs[0];
2310 popup.AppendMenuIcon(ID_DELETE, str, IDI_DELETE);
2311 popup.SetMenuItemData(ID_DELETE, reinterpret_cast<LONG_PTR>(branchs[0]));
2312 bAddSeparator = true;
2314 else if (branchs.size() > 1)
2316 str.LoadString(IDS_DELETE_BRANCHTAG);
2317 submenu.CreatePopupMenu();
2318 for (size_t i = 0; i < branchs.size(); ++i)
2320 submenu.AppendMenuIcon(ID_DELETE + (i << 16), *branchs[i]);
2321 submenu.SetMenuItemData(ID_DELETE + (i << 16), reinterpret_cast<LONG_PTR>(branchs[i]));
2323 submenu.AppendMenuIcon(ID_DELETE + (branchs.size() << 16), IDS_ALL);
2324 submenu.SetMenuItemData(ID_DELETE + (branchs.size() << 16), reinterpret_cast<LONG_PTR>(MAKEINTRESOURCE(IDS_ALL)));
2326 popup.AppendMenuIcon(ID_DELETE,str, IDI_DELETE, submenu.m_hMenu);
2327 bAddSeparator = true;
2330 } // m_ContextMenuMask &GetContextMenuBit(ID_DELETE)
2331 if (bAddSeparator)
2332 popup.AppendMenu(MF_SEPARATOR, NULL);
2334 if ((m_ContextMenuMask & GetContextMenuBit(ID_TOGGLE_ROLLUP)) && (m_ShowFilter & FILTERSHOW_MERGEPOINTS) && !m_LogFilter.load()->IsFilterActive() && !pSelLogEntry->m_CommitHash.IsEmpty())
2336 popup.AppendMenuIcon(ID_TOGGLE_ROLLUP, pSelLogEntry->m_RolledUp ? IDS_LOG_POPUP_EXPAND : IDS_LOG_POPUP_COLLAPSE);
2337 popup.AppendMenu(MF_SEPARATOR, NULL);
2340 } // selectedCount == 1
2342 CIconMenu clipSubMenu;
2343 if (!clipSubMenu.CreatePopupMenu())
2344 return;
2345 if (m_ContextMenuMask & GetContextMenuBit(ID_COPYCLIPBOARD) && m_ColumnRegKey != L"reflog")
2347 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDFULL, IDS_LOG_POPUP_CLIPBOARD_FULL, IDI_COPYCLIP);
2348 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDFULLNOPATHS, IDS_LOG_POPUP_CLIPBOARD_FULLNOPATHS, IDI_COPYCLIP);
2349 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDHASH, IDS_LOG_HASH, IDI_COPYCLIP);
2350 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDAUTHORSFULL, IDS_LOG_POPUP_CLIPBOARD_AUTHORSFULL, IDI_COPYCLIP);
2351 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDAUTHORSNAME, IDS_LOG_POPUP_CLIPBOARD_AUTHORSNAME, IDI_COPYCLIP);
2352 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDAUTHORSEMAIL, IDS_LOG_POPUP_CLIPBOARD_AUTHORSEMAIL, IDI_COPYCLIP);
2353 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDSUBJECTS, IDS_LOG_POPUP_CLIPBOARD_SUBJECTS, IDI_COPYCLIP);
2354 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDMESSAGES, IDS_LOG_POPUP_CLIPBOARD_MSGS, IDI_COPYCLIP);
2355 if (hashMap.find(pSelLogEntry->m_CommitHash) != hashMap.cend() && selectedCount == 1)
2357 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDBRANCHTAG, IDS_LOG_POPUP_CLIPBOARD_TAGBRANCHES, IDI_COPYCLIP);
2358 size_t index = static_cast<size_t>(-1);
2359 CGit::REF_TYPE type = CGit::REF_TYPE::UNKNOWN;
2360 if (IsMouseOnRefLabelFromPopupMenu(pSelLogEntry, point, type, hashMap, nullptr, &index))
2361 clipSubMenu.SetMenuItemData(ID_COPYCLIPBOARDBRANCHTAG, reinterpret_cast<LONG_PTR>(&hashMap.find(pSelLogEntry->m_CommitHash)->second[index]));
2364 CString temp;
2365 temp.LoadString(IDS_LOG_POPUP_COPYTOCLIPBOARD);
2366 popup.InsertMenu(static_cast<UINT>(-1), MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(clipSubMenu.m_hMenu), temp);
2368 else if (m_ContextMenuMask & GetContextMenuBit(ID_COPYCLIPBOARD) && m_ColumnRegKey == L"reflog")
2370 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDFULL, IDS_LOG_POPUP_CLIPBOARD_FULL, IDI_COPYCLIP);
2371 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDHASH, IDS_LOG_HASH, IDI_COPYCLIP);
2372 clipSubMenu.AppendMenuIcon(ID_COPYCLIPBOARDMESSAGES, IDS_LOG_POPUP_CLIPBOARD_MSGS, IDI_COPYCLIP);
2374 CString temp;
2375 temp.LoadString(IDS_LOG_POPUP_COPYTOCLIPBOARD);
2376 popup.InsertMenu(static_cast<UINT>(-1), MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(clipSubMenu.m_hMenu), temp);
2379 if(m_ContextMenuMask&GetContextMenuBit(ID_FINDENTRY))
2380 popup.AppendMenuIcon(ID_FINDENTRY, IDS_LOG_POPUP_FIND, IDI_FILTEREDIT);
2382 if (selectedCount == 1 && (m_ContextMenuMask & GetContextMenuBit(ID_SHOWBRANCHES)) && !pSelLogEntry->m_CommitHash.IsEmpty())
2383 popup.AppendMenuIcon(ID_SHOWBRANCHES, IDS_LOG_POPUP_SHOWBRANCHES, IDI_SHOWBRANCHES);
2385 const int cmd = popup.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_NONOTIFY, point.x, point.y, this);
2386 // DialogEnableWindow(IDOK, FALSE);
2387 // SetPromptApp(&theApp);
2389 this->ContextMenuAction(cmd, FirstSelect, LastSelect, &popup, hashMap);
2391 // EnableOKButton();
2392 } // if (popup.CreatePopupMenu())
2395 bool CGitLogListBase::IsSelectionContinuous()
2397 if ( GetSelectedCount()==1 )
2399 // if only one revision is selected, the selection is of course
2400 // continuous
2401 return true;
2404 POSITION pos = GetFirstSelectedItemPosition();
2405 bool bContinuous = (m_arShownList.size() == m_logEntries.size());
2406 if (bContinuous)
2408 int itemindex = GetNextSelectedItem(pos);
2409 while (pos)
2411 const int nextindex = GetNextSelectedItem(pos);
2412 if (nextindex - itemindex > 1)
2414 bContinuous = false;
2415 break;
2417 itemindex = nextindex;
2420 return bContinuous;
2423 void CGitLogListBase::CopySelectionToClipBoard(int toCopy)
2425 CString sClipdata;
2426 POSITION pos = GetFirstSelectedItemPosition();
2427 if (pos)
2429 CString sRev;
2430 sRev.LoadString(IDS_LOG_REVISION);
2431 CString sAuthor;
2432 sAuthor.LoadString(IDS_LOG_AUTHOR);
2433 CString sDate;
2434 sDate.LoadString(IDS_LOG_DATE);
2435 CString sMessage;
2436 sMessage.LoadString(IDS_LOG_MESSAGE);
2437 CString from(MAKEINTRESOURCE(IDS_STATUSLIST_FROM));
2438 bool first = true;
2439 while (pos)
2441 CString sLogCopyText;
2442 CString sPaths;
2443 GitRevLoglist* pLogEntry = m_arShownList.SafeGetAt(GetNextSelectedItem(pos));
2445 if (toCopy == ID_COPYCLIPBOARDFULL)
2447 sPaths = L"----\r\n";
2448 auto files = pLogEntry->GetFiles(nullptr);
2449 for (int cpPathIndex = 0; cpPathIndex < files.GetCount(); ++cpPathIndex)
2451 auto& file = files[cpPathIndex];
2452 sPaths += file.GetActionName() + L": " + file.GetGitPathString();
2453 if (file.m_Action & (CTGitPath::LOGACTIONS_REPLACED | CTGitPath::LOGACTIONS_COPY) && !file.GetGitOldPathString().IsEmpty())
2455 sPaths += L' ';
2456 sPaths.AppendFormat(from, static_cast<LPCWSTR>(file.GetGitOldPathString()));
2458 sPaths += L"\r\n";
2460 sPaths.Trim();
2461 sPaths += L"\r\n";
2464 if (toCopy == ID_COPYCLIPBOARDFULL || toCopy == ID_COPYCLIPBOARDFULLNOPATHS)
2466 CString sNotesTags;
2467 if (!pLogEntry->m_Notes.IsEmpty())
2469 sNotesTags = L"----\n" + CString(MAKEINTRESOURCE(IDS_NOTES));
2470 sNotesTags += L":\n";
2471 sNotesTags += pLogEntry->m_Notes;
2472 sNotesTags.Replace(L"\n", L"\r\n");
2474 CString tagInfo = GetTagInfo(pLogEntry);
2475 if (!tagInfo.IsEmpty())
2477 sNotesTags += L"----\r\n" + CString(MAKEINTRESOURCE(IDS_PROC_LOG_TAGINFO)) + L":\r\n";
2478 tagInfo.Replace(L"\n", L"\r\n");
2479 sNotesTags += tagInfo;
2482 sLogCopyText.Format(L"%s: %s\r\n%s: %s <%s>\r\n%s: %s\r\n%s:\r\n%s\r\n%s%s\r\n",
2483 static_cast<LPCWSTR>(sRev), static_cast<LPCWSTR>(pLogEntry->m_CommitHash.ToString()),
2484 static_cast<LPCWSTR>(sAuthor), static_cast<LPCWSTR>(pLogEntry->GetAuthorName()), static_cast<LPCWSTR>(pLogEntry->GetAuthorEmail()),
2485 static_cast<LPCWSTR>(sDate),
2486 static_cast<LPCWSTR>(CLoglistUtils::FormatDateAndTime(pLogEntry->GetAuthorDate(), m_DateFormat, true, m_bRelativeTimes)),
2487 static_cast<LPCWSTR>(sMessage), static_cast<LPCWSTR>(pLogEntry->GetSubjectBody(true)),
2488 static_cast<LPCWSTR>(sNotesTags),
2489 static_cast<LPCWSTR>(sPaths));
2490 sClipdata += sLogCopyText;
2492 else if (toCopy == ID_COPYCLIPBOARDAUTHORSFULL)
2494 if (!first)
2495 sClipdata += L"\r\n";
2496 sClipdata += pLogEntry->GetAuthorName();
2497 sClipdata += L" <";
2498 sClipdata += pLogEntry->GetAuthorEmail();
2499 sClipdata += L">";
2501 else if (toCopy == ID_COPYCLIPBOARDAUTHORSNAME)
2503 if (!first)
2504 sClipdata += L"\r\n";
2505 sClipdata += pLogEntry->GetAuthorName();
2507 else if (toCopy == ID_COPYCLIPBOARDAUTHORSEMAIL)
2509 if (!first)
2510 sClipdata += L"\r\n";
2511 sClipdata += pLogEntry->GetAuthorEmail();
2514 else if (toCopy == ID_COPYCLIPBOARDMESSAGES)
2516 sClipdata += L"* ";
2517 sClipdata += pLogEntry->GetSubjectBody(true);
2518 sClipdata += L"\r\n\r\n";
2520 else if (toCopy == ID_COPYCLIPBOARDSUBJECTS)
2522 sClipdata += L"* ";
2523 sClipdata += pLogEntry->GetSubject().Trim();
2524 sClipdata += L"\r\n\r\n";
2526 else
2528 if (!first)
2529 sClipdata += L"\r\n";
2530 sClipdata += pLogEntry->m_CommitHash.ToString();
2533 first = false;
2535 CStringUtils::WriteAsciiStringToClipboard(sClipdata, GetSafeHwnd());
2539 void CGitLogListBase::DiffSelectedRevWithPrevious()
2541 if (s_bThreadRunning)
2542 return;
2544 POSITION pos = GetFirstSelectedItemPosition();
2545 auto FirstSelect = GetNextSelectedItem(pos);
2546 int LastSelect = -1;
2547 while (pos)
2548 LastSelect = GetNextSelectedItem(pos);
2550 auto hashMap{ m_HashMap.load() };
2551 ContextMenuAction(ID_COMPAREWITHPREVIOUS, FirstSelect, LastSelect, nullptr, *hashMap.get());
2554 void CGitLogListBase::OnLvnOdfinditemLoglist(NMHDR *pNMHDR, LRESULT *pResult)
2556 LPNMLVFINDITEM pFindInfo = reinterpret_cast<LPNMLVFINDITEM>(pNMHDR);
2557 *pResult = -1;
2559 if (pFindInfo->lvfi.flags & LVFI_PARAM)
2560 return;
2561 if (pFindInfo->iStart < 0 || pFindInfo->iStart >= static_cast<int>(m_arShownList.size()))
2562 return;
2563 if (!pFindInfo->lvfi.psz)
2564 return;
2565 #if 0
2566 CString sCmp = pFindInfo->lvfi.psz;
2567 CString sRev;
2568 for (int i=pFindInfo->iStart; i<m_arShownList.GetCount(); ++i)
2570 GitRev * pLogEntry = reinterpret_cast<GitRev*>(m_arShownList.SafeGetAt(i));
2571 sRev.Format(L"%ld", pLogEntry->Rev);
2572 if (pFindInfo->lvfi.flags & LVFI_PARTIAL)
2574 if (sCmp.Compare(sRev.Left(sCmp.GetLength()))==0)
2576 *pResult = i;
2577 return;
2580 else
2582 if (sCmp.Compare(sRev)==0)
2584 *pResult = i;
2585 return;
2589 if (pFindInfo->lvfi.flags & LVFI_WRAP)
2591 for (int i=0; i<pFindInfo->iStart; ++i)
2593 PLOGENTRYDATA pLogEntry = reinterpret_cast<PLOGENTRYDATA>(m_arShownList.SafeGetAt(i));
2594 sRev.Format(L"%ld", pLogEntry->Rev);
2595 if (pFindInfo->lvfi.flags & LVFI_PARTIAL)
2597 if (sCmp.Compare(sRev.Left(sCmp.GetLength()))==0)
2599 *pResult = i;
2600 return;
2603 else
2605 if (sCmp.Compare(sRev)==0)
2607 *pResult = i;
2608 return;
2613 #endif
2614 *pResult = -1;
2617 int CGitLogListBase::FillGitLog(CTGitPath *path, CString *range, int info)
2619 ClearText();
2621 this->m_arShownList.SafeRemoveAll();
2623 this->m_logEntries.ClearAll();
2624 if (this->m_logEntries.ParserFromLog(path, 0, info, range))
2625 return -1;
2627 SetItemCountEx(static_cast<int>(m_logEntries.size()));
2629 for (unsigned int i = 0; i < m_logEntries.size(); ++i)
2631 if(m_IsOldFirst)
2632 this->m_arShownList.SafeAdd(&m_logEntries.GetGitRevAt(m_logEntries.size()-i-1));
2633 else
2634 this->m_arShownList.SafeAdd(&m_logEntries.GetGitRevAt(i));
2637 m_critSec.Lock();
2638 std::for_each(m_arShownList.begin(), m_arShownList.end(), [](auto entry) { entry->m_CallDiffAsync = DiffAsync; });
2639 m_critSec.Unlock();
2641 ReloadHashMap();
2643 if(path)
2644 m_Path=*path;
2645 return 0;
2648 int CGitLogListBase::FillGitLog(std::unordered_set<CGitHash>& hashes)
2650 ClearText();
2652 m_arShownList.SafeRemoveAll();
2654 m_logEntries.ClearAll();
2655 if (m_logEntries.Fill(hashes))
2656 return -1;
2658 SetItemCountEx(static_cast<int>(m_logEntries.size()));
2660 for (unsigned int i = 0; i < m_logEntries.size(); ++i)
2662 if (m_IsOldFirst)
2663 m_arShownList.SafeAdd(&m_logEntries.GetGitRevAt(m_logEntries.size() - i - 1));
2664 else
2665 m_arShownList.SafeAdd(&m_logEntries.GetGitRevAt(i));
2668 m_critSec.Lock();
2669 std::for_each(m_arShownList.begin(), m_arShownList.end(), [](auto entry) { entry->m_CallDiffAsync = DiffAsync; });
2670 m_critSec.Unlock();
2672 ReloadHashMap();
2674 return 0;
2677 int CGitLogListBase::BeginFetchLog()
2679 ATLASSERT(IsInWorkingThread());
2680 ClearText();
2682 this->m_arShownList.SafeRemoveAll();
2684 this->m_logEntries.ClearAll();
2686 this->m_LogCache.ClearAllParent();
2688 m_LogCache.FetchCacheIndex(g_Git.m_CurrentDir);
2690 CTGitPath *path;
2691 if(this->m_Path.IsEmpty())
2692 path = nullptr;
2693 else
2694 path=&this->m_Path;
2696 int mask = CGit::LOG_INFO_ONLY_HASH;
2697 if (m_bIncludeBoundaryCommits)
2698 mask |= CGit::LOG_INFO_BOUNDARY;
2699 // if(this->m_bAllBranch)
2700 mask |= m_ShowMask ;
2702 if(m_bShowWC)
2704 this->m_logEntries.insert(m_logEntries.cbegin(), m_wcRev.m_CommitHash);
2705 ResetWcRev();
2706 this->m_LogCache.m_HashMap[m_wcRev.m_CommitHash]=m_wcRev;
2709 CString range;
2711 Locker lock(m_critSec);
2712 range = m_sRange;
2714 if (range.IsEmpty())
2715 range = L"HEAD";
2717 // follow does not work for directories
2718 if (!path || path->IsDirectory())
2719 mask &= ~CGit::LOG_INFO_FOLLOW;
2720 // follow does not work with all branches 8at least in TGit)
2721 if (mask & CGit::LOG_INFO_FOLLOW)
2722 mask &= ~(CGit::LOG_INFO_ALL_BRANCH | CGit::LOG_INFO_BASIC_REFS | CGit::LOG_INFO_LOCAL_BRANCHES);
2724 CString cmd = g_Git.GetLogCmd(range, path, mask, &m_Filter, CRegDWORD(L"Software\\TortoiseGit\\LogOrderBy", CGit::LOG_ORDER_TOPOORDER));
2726 PostMessage(LVM_SETITEMCOUNT, m_logEntries.size(), LVSICF_NOINVALIDATEALL);
2730 CAutoLocker lock(g_Git.m_critGitDllSec);
2731 g_Git.CheckAndInitDll();
2733 catch (const char* msg)
2735 MessageBox(L"Could not initialize libgit.\nlibgit reports:\n" + CUnicodeUtils::GetUnicode(msg), L"TortoiseGit", MB_ICONERROR);
2736 return -1;
2739 if (!g_Git.CanParseRev(range))
2741 if (!(mask & CGit::LOG_INFO_ALL_BRANCH) && !(mask & CGit::LOG_INFO_BASIC_REFS) && !(mask & CGit::LOG_INFO_LOCAL_BRANCHES))
2742 return 0;
2744 // if show all branches, pick any ref as dummy entry ref
2745 STRING_VECTOR list;
2746 if (g_Git.GetRefList(list))
2747 MessageBox(g_Git.GetGitLastErr(L"Could not get all refs."), L"TortoiseGit", MB_ICONERROR);
2748 if (list.empty())
2749 return 0;
2751 cmd = g_Git.GetLogCmd(list[0], path, mask, &m_Filter, CRegDWORD(L"Software\\TortoiseGit\\LogOrderBy", CGit::LOG_ORDER_TOPOORDER));
2754 g_Git.m_critGitDllSec.Lock();
2755 try {
2756 if (git_open_log(&m_DllGitLog, CUnicodeUtils::GetUTF8(cmd)))
2758 g_Git.m_critGitDllSec.Unlock();
2759 return -1;
2762 catch (const char* msg)
2764 g_Git.m_critGitDllSec.Unlock();
2765 MessageBox(L"Could not open log.\nlibgit reports:\n" + CUnicodeUtils::GetUnicode(msg), L"TortoiseGit", MB_ICONERROR);
2766 return -1;
2768 g_Git.m_critGitDllSec.Unlock();
2770 return 0;
2773 BOOL CGitLogListBase::PreTranslateMessage(MSG* pMsg)
2775 if (pMsg->message == WM_KEYDOWN && pMsg->wParam=='\r' && !(GetAsyncKeyState(VK_CONTROL) & 0x8000))
2777 //if (GetFocus()==GetDlgItem(IDC_LOGLIST))
2779 if (CRegDWORD(L"Software\\TortoiseGit\\DiffByDoubleClickInLog", FALSE))
2781 DiffSelectedRevWithPrevious();
2782 return TRUE;
2785 #if 0
2786 if (GetFocus()==GetDlgItem(IDC_LOGMSG))
2788 DiffSelectedFile();
2789 return TRUE;
2791 #endif
2793 else if (pMsg->message == WM_KEYDOWN && pMsg->wParam == 'A' && GetAsyncKeyState(VK_CONTROL)&0x8000)
2795 // select all entries
2796 for (int i=0; i<GetItemCount(); ++i)
2797 SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
2798 return TRUE;
2801 #if 0
2802 if (m_hAccel && !bSkipAccelerator)
2804 int ret = TranslateAccelerator(m_hWnd, m_hAccel, pMsg);
2805 if (ret)
2806 return TRUE;
2809 #endif
2810 //m_tooltips.RelayEvent(pMsg);
2811 return __super::PreTranslateMessage(pMsg);
2814 void CGitLogListBase::OnNMDblclkLoglist(NMHDR * /*pNMHDR*/, LRESULT *pResult)
2816 // a double click on an entry in the revision list has happened
2817 *pResult = 0;
2819 if (CRegDWORD(L"Software\\TortoiseGit\\DiffByDoubleClickInLog", FALSE))
2820 DiffSelectedRevWithPrevious();
2823 void CGitLogListBase::FetchLogAsync(void*)
2825 ReloadHashMap();
2826 StartLoadingThread();
2829 UINT CGitLogListBase::LogThreadEntry(LPVOID pVoid)
2831 return static_cast<CGitLogListBase*>(pVoid)->LogThread();
2834 void CGitLogListBase::GetTimeRange(CTime &oldest, CTime &latest)
2836 //CTime time;
2837 oldest=CTime::GetCurrentTime();
2838 latest=CTime(1971,1,2,0,0,0);
2839 for (unsigned int i = 0; i < m_logEntries.size(); ++i)
2841 if(m_logEntries[i].IsEmpty())
2842 continue;
2844 if (m_logEntries.GetGitRevAt(i).GetCommitterDate().GetTime() < oldest.GetTime())
2845 oldest = m_logEntries.GetGitRevAt(i).GetCommitterDate().GetTime();
2847 if (m_logEntries.GetGitRevAt(i).GetCommitterDate().GetTime() > latest.GetTime())
2848 latest = m_logEntries.GetGitRevAt(i).GetCommitterDate().GetTime();
2852 if(latest<oldest)
2853 latest=oldest;
2856 UINT CGitLogListBase::LogThread()
2858 ::PostMessage(this->GetParent()->m_hWnd, MSG_LOAD_PERCENTAGE, GITLOG_START, 0);
2860 ULONGLONG t1,t2;
2862 if(BeginFetchLog())
2864 InterlockedExchange(&s_bThreadRunning, FALSE);
2865 InterlockedExchange(&m_bNoDispUpdates, FALSE);
2867 return 1;
2870 // create a copy we can safely work on in this thread
2871 auto shared_filter{ m_LogFilter.load() };
2872 const auto& filter = *shared_filter;
2874 TRACE(L"\n===Begin===\n");
2875 //Update work copy item;
2877 if (!m_logEntries.empty())
2879 GitRevLoglist* pRev = &m_logEntries.GetGitRevAt(0);
2881 m_arShownList.SafeAdd(pRev);
2885 InterlockedExchange(&m_bNoDispUpdates, FALSE);
2887 // store commit number of the last selected commit/line before the refresh or -1
2888 int lastSelectedHashNItem = -1;
2889 if (m_lastSelectedHash.IsEmpty())
2890 lastSelectedHashNItem = 0;
2892 int ret = 0;
2894 bool shouldWalk = true;
2895 CString range;
2897 Locker lock(m_critSec);
2898 range = m_sRange;
2900 if (!g_Git.CanParseRev(range))
2902 // walk revisions if show all branches and there exists any ref
2903 if (!(m_ShowMask & CGit::LOG_INFO_ALL_BRANCH) && !(m_ShowMask & CGit::LOG_INFO_BASIC_REFS) && !(m_ShowMask & CGit::LOG_INFO_LOCAL_BRANCHES))
2904 shouldWalk = false;
2905 else
2907 STRING_VECTOR list;
2908 if (g_Git.GetRefList(list))
2909 MessageBox(g_Git.GetGitLastErr(L"Could not get all refs."), L"TortoiseGit", MB_ICONERROR);
2910 if (list.empty())
2911 shouldWalk = false;
2915 if (shouldWalk)
2917 g_Git.m_critGitDllSec.Lock();
2918 if (!m_DllGitLog)
2920 MessageBox(L"Opening log failed.", L"TortoiseGit", MB_ICONERROR);
2921 g_Git.m_critGitDllSec.Unlock();
2922 InterlockedExchange(&s_bThreadRunning, FALSE);
2923 InterlockedExchange(&m_bNoDispUpdates, FALSE);
2924 return 1;
2926 int total = 0;
2929 if (git_get_log_firstcommit(m_DllGitLog) < 0)
2931 MessageBox(L"Getting first commit and preparing the revision walk failed. Broken repository?", L"TortoiseGit", MB_ICONERROR);
2932 git_close_log(m_DllGitLog, 0);
2933 g_Git.m_critGitDllSec.Unlock();
2934 InterlockedExchange(&s_bThreadRunning, FALSE);
2935 InterlockedExchange(&m_bNoDispUpdates, FALSE);
2936 return 1;
2938 total = git_get_log_estimate_commit_count(m_DllGitLog);
2940 catch (const char* msg)
2942 MessageBox(L"Could not get first commit.\nlibgit reports:\n" + CUnicodeUtils::GetUnicode(msg), L"TortoiseGit", MB_ICONERROR);
2943 ret = -1;
2945 g_Git.m_critGitDllSec.Unlock();
2947 if (CGitMailmap::ShouldLoadMailmap())
2948 GitRevLoglist::s_Mailmap = std::make_shared<CGitMailmap>();
2949 else if (GitRevLoglist::s_Mailmap.load())
2950 GitRevLoglist::s_Mailmap.store(nullptr);
2951 auto mailmap{ GitRevLoglist::s_Mailmap.load() };
2953 auto hashMapSharedPtr{ m_HashMap.load() };
2954 const auto& hashMap = *hashMapSharedPtr;
2956 auto rollUpStatesSharedPtr{ m_RollUpStates.load() };
2957 const auto &rollUpStates = *rollUpStatesSharedPtr;
2958 std::unordered_set<CGitHash> collapsedNodes, expandedNodes;
2960 GIT_COMMIT commit;
2961 t2 = t1 = GetTickCount64();
2962 int oldprecentage = 0;
2963 size_t oldsize = m_logEntries.size();
2964 std::unordered_map<CGitHash, std::unordered_set<CGitHash>> commitChildren;
2965 while (ret== 0 && !m_bExitThread)
2967 g_Git.m_critGitDllSec.Lock();
2970 [&] { ret = git_get_log_nextcommit(this->m_DllGitLog, &commit, m_ShowMask & CGit::LOG_INFO_FOLLOW); } ();
2972 catch (const char* msg)
2974 g_Git.m_critGitDllSec.Unlock();
2975 MessageBox(L"Could not get next commit.\nlibgit reports:\n" + CUnicodeUtils::GetUnicode(msg), L"TortoiseGit", MB_ICONERROR);
2976 break;
2979 if(ret)
2981 g_Git.m_critGitDllSec.Unlock();
2982 if (ret != -2) // other than end of revision walking
2983 MessageBox((L"Could not get next commit.\nlibgit returns:" + std::to_wstring(ret)).c_str(), L"TortoiseGit", MB_ICONERROR);
2984 break;
2987 if (commit.m_ignore == 1)
2989 git_free_commit(&commit);
2990 g_Git.m_critGitDllSec.Unlock();
2991 continue;
2994 //printf("%s\r\n",commit.GetSubject());
2995 if(m_bExitThread)
2997 git_free_commit(&commit);
2998 g_Git.m_critGitDllSec.Unlock();
2999 break;
3002 CGitHash hash = CGitHash::FromRaw(commit.m_hash);
3004 GitRevLoglist* pRev = m_LogCache.GetCacheData(hash);
3005 pRev->Parse(&commit, mailmap.get()); // better parse here than on GITLOG_END in LogDlg::OnLogListLoading for updating the DateSelectors
3007 char* note = nullptr;
3010 git_get_notes(commit.m_hash, &note);
3012 catch (const char* msg)
3014 g_Git.m_critGitDllSec.Unlock();
3015 MessageBox(L"Could not get commit notes.\nlibgit reports:\n" + CUnicodeUtils::GetUnicode(msg), L"TortoiseGit", MB_ICONERROR);
3016 break;
3019 if(note)
3021 pRev->m_Notes = CUnicodeUtils::GetUnicode(note);
3022 free(note);
3023 note = nullptr;
3025 git_free_commit(&commit);
3026 g_Git.m_critGitDllSec.Unlock();
3028 if(!pRev->m_IsDiffFiles)
3030 pRev->m_CallDiffAsync = DiffAsync;
3033 if (m_ShowFilter & FILTERSHOW_MERGEPOINTS) // See also ShouldShowFilter()
3035 for (size_t i = 0; i < pRev->m_ParentHash.size(); ++i)
3037 const CGitHash &parentHash = pRev->m_ParentHash[i];
3038 auto it = commitChildren.find(parentHash);
3039 if (it == commitChildren.end())
3040 it = commitChildren.insert(make_pair(parentHash, std::unordered_set<CGitHash>())).first;
3041 it->second.insert(pRev->m_CommitHash);
3045 #ifdef DEBUG
3046 //pRev->DbgPrint();
3047 #endif
3048 bool visible;
3049 if (rollUpStates.empty() || !(m_ShowFilter & FILTERSHOW_MERGEPOINTS))
3051 visible = ShouldShowFilter(pRev, commitChildren, hashMap);
3052 pRev->m_RolledUp = !ShouldShowAnyFilter();
3053 pRev->m_RolledUpIsForced = false;
3055 else // !(rollUpStates.empty())
3057 auto itChildren = commitChildren.find(pRev->m_CommitHash);
3058 const bool hasChildren = itChildren != commitChildren.end();
3059 const bool isFork = hasChildren && (itChildren->second.size() > 1);
3060 const bool isChildExpanded = hasChildren && !isFork && expandedNodes.count(*itChildren->second.begin());
3061 const bool isChildCollapsed = hasChildren && !isFork && !isChildExpanded && collapsedNodes.count(*itChildren->second.begin());
3063 auto itForcedState = rollUpStates.find(pRev->m_CommitHash);
3064 const bool hasForcedState = itForcedState != rollUpStates.end();
3065 const bool forcedRollUp = hasForcedState && (itForcedState->second == RollUpState::Collapse);
3067 const bool shouldShowAny = ShouldShowAnyFilter();
3068 const bool shouldShowSpecial = ShouldShowRefsFilter(pRev, hashMap) || ShouldShowMergePointsFilter(pRev, commitChildren);
3070 const bool isSpecial = isFork || shouldShowSpecial;
3071 const bool isRegular = !isSpecial;
3073 visible = isSpecial || isChildExpanded || (shouldShowAny && !isChildCollapsed);
3075 const bool rolledUpByDefault = (isSpecial && !shouldShowAny) || (isRegular && isChildCollapsed);
3076 pRev->m_RolledUp = (visible && hasForcedState) ? forcedRollUp : rolledUpByDefault;
3077 pRev->m_RolledUpIsForced = (pRev->m_RolledUp != rolledUpByDefault);
3079 if (pRev->m_RolledUp)
3080 collapsedNodes.insert(pRev->m_CommitHash);
3081 else
3082 expandedNodes.insert(pRev->m_CommitHash);
3085 if (visible && !filter(pRev, this, hashMap))
3086 visible = false;
3087 this->m_critSec.Lock();
3088 m_logEntries.append(hash, visible, m_ShowMask & CGit::LOG_INFO_FIRST_PARENT);
3089 if (visible)
3090 m_arShownList.push_back(pRev); // push_back is ok here, because we use the very same lock, otherwise use SafeAdd
3091 this->m_critSec.Unlock();
3093 if (!visible)
3094 continue;
3096 if (lastSelectedHashNItem == -1 && hash == m_lastSelectedHash)
3097 lastSelectedHashNItem = static_cast<int>(m_arShownList.size()) - 1;
3099 t2 = GetTickCount64();
3101 if (t2 - t1 > 500UL || (m_logEntries.size() - oldsize > 100))
3103 //update UI
3104 int percent = static_cast<int>(m_logEntries.size() * 100 / (total + 1));
3105 if(percent > 99)
3106 percent =99;
3107 if (percent <= GITLOG_START)
3108 percent = GITLOG_START +1;
3110 oldsize = m_logEntries.size();
3111 PostMessage(LVM_SETITEMCOUNT, this->m_logEntries.size(), LVSICF_NOINVALIDATEALL|LVSICF_NOSCROLL);
3113 if (percent > oldprecentage)
3115 ::PostMessage(this->GetParent()->m_hWnd,MSG_LOAD_PERCENTAGE, percent, 0);
3116 oldprecentage = percent;
3119 if (lastSelectedHashNItem >= 0)
3120 PostMessage(m_ScrollToMessage, lastSelectedHashNItem);
3122 t1 = t2;
3125 g_Git.m_critGitDllSec.Lock();
3126 git_close_log(m_DllGitLog, 1);
3127 g_Git.m_critGitDllSec.Unlock();
3130 if (m_bExitThread)
3132 InterlockedExchange(&s_bThreadRunning, FALSE);
3133 return 0;
3136 //Update UI;
3137 PostMessage(LVM_SETITEMCOUNT, this->m_logEntries.size(), LVSICF_NOINVALIDATEALL|LVSICF_NOSCROLL);
3139 if (lastSelectedHashNItem >= 0)
3140 PostMessage(m_ScrollToMessage, lastSelectedHashNItem);
3142 if (this->m_hWnd)
3143 ::PostMessage(this->GetParent()->m_hWnd,MSG_LOAD_PERCENTAGE, GITLOG_END, 0);
3145 InterlockedExchange(&s_bThreadRunning, FALSE);
3147 return 0;
3150 void CGitLogListBase::FetchRemoteList()
3152 STRING_VECTOR remoteList;
3153 m_SingleRemote.Empty();
3154 if (!g_Git.GetRemoteList(remoteList) && remoteList.size() == 1)
3155 m_SingleRemote = remoteList[0];
3158 void CGitLogListBase::FetchTrackingBranchList()
3160 m_TrackingMap.clear();
3161 auto hashMap{ m_HashMap.load() };
3162 for (auto it = hashMap->cbegin(); it != hashMap->cend(); ++it)
3164 for (const auto& ref : it->second)
3166 CString branchName;
3167 if (CGit::GetShortName(ref, branchName, L"refs/heads/"))
3169 CString pullRemote, pullBranch;
3170 g_Git.GetRemoteTrackedBranch(branchName, pullRemote, pullBranch);
3171 if (!pullRemote.IsEmpty() && !pullBranch.IsEmpty())
3172 m_TrackingMap[branchName] = std::make_pair(pullRemote, pullBranch);
3178 void CGitLogListBase::Refresh(BOOL IsCleanFilter)
3180 SafeTerminateThread();
3182 this->SetItemCountEx(0);
3183 this->Clear();
3185 ResetWcRev();
3187 ShowGraphColumn((m_ShowMask & CGit::LOG_INFO_FOLLOW) ? false : true);
3189 //Update branch and Tag info
3190 ReloadHashMap();
3191 if (m_pFindDialog)
3192 m_pFindDialog->RefreshList();
3193 //Assume Thread have exited
3194 //if(!s_bThreadRunning)
3196 m_logEntries.clear();
3198 if (IsCleanFilter)
3199 m_LogFilter = std::make_shared<CLogDlgFilter>();
3201 SafeTerminateAsyncDiffThread();
3202 m_AsynDiffListLock.Lock();
3203 m_AsynDiffList.clear();
3204 m_AsynDiffListLock.Unlock();
3205 StartAsyncDiffThread();
3207 StartLoadingThread();
3211 void CGitLogListBase::StartAsyncDiffThread()
3213 if (m_AsyncThreadExit)
3214 return;
3215 if (InterlockedExchange(&m_AsyncThreadRunning, TRUE) != FALSE)
3216 return;
3217 m_DiffingThread = AfxBeginThread(AsyncThread, this, THREAD_PRIORITY_BELOW_NORMAL, 0, CREATE_SUSPENDED);
3218 if (!m_DiffingThread)
3220 InterlockedExchange(&m_AsyncThreadRunning, FALSE);
3221 CMessageBox::Show(GetSafeHwnd(), IDS_ERR_THREADSTARTFAILED, IDS_APPNAME, MB_OK | MB_ICONERROR);
3222 return;
3224 m_DiffingThread->m_bAutoDelete = FALSE;
3225 m_DiffingThread->ResumeThread();
3228 void CGitLogListBase::StartLoadingThread()
3230 if (InterlockedExchange(&s_bThreadRunning, TRUE) != FALSE)
3231 return;
3232 InterlockedExchange(&m_bNoDispUpdates, TRUE);
3233 InterlockedExchange(&m_bExitThread, FALSE);
3234 m_LoadingThread = AfxBeginThread(LogThreadEntry, this, THREAD_PRIORITY_LOWEST, 0, CREATE_SUSPENDED);
3235 if (!m_LoadingThread)
3237 InterlockedExchange(&s_bThreadRunning, FALSE);
3238 InterlockedExchange(&m_bNoDispUpdates, FALSE);
3239 CMessageBox::Show(GetSafeHwnd(), IDS_ERR_THREADSTARTFAILED, IDS_APPNAME, MB_OK | MB_ICONERROR);
3240 return;
3242 m_LoadingThread->m_bAutoDelete = FALSE;
3243 m_LoadingThread->ResumeThread();
3246 bool CGitLogListBase::ShouldShowAnyFilter()
3248 return m_ShowFilter & FILTERSHOW_ANYCOMMIT;
3251 bool CGitLogListBase::ShouldShowRefsFilter(GitRevLoglist* pRev, const MAP_HASH_NAME& hashMap)
3253 if (!(m_ShowFilter & FILTERSHOW_REFS))
3254 return false;
3256 // Keep the head.
3257 if (pRev->m_CommitHash == m_HeadHash)
3258 return true;
3260 // Keep all refs.
3261 auto refsIt = hashMap.find(pRev->m_CommitHash);
3262 if (refsIt == hashMap.cend())
3263 return false;
3264 const auto& refList = refsIt->second;
3265 for (const CString &str : refList)
3267 if (CStringUtils::StartsWith(str, L"refs/heads/"))
3269 if (m_ShowRefMask & LOGLIST_SHOWLOCALBRANCHES)
3270 return true;
3272 else if (CStringUtils::StartsWith(str, L"refs/remotes/"))
3274 if (m_ShowRefMask & LOGLIST_SHOWREMOTEBRANCHES)
3275 return true;
3277 else if (CStringUtils::StartsWith(str, L"refs/tags/"))
3279 if (m_ShowRefMask & LOGLIST_SHOWTAGS)
3280 return true;
3282 else if (CStringUtils::StartsWith(str, L"refs/stash"))
3284 if (m_ShowRefMask & LOGLIST_SHOWSTASH)
3285 return true;
3287 else if (CStringUtils::StartsWith(str, L"refs/bisect/"))
3289 if (m_ShowRefMask & LOGLIST_SHOWBISECT)
3290 return true;
3293 return false;
3296 bool CGitLogListBase::ShouldShowMergePointsFilter(GitRevLoglist* pRev, const std::unordered_map<CGitHash, std::unordered_set<CGitHash>>& commitChildren)
3298 if (!(m_ShowFilter & FILTERSHOW_MERGEPOINTS))
3299 return false;
3301 if (pRev->ParentsCount() > 1)
3302 return true;
3304 if (auto childrenIt = commitChildren.find(pRev->m_CommitHash); childrenIt != commitChildren.end())
3306 const std::unordered_set<CGitHash> &children = childrenIt->second;
3307 if (children.size() > 1)
3308 return true;
3310 return false;
3313 bool CGitLogListBase::ShouldShowFilter(GitRevLoglist* pRev, const std::unordered_map<CGitHash, std::unordered_set<CGitHash>>& commitChildren, const MAP_HASH_NAME& hashMap)
3315 return ShouldShowAnyFilter() || ShouldShowRefsFilter(pRev, hashMap) || ShouldShowMergePointsFilter(pRev, commitChildren);
3318 void CGitLogListBase::ShowGraphColumn(bool bShow)
3320 // HACK to hide graph column
3321 if (bShow)
3322 SetColumnWidth(0, m_ColumnManager.GetWidth(0, false));
3323 else
3324 SetColumnWidth(0, 0);
3327 CString CGitLogListBase::GetTagInfo(GitRev* pLogEntry) const
3329 auto hashMap{ m_HashMap.load() };
3330 auto refs = hashMap->find(pLogEntry->m_CommitHash);
3331 if (refs == hashMap->end())
3332 return L"";
3334 return GetTagInfo(refs->second);
3337 CString CGitLogListBase::GetTagInfo(const STRING_VECTOR& refs) const
3339 CString tagInfo;
3340 for (auto it = refs.cbegin(); it != refs.cend(); ++it)
3342 if (!CStringUtils::StartsWith((*it), L"refs/tags/"))
3343 continue;
3344 if (!CStringUtils::EndsWith((*it), L"^{}"))
3345 continue;
3347 CString cmd;
3348 cmd.Format(L"git.exe cat-file tag %s", static_cast<LPCWSTR>((*it).Left((*it).GetLength() - static_cast<int>(wcslen(L"^{}")))));
3349 CString output;
3350 if (g_Git.Run(cmd, &output, nullptr, CP_UTF8) != 0)
3351 continue;
3353 // parse tag date
3356 // this assumes that in the header of the tag there is no ">" before the "tagger " header entry
3357 int pos1 = output.Find(L'>');
3358 if (pos1 < 0)
3359 break;
3360 ++pos1;
3361 if (output[pos1] == L' ')
3362 ++pos1;
3363 int pos2 = output.Find(L'\n', pos1);
3364 if (pos2 < 0)
3365 break;
3367 CString str = output.Mid(pos1, pos2 - pos1);
3368 wchar_t* pEnd = nullptr;
3369 errno = 0;
3370 auto number = wcstoumax(str.GetBuffer(), &pEnd, 10);
3371 if (str.GetBuffer() == pEnd)
3372 break;
3373 if (errno == ERANGE)
3374 break;
3376 output.Delete(pos1, pos2 - pos1);
3377 output.Insert(pos1, static_cast<LPCWSTR>(CLoglistUtils::FormatDateAndTime(CTime(number), m_DateFormat, true, m_bRelativeTimes)));
3378 } while (0);
3379 output.Trim().AppendChar(L'\n');
3380 tagInfo += output;
3382 return tagInfo;
3385 void CGitLogListBase::Clear()
3387 m_arShownList.SafeRemoveAll();
3388 DeleteAllItems();
3390 m_logEntries.ClearAll();
3393 void CGitLogListBase::OnDestroy()
3395 SafeTerminateThread();
3396 SafeTerminateAsyncDiffThread();
3398 int retry = 0;
3399 while(m_LogCache.SaveCache())
3401 if(retry > 5)
3402 break;
3403 Sleep(1000);
3405 ++retry;
3407 //if(CMessageBox::Show(nullptr, L"Cannot Save Log Cache to Disk. To retry click yes. To give up click no.", L"TortoiseGit",
3408 // MB_YESNO) == IDNO)
3409 // break;
3412 __super::OnDestroy();
3415 LRESULT CGitLogListBase::OnLoad(WPARAM wParam,LPARAM /*lParam*/)
3417 CRect rect;
3418 const int i = static_cast<int>(wParam);
3419 this->GetItemRect(i,&rect,LVIR_BOUNDS);
3420 this->InvalidateRect(rect);
3422 return 0;
3426 * Save column widths to the registry
3428 void CGitLogListBase::SaveColumnWidths()
3430 // HACK that graph column is always shown
3431 if (m_ColumnManager.GetColumnCount() > 0)
3432 SetColumnWidth(0, m_ColumnManager.GetWidth(0, false));
3434 __super::SaveColumnWidths();
3437 int CGitLogListBase::GetHeadIndex()
3439 if(m_HeadHash.IsEmpty())
3440 return -1;
3442 for (size_t i = 0; i < m_arShownList.size(); ++i)
3444 GitRev* pRev = m_arShownList.SafeGetAt(i);
3445 if(pRev)
3447 if (pRev->m_CommitHash == m_HeadHash)
3448 return static_cast<int>(i);
3451 return -1;
3453 void CGitLogListBase::OnFind()
3455 if (!m_pFindDialog)
3457 m_pFindDialog = new CFindDlg(this);
3458 m_pFindDialog->Create(this);
3460 else
3462 m_pFindDialog->SetFocus();
3463 return;
3467 LRESULT CGitLogListBase::OnScrollToMessage(WPARAM itemToSelect, LPARAM /*lParam*/)
3469 if (GetSelectedCount() != 0)
3470 return 0;
3472 CGitHash theSelectedHash = m_lastSelectedHash;
3473 SetItemState(static_cast<int>(itemToSelect), LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
3474 m_lastSelectedHash = theSelectedHash;
3476 const int countPerPage = GetCountPerPage();
3477 EnsureVisible(max(0, static_cast<int>(itemToSelect) - countPerPage / 2), FALSE);
3478 EnsureVisible(min(GetItemCount(), static_cast<int>(itemToSelect) + countPerPage / 2), FALSE);
3479 EnsureVisible(static_cast<int>(itemToSelect), FALSE);
3480 return 0;
3483 LRESULT CGitLogListBase::OnScrollToRef(WPARAM wParam, LPARAM /*lParam*/)
3485 CString* ref = reinterpret_cast<CString*>(wParam);
3486 if (!ref || ref->IsEmpty())
3487 return 1;
3489 const bool bShift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
3491 CGitHash hash;
3492 if (g_Git.GetHash(hash, *ref + L"^{}")) // add ^{} in order to get the correct SHA-1 (especially for signed tags)
3493 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of ref \"" + *ref + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
3495 if (hash.IsEmpty())
3496 return 1;
3498 bool bFound = false;
3499 const int cnt = static_cast<int>(m_arShownList.size());
3500 int i;
3501 for (i = 0; i < cnt; ++i)
3503 GitRev* pLogEntry = m_arShownList.SafeGetAt(i);
3504 if (pLogEntry && pLogEntry->m_CommitHash == hash)
3506 bFound = true;
3507 break;
3510 if (!bFound)
3511 return 1;
3513 EnsureVisible(i, FALSE);
3514 if (!bShift)
3516 SetItemState(GetSelectionMark(), 0, LVIS_SELECTED);
3517 SetItemState(i, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
3518 SetSelectionMark(i);
3520 else
3522 GitRev* pLogEntry = m_arShownList.SafeGetAt(i);
3523 if (pLogEntry)
3524 m_highlight = pLogEntry->m_CommitHash;
3526 Invalidate();
3527 UpdateData(FALSE);
3529 return 0;
3532 LRESULT CGitLogListBase::OnFindDialogMessage(WPARAM /*wParam*/, LPARAM /*lParam*/)
3534 ASSERT(m_pFindDialog);
3535 bool bFound = false;
3536 int i=0;
3538 if (m_pFindDialog->IsTerminating())
3540 // invalidate the handle identifying the dialog box.
3541 m_pFindDialog = nullptr;
3542 return 0;
3545 const int cnt = static_cast<int>(m_arShownList.size());
3546 const bool bShift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
3548 if(m_pFindDialog->IsRef())
3550 CString str;
3551 str=m_pFindDialog->GetFindString();
3553 CGitHash hash;
3555 if(!str.IsEmpty())
3557 if (g_Git.GetHash(hash, str + L"^{}")) // add ^{} in order to get the correct SHA-1 (especially for signed tags)
3558 MessageBox(g_Git.GetGitLastErr(L"Could not get hash of ref \"" + str + L"^{}\"."), L"TortoiseGit", MB_ICONERROR);
3561 if(!hash.IsEmpty())
3563 for (i = 0; i < cnt; ++i)
3565 GitRev* pLogEntry = m_arShownList.SafeGetAt(i);
3566 if(pLogEntry && pLogEntry->m_CommitHash == hash)
3568 bFound = true;
3569 break;
3573 if (!bFound)
3575 m_pFindDialog->FlashWindowEx(FLASHW_ALL, 2, 100);
3576 return 0;
3580 if (m_pFindDialog->FindNext() && !bFound)
3582 //read data from dialog
3583 CLogDlgFilter filter { m_pFindDialog->GetFindString(), m_pFindDialog->Regex(), LOGFILTER_ALL, m_pFindDialog->MatchCase() == TRUE };
3585 auto hashMapSharedPtr{ m_HashMap.load() };
3586 auto& hashMap = *hashMapSharedPtr;
3588 for (i = m_nSearchIndex + 1; ; ++i)
3590 if (i >= cnt)
3592 i = 0;
3593 m_pFindDialog->FlashWindowEx(FLASHW_ALL, 2, 100);
3595 if (m_nSearchIndex >= 0)
3597 if (i == m_nSearchIndex)
3599 ::MessageBeep(0xFFFFFFFF);
3600 m_pFindDialog->FlashWindowEx(FLASHW_ALL, 3, 100);
3601 break;
3605 if (filter(m_arShownList.SafeGetAt(i), this, hashMap))
3607 bFound = true;
3608 break;
3611 } // if(m_pFindDialog->FindNext())
3613 if (bFound)
3615 m_nSearchIndex = i;
3616 EnsureVisible(i, FALSE);
3617 if (!bShift)
3619 SetItemState(GetSelectionMark(), 0, LVIS_SELECTED);
3620 SetItemState(i, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
3621 SetSelectionMark(i);
3623 else
3625 GitRev* pLogEntry = m_arShownList.SafeGetAt(i);
3626 if (pLogEntry)
3627 m_highlight = pLogEntry->m_CommitHash;
3629 Invalidate();
3630 //FillLogMessageCtrl();
3631 UpdateData(FALSE);
3634 return 0;
3637 INT_PTR CGitLogListBase::OnToolHitTest(CPoint point, TOOLINFO * pTI) const
3639 LVHITTESTINFO lvhitTestInfo;
3641 lvhitTestInfo.pt = point;
3643 const int nItem = ListView_SubItemHitTest(m_hWnd, &lvhitTestInfo);
3644 const int nSubItem = lvhitTestInfo.iSubItem;
3646 UINT nFlags = lvhitTestInfo.flags;
3648 // nFlags is 0 if the SubItemHitTest fails
3649 // Therefore, 0 & <anything> will equal false
3650 if (nFlags & LVHT_ONITEM)
3652 // Get the client area occupied by this control
3653 RECT rcClient;
3654 GetClientRect(&rcClient);
3656 // Fill in the TOOLINFO structure
3657 pTI->hwnd = m_hWnd;
3658 pTI->uId = static_cast<UINT>((nItem << 10) + (nSubItem & 0x3ff) + 1);
3659 pTI->lpszText = LPSTR_TEXTCALLBACK;
3660 pTI->rect = rcClient;
3662 return pTI->uId; // By returning a unique value per listItem,
3663 // we ensure that when the mouse moves over another list item,
3664 // the tooltip will change
3666 else
3668 // Otherwise, we aren't interested, so let the message propagate
3669 return -1;
3673 BOOL CGitLogListBase::OnToolTipText(UINT /*id*/, NMHDR* pNMHDR, LRESULT* pResult)
3675 auto pTTTA = reinterpret_cast<TOOLTIPTEXTA*>(pNMHDR);
3676 auto pTTTW = reinterpret_cast<TOOLTIPTEXTW*>(pNMHDR);
3678 *pResult = 0;
3680 // Ignore messages from the built in tooltip, we are processing them internally
3681 if ((pNMHDR->idFrom == reinterpret_cast<UINT_PTR>(m_hWnd)) &&
3682 (((pNMHDR->code == TTN_NEEDTEXTA) && (pTTTA->uFlags & TTF_IDISHWND)) ||
3683 ((pNMHDR->code == TTN_NEEDTEXTW) && (pTTTW->uFlags & TTF_IDISHWND))))
3684 return FALSE;
3686 // Get the mouse position
3687 const MSG* pMessage = GetCurrentMessage();
3689 CPoint pt;
3690 pt = pMessage->pt;
3691 ScreenToClient(&pt);
3693 // Check if the point falls onto a list item
3694 LVHITTESTINFO lvhitTestInfo;
3695 lvhitTestInfo.pt = pt;
3697 const int nItem = SubItemHitTest(&lvhitTestInfo);
3699 if (lvhitTestInfo.flags & LVHT_ONITEM)
3701 // Get branch description first
3702 CString strTipText;
3703 if (lvhitTestInfo.iSubItem == LOGLIST_MESSAGE)
3705 CString branch;
3706 CGit::REF_TYPE type = CGit::REF_TYPE::LOCAL_BRANCH;
3707 auto hashMap{ m_HashMap.load() };
3708 if (IsMouseOnRefLabel(m_arShownList.SafeGetAt(nItem), lvhitTestInfo.pt, type, *hashMap.get(), &branch))
3710 MAP_STRING_STRING descriptions;
3711 g_Git.GetBranchDescriptions(descriptions);
3712 if (descriptions.find(branch) != descriptions.cend())
3714 strTipText.LoadString(IDS_DESCRIPTION);
3715 strTipText += L":\n";
3716 strTipText += descriptions[branch];
3721 bool followMousePos = false;
3722 if (!strTipText.IsEmpty())
3723 followMousePos = true;
3724 else
3725 strTipText = GetToolTipText(nItem, lvhitTestInfo.iSubItem);
3726 if (strTipText.IsEmpty())
3727 return FALSE;
3729 // we want multiline tooltips
3730 ::SendMessage(pNMHDR->hwndFrom, TTM_SETMAXTIPWIDTH, 0, SHRT_MAX);
3732 if (strTipText.GetLength() >= _countof(m_wszTip))
3734 strTipText.Truncate(_countof(m_wszTip) - 1 - 3);
3735 strTipText += L"...";
3737 wcsncpy_s(m_wszTip, strTipText, _TRUNCATE);
3738 // handle Unicode as well as non-Unicode requests
3739 if (pNMHDR->code == TTN_NEEDTEXTA)
3741 pTTTA->hinst = nullptr;
3742 pTTTA->lpszText = m_szTip;
3743 ::WideCharToMultiByte(CP_ACP, 0, m_wszTip, -1, m_szTip, 8192, nullptr, nullptr);
3745 else
3747 pTTTW->hinst = nullptr;
3748 pTTTW->lpszText = m_wszTip;
3751 CRect rect;
3752 GetSubItemRect(nItem, lvhitTestInfo.iSubItem, LVIR_LABEL, rect);
3753 if (followMousePos)
3754 rect.MoveToXY(pt.x, pt.y + CDPIAware::Instance().ScaleY(GetSafeHwnd(), 18)); // 18: to act like a normal tooltip
3755 ClientToScreen(rect);
3756 ::SetWindowPos(pNMHDR->hwndFrom, HWND_TOP, rect.left, rect.top, 0, 0, SWP_NOACTIVATE|SWP_NOSIZE|SWP_NOOWNERZORDER);
3758 return TRUE; // We found a tool tip,
3759 // tell the framework this message has been handled
3762 return FALSE; // We didn't handle the message,
3763 // let the framework continue propagating the message
3766 CString CGitLogListBase::GetToolTipText(int nItem, int nSubItem)
3768 if (nSubItem == LOGLIST_MESSAGE && !m_bTagsBranchesOnRightSide)
3770 GitRevLoglist* pLogEntry = m_arShownList.SafeGetAt(nItem);
3771 if (pLogEntry == nullptr)
3772 return CString();
3773 auto hashMap{ m_HashMap.load() };
3774 if (hashMap->find(pLogEntry->m_CommitHash) == hashMap->cend() && !m_submoduleInfo.AnyMatches(pLogEntry->m_CommitHash))
3775 return CString();
3776 return pLogEntry->GetSubject();
3778 else if (nSubItem == LOGLIST_DATE && m_bRelativeTimes)
3780 GitRevLoglist* pLogEntry = m_arShownList.SafeGetAt(nItem);
3781 if (pLogEntry == nullptr)
3782 return CString();
3783 return CLoglistUtils::FormatDateAndTime(pLogEntry->GetAuthorDate(), m_DateFormat, true, false);
3785 else if (nSubItem == LOGLIST_COMMIT_DATE && m_bRelativeTimes)
3787 GitRevLoglist* pLogEntry = m_arShownList.SafeGetAt(nItem);
3788 if (pLogEntry == nullptr)
3789 return CString();
3790 return CLoglistUtils::FormatDateAndTime(pLogEntry->GetCommitterDate(), m_DateFormat, true, false);
3792 else if (nSubItem == LOGLIST_ACTION)
3794 GitRevLoglist* pLogEntry = m_arShownList.SafeGetAt(nItem);
3795 if (pLogEntry == nullptr)
3796 return CString();
3798 const int actions = pLogEntry->GetAction(this);
3799 if (!pLogEntry->m_IsDiffFiles)
3800 return CString(MAKEINTRESOURCE(IDS_PROC_LOG_FETCHINGFILES));
3801 else if (pLogEntry->m_IsDiffFiles == 2)
3802 return CString(MAKEINTRESOURCE(IDS_PROC_LOG_FETCHFILESERROR));
3804 CString actionText;
3805 if (actions & CTGitPath::LOGACTIONS_MODIFIED)
3806 actionText += CTGitPath::GetActionName(CTGitPath::LOGACTIONS_MODIFIED);
3808 if (actions & CTGitPath::LOGACTIONS_ADDED)
3810 if (!actionText.IsEmpty())
3811 actionText += L"\r\n";
3812 actionText += CTGitPath::GetActionName(CTGitPath::LOGACTIONS_ADDED);
3815 if (actions & CTGitPath::LOGACTIONS_DELETED)
3817 if (!actionText.IsEmpty())
3818 actionText += L"\r\n";
3819 actionText += CTGitPath::GetActionName(CTGitPath::LOGACTIONS_DELETED);
3822 if (actions & CTGitPath::LOGACTIONS_REPLACED)
3824 if (!actionText.IsEmpty())
3825 actionText += L"\r\n";
3826 actionText += CTGitPath::GetActionName(CTGitPath::LOGACTIONS_REPLACED);
3829 if (actions & CTGitPath::LOGACTIONS_UNMERGED)
3831 if (!actionText.IsEmpty())
3832 actionText += L"\r\n";
3833 actionText += CTGitPath::GetActionName(CTGitPath::LOGACTIONS_UNMERGED);
3836 if (!actionText.IsEmpty())
3838 CString sTitle(MAKEINTRESOURCE(IDS_LOG_ACTIONS));
3839 return sTitle + L":\r\n" + actionText;
3842 return CString();
3845 bool CGitLogListBase::IsMouseOnRefLabelFromPopupMenu(const GitRevLoglist* pLogEntry, const CPoint& point, CGit::REF_TYPE& type, const MAP_HASH_NAME& hashMap, CString* pShortname /*nullptr*/, size_t* pIndex /*nullptr*/)
3847 POINT pt = point;
3848 ScreenToClient(&pt);
3849 return IsMouseOnRefLabel(pLogEntry, pt, type, hashMap, pShortname, pIndex);
3852 bool CGitLogListBase::IsMouseOnRefLabel(const GitRevLoglist* pLogEntry, const POINT& pt, CGit::REF_TYPE& type, const MAP_HASH_NAME& hashMap, CString* pShortname /*nullptr*/, size_t* pIndex /*nullptr*/)
3854 if (!pLogEntry)
3855 return false;
3857 auto refList = hashMap.find(pLogEntry->m_CommitHash);
3858 if (refList == hashMap.cend())
3859 return false;
3861 for (size_t i = 0; i < refList->second.size(); ++i)
3863 const auto labelpos = m_RefLabelPosMap.find(refList->second[i]);
3864 if (labelpos == m_RefLabelPosMap.cend() || !labelpos->second.PtInRect(pt))
3865 continue;
3867 CGit::REF_TYPE foundType;
3868 if (pShortname)
3869 *pShortname = CGit::GetShortName(refList->second[i], &foundType);
3870 else
3871 CGit::GetShortName(refList->second[i], &foundType);
3872 if (foundType != type && type != CGit::REF_TYPE::UNKNOWN)
3873 return false;
3875 type = foundType;
3876 if (pIndex)
3877 *pIndex = i;
3878 return true;
3880 return false;
3883 void CGitLogListBase::OnBeginDrag(NMHDR* /*pnmhdr*/, LRESULT* pResult)
3885 *pResult = 0;
3887 if (!m_bDragndropEnabled || GetSelectedCount() == 0 || !IsSelectionContinuous())
3888 return;
3890 m_bDragging = true;
3891 m_nDropIndex = -1;
3892 m_nDropMarkerLast = -1;
3893 m_nDropMarkerLastHot = GetHotItem();
3894 SetCapture();
3897 void CGitLogListBase::OnMouseMove(UINT nFlags, CPoint point)
3899 __super::OnMouseMove(nFlags, point);
3901 if (!m_bDragging)
3902 return;
3904 CPoint dropPoint = point;
3905 ClientToScreen(&dropPoint);
3907 if (WindowFromPoint(dropPoint) != this)
3909 SetCursor(LoadCursor(nullptr, IDC_NO));
3910 m_nDropIndex = -1;
3911 DrawDropInsertMarker(m_nDropIndex);
3912 return;
3915 SetCursor(LoadCursor(nullptr, IDC_ARROW));
3916 ScreenToClient(&dropPoint);
3918 dropPoint.y += 10;
3919 m_nDropIndex = HitTest(dropPoint);
3921 if (m_nDropIndex == -1) // might be last item, allow to move past last item
3923 dropPoint.y -= 10;
3924 m_nDropIndex = HitTest(dropPoint);
3925 if (m_nDropIndex != -1)
3926 m_nDropIndex = GetItemCount();
3929 POSITION pos = GetFirstSelectedItemPosition();
3930 const int first = GetNextSelectedItem(pos);
3931 int last = first;
3932 while (pos)
3933 last = GetNextSelectedItem(pos);
3934 if (m_nDropIndex == -1 || (m_nDropIndex >= first && m_nDropIndex - 1 <= last))
3936 SetCursor(LoadCursor(nullptr, IDC_NO));
3937 m_nDropIndex = -1;
3940 // handle auto scrolling
3941 const int hotItem = GetHotItem();
3942 const int topindex = GetTopIndex();
3943 if (hotItem == topindex && hotItem != 0)
3944 EnsureVisible(hotItem - 1, FALSE);
3945 else if (hotItem >= topindex + GetCountPerPage() - 1 && hotItem + 1 < GetItemCount())
3946 EnsureVisible(hotItem + 1, FALSE);
3948 DrawDropInsertMarker(m_nDropIndex);
3951 void CGitLogListBase::OnLButtonUp(UINT nFlags, CPoint point)
3953 if (m_bDragging)
3955 ::ReleaseCapture();
3956 SetCursor(LoadCursor(nullptr, IDC_HAND));
3957 m_bDragging = false;
3959 CRect rect;
3960 GetItemRect(m_nDropMarkerLast, &rect, 0);
3961 rect.bottom = rect.top + 2;
3962 rect.top -= 2;
3963 InvalidateRect(&rect, 0);
3965 CPoint pt(point);
3966 ClientToScreen(&pt);
3967 if (WindowFromPoint(pt) == this && m_nDropIndex != -1)
3968 GetParent()->PostMessage(MSG_COMMITS_REORDERED, m_nDropIndex, 0);
3971 __super::OnLButtonUp(nFlags, point);
3974 void CGitLogListBase::DrawDropInsertMarker(int nIndex)
3976 if (m_nDropMarkerLast != nIndex)
3978 CRect rect;
3979 if (GetItemRect(m_nDropMarkerLast, &rect, 0))
3981 rect.bottom = rect.top + 2;
3982 rect.top -= 2;
3983 InvalidateRect(&rect, 0);
3985 else if (m_nDropMarkerLast == GetItemCount())
3986 DrawDropInsertMarkerLine(m_nDropMarkerLast); // double painting = removal
3987 m_nDropMarkerLast = nIndex;
3989 if (nIndex < 0)
3990 return;
3992 DrawDropInsertMarkerLine(m_nDropMarkerLast);
3994 else if (m_nDropMarkerLastHot != GetHotItem())
3996 m_nDropMarkerLastHot = GetHotItem();
3997 m_nDropMarkerLast = -1;
4001 void CGitLogListBase::DrawDropInsertMarkerLine(int nIndex)
4003 CBrush* pBrush = CDC::GetHalftoneBrush();
4004 CDC* pDC = GetDC();
4006 CRect rect;
4007 if (nIndex < GetItemCount())
4009 GetItemRect(nIndex, &rect, 0);
4010 rect.bottom = rect.top + 2;
4011 rect.top -= 2;
4013 else
4015 GetItemRect(nIndex - 1, &rect, 0);
4016 rect.top = rect.bottom - 2;
4017 rect.bottom += 2;
4020 CBrush* pBrushOld = pDC->SelectObject(pBrush);
4021 pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATINVERT);
4022 pDC->SelectObject(pBrushOld);
4024 ReleaseDC(pDC);
4027 ULONG CGitLogListBase::GetGestureStatus(CPoint /*ptTouch*/)
4029 return 0;
4032 void CGitLogListBase::DrawListItemWithMatchesRect(NMLVCUSTOMDRAW* pLVCD, const std::vector<CHARRANGE>& ranges, CRect rect, const CString& text, CColors& colors, HTHEME hTheme /*= nullptr*/, int txtState /*= 0*/)
4034 int drawPos = 0;
4035 COLORREF textColor = pLVCD->clrText;
4036 RECT rc = rect;
4037 if (!hTheme)
4039 ::SetTextColor(pLVCD->nmcd.hdc, textColor);
4040 SetBkMode(pLVCD->nmcd.hdc, TRANSPARENT);
4042 DTTOPTS opts = { 0 };
4043 opts.dwSize = sizeof(opts);
4044 opts.crText = textColor;
4045 opts.dwFlags = DTT_TEXTCOLOR;
4047 for (auto it = ranges.cbegin(); it != ranges.cend(); ++it)
4049 rc = rect;
4050 if (it->cpMin - drawPos)
4052 if (!hTheme)
4054 DrawText(pLVCD->nmcd.hdc, text.Mid(drawPos), it->cpMin - drawPos, &rc, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS);
4055 DrawText(pLVCD->nmcd.hdc, text.Mid(drawPos), it->cpMin - drawPos, &rc, DT_CALCRECT | DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS);
4057 else
4059 DrawThemeTextEx(hTheme, pLVCD->nmcd.hdc, LVP_LISTITEM, txtState, text.Mid(drawPos), it->cpMin - drawPos, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS, &rc, &opts);
4060 GetThemeTextExtent(hTheme, pLVCD->nmcd.hdc, LVP_LISTITEM, txtState, text.Mid(drawPos), it->cpMin - drawPos, DT_CALCRECT | DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS, &rect, &rc);
4062 rect.left = rc.right;
4064 rc = rect;
4065 drawPos = it->cpMin;
4066 if (it->cpMax - drawPos)
4068 if (!hTheme)
4070 ::SetTextColor(pLVCD->nmcd.hdc, CTheme::Instance().GetThemeColor(colors.GetColor(CColors::FilterMatch), true));
4071 DrawText(pLVCD->nmcd.hdc, text.Mid(drawPos), it->cpMax - drawPos, &rc, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS);
4072 DrawText(pLVCD->nmcd.hdc, text.Mid(drawPos), it->cpMax - drawPos, &rc, DT_CALCRECT | DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS);
4073 ::SetTextColor(pLVCD->nmcd.hdc, textColor);
4075 else
4077 opts.crText = CTheme::Instance().GetThemeColor(colors.GetColor(CColors::FilterMatch), true);
4078 DrawThemeTextEx(hTheme, pLVCD->nmcd.hdc, LVP_LISTITEM, txtState, text.Mid(drawPos), it->cpMax - drawPos, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS, &rc, &opts);
4079 GetThemeTextExtent(hTheme, pLVCD->nmcd.hdc, LVP_LISTITEM, txtState, text.Mid(drawPos), it->cpMax - drawPos, DT_CALCRECT | DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS, &rect, &rc);
4080 opts.crText = textColor;
4082 rect.left = rc.right;
4084 rc = rect;
4085 drawPos = it->cpMax;
4087 if (!hTheme)
4088 DrawText(pLVCD->nmcd.hdc, text.Mid(drawPos), -1, &rc, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS);
4089 else
4090 DrawThemeTextEx(hTheme, pLVCD->nmcd.hdc, LVP_LISTITEM, txtState, text.Mid(drawPos), -1, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS, &rc, &opts);
4093 bool CGitLogListBase::DrawListItemWithMatchesIfEnabled(std::shared_ptr<CLogDlgFilter> filter, DWORD selectedFilter, NMLVCUSTOMDRAW* pLVCD, LRESULT* pResult)
4095 if ((filter->GetSelectedFilters() & selectedFilter) && filter->IsFilterActive())
4097 CRect rect;
4098 GetSubItemRect(static_cast<int>(pLVCD->nmcd.dwItemSpec), pLVCD->iSubItem, LVIR_BOUNDS, rect);
4100 FillBackGround(pLVCD->nmcd.hdc, pLVCD->nmcd.dwItemSpec, rect);
4102 *pResult = DrawListItemWithMatches(filter.get(), *this, pLVCD, m_Colors);
4103 return true;
4105 return false;
4108 LRESULT CGitLogListBase::DrawListItemWithMatches(CFilterHelper* filter, CListCtrl& listCtrl, NMLVCUSTOMDRAW* pLVCD, CColors& colors)
4110 CString text = static_cast<LPCWSTR>(listCtrl.GetItemText(static_cast<int>(pLVCD->nmcd.dwItemSpec), pLVCD->iSubItem));
4111 if (text.IsEmpty())
4112 return CDRF_DODEFAULT;
4114 std::vector<CHARRANGE> ranges;
4115 filter->GetMatchRanges(ranges, text, 0);
4116 if (ranges.empty())
4117 return CDRF_DODEFAULT;
4119 // even though we initialize the 'rect' here with nmcd.rc,
4120 // we must not use it but use the rects from GetItemRect()
4121 // and GetSubItemRect(). Because on XP, the nmcd.rc has
4122 // bogus data in it.
4123 CRect rect = pLVCD->nmcd.rc;
4125 // find the margin where the text label starts
4126 CRect labelRC, boundsRC, iconRC;
4127 listCtrl.GetItemRect(static_cast<int>(pLVCD->nmcd.dwItemSpec), &labelRC, LVIR_LABEL);
4128 listCtrl.GetItemRect(static_cast<int>(pLVCD->nmcd.dwItemSpec), &iconRC, LVIR_ICON);
4129 listCtrl.GetItemRect(static_cast<int>(pLVCD->nmcd.dwItemSpec), &boundsRC, LVIR_BOUNDS);
4131 int leftmargin = labelRC.left - boundsRC.left;
4132 if (pLVCD->iSubItem)
4133 leftmargin -= iconRC.Width();
4135 if (pLVCD->iSubItem != 0)
4136 listCtrl.GetSubItemRect(static_cast<int>(pLVCD->nmcd.dwItemSpec), pLVCD->iSubItem, LVIR_BOUNDS, rect);
4138 int borderWidth = 0;
4139 CAutoThemeData hTheme;
4140 if (IsAppThemed())
4142 hTheme = OpenThemeData(listCtrl.m_hWnd, L"Explorer::ListView;ListView");
4143 GetThemeMetric(hTheme, pLVCD->nmcd.hdc, LVP_LISTITEM, LISS_NORMAL, TMT_BORDERSIZE, &borderWidth);
4145 else
4146 borderWidth = GetSystemMetrics(SM_CXBORDER);
4148 if (listCtrl.GetExtendedStyle() & LVS_EX_CHECKBOXES)
4150 // I'm not very happy about this fixed margin here
4151 // but I haven't found a way to ask the system what
4152 // the margin really is.
4153 // At least it works on XP/Vista/win7/win8, and even with
4154 // increased font sizes
4155 leftmargin = 4;
4158 LVITEM item = { 0 };
4159 item.iItem = static_cast<int>(pLVCD->nmcd.dwItemSpec);
4160 item.iSubItem = 0;
4161 item.mask = LVIF_IMAGE | LVIF_STATE;
4162 item.stateMask = static_cast<UINT>(-1);
4163 listCtrl.GetItem(&item);
4165 // fill background
4166 int txtState = LISS_NORMAL;
4167 if (!hTheme)
4169 HBRUSH brush = nullptr;
4170 if (item.state & LVIS_SELECTED)
4172 if (::GetFocus() == listCtrl.GetSafeHwnd())
4174 brush = ::CreateSolidBrush(::GetSysColor(COLOR_HIGHLIGHT));
4175 pLVCD->clrText = ::GetSysColor(COLOR_HIGHLIGHTTEXT);
4177 else
4179 brush = ::CreateSolidBrush(::GetSysColor(COLOR_BTNFACE));
4180 pLVCD->clrText = CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor : GetSysColor(COLOR_WINDOWTEXT);
4184 else
4185 brush = ::CreateSolidBrush(::GetSysColor(COLOR_WINDOW));
4186 CRect my;
4187 listCtrl.GetSubItemRect(static_cast<int>(pLVCD->nmcd.dwItemSpec), pLVCD->iSubItem, LVIR_LABEL, my);
4188 ::FillRect(pLVCD->nmcd.hdc, my, brush);
4189 ::DeleteObject(brush);
4191 else
4193 if (listCtrl.GetHotItem() == static_cast<int>(pLVCD->nmcd.dwItemSpec))
4195 if (item.state & LVIS_SELECTED)
4196 txtState = LISS_HOTSELECTED;
4197 else
4198 txtState = LISS_HOT;
4200 else if (item.state & LVIS_SELECTED)
4202 if (::GetFocus() == listCtrl.GetSafeHwnd())
4203 txtState = LISS_SELECTED;
4204 else
4205 txtState = LISS_SELECTEDNOTFOCUS;
4208 if (IsThemeBackgroundPartiallyTransparent(hTheme, LVP_LISTDETAIL, txtState))
4209 DrawThemeParentBackground(listCtrl.m_hWnd, pLVCD->nmcd.hdc, &rect);
4210 else
4212 HBRUSH brush = ::CreateSolidBrush(pLVCD->clrTextBk);
4213 ::FillRect(pLVCD->nmcd.hdc, rect, brush);
4214 ::DeleteObject(brush);
4216 if (txtState != LISS_NORMAL)
4218 CRect my;
4219 listCtrl.GetSubItemRect(static_cast<int>(pLVCD->nmcd.dwItemSpec), pLVCD->iSubItem, LVIR_LABEL, my);
4220 if (pLVCD->iSubItem == 0)
4222 // also fill the icon part of the line
4223 my.top = 0;
4224 my.left = 0;
4227 // calculate background for rect of whole line, but limit redrawing to SubItem rect
4228 DrawThemeBackground(hTheme, pLVCD->nmcd.hdc, LVP_LISTITEM, txtState, boundsRC, my);
4232 // draw the icon for the first column
4233 if (pLVCD->iSubItem == 0)
4235 rect = boundsRC;
4236 rect.right = rect.left + listCtrl.GetColumnWidth(0);
4237 rect.left = iconRC.left;
4239 if (item.iImage >= 0)
4241 POINT pt;
4242 pt.x = rect.left;
4243 pt.y = rect.top;
4244 CDC dc;
4245 dc.Attach(pLVCD->nmcd.hdc);
4246 int style = ILD_TRANSPARENT;
4247 if (!hTheme)
4249 auto whitebrush = ::CreateSolidBrush(::GetSysColor(COLOR_WINDOW));
4250 ::FillRect(dc, iconRC, whitebrush);
4251 ::DeleteObject(whitebrush);
4252 if (item.state & LVIS_SELECTED)
4254 if (::GetFocus() == listCtrl.GetSafeHwnd())
4255 style = ILD_SELECTED;
4256 else
4257 style = ILD_FOCUS;
4260 listCtrl.GetImageList(LVSIL_SMALL)->Draw(&dc, item.iImage, pt, style);
4261 dc.Detach();
4262 leftmargin -= iconRC.left;
4264 else
4266 RECT irc = boundsRC;
4267 irc.left += borderWidth;
4268 irc.right = iconRC.left;
4270 int state = 0;
4271 if (item.state & LVIS_SELECTED)
4273 if (listCtrl.GetHotItem() == item.iItem)
4274 state = CBS_CHECKEDHOT;
4275 else
4276 state = CBS_CHECKEDNORMAL;
4278 else
4280 if (listCtrl.GetHotItem() == item.iItem)
4281 state = CBS_UNCHECKEDHOT;
4283 if ((state) && (listCtrl.GetExtendedStyle() & LVS_EX_CHECKBOXES))
4285 CAutoThemeData hTheme2 = OpenThemeData(listCtrl.m_hWnd, L"BUTTON");
4286 DrawThemeBackground(hTheme2, pLVCD->nmcd.hdc, BP_CHECKBOX, state, &irc, NULL);
4290 InflateRect(&rect, -(2 * borderWidth), 0);
4292 rect.left += leftmargin;
4293 RECT rc = rect;
4295 // is the column left- or right-aligned? (we don't handle centered (yet))
4296 LVCOLUMN Column;
4297 Column.mask = LVCF_FMT;
4298 listCtrl.GetColumn(pLVCD->iSubItem, &Column);
4299 if (Column.fmt & LVCFMT_RIGHT)
4301 DrawText(pLVCD->nmcd.hdc, text, -1, &rc, DT_CALCRECT | DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS);
4302 rect.left = rect.right - (rc.right - rc.left);
4303 if (!hTheme)
4305 rect.left += 2 * borderWidth;
4306 rect.right += 2 * borderWidth;
4310 DrawListItemWithMatchesRect(pLVCD, ranges, rect, text, colors, hTheme, txtState);
4312 return CDRF_SKIPDEFAULT;