Files
ClaudeDo/src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs

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;
}
}