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