Skip to content

Commit da28b62

Browse files
claude[bot]claude
andauthored
fix(pm): drop the phantom # from os-regen-merge.sh's .gitattributes reader (#15700)
`grep 'merge=os-regen' .gitattributes | awk '{print $1}'` also matched the header comment that quotes the literal in prose and took ITS first field, so the run-time path list carried a pathspec that was literally `#`. Harmless by luck — `#` matches no tracked path, so both `git diff --name-only … -- "${regen_paths[@]}"` calls ignored it — but the pattern COUNT printed to the operator is the only check that the script is reading the right surface, and it was off by one in the one place the design deliberately keeps no second copy. Measured on origin/main 95d5cbb: 19 entries produced, 1 literally `#`, 18 real patterns. The repaired reader answers 18, matching both the 18 rows in the file and `scripts/git-merge-regen.mjs`'s independent reader. The grammar was measured, not assumed (git 2.43.0, scratch repos): a leading-`#` line is a comment even when indented, and there is NO inline trailing comment — git rejects such a row whole and routes nothing. Hence comment lines are dropped before the field split, and `merge=os-regen` is anchored as a whitespace- delimited token. Pinned in the script's own `--self-test`: a bait `.gitattributes` with prose quoting the literal plus three real rows, asserting the exact printed COUNT (so an empty population reds too), the git-grammar measurement with a firing control, and a discriminating mutation that disables the comment skip and watches the phantom come back. Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk Co-authored-by: Claude <noreply@anthropic.com>
1 parent a5cef37 commit da28b62

1 file changed

Lines changed: 132 additions & 1 deletion

File tree

