-
-
Notifications
You must be signed in to change notification settings - Fork 309
Expand file tree
/
Copy pathpre-commit
More file actions
executable file
·117 lines (106 loc) · 5.45 KB
/
Copy pathpre-commit
File metadata and controls
executable file
·117 lines (106 loc) · 5.45 KB
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/bin/bash
#
# Git pre-commit hook: normalize the formatting of the sources about to be committed, then run
# the tests.
#
# Install it with:
# ln -s ../../.github/pre-commit .git/hooks/pre-commit
# A symlink keeps the installed hook in step with this file. Copying it works too, but then it
# has to be copied again every time this file changes.
set -u
# Run from the top of the working tree, whatever directory git invoked the hook from. Asking git
# is what makes this independent of where the hook was installed; computing the path relative to
# the script only worked while the hook sat in .git/hooks.
TOP=$(git rev-parse --show-toplevel) || exit 1
cd "$TOP" || exit 1
# Run maven, showing what it is doing while it does it. The formatter takes a couple of seconds and
# the tests take about a minute, and a hook that prints nothing at all while it works cannot be told
# apart from one that has hung, so stream a line per module and a dot per test class as they go. The
# full output is still held back, because it is only worth reading when the run fails: it goes to a
# file, and the caller prints that file if the run fails. MVN_SECONDS is how long the run took.
MVN_LOG=$(mktemp) || exit 1
trap 'rm -f "$MVN_LOG"' EXIT
run_maven() {
local start=$SECONDS
./mvnw -B --no-transfer-progress "$@" 2>&1 | tee "$MVN_LOG" | awk '
# "[INFO] Building <name> <version> [n/m]", where the [n/m] is absent in a single-module
# build, which is what the v4 branch is.
/^\[INFO\] Building / {
if (open) printf "\n";
idx = ""; last = NF - 1;
if ($NF ~ /^\[[0-9]+\/[0-9]+\]$/) { idx = $NF " "; last = NF - 2 }
name = "";
for (i = 3; i <= last; i++) name = name (i > 3 ? " " : "") $i;
printf " %s%s ", idx, name;
open = 1; col = 0; fflush();
}
# One dot per test class. The core module has a couple of hundred of them, so wrap the
# dots rather than let the terminal wrap them at whatever width it happens to be.
/^\[INFO\] Running / {
if (col == 50) { printf "\n "; col = 0 }
printf "."; col++; fflush();
}
END { if (open) printf "\n" }
' >&2
# Read straight off the pipeline: any command in between would overwrite PIPESTATUS.
local status=${PIPESTATUS[0]}
MVN_SECONDS=$((SECONDS - start))
return "$status"
}
# The Java sources this commit is about to record. R is in the filter because a staged rename is
# reported as R rather than M, and --name-only then gives the new path, which is the one to
# reformat and re-stage. Without it, a file renamed and reformatted in the same commit is rewritten
# in the working tree by the next build while the index keeps the unformatted content.
STAGED=$(git diff --cached --name-only --diff-filter=ACMR -- '*.java')
# Normalize them. The plugin does take a -Dformatter.includes list, but its patterns are relative
# to each module's source roots while git reports paths from the top of the tree, so formatting the
# whole tree and re-staging only what was staged is the simpler thing to get right; anything it
# rewrites that was not staged simply stays unstaged. The v4 branch has no formatter plugin and one
# installed hook serves every worktree, so check for the plugin rather than failing there.
if [ -n "$STAGED" ] && grep -q formatter-maven-plugin pom.xml; then
# The formatter rewrites whole files, so re-staging a file that was only partly staged would
# also commit the part that was deliberately left out. Stop rather than do that. This check
# belongs here and not above it: with no formatter to run, a partly staged file is fine.
PARTIAL=
while IFS= read -r f; do
[ -n "$f" ] || continue
if [ -n "$(git diff --name-only -- "$f")" ]; then
PARTIAL="$PARTIAL $f
"
fi
done <<< "$STAGED"
if [ -n "$PARTIAL" ]; then
echo "pre-commit: these files are only partly staged, so they cannot be reformatted safely:" >&2
printf '%s' "$PARTIAL" >&2
echo "Stage each of them in full, or commit them on their own." >&2
exit 1
fi
echo "pre-commit: normalizing the formatting of the sources" >&2
if ! run_maven formatter:format; then
echo "pre-commit: the formatter failed" >&2
cat "$MVN_LOG" >&2
exit 1
fi
# Re-stage only what was already staged, so formatting cannot sweep anything else in.
while IFS= read -r f; do
[ -n "$f" ] && [ -e "$f" ] && git add -- "$f"
done <<< "$STAGED"
# git refuses to record an empty commit, but it decides that before the hook runs, so a commit
# whose only content was misformatting slips past that check and lands as an empty commit.
# Refuse it here instead, which is what git would have done had the source been normalized
# before it was staged.
if git diff --cached --quiet; then
echo "pre-commit: nothing left to commit -- the only staged change was formatting, which the" >&2
echo "formatter has now undone. The working tree is normalized." >&2
exit 1
fi
fi
# Run the tests, and print the output when they fail. An earlier version captured that output and
# then discarded it, leaving nothing to go on but the words "Unit test failure".
echo "pre-commit: running the tests -- this is the slow part of a commit" >&2
if ! run_maven test; then
echo "pre-commit: unit test failure" >&2
cat "$MVN_LOG" >&2
exit 1
fi
echo "pre-commit: tests passed in ${MVN_SECONDS}s" >&2