Whamcloud - gitweb
LUDOC-249 defrag: lfs_migrate can be used to defrag.
[doc/manual.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 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,5\}-[0-9]\{1,5\} [-a-z0-9]\{2,11\}: "
33         readonly JIRA_FMT_B="^[A-Z]\{2,5\}-[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
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 }
139
140 # Add a new unique Change-Id
141 #
142 new_changeid() {
143         local NEWID=$({
144                         git var GIT_AUTHOR_IDENT
145                         git var GIT_COMMITTER_IDENT
146                         git write-tree
147                         git rev-parse HEAD 2>/dev/null
148                         grep -v "^$SIGNOFF" "$ORIGINAL" | git stripspace -s
149                 } | git hash-object --stdin)
150         (( ${#NEWID} > 0 )) ||
151                 die "git hash-object failed for $CHANGEID:"
152
153         echo "$CHANGEID I$NEWID"
154 }
155
156 # A commit message error was encountered.
157 # All output redirected to stderr.
158 #
159 error() {
160         (( ${#LINE} > 0 )) && echo "line $NUM: $LINE"
161         echo "error: commit message $*" | fmt
162         HAS_ERROR=true
163 } 1>&2
164
165 usage() {
166         exec 1>&2
167         cat <<- EOF
168
169         See http://wiki.whamcloud.com/display/PUB/Commit+Comments
170         for full details.  An example valid commit comment is:
171
172         LUDOC-nnn component: short description of change under 64 columns
173
174         The "component:" should be a lower-case single-word section of the Lustre
175         manual that best encompasses the change being made.  Examples of sections
176         include: setup, configuration, debug. This list is not exhaustive, but is a
177         guideline.
178
179         The commit comment should contain a detailed explanation of the change
180         being made.  This can be as long as you'd like.  Wrap lines at/under $WIDTH_REG
181         columns.
182
183         Finish the comment with a blank line and a blank-line-free
184         sign off section:
185
186         $SIGNOFF Your Real Name <your_email@domain.name>
187         $CHANGEID Ixxxx(added automatically if missing)xxxx
188
189         The "$CHANGEID" line should only be there when updating a previous
190         commit/submission.  Copy the one from the original commit.
191
192         The "sign off section" may also include several other tag lines:
193         $(for T in $(tr '|' ' ' <<< "$INNOCUOUS"); do       \
194              echo "    $T: Some Person <email@domain.com>"; \
195           done)
196         $TESTPARAMS optional additional test parameters
197         {Organization}-bug-id: associated external change identifier
198         EOF
199
200         mv "$ORIGINAL" "$SAVE" &&
201                 echo "$0: saved original commit comment to $SAVE" 1>&2
202 }
203
204 init ${1+"$@"}
205 exec 3< "$ORIGINAL" 4> "$REVISED" || exit 1
206
207 while IFS= read -u3 LINE; do
208         ((NUM += 1))
209         case "$LINE" in
210         $SIGNOFF* )   do_signoff   ;;
211         $CHANGEID* )  do_changeid  ;;
212         $TESTPARAMS* ) ck_wrapup   ;;
213
214         "")
215                 HAS_LAST_BLANK=true
216                 $IS_WRAPPING_UP && continue
217                 ;;
218
219         \#*)
220                 continue ## ignore and suppress comments
221                 ;;
222
223         "diff --git a/"* )
224                 # Beginning of uncommented diffstat from "commit -v".  If
225                 # there are diff and index lines, skip the rest of the input:
226                 #   diff --git a/build/commit-msg b/build/commit-msg
227                 #   index 80a3442..acb4c50 100755
228                 #   deleted file mode 100644
229                 #   old mode 100644
230                 # If a "diff --git" line is not followed by one of these
231                 # lines, do the default line processing on both lines.
232                 #
233                 IFS= read -u3 INDEX || break
234                 ((NUM += 1))
235                 case "$INDEX" in
236                 "index "[0-9a-fA-F]*) break ;;
237                 "deleted file mode "*) break  ;;
238                 "old mode "*) break ;;
239                 esac
240                 LINE=${LINE}$'\n'${INDEX}
241                 do_default_line
242                 ;;
243
244         *)
245                 if [[ "$LINE" =~ ^($INNOCUOUS): ]]; then
246                         do_innocuous
247                 elif [[ "$LINE" =~ ^[A-Za-z0-9_-]+-bug-id: ]]; then
248                         ck_wrapup
249                 else
250                         # Allow arbitrary external bug identifiers for tracking.
251                         # I can't seem to find a pattern for the "case" that
252                         # checks for "*-bug-id", so this is checked here.
253                         do_default_line
254                 fi
255                 ;;
256         esac
257
258         echo "$LINE" >&4
259 done
260
261 (( NUM <= 0 )) && die "empty commit message"
262
263 unset LINE
264 $HAS_SIGNOFF || error "missing valid $SIGNOFF: line."
265
266 if $HAS_ERROR; then
267         exec 3<&- 4>&-
268         usage
269         rm "$REVISED"
270         exit 1
271 fi
272
273 $HAS_CHANGEID || new_changeid >&4
274 exec 3<&- 4>&-
275
276 mv "$REVISED" "$ORIGINAL"
277
278 ## Local Variables:
279 ## Mode: shell-script
280 ## sh-basic-offset:          8
281 ## sh-indent-after-do:       8
282 ## sh-indentation:           8
283 ## sh-indent-for-case-label: 0
284 ## sh-indent-for-case-alt:   8
285 ## End: