tinymux/hooks/pre-commit

142 lines
4.2 KiB
Text
Raw Permalink Normal View History

#!/bin/bash
#
# Pre-commit hook: warn when committing generated files without their sources.
# This catches the common mistake of editing generated output instead of the
# real source (e.g., editing art_scan.cpp instead of art_scan.rl).
#
# Uses parallel arrays (not associative arrays) for bash 3.2 compatibility,
# since macOS still ships /bin/bash as 3.2.
build(ragel): strip #line directives so the outputs are canonical (#1950) Ragel's #line values depend on the Ragel build rather than on our source, so regenerating with a different Ragel rewrites dozens of #line-only lines and leaves the tree dirty after any pull that touches a .rl. That is not cosmetic: dounix.sh packages from the working tree, not from a git export, so the churn ships -- into the tarball and into the generated .patch.gz. Stripping the directives makes each output a function of its .rl alone. Same input, same file, every box. The cost is that a debugger reports positions in the generated .c rather than the .rl, which is the accepted trade for an artifact that can be compared by hash. Applied to all five Ragel outputs, not the four in docs/generated-files.md -- date_scan.cpp is also Ragel-generated and had 102 directives. color_ops.c 194 art_scan.cpp 24 date_scan.cpp 102 ast_scan.cpp 43 muxescape.cpp 16 total 379 sed rather than sed -i: the -i spelling differs between GNU and BSD, and this has to run on Linux, macOS and FreeBSD. Deleting whole lines is safe here -- all 379 directives are anchored at column 0 and none carries trailing code, both checked before the change. Makefile.in is hand-edited to match rather than regenerated: this box has automake 1.18.1 and the tree's Makefile.in came from 1.16.5, so autoreconf would churn whole files for a five-line change (#1477). The inserted lines are identical to the Makefile.am ones. The pre-commit hook needed teaching, and this commit is the example: it rejects a generated file staged without its source, but changing how a file is generated rewrites the output while the .rl stays untouched. The build rule now counts as a source. Verified all three ways -- a bare generated file is still blocked, and staging it with either its .rl or its Makefile.am is accepted. Verified on macOS/arm64: - regeneration is idempotent: all five byte-identical across two runs - the generated diffs are 379 deletions, every one a #line, nothing added - outputs return to mode 444, no .tmp files left behind - make test 35 passed / 1 skipped / 0 failed (jit=yes stubslave=no nls=yes realitylvls=yes wodrealms=yes) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:21:47 -06:00
# Generated file (index N) maps to source file at GENERATED_SRCS[N], and
# optionally to the build rule that produces it at GENERATED_RULES[N].
# Use "NEVER_EDIT" for third-party vendored files that should not be edited.
build(ragel): strip #line directives so the outputs are canonical (#1950) Ragel's #line values depend on the Ragel build rather than on our source, so regenerating with a different Ragel rewrites dozens of #line-only lines and leaves the tree dirty after any pull that touches a .rl. That is not cosmetic: dounix.sh packages from the working tree, not from a git export, so the churn ships -- into the tarball and into the generated .patch.gz. Stripping the directives makes each output a function of its .rl alone. Same input, same file, every box. The cost is that a debugger reports positions in the generated .c rather than the .rl, which is the accepted trade for an artifact that can be compared by hash. Applied to all five Ragel outputs, not the four in docs/generated-files.md -- date_scan.cpp is also Ragel-generated and had 102 directives. color_ops.c 194 art_scan.cpp 24 date_scan.cpp 102 ast_scan.cpp 43 muxescape.cpp 16 total 379 sed rather than sed -i: the -i spelling differs between GNU and BSD, and this has to run on Linux, macOS and FreeBSD. Deleting whole lines is safe here -- all 379 directives are anchored at column 0 and none carries trailing code, both checked before the change. Makefile.in is hand-edited to match rather than regenerated: this box has automake 1.18.1 and the tree's Makefile.in came from 1.16.5, so autoreconf would churn whole files for a five-line change (#1477). The inserted lines are identical to the Makefile.am ones. The pre-commit hook needed teaching, and this commit is the example: it rejects a generated file staged without its source, but changing how a file is generated rewrites the output while the .rl stays untouched. The build rule now counts as a source. Verified all three ways -- a bare generated file is still blocked, and staging it with either its .rl or its Makefile.am is accepted. Verified on macOS/arm64: - regeneration is idempotent: all five byte-identical across two runs - the generated diffs are 379 deletions, every one a #line, nothing added - outputs return to mode 444, no .tmp files left behind - make test 35 passed / 1 skipped / 0 failed (jit=yes stubslave=no nls=yes realitylvls=yes wodrealms=yes) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:21:47 -06:00
#
# The rule counts as a source (#1950). Changing how a file is generated --
# a ragel flag, the #line strip -- legitimately rewrites the output while the
# .rl is untouched, and without this the hook rejects exactly the commits it
# should be happiest about. Leave the entry empty where there is no single
# rule file.
GENERATED_GENS=(
# Ragel scanners
"mux/modules/engine/art_scan.cpp"
"mux/modules/engine/ast_scan.cpp"
"mux/lib/color_ops.c"
build(ragel): guard date_scan and strip #line from the remaining outputs (#2029) Two remainders from #2025. ## date_scan.cpp was unguarded hooks/pre-commit knew about four Ragel outputs; mux/lib/date_scan.cpp was absent from GENERATED_GENS entirely, so it could be hand-edited and committed without its .rl and nothing objected -- while being listed in docs/generated-files.md and shipped via unix/TOC.patchable. Added to all three parallel arrays, which now hold 27 each. Verified by alignment rather than by count: equal lengths prove nothing about correspondence, so the check confirms every ragel triple's basenames match and its rule lives in the same directory as its output. Then exercised: staging date_scan.cpp alone is now blocked, naming date_scan.rl as its source. ## Six more outputs carried #line #2025's scope was right for what the mux build regenerates and dounix.sh ships, but these have live rules of their own and could still dirty a tree: testcases/tools/unformat.c 28 testcases/tools/Makefile (%.c: %.rl) testcases/tools/reformat.c 23 same ragel/trigger_match.c 16 ragel/Makefile tools/ansify/ansify.c 19 tools/ansify/Makefile (make regen) client/tf/src/script_lex.cpp 67 client/tf/CMakeLists.txt client/tf/src/input_lex.cpp 62 same The issue reported "no rule found" for trigger_match.c and ansify.c. Both were wrong: ragel/Makefile:25 generates trigger_match.c on dependency, and tools/ansify/Makefile has an explicit `regen` target. Neither is inert -- ansify's is opt-in rather than implicit, which is why it reads as absent. Each regenerated through its OWN rule rather than by running sed by hand, so the rule is what is under test. All six are idempotent across a second regeneration, and the diffs are 215 deletions, every one a #line, nothing added. ragel/color_ops.c is generated from the same ../mux/lib/color_ops.rl as the shipped copy but is not tracked, so it could not dirty anything; its rule gets the strip anyway, so the two generations of one source cannot disagree if it is ever checked in. client/tf could not be built here -- src/regex_utils.h needs pcre2.h, which is not on that target's include path on macOS -- but that is a pre-existing gap unrelated to this change. The Ragel custom commands themselves DID run (cmake configure succeeds once NCURSESW_LIB is pointed at a macOS ncurses), and both outputs came back stripped, so the CMake edit is exercised rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 00:59:11 -06:00
"mux/lib/date_scan.cpp"
"mux/muxescape/muxescape.cpp"
# Unicode tables (source is the utf/ pipeline, not a single file)
"mux/include/utf8tables.h"
"mux/lib/utf8tables.cpp"
"mux/include/unicode_tables_c.h"
"mux/lib/unicode_tables.c"
"mux/include/ducet_cetable.h"
"mux/rv64/src/unicode_tables.c"
# Autoconf
"mux/configure"
"mux/aclocal.m4"
# Protocol Buffers
"client/console/src/hydra.pb.h"
"client/console/src/hydra.pb.cc"
"client/console/src/hydra.grpc.pb.h"
"client/console/src/hydra.grpc.pb.cc"
"client/win32gui/src/hydra.pb.h"
"client/win32gui/src/hydra.pb.cc"
"client/win32gui/src/hydra.grpc.pb.h"
"client/win32gui/src/hydra.grpc.pb.cc"
"client/tf/src/proto/hydra.pb.h"
"client/tf/src/proto/hydra.pb.cc"
"client/tf/src/proto/hydra.grpc.pb.h"
"client/tf/src/proto/hydra.grpc.pb.cc"
# SQLite amalgamation (third-party)
"mux/sqlite/sqlite3.c"
"mux/sqlite/sqlite3.h"
)
GENERATED_SRCS=(
"mux/modules/engine/art_scan.rl"
"mux/modules/engine/ast_scan.rl"
"mux/lib/color_ops.rl"
build(ragel): guard date_scan and strip #line from the remaining outputs (#2029) Two remainders from #2025. ## date_scan.cpp was unguarded hooks/pre-commit knew about four Ragel outputs; mux/lib/date_scan.cpp was absent from GENERATED_GENS entirely, so it could be hand-edited and committed without its .rl and nothing objected -- while being listed in docs/generated-files.md and shipped via unix/TOC.patchable. Added to all three parallel arrays, which now hold 27 each. Verified by alignment rather than by count: equal lengths prove nothing about correspondence, so the check confirms every ragel triple's basenames match and its rule lives in the same directory as its output. Then exercised: staging date_scan.cpp alone is now blocked, naming date_scan.rl as its source. ## Six more outputs carried #line #2025's scope was right for what the mux build regenerates and dounix.sh ships, but these have live rules of their own and could still dirty a tree: testcases/tools/unformat.c 28 testcases/tools/Makefile (%.c: %.rl) testcases/tools/reformat.c 23 same ragel/trigger_match.c 16 ragel/Makefile tools/ansify/ansify.c 19 tools/ansify/Makefile (make regen) client/tf/src/script_lex.cpp 67 client/tf/CMakeLists.txt client/tf/src/input_lex.cpp 62 same The issue reported "no rule found" for trigger_match.c and ansify.c. Both were wrong: ragel/Makefile:25 generates trigger_match.c on dependency, and tools/ansify/Makefile has an explicit `regen` target. Neither is inert -- ansify's is opt-in rather than implicit, which is why it reads as absent. Each regenerated through its OWN rule rather than by running sed by hand, so the rule is what is under test. All six are idempotent across a second regeneration, and the diffs are 215 deletions, every one a #line, nothing added. ragel/color_ops.c is generated from the same ../mux/lib/color_ops.rl as the shipped copy but is not tracked, so it could not dirty anything; its rule gets the strip anyway, so the two generations of one source cannot disagree if it is ever checked in. client/tf could not be built here -- src/regex_utils.h needs pcre2.h, which is not on that target's include path on macOS -- but that is a pre-existing gap unrelated to this change. The Ragel custom commands themselves DID run (cmake configure succeeds once NCURSESW_LIB is pointed at a macOS ncurses), and both outputs came back stripped, so the CMake edit is exercised rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 00:59:11 -06:00
"mux/lib/date_scan.rl"
"mux/muxescape/muxescape.rl"
"utf/"
"utf/"
"utf/"
"utf/"
"utf/"
"utf/"
"mux/configure.ac"
"mux/configure.ac"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"mux/proxy/hydra.proto"
"NEVER_EDIT"
"NEVER_EDIT"
)
build(ragel): strip #line directives so the outputs are canonical (#1950) Ragel's #line values depend on the Ragel build rather than on our source, so regenerating with a different Ragel rewrites dozens of #line-only lines and leaves the tree dirty after any pull that touches a .rl. That is not cosmetic: dounix.sh packages from the working tree, not from a git export, so the churn ships -- into the tarball and into the generated .patch.gz. Stripping the directives makes each output a function of its .rl alone. Same input, same file, every box. The cost is that a debugger reports positions in the generated .c rather than the .rl, which is the accepted trade for an artifact that can be compared by hash. Applied to all five Ragel outputs, not the four in docs/generated-files.md -- date_scan.cpp is also Ragel-generated and had 102 directives. color_ops.c 194 art_scan.cpp 24 date_scan.cpp 102 ast_scan.cpp 43 muxescape.cpp 16 total 379 sed rather than sed -i: the -i spelling differs between GNU and BSD, and this has to run on Linux, macOS and FreeBSD. Deleting whole lines is safe here -- all 379 directives are anchored at column 0 and none carries trailing code, both checked before the change. Makefile.in is hand-edited to match rather than regenerated: this box has automake 1.18.1 and the tree's Makefile.in came from 1.16.5, so autoreconf would churn whole files for a five-line change (#1477). The inserted lines are identical to the Makefile.am ones. The pre-commit hook needed teaching, and this commit is the example: it rejects a generated file staged without its source, but changing how a file is generated rewrites the output while the .rl stays untouched. The build rule now counts as a source. Verified all three ways -- a bare generated file is still blocked, and staging it with either its .rl or its Makefile.am is accepted. Verified on macOS/arm64: - regeneration is idempotent: all five byte-identical across two runs - the generated diffs are 379 deletions, every one a #line, nothing added - outputs return to mode 444, no .tmp files left behind - make test 35 passed / 1 skipped / 0 failed (jit=yes stubslave=no nls=yes realitylvls=yes wodrealms=yes) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:21:47 -06:00
# Build rule that generates GENERATED_GENS[N], where there is a single one.
# Same index; "" means none, and the source check stands alone.
GENERATED_RULES=(
"mux/modules/engine/Makefile.am"
"mux/modules/engine/Makefile.am"
"mux/lib/Makefile.am"
build(ragel): guard date_scan and strip #line from the remaining outputs (#2029) Two remainders from #2025. ## date_scan.cpp was unguarded hooks/pre-commit knew about four Ragel outputs; mux/lib/date_scan.cpp was absent from GENERATED_GENS entirely, so it could be hand-edited and committed without its .rl and nothing objected -- while being listed in docs/generated-files.md and shipped via unix/TOC.patchable. Added to all three parallel arrays, which now hold 27 each. Verified by alignment rather than by count: equal lengths prove nothing about correspondence, so the check confirms every ragel triple's basenames match and its rule lives in the same directory as its output. Then exercised: staging date_scan.cpp alone is now blocked, naming date_scan.rl as its source. ## Six more outputs carried #line #2025's scope was right for what the mux build regenerates and dounix.sh ships, but these have live rules of their own and could still dirty a tree: testcases/tools/unformat.c 28 testcases/tools/Makefile (%.c: %.rl) testcases/tools/reformat.c 23 same ragel/trigger_match.c 16 ragel/Makefile tools/ansify/ansify.c 19 tools/ansify/Makefile (make regen) client/tf/src/script_lex.cpp 67 client/tf/CMakeLists.txt client/tf/src/input_lex.cpp 62 same The issue reported "no rule found" for trigger_match.c and ansify.c. Both were wrong: ragel/Makefile:25 generates trigger_match.c on dependency, and tools/ansify/Makefile has an explicit `regen` target. Neither is inert -- ansify's is opt-in rather than implicit, which is why it reads as absent. Each regenerated through its OWN rule rather than by running sed by hand, so the rule is what is under test. All six are idempotent across a second regeneration, and the diffs are 215 deletions, every one a #line, nothing added. ragel/color_ops.c is generated from the same ../mux/lib/color_ops.rl as the shipped copy but is not tracked, so it could not dirty anything; its rule gets the strip anyway, so the two generations of one source cannot disagree if it is ever checked in. client/tf could not be built here -- src/regex_utils.h needs pcre2.h, which is not on that target's include path on macOS -- but that is a pre-existing gap unrelated to this change. The Ragel custom commands themselves DID run (cmake configure succeeds once NCURSESW_LIB is pointed at a macOS ncurses), and both outputs came back stripped, so the CMake edit is exercised rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 00:59:11 -06:00
"mux/lib/Makefile.am"
build(ragel): strip #line directives so the outputs are canonical (#1950) Ragel's #line values depend on the Ragel build rather than on our source, so regenerating with a different Ragel rewrites dozens of #line-only lines and leaves the tree dirty after any pull that touches a .rl. That is not cosmetic: dounix.sh packages from the working tree, not from a git export, so the churn ships -- into the tarball and into the generated .patch.gz. Stripping the directives makes each output a function of its .rl alone. Same input, same file, every box. The cost is that a debugger reports positions in the generated .c rather than the .rl, which is the accepted trade for an artifact that can be compared by hash. Applied to all five Ragel outputs, not the four in docs/generated-files.md -- date_scan.cpp is also Ragel-generated and had 102 directives. color_ops.c 194 art_scan.cpp 24 date_scan.cpp 102 ast_scan.cpp 43 muxescape.cpp 16 total 379 sed rather than sed -i: the -i spelling differs between GNU and BSD, and this has to run on Linux, macOS and FreeBSD. Deleting whole lines is safe here -- all 379 directives are anchored at column 0 and none carries trailing code, both checked before the change. Makefile.in is hand-edited to match rather than regenerated: this box has automake 1.18.1 and the tree's Makefile.in came from 1.16.5, so autoreconf would churn whole files for a five-line change (#1477). The inserted lines are identical to the Makefile.am ones. The pre-commit hook needed teaching, and this commit is the example: it rejects a generated file staged without its source, but changing how a file is generated rewrites the output while the .rl stays untouched. The build rule now counts as a source. Verified all three ways -- a bare generated file is still blocked, and staging it with either its .rl or its Makefile.am is accepted. Verified on macOS/arm64: - regeneration is idempotent: all five byte-identical across two runs - the generated diffs are 379 deletions, every one a #line, nothing added - outputs return to mode 444, no .tmp files left behind - make test 35 passed / 1 skipped / 0 failed (jit=yes stubslave=no nls=yes realitylvls=yes wodrealms=yes) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:21:47 -06:00
"mux/muxescape/Makefile.am"
"" "" "" "" "" ""
"" ""
"" "" "" "" "" "" "" "" "" "" "" ""
"" ""
)
staged=$(git diff --cached --name-only)
warnings=""
for i in "${!GENERATED_GENS[@]}"; do
gen="${GENERATED_GENS[$i]}"
src="${GENERATED_SRCS[$i]}"
if echo "$staged" | grep -qx "$gen"; then
if [ "$src" = "NEVER_EDIT" ]; then
# Third-party vendored file — should never be hand-edited
warnings="${warnings} ${gen} (third-party vendored file — replace, don't edit)\n"
elif ! echo "$staged" | grep -q "^${src}"; then
build(ragel): strip #line directives so the outputs are canonical (#1950) Ragel's #line values depend on the Ragel build rather than on our source, so regenerating with a different Ragel rewrites dozens of #line-only lines and leaves the tree dirty after any pull that touches a .rl. That is not cosmetic: dounix.sh packages from the working tree, not from a git export, so the churn ships -- into the tarball and into the generated .patch.gz. Stripping the directives makes each output a function of its .rl alone. Same input, same file, every box. The cost is that a debugger reports positions in the generated .c rather than the .rl, which is the accepted trade for an artifact that can be compared by hash. Applied to all five Ragel outputs, not the four in docs/generated-files.md -- date_scan.cpp is also Ragel-generated and had 102 directives. color_ops.c 194 art_scan.cpp 24 date_scan.cpp 102 ast_scan.cpp 43 muxescape.cpp 16 total 379 sed rather than sed -i: the -i spelling differs between GNU and BSD, and this has to run on Linux, macOS and FreeBSD. Deleting whole lines is safe here -- all 379 directives are anchored at column 0 and none carries trailing code, both checked before the change. Makefile.in is hand-edited to match rather than regenerated: this box has automake 1.18.1 and the tree's Makefile.in came from 1.16.5, so autoreconf would churn whole files for a five-line change (#1477). The inserted lines are identical to the Makefile.am ones. The pre-commit hook needed teaching, and this commit is the example: it rejects a generated file staged without its source, but changing how a file is generated rewrites the output while the .rl stays untouched. The build rule now counts as a source. Verified all three ways -- a bare generated file is still blocked, and staging it with either its .rl or its Makefile.am is accepted. Verified on macOS/arm64: - regeneration is idempotent: all five byte-identical across two runs - the generated diffs are 379 deletions, every one a #line, nothing added - outputs return to mode 444, no .tmp files left behind - make test 35 passed / 1 skipped / 0 failed (jit=yes stubslave=no nls=yes realitylvls=yes wodrealms=yes) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:21:47 -06:00
# Source is not staged. The build rule counts too: a change to
# how the file is generated rewrites the output without touching
# the source, and that is a legitimate commit (#1950).
rule="${GENERATED_RULES[$i]}"
if [ -z "$rule" ] || ! echo "$staged" | grep -qx "$rule"; then
warnings="${warnings} ${gen} (source: ${src})\n"
fi
fi
fi
done
if [ -n "$warnings" ]; then
echo ""
echo "WARNING: Generated file(s) staged WITHOUT their source:"
echo ""
printf "$warnings"
echo ""
echo "Did you edit a generated file by mistake?"
echo "Edit the source and regenerate instead."
echo ""
echo "To commit anyway: git commit --no-verify"
exit 1
fi