#!/usr/bin/perl -w ############################################################################### # # # cvsdir.pl # # # ############################################################################### # # # Description: This is a wrapper for "ls -la" which folds some CVS # # data into directory listings. # # # # Author: Mark Zieg # # # # Date: Dec 6, 2002 # # # # Distribution: Freeware (BSD license) # # # # Notes: Tested under cvs 1.11.2 under RH 7.2. YMMV. # # # ############################################################################### ################################ Dependencies ################################# use strict; use Cwd; use Getopt::Long; ################################## Globals #################################### my $CVS_BIN = '/usr/bin/cvs'; my $LS_BIN = '/bin/ls'; my @LS_OPTS_FALLBACK = qw( -lav --color=always ); # non-Linux/Cygwin folks my @LS_OPTS_INTERNAL = qw( -lav --color=always ); # might want to change my $StupidCygwinPlus = 0; my $rh_Opts = {}; ################################### main() #################################### MAIN: { GetOptions( "revision!" => \$rh_Opts->{Revision}, "help!" => \$rh_Opts->{Help}, "debug!" => \$rh_Opts->{Debug}, ) || Usage(); Usage() if $rh_Opts->{Help}; # were we given any cmd-line args? if( $ARGV[0] ) { if( -d "$ARGV[0]/CVS" ) { chdir( $ARGV[0] ); } else { Fallback( "cmd-line arg is not CVS-controlled directory" ); } } # fallback to normal operation if this is not a CVS directory Fallback( "not in CVS-controlled directory" ) if( ! -d "CVS" ); # must be a CVS directory! # get CVS information my $rh_Files = {}; FillStatus( $rh_Files ); FillUpdate( $rh_Files ); # get filesystem information my $ra_Files = FillLs( $rh_Files ); # determine column widths my $rh_Widths = FieldWidths( $ra_Files ); # display listing Display( $ra_Files, $rh_Widths ); } ############################ Functional Implementation ######################## ######################### sub Fallback { my $msg = shift; warn( "Fallback: $msg" ) if $rh_Opts->{Debug}; exec( $LS_BIN, @LS_OPTS_FALLBACK, @ARGV ); } sub Display { my $ra_Files = shift; my $rh_Width = shift; foreach my $rh_File (@{$ra_Files}) { # default to filesystem data my $Owner = $rh_File->{Owner}; my $Group = $rh_File->{Group}; my $Status = ""; my $Code = ""; my $Sticky = ""; # swap in CVS data where appropriate if( $rh_File->{Code} ) { $Code = $rh_File->{Code}; } if( defined( $rh_File->{Sticky} ) ) { $Sticky = "S"; $Group = $rh_File->{Sticky}; } if( $rh_File->{WorkingRev} ) { $Status = $rh_File->{Status}; if( $Status !~ /up-to-date/i || $rh_Opts->{Revision} ) { $Owner = $rh_File->{WorkingRev}; $Group = $rh_File->{RepositoryRev}; } } # display line # drwxrwxr-x mzieg oa 4096 Dec 6 2002 foo.c # drwxrwxr-xU 1.1 1.2 Needs Patch 4096 Dec 6 16:59 bar.c printf( "%-*s %1s%1s %-*s %-*s %*s %*s %*s %*s %*s %s\n", $StupidCygwinPlus ? 11 : 10, $rh_File->{Flags}, $Code, $Sticky, max( $rh_Width->{Owner}, $rh_Width->{WorkingRev} ), $Owner, max( $rh_Width->{Group}, $rh_Width->{RepositoryRev}, $rh_Width->{Sticky} ), $Group, $rh_Width->{Status}, $Status, $rh_Width->{Size}, $rh_File->{Size}, $rh_Width->{Month}, $rh_File->{Month}, $rh_Width->{Day}, $rh_File->{Day}, $rh_Width->{YearTime}, $rh_File->{YearTime}, $rh_File->{DisplayName} ); } } ######################### 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; } ######################### sub FillLs { my $rh_Files = shift; my $ra_Files = []; # iterate through listing, merging in filesystem data my @Lines = `$LS_BIN @LS_OPTS_INTERNAL`; foreach my $Line (@Lines) { # check for stupid Cygwin "+" after directory perms if( $Line =~ /d.{9}\+/ ) { $StupidCygwinPlus = 1; } # break directory columns into fields my @Fields = split( /\s+/, $Line ); # handle filename -vs- displayname my $DisplayName = $Fields[8]; my $Filename = $DisplayName; next unless $DisplayName; if( $Filename =~ /\033/ ) { $Filename =~ s/\033.*?m//g; } next unless $Filename; # create a new rec if one doesn't exist my $rh_File = $rh_Files->{$Filename} || {}; $rh_Files->{$Filename} = $rh_File; # populate more data my $i = 0; foreach my $Field (qw(Flags Blocks Owner Group Size Month Day YearTime Filename)) { $rh_File->{$Field} = $Fields[$i++]; } $rh_File->{DisplayName} = $DisplayName; $rh_File->{Status} ||= "unknown"; # push them onto a queue in `ls` order push( @{$ra_Files}, $rh_File ); } return $ra_Files; } ########################## sub FillUpdate { my $rh_Files = shift; my $rh_File; my @Lines = `$CVS_BIN -q -n update -l 2>/dev/null`; Fallback( "cvs -q -n update -l returned $?: $!" ) if( $? ); foreach my $Line (@Lines) { # parse line chomp( $Line ); my( $Code, $Filename ) = split( /\s+/, $Line ); # create a new rec if one doesn't exist my $rh_File = $rh_Files->{$Filename} || {}; $rh_Files->{$Filename} = $rh_File; # populate more data $rh_File->{Code} = $Code; $rh_File->{Status} ||= "unknown"; } } ##########################3 sub FillStatus { my $rh_Files = shift; my $rh_File; my @Lines = `$CVS_BIN -Q status -l 2>/dev/null`; Fallback( "cvs -Q status -l returned $?: $!" ) if( $? ); my $State = "Separator"; 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 if one doesn't exist $rh_File = $rh_Files->{$Filename} || {}; $rh_Files->{$Filename} = $rh_File; # populate more data $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.]+)/ ) { # populate more data $rh_File->{WorkingRev} = $1; $State = "Repository"; } elsif( $Line =~ /^\s+Working revision:\s+(New.*?)\s*$/ ) { # populate more data $rh_File->{WorkingRev} = "new"; $State = "Repository"; } 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; $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" ); } } } ######################### sub max { return 0 if $#_ == -1; my $max = shift; foreach my $foo (@_) { if( defined( $foo ) && $foo > $max ) { $max = $foo; } } return $max; } ######################### sub Usage { my $RevisionTag = '$Revision: 1.1 $'; my ($Revision) = ( $RevisionTag =~ /([0-9.]+)/ ); print "cvsdir $Revision, by Mark Zieg\n"; print "usage: cvsdir [directory] [--help]\n"; print "\n"; print "This script will display the CURRENT directory with CVS-augmented\n"; print "fields, if the current directory is indeed under CVS revision control.\n"; print "If the current directory is not under CVS, or if any arguments are\n"; print "passed at all, then the script will operate exactly like:\n\n"; print " \$ $LS_BIN @LS_OPTS_INTERNAL\n\n"; exit(0); }