using System.Diagnostics; using System.Text; using ClaudeDo.Data.Git; namespace ClaudeDo.Worker.Skills; public sealed class GitRepoCloner : IRepoCloner { private readonly GitService _git; public GitRepoCloner(GitService git) => _git = git; public async Task CloneAsync(string url, string destDir, CancellationToken ct) { var parentDir = Path.GetDirectoryName(destDir); if (!string.IsNullOrEmpty(parentDir)) Directory.CreateDirectory(parentDir); var (exitCode, stderr) = await RunGitAsync(["clone", "--depth", "1", url, destDir], ct); if (exitCode != 0) throw new InvalidOperationException($"git clone '{url}' failed (exit {exitCode}): {stderr}"); var pinnedRef = await _git.RevParseHeadAsync(destDir, ct); return new ClonedRepo(destDir, pinnedRef); } private static async Task<(int ExitCode, string Stderr)> RunGitAsync(IEnumerable args, CancellationToken ct) { var psi = new ProcessStartInfo { FileName = "git", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, }; foreach (var a in args) psi.ArgumentList.Add(a); using var proc = new Process { StartInfo = psi }; proc.Start(); await using var ctr = ct.Register(() => { try { proc.Kill(entireProcessTree: true); } catch { /* already exited */ } }); var stdoutTask = proc.StandardOutput.ReadToEndAsync(); var stderrTask = proc.StandardError.ReadToEndAsync(); await proc.WaitForExitAsync(CancellationToken.None); await stdoutTask; var stderr = await stderrTask; ct.ThrowIfCancellationRequested(); return (proc.ExitCode, stderr.TrimEnd()); } }