Whamcloud - gitweb
LU-9019 lfsck: migrate to 64 bit time
[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 TESTPARAMS="Test-Parameters:"
23         readonly INNOCUOUS=$(echo \
24                         Acked-by \
25                         Tested-by \
26                         Reported-by \
27                         Reviewed-by \
28                         CC \
29                 | tr ' ' '|')
30         readonly WIDTH_SUM=62
31         readonly WIDTH_REG=70
32         readonly JIRA_FMT_A="^[A-Z]\{2,9\}-[0-9]\{1,5\} [-a-z0-9]\{2,11\}: "
33         readonly JIRA_FMT_B="^[A-Z]\{2,9\}-[0-9]\{1,5\} "
34
35         # Identify a name followed by an email address.
36         #
37         readonly EMAILPAT=$'[ \t]*[^<> ]* [^<>]* <[^@ \t>]+@[a-zA-Z0-9.-]+\.[a-z]+>'
38
39         HAS_ERROR=false
40         HAS_SUMMARY=false
41         HAS_LAST_BLANK=false
42         HAS_BODY=false
43         HAS_SIGNOFF=false
44         HAS_CHANGEID=false
45         NEEDS_FIRST_LINE=true
46
47         IS_WRAPPING_UP=false
48
49         LINE=""
50         NUM=0
51         set +a
52 }
53
54 # die: commit-msg fatal error: script error or empty input message
55 # All output redirected to stderr.
56 #
57 die() {
58         echo "commit-msg fatal error:  $*"
59         test -f "$REVISED" && rm -f "$REVISED"
60         exit 1
61 } 1>&2
62
63 # Called when doing the final "wrap up" clause because we've found
64 # one of the tagged lines that belongs in the final section.
65 #
66 function ck_wrapup() {
67         $IS_WRAPPING_UP && return
68
69         $HAS_LAST_BLANK || error "blank line must preceed signoff section"
70         $HAS_SUMMARY    || error "missing commit summary line."
71         $HAS_BODY       || error "missing commit description."
72
73         HAS_LAST_BLANK=false
74         IS_WRAPPING_UP=true
75 }
76
77 function do_signoff() {
78         ck_wrapup
79         # Signed-off-by: First Last <email@host.domain>
80         local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
81         if (( ${#txt} == 0 )); then
82                 error "$SIGNOFF line requires name and email address"
83         else
84                 HAS_SIGNOFF=true # require at least one
85         fi
86 }
87
88 function do_changeid() {
89         ck_wrapup
90         $HAS_CHANGEID && error "multiple $CHANGEID lines are not allowed"
91
92         # Change-Id: I1234567890123456789012345678901234567890
93         # capital "I" plus 40 hex digits
94         #
95         local txt=$(echo "$LINE" | grep "^$CHANGEID I[0-9a-fA-F]\{40\}\$")
96         (( ${#txt} > 0 )) ||
97                 error "has invalid $CHANGEID line for Gerrit tracking"
98
99         HAS_CHANGEID=true
100 }
101
102 function do_testparams() {
103         ck_wrapup
104
105         grep -q mdsfilesystemtype <<< $LINE &&
106                 error "mdsfilesystemtype is deprecated, use mdtfilesystemtype"
107 }
108
109 # All "innocuous" lines specify a person and email address
110 #
111 function do_innocuous() {
112         ck_wrapup
113         local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
114         (( ${#txt} == 0 )) && error "invalid name and address"
115 }
116
117 function do_default_line() {
118         $IS_WRAPPING_UP && {
119                 error "invalid signoff section line"
120                 return
121         }
122         if ${NEEDS_FIRST_LINE}; then
123                 HAS_JIRA_COMPONENT=$(echo "$LINE" | grep "$JIRA_FMT_A")
124
125                 if (( ${#HAS_JIRA_COMPONENT} == 0 )); then
126                         HAS_JIRA=$(echo "$LINE" | grep "$JIRA_FMT_B")
127                         if (( ${#HAS_JIRA} > 0 )); then
128                                 error "has no component in summary."
129                         else
130                                 error "missing JIRA ticket number."
131                         fi
132                 elif (( ${#LINE} > WIDTH_SUM )); then
133                         error "summary longer than $WIDTH_SUM columns."
134                 else
135                         HAS_SUMMARY=true
136                 fi
137                 NEEDS_FIRST_LINE=false
138
139         elif (( ${#LINE} > WIDTH_REG )); then
140                 error "has line longer than $WIDTH_REG columns."
141         elif ! $HAS_BODY && ! $HAS_LAST_BLANK; then
142                 error "has no blank line after summary."
143         else
144                 HAS_BODY=true
145         fi
146         HAS_LAST_BLANK=false
147 }
148
149 # Add a new unique Change-Id
150 #
151 new_changeid() {
152         local NEWID=$({
153                         git var GIT_AUTHOR_IDENT
154                         git var GIT_COMMITTER_IDENT
155                         git write-tree
156                         git rev-parse HEAD 2>/dev/null
157                         grep -v "^$SIGNOFF" "$ORIGINAL" | git stripspace -s
158                 } | git hash-object --stdin)
159         (( ${#NEWID} > 0 )) ||
160                 die "git hash-object failed for $CHANGEID:"
161
162         echo "$CHANGEID I$NEWID"
163 }
164
165 # A commit message error was encountered.
166 # All output redirected to stderr.
167 #
168 error() {
169         (( ${#LINE} > 0 )) && echo "line $NUM: $LINE"
170         echo "error: commit message $*" | fmt
171         HAS_ERROR=true
172 } 1>&2
173
174 usage() {
175         exec 1>&2
176         cat <<- EOF
177
178         See https://wiki.hpdd.intel.com/display/PUB/Commit+Comments
179         for full details.  An example valid commit comment is:
180
181         LU-nnn component: short description of change under 64 columns
182
183         The "component:" should be a lower-case single-word subsystem of the
184         Lustre code that best encompasses the change being made.  Examples of
185         components include modules like: llite, lov, lmv, osc, mdc, ldlm, lnet,
186         ptlrpc, mds, oss, osd, ldiskfs, libcfs, socklnd, o2iblnd; functional
187         subsystems like: recovery, quota, grant; and auxilliary areas like:
188         build, tests, docs.  This list is not exhaustive, but is a guideline.
189
190         The commit comment should contain a detailed explanation of the change
191         being made.  This can be as long as you'd like.  Please give details
192         of what problem was solved (including error messages or problems that
193         were seen), a good high-level description of how it was solved, and
194         which parts of the code were changed (including important functions
195         that were changed, if this is useful to understand the patch, and
196         for easier searching).  Wrap lines at/under $WIDTH_REG columns.
197
198         Finish the comment with a blank line and a blank-line-free
199         sign off section:
200
201         $SIGNOFF Your Real Name <your_email@domain.name>
202         $CHANGEID Ixxxx(added automatically if missing)xxxx
203
204         The "$CHANGEID" line should only be there when updating a previous
205         commit/submission.  Copy the one from the original commit.
206
207         The "sign off section" may also include several other tag lines:
208         $(for T in $(tr '|' ' ' <<< "$INNOCUOUS"); do       \
209              echo "    $T: Some Person <email@domain.com>"; \
210           done)
211         $TESTPARAMS optional additional test parameters
212         {Organization}-bug-id: associated external change identifier
213         EOF
214
215         mv "$ORIGINAL" "$SAVE" &&
216                 echo "$0: saved original commit comment to $SAVE" 1>&2
217 }
218
219 init ${1+"$@"}
220 exec 3< "$ORIGINAL" 4> "$REVISED" || exit 1
221
222 while IFS= read -u3 LINE; do
223         ((NUM += 1))
224         case "$LINE" in
225         $SIGNOFF* )   do_signoff     ;;
226         $CHANGEID* )  do_changeid    ;;
227         $TESTPARAMS* ) do_testparams ;;
228
229         "")
230                 HAS_LAST_BLANK=true
231
232                 # Do not emit blank lines before summary line or after
233                 # the tag lines have begun.
234                 #
235                 ${NEEDS_FIRST_LINE} || ${IS_WRAPPING_UP} && continue
236                 ;;
237
238         \#*)
239                 continue ## ignore and suppress comments
240                 ;;
241
242         "diff --git a/"* )
243                 # Beginning of uncommented diffstat from "commit -v".  If
244                 # there are diff and index lines, skip the rest of the input:
245                 #   diff --git a/build/commit-msg b/build/commit-msg
246                 #   index 80a3442..acb4c50 100755
247                 #   deleted file mode 100644
248                 #   old mode 100644
249                 # If a "diff --git" line is not followed by one of these
250                 # lines, do the default line processing on both lines.
251                 #
252                 IFS= read -u3 INDEX || break
253                 ((NUM += 1))
254                 case "$INDEX" in
255                 "index "[0-9a-fA-F]*) break ;;
256                 "deleted file mode "*) break  ;;
257                 "old mode "*) break ;;
258                 "new file mode "*) break ;;
259                 esac
260                 LINE=${LINE}$'\n'${INDEX}
261                 do_default_line
262                 ;;
263
264         *)
265                 if [[ "$LINE" =~ ^($INNOCUOUS): ]]; then
266                         do_innocuous
267
268                 elif [[ "$LINE" =~ ^[A-Za-z0-9_-]+-bug-id: ]]; then
269                         # Allow arbitrary external bug identifiers for tracking.
270                         #
271                         ck_wrapup
272
273                 else
274                         do_default_line
275                 fi
276                 ;;
277         esac
278
279         echo "$LINE" >&4
280 done
281
282 (( NUM <= 0 )) && die "empty commit message"
283
284 unset LINE
285 $HAS_SIGNOFF || error "missing valid $SIGNOFF: line."
286
287 if $HAS_ERROR; then
288         exec 3<&- 4>&-
289         usage
290         rm "$REVISED"
291         exit 1
292 fi
293
294 $HAS_CHANGEID || new_changeid >&4
295 exec 3<&- 4>&-
296
297 mv "$REVISED" "$ORIGINAL"
298
299 ## Local Variables:
300 ## Mode: shell-script
301 ## sh-basic-offset:          8
302 ## sh-indent-after-do:       8
303 ## sh-indentation:           8
304 ## sh-indent-for-case-label: 0
305 ## sh-indent-for-case-alt:   8
306 ## indent-tabs-mode:         t
307 ## End: