diff options
Diffstat (limited to 'src/marlin.core/functional/GitBinaryClient.cs')
| -rw-r--r-- | src/marlin.core/functional/GitBinaryClient.cs | 133 |
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; + } +} |