scripts/pm/os-regen-merge.sh

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,51 @@ mode_run() {
213213
# the form #12142 standardised; its own note attributes the trap to the empty
214214
# list, which measures clean — an all-empty read leaves the body unexecuted
215215
# and the `while` at status 0.)
216+
#
217+
# The READER admits only what git itself routes, and .gitattributes' grammar
218+
# was MEASURED for this rather than assumed (git 2.43.0, scratch repos):
219+
#
220+
# a leading-`#` line is a COMMENT, and leading whitespace does not undo
221+
# that — ` # foo bar` assigns nothing to a file named `#`, while the
222+
# escaped-pattern control `\# foo bar` on that same file assigns `foo` and
223+
# `bar`. So comment lines must be dropped BEFORE the field split.
224+
#
225+
# there is NO inline trailing comment. `x merge=os-regen # note` does not
226+
# route x with a note on the end; git rejects the LINE — `# is not a valid
227+
# attribute name: .gitattributes:1` on stderr, and `git check-attr merge --
228+
# x` then answers `unspecified`. Nothing to strip, because no such row can
229+
# ever be live. Pinned in the self-test against real git, so the day git
230+
# grows one this decision reddens instead of rotting.
231+
#
232+
# attributes are WHITESPACE-delimited tokens, so `merge=os-regenX` is a
233+
# different driver and not this one — hence the token anchor below rather
234+
# than a bare substring.
235+
#
236+
# The spelling this replaced was `grep 'merge=os-regen' .gitattributes | awk
237+
# '{print $1}'`, which also matched the header comment that quotes the
238+
# literal in prose and took ITS first field, so the list carried a pathspec
239+
# that was literally `#`. Harmless by luck — `#` matches no tracked path, so
240+
# both `git diff`s below ignored it — but the COUNT printed just below is the
241+
# operator's only check that this script is looking at the right surface, and
242+
# it was off by one in the one place the design deliberately keeps no second
243+
# copy to compare against. Measured at the repair on origin/main: 19 entries
244+
# produced, 1 of them literally `#`, 18 real patterns. It had already been
245+
# wrong at two different values (18 then 19) across an intervening edit, so
246+
# the impurity is in the construction, not in one snapshot of the file.
247+
#
248+
# `scripts/git-merge-regen.mjs`'s own reader (`reconcileAttributes`) already
249+
# skipped comment lines; this is that same rule, in awk.
250+
#
251+
# `[ \t]` rather than `[[:space:]]`: the bash 3.2 floor above is a macOS
252+
# floor, and that host's awk is not gawk.
216253
regen_paths=()
217254
regen_line=''
218255
while IFS= read -r regen_line; do
219256
if [[ -n "$regen_line" ]]; then regen_paths+=("$regen_line"); fi
220-
done < <(grep 'merge=os-regen' .gitattributes | awk '{print $1}')
257+
done < <(awk '
258+
/^[ \t]*#/ { next }
259+
/(^|[ \t])merge=os-regen([ \t]|$)/ { print $1 }
260+
' .gitattributes)
221261
if [ "${#regen_paths[@]}" -eq 0 ]; then
222262
echo "✗ no merge=os-regen entries found in .gitattributes — refusing to guess" >&2
223263
exit 1
@@ -558,6 +598,32 @@ DRIVER
558598
git fetch -q origin main
559599
}
560600

601+
# Build the standard fixture in $1, then replace its .gitattributes with one
602+
# shaped like the REAL file: prose that quotes the literal `merge=os-regen`,
603+
# and exactly THREE real rows. Leaves the cwd in the clone, on `feature`, with
604+
# the new routing committed.
605+
#
606+
# Two prose shapes, on purpose, because they fail the reader differently:
607+
# the header line quotes the literal in BACKTICKS — caught by the token
608+
# anchor even with comment-skipping off;
609+
# the indented line mentions it whitespace-delimited — caught ONLY by
610+
# comment-skipping, which is what makes it the discriminator for 8b.
611+
# The real .gitattributes carries the backticked shape (its line 36); the
612+
# indented one is the near neighbour that a future edit can add for free.
613+
st_fixture_list_bait() {
614+
st_fixture "$1" >/dev/null 2>&1
615+
cat > .gitattributes <<'ATTRS'
616+
# Routed generated artifacts. `merge=os-regen` hands these to the driver.
617+
#
618+
# an indented comment that also mentions merge=os-regen in prose
619+
gen/** merge=os-regen
620+
docs/generated-guide.md merge=os-regen
621+
data/*.json merge=os-regen
622+
ATTRS
623+
git add -A
624+
git commit -qm 'route three paths, with prose that quotes the literal'
625+
}
626+
561627
mode_self_test() {
562628
tmp="$(mktemp -d "${TMPDIR:-/tmp}/os-regen-merge-selftest.XXXXXX")"
563629
trap 'rm -rf "$tmp"' EXIT INT TERM
@@ -737,6 +803,71 @@ mode_self_test() {
737803
"$(printf '%s' "$out" | grep -c 'Do not resolve generated files textually' || true)" 0
738804
cd "$here"
739805

806+
# --- 8. THE LIST ITSELF. The printed pattern count is the operator's ONLY
807+
# check that this script is reading the right surface — the header
808+
# says so, and says why there is deliberately no second copy to check
809+
# it against. So it is pinned here, against a fixture .gitattributes
810+
# carrying the prose shape that used to enter the list as a pathspec
811+
# literally `#`.
812+
#
813+
# The assertion is an EXACT COUNT, never "no entry equals `#`": a
814+
# count also reds the day the read silently matches ZERO lines, which
815+
# an absence assertion passes with flying colours.
816+
st_fixture_list_bait "$tmp/h"
817+
out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$?
818+
st_case 'a run over the bait .gitattributes exits 0' "$rc" 0
819+
st_case 'the pattern count counts ROWS, not prose that quotes the literal' \
820+
"$(printf '%s' "$out" | sed -n 's/^→ os-regen paths (from .gitattributes, \([0-9]*\) patterns):$/\1/p')" 3
821+
st_case 'and no phantom # pathspec is listed' \
822+
"$(printf '%s' "$out" | grep -c '^ #$' || true)" 0
823+
st_case 'and the three real rows all are' \
824+
"$(printf '%s' "$out" | grep -cE '^ (gen/[*][*]|docs/generated-guide[.]md|data/[*][.]json)$' || true)" 3
825+
# The fixture really is bait: the pre-repair spelling over-counts it by two.
826+
st_case 'the pre-repair spelling over-counts the same file (fixture is bait)' \
827+
"$(grep 'merge=os-regen' .gitattributes | awk '{print $1}' | wc -l | tr -d ' ')" 5
828+
cd "$here"
829+
830+
# --- 8a. THE GRAMMAR the reader was decided against, pinned against REAL git
831+
# rather than assumed. .gitattributes has no inline trailing comment:
832+
# git rejects the whole row and routes nothing. That measurement is
833+
# why the reader strips comment LINES only and does not try to trim a
834+
# trailing `#`. If git ever grows one, this reddens and the decision
835+
# gets revisited instead of rotting.
836+
st_fixture "$tmp/i" >/dev/null 2>&1
837+
printf 'inline.txt merge=os-regen # trailing note\n' > .gitattributes
838+
: > inline.txt
839+
st_case 'git does NOT admit an inline trailing comment — the row routes nothing' \
840+
"$(git check-attr merge -- inline.txt 2>/dev/null)" 'inline.txt: merge: unspecified'
841+
st_case 'and names # as the invalid attribute name' \
842+
"$(git check-attr merge -- inline.txt 2>&1 >/dev/null | grep -c 'is not a valid attribute name' || true)" 1
843+
# Firing control for the two absences above: the same row WITHOUT the trailing
844+
# comment does route. Without it, a git that stopped reading .gitattributes at
845+
# all would pass both cases above.
846+
printf 'inline.txt merge=os-regen\n' > .gitattributes
847+
st_case 'control: the same row without the note DOES route' \
848+
"$(git check-attr merge -- inline.txt 2>/dev/null)" 'inline.txt: merge: os-regen'
849+
cd "$here"
850+
851+
# --- 8b. THE DISCRIMINATING MUTATION for case 8. Disable the reader's
852+
# comment-line skip and watch the count come back one too high — the
853+
# phantom is exactly what that rule removes. Same perl/\Q..\E literal
854+
# replacement 6b uses, keyed off the awk rule rather than a message.
855+
mutated_list="$tmp/mutated-list-os-regen-merge.sh"
856+
MUT_ANCHOR=' /^[ \t]*#/ { next }' \
857+
MUT_INSERT=' /^[ \t]*#ZZ-NEVER-MATCHES-ZZ/ { next }' \
858+
perl -0777 -pe 's/\Q$ENV{MUT_ANCHOR}\E/$ENV{MUT_INSERT}/' "$SELF" > "$mutated_list"
859+
st_case 'the list mutation actually changed the script text' \
860+
"$(diff -q "$SELF" "$mutated_list" >/dev/null 2>&1; echo $?)" 1
861+
st_case 'and the mutated script still parses' \
862+
"$(bash -n "$mutated_list" >/dev/null 2>&1; echo $?)" 0
863+
st_fixture_list_bait "$tmp/h-mutated"
864+
mut_out="$(bash "$mutated_list" 2>&1)" && mut_rc=0 || mut_rc=$?
865+
st_case 'mutated: the phantom # is back in the list (proves case 8 bites)' \
866+
"$(printf '%s' "$mut_out" | grep -c '^ #$' || true)" 1
867+
st_case 'mutated: and the count is one too high' \
868+
"$(printf '%s' "$mut_out" | sed -n 's/^→ os-regen paths (from .gitattributes, \([0-9]*\) patterns):$/\1/p')" 4
869+
cd "$here"
870+
740871
if [ "$st_fail" -ne 0 ]; then
741872
printf '✗ os-regen-merge self-test: %d case(s) failed.\n' "$st_fail"
742873
return 1

0 commit comments

Comments
 (0)