fix(git): hard-sync target branch to origin instead of bare pull

_sync_target_branch ran a bare `git pull` after checkout; on a workspace
clone whose local target branch diverged from origin (inevitable once
remote history is rewritten) modern git fatals with "Need to specify how
to reconcile divergent branches". On the CEO approve-and-merge path the
GitHub merge had already landed, so the failure surfaced as a spurious
400 "Merge failed" and every retry re-hit the same wedged clone.

Sync is now fetch + `reset --hard origin/<branch>` — remote-authoritative,
matching the helper's intent and self-healing the divergence for all
call sites (CEO merge, PM 409-retry sync, post-merge best-effort sync).
This commit is contained in:
Renn F
2026-07-19 00:06:33 +02:00
parent ec74298faa
commit 945ce006fd
2 changed files with 20 additions and 12 deletions
+13 -4
View File
@@ -3405,13 +3405,19 @@ class GitService(BaseService):
async def _sync_target_branch(
self, workspace: Path, target_branch: str, git_token: str
) -> str:
"""Checkout + pull the target branch, return the tip commit hash.
"""Checkout + hard-sync the target branch to origin, return its tip.
If the target branch has no local ref (common in agent workspaces that
only ever checked out their own task branch), fetch it from origin and
create a tracking branch before pulling. This prevents the "parent
branch doesn't exist locally" SERVICE_ERROR that blocks every leaf→cell
create a tracking branch first. This prevents the "parent branch
doesn't exist locally" SERVICE_ERROR that blocks every leaf→cell
merge in a shared workspace.
The sync is fetch + ``reset --hard origin/<branch>``, never ``pull``:
a bare pull fatals on a divergent local ref ("Need to specify how to
reconcile divergent branches"), and a local target branch that has
drifted from origin in a workspace clone is cruft by definition — the
remote side of the merge is authoritative.
"""
checkout = await self._run_git(
workspace, ["checkout", target_branch], check=False
@@ -3439,7 +3445,10 @@ class GitService(BaseService):
"tracking_stderr": tracking.stderr.strip(),
},
)
await self._run_git(workspace, ["pull"], token=git_token)
await self._run_git(
workspace, ["fetch", "origin", target_branch], token=git_token
)
await self._run_git(workspace, ["reset", "--hard", f"origin/{target_branch}"])
log_result = await self._run_git(workspace, ["log", "-1", "--format=%H"])
return log_result.stdout.strip()
+7 -8
View File
@@ -918,7 +918,7 @@ async def test_push_propagates_non_gh001_error_unchanged() -> None:
@pytest.mark.asyncio
async def test_sync_target_branch_uses_local_ref_when_present() -> None:
"""If the target branch already exists locally, just checkout + pull."""
"""If the target branch exists locally, checkout + hard-sync to origin."""
svc = _service()
calls: list[list[str]] = []
@@ -926,8 +926,6 @@ async def test_sync_target_branch_uses_local_ref_when_present() -> None:
calls.append(args)
if args[:2] == ["checkout", "feature/backend/parent"]:
return MagicMock(returncode=0, stdout="", stderr="")
if args[:1] == ["pull"]:
return MagicMock(returncode=0, stdout="", stderr="")
if args[:2] == ["log", "-1"]:
return MagicMock(returncode=0, stdout="local-tip-sha", stderr="")
return MagicMock(returncode=0, stdout="", stderr="")
@@ -938,8 +936,10 @@ async def test_sync_target_branch_uses_local_ref_when_present() -> None:
)
assert sha == "local-tip-sha"
assert ["checkout", "feature/backend/parent"] in calls
assert ["pull"] in calls
assert ["fetch", "origin", "feature/backend/parent"] not in calls
assert ["fetch", "origin", "feature/backend/parent"] in calls
assert ["reset", "--hard", "origin/feature/backend/parent"] in calls
assert ["pull"] not in calls
assert ["checkout", "-b", "feature/backend/parent"] not in [c[:3] for c in calls]
@pytest.mark.asyncio
@@ -956,8 +956,6 @@ async def test_sync_target_branch_fetches_and_tracks_when_local_ref_missing() ->
return MagicMock(returncode=0, stdout="", stderr="")
if args[:3] == ["checkout", "-b", "feature/backend/parent"]:
return MagicMock(returncode=0, stdout="", stderr="")
if args[:1] == ["pull"]:
return MagicMock(returncode=0, stdout="", stderr="")
if args[:2] == ["log", "-1"]:
return MagicMock(returncode=0, stdout="origin-tip-sha", stderr="")
return MagicMock(returncode=0, stdout="", stderr="")
@@ -975,7 +973,8 @@ async def test_sync_target_branch_fetches_and_tracks_when_local_ref_missing() ->
"feature/backend/parent",
"origin/feature/backend/parent",
] in calls
assert ["pull"] in calls
assert ["reset", "--hard", "origin/feature/backend/parent"] in calls
assert ["pull"] not in calls
@pytest.mark.asyncio