using System.Diagnostics; namespace ClaudeDo.Worker.Tests.Infrastructure; public sealed class GitRepoFixture : IDisposable { public string RepoDir { get; } public string BaseCommit { get; } public GitRepoFixture() { RepoDir = Path.Combine(Path.GetTempPath(), $"claudedo_gittest_{Guid.NewGuid():N}"); Directory.CreateDirectory(RepoDir); RunGit(RepoDir, "init"); RunGit(RepoDir, "config", "user.name", "test"); RunGit(RepoDir, "config", "user.email", "test@example.com"); File.WriteAllText(Path.Combine(RepoDir, "README.md"), "# test repo"); RunGit(RepoDir, "add", "-A"); RunGit(RepoDir, "commit", "-m", "initial commit"); BaseCommit = RunGit(RepoDir, "rev-parse", "HEAD").Trim(); } public void Dispose() { try { // Force-remove read-only .git objects on Windows. ForceDeleteDirectory(RepoDir); } catch { /* best effort */ } } public static bool IsGitAvailable() { try { var psi = new ProcessStartInfo("git", "--version") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; using var p = Process.Start(psi)!; p.WaitForExit(5000); return p.ExitCode == 0; } catch { return false; } } internal static string RunGit(string workDir, params string[] args) { var psi = new ProcessStartInfo("git") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; psi.ArgumentList.Add("-C"); psi.ArgumentList.Add(workDir); foreach (var a in args) psi.ArgumentList.Add(a); using var proc = Process.Start(psi)!; var stdout = proc.StandardOutput.ReadToEnd(); var stderr = proc.StandardError.ReadToEnd(); proc.WaitForExit(); if (proc.ExitCode != 0) throw new InvalidOperationException($"git {string.Join(' ', args)} failed: {stderr}"); return stdout; } private static void ForceDeleteDirectory(string path) { if (!Directory.Exists(path)) return; foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) { File.SetAttributes(file, FileAttributes.Normal); } Directory.Delete(path, true); } }