Skip to content
44 changes: 44 additions & 0 deletions src/Commands/QueryCommitChangedLines.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
124 changes: 124 additions & 0 deletions src/Commands/QueryFileChangedLines.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
40 changes: 39 additions & 1 deletion src/Models/Commit.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

using Avalonia;
using Avalonia.Media;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
<x:String x:Key="Text.CommitDetail.Info.Parents" xml:space="preserve">PARENTS</x:String>
<x:String x:Key="Text.CommitDetail.Info.Refs" xml:space="preserve">REFS</x:String>
<x:String x:Key="Text.CommitDetail.Info.SHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.CommitDetail.Info.Stats" xml:space="preserve">STATS</x:String>
<x:String x:Key="Text.CommitDetail.Info.WebLinks" xml:space="preserve">Open in Browser</x:String>
<x:String x:Key="Text.CommitMessageTextBox.SubjectPlaceholder" xml:space="preserve">Enter commit subject</x:String>
<x:String x:Key="Text.CommitMessageTextBox.MessagePlaceholder" xml:space="preserve">Description</x:String>
Expand Down
45 changes: 44 additions & 1 deletion src/ViewModels/CommitDetail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -654,6 +678,24 @@ private void Refresh()
});
}

public void PreloadLineCountData(List<string> 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<Models.Hyperlink> ParseLinksInMessage(string message)
{
var links = new List<Models.Hyperlink>();
Expand Down Expand Up @@ -878,6 +920,7 @@ private void CalcRevisionFileSearchSuggestion()
private CancellationTokenSource _cancellationSource = null;
private List<string> _revisionFiles = null;
private string _revisionFileSearchFilter = string.Empty;
private Dictionary<string, (int added, int removed)> _lineCountCache = new Dictionary<string, (int added, int removed)>();
private List<string> _revisionFileSearchSuggestion = null;
}
}
50 changes: 49 additions & 1 deletion src/ViewModels/DiffContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace SourceGit.ViewModels
{
public class DiffContext : ObservableObject
public partial class DiffContext : ObservableObject
{
public string Title
{
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}

Expand Down
Loading