#!/usr/local/bin/perl -w =head1 NAME vcp - Copy versions of files between repositories and/or RevML =head1 SYNOPSIS vcp [vcp_opts] src[:files] [src_opts] [dest:[]] [dest_opts] vcp cvs:/mymodule/... -r 1.1: p4://depot/mymodule vcp p4://depot/mainbranch/...@100-105 revml: vcp help vcp revml: --dtd --save_dtd =head1 STATUS Alpha code. Please report bugs to revml@perforce.com. =head1 DESCRIPTION vcp ('version copy') copies versions and version ranges of files from one repository to another, translating as much as possible along the way. This allows you to copy and translate ranges of revisions of files between different vendors' revision storage systems. Inputs and output classes currently available are: Class Source Destination cvs x x p4 x x revml x x The general syntax of the vcp command line is: vcp [vcp_opts] [src_opts] [dest_opts] where CsourceE> and CdestE> are composed of fields separated by delimiters like so: scheme:user(view):password@server:filespec where scheme is a repository type ('p4', 'cvs', 'revml'), or special command ('help', 'save_dtd'). user, view, and password are optional values, one or more of which may be required for repository access. CVS does not use (view). For p4, (view) is the client setting (P4CLIENT or -c option). server is the repository spec, CVSROOT for CVS or P4PORT for p4. filespec is the file specification for the files to move. As much as possible, this spec is similar to the native filespecs used by the repository indicated by the scheme. Most CsourceE> and CdestE> specs will omit one or more of the fields. For instance, Filespecs or passwords are often omitted from the destination, in which case the leading colons for these fields are also optional. If no user, view, or password is supplied, the "@" is optional. That's a bit confusing, here are some examples of stripped-down specs: cvs:server:/foo p4:user@server://depot/foo/... p4:user:password@public.perforce.com:1666://depot/foo/... Options and formats for of individual schemes can be found in the relevant manpages: L, L, L, L, L, L. At some point, the help command will be extended to be able to display them. =head2 OPTIONS All general options to vcp must precede the first scheme. Scheme-specific options must come after the affected source or destination spec and before the next one. =over =item --debug , -d Enables display of debugging information. A debug spec is part or all of a module name like C or a perl5 regular expression to match module names. Debug specs are not case insensitively. The most general, show-me-everything debug option is: -d ".*" The quotations are needed to slip the ".*" past most command shells. Any debug specs that don't match anything during a run are printed out when vcp exits in order to help identify mispelled patterns. vcp will also list all of the internal names that didn't match during a run to give clues as to what specs might be useful. The special name 'what' is guaranteed to not match anything, so you can do vcp -d what ... to see the list of names that might be useful for the arguments '...' . You may use multiple C<-d> options or provide a comma separated list to enable debugging within that module. Do not start a pattern with a "-". Debugging messages are emitted to stderr. See L for how to specify debug options in the environment. =item --help, -h, -? These are all equivalent to C. =back =head1 ARGUMENTS There are two special schemes, "help" and "save_dtd". =over =item help Displays the full help text. This will be extended to allow the retrieval of all of the manpages provided with VCP. =item save_dtd [] This is primarily for vcp maintainers. Outputs the DTD to stdout if is C<->, or to a file named like v1_000.pm if looks like a version number, or in a module named after if contains '::'. This file is placed in ./lib/RevML/DTD or ./RevML/DTD or ./, whichever is found first. No directories will be created. =back =head1 ENVIRONMENT The environment is often used to set context for the source and destination by way of variables like P4USER, P4CLIENT, CVSROOT, etc. There is also one environment variable that is used to enable command line debugging. The VCPDEBUG variable acts just like a leading "-d=$VCPDEBUG" was present on the command line. VCPDEBUG=main,p4 (see L for more info). This is useful when VCP is embedded in another application, like a makefile or a test suite. =cut use strict ; use Getopt::Long ; use File::Basename ; use File::Spec ; use VCP ; use VCP::Debug qw( :debug ) ; use XML::Doctype ; { my $pname = basename( $0 ) ; my $dtd_spec ; my $arg = 'help' ; usage_and_exit() unless @ARGV ; enable_debug( split /,/, $ENV{VCPDEBUG} ) if defined $ENV{VCPDEBUG} ; debug "vcp: ", join " ", map "'$_'", $pname, @ARGV if debugging "main" ; ## Parse up to the first non-option, then let sources & dests parse ## from there. Getopt::Long::Configure( qw( no_auto_abbrev no_bundling no_permute ) ) ; GetOptions( 'debug|d=s' => sub { enable_debug( length $_[1] ? split /,/, $_[1] : () ) }, 'help|h|?' => \&help_and_exit, 'versions' => \&versions_and_exit, ) or options_and_exit() ; usage_and_exit() unless @ARGV ; $arg = shift ; help_and_exit() if $arg eq 'help' ; my @errors ; ## We pass \@ARGV to the constructors for source and dest so that ## they may parse some of @ARGV and leave the rest. Actually, that's ## only important for sources, since the dests should consume it all ## anyway. But, for consistency's sake, I do the same to both. my $source ; if ( defined $arg ) { my ( $scheme, $spec ) = $arg =~ /^(.*?)(?::(.*))?$/ ; if ( defined $spec && ! length $spec && @ARGV && ( $ARGV[0] eq '-' || substr( $ARGV[0], 0, 1 ) ne '-' ) && index( $ARGV[0], ':' ) < 0 ) { $spec = shift ; } $spec = defined $spec ? "$scheme:$spec" : $scheme ; eval { $source = load_module( "VCP::Source::$scheme", $spec, \@ARGV ); die "unknown source scheme '$scheme', try ", list_modules( "VCP::Source" ), "\n" unless defined $source ; } ; push @errors, $@ if $@ ; } my $dest ; if ( defined $source ? $source->dest_expected : @ARGV ) { my $scheme ; my $spec = '' ; if ( @ARGV ) { ( $scheme, $spec ) = shift =~ /^(.*?)(?::(.*))?$/ ; if ( defined $spec && ! length $spec && @ARGV && ( $ARGV[0] eq '-' || substr( $ARGV[0], 0, 1 ) ne '-' ) && index( $ARGV[0], ':' ) < 0 ) { $spec = shift ; } } else { $scheme = 'revml' ; } $spec = defined $spec ? "$scheme:$spec" : $scheme ; eval { $dest = load_module("VCP::Dest::$scheme", $spec, \@ARGV ); die "unknown destination scheme '$scheme', try ", list_modules( "VCP::Dest" ), "\n" unless defined $dest ; } ; push @errors, $@ if $@ ; @ARGV = () ; } elsif ( @ARGV ) { push @errors, "extra parameters: " . join( ' ', @ARGV ) . "\n" ; } if ( debugging ) { debug 'vcp: no dest expected' unless ! $source || $source->dest_expected ; debug 'vcp: $source is ', $source ; debug 'vcp: $dest is ', $dest ; } unless ( @errors ) { my $cp = VCP->new( $source, $dest ) ; my $header = {} ; my $footer = {} ; $cp->copy_all( $header, $footer ) ; } if ( @errors ) { my $errors = join( '', @errors ) ; $errors =~ s/^/$pname: /mg ; die $errors ; } } ############################################################################### ############################################################################### sub load_module { my ( $name, @args ) = @_ ; my $filename = $name ; $filename =~ s{::}{/}g ; my $x ; { local $@ ; my $v = eval "require '$filename.pm'; 1" ; return undef if $@ && $@ =~ /^Can't locate $filename.pm/ ; $x = $@ ; } die $x if $x ; debug "vcp: loaded '$name' from '", $INC{"$filename.pm"}, "'" if debugging 'main', $name ; return $name->new( @args ) ;#if $v == 1 ; } sub list_modules { my ( $prefix ) = @_ ; my $dirname = $prefix . '::' ; $dirname =~ s{(::)+}{/}g ; my %seen ; for ( @INC ) { my $dir = File::Spec->catdir( $_, $dirname ) ; opendir( D, $dir ) or next ; my @files = grep $_ !~ /^\.\.?$/ && s/\.pm$//, readdir D ; closedir D ; $seen{$_} = 1 for @files ; } my $list = join( ', ', map "$_:", sort keys %seen ) ; $list =~ s/,([^,]*)$/ or$1/ ; return $list ; } sub usage_and_exit { require Pod::Usage ; Pod::Usage::pod2usage( -message => shift, -verbose => 0, -exitval => 1 ) ; } sub options_and_exit { require Pod::Usage ; Pod::Usage::pod2usage( -verbose => 1, -exitval => 1 ) ; } sub help_and_exit { require Pod::Usage ; Pod::Usage::pod2usage( -verbose => 2, -exitval => 0 ) ; } sub versions_and_exit { require File::Find ; my $require_module = sub { return unless m/\.pm$/ ; ## Avoid "name used only once" warning my $fn = $File::Find::name ; $fn = $File::Find::name ; require $fn ; } ; File::Find::find( { no_chdir => 1, wanted => $require_module, }, grep -d $_, map { ( File::Spec->catdir( $_, "lib", "VCP", "Source" ), File::Spec->catdir( $_, "lib", "VCP", "Dest" ), ) ; } @INC ) ; my %vers ; my %no_vers ; my $recur ; $recur = sub { my ( $pkg_namespace ) = @_ ; no strict "refs" ; my $pkg_name = substr( $pkg_namespace, 0, -2 ) ; ## The grep means "only bother with namespaces that contain somthing ## other than child namespaces. if ( ! grep /::/, keys %{$pkg_namespace} ) { if ( exists ${$pkg_namespace}{VERSION} ) { $vers{$pkg_name} = ${"${pkg_namespace}VERSION"} } else { $no_vers{$pkg_name} = undef ; } } my $prefix = $pkg_namespace eq "main::" ? "" : $pkg_namespace ; for ( keys %{$pkg_namespace} ) { next unless /::$/ ; next if /^main::/ ; $recur->( "$prefix$_" ) ; } } ; $recur->( "main::" ) ; my $max_len = 0 ; $max_len = length > $max_len ? length : $max_len for keys %vers ; print "Package \$VERSIONs:\n" ; for ( sort keys %vers ) { printf( " %-${max_len}s: %s\n", $_, defined $vers{$_} ? $vers{$_} : "undef" ) ; } print "No \$VERSION found for: ", join( ", ", sort keys %no_vers ), "\n" ; $max_len = 0 ; $max_len = length > $max_len ? length : $max_len for values %INC ; print "\nFile sizes:\n" ; for ( sort values %INC ) { printf( " %-${max_len}s: %7d\n", $_, -s $_ ) ; } print "\nperl -V:\n" ; my $v = `$^X -V` ; $v =~ s/^/ /gm ; print $v ; exit ; } =head1 AUTHOR Barrie Slaymaker =head1 COPYRIGHT This program is licensed under the provisions of the BSD license: Copyright (c) 2000, 2001, Perforce Software, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the Perforce Software, Inc.nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 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 REGENTS OR 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. This notice is also included in the LICENSE file accompanying this distribution, which also must be retained in redistributions. =cut