#!/usr/bin/perl # IBM_PROLOG_BEGIN_TAG # This is an automatically generated prolog. # # $Source: src/tools/hooks/verify-commit $ # # OpenPOWER sbe Project # # Contributors Listed Below - COPYRIGHT 2016 # [+] International Business Machines Corp. # # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. See the License for the specific language governing # permissions and limitations under the License. # # IBM_PROLOG_END_TAG use strict; my $issueFound = 0; my $errorFound = 0; my $projectName = $ENV{'PROJECT_NAME'}; # Relative path of import tree from project root my $importPrefix = $ENV{'IMPORT_REL_PATH'}."/"; verifyPatchSet(); # Verify the patch contents. verifyCommitMsg(); # Verify the commit message. # Finish out the divider. if ($issueFound) { print "------------------------------------------------------------\n"; } # Return a bad RC if we found an error. Let warnings pass. exit ($errorFound ? -1 : 0); ########################### Subroutines ################################ # @sub verifyPatchSet # # Extract the contents (lines changed) from the patch set and verify. # sub verifyPatchSet { # git-diff options: # * Diff against the previous commit (HEAD~1) # * Filter only added and modified files (AM). # * Show only the lines changed, with no context (U0). # Grep only the lines marked with "+" (instead of "-") to find just the # actions done by this patchset and not the content removed. open PATCH_CONTENTS, "git diff HEAD~1 --diff-filter=AM -U0 | ". "grep -e \"^+\" -e \"^@@.*+[0-9]\\+\" |"; my %fileContents = (); my $lastFile = ""; my $fileLines = (); my $lineCount = 0; while (my $line = ) { chomp $line; # Line starting with "+++ b/path/to/file" indicate a new file. if ($line =~ m/^\+\+\+ b\/(.*)/) { # Save previous file into the map. if ($lastFile ne "") { $fileContents{$lastFile} = $fileLines; $fileLines = (); $lineCount = 0; } $lastFile = $1; } # Lines starting with "@@" indicates a seek in the file, so update # line numbers. elsif ($line =~ m/^@@.*\+([0-9]+)/) { $lineCount = $1 - 1; } else { $line =~ s/^\+//; # filter off the leading + symbol. $lineCount++; push @{$fileLines}, [$line, $lineCount]; } } if ($lastFile ne "") # Save last file into the map. { $fileContents{$lastFile} = $fileLines; $fileLines = (); } # Verify each line of each file. foreach my $file (sort keys %fileContents) { foreach my $line (@{$fileContents{$file}}) { verifyFileLine($file, @{$line}[0], @{$line}[1]); } } } # @sub verifyFileLine # # Checks a particular line of the file for the following issues: # * Warning: Lines longer than 80 characters, except in trace statement. # * Warning: Trailing whitespace. # * Warning: Tab characters outside of makefiles. # * Warning: TODO or FIXME type tag without a corresponding RTC number. # * Warning: NOMERGE tag found. # sub verifyFileLine { my ($file,$line,$count) = @_; # Check if file was mirrored my $mirror = 0; if ($file =~ m/^$importPrefix/) { $mirror = 1; } # Check line length. if (length($line) > 80) { # Allow trace statements to slide. if (($line =~ m/TRAC[DSFU]/) || ($line =~m/TS_FAIL/) || ($line =~m/printk/) || ($line =~m/displayf/) || ($line =~ m/FAPI_(INF|IMP|ERR|DBG|SCAN)/)) { } else { warning($file,$line,$count, (sprintf "Length is more than 80 characters (%d).", length($line)) ); } } # Check trailing whitespace. if ($line =~ m/\s$/) { warning($file,$line,$count, "Trailing whitespace found."); } # Check tabs. if ($line =~ m/\t/) { # Makefiles are ok (require tabs). if (not (($file =~ m/makefile/) || ($file =~ m/\.mk/))) { warning($file,$line,$count, "Tab character found."); } } # Check "TODO" or "FIXME" type comments. if (($line =~ m/TODO/i) || ($line =~ m/FIXME/i)) { if ( (not ($line =~ m/RTC[\s:]\s*[0-9]+/)) && (not ($line =~ m/CQ[\s:]\s*[A-Z][A-Z][0-9]+/))) { warning($file,$line,$count, "TODO/FIXME tag without corresponding RTC or CQ number."); } } # Check "NOMERGE" type comment. if ($line =~ m/NOMERGE/i) { warning($file,$line,$count, "NOMERGE tag found."); } # Check for "Confidential", unless it's a mirrored commit if ($line =~ m/Confidential/i && $projectName =~ m/HostBoot/i && !$mirror) { unless (($file =~ m/verify-commit/) || ($file =~ m/addCopyright/)) { error($file,$line,$count, "File contains 'Confidential'."); } } } # @sub verifyCommitMsg # # Looks at the commit message to verify the following items: # * Topic is exactly 1 line long. # * Lines are less than 80 characters. # * No trailing whitespace. # * Tags, such as 'RTC:', are only found in the footer. # * Untagged lines are not found in the footer. # * RTC tag is formatted correctly. # * Warning for lacking RTC tag. # sub verifyCommitMsg { open COMMIT_CONTENTS, "git log -n1 --pretty=format:%B |"; my $lineCount = 0; my $rtcTag = ""; my $cqTag = ""; my $taggedLine = ""; my $untaggedLine = ""; while (my $line = ) { $lineCount++; chomp $line; # Check line length over 80 characters. if (length($line) > 80) { error("Commit Message",$line,$lineCount, (sprintf "Length is more than 80 characters (%d).", length($line)) ); } # Check trailing whitespace. if ($line =~ m/[^\s]+\s$/) { error("Commit Message",$line,$lineCount, "Trailing whitespace found."); } # Blank line indicates a new "section". if ($line =~ m/^$/) { # Check for tags outside of the footer. # (new section implies previous section was not a footer) if ("" ne $taggedLine) { error("Commit Message",$taggedLine,$lineCount, "Tagged line found outside commit-msg footer."); } $rtcTag = ""; $cqTag = ""; $untaggedLine = ""; $taggedLine = ""; } else { # Check for multi-line topic. if ($lineCount == 2) { error("Commit Message",$line,$lineCount, "Topic must be only one line long."); } } # Verify format of RTC message. if ($line =~ m/^\s*RTC:\s*[0-9]+(.*)/) { if ("" ne $rtcTag) { error("Commit Message",$line,$lineCount, "Duplicate RTC tag found."); } $rtcTag = $line; if ("" ne $1) { error("Commit Message",$line,$lineCount, (sprintf "RTC tag format incorrect (%s).", $1)); } } if ($line =~ m/^\s*CQ:\s*[A-Z][A-Z][0-9]+(.*)/) { if ("" ne $cqTag) { error("Commit Message",$line,$lineCount, "Duplicate CQ tag found."); } $cqTag = $line; if ("" ne $1) { error("Commit Message",$line,$lineCount, (sprintf "CQ tag format incorrect (%s).", $1)); } } # Identify if this is a tagged line or a non-tagged line and store # away. if ($line =~ m/^\s*[A-Za-z0-9\-_]+:[^:]/) { # We allow lines that look like tags in the topic like... # "FOO: Adding support for BAR." if ($lineCount > 1) { $taggedLine = $line; } } else { $untaggedLine = $line; } } # Warn for missing RTC tag. if (("" eq $rtcTag) && ("" eq $cqTag)) { warning("Commit Message","",$lineCount, "Neither RTC nor CQ tag found."); } # Error for a mix of tag / untagged in the last section (ie. untagged # lines in the footer). if (("" ne $untaggedLine) && ("" ne $taggedLine)) { error("Commit Message",$untaggedLine,$lineCount, "Untagged line found in footer."); } } sub warning { my ($file, $line, $count, $statement) = @_; print "------------------------------------------------------------\n"; print "WARNING: $statement\n"; print " $file:$count\n"; print " $line\n"; $issueFound = 1; } sub error { my ($file, $line, $count, $statement) = @_; print "------------------------------------------------------------\n"; print "ERROR: $statement\n"; print " $file:$count\n"; print " $line\n"; $issueFound = 1; $errorFound = 1; }