Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 84 additions & 28 deletions src/Models/CommitGraph.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;

using Avalonia;
Expand Down Expand Up @@ -62,7 +62,7 @@ public class Dot
public List<Link> Links { get; } = [];
public List<Dot> Dots { get; } = [];

public static CommitGraph Parse(List<Commit> commits, bool firstParentOnlyEnabled)
public static CommitGraph Parse(List<Commit> commits, bool firstParentOnlyEnabled, bool alwaysShowCurrentHeadOnLeft)
{
const double unitWidth = 12;
const double halfWidth = 6;
Expand All @@ -75,49 +75,114 @@ public static CommitGraph Parse(List<Commit> commits, bool firstParentOnlyEnable
var offsetY = -halfHeight;
var colorPicker = new ColorPicker();

// 1. Pre-scan for the Grand Lineage (Trunk)
var headPathSHAs = new HashSet<string>();
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);
}

isMerged = isMerged || l.IsMerged;
}
else
{
offsetX += unitWidth;
l.Pass(offsetX, offsetY, halfHeight);
l.Pass(currentOffsetX, offsetY, halfHeight);
}
}

Expand All @@ -133,11 +198,10 @@ public static CommitGraph Parse(List<Commit> 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);
}
Expand All @@ -149,7 +213,7 @@ public static CommitGraph Parse(List<Commit> 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)
Expand Down Expand Up @@ -187,10 +251,10 @@ public static CommitGraph Parse(List<Commit> 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);
}
Expand All @@ -200,7 +264,7 @@ public static CommitGraph Parse(List<Commit> 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.
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -273,9 +338,6 @@ public PathHelper(string next, bool isMerged, int color, Point start, Point to)
/// <summary>
/// A path that just passed this row.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="halfHeight"></param>
public void Pass(double x, double y, double halfHeight)
{
if (x > LastX)
Expand All @@ -297,9 +359,6 @@ public void Pass(double x, double y, double halfHeight)
/// <summary>
/// A path that has commit in this row but not ended
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="halfHeight"></param>
public void Goto(double x, double y, double halfHeight)
{
if (x > LastX)
Expand All @@ -324,9 +383,6 @@ public void Goto(double x, double y, double halfHeight)
/// <summary>
/// A path that has commit in this row and end.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="halfHeight"></param>
public void End(double x, double y, double halfHeight)
{
if (x > LastX)
Expand Down Expand Up @@ -385,4 +441,4 @@ private void Add(double x, double y)
Colors.Teal,
];
}
}
}
1 change: 1 addition & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@
<x:String x:Key="Text.Preferences.DiffMerge.Path.Placeholder" xml:space="preserve">Input path for diff/merge tool</x:String>
<x:String x:Key="Text.Preferences.DiffMerge.Type" xml:space="preserve">Tool</x:String>
<x:String x:Key="Text.Preferences.General" xml:space="preserve">GENERAL</x:String>
<x:String x:Key="Text.Preferences.General.AlwaysShowCurrentHeadOnLeft" xml:space="preserve">Always show current HEAD on the left</x:String>
<x:String x:Key="Text.Preferences.General.Check4UpdatesOnStartup" xml:space="preserve">Check for updates on startup</x:String>
<x:String x:Key="Text.Preferences.General.DateFormat" xml:space="preserve">Date Format</x:String>
<x:String x:Key="Text.Preferences.General.EnableCompactFolders" xml:space="preserve">Enable compact folders in changes tree</x:String>
Expand Down
1 change: 1 addition & 0 deletions src/Resources/Locales/zh_CN.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@
<x:String x:Key="Text.Preferences.DiffMerge.Path.Placeholder" xml:space="preserve">填写工具可执行文件所在位置</x:String>
<x:String x:Key="Text.Preferences.DiffMerge.Type" xml:space="preserve">工具</x:String>
<x:String x:Key="Text.Preferences.General" xml:space="preserve">通用配置</x:String>
<x:String x:Key="Text.Preferences.General.AlwaysShowCurrentHeadOnLeft" xml:space="preserve">HEAD 路径永远显示在最左边</x:String>
<x:String x:Key="Text.Preferences.General.Check4UpdatesOnStartup" xml:space="preserve">启动时检测软件更新</x:String>
<x:String x:Key="Text.Preferences.General.DateFormat" xml:space="preserve">日期时间格式</x:String>
<x:String x:Key="Text.Preferences.General.EnableCompactFolders" xml:space="preserve">在变更列表树中启用紧凑文件夹模式</x:String>
Expand Down
7 changes: 7 additions & 0 deletions src/ViewModels/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -833,6 +839,7 @@ private bool RemoveInvalidRepositoriesRecursive(List<RepositoryNode> 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;
Expand Down
2 changes: 1 addition & 1 deletion src/ViewModels/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(() =>
{
Expand Down
11 changes: 8 additions & 3 deletions src/Views/Preferences.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preferences.General}"/>
</TabItem.Header>
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preferences.General.Locale}"
HorizontalAlignment="Right"
Expand Down Expand Up @@ -167,16 +167,21 @@
IsChecked="{Binding ShowChildren, Mode=TwoWay}"/>

<CheckBox Grid.Row="10" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.General.AlwaysShowCurrentHeadOnLeft}"
IsChecked="{Binding AlwaysShowCurrentHeadOnLeftInGraph, Mode=TwoWay}"/>

<CheckBox Grid.Row="11" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.General.EnableCompactFolders}"
IsChecked="{Binding EnableCompactFoldersInChangesTree, Mode=TwoWay}"/>

<CheckBox Grid.Row="11" Grid.Column="1"
<CheckBox Grid.Row="12" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.General.UseGitHubStyleAvatar}"
IsChecked="{Binding UseGitHubStyleAvatar, Mode=TwoWay}"/>

<CheckBox Grid.Row="12" Grid.Column="1"
<CheckBox Grid.Row="13" Grid.Column="1"
Height="32"
Content="{DynamicResource Text.Preferences.General.Check4UpdatesOnStartup}"
IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"
Expand Down
Loading