Skip to content

Commit d93a203

Browse files
committed
Enforce the use of Change-Id in each commit
To prepare for code review systems like Gerrit, every git commit message must include a Change‑Id. Gerrit groups commits into a single review using a Change-Id in the commit message footer. e.g., if a change needs updating, a follow-up commit can address issues, and Gerrit will link the new version to the original review—even across cherry-picks and rebases. Reference: https://gerrit-review.googlesource.com/Documentation/user-changeid.html Change-Id: Ib344622a3887387f9ac954dcd599c4ba45836419
1 parent 217b86d commit d93a203

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

scripts/aspell-pws

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,6 @@ Shannon
295295
http
296296
https
297297
tcp
298+
awk
299+
sed
300+
changeid

scripts/commit-msg.hook

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,163 @@ validate_commit_message() {
280280
test $? -eq 1 || add_warning 1 "Do not start the subject line with whitespace"
281281
}
282282

283+
unset GREP_OPTIONS
284+
285+
CHANGE_ID_AFTER="Bug|Issue|Test"
286+
MSG="$1"
287+
288+
# Ensure that a unique Change-Id is present, and generate one if it is not.
289+
#
290+
# Partially taken from Gerrit Code Review 3.3.0-56-gbcecc47463
291+
add_change_id() {
292+
clean_message=`sed -e '
293+
/^diff --git .*/{
294+
s///
295+
q
296+
}
297+
/^Signed-off-by:/d
298+
/^#/d
299+
' "$MSG" | git stripspace`
300+
if test -z "$clean_message"
301+
then
302+
return
303+
fi
304+
305+
# Does Change-Id: already exist? if so, exit (no change).
306+
if grep -i '^Change-Id:' "$MSG" >/dev/null
307+
then
308+
return
309+
fi
310+
311+
id=`_gen_changeid`
312+
T="$MSG.tmp.$$"
313+
AWK=awk
314+
315+
# Get core.commentChar from git config or use default symbol
316+
commentChar=`git config --get core.commentChar`
317+
commentChar=${commentChar:-#}
318+
319+
# How this works:
320+
# - parse the commit message as (textLine+ blankLine*)*
321+
# - assume textLine+ to be a footer until proven otherwise
322+
# - exception: the first block is not footer (as it is the title)
323+
# - read textLine+ into a variable
324+
# - then count blankLines
325+
# - once the next textLine appears, print textLine+ blankLine* as these
326+
# aren't footer
327+
# - in END, the last textLine+ block is available for footer parsing
328+
awk '
329+
BEGIN {
330+
# while we start with the assumption that textLine+
331+
# is a footer, the first block is not.
332+
isFooter = 0
333+
footerComment = 0
334+
blankLines = 0
335+
}
336+
337+
# Skip lines starting with commentChar without any spaces before it.
338+
/^'"$commentChar"'/ { next }
339+
340+
# Skip the line starting with the diff command and everything after it,
341+
# up to the end of the file, assuming it is only patch data.
342+
# If more than one line before the diff was empty, strip all but one.
343+
/^diff --git / {
344+
blankLines = 0
345+
while (getline) { }
346+
next
347+
}
348+
349+
# Count blank lines outside footer comments
350+
/^$/ && (footerComment == 0) {
351+
blankLines++
352+
next
353+
}
354+
355+
# Catch footer comment
356+
/^\[[a-zA-Z0-9-]+:/ && (isFooter == 1) {
357+
footerComment = 1
358+
}
359+
360+
/]$/ && (footerComment == 1) {
361+
footerComment = 2
362+
}
363+
364+
# We have a non-blank line after blank lines. Handle this.
365+
(blankLines > 0) {
366+
print lines
367+
for (i = 0; i < blankLines; i++) {
368+
print ""
369+
}
370+
371+
lines = ""
372+
blankLines = 0
373+
isFooter = 1
374+
footerComment = 0
375+
}
376+
377+
# Detect that the current block is not the footer
378+
(footerComment == 0) && (!/^\[?[a-zA-Z0-9-]+:/ || /^[a-zA-Z0-9-]+:\/\//) {
379+
isFooter = 0
380+
}
381+
382+
{
383+
# We need this information about the current last comment line
384+
if (footerComment == 2) {
385+
footerComment = 0
386+
}
387+
if (lines != "") {
388+
lines = lines "\n";
389+
}
390+
lines = lines $0
391+
}
392+
393+
# Footer handling:
394+
# If the last block is considered a footer, splice in the Change-Id at the
395+
# right place.
396+
# Look for the right place to inject Change-Id by considering
397+
# CHANGE_ID_AFTER. Keys listed in it (case insensitive) come first,
398+
# then Change-Id, then everything else (eg. Signed-off-by:).
399+
#
400+
# Otherwise just print the last block, a new line and the Change-Id as a
401+
# block of its own.
402+
END {
403+
unprinted = 1
404+
if (isFooter == 0) {
405+
print lines "\n"
406+
lines = ""
407+
}
408+
changeIdAfter = "^(" tolower("'"$CHANGE_ID_AFTER"'") "):"
409+
numlines = split(lines, footer, "\n")
410+
for (line = 1; line <= numlines; line++) {
411+
if (unprinted && match(tolower(footer[line]), changeIdAfter) != 1) {
412+
unprinted = 0
413+
print "Change-Id: I'"$id"'"
414+
}
415+
print footer[line]
416+
}
417+
if (unprinted) {
418+
print "Change-Id: I'"$id"'"
419+
}
420+
}' "$MSG" > "$T" && mv "$T" "$MSG" || rm -f "$T"
421+
}
422+
423+
_gen_changeid_input() {
424+
echo "tree `git write-tree`"
425+
if parent=`git rev-parse "HEAD^0" 2>/dev/null`
426+
then
427+
echo "parent $parent"
428+
fi
429+
echo "author `git var GIT_AUTHOR_IDENT`"
430+
echo "committer `git var GIT_COMMITTER_IDENT`"
431+
echo
432+
printf '%s' "$clean_message"
433+
}
434+
435+
_gen_changeid() {
436+
_gen_changeid_input |
437+
git hash-object -t commit --stdin
438+
}
439+
283440
#
284441
# It's showtime.
285442
#
@@ -300,6 +457,8 @@ while true; do
300457

301458
validate_commit_message
302459

460+
add_change_id
461+
303462
# if there are no WARNINGS are empty then we're good to break out of here
304463
test ${#WARNINGS[@]} -eq 0 && exit 0;
305464

0 commit comments

Comments
 (0)