fix(FN-5456): tighten branch-autocorrect verify and add real-git regression
Follow-up to a3ec2e55c addressing code-review feedback:
- Restrict the existence check to local heads via
`git show-ref --verify --quiet refs/heads/<name>` so a stray tag or
remote ref with the same name cannot satisfy the check and lead the
subsequent `git checkout` to a detached HEAD on the wrong object.
- Use `git branch -M` (force) instead of `-m` for the rename path so
case-only renames succeed on case-insensitive filesystems (macOS,
default Windows).
- Document the FN-5456 invariant on the function's doc comment.
- Add a real-git regression suite covering the three paths the fix
governs: no creation when expected ref is missing, switch-only when
it exists, and rejection of same-named tags.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,12 @@ async function runGit(command: string, worktreePath: string): Promise<{ ok: true
|
||||
/**
|
||||
* FN-4474: Recover wrong-branch invariant failures by renaming/checking out
|
||||
* the expected branch without throwing terminal executor errors.
|
||||
*
|
||||
* FN-5456 invariant: this function MUST NOT create a branch from arbitrary
|
||||
* HEAD. It may only rename an existing branch or switch to an already-existing
|
||||
* expected ref; if neither applies it returns `failed` so upstream recovery
|
||||
* (which knows the proper base SHA) can re-anchor via `prepareForTask` /
|
||||
* `reanchorBranchToBase`.
|
||||
*/
|
||||
export async function attemptBranchAutocorrect({
|
||||
worktreePath,
|
||||
@@ -76,7 +82,9 @@ export async function attemptBranchAutocorrect({
|
||||
}
|
||||
|
||||
if (isFreshBranch) {
|
||||
const rename = await runGit(`git branch -m ${observedArg} ${expectedArg}`, worktreePath);
|
||||
// `-M` (force) lets us recover from case-only renames on case-insensitive
|
||||
// filesystems (macOS, default Windows) where `-m` rejects `foo` → `Foo`.
|
||||
const rename = await runGit(`git branch -M ${observedArg} ${expectedArg}`, worktreePath);
|
||||
if (rename.ok) {
|
||||
return { status: "renamed" };
|
||||
}
|
||||
@@ -85,10 +93,13 @@ export async function attemptBranchAutocorrect({
|
||||
// FN-5456: must NOT use `git checkout -B` with no start point — that would
|
||||
// create (or reset) the expected branch at whatever HEAD currently is,
|
||||
// capturing the previous occupant's tip (the "branch: Created from HEAD"
|
||||
// contamination pattern). Plain `git checkout` only switches to an existing
|
||||
// ref; if `expected` doesn't already exist the caller will surface a
|
||||
// wrong-branch failure with proper base resolution upstream.
|
||||
const verifyExpected = await runGit(`git rev-parse --verify --quiet ${expectedArg}`, worktreePath);
|
||||
// contamination pattern). Restrict the verify to refs/heads/ so a stray tag
|
||||
// or remote ref with the same name cannot satisfy the check and lead the
|
||||
// subsequent `git checkout` to a detached HEAD on the wrong object.
|
||||
const verifyExpected = await runGit(
|
||||
`git show-ref --verify --quiet refs/heads/${expectedArg}`,
|
||||
worktreePath,
|
||||
);
|
||||
if (!verifyExpected.ok) {
|
||||
return { status: "failed", reason: `expected branch ${expected} does not exist` };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user