#!/usr/local/perl-5.8.0/bin/perl -w
use strict;
use warnings;
use File::Find::Rule;
use Date::Parse qw( str2time );
use Text::Glob qw( glob_to_regex );
use File::Spec::Functions qw( catfile splitpath );
use File::Copy ();

our $VERSION = 0.02;

=head1 NAME

cvn - a unified wrapper around cvs and svn

=head1 SYNOPSIS

cvn [cvs|svn command here]

=head1 DESCRIPTION

C<cvn> at its simplest provides a way to automatically invoke either
the svn of cvs binary, depending on whether the current working
directory is held under CVS or Subversion.

It also simulates some commands in the cases (eg cvs doesn't support
C<st> or offline diffing) where one of the apps is deficient (where
possible)

=head1 Notes on simulated commands

=cut

sub get_cvs_entries {
    my %entries;
    for ( find( directory => name => 'CVS', in => '.' ) ) {
        my $dir = (splitpath $_)[1];
        open my $fh, "$_/Entries" or next;
        while (<$fh>) {
            next if /^D/;
            chomp;
            my %field;
            @field{qw( foo name version modified bar baz )} = split /\//, $_;
            $field{mtime} = str2time $field{modified}, "UTC";
            my $filename = $dir ? catfile($dir, $field{name}) : $field{name};
            $field{path} = $filename;
            $entries{ $filename } = \%field;
        }
    }
    return %entries;
}

sub get_cvs_ignores {
    my @ignored;
    for my $ignore ( find( file => name => '.cvsignore', in => '.' ) ) {
        my $dir = (splitpath $_)[1];
        open my $fh, "$ignore" or next;
        while (<$fh>) {
            chomp;
            my $filename = $dir ? catfile($dir, $_) : $_;
            push @ignored, $filename;
        }
    }
    return @ignored;
}

sub non_cvs_files {
    find( or => [ find( directory =>
                        name => 'CVS',
                        prune =>
                        discard =>),
                  find( file => ),
                ]
        );
}

my %simulated;

=head2 C<st>

simulated under CVS by comparing the server-modified date in
CVS/Entries with the mtime of the file(s)

=cut

$simulated{cvs}{st} = sub {
    if (@_ && $_[0] eq '-v') {
        print "$0: can't emulate st -v under CVS\n";
        exit 1;
    }
    my %entries = get_cvs_entries;
    for my $file ( non_cvs_files->in('.') ) {
        # TODO honour .cvsignore
        !$entries{ $file }
          and do { print "? $file\n"; next };

        (stat $file)[9] > $entries{ $file }{mtime}
          and do { print "M $file\n"; next };
    }
    return 0;
};


=head2 C<up>

extended for CVS to keep texts in CVS/text_$file_$rev

=cut

sub _text_name {
    our %entry;
    local *entry = shift;
    my $dir = (splitpath $entry{path})[1];
    ($dir ? "$dir/" : "") . "CVS/text_$entry{name}_$entry{version}";
}

$simulated{cvs}{up} = sub {
    system 'cvs', 'up', @_;
    my %entries = get_cvs_entries;
    for my $file ( keys %entries ) {
        our %entry;
        local *entry = $entries{ $file };
        my $text = _text_name( \%entry );
        next if -e $text;

        if ((stat $file)[9] > $entry{mtime}) {
            # we seem to have local mods, pull from the repository
            `cvs up -p -r $entry{version} $file > $text 2> /dev/null`;
        }
        else {
            File::Copy::copy($file, $text);
        }
        # touch it back
        utime $entry{mtime}, $entry{mtime}, $text;
    }
};


=head2 C<diff>

extended for CVS to attempt to use the locally cached text(s)

=cut

$simulated{cvs}{diff} = sub {
    my %entries = get_cvs_entries;
    for my $file (@_ ? @_ : non_cvs_files->in('.')) {
        next unless $entries{$file};
        my $text = _text_name( $entries{$file} );
        if (-e $text) {
            system 'diff', '-u', $text, $file;
        }
        else {
            system 'cvs', 'diff', '-u', $file;
        }
    }
};


=head2 C<revert>

extended for CVS to attempt to use the locally cached text(s)

=cut

$simulated{cvs}{revert} = sub {
    my %entries = get_cvs_entries;
    for my $file (@_ ? @_ : non_cvs_files->in('.')) {
        next unless $entries{$file};
        print "Reverting: $file\n";
        our %entry; local *entry = $entries{$file};
        my $text = _text_name( \%entry );
        if (-e $text) {
            File::Copy::copy($text, $file);
        }
        else {
            `cvs up -p -r $entry{version} $file > $file`;
        }
        # touch it back so we know it's good
        utime $entry{mtime}, $entry{mtime}, $file;
        print "$file reverted\n";
    }
};

=head2 C<version>

simulated in both to return the version of the cvn binary

=cut

$simulated{all}{version} = sub {
    print "cvn version $VERSION\n";
    return 0;
};

sub simulate {
    my $app = shift;
    my $cmd = shift;
    my $sub = $simulated{ $app || '' }{ $cmd }
      ||      $simulated{ 'all' }{ $cmd }
      || return;
    exit $sub->(@_);
}

my $command;
$command ||= 'svn' if -d '.svn';
$command ||= 'cvs' if -d 'CVS';

simulate $command, @ARGV;

unless ( $command ) {
    print "svn: current directory isn't under vcs\n";
    exit 1;
}
exec     $command, @ARGV;

=head1 TODO

honour .cvsignore for CVS C<st>

improve parameter parsing

improve documentation

be a little more paranoid about invoking commands

=head1 AUTHOR

Richard Clamp <richardc@unixbeard.net>

=head1 COPYRIGHT

Copyright (C) 2002 Richard Clamp.  All Rights Reserved.

This module is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut
