diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 82505c351..23c540a40 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Avalonia; @@ -62,7 +62,7 @@ public class Dot public List Links { get; } = []; public List Dots { get; } = []; - public static CommitGraph Parse(List commits, bool firstParentOnlyEnabled) + public static CommitGraph Parse(List commits, bool firstParentOnlyEnabled, bool alwaysShowCurrentHeadOnLeft) { const double unitWidth = 12; const double halfWidth = 6; @@ -75,40 +75,106 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable var offsetY = -halfHeight; var colorPicker = new ColorPicker(); + // 1. Pre-scan for the Grand Lineage (Trunk) + var headPathSHAs = new HashSet(); + string topTrunkSHA = null; + if (alwaysShowCurrentHeadOnLeft) + { + var head = commits.Find(x => x.IsCurrentHead); + if (head != null) + { + headPathSHAs.Add(head.SHA); + + // Trace DOWN (Ancestors) via first parent + string currentDown = head.SHA; + for (int i = commits.IndexOf(head); i < commits.Count; i++) + { + if (commits[i].SHA == currentDown) + { + headPathSHAs.Add(currentDown); + if (commits[i].Parents.Count > 0) + currentDown = commits[i].Parents[0]; + } + } + + // Trace UP (Descendants) via strict single line + string currentUp = head.SHA; + for (int i = commits.IndexOf(head) - 1; i >= 0; i--) + { + if (commits[i].Parents.Count > 0 && commits[i].Parents[0] == currentUp) + { + headPathSHAs.Add(commits[i].SHA); + currentUp = commits[i].SHA; + } + } + + // Find the top-most trunk commit to anchor the Phantom Path + foreach (var c in commits) + { + if (headPathSHAs.Contains(c.SHA)) + { + topTrunkSHA = c.SHA; + break; + } + } + } + } + + // 2. Inject Phantom Path for Trunk at Y=0. + // This forces the Trunk to occupy Slot 0 (X=10) permanently, simulating an uncommitted node. + if (topTrunkSHA != null) + { + var phantom = new PathHelper(topTrunkSHA, false, colorPicker.Next(), new Point(10.0, offsetY)); + phantom.IsTrunk = true; + unsolved.Add(phantom); + temp.Paths.Add(phantom.Path); + } + foreach (var commit in commits) { PathHelper major = null; var isMerged = commit.IsMerged; + bool isCommitTrunk = alwaysShowCurrentHeadOnLeft && headPathSHAs.Contains(commit.SHA); // Update current y offset offsetY += unitHeight; // Find first curves that links to this commit and marks others that links to this commit ended. - var offsetX = 4 - halfWidth; - var maxOffsetOld = unsolved.Count > 0 ? unsolved[^1].LastX : offsetX + unitWidth; + var maxOffsetOld = 0.0; + for (int i = 0; i < unsolved.Count; i++) + { + if (unsolved[i].LastX > maxOffsetOld) + maxOffsetOld = unsolved[i].LastX; + } + + var currentOffsetX = 4 - halfWidth; foreach (var l in unsolved) { + currentOffsetX += unitWidth; + if (l.Next.Equals(commit.SHA, StringComparison.Ordinal)) { - if (major == null) + // Only Trunk paths can claim major status for Trunk commits. + bool canBeMajor = !isCommitTrunk || l.IsTrunk; + + if (major == null && canBeMajor) { - offsetX += unitWidth; major = l; if (commit.Parents.Count > 0) { major.Next = commit.Parents[0]; - major.Goto(offsetX, offsetY, halfHeight); + major.Goto(currentOffsetX, offsetY, halfHeight); } else { - major.End(offsetX, offsetY, halfHeight); + major.End(currentOffsetX, offsetY, halfHeight); ended.Add(l); } } else { - l.End(major.LastX, offsetY, halfHeight); + l.End(major?.LastX ?? currentOffsetX, offsetY, halfHeight); ended.Add(l); } @@ -116,8 +182,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable } else { - offsetX += unitWidth; - l.Pass(offsetX, offsetY, halfHeight); + l.Pass(currentOffsetX, offsetY, halfHeight); } } @@ -133,11 +198,10 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable // Otherwise, create new curve for new merged commit if (major == null) { - offsetX += unitWidth; - if (commit.Parents.Count > 0) { - major = new PathHelper(commit.Parents[0], isMerged, colorPicker.Next(), new Point(offsetX, offsetY)); + currentOffsetX += unitWidth; + major = new PathHelper(commit.Parents[0], isMerged, colorPicker.Next(), new Point(currentOffsetX, offsetY)); unsolved.Add(major); temp.Paths.Add(major.Path); } @@ -149,7 +213,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable } // Calculate link position of this commit. - var position = new Point(major?.LastX ?? offsetX, offsetY); + var position = new Point(major?.LastX ?? Math.Max(currentOffsetX, 10.0), offsetY); var dotColor = major?.Path.Color ?? 0; var anchor = new Dot() { Center = position, Color = dotColor, IsMerged = isMerged }; if (commit.IsCurrentHead) @@ -187,10 +251,10 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable } else { - offsetX += unitWidth; + currentOffsetX += unitWidth; // Create new curve for parent commit that not includes before - var l = new PathHelper(parentHash, isMerged, colorPicker.Next(), position, new Point(offsetX, position.Y + halfHeight)); + var l = new PathHelper(parentHash, isMerged, colorPicker.Next(), position, new Point(currentOffsetX, position.Y + halfHeight)); unsolved.Add(l); temp.Paths.Add(l.Path); } @@ -200,7 +264,7 @@ public static CommitGraph Parse(List commits, bool firstParentOnlyEnable // Margins & merge state (used by Views.Histories). commit.IsMerged = isMerged; commit.Color = dotColor; - commit.LeftMargin = Math.Max(offsetX, maxOffsetOld) + halfWidth + 2; + commit.LeftMargin = Math.Max(currentOffsetX, maxOffsetOld) + halfWidth + 2; } // Deal with curves haven't ended yet. @@ -246,6 +310,7 @@ private class PathHelper public Path Path { get; private set; } public string Next { get; set; } public double LastX { get; private set; } + public bool IsTrunk { get; set; } = false; public bool IsMerged => Path.IsMerged; @@ -273,9 +338,6 @@ public PathHelper(string next, bool isMerged, int color, Point start, Point to) /// /// A path that just passed this row. /// - /// - /// - /// public void Pass(double x, double y, double halfHeight) { if (x > LastX) @@ -297,9 +359,6 @@ public void Pass(double x, double y, double halfHeight) /// /// A path that has commit in this row but not ended /// - /// - /// - /// public void Goto(double x, double y, double halfHeight) { if (x > LastX) @@ -324,9 +383,6 @@ public void Goto(double x, double y, double halfHeight) /// /// A path that has commit in this row and end. /// - /// - /// - /// public void End(double x, double y, double halfHeight) { if (x > LastX) @@ -385,4 +441,4 @@ private void Add(double x, double y) Colors.Teal, ]; } -} +} \ No newline at end of file diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 5eae0bc8a..92d0bb1f9 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -653,6 +653,7 @@ Input path for diff/merge tool Tool GENERAL + Always show current HEAD on the left Check for updates on startup Date Format Enable compact folders in changes tree diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index aeb6ce936..4935b8df2 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -657,6 +657,7 @@ 填写工具可执行文件所在位置 工具 通用配置 + HEAD 路径永远显示在最左边 启动时检测软件更新 日期时间格式 在变更列表树中启用紧凑文件夹模式 diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index da7fe3e5d..4c2ec236b 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -211,6 +211,12 @@ public string IgnoreUpdateTag set => SetProperty(ref _ignoreUpdateTag, value); } + public bool AlwaysShowCurrentHeadOnLeftInGraph + { + get => _alwaysShowCurrentHeadOnLeftInGraph; + set => SetProperty(ref _alwaysShowCurrentHeadOnLeftInGraph, value); + } + public bool ShowTagsInGraph { get => _showTagsInGraph; @@ -833,6 +839,7 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private string _ignoreUpdateTag = string.Empty; private bool _showTagsInGraph = true; + private bool _alwaysShowCurrentHeadOnLeftInGraph = false; private bool _useTwoColumnsLayoutInHistories = false; private bool _displayTimeAsPeriodInHistories = false; private bool _useSideBySideDiff = false; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index c85f12d50..0fafebce5 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1212,7 +1212,7 @@ public void RefreshCommits() .Append(_uiStates.BuildHistoryParams()); var commits = await new Commands.QueryCommits(FullPath, builder.ToString()).GetResultAsync().ConfigureAwait(false); - var graph = Models.CommitGraph.Parse(commits, _uiStates.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)); + var graph = Models.CommitGraph.Parse(commits, _uiStates.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly), Preferences.Instance.AlwaysShowCurrentHeadOnLeftInGraph); Dispatcher.UIThread.Invoke(() => { diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index 4b4688752..8f0ed5fe7 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -47,7 +47,7 @@ - + + + - -