@biffo/cli 0.300.1 → 0.301.1

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.
Files changed (2) hide show
  1. package/dist/index.js +89 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1281,6 +1281,17 @@ var GitAdapter = class {
1281
1281
  const { stdout, exitCode } = await execa("git", ["rev-parse", "HEAD"], { cwd, reject: false });
1282
1282
  return exitCode === 0 ? stdout.trim() : null;
1283
1283
  }
1284
+ /**
1285
+ * `branch`'s own tip SHA, without checking it out — the bare-branch
1286
+ * counterpart to `headSha` (#1682 milestone 2). A branch with no linked
1287
+ * worktree has no `cwd` of its own to run `rev-parse HEAD` against, so this
1288
+ * resolves the ref by name instead; `cwd` only needs to be *any* worktree of
1289
+ * the same clone; local refs are shared across all of them.
1290
+ */
1291
+ async branchSha(cwd, branch) {
1292
+ const { stdout, exitCode } = await execa("git", ["rev-parse", branch], { cwd, reject: false });
1293
+ return exitCode === 0 ? stdout.trim() : null;
1294
+ }
1284
1295
  /**
1285
1296
  * Is `ancestor` reachable from `descendant` (including `ancestor ===
1286
1297
  * descendant`)? Three-valued, not boolean: `git merge-base --is-ancestor`
@@ -15037,6 +15048,43 @@ async function reapAll(cwd, branches, worktrees, currentBranch, deps) {
15037
15048
  }
15038
15049
  return outcomes;
15039
15050
  }
15051
+ function findBareBranchCandidates(branches, worktrees) {
15052
+ const worktreeBranches = new Set(worktrees.map((w) => w.branch));
15053
+ return branches.filter((b) => b.track.includes("gone") && !worktreeBranches.has(b.name)).map((b) => ({ branch: b.name }));
15054
+ }
15055
+ async function reapBareBranch(cwd, candidate, deps) {
15056
+ const { git, github } = deps;
15057
+ const prVerdict = await github.prVerdictForBranch(cwd, candidate.branch);
15058
+ let mergeContainsHead = true;
15059
+ if (prVerdict === "merged") {
15060
+ const [branchTip, mergedHeadSha] = await Promise.all([
15061
+ git.branchSha(cwd, candidate.branch),
15062
+ github.mergedHeadSha(cwd, candidate.branch)
15063
+ ]);
15064
+ mergeContainsHead = branchTip === null || mergedHeadSha === null ? null : await git.isAncestor(cwd, branchTip, mergedHeadSha);
15065
+ }
15066
+ const verdict = classifyReapCandidate({
15067
+ isDetached: false,
15068
+ isDirty: false,
15069
+ prVerdict,
15070
+ mergeContainsHead
15071
+ });
15072
+ if (verdict.action === "keep") {
15073
+ return { candidate, verdict, branchDeleted: null };
15074
+ }
15075
+ const branchDeleted = await git.deleteBranch(cwd, candidate.branch);
15076
+ return { candidate, verdict, branchDeleted };
15077
+ }
15078
+ async function reapAllBareBranches(cwd, branches, worktrees, currentBranch, deps) {
15079
+ const candidates = findBareBranchCandidates(branches, worktrees).filter(
15080
+ (c) => c.branch !== currentBranch
15081
+ );
15082
+ const outcomes = [];
15083
+ for (const candidate of candidates) {
15084
+ outcomes.push(await reapBareBranch(cwd, candidate, deps));
15085
+ }
15086
+ return outcomes;
15087
+ }
15040
15088
 
15041
15089
  // src/commands/doctor.ts
15042
15090
  var INTEGRATION_BRANCH = "dev";
@@ -15044,7 +15092,7 @@ var doctorCommand = new Command25("doctor").description(
15044
15092
  "Report repo-state conditions that make everything read from this checkout unreliable"
15045
15093
  ).option("--cwd <path>", "Repo root to inspect (defaults to the current directory)").option("--no-fetch", "Skip the fetch; report against refs as they already are locally").option(
15046
15094
  "--fix",
15047
- "Also remove any worktree PROVEN safe: one whose branch's PR merged, verified via GitHub (never local commit reachability \u2014 a squash merge rewrites every SHA), AND whose current HEAD is confirmed contained in what that PR actually shipped \u2014 not merely on a branch of the same name (#1810). Everything else (no PR, PR open, PR closed unmerged, detached HEAD, uncommitted changes, commits ahead of what merged, or that containment could not be confirmed) is reported, never touched. Branch deletion is not yet automated here \u2014 that is #1682 milestone 2."
15095
+ "Also remove any worktree, and delete any bare local branch, PROVEN safe: one whose branch's PR merged, verified via GitHub (never local commit reachability \u2014 a squash merge rewrites every SHA), AND whose current tip is confirmed contained in what that PR actually shipped \u2014 not merely on a branch of the same name (#1810). Everything else (no PR, PR open, PR closed unmerged, detached HEAD, uncommitted changes, commits ahead of what merged, or that containment could not be confirmed) is reported, never touched."
15048
15096
  ).action(async (options) => {
15049
15097
  const cwd = options.cwd ? resolve20(options.cwd) : process.cwd();
15050
15098
  const git = new GitAdapter();
@@ -15053,8 +15101,11 @@ var doctorCommand = new Command25("doctor").description(
15053
15101
  const findings = runDoctorChecks(facts);
15054
15102
  printFindings(findings);
15055
15103
  if (options.fix === true) {
15056
- const outcomes = await runDoctorFix(cwd, facts, { git, github: new GithubCliAdapter() });
15104
+ const github = new GithubCliAdapter();
15105
+ const outcomes = await runDoctorFix(cwd, facts, { git, github });
15057
15106
  printReapOutcomes(outcomes);
15107
+ const branchOutcomes = await runDoctorFixBranches(cwd, facts, { git, github });
15108
+ printBranchReapOutcomes(branchOutcomes);
15058
15109
  }
15059
15110
  if (findings.some((f) => f.severity === "error")) process.exit(1);
15060
15111
  } catch (err) {
@@ -15104,6 +15155,9 @@ async function gatherRepoFacts(options, deps) {
15104
15155
  async function runDoctorFix(cwd, facts, deps = { git: new GitAdapter(), github: new GithubCliAdapter() }) {
15105
15156
  return reapAll(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps);
15106
15157
  }
15158
+ async function runDoctorFixBranches(cwd, facts, deps = { git: new GitAdapter(), github: new GithubCliAdapter() }) {
15159
+ return reapAllBareBranches(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps);
15160
+ }
15107
15161
  function readLocalCoreVersion(cwd) {
15108
15162
  const path = join61(cwd, INSTANCE_CORE_FILE);
15109
15163
  if (!existsSync53(path)) return null;
@@ -15204,6 +15258,39 @@ function printReapOutcomes(outcomes) {
15204
15258
  )
15205
15259
  );
15206
15260
  }
15261
+ function printBranchReapOutcomes(outcomes) {
15262
+ if (outcomes.length === 0) {
15263
+ console.log(chalk21.dim(" --fix (branches): no bare branch with a gone upstream to consider.\n"));
15264
+ return;
15265
+ }
15266
+ const attempted = outcomes.filter((o) => o.verdict.action === "reap");
15267
+ const kept = outcomes.filter((o) => o.verdict.action === "keep");
15268
+ const deleted = attempted.filter((o) => o.branchDeleted === true);
15269
+ const failed = attempted.filter((o) => o.branchDeleted !== true);
15270
+ console.log("");
15271
+ for (const o of attempted) {
15272
+ if (o.branchDeleted === true) {
15273
+ console.log(chalk21.green(` deleted ${o.candidate.branch}`));
15274
+ } else {
15275
+ console.log(
15276
+ chalk21.red(
15277
+ ` FAILED ${o.candidate.branch} \u2014 judged safe to delete but 'git branch -D' did not succeed; left as-is`
15278
+ )
15279
+ );
15280
+ }
15281
+ }
15282
+ for (const o of kept) {
15283
+ const reason = o.verdict.reason === void 0 ? "unknown" : KEEP_REASON_TEXT[o.verdict.reason];
15284
+ console.log(chalk21.dim(` kept ${o.candidate.branch} \u2014 ${reason}`));
15285
+ }
15286
+ console.log(
15287
+ chalk21.dim(
15288
+ `
15289
+ --fix (branches): ${String(deleted.length)} deleted, ${String(failed.length)} failed, ${String(kept.length)} kept, of ${String(outcomes.length)} branch(es) considered.
15290
+ `
15291
+ )
15292
+ );
15293
+ }
15207
15294
 
15208
15295
  // src/commands/teardown.ts
15209
15296
  import { execSync as execSync7 } from "child_process";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.300.1",
3
+ "version": "0.301.1",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",