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