#!/usr/local/bin/perl # vim:sts=4 sw=4 et # perltidy -bext=/ -se -i=4 -it=2 -ci=2 -xci -l=132 -pt=2 -ce -cti=1 -cab=4 -cb -cbo=0 -wbb="% + - * / x != == >= <= =~ !~ < > | &" -enc=utf8 -wn -sot -sct -asc -tqw -sbq=0 -csc -csct=30 use strict; use warnings; use 5.024; use Git; ################################################################ # Helper functions ################################################################ my $git = Git->repository; my $branch_main = 'refs/heads/main'; my $re_quarterlies = qr{^refs/heads/20\d\dQ\d\z}oms; { # sub context to avoid leaking @push_options my @push_options; for (my $i = 0 ; $i < $ENV{GIT_PUSH_OPTION_COUNT} ; ++$i) { push @push_options, $ENV{"GIT_PUSH_OPTION_${i}"}; } sub has_option { my ($opt) = @_; for (@push_options) { return 1 if $opt eq $_; } return 0; } ## end sub has_option } my @quarterlies; for ($git->command('show-ref', '--heads')) { my ($hash, $ref) = split / /; next if $ref !~ m{refs/heads/\d\d\d\dQ\d\z}oms; push @quarterlies, $ref; } @quarterlies = sort @quarterlies; my $latest_quarterly = $quarterlies[-1]; sub do_die { die "\n================================================================\n" . join("\n", @_) . "\n================================================================\n\n"; } sub short_ref { my ($ref) = @_; $ref =~ s{refs/heads/}{}oms; return $ref; } ################################################################ # Here starts actual hooks ################################################################ # Some filenames are forbidden in the repo. sub deny_filenames { my ($rev, @changed) = @_; for my $line (@changed) { if ( $line =~ m{\A(?:CVS|[.](?:svn|git))\z}oms # cvs/svn/git || $line =~ m{[.](?:rej|orig)\z}oms # patch || $line =~ m{[.]core\z}oms # core file ) { do_die 'This file in your commit look suspiciously like core file,', 'patch leftover, CVS, Subversion or git directory:', $line, 'Please double-check your commit and try committing again.'; } ## end if ($line =~ m{\A(?:CVS|[.](?:svn|git))\z}oms...) if ($line =~ m{(?:\A|/)Makefile.local\z}oms) { do_die 'Makefile.local is a user file and MUST NOT be committed:', $line; } } ## end for my $line (@changed) } ## end sub deny_filenames ################################################################ # Empty files forbidden sub empty { my ($rev, @changed) = @_; for my $file (@changed) { my $content = $git->command('show', "$rev:$file"); next if length($content) > 0; do_die "Some files in your commit are empty: $file", "Please fix this and try committing again."; } ## end for my $file (@changed) } ## end sub empty ################################################################ # vulm.xml has to be committed alone, as it is never merged back to the # quarterly branches. sub vuxml_unique { my ($ref, $rev, @changed) = @_; my ($seen, $other) = (0, 0); for my $line (@changed) { if ($line =~ m{\Asecurity/vuxml/vuln(?:-\d{4})?.xml\z}oms) { $seen = 1; } else { $other = 1; } } ## end for my $line (@changed) if ($seen && $branch_main ne $ref) { do_die "Commits to security/vuxml/vuln.xml are only allowed on main"; } if ($seen && $other) { do_die "Commit to security/vuxml/vuln.xml first, and then other files"; } } ## end sub vuxml_unique ################################################################ # Check git cherry-pick ran with `-x` sub cherry_pick { my ($ref, $log) = @_; # Only do this on quarterlies return if $ref !~ $re_quarterlies; # Gave the magic push option return if has_option('direct-quarterly-commit'); # Found the actual magic words return if $log =~ m{ ^ # Start of line [(] # A litteral opening parenthesis cherry[ ]picked[ ]from[ ]commit # The magic words [0-9a-fA-F]{30,} # A commit hash [)] # A litteral closing parenthesis $ # End of line }omsx; do_die # Comments "$ENV{GL_USER}, you are pushing a commit to ${\(short_ref($ref))} which does", # to get this 'not seems to be a cherry-pick.', # indented '', # properly 'If you did a cherry-pick, you probably forgot to add `-x`,', # by 'make sure you do run `git cherry-pick -x `.', # perltidy '', # 'If you did a direct commit, make sure it was approved first, and then run:', # "\tgit push --push-option=direct-quarterly-commit"; } ## end sub cherry_pick ################################################################ # Check that commits only go to the latest quarterly branch sub unsupported_quarterly { my ($ref) = @_; # Only applies to quarterlies return if $ref !~ $re_quarterlies; # Gave the magic push option return if has_option('direct-quarterly-commit'); do_die # Comments "$ENV{GL_USER}, you are pushing a commit to ${\(short_ref($ref))} which is not", # for "the latest quarterly branch. The latest is ${\(short_ref($latest_quarterly))}.", # indentation '', # 'Please check that you really mean to do this, and got approval, use:', # "\tgit push --push-option=unsupported-quarterly"; } ## end sub unsupported_quarterly ################################################################ # Check log does not contains stuff generated by phabricator sub stomp_bad_formatting { my ($log) = @_; if ($log =~ m|\n\nReviewers:[\t ]+|oms) { do_die "Non-standard/badly formatted template - found 'Reviewers:' instead of 'Reviewed by:'."; } if ($log =~ m|\n\nSubscribers:[\t ]+|oms) { do_die "Non-standard/badly formatted template - found 'Subscribers:'."; } } ## end sub stomp_bad_formatting ################################################################ # Main loop, everything called in here. for () { chomp; my ($old, $new, $ref) = split / /; unsupported_quarterly($ref); for my $rev ($git->command('log', '--format=%H', $new, '--not', '--all')) { my @changed = $git->command('diff', '--name-only', "$rev~1..$rev"); my $log = $git->command('show', '-s', '--format=%B', $rev); deny_filenames($rev, @changed); vuxml_unique($ref, $rev, @changed); cherry_pick($ref, $log); stomp_bad_formatting($log); empty($rev, @changed); } ## end for my $rev ($git->command('log',...)) } ## end for () exit 1;