From a9bc03fb01b5d1ac41bf5592f384d22df5425aa4 Mon Sep 17 00:00:00 2001 From: Melodi <62381002+Melodi17@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:02:31 +1000 Subject: feat: vcs and secret management support --- src/marlin.core/functional/GitBinaryClient.cs | 133 ++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/marlin.core/functional/GitBinaryClient.cs (limited to 'src/marlin.core/functional/GitBinaryClient.cs') diff --git a/src/marlin.core/functional/GitBinaryClient.cs b/src/marlin.core/functional/GitBinaryClient.cs new file mode 100644 index 0000000..001f2c8 --- /dev/null +++ b/src/marlin.core/functional/GitBinaryClient.cs @@ -0,0 +1,133 @@ +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; + } +} -- cgit v1.3.1