107 lines
5.0 KiB
Markdown
107 lines
5.0 KiB
Markdown
# Git hooks
|
|
|
|
```
|
|
Status: Current
|
|
Owner: _null
|
|
Last reviewed: 2026-08-18
|
|
Governs: .githooks/ — what runs before and after a commit
|
|
Review trigger: A new guard the repository wants run before a commit; any change
|
|
to what a commit message must contain; any change to which
|
|
Gradle tasks pre-commit runs
|
|
```
|
|
|
|
Three hooks, and the reason they live in the repository rather than in
|
|
`.git/hooks`: that directory is not versioned, so a hook living there protects
|
|
exactly one clone on exactly one machine.
|
|
|
|
## The hooks are in `.githooks/`, and only there
|
|
|
|
The template this repository adopted keeps a master copy under
|
|
`docs/architecture/githooks/` and installs copies into `.githooks/`. **Period
|
|
does not**, deliberately: `pre-commit` here is adapted for Gradle rather than
|
|
npm, so a second copy would be a second version of a file somebody edits once
|
|
and forgets — the exact failure `../../DOC_TRUST_MAP.md` exists to prevent.
|
|
|
|
So [`.githooks/`](../../../.githooks) holds the hooks and this document
|
|
describes them. One copy of the code, one copy of the explanation.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
git config core.hooksPath .githooks
|
|
```
|
|
|
|
That is the whole setup, and it is **per clone** — every checkout runs it once,
|
|
including a fresh clone on the same machine. An uninstalled hook fails silently,
|
|
which is the same class of problem the hooks exist to prevent.
|
|
|
|
## What each one does
|
|
|
|
| Hook | Guard |
|
|
| --- | --- |
|
|
| `pre-commit` | the staged-diff secret scan; then `:domain:cycle:test` and `:domain:prediction:test` when `.kt`/`.kts` or a build file is staged; then `scripts/schema-guard.sh` when a Room entity or the schema export is staged |
|
|
| `commit-msg` | refuses a message with no conventional type — the closed vocabulary is in the hook's own header |
|
|
| `post-commit` | pushes to `origin`, so a guarded commit does not sit unpushed |
|
|
|
|
## Two things worth knowing before you rely on them
|
|
|
|
**`pre-commit` does not compile the Android modules.** It runs the two pure-JVM
|
|
suites, which need no SDK and take about a second. Compiling `:app` needs the
|
|
Android SDK and half a minute, and a hook people reach for `--no-verify` to
|
|
avoid is worse than one that checks less. `./gradlew assembleRelease` belongs to
|
|
[`../../security/SECURITY_CHECKLIST.md`](../../security/SECURITY_CHECKLIST.md),
|
|
which is where it is.
|
|
|
|
The suite it *does* run is not an arbitrary subset: it is the prediction
|
|
acceptance cases from
|
|
[`../../planning/PRODUCT_PLAN.md` §51](../../planning/PRODUCT_PLAN.md), which
|
|
guard the one claim this product is built on.
|
|
|
|
**The schema guard is here rather than in the suite for a reason worth knowing.**
|
|
Room rewrites the schema export during compilation, so by the time any test
|
|
runs, both sides of any in-process comparison describe the changed entity and
|
|
agree. Only git can see that an already-committed schema file changed, and only
|
|
a hook can ask git before the commit exists. `scripts/schema-guard.sh` carries
|
|
the proof in its header — the check it replaced was watched staying green over
|
|
exactly the failure it claimed to catch.
|
|
|
|
It exits `2` for "nothing was checked", and the hook treats `2` as a refusal.
|
|
A missing schema directory is not a clean schema.
|
|
|
|
**The secret scan's `2` is handled differently, and deliberately.** A commit that
|
|
only deletes files gives it nothing to scan, which is the right answer rather
|
|
than a fault — so the hook accepts a `2` when the staged diff adds no lines, and
|
|
refuses it whenever the diff adds any. Before that distinction existed, `git rm`
|
|
was impossible and the refusal claimed a credential had been found.
|
|
|
|
**`post-commit` pushes.** That is the intent — the commit that first added a
|
|
pre-commit hook to the project this came from sat unpushed for a day, guarded
|
|
and invisible — but it is a surprise if you were not expecting it. It never
|
|
forces, stays out of the way mid-rebase, and `SKIP_PUSH=1` opts out loudly.
|
|
|
|
It has a second consequence worth knowing about: the push is what the Command
|
|
Center reads, so **whatever documentation was not in that commit is now behind
|
|
the code by one push**. That is the mechanical reason
|
|
[`../../WORK_CYCLE.md`](../../WORK_CYCLE.md) asks for doc edits in the same
|
|
commit as the change rather than in a tidy-up afterwards — with this hook
|
|
installed, "I will document it next commit" means the site has already published
|
|
the version without it.
|
|
|
|
## Escape hatches, and why they are loud
|
|
|
|
```bash
|
|
SKIP_GUARDS=1 git commit ... # skips the scan and the tests, and says so
|
|
SKIP_PUSH=1 git commit ... # commits without publishing, and says so
|
|
git commit --no-verify ... # skips the hooks entirely, silently
|
|
```
|
|
|
|
Prefer the first two. They leave a line in the terminal saying the guard did not
|
|
run, which is the difference between a deliberate exception and a habit.
|
|
|
|
## Why a hook and not CI
|
|
|
|
Both, eventually. These are the guards that must run before the artifact exists:
|
|
a check that fires after a push, or after a Play upload, catches the problem once
|
|
it is already somewhere it cannot be taken back from. CI is the second opinion;
|
|
this is the one that runs first.
|