1*a7c91847Schristos#! @PERL@ -w 2*a7c91847Schristos######################################################################## 3*a7c91847Schristos# 4*a7c91847Schristos# Copyright (c) 2000, 2001 by Donald Sharp <sharpd@cisco.com> 5*a7c91847Schristos# All Rights Reserved 6*a7c91847Schristos# 7*a7c91847Schristos# Some portions Copyright (c) 2002, 2003 by 8*a7c91847Schristos# Derek R. Price <mailto:derek@ximbiot.com> 9*a7c91847Schristos# & Ximbiot <http://ximbiot.com>. 10*a7c91847Schristos# All rights reserved. 11*a7c91847Schristos# 12*a7c91847Schristos# Permission is granted to copy and/or distribute this file, with or 13*a7c91847Schristos# without modifications, provided this notice is preserved. 14*a7c91847Schristos# 15*a7c91847Schristos# This program is free software; you can redistribute it and/or modify 16*a7c91847Schristos# it under the terms of the GNU General Public License as published by 17*a7c91847Schristos# the Free Software Foundation; either version 2, or (at your option) 18*a7c91847Schristos# any later version. 19*a7c91847Schristos# 20*a7c91847Schristos# This program is distributed in the hope that it will be useful, 21*a7c91847Schristos# but WITHOUT ANY WARRANTY; without even the implied warranty of 22*a7c91847Schristos# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23*a7c91847Schristos# GNU General Public License for more details. 24*a7c91847Schristos# 25*a7c91847Schristos######################################################################## 26*a7c91847Schristos 27*a7c91847Schristos=head1 validate_repo.pl 28*a7c91847Schristos 29*a7c91847SchristosScript to check the integrity of the Repository. 30*a7c91847Schristos 31*a7c91847Schristos=head1 SYNOPSIS 32*a7c91847Schristos 33*a7c91847Schristos perldoc validate_repo.pl 34*a7c91847Schristos validate_repo.pl --help [--verbose!] 35*a7c91847Schristos validate_repo.pl [--verbose!] [--cvsroot=CVSROOT] [--exec=SCRIPT]... 36*a7c91847Schristos [--all-revisions!] [module]... 37*a7c91847Schristos 38*a7c91847Schristos=head1 DESCRIPTION 39*a7c91847Schristos 40*a7c91847SchristosThis script will search through a repository and determine if any of the 41*a7c91847Schristosfiles in it are corrupted. 42*a7c91847Schristos 43*a7c91847SchristosThis is normally accomplished by checking out all I<important> revisions, where 44*a7c91847SchristosI<important> revisions are defined as the smallest set which, when checked out, 45*a7c91847Schristoswill cause each and every revision's integrity to be verified. This resolves 46*a7c91847Schristosto the most recent revision on each branch and the first and last revisions on 47*a7c91847Schristosthe trunk. 48*a7c91847Schristos 49*a7c91847SchristosPlease do not run this script inside of the repository itself. This will cause 50*a7c91847Schristosit too fail. 51*a7c91847Schristos 52*a7c91847Schristos=head1 OPTIONS 53*a7c91847Schristos 54*a7c91847Schristos=over 55*a7c91847Schristos 56*a7c91847Schristos=item C<--help> 57*a7c91847Schristos 58*a7c91847SchristosPrint this very help text (or, with C<--verbose>, act like 59*a7c91847SchristosC<perldoc validate_repo.pl>). 60*a7c91847Schristos 61*a7c91847Schristos=item C<-a> or C<--all-revisions> 62*a7c91847Schristos 63*a7c91847SchristosCheck out each and every revision rather than just the I<important> ones. 64*a7c91847SchristosThis flag is useful with C<--exec> to execute the C<SCRIPT> (from C<--exec> 65*a7c91847Schristosbelow) on a checked out copy of each and every revision. 66*a7c91847Schristos 67*a7c91847Schristos=item C<-d> or C<--cvsroot=CVSROOT> 68*a7c91847Schristos 69*a7c91847SchristosUse repository specified by C<CVSROOT>. Defaults to the contents of the 70*a7c91847SchristosF<./CVS/Root> file when it exists and is readable, then to the contents of the 71*a7c91847SchristosC<$CVSROOT> environment variable when it is set and non-empty. 72*a7c91847Schristos 73*a7c91847Schristos=item C<-e> or C<--exec=SCRIPT> 74*a7c91847Schristos 75*a7c91847SchristosExecute (as from command prompt) C<SCRIPT> if it exists as a file, is readable, 76*a7c91847Schristosand is executable, or evaluate (as a perl script) C<SCRIPT> for a checked out 77*a7c91847Schristoscopy of each I<important> revision of each RCS archive in CVSROOT. Executed 78*a7c91847Schristosscripts are passed C<CVSROOT FILE REVISION FNO>, where C<CVSROOT> is what 79*a7c91847Schristosyou'd think, C<FILE> is the path to the file relative to C<CVSROOT> and 80*a7c91847Schristossuitable for use as an argument to C<cvs co>, C<cvs rlog>, and so on, 81*a7c91847SchristosC<REVISION> is the revision of the checked out file, and C<FNO> is the file 82*a7c91847Schristosnumber of the open, read-only file descriptor containing the checked out 83*a7c91847Schristoscontents of revision C<REVISION> of C<FILE>. An evaluated C<SCRIPT> will find 84*a7c91847Schristosthe same four arguments in the same order in C<@_>, except that C<FNO> will be 85*a7c91847Schristosan open file handle. 86*a7c91847Schristos 87*a7c91847SchristosWith C<--all-revisions>, execute or evaluate C<SCRIPT> for a checked out 88*a7c91847Schristosversion of each revsion in the RCS archive. 89*a7c91847Schristos 90*a7c91847Schristos=item C<-v> or C<--verbose> 91*a7c91847Schristos 92*a7c91847SchristosPrint verbose debugging information (or, when specified with C<--help>, act 93*a7c91847Schristoslike C<perldoc validate_repo.pl>). 94*a7c91847Schristos 95*a7c91847Schristos=head1 ARGUMENTS 96*a7c91847Schristos 97*a7c91847Schristos=over 98*a7c91847Schristos 99*a7c91847Schristos=item C<modules> 100*a7c91847Schristos 101*a7c91847SchristosThe module in the repository to examine. Defaults to the contents of the 102*a7c91847SchristosF<./CVS/Repository> file when it exists and is readable, then to F<.> 103*a7c91847Schristos(all modules). 104*a7c91847Schristos 105*a7c91847Schristos=head1 EXAMPLES 106*a7c91847Schristos 107*a7c91847Schristos setenv CVSROOT /release/111/cvs 108*a7c91847Schristos validate_repo.pl 109*a7c91847Schristos 110*a7c91847Schristos 111*a7c91847Schristos validate_repo.pl -d /another/cvsroot --verbose --exec ' 112*a7c91847Schristos system "grep \"This string means Im a bad, bad file!\" <&" 113*a7c91847Schristos . fileno( $_[3] ) 114*a7c91847Schristos . ">/dev/null" 115*a7c91847Schristos or die "Revision $_[2] of $_[0]/$_[1],v is bad, bad, bad!"' 116*a7c91847Schristos 117*a7c91847Schristos=head1 SEE ALSO 118*a7c91847Schristos 119*a7c91847SchristosNone. 120*a7c91847Schristos 121*a7c91847Schristos=cut 122*a7c91847Schristos 123*a7c91847Schristos###################################################################### 124*a7c91847Schristos# MODULES # 125*a7c91847Schristos###################################################################### 126*a7c91847Schristosuse strict; 127*a7c91847Schristos 128*a7c91847Schristosuse Fcntl qw( F_GETFD F_SETFD ); 129*a7c91847Schristosuse File::Find; 130*a7c91847Schristosuse File::Basename; 131*a7c91847Schristosuse File::Path; 132*a7c91847Schristosuse File::Spec; 133*a7c91847Schristosuse Getopt::Long; 134*a7c91847Schristosuse IO::File; 135*a7c91847Schristosuse Pod::Usage; 136*a7c91847Schristos 137*a7c91847Schristos###################################################################### 138*a7c91847Schristos# GLOBALS # 139*a7c91847Schristos###################################################################### 140*a7c91847Schristos 141*a7c91847Schristosuse vars qw( 142*a7c91847Schristos $all_revisions 143*a7c91847Schristos $cvsroot 144*a7c91847Schristos @extra_files 145*a7c91847Schristos @ignore_files 146*a7c91847Schristos $ignored_files 147*a7c91847Schristos @invalid_revs 148*a7c91847Schristos @list_of_broken_files 149*a7c91847Schristos @scripts 150*a7c91847Schristos $total_files 151*a7c91847Schristos $total_interesting_revisions 152*a7c91847Schristos $total_revisions 153*a7c91847Schristos $verbose 154*a7c91847Schristos ); 155*a7c91847Schristos 156*a7c91847Schristos 157*a7c91847Schristos 158*a7c91847Schristos###################################################################### 159*a7c91847Schristos# SUBROUTINES # 160*a7c91847Schristos###################################################################### 161*a7c91847Schristos 162*a7c91847Schristos###################################################################### 163*a7c91847Schristos# 164*a7c91847Schristos# NAME : 165*a7c91847Schristos# main 166*a7c91847Schristos# 167*a7c91847Schristos# PURPOSE : 168*a7c91847Schristos# To search the repository for broken files 169*a7c91847Schristos# 170*a7c91847Schristos# PARAMETERS : 171*a7c91847Schristos# NONE 172*a7c91847Schristos# 173*a7c91847Schristos# GLOBALS : 174*a7c91847Schristos# $cvsroot - The CVS repository to search through. 175*a7c91847Schristos# $ENV{ CVSROOT } - The default CVS repository to search through. 176*a7c91847Schristos# @list_of_broken_files - The list of files that need to 177*a7c91847Schristos# be fixed. 178*a7c91847Schristos# $verbose - is verbose mode on? 179*a7c91847Schristos# @scripts - scripts to run on checked out files. 180*a7c91847Schristos# $total_revisions - The number of revisions considered 181*a7c91847Schristos# $total_interesting_revisions - The number of revisions used 182*a7c91847Schristos# $total_files - The total number of files looked at. 183*a7c91847Schristos# 184*a7c91847Schristos# RETURNS : 185*a7c91847Schristos# A list of broken files 186*a7c91847Schristos# 187*a7c91847Schristos# COMMENTS : 188*a7c91847Schristos# Do not run this script inside the repository. Choose 189*a7c91847Schristos# a nice safe spot( like /tmp ) outside of the repository. 190*a7c91847Schristos# 191*a7c91847Schristos###################################################################### 192*a7c91847Schristossub main 193*a7c91847Schristos{ 194*a7c91847Schristos my $help; 195*a7c91847Schristos 196*a7c91847Schristos $ignored_files = 0; 197*a7c91847Schristos $total_files = 0; 198*a7c91847Schristos $total_interesting_revisions = 0; 199*a7c91847Schristos $total_revisions = 0; 200*a7c91847Schristos 201*a7c91847Schristos Getopt::Long::Configure( "bundling" ); 202*a7c91847Schristos unless( GetOptions( 203*a7c91847Schristos 'all-revisions|a!' => \$all_revisions, 204*a7c91847Schristos 'cvsroot|d=s' => \$cvsroot, 205*a7c91847Schristos 'exec|e=s' => \@scripts, 206*a7c91847Schristos 'help|h|?!' => \$help, 207*a7c91847Schristos 'verbose|v!' => \$verbose 208*a7c91847Schristos ) 209*a7c91847Schristos ) 210*a7c91847Schristos { 211*a7c91847Schristos pod2usage( 2 ); 212*a7c91847Schristos exit 2; 213*a7c91847Schristos } 214*a7c91847Schristos 215*a7c91847Schristos pod2usage( -exitval => 2, 216*a7c91847Schristos -verbose => $verbose ? 2 : 1, 217*a7c91847Schristos -output => \*STDOUT ) 218*a7c91847Schristos if $help; 219*a7c91847Schristos 220*a7c91847Schristos verbose( "Verbose Mode Turned On\n" ); 221*a7c91847Schristos 222*a7c91847Schristos if( !$cvsroot && -f "CVS/Root" && -r "CVS/Root" ) 223*a7c91847Schristos { 224*a7c91847Schristos my $file = new IO::File "< CVS/Root"; 225*a7c91847Schristos $cvsroot = $file->getline; 226*a7c91847Schristos chomp $cvsroot; 227*a7c91847Schristos } 228*a7c91847Schristos $cvsroot = $ENV{'CVSROOT'} unless $cvsroot; 229*a7c91847Schristos pod2usage( "error: Must set CVSROOT" ) unless $cvsroot; 230*a7c91847Schristos 231*a7c91847Schristos if( $cvsroot =~ /^:\w+:/ && $cvsroot !~ /^:local:/ 232*a7c91847Schristos || $cvsroot =~ /@/ ) 233*a7c91847Schristos { 234*a7c91847Schristos print STDERR "CVSROOT must be :local:\n"; 235*a7c91847Schristos exit 2; 236*a7c91847Schristos } 237*a7c91847Schristos 238*a7c91847Schristos for (@scripts) 239*a7c91847Schristos { 240*a7c91847Schristos $_ = File::Spec->rel2abs( $_ ) unless /\n/ || !-x $_; 241*a7c91847Schristos } 242*a7c91847Schristos 243*a7c91847Schristos 244*a7c91847Schristos if( !scalar( @ARGV ) && -f "CVS/Repository" && -r "CVS/Repository" ) 245*a7c91847Schristos { 246*a7c91847Schristos my $file = new IO::File "< CVS/Repository"; 247*a7c91847Schristos my $module = $file->getline; 248*a7c91847Schristos chomp $module; 249*a7c91847Schristos push @ARGV, $module; 250*a7c91847Schristos } 251*a7c91847Schristos 252*a7c91847Schristos push @ARGV, "." unless( scalar @ARGV ); 253*a7c91847Schristos 254*a7c91847Schristos foreach my $directory_to_look_at ( @ARGV ) 255*a7c91847Schristos { 256*a7c91847Schristos $directory_to_look_at = File::Spec->catfile( $cvsroot, 257*a7c91847Schristos $directory_to_look_at ); 258*a7c91847Schristos 259*a7c91847Schristos my $sym_count = 0; 260*a7c91847Schristos while( -l $directory_to_look_at ) 261*a7c91847Schristos { 262*a7c91847Schristos $directory_to_look_at = readlink( $directory_to_look_at ); 263*a7c91847Schristos $sym_count += 1; 264*a7c91847Schristos die( "Encountered too many symlinks for CVSROOT ($cvsroot)\n" ) 265*a7c91847Schristos if( $sym_count > 5 ); 266*a7c91847Schristos } 267*a7c91847Schristos 268*a7c91847Schristos # Remove indirections. 269*a7c91847Schristos $directory_to_look_at =~ s#(/+.)*$##o; 270*a7c91847Schristos 271*a7c91847Schristos verbose( "Processing: $directory_to_look_at\n" ); 272*a7c91847Schristos @ignore_files = get_ignore_files_from_cvsroot( $directory_to_look_at ); 273*a7c91847Schristos find( \&process_file, $directory_to_look_at ); 274*a7c91847Schristos } 275*a7c91847Schristos 276*a7c91847Schristos print "List of corrupted files\n" if @list_of_broken_files; 277*a7c91847Schristos foreach my $broken ( @list_of_broken_files ) 278*a7c91847Schristos { 279*a7c91847Schristos print( "**** File: $broken\n" ); 280*a7c91847Schristos } 281*a7c91847Schristos 282*a7c91847Schristos print "List of Files containing invalid revisions:\n" 283*a7c91847Schristos if @invalid_revs; 284*a7c91847Schristos foreach ( @invalid_revs ) 285*a7c91847Schristos { 286*a7c91847Schristos print( "**** File: ($_->{'rev'}) $_->{'file'}\n" ); 287*a7c91847Schristos } 288*a7c91847Schristos 289*a7c91847Schristos print "List of Files That Don't belong in Repository:\n" 290*a7c91847Schristos if @extra_files; 291*a7c91847Schristos foreach my $extra ( @extra_files ) 292*a7c91847Schristos { 293*a7c91847Schristos print( "**** File: $extra\n" ); 294*a7c91847Schristos } 295*a7c91847Schristos print( "Total Files: $total_files Corrupted files: " 296*a7c91847Schristos . scalar( @list_of_broken_files ) 297*a7c91847Schristos . " Invalid revs: " 298*a7c91847Schristos . scalar( @invalid_revs ) 299*a7c91847Schristos . " Extra files: " 300*a7c91847Schristos . scalar( @extra_files ) 301*a7c91847Schristos . " Ignored Files: $ignored_files\n" ); 302*a7c91847Schristos print( "Total Revisions: $total_revisions Interesting Revisions: $total_interesting_revisions\n" ); 303*a7c91847Schristos} 304*a7c91847Schristos 305*a7c91847Schristos 306*a7c91847Schristos 307*a7c91847Schristossub verbose 308*a7c91847Schristos{ 309*a7c91847Schristos print STDERR @_ if $verbose; 310*a7c91847Schristos} 311*a7c91847Schristos 312*a7c91847Schristos 313*a7c91847Schristos 314*a7c91847Schristos###################################################################### 315*a7c91847Schristos# 316*a7c91847Schristos# NAME : 317*a7c91847Schristos# process_file 318*a7c91847Schristos# 319*a7c91847Schristos# PURPOSE : 320*a7c91847Schristos# This function is called by the find function, its purpose 321*a7c91847Schristos# is to decide if it is important to look at a file or not. When 322*a7c91847Schristos# a file is important, we log it or call &look_at_cvs_file on it. 323*a7c91847Schristos# 324*a7c91847Schristos# ALGORITHM 325*a7c91847Schristos# 1) If the file is an archive file, we call &look_at_cvs_file on 326*a7c91847Schristos# it. 327*a7c91847Schristos# 2) Else, if the file is not in the ignore list, we store its name 328*a7c91847Schristos# for later. 329*a7c91847Schristos# 330*a7c91847Schristos# PARAMETERS : 331*a7c91847Schristos# NONE 332*a7c91847Schristos# 333*a7c91847Schristos# GLOBALS : 334*a7c91847Schristos# $cvsroot - The CVS repository to search through 335*a7c91847Schristos# @ignore_files - File patterns we can afford to ignore. 336*a7c91847Schristos# $File::Find::name - The absolute path of the file being examined. 337*a7c91847Schristos# 338*a7c91847Schristos# RETURNS : 339*a7c91847Schristos# NONE 340*a7c91847Schristos# 341*a7c91847Schristos# COMMENTS : 342*a7c91847Schristos# NONE 343*a7c91847Schristos# 344*a7c91847Schristos###################################################################### 345*a7c91847Schristossub process_file 346*a7c91847Schristos{ 347*a7c91847Schristos if( ! -d $File::Find::name ) 348*a7c91847Schristos { 349*a7c91847Schristos my $path = $File::Find::name; 350*a7c91847Schristos $path =~ s#^$cvsroot/(\./)*##; 351*a7c91847Schristos $total_files++; 352*a7c91847Schristos 353*a7c91847Schristos verbose( "Examining `$path'\n" ); 354*a7c91847Schristos 355*a7c91847Schristos if( $path =~ s/,v$// ) 356*a7c91847Schristos { 357*a7c91847Schristos look_at_cvs_file( $path ); 358*a7c91847Schristos } 359*a7c91847Schristos elsif( !grep { $path =~ $_ } @ignore_files ) 360*a7c91847Schristos { 361*a7c91847Schristos push @extra_files, $path; 362*a7c91847Schristos verbose( "Adding unrecognized file `$path' to corrupted list.\n" ); 363*a7c91847Schristos } 364*a7c91847Schristos else 365*a7c91847Schristos { 366*a7c91847Schristos $ignored_files++; 367*a7c91847Schristos verbose( "Ignoring `$path'\n" ); 368*a7c91847Schristos } 369*a7c91847Schristos } 370*a7c91847Schristos} 371*a7c91847Schristos 372*a7c91847Schristos###################################################################### 373*a7c91847Schristos# 374*a7c91847Schristos# NAME : 375*a7c91847Schristos# look_at_cvs_file 376*a7c91847Schristos# 377*a7c91847Schristos# PURPOSE : 378*a7c91847Schristos# To decide if a file is broken or not. The algorithm is: 379*a7c91847Schristos# a) Get the revision history for the file. 380*a7c91847Schristos# - If that fails the file is broken, save the fact 381*a7c91847Schristos# and continue processing other files. 382*a7c91847Schristos# - If that succeeds we have a list of revisions. 383*a7c91847Schristos# b) For each revision call &check_revision on the file. 384*a7c91847Schristos# - If that fails the file is broken, save the fact 385*a7c91847Schristos# and continue processing other files. 386*a7c91847Schristos# c) Continue on 387*a7c91847Schristos# 388*a7c91847Schristos# PARAMETERS : 389*a7c91847Schristos# $file - The path of the file to look at, relative to $cvsroot and 390*a7c91847Schristos# suitable for use as an argument to `cvs co', `cvs rlog', and 391*a7c91847Schristos# the rest of CVS's r* commands. 392*a7c91847Schristos# 393*a7c91847Schristos# GLOBALS : 394*a7c91847Schristos# NONE 395*a7c91847Schristos# 396*a7c91847Schristos# RETURNS : 397*a7c91847Schristos# NONE 398*a7c91847Schristos# 399*a7c91847Schristos# COMMENTS : 400*a7c91847Schristos# We have to handle Attic files in a special manner. 401*a7c91847Schristos# Basically remove the Attic from the string if it 402*a7c91847Schristos# exists at the end of the $path variable. 403*a7c91847Schristos# 404*a7c91847Schristos###################################################################### 405*a7c91847Schristossub look_at_cvs_file 406*a7c91847Schristos{ 407*a7c91847Schristos my( $file ) = @_; 408*a7c91847Schristos my( $name, $path ) = fileparse( $file ); 409*a7c91847Schristos 410*a7c91847Schristos $file = $path . $name if $path =~ s#Attic/$##; 411*a7c91847Schristos 412*a7c91847Schristos my( $finfo, $rinfo ) = get_history( $file ); 413*a7c91847Schristos 414*a7c91847Schristos unless( defined $rinfo ) 415*a7c91847Schristos { 416*a7c91847Schristos verbose( "\t`$file' is corrupted. It was determined to contain no\n" 417*a7c91847Schristos . "\trevisions via a cvs rlog command\n" ); 418*a7c91847Schristos push( @list_of_broken_files, $file ); 419*a7c91847Schristos return(); 420*a7c91847Schristos } 421*a7c91847Schristos 422*a7c91847Schristos my @int_revisions = 423*a7c91847Schristos $all_revisions ? keys %$rinfo 424*a7c91847Schristos : find_interesting_revisions( keys %$rinfo ); 425*a7c91847Schristos 426*a7c91847Schristos foreach my $revision ( @int_revisions ) 427*a7c91847Schristos { 428*a7c91847Schristos verbose( "\t\tLooking at Revision: $revision\n" ); 429*a7c91847Schristos if( !check_revision( $file, $revision, $finfo, $rinfo ) ) 430*a7c91847Schristos { 431*a7c91847Schristos verbose( "\t$file is corrupted in revision: $revision\n" ); 432*a7c91847Schristos push( @list_of_broken_files, $file ); 433*a7c91847Schristos return(); 434*a7c91847Schristos } 435*a7c91847Schristos } 436*a7c91847Schristos} 437*a7c91847Schristos 438*a7c91847Schristos###################################################################### 439*a7c91847Schristos# 440*a7c91847Schristos# NAME : 441*a7c91847Schristos# get_history 442*a7c91847Schristos# 443*a7c91847Schristos# PURPOSE : 444*a7c91847Schristos# To retrieve an array of revision numbers. 445*a7c91847Schristos# 446*a7c91847Schristos# PARAMETERS : 447*a7c91847Schristos# $file - The file to retrieve the revision numbers for 448*a7c91847Schristos# 449*a7c91847Schristos# GLOBALS : 450*a7c91847Schristos# $cvsroot - the CVSROOT we are examining 451*a7c91847Schristos# 452*a7c91847Schristos# RETURNS : 453*a7c91847Schristos# On Success - A hash of revision info, indexed by revision numbers. 454*a7c91847Schristos# On Failure - undef. 455*a7c91847Schristos# 456*a7c91847Schristos# COMMENTS : 457*a7c91847Schristos# The $_ is saved off because The File::find functionality 458*a7c91847Schristos# expects the $_ to not have been changed. 459*a7c91847Schristos# The -N option for the rlog command means to spit out 460*a7c91847Schristos# tags or branch names. 461*a7c91847Schristos# 462*a7c91847Schristos###################################################################### 463*a7c91847Schristossub get_history 464*a7c91847Schristos{ 465*a7c91847Schristos my( $file ) = @_; 466*a7c91847Schristos $file =~ s/(["\$`\\])/\\$1/g; 467*a7c91847Schristos my %finfo; # Info about the file. 468*a7c91847Schristos my %rinfo; # Info about revisions in the file. 469*a7c91847Schristos my $revision; 470*a7c91847Schristos 471*a7c91847Schristos my $fh = new IO::File( "cvs -d $cvsroot rlog -N \"$file\"" 472*a7c91847Schristos . ($verbose ? "" : " 2>&1") . " |" ) 473*a7c91847Schristos or die( "unable to run `cvs rlog', help" ); 474*a7c91847Schristos 475*a7c91847Schristos my $ignore = -1; 476*a7c91847Schristos while( my $line = $fh->getline ) 477*a7c91847Schristos { 478*a7c91847Schristos if( $ignore == 1 ) 479*a7c91847Schristos { 480*a7c91847Schristos if( ( $revision ) = $line =~ /^revision (.*?)(\tlocked by: \S+;)?$/ ) 481*a7c91847Schristos { 482*a7c91847Schristos unless($revision =~ m/^\d+\.\d+(?:\.\d+\.\d+)*$/) 483*a7c91847Schristos { 484*a7c91847Schristos push @invalid_revs, { 'file' => $file, 'rev' => $revision }; 485*a7c91847Schristos verbose( "Adding invalid revision `$revision' of file `$file' to invalid revs list.\n" ); 486*a7c91847Schristos } 487*a7c91847Schristos 488*a7c91847Schristos $ignore++; 489*a7c91847Schristos next; 490*a7c91847Schristos } 491*a7c91847Schristos 492*a7c91847Schristos # We require ---- before a ^revision tag, not a revision 493*a7c91847Schristos # after every ----. 494*a7c91847Schristos $ignore = 0; 495*a7c91847Schristos } 496*a7c91847Schristos if( $ignore == 2 ) 497*a7c91847Schristos { 498*a7c91847Schristos if( my ( $date, $author, $state ) = 499*a7c91847Schristos $line =~ /^date: (\S+ \S+); author: ([^;]+); state: (\S+);/ ) 500*a7c91847Schristos { 501*a7c91847Schristos $rinfo{$revision} = 502*a7c91847Schristos { 503*a7c91847Schristos 'date' => $date, 504*a7c91847Schristos 'author' => $author, 505*a7c91847Schristos 'state' => $state 506*a7c91847Schristos } 507*a7c91847Schristos } 508*a7c91847Schristos else 509*a7c91847Schristos { 510*a7c91847Schristos die "Couldn't read date/author/state for revision $revision\n" 511*a7c91847Schristos . "of $file from `cvs rlog'.\n" 512*a7c91847Schristos . "line = $line"; 513*a7c91847Schristos } 514*a7c91847Schristos $ignore = 0; 515*a7c91847Schristos next; 516*a7c91847Schristos } 517*a7c91847Schristos if( $ignore == -1 ) 518*a7c91847Schristos { 519*a7c91847Schristos # Until we find the first ---- below, we can read general file info 520*a7c91847Schristos if( my ( $kwmode ) = 521*a7c91847Schristos $line =~ /^keyword substitution: (\S+)$/ ) 522*a7c91847Schristos { 523*a7c91847Schristos $finfo{'kwmode'} = $kwmode; 524*a7c91847Schristos next; 525*a7c91847Schristos } 526*a7c91847Schristos } 527*a7c91847Schristos # rlog outputs a "----" line before the actual revision 528*a7c91847Schristos # without this we'll pick up peoples comments if they 529*a7c91847Schristos # happen to start with revision 530*a7c91847Schristos if( $line =~ /^----------------------------$/ ) 531*a7c91847Schristos { 532*a7c91847Schristos # Catch this case when $ignore == -1 or 0 533*a7c91847Schristos $ignore = 1; 534*a7c91847Schristos next; 535*a7c91847Schristos } 536*a7c91847Schristos } 537*a7c91847Schristos if( $verbose ) 538*a7c91847Schristos { 539*a7c91847Schristos for (keys %rinfo) 540*a7c91847Schristos { 541*a7c91847Schristos verbose( "Revision $_: " ); 542*a7c91847Schristos verbose( join( ", ", %{$rinfo{$_}} ) ); 543*a7c91847Schristos verbose( "\n" ); 544*a7c91847Schristos } 545*a7c91847Schristos } 546*a7c91847Schristos 547*a7c91847Schristos die "Syserr closing pipe from `cvs co': $!" 548*a7c91847Schristos if !$fh->close && $!; 549*a7c91847Schristos return if $?; 550*a7c91847Schristos 551*a7c91847Schristos return( \%finfo, %rinfo ? \%rinfo : undef ); 552*a7c91847Schristos} 553*a7c91847Schristos 554*a7c91847Schristos###################################################################### 555*a7c91847Schristos# 556*a7c91847Schristos# NAME : 557*a7c91847Schristos# check_revision 558*a7c91847Schristos# 559*a7c91847Schristos# PURPOSE : 560*a7c91847Schristos# Given a file and a revision number ensure that we can check out that 561*a7c91847Schristos# file. 562*a7c91847Schristos# 563*a7c91847Schristos# If the user has specified any scripts (passed in as arguments to --exec 564*a7c91847Schristos# and stored in @scripts), run them on the checked out revision. If 565*a7c91847Schristos# executable scripts exit with a non-zero status or evaluated scripts set 566*a7c91847Schristos# $@ (die), print $status or $@ as a warning. 567*a7c91847Schristos# 568*a7c91847Schristos# PARAMETERS : 569*a7c91847Schristos# $file - The file to look at. 570*a7c91847Schristos# $revision - The revision to look at. 571*a7c91847Schristos# $rinfo - A reference to a hash containing information about the 572*a7c91847Schristos# revisions in $file. 573*a7c91847Schristos# For instance, $rinfo->{$revision}->{'date'} contains the 574*a7c91847Schristos# date revision $revision was committed. 575*a7c91847Schristos# 576*a7c91847Schristos# GLOBALS : 577*a7c91847Schristos# NONE 578*a7c91847Schristos# 579*a7c91847Schristos# RETURNS : 580*a7c91847Schristos# If we can get the File - 1 581*a7c91847Schristos# If we can not get the File - 0 582*a7c91847Schristos# 583*a7c91847Schristos# COMMENTS : 584*a7c91847Schristos# cvs command line options are as followed: 585*a7c91847Schristos# -n - Do not run any checkout program as specified by the -o 586*a7c91847Schristos# option in the modules file 587*a7c91847Schristos# -p - Put all output to standard out. 588*a7c91847Schristos# -r - The revision of the file that we would like to look at. 589*a7c91847Schristos# -ko - Get the revision exactly as checked in - do not allow 590*a7c91847Schristos# RCS keyword substitution. 591*a7c91847Schristos# Please note that cvs will return 0 for being able to successfully 592*a7c91847Schristos# read the file and 1 for failure to read the file. 593*a7c91847Schristos# 594*a7c91847Schristos###################################################################### 595*a7c91847Schristossub check_revision 596*a7c91847Schristos{ 597*a7c91847Schristos my( $file, $revision, $finfo, $rinfo ) = @_; 598*a7c91847Schristos $file =~ s/(["\$`\\])/\\$1/g; 599*a7c91847Schristos 600*a7c91847Schristos # Allow binaries to be checked out as such. Otherwise, use -ko to avoid 601*a7c91847Schristos # replacing keywords in the files. 602*a7c91847Schristos my $kwmode = $finfo->{'kwmode'} eq 'b' ? '' : ' -ko'; 603*a7c91847Schristos my $command = "cvs -d $cvsroot co$kwmode -npr $revision \"$file\""; 604*a7c91847Schristos my $ret_code; 605*a7c91847Schristos verbose( "Executing `$command'.\n" ); 606*a7c91847Schristos if( @scripts ) 607*a7c91847Schristos { 608*a7c91847Schristos my $fh = new IO::File $command . ($verbose ? "" : " 2>&1") . " |"; 609*a7c91847Schristos fcntl( $fh, F_SETFD, 0 ) 610*a7c91847Schristos or die "Can't clear close-on-exec flag on filehandle: $!"; 611*a7c91847Schristos my $count; 612*a7c91847Schristos foreach my $script (@scripts) 613*a7c91847Schristos { 614*a7c91847Schristos $count++; 615*a7c91847Schristos if( $script !~ /\n/ && -x $script ) 616*a7c91847Schristos { 617*a7c91847Schristos # exec external script 618*a7c91847Schristos my $status = system $script, $cvsroot, $file, $revision, 619*a7c91847Schristos fileno( $fh ); 620*a7c91847Schristos warn "`$script $cvsroot $file $revision " 621*a7c91847Schristos . fileno( $fh ) 622*a7c91847Schristos . "' exited with code $status" 623*a7c91847Schristos if $status; 624*a7c91847Schristos } 625*a7c91847Schristos else 626*a7c91847Schristos { 627*a7c91847Schristos # eval script 628*a7c91847Schristos @_ = ($cvsroot, $file, $revision, $fh); 629*a7c91847Schristos eval $script; 630*a7c91847Schristos warn "script $count ($cvsroot, $file, $revision, $fh) exited abnormally: $@" 631*a7c91847Schristos if $@; 632*a7c91847Schristos } 633*a7c91847Schristos } 634*a7c91847Schristos # Read any data left so the close will work even if our called script 635*a7c91847Schristos # didn't finish reading the data. 636*a7c91847Schristos () = $fh->getlines; # force list context 637*a7c91847Schristos die "Syserr closing pipe from `cvs co': $!" 638*a7c91847Schristos if !$fh->close && $!; 639*a7c91847Schristos $ret_code = $?; 640*a7c91847Schristos } 641*a7c91847Schristos else 642*a7c91847Schristos { 643*a7c91847Schristos $ret_code = 0xffff & system "$command >/dev/null 2>&1"; 644*a7c91847Schristos } 645*a7c91847Schristos 646*a7c91847Schristos return !$ret_code; 647*a7c91847Schristos} 648*a7c91847Schristos 649*a7c91847Schristos###################################################################### 650*a7c91847Schristos# 651*a7c91847Schristos# NAME : 652*a7c91847Schristos# find_interesting_revisions 653*a7c91847Schristos# 654*a7c91847Schristos# PURPOSE : 655*a7c91847Schristos# CVS stores information in a logical manner. We only really 656*a7c91847Schristos# need to look at some interestin revisions. These are: 657*a7c91847Schristos# The first version 658*a7c91847Schristos# And the last version on every branch. 659*a7c91847Schristos# This is because cvs stores changes descending from 660*a7c91847Schristos# main line. ie suppose the last version on mainline is 1.6 661*a7c91847Schristos# version 1.6 of the file is stored in toto. version 1.5 662*a7c91847Schristos# is stored as a diff between 1.5 and 1.6. 1.4 is stored 663*a7c91847Schristos# as a diff between 1.5 and 1.4. 664*a7c91847Schristos# branches are stored a little differently. They are 665*a7c91847Schristos# stored in ascending order. Suppose there is a branch 666*a7c91847Schristos# on 1.4 of the file. The first branches revision number 667*a7c91847Schristos# would be 1.4.1.1. This is stored as a diff between 668*a7c91847Schristos# version 1.4 and 1.4.1.1. The 1.4.1.2 version is stored 669*a7c91847Schristos# as a diff between 1.4.1.1 and 1.4.1.2. Therefore 670*a7c91847Schristos# we are only interested in the earliest revision number 671*a7c91847Schristos# and the highest revision number on a branch. 672*a7c91847Schristos# 673*a7c91847Schristos# PARAMETERS : 674*a7c91847Schristos# @revisions - The list of revisions to find interesting ones 675*a7c91847Schristos# 676*a7c91847Schristos# GLOBALS : 677*a7c91847Schristos# NONE 678*a7c91847Schristos# 679*a7c91847Schristos# RETURNS : 680*a7c91847Schristos# @new_revisions - The list of revisions that we find interesting 681*a7c91847Schristos# 682*a7c91847Schristos# COMMENTS : 683*a7c91847Schristos# 684*a7c91847Schristos###################################################################### 685*a7c91847Schristossub find_interesting_revisions 686*a7c91847Schristos{ 687*a7c91847Schristos my( @revisions ) = @_; 688*a7c91847Schristos my @new_revisions; 689*a7c91847Schristos my %max_branch_revision; 690*a7c91847Schristos my $branch_number; 691*a7c91847Schristos my $branch_rev; 692*a7c91847Schristos my $key; 693*a7c91847Schristos my $value; 694*a7c91847Schristos 695*a7c91847Schristos foreach my $revision( @revisions ) 696*a7c91847Schristos { 697*a7c91847Schristos ( $branch_number, $branch_rev ) = branch_split( $revision ); 698*a7c91847Schristos $max_branch_revision{$branch_number} = $branch_rev 699*a7c91847Schristos if( !exists $max_branch_revision{$branch_number} 700*a7c91847Schristos || $max_branch_revision{$branch_number} < $branch_rev ); 701*a7c91847Schristos } 702*a7c91847Schristos 703*a7c91847Schristos push( @new_revisions, "1.1" ) unless (exists $max_branch_revision{1} 704*a7c91847Schristos && $max_branch_revision{1} == 1); 705*a7c91847Schristos while( ( $key, $value ) = each ( %max_branch_revision ) ) 706*a7c91847Schristos { 707*a7c91847Schristos push( @new_revisions, $key . "." . $value ); 708*a7c91847Schristos } 709*a7c91847Schristos 710*a7c91847Schristos my $nrc; 711*a7c91847Schristos my $rc; 712*a7c91847Schristos 713*a7c91847Schristos $rc = @revisions; 714*a7c91847Schristos $nrc = @new_revisions; 715*a7c91847Schristos 716*a7c91847Schristos $total_revisions += $rc; 717*a7c91847Schristos $total_interesting_revisions += $nrc; 718*a7c91847Schristos 719*a7c91847Schristos verbose( "\t\tTotal Revisions: $rc Interesting Revisions: $nrc\n" ); 720*a7c91847Schristos 721*a7c91847Schristos return( @new_revisions ); 722*a7c91847Schristos} 723*a7c91847Schristos 724*a7c91847Schristos 725*a7c91847Schristos 726*a7c91847Schristos###################################################################### 727*a7c91847Schristos# 728*a7c91847Schristos# NAME : 729*a7c91847Schristos# branch_split 730*a7c91847Schristos# 731*a7c91847Schristos# PURPOSE : 732*a7c91847Schristos# To split up a revision number up into the branch part and 733*a7c91847Schristos# the number part. For Instance: 734*a7c91847Schristos# 1.1.1.1 - is split 1.1.1 and 1 735*a7c91847Schristos# 2.1 - is split 2 and 1 736*a7c91847Schristos# 1.3.4.5.7.8 - is split 1.3.4.5.7 and 8 737*a7c91847Schristos# 738*a7c91847Schristos# PARAMETERS : 739*a7c91847Schristos# $revision - The revision to look at. 740*a7c91847Schristos# 741*a7c91847Schristos# GLOBALS : 742*a7c91847Schristos# NONE 743*a7c91847Schristos# 744*a7c91847Schristos# RETURNS : 745*a7c91847Schristos# ( $branch, $revision ) - 746*a7c91847Schristos# $branch - The branch part of the revision number 747*a7c91847Schristos# $revision - The revision part of the revision number 748*a7c91847Schristos# 749*a7c91847Schristos# COMMENTS : 750*a7c91847Schristos# NONE 751*a7c91847Schristos# 752*a7c91847Schristos###################################################################### 753*a7c91847Schristossub branch_split 754*a7c91847Schristos{ 755*a7c91847Schristos my( $revision ) = @_; 756*a7c91847Schristos my $branch; 757*a7c91847Schristos my $version; 758*a7c91847Schristos my @split_rev; 759*a7c91847Schristos my $count; 760*a7c91847Schristos 761*a7c91847Schristos @split_rev = split /\./, $revision; 762*a7c91847Schristos 763*a7c91847Schristos my $numbers = @split_rev; 764*a7c91847Schristos @split_rev = reverse( @split_rev ); 765*a7c91847Schristos $branch = pop( @split_rev ); 766*a7c91847Schristos for( $count = 0; $count < $numbers - 2 ; $count++ ) 767*a7c91847Schristos { 768*a7c91847Schristos $branch .= "." . pop( @split_rev ); 769*a7c91847Schristos } 770*a7c91847Schristos 771*a7c91847Schristos return( $branch, pop( @split_rev ) ); 772*a7c91847Schristos} 773*a7c91847Schristos 774*a7c91847Schristos###################################################################### 775*a7c91847Schristos# 776*a7c91847Schristos# NAME : 777*a7c91847Schristos# get_ignore_files_from_cvsroot 778*a7c91847Schristos# 779*a7c91847Schristos# PURPOSE : 780*a7c91847Schristos# Retrieve the list of files from the CVSROOT/ directory 781*a7c91847Schristos# that should be ignored. 782*a7c91847Schristos# These are the regular files (e.g., commitinfo, loginfo) 783*a7c91847Schristos# and those specified in the checkoutlist file. 784*a7c91847Schristos# 785*a7c91847Schristos# PARAMETERS : 786*a7c91847Schristos# The CVSROOT 787*a7c91847Schristos# 788*a7c91847Schristos# GLOBALS : 789*a7c91847Schristos# NONE 790*a7c91847Schristos# 791*a7c91847Schristos# RETURNS : 792*a7c91847Schristos# @ignore - the list of files to ignore 793*a7c91847Schristos# 794*a7c91847Schristos# COMMENTS : 795*a7c91847Schristos# NONE 796*a7c91847Schristos# 797*a7c91847Schristos###################################################################### 798*a7c91847Schristossub get_ignore_files_from_cvsroot { 799*a7c91847Schristos my( $cvsroot ) = @_; 800*a7c91847Schristos my @ignore = ( 801*a7c91847Schristos qr{CVS/fileattr$}o, 802*a7c91847Schristos qr{^(./)?CVSROOT/.#[^/]*$}o, 803*a7c91847Schristos qr{^(./)?CVSROOT/checkoutlist$}o, 804*a7c91847Schristos qr{^(./)?CVSROOT/commitinfo$}o, 805*a7c91847Schristos qr{^(./)?CVSROOT/config$}o, 806*a7c91847Schristos qr{^(./)?CVSROOT/cvsignore$}o, 807*a7c91847Schristos qr{^(./)?CVSROOT/cvswrappers$}o, 808*a7c91847Schristos qr{^(./)?CVSROOT/editinfo$}o, 809*a7c91847Schristos qr{^(./)?CVSROOT/history$}o, 810*a7c91847Schristos qr{^(./)?CVSROOT/loginfo$}o, 811*a7c91847Schristos qr{^(./)?CVSROOT/modules$}o, 812*a7c91847Schristos qr{^(./)?CVSROOT/notify$}o, 813*a7c91847Schristos qr{^(./)?CVSROOT/passwd$}o, 814*a7c91847Schristos qr{^(./)?CVSROOT/postadmin$}o, 815*a7c91847Schristos qr{^(./)?CVSROOT/postproxy$}o, 816*a7c91847Schristos qr{^(./)?CVSROOT/posttag$}o, 817*a7c91847Schristos qr{^(./)?CVSROOT/postwatch$}o, 818*a7c91847Schristos qr{^(./)?CVSROOT/preproxy$}o, 819*a7c91847Schristos qr{^(./)?CVSROOT/rcsinfo$}o, 820*a7c91847Schristos qr{^(./)?CVSROOT/readers$}o, 821*a7c91847Schristos qr{^(./)?CVSROOT/taginfo$}o, 822*a7c91847Schristos qr{^(./)?CVSROOT/val-tags$}o, 823*a7c91847Schristos qr{^(./)?CVSROOT/verifymsg$}o, 824*a7c91847Schristos qr{^(./)?CVSROOT/writers$}o 825*a7c91847Schristos ); 826*a7c91847Schristos 827*a7c91847Schristos my $checkoutlist_file = "$cvsroot/CVSROOT/checkoutlist"; 828*a7c91847Schristos if( -f $checkoutlist_file && -r $checkoutlist_file ) 829*a7c91847Schristos { 830*a7c91847Schristos my $fh = new IO::File "<$checkoutlist_file" 831*a7c91847Schristos or die "Unable to read checkoutlist file ($checkoutlist_file): $!\n"; 832*a7c91847Schristos 833*a7c91847Schristos my @list = $fh->getlines; 834*a7c91847Schristos chomp( @list ); 835*a7c91847Schristos $fh->close or die( "Unable to close checkoutlist file: $!\n" ); 836*a7c91847Schristos 837*a7c91847Schristos foreach my $line( @list ) 838*a7c91847Schristos { 839*a7c91847Schristos next if( $line =~ /^#/ || $line =~ /^\s*$/ ); 840*a7c91847Schristos $line =~ s/^\s*(\S+)(\s+.*)?$/$1/; 841*a7c91847Schristos push @ignore, qr{^(./)?CVSROOT/$line$}; 842*a7c91847Schristos } 843*a7c91847Schristos } 844*a7c91847Schristos 845*a7c91847Schristos return @ignore; 846*a7c91847Schristos} 847*a7c91847Schristos 848*a7c91847Schristos 849*a7c91847Schristos 850*a7c91847Schristos###### 851*a7c91847Schristos###### Go. 852*a7c91847Schristos###### 853*a7c91847Schristos 854*a7c91847Schristosexit main @ARGV; 855*a7c91847Schristos 856*a7c91847Schristos# vim:tabstop=4:shiftwidth=4 857