#!/usr/bin/perl

use POSIX qw(mktime ctime);
use Time::Local qw( timegm );

# Offline check for status of files in a checked-out
# CVS module. 
# Dirk Mueller <mueller@kde.org> Oct 2001

# based on cvschanged by
# Sirtaj Singh Kang <taj@kde.org> Nov 1998.

if ( defined $ARGV[0] && $ARGV[0] eq "--help") {
  print "cvscheck (c) 2001 Dirk Mueller <mueller\@kde.org>\n\nUsage:\n";
  print "   cvscheck <dir>\n\n";
  print "Prints information about the status of your local CVS checkout without\n";
  print "communicating with the server (therefore in speed only limited by your\n";
  print "hard-disk throughput, much unlike cvs -n up).\n\n";
  print "Every file is printed with a status character in front of its name:\n";
  print "? foobar.c   file is not known to CVS - maybe you should add it?\n";
  print "M foobar.c   file is for sure locally modified.\n";
  print "m foobar.c   file *might* have local changes (needs a diff with the server).\n";
  print "C foobar.c   file has a CVS conflict and therefore cannot be committed.\n";
  print "U foobar.c   file is in CVS but its somehow missing in your local checkout.\n";
  print "T foobar.c   file has an unusual sticky CVS tag.\n";
  print "A foobar.c   you cvs add'ed this file but did not yet commit.\n";
  print "R foobar.c   you cvs rm'ed this file but did not yet commit.\n";
 
  exit;
}

# default is HEAD
$standardtag = "";
@dirqueue = @ARGV;
@merged = ();
@uncommitted = ();
@missing = ();
@tagged = ();
@removed = ();
@unknown = ();
@modified = ();
@conflicts = ();

@monthlist = ( "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
 "Sep", "Oct", "Nov", "Dec" );
%months = ();

# convert text stamp to GMT
sub strToTime
{
	my( $timestr ) = @_;

	if( ! ($timestr =~ 
		/^(\w+)\s*(\w+)\s*(\d+)\s*(\d+):(\d+):(\d+)\s*(\d+)/) ) {

		return -1;
	}

	# CVS timestamps are in GMT.

	my( $tm ) = timegm( $6, $5, $4, $3, $months{ $2 }, $7 - 1900);

	return $tm;
}

