11#! /usr/bin/env bash
22
3- #  Check that every non-merge commit after the specified base commit has a commit
4- #  message ending with a valid Change-Id line. A valid Change-Id line must be the
5- #  last non-empty line of the commit message and follow the format:
6- # 
7- #    Change-Id: I<hexadecimal_hash>
3+ #  This script checks:
4+ #  1. Change-Id presence (indicates commit-msg hook processing)
5+ #  2. Commit message quality (indicates pre-commit hook compliance)
6+ #  3. Bypass detection (detects --no-verify usage or web interface commits)
87# 
98#  Merge commits are excluded from this check.
109
@@ -24,26 +23,215 @@ BASE_COMMIT="0b8be2c15160c216e8b6ec82c99a000e81c0e429"
2423#  Get a list of non-merge commit hashes after BASE_COMMIT.
2524commits=$( git rev-list --no-merges " ${BASE_COMMIT} "  ..HEAD) 
2625
26+ #  Hook bypass detection patterns
27+ BYPASS_INDICATORS=(
28+     " --no-verify" 
29+     " WIP" 
30+ )
31+ 
32+ #  Quality patterns that indicate hook processing
33+ PROBLEMATIC_PATTERNS=(
34+     ' ^[a-z]'                                                #  Uncapitalized subjects
35+     ' \.$'                                                   #  Ending with period
36+     ' ^.{1,10}$'                                            #  Too short subjects
37+     ' ^.{80,}'                                              #  Too long subjects
38+     ' ^(Update|Fix|Change|Modify) [a-zA-Z0-9_-]+\.(c|h)$'   #  Generic filename updates
39+ )
40+ 
41+ #  Early exit if no commits to check
42+ [[ -z  " $commits "   ]] &&  { echo  -e " ${GREEN} No commits to check.${NC} " ;  exit  0;  }
43+ 
44+ #  Pre-compute indicator patterns for faster matching
45+ bypass_pattern=" " 
46+ for  indicator  in  " ${BYPASS_INDICATORS[@]} " ;  do 
47+   bypass_pattern+=" |${indicator,,} " 
48+ done 
49+ bypass_pattern=" ${bypass_pattern# |} " 
50+ 
51+ #  Ultra-fast approach: minimize git calls and parsing overhead
2752failed=0
53+ warnings=0
54+ suspicious_commits=()
55+ 
56+ #  Cache all commit data at once using the most efficient git format
57+ declare  -A commit_cache short_cache subject_cache msg_cache
58+ 
59+ #  Single git call to populate all caches - handle multiline messages properly
60+ current_commit=" " 
61+ current_msg=" " 
62+ reading_msg=false
63+ 
64+ while  IFS= read  -r line;  do 
65+   case  " $line "   in 
66+     " COMMIT " * )
67+       #  Save previous message if we were reading one
68+       if  [[ " $reading_msg "   =  true  &&  -n  " $current_commit "   ]];  then 
69+         msg_cache[" $current_commit "  ]=" $current_msg " 
70+       fi 
71+       current_commit=" ${line# COMMIT } " 
72+       commit_cache[" $current_commit "  ]=1
73+       reading_msg=false
74+       current_msg=" " 
75+       ;;
76+     " SHORT " * )
77+       short_cache[" $current_commit "  ]=" ${line# SHORT } " 
78+       ;;
79+     " SUBJECT " * )
80+       subject_cache[" $current_commit "  ]=" ${line# SUBJECT } " 
81+       ;;
82+     " MSGSTART"  )
83+       reading_msg=true
84+       current_msg=" " 
85+       ;;
86+     * )
87+       #  If we're reading a message, accumulate lines
88+       if  [[ " $reading_msg "   =  true  ]];  then 
89+         if  [[ -z  " $current_msg "   ]];  then 
90+           current_msg=" $line " 
91+         else 
92+           current_msg=" $current_msg " $' \n ' " $line " 
93+         fi 
94+       fi 
95+       ;;
96+   esac 
97+ done  <  <( git log --format=" COMMIT %H%nSHORT %h%nSUBJECT %s%nMSGSTART%n%B"   --no-merges " ${BASE_COMMIT} ..HEAD" ) 
2898
29- for  commit  in  $commits ;  do 
30-   #  Retrieve the commit message for the given commit.
31-   commit_msg=$( git log -1 --format=%B " ${commit} " ) 
99+ #  Save the last message
100+ if  [[ " $reading_msg "   =  true  &&  -n  " $current_commit "   ]];  then 
101+   msg_cache[" $current_commit "  ]=" $current_msg " 
102+ fi 
103+ 
104+ #  Process cached data - no more git calls needed
105+ for  commit  in  " ${! commit_cache[@]} " ;  do 
106+   [[ -z  " $commit "   ]] &&  continue 
107+ 
108+   short_hash=" ${short_cache[$commit]} " 
109+   subject=" ${subject_cache[$commit]} " 
110+   full_msg=" ${msg_cache[$commit]} " 
32111
33-   #  Extract the last non-empty line from the commit message.
34-   last_line=$( echo " $commit_msg "   |  awk ' NF {line=$0} END {print line}' ) 
112+   #  Initialize issue tracking
113+   has_issues=0
114+   has_warnings=0
115+   issue_list=" " 
116+   warning_list=" " 
117+ 
118+   #  Check 1: Change-Id validation (fastest check first)
119+   if  [[ " $full_msg "   !=  * " Change-Id: I" *  ]];  then 
120+     has_issues=1
121+     issue_list+=" Missing valid Change-Id (likely bypassed commit-msg hook)|" 
122+     (( failed++ )) 
123+   fi 
35124
36-   #  Check if the last line matches the expected Change-Id format.
37-   if  [[ !  $last_line  =~  ^Change-Id:\  I[0-9a-fA-F]+$ ]];  then 
38-     subject=$( git log -1 --format=%s " ${commit} " ) 
39-     short_hash=$( git rev-parse --short " ${commit} " ) 
40-     echo  " Commit ${short_hash}  with subject '$subject ' does not end with a valid Change-Id." 
41-     failed=1
125+   #  Check 2: Bypass indicators (single pattern match)
126+   full_msg_lower=" ${full_msg,,} " 
127+   if  [[ " $full_msg_lower "   =~  ($bypass_pattern ) ]];  then 
128+     has_warnings=1
129+     warning_list+=" Contains bypass indicator: '${BASH_REMATCH[1]} '|" 
130+     (( warnings++ )) 
131+   fi 
132+ 
133+   #  Check 3: Subject validation (batch character operations)
134+   subject_len=${# subject} 
135+   first_char=" ${subject: 0: 1} " 
136+   last_char=" ${subject:  -1} " 
137+ 
138+   #  Length checks
139+   if  [[ $subject_len  -le  10 ]];  then 
140+     has_warnings=1
141+     warning_list+=" Subject very short ($subject_len  chars)|" 
142+     (( warnings++ )) 
143+   elif  [[ $subject_len  -ge  80 ]];  then 
144+     has_issues=1
145+     issue_list+=" Subject too long ($subject_len  chars)|" 
146+     (( failed++ )) 
147+   fi 
148+ 
149+   #  Character validation using ASCII values
150+   if  [[ ${# first_char}  -eq  1 ]];  then 
151+     #  Check if it's a lowercase letter
152+     case  " $first_char "   in 
153+       [a-z])
154+         has_issues=1
155+         issue_list+=" Subject not capitalized|" 
156+         (( failed++ )) 
157+         ;;
158+     esac 
159+   fi 
160+ 
161+   #  Period check
162+   if  [[ " $last_char "   ==  " ."   ]];  then 
163+     has_issues=1
164+     issue_list+=" Subject ends with period|" 
165+     (( failed++ )) 
166+   fi 
167+ 
168+   #  Generic filename check (simplified pattern)
169+   if  [[ " $subject "   =~  ^(Update| Fix| Change| Modify)[[:space:]] ]];  then 
170+     if  [[ " $subject "   =~  \. (c| h)$ ]];  then 
171+       has_warnings=1
172+       warning_list+=" Generic filename-only subject|" 
173+       (( warnings++ )) 
174+     fi 
175+   fi 
176+ 
177+   #  Check 4: Web interface (string contains check)
178+   if  [[ " $full_msg "   ==  * " Co-authored-by:" *  ]];  then 
179+     if  [[ " $full_msg "   !=  * " Change-Id:" *  ]];  then 
180+       has_issues=1
181+       issue_list+=" Likely created via GitHub web interface|" 
182+       (( failed++ )) 
183+     fi 
184+   fi 
185+ 
186+   #  Check 5: Queue.c body (most expensive - do last and only when needed)
187+   if  [[ " $full_msg "   =~  ^[^$' \n '  ]* $' \n '  [[:space:]]* $' \n '  Change-Id: ]];  then 
188+     #  Body appears empty - check if queue.c was modified
189+     if  git diff-tree --no-commit-id --name-only -r " $commit "   |  grep -q " ^queue\.c$" ;  then 
190+       has_issues=1
191+       issue_list+=" Missing commit body for queue.c changes|" 
192+       (( failed++ )) 
193+     fi 
194+   fi 
195+ 
196+   #  Report issues (only if found)
197+   if  [[ $has_issues  -eq  1 ||  $has_warnings  -eq  1 ]];  then 
198+     echo  -e " ${YELLOW} Commit ${short_hash} :${NC}  ${subject} " 
199+ 
200+     if  [[ $has_issues  -eq  1 ]];  then 
201+       IFS=' |'   read  -ra issues <<<  " ${issue_list%|}" 
202+       for  issue  in  " ${issues[@]} " ;  do 
203+         [[ -n  " $issue "   ]] &&  echo  -e "   [ ${RED} FAIL${NC}  ] $issue " 
204+       done 
205+       suspicious_commits+=(" $short_hash : $subject "  )
206+     fi 
207+ 
208+     if  [[ $has_warnings  -eq  1 ]];  then 
209+       IFS=' |'   read  -ra warnings_arr <<<  " ${warning_list%|}" 
210+       for  warning  in  " ${warnings_arr[@]} " ;  do 
211+         [[ -n  " $warning "   ]] &&  echo  -e "   ${YELLOW} !${NC}  $warning " 
212+       done 
213+     fi 
42214  fi 
43215done 
44216
45- if  [ $failed  -ne  0 ];  then 
46-   throw " Some commits are missing a valid Change-Id. Please amend the commit messages accordingly." 
217+ if  [[ $failed  -gt  0 ]];  then 
218+   echo  -e " \n${RED} Problematic commits detected:${NC} " 
219+   for  commit  in  " ${suspicious_commits[@]} " ;  do 
220+     echo  -e "   ${RED} •${NC}  $commit " 
221+   done 
222+ 
223+   echo  -e " \n${RED} These commits likely bypassed git hooks. Recommended actions:${NC} " 
224+   echo  -e " 1. ${YELLOW} Verify hooks are installed:${NC}  scripts/install-git-hooks" 
225+   echo  -e " 2. ${YELLOW} Never use --no-verify flag${NC} " 
226+   echo  -e " 3. ${YELLOW} Avoid GitHub web interface for commits${NC} " 
227+   echo  -e " 4. ${YELLOW} Amend commits if possible:${NC}  git rebase -i ${BASE_COMMIT} " 
228+   echo 
229+ 
230+   throw " Git hook compliance validation failed. Please fix the issues above." 
231+ fi 
232+ 
233+ if  [[ $warnings  -gt  0 ]];  then 
234+   echo  -e " \n${YELLOW} Some commits have quality warnings but passed basic validation.${NC} " 
47235fi 
48236
49237exit  0
0 commit comments