580 lines
16 KiB
Perl
580 lines
16 KiB
Perl
#*************************************************************************
|
|
#
|
|
# 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"
|