#!/usr/bin/perl -w ############################################################################### # # # cvsManifest.pl # # # ############################################################################### # # # Description: This script uses 'cvs status' to generate a full project # # manifest (file/revision/date/author/etc) # # # # Author: Mark Zieg # # # # Date: Feb 18, 2003 # # # # Distribution: Freeware (BSD license) # # # ############################################################################### ################################ Dependencies ################################# use strict; use Cwd; ################################## Globals #################################### my $MD5_BIN = 'md5sum'; my $CVSROOT; ################################### main() #################################### MAIN: { # script only works in CVS-controlled directories if( ! -d "CVS" ) { die( "error: cvsManifest.pl should be run from a directory under CVS control.\n" ); } # must be a CVS directory! # get CVSROOT metadata $CVSROOT = GetCvsRoot(); # get 'cvs status' information my $ra_Files = FillStatus(); # prettify paths FillPaths( $ra_Files ); if( `which $MD5_BIN` ) { FillMD5( $ra_Files ); } else { warn( "not computing MD5 digests" ); } # determine column widths my $rh_Widths = FieldWidths( $ra_Files ); # display listing Display( $ra_Files, $rh_Widths ); } ############################ Functional Implementation ######################## ############################ sub Display { my $ra_Files = shift; my $rh_Width = shift; foreach my $rh_File (sort { ($a->{RelPath}.$a->{Name}) cmp ($b->{RelPath}.$b->{Name}) } @{$ra_Files}) { printf( "%-*s %-*s %-*s %-*s\n", $rh_Width->{Name} + $rh_Width->{RelPath} + 1, $rh_File->{RelPath}."/".$rh_File->{Name}, $rh_Width->{WorkingRev}, $rh_File->{WorkingRev}, $rh_Width->{WorkingDate}, $rh_File->{WorkingDate}, $rh_Width->{MD5}, $rh_File->{MD5} ); } } ######################### sub FillPaths { my $ra_Files = shift; foreach my $rh_File (@{$ra_Files}) { my $CvsPath = $rh_File->{RcsPathname}; my ($RelPath) = ( $CvsPath =~ /^$CVSROOT\/(.*)\/[^\/]+$/ ); $RelPath =~ s/\/Attic//g; $rh_File->{RelPath} = $RelPath; } } ######################### sub FillMD5 { my $ra_Files = shift; foreach my $rh_File (@{$ra_Files}) { my $Pathname = $rh_File->{LocalPath}; my $Cmd = "$MD5_BIN $Pathname 2>/dev/null"; #warn( "running $Cmd from " . getcwd() ); my $Result = `$Cmd`; if( !$? ) { my ($Digest) = ( $Result =~ /^(\S+)/ ); $rh_File->{MD5} = $Digest; } else { $rh_File->{MD5} = "error"; } } } ######################### sub FieldWidths { my $ra_Files = shift; my $rh_Widths = {}; foreach my $rh_File (@{$ra_Files}) { foreach my $Field (keys %{$rh_File} ) { $rh_Widths->{$Field} ||= 0; my $Len = length( "$rh_File->{$Field}" ); if( $rh_Widths->{$Field} < $Len ) { $rh_Widths->{$Field} = $Len; } } } return $rh_Widths; } ##########################3 sub FillStatus { my $ra_Files = []; my @FindFiles = `find . -type f | grep -v CVS`; foreach my $FindFile (@FindFiles) { chomp( $FindFile ); my $rh_File = {}; my @Lines = `cvs -Q status $FindFile 2>/dev/null`; my $State = "Separator"; my $Error = 0; foreach my $Line (@Lines) { chomp( $Line ); next if $Line =~ /^\s*$/; # skip blanks if( $State eq "Separator" ) { if( $Line =~ /^=+/ ) { $State = "File"; } elsif( $Line =~ /^\?/ ) { # encountered file not under revision control -- ignore } else { die( "Parse error [State $State]: $Line\n" ); } } elsif( $State eq "File" ) { if( $Line =~ /^File: (.*?)\s*Status: (.*?)\s*$/ ) { my $Filename = $1; my $Status = $2; # create a new rec # populate more data $rh_File->{LocalPath} = $FindFile; $rh_File->{Name} = $Filename; $rh_File->{Status} = $Status; $State = "Working"; } else { die( "Parse error [State $State]: $Line\n" ); } } elsif( $State eq "Working" ) { if( $Line =~ /^\s+Working revision:\s+([-0-9.]+)\s*(.*?)\s*$/ ) { # populate more data $rh_File->{WorkingRev} = $1; $rh_File->{WorkingDate} = $2 ? $2 : "unknown"; $State = "Repository"; } elsif( $Line =~ /^\s+Working revision:\s+(New.*?)\s*$/ ) { # populate more data $rh_File->{WorkingRev} = "new"; $State = "Repository"; } elsif( $Line =~ /No entry for/ ) { $Error = 1; last; } else { die( "Parse error [State $State]: $Line\n" ); } } elsif( $State eq "Repository" ) { if( $Line =~ /^\s+Repository revision:\s+([0-9.]+)\s*(.*?)\s*$/ ) { # populate more data $rh_File->{RepositoryRev} = $1; $rh_File->{RcsPathname} = $2 ? $2 : "unknown"; $State = "Sticky"; } elsif( $Line =~ /^\s+Repository revision:\s+(No.*?)\s*$/ ) { # populate more data $rh_File->{RepositoryRev} = "n/a"; $State = "Sticky"; } else { die( "Parse error [State $State]: $Line\n" ); } } elsif( $State eq "Sticky" ) { # populate more data if( $Line =~ /^\s+Sticky Tag:\s+(.*?)\s*$/ ) { $rh_File->{StickyTag} = $1; if( $1 !~ /\(none\)/i ) { my ($Label) = ( $1 =~ /^(\S+)/ ); $Label =~ s/([-_]branch)|(branch[-_])//i; $rh_File->{Sticky} = $Label; } } elsif( $Line =~ /^\s+Sticky Date:\s+(.*?)\s*$/ ) { $rh_File->{StickyDate} = $1; $rh_File->{Sticky} = $1 unless $1 eq "(none)"; } elsif( $Line =~ /^\s+Sticky Option:\s+(.*?)\s*$/ ) { $rh_File->{StickyOption} = $1; $rh_File->{Sticky} = $1 unless $1 eq "(none)"; } elsif( $Line =~ /^\s+Sticky Options:\s+(.*?)\s*$/ ) { $rh_File->{StickyOptions} = $1; } elsif( $Line =~ /^=+/ ) { $State = "File"; } else { die( "Parse error [State $State]: $Line\n" ); } } else { die( "Parse error [State $State]: $Line\n" ); } } push( @{$ra_Files}, $rh_File ) unless $Error; } return $ra_Files; } ####################################### sub GetCvsRoot { open( INFILE, "CVS/Root" ) || die( "Can't read CVS/Root: $!\n" ); my $Line = ; chomp( $Line ); close( INFILE ); my ($Root) = ( $Line =~ /([^:]+)$/ ); return $Root; } ####################################### sub GetCvsRepository { open( INFILE, "CVS/Repository" ) || die( "Can't read CVS/Repository: $!\n" ); my $Line = ; chomp( $Line ); close( INFILE ); return $Line; } ######################### sub max { return 0 if $#_ == -1; my $max = shift; foreach my $foo (@_) { if( defined( $foo ) && $foo > $max ) { $max = $foo; } } return $max; }