#!/usr/bin/perl -w # # $FreeBSD$ # # # Perl filter to handle pre-commit checking of files. This program # records the last directory where commits will be taking place for # use by the log_accum.pl script. For new files, it forces the # existence of a RCS "Id" keyword in the first ten lines of the file. # For existing files, it checks the version number in the "Id" line to # prevent losing changes because an old version of a file was copied # into the direcory. # # Possible future enhancements: # # # Check for cruft left by unresolved conflicts. Search for # "^<<<<<<<$", "^-------$", and "^>>>>>>>$". # # Look for a copyright and automagically update it to the # current year. # # Contributed by David Hampton <hampton@cisco.com> # require 5.003; # to be sure. log_accum needs perl5 use strict; use lib $ENV{CVSROOT}; use CVSROOT::cfg; my $CVSROOT = $ENV{'CVSROOT'} || die "Can't determine \$CVSROOT!"; ############################################################ # # Constants # ############################################################ my $ENTRIES = "CVS/Entries"; # The "Id" header to check for. my $HEADER = $cfg::IDHEADER; ############################################################ # # Error messages # ############################################################ my $NoId = "%s - \"\$$HEADER\$\" keyword is either missing or corrupt.\n"; # Protect string from substitution by RCS. my $NoName = "%s - The ID line should contain only \$$HEADER\$ for " . "a newly created file.\n"; #$DelPath = " #%s - The old path and version has been deleted from \$$HEADER\$.\n"; my $BadName = "%s - The pathname '%s' in the \$$HEADER\$ line does not match the actual filename.\n"; my $BadVersion = "%s - GRRR!! You spammed your copy of the file which was based upon version %s, with a different version based upon %s. Please move your '%s' out of the way, perform an update to get the current version, and then CAREFULLY merge your changes into that file.\n"; my $DOSLineBreak = "%s - Dos/Windows/Mac linebreaks encountered (line %d).\n"; my $DOSLineErr = "PLEASE use only UNIX linebreaks.\n"; ############################################################ # # Subroutines # ############################################################ # Write a single line to a file. sub write_line { my $filename = shift; # File to write to. my $line = shift; # Line to write to the file. open FILE, ">$filename" or die "Cannot open $filename, stopped\n"; print FILE "$line\n"; close FILE; } # Check to see whether a file is mentioned in the exclusion file. sub exclude_file { my $filename = shift; my $directory = shift; my $path = "$directory/$filename"; if (open(EX, "< $cfg::EXCLUDE_FILE")) { while (<EX>) { chomp; my $ex_entry = $_; # Skip comments and blank lines. next if $ex_entry =~ /^#/; next if $ex_entry =~ /^$/; if ($path =~ /$ex_entry/) { close(EX); return(1); } } close(EX); } # File shouldn't be excluded. return(0); } sub check_version { my $filename = shift; my $directory = shift; my $hastag = shift; my $lastversion = shift; my $found_rcsid; # True if our rcsid was found in the file. my $rcsid; # The rcsid that was in the file. my $rcsid_info; # The expanded values of the rcsid. my $rname; # The file pathname, parsed from the rcsid. my $version; # The file version, parsed from the rcsid. my $dos_line_brk_found; # True if we found a DOS line break in the file. my $line_number; # Keep track of where the line break is. # not present - either removed or let cvs deal with it. return 0 unless -f $filename; # Search the file for our rcsid. open FILE, $filename or die "Cannot open $filename, stopped\n"; $found_rcsid = 0; $dos_line_brk_found = 0; $line_number = 0; $rcsid_info = ""; while (<FILE>) { $line_number++; if ( /^.*(\$$HEADER(: ([^\$]* )?)?\$)/) { $rcsid = $1; $rcsid_info = $3 || ""; $found_rcsid = 1; } elsif ( $cfg::NO_DOS_LINEBREAKS and /\r/ ) { # Found a DOS linebreak printf($DOSLineBreak, "$directory/$filename", $line_number); $dos_line_brk_found = 1; } } close FILE; ($rname, $version) = split /\s/, $rcsid_info; # The file must not contain DOS linebreaks. if ($dos_line_brk_found) { print $DOSLineErr; return(1); } # The file should have had an rcsid in it! unless ($found_rcsid) { printf($NoId, "$directory/$filename"); return(1); } # Ignore version mismatches (MFC spamming etc) on branches. if ($hastag) { return (0); } # A new file should have an unexpanded rcsid. if ($lastversion eq '0') { unless ($rcsid_info eq "") { printf($NoName, "$directory/$filename"); return(1); } return(0); } # It's ok for the rcsid to be not expanded. if ($rcsid_info eq "") { return (0); # if ($directory =~ /^ports\//) { # return (0); # ok for ports # } # # Don't know whether to allow or trap this. It means # # one could bypass the version spam checks by simply # # using a bare tag. # printf($DelPath, "$directory/$filename"); # return(1); } # Check that the file name in the rcsid matches reality. if ($rname ne "$directory/$filename,v") { # If ports and the pathname is just the basename # (eg: somebody sent in a port with $Id$ and the # committer changed Id -> $HEADER and the version # numbers otherwise match. if (!($directory =~ /^ports\// && $rname eq "$filename,v")) { printf($BadName, "$directory/$filename,v", $rname); return(1); } } # Check that the version in the rcsid matches reality. if ($lastversion ne $version) { printf($BadVersion, $filename, $lastversion, $version, "$directory/$filename"); return(1); } return(0); } # Do file fixups, i.e. replacing $ Id: .* $ with $ Id $. sub fix_up_file { my $filename = shift; # not present - either removed or let cvs deal with it. return 0 unless -f $filename; open F, "< $filename" or die "Can't open $filename!\n"; my @file = <F>; close F; open F, "> $filename.tmp" or die "Can't create $filename tmpfile!\n"; while (@file) { my $line = shift @file; $line =~ s/\$$HEADER:.*?\$/\$$HEADER\$/g; print F $line or die "Out of disk space?\n"; } close F; # overwrite the original file...... system($cfg::PROG_MV, "$filename.tmp", $filename); die "Can't recreate $filename!\n" if $? >> 8; } ############################################################# # # Main Body # ############################################################ #print("ARGV - ", join(":", @ARGV), "\n"); #print("id - ", $cfg::PID, "\n"); # # Suck in the Entries file # my %cvsversion; my %cvstag; open ENTRIES, $ENTRIES or die "Cannot open $ENTRIES.\n"; while (<ENTRIES>) { chomp; next if /^D/; my ($filename, $ver, $stamp, $opt, $tag) = split '/', substr($_, 1); $cvsversion{$filename} = $ver; $cvstag{$filename} = $tag; $stamp = $opt; #silence -w } close ENTRIES; my $directory = $ARGV[0]; shift @ARGV; $directory =~ s,^$CVSROOT[/]+,,; my $check_id = 0; if ($directory =~ /^src\/contrib/) { $check_id = 3; } if ($directory =~ /^src\/crypto/) { $check_id = 3; } # # Now check each file name passed in, except those excluded. # if ($cfg::CHECK_HEADERS or $cfg::WARN_HEADERS) { my $failed = 0; foreach my $arg (@ARGV) { my $hastag = ($cvstag{$arg} ne ''); next if ($check_id == 3 && $hastag); # Ignore the file if it's in the exclude list. next if exclude_file($arg, $directory); # Check to make sure that the file hasn't had # it's revision string changed. $failed += &check_version($arg, $directory, $hastag, $cvsversion{$arg}); # Unexpand the rcsid if required. fix_up_file($arg) if $cfg::UNEXPAND_RCSID and !$failed; } if ($failed and not $cfg::WARN_HEADERS) { print "\n"; unlink($cfg::LAST_FILE); exit(1); } } # # Record this directory as the last one checked. This will be used # by the log_accumulate script to determine when it is processing # the final directory of a multi-directory commit. # &write_line($cfg::LAST_FILE, $directory); exit(0);