1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
#!/bin/sh
# Format the code being committed, so what lands in git is already formatted.
#
# C with clang-format (.clang-format), Python with ruff -- imports sorted, then
# laid out (ruff.toml).
#
# Install with `make hooks`, which points `core.hooksPath` here. Hooks are not
# versioned by git, so a hook in a repo has to be a file somebody opts into --
# there is no way to ship one that runs on clone, and a repo that could would be
# a repo that runs code on clone.
#
# Skip a commit with `git commit --no-verify` when you mean to.
set -e
# Only what is staged, and never the vendored tree: libghostty-vt is somebody
# else's code, pinned by commit (D12), and reformatting it would turn every
# future re-vendor into a merge.
staged=$(git diff --cached --name-only --diff-filter=ACMR | grep -v '^vendor/' || true)
c_files=$(printf '%s\n' "$staged" | grep -E '\.[ch]$' || true)
py_files=$(printf '%s\n' "$staged" | grep -E '\.py$' || true)
# ...plus the contrib scripts, which are Python with no extension on them.
for f in $staged; do
case $f in *.*) continue ;; esac
if [ -f "$f" ] && head -1 "$f" | grep -q python; then
py_files=$(printf '%s\n%s' "$py_files" "$f")
fi
done
c_files=$(printf '%s\n' "$c_files" | sed '/^$/d')
py_files=$(printf '%s\n' "$py_files" | sed '/^$/d')
files=$(printf '%s\n%s\n' "$c_files" "$py_files" | sed '/^$/d')
[ -n "$files" ] || exit 0
# Where ruff is. The repo-local venv first, before $PATH, for the same reason the
# Makefile prefers it: ruff.toml pins a version, and a system ruff of the wrong
# one would refuse the commit while the venv sitting right there has the version
# it asked for. Off the toplevel, so this works from a subdirectory.
root=$(git rev-parse --show-toplevel)
if [ -x "$root/.venv/bin/ruff" ]; then
ruff="$root/.venv/bin/ruff"
else
ruff=$(command -v ruff 2>/dev/null || true)
fi
# Named together, so somebody setting up a machine installs both at once rather
# than discovering the second one on their next commit.
missing=""
if [ -n "$c_files" ] && ! command -v clang-format >/dev/null 2>&1; then
missing="$missing clang-format"
fi
if [ -n "$py_files" ] && [ -z "$ruff" ]; then
missing="$missing ruff"
fi
if [ -n "$missing" ]; then
echo "pre-commit: not installed:$missing" >&2
echo " and this commit touches code they format." >&2
echo " ruff: make tools (a venv in .venv, version pinned)" >&2
echo " clang-format: apt install clang-format | brew install clang-format" >&2
echo " ...or commit with --no-verify if you know what you are doing." >&2
exit 1
fi
# A file with unstaged changes is the one case where formatting in place is
# dangerous: the formatters rewrite the *working tree*, and re-staging afterwards
# would commit work that was deliberately left out of the index. So say so and
# stop, rather than quietly widening a commit somebody built with `git add -p`.
partial=""
for f in $files; do
if ! git diff --quiet -- "$f"; then partial="$partial $f"; fi
done
if [ -n "$partial" ]; then
echo "pre-commit: these files are only partly staged, so formatting them" >&2
echo " in place would add work you left out of the commit:" >&2
for f in $partial; do echo " $f" >&2; done
echo " stage them fully, stash the rest, or use --no-verify." >&2
exit 1
fi
if [ -n "$c_files" ]; then
clang-format -i $c_files
fi
if [ -n "$py_files" ]; then
# Imports first, then layout: sorting can leave a line the formatter then wraps.
# `--select I` on the command line as well as in ruff.toml, so a stray `select`
# somewhere else cannot turn this hook into a linter that blocks commits over an
# unused variable.
"$ruff" check -q --select I --fix $py_files
"$ruff" format -q $py_files
fi
git add -- $files
# Said out loud, because a hook that changes your commit without mentioning it is
# a hook people learn to distrust.
echo "pre-commit: formatted $(printf '%s' "$files" | tr '\n' ' ' | sed 's/ *$//')"
exit 0
|