Skip to content

Commit

Permalink
WIP gitextensions#5125 RevisionGrid Graph: Nearest branch in tooltip
Browse files Browse the repository at this point in the history
  • Loading branch information
mstv committed Oct 19, 2018
1 parent 47006b1 commit ff76129
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 77 deletions.
2 changes: 2 additions & 0 deletions GitUI/GitUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@
<Compile Include="UserControls\GitItemStatusWithParent.cs" />
<Compile Include="UserControls\RevisionGrid\CellStyle.cs" />
<Compile Include="UserControls\ListViewGroupHitInfo.cs" />
<Compile Include="UserControls\RevisionGrid\Graph\LaneInfoProvider.cs" />
<Compile Include="UserControls\RevisionGrid\Graph\LaneNodeLocator.cs" />
<Compile Include="UserControls\RevisionGrid\Graph\JunctionColorProvider.cs" />
<Compile Include="UserControls\RevisionGrid\Graph\JunctionStyler.cs" />
<Compile Include="UserControls\RevisionGrid\Columns\MultilineIndicator.cs" />
Expand Down
93 changes: 17 additions & 76 deletions GitUI/UserControls/RevisionGrid/Columns/GraphColumnProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using GitCommands;
using GitExtUtils.GitUI;
Expand All @@ -17,13 +16,12 @@ namespace GitUI.UserControls.RevisionGrid.Columns
internal sealed class GraphColumnProvider : ColumnProvider
{
private const int MaxLanes = 40;

private static readonly int _nodeDimension = DpiUtil.Scale(10);
private static readonly int _laneWidth = DpiUtil.Scale(16);
private static readonly int _laneLineWidth = DpiUtil.Scale(2);
private static readonly int LaneLineWidth = DpiUtil.Scale(2);
private static readonly int LaneWidth = DpiUtil.Scale(16);
private static readonly int NodeDimension = DpiUtil.Scale(10);

private readonly JunctionStyler _junctionStyler = new JunctionStyler(new JunctionColorProvider());

private readonly LaneInfoProvider _laneInfoProvider;
private readonly RevisionGridControl _grid;
private readonly GraphModel _graphModel;

Expand All @@ -41,6 +39,7 @@ public GraphColumnProvider(RevisionGridControl grid, GraphModel graphModel)
{
_grid = grid;
_graphModel = graphModel;
_laneInfoProvider = new LaneInfoProvider(new LaneNodeLocator(_graphModel));

// TODO is it worth creating a lighter-weight column type?

Expand Down Expand Up @@ -251,7 +250,7 @@ void EnsureCacheIsLargeEnough()
}

_graphBitmap = new Bitmap(
Math.Max(width, _laneWidth * 3),
Math.Max(width, LaneWidth * 3),
height,
PixelFormat.Format32bppPArgb);
_graphBitmapGraphics = Graphics.FromImage(_graphBitmap);
Expand Down Expand Up @@ -286,7 +285,7 @@ bool DrawItem(Graphics g, ILaneRow row)

for (int lane = 0; lane < row.Count; lane++)
{
int mid = g.RenderingOrigin.X + (int)((lane + 0.5) * _laneWidth);
int mid = g.RenderingOrigin.X + (int)((lane + 0.5) * LaneWidth);

for (int item = 0; item < row.LaneInfoCount(lane); item++)
{
Expand All @@ -304,13 +303,13 @@ bool DrawItem(Graphics g, ILaneRow row)
bool sameLane = laneInfo.ConnectLane == lane;
int x0 = mid;
int y0 = top - 1;
int x1 = sameLane ? x0 : mid + ((laneInfo.ConnectLane - lane) * _laneWidth);
int x1 = sameLane ? x0 : mid + ((laneInfo.ConnectLane - lane) * LaneWidth);
int y1 = top + rowHeight;

var p0 = new Point(x0, y0);
var p1 = new Point(x1, y1);

using (var lanePen = new Pen(laneBrush, _laneLineWidth))
using (var lanePen = new Pen(laneBrush, LaneLineWidth))
{
if (sameLane)
{
Expand Down Expand Up @@ -347,10 +346,10 @@ bool DrawItem(Graphics g, ILaneRow row)

// Draw node
var nodeRect = new Rectangle(
g.RenderingOrigin.X + ((_laneWidth - _nodeDimension) / 2) + (row.NodeLane * _laneWidth),
g.RenderingOrigin.Y + ((rowHeight - _nodeDimension) / 2),
_nodeDimension,
_nodeDimension);
g.RenderingOrigin.X + ((LaneWidth - NodeDimension) / 2) + (row.NodeLane * LaneWidth),
g.RenderingOrigin.Y + ((rowHeight - NodeDimension) / 2),
NodeDimension,
NodeDimension);

Color? nodeColor = null;

Expand All @@ -370,7 +369,7 @@ bool DrawItem(Graphics g, ILaneRow row)
}
else //// Circle
{
nodeRect.Width = nodeRect.Height = _nodeDimension - 1;
nodeRect.Width = nodeRect.Height = NodeDimension - 1;

g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillEllipse(nodeBrush, nodeRect);
Expand Down Expand Up @@ -493,7 +492,7 @@ private void UpdateGraphColumnWidth(in VisibleRowRange range)
: MaxLanes;

laneCount = Math.Min(laneCount, maxLanes);
var columnWidth = (_laneWidth * laneCount) + ColumnLeftMargin;
var columnWidth = (LaneWidth * laneCount) + ColumnLeftMargin;
if (Column.Width != columnWidth && columnWidth > Column.MinimumWidth)
{
Column.Width = columnWidth;
Expand All @@ -508,66 +507,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;

string GetLaneInfo(int x, int rowIndex)
{
int lane = x / _laneWidth;
var laneInfoText = new StringBuilder();
lock (_graphModel)
{
ILaneRow laneRow = _graphModel.GetLaneRow(rowIndex);
if (laneRow != null)
{
Node node = null;
if (lane == laneRow.NodeLane)
{
node = laneRow.Node;
if (!node.Revision.IsArtificial)
{
laneInfoText.AppendLine(node.Revision.Guid);
}
}
else if (lane >= 0 && lane < laneRow.Count)
{
for (int laneInfoIndex = 0, laneInfoCount = laneRow.LaneInfoCount(lane); laneInfoIndex < laneInfoCount; ++laneInfoIndex)
{
// search for next node below this row
LaneInfo laneInfo = laneRow[lane, laneInfoIndex];
Junction firstJunction = laneInfo.Junctions.First();
for (int nodeIndex = 0, nodeCount = firstJunction.NodeCount; nodeIndex < nodeCount; ++nodeIndex)
{
Node laneNode = firstJunction[nodeIndex];
if (laneNode.Index > rowIndex)
{
node = laneNode;
break; // from for (nodes)
}
}
}
}

if (node != null)
{
if (laneInfoText.Length > 0)
{
laneInfoText.AppendLine();
}

laneInfoText.Append(node.Revision.Body ?? node.Revision.Subject);
}
}
}

return laneInfoText.ToString();
}
toolTip = _laneInfoProvider.GetLaneInfo(e.X - ColumnLeftMargin, e.RowIndex, LaneWidth);
return true;
}
}
}
7 changes: 6 additions & 1 deletion GitUI/UserControls/RevisionGrid/Graph/GraphModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@

namespace GitUI.UserControls.RevisionGrid.Graph
{
internal sealed class GraphModel
internal interface ILaneRowProvider
{
ILaneRow GetLaneRow(int row);
}

internal sealed class GraphModel : ILaneRowProvider
{
public event Action Updated;

Expand Down
193 changes: 193 additions & 0 deletions GitUI/UserControls/RevisionGrid/Graph/LaneInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using JetBrains.Annotations;

namespace GitUI.UserControls.RevisionGrid.Graph
{
internal sealed class LaneInfoProvider
{
private readonly ILaneNodeLocator _nodeLocator;

public LaneInfoProvider(ILaneNodeLocator nodeLocator)
{
_nodeLocator = nodeLocator;
}

public string GetLaneInfo(int x, int rowIndex, int laneWidth)
{
var node = _nodeLocator.FindNextNode(x, rowIndex, laneWidth);
if (node == null)
{
return string.Empty;
}

var laneInfoText = new StringBuilder();
if (!node.Revision.IsArtificial)
{
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.AppendLine();
}

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.");
}
}

return laneInfoText.ToString();
}

private class References
{
private static readonly Regex MergeRegex = new Regex("(?i)^merged? (pull request (.*) from )?(.*branch )?'?([^ ']+)'?( into (.*))?\\.?$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly HashSet<Node> _visitedNodes = new HashSet<Node>();

internal References([NotNull] Node node)
{
AddReferencesOf(node, previousDescJunction: null);
}

internal string CommittedTo { get; private set; }
internal string MergedWith { get; private set; }

private bool AddReferencesOf([NotNull] Node node, [CanBeNull] Junction previousDescJunction)
{
if (!_visitedNodes.Add(node))
{
return false;
}

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
var nodeFound = false;
for (var nodeIndex = descJunction.NodeCount - 1; nodeIndex > 0; --nodeIndex)
{
var 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;
}

if (gitReference.IsStash && CommittedTo == null)
{
CommittedTo = gitReference.Name;
return true;
}
}

return false;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="node">the node of the revision to evaluate</param>
/// <param name="descJunction">
/// the descending junction the node is part of
/// (used for the decision whether the node belongs the first or second branch of the merge)
/// </param>
private bool CheckForMerge([NotNull] Node node, [CanBeNull] Junction descJunction)
{
var isTheFirstBranch = descJunction == null || node.Ancestors.Count == 0 || node.Ancestors.First() == descJunction;
string mergedInto;
string mergedWith;
(mergedInto, mergedWith) = ParseMergeMessage(node, isTheFirstBranch);

if (mergedInto != null)
{
CommittedTo = isTheFirstBranch ? mergedInto : mergedWith;
}

if (MergedWith == null)
{
MergedWith = mergedWith ?? string.Empty;
}

return CommittedTo != null;
}

private static (string into, string with) ParseMergeMessage([NotNull] Node node, bool appendPullRequest)
{
string into = null;
string with = null;
var match = MergeRegex.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);
}
}
}
}
Loading

0 comments on commit ff76129

Please sign in to comment.