Hack 9. Automate Checkin Code Reviews


Hack 9. Automate Checkin Code Reviews

Let Perl::Tidy be your first code reviewon every Subversion checkin!

In a multideveloper project, relying on developers to follow the coding standards without fail and to run perltidy against all of their code ("Enforce Local Style" [Hack #7]) before every checkin is unrealistic, especially because this is tedious work. Fortunately, this is an automatable process. If you use Subversion (or Svk), it's easy to write a hook that checks code for tidiness, however you define it.

The Hack

For various reasons, it's not possible to manipulate the committed files with a pre-commit hook in Subversion. That's why this is a hack.


Within your Subversion repository, copy the hooks/post-commit.tmpl file to hooks/post-commitunless you already have the file. Remove all code that runs other commands (again, unless you're already using it). Add a single line:

perl /usr/local/bin/check_tidy_file.pl "$REPOS" "$REV"

Adjust the file path appropriately. Make the hooks/post-commit file executable with chmod +x on Unix.

Finally, save the check_tidy_file.pl program to the path you used in the file. The program is:

#!/usr/bin/perl

use strict;
use warnings;

use Perl::Tidy;
               
use File::Temp;
use File::Spec::Functions;

my $svnlook      = '/usr/bin/svnlook';
my $diff         = '/usr/bin/diff -u';

# eat the arguments so as not to confuse Perl::Tidy
my ($repo, $rev) = @ARGV;
@ARGV            = ( );

my @diffs;

for my $changed_file (get_changed_perl_files( $repo, $rev ))
{
    my $source = get_revision( $repo, $rev, $changed_file );
    Perl::Tidy::perltidy( source => \\$source, destination => \\(my $dest) );
    push @diffs, get_diff( $changed_file, $source, $dest );
}

report_diffs( @diffs );

sub get_changed_perl_files
{
    my ($repo, $rev) = @_;

    my @files;

    for my $change (\Q$svnlook changed $repo -r $rev\Q)
    {
        my ($status, $file) =  split( /\\s+/, $change );
        next unless $file   =~ /\\.p[lm]\\z/;
        push @files, $file;
    }

    return @files;
}

sub get_revision
{
    my ($repo, $rev, $file) = @_;
    return scalar \Q$svnlook cat $repo -r $rev $file\Q;
}

sub get_diff
{
    my $filename        = shift;
    return if $_[0] eq $_[1];

    my $dir   = File::Temp::tempdir( );
    my @files = map { catdir( $dir, $filename . $_ ) } qw( .orig .tidy );

    for my $file (@files)
    {
        open( my $out, '>', $file ) or die "Couldn't write $file: $!\\n";
        print $out shift;
        close $out;
    }

    return scalar \Q$diff @files\Q;
}

sub report_diffs
{
    for my $diff (@_)
    {
        warn "Error:\\n$diff\\n";
    }
}

When Subversion finishes committing a checkin to the repository, it calls the hooks/post-commit script, which itself launches other programs, passing the repository path and the number of the just-committed revision. This program uses the svnlook command to find the modified files, skipping everything that's not a Perl program or module (files ending in .pl or .pm).

For each of these files, it grabs the entire contents from the just-completed revision and runs it through Perl::Tidy (the actual engine of the perltidy utility). If the resulting file is the same as the revision, everything is fine. Otherwise, it runs a diff utility to see the changes necessary to make the file tidy. From there, report_diffs( ) receives a list of these differences.

Hacking the Hack

As it is now, the program is only useful when run directly with the path to the repository and a revision number. It could instead write the differences to a file, automatically check in the revised versions in a new checkin, or e-mail the diffs to a list of programmers.

To use a .perltidyrc file with the tidier program, add the perltidy => $rcfile_path arguments to the perltidy( ) call, where $rcfile_path contains the path to the .perltidyrc file to use.