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 BUILD_PARAMS="Build-Parameters:"
24 readonly TEST_PARAMS="Test-Parameters:"
25 readonly TEST_PARAMS2="Test-parameters:"
26 readonly LUSTRE_CHANGE="Lustre-change:"
27 readonly LUSTRE_COMMIT="Lustre-commit:"
28 readonly LINUX_COMMIT="Linux-commit:"
29 readonly EMAILS=$(echo \
37 # allow temporary override for rare cases (e.g. merge commits)
38 readonly WIDTH_SUM=${WIDTH_SUM:-62}
39 readonly WIDTH_REG=${WIDTH_REG:-70}
40 readonly JIRA_FMT_A="^[A-Z]\{2,9\}-[0-9]\{1,5\} [-a-z0-9]\{2,11\}: "
41 readonly JIRA_FMT_B="^[A-Z]\{2,9\}-[0-9]\{1,5\} "
42 readonly GERRIT_URL="https://review.whamcloud.com"
44 # Identify a name followed by an email address.
46 readonly EMAILPAT=$'[ \t]*[-._ [:alnum:]]* <[^@ \t>]+@[a-zA-Z0-9.-]+\.[a-z]+>'
63 # die: commit-msg fatal error: script error or empty input message
64 # All output redirected to stderr.
67 echo "commit-msg fatal error: $*"
68 test -f "$REVISED" && rm -f "$REVISED"
72 # Called when doing the final "wrap up" clause because we've found
73 # one of the tagged lines that belongs in the final section.
75 function ck_wrapup_started() {
76 $IS_WRAPPING_UP && return
78 $HAS_LAST_BLANK || error "blank line must preceed signoff section"
79 $HAS_SUMMARY || error "missing commit summary line."
80 $HAS_BODY || error "missing commit description."
86 function ck_is_ascii() {
88 [[ "${LINE//[![:alnum:][:blank:][:punct:]]/}" == "$LINE" ]] ||
89 error "non-printable characters in '$LINE'"
92 function ck_wrapup() {
97 function do_signoff() {
99 # Signed-off-by: First Last <email@host.domain>
100 local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
101 if (( ${#txt} == 0 )); then
102 error "$SIGNOFF line needs full name and email address"
104 HAS_SIGNOFF=true # require at least one
108 function do_changeid() {
110 $HAS_CHANGEID && error "multiple $CHANGEID lines not allowed"
112 # Change-Id: I1234567890123456789012345678901234567890
113 # capital "I" plus 40 hex digits
115 local txt=$(echo "$LINE" | grep "^$CHANGEID I[0-9a-fA-F]\{40\}\$")
117 error "has invalid $CHANGEID line for Gerrit tracking"
122 function do_buildparams() {
125 grep -Eq "\<client|\<server|arch=|distro=" <<< $LINE ||
126 error "only {client,server}{distro,arch}= supported"
129 function do_testparams() {
132 grep -q mdsfilesystemtype <<< $LINE &&
133 error "mdsfilesystemtype is deprecated, use fstype"
136 function do_fixes() {
139 local commit=$(awk '{ print $2 }' <<<$LINE)
140 git describe --tags $commit 2>&1 | grep "[Nn]ot a valid" &&
141 error "invalid $FIXES hash, want '<12hex> (\"summary line\")'"
144 # All "emails" lines specify a person and email address
146 function do_emails() {
148 local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
149 (( ${#txt} == 0 )) && error "${LINE%: *} invalid name and email"
152 # All "change" lines specify a Gerrit URL
154 function do_change() {
155 local url="${LINE#*change: }"
158 [[ $url =~ $GERRIT_URL/[0-9][0-9][0-9] ]] ||
159 error "bad Gerrit URL, use '$GERRIT_URL/nnnnn' format"
162 # All "commit" lines specify a commit hash, but the commit may be in
163 # another repo, so the hash can't be directly verified
165 function do_commit() {
166 local val=${LINE#*commit: }
169 if [[ $val =~ TBD ]]; then
170 val=${val#TBD (from }
174 [[ $val =~ [g-zG-Z] ]] && error "bad commit hash '$val', non-hex chars"
175 (( ${#val} == 40 )) || error "bad commit hash '$val', not 40 chars"
178 function do_default_line() {
180 error "invalid signoff section line"
183 if ${NEEDS_FIRST_LINE}; then
184 HAS_JIRA_COMPONENT=$(echo "$LINE" | grep "$JIRA_FMT_A")
186 if (( ${#HAS_JIRA_COMPONENT} == 0 )); then
187 HAS_JIRA=$(echo "$LINE" | grep "$JIRA_FMT_B")
188 if (( ${#HAS_JIRA} > 0 )); then
189 error "has no component in summary."
191 error "missing JIRA ticket number."
193 elif (( ${#LINE} > WIDTH_SUM )); then
194 error "summary longer than $WIDTH_SUM columns."
198 NEEDS_FIRST_LINE=false
200 elif (( ${#LINE} > WIDTH_REG )) && ! [[ $LINE =~ http ]]; then
201 # ignore long lines containing URLs
202 error "has line longer than $WIDTH_REG columns."
203 elif ! $HAS_BODY && ! $HAS_LAST_BLANK; then
204 error "has no blank line after summary."
212 # Add a new unique Change-Id
216 git var GIT_AUTHOR_IDENT
217 git var GIT_COMMITTER_IDENT
219 git rev-parse HEAD 2>/dev/null
220 grep -v "^$SIGNOFF" "$ORIGINAL" | git stripspace -s
221 } | git hash-object --stdin)
222 (( ${#NEWID} > 0 )) ||
223 die "git hash-object failed for $CHANGEID:"
225 echo "$CHANGEID I$NEWID"
228 # A commit message error was encountered.
229 # All output redirected to stderr.
232 (( ${#LINE} > 0 )) && echo "line $NUM: $LINE"
233 echo "error: commit message $*"
238 echo "Run '$0 --help' for longer commit message help." 1>&2
240 mv "$ORIGINAL" "$SAVE" &&
241 echo "$0: saved original commit comment to $SAVE" 1>&2
247 Normally '$0' is invoked automatically by "git commit".
249 See https://wiki.whamcloud.com/display/PUB/Commit+Comments
250 for full details. A good example of a valid commit comment is:
252 LU-nnnnn component: short description of change under 64 columns
254 The "component:" should be a lower-case single-word subsystem of the
255 Lustre code best covering the patch. Example components include:
256 llite, lov, lmv, osc, mdc, ldlm, lnet, ptlrpc, mds, oss, osd,
257 ldiskfs, libcfs, socklnd, o2iblnd, recovery, quota, grant,
258 build, tests, docs. This list is not exhaustive, but a guideline.
260 The comment body should explan the change being made. This can be
261 as long as needed. Please include details of the problem that was
262 solved (including error messages that were seen), a good high-level
263 description of how it was solved, and which parts of the code were
264 changed (including important functions that were changed, if this
265 is useful to understand the patch, and for easier searching).
266 Performance patches should quanify the improvements being seen.
267 Wrap lines at/under $WIDTH_REG columns. Only ASCII text allowed.
269 Optionally, if the patch is backported from master, include links
270 to the original patch to simplify tracking it across branches/repos:
272 $LUSTRE_CHANGE $GERRIT_URL/nnnn
273 $LUSTRE_COMMIT 40-char-git-hash-of-patch-on-master
275 $LUSTRE_COMMIT TBD (from 40-char-hash-of-unlanded-patch)
277 Finish the comment with a blank line followed by the signoff section.
278 The "$CHANGEID" line should only be present when updating a previous
279 commit/submission. Keep the same $CHANGEID for ported patches. It
280 will automatically be added by the Git commit-msg hook if missing.
282 $BUILD_PARAMS extra build options, see https://build.whamcloud.com/
283 $TEST_PARAMS extra test options, see https://wiki.whamcloud.com/x/dICC
284 $FIXES 12-char-hash ("commit summary line of original broken patch")
285 $SIGNOFF Your Real Name <your_email@domain.name>
286 $CHANGEID Ixxxx(added automatically if missing)xxxx
288 The "signoff section" may optionally include other reviewer lines:
289 $(for T in $(tr '|' ' ' <<< "$EMAILS"); do \
290 echo " $T: Some Person <email@example.com>"; \
292 {Organization}-bug-id: associated external change identifier
296 [[ "$1" == "--help" ]] && init && usage && exit 0
299 exec 3< "$ORIGINAL" 4> "$REVISED" || exit 1
301 while IFS= read -u3 LINE; do
304 $SIGNOFF* ) do_signoff ;;
305 $CHANGEID* ) do_changeid ;;
306 $FIXES* ) do_fixes ;;
307 $BUILD_PARAMS* ) do_buildparams ;;
308 $TEST_PARAMS* ) do_testparams ;;
309 $TEST_PARAMS2* ) do_testparams ;;
310 $LUSTRE_CHANGE* ) do_change ;;
311 $LUSTRE_COMMIT* ) do_commit ;;
312 $LINUX_COMMIT* ) do_commit ;;
317 # Do not emit blank lines before summary line or after
318 # the tag lines have begun.
320 ${NEEDS_FIRST_LINE} || ${IS_WRAPPING_UP} && continue
324 continue ## ignore and suppress comments
328 # Beginning of uncommented diffstat from "commit -v". If
329 # there are diff and index lines, skip the rest of the input:
330 # diff --git a/build/commit-msg b/build/commit-msg
331 # index 80a3442..acb4c50 100755
332 # deleted file mode 100644
334 # If a "diff --git" line is not followed by one of these
335 # lines, do the default line processing on both lines.
337 IFS= read -u3 INDEX || break
340 "index "[0-9a-fA-F]*) break ;;
341 "deleted file mode "*) break ;;
342 "old mode "*) break ;;
343 "new file mode "*) break ;;
345 LINE=${LINE}$'\n'${INDEX}
350 if [[ "$LINE" =~ ^($EMAILS): ]]; then
353 elif [[ "$LINE" =~ ^[A-Z][A-Za-z0-9_-]*-bug-id: ]]; then
354 # Allow arbitrary external bug identifiers for tracking.
367 (( NUM <= 0 )) && die "empty commit message"
370 $HAS_SIGNOFF || error "missing valid $SIGNOFF: line."
372 if $HAS_ERROR && [[ -z "$SKIP" ]]; then
379 $HAS_CHANGEID || new_changeid >&4
382 mv "$REVISED" "$ORIGINAL"
385 ## Mode: shell-script
386 ## sh-basic-offset: 8
387 ## sh-indent-after-do: 8
389 ## sh-indent-for-case-label: 0
390 ## sh-indent-for-case-alt: 8
391 ## indent-tabs-mode: t