]> CyberLeo.Net >> Repos - FreeBSD/releng/9.2.git/blob - contrib/cvs/contrib/check_cvs.in
- Copy stable/9 to releng/9.2 as part of the 9.2-RELEASE cycle.
[FreeBSD/releng/9.2.git] / contrib / cvs / contrib / check_cvs.in
1 #! @PERL@ -w
2 ########################################################################
3 #  Copyright (c) 2000, 2001 by Donald Sharp <sharpd@cisco.com>
4 #  All Rights Reserved
5 #
6 #  Permission is granted to copy and/or distribute this file, with or
7 #  without modifications, provided this notice is preserved.
8 #
9 ########################################################################
10
11 =head1 check_cvs.pl
12
13     Script to check the integrity of the Repository
14
15 =head1 SYNOPSIS
16
17     check_cvs.pl
18
19 =head1 DESCRIPTION
20
21     This script will search through a repository and determine if
22     any of the files in it are corrupted.
23
24     Please do not run this script inside of the repository itself,
25     it will cause it too fail.
26
27     Also it currently can only be run over the entire repository,
28     so only point your CVSROOT at the actual CVSROOT.
29
30 =head1 OPTIONS
31
32     There are no options.
33
34 =head1 EXAMPLES
35
36     setenv CVSROOT /release/111/cvs
37
38     # To see more verbose output
39     setenv CVSDEBUGEDIT 1
40
41     check_cvs.pl
42
43 =head1 SEE ALSO
44
45     None
46
47 =cut
48
49 ######################################################################
50 #                    MODULES                                         #
51 ######################################################################
52 use strict;
53
54 use File::Find;
55 use File::Basename;
56 use File::Path;
57 use Cwd;
58
59 ######################################################################
60 #                    GLOBALS                                         #
61 ######################################################################
62
63 my @list_of_broken_files;
64 my @extra_files;
65 my $verbose = 0;
66
67 my $total_revisions;
68 my $total_interesting_revisions;
69 my $total_files;
70 my @ignore_files;
71
72 ######################################################################
73 #                    SUBROUTINES                                     #
74 ######################################################################
75
76 ######################################################################
77 #
78 #    NAME :
79 #      main
80 #
81 #    PURPOSE :
82 #      To search the repository for broken files
83 #
84 #    PARAMETERS :
85 #      NONE
86 #
87 #    GLOBALS :
88 #      $ENV{ CVSROOT }       - The CVS repository to search through
89 #      $ENV{ CVSDEBUGEDIT }  - Turn on Debugging.
90 #      @list_of_broken_files - The list of files that need to
91 #                              be fixed.
92 #      $verbose              - is verbose mode on?
93 #      $total_revisions      - The number of revisions considered
94 #      $total_interesting_revisions - The number of revisions used
95 #      $total_files          - The total number of files looked at.
96 #
97 #    RETURNS :
98 #      A list of broken files
99 #
100 #    COMMENTS :
101 #      Do not run this script inside the repository.  Choose
102 #      a nice safe spot( like /tmp ) outside of the repository.
103 #
104 ######################################################################
105 my $directory_to_look_at;
106
107 select (STDOUT); $| = 1;    # make unbuffered
108
109 $total_revisions = 0;
110 $total_interesting_revisions = 0;
111 $total_files = 0;
112
113 if( !exists( $ENV{ CVSROOT } ) )
114 {
115     die( "The script should be run with the CVSROOT environment variable set" );
116 }
117
118 if( exists( $ENV{ CVSDEBUGEDIT } ) )
119 {
120     $verbose = 1;
121     print( "Verbose Mode Turned On\n" );
122 }
123
124 $directory_to_look_at = $ENV{ CVSROOT };
125 my $sym_count = 0;
126 while( -l $directory_to_look_at )
127 {
128     $directory_to_look_at = readlink( $directory_to_look_at );
129     $sym_count += 1;
130     if( $sym_count > 5 )
131     {
132         die( "Encountered too many symlinks for $ENV{ CVSROOT }\n" );
133     }
134 }
135
136 print( "Processing: $directory_to_look_at\n" ) if( $verbose );
137 @ignore_files = &get_ignore_files_from_cvsroot( $directory_to_look_at );
138 find( \&process_file, $directory_to_look_at );
139
140 my $num_files = @list_of_broken_files;
141 print( "List of corrupted files\n" ) if( $num_files > 0 );
142 foreach my $broken ( @list_of_broken_files )
143 {
144     print( "**** File: $broken\n" );
145 }
146
147 $num_files = @extra_files;
148 print( "List of Files That Don't belong in Repository:\n" ) if( $num_files > 0 );
149 foreach my $extra ( @extra_files )
150 {
151     print( "**** File: $extra\n" );
152 }
153 print( "Total Files: $total_files\n" );
154 print( "Total Revisions: $total_revisions Interesting Revisions: $total_interesting_revisions\n" );
155
156 ######################################################################
157 #
158 #    NAME :
159 #      process_file
160 #
161 #    PURPOSE :
162 #      This function is called by the find function, it's purpose
163 #      is to decide if it is important to look at a file or not.
164 #      We only care about files that have the ,v at the end.
165 #
166 #    PARAMETERS :
167 #      NONE
168 #
169 #    GLOBALS :
170 #      $ENV{ CVSROOT } - The CVS repository to search through
171 #
172 #    RETURNS :
173 #      NONE
174 #
175 #    COMMENTS :
176 #      NONE
177 #
178 ######################################################################
179 sub process_file
180 {
181     my $path = $File::Find::name;
182
183     $total_files += 1;
184     $path =~ s/^$directory_to_look_at\///;
185
186     print( "\tProcessing File: $path\n" ) if( $verbose );
187     if( $path =~ /,v$/ )
188     {
189         $path =~ s/,v$//;
190         look_at_cvs_file( $path );
191     }
192     elsif( ! -d $File::Find::name )
193     {
194         my $save = 0;
195
196         foreach my $ignore ( @ignore_files )
197         {
198             if( $path =~ /$ignore/ )
199             {
200                 $save = 1;
201                 last;
202             }
203         }
204
205         if( !$save )
206         {
207             push( @extra_files, $path );
208         }
209     }
210 }
211
212 ######################################################################
213 #
214 #    NAME :
215 #      look_at_cvs_file
216 #
217 #    PURPOSE :
218 #      To decide if a file is broken or not.  The algorithm is:
219 #      a)  Get the revision history for the file.
220 #              - If that fails the file is broken, save the fact
221 #                and continue processing other files.
222 #              - If that succeeds we have a list of revisions.
223 #      b)  For Each revision try to retrieve that version
224 #              - If that fails the file is broken, save the fact
225 #                and continue processing other files.
226 #      c)  Continue on 
227 #
228 #    PARAMETERS :
229 #      $file - The file to look at.
230 #
231 #    GLOBALS :
232 #      NONE
233 #
234 #    RETURNS :
235 #      NONE
236 #
237 #    COMMENTS :
238 #      We have to handle Attic files in a special manner.
239 #      Basically remove the Attic from the string if it
240 #      exists at the end of the $path variable.
241 #
242 ######################################################################
243 sub look_at_cvs_file
244 {
245     my( $file ) = @_;
246     my( $name, $path, $suffix ) = fileparse( $file );
247
248     if( $path =~ s/Attic\/$// )
249     {
250         $file = $path . $name;
251     }
252
253     my $revisions = get_history( $name );
254
255     if( !defined( $revisions ) )
256     {
257         print( "\t$file is corrupted, this was determined via a cvs log command\n" ) if( $verbose );
258         push( @list_of_broken_files, $file );
259         return();
260     }
261
262     my @int_revisions = find_interesting_revisions( @$revisions );
263
264     foreach my $revision ( @int_revisions )
265     {
266         print( "\t\tLooking at Revision: $revision\n" ) if( $verbose );
267         if( !check_revision( $file, $revision ) )
268         {
269             print( "\t$file is corrupted in revision: $revision\n" ) if( $verbose );
270             push( @list_of_broken_files, $file );
271             return();
272         }
273     }
274
275 }
276
277 ######################################################################
278 #
279 #    NAME :
280 #      get_history
281 #
282 #    PURPOSE :
283 #      To retrieve a array of revision numbers.
284 #
285 #    PARAMETERS :
286 #      $file - The file to retrieve the revision numbers for
287 #
288 #    GLOBALS :
289 #      NONE
290 #
291 #    RETURNS :
292 #      On Success - Reference to the list of revision numbers
293 #      On Failure - undef.
294 #
295 #    COMMENTS :
296 #      The $_ is saved off because The File::find functionality
297 #      expects the $_ to not have been changed.
298 #      The -N option for the rlog command means to spit out 
299 #      tags or branch names.
300 #
301 ######################################################################
302 sub get_history
303 {
304     my( $file ) = @_;
305     $file =~ s/(["\$`\\])/\\$1/g;
306     my @revisions;
307     my $revision;
308     my $ignore = 1;
309     my $save_ = $_;
310
311     open( FILE, "rlog -N \"$file\" 2>&1 |" ) or die( "unable to run rlog, help" );
312
313     while( <FILE> )
314     {
315         #rlog outputs a "----" line before the actual revision
316         #without this we'll pick up peoples comments if they 
317         #happen to start with revision
318         if( /^----------------------------$/ )
319         {
320             $ignore = 0;
321             next;
322         }
323
324         if( ( !$ignore ) && ( ( $revision ) = m/^revision (\S+)/ ) )
325         {
326             push( @revisions, $revision );
327             $ignore = 1;
328         }
329     }
330
331     $_ = $save_;
332
333     if( !close( FILE ) )
334     {
335         return( undef );
336     }
337
338     return( \@revisions );
339 }
340
341 ######################################################################
342 #
343 #    NAME :
344 #      check_revision
345 #
346 #    PURPOSE :
347 #      Given a file and a revision number ensure that we can 
348 #      check out that file
349 #
350 #    PARAMETERS :
351 #      $file     - The file to look at.
352 #      $revision - The revision to look at.
353 #
354 #    GLOBALS :
355 #      NONE
356 #
357 #    RETURNS :
358 #      If we can get the File - 1
359 #      If we can not get the File - 0
360 #
361 #    COMMENTS :
362 #      cvs command line options are as followed:
363 #        -n - Do not run any checkout program as specified by the -o
364 #             option in the modules file
365 #        -p - Put all output to standard out.
366 #        -r - The revision of the file that we would like to look at.
367 #      Please note that cvs will return 0 for being able to successfully
368 #      read the file and 1 for failure to read the file.
369 #
370 ######################################################################
371 sub check_revision
372 {
373     my( $file, $revision ) = @_;
374     $file =~ s/(["\$`\\])/\\$1/g;
375
376     my $cwd = getcwd();
377     chdir( "/tmp" );
378
379     my $ret_code = 0xffff & system( "cvs co -n -p -r $revision \"$file\" > /dev/null 2>&1" );
380
381     chdir( $cwd );
382     return( 1 ) if ( $ret_code == 0 );
383     return( 0 );
384
385     return( $ret_code );
386 }
387
388 ######################################################################
389 #
390 #    NAME :
391 #      find_interesting_revisions
392 #
393 #    PURPOSE :
394 #      CVS stores information in a logical manner.  We only really
395 #      need to look at some interestin revisions.  These are:
396 #      The first version
397 #      And the last version on every branch.
398 #      This is because cvs stores changes descending from 
399 #      main line. ie suppose the last version on mainline is 1.6
400 #      version 1.6 of the file is stored in toto.  version 1.5
401 #      is stored as a diff between 1.5 and 1.6.  1.4 is stored 
402 #      as a diff between 1.5 and 1.4.
403 #      branches are stored a little differently.  They are 
404 #      stored in ascending order.  Suppose there is a branch
405 #      on 1.4 of the file.  The first branches revision number
406 #      would be 1.4.1.1.  This is stored as a diff between 
407 #      version 1.4 and 1.4.1.1.  The 1.4.1.2 version is stored
408 #      as a diff between 1.4.1.1 and 1.4.1.2.  Therefore
409 #      we are only interested in the earliest revision number
410 #      and the highest revision number on a branch.
411 #
412 #    PARAMETERS :
413 #      @revisions - The list of revisions to find interesting ones
414 #
415 #    GLOBALS :
416 #      NONE
417 #
418 #    RETURNS :
419 #      @new_revisions - The list of revisions that we find interesting
420 #
421 #    COMMENTS :
422 #
423 ######################################################################
424 sub find_interesting_revisions
425 {
426     my( @revisions ) = @_;
427     my @new_revisions;
428     my %branch_revision;
429     my $branch_number;
430     my $branch_rev;
431     my $key;
432     my $value;
433
434     START_OVER:
435     foreach my $revision( @revisions )
436     {
437         my $start_over = 0;
438         ( $branch_number, $branch_rev ) = branch_split( $revision );
439
440         #if the number of elements in the branch is 1
441         #and the new branch is less than the old branch
442         if( elements_in_branch( $branch_number ) == 1 )
443         {
444             ( $start_over,
445               %branch_revision ) = find_int_mainline_revision( $branch_number,
446                                                                $branch_rev,
447                                                                %branch_revision );
448             next START_OVER if( $start_over );
449         }
450
451         %branch_revision = find_int_branch_revision( $branch_number,
452                                                      $branch_rev,
453                                                      %branch_revision );
454
455     }
456
457     %branch_revision = remove_duplicate_branches( %branch_revision );
458
459     while( ( $key, $value ) = each ( %branch_revision ) )
460     {
461         push( @new_revisions, $key . "." . $value );
462     }
463
464     my $nrc;
465     my $rc;
466
467     $rc = @revisions;
468     $nrc = @new_revisions;
469
470     $total_revisions += $rc;
471     $total_interesting_revisions += $nrc;
472
473     print( "\t\tTotal Revisions: $rc Interesting Revisions: $nrc\n" ) if( $verbose );
474
475     return( @new_revisions );
476 }
477
478 ########################################################################
479 #
480 #    NAME :
481 #      remove_duplicate_branches
482 #
483 #    PURPOSE :
484 #      To remove from the list of branches that we are interested
485 #      in duplication that will cause cvs to check a revision multiple
486 #      times.  For Instance revision 1.1.1.1 should be prefered
487 #      to be checked over revision 1.1, as that v1.1.1.1 can
488 #      only be retrieved by going through v1.1.  Therefore
489 #      we should remove v1.1 from the list of branches that
490 #      are interesting.
491 #
492 #    PARAMETERS :
493 #      %branch_revisions - The hash of the interesting revisions
494 #
495 #    GLOBALS :
496 #      NONE
497 #
498 #    RETURNS :
499 #      %branch_revisions - The hash of the modified interesting revisions
500 #
501 #    COMMENTS :
502 #      NONE
503 #
504 ########################################################################
505 sub remove_duplicate_branches
506 {
507     my( %branch_revisions ) = @_;
508     my $key;
509     my $value;
510     my $branch_comp;
511     my $branch;
512
513
514   RESTART:
515     {
516         my @keys = keys( %branch_revisions );
517         while( ( $key, $value ) = each ( %branch_revisions ) )
518         {
519             $branch_comp = $key . "." . $value;
520             foreach $branch ( @keys )
521             {
522                 if( $branch eq $key )
523                 {
524                     next;
525                 }
526                 if( elements_in_branch( $branch_comp ) ==
527                     elements_in_branch( $branch ) - 1 )
528                 {
529                     if( $branch =~ /^$branch_comp/ )
530                     {
531                         delete( $branch_revisions{ $key } );
532                         goto RESTART;
533                     }
534                 }
535             }
536         }
537     }
538
539     return( %branch_revisions );
540 }
541
542 ######################################################################
543 #
544 #    NAME :
545 #      find_int_branch_revision
546 #
547 #    PURPOSE :
548 #      To Find a interesting branch revision.
549 #      Algorithm:
550 #        If the $branch_revision exists in the interesting branch
551 #        hash and the new $branch_rev is less than currently saved
552 #        one replace it with the new $branch_rev.
553 #        else if the $branch_revision doesn't exist in the interesting
554 #        branch hash, then just store the $branch_number and $branch_rev
555 #
556 #    PARAMETERS :
557 #      $branch_number - The branch that we are looking at
558 #      $branch_rev    - The particular revision we are looking
559 #                       at on the $branch_number.
560 #      %branch_revision - The hash storing the interesting branches
561 #                         and the revisions on them.
562 #
563 #    GLOBALS :
564 #      NONE
565 #
566 #    RETURNS :
567 #      %branch_revision - The modified hash that stores interesting
568 #                         branches.
569 #
570 #    COMMENTS :
571 #      NONE
572 #
573 ######################################################################
574 sub find_int_branch_revision
575 {
576     my( $branch_number, $branch_rev, %branch_revision ) = @_;
577
578     if( exists( $branch_revision{ $branch_number } ) )
579     {
580         if( $branch_rev > $branch_revision{ $branch_number } )
581         {
582             $branch_revision{ $branch_number } = $branch_rev;
583         }
584     }
585     else
586     {
587         $branch_revision{ $branch_number } = $branch_rev;
588     }
589
590     return( %branch_revision );
591 }
592
593 ######################################################################
594 #
595 #    NAME :
596 #      find_int_mainline_revision
597 #
598 #    PURPOSE :
599 #      To Find a interesting mainline revision.
600 #      Algorithm:
601 #        if the $branch_number is less then a branch number
602 #        with one element in it, then delete the old branch_number
603 #        and return.
604 #        if the $branch_number is greater than a branch number
605 #        then return, and tell the calling function that we
606 #        should skip this element, as that it's not important.
607 #        if the $branch_number is the same as a branch number
608 #        with one element in it, then check to see if the
609 #        $branch_rev is less than the stored branch rev if
610 #        it is replace with new $branch_rev.  Else ignore revision
611 #
612 #    PARAMETERS :
613 #      $branch_number - The branch that we are looking at
614 #      $branch_rev    - The particular revision we are looking
615 #                       at on the $branch_number.
616 #      %branch_revision - The hash storing the interesting branches
617 #                         and the revisions on them.
618 #
619 #    GLOBALS :
620 #      NONE
621 #
622 #    RETURNS :
623 #      ( $skip, %branch_revision ) -
624 #      $skip - 1 if we need to ignore this particular $branch_number
625 #              $branch_rev combo.  Else 0.
626 #      %branch_revision - The modified hash that stores interesting
627 #                         branches.
628 #
629 #    COMMENTS :
630 #      NONE
631 #
632 ######################################################################
633 sub find_int_mainline_revision
634 {
635     my( $branch_number, $branch_rev, %branch_revision ) = @_;
636
637     foreach my $key ( keys %branch_revision )
638     {
639         if( elements_in_branch( $key ) == 1 )
640         {
641             if( $branch_number < $key )
642             {
643                 delete( $branch_revision{ $key } );
644                 next;
645             }
646
647             if( $branch_number > $key )
648             {
649                 return( 1, %branch_revision );
650             }
651             if( ( exists( $branch_revision{ $branch_number } ) ) &&
652                 ( $branch_rev < $branch_revision{ $branch_number } ) )
653             {
654                 $branch_revision{ $branch_number } = $branch_rev;
655                 return( 1, %branch_revision );
656             }
657         }
658     }
659
660     return( 0, %branch_revision );
661 }
662
663 ######################################################################
664 #
665 #    NAME :
666 #      elements_in_branch
667 #
668 #    PURPOSE :
669 #      Determine the number of elements in a revision number
670 #      Elements are defined by numbers seperated by ".".
671 #      the revision 1.2.3.4 would have 4 elements
672 #      the revision 1.2.4.5.6.7 would have 6 elements
673 #
674 #    PARAMETERS :
675 #      $branch - The revision to look at.
676 #
677 #    GLOBALS :
678 #      NONE
679 #
680 #    RETURNS :
681 #      $count - The number of elements
682 #
683 #    COMMENTS :
684 #      NONE
685 #
686 ######################################################################
687 sub elements_in_branch
688 {
689     my( $branch ) = @_;
690     my @split_rev;
691
692     @split_rev = split /\./, $branch;
693
694     my $count = @split_rev;
695     return( $count );
696 }
697
698 ######################################################################
699 #
700 #    NAME :
701 #      branch_split
702 #
703 #    PURPOSE :
704 #      To split up a revision number up into the branch part and
705 #      the number part.  For Instance:
706 #      1.1.1.1 - is split 1.1.1 and 1
707 #      2.1     - is split 2 and 1
708 #      1.3.4.5.7.8 - is split 1.3.4.5.7 and 8
709 #
710 #    PARAMETERS :
711 #      $revision - The revision to look at.
712 #
713 #    GLOBALS :
714 #      NONE
715 #
716 #    RETURNS :
717 #      ( $branch, $revision ) - 
718 #      $branch - The branch part of the revision number 
719 #      $revision - The revision part of the revision number
720 #
721 #    COMMENTS :
722 #      NONE
723 #
724 ######################################################################
725 sub branch_split
726 {
727     my( $revision ) = @_;
728     my $branch;
729     my $version;
730     my @split_rev;
731     my $count;
732
733     @split_rev = split /\./, $revision;
734
735     my $numbers = @split_rev;
736     @split_rev = reverse( @split_rev );
737     $branch = pop( @split_rev );
738     for( $count = 0; $count < $numbers - 2 ; $count++ )
739     {
740         $branch .= "." . pop( @split_rev );
741     }
742
743     return( $branch, pop( @split_rev ) );
744 }
745
746 ######################################################################
747 #
748 #    NAME :
749 #      get_ignore_files_from_cvsroot
750 #
751 #    PURPOSE :
752 #      Retrieve the list of files from the CVSROOT/ directory
753 #      that should be ignored. 
754 #      These are the regular files (e.g., commitinfo, loginfo)
755 #      and those specified in the checkoutlist file.
756 #
757 #    PARAMETERS :
758 #      The CVSROOT
759 #
760 #    GLOBALS :
761 #      NONE
762 #
763 #    RETURNS :
764 #      @ignore - the list of files to ignore
765 #
766 #    COMMENTS :
767 #      NONE
768 #
769 ######################################################################
770 sub get_ignore_files_from_cvsroot {
771     my( $cvsroot ) = @_;
772     my @ignore = ( 'CVS\/fileattr$',
773                    '^CVSROOT\/loginfo',
774                    '^CVSROOT\/.#loginfo',
775                    '^CVSROOT\/rcsinfo',
776                    '^CVSROOT\/.#rcsinfo',
777                    '^CVSROOT\/editinfo',
778                    '^CVSROOT\/.#editinfo',
779                    '^CVSROOT\/verifymsg',
780                    '^CVSROOT\/.#verifymsg',
781                    '^CVSROOT\/commitinfo',
782                    '^CVSROOT\/.#commitinfo',
783                    '^CVSROOT\/taginfo',
784                    '^CVSROOT\/.#taginfo',
785                    '^CVSROOT\/cvsignore',
786                    '^CVSROOT\/.#cvsignore',
787                    '^CVSROOT\/checkoutlist',
788                    '^CVSROOT\/.#checkoutlist',
789                    '^CVSROOT\/cvswrappers',
790                    '^CVSROOT\/.#cvswrappers',
791                    '^CVSROOT\/notify',
792                    '^CVSROOT\/.#notify',
793                    '^CVSROOT\/modules',
794                    '^CVSROOT\/.#modules',
795                    '^CVSROOT\/readers',
796                    '^CVSROOT\/.#readers',
797                    '^CVSROOT\/writers',
798                    '^CVSROOT\/.#writers',
799                    '^CVSROOT\/passwd',
800                    '^CVSROOT\/config',
801                    '^CVSROOT\/.#config',
802                    '^CVSROOT\/val-tags',
803                    '^CVSROOT\/.#val-tags',
804                    '^CVSROOT\/history' );
805     my $checkoutlist_file = "$cvsroot\/CVSROOT\/checkoutlist";
806     open( CHECKOUTLIST, "<$cvsroot\/CVSROOT\/checkoutlist" )
807         or die( "Unable to read checkoutlist file: $!\n" );
808
809     my @list = <CHECKOUTLIST>;
810     chomp( @list );
811     close( CHECKOUTLIST )
812         or die( "Unable to close checkoutlist file: $!\n" );
813
814     foreach my $line( @list )
815     {
816         next if( $line =~ /^#/ || $line =~ /^$/ );
817         if( $line =~ /^\s*(\S*)\s*/ ) { $line = $1 };
818         push( @ignore, "^CVSROOT\/$line", "^CVSROOT\/\.#$line" );
819     }   
820
821     return @ignore;
822 }