#*************************************************************************
#
#   OpenOffice.org - a multi-platform office productivity suite
#
#   $RCSfile: Cvs.pm,v $
#
#   $Revision: 1.25 $
#
#   last change: $Author: rt $ $Date: 2007-05-03 16:37:57 $
#
#   The Contents of this file are made available subject to
#   the terms of GNU Lesser General Public License Version 2.1.
#
#
#     GNU Lesser General Public License Version 2.1
#     =============================================
#     Copyright 2005 by Sun Microsystems, Inc.
#     901 San Antonio Road, Palo Alto, CA 94303, USA
#
#     This library is free software; you can redistribute it and/or
#     modify it under the terms of the GNU Lesser General Public
#     License version 2.1, as published by the Free Software Foundation.
#
#     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
#
#*************************************************************************

#
# Cvs.pm - package for manipulating CVS archives
#

package Cvs;
use strict;

use Carp;
use CwsConfig;

##### constructor ####

sub new
{
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self = {};
    $self->{NAME} = undef;
    $self->{HEAD} = undef;
    $self->{FLAGS} = undef;
    my $conf = CwsConfig::get_config();
    if ( $conf->cvs_binary() ) {
        $self->{CVS_BINARY} = $conf->cvs_binary();
    }
    else {
        if ($^O eq "MSWin32" ) {
            $self->{CVS_BINARY} = "cvsclt2.exe";
        }
        else {
            $self->{CVS_BINARY} = "cvs.clt2";
        }
    }
    $self->{ARCHIVE_PATH} = undef;
    $self->{REPOSITORY_REV} = undef;
    $self->{REV_DATA} = {};
    $self->{REV_SORTED} = [];
    $self->{REV_TAGS} = {};
    $self->{TAGS} = {};
    $self->{"_PARSED"} = undef;
    $self->{"_SORTED"} = undef;
    $self->{"_TAGGED"} = undef;
    bless ($self, $class);
    return $self;
}

#### methods to access per object data ####

sub name
{
    my $self = shift;
    if ( @_ ) {
        $self->{NAME} = shift;
    }
    return $self->{NAME};
}

sub cvs_binary
{
    my $self = shift;
    if ( @_ ) {
        $self->{CVS_BINARY} = shift;
    }
    return $self->{CVS_BINARY};
}

sub get_data_by_rev
{
    my $self = shift;
    $self->parse_log();

    return $self->{REV_DATA};
}

