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() {
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 do_signoff() {
87 # Signed-off-by: First Last <email@host.domain>
88 local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
89 if (( ${#txt} == 0 )); then
90 error "$SIGNOFF line needs full name and email address"
92 HAS_SIGNOFF=true # require at least one
96 function do_changeid() {
98 $HAS_CHANGEID && error "multiple $CHANGEID lines not allowed"
100 # Change-Id: I1234567890123456789012345678901234567890
101 # capital "I" plus 40 hex digits
103 local txt=$(echo "$LINE" | grep "^$CHANGEID I[0-9a-fA-F]\{40\}\$")
105 error "has invalid $CHANGEID line for Gerrit tracking"
110 function do_testparams() {
113 grep -q mdsfilesystemtype <<< $LINE &&
114 error "mdsfilesystemtype is deprecated, use fstype"
117 function do_fixes() {
120 local commit=$(awk '{ print $2 }' <<<$LINE)
121 git describe --tags $commit 2>&1 | grep "[Nn]ot a valid" &&
122 error "invalid $FIXES hash, want '<12hex> (\"summary line\")'"
125 # All "emails" lines specify a person and email address
127 function do_emails() {
129 local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
130 (( ${#txt} == 0 )) && error "${LINE%: *} invalid name and email"
133 # All "change" lines specify a Gerrit URL
135 function do_change() {
136 local url="${LINE#*change: }"
137 [[ $url =~ $GERRIT_URL/[0-9][0-9][0-9] ]] ||
138 error "bad Gerrit URL, use '$GERRIT_URL/nnnnn' format"
141 # All "commit" lines specify a commit hash, but the commit may be in
142 # another repo, so the hash can't be directly verified
144 function do_commit() {
145 local val=${LINE#*commit: }
146 if [[ $val =~ TBD ]]; then
147 val=${val#TBD (from }
151 [[ $val =~ [g-zG-Z] ]] && error "bad commit hash '$val', non-hex chars"
152 (( ${#val} == 40 )) || error "bad commit hash '$val', not 40 chars"
155 function do_default_line() {
157 error "invalid signoff section line"
160 if ${NEEDS_FIRST_LINE}; then
161 HAS_JIRA_COMPONENT=$(echo "$LINE" | grep "$JIRA_FMT_A")
163 if (( ${#HAS_JIRA_COMPONENT} == 0 )); then
164 HAS_JIRA=$(echo "$LINE" | grep "$JIRA_FMT_B")
165 if (( ${#HAS_JIRA} > 0 )); then
166 error "has no component in summary."
168 error "missing JIRA ticket number."
170 elif (( ${#LINE} > WIDTH_SUM )); then
171 error "summary longer than $WIDTH_SUM columns."
175 NEEDS_FIRST_LINE=false
177 elif (( ${#LINE} > WIDTH_REG )) && ! [[ $LINE =~ http ]]; then
178 # ignore long lines containing URLs
179 error "has line longer than $WIDTH_REG columns."
180 elif ! $HAS_BODY && ! $HAS_LAST_BLANK; then
181 error "has no blank line after summary."
188 # Add a new unique Change-Id
192 git var GIT_AUTHOR_IDENT
193 git var GIT_COMMITTER_IDENT
195 git rev-parse HEAD 2>/dev/null
196 grep -v "^$SIGNOFF" "$ORIGINAL" | git stripspace -s
197 } | git hash-object --stdin)
198 (( ${#NEWID} > 0 )) ||
199 die "git hash-object failed for $CHANGEID:"
201 echo "$CHANGEID I$NEWID"
204 # A commit message error was encountered.
205 # All output redirected to stderr.
208 (( ${#LINE} > 0 )) && echo "line $NUM: $LINE"
209 echo "error: commit message $*"
214 echo "Run '$0 --help' for longer commit message help." 1>&2
216 mv "$ORIGINAL" "$SAVE" &&
217 echo "$0: saved original commit comment to $SAVE" 1>&2
223 Normally '$0' is invoked automatically by "git commit".
225 See https://wiki.whamcloud.com/display/PUB/Commit+Comments
226 for full details. A good example of a valid commit comment is:
228 LU-nnnnn component: short description of change under 64 columns
230 The "component:" should be a lower-case single-word subsystem of the
231 Lustre code best covering the patch. Example components include:
232 llite, lov, lmv, osc, mdc, ldlm, lnet, ptlrpc, mds, oss, osd,
233 ldiskfs, libcfs, socklnd, o2iblnd, recovery, quota, grant,
234 build, tests, docs. This list is not exhaustive, but a guideline.
236 The comment body should explan the change being made. This can be
237 as long as needed. Please include details of the problem that was
238 solved (including error messages that were seen), a good high-level
239 description of how it was solved, and which parts of the code were
240 changed (including important functions that were changed, if this
241 is useful to understand the patch, and for easier searching).
242 Performance patches should quanify the improvements being seen.
243 Wrap lines at/under $WIDTH_REG columns.
245 Optionally, if the patch is backported from master, include links
246 to the original patch to simplify tracking it across branches/repos:
248 $LUSTRE_CHANGE $GERRIT_URL/nnnn
249 $LUSTRE_COMMIT 40-char-git-hash-of-patch-on-master
251 $LUSTRE_COMMIT TBD (from 40-char-hash-of-unlanded-patch)
253 Finish the comment with a blank line followed by the signoff section.
254 The "$CHANGEID" line should only be present when updating a previous
255 commit/submission. Keep the same $CHANGEID for ported patches. It
256 will automatically be added by the Git commit-msg hook if missing.
258 $TEST_PARAMS extra test options, see https://wiki.whamcloud.com/x/dICC
259 $FIXES 12-char-hash ("commit summary line of original broken patch")
260 $SIGNOFF Your Real Name <your_email@domain.name>
261 $CHANGEID Ixxxx(added automatically if missing)xxxx
263 The "signoff section" may optionally include other reviewer lines:
264 $(for T in $(tr '|' ' ' <<< "$EMAILS"); do \
265 echo " $T: Some Person <email@example.com>"; \
267 {Organization}-bug-id: associated external change identifier
271 [[ "$1" == "--help" ]] && init && usage && exit 0
274 exec 3< "$ORIGINAL" 4> "$REVISED" || exit 1
276 while IFS= read -u3 LINE; do
279 $SIGNOFF* ) do_signoff ;;
280 $CHANGEID* ) do_changeid ;;
281 $FIXES* ) do_fixes ;;
282 $TEST_PARAMS* ) do_testparams ;;
283 $TEST_PARAMS2* ) do_testparams ;;
284 $LUSTRE_CHANGE* ) do_change ;;
285 $LUSTRE_COMMIT* ) do_commit ;;
286 $LINUX_COMMIT* ) do_commit ;;
291 # Do not emit blank lines before summary line or after
292 # the tag lines have begun.
294 ${NEEDS_FIRST_LINE} || ${IS_WRAPPING_UP} && continue
298 continue ## ignore and suppress comments
302 # Beginning of uncommented diffstat from "commit -v". If
303 # there are diff and index lines, skip the rest of the input:
304 # diff --git a/build/commit-msg b/build/commit-msg
305 # index 80a3442..acb4c50 100755
306 # deleted file mode 100644
308 # If a "diff --git" line is not followed by one of these
309 # lines, do the default line processing on both lines.
311 IFS= read -u3 INDEX || break
314 "index "[0-9a-fA-F]*) break ;;
315 "deleted file mode "*) break ;;
316 "old mode "*) break ;;
317 "new file mode "*) break ;;
319 LINE=${LINE}$'\n'${INDEX}
324 if [[ "$LINE" =~ ^($EMAILS): ]]; then
327 elif [[ "$LINE" =~ ^[A-Z][A-Za-z0-9_-]*-bug-id: ]]; then
328 # Allow arbitrary external bug identifiers for tracking.
341 (( NUM <= 0 )) && die "empty commit message"
344 $HAS_SIGNOFF || error "missing valid $SIGNOFF: line."
346 if $HAS_ERROR && [[ -z "$SKIP" ]]; then
353 $HAS_CHANGEID || new_changeid >&4
356 mv "$REVISED" "$ORIGINAL"
359 ## Mode: shell-script
360 ## sh-basic-offset: 8
361 ## sh-indent-after-do: 8
363 ## sh-indent-for-case-label: 0
364 ## sh-indent-for-case-alt: 8
365 ## indent-tabs-mode: t