sub processEntries
{
	my ( $dir ) = @_;
        my %dirunknown = ();

        opendir (DIR, "$dir") || warn "Couldn't read '$dir'";
        # first assume all are unknown
        while ( $e = readdir(DIR) ) {
          next if ($e eq ".");
          next if ($e eq "..");
          next if ($e eq "RCS");
          next if ($e eq "SCCS");
          next if ($e eq "CVS");
          next if ($e eq "CVS.adm");
          next if ($e eq "RCSLOG");
          next if ($e eq "tags");
          next if ($e eq "TAGS");
          next if ($e eq ".make.state");
          next if ($e eq ".nse_depinfo");
          next if ($e eq "core");
          next if ($e eq ".libs");
          next if ($e eq ".deps");
          next if ($e =~ /^.+~$/);
          next if ($e =~ /^\#.+$/);
          next if ($e =~ /^\.\#.+$/);
          next if ($e =~ /^,.+$/);
          next if ($e =~ /^_\$.+$/);
          next if ($e =~ /^.+\$$/);
          next if ($e =~ /^.+\.old$/);
          next if ($e =~ /^.+\.bak$/);
          next if ($e =~ /^.+\.BAK$/);
          next if ($e =~ /^.+\.orig$/);
          next if ($e =~ /^.+\.rej$/);
          next if ($e =~ /^\.del-.+$/);
          next if ($e =~ /^.+\.a$/);
          next if ($e =~ /^.+\.olb$/);
          next if ($e =~ /^.+\.o$/);
          next if ($e =~ /^.*\.obj$/);
          next if ($e =~ /^.+\.so$/);
          next if ($e =~ /^.+\.Z$/);
          next if ($e =~ /^.+\.elc$/);
          next if ($e =~ /^.+\.ln$/);
          next if ($e =~ /^cvslog\..*$/);

          # kde specific entries
          # TODO read from CVSROOT/cvsignore !
          next if ($e eq "config.cache");
          next if ($e eq "config.log");
          next if ($e eq "config.status");
          next if ($e eq "index.cache.bz2");
          next if ($e eq ".memdump");
          next if ($e eq "autom4te.cache");
          next if ($e eq "autom4te.cache");
	  next if ($e eq "Makefile.rules.in");
          next if ($e =~ /^.*\.moc$/);
          next if ($e =~ /^.+\.gmo$/);
          next if ($e =~ /^.+\.moc\.[^\.]+$/);
          next if ($e =~ /^.+\.lo$/);
          next if ($e =~ /^.+\.la$/);
          next if ($e =~ /^.+\.rpo$/);
          next if ($e =~ /^.+\.closure$/);
          next if ($e =~ /^.+\.all_cpp\.cpp$/);
          next if ($e =~ /^.+\.all_C\.C$/);
          next if ($e =~ /^.+\.all_cc\.cc$/);
          next if ($e =~ /^.+_meta_unload\.[^\.]+$/);
          next if ($e =~ /^.+\.kidl$/);
          next if ($e =~ /^.+_skel\.[^\.]+$/);
	  next if ($e eq "Makefile.rules.in");

          $dirunknown{$e} = 1;
        }
        closedir(DIR);
        if( open(CVSIGNORE, $dir."/.cvsignore") ) {
          while(<CVSIGNORE>) {
            next if (! /^(\S+)\s*$/ );
            my $entry = $1;
            if ($entry =~ /[\*\?]/) {
              my $pattern = quotemeta $entry;
              $pattern =~ s/\\\*/.*/g;
              $pattern =~ s/\\\?/./g;
              foreach $m (keys (%dirunknown)) {
                $dirunknown{$m} = 0 if ($m =~ /^$pattern$/);
              }
              next;
            }
            $dirunknown{$entry} = 0;
          }
          close(CVSIGNORE);
        }

	if ( !open( ENTRIES, $dir."/CVS/Entries" ) ) {
          print "I CVS/Entries missing in $dir\n";
          return;
        }
        my $oldstandardtag = $standardtag;
        my $staginfo = "";
        if( open(CVSTAG, $dir."/CVS/Tag" ) ) {
          my $line = <CVSTAG>;
          if($line =~ /^[TN](.+)$/) {
            $standardtag = $1;
            $staginfo = $1;
          }
          else {
            # something with D - assume HEAD
            $oldstandardtag = $standardtag = ""; # its HEAD
            print "I $dir has sticky date: $line\n";
          }
          close(CVSTAG);
        }
        else {
          $standardtag = ""; # its HEAD
          $staginfo = "(HEAD)";
        }
        print "I $dir has sticky tag $staginfo\n" if($standardtag ne $oldstandardtag);
	while( <ENTRIES> ) {
          if ( m#^\s*D/([^/]+)/# ) {
               push ( @dirqueue, "$dir/$1" ) if (-d "$dir/$1");
               $dirunknown{$1} = 0;
               next;
            }

          next if !m#^\s*/([^/]+)/([-]*[\d\.]*)/([^/]+)/([^/]*)/(\S*)$#;
          $fname = $1;
          $ver = $2;
          $stamp = $3;
          $options = $4;
          $tag = $5;
          $tag = $1 if ($tag =~ /^T(.+)$/);

          $dirunknown{$fname} = 0;

          my $taginfo="";
          if ( $tag ne $standardtag ) {
            if ($tag eq "") {
              $taginfo = " (HEAD)";
            }
            else {
              $taginfo = " ($tag)";
            }
          }
          if ($options =~ /^\-k(.)$/) {
            $taginfo .= " (no RCS-tags)" if($1 eq "o" or $1 eq "b");
            $taginfo .= " (RCS values only)" if($1 eq "v");
            $taginfo .= " (RCS keywords only)" if($1 eq "k");
          }
          my $state = $stamp;
          if( $stamp =~ m(^(.+)\+(.+)$) ) {
            $state = $1;
            $stamp = $2;
          }
          if ( $state =~ /merge/ ) {
            # modified version merged with update from server
            # check for a conflict
            if ( open (F, "$dir/$fname") ) {
              my @conflict = grep /^<<<<<<</, <F>;
              close (F);
              if( @conflict ) {
                push @conflicts, "$dir/$fname$taginfo";
                next;
              }
            } 
            else {
              push @missing, "$dir/$fname$taginfo";
              next;
            }
          }
          if ( $ver =~ /^\-.*/ ) {
            push @removed, "$dir/$fname$taginfo";
            next;
          }
          $mtm = strToTime( $stamp );
          if( $mtm < 0 ) {
            if ( $ver eq "0" ) {
              push @uncommitted, "$dir/$fname$taginfo";
            }
            else {
              push @merged, "$dir/$fname$taginfo";
            }
            next;
          }
          @sparams = lstat( "$dir/$fname" );

          if ( $#sparams < 0 ) {
            push @missing, "$dir/$fname$taginfo";
            next;
          }
          if( $mtm < $sparams[ 9 ] ) {
            push @modified, "$dir/$fname$taginfo";
            next;
          }
          if ( $tag ne $standardtag ) {
            push @tagged, "$dir/$fname$taginfo";
          }
	}
	close( ENTRIES );

        my @unknownlist = sort keys (%dirunknown);
        foreach $entry (@unknownlist) {
          next if ($dirunknown{$entry} == 0);
          # ignore unusual files
          next if (-l "$dir/$entry" );
          # ifnore if its a directory in CVS
          next if (-d "$dir/$entry" and -d "$dir/$entry/CVS");
          push @unknown, "$dir/$entry";
        }
}

# month assoc array for name -> index lookups
$mctr = 0;

foreach $month ( @monthlist ) {
	$months{ $month } = $mctr;
	$mctr++;
}

# Try current directory if none specified

if( $#dirqueue < 0 ) {
	push( @dirqueue, "." );
}

# process directory queue
foreach $dir ( @dirqueue ) {
	processEntries( $dir );
}

foreach $f ( @unknown ) {
  $f =~ s/^\.\///;
  print "? $f\n";
}

foreach $f( @modified ) {
  $f =~ s/^\.\///;
  print "M $f\n";
}

foreach $f ( @missing ) {
    $f =~ s/^\.\///;
    print "U $f\n";
}

foreach $f ( @merged ) {
    $f =~ s/^\.\///;
    print "m $f\n";
}

foreach $f ( @tagged ) {
   $f =~ s/^\.\///;
   print "T $f\n";
}

foreach $f ( @uncommitted ) {
    $f =~ s/^\.\///;
    print "A $f\n";
}

foreach $f ( @removed ) {
    $f =~ s/^\.\///;
    print "R $f\n";
}

foreach $f ( @conflicts ) {
    $f =~ s/^\.\///;
    print "C $f\n";
}


=head1 NAME

cvscheck -- Lists all files in checked out CVS modules that have been
edited or changed locally. No connection is required to the CVS server,
therefore being extremely fast. 

=head1 AUTHOR

Dirk Mueller <mueller@kde.org>
based on cvschanged by Sirtaj Singh Kang <taj@kde.org>

=cut