sub get_sorted_revs
{
    my $self = shift;

    if ( $self->{"_SORTED"} ) {
        return $self->{REV_SORTED};
    }

    $self->parse_log();

    sub by_rev {
        # comparison function for sorting
        my (@field_a, @field_b, $min_field, $i);

        @field_a = split /\./, $a;
        @field_b = split /\./, $b;
        $min_field = ($#field_a > $#field_b) ? $#field_b : $#field_a;

        for ($i = 0; $i <= $min_field; $i++) {
            if ( ($field_a[$i] < $field_b[$i]) ) {
                return -1;
            }
            if ( ($field_a[$i] > $field_b[$i]) ) {
                return 1;
            }
        }

        if ( $#field_a == $#field_b ) {
            return 0;
        }
        # eg. 1.70 sorts before 1.70.1.0
        ($#field_a < $#field_b) ? return -1 : return 1;
    }

    @{$self->{REV_SORTED}} = sort by_rev (keys %{$self->{REV_DATA}});
    $self->{"_SORTED"} = 1;
    return $self->{REV_SORTED};
}

sub get_tags_by_rev
{
    my $self = shift;
    my ($tag, $rev);

    if ( $self->{"_TAGGED"} ) {
        return $self->{REV_TAGS};
    }

    $self->parse_log();
    foreach $tag (keys %{$self->{TAGS}}) {
        $rev = $self->{TAGS}->{$tag};
        push (@{$self->{REV_TAGS}->{$rev}}, $tag);
    }

    $self->{"_TAGGED"} = 1;
    return $self->{REV_TAGS};
}

sub get_flags
{
    my $self = shift;
    $self->parse_log();

    return $self->{FLAGS};
}

sub get_tags
{
    my $self = shift;

    $self->parse_log();
    return $self->{TAGS};
}

sub get_head
{
    my $self = shift;

    $self->parse_log();
    return $self->{HEAD};
}

sub get_repository_rev
{
    my $self = shift;

    if( !$self->{REPOSITORY_REV} ) {
        # ignore return values
        $self->status();
    }
    return $self->{REPOSITORY_REV};
}

sub get_archive_path
{
    my $self = shift;

    if( !$self->{ARCHIVE_PATH} ) {
        # ignore return values
        $self->status();
    }
    return $self->{ARCHIVE_PATH};
}

sub is_tag
{
    my $self = shift;
    my $tag  = shift;

    my $tags_ref = $self->get_tags();
    return (defined $$tags_ref{$tag}) ? 1 : 0;
}

# Check if $label is branch label and returns revision.
sub get_branch_rev
{
    my $self  = shift;
    my $label = shift;

    return 0 if $label eq '';
    my $tags_ref = $self->get_tags();
    my $rev = $$tags_ref{$label};
    return 0 if !defined($rev);
    my @field = split('\.', $rev);
    # $label is a branch label if rev is of form (...)x.y.0.z
    return 0 if $field[-2] != 0;
    $field[-2] = $field[-1];
    # remove last
    pop @field;
    return join('.', @field);
}

sub get_latest_rev_on_branch
{
    my $self  = shift;
    my $label = shift;

    my $branch_rev = $self->get_branch_rev($label);
    return 0 if !$branch_rev;

    my $latest_rev_on_branch = 0;

    foreach ( @{$self->get_sorted_revs()} ) {
        if ( $_ =~ /^$branch_rev\.(\d+)$/ ) {
            $latest_rev_on_branch = $_;
        }
    }

    # No revision has ever been commited on this branch,
    # return branch root.
    if ( !$latest_rev_on_branch ) {
        $branch_rev =~ /^(.*)\.(\d+)$/;
        $latest_rev_on_branch = $1;
    }
    return $latest_rev_on_branch;
}


#### methods to manipulate archive ####

# Delete a revision. Use with care.
sub delete_rev
{
    my $self = shift;
    my $rev = shift;
    my $file = $self->name();

    my $response_ref = $self->execute("admin -o$rev $file");
    foreach ( @{$response_ref} ) {
        /deleting revision $rev/ && return 1;
    }
    return 0;
}

# Update archive with options $options. Returns 'success' and new revision
# on success or reason of failure. If no update happens because file was
# up-to-date consider operation a success.
sub update
{
    my $self = shift;
    my $options = shift;

    my $file = $self->name();
    my $response_ref = $self->execute("update $options $file");
    my $conflict          = 0;
    my $notknown          = 0;
    my $connectionfailure = 0;
    foreach ( @{$response_ref} ) {
        /conflicts during merge/ && ++$conflict;
        /nothing known about/ && ++$notknown;
        /\[update aborted\]: connect to/ && ++$connectionfailure;
    }
    if ( $conflict || $notknown || $connectionfailure) {
        my $failure = 'unknownfailure';
        $failure = 'conflict' if $conflict;
        $failure = 'notknown' if $notknown;
        $failure = 'connectionfailure' if $connectionfailure;
        return $failure;
    }
    return 'success';
}

# Commit $file with option $option; return 'success' or reason for failure.
# If 'success' return the new revision as second element.
sub commit
{
    my $self = shift;
    my $options = shift;

    my $file = $self->name();
    my $response_ref = $self->execute("commit $options $file");

    # already commited ?
    return 'nothingcommitted' if !@{$response_ref};

    my $conflict           = 0;
    my $uptodate           = 0;
    my $notknown           = 0;
    my $success            = 0;
    my $connectionfailure = 0;
    my $new_revision = undef;
    foreach ( @{$response_ref} ) {
        /Up-to-date check failed/ && ++$uptodate;
        /nothing known about/ && ++$notknown;
        /had a conflict and has not been modified/ && ++$conflict;
        /new revision: (delete);/ && (++$success, $new_revision = $1);
        /new revision: ([\d\.]+);/ && (++$success, $new_revision = $1);
        /\[commit aborted\]: connect to/ && ++$connectionfailure;
    }
    if ( !$success ) {
        my $failure = 'unknownfailure';
        $failure = 'conflict'          if $conflict;
        $failure = 'notuptodate'       if $uptodate;
        $failure = 'notknown'          if $notknown;
        $failure = 'connectionfailure' if $connectionfailure;
        return $failure;
    }
    return wantarray ? ('success', $new_revision) : 'success';
}

# Tag file with specified tag. Options may be specified,
# '-b' for a branch tag and -F for forced tag are valid options.
# '-B' to force moving existing tag also is valid.
# Retagging without moving the tag is considered a succesful
# operation.
sub tag
{
    my $self    = shift;
    my $tag     = shift;
    my $options = shift;

    return 'invalidtag' if !$tag;
    # check for valid options
    if ( $options ) {
        my @elem = split(' ', $options);
        foreach (@elem) {
            unless ( /-B/ || /^-F/ || /-b/ ) {
                return 'invalidoption';
            }
            $options = join(' ', @elem);
        }
    }
    else {
        $options = '';
    }

    my $file = $self->name();
    my $response_ref = $self->execute("tag $options $tag $file");

    unless ( $options =~ /-F/ && $options =~ /-b/ ) {
        # No message from CVS means that tag already exists
        # and has not been moved.
        # If both -F and -b is given, CVS will always return
        # message.
        return 'success' if !@{$response_ref};
    }

    my $tagged             = 0;
    my $cant_move          = 0;
    my $connectionfailure  = 0;
    my $invalidfile        = 0;
    foreach ( @{$response_ref} ) {
        /^T \Q$file\E/ && ++$tagged;
        /NOT MOVING tag/ && ++$cant_move;
        /nothing known about/ && ++$invalidfile;
        /\[tag aborted\]: connect to/ && ++$connectionfailure;
    }
    return 'success'           if $tagged;
    return 'cantmove'          if $cant_move;
    return 'connectionfailure' if $connectionfailure;
    return 'invalidfile'       if $invalidfile;
    # should never happen
    return 'unknownfailure';
}

#### misc operations ####

# Return status information. Note that this is somewhat redundant with
# the information which can be retrieved from the log, but in some cases
# we can avoid the more expansive parsing of the log by calling this method.
# We don't save the status information between calls.
sub status
{
    my $self       = shift;

    my $file = $self->name();
    my ($nofile, $unknownfailure, $connectionfailure);
    my ($status, $working_rev);
    my ($sticky_tag, $branch, $sticky_date, $sticky_options);

    my $response_ref = $self->execute("status $file");
    foreach ( @{$response_ref} ) {
        chomp();
        /File: no file/ && ++$nofile;
        /Status:\s+([\w\-\s]+)$/ && ($status = $1);
        /Working revision:\s+((\d|\.)+)/ && ($working_rev = $1);
        /Repository revision:\s+((\d|\.)+)\s+(\S+)/ && ($self->{REPOSITORY_REV} = $1) && ($self->{ARCHIVE_PATH} = $3);
        /Sticky Tag:\s+(.+)/ && ($sticky_tag = $1);
        /Sticky Date:\s+(.+)/ && ($sticky_date = $1);
        /Sticky Options:\s+(.+)/ && ($sticky_options = $1);
        /\[status aborted\]: connect to/ && ++$connectionfailure;
    }

    return 'connectionfailure' if $connectionfailure;
    # all variables except $status will contain garbage if 'Locally Added'
    # or 'Unknown'
    return $status if ($status eq 'Locally Added' || $status eq 'Unknown');
    # same if $nofile is set
    return $status if $nofile;

    if ( $sticky_tag =~ /([\w\-]+) \(branch: ([\d\.]+)\)$/ ) {
        $sticky_tag = $1;
        $branch = $2;
    }

    $sticky_date = '' if $sticky_date eq '(none)';
    $sticky_options = '' if $sticky_options eq '(none)';

    if ( $sticky_options =~ /\-(\w+)/ ) {
        $sticky_options = $1;
    }

    $unknownfailure++ if !$status;

    return 'unknownerror' if $unknownfailure;
    return ($status, $working_rev, $self->{REPOSITORY_REV}, $sticky_tag, $branch,
            $sticky_date, $sticky_options);
}

# Return a diff between two revision of an archive.
sub diff
{
    my $self       = shift;
    my $rev1       = shift;
    my $rev2       = shift;
    my $options    = shift || '';

    my $file = $self->name();
    my ($nofile, $unknowntagfailure, $unknownrevfailure, $connectionfailure);

    my $response_ref = $self->execute("diff $options -r$rev1 -r$rev2 $file");

    foreach ( @{$response_ref} ){
        /\[diff aborted\]: connect to/ && ++$connectionfailure;
        /cvs \[server aborted\]: no such tag \w+/ && ++$unknowntagfailure;
        /cvs server: tag [\d\.]+ is not in file $file/ && ++$unknownrevfailure;
    }

    return 'connectionfailure' if $connectionfailure;
    return 'unknowntagfailure' if $unknowntagfailure;
    return 'unknownrevfailure' if $unknownrevfailure;
    return wantarray ? @{$response_ref} : $response_ref;
}
#### private methods ####

sub execute
{
    my $self    = shift;
    my $command = shift;
    my $authtimeout = 0;
    my @response;
    while () {
        if ( $authtimeout >= 5 ) {
            # fail after 5 tries
            die("FATAL: OOo CVS server authorization time out, can't continue!\nPlease notify Release Engineering.")
        }
        if ( $authtimeout > 0 ) {
            # sleep 5 seconds after a authorization timeout
            carp("WARNING: OOo CVS server authorization time out, count: $authtimeout, sleeping for 5 seconds ...");
            sleep(5);
        }
        # cvs option "-f" for disabling the reading of $HOME/.cvsrc, if any
        open(CVS, "$self->{CVS_BINARY} -f $command 2>&1 |");
        @response = <CVS>;
        close(CVS);

        foreach ( @response ) {
            if ( /unrecognized auth response/ ) {
                # don't get fooled by comment of rev. 1.14
                /#i25646#: catch 'unrecognized auth response' from OOo CVS server/ && next;
                # ok, seems to be a real timeout
                ++$authtimeout;
            }
        }
        last if !$authtimeout;
    }
    return wantarray ? @response : \@response;
}

sub parse_log
{
    my $self = shift;
    if ( $self->{"_PARSED"} ) {
        return;
    }
    my $file = $self->name();
    my $in_revisions = 0;
    my $in_tags = 0;
    my $rev_data = {};
    my ($rev, $date, $author, $state, $comment, @branches);

    my $response_ref = $self->execute("log $file");

    foreach ( @{$response_ref} ) {
        chomp;

        if ( $in_revisions ) {
            /revision\s((\d|\.)+)$/o && do { $rev = $1; next; };
            /^date:\s(\S+\s\S+);\s+author:\s(\S+);\s+state:\s(\S+);/
                && do { $date = $1; $author = $2; $state = $3; next; };
            /^branches:((\s+(\d|\.)+;)+)$/o && do {
                my $line;
                $line = $1;
                $line =~ s/\s//go;
                @branches = split(/;/, $line);
                next;
            };

            (/^----------------------------$/o || /^=============================================================================$/o) && do
            {
                $rev_data = {DATE => $date,
                             AUTHOR => $author,
                             STATE => $state,
                             COMMENT => $comment,
                             BRANCHES => [ @branches ]};
                $self->{REV_DATA}->{$rev} = $rev_data;
                $comment = undef;
                @branches = ();
                next;
            };

            $comment .= $_ . "\n" ;
        }
        elsif ( $in_tags ) {
            /^keyword\ssubstitution:\s/o && do { $self->{FLAGS} = $'; $in_tags--; next; };
            # tags may contain a hyphen
            /^\t([\w|\-]+):\s((\d|\.)+)$/o && do { $self->{TAGS}->{$1} = $2; next; };
        }
        else {
            /^----------------------------$/o && do { $in_revisions++; next; };
            /^symbolic\snames:$/o && do { $in_tags++; next; };
            /^head:\s((\d|\.)+)$/o && do { $self->{HEAD} = $1; next; };
            /^RCS file:\s((\d|\.)+)$/o && do { $self->{ARCHIVE_PATH} = $1; next; };
        }
    }

    $self->{"_PARSED"} = 1;
}

####

1; # needed by "use" or "require"
