diff --git a/src/Commands/QueryCommitChangedLines.cs b/src/Commands/QueryCommitChangedLines.cs
new file mode 100644
index 00000000..81786a66
--- /dev/null
+++ b/src/Commands/QueryCommitChangedLines.cs
@@ -0,0 +1,44 @@
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Commands
+{
+ public class QueryCommitChangedLines : Command
+ {
+ public QueryCommitChangedLines(string repo, string sha)
+ {
+ WorkingDirectory = repo;
+ Context = repo;
+ Args = $"show --shortstat --oneline {sha}";
+ _pattern = new Regex(@"(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?");
+ }
+
+ public (int, int) Result()
+ {
+ _addedLines = 0;
+ _removedLines = 0;
+ Exec();
+ return (_addedLines, _removedLines);
+ }
+
+ protected override void OnReadline(string line)
+ {
+ var match = _pattern.Match(line);
+ if (match.Success)
+ {
+ if (match.Groups[2].Success)
+ {
+ _addedLines = int.Parse(match.Groups[2].Value);
+ }
+
+ if (match.Groups[3].Success)
+ {
+ _removedLines = int.Parse(match.Groups[3].Value);
+ }
+ }
+ }
+
+ private readonly Regex _pattern;
+ private int _addedLines;
+ private int _removedLines;
+ }
+}
diff --git a/src/Commands/QueryFileChangedLines.cs b/src/Commands/QueryFileChangedLines.cs
new file mode 100644
index 00000000..8c389114
--- /dev/null
+++ b/src/Commands/QueryFileChangedLines.cs
@@ -0,0 +1,124 @@
+using System.IO;
+
+namespace SourceGit.Commands
+{
+ public class QueryFileChangedLines : Command
+ {
+ public QueryFileChangedLines(string repo, string revision1, string revision2, string filePath)
+ {
+ WorkingDirectory = repo;
+ Context = repo;
+ _repo = repo;
+ _filePath = filePath;
+
+ // Handle various diff scenarios
+ if (string.IsNullOrEmpty(revision1) && string.IsNullOrEmpty(revision2))
+ {
+ // Working copy changes (unstaged)
+ Args = $"diff --numstat -- \"{filePath}\"";
+ _checkNewWorkingDirFile = true;
+ }
+ else if (string.IsNullOrEmpty(revision1) && revision2 == "--staged")
+ {
+ // Staged changes
+ Args = $"diff --cached --numstat -- \"{filePath}\"";
+ _checkNewStagedFile = true;
+ }
+ else if (string.IsNullOrEmpty(revision1) || revision1 == "/dev/null")
+ {
+ // New file case - we'll count lines manually
+ _isNewFile = true;
+ _newRevision = revision2;
+ }
+ else
+ {
+ // Comparing two revisions
+ Args = $"diff --numstat {revision1} {revision2} -- \"{filePath}\"";
+ }
+ }
+
+ public (int, int) Result()
+ {
+ _addedLines = 0;
+ _removedLines = 0;
+
+ // Check for new files first
+ if (_isNewFile || _checkNewWorkingDirFile || _checkNewStagedFile)
+ {
+ int lineCount = 0;
+
+ if (_isNewFile && !string.IsNullOrEmpty(_newRevision))
+ {
+ var stream = QueryFileContent.Run(_repo, _newRevision, _filePath);
+ using (var reader = new StreamReader(stream))
+ {
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ lineCount++;
+ }
+ }
+ }
+ else
+ {
+ var fullPath = Path.Combine(_repo, _filePath);
+ if (File.Exists(fullPath))
+ {
+ if (_checkNewWorkingDirFile || _checkNewStagedFile)
+ {
+ Exec();
+ if (_addedLines == 0 && _removedLines == 0)
+ {
+ var lines = File.ReadAllLines(fullPath);
+ lineCount = lines.Length;
+ }
+ else
+ {
+ return (_addedLines, _removedLines);
+ }
+ }
+ else
+ {
+ var lines = File.ReadAllLines(fullPath);
+ lineCount = lines.Length;
+ }
+ }
+ }
+
+ if (lineCount > 0)
+ {
+ return (lineCount, 0);
+ }
+ }
+
+ Exec();
+ return (_addedLines, _removedLines);
+ }
+
+ protected override void OnReadline(string line)
+ {
+ var parts = line.Split('\t');
+ if (parts.Length >= 2)
+ {
+ if (int.TryParse(parts[0], out int added))
+ {
+ _addedLines = added;
+ }
+
+ if (int.TryParse(parts[1], out int removed))
+ {
+ _removedLines = removed;
+ }
+ }
+ }
+
+ private readonly string _repo;
+ private readonly string _filePath;
+ private readonly bool _isNewFile = false;
+ private readonly string _newRevision = null;
+ private readonly bool _checkNewWorkingDirFile = false;
+ private readonly bool _checkNewStagedFile = false;
+ private int _addedLines;
+ private int _removedLines;
+ }
+}
diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs
index 0bad8376..edda603f 100644
--- a/src/Models/Commit.cs
+++ b/src/Models/Commit.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Media;
@@ -15,8 +17,15 @@ public enum CommitSearchMethod
ByFile,
}
- public class Commit
+ public class Commit : INotifyPropertyChanged
{
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
public static double OpacityForNotMerged
{
get;
@@ -48,6 +57,35 @@ public static double OpacityForNotMerged
public Thickness Margin { get; set; } = new Thickness(0);
public IBrush Brush => CommitGraph.Pens[Color].Brush;
+ private int _addedLines = 0;
+ private int _removedLines = 0;
+
+ public int AddedLines
+ {
+ get => _addedLines;
+ set
+ {
+ if (_addedLines != value)
+ {
+ _addedLines = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public int RemovedLines
+ {
+ get => _removedLines;
+ set
+ {
+ if (_removedLines != value)
+ {
+ _removedLines = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
public void ParseDecorators(string data)
{
if (data.Length < 3)
diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml
index ad71427b..94f8c938 100644
--- a/src/Resources/Locales/en_US.axaml
+++ b/src/Resources/Locales/en_US.axaml
@@ -130,6 +130,7 @@
PARENTS
REFS
SHA
+ STATS
Open in Browser
Enter commit subject
Description
diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs
index d04e674b..3a2da602 100644
--- a/src/ViewModels/CommitDetail.cs
+++ b/src/ViewModels/CommitDetail.cs
@@ -548,7 +548,7 @@ public ContextMenu CreateRevisionFileContextMenu(Models.Object file)
menu.Items.Add(resetToThisRevision);
menu.Items.Add(resetToFirstParent);
- menu.Items.Add(new MenuItem() { Header = "-" });
+ menu.Items.Add(new MenuItem { Header = "-" });
if (File.Exists(Path.Combine(fullPath)))
TryToAddContextMenuItemsForGitLFS(menu, file.Path);
@@ -590,6 +590,30 @@ private void Refresh()
if (_commit == null)
return;
+ var sha = _commit.SHA;
+
+ if (_lineCountCache.TryGetValue(sha, out var lineCount))
+ {
+ _commit.AddedLines = lineCount.added;
+ _commit.RemovedLines = lineCount.removed;
+ }
+ else
+ {
+ Task.Run(() =>
+ {
+ (var addedLines, var removedLines) = new Commands.QueryCommitChangedLines(_repo.FullPath, sha).Result();
+ _lineCountCache[sha] = (addedLines, removedLines);
+
+ Dispatcher.UIThread.Invoke(() => {
+ if (_commit != null && _commit.SHA == sha)
+ {
+ _commit.AddedLines = addedLines;
+ _commit.RemovedLines = removedLines;
+ }
+ });
+ });
+ }
+
if (_cancellationSource is { IsCancellationRequested: false })
_cancellationSource.Cancel();
@@ -654,6 +678,24 @@ private void Refresh()
});
}
+ public void PreloadLineCountData(List commitSHAs)
+ {
+ if (commitSHAs == null || commitSHAs.Count == 0)
+ return;
+
+ Task.Run(() =>
+ {
+ foreach (var sha in commitSHAs)
+ {
+ if (!_lineCountCache.ContainsKey(sha))
+ {
+ var (addedLines, removedLines) = new Commands.QueryCommitChangedLines(_repo.FullPath, sha).Result();
+ _lineCountCache[sha] = (addedLines, removedLines);
+ }
+ }
+ });
+ }
+
private List ParseLinksInMessage(string message)
{
var links = new List();
@@ -878,6 +920,7 @@ private void CalcRevisionFileSearchSuggestion()
private CancellationTokenSource _cancellationSource = null;
private List _revisionFiles = null;
private string _revisionFileSearchFilter = string.Empty;
+ private Dictionary _lineCountCache = new Dictionary();
private List _revisionFileSearchSuggestion = null;
}
}
diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs
index 6dd836bf..4775e3e1 100644
--- a/src/ViewModels/DiffContext.cs
+++ b/src/ViewModels/DiffContext.cs
@@ -10,7 +10,7 @@
namespace SourceGit.ViewModels
{
- public class DiffContext : ObservableObject
+ public partial class DiffContext : ObservableObject
{
public string Title
{
@@ -51,6 +51,12 @@ public int UnifiedLines
private set => SetProperty(ref _unifiedLines, value);
}
+ [ObservableProperty]
+ private int _addedLines;
+
+ [ObservableProperty]
+ private int _removedLines;
+
public DiffContext(string repo, Models.DiffOption option, DiffContext previous = null)
{
_repo = repo;
@@ -71,6 +77,48 @@ public DiffContext(string repo, Models.DiffOption option, DiffContext previous =
else
_title = $"{_option.OrgPath} → {_option.Path}";
+ AddedLines = 0;
+ RemovedLines = 0;
+
+ Task.Run(() =>
+ {
+ string oldRevision = "";
+ string newRevision = "";
+ string filePath = option.Path;
+
+ if (option.Revisions.Count == 2)
+ {
+ oldRevision = option.Revisions[0];
+ newRevision = option.Revisions[1];
+
+ if (string.IsNullOrEmpty(oldRevision) || string.IsNullOrEmpty(newRevision))
+ {
+ var result = new Commands.QueryFileChangedLines(repo, oldRevision, newRevision, filePath).Result();
+
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ AddedLines = result.Item1;
+ RemovedLines = result.Item2;
+ });
+ return;
+ }
+ }
+
+ if (option.Revisions.Count == 1 && option.Revisions[0] == "STAGE")
+ {
+ oldRevision = "HEAD";
+ newRevision = "--staged";
+ }
+
+ var lineChanges = new Commands.QueryFileChangedLines(repo, oldRevision, newRevision, filePath).Result();
+
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ AddedLines = lineChanges.Item1;
+ RemovedLines = lineChanges.Item2;
+ });
+ });
+
LoadDiffContent();
}
diff --git a/src/Views/CommitBaseInfo.axaml b/src/Views/CommitBaseInfo.axaml
index f83b43fc..b73e6e8d 100644
--- a/src/Views/CommitBaseInfo.axaml
+++ b/src/Views/CommitBaseInfo.axaml
@@ -51,7 +51,7 @@
-
+
@@ -181,9 +181,13 @@
UseGraphColor="False"/>
+
+
+
+
-
-
+
+ {
+ if (e.PropertyName == nameof(detail.Commit))
+ {
+ InvalidateVisual();
+ }
+ };
+ }
+ }
+
private void OnCopyCommitSHA(object sender, RoutedEventArgs e)
{
if (sender is Button { DataContext: Models.Commit commit })
diff --git a/src/Views/DiffView.axaml b/src/Views/DiffView.axaml
index c4da158d..4442c81c 100644
--- a/src/Views/DiffView.axaml
+++ b/src/Views/DiffView.axaml
@@ -34,6 +34,8 @@
+
+