xref: /dpdk/devtools/check-git-log.sh (revision c1d145834f287aa8cf53de914618a7312f2c360e)
19a98f50eSThomas Monjalon#! /bin/sh
2f83a3d3fSOlivier Matz# SPDX-License-Identifier: BSD-3-Clause
39a98f50eSThomas Monjalon# Copyright 2016 6WIND S.A.
49a98f50eSThomas Monjalon
59a98f50eSThomas Monjalon# Check commit logs (headlines and references)
69a98f50eSThomas Monjalon#
79a98f50eSThomas Monjalon# If any doubt about the formatting, please check in the most recent history:
89a98f50eSThomas Monjalon#	git log --format='%>|(15)%cr   %s' --reverse | grep -i <pattern>
99a98f50eSThomas Monjalon
10b1214d98SCiara Powerprint_usage () {
119a98f50eSThomas Monjalon	cat <<- END_OF_HELP
12b1214d98SCiara Power	usage: $(basename $0) [-h] [-nX|-r range]
139a98f50eSThomas Monjalon
149a98f50eSThomas Monjalon	Check commit log formatting.
15b1214d98SCiara Power	The git commits to be checked can be specified as a "git log" option,
16b1214d98SCiara Power	by latest git commits limited with -n option, or commits in the git
17b1214d98SCiara Power	range specified with -r option.
18b1214d98SCiara Power	e.g. To check only the last commit, ‘-n1’ or ‘-r@~..’ is used.
19b9b10ddbSThomas Monjalon	If no range provided, default is origin/main..HEAD.
209a98f50eSThomas Monjalon	END_OF_HELP
21b1214d98SCiara Power}
229a98f50eSThomas Monjalon
23a3e34aa8SBruce Richardsonselfdir=$(dirname $(readlink -f $0))
24b1214d98SCiara Power# The script caters for two formats, the new preferred format, and the old
25b1214d98SCiara Power# format to ensure backward compatibility.
26b1214d98SCiara Power# The new format is aligned with the format of the checkpatches script,
27b1214d98SCiara Power# and allows for specifying the patches to check by passing -nX or -r range.
28b1214d98SCiara Power# The old format allows for specifying patches by passing -X or range
29b1214d98SCiara Power# as the first argument.
30b9b10ddbSThomas Monjalonrange=${1:-origin/main..}
31b1214d98SCiara Power
32b1214d98SCiara Powerif [ "$range" = '--help' ] ; then
33b1214d98SCiara Power	print_usage
34b1214d98SCiara Power	exit 0
359a98f50eSThomas Monjalon# convert -N to HEAD~N.. in order to comply with git-log-fixes.sh getopts
36b1214d98SCiara Powerelif printf -- "$range" | grep -q '^-[0-9]\+' ; then
37b1214d98SCiara Power	range="HEAD$(printf -- "$range" | sed 's,^-,~,').."
38b1214d98SCiara Powerelse
39b1214d98SCiara Power	while getopts hr:n: ARG ; do
40b1214d98SCiara Power		case $ARG in
41b1214d98SCiara Power			n ) range="HEAD~$OPTARG.." ;;
42b1214d98SCiara Power			r ) range=$OPTARG ;;
43b1214d98SCiara Power			h ) print_usage ; exit 0 ;;
44b1214d98SCiara Power			? ) print_usage ; exit 1 ;;
45b1214d98SCiara Power		esac
46b1214d98SCiara Power	done
47b1214d98SCiara Power	shift $(($OPTIND - 1))
489a98f50eSThomas Monjalonfi
499a98f50eSThomas Monjalon
509a98f50eSThomas Monjaloncommits=$(git log --format='%h' --reverse $range)
519a98f50eSThomas Monjalonheadlines=$(git log --format='%s' --reverse $range)
529a98f50eSThomas Monjalonbodylines=$(git log --format='%b' --reverse $range)
539a98f50eSThomas Monjalonfixes=$(git log --format='%h %s' --reverse $range | grep -i ': *fix' | cut -d' ' -f1)
549a98f50eSThomas Monjalonstablefixes=$($selfdir/git-log-fixes.sh $range | sed '/(N\/A)$/d'  | cut -d' ' -f2)
559a98f50eSThomas Monjalontags=$(git log --format='%b' --reverse $range | grep -i -e 'by *:' -e 'fix.*:')
569a98f50eSThomas Monjalonbytag='\(Reported\|Suggested\|Signed-off\|Acked\|Reviewed\|Tested\)-by:'
5753e65976SJakub Paliderreltag='Coverity issue:\|Bugzilla ID:\|Fixes:\|Cc:'
589a98f50eSThomas Monjalon
59a8354c99SCiara Powerfailure=false
60a8354c99SCiara Power
619a98f50eSThomas Monjalon# check headline format (spacing, no punctuation, no code)
629a98f50eSThomas Monjalonbad=$(echo "$headlines" | grep --color=always \
639a98f50eSThomas Monjalon	-e '	' \
649a98f50eSThomas Monjalon	-e '^ ' \
659a98f50eSThomas Monjalon	-e ' $' \
669a98f50eSThomas Monjalon	-e '\.$' \
679a98f50eSThomas Monjalon	-e '[,;!?&|]' \
689a98f50eSThomas Monjalon	-e ':.*_' \
699a98f50eSThomas Monjalon	-e '^[^:]\+$' \
709a98f50eSThomas Monjalon	-e ':[^ ]' \
719a98f50eSThomas Monjalon	-e ' :' \
729a98f50eSThomas Monjalon	| sed 's,^,\t,')
73a8354c99SCiara Power[ -z "$bad" ] || { printf "Wrong headline format:\n$bad\n" && failure=true;}
749a98f50eSThomas Monjalon
759a98f50eSThomas Monjalon# check headline prefix when touching only drivers, e.g. net/<driver name>
769a98f50eSThomas Monjalonbad=$(for commit in $commits ; do
779a98f50eSThomas Monjalon	headline=$(git log --format='%s' -1 $commit)
789a98f50eSThomas Monjalon	files=$(git diff-tree --no-commit-id --name-only -r $commit)
799a98f50eSThomas Monjalon	[ -z "$(echo "$files" | grep -v '^\(drivers\|doc\|config\)/')" ] ||
809a98f50eSThomas Monjalon		continue
819a98f50eSThomas Monjalon	drv=$(echo "$files" | grep '^drivers/' | cut -d "/" -f 2,3 | sort -u)
829a98f50eSThomas Monjalon	drvgrp=$(echo "$drv" | cut -d "/" -f 1 | uniq)
83*c1d14583SBruce Richardson	if [ "$drv" = "net/intel" ] ; then
84*c1d14583SBruce Richardson		drvgrp=$drv
85*c1d14583SBruce Richardson		drv=$(echo "$files" | grep '^drivers/' | cut -d "/" -f 2,4 | sort -u)
86*c1d14583SBruce Richardson		if [ $(echo "$drv" | wc -l) -ne 1 ] ; then
87*c1d14583SBruce Richardson			drv='net/intel'
88*c1d14583SBruce Richardson		elif [ "$drv" = "net/common" ] ; then
89*c1d14583SBruce Richardson			drv='net/intel/common'
90*c1d14583SBruce Richardson		fi
91*c1d14583SBruce Richardson	fi
929a98f50eSThomas Monjalon	if [ $(echo "$drvgrp" | wc -l) -gt 1 ] ; then
939a98f50eSThomas Monjalon		echo "$headline" | grep -v '^drivers:'
949a98f50eSThomas Monjalon	elif [ $(echo "$drv" | wc -l) -gt 1 ] ; then
9587acacf8SFerruh Yigit		echo "$headline" | grep -v "^drivers/$drvgrp"
969a98f50eSThomas Monjalon	else
979a98f50eSThomas Monjalon		echo "$headline" | grep -v "^$drv"
989a98f50eSThomas Monjalon	fi
999a98f50eSThomas Monjalondone | sed 's,^,\t,')
100a8354c99SCiara Power[ -z "$bad" ] || { printf "Wrong headline prefix:\n$bad\n" && failure=true;}
1019a98f50eSThomas Monjalon
102807274d5SDavid Marchand# check headline prefix for libraries
103807274d5SDavid Marchandbad=$(echo "$headlines" | grep --color=always \
104807274d5SDavid Marchand	-e '^lib/' \
105807274d5SDavid Marchand	| sed 's,^,\t,')
106807274d5SDavid Marchand[ -z "$bad" ] || { printf "Wrong headline prefix:\n$bad\n" && failure=true;}
107807274d5SDavid Marchand
1089a98f50eSThomas Monjalon# check headline label for common typos
1099a98f50eSThomas Monjalonbad=$(echo "$headlines" | grep --color=always \
1109a98f50eSThomas Monjalon	-e '^example[:/]' \
1119a98f50eSThomas Monjalon	-e '^apps/' \
1129a98f50eSThomas Monjalon	-e '^testpmd' \
1139a98f50eSThomas Monjalon	-e 'test-pmd' \
1149a98f50eSThomas Monjalon	-e '^bond:' \
1159a98f50eSThomas Monjalon	| sed 's,^,\t,')
116a8354c99SCiara Power[ -z "$bad" ] || { printf "Wrong headline label:\n$bad\n" && failure=true;}
1179a98f50eSThomas Monjalon
1189a98f50eSThomas Monjalon# check headline lowercase for first words
1199a98f50eSThomas Monjalonbad=$(echo "$headlines" | grep --color=always \
1205831a219SAndy Green	-e '^.*[[:upper:]].*:' \
1215831a219SAndy Green	-e ': *[[:upper:]]' \
1229a98f50eSThomas Monjalon	| sed 's,^,\t,')
123a8354c99SCiara Power[ -z "$bad" ] || { printf "Wrong headline uppercase:\n$bad\n" && failure=true;}
1249a98f50eSThomas Monjalon
125d448efa2SSean Morrissey# check headline case (Rx/Tx, VF, L2, MAC, Linux ...)
126d448efa2SSean MorrisseyIFS='
127d448efa2SSean Morrissey'
128d448efa2SSean Morrisseywords="$selfdir/words-case.txt"
129d448efa2SSean Morrisseyfor word in $(cat $words); do
1308b51fbc0SThomas Monjalon	bad=$(echo "$headlines" | grep -iw $word | grep -vw $word)
131d448efa2SSean Morrissey	if [ "$word" = "Tx" ]; then
1327f6b150cSThomas Monjalon		bad=$(echo $bad | grep -v 'OCTEON TX')
133d448efa2SSean Morrissey	fi
134d448efa2SSean Morrissey	for bad_line in $bad; do
1358b51fbc0SThomas Monjalon		bad_word=$(echo $bad_line | cut -d":" -f2 | grep -iwo $word)
136a8354c99SCiara Power		[ -z "$bad_word" ] || { printf "Wrong headline case:\n\
137a8354c99SCiara Power			\"$bad_line\": $bad_word --> $word\n" && failure=true;}
138d448efa2SSean Morrissey	done
139d448efa2SSean Morrisseydone
1409a98f50eSThomas Monjalon
1419a98f50eSThomas Monjalon# check headline length (60 max)
1429a98f50eSThomas Monjalonbad=$(echo "$headlines" |
1439a98f50eSThomas Monjalon	awk 'length>60 {print}' |
1449a98f50eSThomas Monjalon	sed 's,^,\t,')
145a8354c99SCiara Power[ -z "$bad" ] || { printf "Headline too long:\n$bad\n" && failure=true;}
1469a98f50eSThomas Monjalon
1479a98f50eSThomas Monjalon# check body lines length (75 max)
1489a98f50eSThomas Monjalonbad=$(echo "$bodylines" | grep -v '^Fixes:' |
1499a98f50eSThomas Monjalon	awk 'length>75 {print}' |
1509a98f50eSThomas Monjalon	sed 's,^,\t,')
151a8354c99SCiara Power[ -z "$bad" ] || { printf "Line too long:\n$bad\n" && failure=true;}
1529a98f50eSThomas Monjalon
1539a98f50eSThomas Monjalon# check starting commit message with "It"
1549a98f50eSThomas Monjalonbad=$(for commit in $commits ; do
1559a98f50eSThomas Monjalon	firstbodyline=$(git log --format='%b' -1 $commit | head -n1)
1569a98f50eSThomas Monjalon	echo "$firstbodyline" | grep --color=always -ie '^It '
1579a98f50eSThomas Monjalondone | sed 's,^,\t,')
158a8354c99SCiara Power[ -z "$bad" ] || { printf "Wrong beginning of commit message:\n$bad\n"\
159a8354c99SCiara Power	&& failure=true;}
1609a98f50eSThomas Monjalon
1619a98f50eSThomas Monjalon# check tags spelling
1629a98f50eSThomas Monjalonbad=$(echo "$tags" |
1639a98f50eSThomas Monjalon	grep -v "^$bytag [^,]* <.*@.*>$" |
1649a98f50eSThomas Monjalon	grep -v '^Fixes: [0-9a-f]\{7\}[0-9a-f]* (".*")$' |
1659a98f50eSThomas Monjalon	sed 's,^.,\t&,')
166a8354c99SCiara Power[ -z "$bad" ] || { printf "Wrong tag:\n$bad\n" && failure=true;}
1679a98f50eSThomas Monjalon
1687a8735e2SDavid Marchand# check missing Coverity issue: tag
1697a8735e2SDavid Marchandbad=$(for commit in $commits; do
1707a8735e2SDavid Marchand	body=$(git log --format='%b' -1 $commit)
1717a8735e2SDavid Marchand	echo "$body" | grep -qi coverity || continue
1727a8735e2SDavid Marchand	echo "$body" | grep -q '^Coverity issue:' && continue
1737a8735e2SDavid Marchand	git log --format='\t%s' -1 $commit
1747a8735e2SDavid Marchanddone)
175a8354c99SCiara Power[ -z "$bad" ] || { printf "Missing 'Coverity issue:' tag:\n$bad\n"\
176a8354c99SCiara Power	&& failure=true;}
1777a8735e2SDavid Marchand
1787a8735e2SDavid Marchand# check missing Bugzilla ID: tag
1797a8735e2SDavid Marchandbad=$(for commit in $commits; do
1807a8735e2SDavid Marchand	body=$(git log --format='%b' -1 $commit)
1817a8735e2SDavid Marchand	echo "$body" | grep -qi bugzilla || continue
1827a8735e2SDavid Marchand	echo "$body" | grep -q '^Bugzilla ID:' && continue
1837a8735e2SDavid Marchand	git log --format='\t%s' -1 $commit
1847a8735e2SDavid Marchanddone)
185a8354c99SCiara Power[ -z "$bad" ] || { printf "Missing 'Bugzilla ID:' tag:\n$bad\n"\
186a8354c99SCiara Power	&& failure=true;}
1877a8735e2SDavid Marchand
1889a98f50eSThomas Monjalon# check missing Fixes: tag
1899a98f50eSThomas Monjalonbad=$(for fix in $fixes ; do
1909a98f50eSThomas Monjalon	git log --format='%b' -1 $fix | grep -q '^Fixes: ' ||
1919a98f50eSThomas Monjalon		git log --format='\t%s' -1 $fix
1929a98f50eSThomas Monjalondone)
193a8354c99SCiara Power[ -z "$bad" ] || { printf "Missing 'Fixes' tag:\n$bad\n" && failure=true;}
1949a98f50eSThomas Monjalon
1959a98f50eSThomas Monjalon# check Fixes: reference
1969a98f50eSThomas Monjalonfixtags=$(echo "$tags" | grep '^Fixes: ')
1979a98f50eSThomas Monjalonbad=$(for fixtag in $fixtags ; do
1989a98f50eSThomas Monjalon	hash=$(echo "$fixtag" | sed 's,^Fixes: \([0-9a-f]*\).*,\1,')
1999a98f50eSThomas Monjalon	if git branch --contains $hash 2>&- | grep -q '^\*' ; then
2009a98f50eSThomas Monjalon		good="Fixes: $hash "$(git log --format='("%s")' -1 $hash 2>&-)
2019a98f50eSThomas Monjalon	else
2029a98f50eSThomas Monjalon		good="reference not in current branch"
2039a98f50eSThomas Monjalon	fi
2049a98f50eSThomas Monjalon	printf "$fixtag" | grep -v "^$good$"
2059a98f50eSThomas Monjalondone | sed 's,^,\t,')
206a8354c99SCiara Power[ -z "$bad" ] || { printf "Wrong 'Fixes' reference:\n$bad\n" && failure=true;}
2079a98f50eSThomas Monjalon
20892fdc232SThomas Monjalon# check Cc: stable@dpdk.org for fixes
2099a98f50eSThomas Monjalonbad=$(for fix in $stablefixes ; do
21092fdc232SThomas Monjalon	git log --format='%b' -1 $fix | grep -qi '^Cc: *stable@dpdk.org' ||
2119a98f50eSThomas Monjalon		git log --format='\t%s' -1 $fix
2129a98f50eSThomas Monjalondone)
213a8354c99SCiara Power[ -z "$bad" ] || { printf "Is it candidate for Cc: stable@dpdk.org backport?\n$bad\n"\
214a8354c99SCiara Power	&& failure=true;}
215a8354c99SCiara Power
21653e65976SJakub Palider# check tag sequence
21753e65976SJakub Paliderbad=$(for commit in $commits; do
21853e65976SJakub Palider	body=$(git log --format='%b' -1 $commit)
21953e65976SJakub Palider	echo "$body" |
22053e65976SJakub Palider	grep -o -e "$reltag\|^[[:blank:]]*$\|$bytag" |
22153e65976SJakub Palider	# retrieve tags only
22253e65976SJakub Palider	cut -f1 -d":" |
22353e65976SJakub Palider	# it is okay to have several tags of the same type
22453e65976SJakub Palider	# but for processing we need to squash them
22553e65976SJakub Palider	uniq |
22653e65976SJakub Palider	# make sure the tags are in the proper order as presented in SEQ
22753e65976SJakub Palider	awk -v subject="$(git log --format='\t%s' -1 $commit)" 'BEGIN{
22853e65976SJakub Palider		SEQ[0] = "Coverity issue";
22953e65976SJakub Palider		SEQ[1] = "Bugzilla ID";
23053e65976SJakub Palider		SEQ[2] = "Fixes";
23153e65976SJakub Palider		SEQ[3] = "Cc";
23253e65976SJakub Palider		SEQ[4] = "^$";
23353e65976SJakub Palider		SEQ[5] = "Reported-by";
23453e65976SJakub Palider		SEQ[6] = "Suggested-by";
23553e65976SJakub Palider		SEQ[7] = "Signed-off-by";
23653e65976SJakub Palider		SEQ[8] = "Acked-by";
23753e65976SJakub Palider		SEQ[9] = "Reviewed-by";
23853e65976SJakub Palider		SEQ[10] = "Tested-by";
23953e65976SJakub Palider		latest = 0;
24053e65976SJakub Palider		chronological = 0;
24153e65976SJakub Palider	}
24253e65976SJakub Palider	{
24353e65976SJakub Palider		for (seq = 0; seq < length(SEQ); seq++) {
24453e65976SJakub Palider			if (chronological == 1)
24553e65976SJakub Palider				continue;
24653e65976SJakub Palider			if (match($0, SEQ[seq])) {
24753e65976SJakub Palider				if (seq < latest) {
24853e65976SJakub Palider					print subject " (" $0 ":)";
24953e65976SJakub Palider					break;
25053e65976SJakub Palider				} else {
25153e65976SJakub Palider					latest = seq;
25253e65976SJakub Palider				}
25353e65976SJakub Palider			}
25453e65976SJakub Palider		}
25553e65976SJakub Palider		if (match($0, "Signed-off-by"))
25653e65976SJakub Palider			chronological = 1;
25753e65976SJakub Palider	 }'
25853e65976SJakub Paliderdone)
25953e65976SJakub Palider[ -z "$bad" ] || { printf "Wrong tag order: \n$bad\n"\
26053e65976SJakub Palider	&& failure=true;}
26153e65976SJakub Palider
26253e65976SJakub Palider# check required tag
26353e65976SJakub Paliderbad=$(for commit in $commits; do
26453e65976SJakub Palider	body=$(git log --format='%b' -1 $commit)
26553e65976SJakub Palider	echo $body | grep -q "Signed-off-by:" ||
26653e65976SJakub Palider	git log --format='\t%s' -1 $commit
26753e65976SJakub Paliderdone)
26853e65976SJakub Palider[ -z "$bad" ] || { printf "Missing 'Signed-off-by:' tag: \n$bad\n"\
26953e65976SJakub Palider	&& failure=true;}
27053e65976SJakub Palider
27183812de4SThomas Monjalon# check names
27283812de4SThomas Monjalonnames=$(git log --format='From: %an <%ae>%n%b' --reverse $range |
27383812de4SThomas Monjalon	sed -rn 's,.*: (.*<.*@.*>),\1,p' |
27483812de4SThomas Monjalon	sort -u)
27583812de4SThomas Monjalonbad=$(for contributor in $names ; do
2768aec1d8fSRaslan Darawsheh	contributor=$(echo $contributor | sed 's,(,\\(,')
27783812de4SThomas Monjalon	! grep -qE "^$contributor($| <)" $selfdir/../.mailmap || continue
2786fd14c1bSThomas Monjalon	name=${contributor%% <*}
2796fd14c1bSThomas Monjalon	if grep -q "^$name <" $selfdir/../.mailmap ; then
28083812de4SThomas Monjalon		printf "\t$contributor is not the primary email address\n"
28183812de4SThomas Monjalon	else
28283812de4SThomas Monjalon		printf "\t$contributor is unknown in .mailmap\n"
28383812de4SThomas Monjalon	fi
28483812de4SThomas Monjalondone)
28583812de4SThomas Monjalon[ -z "$bad" ] || { printf "Contributor name/email mismatch with .mailmap: \n$bad\n"\
28683812de4SThomas Monjalon	&& failure=true;}
28783812de4SThomas Monjalon
288a8354c99SCiara Powertotal=$(echo "$commits" | wc -l)
289a8354c99SCiara Powerif $failure ; then
290a8354c99SCiara Power	printf "\nInvalid patch(es) found - checked $total patch"
291a8354c99SCiara Powerelse
292a8354c99SCiara Power	printf "\n$total/$total valid patch"
293a8354c99SCiara Powerfi
294a8354c99SCiara Power[ $total -le 1 ] || printf 'es'
295a8354c99SCiara Powerprintf '\n'
296a8354c99SCiara Power$failure && exit 1 || exit 0
297