Files
ClaudeDo/src/ClaudeDo.Worker/Skills/GitRepoCloner.cs
T

62 lines
2.0 KiB
C#

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<ClonedRepo> 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<string> 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());
}
}