package VCP::Dest::revml ;
=head1 NAME
VCP::Dest::bmap - Outputs a branch map from a repository
=head1 SYNOPSIS
bmap:[<output-file>]
bmap:[<output-file>] --dtd <bmap.dtd>
bmap:[<output-file>] --version <version>
=head1 DESCRIPTION
The --dtd and --version options cause the output to be checked against
a particular version of revml. This does I<not> cause output to be in
that version, but makes sure that output is compliant with that version.
=head1 EXTERNAL METHODS
=over
=cut
use strict ;
use Carp ;
use Digest::MD5 ;
use Fcntl ;
use Getopt::Long ;
use MIME::Base64 ;
use RevML::Doctype ;
use RevML::Writer ;
use Symbol ;
use UNIVERSAL qw( isa ) ;
use VCP::Debug ':debug' ;
use VCP::Rev ;
use Text::Diff ;
use vars qw( $VERSION ) ;
$VERSION = 0.1 ;
use base 'VCP::Dest' ;
use fields (
'OUT_NAME', ## The name of the output file, or '-' for stdout
'OUT_FH', ## The handle of the output file
'WRITER', ## The XML::AutoWriter instance write with
) ;
=item new
Creates a new instance. The only parameter is '-dtd', which overrides
the default DTD found by searching for modules matching RevML::DTD:v*.pm.
Attempts to create the output file if one is specified.
=cut
sub new {
my $class = shift ;
$class = ref $class || $class ;
my VCP::Dest::revml $self = $class->SUPER::new( @_ ) ;
my @errors ;
my ( $spec, $options ) = @_ ;
my $parsed_spec = $self->parse_repo_spec( $spec ) ;
my $file_name = $parsed_spec->{FILES} ;
$self->{OUT_NAME} = defined $file_name && length $file_name
? $file_name
: '-' ;
if ( $self->{OUT_NAME} eq '-' ) {
$self->{OUT_FH} = \*STDOUT ;
## TODO: Check OUT_FH for writability when it's set to STDOUT
}
else {
require Symbol ;
$self->{OUT_FH} = Symbol::gensym ;
## TODO: Provide a '-f' force option
open( $self->{OUT_FH}, ">$self->{OUT_NAME}" )
or die "$!: $self->{OUT_NAME}" ;
}
## BUG: Can't undo this AFAIK, so we're permanently altering STDOUT
## if OUT_NAME eq '-'.
binmode $self->{OUT_FH};
my $doctype ;
my @sort_spec ;
{
local *ARGV = $options ;
GetOptions(
'dtd|version' => sub {
$doctype = RevML::Doctype->new( shift @$options ) ;
},
"k|sort-by=s" => \@sort_spec,
) or $self->usage_and_exit ;
}
$self->set_sort_spec( @sort_spec ) if @sort_spec ;
$doctype = RevML::Doctype->new
unless $doctype ;
die join( '', @errors ) if @errors ;
$self->writer(
RevML::Writer->new(
DOCTYPE => $doctype,
OUTPUT => $self->{OUT_FH},
)
);
return $self ;
}
sub _ISO8601(;$) {
my @f = reverse( ( @_ ? gmtime( shift ) : gmtime )[0..5] ) ;
$f[0] += 1900 ;
$f[1] ++ ; ## Month of year needs to be 1..12
return sprintf( "%04d-%02d-%02d %02d:%02d:%02dZ", @f ) ;
}
sub _emit_characters {
my ( $w, $buf ) = @_ ;
$w->setDataMode( 0 ) ;
## Note that we don't let XML munge \r to be \n!!
while ( $$buf =~ m{\G(?:
( [\x00-\x08\x0b-\x1f\x7f-\xff])
| ([^\x00-\x08\x0b-\x1f\x7f-\xff]*)
)}gx
) {
if ( defined $1 ) {
$w->char( "", code => sprintf( "0x%02x", ord $1 ) ) ;
}
else {
$w->characters( $2 ) ;
}
}
}
sub handle_rev {
my VCP::Dest::revml $self = shift ;
my VCP::Rev $r ;
( $r ) = @_ ;
my $w = $self->writer ;
if ( $self->none_seen ) {
$w->setDataMode( 1 ) ;
$w->xmlDecl ;
my $h = $self->header ;
## VCP::Source::revml passes through the original date. Other sources
## don't.
$w->time(
defined $h->{time}
? _ISO8601 $h->{time}
: _ISO8601
) ;
$w->rep_type( $h->{rep_type} ) ;
$w->rep_desc( $h->{rep_desc} ) ;
$w->rev_root( $h->{rev_root} ) ;
}
my VCP::Rev $saw = $self->last_seen( $r ) ;
## If there's no work path for the current file, keep the previous one.
## This is a cheat that allows us to diff against the last known version
## if a file is deleted and then re-added. Without this line, we would
## have to include the new version of the file.
$self->last_seen( $saw ) if $saw && ! defined $r->work_path ;
my $fn = $r->name ;
my $is_base_rev = $r->is_base_rev ;
die(
"Saw '", $saw->as_string,
"', but found a later base rev '" . $r->as_string, "'"
) if $saw && $is_base_rev ;
## type and rev_id are not provided for VSS deletes
debug "vcp: emitting revml for ", $r->as_string
if debugging $self;
$w->start_rev ;
$w->name( $fn ) ;
$w->type( $r->type ) if defined $r->type ;
$w->p4_info( $r->p4_info ) if defined $r->p4_info ;
$w->cvs_info( $r->cvs_info ) if defined $r->cvs_info ;
$w->rev_id( $r->rev_id ) if defined $r->rev_id ;
$w->change_id( $r->change_id ) if defined $r->change_id ;
$w->time( _ISO8601 $r->time ) if defined $r->time ;
$w->mod_time( _ISO8601 $r->mod_time ) if defined $r->mod_time ;
$w->user_id( $r->user_id ) if defined $r->user_id;
## Sorted for readability & testability
$w->label( $_ ) for sort $r->labels ;
if ( defined $r->comment && length $r->comment ) {
$w->start_comment ;
my $c = $r->comment ;
_emit_characters( $w, \$c ) ;
$w->end_comment ;
$w->setDataMode( 1 ) ;
}
my $convert_crs = $^O =~ /Win32/ && ( $r->type || "" ) eq "text" ;
my $digestion ;
my $close_it ;
my $cp = $r->work_path ;
if ( $is_base_rev ) {
sysopen( F, $cp, O_RDONLY ) or die "$!: $cp\n" ;
binmode F ;
$digestion = 1 ;
$close_it = 1 ;
}
elsif ( $r->action eq 'delete' ) {
$w->delete() ;
$self->delete_seen( $r ) ;
}
else {
sysopen( F, $cp, O_RDONLY ) or die "$!: $cp\n" ;
## need to binmode it so ^Z can pass through, need to do \r and
## \r\n -> \n conversion ourselves.
binmode F ;
$close_it = 1 ;
my $buf ;
my $read ;
my $has_nul ;
my $total_char_count = 0 ;
my $bin_char_count = 0 ;
while ( ! $has_nul ) {
$read = sysread( F, $buf, 100_000 ) ;
die "$! reading $cp\n" unless defined $read ;
last unless $read ;
$has_nul = $buf =~ tr/\x00// ;
$bin_char_count += $buf =~ tr/\x00-\x08\x0b-\x1f\x7f-\xff// ;
$total_char_count += length $buf ;
} ;
sysseek( F, 0, 0 ) or die "$! seeking on $cp\n" ;
$buf = '' unless $read ;
## base64 generate 77 chars (including the newline) for every 57 chars
## of input. A '<char code="0x01" />' element is 20 chars.
my $encoding = $bin_char_count * 20 > $total_char_count * 77/57
? "base64"
: "none" ;
if ( ! $saw ## First rev, can't delta
|| ! defined $saw->work_path ## No file, can't delta
|| $has_nul ## patch would barf, can't delta
|| $encoding ne "none" ## base64, can't delta
) {
## Full content, no delta.
$w->start_content( encoding => $encoding ) ;
my $delete_nl ;
while () {
## Odd chunk size is because base64 is most concise with
## chunk sizes a multiple of 57 bytes long.
$read = sysread( F, $buf, 57_000 ) ;
die "$! reading $cp\n" unless defined $read ;
last unless $read ;
if ( $convert_crs ) {
substr( $buf, 0, 1 ) = ""
if $delete_nl && substr( $buf, 0, 1 ) eq "\n" ;
$delete_nl = substr( $buf, -1 ) eq "\n" ;
$buf =~ s/(\r\n|\r)/\n/g ; ## ouch, that's gotta hurt.
}
if ( $encoding eq "none" ) {
_emit_characters( $w, \$buf ) ;
}
else {
$w->characters( encode_base64( $buf ) ) ;
}
}
$w->end_content ;
$w->setDataMode( 1 ) ;
}
else {
## Delta from previous version
$w->base_name( $saw->name )
if $saw->name ne $r->name ;
$w->base_rev_id( $saw->rev_id ) ;
$w->start_delta( type => 'diff-u', encoding => 'none' ) ;
my $old_cp = $saw->work_path ;
die "vcp: no old work path for '", $saw->name, "'\n"
unless defined $old_cp && length $old_cp ;
die "vcp: old work path '$old_cp' not found for '", $saw->name, "'\n"
unless -f $old_cp ;
## TODO: Include entire contents if diff is larger than the contents.
## Accumulate a bunch of output so that characters can make a
## knowledgable CDATA vs <& escaping decision.
my @output ;
my $outlen = 0 ;
my $delete_nl ;
## TODO: Write a "minimal" diff output handler that doesn't
## emit any lines from $old_cp, since they are redundant.
diff $old_cp, $cp,
{
## Not passing file names, so no filename header.
STYLE => "VCP::DiffFormat",
OUTPUT => sub {
push @output, $_[0] ;
## Assume no lines split between \r and \n because
## diff() splits based on lines, so we can just
## do a simple conversion here.
$output[-1] =~ s/\r\n|\r/\n/g if $convert_crs ;
$outlen += length $_[0] ;
return unless $outlen > 100_000 ;
_emit_characters( $w, \join "", splice @output ) ;
},
} ;
_emit_characters( $w, \join "", splice @output ) if $outlen ;
$w->end_delta ;
$w->setDataMode( 1 ) ;
} ;
$digestion = 1 ;
}
if ( $digestion ) {
## TODO: See if this should be seek or sysseek.
sysseek F, 0, 0 or die "$!: $cp" ;
my $d= Digest::MD5->new ;
## gotta do this by hand, since it's in binmode and we want
## to handle ^Z and lone \r's.
my $delete_nl ;
my $read ;
my $buf ;
while () {
$read = sysread( F, $buf, 10_000 ) ;
die "$! reading $cp\n" unless defined $read ;
last unless $read ;
if ( $convert_crs ) {
substr( $buf, 0, 1 ) = ""
if $delete_nl && substr( $buf, 0, 1 ) eq "\n" ;
$delete_nl = substr( $buf, -1 ) eq "\n" ;
$buf =~ s/(\r\n|\r)/\n/g ; ## ouch, that's gotta hurt.
}
$d->add( $buf ) ;
}
$d->addfile( \*F ) ;
$w->digest( $d->b64digest, type => 'MD5', encoding => 'base64' ) ;
}
if ( $close_it ) {
close F ;
}
$w->end_rev ;
# $self->last_seen( $r ) ;
}
sub handle_footer {
my VCP::Dest::revml $self = shift ;
my ( $footer ) = @_ ;
$self->writer->endAllTags() unless $self->none_seen ;
return ;
}
sub writer {
my VCP::Dest::revml $self = shift ;
$self->{WRITER} = shift if @_ ;
return $self->{WRITER} ;
}
=back
=head1 AUTHOR
Barrie Slaymaker <barries@slaysys.com>
=head1 COPYRIGHT
Copyright (c) 2000, 2001, 2002 Perforce Software, Inc.
All rights reserved.
See L<VCP::License|VCP::License> (C<vcp help license>) for the terms of use.
=cut
1
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #3 | 2245 | Barrie Slaymaker | cvs -r (re)implemented for direct reads, passes all cvs-only tests | ||
| #2 | 2009 | Barrie Slaymaker |
lots of fixes, improve core support for branches and VCP::Source::cvs now supports branches. |
||
| #1 | 1998 | Barrie Slaymaker | Initial, revml and core VCP support for branches |