info-cvs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

CVSTAT (perl script to format cvs status commands)


From: Robert Bresner
Subject: CVSTAT (perl script to format cvs status commands)
Date: Fri, 15 Dec 2000 14:29:12 -0500

Attached.
#!/usr/bin/perl -w

#    Copyright (C) 2000 and on and on
#    by Robert Bresner
#    Open Link Financial, Inc
#    based on an idea by Danny Sadinoff (Hi Danny :-)
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
# cvstat - parses output of cvs stat and offers it in a handy readable format.
#  cvstat -F parses cvs status only, and does not report files that need first 
checkout (as cvs status does not)
#  cvstat without the -F parses 'cvs -n update' and the CVS/Entries files and 
sorts the output
#  cvstat -B will compare file timestamps to CVS/Entries timestamps and report 
locally modified files
#   ( Could report "Needs first/last checkin" also, but I never got around to 
it. )
#   -B is very very fast. (cvstat -BL is a nice way to reduce a list to a 
reg'lar cvstat )
# I wrote this program a long while back while still learning Perl, so it's not 
the prettiest
#  code in the world. But, it works for me

use strict;
use FileHandle;
use Cwd;

my $Recursive = 1;
my $verbose = 0;
my @Files;
my $Rev;
my $ShowList = 0;
my $FastStat = 0;
my $workdir;
my $DoneHeader = 0;
my $CurTag;
my $ShowEmptyTags = 1;
my $Quiet = 0;
my $FNameOnly = 0;
my $FinTime = 0;
my( $U, $M, $A, $R, $C, $P, $T, $Q) = (0, 1, 1, 1, 1, 1, 0, 0);
my @ListFiles;
my %UpdateHash;  # Keeps track of info during a slow stat
my %EntriesHash; # Saves Entries files files to check for 'needs first 
checkoout' files
my $Cwd;   # Directory where cvstat is being run from, this part of path is 
removed from filenames later.
my( $ST, $SF, $SS ) = (0, 0, 0);   # Sort flags;
my( $NewRoot, @CVSVars ) = ('');
my $ZC = '';
my $NoBranch = 0;
my $NetComp = 0;
my $UseCVSROOT = 0;
my $BLAZINGLY_FAST = 0;         # In caps cause it's just THAT fast.
my $tag= '';                    # Tag check for BFast mode
$| = 1;
GetArgs();
Go();
print "@ListFiles\n" if $ShowList;
################################################################################

sub CheckStatus
{
   my $CVStat = shift;

   return 'C' if $CVStat =~ /Needs Merge/;
   return 'U' if $CVStat =~ /Up-to-date/;
   return 'M' if $CVStat =~ /Locally Modified/;
   return 'P' if $CVStat =~ /Needs Update|Needs Patch/;
   return 'A' if $CVStat =~ /Locally Added/;
   return 'R' if $CVStat =~ /Locally Removed/;
   return 'Removed' if $CVStat =~ /Needs Checkout/;

   # Needs Checkout -- ? Deleted from repository, other directory reports 
this... Perhaps I'll handle that sometime.
   return $CVStat;
}

