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