From 745d45c083daff2d2cebcfb2f974962aedeb22cb Mon Sep 17 00:00:00 2001 From: Michael Seibt Date: Mon, 10 Sep 2018 01:02:59 +0200 Subject: [PATCH] #5125 RevisionGrid Graph: Branches and tags in tooltip. --- .../Columns/CommitIdColumnProvider.cs | 21 +- .../Columns/GraphColumnProvider.cs | 235 +++++++++++++++++- .../RevisionGridToolTipProvider.cs | 9 + 3 files changed, 253 insertions(+), 12 deletions(-) diff --git a/GitUI/UserControls/RevisionGrid/Columns/CommitIdColumnProvider.cs b/GitUI/UserControls/RevisionGrid/Columns/CommitIdColumnProvider.cs index fed8f27d4ed..f2df984f3a7 100644 --- a/GitUI/UserControls/RevisionGrid/Columns/CommitIdColumnProvider.cs +++ b/GitUI/UserControls/RevisionGrid/Columns/CommitIdColumnProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Text; using System.Windows.Forms; using GitCommands; using GitExtUtils.GitUI; @@ -66,8 +67,26 @@ public override bool TryGetToolTip(DataGridViewCellMouseEventArgs e, GitRevision return false; } - toolTip = revision.Guid; + toolTip = Format(revision.Guid); return true; } + + public static string Format(string guid) + { + var text = new StringBuilder(); + for (int pos = 0, length = guid.Length; pos < length; pos += 4) + { + if (pos > 0) + { + text.Append(MinimumSpace); + } + + text.Append(guid.Substring(pos, 4)); + } + + return text.ToString(); + } + + private const char MinimumSpace = '\x200A'; } } \ No newline at end of file diff --git a/GitUI/UserControls/RevisionGrid/Columns/GraphColumnProvider.cs b/GitUI/UserControls/RevisionGrid/Columns/GraphColumnProvider.cs index 96d38ab4715..408f9a71514 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,9 +552,32 @@ string GetLaneInfo(int x, int rowIndex) if (node != null) { - if (laneInfoText.Length > 0) + if (!node.Revision.IsArtificial) { - laneInfoText.AppendLine(); + laneInfoText.AppendLine(CommitIdColumnProvider.Format(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(Format("\nContained in branches: {0}\n", references.Branches)); + laneInfoText.Append(Format("\nContained in tags: {0}\n", references.Tags)); + + string Format(string format, IEnumerable list) + { + return list.Any() ? string.Format(format, string.Join(", ", list)) : string.Empty; + } + + // laneInfoText.Append(references.DebugInfo); + + laneInfoText.AppendLine().AppendLine(); } laneInfoText.Append(node.Revision.Body ?? node.Revision.Subject); @@ -569,5 +588,199 @@ string GetLaneInfo(int x, int rowIndex) return laneInfoText.ToString(); } } + + private class Reference + { + internal readonly string LocalName; + internal bool IsLocal { get; private set; } = false; + internal List Remotes { get; } = new List(); + + internal Reference(string localName) + { + LocalName = localName; + } + + internal void Add([NotNull] IGitRef gitReference) + { + if (gitReference.IsHead) + { + IsLocal = true; + } + else if (gitReference.IsRemote) + { + Remotes.Add(gitReference.Remote); + } + } + + public override string ToString() + { + var list = new StringBuilder(); + + if (Remotes.Any()) + { + if (IsLocal) + { + list.Append("["); + } + + list.Append(Remotes.Join("|")).Append("/"); + + if (IsLocal) + { + list.Append("]"); + } + } + + list.Append(LocalName); + + return list.ToString(); + } + } + + private class References + { + private HashSet _visitedNodes = new HashSet(); + private Dictionary _referenceByName = new Dictionary(); + internal List Tags { get; } = new List(); + internal List Branches { get; } = new List(); + 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 void AddReferencesOf([NotNull] Node node, [CanBeNull] Junction previousDescJunction) + { + if (_visitedNodes.Add(node)) + { + CheckForMerge(node, previousDescJunction); + node.Revision.Refs.ForEach(reference => Add(reference, node, previousDescJunction)); + 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) + { + innerNode.Revision.Refs.ForEach(reference => Add(reference, node, descJunction)); + } + else + { + nodeFound = innerNode == node; + } + } + + // handle the youngest and its descendants + AddReferencesOf(descJunction.Youngest, descJunction); + } + } + } + + private void Add([NotNull] IGitRef gitReference, [NotNull] Node node, [CanBeNull] Junction descJunction) + { + Reference reference; + if (!_referenceByName.TryGetValue(gitReference.LocalName, out reference)) + { + reference = new Reference(gitReference.LocalName); + _referenceByName.Add(gitReference.LocalName, reference); + if (gitReference.IsTag) + { + Tags.Add(reference); + } + else if (gitReference.IsHead || gitReference.IsRemote) + { + Branches.Add(reference); + if (CommittedTo == null) + { + CheckForMerge(node, descJunction); + CommittedTo = CommittedTo ?? gitReference.Name; + } + } + else if (gitReference.IsStash && CommittedTo == null) + { + CommittedTo = gitReference.Name; + } + } + + reference.Add(gitReference); + } + + /// + /// Checks whether the commit message is a merge message + /// though only if CommittedTo has not been set yet + /// 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 void CheckForMerge([NotNull] Node node, [CanBeNull] Junction descJunction) + { + if (CommittedTo == null) + { + 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 : ""); + } + } + } + } + + 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; }