#!/usr/bin/perl
# (c) 2003 Alex Fritze <alex@croczilla.com>
#
# cvslstat version 1.0

use POSIX;

sub usage {
  print <<EOF;
'cvslstat' operates in the working directory of a CVS repository 
and lists files that have been modified or have conflicts.
Similar to 'cvs status', but doesn't query the repository.

Usage: cvslstat [OPTION]...

  -l          process this directory only (not recursive)
  -q          quiet mode: print only filenames, no status info
  -c          report files with conflicts (== '-u' & '-e')
  -u          report files with conflicts that have not been edited yet
  -e          report files with conflicts that have been locally edited
  -m          report files that are locally modified
  -v          verbose mode: print cvs timestamps (unless -q is specified)
  -vv         debug mode: print cvs timstamps & file timestamps
  -d          print directories traversed
      --help  display this message

  If neither '-c', '-u', '-e' or '-m' are given, cvslstat will execute 
  as if '-c' and '-m' had been specified.

Known bugs: 
  Under Windows, the CVS timestamp and file modification time are
  sometimes out by a second. In that case, cvslstat will wrongly
  report the file as modified. (This is with CVS 1.10.5; might be fixed
  by later versions.)

Quirks:
  Command line options have to be provided one-by-one, i.e. use

     cvslstat -c -v

  instead of

     cvslstat -cv

Examples:
  Find all files with conflicts: 

     cvslstat -c

  Open all files with unedited conflicts in xemacs: 

     xemacs `cvslstat -u -q`

  Commit all locally edited files: 

     cvs commit `cvslstat -m -q`

EOF
  exit;
}

#----------------------------------------------------------------------
# timestamp_eq : compare 2 timestamps for equality

sub timestamp_eq {
  my ($x,$y)=@_;
  # we can't just do string comparsion, because some Windows CVS
  # versions seem to store the date with leading 0's. Hopefully
  # that's the only difference to asctime...

  # normalize both timestamps by converting all 0's to spaces:
  $x =~ s/0/\ /g;
  $y =~ s/0/\ /g;

  return ($x eq $y);
}

#----------------------------------------------------------------------
# process_dir : process the given directory

sub process_dir {
  my $dir=$_[0];
  my (%conflict, %modified, %subdir, %unresolved);
  open(ENTRIES, $dir . "/CVS/Entries")
    or die "Need a CVS source repository! Can't open Entries file in $dir: $!\n";
  while (<ENTRIES>) {
    if (/^\/(.*)\/(.*)\/(.+)\/(.*)\/(.*)/) {
      my $filename = $1;
      my $timestamp = $3;
      my $mtime = asctime(gmtime((stat($dir . "/" . $filename))[9]));
      chomp $mtime;
      if ($timestamp =~ /(.+)\+(.+)/) {
        if ($edited || $unedited) {
          $conflict{$filename} = $1;
          if (timestamp_eq($mtime,$2)) {
            if ($unedited) {
              $unresolved{$filename} = 1;
            }
            else {
              # we don't want files that have been edited:
              delete($conflict{$filename});
            }
          }
          elsif (!$edited) {
            # we only want files that have not been resolved:
            delete($conflict{$filename});
          }
        }
      }
      elsif ($modifieds && (!timestamp_eq($mtime,$timestamp))) {
        $modified{$filename} = $timestamp;
      }
    }
    elsif (/^D\/([^\/]*)\//) {
      $subdir{$1} = 1;
    }
  }
  close(ENTRIES);

  open(LOG, $dir . "/CVS/Entries.Log"); # fail silently; file is optional
  while (<LOG>) {
    if (/^A D\/([^\/]*)\//) {
      $subdir{$1} = 1;
    }
    elsif (/^R D\/([^\/]*)\//) {
      delete($subdir{$1});
    }
    elsif (/^[AR]/) { die "unknown entry in Entries.Log!" }
  }
  close(LOG);

  if ($directories) {
    print "* $dir:\n";
  }

  while (($file, $description) = each (%conflict)) {
    print "$dir/$file";
    if (!$quiet) {
      print " had a merge conflict";
      if ($verbose) {
        print " (cvs timestamp = '$description'";
        if ($veryverbose) {
          my $mtime = asctime(gmtime((stat($dir . "/" . $file))[9]));
          chomp $mtime;
          print "!='$mtime'";
        }
        print ")";
      }
      if ($unresolved{$file}) {
        print " and has not been edited";
      }
      else {
        print " but has since been editied";
      }
    }
    print "\n";
  }

  while (($file, $description) = each (%modified)) {
    print "$dir/$file";
    if (!$quiet) {
      print " is locally modified";
      if ($verbose) {
        print " (cvs timestamp = '$description'";
        if ($veryverbose) {
          my $mtime = asctime(gmtime((stat($dir . "/" . $file))[9]));
          chomp $mtime;
          print "!='$mtime'";
        }
        print ")";
      }
    }
    print "\n";
  }

  # now process subdirs if requested:
  if (!$local) {
    foreach $key (keys %subdir) {
      process_dir($dir . "/" . $key);
    }
  }
}

# ----------------------------------------------------------------------
# main

# parse options:
while ($_ = $ARGV[0], /^-/) {
  shift;
  if (/^--help$/) {
    usage;
  }
  elsif (/^-l$/) {
    $local = 1;
  }
  elsif (/^-q$/) {
    $quiet = 1;
  }
  elsif (/^-c$/) {
    $unedited = 1;
    $edited = 1;
  }
  elsif (/^-u$/) {
    $unedited = 1;
  }
  elsif (/^-e$/) {
    $edited = 1;
  }
  elsif (/^-m$/) {
    $modifieds = 1;
  }
  elsif (/^-v$/) {
    $verbose = 1;
  }
  elsif (/^-vv$/) {
    $verbose = 1;
    $veryverbose = 1;
  }
  elsif (/^-d$/) {
    $directories = 1;
  }
  else {
    usage;
  }
}

if (!$edited && !$unedited && !$modifieds) {
  $edited = 1;
  $unedited = 1;
  $modifieds = 1;
}

if ($ARGV[0]) {
  usage;
}

# walk the CVS directories:
process_dir(".");
