From 339027258b16f8bb3baa369ccd6fc486cdf9020f Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Tue, 18 Aug 2026 15:17:15 -0400 Subject: [PATCH] chore(repo): adopt the sibling repos' hooks, and two of our own Takes prek.toml's shape from infra, headquarters and arena, keeping what this repo has files for and dropping what it does not: no terraform, no prettier or astro, no ruff. check-xml earns its place on one file, bridge/log4j2-quiet.xml, which nothing else parses until MegaMek refuses to start. Two hooks are this repo's own. crate-boundaries runs the boundary check when a Cargo.toml changes, which is the file a boundary breaks in - a `use` cannot add a dependency, and the breakage otherwise surfaces much later as "why can this not compile to wasm". commit-scope checks Conventional Commits and that the scope names an epic in headquarters' plan/. That scope check differs from headquarters' in two ways on purpose. It enforces the form as well as the scope, since this repo has had the convention since its second commit. And the epic ids are written down rather than read from plan/, because helm has no plan/ and a sibling checkout is not guaranteed to exist - a list that is occasionally stale beats a hook that passes silently whenever a directory is missing. cargo clippy runs with -j 2, which the siblings do not need. A full-parallel cargo build has taken this machine down, and a hook runs on every commit. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 24 ++++++++ prek.toml | 84 +++++++++++++++++++++++++ scripts/check-commit-scope.sh | 112 ++++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 prek.toml create mode 100755 scripts/check-commit-scope.sh diff --git a/README.md b/README.md index c2a5a1b..cf9275c 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,30 @@ cargo build --release The `.sqlite` is not committed. It is derived, it is 30MB, and MegaMek's data is CC BY-NC-SA 4.0 — build it locally. +## Hooks + +``` +prek install +``` + +Installs the pre-commit and commit-msg hooks from `prek.toml`. Same set as the +sibling repos where the file types overlap — whitespace, TOML and XML checks, +shellcheck, `cargo fmt` and `cargo clippy` — plus two this repo needs of its +own. + +`crate-boundaries` runs `scripts/check-boundaries.sh` when a `Cargo.toml` +changes. That is the file a boundary breaks in, and the breakage otherwise +surfaces much later as "why can this not compile to wasm". + +`commit-scope` checks the subject is Conventional Commits and that its scope +names an epic in headquarters' `plan/` — `unit-library`, `unit-search`, +`unit-rules` — or one of a short list of non-epic scopes. A commit with no +scope passes. + +`cargo clippy` runs with `-j 2` here where the sibling repos do not bother: a +full-parallel cargo build has taken this machine down, and a hook runs on +every commit. + ## What comes from where `helm-unitfile` reads MegaMek's two formats directly: `.mtf` for Meks, `.blk` diff --git a/prek.toml b/prek.toml new file mode 100644 index 0000000..ce18c57 --- /dev/null +++ b/prek.toml @@ -0,0 +1,84 @@ +# Hooks for prek (https://prek.j178.dev/). See README for setup. +# +# Same shape as the sibling repos, minus what this one has no files for: +# no terraform (infra's), no prettier or astro (headquarters' web), no ruff +# (arena's Python helpers, and there is no Python here). + +# The commit-msg hook needs its own git shim, so `prek install` alone does not +# get it. Naming it here means a plain `prek install` installs both. +default_install_hook_types = ["pre-commit", "commit-msg"] + +[[repos]] +repo = "https://github.com/pre-commit/pre-commit-hooks" +rev = "v6.0.0" +# check-xml earns its place on one file, bridge/log4j2-quiet.xml, which +# nothing else parses until MegaMek refuses to start. check-yaml and +# check-json are here for the CI config this repo does not have yet; they cost +# nothing while they match nothing. +# +# check-added-large-files matters more here than in the sibling repos: the +# built database is 100MB and the bridge's dumps are 33MB, and while +# .gitignore covers them by name, a differently-named copy would sail in. +hooks = [ + { id = "check-merge-conflict" }, + { id = "check-added-large-files" }, + { id = "check-toml" }, + { id = "check-yaml" }, + { id = "check-json" }, + { id = "check-xml" }, + { id = "mixed-line-ending", args = ["--fix=lf"] }, + { id = "end-of-file-fixer" }, + { id = "trailing-whitespace" }, +] + +[[repos]] +repo = "https://github.com/shellcheck-py/shellcheck-py" +rev = "v0.11.0.1" +# Same flags as arena's and headquarters' gates: -x follows sourced files, and +# info-level notes do not gate. +hooks = [{ id = "shellcheck", args = ["-x", "--severity=warning"] }] + +[[repos]] +repo = "local" + +[[repos.hooks]] +id = "cargo-fmt" +name = "cargo fmt" +language = "system" +entry = "cargo fmt --all --check" +pass_filenames = false +files = '\.rs$' + +# -j 2, which the sibling repos do not need. A full-parallel cargo build has +# taken this machine down before, and a hook runs on every commit, so it is +# the last place to let one off the leash. +[[repos.hooks]] +id = "cargo-clippy" +name = "cargo clippy" +language = "system" +entry = "cargo clippy -j 2 --workspace --all-targets -- -D warnings" +pass_filenames = false +files = '(\.rs|Cargo\.(toml|lock))$' + +# The crate graph this repo's design rests on: no I/O dependency may reach the +# crates that have to compile to wasm, and no producer of ComputedStats may +# reach helm-db. Both erode by adding one line to a Cargo.toml, and the +# failure surfaces much later as "why can this not compile to wasm", so the +# trigger is Cargo.toml rather than every Rust file - a `use` cannot add a +# dependency, and this way the hook stays off the common path. +[[repos.hooks]] +id = "crate-boundaries" +name = "crate boundaries" +language = "system" +entry = "scripts/check-boundaries.sh" +pass_filenames = false +files = 'Cargo\.toml$' + +# Conventional Commits, with the scope naming the epic in headquarters' plan/ +# that the work belongs to. The script says what else is allowed and why. +[[repos.hooks]] +id = "commit-scope" +name = "commit scope" +language = "system" +entry = "scripts/check-commit-scope.sh" +stages = ["commit-msg"] diff --git a/scripts/check-commit-scope.sh b/scripts/check-commit-scope.sh new file mode 100755 index 0000000..afc0e03 --- /dev/null +++ b/scripts/check-commit-scope.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# +# Check a commit message against Conventional Commits, and its scope against +# the epics this repo's work belongs to. +# +# Two differences from headquarters' hook of the same name, both deliberate. +# +# It enforces the *form* as well as the scope. headquarters lets an +# unconventional subject through, because that repo has years of them; this +# one has had the convention from its second commit and there is no reason to +# let it slip. +# +# And the epic ids are written down here rather than read from plan/, because +# helm has no plan/ - its epics live in headquarters, which is not guaranteed +# to be checked out beside this repo and is certainly not guaranteed to be at +# any particular revision. A hardcoded list that is occasionally stale is +# better than a hook that passes silently whenever a sibling directory is +# missing. +# +# Run by prek at the commit-msg stage, with the message file as $1. + +set -euo pipefail + +# Conventional Commits types. +TYPES="feat fix docs style refactor perf test build ci chore revert" + +# Epic ids from headquarters' plan/. Add one here when an epic that helm does +# work for is added there. +EPIC_SCOPES="unit-library unit-search unit-rules" + +# Scopes that are deliberately not epics. Keep this short: every entry is a +# commit that cannot be found from an epic. +NON_EPIC_SCOPES="deps scripts bridge ci repo" + +msg_file="${1:?usage: check-commit-scope.sh }" + +# First line that is not a comment or blank; git strips the comments later. +subject="$(grep -v '^#' "$msg_file" | grep -m1 . || true)" + +# An empty message aborts the commit on git's side, so it is not this hook's +# to complain about. +[ -n "$subject" ] || exit 0 + +# A revert or a merge git wrote itself is not the author's prose to police. +case "$subject" in +Revert\ * | Merge\ *) exit 0 ;; +esac + +# type(scope)!: subject, where the scope is optional. The pattern is a +# variable because bash's [[ ]] parser trips on an unquoted ")". +full_re='^([a-z]+)(\(([^)]*)\))?!?: .+' +if [[ ! "$subject" =~ $full_re ]]; then + { + echo "commit subject is not Conventional Commits:" + echo " $subject" + echo + echo "Write: type(scope): what changed" + echo " type: what changed" + echo + # shellcheck disable=SC2086 + echo "Types: $(printf '%s, ' $TYPES | sed 's/, $//')" + echo "Scopes: $EPIC_SCOPES $NON_EPIC_SCOPES" + } >&2 + exit 1 +fi + +type="${BASH_REMATCH[1]}" +scopes="${BASH_REMATCH[3]}" + +case " $TYPES " in +*" $type "*) ;; +*) + { + echo "unknown commit type: $type" + echo + # shellcheck disable=SC2086 + printf ' %s\n' $TYPES | sort + } >&2 + exit 1 + ;; +esac + +# No scope is allowed: not every commit belongs to an epic. +[ -n "$scopes" ] || exit 0 + +known="$EPIC_SCOPES $NON_EPIC_SCOPES" +bad="" +IFS=',' read -ra parts <<<"$scopes" +for scope in "${parts[@]}"; do + scope="$(printf '%s' "$scope" | tr -d '[:space:]')" + [ -n "$scope" ] || continue + case " $known " in + *" $scope "*) ;; + *) bad="$bad $scope" ;; + esac +done + +[ -n "$bad" ] || exit 0 + +{ + echo "commit scope is not an epic or a known non-epic:$bad" + echo + echo "A scope names the epic in headquarters' plan/ that the work belongs" + echo "to, so a commit is findable from it. Use one of:" + echo + # shellcheck disable=SC2086 + printf ' %s\n' $known | sort -u + echo + echo "or drop the scope. If a new epic exists in headquarters, add its id" + echo "to EPIC_SCOPES in this script." +} >&2 +exit 1 -- 2.51.2