The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Cvs::Command::Base;

use strict;
use Cwd;
use IPC::Run;
use base qw(Class::Accessor);

Cvs::Command::Base->mk_accessors
(qw(
    cvsroot command args cvs need_workdir
    initial_context result go_into_workdir
));

sub new
{
    my($proto, $cvs, @args) = @_;
    my $class = ref $proto || $proto;
    my $self = {};
    bless($self, $class);
    $self->cvs($cvs) or
      die "this shouldn't happend";
    $self->go_into_workdir(1);
    $self->need_workdir(1);
    return ($self->init(@args))[0];
}

sub init
{
    return shift;
}

sub workdir {shift->cvs->workdir()}

sub run
{
    my($self) = @_;
    my $debug = $self->cvs->debug();

    #
    # Preparing environment and parameters
    #
    my $old_pwd;
    if($self->need_workdir())
    {
        # this can append when the Cvs object is created without a
        # working directory, and a sub-command that need it is called
        return $self->err_result('no such working directory')
          unless defined $self->workdir();

        # keep current working directory
        $old_pwd = cwd();

        if($self->go_into_workdir())
        {
            print STDERR "** Chdir to: ", $self->cvs->working_directory(), "\n"
              if $debug;
            chdir($self->cvs->working_directory());
        }
        else
        {
            print STDERR "** Chdir to: ", $self->cvs->pwd(), "\n"
              if $debug;
            chdir($self->cvs->pwd());
        }
    }

    # getting sub-command
    my $sub_command = $self->command();
    unless(defined $sub_command)
    {
        if(defined $self->result())
        {
            # this command don't need to be ran
            return $self->result();
        }
        else
        {
            return $self->err_result('empty result, it\'s a bug !')
        }
    }
    # getting cvsroot
    my $cvsroot = $self->cvsroot() || $self->cvs->cvsroot();
    return $self->err_result('no such cvsroot')
      unless(defined $cvsroot);

    # getting context, if none we spawn one
    my $context = $self->initial_context();
    unless(defined $context)
    {
        $context = $self->new_context();
        $self->initial_context($context);
    }

    # bind cvsroot handlers to the context
    $cvsroot->bind($self);
    # bind common handlers
    $self->bind();

    my @command = ('cvs');
    push @command, '-f', '-d', $cvsroot->cvsroot();
    push @command, '-t' if $debug > 1;
    push @command, $sub_command, $self->args();

    #
    # Starting command
    #
    print(STDERR join(' ', '>>', @command), "\n")
      if $debug;
    my($in, $out) = ('', '');
    # pty is needed for login sub-command (and maybe for something
    # else) because it open pty for prompting the password :(
    my $h = IPC::Run::harness(\@command, \$in, '>pty>', \$out, '2>&1');
    $h->start();
    $self->{harness} = $h;


    #
    # Parsing command result
    #
    # It's not trivial to parse the cvs output, because the output may
    # stall, and we never be sure that the command as finish, if a
    # line is complete or if the command is waiting for input (like a
    # password).
    my($first, $last, $line, $match, $debugline);
    while(defined $context && $h->pump && length $out)
    {
        $first = 1;
        $match = 0;

        # flushing the send buffer
        $self->{data} = '';

        print STDERR "** new chunk\n"
          if $debug;

        while(defined $context && $out =~ /.*?(?:\r?\n|$)/g)
        {
            # my unperfect regexp match an empty line at end of
            # certain strings... skip it
            next unless length $&;

            $line = $&;
            $line =~ s/\r/\n/g;
            $line =~ s/\n+/\n/g;
            if($debug)
            {
                if($line =~ /^(?: |S)-> / or 
		  (defined $debugline and $debugline eq 'unterminated'))
                {
                    print STDERR $line;
                    # if the cvs debug line is truncated, try to not
                    # treat next parts as real cvs response component
                    $debugline = $line =~ /\n/ ? undef : 'unterminated';
                    undef $line;
                    next;
                }
                $debugline = $line;
                # make CR and LF visible
                $debugline =~ s/\r/\\r/g;
                $debugline =~ s/\n/\\n/g;
                print STDERR "<< $debugline\n";
            }

            # don't analyse empty lines, but $line have to be set
            next if $line =~ /^\n*$/;

            # Analysing the line: if a context is return, we replace
            # the current one with it to handling context
            # switching. If an undef value is returned, it's means
            # that no further analyse will be expected. The second
            # element is a boolean value which be true if the line
            # matched.
            ($context, $match) = $context->analyse($line);

            # this variable isn't relevant for others than first line,
            # see comments below
            undef($last) unless $first;

            if(not $match)
            {
                if(defined $last)
                {
                    # cvs sends its output in chunks and each chunk
                    # doesn't necessary finish at the end of the
                    # line. So we recover the last line of last chunk
                    # if it was unmatched by any rules and we join it
                    # with the first line of the current chunk if it
                    # wasn't match too, to see if it match more.
                    ($context, $match) = $context->analyse("$last$line");
                    if($debug)
                    {
                        my $un = $match ? '' : 'un';
                        print STDERR
                          "** ${un}matched recomposed line: $last$debugline\n";
                    }
                }
                else
                {
                    print STDERR "** unmatched line: $debugline\n"
                      if $debug;
                }
            }

            $first = 0;
        }
        # we don't want to parse several times the same thing
        $out = '';

        # keep the last line if it doesn't be used, it's maybe an
        # unterminated line. If line end with line-feed, this can't be
        # an unterminated line.
        if($match or not defined $line or $line =~ /\n$/)
        {
            undef($last);
        }
        elsif(length $line)
        {
            $last .= $line;
            print STDERR "** new \$last value: $last\n"
              if $debug;
        }

        # check out if some input want be send
        if(length $self->{data})
        {
            $in = $self->{data};
            print STDERR ">> $in\n"
              if $debug;
            # wait for all input to go
            $h->pump_nb while length $in;
        }
    }

    my $rv = $h->finish() || $?;

    #
    # Restoring/cleaning-up environment
    #
    # exec cleanup codes if any
    if(defined $self->{cleanup})
    {
        print STDERR "** Do some cleanup tasks\n"
          if $debug;
        &$_ for @{$self->{cleanup}};
    }
    # back to the old working directory if needed
    chdir($old_pwd) if $self->need_workdir();

    #
    # Returning the result
    #
    my $result = $self->result;
    # should not happened
    return $self->err_result('empty result, it\'s a bug !')
      unless defined $result;
    $result->success($rv)
      unless defined $result->success();
    return $result;
}

