xref: /netbsd-src/external/gpl2/xcvs/dist/contrib/validate_repo.in (revision a7c918477dd5f12c1da816ba05caf44eab2d06d6)
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