3 # A hook script to check the commit log message to ensure that it has
4 # a well-formed commit summary and body, a valid Signed-off-by: line,
5 # and a Gerrit Change-Id: line (added automatically if missing).
7 # Called by git-commit with one argument, the name of the file
8 # that has the commit message. The hook should exit with non-zero
9 # status after issuing an appropriate message if it wants to stop the
10 # commit. The hook is allowed to edit the commit message file.
12 # Should be installed as .git/hooks/commit-msg.
17 readonly ORIGINAL="$1"
18 readonly REVISED="$(mktemp "$ORIGINAL.XXXXXX")"
19 readonly SAVE="$(basename $ORIGINAL).$(date +%Y%m%d.%H%M%S)"
20 readonly SIGNOFF="Signed-off-by:"
21 readonly CHANGEID="Change-Id:"
22 readonly FIXES="Fixes:"
23 readonly TEST_PARAMS="Test-Parameters:"
24 readonly TEST_PARAMS2="Test-parameters:"
25 readonly LUSTRE_CHANGE="Lustre-change:"
26 readonly LUSTRE_COMMIT="Lustre-commit:"
27 readonly LINUX_COMMIT="Linux-commit:"
28 readonly EMAILS=$(echo \
36 # allow temporary override for rare cases (e.g. merge commits)
37 readonly WIDTH_SUM=${WIDTH_SUM:-62}
38 readonly WIDTH_REG=${WIDTH_REG:-70}
39 readonly JIRA_FMT_A="^[A-Z]\{2,9\}-[0-9]\{1,5\} [-a-z0-9]\{2,11\}: "
40 readonly JIRA_FMT_B="^[A-Z]\{2,9\}-[0-9]\{1,5\} "
41 readonly GERRIT_URL="https://review.whamcloud.com"
43 # Identify a name followed by an email address.
45 readonly EMAILPAT=$'[ \t]*[-._ [:alnum:]]* <[^@ \t>]+@[a-zA-Z0-9.-]+\.[a-z]+>'
62 # die: commit-msg fatal error: script error or empty input message
63 # All output redirected to stderr.
66 echo "commit-msg fatal error: $*"
67 test -f "$REVISED" && rm -f "$REVISED"
71 # Called when doing the final "wrap up" clause because we've found
72 # one of the tagged lines that belongs in the final section.
74 function ck_wrapup_started() {
75 $IS_WRAPPING_UP && return
77 $HAS_LAST_BLANK || error "blank line must preceed signoff section"
78 $HAS_SUMMARY || error "missing commit summary line."
79 $HAS_BODY || error "missing commit description."
85 function ck_is_ascii() {
87 [[ "${LINE//[![:alnum:][:blank:][:punct:]]/}" == "$LINE" ]] ||
88 error "non-printable characters in '$LINE'"
91 function ck_wrapup() {
96 function do_signoff() {
98 # Signed-off-by: First Last <email@host.domain>
99 local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
100 if (( ${#txt} == 0 )); then
101 error "$SIGNOFF line needs full name and email address"
103 HAS_SIGNOFF=true # require at least one
107 function do_changeid() {
109 $HAS_CHANGEID && error "multiple $CHANGEID lines not allowed"
111 # Change-Id: I1234567890123456789012345678901234567890
112 # capital "I" plus 40 hex digits
114 local txt=$(echo "$LINE" | grep "^$CHANGEID I[0-9a-fA-F]\{40\}\$")
116 error "has invalid $CHANGEID line for Gerrit tracking"
121 function do_testparams() {
124 grep -q mdsfilesystemtype <<< $LINE &&
125 error "mdsfilesystemtype is deprecated, use fstype"
128 function do_fixes() {
131 local commit=$(awk '{ print $2 }' <<<$LINE)
132 git describe --tags $commit 2>&1 | grep "[Nn]ot a valid" &&
133 error "invalid $FIXES hash, want '<12hex> (\"summary line\")'"
136 # All "emails" lines specify a person and email address
138 function do_emails() {
140 local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
141 (( ${#txt} == 0 )) && error "${LINE%: *} invalid name and email"
144 # All "change" lines specify a Gerrit URL
146 function do_change() {
147 local url="${LINE#*change: }"
150 [[ $url =~ $GERRIT_URL/[0-9][0-9][0-9] ]] ||
151 error "bad Gerrit URL, use '$GERRIT_URL/nnnnn' format"
154 # All "commit" lines specify a commit hash, but the commit may be in
155 # another repo, so the hash can't be directly verified
157 function do_commit() {
158 local val=${LINE#*commit: }
161 if [[ $val =~ TBD ]]; then
162 val=${val#TBD (from }
166 [[ $val =~ [g-zG-Z] ]] && error "bad commit hash '$val', non-hex chars"
167 (( ${#val} == 40 )) || error "bad commit hash '$val', not 40 chars"
170 function do_default_line() {
172 error "invalid signoff section line"
175 if ${NEEDS_FIRST_LINE}; then
176 HAS_JIRA_COMPONENT=$(echo "$LINE" | grep "$JIRA_FMT_A")
178 if (( ${#HAS_JIRA_COMPONENT} == 0 )); then
179 HAS_JIRA=$(echo "$LINE" | grep "$JIRA_FMT_B")
180 if (( ${#HAS_JIRA} > 0 )); then
181 error "has no component in summary."
183 error "missing JIRA ticket number."
185 elif (( ${#LINE} > WIDTH_SUM )); then
186 error "summary longer than $WIDTH_SUM columns."
190 NEEDS_FIRST_LINE=false
192 elif (( ${#LINE} > WIDTH_REG )) && ! [[ $LINE =~ http ]]; then
193 # ignore long lines containing URLs
194 error "has line longer than $WIDTH_REG columns."
195 elif ! $HAS_BODY && ! $HAS_LAST_BLANK; then
196 error "has no blank line after summary."
204 # Add a new unique Change-Id
208 git var GIT_AUTHOR_IDENT
209 git var GIT_COMMITTER_IDENT
211 git rev-parse HEAD 2>/dev/null
212 grep -v "^$SIGNOFF" "$ORIGINAL" | git stripspace -s
213 } | git hash-object --stdin)
214 (( ${#NEWID} > 0 )) ||
215 die "git hash-object failed for $CHANGEID:"
217 echo "$CHANGEID I$NEWID"
220 # A commit message error was encountered.
221 # All output redirected to stderr.
224 (( ${#LINE} > 0 )) && echo "line $NUM: $LINE"
225 echo "error: commit message $*"
230 echo "Run '$0 --help' for longer commit message help." 1>&2
232 mv "$ORIGINAL" "$SAVE" &&
233 echo "$0: saved original commit comment to $SAVE" 1>&2
239 Normally '$0' is invoked automatically by "git commit".
241 See https://wiki.whamcloud.com/display/PUB/Commit+Comments
242 for full details. A good example of a valid commit comment is:
244 LU-nnnnn component: short description of change under 64 columns
246 The "component:" should be a lower-case single-word subsystem of the
247 Lustre code best covering the patch. Example components include:
248 llite, lov, lmv, osc, mdc, ldlm, lnet, ptlrpc, mds, oss, osd,
249 ldiskfs, libcfs, socklnd, o2iblnd, recovery, quota, grant,
250 build, tests, docs. This list is not exhaustive, but a guideline.
252 The comment body should explan the change being made. This can be
253 as long as needed. Please include details of the problem that was
254 solved (including error messages that were seen), a good high-level
255 description of how it was solved, and which parts of the code were
256 changed (including important functions that were changed, if this
257 is useful to understand the patch, and for easier searching).
258 Performance patches should quanify the improvements being seen.
259 Wrap lines at/under $WIDTH_REG columns. Only ASCII text allowed.
261 Optionally, if the patch is backported from master, include links
262 to the original patch to simplify tracking it across branches/repos:
264 $LUSTRE_CHANGE $GERRIT_URL/nnnn
265 $LUSTRE_COMMIT 40-char-git-hash-of-patch-on-master
267 $LUSTRE_COMMIT TBD (from 40-char-hash-of-unlanded-patch)
269 Finish the comment with a blank line followed by the signoff section.
270 The "$CHANGEID" line should only be present when updating a previous
271 commit/submission. Keep the same $CHANGEID for ported patches. It
272 will automatically be added by the Git commit-msg hook if missing.
274 $TEST_PARAMS extra test options, see https://wiki.whamcloud.com/x/dICC
275 $FIXES 12-char-hash ("commit summary line of original broken patch")
276 $SIGNOFF Your Real Name <your_email@domain.name>
277 $CHANGEID Ixxxx(added automatically if missing)xxxx
279 The "signoff section" may optionally include other reviewer lines:
280 $(for T in $(tr '|' ' ' <<< "$EMAILS"); do \
281 echo " $T: Some Person <email@example.com>"; \
283 {Organization}-bug-id: associated external change identifier
287 [[ "$1" == "--help" ]] && init && usage && exit 0
290 exec 3< "$ORIGINAL" 4> "$REVISED" || exit 1
292 while IFS= read -u3 LINE; do
295 $SIGNOFF* ) do_signoff ;;
296 $CHANGEID* ) do_changeid ;;
297 $FIXES* ) do_fixes ;;
298 $TEST_PARAMS* ) do_testparams ;;
299 $TEST_PARAMS2* ) do_testparams ;;
300 $LUSTRE_CHANGE* ) do_change ;;
301 $LUSTRE_COMMIT* ) do_commit ;;
302 $LINUX_COMMIT* ) do_commit ;;
307 # Do not emit blank lines before summary line or after
308 # the tag lines have begun.
310 ${NEEDS_FIRST_LINE} || ${IS_WRAPPING_UP} && continue
314 continue ## ignore and suppress comments
318 # Beginning of uncommented diffstat from "commit -v". If
319 # there are diff and index lines, skip the rest of the input:
320 # diff --git a/build/commit-msg b/build/commit-msg
321 # index 80a3442..acb4c50 100755
322 # deleted file mode 100644
324 # If a "diff --git" line is not followed by one of these
325 # lines, do the default line processing on both lines.
327 IFS= read -u3 INDEX || break
330 "index "[0-9a-fA-F]*) break ;;
331 "deleted file mode "*) break ;;
332 "old mode "*) break ;;
333 "new file mode "*) break ;;
335 LINE=${LINE}$'\n'${INDEX}
340 if [[ "$LINE" =~ ^($EMAILS): ]]; then
343 elif [[ "$LINE" =~ ^[A-Z][A-Za-z0-9_-]*-bug-id: ]]; then
344 # Allow arbitrary external bug identifiers for tracking.
357 (( NUM <= 0 )) && die "empty commit message"
360 $HAS_SIGNOFF || error "missing valid $SIGNOFF: line."
362 if $HAS_ERROR && [[ -z "$SKIP" ]]; then
369 $HAS_CHANGEID || new_changeid >&4
372 mv "$REVISED" "$ORIGINAL"
375 ## Mode: shell-script
376 ## sh-basic-offset: 8
377 ## sh-indent-after-do: 8
379 ## sh-indent-for-case-label: 0
380 ## sh-indent-for-case-alt: 8
381 ## indent-tabs-mode: t