using System.Diagnostics; using System.Text; namespace ClaudeDo.Data.Git; public sealed class GitService { public async Task IsGitRepoAsync(string dir, CancellationToken ct = default) { var (exitCode, _, _) = await RunGitAsync(dir, ["rev-parse", "--git-dir"], ct); return exitCode == 0; } public async Task RevParseHeadAsync(string dir, CancellationToken ct = default) { var (exitCode, stdout, stderr) = await RunGitAsync(dir, ["rev-parse", "HEAD"], ct); if (exitCode != 0) throw new InvalidOperationException($"git rev-parse HEAD failed (exit {exitCode}): {stderr}"); return stdout.Trim(); } public async Task WorktreeAddAsync(string repoDir, string branchName, string worktreePath, string baseCommit, CancellationToken ct = default) { var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["worktree", "add", "-b", branchName, worktreePath, baseCommit], ct); if (exitCode != 0) throw new InvalidOperationException($"git worktree add failed (exit {exitCode}): {stderr}"); } public async Task HasChangesAsync(string worktreePath, CancellationToken ct = default) { var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, ["status", "--porcelain"], ct); if (exitCode != 0) throw new InvalidOperationException($"git status --porcelain failed (exit {exitCode}): {stderr}"); return !string.IsNullOrWhiteSpace(stdout); } public async Task AddAllAsync(string worktreePath, CancellationToken ct = default) { var (exitCode, _, stderr) = await RunGitAsync(worktreePath, ["add", "-A"], ct); if (exitCode != 0) throw new InvalidOperationException($"git add -A failed (exit {exitCode}): {stderr}"); } public async Task CommitAsync(string worktreePath, string message, CancellationToken ct = default) { // Use -F - (read message from stdin) to handle multi-line messages safely. var (exitCode, _, stderr) = await RunGitAsync(worktreePath, ["commit", "-F", "-"], ct, stdinData: message); if (exitCode != 0) throw new InvalidOperationException($"git commit failed (exit {exitCode}): {stderr}"); } public async Task DiffStatAsync(string worktreePath, string baseCommit, string headCommit, CancellationToken ct = default) { var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, ["diff", "--stat", $"{baseCommit}..{headCommit}"], ct); if (exitCode != 0) throw new InvalidOperationException($"git diff --stat failed (exit {exitCode}): {stderr}"); return stdout.Trim(); } public async Task WorktreeRemoveAsync(string repoDir, string worktreePath, bool force = false, CancellationToken ct = default) { var args = new List { "worktree", "remove" }; if (force) args.Add("--force"); args.Add(worktreePath); var (exitCode, _, stderr) = await RunGitAsync(repoDir, args, ct); if (exitCode != 0) throw new InvalidOperationException($"git worktree remove failed (exit {exitCode}): {stderr}"); } public async Task BranchDeleteAsync(string repoDir, string branchName, bool force = false, CancellationToken ct = default) { var flag = force ? "-D" : "-d"; var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["branch", flag, branchName], ct); if (exitCode != 0) throw new InvalidOperationException($"git branch {flag} failed (exit {exitCode}): {stderr}"); } public async Task MergeFfOnlyAsync(string repoDir, string branchName, CancellationToken ct = default) { var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["merge", "--ff-only", branchName], ct); if (exitCode != 0) throw new InvalidOperationException($"Fast-forward merge of '{branchName}' failed. Manual merge required. git stderr: {stderr}"); } private static async Task<(int ExitCode, string Stdout, string Stderr)> RunGitAsync( string workDir, IEnumerable args, CancellationToken ct, string? stdinData = null) { var psi = new ProcessStartInfo { FileName = "git", RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = stdinData is not null, UseShellExecute = false, CreateNoWindow = true, }; psi.ArgumentList.Add("-C"); psi.ArgumentList.Add(workDir); foreach (var a in args) psi.ArgumentList.Add(a); using var proc = new Process { StartInfo = psi }; proc.Start(); if (stdinData is not null) { await proc.StandardInput.WriteAsync(stdinData.AsMemory(), ct); proc.StandardInput.Close(); } var stdoutTask = proc.StandardOutput.ReadToEndAsync(ct); var stderrTask = proc.StandardError.ReadToEndAsync(ct); await proc.WaitForExitAsync(ct); var stdout = await stdoutTask; var stderr = await stderrTask; return (proc.ExitCode, stdout.TrimEnd(), stderr.TrimEnd()); } }