aboutsummaryrefslogtreecommitdiffstats
path: root/.githooks/pre-commit
blob: 64fa77e6b12b7ceb3353daf0f82df52a3ce53996 (plain) (blame)
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