#! /usr/bin/perl -w # Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies). # Contact: http://www.qt-project.org/ # # You may use this file under the terms of the 3-clause BSD license. # See the file LICENSE from this package for details. # use strict; use Cwd; if ($#ARGV < 0 or $#ARGV > 1 || ($#ARGV == 1 && $ARGV[1] !~ /^(strict|gerrit)$/)) { print STDERR "Usage: $0 [strict]\n"; exit 2; } my $sha1 = $ARGV[0]; my $gerrit = ($#ARGV == 1 && $ARGV[1] eq "gerrit"); my $strict = $gerrit || ($#ARGV == 1 && $ARGV[1] eq "strict"); my $repo = getcwd(); $repo =~ s,/?\.git$,,; $repo =~ s,^.*/,,; my %config = (); for (`git config --list`) { if (/^sanity\.\Q$repo\E\.([^=]+)=(.*$)/) { $config{$1} = $2; } } my %cfg = (); if (defined $ENV{GIT_PUSH}) { foreach my $c (split ",", $ENV{GIT_PUSH}) { $cfg{$c} = 1; } } if (defined $config{flags}) { foreach my $c (split ",", $config{flags}) { $cfg{$c} = 1; } } my $fail = 0; my $printed = $gerrit; my $file = ""; my $fail_file = "-"; my $summary; my ($lpfx, $elpfx) = ($gerrit ? ("", "\n") : ("*** ", "***\n")); sub printerr() { die "cannot run git: ".$! if ($? < 0); die "git crashed with signal ".$? if ($? & 127); die "git exited with status ".($? >> 8) if ($?); } sub complain() { my ($msg, $key, $level) = @_; my $pfx; if (!$printed) { $summary =~ s/^(.{50}).{5,}$/$1\[...]/; print "***\n*** Suspicious changes in commit ".$sha1." (".$summary."):\n"; $printed = 1; } if (length($file)) { if ($file ne $fail_file) { print $elpfx.$lpfx.$file.":\n"; $fail_file = $file; } $pfx = $lpfx." - "; } else { if ($file ne $fail_file) { print $elpfx; $fail_file = ""; } $pfx = $lpfx."- "; } $level = 0 if (!defined($level) || ($level < 0 && $strict && length($key))); if ($level >= 0) { $fail = $level + 1 if ($level >= $fail); if ($gerrit) { print $pfx.$msg."\n"; } else { print $pfx.$msg." (key \"".$key."\")\n"; } } else { print $pfx."Hint: ".$msg."\n"; } } my $ln = 0; my $iswip = defined($cfg{wip}); my $revok = defined($cfg{revby}); my $badlog = defined($cfg{log}); my $parents = 0; my ($badauthor, $badcommitter) = (0, 0); my ($revert1, $revert2, $nonrevert) = (0, 0, 0); open MSG, "git log -1 --pretty=raw ".$sha1." |" or die "cannot run git: $!"; while () { chomp; if (!s/^ //) { if (/^parent /) { $parents++ ; } elsif (/^author .*\.\(none\)/) { $badauthor = 1; } elsif (/^commiter .*\.\(none\)/) { $badcommitter = 1; } next } if ($ln == 0) { $summary = $_; $revert1 = 1 if (/^Revert ".*"$/); if (!$iswip && $parents < 2 && /\bWIP\b|\*{3}|^(?:squash|fixup)! |^(.)\1*$/i) { &complain("Apparently pushing a Work In Progress", "wip", 1); } elsif (!$iswip && !$badlog && length($_) < 7) { &complain("Log message summary is too short", "log"); } elsif (!$badlog && !$revert1 && length($_) > 120) { &complain("Log message summary is excessively long", "log"); } elsif ($parents < 2 && !$revert1 && length($_) > 70) { &complain("Aim for shorter log message summaries", "", -1); } } else { if (/^This reverts commit [[:xdigit:]]{40}\.?$/) { $revert2 = 1; } elsif (!/^[-\w]+:|^$/) { $nonrevert = 1; } if ($ln == 1) { if (!$badlog && $_ ne "") { &complain("2nd line of log message is not empty", "log"); } } else { if (!$revok && /^Reviewed-by: *(pending|TBD)?$/i) { &complain("Bogus or empty Reviewed-by header", "revby", 1); $revok = 1; } } } $ln++; } close MSG; printerr; if ($revert1 && $revert2 && !$nonrevert) { &complain("Revert without justification", "revert", 1); } # These need to be delayed, because at the time they are found the subject is not known yet. if ($badauthor) { &complain("Bogus author email", "email", 1); } if ($badcommitter) { &complain("Bogus committer email", "email", 1); } my $chunk = 0; my @addi = (); my @deli = (); my $nonws; my $ws; my $mixws_check = 0; my $lineno = 0; my @ws_files = (); my %ws_lines = (); # hash of lists my $braces = 0; my $open_key = qr/\s*#\s*if|.*{/; my $close_key = qr/\s*#\s*endif|.*}/; my $kill_all_ws = qr/\s+((?:\"(?:\\.|[^\"])*\"|\S)+)/; # Collapse all whitespace not inside strings. my $kill_nl_ws = qr/((?:\"(?:\\.|[^\"])*\"|\S)+)\s+/; # Collapse all non-leading whitespace not inside strings. sub flushChunk() { my $loc_nonws = 0; my $nlonly = 1; my ($ai, $di) = (0, 0); while (!$loc_nonws) { my ($a, $d) = ("", ""); while ($ai < @addi) { $a = $addi[$ai++]; $a =~ s/\s+$//; if (length($a)) { $nlonly = 0; last; } } while ($di < @deli) { $d = $deli[$di++]; $d =~ s/\s+$//; if (length($d)) { $nlonly = 0; last; } } last if (!length($a) && !length($d)); $a =~ /^$close_key/o and $braces--; $d =~ /^$close_key/o and $braces++; if ($braces) { $a =~ s/$kill_nl_ws/$1/go; $d =~ s/$kill_nl_ws/$1/go; } else { $a =~ s/$kill_all_ws/$1/go; $d =~ s/$kill_all_ws/$1/go; } $loc_nonws = 1 if ($a ne $d); $a =~ /^$open_key/o and $braces++; $d =~ /^$open_key/o and $braces--; } while ($ai < @addi) { my $a = $addi[$ai++]; $a =~ /^$close_key/o and $braces--; $a =~ /^$open_key/o and $braces++; } while ($di < @deli) { my $d = $deli[$di++]; $d =~ /^$close_key/o and $braces++; $d =~ /^$open_key/o and $braces--; } if ($loc_nonws) { $nonws = 1; } elsif (!$nlonly) { $ws = 1; push @ws_files, $file if (!defined($ws_lines{$file})); push @{$ws_lines{$file}}, $lineno - $#addi; } @addi = @deli = (); $chunk = 0; } sub formatSize($) { my $sz = shift; if ($sz >= 10 * 1024 * 1024) { return int($sz / (1024 * 1024))."MiB"; } elsif ($sz >= 10 * 1024) { return int($sz / 1024)."KiB"; } else { return int($sz)."B"; } } sub isExe($) { my $sha = shift; my $type = `git cat-file -p $sha | file -b -`; return $type =~ /^(ELF|PE32) /; } my @style_fails = (); sub styleFail($) { my $why = shift; push @style_fails, $lineno.": ".$why; } sub flushFile() { if (@style_fails) { &complain("Style issues", "style", -1); for my $sf (@style_fails) { print $lpfx." ".$sf."\n"; } @style_fails = (); } } my $merge; my $new_file; my $maybe_bin; my $is_submodule; my $clike; my $size; my $check_gen = 0; my $crlf_fail; my $conflict_fail; my $tabs_check; my $ws_check; my $tsv_check; my $ctlkw_check; open DIFF, "git diff-tree --no-commit-id --diff-filter=ACMR --src-prefix=\@old\@/ --dst-prefix=\@new\@/ --full-index -r -U100000 --cc -C -l1000 --root ".$sha1." |" or die "cannot run git: $!"; while () { if (/^-/) { if ($mixws_check) { /^--- / and next; push @deli, substr($_, 1); $chunk = 1; } next; } if ($lineno < 50 && $check_gen) { if (/All changes made in this file will be lost|This file is automatically generated|DO NOT EDIT|DO NOT delete this file|[Gg]enerated by|uicgenerated|produced by gperf/) { &complain("Adding generated file", "generated") if ($new_file && !defined($cfg{generated})); $ws_check = 0; $check_gen = 0; } } if (/^\+/) { if (/^\+\+\+ /) { # This indicates a text file; binary files have "Binary files ... and ... differ" instead. $maybe_bin = 0; if ($file =~ /(~|\.(old|bak))$/i) { &complain("Adding backup file", "backup") if ($new_file && !defined($cfg{backup})); $ws_check = 0; } elsif ($file =~ /\.(prl|la|pc|ilk)$/i) { &complain("Adding build artifact", "generated") if ($new_file && !defined($cfg{generated})); $ws_check = 0; } else { $check_gen = 1; } next; } $lineno++; if ($merge) { # Consider only lines which are new relative to both parents, i.e., were added during the merge. s/^\+\+// or next; } else { $_ = substr($_, 1); if ($mixws_check) { push @addi, $_; $chunk = 1; } } if (!$crlf_fail && /\r\n$/) { $crlf_fail = 1; &complain("CRLF line endings", "crlf"); } if (!$conflict_fail && /^(?:[<>=]){7}( |$)/) { &complain("Unresolved merge conflict", "conflict"); $conflict_fail = 1; } if ($ws_check) { if ($tsv_check) { styleFail("Mixing spaces with TABs") if (/^ +\t|\t +/); } else { styleFail("Space indent followed by a TAB character") if (/^ +\t/); styleFail("TAB character in non-leading whitespace") if (/\S *\t/); styleFail("Trailing whitespace") if (/[ \t]\r?\n$/); if ($tabs_check) { styleFail("Leading tabs") if (/^\t+/); if ($ctlkw_check) { styleFail("Flow control keywords must be followed by a single space") if (/\b(if|for|foreach|Q_FOREACH|while|do|switch)(| +)\(/); } } } } } else { flushChunk() if ($chunk); if (/^ /) { $lineno++ if (!$merge || !/^ -/); next; } if ($merge ? /^\@\@\@ -\S+ -\S+ \+(\d+)/ : /^\@\@ -\S+ \+(\d+)/) { $lineno = $1 - 1; next; } if (/^diff /) { flushFile(); if (/^diff --git \@old\@\/.+ \@new\@\/(.+)$/) { $merge = 0; } elsif (/^diff --cc (.+)$/) { $merge = 1; } else { print STDERR "Warning: cannot parse diff header '".$_."'\n"; next; } $file = $1; #print "*** got file ".$file.".\n"; $clike = ($file =~ /\.(c|cc|cpp|c\+\+|cxx|qdoc|m|mm|h|hpp|hxx|cs|java|js|qs|qml|g|y|ypp|pl|glsl)$/i); my $foreign = ($file =~ /\/3rdparty\//); $new_file = 0; $maybe_bin = 0; $is_submodule = 0; $crlf_fail = defined($cfg{crlf}); $mixws_check = !$merge && !$foreign && $clike && !defined($cfg{mixws}); $ws_check = !defined($cfg{style}) && !$foreign && ($file !~ /\.(ts|diff|patch)$|^\.gitmodules$/); $tsv_check = $ws_check && ($file =~ /((^|\/)objects\.map$|\.tsv$)/); $tabs_check = $ws_check && !$tsv_check && !defined($cfg{tabs}) && ($file !~ /((^|\/)Makefile\b|debian[.\/]rules|\.(def|spec|changes)$)/); $ctlkw_check = $tabs_check && $clike; $conflict_fail = defined($cfg{conflict}); $braces = 0; $check_gen = 0; next; } if ($maybe_bin && /^Binary files /) { if ($new_file) { if (!defined($cfg{generated}) && ($file =~ /\.(obj|o|lib|a|dll|so|exe|exp|qm|pdb|idb|suo)$/i || isExe($maybe_bin))) { &complain("Adding build artifact", "generated"); } } else { if (!defined($cfg{giant}) && $size > (2 << 20)) { &complain("Changing huge binary file (".formatSize($size)." > 2MiB)", "giant", 1); } } next; } if ($_ eq "new file mode 160000\n") { $is_submodule = 1; next; } if (!$is_submodule && /^index ([\w,]+)\.\.(\w+)(?! 160000)( |$)/) { my ($old_trees, $new_tree) = ($1, $2); #print "*** got index $old_trees $new_tree.\n"; $size = `git cat-file -s $new_tree`; my $issrc = $clike || ($file =~ /\.(s|asm|pas|l|m4|bat|cmd|sh|py|qdoc(conf)?)$/i); if ($old_trees =~ /^0{40}(,0{40})*$/) { #print "*** adding ".$file.".\n"; if (!$conflict_fail && $file =~ /\.(BACKUP|BASE|LOCAL|REMOTE)\.[^\/]+$/) { &complain("Adding temporary file from merge conflict resolution", "conflict", 1); $conflict_fail = 1; } if (!defined($cfg{alien}) && $file =~ /\.(sln|vcproj|vcxproj|user)$/i) { &complain("Warning: Adding alien build system file", "alien"); } if ($size > (2 << 20)) { if (!defined($cfg{giant})) { &complain("Adding huge file (".formatSize($size)." > 2MiB)", "giant", 1); } } elsif ($size > 50000 && !$issrc && !defined($cfg{size})) { &complain("Warning: Adding big file (".formatSize($size)." > 50kB)", "size"); } $size = 0; $new_file = 1; } elsif ($size > 20000 && !$issrc && !defined($cfg{size})) { my $old_size = 0; for my $old_tree (split(/,/, $old_trees)) { my $osz = `git cat-file -s $old_tree`; $old_size = $osz if ($osz > $old_size); } if ($size > $old_size * 3 / 2) { &complain("Warning: Increasing file size by more than 50% (". formatSize($old_size)." => ".formatSize($size).")", "size"); } } $maybe_bin = $new_tree; next; } } } close DIFF; printerr; flushFile(); if ($mixws_check) { flushChunk() if ($chunk); if ($nonws and $ws) { $file = ""; &complain("Mixing whitespace-only changes with other changes", "mixws", -1); for my $fn (@ws_files) { print $lpfx." WS-only in ".$fn.": ".join(", ", @{$ws_lines{$fn}})."\n"; } } } exit ($gerrit ? (!$fail ? 11 : (10 - $fail)) : $fail)