sub Go
{

   # If FastStating, just read the output of 'cvs status'
   #  otherwise, read the CVS/Entries file and try cvs -n update

   my $Cmd;
   $Cmd = "cvs -q";
   $Cmd .= " $ZC" if $ZC;
   $Cmd .= " $NewRoot";
   $Cmd .= " @CVSVars" if @CVSVars;
   if( $FastStat )  {
      $Cmd .= " status";
      $Cmd .= " -l" unless $Recursive;
      $Cmd .= " @Files" if @Files;
   }
   elsif( $BLAZINGLY_FAST )   {
      BlazeGo( @Files );
      exit( 0 );
   }
   else  {
      $Cmd .= " -n update";
      $Cmd .= " -l" unless $Recursive;
      $Cmd .= " @Files" if @Files;
   }

   my $FH = new FileHandle;
   print "Opening command pipe: $Cmd\n" if $verbose;
   open( $FH, "$Cmd|")
      or die "Cannot run command $Cmd: $!\n";

   if( $FastStat )  {  # FASTSTAT
      # Just parse the cvs status output.
      my( $File, $Status, $Tag);

      while( <$FH> )  {  
         print if $verbose;
         if( /^\? (.*)$/ )  {  PrintEntry('Q', $1);  }
         if( /^File: (.*)\s+Status: (.*)$/ )  {
            $File = $1;
            $Status = CheckStatus($2);
         }

         if( /Repository revision:/ )  {
            print "REPOSITORY\n" if $verbose;
            if( /Repository revision:.*?\/(.*?)$/ )  {
               my $FDir = $1;
               print "File $File in dir $FDir\n" if $verbose;
               if( $Status eq 'newbie' )  {
                  PrintEntry('Added', $File, 'none');
               }
            }
         }
         if( /Sticky Tag:\s+(.*)\s+/ )  {
            $Tag = $1;
            if( $NoBranch and /\(branch\:/ )  {  # If this is a branch, not a 
tag
               $Tag =~ s/\(branch:.*?\)//;
            }
            PrintEntry($Status, $File, $Tag);
         }
         if( /No entry for/ )   {
            $Status = 'newbie';
         }
      }
   }
   else  {  # ORIGINAL FLAVOR
      # Ok. Since this is the slow mode, read the Entries files for info first, 
      # then compare that info to the data from the cvs update

      while( <$FH> )  {
         /^\? (.*)$/ and do  {
            $UpdateHash{$1} = 'Q';
            next;
         };
         if( /(.) (.*)$/ )  {
            my ($file, $stat) = ($2, $1);
            if( $stat eq 'U' )  {
               $stat = 'P';
            }
            $UpdateHash{$file} = $stat;
            # print "SAVING FOR LATER: $2 => $1\n";
         }
      }
      # Ok, now I have a list of all the files. Search the Entries files for 
tags and such...
      # Saving info in UpdateHash: $File => $Stat|$Tag
      ReadEntries( $Cwd );

      # Now the %UpdateHash is filled with what needs printed.
      foreach my $File( sort MyHashSort keys %UpdateHash )  {
         my( $Stat, $Tag ) = split /\|/, $UpdateHash{$File};
         PrintEntry($Stat, $File, $Tag);
      }
   }
   close $FH;
}

sub MyHashSort
{
   # $a and $b are keys to %UpdateHash.
   # I wrote this program before I learned about hash of hashes
   return $a cmp $b
      if( $SF );

   my( $AStat, $ATag ) = split /\|/, $UpdateHash{$a};
   my( $BStat, $BTag ) = split /\|/, $UpdateHash{$b};

   $ATag = '' unless defined $ATag;
   $BTag = '' unless defined $BTag;

   return $AStat cmp $BStat
      if( $SS );
   return $ATag cmp $BTag
      if( $ST );

   return 0;
}

sub ReadEntries
{
   my $Dir = shift;
   # First check for and read the CVS/Entries file
   # print "Reading CVS/Entries for $Dir\n";
   if( -e "$Dir/CVS/Entries" )  {
      my $EFH = new IO::File("$Dir/CVS/Entries", 'r')
         or warn "WARNING: Could not read $Dir/CVS/Entries file: $!\n";
      while( <$EFH> )  {
         /^D/ and next;  # Skip directory entries
         chomp;
         my( $ThisFile, $ThisTag);
         if( /\/(.*?)\/.*\/(.*)$/ )  {  # I only want the file and tag info. 
Thank you.
            ( $ThisFile, $ThisTag ) = ("$Dir/$1", $2);
            # print "GOT THIS: FILE $ThisFile   TAG: $ThisTag\n";
         }
         else  {
            warn "Cannot parse $Dir/CVS/Entries line: $_\n";
            next;
         }
         $ThisTag = '' unless defined $ThisTag;
         $ThisFile =~ s/^$Cwd\///;
         $EntriesHash{$ThisFile} = "$ThisTag";
         if( exists $UpdateHash{$ThisFile} )  {
            $UpdateHash{$ThisFile} .= "|$ThisTag";
         }
         else  {
            $UpdateHash{$ThisFile} = "U|$ThisTag";  # No hash entry yet means 
it's up-to-date
         }
      }
   }
}

sub GetStat
{
   my $Stat = shift;

   return 'Up-To-Date' if $Stat eq 'U';
   return 'Locally Modified' if $Stat eq 'M';
   return 'Needs First Checkin' if $Stat eq 'A';
   return 'Removed, Needs Last Checkin' if $Stat eq 'R';
   return '*Needs Merged' if $Stat eq 'C';
   return 'Needs Update' if $Stat eq 'P';
   return 'Unknown File' if $Stat eq 'Q';

   return $Stat;
}

sub PrintEntry
{

   my( $Type, $File, $Tag ) = @_;
   print "CHECKING ENTRY FOR PRINT: TYPE $Type\n" if $verbose;

   if( not $T )  {  # If tags are on, everything gets printed, doncha know.
      return if $Type eq 'U' and not $U;
      return if $Type eq 'M' and not $M;
      return if $Type eq 'A' and not $A;
      return if $Type eq 'R' and not $R;
      return if $Type eq 'C' and not $C;
      return if $Type eq 'P' and not $P;
   }
   return if $Type eq 'Q' and not $Q;

   $Type = GetStat($Type);
   $Tag = '' unless $Tag;
   $Tag = $Tag =~ /none/ ? '' : $Tag;  # none may be in parens
   $File =~ s/\s+//g;

   $Tag = '' unless $T;

   if( not $DoneHeader and not $FNameOnly )   {
      my $Header = '';
      $Header .= "Sticky Tag     " if( $T or $ShowEmptyTags );
      $Header .= "Status               File";
      print "$Header\n";
      $DoneHeader = 1;
   }

   printf "%-14s ", $Tag
      if( $T or $ShowEmptyTags and not $FNameOnly );
   printf "%-20s ", $Type
      if( not $FNameOnly );
   print "$File\n";
   push @ListFiles, $File;

}

sub GetArgs
{
   # I wrote this long before learning of the likes of Getopts::Long
   my $RCtr = 0;
   my $DCtr = 0;
   my $usage = '';
   while( <DATA> )  {
      $usage .= $_;
   }

   my $STime = 0;
   $Cwd = cwd();
   $Cwd =~ s/\\/\//g;

   if( exists $ENV{CVSTAT_ARGS} and not (grep /^\-i$/, @ARGV ))  {   # Always 
the last args, the
      unshift @ARGV, split /\s+/, $ENV{CVSTAT_ARGS};
      warn "\tUsing CVSTAT_ARGS env var: $ENV{CVSTAT_ARGS}\n"
         unless (grep /^\-q$/, @ARGV);
   }

   while( @ARGV )  {
      my $a = shift @ARGV;
      if( $a =~ /^\-r(.*?)$/ )  {
         my $arg = $1 ? $1 : shift @ARGV;
         die "Only one -r, thanks. $0 for help\n"
            if $Rev;
         die "-r requires a tag/branch/revision. $0 -h for help\n"
            unless $arg;
         $a = '-r';
         print "GOT -r ARG: $a $arg\n" if $verbose;
         $Rev = $arg;
         $tag = $arg;
      }
      elsif( $a =~ /^\-B$/ )  {
         print "GOT -B ARG: $a\n" if $verbose;
         $BLAZINGLY_FAST = 1;
      }
      elsif( $a =~ /^\-i$/ )  {
         print "GOT -i ARG: $a\n" if $verbose;
         # The %CVSTAT_ARGS% var wouldn't have been added above if -i was used.
      }
      elsif( $a =~ /^\-d(.*?)$/ )  {
         my $arg = $1 ? $1 : shift @ARGV;
         $a = '-d';
         print "GOT: -d ARG: $a $arg\n" if $verbose;
         $NewRoot = "$a $arg";
      }
      elsif( $a =~ /^\-R$/ )  {
         print "GOT: -R\n" if $verbose;
         if( not exists $ENV{CVSROOT}  )  {
            die "Cannot use -R if env var CVSROOT is not set. -h for help\n";
         }
         $NewRoot = "-d $ENV{CVSROOT}";
      }
      elsif( $a =~ /^\-z(.*?)$/ )  {   # Compression level
         my $arg = $1 ? $1 : shift @ARGV;
         $a = '-z';
         print "GOT: -z ARG: $a $arg\n" if $verbose;
         $ZC = "$a $arg";
      }
      elsif( $a =~ /^\-h$/ )  {
         print "GOT -h ARG: $a\n" if $verbose;
         die "$usage\n";
      }
      elsif( $a =~ /^\-l$/ )  {
         print "GOT -l ARG: $a\n" if $verbose;
         $Recursive = 0;
      }
      elsif( $a =~ /^\-b$/ )  {
         print "GOT -b ARG: $a\n" if $verbose;
         $NoBranch = 1;
      }
      elsif( $a =~ /^\-c$/ )  {
         print "GOT -c ARG: $a\n" if $verbose;
         $ShowEmptyTags = 0;
      }
      elsif( $a =~ /^\-t$/ )  {
         print "GOT -t ARG: $a\n" if $verbose;
         print "Start time: ".(localtime)."\n"
            unless $STime;
         $STime = 1;
      }
      elsif( $a =~ /^\-tt$/ )  {
         print "GOT -tt ARG: $a\n" if $verbose;
         print "Start time: ".(localtime)."\n"
            unless $STime;
         $FinTime = 1;
         $STime = 1;
      }
      elsif( $a =~ /^\-L$/ )  {
         print "GOT -L ARG: $a\n" if $verbose;
         $ShowList = 1;
      }
      elsif( $a =~ /^\-o$/ )  {
         print "GOT -o ARG: $a\n" if $verbose;
         $FNameOnly = 1;
      }
      elsif( $a =~ /^\-F$/ )  {
         print "GOT -F ARG: $a\n" if $verbose;
         $FastStat = 1;
      }
      elsif( $a =~ /^\-q$/ )  {
         print "GOT -q ARG: $a\n" if $verbose;
         $Quiet = 1;
      }
      elsif( $a =~ /^\-V$/ )  {
         print "GOT -V ARG: $a\n" if $verbose;
         $verbose = 1;
      }
      elsif( $a =~ /^\-sf$/ )  {
         print "GOT -sf ARG: $a\n" if $verbose;
         $SF = 1;
      }
      elsif( $a =~ /^\-st$/ )  {
         print "GOT -st ARG: $a\n" if $verbose;
         $ST = 1;
      }
      elsif( $a =~ /^\-ss$/ )  {
         print "GOT -ss ARG: $a\n" if $verbose;
         $SS = 1;
      }
      elsif( $a =~ /^\-s(.*?)$/ )  {   # After the sorts for a reason.
         my $arg = $1 ? $1 : shift @ARGV;
         $a = '-s';
         print "GOT: -s ARG: $a $arg\n" if $verbose;
         push @CVSVars, "$a $arg";
      }
      elsif( $a =~ /^\-/ )  {
         print "GOT UNKNOWN ARG: $a\n" if $verbose;
         die "Unknown option: '$a'\ncvstat -h for help\n";
      }
      elsif( $a =~ /^\+/ )  {
         $a =~ /\+\+/ and $U = $M = $A = $R = $C = $P = $T = $Q = 1;
         $a =~ /U/ and $U = 1;
         $a =~ /M/ and $M = 1;
         $a =~ /A/ and $A = 1;
         $a =~ /R/ and $R = 1;
         $a =~ /C/ and $C = 1;
         $a =~ /P/ and $P = 1;
         $a =~ /T/ and $T = 1;
         $a =~ /Q/ and $Q = 1;
      }
      elsif( $a =~ /^!/ )  {
         $a =~ /!!/ and $U = $M = $A = $R = $C = $P = $T = $Q = 0;
         $a =~ /U/ and $U = 0;
         $a =~ /M/ and $M = 0;
         $a =~ /A/ and $A = 0;
         $a =~ /R/ and $R = 0;
         $a =~ /C/ and $C = 0;
         $a =~ /P/ and $P = 0;
         $a =~ /T/ and $T = 0;
         $a =~ /Q/ and $Q = 0;
      }
      else  {
         push @Files, $a;
      }
   }

   die "Mock mock mock. cvstat is useless unless some file status type is on. 
cvstat -h for help\n"
      if( $U + $M + $A + $R + $C + $P + $T + $Q == 0 );

   print "\n\t%%% WARNING: Fast-stat does not report files that need first 
checkout\n"  # not for lack of trying, tho.
      if( $FastStat and not $Quiet );

   if( $verbose )  {
      print "      up-to-date: ". ($U ? 'U' : '!U') . "\n";
      print "locally modified: ". ($M ? 'M' : '!M') . "\n";
      print "   locally added: ". ($A ? 'A' : '!A') . "\n";
      print " locally removed: ". ($R ? 'R' : '!R') . "\n";
      print "    needs merged: ". ($C ? 'C' : '!C') . "\n";
      print "     needs patch: ". ($P ? 'P' : '!P') . "\n";
      print "   unknown files: ". ($Q ? 'Q' : '!Q') . "\n";
   }

   if( $SF + $ST + $SS == 0 )  {
      $SF = 1;
   }
}

END  {
   print "Finished: ".(localtime)."\n"
      if $FinTime;
}


# For BLAZINGLY FAST MODE(tm)
sub GetEntries
{
   my( $Dir, $HRef ) = @_;
   my @Files;
   opendir DIR, $Dir
      or die "Cannot read dir $Dir: $!\n";
   ( @Files ) = grep /Entries/, readdir DIR;
   closedir DIR;

   foreach my $EFile( @Files )  {
      my $FH = new IO::File( "$Dir/$EFile" )
         or die "cannot read $Dir/$EFile: $!\n";
      while( <$FH> )   {
         next if /^D|^.\s+D/;           # dir recursion is handled in Go()
                                        # The second half of that regex should 
handle Entries.Log files.
         chomp;
         my( $File, $Date, $Tag);
         ( undef, $File, undef, $Date, undef, $Tag ) = split '/', $_;
         $Tag = "MAINLINE" unless $Tag;
         $Tag =~ s/^T//;
         $HRef->{$File} = [$Date,$Tag];
         print "\t$File -> $Date" if $verbose;
         print "   TAG: " . ( $Tag ? $Tag : '' ) . "\n" if $verbose;
      }
   }
}

# I knew a girl named Blaze in college. She might have spelled it differently.
sub BlazeGo
{
   my @Days = qw( Sun Mon Tue Wed Thu Fri Sat );
   my @Months = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
   my @List;
   my @Things = @_ ? @_ : ( '.' );

   $tag = 'MAINLINE' if( $tag and $tag eq 'none' );

   foreach my $Dir( @Things )  {
      my %EFiles;

      if( -d "$Dir/CVS" )  {
         print "CHECKING $Dir/CVS/Entries*\n" if $verbose;
         GetEntries( "$Dir/CVS", \%EFiles );
      }
      else  {
         print "NO CVS DIR HERE: $Dir\n" if $verbose;
         next;
      }

      my @Files;
      opendir DIR, $Dir
         or die "Cannot readdir $Dir; $!\n";
      ( @Files ) = readdir DIR;
      closedir DIR;

      foreach my $File( @Files )   {
         next if( -d "$Dir/$File" and $File =~ /^\.\.?/ );
         if( -d "$Dir/$File" )  {
            BlazeGo( "$Dir/$File" );
            next;
         }

         my( $D, $F );
         ( $D = $Dir ) =~ s/^\.\///;
         $F = $D ? "$D/$File" : $File;

         if( exists $EFiles{$File} )   {
            # Found a CVS file, check timestamp for modification
            # CVS Times are reported in GMT, wheras the localtime is indeed in 
the local
            #  timezone. Convert local time to gmtime.
            # The time string from the Entries file has a little different 
formatting, so I
            #  have to put the time together myself.

            my $ctime;
            my @gmtime = gmtime( (stat "$Dir/$File")[9]);

            $ctime = sprintf( "%s %s %02d %02d:%02d:%02d %04d",
                              $Days[$gmtime[6]], $Months[$gmtime[4]], 
$gmtime[3],
                              $gmtime[2], $gmtime[1], $gmtime[0], 1900 + 
$gmtime[5] );

            # different cvs's seem to form the datetime string differently. So, 
            # rip the cvs time apart and put it back together in the same 
format.
            # print STDOUT "$EFiles{$File}->[0]\n";
            my $cvtime;
            if( $EFiles{$File}->[0] =~ /Result|dummy/ )   {
               $cvtime = localtime();
            }
            else  {
               my( $w, $m, $d, $h, $mi, $s, $y ) = split /[\:\s]+/, 
$EFiles{$File}->[0];
               $cvtime = sprintf( "%s %s %02d %02d:%02d:%02d %04d",
                                  $w, $m, $d, $h, $mi, $s, $y );
            }

            print "\t\tCVS FILES LOCAL TIME: $File [$ctime] [$cvtime]]\n"
               if $verbose;

            $F =~ s/\.\///g;
            if( $ctime ne $cvtime )  {
               print STDOUT "\tLocally Modified\t\t$F\n";
               push @List, $F;
            }
            else  {
               #print "UP-TO-DATE: $F [$ctime][$cvtime]\n";
            }
            # Now check tags
            if( $tag and( $EFiles{$File}->[1] ne $tag ))  {
               print STDOUT "\tDIFFERENT TAG   \t\t$F [$tag != 
$EFiles{$File}->[1]]\n";
            }
         }
         else  {
            print STDOUT "\tUnknown File    \t\t$F\n" if $U;
         }
      }
   }
   print "@List\n" if $ShowList;
}


