Hack #9. Automate Checkin Code Reviews

Let Perl::Tidy be your first code review—on 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

Tip

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-commit—unless 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.

Get Perl Hacks now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.