@biffo/cli 0.312.4 → 0.313.0
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.
|
@@ -166,4 +166,22 @@ fi
|
|
|
166
166
|
# without the file passed its pre-push having checked nothing, which is the
|
|
167
167
|
# same defect one level up from the one #855 recorded. A gate that cannot run
|
|
168
168
|
# is not a gate that passed.
|
|
169
|
-
|
|
169
|
+
#
|
|
170
|
+
# Not a bare `exec` (biffo-template#1985): git's own generic
|
|
171
|
+
# "error: failed to push some refs" line is printed by git itself, after this
|
|
172
|
+
# hook exits, and is the literal last thing in a truncated-tail read of a long
|
|
173
|
+
# agent transcript. Nothing distinguished it from a real auth/network
|
|
174
|
+
# rejection -- three same-day dispatches on tabsii-platform#1403 diagnosed and
|
|
175
|
+
# "fixed" a token before noticing the real cause was a local gate. verify.sh's
|
|
176
|
+
# own failure text (above) already names the cause; this adds one more line,
|
|
177
|
+
# deliberately placed as close as this hook can get to git's trailing output,
|
|
178
|
+
# so a reader who only sees the last few lines still sees it.
|
|
179
|
+
sh scripts/biffo.sh verify
|
|
180
|
+
verify_status=$?
|
|
181
|
+
if [ "$verify_status" -ne 0 ]; then
|
|
182
|
+
printf '\n\033[31m>>> pre-push: BLOCKED BY THE LOCAL GATE ABOVE, not by git or GitHub.\033[0m\n' >&2
|
|
183
|
+
printf ' See "verify failed:" above for which check(s) and why. Any\n' >&2
|
|
184
|
+
printf ' "failed to push some refs" line below this is git relaying that\n' >&2
|
|
185
|
+
printf ' same exit code -- it is not a separate rejection.\n' >&2
|
|
186
|
+
fi
|
|
187
|
+
exit "$verify_status"
|
|
@@ -166,4 +166,22 @@ fi
|
|
|
166
166
|
# without the file passed its pre-push having checked nothing, which is the
|
|
167
167
|
# same defect one level up from the one #855 recorded. A gate that cannot run
|
|
168
168
|
# is not a gate that passed.
|
|
169
|
-
|
|
169
|
+
#
|
|
170
|
+
# Not a bare `exec` (biffo-template#1985): git's own generic
|
|
171
|
+
# "error: failed to push some refs" line is printed by git itself, after this
|
|
172
|
+
# hook exits, and is the literal last thing in a truncated-tail read of a long
|
|
173
|
+
# agent transcript. Nothing distinguished it from a real auth/network
|
|
174
|
+
# rejection -- three same-day dispatches on tabsii-platform#1403 diagnosed and
|
|
175
|
+
# "fixed" a token before noticing the real cause was a local gate. verify.sh's
|
|
176
|
+
# own failure text (above) already names the cause; this adds one more line,
|
|
177
|
+
# deliberately placed as close as this hook can get to git's trailing output,
|
|
178
|
+
# so a reader who only sees the last few lines still sees it.
|
|
179
|
+
sh scripts/biffo.sh verify
|
|
180
|
+
verify_status=$?
|
|
181
|
+
if [ "$verify_status" -ne 0 ]; then
|
|
182
|
+
printf '\n\033[31m>>> pre-push: BLOCKED BY THE LOCAL GATE ABOVE, not by git or GitHub.\033[0m\n' >&2
|
|
183
|
+
printf ' See "verify failed:" above for which check(s) and why. Any\n' >&2
|
|
184
|
+
printf ' "failed to push some refs" line below this is git relaying that\n' >&2
|
|
185
|
+
printf ' same exit code -- it is not a separate rejection.\n' >&2
|
|
186
|
+
fi
|
|
187
|
+
exit "$verify_status"
|
package/dist/index.js
CHANGED
|
@@ -16299,7 +16299,7 @@ function rawArgsAfter(subcommand) {
|
|
|
16299
16299
|
|
|
16300
16300
|
// src/commands/doctor.ts
|
|
16301
16301
|
import { existsSync as existsSync56, readFileSync as readFileSync47 } from "fs";
|
|
16302
|
-
import { join as
|
|
16302
|
+
import { join as join69, resolve as resolve20 } from "path";
|
|
16303
16303
|
import chalk21 from "chalk";
|
|
16304
16304
|
import { Command as Command25 } from "commander";
|
|
16305
16305
|
|
|
@@ -16638,33 +16638,110 @@ async function reapAllBareBranches(cwd, branches, worktrees, currentBranch, deps
|
|
|
16638
16638
|
return outcomes;
|
|
16639
16639
|
}
|
|
16640
16640
|
|
|
16641
|
-
// src/
|
|
16641
|
+
// src/lib/scratch-clone-scan.ts
|
|
16642
|
+
import { readdirSync as readdirSync29, statSync as statSync18 } from "fs";
|
|
16643
|
+
import { join as join68 } from "path";
|
|
16642
16644
|
var INTEGRATION_BRANCH = "dev";
|
|
16645
|
+
function isPlainCloneDir(path) {
|
|
16646
|
+
try {
|
|
16647
|
+
return statSync18(join68(path, ".git")).isDirectory();
|
|
16648
|
+
} catch {
|
|
16649
|
+
return false;
|
|
16650
|
+
}
|
|
16651
|
+
}
|
|
16652
|
+
function repoNameFromRemoteUrl(url) {
|
|
16653
|
+
const trimmed = url.trim();
|
|
16654
|
+
if (trimmed === "") return null;
|
|
16655
|
+
const match = /\/([^/]+?)(\.git)?\/?$/.exec(trimmed);
|
|
16656
|
+
return match?.[1] && match[1] !== "" ? match[1] : null;
|
|
16657
|
+
}
|
|
16658
|
+
async function findScratchCloneCandidates(estateRoot, deps) {
|
|
16659
|
+
const names = readdirSync29(estateRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
16660
|
+
const candidates = [];
|
|
16661
|
+
for (const name of names) {
|
|
16662
|
+
const path = join68(estateRoot, name);
|
|
16663
|
+
if (!isPlainCloneDir(path)) continue;
|
|
16664
|
+
let branch;
|
|
16665
|
+
try {
|
|
16666
|
+
branch = await deps.git.currentBranch(path);
|
|
16667
|
+
} catch {
|
|
16668
|
+
candidates.push({ path, branch: "", invalidRepo: true });
|
|
16669
|
+
continue;
|
|
16670
|
+
}
|
|
16671
|
+
const remoteUrl = await deps.git.getRemoteUrl(path).catch(() => "");
|
|
16672
|
+
const repoName = remoteUrl === "" ? null : repoNameFromRemoteUrl(remoteUrl);
|
|
16673
|
+
const looksLikeKnownRepoCheckout = repoName !== null && repoName.toLowerCase() === name.toLowerCase() && branch === INTEGRATION_BRANCH;
|
|
16674
|
+
if (looksLikeKnownRepoCheckout) continue;
|
|
16675
|
+
candidates.push({ path, branch });
|
|
16676
|
+
}
|
|
16677
|
+
return candidates;
|
|
16678
|
+
}
|
|
16679
|
+
async function classifyScratchClones(candidates, deps) {
|
|
16680
|
+
const reports = [];
|
|
16681
|
+
for (const candidate of candidates) {
|
|
16682
|
+
if (candidate.invalidRepo === true) {
|
|
16683
|
+
reports.push({ candidate, verdict: { action: "keep", reason: "not-a-git-repository" } });
|
|
16684
|
+
continue;
|
|
16685
|
+
}
|
|
16686
|
+
const isDetached = candidate.branch === "HEAD" || candidate.branch === "";
|
|
16687
|
+
const isDirty = await deps.git.hasUncommittedChanges(candidate.path);
|
|
16688
|
+
const prVerdict = isDetached || isDirty ? "unknown" : await deps.github.prVerdictForBranch(candidate.path, candidate.branch);
|
|
16689
|
+
let mergeContainsHead = true;
|
|
16690
|
+
if (prVerdict === "merged") {
|
|
16691
|
+
const [headSha, mergedHeadSha] = await Promise.all([
|
|
16692
|
+
deps.git.headSha(candidate.path),
|
|
16693
|
+
deps.github.mergedHeadSha(candidate.path, candidate.branch)
|
|
16694
|
+
]);
|
|
16695
|
+
mergeContainsHead = headSha === null || mergedHeadSha === null ? null : await deps.git.isAncestor(candidate.path, headSha, mergedHeadSha);
|
|
16696
|
+
}
|
|
16697
|
+
const verdict = classifyReapCandidate({
|
|
16698
|
+
isDetached,
|
|
16699
|
+
isDirty,
|
|
16700
|
+
hasFleetClaim: false,
|
|
16701
|
+
prVerdict,
|
|
16702
|
+
mergeContainsHead
|
|
16703
|
+
});
|
|
16704
|
+
reports.push({ candidate, verdict });
|
|
16705
|
+
}
|
|
16706
|
+
return reports;
|
|
16707
|
+
}
|
|
16708
|
+
|
|
16709
|
+
// src/commands/doctor.ts
|
|
16710
|
+
var INTEGRATION_BRANCH2 = "dev";
|
|
16643
16711
|
var doctorCommand = new Command25("doctor").description(
|
|
16644
16712
|
"Report repo-state conditions that make everything read from this checkout unreliable"
|
|
16645
16713
|
).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(
|
|
16646
16714
|
"--fix",
|
|
16647
16715
|
"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."
|
|
16648
|
-
).
|
|
16649
|
-
|
|
16650
|
-
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
-
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16716
|
+
).option(
|
|
16717
|
+
"--scratch-clones <estateRoot>",
|
|
16718
|
+
"Instead of checking one repo, scan <estateRoot> for top-level plain `git clone` directories (#1949) \u2014 invisible to every check above, which all start from `git worktree list`. Each is classified the same way `--fix` classifies a worktree (GitHub's own PR verdict), but nothing is ever removed by this mode: a wrongly-excluded directory costs a report line, an `rm -rf` on a wrongly-included one would not be recoverable the way `git worktree remove` is."
|
|
16719
|
+
).action(
|
|
16720
|
+
async (options) => {
|
|
16721
|
+
if (options.scratchClones !== void 0) {
|
|
16722
|
+
await runScratchCloneScan(resolve20(options.scratchClones));
|
|
16723
|
+
return;
|
|
16724
|
+
}
|
|
16725
|
+
const cwd = options.cwd ? resolve20(options.cwd) : process.cwd();
|
|
16726
|
+
const git = new GitAdapter();
|
|
16727
|
+
try {
|
|
16728
|
+
const facts = await gatherRepoFacts({ cwd, fetch: options.fetch !== false }, { git });
|
|
16729
|
+
const findings = runDoctorChecks(facts);
|
|
16730
|
+
printFindings(findings);
|
|
16731
|
+
if (options.fix === true) {
|
|
16732
|
+
const github = new GithubCliAdapter();
|
|
16733
|
+
const outcomes = await runDoctorFix(cwd, facts, { git, github });
|
|
16734
|
+
printReapOutcomes(outcomes);
|
|
16735
|
+
const branchOutcomes = await runDoctorFixBranches(cwd, facts, { git, github });
|
|
16736
|
+
printBranchReapOutcomes(branchOutcomes);
|
|
16737
|
+
}
|
|
16738
|
+
if (findings.some((f) => f.severity === "error")) process.exit(1);
|
|
16739
|
+
} catch (err) {
|
|
16740
|
+
log.error(err.message);
|
|
16741
|
+
process.exit(1);
|
|
16742
|
+
}
|
|
16666
16743
|
}
|
|
16667
|
-
|
|
16744
|
+
);
|
|
16668
16745
|
async function gatherRepoFacts(options, deps) {
|
|
16669
16746
|
const { git } = deps;
|
|
16670
16747
|
if (!await git.isGitRepo(options.cwd)) {
|
|
@@ -16680,13 +16757,13 @@ async function gatherRepoFacts(options, deps) {
|
|
|
16680
16757
|
const worktrees = await Promise.all(
|
|
16681
16758
|
worktreePaths.map(async (w) => ({
|
|
16682
16759
|
...w,
|
|
16683
|
-
behind: await git.countBehind(options.cwd, w.branch, `origin/${
|
|
16760
|
+
behind: await git.countBehind(options.cwd, w.branch, `origin/${INTEGRATION_BRANCH2}`)
|
|
16684
16761
|
}))
|
|
16685
16762
|
);
|
|
16686
16763
|
return {
|
|
16687
16764
|
currentBranch,
|
|
16688
16765
|
isPrimary,
|
|
16689
|
-
integrationBranch:
|
|
16766
|
+
integrationBranch: INTEGRATION_BRANCH2,
|
|
16690
16767
|
ahead,
|
|
16691
16768
|
behind,
|
|
16692
16769
|
hasUpstream,
|
|
@@ -16697,7 +16774,7 @@ async function gatherRepoFacts(options, deps) {
|
|
|
16697
16774
|
// a fault in it.
|
|
16698
16775
|
localCoreVersion: readLocalCoreVersion(options.cwd),
|
|
16699
16776
|
remoteCoreVersion: parseCoreRecord(
|
|
16700
|
-
await git.showFileAtRef(options.cwd, `origin/${
|
|
16777
|
+
await git.showFileAtRef(options.cwd, `origin/${INTEGRATION_BRANCH2}`, INSTANCE_CORE_FILE)
|
|
16701
16778
|
),
|
|
16702
16779
|
fossilCoreVersion: readFossil(options.cwd),
|
|
16703
16780
|
branches,
|
|
@@ -16719,8 +16796,50 @@ async function runDoctorFixBranches(cwd, facts, deps = { git: new GitAdapter(),
|
|
|
16719
16796
|
remoteBranchNames
|
|
16720
16797
|
);
|
|
16721
16798
|
}
|
|
16799
|
+
async function runScratchCloneScan(estateRoot) {
|
|
16800
|
+
const git = new GitAdapter();
|
|
16801
|
+
const github = new GithubCliAdapter();
|
|
16802
|
+
try {
|
|
16803
|
+
const candidates = await findScratchCloneCandidates(estateRoot, { git });
|
|
16804
|
+
const reports = await classifyScratchClones(candidates, { git, github });
|
|
16805
|
+
printScratchCloneReports(estateRoot, reports);
|
|
16806
|
+
} catch (err) {
|
|
16807
|
+
log.error(err.message);
|
|
16808
|
+
process.exit(1);
|
|
16809
|
+
}
|
|
16810
|
+
}
|
|
16811
|
+
function printScratchCloneReports(estateRoot, reports) {
|
|
16812
|
+
if (reports.length === 0) {
|
|
16813
|
+
console.log(
|
|
16814
|
+
chalk21.dim(` scratch-clones: no plain git-clone directory found under ${estateRoot}.
|
|
16815
|
+
`)
|
|
16816
|
+
);
|
|
16817
|
+
return;
|
|
16818
|
+
}
|
|
16819
|
+
const reapable = reports.filter((r) => r.verdict.action === "reap");
|
|
16820
|
+
const kept = reports.filter((r) => r.verdict.action === "keep");
|
|
16821
|
+
console.log("");
|
|
16822
|
+
for (const r of reapable) {
|
|
16823
|
+
console.log(
|
|
16824
|
+
chalk21.yellow(
|
|
16825
|
+
` reapable ${r.candidate.path} (${r.candidate.branch}) \u2014 branch's PR merged and this clone's tip is contained in what shipped; plain git clone, not a worktree, so \`git worktree remove\` does not apply \u2014 remove by hand once confirmed: rm -rf '${r.candidate.path}'`
|
|
16826
|
+
)
|
|
16827
|
+
);
|
|
16828
|
+
}
|
|
16829
|
+
for (const r of kept) {
|
|
16830
|
+
const reason = r.verdict.reason === void 0 ? "unknown" : KEEP_REASON_TEXT[r.verdict.reason];
|
|
16831
|
+
console.log(chalk21.dim(` kept ${r.candidate.path} (${r.candidate.branch}) \u2014 ${reason}`));
|
|
16832
|
+
}
|
|
16833
|
+
console.log(
|
|
16834
|
+
chalk21.dim(
|
|
16835
|
+
`
|
|
16836
|
+
scratch-clones: ${String(reapable.length)} reapable, ${String(kept.length)} kept, of ${String(reports.length)} plain git-clone director${reports.length === 1 ? "y" : "ies"} found under ${estateRoot}.
|
|
16837
|
+
`
|
|
16838
|
+
)
|
|
16839
|
+
);
|
|
16840
|
+
}
|
|
16722
16841
|
function readLocalCoreVersion(cwd) {
|
|
16723
|
-
const path =
|
|
16842
|
+
const path = join69(cwd, INSTANCE_CORE_FILE);
|
|
16724
16843
|
if (!existsSync56(path)) return null;
|
|
16725
16844
|
try {
|
|
16726
16845
|
return extractVersionField(readFileSync47(path, "utf8"));
|
|
@@ -16743,7 +16862,7 @@ function extractVersionField(contents) {
|
|
|
16743
16862
|
return match?.[1] ?? null;
|
|
16744
16863
|
}
|
|
16745
16864
|
function readFossil(cwd) {
|
|
16746
|
-
const path =
|
|
16865
|
+
const path = join69(cwd, CORE_VERSION_FILE);
|
|
16747
16866
|
if (!existsSync56(path)) return null;
|
|
16748
16867
|
try {
|
|
16749
16868
|
const value = readFileSync47(path, "utf8").trim();
|
|
@@ -16783,7 +16902,8 @@ var KEEP_REASON_TEXT = {
|
|
|
16783
16902
|
"no-pr": "no PR was ever opened from this branch",
|
|
16784
16903
|
"unknown-pr-verdict": "could not read this branch's PR state from GitHub",
|
|
16785
16904
|
"commits-not-in-merge": "worktree HEAD includes commits the merged PR never shipped",
|
|
16786
|
-
"unknown-merge-head": "could not confirm worktree HEAD is contained in what merged"
|
|
16905
|
+
"unknown-merge-head": "could not confirm worktree HEAD is contained in what merged",
|
|
16906
|
+
"not-a-git-repository": "the .git directory exists but is not a resolvable, initialised git repository"
|
|
16787
16907
|
};
|
|
16788
16908
|
function printReapOutcomes(outcomes) {
|
|
16789
16909
|
if (outcomes.length === 0) {
|
|
@@ -17277,18 +17397,18 @@ function resolveGithubToken4() {
|
|
|
17277
17397
|
|
|
17278
17398
|
// src/lib/packaged-script-command.ts
|
|
17279
17399
|
import { spawnSync } from "child_process";
|
|
17280
|
-
import { chmodSync as chmodSync2, statSync as
|
|
17400
|
+
import { chmodSync as chmodSync2, statSync as statSync19 } from "fs";
|
|
17281
17401
|
import { dirname as dirname12 } from "path";
|
|
17282
17402
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
17283
17403
|
import { Command as Command27 } from "commander";
|
|
17284
17404
|
|
|
17285
17405
|
// src/lib/packaged-scripts.ts
|
|
17286
17406
|
import { existsSync as existsSync57 } from "fs";
|
|
17287
|
-
import { dirname as dirname11, join as
|
|
17407
|
+
import { dirname as dirname11, join as join70 } from "path";
|
|
17288
17408
|
function findPackagedScript(startDir, relativePath) {
|
|
17289
17409
|
let dir = startDir;
|
|
17290
17410
|
for (; ; ) {
|
|
17291
|
-
const candidate =
|
|
17411
|
+
const candidate = join70(dir, relativePath);
|
|
17292
17412
|
if (existsSync57(candidate)) return candidate;
|
|
17293
17413
|
const parent = dirname11(dir);
|
|
17294
17414
|
if (parent === dir) return null;
|
|
@@ -17303,7 +17423,7 @@ It ships with this package via cli/scripts/packaged-root-assets.mjs; if you are
|
|
|
17303
17423
|
// src/lib/packaged-script-command.ts
|
|
17304
17424
|
function ensureExecutable(script) {
|
|
17305
17425
|
try {
|
|
17306
|
-
const { mode } =
|
|
17426
|
+
const { mode } = statSync19(script);
|
|
17307
17427
|
if ((mode & 73) === 0) chmodSync2(script, mode | 493);
|
|
17308
17428
|
} catch {
|
|
17309
17429
|
}
|
package/package.json
CHANGED
package/scripts/verify.sh
CHANGED
|
@@ -1273,6 +1273,10 @@ elif [ -z "$PG_TEST_DSN" ]; then
|
|
|
1273
1273
|
printf ' the pg-test lane above did not run. CI treats it as a required check, so a\n'
|
|
1274
1274
|
printf ' push here would report green without having checked the thing most likely\n'
|
|
1275
1275
|
printf ' to need it.\n\n'
|
|
1276
|
+
printf ' This is NOT a credentials or network problem. It is a local gate refusing\n'
|
|
1277
|
+
printf ' to push until the Postgres test lane runs. If git prints its own generic\n'
|
|
1278
|
+
printf ' "failed to push some refs" line below this, that is git relaying THIS exit\n'
|
|
1279
|
+
printf ' code -- not a rejection by GitHub or a sign of a bad token.\n\n'
|
|
1276
1280
|
printf ' Get a DSN and re-run:\n'
|
|
1277
1281
|
printf ' eval "$(sh scripts/pg-test-db.sh --export)" # if this repo ships one\n'
|
|
1278
1282
|
printf ' export BIFFO_TEST_PG_DSN=postgresql+asyncpg://user:pass@host:port/db\n\n'
|