namespace marlin.core.functional; using System.Diagnostics; public class GitBinaryClient : IGitClient { private readonly IGitCredential? _credential; public GitBinaryClient(IGitCredential? credential = null) { this._credential = credential; } public async Task> GetRemoteRefs(string remote) { string stdout = await this.RunAsync( authenticate: true, "ls-remote", "--heads", "--tags", remote); return ParseRefs(stdout); } public async Task Init(string repoPath) { Directory.CreateDirectory(repoPath); await this.RunAsync(authenticate: false, "init", "--bare", repoPath); } public async Task FetchTrees(string repoPath, string remote, IEnumerable refs, bool skipBlobs = false) { var refList = refs as IReadOnlyCollection ?? refs.ToArray(); if (refList.Count == 0) return; var args = new List { "-C", repoPath, "fetch" }; if (skipBlobs) args.Add("--filter=blob:none"); args.Add(remote); args.AddRange(refList); await this.RunAsync(authenticate: true, args.ToArray()); } public async Task ListTree(string repoPath, string hash, string filterPath = ".") { string stdout = await this.RunAsync( authenticate: false, "-C", repoPath, "ls-tree", "-r", "--name-only", hash, "--", filterPath); return stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries); } public Task CatFile(string repoPath, string hash, string fileName) { return this.RunRawAsync( authenticate: false, "-C", repoPath, "show", $"{hash}:{fileName}"); } private Task RunAsync(bool authenticate, params string[] arguments) => this.RunAsync(authenticate, async stream => { using var reader = new StreamReader(stream); return await reader.ReadToEndAsync(); }, arguments); private Task RunRawAsync(bool authenticate, params string[] arguments) => this.RunAsync(authenticate, async stream => { using var buffer = new MemoryStream(); await stream.CopyToAsync(buffer); return buffer.ToArray(); }, arguments); private async Task RunAsync(bool authenticate, Func> readStdout, string[] arguments) { var psi = new ProcessStartInfo("git") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; foreach (string arg in arguments) psi.ArgumentList.Add(arg); psi.Environment["GIT_TERMINAL_PROMPT"] = "0"; IAsyncDisposable? credentialScope = authenticate && this._credential != null ? await this._credential.ApplyAsync(psi) : null; try { using var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start git process."); Task stdoutTask = readStdout(process.StandardOutput.BaseStream); Task stderrTask = process.StandardError.ReadToEndAsync(); await process.WaitForExitAsync(); T stdout = await stdoutTask; string stderr = await stderrTask; if (process.ExitCode != 0) throw new GitCommandException(string.Join(' ', arguments), process.ExitCode, stderr); return stdout; } finally { if (credentialScope != null) await credentialScope.DisposeAsync(); } } private static Dictionary ParseRefs(string stdout) { var result = new Dictionary(); foreach (string line in stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { string[] parts = line.Split('\t'); if (parts.Length != 2) continue; // ignore a malformed line rather than throw over one bad entry result[parts[1]] = parts[0]; } return result; } }