#6872·atlantis

merge checkout strategy: mergeAgain fails with `git reset --hard refs/remotes/origin/<base>` after a PR is retargeted to a new base branch

Author: jjoosCreated Sep 10, 2026Updated Sep 10, 2026

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 128

This 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.

  1. Clone() fast path (L138–146) runs isBranchAtTargetRef(p.HeadCommit). Under the merge strategy this compares HEAD^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 the remoteHasBranch guard at L185 — is never reached. Even in the slow path the guard only runs when isUpToDate == false.

  2. MergeAgain() (L206) then calls recheckDiverged() (L265), whose git remote update fails 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 origin

    Per the documented behaviour it returns true ("we prefer to assume divergence for safety").

  3. mergeAgain() (L814) unconditionally runs:

    go
    w.wrappedGit(logger, c, "reset", "--hard", fmt.Sprintf("refs/remotes/origin/%s", c.pr.BaseBranch))

    refs/remotes/origin/main does not exist, because forceClone had cloned with --branch feature/branch-a --single-branch, leaving exactly one remote-tracking ref. It fails, and there is no remoteHasBranch check on this path and no fallback to forceClone.

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>.git

Reproduction Steps

  1. Run Atlantis with --checkout-strategy=merge and a persistent data volume.
  2. Open PR A (feature/branch-amain).
  3. Open PR B (feature/branch-bfeature/branch-a), stacked on A.
  4. Let Atlantis plan PR B, so its working dir is cloned with --branch feature/branch-a --single-branch.
  5. Merge PR A and delete feature/branch-a. The forge retargets PR B onto main.
  6. Comment atlantis plan on 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
json
{"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.2 on 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.go is identical on main at the time of writing, so it should still reproduce
  • Atlantis flags: --checkout-strategy=merge (via ATLANTIS_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 in MergeAgain() before it), check remoteHasBranch(c.pr.BaseBranch) and fall back to forceClone when the ref is missing, instead of resetting to a ref that cannot exist.
  • In the Clone() fast path (L138–146), also require remoteHasBranch(c.pr.BaseBranch) when w.CheckoutMerge is 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.