#!/usr/bin/perl # IBM_PROLOG_BEGIN_TAG # This is an automatically generated prolog. # # $Source: src/build/tools/verify-commit $ # # OpenPOWER HostBoot Project # # Contributors Listed Below - COPYRIGHT 2013,2017 # [+] 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; use Getopt::Long qw(:config pass_through); my $issueFound = 0; my $errorFound = 0; my $warningCount = 0; my $showAll = 0; use constant MAX_WARNINGS => 5; use constant MAX_CODE_LINE_LENGTH => 80; my $projectName = $ENV{'PROJECT_NAME'}; # Relative path of import tree from project root my $importPrefix = $ENV{'IMPORT_REL_PATH'}."/"; my $rtcNumber = rtc_workitem_num(); my $cqNumber = cq_workitem_num(); GetOptions("show-all" => \$showAll); verifyPatchSet(); # Verify the patch contents. verifyCommitMsg(); # Verify the commit message. verifyTodoFixme(); # Make sure there are no TODO/FIXME # associated with current RTC/CQ # Finish out the divider. if ($issueFound) { print "------------------------------------------------------------\n"; my $hiddenWarnings = $warningCount - MAX_WARNINGS; if ($hiddenWarnings > 0 && !$showAll) { print " $hiddenWarnings Warning(s) Hidden\n"; print " Run 'verify-commit --show-all'\n"; 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 MAX_CODE_LINE_LENGTH 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) > MAX_CODE_LINE_LENGTH) { if (($line =~ m/TRAC[DSFU]/) || ($line =~m/TS_FAIL/) || ($line =~m/printk/) || ($line =~m/displayf/) || ($line =~ m/FAPI_(INF|IMP|ERR|DBG|SCAN)/) || ($line =~ m/print/)) { } else { warning($file,$line,$count, (sprintf "Length is more than %d characters (%d).", MAX_CODE_LINE_LENGTH, 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/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 $changeId = ""; 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)); } } if ($line =~ m/^\s*Change-Id:\s*[I][\w]+(.*)/) { if ("" ne $changeId) { error("Commit Message",$line,$lineCount, "Mulitple Change-Id's found."); } $changeId = $line; if ("" ne $1) { error("Commit Message",$line,$lineCount, (sprintf "Change-Id 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." # Unless the Change-Id is in the topic if ($lineCount > 1 || ($line =~ m/Change-Id/)) { $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 missing Change-Id. if ("" eq $changeId) { error("Commit Message","",$lineCount, "Change-Id not 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 verifyTodoFixme # # Verifies that there are no Todo or Fixme # tags in the code with the current commit's # RTC or CQ number. # sub verifyTodoFixme { my $file; my $commentText; my $lineNumber; # Get the list of TODO/FIXME lines in src/ open TODO_LIST, "egrep -rn '(TODO.*(CQ|RTC).*)|(FIXME.*(CQ|RTC).*)' src/* |" or die "Cannot get the list of TODO/FIXME lines.\n"; while(my $line = ) { chomp $line; if(check_rtc_todo_fixme($line)) { ($file, $commentText, $lineNumber) = parse_todo_fixme_line($line); strong_warning($file, $commentText, $lineNumber, "TODO/FIXME tag with the same RTC number as in the current commit message."); } elsif(check_cq_todo_fixme($line)) { ($file, $commentText, $lineNumber) = parse_todo_fixme_line($line); strong_warning($file, $commentText, $lineNumber, "TODO/FIXME tag with the same CQ number as in the current commit message."); } } } sub parse_todo_fixme_line { my $line = shift; my ($file, $lineNumber, $commentText) = split(/:/, $line, 3); return ($file, $commentText, $lineNumber); } sub check_rtc_todo_fixme { my $line = shift; if($line =~ m/RTC\s*:*\s*([0-9]+)/ && $1 eq $rtcNumber && $rtcNumber ne "") { return 1; } return 0; } sub check_cq_todo_fixme { my $line = shift; if($line =~ m/CQ\s*:*\s*([A-Z][A-Z][0-9]+)/ && $1 eq $cqNumber && $cqNumber ne "") { return 1; } return 0; } sub warning { my ($file, $line, $count, $statement) = @_; if ($warningCount < MAX_WARNINGS || $showAll) { print "------------------------------------------------------------\n"; print "WARNING: $statement\n"; print " $file:$count\n"; print " $line\n"; } $issueFound = 1; $warningCount++; } sub strong_warning { my ($file, $line, $count, $statement) = @_; # Always show strong warnings print "------------------------------------------------------------\n"; print "***WARNING: $statement\n"; print " $file:$count\n"; print " $line\n"; $issueFound = 1; $warningCount++; } sub error { my ($file, $line, $count, $statement) = @_; print "------------------------------------------------------------\n"; print "ERROR: $statement\n"; print " $file:$count\n"; print " $line\n"; $issueFound = 1; $errorFound = 1; } # sub rtc_workitem_num # # Determines the RTC WorkItem associated with a git commit. # # @param[in] commit - The git commit, or no parameter for last commit # # @return string - RTC WorkItem number (or ""). # sub rtc_workitem_num { my $commit = shift; my $message; if(defined($commit)) { $message = git_commit_msg($commit); } else { $message = git_commit_msg(""); } if ($message =~ m/RTC:\s*([0-9]+)/) { return $1; } else { return ""; } } # sub cq_workitem_num # # Determines the CQ WorkItem associated with a git commit. # # @param[in] commit - The git commit, or no parameter for last commit # # @return string - CQ WorkItem number (or ""). # sub cq_workitem_num { my $commit = shift; my $message; if(defined($commit)) { $message = git_commit_msg($commit); } else { $message = git_commit_msg(""); } if ($message =~ m/CQ:\s*([A-Z][A-Z][0-9]+)/) { return $1; } else { return ""; } } # sub git_commit_msg # # Get the entire commit message associated with a commit. # # @param[in] commit - The commit to examine. # @return string - The commit message. # sub git_commit_msg { my $commit = shift; open COMMAND, "git log -n1 --pretty=%B $commit |"; my $message = ""; while (my $line = ) { $message = $message.$line; } close COMMAND; return $message; }