sub err_result
{
    my($self, $msg) = @_;

    my $result = $self->result();
    unless(defined $result)
    {
        $result = new Cvs::Result::Base;
        $self->result($result);
    }

    $result->success(0);
    $result->error($msg);
    return $result;
}

sub push_cleanup
{
    my($self, $code) = @_;
    push @{$self->{cleanup}}, $code;
}

sub restart
{
    my($self) = @_;
    # restart command
    $self->{harness}->finish();
    $self->{harness}->start();
}

sub send
{
    my($self, $data) = @_;
    if(defined $data)
    {
        $self->{data} .= $data;
    }
}

sub bind
{
    my($self) = @_;

    my $context = $self->initial_context();
    $context->push_handler
    (
     qr/^cvs \[.* aborted\]: (.+)$/, sub
     {
         $self->err_result(shift->[1]);
         return $context->finish();
     }
    );
}

sub default_params
{
    my($self, %param) = @_;

    foreach(keys %param)
    {
        $self->{param}->{$_} ||= $param{$_};
    }
}

sub param
{
    my($self, $param) = @_;

    if(defined $param && ref $param eq 'HASH')
    {
        foreach(keys %$param)
        {
            $self->{param}->{$_} = $param->{$_}
              if exists $param->{$_};
        }
    }
    return $self->{param} || {};
}

sub push_arg
{
    my($self, @args) = @_;
    push @{$self->{args}}, @args;
}

sub args
{
    my($self) = @_;
    return @{$self->{args}||[]};
}

sub new_context
{
    my($self) = @_;
    return Cvs::Command::Context->new();
}

sub error
{
    my($self, @msg) = @_;
    my $package = ref $self || $self;
    no strict 'refs';
    if(@msg)
    {
        ${$package."::ERROR"} = join(' ', @msg);
        return undef;
    }
    else
    {
        return ${$package."::ERROR"};
    }
}

package Cvs::Command::Context;

use strict;
use constant LAST => -1;
use constant FINISH => -2;
use constant CONTINUE => -3;
use constant RESCAN => -4;

sub new
{
    my($proto) = @_;
    my $class = ref $proto || $proto;
    my $self = {};
    $self->{rules} = [];
    bless($self, $class);
    return $self;
}

sub last {return -1}
sub finish {return -2}
sub continue {return -3}
sub catched {return shift->{catched}}

sub rescan_with
{
    my($self, $context) = @_;

    if(defined $context)
    {
        $self->{rescan_context} = $context;
        return RESCAN;
    }
    return $self->{rescan_context};
}

sub push_handler
{
    my($self, $pattern, $code, @args) = @_;
    push @{$self->{rules}}, [$pattern, $code, @args];
}

sub analyse
{
    my($self, $line) = @_;

    my $match = 0;
    foreach (@{$self->{rules}})
    {
        my($pattern, $code, @args) = @$_;
        if(my @match = $line =~ /$pattern/)
        {
            $match++;
            my $rv = &$code([$line, @match], @args);
            if(defined $rv)
            {
                if(ref $rv eq 'Cvs::Command::Context')
                {
                    # switching to another area
                    return($rv, $match);
                }
                elsif($rv eq $self->continue)
                {
                    next;
                }
                elsif($rv eq $self->finish)
                {
                    return(undef, $match);
                }
                elsif($rv eq RESCAN)
                {
                    my $context = $self->rescan_with();
                    if(defined $context)
                    {
                        return $context->analyse($line);
                    }
                }
            }
            # if last (default behavior)
            return($self, $match);
        }
    }

    return($self, $match);
}

1;
=pod

=head1 LICENCE

This library is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of the
License, or (at your option) any later version.

This library 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
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA

=head1 COPYRIGHT

Copyright (C) 2003 - Olivier Poitrey