From 7097d7cbb3e4418f5add012a5ac114e26b63b3dd Mon Sep 17 00:00:00 2001 From: Michael Seibt Date: Mon, 10 Sep 2018 01:02:59 +0200 Subject: [PATCH] #5125 RevisionGrid Graph: Nearest branch in tooltip --- .../Columns/GraphColumnProvider.cs | 189 ++++++++++++++++-- .../RevisionGridToolTipProvider.cs | 9 + 2 files changed, 186 insertions(+), 12 deletions(-) diff --git a/GitUI/UserControls/RevisionGrid/Columns/GraphColumnProvider.cs b/GitUI/UserControls/RevisionGrid/Columns/GraphColumnProvider.cs index 53367109a02..5cd01c50050 100644 --- a/GitUI/UserControls/RevisionGrid/Columns/GraphColumnProvider.cs +++ b/GitUI/UserControls/RevisionGrid/Columns/GraphColumnProvider.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Windows.Forms; using GitCommands; using GitExtUtils.GitUI; @@ -508,14 +510,8 @@ private void ClearDrawCache() public override bool TryGetToolTip(DataGridViewCellMouseEventArgs e, GitRevision revision, out string toolTip) { - if (!revision.IsArtificial) - { - toolTip = GetLaneInfo(e.X - ColumnLeftMargin, e.RowIndex); - return true; - } - - toolTip = default; - return false; + toolTip = GetLaneInfo(e.X - ColumnLeftMargin, e.RowIndex); + return true; string GetLaneInfo(int x, int rowIndex) { @@ -532,7 +528,7 @@ string GetLaneInfo(int x, int rowIndex) node = laneRow.Node; if (!node.Revision.IsArtificial) { - laneInfoText.AppendLine(node.Revision.Guid); + laneInfoText.Append("* "); } } else if (lane >= 0 && lane < laneRow.Count) @@ -556,12 +552,38 @@ string GetLaneInfo(int x, int rowIndex) if (node != null) { - if (laneInfoText.Length > 0) + if (!node.Revision.IsArtificial) { - laneInfoText.AppendLine(); + laneInfoText.AppendLine(node.Revision.Guid); + + var references = new References(node); + + if (references.CommittedTo.IsNotNullOrWhitespace()) + { + laneInfoText.AppendFormat("\nBranch: {0}", references.CommittedTo); + if (references.MergedWith.IsNotNullOrWhitespace()) + { + laneInfoText.AppendFormat(" (merged with {0})", references.MergedWith); + } + } + + // laneInfoText.Append(references.DebugInfo); + + laneInfoText.AppendLine().AppendLine(); } - laneInfoText.Append(node.Revision.Body ?? node.Revision.Subject); + if (node.Revision.Body != null) + { + laneInfoText.Append(node.Revision.Body.TrimEnd()); + } + else + { + laneInfoText.Append(node.Revision.Subject); + if (node.Revision.HasMultiLineMessage) + { + laneInfoText.Append("\n\nFull message text is not present in older commits.\nSelect this commit to populate the full message."); + } + } } } } @@ -569,5 +591,148 @@ string GetLaneInfo(int x, int rowIndex) return laneInfoText.ToString(); } } + + private class References + { + private HashSet _visitedNodes = new HashSet(); + internal string CommittedTo { get; private set; } + internal string MergedWith { get; private set; } + internal StringBuilder DebugInfo = new StringBuilder(); + + internal References([NotNull] Node node) + { + AddReferencesOf(node, previousDescJunction: null); + } + + private bool AddReferencesOf([NotNull] Node node, [CanBeNull] Junction previousDescJunction) + { + if (_visitedNodes.Add(node)) + { + if (CheckForMerge(node, previousDescJunction) || FindBranch(node, node, previousDescJunction)) + { + return true; + } + + foreach (var descJunction in node.Descendants) + { + // iterate the inner nodes (i.e. excluding the youngest) beginning with the oldest + bool nodeFound = false; + for (int nodeIndex = descJunction.NodeCount - 1; nodeIndex > 0; --nodeIndex) + { + Node innerNode = descJunction[nodeIndex]; + if (nodeFound) + { + if (FindBranch(innerNode, node, descJunction)) + { + return true; + } + } + else + { + nodeFound = innerNode == node; + } + } + + // handle the youngest and its descendants + if (AddReferencesOf(descJunction.Youngest, descJunction)) + { + return true; + } + } + } + + return false; + } + + private bool FindBranch([NotNull] Node descNode, [NotNull] Node node, [CanBeNull] Junction descJunction) + { + foreach (var gitReference in descNode.Revision.Refs) + { + if (gitReference.IsHead || gitReference.IsRemote) + { + CheckForMerge(node, descJunction); + CommittedTo = CommittedTo ?? gitReference.Name; + return true; + } + else if (gitReference.IsStash && CommittedTo == null) + { + CommittedTo = gitReference.Name; + return true; + } + } + + return false; + } + + /// + /// Checks whether the commit message is a merge message + /// and then if its a merge message, sets CommittedTo and MergedWith. + /// + /// MergedWith is set if it is the current node, i.e. on the first call. + /// MergedWith is set to string.Empty if it is no merge. + /// First/second branch does not matter because it is the message of the current node. + /// + /// the node of the revision to evaluate + /// + /// the descending junction the node is part of + /// (used for the decision whether the node belongs the first or second branch of the merge) + /// + private bool CheckForMerge([NotNull] Node node, [CanBeNull] Junction descJunction) + { + bool isTheFirstBranch = descJunction == null || node.Ancestors.Count == 0 || node.Ancestors.First() == descJunction; + string mergedInto; + string mergedWith; + (mergedInto, mergedWith) = ParseMergeMessage(node, appendPullRequest: isTheFirstBranch); + + if (mergedInto != null) + { + CommittedTo = isTheFirstBranch ? mergedInto : mergedWith; + } + + if (MergedWith == null) + { + MergedWith = mergedWith ?? string.Empty; + } + + if (CommittedTo != null) + { + DebugInfo.AppendFormat(" node: {0}, branch: {1}, descJunction {2} isTheFirstBranch {3}\n", + node.Revision.Guid.Substring(0, 8), CommittedTo, descJunction?.ToString() ?? "(null)", isTheFirstBranch); + foreach (var ancJunction in node.Ancestors) + { + DebugInfo.AppendFormat(" {2}anc {0} -> {1} {3}\n", + ancJunction.Oldest.Revision.Guid.Substring(0, 8), + ancJunction.Youngest.Revision.Guid.Substring(0, 8), + ancJunction == descJunction ? ">\x200A" : " ", + ancJunction == descJunction ? CommittedTo + " " + MergedWith : ""); + } + } + + return CommittedTo != null; + } + + private static readonly Regex _merge = new Regex("(?i)^merged? (pull request (.*) from )?(.*branch )?'?([^ ']+)'?( into (.*))?\\.?$"); + + private static (string into, string with) ParseMergeMessage([NotNull] Node node, bool appendPullRequest = false) + { + string into = null; + string with = null; + Match match = _merge.Match(node.Revision.Subject); + if (match.Success) + { + var matchPullRequest = match.Groups[2]; + var matchWith = match.Groups[4]; + var matchInto = match.Groups[6]; + into = matchInto.Success ? matchInto.Value : "master"; + with = matchWith.Success ? matchWith.Value : "?"; + if (appendPullRequest && matchPullRequest.Success) + { + with += string.Format(" by pull request {0}", matchPullRequest); + } + } + + return (into, with); + } + } } } \ No newline at end of file diff --git a/GitUI/UserControls/RevisionGrid/RevisionGridToolTipProvider.cs b/GitUI/UserControls/RevisionGrid/RevisionGridToolTipProvider.cs index 91af87f843e..b06e2e478f6 100644 --- a/GitUI/UserControls/RevisionGrid/RevisionGridToolTipProvider.cs +++ b/GitUI/UserControls/RevisionGrid/RevisionGridToolTipProvider.cs @@ -53,6 +53,15 @@ string GetToolTipText() provider.TryGetToolTip(e, revision, out var toolTip) && !string.IsNullOrWhiteSpace(toolTip)) { + int lineCount = 0; + for (int pos = 0, length = toolTip.Length; pos < length; ++pos) + { + if (toolTip[pos] == '\n' && ++lineCount == 30) + { + return toolTip.Substring(0, pos + 1) + ">>> TOOLTIP TRUNCATED <<<"; + } + } + return toolTip; }