__DATA__
cvstat prints status and sticky tags from cvs.
        -r tag          report deviations from this tag (Also for Blazingly 
Fast Mode(tm) )
        -B              Blazingly Fast Mode(tm). It really is fast, ya know.
        -l              do not recurse
        -t|-tt          display start|finish time
        -c              hide tags column when not showing tags
        -L              display single-line file list at end for easy 
cut'n'paste (only files in stat list)
        -F              Fast-Stat. WARNING: Does not report "needs first 
checkout" files
        -q              quiet-mode. Hide warnings, only show errors.
        -o              diplay filename info only, no tags, no headers, no 
status.
        -z#             passed on to CVS. Sets the compression level on 
communications with the server.
        -i              Ignore CVSTAT_ARGS
        -b              Do not display the branch revision when +T
        -d cvsroot      set CVSROOT to something other than its current value
        -R              use CVSROOT env var with -d
        -s VAR=VAL      set some other cvs var to a value
        -st             sort by TAG
        -ss             sort by STATUS
        -sf             sort by FILENAME
        -h      display this help message
The following status options can be used in combinations
        !       turns off the following ...
        +       turns on the following ...
        U       display up-to-date files (default: Off)
        M       display locally modified files (default: On)
        A       display locally added files (default: On)
        R       display locally removed files (default: On)
        C       display files with conflicts (default: On)
        P       display files that need patch|checkout (default: On)
        T       display sticky tag info. (default: Off)
        Q       display files not in cvs repository (default: Off)
        !!      Turns off all everything. Useful first option.
        ++      Turns on everything. useful first option.
Example useful commands:
   cvstat +TU !ARCMP    displays only files up-to-date and the sticky tags
   cvstat +MARC !P      displays files you want to check in
   cvstat !! +Q         show only the unknown files
   cvstat ++ !M         show everything except locally modified files
When using -o option, only filenames are displayed (useful for shell scripts). 
Beware
  that all files matching the status options are displayed. When using -o is a 
good time
  to specify the status options.
If sorting is turned on, the whole process takes a little longer, even with -F
Command line args override CVSTAT_ARGS args

reply via email to

[Prev in Thread] Current Thread [Next in Thread]