Whamcloud - gitweb
LU-19098 hsm: don't print progname twice with lhsmtool
[fs/lustre-release.git] / contrib / git-hooks / commit-msg
1 #!/bin/bash
2 #
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).
6 #
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.
11 #
12 # Should be installed as .git/hooks/commit-msg.
13 #
14
15 init() {
16         set -a
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 \
31                         Acked-by \
32                         Tested-by \
33                         Reported-by \
34                         Reviewed-by \
35                         CC \
36                 | tr ' ' '|')
37
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"
44
45         # Identify a name followed by an email address.
46         #
47         readonly EMAILPAT=$'[ \t]*[-._ [:alnum:]]* <[^@ \t>]+@[a-zA-Z0-9.-]+\.[a-z]+>'
48
49         HAS_ERROR=false
50         HAS_SUMMARY=false
51         HAS_LAST_BLANK=false
52         HAS_BODY=false
53         HAS_SIGNOFF=false
54         HAS_CHANGEID=false
55         NEEDS_FIRST_LINE=true
56
57         IS_WRAPPING_UP=false
58
59         LINE=""
60         NUM=0
61         set +a
62 }
63
64 # die: commit-msg fatal error: script error or empty input message
65 # All output redirected to stderr.
66 #
67 die() {
68         echo "commit-msg fatal error:  $*"
69         test -f "$REVISED" && rm -f "$REVISED"
70         exit 1
71 } 1>&2
72
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.
75 #
76 function ck_wrapup_started() {
77         $IS_WRAPPING_UP && return
78
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."
82
83         HAS_LAST_BLANK=false
84         IS_WRAPPING_UP=true
85 }
86
87 function ck_is_ascii() {
88         LANG=C
89         [[ "${LINE//[![:alnum:][:blank:][:punct:]]/}" == "$LINE" ]] ||
90                 error "non-printable characters in '$LINE'"
91 }
92
93 function ck_wrapup() {
94         ck_wrapup_started
95         ck_is_ascii
96 }
97
98 function do_signoff() {
99         ck_wrapup_started
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"
104         else
105                 HAS_SIGNOFF=true # require at least one
106         fi
107 }
108
109 function do_changeid() {
110         ck_wrapup
111         $HAS_CHANGEID && error "multiple $CHANGEID lines not allowed"
112
113         # Change-Id: I1234567890123456789012345678901234567890
114         # capital "I" plus 40 hex digits
115         #
116         local txt=$(echo "$LINE" | grep "^$CHANGEID I[0-9a-fA-F]\{40\}\$")
117         (( ${#txt} > 0 )) ||
118                 error "has invalid $CHANGEID line for Gerrit tracking"
119
120         HAS_CHANGEID=true
121 }
122
123 function do_buildparams() {
124         ck_wrapup
125
126         grep -Eq "\<client|\<server|arch=|distro=" <<< $LINE ||
127                 error "only {client,server}{distro,arch}= supported"
128 }
129
130 function do_coverity() {
131         local cid=$(awk '{ print $2 }' <<<$LINE)
132
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\")'"
137 }
138
139 function do_testparams() {
140         ck_wrapup
141
142         grep -q mdsfilesystemtype <<< $LINE &&
143                 error "mdsfilesystemtype is deprecated, use fstype"
144 }
145
146 function do_fixes() {
147         ck_wrapup
148
149         local commit=$(awk '{ print $2 }' <<<$LINE)
150
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\")'"
155 }
156
157 # All "emails" lines specify a person and email address
158 #
159 function do_emails() {
160         ck_wrapup_started
161         local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
162         (( ${#txt} == 0 )) && error "${LINE%: *} invalid name and email"
163 }
164
165 # All "change" lines specify a Gerrit URL
166 #
167 function do_lustre_change() {
168         local url="${LINE#*change: }"
169
170         ck_is_ascii
171         [[ $url =~ $GERRIT_URL/[0-9][0-9][0-9] ]] ||
172                 error "bad Gerrit URL, use '$GERRIT_URL/nnnnn' format"
173 }
174
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
177 #
178 function do_lustre_commit() {
179         local val=${LINE#*commit: }
180
181         ck_is_ascii
182         if [[ $val =~ TBD ]]; then
183                 val=${val#TBD (from }
184                 val=${val%)}
185         fi
186
187         [[ $val =~ [g-zG-Z] ]] && error "bad commit hash '$val', non-hex chars"
188         (( ${#val} == 40 )) || error "bad commit hash '$val', not 40 chars"
189 }
190
191 function do_default_line() {
192         $IS_WRAPPING_UP && {
193                 error "invalid signoff section line"
194                 return
195         }
196         if ${NEEDS_FIRST_LINE}; then
197                 HAS_JIRA_COMPONENT=$(echo "$LINE" | grep "$JIRA_FMT_A")
198
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."
203                         else
204                                 error "missing JIRA ticket number."
205                         fi
206                 elif (( ${#LINE} > WIDTH_SUM )); then
207                         error "summary longer than $WIDTH_SUM columns."
208                 else
209                         HAS_SUMMARY=true
210                 fi
211                 NEEDS_FIRST_LINE=false
212
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."
218         else
219                 HAS_BODY=true
220         fi
221         HAS_LAST_BLANK=false
222         ck_is_ascii
223 }
224
225 # Add a new unique Change-Id
226 #
227 new_changeid() {
228         local NEWID=$({
229                         git var GIT_AUTHOR_IDENT
230                         git var GIT_COMMITTER_IDENT
231                         git write-tree
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:"
237
238         echo "$CHANGEID I$NEWID"
239 }
240
241 # A commit message error was encountered.
242 # All output redirected to stderr.
243 #
244 error() {
245         (( ${#LINE} > 0 )) && echo "line $NUM: $LINE"
246         echo "error: commit message $*"
247         HAS_ERROR=true
248 } 1>&2
249
250 short() {
251         echo "Run '$0 --help' for longer commit message help." 1>&2
252
253         mv "$ORIGINAL" "$SAVE" &&
254                 echo "$0: saved original commit comment to $SAVE" 1>&2
255 }
256
257 usage() {
258         cat << USAGE
259
260 Normally '$0' is invoked automatically by "git commit".
261
262 See https://wiki.whamcloud.com/display/PUB/Commit+Comments
263 for full details.  A good example of a valid commit comment is:
264
265     LU-nnnnn component: short description of change under 64 columns
266
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.
272
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.
281
282     Optionally, if the patch is backported from master, include links
283     to the original patch to simplify tracking it across branches/repos:
284
285     $LUSTRE_CHANGE $GERRIT_URL/nnnn
286     $LUSTRE_COMMIT 40-char-git-hash-of-patch-on-master
287     or
288     $LUSTRE_COMMIT TBD (from 40-char-hash-of-unlanded-patch)
289
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.
294
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
300
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>"; \
304         done)
305     {Organization}-bug-id: associated external change identifier
306 USAGE
307 }
308
309 [[ "$1" == "--help" ]] && init && usage && exit 0
310
311 init ${1+"$@"}
312 exec 3< "$ORIGINAL" 4> "$REVISED" || exit 1
313
314 while IFS= read -u3 LINE; do
315         ((NUM += 1))
316         case "$LINE" in
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 ;;
327
328         "")
329                 HAS_LAST_BLANK=true
330
331                 # Do not emit blank lines before summary line or after
332                 # the tag lines have begun.
333                 #
334                 ${NEEDS_FIRST_LINE} || ${IS_WRAPPING_UP} && continue
335                 ;;
336
337         \#*)
338                 continue ## ignore and suppress comments
339                 ;;
340
341         "diff --git a/"* )
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
347                 #   old 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.
350                 #
351                 IFS= read -u3 INDEX || break
352                 ((NUM += 1))
353                 case "$INDEX" in
354                 "index "[0-9a-fA-F]*) break ;;
355                 "deleted file mode "*) break  ;;
356                 "old mode "*) break ;;
357                 "new file mode "*) break ;;
358                 esac
359                 LINE=${LINE}$'\n'${INDEX}
360                 do_default_line
361                 ;;
362
363         *)
364                 if [[ "$LINE" =~ ^($EMAILS): ]]; then
365                         do_emails
366
367                 elif [[ "$LINE" =~ ^[A-Z][A-Za-z0-9_-]*-bug-id: ]]; then
368                         # Allow arbitrary external bug identifiers for tracking.
369                         #
370                         ck_wrapup
371
372                 else
373                         do_default_line
374                 fi
375                 ;;
376         esac
377
378         echo "$LINE" >&4
379 done
380
381 (( NUM <= 0 )) && die "empty commit message"
382
383 unset LINE
384 $HAS_SIGNOFF || error "missing valid $SIGNOFF: line."
385
386 if $HAS_ERROR && [[ -z "$SKIP" ]]; then
387         exec 3<&- 4>&-
388         short
389         rm "$REVISED"
390         exit 1
391 fi
392
393 $HAS_CHANGEID || new_changeid >&4
394 exec 3<&- 4>&-
395
396 mv "$REVISED" "$ORIGINAL"
397
398 ## Local Variables:
399 ## Mode: shell-script
400 ## sh-basic-offset:          8
401 ## sh-indent-after-do:       8
402 ## sh-indentation:           8
403 ## sh-indent-for-case-label: 0
404 ## sh-indent-for-case-alt:   8
405 ## indent-tabs-mode:         t
406 ## End: