summaryrefslogtreecommitdiff
path: root/src/marlin.core/functional/GitBinaryClient.cs
diff options
context:
space:
mode:
authorMelodi <62381002+Melodi17@users.noreply.github.com>2026-09-06 09:02:31 +1000
committerMelodi <62381002+Melodi17@users.noreply.github.com>2026-09-06 09:02:31 +1000
commita9bc03fb01b5d1ac41bf5592f384d22df5425aa4 (patch)
tree16ae13e19d2a3cd7488202c30790d09557559d70 /src/marlin.core/functional/GitBinaryClient.cs
parentc00992ffad24212c4398736efd6d707d4cb0cc0d (diff)
feat: vcs and secret management support
Diffstat (limited to 'src/marlin.core/functional/GitBinaryClient.cs')
-rw-r--r--src/marlin.core/functional/GitBinaryClient.cs133
1 files changed, 133 insertions, 0 deletions
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<Dictionary<string, string>> 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<string> refs, bool skipBlobs = false)
+ {
+ var refList = refs as IReadOnlyCollection<string> ?? refs.ToArray();
+ if (refList.Count == 0)
+ return;
+
+ var args = new List<string> { "-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<string[]> 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<byte[]> CatFile(string repoPath, string hash, string fileName)
+ {
+ return this.RunRawAsync(
+ authenticate: false,
+ "-C", repoPath, "show", $"{hash}:{fileName}");
+ }
+
+ private Task<string> RunAsync(bool authenticate, params string[] arguments) =>
+ this.RunAsync(authenticate, async stream =>
+ {
+ using var reader = new StreamReader(stream);
+ return await reader.ReadToEndAsync();
+ }, arguments);
+
+ private Task<byte[]> 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<T> RunAsync<T>(bool authenticate, Func<Stream, Task<T>> 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<T> stdoutTask = readStdout(process.StandardOutput.BaseStream);
+ Task<string> 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<string, string> ParseRefs(string stdout)
+ {
+ var result = new Dictionary<string, string>();
+
+ 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;
+ }
+}