170 lines
5.9 KiB
C#
170 lines
5.9 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 WorktreeDiffLineKind { Header, Hunk, Added, Removed, Context }
|
|
|
|
public sealed partial class WorktreeDiffLineViewModel : ViewModelBase
|
|
{
|
|
public required string Text { get; init; }
|
|
public required WorktreeDiffLineKind Kind { get; init; }
|
|
}
|
|
|
|
public sealed partial class WorktreeNodeViewModel : ViewModelBase
|
|
{
|
|
public required string Name { get; init; }
|
|
public string? Status { get; init; }
|
|
public bool IsDirectory { get; init; }
|
|
public string RelativePath { get; init; } = "";
|
|
public ObservableCollection<WorktreeNodeViewModel> Children { get; } = new();
|
|
[ObservableProperty] private bool _isExpanded = true;
|
|
}
|
|
|
|
public sealed partial class WorktreeModalViewModel : ViewModelBase
|
|
{
|
|
private readonly GitService _git;
|
|
|
|
public ObservableCollection<WorktreeNodeViewModel> Root { get; } = new();
|
|
public ObservableCollection<WorktreeDiffLineViewModel> SelectedFileDiffLines { get; } = new();
|
|
|
|
[ObservableProperty] private string _worktreePath = "";
|
|
[ObservableProperty] private string? _baseCommit;
|
|
[ObservableProperty] private WorktreeNodeViewModel? _selectedNode;
|
|
|
|
// Set by the view (same pattern as DiffModalViewModel.CloseAction)
|
|
public Action? CloseAction { get; set; }
|
|
|
|
public WorktreeModalViewModel(GitService git)
|
|
{
|
|
_git = git;
|
|
}
|
|
|
|
partial void OnSelectedNodeChanged(WorktreeNodeViewModel? value)
|
|
{
|
|
_ = LoadFileDiffAsync(value);
|
|
}
|
|
|
|
private async Task LoadFileDiffAsync(WorktreeNodeViewModel? node)
|
|
{
|
|
SelectedFileDiffLines.Clear();
|
|
|
|
if (node is null || node.IsDirectory || string.IsNullOrEmpty(node.RelativePath))
|
|
return;
|
|
|
|
string diff;
|
|
try
|
|
{
|
|
diff = await _git.GetFileDiffAsync(WorktreePath, BaseCommit, node.RelativePath);
|
|
}
|
|
catch
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var line in diff.Split('\n'))
|
|
{
|
|
var kind = line switch
|
|
{
|
|
_ when line.StartsWith("+++") || line.StartsWith("---") => WorktreeDiffLineKind.Header,
|
|
_ when line.StartsWith("@@") => WorktreeDiffLineKind.Hunk,
|
|
_ when line.StartsWith('+') => WorktreeDiffLineKind.Added,
|
|
_ when line.StartsWith('-') => WorktreeDiffLineKind.Removed,
|
|
_ when line.StartsWith("diff ") || line.StartsWith("index ") || line.StartsWith("\\ ") => WorktreeDiffLineKind.Header,
|
|
_ => WorktreeDiffLineKind.Context,
|
|
};
|
|
SelectedFileDiffLines.Add(new WorktreeDiffLineViewModel { Text = line, Kind = kind });
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Close() => CloseAction?.Invoke();
|
|
|
|
public async Task LoadAsync(CancellationToken ct = default)
|
|
{
|
|
Root.Clear();
|
|
|
|
string stdout;
|
|
bool committedMode = !string.IsNullOrEmpty(BaseCommit);
|
|
try
|
|
{
|
|
stdout = committedMode
|
|
? await _git.GetCommittedFilesAsync(WorktreePath, BaseCommit!, ct)
|
|
: await _git.GetStatusPorcelainAsync(WorktreePath, ct);
|
|
}
|
|
catch { return; }
|
|
|
|
if (string.IsNullOrWhiteSpace(stdout)) return;
|
|
|
|
var dirs = new Dictionary<string, WorktreeNodeViewModel>(StringComparer.Ordinal);
|
|
|
|
foreach (var line in stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
string? path;
|
|
string? status;
|
|
|
|
if (committedMode)
|
|
{
|
|
// diff --name-status format: <status>\t<path>
|
|
var tab = line.IndexOf('\t');
|
|
if (tab < 0) continue;
|
|
var statusChar = line[0];
|
|
status = statusChar != ' ' ? statusChar.ToString() : null;
|
|
path = line[(tab + 1)..].Trim().Replace('\\', '/');
|
|
}
|
|
else
|
|
{
|
|
// porcelain format: XY<space>path
|
|
if (line.Length < 4) continue;
|
|
var xy = line[..2];
|
|
var statusChar = xy[0] != ' ' ? xy[0] : xy[1];
|
|
status = statusChar != ' ' ? statusChar.ToString() : null;
|
|
path = line[3..].Trim().Replace('\\', '/');
|
|
}
|
|
|
|
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
if (segments.Length == 0) continue;
|
|
|
|
WorktreeNodeViewModel? parent = null;
|
|
var accumulated = "";
|
|
for (var i = 0; i < segments.Length - 1; i++)
|
|
{
|
|
accumulated = accumulated.Length == 0 ? segments[i] : accumulated + "/" + segments[i];
|
|
if (!dirs.TryGetValue(accumulated, out var dir))
|
|
{
|
|
dir = new WorktreeNodeViewModel { Name = segments[i], IsDirectory = true };
|
|
dirs[accumulated] = dir;
|
|
if (parent == null) Root.Add(dir);
|
|
else parent.Children.Add(dir);
|
|
}
|
|
parent = dir;
|
|
}
|
|
|
|
var leaf = new WorktreeNodeViewModel
|
|
{
|
|
Name = segments[^1],
|
|
Status = status,
|
|
IsDirectory = false,
|
|
RelativePath = path
|
|
};
|
|
if (parent == null) Root.Add(leaf);
|
|
else parent.Children.Add(leaf);
|
|
}
|
|
|
|
SelectedNode = FindFirstLeaf(Root);
|
|
}
|
|
|
|
private static WorktreeNodeViewModel? FindFirstLeaf(IEnumerable<WorktreeNodeViewModel> nodes)
|
|
{
|
|
foreach (var n in nodes)
|
|
{
|
|
if (!n.IsDirectory) return n;
|
|
var nested = FindFirstLeaf(n.Children);
|
|
if (nested is not null) return nested;
|
|
}
|
|
return null;
|
|
}
|
|
}
|