#************************************************************************* # # 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 = ; 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"