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 BUILD_PARAMS="Build-Parameters:"
21 readonly CHANGEID="Change-Id:"
22 readonly COVERITY="CoverityID:"
23 readonly FIXES="Fixes:"
24 readonly LINUX_COMMIT="Linux-commit:"
25 readonly LUSTRE_CHANGE="Lustre-change:"
26 readonly LUSTRE_COMMIT="Lustre-commit:"
27 readonly SIGNOFF="Signed-off-by:"
28 readonly TEST_PARAMS="Test-Parameters:"
29 readonly TEST_PARAMS2="Test-parameters:"
30 readonly EMAILS=$(echo \
38 # allow temporary override for rare cases (e.g. merge commits)
39 readonly WIDTH_SUM=${WIDTH_SUM:-62}
40 readonly WIDTH_REG=${WIDTH_REG:-70}
41 readonly JIRA_FMT_A="^[A-Z]\{2,9\}-[0-9]\{1,5\} [-a-z0-9]\{2,11\}: "
42 readonly JIRA_FMT_B="^[A-Z]\{2,9\}-[0-9]\{1,5\} "
43 readonly GERRIT_URL="https://review.whamcloud.com"
45 # Identify a name followed by an email address.
47 readonly EMAILPAT=$'[ \t]*[-._ [:alnum:]]* <[^@ \t>]+@[a-zA-Z0-9.-]+\.[a-z]+>'
64 # die: commit-msg fatal error: script error or empty input message
65 # All output redirected to stderr.
68 echo "commit-msg fatal error: $*"
69 test -f "$REVISED" && rm -f "$REVISED"
73 # Called when doing the final "wrap up" clause because we've found
74 # one of the tagged lines that belongs in the final section.
76 function ck_wrapup_started() {
77 $IS_WRAPPING_UP && return
79 $HAS_LAST_BLANK || error "blank line must preceed signoff section"
80 $HAS_SUMMARY || error "missing commit summary line."
81 $HAS_BODY || error "missing commit description."
87 function ck_is_ascii() {
89 [[ "${LINE//[![:alnum:][:blank:][:punct:]]/}" == "$LINE" ]] ||
90 error "non-printable characters in '$LINE'"
93 function ck_wrapup() {
98 function do_signoff() {
100 # Signed-off-by: First Last <email@host.domain>
101 local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
102 if (( ${#txt} == 0 )); then
103 error "$SIGNOFF line needs full name and email address"
105 HAS_SIGNOFF=true # require at least one
109 function do_changeid() {
111 $HAS_CHANGEID && error "multiple $CHANGEID lines not allowed"
113 # Change-Id: I1234567890123456789012345678901234567890
114 # capital "I" plus 40 hex digits
116 local txt=$(echo "$LINE" | grep "^$CHANGEID I[0-9a-fA-F]\{40\}\$")
118 error "has invalid $CHANGEID line for Gerrit tracking"
123 function do_buildparams() {
126 grep -Eq "\<client|\<server|arch=|distro=" <<< $LINE ||
127 error "only {client,server}{distro,arch}= supported"
130 function do_coverity() {
131 local cid=$(awk '{ print $2 }' <<<$LINE)
133 [[ x${cid}x =~ x[0-9]{6,7}x ]] ||
134 error "invalid $COVERITY CID, want 'nncidnn (\"issue type\")'"
135 [[ "$LINE" =~ \ \(\".*\"\) ]] ||
136 error "invalid $COVERITY type, want 'nncidnn (\"issue type\")'"
139 function do_testparams() {
142 grep -q mdsfilesystemtype <<< $LINE &&
143 error "mdsfilesystemtype is deprecated, use fstype"
146 function do_fixes() {
149 local commit=$(awk '{ print $2 }' <<<$LINE)
151 git describe --tags $commit 2>&1 | grep "[Nn]ot a valid" &&
152 error "invalid $FIXES hash, want '<10hex> (\"summary line\")'"
153 [[ "$LINE" =~ \ \(\".*\"\) ]] ||
154 error "invalid $FIXES summary, want '<10hex> (\"summary line\")'"
157 # All "emails" lines specify a person and email address
159 function do_emails() {
161 local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
162 (( ${#txt} == 0 )) && error "${LINE%: *} invalid name and email"
165 # All "change" lines specify a Gerrit URL
167 function do_lustre_change() {
168 local url="${LINE#*change: }"
171 [[ $url =~ $GERRIT_URL/[0-9][0-9][0-9] ]] ||
172 error "bad Gerrit URL, use '$GERRIT_URL/nnnnn' format"
175 # All "commit" lines specify a commit hash, but the commit may be in
176 # another repo, so the hash can't be directly verified
178 function do_lustre_commit() {
179 local val=${LINE#*commit: }
182 if [[ $val =~ TBD ]]; then
183 val=${val#TBD (from }
187 [[ $val =~ [g-zG-Z] ]] && error "bad commit hash '$val', non-hex chars"
188 (( ${#val} == 40 )) || error "bad commit hash '$val', not 40 chars"
191 function do_default_line() {
193 error "invalid signoff section line"
196 if ${NEEDS_FIRST_LINE}; then
197 HAS_JIRA_COMPONENT=$(echo "$LINE" | grep "$JIRA_FMT_A")
199 if (( ${#HAS_JIRA_COMPONENT} == 0 )); then
200 HAS_JIRA=$(echo "$LINE" | grep "$JIRA_FMT_B")
201 if (( ${#HAS_JIRA} > 0 )); then
202 error "has no component in summary."
204 error "missing JIRA ticket number."
206 elif (( ${#LINE} > WIDTH_SUM )); then
207 error "summary longer than $WIDTH_SUM columns."
211 NEEDS_FIRST_LINE=false
213 elif (( ${#LINE} > WIDTH_REG )) && ! [[ $LINE =~ http ]]; then
214 # ignore long lines containing URLs
215 error "has line longer than $WIDTH_REG columns."
216 elif ! $HAS_BODY && ! $HAS_LAST_BLANK; then
217 error "has no blank line after summary."
225 # Add a new unique Change-Id
229 git var GIT_AUTHOR_IDENT
230 git var GIT_COMMITTER_IDENT
232 git rev-parse HEAD 2>/dev/null
233 grep -v "^$SIGNOFF" "$ORIGINAL" | git stripspace -s
234 } | git hash-object --stdin)
235 (( ${#NEWID} > 0 )) ||
236 die "git hash-object failed for $CHANGEID:"
238 echo "$CHANGEID I$NEWID"
241 # A commit message error was encountered.
242 # All output redirected to stderr.
245 (( ${#LINE} > 0 )) && echo "line $NUM: $LINE"
246 echo "error: commit message $*"
251 echo "Run '$0 --help' for longer commit message help." 1>&2
253 mv "$ORIGINAL" "$SAVE" &&
254 echo "$0: saved original commit comment to $SAVE" 1>&2
260 Normally '$0' is invoked automatically by "git commit".
262 See https://wiki.whamcloud.com/display/PUB/Commit+Comments
263 for full details. A good example of a valid commit comment is:
265 LU-nnnnn component: short description of change under 64 columns
267 The "component:" should be a lower-case single-word subsystem of the
268 Lustre code best covering the patch. Example components include:
269 llite, lov, lmv, osc, mdc, ldlm, lnet, ptlrpc, mds, oss, osd,
270 ldiskfs, libcfs, socklnd, o2iblnd, recovery, quota, grant,
271 build, tests, docs. This list is not exhaustive, but a guideline.
273 The comment body should explan the change being made. This can be
274 as long as needed. Please include details of the problem that was
275 solved (including error messages that were seen), a good high-level
276 description of how it was solved, and which parts of the code were
277 changed (including important functions that were changed, if this
278 is useful to understand the patch, and for easier searching).
279 Performance patches should quanify the improvements being seen.
280 Wrap lines at/under $WIDTH_REG columns. Only ASCII text allowed.
282 Optionally, if the patch is backported from master, include links
283 to the original patch to simplify tracking it across branches/repos:
285 $LUSTRE_CHANGE $GERRIT_URL/nnnn
286 $LUSTRE_COMMIT 40-char-git-hash-of-patch-on-master
288 $LUSTRE_COMMIT TBD (from 40-char-hash-of-unlanded-patch)
290 Finish the comment with a blank line followed by the signoff section.
291 The "$CHANGEID" line should only be present when updating a previous
292 commit/submission. Keep the same $CHANGEID for ported patches. It
293 will automatically be added by the Git commit-msg hook if missing.
295 $BUILD_PARAMS extra build options, see https://build.whamcloud.com/
296 $TEST_PARAMS extra test options, see https://wiki.whamcloud.com/x/dICC
297 $FIXES 12-char-hash ("commit summary line of original broken patch")
298 $SIGNOFF Your Real Name <your_email@domain.name>
299 $CHANGEID Ixxxx(added automatically if missing)xxxx
301 The "signoff section" may optionally include other reviewer lines:
302 $(for T in $(tr '|' ' ' <<< "$EMAILS"); do \
303 echo " $T: Some Person <email@example.com>"; \
305 {Organization}-bug-id: associated external change identifier
309 [[ "$1" == "--help" ]] && init && usage && exit 0
312 exec 3< "$ORIGINAL" 4> "$REVISED" || exit 1
314 while IFS= read -u3 LINE; do
317 $BUILD_PARAMS* ) do_buildparams ;;
318 $CHANGEID* ) do_changeid ;;
319 $COVERITY* ) do_coverity ;;
320 $FIXES* ) do_fixes ;;
321 $LINUX_COMMIT* ) do_lustre_commit ;;
322 $LUSTRE_CHANGE* ) do_lustre_change ;;
323 $LUSTRE_COMMIT* ) do_lustre_commit ;;
324 $SIGNOFF* ) do_signoff ;;
325 $TEST_PARAMS* ) do_testparams ;;
326 $TEST_PARAMS2* ) do_testparams ;;
331 # Do not emit blank lines before summary line or after
332 # the tag lines have begun.
334 ${NEEDS_FIRST_LINE} || ${IS_WRAPPING_UP} && continue
338 continue ## ignore and suppress comments
342 # Beginning of uncommented diffstat from "commit -v". If
343 # there are diff and index lines, skip the rest of the input:
344 # diff --git a/build/commit-msg b/build/commit-msg
345 # index 80a3442..acb4c50 100755
346 # deleted file mode 100644
348 # If a "diff --git" line is not followed by one of these
349 # lines, do the default line processing on both lines.
351 IFS= read -u3 INDEX || break
354 "index "[0-9a-fA-F]*) break ;;
355 "deleted file mode "*) break ;;
356 "old mode "*) break ;;
357 "new file mode "*) break ;;
359 LINE=${LINE}$'\n'${INDEX}
364 if [[ "$LINE" =~ ^($EMAILS): ]]; then
367 elif [[ "$LINE" =~ ^[A-Z][A-Za-z0-9_-]*-bug-id: ]]; then
368 # Allow arbitrary external bug identifiers for tracking.
381 (( NUM <= 0 )) && die "empty commit message"
384 $HAS_SIGNOFF || error "missing valid $SIGNOFF: line."
386 if $HAS_ERROR && [[ -z "$SKIP" ]]; then
393 $HAS_CHANGEID || new_changeid >&4
396 mv "$REVISED" "$ORIGINAL"
399 ## Mode: shell-script
400 ## sh-basic-offset: 8
401 ## sh-indent-after-do: 8
403 ## sh-indent-for-case-label: 0
404 ## sh-indent-for-case-alt: 8
405 ## indent-tabs-mode: t