feat(git): add non-destructive merge-tree conflict probe

This commit is contained in:
mika kuns
2026-06-04 23:18:54 +02:00
parent 82390047d2
commit 4098f7f341
2 changed files with 109 additions and 0 deletions

View File

@@ -3,6 +3,8 @@ using System.Text;
namespace ClaudeDo.Data.Git;
public sealed record MergePreview(bool Supported, bool Clean, IReadOnlyList<string> ConflictFiles);
public sealed class GitService
{
public async Task<bool> IsGitRepoAsync(string dir, CancellationToken ct = default)
@@ -236,6 +238,49 @@ public sealed class GitService
.ToList();
}
/// <summary>
/// Non-destructive mergeability probe via `git merge-tree --write-tree`. Writes only
/// loose objects — the working tree, index, and refs are left untouched.
/// </summary>
public async Task<MergePreview> PreviewMergeAsync(
string repoDir, string targetBranch, string sourceBranch, CancellationToken ct = default)
{
var (exitCode, stdout, _) = await RunGitAsync(repoDir,
["merge-tree", "--write-tree", "--name-only", targetBranch, sourceBranch], ct);
if (exitCode == 0)
return new MergePreview(true, true, Array.Empty<string>());
if (exitCode == 1)
{
// stdout: <tree-oid>\n<file>\n...\n\n<informational messages>
var lines = stdout.Split('\n');
var files = new List<string>();
for (int i = 1; i < lines.Length; i++)
{
var line = lines[i].TrimEnd('\r');
if (string.IsNullOrWhiteSpace(line)) break;
files.Add(line.Trim());
}
return new MergePreview(true, false, files);
}
// Any other exit (e.g. git too old: "unknown option --write-tree").
return new MergePreview(false, false, Array.Empty<string>());
}
/// <summary>Count of files that differ on <paramref name="sourceBranch"/> since its merge base with the target.</summary>
public async Task<int> CountChangedFilesAsync(
string repoDir, string targetBranch, string sourceBranch, CancellationToken ct = default)
{
var (exitCode, stdout, _) = await RunGitAsync(repoDir,
["diff", "--name-only", $"{targetBranch}...{sourceBranch}"], ct);
if (exitCode != 0) return 0;
return stdout
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Count(s => s.Length > 0);
}
public async Task MergeFfOnlyAsync(string repoDir, string branchName, CancellationToken ct = default)
{
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["merge", "--ff-only", branchName], ct);