From b3fa69e677a24430e92f6882d293ca8c413e438d Mon Sep 17 00:00:00 2001 From: GEV <67133971+geviraydev@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:01:53 +0800 Subject: [PATCH] feature: add total added/removed line counts to changes views --- src/Commands/QueryDiffLineStats.cs | 64 ++++++++++++++++++++++++++++++ src/ViewModels/CommitDetail.cs | 19 +++++++++ src/ViewModels/Compare.cs | 25 ++++++++++++ src/ViewModels/RevisionCompare.cs | 29 +++++++++++++- src/ViewModels/WorkingCopy.cs | 52 ++++++++++++++++++++++++ src/Views/CommitChanges.axaml | 25 +++++++++--- src/Views/Compare.axaml | 24 ++++++++--- src/Views/RevisionCompare.axaml | 24 ++++++++--- src/Views/WorkingCopy.axaml | 32 ++++++++++++++- 9 files changed, 273 insertions(+), 21 deletions(-) create mode 100644 src/Commands/QueryDiffLineStats.cs diff --git a/src/Commands/QueryDiffLineStats.cs b/src/Commands/QueryDiffLineStats.cs new file mode 100644 index 000000000..0767bc801 --- /dev/null +++ b/src/Commands/QueryDiffLineStats.cs @@ -0,0 +1,64 @@ +using System; +using System.Text; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class QueryDiffLineStats : Command + { + public QueryDiffLineStats(string repo, string start, string end, bool ignoreWhitespace, bool ignoreCRAtEOL) + : this(repo, ignoreWhitespace, ignoreCRAtEOL, $"{(string.IsNullOrEmpty(start) ? "-R" : start)} {end}") + { + } + + public QueryDiffLineStats(string repo, bool staged, bool ignoreWhitespace, bool ignoreCRAtEOL) + : this(repo, ignoreWhitespace, ignoreCRAtEOL, staged ? "--cached" : string.Empty) + { + } + + private QueryDiffLineStats(string repo, bool ignoreWhitespace, bool ignoreCRAtEOL, string suffix) + { + WorkingDirectory = repo; + Context = repo; + RaiseError = false; + + var builder = new StringBuilder(); + builder.Append("diff --no-color --no-ext-diff --numstat -z "); + if (ignoreCRAtEOL) + builder.Append("--ignore-cr-at-eol "); + if (ignoreWhitespace) + builder.Append("--ignore-space-change --ignore-blank-lines "); + builder.Append(suffix); + Args = builder.ToString().TrimEnd(); + } + + public async Task<(int added, int deleted)> GetResultAsync() + { + try + { + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(rs.StdOut)) + return (0, 0); + + int added = 0, deleted = 0; + foreach (var token in rs.StdOut.Split('\0', StringSplitOptions.RemoveEmptyEntries)) + { + var parts = token.Split('\t', 3); + if (parts.Length < 2) + continue; + + added += ParseCount(parts[0]); + deleted += ParseCount(parts[1]); + } + + return (added, deleted); + } + catch + { + return (0, 0); + } + } + + private static int ParseCount(string value) => int.TryParse(value, out var parsed) ? parsed : 0; + } +} diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 442f57033..411163011 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -97,6 +97,18 @@ public List VisibleChanges set => SetProperty(ref _visibleChanges, value); } + public int TotalAddedLines + { + get => _totalAddedLines; + private set => SetProperty(ref _totalAddedLines, value); + } + + public int TotalDeletedLines + { + get => _totalDeletedLines; + private set => SetProperty(ref _totalDeletedLines, value); + } + public List SelectedChanges { get => _selectedChanges; @@ -534,6 +546,9 @@ private void Refresh() { var cmd = new Commands.CompareRevisions(_repo.FullPath, _commit.FirstParentToCompare, _commit.SHA) { CancellationToken = token }; var changes = await cmd.ReadAsync().ConfigureAwait(false); + var pref = Preferences.Instance; + var statsCmd = new Commands.QueryDiffLineStats(_repo.FullPath, _commit.FirstParentToCompare, _commit.SHA, pref.IgnoreWhitespaceChangesInDiff, pref.IgnoreCRAtEOLInDiff) { CancellationToken = token }; + var stats = await statsCmd.GetResultAsync().ConfigureAwait(false); var visible = changes; if (!string.IsNullOrWhiteSpace(_searchChangeFilter)) { @@ -551,6 +566,8 @@ private void Refresh() { Changes = changes; VisibleChanges = visible; + TotalAddedLines = stats.added; + TotalDeletedLines = stats.deleted; if (visible.Count == 0) SelectedChanges = null; @@ -768,6 +785,8 @@ private async Task SetViewingCommitAsync(Models.Object file) private List _changes = []; private List _visibleChanges = []; private List _selectedChanges = null; + private int _totalAddedLines = 0; + private int _totalDeletedLines = 0; private string _searchChangeFilter = string.Empty; private DiffContext _diffContext = null; private string _viewRevisionFilePath = string.Empty; diff --git a/src/ViewModels/Compare.cs b/src/ViewModels/Compare.cs index 9a3f4f490..0fed14d59 100644 --- a/src/ViewModels/Compare.cs +++ b/src/ViewModels/Compare.cs @@ -62,6 +62,18 @@ public int TotalChanges private set => SetProperty(ref _totalChanges, value); } + public int TotalAddedLines + { + get => _totalAddedLines; + private set => SetProperty(ref _totalAddedLines, value); + } + + public int TotalDeletedLines + { + get => _totalDeletedLines; + private set => SetProperty(ref _totalDeletedLines, value); + } + public List VisibleChanges { get => _visibleChanges; @@ -317,6 +329,15 @@ private void UpdateChanges() .ReadAsync() .ConfigureAwait(false); + var pref = Preferences.Instance; + var stats = await new Commands.QueryDiffLineStats( + _repo.FullPath, + _based, + _to, + pref.IgnoreWhitespaceChangesInDiff, + pref.IgnoreCRAtEOLInDiff) + .GetResultAsync().ConfigureAwait(false); + var visible = _changes; if (!string.IsNullOrWhiteSpace(_searchFilter)) { @@ -331,6 +352,8 @@ private void UpdateChanges() Dispatcher.UIThread.Post(() => { TotalChanges = _changes.Count; + TotalAddedLines = stats.added; + TotalDeletedLines = stats.deleted; VisibleChanges = visible; IsLoadingChanges = false; @@ -398,6 +421,8 @@ private string GetSHA(object obj) private Models.Commit _baseHead = null; private Models.Commit _toHead = null; private int _totalChanges = 0; + private int _totalAddedLines = 0; + private int _totalDeletedLines = 0; private List _changes = null; private List _visibleChanges = null; private List _selectedChanges = null; diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index ef38668cc..cfba0dffc 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -59,6 +59,18 @@ public int TotalChanges private set => SetProperty(ref _totalChanges, value); } + public int TotalAddedLines + { + get => _totalAddedLines; + private set => SetProperty(ref _totalAddedLines, value); + } + + public int TotalDeletedLines + { + get => _totalDeletedLines; + private set => SetProperty(ref _totalDeletedLines, value); + } + public List VisibleChanges { get => _visibleChanges; @@ -350,10 +362,21 @@ private void Refresh() { Task.Run(async () => { - _changes = await new Commands.CompareRevisions(_repo.FullPath, GetSHA(_startPoint), GetSHA(_endPoint)) + var startSHA = GetSHA(_startPoint); + var endSHA = GetSHA(_endPoint); + _changes = await new Commands.CompareRevisions(_repo.FullPath, startSHA, endSHA) .ReadAsync() .ConfigureAwait(false); + var pref = Preferences.Instance; + var stats = await new Commands.QueryDiffLineStats( + _repo.FullPath, + startSHA, + endSHA, + pref.IgnoreWhitespaceChangesInDiff, + pref.IgnoreCRAtEOLInDiff) + .GetResultAsync().ConfigureAwait(false); + var visible = _changes; if (!string.IsNullOrWhiteSpace(_searchFilter)) { @@ -368,6 +391,8 @@ private void Refresh() Dispatcher.UIThread.Post(() => { TotalChanges = _changes.Count; + TotalAddedLines = stats.added; + TotalDeletedLines = stats.deleted; VisibleChanges = visible; IsLoading = false; @@ -394,6 +419,8 @@ private string GetDesc(object obj) private object _startPoint = null; private object _endPoint = null; private int _totalChanges = 0; + private int _totalAddedLines = 0; + private int _totalDeletedLines = 0; private List _changes = null; private List _visibleChanges = null; private List _selectedChanges = null; diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index c4a4c6486..23cb55ac1 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -160,6 +161,30 @@ public List VisibleStaged private set => SetProperty(ref _visibleStaged, value); } + public int UnstagedAddedLines + { + get => _unstagedAddedLines; + private set => SetProperty(ref _unstagedAddedLines, value); + } + + public int UnstagedDeletedLines + { + get => _unstagedDeletedLines; + private set => SetProperty(ref _unstagedDeletedLines, value); + } + + public int StagedAddedLines + { + get => _stagedAddedLines; + private set => SetProperty(ref _stagedAddedLines, value); + } + + public int StagedDeletedLines + { + get => _stagedDeletedLines; + private set => SetProperty(ref _stagedDeletedLines, value); + } + public List SelectedUnstaged { get => _selectedUnstaged; @@ -312,6 +337,29 @@ public void SetData(List changes) UpdateInProgressState(); UpdateDetail(); + + _ = Task.Run(async () => + { + var pref = Preferences.Instance; + var unstagedStats = await new Commands.QueryDiffLineStats( + _repo.FullPath, false, pref.IgnoreWhitespaceChangesInDiff, pref.IgnoreCRAtEOLInDiff) + .GetResultAsync().ConfigureAwait(false); + var stagedStats = await new Commands.QueryDiffLineStats( + _repo.FullPath, true, pref.IgnoreWhitespaceChangesInDiff, pref.IgnoreCRAtEOLInDiff) + .GetResultAsync().ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => + { + // A newer SetData may have run while fetching stats; drop this stale update. + if (!ReferenceEquals(_cached, changes)) + return; + + UnstagedAddedLines = unstagedStats.added; + UnstagedDeletedLines = unstagedStats.deleted; + StagedAddedLines = stagedStats.added; + StagedDeletedLines = stagedStats.deleted; + }); + }); } public async Task StageChangesAsync(List changes, Models.Change next) @@ -821,6 +869,10 @@ private bool IsChanged(List old, List cur) private List _visibleStaged = []; private List _selectedUnstaged = []; private List _selectedStaged = []; + private int _unstagedAddedLines = 0; + private int _unstagedDeletedLines = 0; + private int _stagedAddedLines = 0; + private int _stagedDeletedLines = 0; private object _detailContext = null; private string _filter = string.Empty; private string _commitMessage = string.Empty; diff --git a/src/Views/CommitChanges.axaml b/src/Views/CommitChanges.axaml index d7d245ccf..c57797d48 100644 --- a/src/Views/CommitChanges.axaml +++ b/src/Views/CommitChanges.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.CommitChanges" x:DataType="vm:CommitDetail"> @@ -56,12 +57,24 @@ - - - - + + + + + + + + diff --git a/src/Views/Compare.axaml b/src/Views/Compare.axaml index 3f340d495..f781754b6 100644 --- a/src/Views/Compare.axaml +++ b/src/Views/Compare.axaml @@ -193,12 +193,24 @@ - - - - + + + + + + + + diff --git a/src/Views/RevisionCompare.axaml b/src/Views/RevisionCompare.axaml index 5452a4e5e..1f07c756e 100644 --- a/src/Views/RevisionCompare.axaml +++ b/src/Views/RevisionCompare.axaml @@ -123,12 +123,24 @@ - - - - + + + + + + + + diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index 4275460c4..6b29e050b 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -67,7 +67,21 @@ - + + + + +