precommit #
Git hooks for OCaml projects, without the Python pre-commit tool.
Overview #
A formatting or lint rule only holds if something runs it before every
commit, and the usual tool for that, pre-commit, needs Python and a
configuration file per repository. precommit writes the hooks an OCaml
project needs straight into git:
- a pre-commit hook that formats the tree with
dune fmt, re-stages what it changed, and runs themerlintlinter the workspace builds; - a commit-msg hook that rejects messages containing emojis and removes lines mentioning Claude;
- a default
.ocamlformatwhen the project has none.
--hooks fmt,merlint,ai selects which of the three hook behaviours to
install (all by default). The tool also wires a clone to a hooks directory
that the repository tracks in git, and reports which repositories of a tree
are missing hooks.
Installation #
Install with opam:
$ opam install precommit
If opam cannot find the package, it may not yet be released in the public
opam-repository. Add the overlay repository, then install it:
$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install precommit
Usage #
Initialize hooks #
Run in an OCaml project directory (where dune-project exists):
$ precommit init
This creates .git/hooks/pre-commit, .git/hooks/commit-msg and, if
missing, .ocamlformat.
Wire the clone to a tracked hooks directory #
precommit init writes hooks into .git/hooks, which git does not track, so a
fresh clone gets none of them. A repository that carries its hooks in a tracked
directory instead needs core.hooksPath pointed at that directory, once per
clone:
$ precommit link
--hooks-path DIR names the directory, .githooks by default. A relative path
resolves against each worktree's own root, and every linked worktree of a clone
shares the configuration, so one call covers them all, and each worktree runs
the hooks its own checkout carries. Git runs a push's hooks from the git
directory instead, so link also writes .git/.githooks -> ../.githooks: a
push into the repository runs the hooks its main worktree carries.
Ask whether a clone is wired, changing nothing. The exit code is 1 when it is
not, when core.hooksPath names the directory by absolute path (every worktree
would run that one checkout's hooks), when the push link is missing, and when a
hook there is not executable, which git skips silently:
$ precommit link --check
Nothing a repository carries can gate the first commit of an unwired clone: git
runs no hook the clone does not have. --check is what makes that state
visible, from a setup script or by hand.
Setting core.hooksPath retires every hook outside the tracked directory, so
link refuses when git runs one the directory does not carry, and names it;
--force wires anyway.
Check status #
$ precommit status
status shows which hooks are installed. The exit code is 1 if any OCaml
project lacks a hook or .ocamlformat.
Check git history #
$ precommit check
check scans the git history for commits with AI attribution lines, and
precommit fix rewrites them out of the history with git filter-branch,
after backing up the branch to backup/<branch>-before-fix-<timestamp> and
asking for confirmation (-n previews, -y skips the prompt).
Recursive operation #
-r operates on every OCaml project in the subdirectories:
$ precommit init -r
$ precommit status -r
$ precommit check -r
Dry run #
--dry-run shows what would be created without changing anything:
$ precommit init --dry-run
Requirements #
- git
- dune, for formatting
- a workspace that builds
merlint, for the lint hook
Licence #
ISC. See LICENSE.md.