1
// TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2003-2011 - TortoiseSVN
4 // Copyright (C) 2012-2023 - TortoiseGit
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 #include "TortoiseProc.h"
23 #include "Revisiongraphdlg.h"
26 #include "UnicodeUtils.h"
34 static char THIS_FILE
[] = __FILE__
;
37 using namespace Gdiplus
;
39 void CRevisionGraphWnd::InitView()
41 m_bIsCanvasMove
= false;
46 void CRevisionGraphWnd::BuildPreview()
48 m_Preview
.DeleteObject();
52 // is there a point in drawing this at all?
54 int nodeCount
= this->m_Graph
.numberOfNodes();
55 if ((nodeCount
> REVGRAPH_PREVIEW_MAX_NODES
) || (nodeCount
== 0))
58 float origZoom
= m_fZoomFactor
;
60 CRect clientRect
= GetClientRect();
61 CSize
preViewSize(max(CDPIAware::Instance().ScaleX(GetSafeHwnd(), REVGRAPH_PREVIEW_WIDTH
), clientRect
.Width() / 4),
62 max(CDPIAware::Instance().ScaleY(GetSafeHwnd(), REVGRAPH_PREVIEW_HEIGHT
), clientRect
.Height() / 4));
64 // zoom the graph so that it is completely visible in the window
65 CRect graphRect
= GetGraphRect();
66 float horzfact
= float(graphRect
.Width())/float(preViewSize
.cx
);
67 float vertfact
= float(graphRect
.Height())/float(preViewSize
.cy
);
68 m_previewZoom
= min (DEFAULT_ZOOM
, 1.0f
/(max(horzfact
, vertfact
)));
70 // make sure the preview window has a minimal size
72 m_previewWidth
= min(static_cast<LONG
>(max(graphRect
.Width() * m_previewZoom
, 30.0f
)), preViewSize
.cx
);
73 m_previewHeight
= min(static_cast<LONG
>(max(graphRect
.Height() * m_previewZoom
, 30.0f
)), preViewSize
.cy
);
77 if (!dc
.CreateCompatibleDC(&ddc
))
80 m_Preview
.CreateCompatibleBitmap(&ddc
, m_previewWidth
, m_previewHeight
);
81 HBITMAP oldbm
= static_cast<HBITMAP
>(dc
.SelectObject (m_Preview
));
83 // paint the whole graph
84 DoZoom (m_previewZoom
, false);
85 CRect
rect (0, 0, m_previewWidth
, m_previewHeight
);
88 DrawGraph(dev
, rect
, 0, 0, true);
90 // now we have a bitmap the size of the preview window
91 dc
.SelectObject(oldbm
);
94 DoZoom (origZoom
, false);
97 void CRevisionGraphWnd::SetScrollbar (int bar
, int newPos
, int clientMax
, int graphMax
)
99 SCROLLINFO ScrollInfo
= {sizeof(SCROLLINFO
), SIF_ALL
};
100 GetScrollInfo (bar
, &ScrollInfo
);
102 clientMax
= max(1, clientMax
);
103 int oldHeight
= ScrollInfo
.nMax
<= 0 ? clientMax
: ScrollInfo
.nMax
;
104 int newHeight
= static_cast<int>(graphMax
* m_fZoomFactor
);
105 int maxPos
= max (0, newHeight
- clientMax
);
106 int pos
= min (maxPos
, newPos
>= 0
108 : ScrollInfo
.nPos
* newHeight
/ oldHeight
);
110 ScrollInfo
.nPos
= pos
;
112 ScrollInfo
.nMax
= newHeight
;
113 ScrollInfo
.nPage
= clientMax
;
114 ScrollInfo
.nTrackPos
= pos
;
116 SetScrollInfo(bar
, &ScrollInfo
);
119 void CRevisionGraphWnd::SetScrollbars (int nVert
, int nHorz
)
121 CRect clientrect
= GetClientRect();
122 const CRect
& pRect
= GetGraphRect();
124 SetScrollbar (SB_VERT
, nVert
, clientrect
.Height(), pRect
.Height());
125 SetScrollbar (SB_HORZ
, nHorz
, clientrect
.Width(), pRect
.Width());
128 CRect
CRevisionGraphWnd::GetGraphRect()
133 CRect
CRevisionGraphWnd::GetClientRect()
136 CWnd::GetClientRect (&clientRect
);
140 CRect
CRevisionGraphWnd::GetWindowRect()
143 CWnd::GetWindowRect (&windowRect
);
147 CRect
CRevisionGraphWnd::GetViewRect()
150 result
.UnionRect (GetClientRect(), GetGraphRect());
154 int CRevisionGraphWnd::GetEncoderClsid(const WCHAR
* format
, CLSID
* pClsid
)
156 UINT num
= 0; // number of image encoders
157 UINT size
= 0; // size of the image encoder array in bytes
159 if (GetImageEncodersSize(&num
, &size
)!=Ok
)
162 return -1; // Failure
164 auto pImageCodecInfo
= reinterpret_cast<ImageCodecInfo
*>(malloc(size
));
165 if (!pImageCodecInfo
)
166 return -1; // Failure
168 if (GetImageEncoders(num
, size
, pImageCodecInfo
)==Ok
)
170 for(UINT j
= 0; j
< num
; ++j
)
172 if( wcscmp(pImageCodecInfo
[j
].MimeType
, format
) == 0 )
174 *pClsid
= pImageCodecInfo
[j
].Clsid
;
175 free(pImageCodecInfo
);
181 free(pImageCodecInfo
);
182 return -1; // Failure
185 bool CRevisionGraphWnd::FetchRevisionData
186 ( const CString
& /*path*/
187 , CProgressDlg
* /*progress*/
188 , ITaskbarList3
* /*pTaskbarList*/
191 this->m_LogCache
.ClearAllParent();
192 this->m_logEntries
.ClearAll();
194 DWORD infomask
= CGit::LOG_INFO_SIMPILFY_BY_DECORATION
| (m_bShowBranchingsMerges
? CGit::LOG_INFO_SPARSE
: 0);
196 if (!m_FromRev
.IsEmpty())
198 // Format string. (Ex. from "branch1 branch2 branch3" to " ^branch1 ^branch2 ^branch3")
201 const std::wsregex_iterator end
;
202 std::wstring string
{ static_cast<LPCWSTR
>(m_FromRev
) };
203 std::wregex
regex(L
"[^\\s]+");
204 for (std::wsregex_iterator
it(string
.cbegin(), string
.cend(), regex
); it
!= end
; ++it
)
206 range
+= L
" ^" + g_Git
.FixBranchName(it
->str().c_str());
209 catch (std::exception
&) {}
212 if (!m_bLocalBranches
&& !m_bCurrentBranch
&& !m_ToRev
.IsEmpty())
216 const std::wsregex_iterator end
;
217 std::wstring string
{ static_cast<LPCWSTR
>(m_ToRev
) };
218 std::wregex
regex(L
"[^\\s]+");
219 for (std::wsregex_iterator
it(string
.cbegin(), string
.cend(), regex
); it
!= end
; ++it
)
221 range
+= L
' ' + g_Git
.FixBranchName(it
->str().c_str());
224 catch (std::exception
&) {}
228 if (m_bLocalBranches
)
229 infomask
|= CGit::LOG_INFO_ALWAYS_APPLY_RANGE
| CGit::LOG_INFO_LOCAL_BRANCHES
;
230 else if (m_bCurrentBranch
)
233 infomask
|= CGit::LOG_INFO_ALWAYS_APPLY_RANGE
| CGit::LOG_INFO_ALL_BRANCH
;
236 m_logEntries
.ParserFromLog(nullptr, 0, infomask
, &range
);
239 this->m_Graph
.clear();
241 g_Git
.GetSubmodulePointer(m_submoduleInfo
);
244 if (!m_bShowAllTags
|| m_bShowBranchingsMerges
)
246 std::unordered_map
<CGitHash
, std::vector
<CGitHash
>> childMap
;
247 for (size_t i
= 0; i
< m_logEntries
.size(); ++i
)
249 const GitRev
& rev
= m_logEntries
.GetGitRevAt(i
);
250 std::for_each(rev
.m_ParentHash
.cbegin(), rev
.m_ParentHash
.cend(), [&](const auto& parent
) { childMap
[parent
].push_back(m_logEntries
[i
]); });
254 std::unordered_set
<CGitHash
> skipList
;
255 for (size_t i
= 0; i
< m_logEntries
.size(); ++i
)
257 auto& rev
= m_logEntries
.GetGitRevAt(i
);
259 // keep labeled commits
260 const bool isSuperRepoHash
= m_submoduleInfo
.AnyMatches(rev
.m_CommitHash
);
261 if (auto foundNames
= m_HashMap
.find(rev
.m_CommitHash
); foundNames
!= m_HashMap
.cend() || isSuperRepoHash
)
263 // by default any label is enough to keep this commit visible
264 if (m_bShowAllTags
|| isSuperRepoHash
)
267 // if hiding tags, check if there are any branch names for this commit
268 bool haveNonTagNames
= false;
269 for (auto name
: foundNames
->second
)
271 CGit::REF_TYPE refType
;
272 CGit::GetShortName(name
, &refType
);
273 if (refType
!= CGit::REF_TYPE::ANNOTATED_TAG
&& refType
!= CGit::REF_TYPE::TAG
)
275 haveNonTagNames
= true;
283 if (rev
.m_ParentHash
.size() != 1)
286 auto childIt
= childMap
.find(rev
.m_CommitHash
);
287 if (childIt
== childMap
.cend() || childIt
->second
.size() != 1)
290 auto& childRev
= m_logEntries
.GetGitRevAt(m_logEntries
.m_HashMap
.find(childIt
->second
[0])->second
);
291 if (childRev
.m_ParentHash
.size() != 1)
294 // it's ok to erase this commit
295 skipList
.insert(rev
.m_CommitHash
);
297 childRev
.m_ParentHash
[0] = rev
.m_ParentHash
[0];
299 auto& parentChildren
= childMap
[rev
.m_ParentHash
[0]];
300 if (auto it
= std::find(parentChildren
.begin(), parentChildren
.end(), rev
.m_CommitHash
); it
!= parentChildren
.end())
301 *it
= childIt
->second
[0];
303 childMap
.erase(childIt
);
307 m_logEntries
.erase(std::remove_if(m_logEntries
.begin(), m_logEntries
.end(), [&skipList
](const auto& hash
) { return skipList
.find(hash
) != skipList
.cend(); }), m_logEntries
.end());
308 m_logEntries
.m_HashMap
.clear();
309 for (size_t i
= 0; i
< m_logEntries
.size(); ++i
)
310 m_logEntries
.m_HashMap
[m_logEntries
[i
]] = i
;
313 // build revision graph
314 CArray
<ogdf::node
> nodes
;
316 dev
.pDC
= this->GetDC();
317 dev
.graphics
= Graphics::FromHDC(dev
.pDC
->m_hDC
);
318 dev
.graphics
->SetPageUnit (UnitPixel
);
320 Gdiplus::Font
font(CAppUtils::GetLogFontName(), static_cast<REAL
>(m_nFontSize
), FontStyleRegular
);
322 MeasureTextLength(dev
, font
, CString(L
'8', g_Git
.GetShortHASHLength()), commitString
.Width
, commitString
.Height
);
324 m_HeadNode
= nullptr;
326 for (const auto& hash
: m_logEntries
)
328 auto nd
= m_Graph
.newNode();
330 SetNodeRect(dev
, font
, commitString
, &nd
, hash
);
331 if (hash
== m_HeadHash
)
335 for (size_t i
= 0; i
< m_logEntries
.size(); ++i
)
337 const GitRev
& rev
= m_logEntries
.GetGitRevAt(i
);
338 for (size_t j
= 0; j
< rev
.m_ParentHash
.size(); ++j
)
340 auto parentId
= m_logEntries
.m_HashMap
.find(rev
.m_ParentHash
[j
]);
341 if (parentId
== m_logEntries
.m_HashMap
.end())
343 TRACE(L
"Can't found parent node");
344 //new parent node as new node
345 auto nd
= this->m_Graph
.newNode();
346 m_Graph
.newEdge(nodes
[i
], nd
);
347 m_logEntries
.push_back(rev
.m_ParentHash
[j
]);
348 m_logEntries
.m_HashMap
[rev
.m_ParentHash
[j
]] = m_logEntries
.size() - 1;
350 SetNodeRect(dev
, font
, commitString
, &nd
, rev
.m_ParentHash
[j
]);
354 TRACE(L
"edge %d - %d\n",i
, m_logEntries
.m_HashMap
[rev
.m_ParentHash
[j
]]);
355 m_Graph
.newEdge(nodes
[i
], nodes
[parentId
->second
]);
360 //this->m_OHL.layerDistance(30.0);
361 //this->m_OHL.nodeDistance(25.0);
362 //this->m_OHL.weightBalancing(0.8);
364 m_SugiyamLayout
.call(m_GraphAttr
);
368 for (auto v
: m_Graph
.nodes
)
370 double x
= m_GraphAttr
.x(v
) + m_GraphAttr
.width(v
)/2;
371 double y
= m_GraphAttr
.y(v
) + m_GraphAttr
.height(v
)/2;
377 this->m_GraphRect
.top
=m_GraphRect
.left
=0;
378 m_GraphRect
.bottom
= static_cast<LONG
>(ymax
);
379 m_GraphRect
.right
= static_cast<LONG
>(xmax
);
384 bool CRevisionGraphWnd::IsUpdateJobRunning() const
386 return (updateJob
.get() != nullptr) && !updateJob
->IsDone();
389 bool CRevisionGraphWnd::GetShowOverview() const
391 return m_bShowOverview
;
394 void CRevisionGraphWnd::SetShowOverview (bool value
)
396 m_bShowOverview
= value
;
400 CString
CRevisionGraphWnd::GetFriendRefName(ogdf::node v
) const
404 const CGitHash
& hash
= this->m_logEntries
[v
->index()];
405 if (const auto refsIt
= m_HashMap
.find(hash
); refsIt
== m_HashMap
.end())
406 return hash
.ToString();
408 return refsIt
->second
[0];
411 STRING_VECTOR
CRevisionGraphWnd::GetFriendRefNames(ogdf::node v
, const CString
* exclude
, CGit::REF_TYPE
* onlyRefType
) const
414 return STRING_VECTOR();
415 const CGitHash
& hash
= m_logEntries
[v
->index()];
416 if (const auto refsIt
= m_HashMap
.find(hash
); refsIt
== m_HashMap
.end())
417 return STRING_VECTOR();
421 for (const auto& ref
: refsIt
->second
)
423 CGit::REF_TYPE refType
;
424 CString shortName
= CGit::GetShortName(ref
, &refType
);
425 if (exclude
&& *exclude
== shortName
)
429 else if (*onlyRefType
== refType
)
430 list
.push_back(shortName
);
436 void CRevisionGraphWnd::CompareRevs(const CString
& revTo
)
438 ASSERT(m_SelectedEntry1
);
439 ASSERT(!revTo
.IsEmpty() || m_SelectedEntry2
);
441 bool alternativeTool
= !!(GetAsyncKeyState(VK_SHIFT
) & 0x8000);
445 sCmd
.Format(L
"/command:showcompare %s /revision1:%s /revision2:%s",
446 this->m_sPath
.IsEmpty() ? L
"" : static_cast<LPCWSTR
>(L
"/path:\"" + this->m_sPath
+ L
'"'),
447 static_cast<LPCWSTR
>(GetFriendRefName(m_SelectedEntry1
)),
448 !revTo
.IsEmpty() ? static_cast<LPCWSTR
>(revTo
) : static_cast<LPCWSTR
>(GetFriendRefName(m_SelectedEntry2
)));
451 sCmd
+= L
" /alternative";
453 CAppUtils::RunTortoiseGitProc(sCmd
);
456 void CRevisionGraphWnd::UnifiedDiffRevs(bool bHead
)
458 ASSERT(m_SelectedEntry1
);
459 ASSERT(bHead
|| m_SelectedEntry2
);
461 bool alternativeTool
= !!(GetAsyncKeyState(VK_SHIFT
) & 0x8000);
462 CAppUtils::StartShowUnifiedDiff(m_hWnd
, CString(), GetFriendRefName(m_SelectedEntry1
), CString(),
463 bHead
? CString("HEAD") : GetFriendRefName(m_SelectedEntry2
),
467 void CRevisionGraphWnd::DoZoom (float fZoomFactor
, bool updateScrollbars
)
469 float oldzoom
= m_fZoomFactor
;
470 m_fZoomFactor
= fZoomFactor
;
472 m_nFontSize
= max(1, int(DEFAULT_ZOOM_FONT
* fZoomFactor
));
473 if (m_nFontSize
< SMALL_ZOOM_FONT_THRESHOLD
)
474 m_nFontSize
= min(static_cast<int>(SMALL_ZOOM_FONT_THRESHOLD
), int(SMALL_ZOOM_FONT
* fZoomFactor
));
476 if (updateScrollbars
)
478 SCROLLINFO si1
= {sizeof(SCROLLINFO
), SIF_ALL
};
479 GetScrollInfo(SB_VERT
, &si1
);
480 SCROLLINFO si2
= {sizeof(SCROLLINFO
), SIF_ALL
};
481 GetScrollInfo(SB_HORZ
, &si2
);
485 si1
.nPos
= int(float(si1
.nPos
)*m_fZoomFactor
/oldzoom
);
486 si2
.nPos
= int(float(si2
.nPos
)*m_fZoomFactor
/oldzoom
);
487 SetScrollPos (SB_VERT
, si1
.nPos
);
488 SetScrollPos (SB_HORZ
, si2
.nPos
);