- Count additions/deletions per file as lines are parsed. - Surface load failures and empty-diff states via StatusMessage. - Pass the worktree base commit so diffs render against the branch base, not just the working-tree HEAD.
191 lines
6.1 KiB
C#
191 lines
6.1 KiB
C#
using System.Collections.ObjectModel;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using ClaudeDo.Data.Git;
|
|
|
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
|
|
|
public enum DiffLineKind { Add, Del, Ctx }
|
|
|
|
public sealed class DiffLineViewModel
|
|
{
|
|
public required DiffLineKind Kind { get; init; }
|
|
public int? OldNo { get; init; }
|
|
public int? NewNo { get; init; }
|
|
public required string Text { get; init; }
|
|
public string ClassName => Kind switch
|
|
{
|
|
DiffLineKind.Add => "add",
|
|
DiffLineKind.Del => "del",
|
|
_ => "ctx",
|
|
};
|
|
|
|
public string Sign => Kind switch
|
|
{
|
|
DiffLineKind.Add => "+",
|
|
DiffLineKind.Del => "-",
|
|
_ => " ",
|
|
};
|
|
}
|
|
|
|
public sealed class DiffFileViewModel
|
|
{
|
|
public required string Path { get; init; }
|
|
public int Additions { get; set; }
|
|
public int Deletions { get; set; }
|
|
public ObservableCollection<DiffLineViewModel> Lines { get; } = new();
|
|
}
|
|
|
|
public sealed partial class DiffModalViewModel : ViewModelBase
|
|
{
|
|
private readonly GitService _git;
|
|
|
|
public required string WorktreePath { get; init; }
|
|
public string? BaseRef { get; init; }
|
|
public string? TaskId { get; init; }
|
|
public string TaskTitle { get; init; } = "";
|
|
public Func<MergeModalViewModel, Task>? ShowMergeModal { get; set; }
|
|
public Func<MergeModalViewModel>? ResolveMergeVm { get; set; }
|
|
|
|
public ObservableCollection<DiffFileViewModel> Files { get; } = new();
|
|
|
|
[ObservableProperty] private DiffFileViewModel? _selectedFile;
|
|
[ObservableProperty] private string? _statusMessage;
|
|
|
|
// Injected action to close the owning Window
|
|
public Action? CloseAction { get; set; }
|
|
|
|
public DiffModalViewModel(GitService git)
|
|
{
|
|
_git = git;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Close() => CloseAction?.Invoke();
|
|
|
|
private bool CanMerge() =>
|
|
!string.IsNullOrEmpty(TaskId)
|
|
&& ShowMergeModal is not null
|
|
&& ResolveMergeVm is not null;
|
|
|
|
[RelayCommand(CanExecute = nameof(CanMerge))]
|
|
private async Task MergeAsync()
|
|
{
|
|
if (TaskId is null || ShowMergeModal is null || ResolveMergeVm is null) return;
|
|
var vm = ResolveMergeVm();
|
|
await vm.InitializeAsync(TaskId, TaskTitle);
|
|
await ShowMergeModal(vm);
|
|
}
|
|
|
|
public async Task LoadAsync(CancellationToken ct = default)
|
|
{
|
|
Files.Clear();
|
|
StatusMessage = null;
|
|
|
|
string raw;
|
|
try
|
|
{
|
|
raw = BaseRef is not null
|
|
? await _git.GetBranchDiffAsync(WorktreePath, BaseRef, ct)
|
|
: await _git.GetDiffAsync(WorktreePath, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"Failed to load diff: {ex.Message}";
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
{
|
|
StatusMessage = "No changes to show.";
|
|
return;
|
|
}
|
|
|
|
// Parse unified diff — state machine over lines
|
|
DiffFileViewModel? current = null;
|
|
int oldLine = 0, newLine = 0;
|
|
|
|
foreach (var line in raw.Split('\n'))
|
|
{
|
|
if (line.StartsWith("diff --git ", StringComparison.Ordinal))
|
|
{
|
|
// e.g. "diff --git a/src/Foo.cs b/src/Foo.cs"
|
|
var parts = line.Split(' ');
|
|
var path = parts.Length >= 4 ? parts[3][2..] : line;
|
|
current = new DiffFileViewModel { Path = path };
|
|
Files.Add(current);
|
|
oldLine = 0; newLine = 0;
|
|
continue;
|
|
}
|
|
|
|
if (current == null) continue;
|
|
|
|
if (line.StartsWith("@@ ", StringComparison.Ordinal))
|
|
{
|
|
// e.g. "@@ -10,7 +10,9 @@"
|
|
ParseHunkHeader(line, out oldLine, out newLine);
|
|
continue;
|
|
}
|
|
|
|
// Skip diff metadata lines
|
|
if (line.StartsWith("--- ", StringComparison.Ordinal) ||
|
|
line.StartsWith("+++ ", StringComparison.Ordinal) ||
|
|
line.StartsWith("index ", StringComparison.Ordinal) ||
|
|
line.StartsWith("new file", StringComparison.Ordinal) ||
|
|
line.StartsWith("deleted file", StringComparison.Ordinal) ||
|
|
line.StartsWith("Binary ", StringComparison.Ordinal))
|
|
continue;
|
|
|
|
if (line.StartsWith('+'))
|
|
{
|
|
current.Lines.Add(new DiffLineViewModel
|
|
{
|
|
Kind = DiffLineKind.Add,
|
|
NewNo = newLine++,
|
|
Text = line.Length > 1 ? line[1..] : "",
|
|
});
|
|
current.Additions++;
|
|
}
|
|
else if (line.StartsWith('-'))
|
|
{
|
|
current.Lines.Add(new DiffLineViewModel
|
|
{
|
|
Kind = DiffLineKind.Del,
|
|
OldNo = oldLine++,
|
|
Text = line.Length > 1 ? line[1..] : "",
|
|
});
|
|
current.Deletions++;
|
|
}
|
|
else if (line.StartsWith(' '))
|
|
{
|
|
current.Lines.Add(new DiffLineViewModel
|
|
{
|
|
Kind = DiffLineKind.Ctx,
|
|
OldNo = oldLine++,
|
|
NewNo = newLine++,
|
|
Text = line.Length > 1 ? line[1..] : "",
|
|
});
|
|
}
|
|
}
|
|
|
|
SelectedFile = Files.Count > 0 ? Files[0] : null;
|
|
if (Files.Count == 0) StatusMessage = "No changes to show.";
|
|
}
|
|
|
|
private static void ParseHunkHeader(string header, out int oldStart, out int newStart)
|
|
{
|
|
oldStart = 1; newStart = 1;
|
|
// Format: @@ -<old>,<count> +<new>,<count> @@
|
|
var at = header.IndexOf("@@", 3, StringComparison.Ordinal);
|
|
var inner = at > 0 ? header[3..at].Trim() : header;
|
|
var segs = inner.Split(' ');
|
|
foreach (var seg in segs)
|
|
{
|
|
if (seg.StartsWith('-') && int.TryParse(seg[1..].Split(',')[0], out var o))
|
|
oldStart = o;
|
|
else if (seg.StartsWith('+') && int.TryParse(seg[1..].Split(',')[0], out var n))
|
|
newStart = n;
|
|
}
|
|
}
|
|
}
|