From f0649c95b5a417c975ca58dbb4520919aef4be1e Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 19 Jul 2024 09:29:16 +0800 Subject: [PATCH] feature: add an indicator that shows those commits the current branch ahead/behind its upstream --- src/Commands/QueryBranches.cs | 49 ++++----------------- src/Commands/QueryCommits.cs | 73 ++----------------------------- src/Commands/QuerySingleCommit.cs | 68 +--------------------------- src/Commands/QueryTrackStatus.cs | 34 ++++++++++++++ src/Models/Branch.cs | 25 ++++++++++- src/Models/Commit.cs | 70 ++++++++++++++++++++++++++++- src/Models/CommitGraph.cs | 5 ++- src/ViewModels/BranchTreeNode.cs | 9 +--- src/ViewModels/Histories.cs | 2 +- src/ViewModels/Repository.cs | 20 +++++++-- src/Views/BranchTree.axaml | 9 +++- src/Views/Histories.axaml | 10 +++++ 12 files changed, 180 insertions(+), 194 deletions(-) create mode 100644 src/Commands/QueryTrackStatus.cs diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index 9200db4b..c55d6760 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -24,20 +24,8 @@ namespace SourceGit.Commands { Exec(); - foreach (var b in _branches) - { - if (b.IsLocal && !string.IsNullOrEmpty(b.UpstreamTrackStatus)) - { - if (b.UpstreamTrackStatus == "=") - { - b.UpstreamTrackStatus = string.Empty; - } - else - { - b.UpstreamTrackStatus = ParseTrackStatus(b.Name, b.Upstream); - } - } - } + foreach (var b in _needQueryTrackStatus) + b.TrackStatus = new QueryTrackStatus(WorkingDirectory, b.Name, b.Upstream).Result(); return _branches; } @@ -84,35 +72,16 @@ namespace SourceGit.Commands branch.Head = parts[1]; branch.IsCurrent = parts[2] == "*"; branch.Upstream = parts[3]; - branch.UpstreamTrackStatus = parts[4]; + + if (branch.IsLocal && !parts[4].Equals("=", StringComparison.Ordinal)) + _needQueryTrackStatus.Add(branch); + else + branch.TrackStatus = new Models.BranchTrackStatus(); + _branches.Add(branch); } - private string ParseTrackStatus(string local, string upstream) - { - var cmd = new Command(); - cmd.WorkingDirectory = WorkingDirectory; - cmd.Context = Context; - cmd.Args = $"rev-list --left-right --count {local}...{upstream}"; - - var rs = cmd.ReadToEnd(); - if (!rs.IsSuccess) - return string.Empty; - - var match = REG_AHEAD_BEHIND().Match(rs.StdOut); - if (!match.Success) - return string.Empty; - - var ahead = int.Parse(match.Groups[1].Value); - var behind = int.Parse(match.Groups[2].Value); - var track = ""; - if (ahead > 0) - track += $"{ahead}↑"; - if (behind > 0) - track += $" {behind}↓"; - return track.Trim(); - } - private readonly List _branches = new List(); + private List _needQueryTrackStatus = new List(); } } diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 7d4b6991..ef80566e 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -33,7 +33,6 @@ namespace SourceGit.Commands argsBuilder.Append("--all-match -i"); search = argsBuilder.ToString(); } - WorkingDirectory = repo; Context = repo; @@ -63,7 +62,9 @@ namespace SourceGit.Commands ParseParent(line); break; case 2: - ParseDecorators(line); + _current.ParseDecorators(line); + if (_current.IsMerged && !_isHeadFounded) + _isHeadFounded = true; break; case 3: _current.Author = Models.User.FindOrAdd(line); @@ -114,74 +115,6 @@ namespace SourceGit.Commands _current.Parents.Add(data.Substring(idx + 1)); } - private void ParseDecorators(string data) - { - if (data.Length < 3) - return; - - var subs = data.Split(',', StringSplitOptions.RemoveEmptyEntries); - foreach (var sub in subs) - { - var d = sub.Trim(); - if (d.EndsWith("/HEAD", StringComparison.Ordinal)) - continue; - - if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) - { - _current.Decorators.Add(new Models.Decorator() - { - Type = Models.DecoratorType.Tag, - Name = d.Substring(15), - }); - } - else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) - { - _current.IsMerged = true; - _current.Decorators.Add(new Models.Decorator() - { - Type = Models.DecoratorType.CurrentBranchHead, - Name = d.Substring(19), - }); - } - else if (d.Equals("HEAD")) - { - _current.IsMerged = true; - _current.Decorators.Add(new Models.Decorator() - { - Type = Models.DecoratorType.CurrentCommitHead, - Name = d, - }); - } - else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) - { - _current.Decorators.Add(new Models.Decorator() - { - Type = Models.DecoratorType.LocalBranchHead, - Name = d.Substring(11), - }); - } - else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) - { - _current.Decorators.Add(new Models.Decorator() - { - Type = Models.DecoratorType.RemoteBranchHead, - Name = d.Substring(13), - }); - } - } - - _current.Decorators.Sort((l, r) => - { - if (l.Type != r.Type) - return (int)l.Type - (int)r.Type; - else - return string.Compare(l.Name, r.Name, StringComparison.Ordinal); - }); - - if (_current.IsMerged && !_isHeadFounded) - _isHeadFounded = true; - } - private void MarkFirstMerged() { Args = $"log --since=\"{_commits[^1].CommitterTimeStr}\" --format=\"%H\""; diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs index f65b6ce1..eef08b7e 100644 --- a/src/Commands/QuerySingleCommit.cs +++ b/src/Commands/QuerySingleCommit.cs @@ -26,7 +26,7 @@ namespace SourceGit.Commands if (!string.IsNullOrEmpty(lines[1])) commit.Parents.AddRange(lines[1].Split(' ', StringSplitOptions.RemoveEmptyEntries)); if (!string.IsNullOrEmpty(lines[2])) - commit.IsMerged = ParseDecorators(commit.Decorators, lines[2]); + commit.ParseDecorators(lines[2]); commit.Author = Models.User.FindOrAdd(lines[3]); commit.AuthorTime = ulong.Parse(lines[4]); commit.Committer = Models.User.FindOrAdd(lines[5]); @@ -39,70 +39,6 @@ namespace SourceGit.Commands return null; } - private bool ParseDecorators(List decorators, string data) - { - bool isHeadOfCurrent = false; - - var subs = data.Split(',', StringSplitOptions.RemoveEmptyEntries); - foreach (var sub in subs) - { - var d = sub.Trim(); - if (d.EndsWith("/HEAD", StringComparison.Ordinal)) - continue; - - if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) - { - decorators.Add(new Models.Decorator() - { - Type = Models.DecoratorType.Tag, - Name = d.Substring(15), - }); - } - else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) - { - isHeadOfCurrent = true; - decorators.Add(new Models.Decorator() - { - Type = Models.DecoratorType.CurrentBranchHead, - Name = d.Substring(19), - }); - } - else if (d.Equals("HEAD")) - { - isHeadOfCurrent = true; - decorators.Add(new Models.Decorator() - { - Type = Models.DecoratorType.CurrentCommitHead, - Name = d, - }); - } - else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) - { - decorators.Add(new Models.Decorator() - { - Type = Models.DecoratorType.LocalBranchHead, - Name = d.Substring(11), - }); - } - else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) - { - decorators.Add(new Models.Decorator() - { - Type = Models.DecoratorType.RemoteBranchHead, - Name = d.Substring(13), - }); - } - } - - decorators.Sort((l, r) => - { - if (l.Type != r.Type) - return (int)l.Type - (int)r.Type; - else - return string.Compare(l.Name, r.Name, StringComparison.Ordinal); - }); - - return isHeadOfCurrent; - } + } } diff --git a/src/Commands/QueryTrackStatus.cs b/src/Commands/QueryTrackStatus.cs new file mode 100644 index 00000000..ce8e4d0e --- /dev/null +++ b/src/Commands/QueryTrackStatus.cs @@ -0,0 +1,34 @@ +using System; + +namespace SourceGit.Commands +{ + public class QueryTrackStatus : Command + { + public QueryTrackStatus(string repo, string local, string upstream) + { + WorkingDirectory = repo; + Context = repo; + Args = $"rev-list --left-right {local}...{upstream}"; + } + + public Models.BranchTrackStatus Result() + { + var status = new Models.BranchTrackStatus(); + + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return status; + + var lines = rs.StdOut.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + if (line[0] == '>') + status.Behind.Add(line.Substring(1)); + else + status.Ahead.Add(line.Substring(1)); + } + + return status; + } + } +} diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index 07f5374e..f6742d5b 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -1,5 +1,26 @@ -namespace SourceGit.Models +using System.Collections.Generic; + +namespace SourceGit.Models { + public class BranchTrackStatus + { + public List Ahead { get; set; } = new List(); + public List Behind { get; set; } = new List(); + + public override string ToString() + { + if (Ahead.Count == 0 && Behind.Count == 0) + return string.Empty; + + var track = ""; + if (Ahead.Count > 0) + track += $"{Ahead.Count}↑"; + if (Behind.Count > 0) + track += $" {Behind.Count}↓"; + return track.Trim(); + } + } + public class Branch { public string Name { get; set; } @@ -8,7 +29,7 @@ public bool IsLocal { get; set; } public bool IsCurrent { get; set; } public string Upstream { get; set; } - public string UpstreamTrackStatus { get; set; } + public BranchTrackStatus TrackStatus { get; set; } public string Remote { get; set; } public bool IsHead { get; set; } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 1137ad0c..38436fe3 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -12,7 +12,7 @@ namespace SourceGit.Models { get; set; - } = 0.5; + } = 0.65; public string SHA { get; set; } = string.Empty; public User Author { get; set; } = User.Invalid; @@ -23,7 +23,10 @@ namespace SourceGit.Models public List Parents { get; set; } = new List(); public List Decorators { get; set; } = new List(); public bool HasDecorators => Decorators.Count > 0; + public bool IsMerged { get; set; } = false; + public bool CanPushToUpstream { get; set; } = false; + public bool CanPullFromUpstream { get; set; } = false; public Thickness Margin { get; set; } = new Thickness(0); public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss"); @@ -35,5 +38,70 @@ namespace SourceGit.Models public double Opacity => IsMerged ? 1 : OpacityForNotMerged; public FontWeight FontWeight => IsCurrentHead ? FontWeight.Bold : FontWeight.Regular; + + public void ParseDecorators(string data) + { + if (data.Length < 3) + return; + + var subs = data.Split(',', StringSplitOptions.RemoveEmptyEntries); + foreach (var sub in subs) + { + var d = sub.Trim(); + if (d.EndsWith("/HEAD", StringComparison.Ordinal)) + continue; + + if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) + { + Decorators.Add(new Decorator() + { + Type = DecoratorType.Tag, + Name = d.Substring(15), + }); + } + else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) + { + IsMerged = true; + Decorators.Add(new Decorator() + { + Type = DecoratorType.CurrentBranchHead, + Name = d.Substring(19), + }); + } + else if (d.Equals("HEAD")) + { + IsMerged = true; + Decorators.Add(new Decorator() + { + Type = DecoratorType.CurrentCommitHead, + Name = d, + }); + } + else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) + { + Decorators.Add(new Decorator() + { + Type = DecoratorType.LocalBranchHead, + Name = d.Substring(11), + }); + } + else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) + { + Decorators.Add(new Decorator() + { + Type = DecoratorType.RemoteBranchHead, + Name = d.Substring(13), + }); + } + } + + Decorators.Sort((l, r) => + { + if (l.Type != r.Type) + return (int)l.Type - (int)r.Type; + else + return string.Compare(l.Name, r.Name, StringComparison.Ordinal); + }); + } } } diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 6f371594..707bacc2 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -128,7 +128,7 @@ namespace SourceGit.Models _penCount = colors.Count; } - public static CommitGraph Parse(List commits) + public static CommitGraph Parse(List commits, HashSet canPushCommits, HashSet canPullCommits) { double UNIT_WIDTH = 12; double HALF_WIDTH = 6; @@ -148,6 +148,9 @@ namespace SourceGit.Models var isMerged = commit.IsMerged; var oldCount = unsolved.Count; + commit.CanPushToUpstream = canPushCommits.Remove(commit.SHA); + commit.CanPullFromUpstream = !commit.CanPushToUpstream && canPullCommits.Remove(commit.SHA); + // Update current y offset offsetY += UNIT_HEIGHT; diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index 02f5aeff..82acb005 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -36,14 +36,9 @@ namespace SourceGit.ViewModels get => Backend is Models.Branch; } - public bool IsUpstreamTrackStatusVisible + public string TrackStatus { - get => Backend is Models.Branch { IsLocal: true } branch && !string.IsNullOrEmpty(branch.UpstreamTrackStatus); - } - - public string UpstreamTrackStatus - { - get => Backend is Models.Branch branch ? branch.UpstreamTrackStatus : ""; + get => Backend is Models.Branch { IsLocal: true } branch ? branch.TrackStatus.ToString() : string.Empty; } public FontWeight NameFontWeight diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index c5150a38..15549038 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -451,7 +451,7 @@ namespace SourceGit.ViewModels var fastForward = new MenuItem(); fastForward.Header = new Views.NameHighlightedTextBlock("BranchCM.FastForward", upstream); fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); - fastForward.IsEnabled = !string.IsNullOrEmpty(current.UpstreamTrackStatus) && current.UpstreamTrackStatus.IndexOf('↑') < 0; + fastForward.IsEnabled = current.TrackStatus.Ahead.Count == 0; fastForward.Click += (_, e) => { if (PopupHost.CanCreatePopup()) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 5f82953f..dad551d0 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -784,8 +784,20 @@ namespace SourceGit.ViewModels limits += "--branches --remotes --tags"; } - var commits = new Commands.QueryCommits(FullPath, limits).Result(); - var graph = Models.CommitGraph.Parse(commits); + var canPushCommits = new HashSet(); + var canPullCommits = new HashSet(); + var currentBranch = Branches.Find(x => x.IsCurrent); + if (currentBranch != null) + { + foreach (var sha in currentBranch.TrackStatus.Ahead) + canPushCommits.Add(sha); + + foreach (var sha in currentBranch.TrackStatus.Behind) + canPullCommits.Add(sha); + } + + var commits = new Commands.QueryCommits(_fullpath, limits).Result(); + var graph = Models.CommitGraph.Parse(commits, canPushCommits, canPullCommits); Dispatcher.UIThread.Invoke(() => { @@ -1244,7 +1256,7 @@ namespace SourceGit.ViewModels var fastForward = new MenuItem(); fastForward.Header = new Views.NameHighlightedTextBlock("BranchCM.FastForward", upstream); fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); - fastForward.IsEnabled = !string.IsNullOrEmpty(branch.UpstreamTrackStatus) && branch.UpstreamTrackStatus.IndexOf('↑') < 0; + fastForward.IsEnabled = branch.TrackStatus.Ahead.Count == 0; fastForward.Click += (_, e) => { if (PopupHost.CanCreatePopup()) @@ -1295,7 +1307,7 @@ namespace SourceGit.ViewModels var fastForward = new MenuItem(); fastForward.Header = new Views.NameHighlightedTextBlock("BranchCM.FastForward", upstream.FriendlyName); fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); - fastForward.IsEnabled = !string.IsNullOrEmpty(branch.UpstreamTrackStatus) && branch.UpstreamTrackStatus.IndexOf('↑') < 0; + fastForward.IsEnabled = branch.TrackStatus.Ahead.Count == 0; fastForward.Click += (_, e) => { if (PopupHost.CanCreatePopup()) diff --git a/src/Views/BranchTree.axaml b/src/Views/BranchTree.axaml index a2a34065..4099dbaa 100644 --- a/src/Views/BranchTree.axaml +++ b/src/Views/BranchTree.axaml @@ -88,8 +88,13 @@ CornerRadius="9" VerticalAlignment="Center" Background="{DynamicResource Brush.Badge}" - IsVisible="{Binding IsUpstreamTrackStatusVisible}"> - + IsVisible="{Binding TrackStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"> + diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 1a74a08a..44d8d8e9 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -100,6 +100,16 @@ + + + +