fix(#3207): create-and-switch on the first phase/milestone commit (#3363)

* test(#3207): invert fresh-create branching tests to expect create+switch

The four fresh-create branching tests (#3079) locked in create-without-
switch behavior that #3207 identifies as the regression: a fresh phase/
milestone branch is created but HEAD never moves onto it, so the first
phase/milestone-scoped commit lands on the base branch. Invert those
assertions to expect create+switch, and add two new tests:
- fresh-create is non-silent (logs the create+switch) (#3207 AC3)
- a second phase commit does not re-warn once HEAD is on the phase branch (#3207 AC5)

The existing-branch path (#2539) is deliberately byte-for-byte unchanged.

RED — fails on next; the fix follows in a separate fix: commit.

* fix(#3207): create-and-switch on the first phase/milestone commit

The #3079 fix (PR #3141) replaced git checkout -b with git branch
(create-only) unconditionally, including the case where the strategy
branch does not yet exist. That regressed #1278: the first phase- or
milestone-scoped commit no longer landed on the strategy branch — it
stayed on the base branch, and the strategy branch was left as an empty
marker pointing at the pre-phase tip. Every subsequent commit then
warned 'already exists; committing on the current branch instead of
switching', wording that misleads because the tool itself created the
branch moments before.

Re-separate the two cases #3079 collapsed:
- branch does NOT exist -> create AND switch (git checkout -b). The
  #3079 resurrection hazard cannot apply: the branch was just verified
  absent, so there is no merged-and-deleted ref to resurrect and no
  silent move onto an existing unrelated branch (#2539 AC2 is honored
  by the existing-branch arm, which is unchanged). The create is logged
  so the first phase-scoped commit is not silent (#3207 AC3).
- branch ALREADY exists -> unchanged: no switch, commit on current
  branch, non-silent warning (#2539/#3079).

Once the first commit switches HEAD onto the strategy branch, the
currentBranch !== branchName guard skips the block on subsequent
commits, so the misleading 'already exists' warning no longer recurs.

* chore(#3207): add changeset fragment

* chore(#3207): backfill changeset PR number (#3363)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-11 18:08:39 -04:00
committed by GitHub
parent 0396d9cab1
commit e7993d77bf
3 changed files with 145 additions and 33 deletions

View File

@@ -1052,22 +1052,29 @@ function cmdCommit(cwd: string, message: string | undefined, files: string[] | u
if (branchName) {
const currentBranch = execGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });
if (currentBranch.exitCode === 0 && currentBranch.stdout.trim() !== branchName) {
// #2539/#3079: the #1278 intent is to CREATE the phase/milestone branch
// before the FIRST commit on it — not to force-switch an already-
// checked-out working branch onto a DIFFERENT existing branch. The
// prior `git checkout -b` both created AND switched (silently moving
// HEAD), which resurrected merged-and-deleted phase branches (#3079).
// Now: create-if-absent WITHOUT switching, using `git branch` instead
// of `git checkout -b`. The commit always lands on the current branch.
// If the resolved branch already exists, log the resolution so the
// operator sees that the phase branch was resolved and deliberately
// not switched to (#2539 AC2: an auto-checkout mid-commit must never
// happen silently).
// #2539/#3079/#3207: two cases the prior (#3079) code collapsed into one.
// #1278 intent: CREATE the phase/milestone branch before the FIRST commit
// on it so the phase's work accumulates there. #3079/#2539 hazard: never
// silently switch an already-checked-out working branch onto a DIFFERENT
// EXISTING branch — that resurrects merged-and-deleted phase branches and
// silently moves HEAD onto a stale ref (#2539 AC2: an auto-checkout
// mid-commit must never happen silently).
// Reconciliation (#3207): a brand-new branch has no resurrection target,
// so create-and-switch is safe here and is exactly the #1278 intent; an
// EXISTING branch is never switched to (the else arm logs + commits in
// place). The fresh create is logged so the first phase-scoped commit is
// not silent about where the work is landing (#3207 AC3).
const verify = execGit(['rev-parse', '--verify', `refs/heads/${branchName}`], { cwd });
if (verify.exitCode !== 0) {
// Branch does not exist — create it WITHOUT switching.
const create = execGit(['branch', branchName], { cwd });
if (create.exitCode !== 0) {
// Branch does not exist — CREATE AND SWITCH (the #1278 first-commit
// case). checkout -b cannot resurrect anything: the branch was just
// verified absent, so it is created fresh at HEAD.
const create = execGit(['checkout', '-b', branchName], { cwd });
if (create.exitCode === 0) {
process.stderr.write(
`${branchingStrategy} branch "${branchName}" created; switched to it for this commit.\n`
);
} else {
process.stderr.write(
`Warning: could not create ${branchingStrategy} branch "${branchName}" ` +
`(${create.stderr.trim()}); committing on the current branch "${currentBranch.stdout.trim()}".\n`