Repository navigation
Expand file tree
/
Copy pathgit-commit-msg.sh
More file actions
executable file
·179 lines (156 loc) · 7.72 KB
/
Copy pathgit-commit-msg.sh
File metadata and controls
executable file
·179 lines (156 loc) · 7.72 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/bin/sh
# Hold a commit message to the seven rules this log already follows, with the
# widths measured off it: subjects sit at 46 characters in the median and the
# bodies wrap at 72. See scripts/install-git-hooks.sh for installation.
#
# A message arriving as `-` is read from standard input instead of from a file,
# which is how scripts/check-commit-log.sh replays an existing commit through
# the same rules. One list, so a contributor with the hooks installed and one
# without are judged the same way.
set -u
# The rules as prose, printed for scripts/git-prepare-commit-msg.sh to put in
# front of whoever is writing the message. Here rather than there so the list a
# contributor reads and the list this script enforces cannot drift apart.
if [ "${1:-}" = "--rules" ]; then
cat << 'EOF'
1. Separate the subject from the body with a blank line
2. Keep the subject within 50 characters, and say more than one word
3. Capitalize the subject; no trailing period, no backticks
4. Use the imperative mood: "Fix", not "Fixed" or "Fixes"
5. No conventional-commit prefix: not "fix:", not "feat(web):",
and no "fixup!", "squash!" or "amend!": fold those before committing
6. Wrap the body at 72 characters
7. Write a body saying what and why; the diff says how
Printable ASCII throughout. One paragraph carries most changes here; the
per-decision detail belongs in the comment next to the code.
EOF
exit 0
fi
message_file=$1
if [ "$message_file" = "-" ]; then
message_file=$(mktemp) || exit 1
trap 'rm -f "$message_file"' EXIT
cat > "$message_file"
fi
# `git commit -v` puts the diff after a scissors line and drops it before the
# message is stored, so the rules must not see it. Comments go the same way.
# Under `commit.cleanup=verbatim` the comment lines are kept in the stored
# message, so stripping them would judge a message nobody commits. Everything
# below the scissors goes either way: git drops it before storing.
cleanup=$(git config --get commit.cleanup 2> /dev/null) || cleanup=default
message=$(sed '/-\{8,\}[[:space:]]*>8[[:space:]]*-\{8,\}/,$d' "$message_file")
if [ "$cleanup" = verbatim ]; then
: # Kept as written, blank lines included: that is what verbatim means.
else
message=$(printf '%s\n' "$message" | git stripspace --strip-comments)
fi
subject=$(printf '%s\n' "$message" | sed -n '1p')
second=$(printf '%s\n' "$message" | sed -n '2p')
body=$(printf '%s\n' "$message" | sed -n '2,$p')
failed=0
error()
{
printf 'Commit message: %s\n' "$1" >&2
failed=1
}
# An autosquash marker is a note to a rebase that has not happened yet, and the
# subject behind it is a copy of another commit's: the change belongs in that
# commit before this one exists. A merge subject is git's wording, not the
# author's, and judging it is judging a message nobody wrote.
case "$subject" in
"fixup! "* | "squash! "* | "amend! "*)
error "fold this into the commit it fixes: git commit --amend for HEAD, git rebase -i --autosquash for an older one"
exit 1
;;
"Merge branch "* | "Merge branches "* | "Merge tag "* | "Merge commit "* | \
"Merge pull request "* | "Merge remote-tracking branch "*) exit 0 ;;
esac
if [ -z "$subject" ]; then
error "a descriptive subject is required"
elif [ "${#subject}" -gt 50 ]; then
error "subject is ${#subject} characters; keep it within 50"
else
case "$subject" in
*[[:space:]]*) ;;
*) error "subject must say more than one word" ;;
esac
fi
# ASCII covers three rules at once: no tabs or control characters, no CJK, and
# no em dash or typographic quote smuggled in by an editor that autocorrects.
if printf '%s' "$message" | LC_ALL=C grep -q '[^ -~]'; then
error "message must be printable ASCII"
fi
case "$subject" in
[[:space:]]*) error "subject must not start with whitespace" ;;
[[:lower:]]*) error "capitalize the subject" ;;
esac
case "$subject" in
*.) error "subject must not end with a period" ;;
esac
# The subject completes "this commit will ...", so a past tense, a gerund or a
# third person in the first word is the whole tell. Named rather than matched by
# suffix, which would also refuse "Alias", "Seed" and "Bring".
first_word=$(printf '%s' "$subject" | sed 's/[[:space:]].*//' | tr '[:upper:]' '[:lower:]')
case "$first_word" in
added | adds | adding | adjusted | adjusts | adjusting | allowed | allows | \
allowing | avoided | avoids | avoiding | bumped | bumps | bumping | \
changed | changes | changing | checked | checks | checking | cleaned | \
cleans | cleaning | corrected | corrects | correcting | created | creates | \
creating | deleted | deletes | deleting | disabled | disables | disabling | \
dropped | drops | dropping | enabled | enables | enabling | fixed | fixes | \
fixing | handled | handles | handling | implemented | implements | \
implementing | improved | improves | improving | included | includes | \
including | introduced | introduces | introducing | made | makes | making | \
moved | moves | moving | refactored | refactors | refactoring | removed | \
removes | removing | renamed | renames | renaming | replaced | replaces | \
replacing | reverted | reverts | reverting | tested | tests | testing | \
tidied | tidies | tidying | updated | updates | updating | used | uses | \
using | wired | wires | wiring | lets | letting | built | builds | \
building | wrote | writes | writing | ran | runs | running | kept | \
keeps | keeping | sent | sends | sending | brought | brings | bringing | \
took | takes | taking | gave | gives | giving | got | gets | getting | \
began | begins | beginning | chose | chooses | choosing | met | meets | \
meeting | held | holds | holding | told | tells | telling | thought | \
thinks | thinking)
error "use the imperative mood: \"$first_word\" describes what you did"
;;
esac
# This log has no conventional-commit prefixes and no backticks in a subject.
# Both read as an import from another project's tooling.
if printf '%s' "$subject" | grep -qE '^[A-Za-z]+(\([^)]*\))?!?: '; then
error "subject must not use conventional-commit syntax"
fi
case "$subject" in
*'`'*) error "subject must not contain backticks" ;;
esac
# `second` is line 2 and `body` is line 2 onward, so a non-empty line 2 is a
# body that started without a blank line between it and the subject.
if [ -n "$second" ]; then
error "separate the subject from the body with a blank line"
fi
# A subject names the change and cannot also carry the reason for it, which is
# what a reviewer and a later bisect need. Through stripspace, so a body of
# blank or whitespace-only lines kept under verbatim cleanup is still empty.
if [ -z "$(printf '%s\n' "$body" | git stripspace)" ]; then
error "a body is required: say what changed and why"
fi
# The message is ASCII by the rule above, so a byte is a column and grep can
# answer this in one pass.
if printf '%s\n' "$body" | grep -q '.\{73,\}'; then
error "wrap body lines at 72 characters"
fi
# The reasoning per decision lives in the comment next to the code in this tree.
# A body that walks through the implementation duplicates it and then goes stale
# on its own.
if printf '%s\n' "$body" \
| grep -qiE '^(how|implementation( (details|notes|steps))?|steps?|changes)[[:space:]]*:'; then
error "the body carries what and why; how is the diff's job"
fi
# Not an error: long bodies exist in this history. Still worth saying once,
# because the house shape is a premise and its trade, not a retelling.
blanks=$(printf '%s\n' "$body" | git stripspace | grep -c '^$')
if [ "$blanks" -gt 1 ]; then
printf 'Commit message: note, %s paragraphs; one usually carries it.\n' \
"$((blanks + 1))" >&2
fi
exit "$failed"