Whamcloud - gitweb
c1fed2ee1f9b4a43bb6ee5a6daf65c9d49cfbfad
[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 # All "innocuous" lines specify a person and email address
103 #
104 function do_innocuous() {
105         ck_wrapup
106         local txt=$(echo "${LINE#*: }" | grep -E "${EMAILPAT}")
107         (( ${#txt} == 0 )) && error "invalid name and address"
108 }
109
110 function do_default_line() {
111         $IS_WRAPPING_UP && {
112                 error "invalid signoff section line"
113                 return
114         }
115         if ${NEEDS_FIRST_LINE}; then
116                 HAS_JIRA_COMPONENT=$(echo "$LINE" | grep "$JIRA_FMT_A")
117
118                 if (( ${#HAS_JIRA_COMPONENT} == 0 )); then
119                         HAS_JIRA=$(echo "$LINE" | grep "$JIRA_FMT_B")
120                         if (( ${#HAS_JIRA} > 0 )); then
121                                 error "has no component in summary."
122                         else
123                                 error "missing JIRA ticket number."
124                         fi
125                 elif (( ${#LINE} > WIDTH_SUM )); then
126                         error "summary longer than $WIDTH_SUM columns."
127                 else
128                         HAS_SUMMARY=true
129                 fi
130                 NEEDS_FIRST_LINE=false
131
132         elif (( ${#LINE} > WIDTH_REG )); then
133                 error "has line longer than $WIDTH_REG columns."
134         elif ! $HAS_BODY && ! $HAS_LAST_BLANK; then
135                 error "has no blank line after summary."
136         else
137                 HAS_BODY=true
138         fi
139         HAS_LAST_BLANK=false
140 }
141
142 # Add a new unique Change-Id
143 #
144 new_changeid() {
145         local NEWID=$({
146                         git var GIT_AUTHOR_IDENT
147                         git var GIT_COMMITTER_IDENT
148                         git write-tree
149                         git rev-parse HEAD 2>/dev/null
150                         grep -v "^$SIGNOFF" "$ORIGINAL" | git stripspace -s
151                 } | git hash-object --stdin)
152         (( ${#NEWID} > 0 )) ||
153                 die "git hash-object failed for $CHANGEID:"
154
155         echo "$CHANGEID I$NEWID"
156 }
157
158 # A commit message error was encountered.
159 # All output redirected to stderr.
160 #
161 error() {
162         (( ${#LINE} > 0 )) && echo "line $NUM: $LINE"
163         echo "error: commit message $*" | fmt
164         HAS_ERROR=true
165 } 1>&2
166
167 usage() {
168         exec 1>&2
169         cat <<- EOF
170
171         See http://wiki.whamcloud.com/display/PUB/Commit+Comments
172         for full details.  An example valid commit comment is:
173
174         LU-nnn component: short description of change under 64 columns
175
176         The "component:" should be a lower-case single-word subsystem of the
177         Lustre code that best encompasses the change being made.  Examples of
178         components include modules like: llite, lov, lmv, osc, mdc, ldlm, lnet,
179         ptlrpc, mds, oss, osd, ldiskfs, libcfs, socklnd, o2iblnd; functional
180         subsystems like: recovery, quota, grant; and auxilliary areas like:
181         build, tests, docs.  This list is not exhaustive, but is a guideline.
182
183         The commit comment should contain a detailed explanation of the change
184         being made.  This can be as long as you'd like.  Please give details
185         of what problem was solved (including error messages or problems that
186         were seen), a good high-level description of how it was solved, and
187         which parts of the code were changed (including important functions
188         that were changed, if this is useful to understand the patch, and
189         for easier searching).  Wrap lines at/under $WIDTH_REG columns.
190
191         Finish the comment with a blank line and a blank-line-free
192         sign off section:
193
194         $SIGNOFF Your Real Name <your_email@domain.name>
195         $CHANGEID Ixxxx(added automatically if missing)xxxx
196
197         The "$CHANGEID" line should only be there when updating a previous
198         commit/submission.  Copy the one from the original commit.
199
200         The "sign off section" may also include several other tag lines:
201         $(for T in $(tr '|' ' ' <<< "$INNOCUOUS"); do       \
202              echo "    $T: Some Person <email@domain.com>"; \
203           done)
204         $TESTPARAMS optional additional test parameters
205         {Organization}-bug-id: associated external change identifier
206         EOF
207
208         mv "$ORIGINAL" "$SAVE" &&
209                 echo "$0: saved original commit comment to $SAVE" 1>&2
210 }
211
212 init ${1+"$@"}
213 exec 3< "$ORIGINAL" 4> "$REVISED" || exit 1
214
215 while IFS= read -u3 LINE; do
216         ((NUM += 1))
217         case "$LINE" in
218         $SIGNOFF* )   do_signoff   ;;
219         $CHANGEID* )  do_changeid  ;;
220         $TESTPARAMS* ) ck_wrapup   ;;
221
222         "")
223                 HAS_LAST_BLANK=true
224
225                 # Do not emit blank lines before summary line or after
226                 # the tag lines have begun.
227                 #
228                 ${NEEDS_FIRST_LINE} || ${IS_WRAPPING_UP} && continue
229                 ;;
230
231         \#*)
232                 continue ## ignore and suppress comments
233                 ;;
234
235         "diff --git a/"* )
236                 # Beginning of uncommented diffstat from "commit -v".  If
237                 # there are diff and index lines, skip the rest of the input:
238                 #   diff --git a/build/commit-msg b/build/commit-msg
239                 #   index 80a3442..acb4c50 100755
240                 #   deleted file mode 100644
241                 #   old mode 100644
242                 # If a "diff --git" line is not followed by one of these
243                 # lines, do the default line processing on both lines.
244                 #
245                 IFS= read -u3 INDEX || break
246                 ((NUM += 1))
247                 case "$INDEX" in
248                 "index "[0-9a-fA-F]*) break ;;
249                 "deleted file mode "*) break  ;;
250                 "old mode "*) break ;;
251                 "new file mode "*) break ;;
252                 esac
253                 LINE=${LINE}$'\n'${INDEX}
254                 do_default_line
255                 ;;
256
257         *)
258                 if [[ "$LINE" =~ ^($INNOCUOUS): ]]; then
259                         do_innocuous
260
261                 elif [[ "$LINE" =~ ^[A-Za-z0-9_-]+-bug-id: ]]; then
262                         # Allow arbitrary external bug identifiers for tracking.
263                         #
264                         ck_wrapup
265
266                 else
267                         do_default_line
268                 fi
269                 ;;
270         esac
271
272         echo "$LINE" >&4
273 done
274
275 (( NUM <= 0 )) && die "empty commit message"
276
277 unset LINE
278 $HAS_SIGNOFF || error "missing valid $SIGNOFF: line."
279
280 if $HAS_ERROR; then
281         exec 3<&- 4>&-
282         usage
283         rm "$REVISED"
284         exit 1
285 fi
286
287 $HAS_CHANGEID || new_changeid >&4
288 exec 3<&- 4>&-
289
290 mv "$REVISED" "$ORIGINAL"
291
292 ## Local Variables:
293 ## Mode: shell-script
294 ## sh-basic-offset:          8
295 ## sh-indent-after-do:       8
296 ## sh-indentation:           8
297 ## sh-indent-for-case-label: 0
298 ## sh-indent-for-case-alt:   8
299 ## indent-tabs-mode:         t
300 ## End: