@biffo/cli 0.237.0 → 0.238.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.
- package/_skeletons/plugin-template/.githooks/pre-push +8 -5
- package/_skeletons/plugin-template/scripts/verify.sh +6 -2
- package/_skeletons/sibling-template/.githooks/pre-push +8 -5
- package/_skeletons/sibling-template/scripts/verify.sh +6 -2
- package/dist/index.js +24 -0
- package/package.json +5 -2
- package/_skeletons/sibling-template/scripts/hook-audit.sh +0 -145
- package/_skeletons/sibling-template/scripts/pg-test-db.sh +0 -275
- package/_skeletons/sibling-template/scripts/rewrite-scope-check.sh +0 -189
- /package/{_skeletons/plugin-template/scripts → scripts}/hook-audit.sh +0 -0
- /package/{_skeletons/plugin-template/scripts → scripts}/pg-test-db.sh +0 -0
- /package/{_skeletons/plugin-template/scripts → scripts}/rewrite-scope-check.sh +0 -0
|
@@ -31,11 +31,14 @@ cd "$root" || exit 0
|
|
|
31
31
|
# Why it exists: a force-push can quietly add a file to a branch's change set
|
|
32
32
|
# and record a stale copy of somebody else's merged work as your own change.
|
|
33
33
|
# That happened (tabsii-platform#446) and every local gate was green on it.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
# Runs from the version-pinned CLI rather than a per-repo copy (#1109).
|
|
35
|
+
#
|
|
36
|
+
# This used to warn and CONTINUE when the script was absent, which is a
|
|
37
|
+
# fail-open in a gate whose whole job is refusing a push: the one repo missing
|
|
38
|
+
# the file was the one where nothing checked. There is no absent case now --
|
|
39
|
+
# the bridge resolves the script from the package -- and if the bridge itself
|
|
40
|
+
# cannot run, that is a reason to stop, not to wave the push through.
|
|
41
|
+
sh scripts/biffo.sh rewrite-scope-check || exit 1
|
|
39
42
|
|
|
40
43
|
if [ ! -f scripts/verify.sh ]; then
|
|
41
44
|
echo "pre-push: no scripts/verify.sh — NO checks ran (see local-gates.md)" >&2
|
|
@@ -662,8 +662,12 @@ _pg_modules=$(pg_test_modules)
|
|
|
662
662
|
# rebuild), so calling it is better than warning about it. Failure is silent
|
|
663
663
|
# BECAUSE the WARN below is the honest report of it -- no Docker, no server, no
|
|
664
664
|
# schema all end in the same place: the lane did not run, and the gate says so.
|
|
665
|
-
if [ -z "$PG_TEST_DSN" ] && [ -n "$_pg_modules" ] && [ -z "$LIST" ]
|
|
666
|
-
|
|
665
|
+
if [ -z "$PG_TEST_DSN" ] && [ -n "$_pg_modules" ] && [ -z "$LIST" ]; then
|
|
666
|
+
# Through the bridge since #1109; the `-f scripts/pg-test-db.sh` guard went
|
|
667
|
+
# with the copy. Failure stays silent here BECAUSE the WARN below is the
|
|
668
|
+
# honest report of it -- no Docker, no server, no schema and no CLI all end
|
|
669
|
+
# in the same place: the lane did not run, and the gate says so.
|
|
670
|
+
PG_TEST_DSN=$(sh scripts/biffo.sh pg-test-db 2>/dev/null | tail -1) || PG_TEST_DSN=""
|
|
667
671
|
case "$PG_TEST_DSN" in
|
|
668
672
|
postgres*) ;;
|
|
669
673
|
*) PG_TEST_DSN="" ;;
|
|
@@ -31,11 +31,14 @@ cd "$root" || exit 0
|
|
|
31
31
|
# Why it exists: a force-push can quietly add a file to a branch's change set
|
|
32
32
|
# and record a stale copy of somebody else's merged work as your own change.
|
|
33
33
|
# That happened (tabsii-platform#446) and every local gate was green on it.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
# Runs from the version-pinned CLI rather than a per-repo copy (#1109).
|
|
35
|
+
#
|
|
36
|
+
# This used to warn and CONTINUE when the script was absent, which is a
|
|
37
|
+
# fail-open in a gate whose whole job is refusing a push: the one repo missing
|
|
38
|
+
# the file was the one where nothing checked. There is no absent case now --
|
|
39
|
+
# the bridge resolves the script from the package -- and if the bridge itself
|
|
40
|
+
# cannot run, that is a reason to stop, not to wave the push through.
|
|
41
|
+
sh scripts/biffo.sh rewrite-scope-check || exit 1
|
|
39
42
|
|
|
40
43
|
if [ ! -f scripts/verify.sh ]; then
|
|
41
44
|
echo "pre-push: no scripts/verify.sh — NO checks ran (see local-gates.md)" >&2
|
|
@@ -662,8 +662,12 @@ _pg_modules=$(pg_test_modules)
|
|
|
662
662
|
# rebuild), so calling it is better than warning about it. Failure is silent
|
|
663
663
|
# BECAUSE the WARN below is the honest report of it -- no Docker, no server, no
|
|
664
664
|
# schema all end in the same place: the lane did not run, and the gate says so.
|
|
665
|
-
if [ -z "$PG_TEST_DSN" ] && [ -n "$_pg_modules" ] && [ -z "$LIST" ]
|
|
666
|
-
|
|
665
|
+
if [ -z "$PG_TEST_DSN" ] && [ -n "$_pg_modules" ] && [ -z "$LIST" ]; then
|
|
666
|
+
# Through the bridge since #1109; the `-f scripts/pg-test-db.sh` guard went
|
|
667
|
+
# with the copy. Failure stays silent here BECAUSE the WARN below is the
|
|
668
|
+
# honest report of it -- no Docker, no server, no schema and no CLI all end
|
|
669
|
+
# in the same place: the lane did not run, and the gate says so.
|
|
670
|
+
PG_TEST_DSN=$(sh scripts/biffo.sh pg-test-db 2>/dev/null | tail -1) || PG_TEST_DSN=""
|
|
667
671
|
case "$PG_TEST_DSN" in
|
|
668
672
|
postgres*) ;;
|
|
669
673
|
*) PG_TEST_DSN="" ;;
|
package/dist/index.js
CHANGED
|
@@ -10813,6 +10813,27 @@ function packagedScriptCommand(spec) {
|
|
|
10813
10813
|
});
|
|
10814
10814
|
}
|
|
10815
10815
|
|
|
10816
|
+
// src/commands/hook-audit.ts
|
|
10817
|
+
var hookAuditCommand = packagedScriptCommand({
|
|
10818
|
+
name: "hook-audit",
|
|
10819
|
+
script: "scripts/hook-audit.sh",
|
|
10820
|
+
description: "Report whether git hooks will actually fire in every working tree"
|
|
10821
|
+
});
|
|
10822
|
+
|
|
10823
|
+
// src/commands/pg-test-db.ts
|
|
10824
|
+
var pgTestDbCommand = packagedScriptCommand({
|
|
10825
|
+
name: "pg-test-db",
|
|
10826
|
+
script: "scripts/pg-test-db.sh",
|
|
10827
|
+
description: "Provision the local Postgres test database and print its DSN"
|
|
10828
|
+
});
|
|
10829
|
+
|
|
10830
|
+
// src/commands/rewrite-scope-check.ts
|
|
10831
|
+
var rewriteScopeCheckCommand = packagedScriptCommand({
|
|
10832
|
+
name: "rewrite-scope-check",
|
|
10833
|
+
script: "scripts/rewrite-scope-check.sh",
|
|
10834
|
+
description: "Refuse a push whose rewrite scope absorbs merged work from another branch"
|
|
10835
|
+
});
|
|
10836
|
+
|
|
10816
10837
|
// src/commands/branch-health.ts
|
|
10817
10838
|
var branchHealthCommand = packagedScriptCommand({
|
|
10818
10839
|
name: "branch-health",
|
|
@@ -10852,6 +10873,9 @@ program.addCommand(teardownCommand);
|
|
|
10852
10873
|
program.addCommand(waitForChecksCommand);
|
|
10853
10874
|
program.addCommand(branchHealthCommand);
|
|
10854
10875
|
program.addCommand(claimCommand);
|
|
10876
|
+
program.addCommand(hookAuditCommand);
|
|
10877
|
+
program.addCommand(pgTestDbCommand);
|
|
10878
|
+
program.addCommand(rewriteScopeCheckCommand);
|
|
10855
10879
|
program.addCommand(pluginCommand);
|
|
10856
10880
|
program.addCommand(dataCommand);
|
|
10857
10881
|
program.addCommand(coreCommand);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biffo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.238.1",
|
|
4
4
|
"description": "Biffo project scaffolding CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
"_skeletons",
|
|
27
27
|
"scripts/wait-for-checks.sh",
|
|
28
28
|
"scripts/branch-health.sh",
|
|
29
|
-
"scripts/claim.sh"
|
|
29
|
+
"scripts/claim.sh",
|
|
30
|
+
"scripts/hook-audit.sh",
|
|
31
|
+
"scripts/pg-test-db.sh",
|
|
32
|
+
"scripts/rewrite-scope-check.sh"
|
|
30
33
|
],
|
|
31
34
|
"scripts": {
|
|
32
35
|
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# Is a git hook actually going to execute here?
|
|
4
|
-
#
|
|
5
|
-
# ## Why this exists
|
|
6
|
-
#
|
|
7
|
-
# A configured hook that does not run is worse than no hook, because it is
|
|
8
|
-
# assumed to be protecting you. On 2026-07-29 the estate was in exactly that
|
|
9
|
-
# state: `core.hooksPath` pointed at `.husky/_`, a **gitignored** directory
|
|
10
|
-
# created only by `prepare: husky` on `pnpm install` — and git resolves that
|
|
11
|
-
# relative path against *each worktree's* root. Every fresh worktree therefore
|
|
12
|
-
# had no hooks, and git said nothing: no warning, no error, no output.
|
|
13
|
-
#
|
|
14
|
-
# AGENTS.md §1 mandates a fresh worktree per unit of work, so the required
|
|
15
|
-
# workflow disarmed its own gates. 6 of 32 working trees were armed. The pre-push
|
|
16
|
-
# pyright, the pre-commit lint-staged and commitlint had all been silently
|
|
17
|
-
# skipped in the other 26 for as long as anyone had been using worktrees.
|
|
18
|
-
#
|
|
19
|
-
# Nothing detected it because nothing looked. This is the thing that looks.
|
|
20
|
-
#
|
|
21
|
-
# ## Verdicts
|
|
22
|
-
#
|
|
23
|
-
# ARMED git will execute a hook here.
|
|
24
|
-
# DEAD core.hooksPath is set and its target is missing or carries no
|
|
25
|
-
# hooks. Git skips silently. **This is the state that lies to you**,
|
|
26
|
-
# and the only one that makes this script exit non-zero.
|
|
27
|
-
# NO-HOOKS no hooks configured at all. Honest, and visible in this report.
|
|
28
|
-
#
|
|
29
|
-
# Usage:
|
|
30
|
-
# sh scripts/hook-audit.sh # this repo and its worktrees
|
|
31
|
-
# sh scripts/hook-audit.sh --estate ~/code # every repo under a directory
|
|
32
|
-
# sh scripts/hook-audit.sh --quiet # verdict counts only
|
|
33
|
-
|
|
34
|
-
set -uo pipefail
|
|
35
|
-
|
|
36
|
-
ESTATE=""
|
|
37
|
-
QUIET=""
|
|
38
|
-
while [ $# -gt 0 ]; do
|
|
39
|
-
case "$1" in
|
|
40
|
-
--estate) ESTATE="$2"; shift 2 ;;
|
|
41
|
-
--quiet) QUIET=1; shift ;;
|
|
42
|
-
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
43
|
-
esac
|
|
44
|
-
done
|
|
45
|
-
|
|
46
|
-
armed=0
|
|
47
|
-
dead=0
|
|
48
|
-
nohooks=0
|
|
49
|
-
dead_list=""
|
|
50
|
-
|
|
51
|
-
# The three hooks this standard cares about. A hooksPath directory containing
|
|
52
|
-
# none of them is not armed for our purposes even if it holds something else.
|
|
53
|
-
WANTED='^(pre-commit|pre-push|commit-msg)$'
|
|
54
|
-
|
|
55
|
-
report() {
|
|
56
|
-
tree="$1"
|
|
57
|
-
label="$2"
|
|
58
|
-
hp=$(git -C "$tree" config core.hooksPath 2>/dev/null)
|
|
59
|
-
|
|
60
|
-
if [ -z "$hp" ]; then
|
|
61
|
-
# The default hooks directory is in the **common** git dir, which linked
|
|
62
|
-
# worktrees share. Reading "$tree/.git/hooks" is wrong for exactly the trees
|
|
63
|
-
# this audit exists to check: in a linked worktree `.git` is a *file*
|
|
64
|
-
# containing a gitdir pointer, so that path does not exist and every armed
|
|
65
|
-
# worktree was about to be reported NO-HOOKS. Ask git where it actually is.
|
|
66
|
-
#
|
|
67
|
-
# Git ships .sample files there that never execute, so counting the
|
|
68
|
-
# directory as armed merely for being non-empty would be exactly the false
|
|
69
|
-
# comfort this script exists to remove.
|
|
70
|
-
common=$(git -C "$tree" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
|
|
71
|
-
real=$(ls "${common:-$tree/.git}/hooks" 2>/dev/null | grep -vc '\.sample$' || true)
|
|
72
|
-
if [ "${real:-0}" -gt 0 ]; then
|
|
73
|
-
armed=$((armed + 1))
|
|
74
|
-
[ -n "$QUIET" ] || printf '%-56s %-14s \033[32mARMED\033[0m %s\n' "$label" "(default)" "$real hook(s) in .git/hooks"
|
|
75
|
-
else
|
|
76
|
-
nohooks=$((nohooks + 1))
|
|
77
|
-
[ -n "$QUIET" ] || printf '%-56s %-14s \033[33mNO-HOOKS\033[0m %s\n' "$label" "(default)" "no hooks configured"
|
|
78
|
-
fi
|
|
79
|
-
return
|
|
80
|
-
fi
|
|
81
|
-
|
|
82
|
-
# Relative hooksPath resolves against the working tree root — the whole bug.
|
|
83
|
-
case "$hp" in
|
|
84
|
-
/*) dir="$hp" ;;
|
|
85
|
-
*) dir="$tree/$hp" ;;
|
|
86
|
-
esac
|
|
87
|
-
|
|
88
|
-
present=$(ls "$dir" 2>/dev/null | grep -E "$WANTED" | tr '\n' ',' || true)
|
|
89
|
-
if [ -z "$present" ]; then
|
|
90
|
-
dead=$((dead + 1))
|
|
91
|
-
dead_list="$dead_list $label ($hp)
|
|
92
|
-
"
|
|
93
|
-
[ -n "$QUIET" ] || printf '%-56s %-14s \033[31mDEAD\033[0m %s\n' "$label" "$hp" "$hp missing or holds no hooks — git skips ALL hooks silently"
|
|
94
|
-
else
|
|
95
|
-
armed=$((armed + 1))
|
|
96
|
-
[ -n "$QUIET" ] || printf '%-56s %-14s \033[32mARMED\033[0m %s\n' "$label" "$hp" "${present%,}"
|
|
97
|
-
fi
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
walk_repo() {
|
|
101
|
-
root="${1%/}"
|
|
102
|
-
name="$2"
|
|
103
|
-
# Every working tree, not just the primary — the primary is usually the one
|
|
104
|
-
# that IS armed, which is how this went unnoticed for so long.
|
|
105
|
-
git -C "$root" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2}' | while read -r t; do
|
|
106
|
-
[ -d "$t" ] || continue
|
|
107
|
-
if [ "$t" = "$root" ]; then echo "$t|$name"; else echo "$t|$name${t#$root}"; fi
|
|
108
|
-
done
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
[ -n "$QUIET" ] || printf '%-56s %-14s %-8s %s\n' "WORKING TREE" "hooksPath" "VERDICT" "detail"
|
|
112
|
-
|
|
113
|
-
if [ -n "$ESTATE" ]; then
|
|
114
|
-
targets=$(for d in "$ESTATE"/*/; do
|
|
115
|
-
[ -e "$d/.git" ] || continue
|
|
116
|
-
walk_repo "$d" "$(basename "${d%/}")"
|
|
117
|
-
done)
|
|
118
|
-
else
|
|
119
|
-
root=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "not a git repo" >&2; exit 2; }
|
|
120
|
-
# From inside a worktree, --show-toplevel gives the worktree; walk from the
|
|
121
|
-
# common repo so sibling worktrees are audited too.
|
|
122
|
-
common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
|
|
123
|
-
root=$(dirname "$common")
|
|
124
|
-
targets=$(walk_repo "$root" "$(basename "$root")")
|
|
125
|
-
fi
|
|
126
|
-
|
|
127
|
-
while IFS='|' read -r tree label; do
|
|
128
|
-
[ -n "$tree" ] || continue
|
|
129
|
-
report "$tree" "$label"
|
|
130
|
-
done <<EOF
|
|
131
|
-
$targets
|
|
132
|
-
EOF
|
|
133
|
-
|
|
134
|
-
total=$((armed + dead + nohooks))
|
|
135
|
-
printf '\n%s working trees — \033[32m%s armed\033[0m, \033[31m%s dead\033[0m, %s without hooks' "$total" "$armed" "$dead" "$nohooks"
|
|
136
|
-
[ "$total" -gt 0 ] && printf ' (%s%% armed)' "$((100 * armed / total))"
|
|
137
|
-
printf '\n'
|
|
138
|
-
|
|
139
|
-
if [ "$dead" -gt 0 ]; then
|
|
140
|
-
printf '\n\033[31mDEAD working trees — hooks are configured here and are NOT running:\033[0m\n%s' "$dead_list"
|
|
141
|
-
printf 'Every commit and push made in these is unguarded, and nothing says so.\n'
|
|
142
|
-
printf 'Fix: run `pnpm install` there, or move the repo to tracked .githooks/.\n'
|
|
143
|
-
exit 1
|
|
144
|
-
fi
|
|
145
|
-
exit 0
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env sh
|
|
2
|
-
#
|
|
3
|
-
# Give the Postgres-dependent test lane a database with a CURRENT schema, and
|
|
4
|
-
# print its DSN.
|
|
5
|
-
#
|
|
6
|
-
# ## Why this exists
|
|
7
|
-
#
|
|
8
|
-
# `scripts/verify.sh` grew a `pg-test` check (#1089) because on 2026-08-02 nine
|
|
9
|
-
# of thirteen locally-catchable failing CI steps across the estate were one
|
|
10
|
-
# repo's real-Postgres lane -- a required check with no local counterpart at all.
|
|
11
|
-
# But a gate can only run that lane against a database, and no repo documented
|
|
12
|
-
# how to get one: no compose file, no script, no DSN written down. The container
|
|
13
|
-
# that existed on the workstation had been created ad hoc in some earlier session
|
|
14
|
-
# and held a scatter of scratch databases. That undocumented setup WAS the
|
|
15
|
-
# fail-open, because a gate nobody can run is not a gate.
|
|
16
|
-
#
|
|
17
|
-
# ## Why freshness, not rebuild-every-time
|
|
18
|
-
#
|
|
19
|
-
# The expensive failure is not a slow rebuild, it is a STALE one. Measured on
|
|
20
|
-
# tabsii-platform while writing this: a database built about an hour earlier,
|
|
21
|
-
# before two PRs merged, produced **23 failures** in a module that had nothing to
|
|
22
|
-
# do with the change in hand. Rebuilt from the same tree it passed 336/336, and
|
|
23
|
-
# passed 336 again on an immediate re-run -- so the lane was genuinely
|
|
24
|
-
# re-runnable and every one of those failures was the old schema.
|
|
25
|
-
#
|
|
26
|
-
# That is the worst shape a local gate can have. Twenty-three red tests that are
|
|
27
|
-
# not your fault teach people the gate is unreliable, and an unreliable gate gets
|
|
28
|
-
# bypassed -- which H4 pre-registered as the condition refuting the whole
|
|
29
|
-
# local-gate programme. So the schema inputs are fingerprinted and a rebuild
|
|
30
|
-
# happens only when they actually changed: reuse ~0.3s, rebuild ~4s.
|
|
31
|
-
#
|
|
32
|
-
# ## Why it is generic
|
|
33
|
-
#
|
|
34
|
-
# It adapts to the repo rather than being told about it, for the same reason
|
|
35
|
-
# `verify.sh` does: forks drift, and a per-instance copy of this would drift from
|
|
36
|
-
# the DDL layout it is meant to build. Everything instance-specific is DERIVED --
|
|
37
|
-
# the schema directories from `db/imports/*/`, the engine image from whether the
|
|
38
|
-
# DDL asks for PostGIS, and the did-it-build threshold from the number of
|
|
39
|
-
# policies the DDL itself declares. Nothing here names a product.
|
|
40
|
-
#
|
|
41
|
-
# ## Usage
|
|
42
|
-
#
|
|
43
|
-
# eval "$(sh scripts/pg-test-db.sh --export)" # export BIFFO_TEST_PG_DSN
|
|
44
|
-
# sh scripts/pg-test-db.sh # print the DSN on stdout
|
|
45
|
-
# sh scripts/pg-test-db.sh --recreate # force a rebuild
|
|
46
|
-
#
|
|
47
|
-
# Only the DSN reaches stdout, so it is safe to capture; progress goes to stderr.
|
|
48
|
-
#
|
|
49
|
-
# Overridable: BIFFO_PG_HOST, BIFFO_PG_PORT, BIFFO_PG_USER, BIFFO_PG_PASSWORD,
|
|
50
|
-
# BIFFO_PG_DB, BIFFO_PG_CONTAINER, BIFFO_PG_IMAGE.
|
|
51
|
-
#
|
|
52
|
-
# ## Concurrency: derived, not coordinated
|
|
53
|
-
#
|
|
54
|
-
# The port, database name, and container name below default to values DERIVED
|
|
55
|
-
# from this checkout's own path, not fixed constants. A fixed port
|
|
56
|
-
# (`BIFFO_PG_PORT` used to default to `55432` everywhere) meant any two
|
|
57
|
-
# checkouts running this script at the same time -- two worktrees of one repo,
|
|
58
|
-
# or two entirely different repos on one machine -- attached to the SAME
|
|
59
|
-
# Postgres cluster and raced on cluster-wide catalogs like `pg_shdepend`
|
|
60
|
-
# (#1114). A fixed database name (`biffo_test`) meant they then shared one
|
|
61
|
-
# database on top of that (#1120).
|
|
62
|
-
#
|
|
63
|
-
# The fix is not a lock: a machine-wide `flock` around this script would make
|
|
64
|
-
# concurrent runs correct by serializing them, but that is coordination, and
|
|
65
|
-
# this estate's shared-mutable-state defects have twice been solved instead by
|
|
66
|
-
# making the value itself unique per user rather than making users take turns.
|
|
67
|
-
# Deriving from `$REPO_ROOT` gets that for free -- it is already different for
|
|
68
|
-
# every worktree and every repo -- and deterministically, so repeat runs
|
|
69
|
-
# against the SAME checkout still land on the same port/db/container and reuse
|
|
70
|
-
# the fingerprinted schema (see step 2 below) instead of rebuilding it under a
|
|
71
|
-
# fresh identity every time. `BIFFO_PG_PORT`, `BIFFO_PG_DB`, and
|
|
72
|
-
# `BIFFO_PG_CONTAINER` remain explicit overrides; only the *default* changed.
|
|
73
|
-
|
|
74
|
-
set -eu
|
|
75
|
-
|
|
76
|
-
REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
|
77
|
-
|
|
78
|
-
# sha256sum is already a dependency of this script (see `fingerprint` below),
|
|
79
|
-
# so reusing it here for a deterministic, cheap per-checkout key adds nothing
|
|
80
|
-
# new to install. 12 hex chars is ample to keep collisions between checkouts
|
|
81
|
-
# on one machine practically impossible while staying short enough to read in
|
|
82
|
-
# `docker ps` output and a psql prompt.
|
|
83
|
-
_checkout_key=$(printf '%s' "$REPO_ROOT" | sha256sum | cut -c1-12)
|
|
84
|
-
_checkout_suffix=$(printf '%s' "$_checkout_key" | cut -c1-8)
|
|
85
|
-
# IANA's dynamic/private port range (49152-65535, 16384 ports) mapped from the
|
|
86
|
-
# next 4 hex chars of the same hash -- an ephemeral port picked deterministically
|
|
87
|
-
# rather than asked of the OS, because a freshly-random port on every invocation
|
|
88
|
-
# would break the reuse this key is for (a second run against the same checkout
|
|
89
|
-
# has to land back on the same container to find its existing database).
|
|
90
|
-
_checkout_port=$((49152 + (0x$(printf '%s' "$_checkout_key" | cut -c9-12) % 16384)))
|
|
91
|
-
|
|
92
|
-
HOST="${BIFFO_PG_HOST:-localhost}"
|
|
93
|
-
PORT="${BIFFO_PG_PORT:-$_checkout_port}"
|
|
94
|
-
USER_="${BIFFO_PG_USER:-postgres}"
|
|
95
|
-
PASS="${BIFFO_PG_PASSWORD:-postgres}"
|
|
96
|
-
DB="${BIFFO_PG_DB:-biffo_test_$_checkout_suffix}"
|
|
97
|
-
# Keyed the same way as PORT and for the same reason: the container is where
|
|
98
|
-
# the port mapping actually lives (`docker run -p "$PORT:5432"`), so if the
|
|
99
|
-
# container name stayed fixed while the port became per-checkout, a second
|
|
100
|
-
# checkout would find the first checkout's container already occupying that
|
|
101
|
-
# name, "start" it rather than create its own, and then poll forever against a
|
|
102
|
-
# port that container was never bound to.
|
|
103
|
-
CONTAINER="${BIFFO_PG_CONTAINER:-biffo-pg-test-$_checkout_suffix}"
|
|
104
|
-
|
|
105
|
-
RECREATE=0
|
|
106
|
-
EXPORT=0
|
|
107
|
-
for arg in "$@"; do
|
|
108
|
-
case "$arg" in
|
|
109
|
-
--recreate) RECREATE=1 ;;
|
|
110
|
-
--export) EXPORT=1 ;;
|
|
111
|
-
-h | --help)
|
|
112
|
-
sed -n '2,72p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'
|
|
113
|
-
exit 0
|
|
114
|
-
;;
|
|
115
|
-
*)
|
|
116
|
-
echo "unknown argument: $arg" >&2
|
|
117
|
-
exit 2
|
|
118
|
-
;;
|
|
119
|
-
esac
|
|
120
|
-
done
|
|
121
|
-
|
|
122
|
-
say() { echo "pg-test-db: $*" >&2; }
|
|
123
|
-
|
|
124
|
-
cd "$REPO_ROOT"
|
|
125
|
-
|
|
126
|
-
# --- what this repo's schema is made of --------------------------------------
|
|
127
|
-
#
|
|
128
|
-
# `db/imports/<name>/*.sql` is the Biffo DDL-import convention that the API's own
|
|
129
|
-
# `ddl_import.list_sql_files` reads at startup, so deriving from it means this
|
|
130
|
-
# script and the running app agree by construction rather than by someone
|
|
131
|
-
# remembering to update both.
|
|
132
|
-
DDL_FILES=$(find db/imports -mindepth 2 -maxdepth 2 -name '*.sql' 2>/dev/null | LC_ALL=C sort || true)
|
|
133
|
-
ALEMBIC_DIR=""
|
|
134
|
-
for _d in services/api .; do
|
|
135
|
-
[ -f "$_d/alembic.ini" ] && ALEMBIC_DIR="$_d" && break
|
|
136
|
-
done
|
|
137
|
-
|
|
138
|
-
if [ -z "$DDL_FILES" ] && [ -z "$ALEMBIC_DIR" ]; then
|
|
139
|
-
say "no db/imports/*/ DDL and no alembic.ini - this repo has no schema to build"
|
|
140
|
-
exit 1
|
|
141
|
-
fi
|
|
142
|
-
|
|
143
|
-
# PostGIS or plain, decided by what the DDL asks for. A plain `postgres` image
|
|
144
|
-
# fails on the first `CREATE EXTENSION postgis`, and picking the heavier image
|
|
145
|
-
# unconditionally would slow every repo that does not need it.
|
|
146
|
-
if [ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs grep -liE 'EXTENSION[[:space:]]+(IF[[:space:]]+NOT[[:space:]]+EXISTS[[:space:]]+)?postgis' >/dev/null 2>&1; then
|
|
147
|
-
IMAGE="${BIFFO_PG_IMAGE:-postgis/postgis:16-3.4}"
|
|
148
|
-
else
|
|
149
|
-
IMAGE="${BIFFO_PG_IMAGE:-postgres:16}"
|
|
150
|
-
fi
|
|
151
|
-
|
|
152
|
-
export PGPASSWORD="$PASS"
|
|
153
|
-
psql_admin() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d postgres "$@"; }
|
|
154
|
-
psql_db() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" "$@"; }
|
|
155
|
-
|
|
156
|
-
# --- 1. a reachable server ---------------------------------------------------
|
|
157
|
-
#
|
|
158
|
-
# Started here rather than assumed, because "docker run one yourself" is exactly
|
|
159
|
-
# the tribal knowledge this script replaces. An already-running server is reused.
|
|
160
|
-
if ! psql_admin -c 'SELECT 1' >/dev/null 2>&1; then
|
|
161
|
-
if ! command -v docker >/dev/null 2>&1; then
|
|
162
|
-
say "no Postgres at $HOST:$PORT and docker is not installed."
|
|
163
|
-
say "Start one and re-run, or set BIFFO_PG_HOST / BIFFO_PG_PORT."
|
|
164
|
-
exit 1
|
|
165
|
-
fi
|
|
166
|
-
if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then
|
|
167
|
-
say "starting existing container $CONTAINER"
|
|
168
|
-
docker start "$CONTAINER" >/dev/null
|
|
169
|
-
else
|
|
170
|
-
say "creating container $CONTAINER ($IMAGE) on port $PORT"
|
|
171
|
-
docker run -d --name "$CONTAINER" \
|
|
172
|
-
-e POSTGRES_PASSWORD="$PASS" -p "$PORT:5432" "$IMAGE" >/dev/null
|
|
173
|
-
fi
|
|
174
|
-
# Polled, not slept: a cold image pull and a warm restart differ by an order of
|
|
175
|
-
# magnitude, and one fixed sleep is wrong for both.
|
|
176
|
-
_waited=0
|
|
177
|
-
until psql_admin -c 'SELECT 1' >/dev/null 2>&1; do
|
|
178
|
-
_waited=$((_waited + 1))
|
|
179
|
-
if [ "$_waited" -gt 90 ]; then
|
|
180
|
-
say "Postgres did not become ready in 90s"
|
|
181
|
-
exit 1
|
|
182
|
-
fi
|
|
183
|
-
sleep 1
|
|
184
|
-
done
|
|
185
|
-
say "Postgres ready after ${_waited}s"
|
|
186
|
-
fi
|
|
187
|
-
|
|
188
|
-
DSN="postgresql+asyncpg://$USER_:$PASS@$HOST:$PORT/$DB"
|
|
189
|
-
emit() {
|
|
190
|
-
if [ "$EXPORT" -eq 1 ]; then
|
|
191
|
-
echo "export BIFFO_TEST_PG_DSN='$DSN'"
|
|
192
|
-
else
|
|
193
|
-
echo "$DSN"
|
|
194
|
-
fi
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
# --- 2. is the existing schema current? --------------------------------------
|
|
198
|
-
#
|
|
199
|
-
# By CONTENT, not mtime: a branch switch changes content and leaves mtime
|
|
200
|
-
# anywhere. Stored inside the database, so it cannot outlive a drop or describe
|
|
201
|
-
# some other database.
|
|
202
|
-
fingerprint() {
|
|
203
|
-
{
|
|
204
|
-
[ -n "$ALEMBIC_DIR" ] && find "$ALEMBIC_DIR" -name '*.py' -path '*alembic*' -type f 2>/dev/null |
|
|
205
|
-
LC_ALL=C sort | xargs cat 2>/dev/null
|
|
206
|
-
[ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs cat 2>/dev/null
|
|
207
|
-
} | sha256sum | cut -d' ' -f1
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
WANT=$(fingerprint)
|
|
211
|
-
HAVE=""
|
|
212
|
-
if [ "$RECREATE" -eq 0 ] &&
|
|
213
|
-
psql_admin -tAc "SELECT 1 FROM pg_database WHERE datname='$DB'" 2>/dev/null | grep -q 1; then
|
|
214
|
-
HAVE=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
215
|
-
-c "SELECT value FROM biffo_pg_test_fingerprint LIMIT 1" 2>/dev/null || true)
|
|
216
|
-
fi
|
|
217
|
-
|
|
218
|
-
if [ -n "$HAVE" ] && [ "$HAVE" = "$WANT" ]; then
|
|
219
|
-
say "schema is current, reusing $DB"
|
|
220
|
-
emit
|
|
221
|
-
exit 0
|
|
222
|
-
fi
|
|
223
|
-
|
|
224
|
-
[ -n "$HAVE" ] && say "schema inputs changed - rebuilding rather than serving a stale schema"
|
|
225
|
-
|
|
226
|
-
# --- 3. rebuild the way the app and CI do ------------------------------------
|
|
227
|
-
say "rebuilding $DB"
|
|
228
|
-
psql_admin -c "DROP DATABASE IF EXISTS $DB WITH (FORCE)" >/dev/null
|
|
229
|
-
psql_admin -c "CREATE DATABASE $DB" >/dev/null
|
|
230
|
-
|
|
231
|
-
if [ -n "$ALEMBIC_DIR" ]; then
|
|
232
|
-
BIFFO_DATABASE_URL="$DSN" uv run --directory "$ALEMBIC_DIR" alembic upgrade head >/dev/null
|
|
233
|
-
say "alembic upgrade head"
|
|
234
|
-
fi
|
|
235
|
-
|
|
236
|
-
if [ -n "$DDL_FILES" ]; then
|
|
237
|
-
# ONE psql session, sorted by filename, mirroring the API's own DDL import.
|
|
238
|
-
# Session state an early module sets -- typically `SET search_path` in the
|
|
239
|
-
# first file -- has to survive into later ones, so a per-file connection would
|
|
240
|
-
# silently change the meaning of every unqualified name after it. LC_ALL=C
|
|
241
|
-
# keeps the shell's sort byte-ordered to match Python's.
|
|
242
|
-
# shellcheck disable=SC2046
|
|
243
|
-
psql -q -v ON_ERROR_STOP=1 -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
244
|
-
--single-transaction $(echo "$DDL_FILES" | sed 's/^/-f /' | tr '\n' ' ') >/dev/null
|
|
245
|
-
say "$(echo "$DDL_FILES" | wc -l | tr -d ' ') DDL modules applied"
|
|
246
|
-
fi
|
|
247
|
-
|
|
248
|
-
# --- 4. refuse to bless a half-built schema ----------------------------------
|
|
249
|
-
#
|
|
250
|
-
# The threshold is derived, not guessed: count the policies the DDL declares and
|
|
251
|
-
# require the database to hold at least half. Recording a fingerprint against a
|
|
252
|
-
# partial schema is worse than failing, because the NEXT run would trust it and
|
|
253
|
-
# every failure after that would look like the developer's own change.
|
|
254
|
-
if [ -n "$DDL_FILES" ]; then
|
|
255
|
-
_declared=$(echo "$DDL_FILES" | xargs grep -ciE '^[[:space:]]*CREATE[[:space:]]+POLICY' 2>/dev/null |
|
|
256
|
-
awk -F: '{s+=$NF} END {print s+0}')
|
|
257
|
-
if [ "${_declared:-0}" -gt 0 ]; then
|
|
258
|
-
_actual=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
259
|
-
-c "SELECT count(*) FROM pg_policies" 2>/dev/null || echo 0)
|
|
260
|
-
if [ "${_actual:-0}" -lt $((_declared / 2)) ]; then
|
|
261
|
-
say "only ${_actual:-0} policies present against $_declared declared - the schema did not build."
|
|
262
|
-
say "Not recording a fingerprint; fix the DDL and re-run."
|
|
263
|
-
exit 1
|
|
264
|
-
fi
|
|
265
|
-
say "$_actual RLS policies ($_declared declared)"
|
|
266
|
-
fi
|
|
267
|
-
fi
|
|
268
|
-
|
|
269
|
-
psql_db \
|
|
270
|
-
-c "CREATE TABLE IF NOT EXISTS biffo_pg_test_fingerprint (value text primary key)" \
|
|
271
|
-
-c "TRUNCATE biffo_pg_test_fingerprint" \
|
|
272
|
-
-c "INSERT INTO biffo_pg_test_fingerprint (value) VALUES ('$WANT')" >/dev/null
|
|
273
|
-
|
|
274
|
-
say "ready"
|
|
275
|
-
emit
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env sh
|
|
2
|
-
# Refuse a force-push that has quietly started touching a file somebody else
|
|
3
|
-
# just changed.
|
|
4
|
-
#
|
|
5
|
-
# ## The incident (tabsii-platform#446, 2026-07-31)
|
|
6
|
-
#
|
|
7
|
-
# A branch had to be rebuilt to purge a Secret Scan finding from its history.
|
|
8
|
-
# The rebuild used:
|
|
9
|
-
#
|
|
10
|
-
# git reset --soft origin/dev && git add -A
|
|
11
|
-
#
|
|
12
|
-
# `origin/dev` had advanced by two merged PRs since the branch was cut. So
|
|
13
|
-
# `reset --soft` moved HEAD onto the NEWER dev while the working tree still
|
|
14
|
-
# held the OLDER copy of a doc those PRs had edited — and `git add -A`
|
|
15
|
-
# faithfully staged the difference. The resulting commit, whose subject was
|
|
16
|
-
# about RLS policies, silently REVERTED four lines of somebody else's merged
|
|
17
|
-
# work.
|
|
18
|
-
#
|
|
19
|
-
# Nothing local saw it: `verify.sh` passed, 2884 tests passed, the real-Postgres
|
|
20
|
-
# lane passed twice. It surfaced only because GitHub reported the PR as
|
|
21
|
-
# CONFLICTING and somebody went looking for why. **Had those two PRs touched any
|
|
22
|
-
# other file, there would have been no conflict and the revert would have
|
|
23
|
-
# merged.** A conflict is not a detector; it is the lucky case.
|
|
24
|
-
#
|
|
25
|
-
# ## What this checks, and why it is narrow
|
|
26
|
-
#
|
|
27
|
-
# It fires only on the intersection of two conditions, because either alone is
|
|
28
|
-
# far too noisy to be a gate:
|
|
29
|
-
#
|
|
30
|
-
# 1. The push is a REWRITE — the remote tip is not an ancestor of what you are
|
|
31
|
-
# pushing. An ordinary fast-forward push cannot have this problem.
|
|
32
|
-
# 2. A file the branch changes would be written back to content the
|
|
33
|
-
# integration branch ALREADY HAD earlier in its history. Not new work: an
|
|
34
|
-
# older copy.
|
|
35
|
-
#
|
|
36
|
-
# Condition 2 is the accidental-revert signature exactly, and it is the second
|
|
37
|
-
# attempt. The first asked "has the integration branch moved this file since my
|
|
38
|
-
# base?", which sounds equivalent and is not — `git reset --soft origin/dev`
|
|
39
|
-
# makes your base the integration tip, so the answer is always no. **That
|
|
40
|
-
# version scored a clean pass on a faithful reconstruction of the incident
|
|
41
|
-
# above.** `cli/src/lib/rewrite-scope-check.test.ts` reinstates it to prove the
|
|
42
|
-
# tests can tell the two apart.
|
|
43
|
-
#
|
|
44
|
-
# What stays silent, deliberately: amending to add a file nobody else has
|
|
45
|
-
# touched; rebasing onto a newer integration branch; two branches making
|
|
46
|
-
# genuinely different edits to one shared file. Only a STALE copy is a bug.
|
|
47
|
-
#
|
|
48
|
-
# ## What it deliberately does NOT do
|
|
49
|
-
#
|
|
50
|
-
# It does not try to decide whether a change is "intended". That needs intent,
|
|
51
|
-
# which no gate has. It answers a narrower question answerable from history
|
|
52
|
-
# alone: *is the content I am about to push actually older content?*
|
|
53
|
-
#
|
|
54
|
-
# It also never fails the push because it could not run. A guard that blocks
|
|
55
|
-
# work when its own inputs are missing gets disabled, and then it protects
|
|
56
|
-
# nothing. When it cannot answer it says so, loudly, and exits 0 — the same
|
|
57
|
-
# contract `verify.sh` uses for a check whose tool is absent. Absence is
|
|
58
|
-
# reported, never mistaken for a clean result.
|
|
59
|
-
|
|
60
|
-
set -u
|
|
61
|
-
|
|
62
|
-
ZERO=0000000000000000000000000000000000000000
|
|
63
|
-
ESCAPE_HATCH=BIFFO_ALLOW_SCOPE_CHANGE
|
|
64
|
-
|
|
65
|
-
note() { printf 'rewrite-scope: %s\n' "$1" >&2; }
|
|
66
|
-
|
|
67
|
-
# The integration branch every Biffo repo uses (AGENTS.md §2). Resolved rather
|
|
68
|
-
# than assumed: a repo whose remote HEAD says otherwise is a repo this check
|
|
69
|
-
# should not guess about.
|
|
70
|
-
integration=""
|
|
71
|
-
for candidate in dev main master; do
|
|
72
|
-
if git rev-parse --verify --quiet "refs/remotes/origin/$candidate" >/dev/null 2>&1; then
|
|
73
|
-
integration="origin/$candidate"
|
|
74
|
-
break
|
|
75
|
-
fi
|
|
76
|
-
done
|
|
77
|
-
|
|
78
|
-
if [ -z "$integration" ]; then
|
|
79
|
-
note "no origin/dev, origin/main or origin/master — check did NOT run"
|
|
80
|
-
exit 0
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
# The inner filter below runs in a subshell (it is on the right of a pipe), so
|
|
84
|
-
# it cannot assign to a variable in this shell. Collect through a file instead.
|
|
85
|
-
FINDINGS_FILE=$(mktemp) || {
|
|
86
|
-
note "could not create a temp file — check did NOT run"
|
|
87
|
-
exit 0
|
|
88
|
-
}
|
|
89
|
-
trap 'rm -f "$FINDINGS_FILE"' EXIT HUP INT TERM
|
|
90
|
-
|
|
91
|
-
# git feeds pre-push one line per ref: <local ref> <local sha> <remote ref> <remote sha>
|
|
92
|
-
while read -r _local_ref local_sha _remote_ref remote_sha; do
|
|
93
|
-
# Branch deletion, or nothing to push.
|
|
94
|
-
[ "$local_sha" = "$ZERO" ] && continue
|
|
95
|
-
# Brand-new remote branch: there is no previous scope to compare against.
|
|
96
|
-
[ "$remote_sha" = "$ZERO" ] && continue
|
|
97
|
-
# The remote may not have the old object locally (shallow clone, pruned).
|
|
98
|
-
git cat-file -e "$remote_sha^{commit}" 2>/dev/null || {
|
|
99
|
-
note "remote tip $remote_sha not available locally — check did NOT run for this ref"
|
|
100
|
-
continue
|
|
101
|
-
}
|
|
102
|
-
# Fast-forward: not a rewrite, cannot exhibit this defect.
|
|
103
|
-
if git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
|
|
104
|
-
continue
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
new_base=$(git merge-base "$local_sha" "$integration" 2>/dev/null) || continue
|
|
108
|
-
old_base=$(git merge-base "$remote_sha" "$integration" 2>/dev/null) || continue
|
|
109
|
-
|
|
110
|
-
# Every file this branch claims to change, relative to where it forked.
|
|
111
|
-
#
|
|
112
|
-
# Iterated with `while read`, not `for file in $scope`: word splitting would
|
|
113
|
-
# break any path containing a space, and a guard that silently skips the file
|
|
114
|
-
# it should have flagged is worse than no guard. Process substitution
|
|
115
|
-
# (`grep -f <(...)`) is avoided for the same class of reason — it is a
|
|
116
|
-
# bashism, and this runs under `sh` from the pre-push hook.
|
|
117
|
-
git diff --name-only "$new_base" "$local_sha" 2>/dev/null | while IFS= read -r file; do
|
|
118
|
-
[ -z "$file" ] && continue
|
|
119
|
-
|
|
120
|
-
mine=$(git rev-parse --quiet --verify "$local_sha:$file" 2>/dev/null) || continue
|
|
121
|
-
theirs=$(git rev-parse --quiet --verify "$integration:$file" 2>/dev/null) || continue
|
|
122
|
-
# Agreeing with the integration branch is the normal, safe case.
|
|
123
|
-
[ "$mine" = "$theirs" ] && continue
|
|
124
|
-
|
|
125
|
-
# The signature of an accidental revert: the content you are about to push
|
|
126
|
-
# is not new work, it is a version this file ALREADY HAD earlier in the
|
|
127
|
-
# integration branch's history.
|
|
128
|
-
#
|
|
129
|
-
# This is what makes the check work at all. The first version asked "has
|
|
130
|
-
# the integration branch moved this file since my base?", which sounds
|
|
131
|
-
# equivalent and is not: `git reset --soft origin/dev` makes your base the
|
|
132
|
-
# integration tip, so the answer is always no and the guard never fires.
|
|
133
|
-
# Verified against a reconstruction of tabsii-platform#446 — the earlier
|
|
134
|
-
# condition scored a clean pass on the exact commit that reverted two
|
|
135
|
-
# merged PRs.
|
|
136
|
-
#
|
|
137
|
-
# Bounded to recent history: an unbounded walk on a long-lived file costs
|
|
138
|
-
# more than a pre-push gate should, and a stale copy older than this is not
|
|
139
|
-
# the accident this catches.
|
|
140
|
-
git log --format=%H -n 200 "$integration" -- "$file" 2>/dev/null | while IFS= read -r commit; do
|
|
141
|
-
[ -z "$commit" ] && continue
|
|
142
|
-
past=$(git rev-parse --quiet --verify "$commit:$file" 2>/dev/null) || continue
|
|
143
|
-
if [ "$past" = "$mine" ]; then
|
|
144
|
-
printf '%s\n' "$file"
|
|
145
|
-
break
|
|
146
|
-
fi
|
|
147
|
-
done
|
|
148
|
-
done >>"$FINDINGS_FILE"
|
|
149
|
-
done
|
|
150
|
-
|
|
151
|
-
findings=$(cat "$FINDINGS_FILE" 2>/dev/null)
|
|
152
|
-
|
|
153
|
-
[ -z "$findings" ] && exit 0
|
|
154
|
-
|
|
155
|
-
if [ -n "${BIFFO_ALLOW_SCOPE_CHANGE:-}" ]; then
|
|
156
|
-
note "scope change allowed via $ESCAPE_HATCH:"
|
|
157
|
-
printf '%s' "$findings" | sed 's/^/ /' >&2
|
|
158
|
-
exit 0
|
|
159
|
-
fi
|
|
160
|
-
|
|
161
|
-
cat >&2 <<EOF
|
|
162
|
-
|
|
163
|
-
This force-push would write a version of these file(s) that $integration
|
|
164
|
-
ALREADY HAD earlier in its history — not new work, an older copy:
|
|
165
|
-
|
|
166
|
-
$(printf '%s' "$findings" | sed 's/^/ /')
|
|
167
|
-
|
|
168
|
-
That is the signature of an accidental revert. A rewrite (rebase, amend,
|
|
169
|
-
reset --soft) picked up a stale copy of somebody else's merged work and
|
|
170
|
-
recorded it as your change. It merges silently unless it happens to
|
|
171
|
-
conflict, and no test can see it — the content is valid, just old.
|
|
172
|
-
|
|
173
|
-
Check what you are actually about to write:
|
|
174
|
-
|
|
175
|
-
git diff $integration...HEAD -- <file>
|
|
176
|
-
|
|
177
|
-
If it is intended, push again with:
|
|
178
|
-
|
|
179
|
-
$ESCAPE_HATCH=1 git push ...
|
|
180
|
-
|
|
181
|
-
If it is not: restore the file and amend —
|
|
182
|
-
|
|
183
|
-
git checkout $integration -- <file> && git commit --amend --no-edit
|
|
184
|
-
|
|
185
|
-
Rebuilding a branch? Reset to a FIXED SHA (the branch point), never to a
|
|
186
|
-
moving ref like $integration.
|
|
187
|
-
|
|
188
|
-
EOF
|
|
189
|
-
exit 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|