diff --git a/src/ClaudeDo.Data/Git/GitService.cs b/src/ClaudeDo.Data/Git/GitService.cs
index 636fc3a..1b43c45 100644
--- a/src/ClaudeDo.Data/Git/GitService.cs
+++ b/src/ClaudeDo.Data/Git/GitService.cs
@@ -238,6 +238,24 @@ public sealed class GitService
.ToList();
}
+ ///
+ /// Reads a conflicted file's blob at a merge stage: 1=base, 2=ours, 3=theirs.
+ /// Returns null when the stage doesn't exist (e.g. add/add conflict has no base).
+ /// Output is NOT trimmed so file content round-trips exactly.
+ ///
+ public async Task ShowStageAsync(string repoDir, int stage, string path, CancellationToken ct = default)
+ {
+ var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["show", $":{stage}:{path}"], ct, trimOutput: false);
+ return exitCode == 0 ? stdout : null;
+ }
+
+ public async Task AddPathAsync(string repoDir, string path, CancellationToken ct = default)
+ {
+ var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["add", "--", path], ct);
+ if (exitCode != 0)
+ throw new InvalidOperationException($"git add '{path}' failed (exit {exitCode}): {stderr}");
+ }
+
///
/// Non-destructive mergeability probe via `git merge-tree --write-tree`. Writes only
/// loose objects — the working tree, index, and refs are left untouched.
@@ -289,7 +307,7 @@ public sealed class GitService
}
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunGitAsync(
- string workDir, IEnumerable args, CancellationToken ct, string? stdinData = null)
+ string workDir, IEnumerable args, CancellationToken ct, string? stdinData = null, bool trimOutput = true)
{
var psi = new ProcessStartInfo
{
@@ -338,6 +356,6 @@ public sealed class GitService
ct.ThrowIfCancellationRequested();
- return (proc.ExitCode, stdout.TrimEnd(), stderr.TrimEnd());
+ return (proc.ExitCode, trimOutput ? stdout.TrimEnd() : stdout, stderr.TrimEnd());
}
}