My own corner of monopam
README.md

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 the merlint linter the workspace builds;
  • a commit-msg hook that rejects messages containing emojis and removes lines mentioning Claude;
  • a default .ocamlformat when 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.