OSSP CVS Repository

ossp - ossp-pkg/cvsfusion/RCS.pm
Not logged in
[Honeypot]  [Browse]  [Directory]  [Home]  [Login
[Reports]  [Search]  [Ticket]  [Timeline
  [Raw

ossp-pkg/cvsfusion/RCS.pm
##
##  OSSP cvsfusion - CVS Repository Fusion
##  Copyright (c) 2004 Ralf S. Engelschall <rse@engelschall.com>
##  Copyright (c) 2004 The OSSP Project <http://www.ossp.org/>
##  Copyright (c) 2004 Cable & Wireless <http://www.cw.com/>
##
##  This file is part of OSSP cvsfusion, a CVS repository fusion
##  utility which can be found at http://www.ossp.org/pkg/tool/cvsfusion/.
##
##  Permission to use, copy, modify, and distribute this software for
##  any purpose with or without fee is hereby granted, provided that
##  the above copyright notice and this permission notice appear in all
##  copies.
##
##  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
##  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
##  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
##  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
##  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
##  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
##  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
##  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
##  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
##  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
##  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
##  SUCH DAMAGE.
##
##  RCS.pm: RCS file handling
##

require 5;
use strict;
use warnings;

##  _________________________________________________________________________
##
##                       Class "RCS::Global" (SHARED)
##  _________________________________________________________________________
##

package RCS::Global;

#   check whether an entry name is valid
sub valid_entry_name ($$) {
    my ($obj, $name) = @_;

    my $valid = 0;
    if (defined($obj->{$name}) and $name !~ m|^-|) {
        $valid = 1;
    }
    return $valid;
}

#   check whether an entry value is valid
sub valid_entry_value ($$$) {
    my ($obj, $name, $value) = @_;

    my $type   = $obj->{$name}->{-type};
    my $syntax = $obj->{$name}->{-syntax};

    my $valid = 0;
    if ($type eq '$' and not ref($value) and $value =~ m|${syntax}|s) {
        $valid = 1;
    }
    elsif ($type eq '@' and ref($value) eq 'ARRAY') {
        $valid = 1;
        foreach my $v (@{$value}) {
            if ($v !~ m|${syntax}|s) {
                $valid = 0;
                last;
            }
        }
    }
    elsif ($type eq '%' and ref($value) eq 'HASH') {
        $valid = 1;
        foreach my $k (keys(%{$value})) {
            if ($k."::".$value->{$k} !~ m|${syntax}|s) {
                $valid = 0;
                last;
            }
        }
    }
    return $valid;
}

##  _________________________________________________________________________
##
##                        Class "RCS::Object" (ABSTRACT)
##  _________________________________________________________________________
##

package RCS::Object;

require Exporter;
use Carp;

our @ISA       = qw(Exporter);
our @EXPORT_OK = qw(new destroy DESTROY dump);

#   create new object
sub new ($;$) {
    my ($proto) = @_;

    #   create new object
    my $class = ref($proto) || $proto;
    my $self = {};
    bless ($self, $class);

    #   return new object
    return $self;
}

#   destroy object (explicit destructor)
sub destroy ($) {
    my ($self) = @_;
    return;
}

#   destroy object (implicit destructor)
sub DESTROY ($) {
    my ($self) = @_;
    $self->destroy;
    return;
}

#   dump object internals (debugging only)
sub dump ($;$) {
    my ($self, $name) = @_;
    $name ||= "obj";
    eval {
        use Data::Dumper;
    };
    my $d = new Data::Dumper ([$self], [$name]);
    $d->Indent(1);
    $d->Purity(1);
    $d->Terse(0);
    $d->Deepcopy(0);
    my $dump = "# " . ref($self) . " object dump:\n";
    $dump .= $d->Dump();
    return $dump;
}

##  _________________________________________________________________________
##
##                            Class "RCS::Revision"
##  _________________________________________________________________________
##

package RCS::Revision;

require 5;
require Exporter;
use Carp;

our @ISA       = qw(Exporter RCS::Object);
our @EXPORT_OK = qw(new destroy DESTROY dump revision set get);

#   create new object
sub new ($;$) {
    my ($proto, $rev) = @_;

    #   create new object
    my $self = {
        -rev       => undef,
        -order     => [],
        'date'     => { -type => '$', -syntax => qr/.*/, -value => undef },
        'author'   => { -type => '$', -syntax => qr/.*/, -value => undef },
        'state'    => { -type => '$', -syntax => qr/.*/, -value => undef },
        'branches' => { -type => '@', -syntax => qr/.*/, -value => undef },
        'next'     => { -type => '$', -syntax => qr/.*/, -value => undef },
        'log'      => { -type => '$', -syntax => qr/.*/, -value => undef },
        'text'     => { -type => '$', -syntax => qr/.*/, -value => undef },
    };
    my $class = ref($proto) || $proto;
    bless ($self, $class);

    #   optionally set revision
    $self->revision($rev) if (defined($rev));

    #   return new object
    return $self;
}

#   get and/or set revision number
sub revision ($;$) {
    my ($self, $rev) = @_;
    my $old_rev = $self->{-rev};
    if (defined($rev)) {
        $self->{-rev} = $rev;
    }
    return $old_rev;
}

#   set entry into object
sub set ($$$) {
    my ($self, $name, $value) = @_;

    if (not RCS::Global::valid_entry_name($self, $name)) {
        croak "invalid entry \"$name\"";
    }
    if (defined($value)) {
        if (not RCS::Global::valid_entry_value($self, $name, $value)) {
            croak "invalid value \"$value\" for entry \"$name\"";
        }
    }
    my $old_value = $self->{$name}->{-value};
    $self->{$name}->{-value} = $value;
    $self->{-order} = [ grep { $_ ne $name } @{$self->{-order}} ];
    push(@{$self->{-order}}, $name);
    return $old_value;
}

#   get entry from object
sub get ($;$) {
    my ($self, $name) = @_;

    if (not defined($name)) {
        return @{$self->{-order}};
    }
    if (not RCS::Global::valid_entry_name($self, $name)) {
        croak "invalid entry \"$name\"";
    }
    return $self->{$name}->{-value};
}

##  _________________________________________________________________________
##
##                                 Class "RCS"
##  _________________________________________________________________________
##

package RCS;

require Exporter;
use Carp;
use IO::File;
no warnings;

our @ISA       = qw(Exporter RCS::Object);
our @EXPORT_OK = qw(new destroy DESTROY dump load save parse format insert remove lookup set get);

#   create new object
sub new ($;$) {
    my ($proto, $file) = @_;

    #   create new object
    my $self = {
        -order     => [],
        'head'     => { -type => '$', -syntax => qr/.*/, -value => undef },
        'access'   => { -type => '$', -syntax => qr/.*/, -value => undef },
        'symbols'  => { -type => '@', -syntax => qr/.*/, -value => undef },
        'locks'    => { -type => '%', -syntax => qr/.*/, -value => undef },
        'strict'   => { -type => '%', -syntax => qr/.*/, -value => undef },
        'comment'  => { -type => '$', -syntax => qr/.*/, -value => undef },
        'expand'   => { -type => '$', -syntax => qr/.*/, -value => undef },
        'desc'     => { -type => '$', -syntax => qr/.*/, -value => undef },
        -revision  => { -count => 0 },
    };
    my $class = ref($proto) || $proto;
    bless ($self, $class);

    #   optionally load file into object
    $self->load($file) if (defined($file));

    #   return new object
    return $self;
}

#   INTERNAL: quote a RCS string
sub _string_quote {
    my ($str) = @_;
    $str =~ s|\@|\@\@|sg;
    $str = '@' . $str . '@';
    return $str;
}

#   INTERNAL: unquote a RCS string
sub _string_unquote {
    my ($str) = @_;
    $str =~ s|^\@(.*)\@$|$1|s;
    $str =~ s|\@\@|\@|sg;
    return $str;
}

#   load an RCS file into object
sub load ($$) {
    my ($self, $file) = @_;

    #   read RCS file into buffer
    my $io = new IO::File "<$file"
        or croak "RCS file \"$file\": cannot open for reading";
    my $rcs = '';
    $rcs .= $_ while (<$io>);
    $io->close;

    #   parse RCS file content into object
    $self->parse($rcs);

    return;
}

#   INTERNAL: structured revision to sequential number mapping
sub rev2num ($$) {
    my ($self, $rev) = @_;
    foreach my $num (keys(%{$self->{'-revision'}})) {
        next if ($num =~ m|^-|);
        return $num if ($self->{'-revision'}->{$num}->revision() eq $rev);
    }
    my $num = sprintf("REV-NUM(%d)", $self->{'-revision'}->{-count}++);
    $self->{'-revision'}->{$num} = new RCS::Revision $rev;
    return $num;
}

#   INTERNAL: sequential number to structured revision mapping
sub num2rev ($$) {
    my ($self, $num) = @_;
    return $self->{'-revision'}->{$num}->revision();
}

#   INTERNAL: object to sequential number mapping
sub obj2num ($$) {
    my ($self, $obj) = @_;
    foreach my $num (keys(%{$self->{'-revision'}})) {
        next if ($num =~ m|^-|);
        return $num if ($self->{'-revision'}->{$num} eq $obj);
    }
    my $num = sprintf("REV-NUM(%d)", $self->{'-revision'}->{-count}++);
    $self->{'-revision'}->{$num} = $obj;
    return $num;
}

#   INTERNAL: sequential number to object mapping
sub num2obj ($$) {
    my ($self, $num) = @_;
    return $self->{'-revision'}->{$num};
}

#   INTERNAL: check whether argument is a valid sequential number
sub isrevnum ($$) {
    my ($self, $num) = @_;
    return (($num =~ m|^REV-NUM\(\d+\)$| and defined($self->{'-revision'}->{$num})) ? 1 : 0);
}

#   parse a RCS file content into object
#   (see rcsfile(5) for reference)
sub parse ($$) {
    my ($self, $rcs) = @_;

    #   clear entries of object
    foreach my $entry (keys(%{$self})) {
        next if ($entry =~ m|^-|);
        $self->{$entry}->{-value} = undef;
    }

    #   pre-generate reusable regular expressions
    my $re_num     = qr/[\d.]+/;
    my $re_rev     = qr/\d+(?:\.\d+)*/;
    my $re_special = qr/[\$,.:;\@]/;
    my $re_idchar  = qr/[^\$,.:;\@]/;
    my $re_id      = qr/(?:${re_num})?${re_idchar}+(?:${re_idchar}|${re_num})*/;
    my $re_sym     = qr/\d?${re_idchar}+(?:${re_idchar}|\d)*/;
    my $re_str     = qr/(?:\@\@|[^@])*/s;
    my $re_date    = qr/(?:\d{2}|\d{4})\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}/;

    #   parse header section
    while (1) {
        $rcs =~ s|^\s*||s;
        if ($rcs =~ s/^head\s+($re_rev)\s*;//s) {
            $self->{'head'}->{-value} = $self->rev2num($1);
            push(@{$self->{-order}}, 'head');
        }
        elsif ($rcs =~ s/^branch\s+($re_rev)\s*;//s) {
            $self->{'branch'}->{-value} = $self->rev2num($1);
            push(@{$self->{-order}}, 'branch');
        }
        elsif ($rcs =~ s/^access((?:\s+$re_id)*)\s*;//s) {
            $self->{'access'}->{-value} = (defined($1) ? [ split(/\s+/, $1) ] : []);
            push(@{$self->{-order}}, 'access');
        }
        elsif ($rcs =~ s/^symbols((?:\s+$re_sym:$re_rev)*)\s*;//s) {
            my $symbols = { -order => [] };
            if (defined($1)) {
                map { if (m/^(.+):(.+)$/s) {
                $symbols->{$1} = $self->rev2num($2);
                push(@{$symbols->{-order}}, $1);
                } } split(/\s+/, $1);
            } 
            $self->{'symbols'}->{-value} = $symbols;
            push(@{$self->{-order}}, 'symbols');
        }
        elsif ($rcs =~ s/^locks((?:\s+$re_id:$re_rev)*)\s*;//s) {
            my $locks = { -order => [] };
            if (defined($1)) {
                map { if (m/^(.+):(.+)$/s) {
                    $locks->{$1} = $self->rev2num($2);
                    push(@{$locks->{-order}}, $1);
                } } split(/\s+/, $1);
            }
            $self->{'locks'}->{-value} = $locks;
            push(@{$self->{-order}}, 'locks');
        }
        elsif ($rcs =~ s/^strict\s*;//s) {
            $self->{'strict'}->{-value} = "";
            push(@{$self->{-order}}, 'strict');
        }
        #elsif ($rcs =~ s/^comment\s+\@($re_str)\@\s*;//s) { # would maximally span 32K
        elsif ($rcs =~ s/^comment\s+\@//s) {
            my $str = '';
            1 while ($rcs =~ s/^((?:\@\@|[^@]){1,32000})/$str .= $1, ''/se);
            $rcs =~ s/\@\s*;//s;
            $self->{'comment'}->{-value} = &_string_unquote($str);
            push(@{$self->{-order}}, 'comment');
        }
        #elsif ($rcs =~ s/^expand\s+\@($re_str)\@\s*;//s) { # would maximally span 32K
        elsif ($rcs =~ s/^expand\s+\@//s) {
            my $str = '';
            1 while ($rcs =~ s/^((?:\@\@|[^@]){1,32000})/$str .= $1, ''/se);
            $rcs =~ s/\@\s*;//s;
            $self->{'expand'}->{-value} = &_string_unquote($str);
            push(@{$self->{-order}}, 'expand');
        }
        elsif ($rcs =~ s/^([a-z]+)(?:\s*([^;]*));//s) {
            #   currently intentionally just ignored, because does not occur at all
        }
        else {
            last;
        }
    }

    #   parse delta section(s)
    while (1) {
        $rcs =~ s|^\s*||s;
        if ($rcs =~ s/^($re_rev)//s) {
            my $num = $self->rev2num($1);
            my $rev = $self->num2obj($num);
            while (1) {
                $rcs =~ s|^\s*||s;
                if ($rcs =~ s/^date\s+($re_date)\s*;//s) {
                    $rev->set('date', $1);
                }
                elsif ($rcs =~ s/^author\s+($re_id)\s*;//s) {
                    $rev->set('author', $1);
                }
                elsif ($rcs =~ s/^state(?:\s*($re_id))?\s*;//s) {
                    $rev->set('state', $1);
                }
                elsif ($rcs =~ s/^branches(?:\s+((?:\s*$re_rev)*))?\s*;//s) {
                    $rev->set('branches',
                        defined($1) ? [ map { $self->rev2num($_) } split(/\s+/, $1) ] : []);
                }
                elsif ($rcs =~ s/^next(?:\s*($re_rev))?\s*;//s) {
                    $rev->set('next', (defined($1) and $1 ne '' ? $self->rev2num($1) : ''));
                }
                elsif ($rcs =~ m/^desc\s+/s) {
                    last;
                }
                elsif ($rcs =~ s/^([a-z]+)(?:\s*([^;]*));//s) {
                    #   currently intentionally just ignored, because does not occur at all
                }
                else {
                    last;
                }
            }
        }
        else {
            last;
        }
    }

    #   parse description section
    $rcs =~ s|^\s*||s;
    #if ($rcs =~ s/^desc\s+\@($re_str)\@\s*//s) { # would maximally span 32K
    if ($rcs =~ s/^desc\s+\@//s) {
        my $str = '';
        1 while ($rcs =~ s/^((?:\@\@|[^@]){1,32000})/$str .= $1, ''/se);
        $rcs =~ s/\@\s*//s;
        $self->{'desc'}->{-value} = &_string_unquote($str);
        push(@{$self->{-order}}, 'desc');
    }

    #  parse deltatext section(s)
    while (1) {
        $rcs =~ s|^\s*||s;
        if ($rcs =~ s/^($re_rev)//s) {
            my $num = $self->rev2num($1);
            my $rev = $self->num2obj($num);
            my $textseen = 0;
            while (1) {
                $rcs =~ s|^\s*||s;
                #if ($rcs =~ s/^log\s+\@($re_str)\@\s*//s) { # would maximally span 32K
                if ($rcs =~ s/^log\s+\@//s) {
                    my $str = '';
                    1 while ($rcs =~ s/^((?:\@\@|[^@]){1,32000})/$str .= $1, ''/se);
                    $rcs =~ s/\@\s*//s;
                    $rev->set('log', &_string_unquote($str));
                }
                #elsif ($rcs =~ s/^text\s+\@($re_str)\@\s*//s) { # would maximally span 32K
                elsif ($rcs =~ s/^text\s+\@//s) {
                    my $str = '';
                    1 while ($rcs =~ s/^((?:\@\@|[^@]){1,32000})/$str .= $1, ''/se);
                    $rcs =~ s/\@\s*//s;
                    $rev->set('text', &_string_unquote($str));
                    $textseen = 1;
                }
                #elsif ($textseen == 0 and $rcs =~ s/^([a-z]+)(?:\s+\@($re_str)\@\s*)?//s) { # would maximally span 32K
                elsif ($textseen == 0 and $rcs =~ s/^([a-z]+)(?:\s+(\@))?//s) {
                    my ($keyword, $with_str) = ($1, $2);
                    my $str = '';
                    if ($with_str) { 
                        1 while ($rcs =~ s/^((?:\@\@|[^@]){1,32000})/$str .= $1, ''/se);
                        $rcs =~ s/\@\s*//s;
                    }
                    #   currently intentionally just ignored, because does not occur at all
                }
                else {
                    last;
                }
            }
        }
        else {
            last;
        }
    }

    return;
}

#   INTERNAL: return ordered list of revisions
#   (either in branch-first or next-first traversal order)
sub _revlist ($$) {
    my ($self, $branchfirst) = @_;
    my @revs = ();
    if (defined($self->{'head'}->{-value})) {
        &nextrev($self, \@revs, $self->{'head'}->{-value}, $branchfirst);
    }
    sub nextrev ($$$$) {
        my ($self, $revs, $rev, $branchfirst) = @_;
        push(@{$revs}, $rev);
        my $next     = $self->num2obj($rev)->get('next');
        my $branches = $self->num2obj($rev)->get('branches');
        if ($branchfirst) {
            foreach my $branch (@{$branches}) {
                &nextrev($self, $revs, $branch, $branchfirst); 
            }
            &nextrev($self, $revs, $next, $branchfirst) if (defined($next) and $next ne '');
        }
        else {
            &nextrev($self, $revs, $next, $branchfirst) if (defined($next) and $next ne '');
            foreach my $branch (@{$branches}) {
                &nextrev($self, $revs, $branch, $branchfirst); 
            }
        }
        return;
    }
    return @revs;
}

#   INTERNAL: generate output of a value in RCS syntax and layout
sub _genvalue ($$$) {
    my ($self, $val, $tag) = @_;
    my $rcs = '';
    if (ref($val) eq 'ARRAY' and @{$val} > 0) {
        foreach my $v (@{$val}) {
            $v = $self->num2rev($v) if ($self->isrevnum($v));
            $rcs .= "\n\t$v";
        }
    }
    elsif (ref($val) eq 'HASH' and keys(%{$val}) > 0) {
        if (defined($val->{-order})) {
            foreach my $k (@{$val->{-order}}) {
                my $v = $val->{$k};
                $v = $self->num2rev($v) if ($self->isrevnum($v));
                $k = $self->num2rev($k) if ($self->isrevnum($k));
                $rcs .= sprintf("\n\t%s:%s", $k, $v);
            }
        }
        else {
            foreach my $k (keys(%{$val})) {
                my $v = $val->{$k};
                $v = $self->num2rev($v) if ($self->isrevnum($v));
                $k = $self->num2rev($k) if ($self->isrevnum($k));
                $rcs .= sprintf("\n\t%s:%s", $k, $v);
            }
        }
    }
    elsif (not ref($val) and $val ne '') {
        if ($tag eq '@') {
            $rcs .= "\t" . &_string_quote($val);
        }
        else {
            $val = $self->num2rev($val) if ($self->isrevnum($val));
            $rcs .= "\t$val";
        }
    }
    return $rcs;
}

#   save object into RCS file
#   (see rcsfile(5) for reference)
sub save ($$) {
    my ($self, $file) = @_;

    #   format object as RCS file content
    my $rcs = $self->format();

    #   write RCS file content to RCS file
    my $io = new IO::File ">$file"
        or croak "RCS file \"$file\": cannot open for writing";
    $io->print($rcs);
    $io->close;
}

#   format object as RCS file content
sub format ($) {
    my ($self) = @_;
    my $rcs = '';

    #   define known keywords
    my @kw_header    = (qw(head branch access symbols locks- strict comment@ expand@));
    my @kw_delta     = (qw(date author state branches next));
    my @kw_desc      = (qw(desc));
    my @kw_deltatext = (qw(log text));

    #   generate header section
    foreach my $header (
        @kw_header, (grep { not grep(/^\Q$_\E$/, @kw_header) } @{$self->{-order}})
    ) {
        my $tag = '';
        $tag = $1 if ($header =~ s/([@\-])$//s);
        my $val = $self->{$header}->{-value};
        if (defined($val)) {
            $rcs .= $header . $self->_genvalue($val, $tag). ";";
            $rcs .= ($tag eq '-' ? " " : "\n");
        }
    }
    $rcs .= "\n";

    #   generate delta section(s)
    my @revlist = $self->_revlist(0);
    foreach my $rev (@revlist) {
        my $obj = $self->num2obj($rev);
        $rcs .= "\n";
        $rcs .= $obj->revision()."\n";
        $rcs .= "date\t" . $obj->get('date') . ";\t";
        $rcs .= "author " . $obj->get('author') . ";\t";
        $rcs .= "state " . $obj->get('state') . ";\n";
        $rcs .= "branches";
        my $branches = $obj->get('branches');
        if (@{$branches} > 0) {
            foreach my $v (@{$branches}) {
                $rcs .= "\n\t". $self->num2rev($v);
            }
        }
        $rcs .= ";\n";
        my $next = $obj->get('next');
        $rcs .= "next\t" . (defined($next) and $next ne '' ? $self->num2rev($next) : "") . ";\n";
    }

    #   generate description section
    my $desc = $self->{'desc'}->{-value};
    $rcs .= "\n\ndesc\n" . &_string_quote($desc) . "\n";

    #   generate deltatext section(s)
    @revlist = $self->_revlist(1);
    foreach my $rev (@revlist) {
        my $obj = $self->num2obj($rev);
        $rcs .= "\n";
        $rcs .= "\n";
        $rcs .= $obj->revision()."\n";
        my $log  = $obj->get('log')  || '';
        my $text = $obj->get('text') || '';
        $rcs .= "log\n" . &_string_quote($log) . "\n";
        $rcs .= "text\n" . &_string_quote($text) . "\n";
    }

    return $rcs;
}

#   insert a revision object
sub insert ($$) {
    my ($self, $obj) = @_;
    $self->obj2num($obj);
}

#   remove a revision object
sub remove ($$) {
    my ($self, $obj) = @_;
    my $num = $self->obj2num($obj);
    delete $self->{-revision}->{$num};
}

#   lookup a revision object
sub lookup ($;$) {
    my ($self, $id) = @_;

    if (not defined($id)) {
        return map  { $self->{-revision}->{$_} }
               grep { $_ !~ m|^-| }
               keys %{$self->{-revision}};
    }
    else {
        if ($id =~ m|^\d+(\.\d+)*$|) {
            #   lookup by revision number
            my $num = $self->rev2num($id);
            my $obj = $self->num2obj($num);
            return $obj;
        }
        else {
            #   lookup by symbolic tag
            foreach my $symbol (keys(%{$self->{'symbols'}->{-value}})) {
                if ($symbol eq $id) {
                    my $num = $self->{'symbols'}->{-value}->{$symbol};
                    my $obj = $self->num2obj($num);
                    return $obj;
                }
            }
        }
    }
    return undef;
}

#   set entry into object
sub set ($$$) {
    my ($self, $name, $value) = @_;

    if (not RCS::Global::valid_entry_name($self, $name)) {
        croak "invalid entry \"$name\"";
    }
    if (defined($value)) {
        if (not RCS::Global::valid_entry_value($self, $name, $value)) {
            croak "invalid value \"$value\" for entry \"$name\"";
        }
    }
    my $old_value = $self->{$name}->{-value};
    $self->{$name}->{-value} = $value;
    $self->{-order} = [ grep { $_ ne $name } @{$self->{-order}} ];
    push(@{$self->{-order}}, $name);
    return $old_value;
}

#   get entry from object
sub get ($;$) {
    my ($self, $name) = @_;

    if (not defined($name)) {
        return @{$self->{-order}};
    }
    if (not RCS::Global::valid_entry_name($self, $name)) {
        croak "invalid entry \"$name\"";
    }
    return $self->{$name}->{-value};
}

1;

__END__

##  _________________________________________________________________________
##
##                                Manual Page
##  _________________________________________________________________________
##

=pod

=head1 NAME

B<RCS> -- Revision Control System (RCS) File Handling

=head1 DESCRIPTION

This is a Perl API for reading and writing RCS files (I<filename>C<,v>).
It understands the syntax as documented in rcsfile(5) of GNU RCS version
5.7. It tries hard to save RCS files in a determined internal keyword
and revision order.

=head1 METHODS

=over 4

=item C<my $rcs = >B<new>C< RCS>[C< $filename>]C<;>

=item C<$rcs-E<gt>>B<destroy>C<;>

Method B<new> creates a new RCS object and (for convinience reasons)
optionally loads an RCS file via C<$rcs-E<gt>>B<load>C<($filename)>
into it. Method B<destroy> destroys the RCS object and frees all its
resources.

=item C<$rcs-E<gt>>B<load>C<($filename);>

=item C<$rcs-E<gt>>B<save>C<($filename);>

Method B<load> loads the RCS file under C<$filename> into RCS object C<$rcs>.
Methid B<save> saves the RCS file content from RCS object C<$rcs> under C<$filename>.

=item C<$rcs-E<gt>>B<parse>C<($filename, $rcstext);>

=item C<my $rcstext = $rcs-E<gt>>B<format>C<($filename);>

Method B<parse> parses the RCS file content in C<$rcstext> and and
stores the result in RCS object C<$rcs>. Method B<format> formats and
returns the RCS file content in C<$rcs>.

=item C<my $value = $rcs-E<gt>>B<get>C<($name);>

=item C<$rcs-E<gt>>B<set>C<($name, $value);>

Methods B<get> and B<set> get and/or set the value of the RCS entry
identified by C<$name>. Known entries are:

 Name    Type            Example
 head    scalar          '1.42'
 branch  scalar          '1.7'
 access  array reference [ 'foo', 'bar' ]
 symbols hash reference  { 'FOO' => '1.1', 'BAR' => '1.2' }
 locks   hash reference  { 'foo' => '1.3', 'bar' => '1.4' }
 strict  scalar          1
 comment scalar          'foo bar'
 expand  scalar          'b'
 desc    scalar          'foo bar'

=item C<$rcs-E<gt>>B<insert>C<($rev);>

=item C<$rcs-E<gt>>B<remove>C<($rev);>

=item C<my @revlist = $rcs-E<gt>>B<lookup>C<();>

=item C<my $rev = $rcs-E<gt>>B<lookup>C<($num);>

=item C<my $rev = $rcs-E<gt>>B<lookup>C<($tag);>

Methods B<insert> and B<remove> insert and remove a RCS::Revision object
C<$rev> to/from the RCS object C<$rcs>. Method B<lookup> lookups the
RCS file content revision(s) and returns either all existing revision
objects or a particular revision looked up by revision number or
revision tag. The result objects are of type RCS::Revision.

=item C<my $rev = >B<new>C< RCS::Revision>[ C<$rev>]C<;>

=item C<$rev-E<gt>>B<destroy>C<;>

Method B<new> creates a new RCS::Revision object. Method B<destroy>
destroys the RCS object and frees all its resources.

=item [C<my $rev = >]C<$rev-E<gt>>B<revision>C<(>[C<$rev>]C<);>

Method B<revision> gets and/or sets the revision number of the object.

=item C<my @names = $rev-E<gt>>B<get>C<();>

=item C<my $value = $rev-E<gt>>B<get>C<($name);>

=item C<$rev-E<gt>>B<set>C<($name, $value);>

Methods B<get> and B<set> get and/or set the value of the RCS::Revision
entry identified by C<$name>. Known entries are:

 Name     Type            Example
 date     scalar          2004.04.24.10.20.30
 author   scalar          foo
 state    scalar          Exp;
 branches array reference [ '1.1.1', '1.3.2', '1.4.2' ]
 next     scalar          '1.2'
 log      scalar          'foo bar'
 text     scalar          "a0 1\nfoo bar\n"

=back

=head1 SEE ALSO

rcsfile(5).

=head1 HISTORY

The Perl B<RCS> module was implemented in April 2004 for use in B<OSSP
cvsfusion> in order to support the fusion of two CVS repositories on the
RCS file level.

=head1 AUTHOR

Ralf S. Engelschall E<lt>rse@engelschall.comE<gt>

=cut


CVSTrac 2.0.1