Hi list,

since we now have subversion, we might as well use the new features it provides. I wrote a precommit hook on the weekend that does some precommit sanity checks:
- It rejects commits changing files in a cws and outside of it (thus
  hopefully preventing some accidental commits to a master
- It checks all *.{cxx|c|hxx|h} files for correct indentation (no tab
  indentation on added/changed lines)
- Adding a new module has to be properly announced in the commit message
- Adding a new toplevel dir in a module has to be properly announced in
  the commit message (this hopefully prevents accidental commit of
  output trees)
- Never allows changes/deleting of tagged versions

I crucified myself to write the hook in perl because I thought it to be the preferred language for releng stuff (I would have preferred python myself).

All checks can be disabled with adding special strings to the commit message.

Im looking forward to comments, ideas and additions. Is it a good idea to use such a precommit hook? Whats relengs position on this?

Have Fun,

Björn
#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;

#global constants
my $SVNLOOK_CMD="svnlook";

#global vars for checks 
my $log_message = "";   # log message of transaction 
my @added_files = ();   # files added by the transaction
my @deleted_files = (); # files deleted by the transaction
my @updated_files = (); # files updated by the transaction
my @all_files = (); # all files touched by the transaction
my @all_dir = (); # all dirs touched by the transaction

my $error_message = ""; # error message of the precommit hook.
# if no check fails, the string is empty 
# If an error occurs append the message

my @checks = (); # add checks to this array

sub check_cws_isolation
{
    return if($main::log_message =~ /IGNORE CWS ISOLATION/); 
    my %touched_cws_hash = ();
    foreach my $cws (@main::all_dirs)
    {
        if($cws =~ s/^cws\/([^\/]+)\/.*$/$1/)
        {
            $touched_cws_hash{$cws} = 1;
        }
        elsif(!($cws =~ /^cws\/$/))
        {
            $touched_cws_hash{"Non-CWS Location"} = 1;
        }
    }
    my @touched_cws = keys(%touched_cws_hash);
    if($main::debug)
    {
        print "DEBUG: touched CWS: @touched_cws\n";
    }
    if(@touched_cws > 1)
    {
        $main::error_message .= "cws isolation: Commit touches multiple cws. Do 
a seperate commit for each cws.\n";
        $main::error_message .= "cws isolation: Touched cws are: 
@touched_cws.\n";
        $main::error_message .= "cws isolation: Add 'IGNORE CWS ISOLATION' to 
log message to force the commit.\n";
    } 
}
push(@checks, \&check_cws_isolation);

# helper: check if there is a tab-indent in new line
sub check_indentation_from_filediff
{
    my @filediff = @_;
    return if(@filediff == 0);
    my $filename = $filediff[0];
    my $nonconforming = "";
    $filename =~ s/[^:]+: (.*)$/$1/;
    return if(!($filename =~ /^.*\.(cxx|hxx|c|h)$/));
    foreach my $line (@filediff)
    {
        $nonconforming = "true" if($line =~ /^\+\ *\t/);
    }
    if($nonconforming)
    {
        $main::error_message .= "indentation: Commit contains new/changed lines 
with nonconforming indentation.\n";
        $main::error_message .= "indentation: Offending file is: $filename.\n";
        $main::error_message .= "indentation: Add 'NONCONFORMING INDENTATION' 
to log message to force the commit.\n";
    }
}

sub check_indentation
{
    return if($main::log_message =~ /NONCONFORMING INDENTATION/);
    my @diff_lines = split("\n", exec_svnlook("diff --no-diff-deleted"));
    my @one_file_diff = ();
    foreach my $line (@diff_lines)
    {
        if($line =~ /^[A-Za]/)
        {
            check_indentation_from_filediff(@one_file_diff);
            @one_file_diff = ();
        }
        push(@one_file_diff, $line);
    }
    check_indentation_from_filediff(@one_file_diff);
}
push(@checks, \&check_indentation);

sub check_new_modules_in_cws
{
    return if($main::log_message =~ /NEW MODULE/); 
    foreach my $new_module (@added_files)
    {
        if($new_module =~ s/^cws\/([^\/]+)\/([^\/]+)\/$/"$2" in "$3"/)
        {
            $main::error_message .= "new module in cws: Commit contains a new 
module.\n";
            $main::error_message .= "new module in cws: new module is: 
$new_module.\n";
            $main::error_message .= "new module in cws: Add 'NEW MODULE' to log 
message to force the commit.\n";
        }
    }     
}
push(@checks, \&check_new_modules_in_cws);

sub check_new_toplevel_in_module
{
    return if($main::log_message =~ /NEW TOPLEVEL DIR/); 
    foreach my $new_topdir (@added_files)
    {
        if($new_topdir =~ s/^cws\/([^\/]+)\/([^\/]+)\/([^\/]+)\/$/"$3" in 
module "$2" in cws "$1"/)
        {
            $main::error_message .= "new toplevel dir in module: Commit 
containing a new toplevel dir in a module.\n";
            $main::error_message .= "new toplevel dir in module: new toplevel 
dir is: $new_topdir.\n";
            $main::error_message .= "new toplevel dir in module: Add 'NEW 
TOPLEVEL DIR' to log message to force the commit.\n";
        }
    }     
}
push(@checks, \&check_new_toplevel_in_module);

sub check_const_tags
{
    return if($main::log_message =~ /MODIFY TAG/); 
    my @files_to_check = ();
    push(@files_to_check, @deleted_files);
    push(@files_to_check, @updated_files);
    foreach my $file (@files_to_check)
    {
        if($file =~ /^tags\/.*$/)
        {
            $main::error_message .= "const tags: Commit changes a tagged 
version.\n";
            $main::error_message .= "const tags: Changed file is: \"$file\".\n";
            $main::error_message .= "const tags: Add 'MODIFY TAG' to log 
message to force the commit.\n";
        }
    }
}
push(@checks, \&check_const_tags);

# main routines
my %svn_link = (
    "transtype" => "transaction",
    "transnr" => "1",
    "repo" => "."); 

sub exec_svnlook
{
    my $subcmd = $_[0];
    my $transtype = $svn_link{"transtype"};
    my $transnr = $svn_link{"transnr"};
    my $repo = $svn_link{"repo"};
    return `$SVNLOOK_CMD $subcmd --$transtype $transnr $repo`;
}

sub read_transaction_globals
{
    my @filelist = split("\n", exec_svnlook("changed"));
    foreach my $file_entry (@filelist)
    {
        my $file = substr($file_entry, 4);
        push(@all_files, $file);
        if($file_entry =~ /^A/)
        {
            push(@main::added_files, $file);
        }
        elsif($file_entry =~ /^D/)
        {
            push(@main::deleted_files, $file);
        }
        elsif($file_entry =~ /^U/)
        {
            push(@main::updated_files, $file);
        }
    }
    @main::all_dirs = split("\n", exec_svnlook("dirs-changed"));
    $main::log_message = exec_svnlook("log");
}

sub show_transaction_globals
{
    return if(!$main::debug);
    print "DEBUG: Added files\n";
    foreach my $file (@main::added_files)
    {
        print "DEBUG:  $file\n";
    }
    print "DEBUG: Deleted files\n";
    foreach my $file (@main::updated_files)
    {
        print "DEBUG:  $file\n";
    }
    print "DEBUG: Updated files\n";
    foreach my $file (@main::deleted_files)
    {
        print "DEBUG:  $file\n";
    }
    print "DEBUG: Touched dirs\n";
    foreach my $dir (@main::all_dirs)
    {
        print "DEBUG:  $dir\n";
    }
    print "DEBUG: Log message:\n";
    print $main::log_message;
}

sub options
{
    my $revision = "";
    my $transaction = "";
    my $repo = "";
    # read options
    my $result = GetOptions(
        "debug" => \$main::debug,
        "revision=s" => \$revision,
        "repo=s" => \$repo, 
        "transaction=s" => \$transaction);
          
    if(!($transaction eq "" ^ $revision eq ""))
    {
        print "Need exactly one of --transaction, --revision\n";
        exit(1);
    } 
    if($repo eq "")
    {
        print "You need to specify a repository with --repo\n";
        exit(1);
    }
    $svn_link{"transtype"} = $transaction ? "transaction" : "revision";
    $svn_link{"transnr"} = $transaction ? $transaction : $revision;
    $svn_link{"repo"} = $repo;
}

sub main 
{
    options();
    read_transaction_globals();
    show_transaction_globals();

    $main::error_message = "";
    foreach my $check (@checks)
    {
        &$check();
    }
    print STDERR $main::error_message;
    exit(2) if(!($main::error_message eq ""));
}

main();

---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to