#!/usr/bin/perl -w ############################################################################### # # # cvsWhoWhatWhen.pl # # # ############################################################################### # # # Description: CVS script provided to query 'cvs log' and extract out # # WHO made WHAT changes WHEN. # # # # Author: Mark Zieg # # # # Date: Apr 25, 2003 # # # # Usage: see Usage() below # # # # --start defaults to midnight (AM) 'today' (midnight yesterday)# # --end defaults to midnight (PM) 'today' (midnight tonight) # # # ############################################################################### use strict; use Getopt::Long; ####################################### MAIN: { # process cmd-line arguments my $rh_Opts = {}; $rh_Opts->{Start} = $rh_Opts->{End} = GetToday(); my $ArgsOk = GetOptions( "start=s" => \$rh_Opts->{Start}, "end=s" => \$rh_Opts->{End}, "who=s" => \$rh_Opts->{Who}, "logopts=s" => \$rh_Opts->{LogOpts}, "verbose!" => \$rh_Opts->{Verbose}, "patchfile=s" => \$rh_Opts->{Patchfile}, "table!" => \$rh_Opts->{Table}, "debug!" => \$rh_Opts->{Debug}, "help" => \$rh_Opts->{Help}, "diffstart=s" => \$rh_Opts->{DiffStart}, ); $rh_Opts->{Start} =~ s/\D//g; $rh_Opts->{End} =~ s/\D//g; Usage() unless $rh_Opts->{Start} =~ /^\d{8}|\d{12}$/ && $rh_Opts->{End} =~ /^\d{8}|\d{12}$/ && !$rh_Opts->{Help} && $ArgsOk; # determine runtime environment $rh_Opts->{Version} = GetCvsVersion(); $rh_Opts->{SupportLogS} = GetCvsSupportLogS(); $rh_Opts->{Root} = GetCvsRoot(); $rh_Opts->{Repository} = GetCvsRepository(); $rh_Opts->{TZOffset} = GetTZOffset(); # fill out CVS time fields CompleteTime( OPTIONS => $rh_Opts, FIELD => "Start", DEFAULT => "MORNING" ); CompleteTime( OPTIONS => $rh_Opts, FIELD => "End", DEFAULT => "NIGHT" ); # get CVS log output my $ra_Files = GetCvsLog( $rh_Opts ); # print filtered results PrintResults( $rh_Opts, $ra_Files ); # produce optional patchfile GeneratePatchfile( $rh_Opts, $ra_Files ) if $rh_Opts->{Patchfile}; } ####################################### sub PrintResults { my $rh_Opts = shift; my $ra_Files = shift; if( $rh_Opts->{Table} ) { PrintTable( $rh_Opts, $ra_Files ); } else { PrintDetail( $rh_Opts, $ra_Files ); } } ####################################### sub PrintDetail { my $rh_Opts = shift; my $ra_Files = shift; foreach my $rh_File (@{$ra_Files}) { my $FileHeaderPrinted = 0; foreach my $rh_Rev (@{$rh_File->{Revisions}}) { if( !$FileHeaderPrinted ) { print "$rh_File->{RelPath}\n"; $FileHeaderPrinted = 1; } print " Rev: $rh_Rev->{Number}\n"; print " Date: $rh_Rev->{Date} $rh_Rev->{Time}\n"; print " Who: $rh_Rev->{Who}\n"; if( defined( $rh_Rev->{Added} ) ) { print " Mods: added $rh_Rev->{Added} lines, " . "deleted $rh_Rev->{Deleted} lines\n"; } my $First = 1; foreach my $Line (@{$rh_Rev->{Comment}}) { if( $First ) { print " Note: $Line\n"; $First = 0; } else { print " $Line\n"; } } print "\n"; my $PrevRev = PreviousRevision( $rh_Rev->{Number} ); if( $rh_Opts->{Verbose} && $PrevRev ) { my $Cmd = "cvs -Q diff -r$PrevRev -r$rh_Rev->{Number} $rh_File->{LocalPath}"; print " $Cmd\n"; my @DiffLines = `$Cmd`; my $State = "FIRST"; foreach my $Line (@DiffLines) { if( $State eq "FIRST" ) { if( $Line =~ /^(Index|={5})/ ) { $State = "HEADER"; } else { $State = "DIFF"; } } if( $State eq "HEADER" ) { if( $Line =~ /^diff -r[.0-9]+ -r[.0-9]+/ ) { $State = "DIFF"; } } else { print " $Line"; } } print "\n"; } } } } ####################################### sub PrintTable { my $rh_Opts = shift; my $ra_Files = shift; # # generate field widths # my $rh_Widths = {}; foreach my $rh_File (@{$ra_Files}) { foreach my $Field (keys %{$rh_File} ) { $rh_Widths->{"File$Field"} ||= 0; my $Len = length( $rh_File->{$Field} ); if( $rh_Widths->{"File$Field"} < $Len ) { $rh_Widths->{"File$Field"} = $Len; } } foreach my $rh_Rev (@{$rh_File->{Revisions}}) { foreach my $Field (keys %{$rh_Rev} ) { $rh_Widths->{"Rev$Field"} ||= 0; my $Len = $rh_Rev->{$Field} ? length( $rh_Rev->{$Field} ) : 0; if( $rh_Widths->{"Rev$Field"} < $Len ) { $rh_Widths->{"Rev$Field"} = $Len; } } } } # # print table # foreach my $rh_File (@{$ra_Files}) { foreach my $rh_Rev (@{$rh_File->{Revisions}}) { my $Comment = ""; if( $rh_Opts->{Verbose} ) { $Comment = join( " ", @{$rh_Rev->{Comment}} ); $Comment =~ s/ +/ /g; } my $OperAdded = $rh_Rev->{Added} ? "+" : " "; my $OperDeleted = $rh_Rev->{Deleted} ? "-" : " "; printf( "%-*s %-*s %-*s %-*s %-*s %*s %*s %s\n", $rh_Widths->{FileRelPath}, $rh_File->{RelPath}, $rh_Widths->{RevWho}, $rh_Rev->{Who}, $rh_Widths->{RevNumber}, $rh_Rev->{Number}, $rh_Widths->{RevDate}, $rh_Rev->{Date}, $rh_Widths->{RevTime}, $rh_Rev->{Time}, $rh_Widths->{RevAdded}+1, $OperAdded . $rh_Rev->{Added}, $rh_Widths->{RevDeleted}+1, $OperDeleted . $rh_Rev->{Deleted}, $Comment ); } } } ####################################### sub GeneratePatchfile { my $rh_Opts = shift; my $ra_Files = shift; open( PATCHFILE, ">$rh_Opts->{Patchfile}" ) || die( "Can't write $rh_Opts->{Patchfile}: $!\n" ); foreach my $rh_File (@{$ra_Files}) { my ($FirstRev, $LastRev); my $New = 0; foreach my $rh_Rev (@{$rh_File->{Revisions}}) { $FirstRev = PreviousRevision( $rh_Rev->{Number} ); if( !$FirstRev ) { $New = 1; } $LastRev = $rh_Rev->{Number} unless $LastRev; } my $Cmd; if( $rh_Opts->{DiffStart} ) { $Cmd = "cvs diff -I rcs_id -c3 -r$rh_Opts->{DiffStart} -r$LastRev $rh_File->{LocalPath}"; } elsif( $New ) { $Cmd = "cvs diff -N -D 1990/01/01 $rh_File->{LocalPath}"; } else { $Cmd = "cvs diff -I rcs_id -c3 -r$FirstRev -r$LastRev $rh_File->{LocalPath}"; } my @PatchLines = `$Cmd`; foreach my $Line (@PatchLines) { print PATCHFILE $Line; } } close( PATCHFILE ); } ####################################### sub PreviousRevision { my $Rev = shift; my ($Major, undef, $Minor) = ( $Rev =~ /^((\d+\.)+)(\d+)$/ ); if( $Minor > 1 ) { return( sprintf( "%s%d", $Major, $Minor - 1 ) ); } else { return; } } ####################################### sub GetCvsLog { my $rh_Opts = shift; # generate CVS log data # # # start CVS command my $Cmd = "cvs -q log -N "; # add date range, unless specific 'cvs log' options were provided if( $rh_Opts->{LogOpts} ) { $Cmd .= $rh_Opts->{LogOpts} . " "; } else { $Cmd .= "-d\"$rh_Opts->{Start}<=$rh_Opts->{End}\" "; } # add "who", if specified if( $rh_Opts->{Who} ) { $Cmd .= " -w$rh_Opts->{Who} "; } # add -S (don't print file if no revisions are selected) if supported if( $rh_Opts->{SupportLogS} ) { $Cmd .= " -S"; } my $State = "RCS"; my $ra_Files = []; my $rh_File = {}; my $rh_Rev = {}; print "running $Cmd\n" if $rh_Opts->{Debug}; my $EatErrors = $rh_Opts->{Debug} ? "" : "2>/dev/null"; open( CVS, "$Cmd $EatErrors |" ) || die "Can't open FIFO from '$Cmd': $!\n"; while( defined( my $Line = ) ) { chomp( $Line ); if( $State eq "RCS" ) { if( $Line =~ /^RCS file:\s*($rh_Opts->{Root}\/($rh_Opts->{Repository}\/(.*?)),v)\s*$/ ) { $rh_File = {}; $rh_File->{RCS} = $1; $rh_File->{RelPath} = $2; $rh_File->{LocalPath} = $3; $rh_File->{RelPath} =~ s/Attic\///g; $rh_File->{LocalPath} =~ s/Attic\///g; $State = "REVISIONS"; } } elsif( $State eq "REVISIONS" ) { if( $Line =~ /^total revisions:\s*(\d+)\s*;?\s*selected revisions:\s*(\d+)/ ) { if( $2 ) { push( @{$ra_Files}, $rh_File ); $rh_File->{Revisions} = []; $State = "DESCRIPTION"; } else { $State = "RCS"; } } } elsif( $State eq "DESCRIPTION" ) { if( $Line =~ /^description:/ ) { $State = "REVISION_DELIM"; } } elsif( $State eq "REVISION_DELIM" ) { if( $Line =~ /^-{5}/ ) { $State = "REVISION_NUMBER"; } elsif( $Line =~ /^={5}/ ) { $State = "RCS"; } else { warn( "Parse error [$State]: can't parse line: $Line" ); $State = "RCS"; } } elsif( $State eq "REVISION_NUMBER" ) { if( $Line =~ /^revision ([0-9.]+)/ ) { $rh_Rev = {}; $rh_Rev->{Number} = $1; $rh_Rev->{Comment} = []; push( @{$rh_File->{Revisions}}, $rh_Rev ); $State = "REVISION_DATE"; } else { warn( "Parse error [$State]: can't parse line: $Line" ); $State = "RCS"; } } elsif( $State eq "REVISION_DATE" ) { if( $Line =~ /^date:\s*(\S+)\s+(\S+);\s*author:\s*(\S+);\s*state:\s*(\S+);(\s*lines:\s*[-+](\d+)\s+[-+](\d+)\s*)?/ ) { $rh_Rev->{Date} = $1; $rh_Rev->{Time} = $2; $rh_Rev->{Who} = $3; $rh_Rev->{State} = $4; if( $5 ) { $rh_Rev->{Added} = $6; $rh_Rev->{Deleted} = $7; } else { $rh_Rev->{Added} = $rh_Rev->{Deleted} = 0; } $State = "REVISION_COMMENT"; } else { warn( "Parse error [$State]: can't parse line: $Line" ); $State = "RCS"; } } elsif( $State eq "REVISION_COMMENT" ) { if( $Line =~ /^-{5}/ ) { $State = "REVISION_NUMBER"; } elsif( $Line =~ /^={5}/ ) { $State = "RCS"; } else { push( @{$rh_Rev->{Comment}}, $Line ); } } else { die( "Unknown state: $State\n" ); } } return $ra_Files; } ####################################### sub CompleteTime { my %ARGS = @_; my $rh_Opts = $ARGS{OPTIONS}; my $Field = $ARGS{FIELD}; my $Default = $ARGS{DEFAULT}; my $Original = $rh_Opts->{$Field}; die( "Can't parse datetime: $Original\n" ) unless $Original =~ /^(\d{4})(\d{2})(\d{2})((\d{2})(\d{2}))?$/; if( $4 ) { $rh_Opts->{$Field} = sprintf( "%04d-%02d-%02dT%02d:%02d%s", $1, # year $2, # MM $3, # DD $5, # hh $6, # mm $rh_Opts->{TZOffset} # TZ ); } else { my $HHMM = ( $Default eq "MORNING" ) ? "00:00" : "23:59"; $rh_Opts->{$Field} = sprintf( "%04d-%02d-%02dT%s%s", $1, # year $2, # MM $3, # DD $HHMM, # hh:mm $rh_Opts->{TZOffset} # TZ ); } } ####################################### sub GetTZOffset { my $Result; $Result = `date +'%z'`; chomp( $Result ); if( $Result =~ /^([-+]\d{2})(\d{2})?/ ) { return $1; } else { $Result = `date +'%Z'`; chomp( $Result ); if( my $Offset = LookupTZ( $Result ) ) { return $Offset; } else { die( "Can't determine time zone offset!\n" ); } } } ####################################### sub GetCvsVersion { my @Lines = `cvs --version`; if( $? != 0 ) { die( "Can't run 'cvs --version'!\n" ); } else { my $rh_Version = {}; $rh_Version->{Major} = 1; $rh_Version->{Minor} = 0; $rh_Version->{Sub} = 0; $rh_Version->{Patch} = 0; my $Found = 0; foreach my $Line (@Lines) { if( $Line =~ /Concurrent Versions System \(CVS\) (\S+)/ ) { $Found = 1; my $Version = $1; if( $Version =~ /^(\d+)\.(\d+)(\.(\d+)(p(\d+))?)?$/ ) { $rh_Version->{Major} = $1; $rh_Version->{Minor} = $2; if( $3 ) { $rh_Version->{Sub} = $4; if( $5 ) { $rh_Version->{Patch} = $6; } } } else { warn( "Can't parse CVS version: $Version" ); } last; } } warn( "Can't find CVS version!" ) unless $Found; return $rh_Version; } } ####################################### sub GetCvsSupportLogS { my @Lines = `cvs -H log 2>&1`; foreach my $Line (@Lines) { return 1 if( $Line =~ /^\s+-S\s+/ ); } return 0; } ####################################### 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 GetToday { return `date +%Y%m%d`; } ####################################### sub LookupTZ { my $TZ = shift; # this table was borrowed from # http://www.oac.uci.edu/indiv/ehood/MHonArc/doc/resources/timezones.html # note that "Timezone settings in MHonArc are the negative inverse of what # is used in dates in messages" ...hence my negation below. my %Offsets = qw( ACDT -1030 ACST -930 ADT 300 AEDT -1100 AEST -1000 AHDT 900 AHST 1000 AST 400 AT 200 AWDT -900 AWST -800 BAT -300 BDST -200 BET 1100 BST -100 BT -300 BZT2 300 CADT -1030 CAST -930 CAT 1000 CCT -800 CDT 500 CED -200 CET -100 CST 600 EAST -1000 EDT 400 EED -300 EET -200 EEST -300 EST 500 FST -200 FWT -100 GMT 0 GST -1000 HDT 900 HST 1000 IDLE -1200 IDLW 1200 IST -530 IT -330 JST -900 JT -700 KST -900 MDT 600 MED -200 MET -100 MEST -200 MEWT -100 MST 700 MT -800 NDT 230 NFT 330 NT 1100 NST -630 NZ -1100 NZST -1200 NZDT -1300 NZT -1200 PDT 700 PST 800 ROK -900 SAD -1000 SAST -900 SAT -900 SDT -1000 SST -200 SWT -100 USZ3 -400 USZ4 -500 USZ5 -600 USZ6 -700 UT 0 UTC 0 UZ10 -1100 WAT 100 WET 0 WST -800 YDT 800 YST 900 ZP4 -400 ZP5 -500 ZP6 -600 ); if( my $Offset = $Offsets{$TZ} ) { my $SDDDD = sprintf( "%+05d", 0 - $Offset ); my ($SDD) = ( $SDDDD =~ /^([-+]\d{2})/ ); return $SDD; } else { return; } } ####################################### sub Usage { my $RevisionTag = '$Revision: 1.1 $'; my ($Revision) = ( $RevisionTag =~ /([0-9.]+)/ ); print "cvsWhoWhatWhen.pl $Revision, by Mark Zieg\n"; print "Usage: cvsWhoWhatWhen.pl [--start yyyymmdd[hhmm]] [--end yyyymmdd[hhmm]]\n"; print " [--who name] [--verbose] [--table] [--patchfile pathname] [--help]\n"; exit( 1 ); }