1 The hook

Read the tutorial. Build it yourself is the other way to read this part.

just setup

A check that each person must remember to run is a check that someone forgets. We write the check as a recipe. Then a hook runs the recipe before each git commit, and a last recipe tells git to use the hook.

The code before this part. The code after it.

1.1 A recipe that checks the code

A file with a syntax error breaks tally, and nothing finds it before it goes into git. A recipe check compiles every Python file of tally. Python then reports a syntax error at once, without running the code.

A patch to justfile.

At the end of the justfile, the new recipe check runs py_compile, the compiler of Python, on each file of src/tally.

just check && echo "the code compiles"

It prints the code compiles, because every file compiles.

What changed

  • justfile: the recipe compiles every Python file. 4 lines added. The file.
    • look at uv run python -m py_compile src/tally/*.py
The diff of justfile
@@ -13,3 +13,7 @@ run file:
 # Run the tests
 test:
     uv run python -m unittest discover -s tests -q
+
+# Check that every Python file compiles
+check:
+    uv run python -m py_compile src/tally/*.py

This change on GitHub. The code after it.

1.2 A hook script

just check works only when someone runs it. A hook runs it every time. git runs the file pre-commit of its folder of hooks before each git commit, and stops the git commit when the file fails. The hook, hooks/pre-commit, has three lines. They name the shell that runs it, say what it does, and run the check. The file comes whole, as a patch.

A patch to hooks/pre-commit.

The new file hooks/pre-commit runs just check, and nothing else. When just check fails, the hook fails too.

sh hooks/pre-commit; echo "the hook said $?"

The code compiles, so the hook says 0.

What changed

  • hooks/pre-commit: three lines that git runs before a git commit. the hook only runs the check. New, 3 lines. The file.
    • look at just check
The diff of hooks/pre-commit
@@ -0,0 +1,3 @@
+#!/bin/sh
+# Run before each commit: refuse it if the code does not compile.
+just check

This change on GitHub. The code after it.

1.3 Make it runnable

git runs a hook only if the hook is executable, so we make it executable.

This change came from running chmod +x hooks/pre-commit.

Run the hook by hand on a broken file, and see it fail. Then remove the file:

printf 'def (\n' > src/tally/broken.py; ./hooks/pre-commit; echo "the hook said $?"
rm src/tally/broken.py

The hook says 1, so git would stop the git commit.

What changed

  • hooks/pre-commit: Its lines stay as they were. It comes from chmod +x hooks/pre-commit. The file.

This change on GitHub. The code after it.

1.4 Install it

git does not copy hooks with a repository, and does not look in the folder hooks unless it is told to. So each copy of the project installs the hook. A recipe install-hook sets core.hooksPath, the setting of git that names the folder of hooks. The recipe changes the settings of git, so it is a recipe of its own, and not a part of just setup. just setup only runs uv sync, which makes the environment.

A patch to justfile.

At the end of the justfile, the new recipe install-hook runs one line, git config core.hooksPath hooks.

just install-hook && git config core.hooksPath

It prints hooks, so git now runs the hook before each git commit.

What changed

  • justfile: one line of git config, in a recipe of its own. 4 lines added. The file.
    • look at git config core.hooksPath hooks
The diff of justfile
@@ -17,3 +17,7 @@ test:
 # Check that every Python file compiles
 check:
     uv run python -m py_compile src/tally/*.py
+
+# Make git run the hooks in hooks/, before each commit. This changes git's config, so it is not part of setup
+install-hook:
+    git config core.hooksPath hooks

This change on GitHub. The code after it.