/usr/local/CPAN/VCI/VCI/VCS/Cvs/Project.pm
package VCI::VCS::Cvs::Project;
use Moose;
use MooseX::Method;
use IPC::Cmd;
use File::Temp qw(tempdir);
use File::Path;
use VCI::Util;
extends 'VCI::Abstract::Project';
use constant CVSPS_SEPARATOR => "---------------------";
use constant CVSPS_MEMBER => qr/^\s+(.+):(INITIAL|[\d\.]+)->([\d\.]+)(\(DEAD\))?/o;
has 'x_tmp' => (is => 'ro', isa => 'Str', lazy => 1,
default => sub { tempdir('vci.cvs.XXXXXX', TMPDIR => 1,
CLEANUP => 1) });
sub BUILD {
my $self = shift;
$self->_name_never_ends_with_slash();
$self->_name_never_starts_with_slash();
}
method 'get_file' => named (
path => { isa => 'VCI::Type::Path', coerce => 1, required => 1 },
revision => { isa => 'Str' },
) => sub {
my $self = shift;
my ($params) = @_;
my $path = $params->{path};
my $rev = $params->{revision};
confess("Empty path name passed to get_file") if $path->is_empty;
if (defined $rev) {
my $file = $self->file_class->new(path => $path, revision => $rev,
project => $self);
# If $file->time works, then we have a valid file & revision.
return $file if defined eval { $file->time };
undef $@; # Don't mess up anything else that checks $@.
return undef;
}
# MooseX::Method always has a hash key for each parameter, even if they
# weren't passed by the caller.
delete $params->{$_} foreach (grep(!defined $params->{$_}, keys %$params));
return $self->SUPER::get_file(@_);
};
sub _build_history {
my $self = shift;
my @lines = split "\n", $self->x_cvsps_do(undef, 1);
my @commits;
my %current_commit;
my $new_patchset = 1;
# The first line is a CVSPS_SEPARATOR, so we just discard it.
shift @lines;
while (@lines) {
my $line = shift @lines;
if ($line =~ /^Log:\s*$/) {
my @log_lines;
while (@lines) {
last if $lines[0] =~ /^Members:\s*$/;
my $log_line = shift @lines;
push(@log_lines, $log_line);
}
$current_commit{'Log'} = \@log_lines;
}
elsif ($line =~ /^Members:\s*$/) {
my @member_lines;
while (@lines) {
my $member_line = shift @lines;
# This discards the extra newline at the end of "Members:"
last if $member_line eq '';
push(@member_lines, $member_line);
}
$current_commit{'Members'} = \@member_lines;
}
elsif ($line =~ /^(\S+):\s+(.+)?$/) {
my ($field, $value) = ($1, $2);
$current_commit{$field} = $value;
}
elsif ($new_patchset and $line =~ /^PatchSet\s+(\d+)\s*$/) {
$current_commit{PatchSet} = $1;
$new_patchset = 0;
}
elsif ($line eq CVSPS_SEPARATOR) {
$new_patchset = 1;
push(@commits, $self->_x_commit_from_patchset(\%current_commit));
%current_commit = ();
}
else {
warn "Unparsed cvsps line: $line";
}
}
if (keys %current_commit) {
push(@commits, $self->_x_commit_from_patchset(\%current_commit));
}
return $self->history_class->new(commits => \@commits, project => $self);
}
sub _x_commit_from_patchset {
my ($self, $data) = @_;
my $log_lines = $data->{Log};
# There's an extra newline at the end of @log_lines.
pop @$log_lines;
my ($added, $removed, $modified) = $self->_x_parse_members($data);
return $self->commit_class->new(
revision => $data->{PatchSet},
time => $data->{Date} . ' UTC',
committer => $data->{Author},
message => join("\n", @$log_lines),
added => $added,
removed => $removed,
modified => $modified,
project => $self,
);
}
sub _x_parse_members {
my ($self, $data) = @_;
my $members = $data->{Members};
my $date = $data->{Date} . 'UTC';
my (@added, @removed, @modified);
foreach my $item (@$members) {
if ($item =~ CVSPS_MEMBER) {
my ($path, $from_rev, $to_rev, $dead) = ($1, $2, $3, $4);
my $file = $self->file_class->new(
path => $path, revision => $to_rev, project => $self,
time => $date);
if ($from_rev eq 'INITIAL') {
push(@added, $file);
}
elsif ($dead) {
push(@removed, $file);
}
else {
push(@modified, $file);
}
}
else {
warn "Failed to parse message item: [$item] for patchset "
. $data->{PatchSet};
}
}
return (\@added, \@removed, \@modified);
}
sub x_cvsps_do {
my ($self, $addl_args) = @_;
$addl_args ||= [];
my @args = (@$addl_args, '-u', '-b', 'HEAD', $self->name);
my $root = $self->repository->root;
my $cvsps = $self->vci->x_cvsps;
if ($self->vci->debug) {
print STDERR "Running CVSROOT=$root $cvsps " . join(' ', @args)
. "\n";
}
# Just using the --root argument of cvsps doesn't work.
local $ENV{CVSROOT} = $root;
local $ENV{TZ} = 'UTC';
# XXX cvsps must be able to write to $HOME or this will fail.
my ($success, $error_msg, $all, $stdout, $stderr) =
IPC::Cmd::run(command => [$self->vci->x_cvsps, @args]);
if (!$success) {
confess "$error_msg: " . join('', @$stderr);
}
return join('', @$stdout);
}
sub DEMOLISH {
File::Path::rmtree($_[0]->x_tmp) if $_[0]->{x_tmp};
}
__PACKAGE__->meta->make_immutable;
1;