Follow-up to r29036: Now that the "mergeinfo" transaction file is no
[svn.git] / contrib / client-side / svncopy / svncopy.pl.in
blobc9c05a06e06c4419977cec2dc0ba268b711c4b23
1 #! /usr/bin/perl
3 # svncopy.pl -- Utility script for copying with branching/tagging.
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
10 # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
11 # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
12 # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
13 # NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
14 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
15 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
16 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
17 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
18 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
19 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
21 # You should have received a copy of the GNU General Public License along
22 # with this program; if not, write to the Free Software Foundation, Inc.,
23 # 59 Temple Place - Suite 330, Boston MA 02111-1307 USA.
25 # This product makes use of software developed by
26 # CollabNet (http://www.Collab.Net/), see http://subversion.tigris.org/.
28 # This software consists of voluntary contributions made by many
29 # individuals. For exact contribution history, see the revision
30 # history and logs, available at http://subversion.tigris.org/.
31 #------------------------------------------------------------------------------
33 #------------------------------------------------------------------------------
35 # This script copies one Subversion location to another, in the same way as
36 # svn copy. Using the script allows proper branching and tagging of URLs
37 # containing svn:externals definitions.
39 # For more information see the pod documentation at the foot of the file,
40 # or run svncopy.pl -?.
42 #------------------------------------------------------------------------------
45 # Include files
47 use Cwd;
48 use File::Temp 0.12 qw(tempdir tempfile);
49 use Getopt::Long 2.25;
50 use Pod::Usage;
51 use URI 1.17;
54 # Global definitions
57 # Specify the location of the svn command.
58 my $svn = '@SVN_BINDIR@/svn';
60 # Input parameters
61 my $testscript = 0;
62 my $verbose = 0;
63 my $pin_externals = 0;
64 my $update_externals = 0;
65 my @sources;
66 my $destination;
67 my $message;
68 my @svn_options = ();
70 # Internal information
71 my %externals_hash;
72 my $temp_dir;
74 # Error handling
75 my @errors = ();
76 my @warnings = ();
78 # Testing-specific variables
79 my $hideerrors = 0;
82 #------------------------------------------------------------------------------
83 # Main execution block
87 # Process arguments
89 GetOptions( "pin-externals|tag|t" => \$pin_externals,
90 "update-externals|branch|b" => \$update_externals,
91 "message|m=s" => \$message,
92 "revision|r=s" => \$revision,
93 "verbose!" => \$verbose,
94 "quiet|q" => sub { $verbose = 0; push( @svn_options, "--quiet" ) },
95 "file|F=s" => sub { push( @svn_options, "--file", $_[1] ) },
96 "username=s" => sub { push( @svn_options, "--username", $_[1] ) },
97 "password=s" => sub { push( @svn_options, "--password", $_[1] ) },
98 "no_auth_cache" => sub { push( @svn_options, "--no-auth-cache" ) },
99 "force-log" => sub { push( @svn_options, "--force-log" ) },
100 "encoding=s" => sub { push( @svn_options, "--encoding", $_[1] ) },
101 "config-dir=s" => sub { push( @svn_options, "--config-dir", $_[1] ) },
102 "help|?" => sub{ Usage() },
103 ) or Usage();
105 # Put in a signal handler to clean up any temporary directories.
106 sub catch_signal {
107 my $signal = shift;
108 warn "$0: caught signal $signal. Quitting now.\n";
109 exit 1;
112 $SIG{HUP} = \&catch_signal;
113 $SIG{INT} = \&catch_signal;
114 $SIG{TERM} = \&catch_signal;
115 $SIG{PIPE} = \&catch_signal;
118 # Check our parameters
120 if ( @ARGV < 2 )
122 Usage( "Please specify source and destination" );
123 exit 1;
127 # Get source(s) and destination.
129 push ( @sources, shift( @ARGV ) );
130 $destination = shift( @ARGV );
131 while ( scalar( @ARGV ) )
133 push( @sources, $destination );
134 $destination = shift( @ARGV );
138 # Any validation errors? If so, bomb out.
140 if ( scalar( @errors ) > 0 )
142 print "\n", @errors;
143 Usage();
144 exit scalar( @errors );
148 # Now do the main processing.
149 # This will update @errors if anything goes wrong.
151 if ( !DoCopy( \@sources, $destination, $message ) )
153 print "\n*****************************************************************\n";
154 print "Errors:\n";
155 print @errors;
158 exit scalar( @errors );
161 #------------------------------------------------------------------------------
162 # Function: DoCopy
164 # Does the work of the copy.
166 # Parameters:
167 # sources Reference to array containing source URLs
168 # destination Destination URL
169 # message Commit message to use
171 # Returns: 0 on error
173 # Updates @errors.
174 #------------------------------------------------------------------------------
175 sub DoCopy
177 my ( $sourceref, $destination, $message ) = @_;
178 my @sources = @$sourceref;
179 my $revstr = "";
180 my $src;
181 my $startdir = cwd;
182 my $starterrors = scalar( @errors );
184 print "\n=================================================================\n";
185 $revstr = "\@$revision" if $revision;
186 print "=== Copying from:\n";
187 foreach $src ( @sources ) { print "=== $src$revstr\n"; }
188 print "===\n";
189 print "=== Copying to:\n";
190 print "=== $destination\n";
191 print "===\n";
192 print "=== - branching (updating fully-contained svn:externals definitions)\n" if $update_externals;
193 if ( $pin_externals )
195 my $revtext = $revision ? "revision $revision" : "current revision";
196 print "=== - tagging (pinning all svn:externals definitions to $revtext)\n";
198 print "===\n" if ( $update_externals or $pin_externals );
200 # Convert destination to URI
201 $destination =~ s|/*$||;
202 my $destination_uri = URI->new($destination);
205 # Generate a message if we don't have one.
207 unless ( $message )
209 $message = "svncopy.pl: Copied to '$destination'\n";
210 foreach $src ( @sources )
212 $message .= " Copied from '$src'\n";
217 # Create a temporary directory to work in.
219 my ( $auto_temp_dir, $dest_dir ) =
220 PrepareDirectory( $destination_uri, "svncopy.pl to '$destination'\n - creating intermediate directory" );
221 $temp_dir = $auto_temp_dir->temp_dir();
222 chdir( $temp_dir );
224 foreach $src ( @sources )
226 # Convert source to URI
227 $src =~ s|/*$||;
228 my $source_uri = URI->new($src);
231 # Do the initial copy into our temporary. Note this will create a
232 # subdirectory with the same name as the last directory in $source.
234 if ( !CopyToWorkDir( $src, $dest_dir ) )
236 error( "Copy failed" );
237 return 0;
242 # Do any processing.
244 if ( $pin_externals or $update_externals )
246 if ( !UpdateExternals( $sourceref, $destination, $dest_dir, \$message ) )
248 error( "Couldn't update svn:externals" );
249 return 0;
254 # And check in the new.
256 DoCommit( $dest_dir, $message ) or die "Couldn't commit\n";
258 # Make sure we finish in the directory we started
259 chdir( $startdir );
261 print "=== ... copy complete\n";
262 print "=================================================================\n";
264 # Return whether there was an error.
265 return ( scalar( @errors ) == $starterrors );
269 #------------------------------------------------------------------------------
270 # Function: PrepareDirectory
272 # Prepares a temporary directory to work in.
274 # Parameters:
275 # destination Destination URI
276 # message Commit message
278 # Returns: temporary directory and subdirectory to work in
279 #------------------------------------------------------------------------------
280 sub PrepareDirectory
282 my ( $destination, $message ) = @_;
284 my $auto_temp_dir = Temp::Delete->new();
285 $temp_dir = $auto_temp_dir->temp_dir();
286 info( "Using temporary directory $temp_dir\n" );
289 # Our working destination directory has the same name as the last directory
290 # in the destination URI.
292 my @path_segments = grep { length($_) } $destination->path_segments;
293 my $new_dir = pop( @path_segments );
294 my $dest_dir = "$temp_dir/$new_dir";
296 # Make sure the destination directory exists in Subversion.
297 info( "Creating intermediate directories (if necessary)\n" );
298 if ( !CreateSVNDirectories( $destination, $message ) )
300 error( "Couldn't create parent directories for '$destination'" );
301 return;
304 # Check out the destination.
305 info( "Checking out destination directory '$destination'\n" );
306 if ( 0 != SVNCall( 'co', $destination, $dest_dir ) )
308 error( "Couldn't check out '$destination' into work directory." );
309 return;
312 return ( $auto_temp_dir, $dest_dir );
316 #------------------------------------------------------------------------------
317 # Function: CopyToWorkDir
319 # Does the svn copy into the temporary directory.
321 # Parameters:
322 # source The URI to copy from
323 # work_dir The working directory
325 # Returns: 1 on success
326 #------------------------------------------------------------------------------
327 sub CopyToWorkDir
329 my ( $source, $work_dir ) = @_;
330 my $dest_dir = DestinationSubdir( $source, $work_dir );
331 my @commandline = ();
333 push( @commandline, "--revision", $revision ) if ( $revision );
335 push( @commandline, $source, $work_dir );
337 my $exit = SVNCall( "copy", @commandline );
339 error( "$0: svn copy failed" ) if ( 0 != $exit );
341 return ( 0 == $exit );
345 #------------------------------------------------------------------------------
346 # Function: DestinationSubdir
348 # Returns the destination directory for a given source and a destination root
349 # directory.
351 # Parameters:
352 # source The URL to copy from
353 # destination The working directory
355 # Returns: The relevant directory
356 #------------------------------------------------------------------------------
357 sub DestinationSubdir
359 my ( $source, $destination ) = @_;
360 my $subdir;
362 # Make sure source and destination are consistent about separator.
363 # Note every function we call can handle Unix path format, so we
364 # default to that.
365 $source =~ s|\\|/|g;
366 $destination =~ s|\\|/|g;
368 # Find the last directory - that's the subdir we'll use in $destination
369 if ( $source =~ m"/([^/]+)/*$" )
371 $subdir = $1;
373 else
375 $subdir = $source;
377 return "$destination/$subdir";
381 #------------------------------------------------------------------------------
382 # Function: UpdateExternals
384 # Updates the svn:externals in the tree according to the --pin-externals or
385 # --update_externals options.
387 # Parameters:
388 # sourceref Ref to the URLs to copy from
389 # destination The URL being copied to
390 # work_dir The working directory
391 # msgref Ref to message string to update with changes
393 # Returns: 1 on success
394 #------------------------------------------------------------------------------
395 sub UpdateExternals
397 my ( $sourceref, $destination, $work_dir, $msgref ) = @_;
398 my @commandline = ();
399 my $msg;
400 my @dirfiles;
401 my %extlist;
403 # Check the externals on this directory and subdirectories
404 info( "Checking '$work_dir'\n" );
405 %extlist = GetRecursiveExternals( $work_dir );
407 # And do the update
408 while ( my ( $subdir, $exts ) = each ( %extlist ) )
410 my @externals = @$exts;
411 if ( scalar( @externals ) )
413 UpdateExternalsOnDir( $sourceref, $destination, $subdir, $msgref, \@externals );
417 return 1;
421 #------------------------------------------------------------------------------
422 # Function: UpdateExternalsOnDir
424 # Updates the svn:externals in the tree according to the --pin-externals or
425 # --update_externals options.
427 # Parameters:
428 # sourceref Ref to the URLs to copy from
429 # destination The URL being copied to
430 # work_dir The working directory
431 # externals Ref to the externals on the directory
432 # msgref Ref to message string to update with changes
434 # Returns: 1 on success
435 #------------------------------------------------------------------------------
436 sub UpdateExternalsOnDir
438 my ( $sourceref, $destination, $work_dir, $msgref, $externalsref ) = @_;
439 my @sources = @$sourceref;
440 my @externals = @$externalsref;
441 my @new_externals;
442 my %changed;
444 # Do any updating required
445 foreach my $external ( @externals )
447 chomp( $external );
448 next unless ( $external =~ m"^(\S+)(\s+)(?:-r\s*(\d+)\s+)?(.*)" );
449 my ( $ext_dir, $spacing, $ext_rev, $ext_val ) = ( $1, $2, $3, $4 );
451 info( " - Found $ext_dir => '$ext_val'" );
452 info( " ($ext_rev)" ) if $ext_rev;
453 info( "\n" );
455 $externals_hash{ "$ext_val" } = $ext_rev;
457 # Only update if it's not pinned to a version
458 if ( !$ext_rev )
460 if ( $update_externals )
462 my $old_external = $external;
463 foreach my $source ( @sources )
465 my $dest_dir = DestinationSubdir( $source, $destination );
466 #info( "Checking against '$source'\n" );
467 if ( $ext_val =~ s|^$source|$dest_dir| )
469 $external = "$ext_dir$spacing$ext_val";
470 info( " - updated '$old_external' to '$external'\n" );
471 $changed{$old_external} = $external;
475 elsif ( $pin_externals )
477 # Find the last revision of the destination and pin to that.
478 my $old_external = $external;
479 my $rev = LatestRevision( $ext_val, $revision );
480 #info( "Pinning '$ext_val' to '$rev'\n" );
481 $external = "$ext_dir -r $rev$spacing$ext_val";
482 info( " - updated '$old_external' to '$external'\n" );
483 $changed{$old_external} = $external;
486 push( @new_externals, $external );
489 # And write the changes back
490 if ( scalar( %changed ) )
492 # Update the commit log message
493 my %info = SVNInfo( $work_dir );
494 $$msgref .= "\n * $info{URL}: update svn:externals\n";
495 while ( my ( $old, $new ) = each( %changed ) )
497 $$msgref .= " from '$old' to '$new'\n";
498 info( " '$old' => '$new'\n" );
501 # And set the new externals
502 my ($handle, $tmpfile) = tempfile( DIR => $temp_dir );
503 print $handle join( "\n", @new_externals );
504 close($handle);
505 SVNCall( "propset", "--file", $tmpfile, "svn:externals", $work_dir );
510 #------------------------------------------------------------------------------
511 # Function: GetRecursiveExternals
513 # This function retrieves the svn:externals value from the
514 # specified URL or location and subdirectories.
516 # Parameters:
517 # location location of SVN object - file/dir or URL.
519 # Returns: hash
520 #------------------------------------------------------------------------------
521 sub GetRecursiveExternals
523 my ( $location ) = @_;
524 my %retval;
525 my $externals;
526 my $subdir = ".";
528 my ( $status, @externals ) = SVNCall( "propget", "-R", "svn:externals", $location );
530 foreach my $external ( @externals )
532 chomp( $external );
534 if ( $external =~ m"(.*) - (.*\s.*)" )
536 $subdir = $1;
537 $external = $2;
540 push( @{$retval{$subdir}}, $external ) unless $external =~ m"^\s*$";
543 return %retval;
547 #------------------------------------------------------------------------------
548 # Function: SVNInfo
550 # Gets the info about the given file.
552 # Parameters:
553 # file The SVN object to query
555 # Returns: hash with the info
556 #------------------------------------------------------------------------------
557 sub SVNInfo
559 my $file = shift;
560 my $old_verbose = $verbose;
561 $verbose = 0;
562 my ( $retval, @output ) = SVNCall( "info", $file );
563 $verbose = $old_verbose;
564 my %info;
566 return if ( 0 != $retval );
568 foreach my $line ( @output )
570 if ( $line =~ "^(.*): (.*)" )
572 $info{ $1 } = $2;
576 return %info;
580 #------------------------------------------------------------------------------
581 # Function: LatestRevision
583 # Returns the repository revision of the last change to the given object not
584 # later than the given revision (i.e. it may return revision, but won't
585 # return revision+1).
587 # Parameters:
588 # source The URL to check
589 # revision The revision of the URL to check from (if not supplied
590 # defaults to last revision).
592 # Returns: The relevant revision number
593 #------------------------------------------------------------------------------
594 sub LatestRevision
596 my ( $source, $revision ) = @_;
597 my $revtext = "";
599 if ( $revision )
601 $revtext = "--revision $revision:0";
604 my $old_verbose = $verbose;
605 $verbose = 0;
606 my ( $retval, @output ) = SVNCall( "log -q", $revtext, $source );
607 $verbose = $old_verbose;
609 if ( 0 != $retval )
611 error( "LatestRevision: log -q on '$source' failed" );
612 return -1;
616 # The second line should give us the info we need: e.g.
618 # >svn log -q http://subversion/svn/scratch/ianb/svncopy-update/source/dirA
619 # ------------------------------------------------------------------------
620 # r1429 | ib | 2004-06-14 17:39:36 +0100 (Mon, 14 Jun 2004)
621 # ------------------------------------------------------------------------
622 # r1423 | ib | 2004-06-14 17:39:26 +0100 (Mon, 14 Jun 2004)
623 # ------------------------------------------------------------------------
624 # r1422 | ib | 2004-06-14 17:39:23 +0100 (Mon, 14 Jun 2004)
625 # ------------------------------------------------------------------------
626 # r1421 | ib | 2004-06-14 17:39:22 +0100 (Mon, 14 Jun 2004)
627 # ------------------------------------------------------------------------
629 # The second line starts with the latest revision number.
631 if ( $output[1] =~ m"^r(\d+) \|" )
633 return $1;
636 error( "LatestRevision: log output not formatted as expected\n" );
638 return -1;
642 #------------------------------------------------------------------------------
643 # Function: DoCommit
645 # svn commits the temporary directory.
647 # Parameters:
648 # work_dir The working directory
649 # message Commit message
651 # Returns: non-zero on success
652 #------------------------------------------------------------------------------
653 sub DoCommit
655 my ( $work_dir, $message ) = @_;
656 my @commandline = ();
658 # Prepare a file containing the message
659 my ($handle, $messagefile) = tempfile( DIR => $temp_dir );
660 print $handle $message;
661 close($handle);
662 push( @commandline, "--file", $messagefile );
664 push( @commandline, $work_dir );
666 my ( $exit ) = SVNCall( "commit", @commandline );
668 error( "$0: svn commit failed" ) if ( 0 != $exit );
670 return ( 0 == $exit );
674 #------------------------------------------------------------------------------
675 # Function: SVNCall
677 # Makes a call to subversion.
679 # Parameters:
680 # command Subversion command
681 # options Other options to pass to Subversion
683 # Returns: exit status, output from command
684 #------------------------------------------------------------------------------
685 sub SVNCall
687 my ( $command, @options ) = @_;
689 my @commandline = ( $svn, $command, @svn_options, @options );
691 info( " > ", join( " ", @commandline ), "\n" );
693 my @output = qx( @commandline 2>&1 );
695 my $result = $?;
696 my $exit = $result >> 8;
697 my $signal = $result & 127;
698 my $cd = $result & 128 ? "with core dump" : "";
699 if ($signal or $cd)
701 error( "$0: 'svn $command' failed $cd: exit=$exit signal=$signal\n" );
704 if ( $exit > 0 )
706 info( join( "\n", @output ) );
708 if ( wantarray )
710 return ( $exit, @output );
713 return $exit;
717 #------------------------------------------------------------------------------
718 # Function: FindRepositoryRoot
720 # Returns the root of the repository for a given URL. Do
721 # this with the svn log command. Take the svn_url hostname and port
722 # as the initial url and append to it successive portions of the final
723 # path until svn log succeeds.
725 # Parameters:
726 # URI URI within repository
728 # Returns: A URI for the root, or undefined on error
729 #------------------------------------------------------------------------------
730 sub FindRepositoryRoot
732 my $URI = shift;
733 my $repos_root_uri;
734 my $repos_root_uri_path;
735 my $old_verbose = $verbose;
736 $verbose = 0;
738 info( "Finding the root URL of '$URI'.\n" );
740 my $r = $URI->clone;
741 my @path_segments = grep { length($_) } $r->path_segments;
742 unshift(@path_segments, '');
743 $r->path('');
744 my @r_path_segments;
746 while (@path_segments)
748 $repos_root_uri_path = shift @path_segments;
749 push(@r_path_segments, $repos_root_uri_path);
750 $r->path_segments(@r_path_segments);
751 if ( SVNCall( 'log', '-r', 'HEAD', $r ) == 0 )
753 $repos_root_uri = $r;
754 last;
758 $verbose = $old_verbose;
760 if ($repos_root_uri)
762 info( "Determined that the svn root URL is $repos_root_uri.\n\n" );
763 return $repos_root_uri;
765 else
767 error( "$0: cannot determine root svn URL for '$URI'.\n" );
768 return;
773 #------------------------------------------------------------------------------
774 # Function: CreateSVNDirectories
776 # Creates a directory in Subversion, including all intermediate directories.
778 # Parameters:
779 # URI directory path to create.
780 # message commit message (optional).
782 # Returns: 1 on success, 0 on error
783 #------------------------------------------------------------------------------
784 sub CreateSVNDirectories
786 my ( $URI, $message ) = @_;
787 my $r = $URI->clone;
788 my @path_segments = grep { length($_) } $r->path_segments;
789 my @r_path_segments;
790 unshift(@path_segments, '');
791 $r->path('');
793 my $found_root = 0;
794 my $found_tail = 0;
796 # Prepare a file containing the message
797 my ($handle, $messagefile) = tempfile( DIR => $temp_dir );
798 print $handle $message;
799 close($handle);
800 my @msgcmd = ( "--file", $messagefile );
802 # We're going to get errors while we do this. Don't show the user.
803 my $old_verbose = $verbose;
804 $verbose = 0;
805 # Find the repository root
806 while (@path_segments)
808 my $segment = shift @path_segments;
809 push( @r_path_segments, $segment );
810 $r->path_segments( @r_path_segments );
811 if ( !$found_root )
813 if ( SVNCall( 'log', '-r', 'HEAD', $r ) == 0 )
815 # We've found the root of the repository.
816 $found_root = 1;
819 elsif ( !$found_tail )
821 if ( SVNCall( 'log', '-r', 'HEAD', $r ) != 0 )
823 # We've found the first directory which doesn't exist.
824 $found_tail = 1;
828 if ( $found_tail )
830 # We're creating directories
831 $verbose = $old_verbose;
832 if ( 0 != SVNCall( 'mkdir', @msgcmd, $r ) )
834 error( "Couldn't create directory '$r'" );
835 return 0;
839 $verbose = $old_verbose;
841 return 1;
845 #------------------------------------------------------------------------------
846 # Function: info
848 # Prints out an informational message in verbose mode
850 # Parameters:
851 # @_ The message(s) to print
853 # Returns: none
854 #------------------------------------------------------------------------------
855 sub info
857 if ( $verbose )
859 print @_;
864 #------------------------------------------------------------------------------
865 # Function: error
867 # Prints out and logs an error message
869 # Parameters:
870 # @_ The error messages
872 # Returns: none
873 #------------------------------------------------------------------------------
874 sub error
876 my $error;
878 # This is used during testing
879 if ( $hideerrors )
881 return;
884 # Now print out each error message and add it to the list.
885 foreach $error ( @_ )
887 my $text = "svncopy.pl: $error\n";
888 push( @errors, $text );
889 if ( $verbose )
891 print $text;
897 #------------------------------------------------------------------------------
898 # Function: Usage
900 # Prints out usage information.
902 # Parameters:
903 # optional error message
905 # Returns: none
906 #------------------------------------------------------------------------------
907 sub Usage
909 my $msg;
910 $msg = "\n*** $_[0] ***\n" if $_[0];
912 pod2usage( { -message => $msg,
913 -verbose => 0 } );
917 #------------------------------------------------------------------------------
918 # This package exists just to delete the temporary directory.
919 #------------------------------------------------------------------------------
920 package Temp::Delete;
922 use File::Temp 0.12 qw(tempdir);
924 sub new
926 my $this = shift;
927 my $class = ref($this) || $this;
928 my $self = {};
929 bless $self, $class;
931 my $temp_dir = tempdir("svncopy_XXXXXXXXXX", TMPDIR => 1);
933 $self->{tempdir} = $temp_dir;
935 return $self;
938 sub temp_dir
940 my $self = shift;
941 return $self->{tempdir};
944 sub DESTROY
946 my $self = shift;
947 my $temp_dir = $self->{tempdir};
948 if ( scalar( @errors ) )
950 print "Leaving $temp_dir for inspection\n";
952 else
954 info( "Cleaning up $temp_dir\n" );
955 File::Path::rmtree([$temp_dir], 0, 0);
960 #------------------------------------------------------------------------------
961 # Documentation follows, in pod format.
962 #------------------------------------------------------------------------------
963 __END__
965 =head1 NAME
967 B<svncopy> - extended form of B<svn copy>
969 =head1 SYNOPSIS
971 B<svncopy.pl> [option ...] source [source ...] destination
973 This script copies one Subversion location or set of locations to another,
974 in the same way as B<svn copy>. Using the script allows more advanced operations,
975 in particular allowing svn:externals to be dealt with properly for branching
976 or tagging.
978 Parameters:
979 source Subversion item to copy from.
980 Multiple sources can be given.
981 destination Destination to copy to.
983 Options:
984 -t [--tag] : set svn:externals to current version
985 [--pin-externals ]
986 -b [--branch] : update fully contained svn:externals
987 [--update-externals]
988 -m [--message] arg : specify commit message ARG
989 -F [--file] arg : read data from file ARG
990 -r [--revision] arg : ARG (some commands also take ARG1:ARG2 range)
991 A revision argument can be one of:
992 NUMBER revision number
993 "{" DATE "}" revision at start of the date
994 "HEAD" latest in repository
995 "BASE" base rev of item's working copy
996 "COMMITTED" last commit at or before BASE
997 "PREV" revision just before COMMITTED
998 -q [--quiet] : print as little as possible
999 --username arg : specify a username ARG
1000 --password arg : specify a password ARG
1001 --no-auth-cache : do not cache authentication tokens
1002 --force-log : force validity of log message source
1003 --encoding arg : treat value as being in charset encoding ARG
1004 --config-dir arg : read user config files from directory ARG
1005 --[no]verbose : sets the script to give lots of output
1007 =head1 PARAMETERS
1009 =over
1011 =item B<source>
1013 The subversion item or items to copy from.
1015 =item B<destination>
1017 The destination URL to copy to.
1019 =back
1021 =head1 OPTIONS
1023 =over
1025 =item B<-t [--pin-externals or --tag]>
1027 Update any svn:externals to ensure they have a version number,
1028 using the current destination version if none is already specified.
1029 Useful for tagging operations.
1031 =item B<-b [--update-externals or --branch]>
1033 Update any unversioned svn:externals which point to a location
1034 within one of the sources so that they point to the corresponding
1035 location within the destination.
1037 Note: --pin-externals and --update-externals are mutually exclusive.
1039 =item B<-m [--message] arg>
1041 Specify commit message ARG
1043 =item B<-F [--file] arg>
1045 Read data from file ARG
1047 =item B<-r [--revision] arg>
1049 ARG (some commands also take ARG1:ARG2 range)
1050 A revision argument can be one of:
1052 NUMBER revision number
1053 "{" DATE "}" revision at start of the date
1054 "HEAD" latest in repository
1055 "BASE" base rev of item's working copy
1056 "COMMITTED" last commit at or before BASE
1057 "PREV" revision just before COMMITTED
1059 =item B<-q [--quiet]>
1061 Print as little as possible
1063 =item B<--username arg>
1065 Specify a username ARG
1067 =item B<--password arg>
1069 Specify a password ARG
1071 =item B<--no-auth-cache>
1073 Do not cache authentication tokens
1075 =item B<--force-log>
1077 Force validity of log message source
1079 =item B<--encoding arg>
1081 Treat value as being in charset encoding ARG
1083 =item B<--config-dir arg>
1085 Read user configuration files from directory ARG
1087 =item B<--[no]verbose>
1089 Sets the script to give lots of output when it runs.
1091 =item B<--help>
1093 Print a brief help message and exits.
1095 =back
1097 =head1 DESCRIPTION
1099 This script performs an B<svn copy> command. It allows extra processing to get
1100 around the following limitations of B<svn copy>:
1102 svn:externals definitions are (in Subversion 1.0 and 1.1 at least) absolute paths.
1103 This means that an B<svn copy> used as a branch or tag operation on a tree with
1104 embedded svn:externals will not do what is expected. The svn:externals
1105 will still point at the original location and will not be pinned down.
1107 B<svncopy --update-externals> (or B<svncopy --branch>) will update any
1108 unversioned svn:externals in the destination tree which point at locations
1109 within one of the source trees so that they point to the corresponding locations
1110 within the destination tree instead. This effectively updates the reference to
1111 point to the destination tree, and is the behaviour you want for branching.
1113 B<svncopy --pin-externals> (or B<svncopy --tag>) will update any unversioned
1114 svn:externals in the destination tree to contain the current version of the
1115 directory listed in the svn:externals definition. This effectively pins
1116 the reference to the current version, and is the behaviour you want for tagging.
1118 Note: both forms of the command leave unchanged any svn:externals which
1119 already contain a version number.
1121 =cut
1123 #------------------------------- END OF FILE ----------------------------------