merge checkout strategy: mergeAgain fails with `git reset --hard refs/remotes/origin/<base>` after a PR is retargeted to a new base branch
Community Note
- Please vote on this issue by adding a reaction to the original issue to help the community and maintainers prioritize this request. Searching for pre-existing feature requests helps us consolidate datapoints for identical requirements into a single place, thank you!
- Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.
- If you are interested in working on this issue or have submitted a pull request, please leave a comment.
Overview of the Issue
With --checkout-strategy=merge and a persistent data volume, a PR whose base branch changes while its head commit stays the same wedges its working directory permanently. Every subsequent plan fails with:
running git reset --hard refs/remotes/origin/main: fatal: ambiguous argument 'refs/remotes/origin/main': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
: exit status 128This is the common "stacked PR" flow. feature/branch-b targets feature/branch-a; feature/branch-a merges and is deleted; the forge retargets the open PR onto main. (On GitLab this is automatic — it posts "deleted the feature/branch-a branch. This merge request now targets the main branch". GitHub does the same retarget when a base branch is deleted, and a user can retarget manually on either forge.)
Because the head commit never changed, the working dir is never re-cloned, and it only ever had a remote-tracking ref for the old base branch.
Why the existing guard does not catch it
#6146 / #6177 added remoteHasBranch so a changed base branch forces a re-clone. That guard lives in attemptReuseCloneDir and is unreachable in this scenario. Line numbers below are server/events/working_dir.go at v0.47.1.
Clone()fast path (L138–146) runsisBranchAtTargetRef(p.HeadCommit). Under the merge strategy this comparesHEAD^2, which is still the unchanged PR head, so it returns early:repo is at correct commit "<sha>" so will not re-clone (fast path)attemptReuseCloneDir— and therefore theremoteHasBranchguard at L185 — is never reached. Even in the slow path the guard only runs whenisUpToDate == false.MergeAgain()(L206) then callsrecheckDiverged()(L265), whosegit remote updatefails because the old base branch was deleted from the remote:getting remote update failed: Fetching origin fatal: couldn't find remote ref refs/heads/feature/branch-a error: could not fetch originPer the documented behaviour it returns
true("we prefer to assume divergence for safety").mergeAgain()(L814) unconditionally runs:w.wrappedGit(logger, c, "reset", "--hard", fmt.Sprintf("refs/remotes/origin/%s", c.pr.BaseBranch))refs/remotes/origin/maindoes not exist, becauseforceClonehad cloned with--branch feature/branch-a --single-branch, leaving exactly one remote-tracking ref. It fails, and there is noremoteHasBranchcheck on this path and no fallback toforceClone.
The state is not self-healing: every retry takes the same fast path and fails identically. The only recovery is deleting the working directory by hand.
Confirmed on the affected checkout — note the single, stale refspec:
$ git -C <data-dir>/repos/<org>/<repo>/123/default config --get-all remote.origin.fetch
+refs/heads/feature/branch-a:refs/remotes/origin/feature/branch-a
$ git -C <data-dir>/repos/<org>/<repo>/123/default reflog --date=iso
<sha1> HEAD@{...}: merge <pr-head-sha>: Merge made by the 'ort' strategy.
<sha2> HEAD@{...}: reset: moving to origin/feature/branch-a
...
<sha3> HEAD@{...}: clone: from https://<host>/<org>/<repo>.gitReproduction Steps
- Run Atlantis with
--checkout-strategy=mergeand a persistent data volume. - Open PR A (
feature/branch-a→main). - Open PR B (
feature/branch-b→feature/branch-a), stacked on A. - Let Atlantis plan PR B, so its working dir is cloned with
--branch feature/branch-a --single-branch. - Merge PR A and delete
feature/branch-a. The forge retargets PR B ontomain. - Comment
atlantis planon PR B without pushing any new commit.
PR B now fails with the git reset --hard refs/remotes/origin/main error above, and keeps failing on every retry.
Logs
Logs{"level":"info","caller":"events/working_dir.go:143","msg":"repo is at correct commit \"<pr-head-sha>\" so will not re-clone (fast path)","json":{"repo":"<org>/<repo>","pull":"123"}}
{"level":"warn","caller":"events/working_dir.go:311","msg":"getting remote update failed: Fetching origin\nfatal: couldn't find remote ref refs/heads/feature/branch-a\nerror: could not fetch origin\nFetching source\nFrom https://<host>/<org>/<repo>\n <old>..<new> main -> source/main\n","json":{"repo":"<org>/<repo>","pull":"123"}}
{"level":"info","caller":"events/working_dir.go:251","msg":"base branch may have been updated, using merge strategy and will merge again","json":{"repo":"<org>/<repo>","pull":"123"}}
{"level":"error","caller":"events/project_command_runner.go","msg":"Error running plan operation: running git reset --hard refs/remotes/origin/main: fatal: ambiguous argument 'refs/remotes/origin/main': unknown revision or path not in the working tree.\nUse '--' to separate paths from revisions, like this:\n'git <command> [<revision>...] -- [<file>...]'\n: exit status 128","json":{"repo":"<org>/<repo>","pull":"123"}}Environment details
- Atlantis version:
v0.47.1 - Deployment method: Helm chart
6.9.2on Kubernetes, with a persistent volume for--data-dir - If not running the latest Atlantis version have you tried to reproduce this issue on the latest version: the relevant code in
server/events/working_dir.gois identical onmainat the time of writing, so it should still reproduce - Atlantis flags:
--checkout-strategy=merge(viaATLANTIS_CHECKOUT_STRATEGY), GitLab VCS
Additional Context
Suggested fix — make the base-branch check reachable on the paths that actually run for an unchanged head commit. Either or both of:
- In
mergeAgain()(or inMergeAgain()before it), checkremoteHasBranch(c.pr.BaseBranch)and fall back toforceClonewhen the ref is missing, instead of resetting to a ref that cannot exist. - In the
Clone()fast path (L138–146), also requireremoteHasBranch(c.pr.BaseBranch)whenw.CheckoutMergeis set, so a retargeted PR does not short-circuit past the existing guard.
The first is the more complete fix, since MergeAgain() is reached even when Clone() legitimately reuses the directory.
Related: #6146, #6177 (added remoteHasBranch to the Clone path).
I am happy to open a PR for this if maintainers agree with the approach.
Source: runatlantis/atlantis