@biffo/cli 0.309.2 → 0.309.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -170,10 +170,18 @@ because `--auto` waits for ever on a check that cannot re-evaluate itself (a
170
170
 
171
171
  ## 6. Push honestly, and verify the remote has your commit
172
172
 
173
- - **Push with the exit status visible:** `git push origin HEAD; echo $?`. A pipe
174
- (`git push | tail`) reports the pipe's status, not git's — a rejected push then
175
- reads as success and the commit is silently lost. Never trust a "pushed"
176
- message printed unconditionally after a pipe.
173
+ - **Push with the exit status visible, and record the upstream:**
174
+ `git push -u origin HEAD; echo $?`. A pipe (`git push | tail`) reports the
175
+ pipe's status, not git's — a rejected push then reads as success and the
176
+ commit is silently lost. Never trust a "pushed" message printed
177
+ unconditionally after a pipe.
178
+ - **The `-u` is not optional (biffo-template#1954).** `git worktree add -b
179
+ <branch> <path> origin/dev` sets the new branch's upstream to `origin/dev`
180
+ by default — a plain `git push origin HEAD`, with no `-u`, never corrects
181
+ it. A branch left tracking `origin/dev` can be pushed, merged, and have its
182
+ own remote copy deleted, while `git branch -vv` never reports it `[gone]`
183
+ (the ref it is actually tracking never goes anywhere) — so `doctor --fix`
184
+ and every other tool reading that marker stays permanently blind to it.
177
185
  - **Confirm the remote actually has the commit before relying on it,** especially
178
186
  before merge: `git log origin/<branch> -1`. A green PR page is not proof your
179
187
  latest local commit reached it.
@@ -170,10 +170,18 @@ because `--auto` waits for ever on a check that cannot re-evaluate itself (a
170
170
 
171
171
  ## 6. Push honestly, and verify the remote has your commit
172
172
 
173
- - **Push with the exit status visible:** `git push origin HEAD; echo $?`. A pipe
174
- (`git push | tail`) reports the pipe's status, not git's — a rejected push then
175
- reads as success and the commit is silently lost. Never trust a "pushed"
176
- message printed unconditionally after a pipe.
173
+ - **Push with the exit status visible, and record the upstream:**
174
+ `git push -u origin HEAD; echo $?`. A pipe (`git push | tail`) reports the
175
+ pipe's status, not git's — a rejected push then reads as success and the
176
+ commit is silently lost. Never trust a "pushed" message printed
177
+ unconditionally after a pipe.
178
+ - **The `-u` is not optional (biffo-template#1954).** `git worktree add -b
179
+ <branch> <path> origin/dev` sets the new branch's upstream to `origin/dev`
180
+ by default — a plain `git push origin HEAD`, with no `-u`, never corrects
181
+ it. A branch left tracking `origin/dev` can be pushed, merged, and have its
182
+ own remote copy deleted, while `git branch -vv` never reports it `[gone]`
183
+ (the ref it is actually tracking never goes anywhere) — so `doctor --fix`
184
+ and every other tool reading that marker stays permanently blind to it.
177
185
  - **Confirm the remote actually has the commit before relying on it,** especially
178
186
  before merge: `git log origin/<branch> -1`. A green PR page is not proof your
179
187
  latest local commit reached it.
package/dist/index.js CHANGED
@@ -1449,6 +1449,39 @@ var GitAdapter = class {
1449
1449
  const { stdout, exitCode } = await execa("git", ["rev-parse", "HEAD"], { cwd, reject: false });
1450
1450
  return exitCode === 0 ? stdout.trim() : null;
1451
1451
  }
1452
+ /**
1453
+ * Every local branch name that currently has a same-named remote-tracking
1454
+ * ref under `refs/remotes/origin/` (biffo-template#1954) — i.e. `git
1455
+ * for-each-ref refs/remotes/origin`, stripped of the `refs/remotes/origin/`
1456
+ * prefix, with the `HEAD` symref excluded (it names no branch).
1457
+ *
1458
+ * This is the fact `doctor --fix`'s reap-candidate detection needs instead
1459
+ * of trusting a branch's own *configured* upstream (`%(upstream:track)`,
1460
+ * i.e. `git branch -vv`'s `[gone]` marker): `git worktree add -b <branch>
1461
+ * <path> origin/dev` sets the new branch's upstream to `origin/dev` by
1462
+ * default, and a plain `git push origin HEAD` (no `-u`) never corrects
1463
+ * it — so a branch can be pushed, merged, and have its OWN remote copy
1464
+ * deleted, while its `[gone]` marker never fires because the thing it is
1465
+ * actually tracking (`origin/dev`) never goes anywhere. Checking for a
1466
+ * same-named ref sidesteps whatever the branch happens to be configured to
1467
+ * track entirely.
1468
+ *
1469
+ * Absence from this set does NOT by itself prove a branch's remote copy
1470
+ * was deleted — a branch never pushed under its own name is equally
1471
+ * absent. See `doctor-reaper.ts`'s `hasProvenGoneRemote` for why that
1472
+ * distinction is safe to skip here (unlike in `upgrade-branch-reaper.ts`,
1473
+ * which has no downstream verification and must not skip it).
1474
+ */
1475
+ async listRemoteBranchNames(cwd) {
1476
+ const { stdout } = await execa(
1477
+ "git",
1478
+ ["for-each-ref", "--format=%(refname:lstrip=3)", "refs/remotes/origin"],
1479
+ { cwd }
1480
+ );
1481
+ return new Set(
1482
+ stdout.split("\n").map((line) => line.trim()).filter((name) => name !== "" && name !== "HEAD")
1483
+ );
1484
+ }
1452
1485
  /**
1453
1486
  * `branch`'s own tip SHA, without checking it out — the bare-branch
1454
1487
  * counterpart to `headSha` (#1682 milestone 2). A branch with no linked
@@ -16300,8 +16333,13 @@ function classifyReapCandidate(facts) {
16300
16333
  return { action: "keep", reason: "unknown-pr-verdict" };
16301
16334
  }
16302
16335
  }
16303
- function findReapCandidates(branches, worktrees) {
16304
- const goneBranches = new Set(branches.filter((b) => b.track.includes("gone")).map((b) => b.name));
16336
+ function hasProvenGoneRemote(branch, remoteBranchNames) {
16337
+ return !remoteBranchNames.has(branch.name);
16338
+ }
16339
+ function findReapCandidates(branches, worktrees, remoteBranchNames) {
16340
+ const goneBranches = new Set(
16341
+ branches.filter((b) => hasProvenGoneRemote(b, remoteBranchNames)).map((b) => b.name)
16342
+ );
16305
16343
  return worktrees.filter((w) => goneBranches.has(w.branch)).map((w) => ({ branch: w.branch, worktreePath: w.path }));
16306
16344
  }
16307
16345
  async function reapCandidate(cwd, candidate, deps) {
@@ -16345,8 +16383,8 @@ async function reapCandidate(cwd, candidate, deps) {
16345
16383
  const worktreeRemoved = await git.removeWorktree(cwd, candidate.worktreePath);
16346
16384
  return { candidate, verdict, worktreeRemoved, staleClaimCleared };
16347
16385
  }
16348
- async function reapAll(cwd, branches, worktrees, currentBranch, deps) {
16349
- const candidates = findReapCandidates(branches, worktrees).filter(
16386
+ async function reapAll(cwd, branches, worktrees, currentBranch, deps, remoteBranchNames) {
16387
+ const candidates = findReapCandidates(branches, worktrees, remoteBranchNames).filter(
16350
16388
  (c) => c.branch !== currentBranch
16351
16389
  );
16352
16390
  const outcomes = [];
@@ -16355,9 +16393,9 @@ async function reapAll(cwd, branches, worktrees, currentBranch, deps) {
16355
16393
  }
16356
16394
  return outcomes;
16357
16395
  }
16358
- function findBareBranchCandidates(branches, worktrees) {
16396
+ function findBareBranchCandidates(branches, worktrees, remoteBranchNames) {
16359
16397
  const worktreeBranches = new Set(worktrees.map((w) => w.branch));
16360
- return branches.filter((b) => b.track.includes("gone") && !worktreeBranches.has(b.name)).map((b) => ({ branch: b.name }));
16398
+ return branches.filter((b) => hasProvenGoneRemote(b, remoteBranchNames) && !worktreeBranches.has(b.name)).map((b) => ({ branch: b.name }));
16361
16399
  }
16362
16400
  async function reapBareBranch(cwd, candidate, deps) {
16363
16401
  const { git, github } = deps;
@@ -16386,8 +16424,8 @@ async function reapBareBranch(cwd, candidate, deps) {
16386
16424
  const branchDeleted = await git.deleteBranch(cwd, candidate.branch);
16387
16425
  return { candidate, verdict, branchDeleted };
16388
16426
  }
16389
- async function reapAllBareBranches(cwd, branches, worktrees, currentBranch, deps) {
16390
- const candidates = findBareBranchCandidates(branches, worktrees).filter(
16427
+ async function reapAllBareBranches(cwd, branches, worktrees, currentBranch, deps, remoteBranchNames) {
16428
+ const candidates = findBareBranchCandidates(branches, worktrees, remoteBranchNames).filter(
16391
16429
  (c) => c.branch !== currentBranch
16392
16430
  );
16393
16431
  const outcomes = [];
@@ -16464,10 +16502,19 @@ async function gatherRepoFacts(options, deps) {
16464
16502
  };
16465
16503
  }
16466
16504
  async function runDoctorFix(cwd, facts, deps = { git: new GitAdapter(), github: new GithubCliAdapter() }) {
16467
- return reapAll(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps);
16505
+ const remoteBranchNames = await deps.git.listRemoteBranchNames(cwd);
16506
+ return reapAll(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps, remoteBranchNames);
16468
16507
  }
16469
16508
  async function runDoctorFixBranches(cwd, facts, deps = { git: new GitAdapter(), github: new GithubCliAdapter() }) {
16470
- return reapAllBareBranches(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps);
16509
+ const remoteBranchNames = await deps.git.listRemoteBranchNames(cwd);
16510
+ return reapAllBareBranches(
16511
+ cwd,
16512
+ facts.branches,
16513
+ facts.worktrees,
16514
+ facts.currentBranch,
16515
+ deps,
16516
+ remoteBranchNames
16517
+ );
16471
16518
  }
16472
16519
  function readLocalCoreVersion(cwd) {
16473
16520
  const path = join69(cwd, INSTANCE_CORE_FILE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.309.2",
3
+ "version": "0.309.3",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",