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 @@ + +