using System; using System.Collections.Generic; using Avalonia; using Avalonia.Media; namespace SourceGit.Models { public class CommitGraph { public class Path { public List Points = new List(); public int Color = 0; } public class PathHelper { public string Next; public bool IsMerged; public double LastX; public double LastY; public double EndY; public Path Path; public PathHelper(string next, bool isMerged, int color, Point start) { Next = next; IsMerged = isMerged; LastX = start.X; LastY = start.Y; EndY = LastY; Path = new Path(); Path.Color = color; Path.Points.Add(start); } public PathHelper(string next, bool isMerged, int color, Point start, Point to) { Next = next; IsMerged = isMerged; LastX = to.X; LastY = to.Y; EndY = LastY; Path = new Path(); Path.Color = color; Path.Points.Add(start); Path.Points.Add(to); } public void Add(double x, double y, double halfHeight, bool isEnd = false) { if (x > LastX) { Add(new Point(LastX, LastY)); Add(new Point(x, y - halfHeight)); if (isEnd) Add(new Point(x, y)); } else if (x < LastX) { var testY = LastY + halfHeight; if (y > testY) Add(new Point(LastX, testY)); if (!isEnd) y += halfHeight; Add(new Point(x, y)); } else if (isEnd) { Add(new Point(x, y)); } LastX = x; LastY = y; } private void Add(Point p) { if (EndY < p.Y) { Path.Points.Add(p); EndY = p.Y; } } } public class Link { public Point Start; public Point Control; public Point End; public int Color; } public enum DotType { Default, Head, Merge, } public class Dot { public DotType Type; public Point Center; public int Color; } public List Paths { get; set; } = new List(); public List Links { get; set; } = new List(); public List Dots { get; set; } = new List(); public static List Pens { get; private set; } = new List(); public static void SetDefaultPens(double thickness = 2) { SetPens(_defaultPenColors, thickness); } public static void SetPens(List colors, double thickness) { Pens.Clear(); foreach (var c in colors) Pens.Add(new Pen(c.ToUInt32(), thickness)); _penCount = colors.Count; } public static CommitGraph Parse(List commits, bool firstParentOnlyEnabled) { double UNIT_WIDTH = 12; double HALF_WIDTH = 6; double UNIT_HEIGHT = 28; double HALF_HEIGHT = 14; double H_MARGIN = 2; var temp = new CommitGraph(); var unsolved = new List(); var mapUnsolved = new Dictionary(); var ended = new List(); var offsetY = -HALF_HEIGHT; var colorIdx = 0; foreach (var commit in commits) { var major = null as PathHelper; var isMerged = commit.IsMerged; var oldCount = unsolved.Count; // Update current y offset offsetY += UNIT_HEIGHT; // Find first curves that links to this commit and marks others that links to this commit ended. double offsetX = H_MARGIN - HALF_WIDTH; foreach (var l in unsolved) { if (l.Next == commit.SHA) { if (major == null) { offsetX += UNIT_WIDTH; major = l; if (commit.Parents.Count > 0) { major.Next = commit.Parents[0]; if (!mapUnsolved.ContainsKey(major.Next)) mapUnsolved.Add(major.Next, major); } else { major.Next = "ENDED"; ended.Add(l); } major.Add(offsetX, offsetY, HALF_HEIGHT); } else { ended.Add(l); } isMerged = isMerged || l.IsMerged; major.IsMerged = isMerged; } else { if (!mapUnsolved.ContainsKey(l.Next)) mapUnsolved.Add(l.Next, l); offsetX += UNIT_WIDTH; l.Add(offsetX, offsetY, HALF_HEIGHT); } } // Create new curve for branch head if (major == null) { offsetX += UNIT_WIDTH; if (commit.Parents.Count > 0) { major = new PathHelper(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY)); unsolved.Add(major); temp.Paths.Add(major.Path); } colorIdx = (colorIdx + 1) % _penCount; } // Calculate link position of this commit. Point position = new Point(offsetX, offsetY); int dotColor = 0; if (major != null) { position = new Point(major.LastX, offsetY); dotColor = major.Path.Color; } Dot anchor = new Dot() { Center = position, Color = dotColor }; if (commit.IsCurrentHead) anchor.Type = DotType.Head; else if (commit.Parents.Count > 1) anchor.Type = DotType.Merge; else anchor.Type = DotType.Default; temp.Dots.Add(anchor); // Deal with other parents (the first parent has been processed) if (!firstParentOnlyEnabled) { for (int j = 1; j < commit.Parents.Count; j++) { var parent = commit.Parents[j]; if (mapUnsolved.TryGetValue(parent, out var value)) { // Try to change the merge state of linked graph var l = value; if (isMerged) l.IsMerged = true; var link = new Link(); link.Start = position; link.End = new Point(l.LastX, offsetY + HALF_HEIGHT); link.Control = new Point(link.End.X, link.Start.Y); link.Color = l.Path.Color; temp.Links.Add(link); } else { offsetX += UNIT_WIDTH; // Create new curve for parent commit that not includes before var l = new PathHelper(parent, isMerged, colorIdx, position, new Point(offsetX, position.Y + HALF_HEIGHT)); unsolved.Add(l); temp.Paths.Add(l.Path); colorIdx = (colorIdx + 1) % _penCount; } } } // Remove ended curves from unsolved foreach (var l in ended) { l.Add(position.X, position.Y, HALF_HEIGHT, true); unsolved.Remove(l); } // Margins & merge state (used by Views.Histories). commit.IsMerged = isMerged; commit.Margin = new Thickness(Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH + H_MARGIN) + H_MARGIN, 0, 0, 0); // Clean up ended.Clear(); mapUnsolved.Clear(); } // Deal with curves haven't ended yet. for (int i = 0; i < unsolved.Count; i++) { var path = unsolved[i]; var endY = (commits.Count - 0.5) * UNIT_HEIGHT; if (path.Path.Points.Count == 1 && Math.Abs(path.Path.Points[0].Y - endY) < 0.0001) continue; path.Add((i + 0.5) * UNIT_WIDTH + H_MARGIN, endY + HALF_HEIGHT, HALF_HEIGHT, true); } unsolved.Clear(); return temp; } private static int _penCount = 0; private static readonly List _defaultPenColors = [ Colors.Orange, Colors.ForestGreen, Colors.Gold, Colors.Magenta, Colors.Red, Colors.Gray, Colors.Turquoise, Colors.Olive, Colors.Khaki, Colors.Lime, ]; } }