Whamcloud - gitweb
LU-17662 osd-zfs: Support for ZFS 2.2.3
[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 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 \
29                         Acked-by \
30                         Tested-by \
31                         Reported-by \
32                         Reviewed-by \
33                         CC \
34                 | tr ' ' '|')
35
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"
42
43         # Identify a name followed by an email address.
44         #
45         readonly EMAILPAT=$'[ \t]*[-._ [:alnum:]]* <[^@ \t>]+@[a-zA-Z0-9.-]+\.[a-z]+>'
46
47         HAS_ERROR=false
48         HAS_SUMMARY=false
49         HAS_LAST_BLANK=false
50         HAS_BODY=false
51         HAS_SIGNOFF=false
52         HAS_CHANGEID=false
53         NEEDS_FIRST_LINE=true
54
55         IS_WRAPPING_UP=false
56
57         LINE=""
58         NUM=0
59         set +a
60 }
61
62 # die: commit-msg fatal error: script error or empty input message
63 # All output redirected to stderr.
64 #
65 die() {
66         echo "commit-msg fatal error:  $*"
67         test -f "$REVISED" && rm -f "$REVISED"
68         exit 1
69 } 1>&2
70
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.
73 #
74 function ck_wrapup_started() {
75         $IS_WRAPPING_UP && return
76
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."
80
81         HAS_LAST_BLANK=false
82         IS_WRAPPING_UP=true
83 }
84
85 function ck_is_ascii() {
86         LANG=C
87         [[ "${LINE//[![:alnum:][:blank:][:punct:]]/}" == "$LINE" ]] ||
88                 error "non-printable characters in '$LINE'"
89 }
90
91 function ck_wrapup() {
92         ck_wrapup_started
93         ck_is_ascii
94 }
95
96 function do_signoff() {
97         ck_wrapup_started
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"
102         else
103                 HAS_SIGNOFF=true # require at least one
104         fi
105 }
106
107 function do_changeid() {
108         ck_wrapup
109         $HAS_CHANGEID && error "multiple $CHANGEID lines not allowed"
110
111         # Change-Id: I1234567890123456789012345678901234567890
112         # capital "I" plus 40 hex digits
113         #
114         local txt=$(echo "$LINE" | grep "^$CHANGEID I[0-9a-fA-F]\{40\}\$")
115         (( ${#txt} > 0 )) ||
116                 error "has invalid $CHANGEID line for Gerrit tracking"
117
118         HAS_CHANGEID=true
119 }
120
121 function do_testparams() {
122         ck_wrapup
123
124         grep -q mdsfilesystemtype <<< $LINE &&
125                 error "mdsfilesystemtype is deprecated, use fstype"
126 }
127
128 function do_fixes() {
129         ck_wrapup
130
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\")'"
134 }
135
136 # All "emails" lines specify a person and email address
137 #
138 function do_emails() {
139         ck_wrapup_started
140         local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
141         (( ${#txt} == 0 )) && error "${LINE%: *} invalid name and email"
142 }
143
144 # All "change" lines specify a Gerrit URL
145 #
146 function do_change() {
147         local url="${LINE#*change: }"
148
149         ck_is_ascii
150         [[ $url =~ $GERRIT_URL/[0-9][0-9][0-9] ]] ||
151                 error "bad Gerrit URL, use '$GERRIT_URL/nnnnn' format"
152 }
153
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
156 #
157 function do_commit() {
158         local val=${LINE#*commit: }
159
160         ck_is_ascii
161         if [[ $val =~ TBD ]]; then
162                 val=${val#TBD (from }
163                 val=${val%)}
164         fi
165
166         [[ $val =~ [g-zG-Z] ]] && error "bad commit hash '$val', non-hex chars"
167         (( ${#val} == 40 )) || error "bad commit hash '$val', not 40 chars"
168 }
169
170 function do_default_line() {
171         $IS_WRAPPING_UP && {
172                 error "invalid signoff section line"
173                 return
174         }
175         if ${NEEDS_FIRST_LINE}; then
176                 HAS_JIRA_COMPONENT=$(echo "$LINE" | grep "$JIRA_FMT_A")
177
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."
182                         else
183                                 error "missing JIRA ticket number."
184                         fi
185                 elif (( ${#LINE} > WIDTH_SUM )); then
186                         error "summary longer than $WIDTH_SUM columns."
187                 else
188                         HAS_SUMMARY=true
189                 fi
190                 NEEDS_FIRST_LINE=false
191
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."
197         else
198                 HAS_BODY=true
199         fi
200         HAS_LAST_BLANK=false
201         ck_is_ascii
202 }
203
204 # Add a new unique Change-Id
205 #
206 new_changeid() {
207         local NEWID=$({
208                         git var GIT_AUTHOR_IDENT
209                         git var GIT_COMMITTER_IDENT
210                         git write-tree
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:"
216
217         echo "$CHANGEID I$NEWID"
218 }
219
220 # A commit message error was encountered.
221 # All output redirected to stderr.
222 #
223 error() {
224         (( ${#LINE} > 0 )) && echo "line $NUM: $LINE"
225         echo "error: commit message $*"
226         HAS_ERROR=true
227 } 1>&2
228
229 short() {
230         echo "Run '$0 --help' for longer commit message help." 1>&2
231
232         mv "$ORIGINAL" "$SAVE" &&
233                 echo "$0: saved original commit comment to $SAVE" 1>&2
234 }
235
236 usage() {
237         cat << USAGE
238
239 Normally '$0' is invoked automatically by "git commit".
240
241 See https://wiki.whamcloud.com/display/PUB/Commit+Comments
242 for full details.  A good example of a valid commit comment is:
243
244     LU-nnnnn component: short description of change under 64 columns
245
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.
251
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.
260
261     Optionally, if the patch is backported from master, include links
262     to the original patch to simplify tracking it across branches/repos:
263
264     $LUSTRE_CHANGE $GERRIT_URL/nnnn
265     $LUSTRE_COMMIT 40-char-git-hash-of-patch-on-master
266     or
267     $LUSTRE_COMMIT TBD (from 40-char-hash-of-unlanded-patch)
268
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.
273
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
278
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>"; \
282         done)
283     {Organization}-bug-id: associated external change identifier
284 USAGE
285 }
286
287 [[ "$1" == "--help" ]] && init && usage && exit 0
288
289 init ${1+"$@"}
290 exec 3< "$ORIGINAL" 4> "$REVISED" || exit 1
291
292 while IFS= read -u3 LINE; do
293         ((NUM += 1))
294         case "$LINE" in
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 ;;
303
304         "")
305                 HAS_LAST_BLANK=true
306
307                 # Do not emit blank lines before summary line or after
308                 # the tag lines have begun.
309                 #
310                 ${NEEDS_FIRST_LINE} || ${IS_WRAPPING_UP} && continue
311                 ;;
312
313         \#*)
314                 continue ## ignore and suppress comments
315                 ;;
316
317         "diff --git a/"* )
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
323                 #   old 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.
326                 #
327                 IFS= read -u3 INDEX || break
328                 ((NUM += 1))
329                 case "$INDEX" in
330                 "index "[0-9a-fA-F]*) break ;;
331                 "deleted file mode "*) break  ;;
332                 "old mode "*) break ;;
333                 "new file mode "*) break ;;
334                 esac
335                 LINE=${LINE}$'\n'${INDEX}
336                 do_default_line
337                 ;;
338
339         *)
340                 if [[ "$LINE" =~ ^($EMAILS): ]]; then
341                         do_emails
342
343                 elif [[ "$LINE" =~ ^[A-Z][A-Za-z0-9_-]*-bug-id: ]]; then
344                         # Allow arbitrary external bug identifiers for tracking.
345                         #
346                         ck_wrapup
347
348                 else
349                         do_default_line
350                 fi
351                 ;;
352         esac
353
354         echo "$LINE" >&4
355 done
356
357 (( NUM <= 0 )) && die "empty commit message"
358
359 unset LINE
360 $HAS_SIGNOFF || error "missing valid $SIGNOFF: line."
361
362 if $HAS_ERROR && [[ -z "$SKIP" ]]; then
363         exec 3<&- 4>&-
364         short
365         rm "$REVISED"
366         exit 1
367 fi
368
369 $HAS_CHANGEID || new_changeid >&4
370 exec 3<&- 4>&-
371
372 mv "$REVISED" "$ORIGINAL"
373
374 ## Local Variables:
375 ## Mode: shell-script
376 ## sh-basic-offset:          8
377 ## sh-indent-after-do:       8
378 ## sh-indentation:           8
379 ## sh-indent-for-case-label: 0
380 ## sh-indent-for-case-alt:   8
381 ## indent-tabs-mode:         t
382 ## End: