fix(worker): honour targetBranch in MergeAsync by checking out before merge
Add GitService.CheckoutBranchAsync; compare targetBranch to current HEAD before MergeNoFfAsync and switch when they differ. Returns Blocked if the branch does not exist. Add three new tests (two service, one GitService). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -160,6 +160,13 @@ public sealed class GitService
|
|||||||
return stdout.Trim();
|
return stdout.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task CheckoutBranchAsync(string repoDir, string branchName, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["checkout", branchName], ct);
|
||||||
|
if (exitCode != 0)
|
||||||
|
throw new InvalidOperationException($"git checkout '{branchName}' failed (exit {exitCode}): {stderr}");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<string>> ListLocalBranchesAsync(string repoDir, CancellationToken ct = default)
|
public async Task<List<string>> ListLocalBranchesAsync(string repoDir, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
|
var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
|
||||||
|
|||||||
@@ -75,6 +75,13 @@ public sealed class TaskMergeService
|
|||||||
if (await _git.HasChangesAsync(list.WorkingDir, ct))
|
if (await _git.HasChangesAsync(list.WorkingDir, ct))
|
||||||
return Blocked("target working tree has uncommitted changes");
|
return Blocked("target working tree has uncommitted changes");
|
||||||
|
|
||||||
|
var currentBranch = await _git.GetCurrentBranchAsync(list.WorkingDir, ct);
|
||||||
|
if (!string.Equals(currentBranch, targetBranch, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
try { await _git.CheckoutBranchAsync(list.WorkingDir, targetBranch, ct); }
|
||||||
|
catch (Exception ex) { return Blocked($"failed to switch target branch: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
var (exitCode, stderr) = await _git.MergeNoFfAsync(list.WorkingDir, wt.BranchName, commitMessage, ct);
|
var (exitCode, stderr) = await _git.MergeNoFfAsync(list.WorkingDir, wt.BranchName, commitMessage, ct);
|
||||||
if (exitCode != 0)
|
if (exitCode != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -155,6 +155,20 @@ public class GitServiceMergeTests : IDisposable
|
|||||||
Assert.False(await git.IsMidMergeAsync(repo.RepoDir));
|
Assert.False(await git.IsMidMergeAsync(repo.RepoDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckoutBranchAsync_ExistingBranch_SwitchesBranch()
|
||||||
|
{
|
||||||
|
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||||
|
var repo = NewRepo();
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "branch", "feature/checkout-test");
|
||||||
|
|
||||||
|
var git = new GitService();
|
||||||
|
await git.CheckoutBranchAsync(repo.RepoDir, "feature/checkout-test");
|
||||||
|
|
||||||
|
var current = await git.GetCurrentBranchAsync(repo.RepoDir);
|
||||||
|
Assert.Equal("feature/checkout-test", current);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ListConflictedFilesAsync_MidConflict_ReturnsConflictedFile()
|
public async Task ListConflictedFilesAsync_MidConflict_ReturnsConflictedFile()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -293,6 +293,64 @@ public class TaskMergeServiceTests : IDisposable
|
|||||||
Assert.Equal("blocked", result.Status);
|
Assert.Equal("blocked", result.Status);
|
||||||
Assert.Contains("uncommitted", result.ErrorMessage ?? "");
|
Assert.Contains("uncommitted", result.ErrorMessage ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MergeAsync_TargetBranchDifferentFromHead_ChecksOutBeforeMerging()
|
||||||
|
{
|
||||||
|
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||||
|
|
||||||
|
var repo = NewRepo();
|
||||||
|
var db = NewDb();
|
||||||
|
var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);
|
||||||
|
|
||||||
|
// Create a feature branch in the repo (from current HEAD).
|
||||||
|
GitRepoFixture.RunGit(repo.RepoDir, "branch", "feature/target");
|
||||||
|
|
||||||
|
var wtMgr = BuildWorktreeManager(db);
|
||||||
|
var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
|
||||||
|
_wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
|
||||||
|
|
||||||
|
File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "feat.txt"), "data\n");
|
||||||
|
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
|
||||||
|
|
||||||
|
var (svc, _) = BuildService(db);
|
||||||
|
// Repo is currently on main/master; request merge into feature/target.
|
||||||
|
var result = await svc.MergeAsync(task.Id, "feature/target", removeWorktree: false,
|
||||||
|
commitMessage: "Merge into feature", ct: CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal("merged", result.Status);
|
||||||
|
|
||||||
|
// HEAD must now be feature/target.
|
||||||
|
var head = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
|
||||||
|
Assert.Equal("feature/target", head);
|
||||||
|
|
||||||
|
// The merged file must exist on feature/target.
|
||||||
|
Assert.True(File.Exists(Path.Combine(repo.RepoDir, "feat.txt")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MergeAsync_TargetBranchDoesNotExist_ReturnsBlocked()
|
||||||
|
{
|
||||||
|
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||||
|
|
||||||
|
var repo = NewRepo();
|
||||||
|
var db = NewDb();
|
||||||
|
var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);
|
||||||
|
|
||||||
|
var wtMgr = BuildWorktreeManager(db);
|
||||||
|
var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
|
||||||
|
_wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
|
||||||
|
|
||||||
|
File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "x.txt"), "x\n");
|
||||||
|
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
|
||||||
|
|
||||||
|
var (svc, _) = BuildService(db);
|
||||||
|
var result = await svc.MergeAsync(task.Id, "nonexistent/branch", removeWorktree: false,
|
||||||
|
commitMessage: "Merge", ct: CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal("blocked", result.Status);
|
||||||
|
Assert.Contains("switch target branch", result.ErrorMessage ?? "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Test doubles
|
#region Test doubles
|
||||||
|
|||||||
Reference in New Issue
Block a user