#!/usr/bin/env perl -w # # $Id: //guest/vitalii_pokrovskii/svn2p4/svn2p4.pl#3 $ # # Copyright (C) 2006-2007 Blue Coat Systems, Inc. All rights reserved. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute # it freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; # you must not claim that you wrote the original software. # If you use this software in a product, an acknowledgment in the # product documentation would be appreciated but is not required. # # 2. This notice may not be removed or altered from any distribution. # # * THIS SOFTWARE IS PROVIDED BY BLUE COAT `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 BLUE COAT OR # * ITS LICENSORS 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. # ------------------------- # --- Global properties --- # ------------------------- use strict; use Cwd; use File::Basename; use File::Copy; use File::Find; use File::Path; use FileHandle; use Sys::Hostname; use constant true => 1; use constant false => 0; # Edit the below entries to configure your migration # environment. my %g = ( # # Runtime properties: # make_svn_repo_map_and_quit => true, die_on_warning => false, err_max_count => 1, wrn_max_count => 100, debug => $ENV{SVN2P4_DEBUG} || false, silent => $ENV{SVN2P4_SILENT} || false, verbose => $ENV{SVN2P4_VERBOSE} || true, very_verbose => $ENV{SVN2P4_VERY_VERBOSE} || false, # # Perforce and Subversion properties: # p4_user => "perforce", p4_port => "proteus-new.bluecoat.com:1999", p4_client => "svn2p4_collab.net.2", #p4_passwd => "perforce", svn_url => "http://svn.collab.net/repos/svn", svn_working_copy => cwd() . "/svn", p4_dummy_file => ".svn2p4", p4_dummy_file_default_path => "//depot/collab.net.2/.svn2p4", # svn_url => "http://svn.apache.org/repos/asf", # svn_url => "http://10.107.1.32/svn", # svn_url => "file:///usr/local/svnroot", svn2p4_repo_map => { "/trunk" => "//depot/collab.net.2/main", "/branches" => "//depot/collab.net.2/branches", "/tags" => "//depot/collab.net.2/tags", "/.svnignore" => "//depot/collab.net.2/misc/.svnignore", "/COMMITTERS" => "//depot/collab.net.2/misc/COMMITTERS", "/README" => "//depot/collab.net.2/misc/README", "/modules" => "//depot/collab.net.2/misc/modules", "/developer-resources" => "//depot/collab.net.2/misc/developer-resources", "/perl-bindings-improvements" => "//depot/collab.net.2/misc/perl-bindings-improvements", "/svn-logos" => "//depot/collab.net.2/misc/svn-logos", }, svn2p4_tag_migrate_target => "branch", #svn2p4_tag_migrate_target => "label", # # Data file definitions: # svn_all_rev_log => "svn_all_rev_log.txt", svn2p4_last_migrated_rev_file => "svn2p4_last_migrated_rev.txt", svn2p4_rev_map_file => "svn2p4_rev_map.csv", svn2p4_repo_map_to_skip_file => "svn2p4_repo_map_to_skip.csv", svn_exclude_rev_file => "svn_exclude_rev.txt", svn_exclude_node_file => "svn_exclude_node.txt", # # Do not change: # version => '0.22', backslash => '\\', blank => "", dev_null => $^O eq 'MSWin32' ? 'nul' : '/dev/null', dash => '-', err_count => 0, log_dir => "logs", log_file => basename ($0) . ".log", log_opened => false, script => basename ($0), script_dir => dirname ($0), sharp => '#', slash => '/', space => " ", start_time => time, wrn_count => 0, svn_repo_map_prop => { status => "status", comes_from => "comes_from", depends_on => "depends_on", entry_type => "entry_type", source_for => "source_for", }, ); # 'very_verbose' should turn 'verbose' on as well. # $g{verbose} = $g{verbose} || $g{very_verbose}; my %svn_repo_map = (); my %svn_repo_map_to_skip = (); my %svn2p4_rev_map = (); $g{svn2p4_repo_map_svn} = [ sort keys %{$g{svn2p4_repo_map}} ]; $ENV{P4PORT} = $g{p4_port}; $ENV{P4USER} = $g{p4_user}; $ENV{P4CLIENT} = $g{p4_client}; #$ENV{P4PASSWD} = $g{p4_passwd}; foreach my $key ( keys %SIG ) { $SIG{$key} = "signal_handler"; } delete $SIG{CHLD}; delete $SIG{WINCH}; delete $SIG{HUP}; # Enable output autoflush $| = 1; # ------------- # --- Subs --- # ------------- sub array_to_file { # Write array contents to a file. my ($file, $a) = @_; my $ref = ref ($a); if ( $ref ne 'ARRAY' ) { tout (3, "Expected array reference, received '$ref'."); finalize (1); } elsif ( ! scalar(@$a) ) { tout (3, "Received an empty array."); bump_wrn_count (); return false; } my @a = @$a; my $fh = fhw ($file); foreach my $line (@a) { next if !defined($line); $line = "$line\n" if ($line !~ /\n$/); $fh->write($line); } $fh->close; return true; } sub arrays_are_equal { # Return true if two given arrays are equal. my ($a, $b) = @_; return false if ( @$a != @$b ); for (my $i = 0; $i < @$a; $i++ ) { return false if ($a->[$i] ne $b->[$i]); } return true; } sub bump_err_count { # Increment err_count and finalize if it exceeds err_max_count. my $final = shift; if (++$g{err_count} > $g{err_max_count}) { finalize (0, "Exceeded maximum error count of $g{err_max_count}.") if !$final; } } sub bump_wrn_count { # Increment wrn_count and finalize if it exceeds wrn_max_count # or die_on_warning is set to true. my $final = shift; if (++$g{wrn_count} > $g{wrn_max_count}) { finalize (0, "Exceeded maximum warning count of $g{wrn_max_count}.") if !$final; } finalize (0, "Dying on warning.") if $g{die_on_warning} and !$final; } sub cp { # Copy a file, finalize in case of failure. my ($from, $to) = @_; if ( copy ($from, $to) ) { if ( $g{very_verbose} ) { tout (0, "Copied '$from'"); tout (0, " to '$to'"); } return true; } finalize (1, "Problem copying '$from' to '$to': $!"); } sub dos2unix_path { # dos2unix path conversion. my ($s) = @_; $s =~ s/\\/\//g; return $s; } sub ends_with { # Return true if string $s ends with string $x. my ($s, $x) = @_; $x = quotemeta($x); return $s =~ m/$x$/; } sub etime { # Compute elapsed time and return it as a string. my $t = time - int($g{start_time}); my $oneday = 86400; my $onehour = 3600; my $oneminute = 60; my $day = int($t/$oneday); my $hr = int(($t-$day*$oneday)/$onehour); my $min = int(($t-$day*$oneday-$hr*$onehour)/$oneminute); my $sec = $t-$day*$oneday-$hr*$onehour-$min*$oneminute; my $etime = $g{blank}; $etime = "$day day " if $day == 1; $etime = "$day days " if $day > 1; $etime .= "$hr hour " if $hr == 1; $etime .= "$hr hours " if $hr > 1; $etime .= "$min min " if $min > 0; $etime .= "$sec sec" if $sec >= 0; return $etime; } sub fha { # Create and return a filehandle for append. my ($file) = @_; my $handle = undef; my $dir = dirname ($file); if ( !$dir) { tout (3, "Cannot append to '$file', directory does not exist."); finalize (1); } $handle = new FileHandle ($file, ">>"); if ( !$handle ) { finalize (1, "Could not open file '$file' for append: $!"); } return $handle; } sub fhr { # Create and return a filehandle for read. my ($file) = @_; my $handle = undef; if ( !$file ) { tout (3, "Cannot find file '$file'."); finalize (1); } $handle = new FileHandle ($file, "<"); if ( !$handle ) { finalize (1, "Could not open file '$file' for read: $!"); } return $handle; } sub fhw { # Create and return a filehandle for write. my ($file) = @_; my $handle = undef; my $dir = dirname ($file); if ( !$dir) { tout (3, "Cannot write to '$file', directory does not exist."); finalize (1); } $handle = new FileHandle ($file, ">"); if ( !$handle ) { finalize (1, "Could not open file '$file' for write: $!"); } return $handle; } sub finalize { # Finalize script execution. my ($exitcode, $message) = @_; tout (-$exitcode, $message) if $message; tout (0,"$g{script} completed with $g{err_count} error(s) and $g{wrn_count} warning(s)."); tout (0,"Elapsed time: " . etime . "."); close LOG if $g{log_opened}; exit $exitcode; } #get_dir_filelist #{ # # Return list of files in the given directory. # my ($dir) = @_; # # opendir (DIR, $dir) or # finalize (1, "Problem reading directory '$dir'."); # my @a = grep !/^\.\.?\z/, readdir DIR; # closedir (DIR); # return \@a; #} sub get_common_stem { # Compute common stem for paths listed in the given # array reference and return the stem. # # NOTE: File separators should all be forward slash. my ($ar, $offset) = @_; my $size = scalar(@$ar); my $stem = "$g{slash}"; if ( $size == 0 ) { return $stem; } if ( $size == 1 ) { return $ar->[0]; } my @a = ( [] ); my $min = 1000; my $i = 0; foreach my $p (@$ar) { my @b = split /$g{slash}/, $p; my $s = scalar (@b); if ( $s < $min ) { $min = $s; } $a[$i] = [ @b ]; $i++; } my $depth = -1; L0: for ( my $j = $offset; $j < $min; $j++ ) { my $f = $a[0][$j]; L1: for ( my $i = 1; $i < $size; $i++ ) { if ( !defined($a[$i][$j]) || $a[$i][$j] ne $f ) { $depth = $j; last L0; } } } if ( $depth == -1 ) { $depth = $min; } if ( $depth == $offset ) { my $hr = (); ## No common stem. Return a hash reference ## with a list of next-level nodes. # #for ( my $i = 0; $i < $size; $i++ ) { # $stem = "$g{slash}$a[$i][$offset]"; # $hr->{$stem} = $g{blank}; #} # The below solution might be better # in terms of performance: # No common stem. Return original list. foreach my $p (@$ar) { $hr->{$p} = $g{blank}; } return $hr; } elsif ( $depth == $offset+1 ) { # The below solution might be better # in terms of performance: # Main stem. Return original list. my $hr = (); foreach my $p (@$ar) { $hr->{$p} = $g{blank}; } return $hr; } else { $depth = $depth - 1; } @a = @{$a[0]}[0..$depth]; $stem = join "$g{slash}", @a; return $stem; } sub get_stem { # Return common stem for the given path and list entries. # Fail if no common stem is found or if there is more than one # matching entry. my ($path, $ar, $offset) = @_; my @a = split /\//, $path; my @list = (); my $max = 0; if ( !defined($offset) ) { $offset = 0; } foreach my $x (@$ar) { my @b = split /\//, $x; if ( @b > $max ) { $max = @b; } push @list, \@b; } my $i = @a > $max ? $max : @a; my $j = 0; while ( $i > $offset ) { @a = @a[0..$i-1]; foreach my $x (@list) { @$x = @$x[0..$i-1]; if ( arrays_are_equal (\@a,$x) ) { $j++; } } if ( $j == 1 ) { return join ("/",@a); } if ( $j > 1 ) { finalize (1, "Ambiguous match for '$path' in (@$ar). " . "Revise your path mapping and re-start."); } $i--; } finalize (1, "No common stem for '$path' and (@$ar). " . "Revise your path mapping and re-start."); } sub get_svn_conf_data { # Load data from conf files. if ( -f $g{svn_exclude_rev_file} ) { tout (0, "Loading '$g{svn_exclude_rev_file}'."); open (LIST, "$g{svn_exclude_rev_file}"); my @a = ; close (LIST); chomp @a; $g{svn_exclude_rev_map} = {}; foreach my $v (@a) { next if is_null_or_empty_or_comment_string ($v); $g{svn_exclude_rev_map}{$v} = $g{blank}; } if ( scalar(keys(%{$g{svn_exclude_rev_map}})) == 0 ) { finalize (1, "Data file '$g{svn_exclude_rev_file}' is empty."); } $g{svn_exclude_rev} = true; } if ( -f $g{svn_exclude_node_file} ) { tout (0, "Loading '$g{svn_exclude_node_file}'."); open (LIST, "$g{svn_exclude_node_file}"); my @a = ; close (LIST); chomp @a; $g{svn_exclude_node_map} = {}; tout (0, "Validating exclude node list."); foreach my $v (@a) { next if is_null_or_empty_or_comment_string ($v); get_stem ($v, $g{svn2p4_repo_map_svn}, 1); $g{svn_exclude_node_map}{$v} = $g{blank}; } if ( scalar(keys(%{$g{svn_exclude_node_map}})) == 0 ) { finalize (1, "Data file '$g{svn_exclude_node_file}' is empty."); } $g{svn_exclude_node} = true; } } sub get_svn_next_rev { my $rev = 0; if ( -f "$g{svn2p4_last_migrated_rev_file}" ) { $rev = get_svn_last_rev (); if ( ! %svn2p4_rev_map ) { get_svn2p4_rev_map (); } } $g{svn_last_rev} = $rev; $g{svn_this_rev} = ++$rev; return $rev; } sub get_svn_last_rev { my $r = undef; open (LAST, "<$g{svn2p4_last_migrated_rev_file}"); $r = ; close (LAST); chomp $r; return $r; } sub is_svn_file { # Return true if the given svn node is a file. my ($p) = @_; $p = "$g{svn_working_copy}/$p"; my $hr = get_svn_path_info ($p); return $hr->{"Node Kind"} eq "file"; } sub is_svn_dir { # Return true if the given svn node is a directory. my ($p) = @_; $p = "$g{svn_working_copy}/$p"; my $hr = get_svn_path_info ($p); return $hr->{"Node Kind"} eq "directory"; } sub is_subnode { # Return true if svn path is a sub-node of given node. my ( $path, $node) = @_; my $n = quotemeta ($node); my $subnode = $path; $subnode =~ s/$n//; return false if ( $subnode eq $path ); return true if ( $subnode eq $g{blank} ); return true if ( starts_with ($subnode, $g{slash}) ); return false; } sub get_svn_path_info { # Return hash reference containing info about a node # (file/directory URL) in the svn repository. my ($node) = @_; my $ar = run_cmd ("svn info $node"); my %h = (); foreach my $entry (@$ar) { if ( is_null_or_empty_string ($entry) ) { next; } my @a = split /:/, $entry; my $key = trim ($a[0]); $entry =~ s/$key: //; $h{$key} = $entry; } return \%h; } sub get_svn_repo_map { # Analize the complete svn revision log to obtain # the dependency map for top level trees in the # repository and store it in the hash svn_repo_map. # # TODO: # Time permitting, re-write map generation and parsing # code to support traversing to any depth. my $m = \%svn_repo_map; my ($info, $path, $t, $t1, $u, $u1) = (); my $top = run_cmd ("svn list $g{svn_url}"); for ( my $i = 0; $i < scalar (@$top); $i++ ) { $t = $top->[$i]; $t =~ s/\/$//; $u = "/$t"; if ( $g{verbose} ) { tout (0, "Processing '$u'."); } $m->{$u} = {}; $m->{$u}->{status} = "active"; $m->{$u}->{comes_from} = "self-contained"; $m->{$u}->{depends_on} = {}; $m->{$u}->{source_for} = {}; $info = get_svn_path_info ("$g{svn_url}/$t"); if ( $info->{"Node Kind"} eq "file" ) { $m->{$u}->{entry_type} = "file"; next; } my $n = run_cmd ("svn list $g{svn_url}/$t"); for ( my $j = 0; $j < scalar (@$n); $j++ ) { $t1 = $n->[$j]; $t1 =~ s/\/$//; $u1 = "/$t/$t1"; if ( $g{very_verbose} ) { tout (0, "Processing '$u1'."); } $path = "$t/$t1"; $m->{$u}->{$u1} = {}; $m->{$u}->{$u1}->{status} = "active"; $m->{$u}->{$u1}->{depends_on} = {}; $m->{$u}->{$u1}->{source_for} = {}; $m->{$u}->{$u1}->{comes_from} = "self-contained"; # Depending connection speed, the below code can be slow. # TODO: Un-comment the code below to include path type # information for second-level nodes. # # $info = get_svn_path_info ("$g{svn_url}/$path"); # if ( $info->{"Node Kind"} eq "file" ) { # $m->{$u}->{$u1}->{entry_type} = "file"; # } else { # $m->{$u}->{$u1}->{entry_type} = "directory"; # } } } # Process the log. my $fh = fhr ( $g{svn_all_rev_log} ); my $marker = '-' x 72; my @a = (); $a[0] = $fh->getline(); chomp $a[0]; my $i = 1; while ( my $line = $fh->getline() ) { chomp $line; if ( $line !~ /^${marker}$/ ) { $a[$i] = $line; $i++; next; } $a[$i] = $line; my $hr = parse_svn_rev_log (\@a); update_svn_repo_map ($hr); @a = (); $a[0] = $line; $i = 1; } $fh->close; if ( $g{verbose} ) { prir (0, $m, "svn_repo_map"); } } sub get_svn_repo_map_to_skip { # Generate a list of "redundant" nodes (i.e. nodes that can be # skipped by the migration). The list can be useful for pre-migration # dependencies analysis and preparing the node exclude list. # Use the following criteria to identify redundant nodes: # # - node is not a source # - node is only source to itself # - node is a source to a deleted node that is not a source # my $m = \%svn_repo_map; my $skip = \%svn_repo_map_to_skip; my @a = sort keys %$m; foreach my $k (@a) { if ( is_svn_repo_map_to_skip_node ($k) ) { $skip->{$k} = $m->{$k}; # Skipping the whole node, no need to recurse into sub-nodes. next; } my @b = sort keys %{$m->{$k}}; my $i = 0; my $j = 0; foreach my $n (@b) { my $hr = $m->{$k}->{$n}; if ( ref($hr) ne 'HASH' ) { next; } if ( $n eq "source_for" ) { next; } if ( $n eq "depends_on" ) { next; } if ( $n eq "comes_from" ) { next; } if ( is_svn_repo_map_to_skip_node ($k,$n) ) { $j++; $skip->{$k}->{$n} = $hr; } $i++; } if ( $i == $j ) { # The node cannot be skiped but all sub-nodes can be skipped: # the condition means none of the sub-nodes can be skipped. delete $skip->{$k}; } } if ( $g{verbose} ) { tout (0, "Printing the list of redundant nodes."); prir (0, $skip, "svn_repo_map_to_skip"); } } sub is_svn_repo_map_to_skip_node { # Return true if the given hash reference is a "redundant" svn node. my ($t1, $t2) = @_; my $m = \%svn_repo_map; my @a = $t2 ? keys (%{$m->{$t1}->{$t2}->{source_for}}) : keys (%{$m->{$t1}->{source_for}}); if ( scalar @a == 0 ) { return true; } if ( !$t2 ) { return false; } foreach my $n ( @a ) { my ($t0, $t1, $t2, $t3) = split /\//, $n; my $r = $t2 ? $m->{"/$t1"}->{$n} : $m->{$n}; my @b = keys ( %{$r->{source_for}} ); if ( scalar(@b) > 0 ) { return false; } if ( $r->{status} eq "active" ) { return false; } } return true; } sub save_svn_repo_map_to_skip { # Persist svn_repo_map_to_skip map by saving it to a file # identified by $g{svn2p4_repo_map_to_skip_file} in csv format: # # , , # # if is found to be "self-contained" then # and fields will be empty. Otherwise the fields are # source directory and source revision, respectively. The latter # normally is a branchpoint or tagpoint for . my $m = \%svn_repo_map_to_skip; if ( !defined $m ) { tout (2, "The table 'svn_repo_map_to_skip' is not defined."); return; } my @to_save = (); my $pr = $g{svn_repo_map_prop}; push (@to_save, "#"); push (@to_save, "# ,,"); push (@to_save, "#"); foreach my $t1 (sort keys (%$m)) { foreach my $t2 ( sort keys (%{$m->{$t1}})) { next if ( $pr->{$t2} ); my $comes_from = $m->{$t1}->{$t2}->{comes_from}; if ( $comes_from eq "self-contained" ) { push (@to_save, "$t2,,"); next; } my $r = $m->{$t1}->{$t2}->{depends_on}; my $rev = "undef"; foreach my $node (keys %{$r}) { if ( $node eq $comes_from ) { ($rev) = keys %{$r->{$node}}; } } push (@to_save, "$t2,$comes_from,$rev"); } } array_to_file ( $g{svn2p4_repo_map_to_skip_file}, \@to_save ); } sub update_svn_repo_map { # Use the contents of revision log to update # the svn_repo_map table. my ($hr) = @_; my $m = \%svn_repo_map; my ($info, $lvl, $path, $r, $slvl, $src, $src_rev, $root, $root_src, $t, $t1, $u, $u1) = (); my @a = (); my @b = (); my @path = keys %{$hr->{path}}; foreach my $p ( @path ) { @a = split /\//, $p; $lvl = scalar (@a) - 1; $t = $a[1]; $t1 = $a[2]; $u = "/$t"; $u1 = $lvl == 1 ? "" : "/$t/$t1"; $path = $lvl == 1 ? "$t" : "$t/$t1"; $r = undef; if ( $lvl == 1 and !$m->{$u} ) { $m->{$u} = {}; $r = $m->{$u}; } elsif ( $lvl > 1 and !$m->{$u}->{$u1} ) { $m->{$u}->{$u1} = {}; $r = $m->{$u}->{$u1}; } if ( $r ) { $r->{status} = "deleted"; $r->{comes_from} = "self-contained" if !$r->{comes_from}; $r->{depends_on} = {} if !$r->{depends_on}; $r->{source_for} = {} if !$r->{source_for}; } $src = $hr->{path}->{$p}->{source}; next if ( !$src ); if ( !$r ) { $r = $m->{$u} if ( $lvl == 1); $r = $m->{$u}->{$u1} if ($lvl > 1); } if ( $hr->{path}->{$p}->{action} eq "A" and $lvl <= 2 ) { $r->{comes_from} = $src; } $src_rev = $hr->{path}->{$p}->{source_rev}; $root = $lvl == 1 ? "$u" : "$u1"; @b = split /\//, $src; $slvl = scalar(@b) - 1; $root_src = $slvl == 1 ? "/$b[1]" : "/$b[1]/$b[2]"; if ( $root eq $root_src ) { next; } if ( $lvl == 1 ) { $m->{$u}->{depends_on}->{$root_src} = {}; $m->{$u}->{depends_on}->{$root_src}->{$src_rev} = {}; } elsif ( $lvl >= 2 ) { $m->{$u}->{$u1}->{depends_on}->{$src} = {}; $m->{$u}->{$u1}->{depends_on}->{$src}->{$src_rev} = {}; } if ( $slvl == 1 ) { $m->{$root_src}->{source_for}->{$u} = {}; } elsif ( $slvl >= 2 ) { $m->{"/$b[1]"}->{$root_src}->{source_for}->{$root} = {}; $m->{"/$b[1]"}->{source_for}->{$u} = {}; } } } sub set_svn_last_rev { my ($rev) = @_; open (LAST, ">$g{svn2p4_last_migrated_rev_file}"); print LAST "$rev\n"; close (LAST); $svn2p4_rev_map{$rev} = $g{p4_this_rev}; save_svn2p4_rev_map ($rev); } sub get_svn_all_rev_log { # Obtain a complete revision log from svn and store it # in the file identified by $g{svn_all_rev_log}. # # TODO: This command may crash svn on Windows while getting very large # changesets. If this happens, create the file manually. # # TODO: # The below syntax may not work if head revision is the range of # hundred thousands (critical size varies depending on the overall repository # size) because the connection may expire. Default revision order i.e. # HEAD:1 seems to work regardless of repository size. # The order does not matter for repository tree map generation. # For migration proper, read individual changeset logs one by one or read # them from the bottom up to ensure they are processed in natural order. # run_cmd ("svn log -v -r 1:HEAD $g{svn_url} > $g{svn_all_rev_log}"); # # run_cmd ("svn log -v $g{svn_url} > $g{svn_all_rev_log}"); return; } sub get_p4_clientspec { # Obtain p4 clientspec data and return it in a hash reference. my %client = (); my $view = {}; my $a = run_cmd("p4 client -o"); trim ($a); for (my $i = 0; $i < scalar(@$a); $i++) { my $x = $a->[$i]; next if (is_null_or_empty_or_comment_string($x)); my @b = split /\t/, $x; my $d = $b[1]; if ($x =~ /^Client/) { $client{client} = $d; } elsif ($x =~ /^Update/) { $client{update} = $d; } elsif ($x =~ /^Access/) { $client{access} = $d; } elsif ($x =~ /^Owner/) { $client{owner} = $d; } elsif ($x =~ /^Host/) { $client{host} = $d; } elsif ($x =~ /^Description/) { $client{description} = $a->[$i+1]; } elsif ($x =~ /^Root/) { $client{root} = dos2unix_path($d); } elsif ($x =~ /^Options/) { $client{options} = $d; } elsif ($x =~ /^LineEnd/) { $client{lineend} = $d; } elsif ($x =~ /^View/) { next; } elsif ( $x =~ /\/\// ) { $x =~ s/\/\.\.\.//g; my ($y, $z) = split / /, $x; $view->{$y} = $z; } } if ( $client{client} ne $g{p4_client} ) { finalize (1, "Perforce client name mismatch: expected " . "'$g{p4_client}' but found '$client{client}'."); } if ( $client{host} ) { my $host = hostname (); if ( $client{host} ne $host ) { finalize (1, "Perforce client host name mismatch: expected " . "'$client{host}' but found '$host'."); } } else { $client{host} = $g{blank}; } $client{view} = $view; $g{p4_clientspec} = \%client; $g{p4_clientspec_view} = [ sort keys %{$view} ]; return \%client; } sub set_p4_dummy_file_default_path_local { my $a = run_p4_cmd ("where $g{p4_dummy_file_default_path} 2>&1"); if ( $a->[0] =~ /must refer to client/ ) { finalize (1, "Unable to determine local path for " . "'$g{p4_dummy_file_default_path}': $a->[0]."); } my @a = split / /, $a->[0]; $g{p4_dummy_file_default_path_local} = $a[2]; } sub get_p4_info { my $a = run_cmd ("p4 info 2>&1"); my %info = (); trim ($a); foreach my $x (@$a) { $_ = $x; s/^.*: //; if ( $x =~ /User name/ ) { $info{user} = $_; } elsif ( $x =~ /Current directory/ ) { s/^\/cygdrive\/(.)/$1:/; $info{cwd} = $_; } elsif ( $x =~ /Server address/ ) { $info{server_address} = $_; } elsif ( $x =~ /Server date/ ) { $info{server_date} = $_; } elsif ( $x =~ /Server version/ ) { $info{server_version} = $_; } elsif ( $x =~ /Server license/ ) { $info{server_license} = $_; } } $g{p4_server_info} = \%info; return \%info; } sub get_svn_rev_log { # Obtain and parse svn change description. my $rev = shift; my $ar = run_cmd ("svn log -v -r $rev $g{svn_url}"); chomp @$ar; parse_svn_rev_log ($ar); } sub get_svn2p4_rev_map { # Read svn2p4 revision map. my $file = $g{svn2p4_rev_map_file}; if (! -f "$file" ) { finalize (1, "Could not find file '$file'."); } my $fh = fhr ($file); my @a = <$fh>; chomp @a; foreach my $entry ( @a ) { next if is_null_or_empty_or_comment_string ($entry); my @b = split /,/, $entry; trim (\@b); $svn2p4_rev_map{$b[0]} = $b[1]; } return true; } sub save_svn2p4_rev_map { # Save svn2p4 revision map to file. my $rev = shift; my $file = $g{svn2p4_rev_map_file}; my $fh; if ( ! -f $file ) { $fh = fhw ($file); $fh->write("#\n"); $fh->write("# Do not edit!\n"); $fh->write("#\n"); $fh->write("# ,\n"); $fh->write("#\n"); sub numerically { $a <=> $b } foreach my $key (sort numerically keys %svn2p4_rev_map ) { $fh->write("${key},$svn2p4_rev_map{$key}\n"); } $fh->close(); return true; } $fh = fha ($file); $fh->write("$rev,$svn2p4_rev_map{$rev}\n"); $fh->close(); return true; } sub get_svn_head_rev { if ($g{svn_head_rev} && $g{svn_this_rev} && $g{svn_head_rev} >= $g{svn_this_rev}) { return $g{svn_head_rev}; } my $a = run_cmd("svn -q -r HEAD log $g{svn_url}"); $a->[1]=~ s/^.*r([0-9]+).*$/$1/; $g{svn_head_rev} = $a->[1]; return $g{svn_head_rev}; } sub get_svn_info { my $a = run_cmd ("svn --quiet --version"); my $b = run_cmd ("svn info $g{svn_url}"); if ( ! -d $g{svn_working_copy} ) { tout (0, "Creating svn working copy '$g{svn_working_copy}'."); run_cmd ("svn co -r 1 $g{svn_url} $g{svn_working_copy}"); } my $c = run_cmd ("svn info $g{svn_working_copy}"); my %info = (); $info{svn_version} = $a->[0]; foreach (@$b) { my $x = $_; $x =~ s/^.*: //; if ( /URL/) { $info{svn_url} = $x; } elsif ( /Path/) { $info{svn_working_copy} = $x; } elsif ( /Repository Root/ ) { $info{svn_repo_root} = $x; } elsif ( /Repository UUID/ ) { $info{svn_repo_uuid} = $x; } elsif ( /Node Kind/ ) { $info{svn_node_kind} = $x; } elsif ( /Last Changed Author/ ) { $info{svn_head_rev_author} = $x; } elsif ( /Last Changed Date/ ) { $info{svn_head_rev_date} = $x; } elsif ( /Last Changed Rev/ ) { $info{svn_head_rev} = $x; } } foreach (@$c) { my $x = $_; $x =~ s/^.*: //; if ( /Schedule/ ) { $info{svn_have_schedule} = $x; } elsif ( /Last Changed Author/ ) { $info{svn_have_rev_author} = $x; } elsif ( /Last Changed Date/ ) { $info{svn_have_rev_date} = $x; } elsif ( /Last Changed Rev/ ) { $info{svn_have_rev} = $x; } } $g{svn_info} = \%info; return \%info; } sub get_timestamp { my @a = localtime; $a[0] = "0$a[0]" if ( $a[0] < 10 ); $a[1] = "0$a[1]" if ( $a[1] < 10 ); $a[2] = "0$a[2]" if ( $a[2] < 10 ); $a[3] = "0$a[3]" if ( $a[3] < 10 ); $a[4]++; $a[4] = "0$a[4]" if ( $a[4] < 10 ); $a[5] += 1900; return "$a[5]$a[4]$a[3]-$a[2]$a[1]$a[0]"; } sub greeting { # Print script greeting and the copyright message. my @t = localtime(); my $year = $t[5] + 1900; if ( $year ne "2006" ) { $year = "2006-$year"; } out ("\n$g{script} r$g{version} (c) Copyright $year Blue Coat Systems, Inc. " . "All rights reserved.\n\n"); } sub is_null_or_empty_string { # Return true if subject string is null or empty, # otherwise return false. my ($s) = @_; return true if ( !defined($s) ); return true if ( length(trim($s)) == 0 ); return false; } sub is_null_or_empty_or_comment_string { # Return true if subject string is null, empty or a comment, # otherwise return false. my ($s) = @_; return true if ( !defined($s) ); return true if ( length(trim($s)) == 0 ); return true if ( starts_with (trim($s),$g{sharp}) ); return false; } sub is_p4_branchspec { # Return true if the given name is a p4 branchspec. my ($name) = @_; my $ar = run_p4_cmd ("branches"); foreach my $s (@$ar) { my @a = split / /, $s; if ( $a[1] eq $name ) { return true; } } return false; } sub is_p4_label { # Return true if the given name is a p4 label. my ($name) = @_; my $ar = run_p4_cmd ("labels"); foreach my $s (@$ar) { my @a = split / /, $s; if ( $a[1] eq $name ) { return true; } } return false; } sub is_p4_up { # Return true if Perforce is up, otherwise return false. return system ("p4 > $g{dev_null} 2>&1") == 0 || finalize (1,"Perforce is not available."); } sub md { # Create a new directory. Return true on success, # finalize on failure. my ($dir) = @_; if (! -d $dir) { if (mkpath($dir,0,0777)) { tout (0,"Created directory '$dir'.") if $g{very_verbose}; return true; } else { finalize (1,"Failure creating directory '$dir': $!"); } } return true; } sub open_log { # Open process log. Move old log to directory identified by # $g{log_dir}. md ($g{log_dir}); if ( -f "$g{log_file}" ) { move ("$g{log_file}", "$g{log_dir}/$g{script}" . "." . get_timestamp . ".log"); } open ( LOG, ">$g{log_file}" ) or finalize (1, "Could not create session log '$g{log_file}': $!"); $g{log_opened} = 1; return $g{log_file}; } sub out { # Pring a string to STDOUT and script log. print ( LOG $_[0] ) if $g{log_opened}; print ( STDOUT $_[0] ) if !$g{silent}; } sub parse_svn_rev_log { # Parse svn change log and store revision data in # $g{svn_this_change}. Return a hash reference containing # the data. my ($ar) = @_; my $hr = {}; # This assumes the log entry has its line-ends removed. # chomp @$ar; my @a = split /\|/, $ar->[1]; trim (\@a); ( $hr->{rev}, $hr->{author}, $hr->{date} ) = @a; $hr->{rev} =~ s/^r//; if ( is_null_or_empty_string ($ar->[2]) ) { # Special case: changeset contains no changes $hr->{path} = {}; } else { my $i = 3; my $x = $ar->[$i]; while ( ! is_null_or_empty_string ($x) ) { $x = trim ($x); my $action = $x; $action =~ s/ .*$//; my $path = $x; $path =~ s/^. //; $path =~ s/ \(from .*\)$//; $hr->{path}->{$path}->{action} = $action; my $source = $x; $source =~ s/^.* \(from //g; if ( $source ne $x ) { $source =~ s/\)$//; my $source_rev = $source; $source_rev =~ s/^.*://; $source =~ s/:$source_rev$//; $hr->{path}->{$path}->{source} = $source; $hr->{path}->{$path}->{source_rev} = $source_rev; } $x = $ar->[++$i]; } } $hr->{log} = $ar; # Compute common stem for files in the change and add it to # change data. @a = keys (%{$hr->{path}}); $hr->{common_stem} = get_common_stem (\@a, 1); $g{svn_this_change} = $hr; if ( $g{very_verbose} ) { prir (0,$hr,'change'); } return $hr; } sub prig { # Print the (indented) contents of the global hash %g # (useful for debugging purposes). tout(0, $g{dash} x 50); prir (0,\%g,'g'); tout(0, $g{dash} x 50); } sub prir { # Print (recursively) the contents of a reference (array or hash). my ($p, $ref, $ref_name) = @_; my @a = (); my $rtype = ref $ref; my $is_array = $rtype eq 'ARRAY'; my $is_hash = $rtype eq 'HASH'; if ( !($is_array || $is_hash) ) { return false; } my $ropen = '{'; my $rclose = '}'; if ( $is_hash ) { @a = sort( keys(%$ref) ); } elsif ( $is_array ) { @a = @$ref; $ropen = '['; $rclose = ']'; } # Un-comment the code below to print right-justified output. # my $maxlen = 0; # my $len; # for ( my $i = 0; $i < scalar(@a); $i++ ) { # $len = length $a[$i]; # $maxlen = $len if $len > $maxlen; # } my $pp = $g{space} x $p; my $padlen = $p; if ( $ref_name ) { $padlen = $p + length($ref_name) + 3; } my $pad = $g{space} x $padlen; tout (0, "${pp}${ref_name} = $ropen") if ( $ref_name ); tout (0, "${pp}$ropen") if ( !$ref_name ); foreach my $k ( @a ) { # Right-justified padding: #my $pad1len = $padlen + $maxlen - length($k); # Left-justified padding: my $pad1len = $p + 4; my $vref = undef; $vref = ref($ref->{$k}) if ( $is_hash ); $vref = ref($k) if ( $is_array ); my $pad1 = $g{space} x $pad1len; if ( $vref eq 'HASH' || $vref eq 'ARRAY' ) { prir ($pad1len, $ref->{$k}, $k); } else { tout (0, $pad1 . "$k = '$ref->{$k}'") if ( $is_hash ); tout (0, $pad1 . "'$k'") if ( $is_array ); } } tout (0, "${pp}${rclose}"); return true; } sub run_cmd { # Execute a system command and return command output # in array reference. my ($cmd) = @_; my @a = qx /$cmd 2>&1/; chomp @a; my $to_string = "@a"; $to_string =~ s/\s{1,}/ /g; if ( $? == 0 ) { return \@a; } if ( $a[0] =~ /svn.*PROPFIND/ || $a[0] =~ /svn.*Service Unavailable/ ) { # Re-try svn command tout (0,"Problem executing command '$cmd': $to_string"); my $i = 0; while ( $i < 5 ) { tout (0, "Try again in 60 sec: '$cmd'."); sleep 60; @a = qx/$cmd 2>&1/; if ( $? == 0 ) { chomp @a; return \@a; } $i++; } } if ( ! @a ) { push (@a, ""); } # TODO: # tout (2,"Problem executing command '$cmd': $to_string"); finalize (1,"Problem executing command '$cmd': $to_string"); } sub run_p4_cmd { # Execute p4 command and optionally print its output. my ($cmd) = @_; $cmd = "p4 $cmd"; if ( $g{very_verbose} ) { tout (0, "Executing '$cmd'."); } my $ar = run_cmd ($cmd); if ( $g{very_verbose} ) { foreach my $line (@$ar) { tout (0, "p4: $line"); } } return $ar; } sub set_p4_changespec { # Create new Perforce changespec. Insert svn log as change description. # Return pending change number. my @a = @{$_[0]}; is_p4_up (); my $a = qx /p4 opened 2>&1/; $a =~ /not opened/ or finalize (1, "Client '$g{p4_client}' has files opened for edit. " . "Please revert the files and re-start."); for ( my $i = 0; $i < scalar(@a); $i++ ) { $a[$i] = "\t$a[$i]"; } my @spec = qx /p4 change -o/; my $offset = 0; my $file = "$g{p4_client}_spec.txt"; if ( -f "$file" ) { unlink "$file"; } for ( my $i = 0; $i < scalar(@spec); $i++ ) { if ( $spec[$i] =~ // ) { $offset = $i + 1; $spec[$i] =~ s//Created by $g{script}./; } } splice (@spec, $offset, 0, @a); array_to_file ($file, \@spec); @a = split /$g{space}/, qx/p4 change -i < $file/; if ( $a[1] !~ /^\d+$/ ) { chomp @a; tout (3, "Problem getting pending change number: '@a'"); finalize (1, "Expected a whole number, received '$a[1]'."); } $g{p4_this_rev} = $a[1]; if ( $g{verbose} ) { tout(0,"Created pending p4 change '$g{p4_this_rev}'."); } return $g{p4_this_rev}; } sub set_p4_branchspec { # Create new p4 branchspec. my ($branchspec, $from, $to) = @_; my @log = @{$g{svn_this_change}->{log}}; my @spec = qx /p4 branch -o $branchspec/; my $offset = 0; my $file = "$g{p4_client}_branchspec.txt"; if ( -f "$file" ) { unlink "$file"; } for ( my $i = 0; $i < scalar(@spec); $i++ ) { if ( $spec[$i] =~ /Created by $g{p4_user}/ ) { $offset = $i + 1; $spec[$i] =~ s/$g{p4_user}/$g{script}/; } if ( $spec[$i] =~ /^View:/ ) { $spec[$i+1] = "\t${from}/... ${to}/..."; $spec[$i+2] = $g{blank}; @spec = @spec[0..$i+2]; last; } } for (my $i = 0; $i < scalar(@log); $i++) { $log[$i] = "\t$log[$i]"; } splice (@spec, $offset, 0, @log); array_to_file ($file, \@spec); my @a = qx/p4 branch -i < $file 2>&1/; if ( $a[0] !~ /^Branch .* saved./ ) { chomp @a; finalize (1, "Problem creating p4 branchspec: '@a'"); } tout(0,"Created p4 branchspec '$branchspec'."); return $branchspec; } sub set_p4_labelspec { # Create new Perforce labelspec. my ($label, $p4_view) = @_; # TODO: Hardcode a broad view to support complex # cases of tag migration. # # $p4_view = "//"; my @log = @{$g{svn_this_change}->{log}}; my @spec = qx /p4 label -o $label/; my $offset = 0; my $file = "$g{p4_client}_labelspec.txt"; if ( -f "$file" ) { unlink "$file"; } for ( my $i = 0; $i < scalar(@spec); $i++ ) { if ( $spec[$i] =~ /Created by $g{p4_user}/ ) { $offset = $i + 1; $spec[$i] =~ s/$g{p4_user}/$g{script}/; } if ( $spec[$i] =~ /^View:/ ) { $spec[$i+1] = "\t${p4_view}/..."; $spec[$i+2] = $g{blank}; @spec = @spec[0..$i+2]; last; } } for (my $i = 0; $i < scalar(@log); $i++) { $log[$i] = "\t$log[$i]"; } splice (@spec, $offset, 0, @log); array_to_file ($file, \@spec); my @a = qx/p4 label -i < $file 2>&1/; if ( $a[0] !~ /^Label .* saved./ ) { chomp @a; finalize (1, "Problem creating labelspec: '@a'"); } tout(0,"Created p4 labelspec '$label'."); return $label; } sub svn2p4_migrate { # Synchronize svn and p4 clients with the given svn revision. # Prepare 'add', 'delete', 'edit', 'integrate', 'branch' and # 'label' lists for the p4 client and execute the migration # based on the contents of those lists. my ($rev) = @_; my $g = \%g; my $r = $g{svn_this_change}; if ( $r->{rev} != $rev ) { # Something is wrong, sub called out of sequence? finalize (1, "Current revision is '$rev' but the log is for revision " . "'$r->{rev}'."); } $g{p4_this_rev_submitted} = false; $g{p4_changespec_created} = false; my @a = sort keys (%{$r->{path}}); $r->{count_path} = scalar (@a); $r->{count_tag} = 0; $r->{count_add} = 0; $r->{count_delete} = 0; $r->{count_edit} = 0; $r->{count_integ} = 0; if ( $r->{count_path} == 0 ) { tout (0, "File list for revision '$rev' is empty."); submit_p4_dummy_file (); return; } # Apply the exclude lists. if ( $g{svn_exclude_rev} ) { if ( exists $g{svn_exclude_rev_map}{$rev} ) { tout (0, "Skipping svn revisison '$rev' as per exclude list."); submit_p4_dummy_file (); return; } } if ( $g{svn_exclude_node} ) { my @exnodes = keys %{$g{svn_exclude_node_map}}; foreach my $exnode (@exnodes) { foreach my $node (@a) { if ( is_subnode ($node, $exnode) ) { tout (0, "Skipping svn node '$node' as per exclude list."); delete $r->{path}->{$node}; $r->{count_path}--; next; } my $s = $r->{path}->{$node}->{source}; if ( $s) { if ( is_subnode ($s, $exnode) ) { tout (0, "Skipping svn node '$node' because its source " . "'$s' is in the exclude list."); delete $r->{path}->{$node}; $r->{count_path}--; } } } } } if ( $r->{count_path} < 0 ) { finalize (1, "Problem applying the exclude list to svn change '$r->{rev}.'"); } if ( $r->{count_path} == 0 ) { tout (0, "Filtered file list for svn revision '$rev' is empty."); tout (0, "Revision '$rev' will be skipped."); submit_p4_dummy_file (); return; } # Obtain p4 paths. set_p4_depot_and_local_path (); # Obtain svn revision. tout (0, "Checking out svn revision '$rev'.") if ( $g{verbose} ); svn_checkout ($rev); # Create a pending Perforce changelist. We only need one p4 change # for every svn change we migrate so it's convenient to do it here. # If (as a result of future developments) this 1-to-1 relationship # is no longer universally observed, make sure to modify p4 change # creation logic. $g{svn_this_change}->{rev_p4} = set_p4_changespec ($g{svn_this_change}->{log}); $g{p4_this_rev} = $g{svn_this_change}->{rev_p4}; $g{p4_changespec_created} = true; # We support a general case where a single svn change handles both # branching and tagging. if ( is_svn_branchpoint () ) { migrate_svn_branch (); } if ( $g{svn2p4_tag_migrate_target} eq "label" ) { if ( is_svn_tagpoint () ) { migrate_svn_tag (); } } migrate_svn_change (); if ( $g{p4_this_rev_submitted} ) { # We take no action, change already submitted. } elsif ( is_p4_empty_changelist ($g{p4_this_rev}) ) { tout (0, "Migrating svn revision '$rev' resulted in empty p4 change, " . "will submit dummy check-in."); submit_p4_dummy_file (); } else { submit_p4_change (); } prir (0,$r,"edited_change") if $g{very_verbose}; } sub submit_p4_change { # Submit pending p4 change and update change number. my $ar = run_p4_cmd ("submit -c $g{p4_this_rev}"); my @a = @$ar; if ( $a[$#a] =~ /renamed change/ ) { @a = split / /, $a[$#a]; $g{p4_this_rev} = $a[4]; $g{svn_this_change}->{rev_p4} = $a[4]; } $g{p4_this_rev_submitted} = true; $g{p4_changespec_created} = false; tout (0, "Submitted p4 change '$g{p4_this_rev}'."); } sub is_p4_empty_changelist { # Return true if the given p4 pending changelist is empty. my $ar = run_p4_cmd ("opened -c $g{p4_this_rev}"); if ($ar->[0] =~ /not opened on this client/ || $ar->[0] =~ /not opened anywhere/ ) { return true; } return false; } sub is_svn_branchpoint { # Return true if current svn change is a branchpoint. $g{svn_this_change}{is_branchpoint} = false; my $r = $g{svn_this_change}->{path}; my @a = sort keys (%{$r}); if ( scalar(@a) == 0 ) { return false; } my $p = $a[0]; if ($p =~ /^.*\/branches\/.*$/ ) { $_ = $p; s/^.*\/branches\///; if ( $_ !~ /.*\/.*/ && $r->{$p}->{action} eq "A" && $r->{$p}->{source} && $r->{$p}->{source_rev} ) { $g{svn_this_change}{is_branchpoint} = true; return true; } } return false; } sub is_svn_tagpoint { # Return true if current svn revision is a tagpoint. $g{svn_this_change}{is_tagpoint} = false; if ( defined $g{svn_this_change}->{path_tag} ) { $g{svn_this_change}{is_tagpoint} = true; return true; } return false; } sub get_svn_source_for_p4_label { # Return svn source that was used for creating the given p4 label. # TODO: Needs to be refined. my ($label) = @_; my $ar = run_p4_cmd ("files \@$label"); my $p = $ar->[0]; my @a = split /\//, $p; $ar = undef; while ( my ($k, $v) = each %{$g{svn2p4_repo_map}} ) { if ( starts_with ($p, $v) ) { $p =~ s/$v\///; @a = split /\//, $p; return "$k/$a[0]"; } } finalize (1, "Unable to determine svn source for p4 label '$label'."); } sub migrate_svn_branch { # Create new p4 branchspec and migrate svn branch using the spec. my $r = $g{svn_this_change}->{path}; my @path = sort keys (%{$r}); if ( scalar(@path) == 0 ) { return; } my ($p4_branchspec, $source, $source_p4, $source_p4_local, $source_rev_p4, $source_rev_svn, $svn_branch_name, $svn_branch_path, $p4_branch_name, $p4_branch_path, $p4_branch_path_local, $label, $label_svn_source) = (); my $requires_add = false; my $requires_edit = false; my $source_is_tag = false; # Identify branch name. my $p = $path[0]; my $x = $p; $x =~ s/^.*\/branches\///; $svn_branch_name = $x; $svn_branch_path = $p; $source = $r->{$p}->{source}; $source_rev_svn = $r->{$p}->{source_rev}; $source_rev_p4 = $svn2p4_rev_map{$source_rev_svn}; $r->{$p}->{source_rev_p4} = $source_rev_p4; if ( $source =~ /\/tags\// && $g{svn2p4_tag_migrate_target} eq "label" ) { # Branch source is a tag, we need to branch from an existing p4 label. $label = $source; $label =~ s/\/tags\///; $label = "svn2p4_tag_$label"; if ( ! is_p4_label ($label) ) { finalize (1, "Cannot branch from label '$label': The label does not exist."); } $source_is_tag = true; $source_rev_p4 = $label; $label_svn_source = get_svn_source_for_p4_label ($label); ( $source_p4, $source_p4_local ) = get_p4_depot_and_local_path ($label_svn_source); } else { ( $source_p4, $source_p4_local ) = get_p4_depot_and_local_path ($source); } $r->{$p}->{p4_depot_path} = $source_p4; $r->{$p}->{p4_local_path} = $source_p4_local; ( $p4_branch_path, $p4_branch_path_local ) = get_p4_depot_and_local_path ($svn_branch_path); $r->{$p}->{svn_local_path} = "$g{svn_working_copy}${p}"; if ( $p4_branch_path eq $source_p4 ) { finalize (1, "Cannot branch '$source_p4' onto itself."); } $p4_branchspec = "svn2p4_$svn_branch_name"; # One scenario should be uncommon (the branch does not reside # at the top level of the 'branches' folder) but it's clearly # possible. The branch will still be migrated but no brachspec # will be created. # TODO: # No immediate need to cover it. #... #if ( $svn_branch =~ /\// ) { # $svn_branch=~ s/^.*\///; #} #... # $p4_branch_name = $svn_branch_name; tout (0, "Migrating svn branch '$svn_branch_name': ${source}:${source_rev_svn} -> " . "${p4_branch_path}/...\@${source_rev_p4}"); # Note if the branchspec already exists. It means the branch # needs to be overlayed in Perforce as it was in Subversion. if ( ! is_p4_branchspec ($p4_branchspec) ) { set_p4_branchspec ($p4_branchspec, $source_p4, $p4_branch_path); } else { tout (0, "Branchspec '$p4_branchspec' already exists. It will be reset."); $g{p4_branchspec_exists} = true; # Reset the spec run_p4_cmd ("branch -d $p4_branchspec"); set_p4_branchspec ($p4_branchspec, $source_p4, $p4_branch_path); } # Set p4 depot and local paths for other entries in the revision. # If the source is a tag and there are additional entries in the # list, we need to review and implement a solution for such a # scenario. if ( $source_is_tag && scalar (@path) > 1 ) { finalize (1, "Still need to implement support for multiple actions " . "when branching from a tag."); } for (my $i = 1; $i < scalar (@path); $i++) { $p = $path[$i]; $x = $p; if ( $r->{$p}->{action} eq "D" ) { # All changes already done. } elsif ( $r->{$p}->{action} eq "R" ) { # Replacement can come from the same source (different revision) or # from other source and revision. my $so = $r->{$p}->{source}; my $so_rev_svn = $r->{$p}->{source_rev}; my $so_rev_p4 = $svn2p4_rev_map{$so_rev_svn}; my ($so_p4, $so_p4_local ) = get_p4_depot_and_local_path ($so); $r->{$p}->{source_rev_p4} = $so_rev_p4; $r->{$p}->{p4_depot_path_source} = $so_p4; $r->{$p}->{p4_local_path_source} = $so_p4_local; # Replacing from a source may work as integrate/edit # so we need to account for possible file changes that # were introduced by edit after the file was integrated. # Let's re-add the files, this will copy the files from # svn working copy. $g{svn_this_change}{path_add}->{$p} = $r->{$p}; $g{svn_this_change}{count_add}++; $requires_add = true; } elsif ( $r->{$p}->{action} eq "A" ) { if ( $r->{$p}->{source} ) { # Addition can come from the same source (different revision) or # from other source and revision. my $so = $r->{$p}->{source}; my $so_rev_svn = $r->{$p}->{source_rev}; my $so_rev_p4 = $svn2p4_rev_map{$so_rev_svn}; my ($so_p4, $so_p4_local ) = get_p4_depot_and_local_path ($so); $r->{$p}->{source_rev_p4} = $so_rev_p4; $r->{$p}->{p4_depot_path_source} = $so_p4; $r->{$p}->{p4_local_path_source} = $so_p4_local; # Replacing from a source may work as integrate/edit # so we need to account for possible file changes that # were introduced by edit after the file was integrated. # Let's re-add the files, this will copy the files from # svn working copy. $g{svn_this_change}{path_add}->{$p} = $r->{$p}; $g{svn_this_change}{count_add}++; $requires_add = true; } else { # Adding files while branching. $g{svn_this_change}{path_add}->{$p} = $r->{$p}; delete $r->{$p}; $g{svn_this_change}{count_add}++; $g{svn_this_change}{count_path}--; $requires_add = true; } } elsif ( $r->{$p}->{action} eq "M" ) { # Branch/edit if ( $r->{$p}->{source} ) { finalize (1, "Still need to implement 'M' action with source."); } else { $g{svn_this_change}{path_edit}->{$p} = $r->{$p}; delete $r->{$p}; $g{svn_this_change}{count_edit}++; $g{svn_this_change}{count_path}--; $requires_edit = true; } } else { finalize (1, "Still need to implement branching actions other than 'branch', " . "'add', 'edit', 'replace' and 'delete'."); } } # Migrate the branch. my $p4_change = $g{p4_this_rev}; my $ar; if ( $g{p4_branchspec_exists} ) { my $ar = run_p4_cmd ("integrate -d -f -c $p4_change -b $p4_branchspec \@${source_rev_p4}"); # To test filespec based integration: #my $ar = run_p4_cmd # ("integrate -c $p4_change -d -f \"$source_p4/...\@${source_rev_p4}\" \"$p4_branch_path/...\""); run_p4_cmd ("resolve -at"); } else { my $ar = run_p4_cmd ("integrate -f -d -c $p4_change -b $p4_branchspec \@${source_rev_p4}"); } # As a general case it's a branch/edit command so we have to support the "edit" part: # Make sure files get copied from svn workspace after they are opened for branching. # TODO: The below code needs to be cleaned up or better replaced. # #my $p4_root; #my $svn_root; # #foreach my $f (@$ar) { # $f =~ s/#[0-9]+.*$//; # if ( !$svn_root ) { # while ( my ($k,$v) = each %{$g{svn2p4_repo_map}} ) { # if ( starts_with ($f, $v) ) { # $p4_root = $v; # $svn_root = $k; # #tout (0, "p4_root = $p4_root"); # #tout (0, "svn_root = $svn_root"); # last; # } # } # } # my $f_svn = $f; # $f_svn =~ s/$p4_root/$svn_root/; # my ($f_p4, $f_p4_local) = get_p4_depot_and_local_path ($f_svn); # my $f_svn_local = "$g{svn_working_copy}$f_svn"; # if ( -f $f_svn_local ) { # if ( -e $f_p4_local && ! -w $f_p4_local ) { # #tout (0, "Copying to read-only file '$f_p4_local'."); # chmod 0775, $f_p4_local; # } # cp ($f_svn_local, $f_p4_local); # } else { # #tout (0, "File '$f_svn_local' not found."); # } #} if ( $requires_add || $requires_edit ) { # Need to update path list. @path = sort keys (%${r}); } for (my $i = 1; $i < scalar (@path); $i++) { $p = $path[$i]; if ( $r->{$p}->{action} eq "D" ) { $x = append_p4_wildcard ($r->{$p}->{p4_depot_path}, $r->{$p}->{p4_local_path}, $p); run_p4_cmd ("revert -c $p4_change \"$x\""); } elsif ( $r->{$p}->{action} eq "R" ) { $x = append_p4_wildcard ($r->{$p}->{p4_depot_path_source}, $r->{$p}->{p4_local_path_source}, $p); my $y = append_p4_wildcard ($r->{$p}->{p4_depot_path}, $r->{$p}->{p4_local_path}, $p); run_p4_cmd ("revert -c $p4_change \"$y\""); run_p4_cmd ("integrate -f -d -c $p4_change \"$x\@$r->{$p}->{source_rev_p4}\" \"$y\""); } elsif ( $r->{$p}->{action} eq "A" ) { $x = append_p4_wildcard ($r->{$p}->{p4_depot_path_source}, $r->{$p}->{p4_local_path_source}, $p); my $y = append_p4_wildcard ($r->{$p}->{p4_depot_path}, $r->{$p}->{p4_local_path}, $p); run_p4_cmd ("integrate -f -d -c $p4_change \"$x\@$r->{$p}->{source_rev_p4}\" \"$y\""); } else { finalize (1, "Still need to implement branching action '$r->{$p}->{action}'."); } } if ( $requires_add ) { migrate_svn_change_add (); } if ( $requires_edit ) { migrate_svn_change_edit (); } } sub append_p4_wildcard { # Append the '/...' wildcard to the given p4 path if it is # a directory. my ($p4_path,$p4_path_local,$svn_path) = @_; if ( -e $p4_path_local ) { $p4_path = "$p4_path/..." if ( -d $p4_path_local ); } else { #my $hr = get_svn_path_info ("$g{svn_url}/$svn_path"); my $hr = get_svn_path_info ("$g{svn_working_copy}/$svn_path"); if (!$hr) { finalize (1, "Unable to determine node kind for '$p4_path'."); } if ( $hr->{'Node Kind'} eq "directory" ) { $p4_path = "$p4_path/..."; } } return $p4_path; } sub migrate_svn_tag { # Create new p4 label and migrate svn tag to that label. my $r = $g{svn_this_change}->{path_tag}; my @path = sort keys (%{$r}); my $p; for ( my $i = 0; $i < scalar(@path); $i++ ) { if ( $path[$i] =~ /^.*\/tags\/*/ ) { $p = $path[$i]; } } if ( is_new_tags_dir ($p) ) { tout (0, "Skipping a redundant step: " . "creation of the '$p' directory."); $g{svn_this_change}{is_tagpoint} = false; return; } # Make sure there are no unexpected changes in the log. my @a = sort keys (%{$g{svn_this_change}->{path}}); if ( scalar (@a) > 0 ) { tout (2, "Watch scenario: Tagging change contains non-tag actions:"); foreach my $k (@a) { tout (3, "'$k'"); } # finalize (1); } my ($label, $source, $source_p4, $source_p4_local, $source_rev_p4, $source_rev_svn, $tag, $tag_path, $source_label, $source_is_tag, $label_svn_source) = (); $source_is_tag = false; # Identify tag name. my $x = $p; $x =~ s/^.*\/tags\///; $tag = $x; $label = "svn2p4_tag_$tag"; if ( is_p4_label ($label) ) { if ( $r->{$p}->{action} eq "D" ) { run_p4_cmd ("label -d $label"); submit_p4_dummy_file (); return; } } $tag_path = $p; $source = $r->{$p}->{source}; if ( !defined ($source) && $r->{$p}->{action} ne "D" ) { # All label changes require source. No source means we have # a change that cannot be emulated with p4 labels. We print an # error message and quit. The issue should be resolved manually. finalize (1, "No source for a tag change. This action cannot be " . "emulated with p4 labels."); } if ( $source =~ /\/tags\// ) { # Tag source is a tag, we need to replicate an existing p4 label. $source_label = $source; $source_label =~ s/\/tags\///; $source_label = "svn2p4_$source_label"; if ( ! is_p4_label ($source_label) ) { finalize (1, "Cannot make a label from label '$source_label': The label does not exist."); } $source_is_tag = true; $source_rev_p4 = $source_label; $source_rev_svn = $r->{$p}->{source_rev}; $label_svn_source = get_svn_source_for_p4_label ($source_label); ( $source_p4, $source_p4_local ) = get_p4_depot_and_local_path ($label_svn_source); } else { $source_rev_svn = $r->{$p}->{source_rev}; $source_rev_p4 = $svn2p4_rev_map{$source_rev_svn}; $r->{$p}->{source_rev_p4} = $source_rev_p4; ( $source_p4, $source_p4_local ) = get_p4_depot_and_local_path ($source); $r->{$p}->{p4_depot_path} = $source_p4; $r->{$p}->{p4_local_path} = $source_p4_local; $r->{$p}->{svn_local_path} = "$g{svn_working_copy}${p}"; } my %parent = (); $parent{"$source_p4"} = $source_rev_p4; $label = "svn2p4_tag_$tag"; # The below scenario should be uncommon (the tag does not # reside at the top level of the 'tags' folder). # No immediate need to cover it. #... #if ( $label =~ /\// ) { # $label =~ s/^.*\///; #} #... # tout (0, "Migrating svn tag '$tag': ${source}:${source_rev_svn} -> " . "${source_p4}/...\@${source_rev_p4}"); # Stop if the label already exists. This means previous label # creation step failed or this revision is to patch an existing # label. The latter scenario is considered uncommon so it # will be covered when we actually encounter it. if ( is_p4_label ($label) ) { finalize (1, "Label '$label' already exists."); } set_p4_labelspec ($label, $source_p4); # Create label from label if ( $source_is_tag ) { run_p4_cmd ("labelsync -l $label \@$source_label"); submit_p4_dummy_file (); return; } # Set p4 depot and local paths for other entries in the revision. for (my $i = 1; $i < scalar (@path); $i++) { $p = $path[$i]; $x = $p; if ( $r->{$p}->{action} eq "D" ) { $x =~ s/^$tag_path/$source_p4/; $r->{$p}->{p4_depot_path} = $x; $x =~ s/^$source_p4/$source_p4_local/; $r->{$p}->{p4_local_path} = $x; $r->{$p}->{svn_local_path} = "$g{svn_working_copy}${p}"; } elsif ( $r->{$p}->{action} eq "R" ) { # Replacement can come from the same source (different revision) or # from other source and revision. my $so = $r->{$p}->{source}; my $so_rev_svn = $r->{$p}->{source_rev}; my $so_rev_p4 = $svn2p4_rev_map{$so_rev_svn}; my ($so_p4, $so_p4_local ) = get_p4_depot_and_local_path ($so); $r->{$p}->{source_rev_p4} = $so_rev_p4; $r->{$p}->{p4_depot_path} = $so_p4; $r->{$p}->{p4_local_path} = $so_p4_local; } elsif ( $r->{$p}->{action} eq "A" ) { # New addition can come from the same source (different revision) or # from other source and revision. my $so = $r->{$p}->{source}; my $so_rev_svn = $r->{$p}->{source_rev}; my $so_rev_p4 = $svn2p4_rev_map{$so_rev_svn}; my ($so_p4, $so_p4_local ) = get_p4_depot_and_local_path ($so); $r->{$p}->{source_rev_p4} = $so_rev_p4; $r->{$p}->{p4_depot_path} = $so_p4; $r->{$p}->{p4_local_path} = $so_p4_local; #my $hr = get_svn_path_info ("$g{svn_url}/$so"); #if ( $hr->{'Node Kind'} eq 'directory' ) { # $parent{"$so_p4"} = $so_rev_p4; #} } else { finalize (1, "Still need to implement tagging actions other than " . "'add', 'delete' and 'replace."); } } # Populate the label. run_p4_cmd ("tag -l $label \"$source_p4/...\@${source_rev_p4}\""); for (my $i = 1; $i < scalar (@path); $i++) { $p = $path[$i]; if ( $r->{$p}->{action} eq "D" ) { $x = $r->{$p}->{p4_depot_path}; #$x = append_p4_wildcard ($x, $r->{$p}->{p4_local_path}, $p); # # TODO: The above call may fail if we delete an item that was # added as part of this tag creation from a different # source/revision. Since source/revision is not known we should # preserve it in the %parent hash and then use it here. # Until it's done, run the below command twice assuming both file and directory # kind. # It will fail to remove the entries from the tag if the source is # different from the tag source but we can leave with it for now. # run_p4_cmd ("tag -d -l $label \"$x\""); $x = "$x/..."; run_p4_cmd ("tag -d -l $label \"$x\""); } elsif ( $r->{$p}->{action} eq "R" ) { $x = $r->{$p}->{p4_depot_path}; if ( ! starts_with ($x, $source_p4) ) { # TODO: Need to implement solution for this scenario # that is not supported directly in Perforce labels. #finalize (1, "Tag replace source '$x' differs from " . tout (2, "Tag replace source '$x' differs from " . "tag source '$source_p4'."); } #$x = append_p4_wildcard ($r->{$p}->{p4_depot_path}, $r->{$p}->{p4_local_path}, $r->{$p}->{source}); run_p4_cmd ("tag -l $label \"$x\@$r->{$p}->{source_rev_p4}\""); $x = "$x/..."; run_p4_cmd ("tag -l $label \"$x\@$r->{$p}->{source_rev_p4}\""); } elsif ( $r->{$p}->{action} eq "A" ) { $x = $r->{$p}->{p4_depot_path}; if ( ! starts_with ($x, $source_p4) ) { # finalize (1, "Tag add source '$x' differs from " . tout (2, "Tag add source '$x' differs from " . "tag source '$source_p4'."); } #$x = append_p4_wildcard ($r->{$p}->{p4_depot_path}, $r->{$p}->{p4_local_path}, $r->{$p}->{source}); run_p4_cmd ("tag -l $label \"$x\@$r->{$p}->{source_rev_p4}\""); $x = "$x/..."; run_p4_cmd ("tag -l $label \"$x\@$r->{$p}->{source_rev_p4}\""); } else { finalize (1, "Still need to implement tagging action '$r->{$p}->{action}'."); } } # TODO: svn changeset may include other changes in addition to tag # creation. Postpone empty check-in until all transactions in the # log get processed. # #submit_p4_dummy_file (); } sub get_p4_depot_and_local_path { # Return p4 depot path and p4 local path for the given svn # repository path, based on svn2p4_repo_map and p4 client view. my ($svn_path) = @_; my ($p4_depot_path, $p4_local_path) = (); my $s = get_stem ($svn_path, $g{svn2p4_repo_map_svn}, 1); my $s1 = get_stem ($g{svn2p4_repo_map}->{$s}, $g{p4_clientspec_view}, 2); $_ = $svn_path; s/^$s/$g{svn2p4_repo_map}->{$s}/; $p4_depot_path = $_; s/^$s1/$g{p4_clientspec}->{view}->{$s1}/; s/\/\/$g{p4_client}/$g{p4_clientspec}->{root}/; $p4_local_path = $_; return ($p4_depot_path, $p4_local_path); } sub get_p4_filelist { # Generate a list of files in the given p4 node. my ($p) = @_; my @a = (); if ( is_svn_file ($p) ) { push @a, $p; return \@a; } my ($svn_root, $p4_root) = (); foreach my $k ( @{$g{svn2p4_repo_map_svn}} ) { if ( starts_with ($p, $k) ) { $svn_root = $k; $p4_root = $g{svn2p4_repo_map}{$k}; last; } } my $r = $g{svn_this_change}{path}; my $p4_path = "$r->{$p}->{p4_depot_path}/..."; my $list = run_p4_cmd ("files \"$p4_path\""); foreach my $f (@$list) { $f =~ s/#[0-9]+.*$//; $f =~ s/$p4_root/$svn_root/; my ($a, $b) = get_p4_depot_and_local_path ($f); $r->{$f}->{svn_local_path} = "$g{svn_working_copy}$f"; $r->{$f}->{p4_depot_path} = $a; $r->{$f}->{p4_local_path} = $b; push @a, $f; } return \@a; } sub migrate_svn_change { # Migrate svn changes that are not branching or tagging. my $r = $g{svn_this_change}; my @a = sort keys %{$r->{path}}; if ( scalar(@a) == 0 ) { return; } foreach my $p (@a) { my $action = $r->{path}->{$p}->{action}; my $source = $r->{path}->{$p}->{source}; if ( $action eq "A" ) { if ( $source && ($source ne $p) ) { # Integration $r->{path_integ}->{$p} = $r->{path}->{$p}; $r->{count_integ}++; $r->{count_path}--; tout (0, "Added to 'path_integ': '$p'.") if $g{very_verbose}; } else { # Adding nodes my $ar = []; $ar->[0] = $p; $r->{count_path}--; if ( $source ) { # Re-adding deleted files $ar = get_p4_filelist ($p); } foreach my $f (@$ar) { $r->{path_add}->{$f} = $r->{path}->{$f}; $r->{count_add}++; tout (0, "Added to 'path_add': '$f'.") if $g{very_verbose}; } } } elsif ( $action eq "D" ) { # Deleting nodes $r->{path_delete}->{$p} = $r->{path}->{$p}; $r->{count_delete}++; $r->{count_path}--; tout (0, "Added to 'path_delete': '$p'.") if $g{very_verbose}; } elsif ( $action eq "M" ) { if ( $source ) { # Integrating nodes $r->{path_integ}->{$p} = $r->{path}->{$p}; $r->{count_integ}++; $r->{count_path}--; tout (0, "Added to 'path_integ': '$p'.") if $g{very_verbose}; } else { # Editing nodes $r->{path_edit}->{$p} = $r->{path}->{$p}; $r->{count_edit}++; $r->{count_path}--; tout (0, "Added to 'path_edit': '$p'.") if $g{very_verbose}; } } elsif ( $action eq "R" ) { # Replacing nodes $r->{path_integ}->{$p} = $r->{path}->{$p}; $r->{count_integ}++; $r->{count_path}--; tout (0, "Added new entry to 'path_integ': '$p'.") if $g{very_verbose}; } else { # It must be something we have not seen before, or the script # failed to correctly read/interpret the change log. finalize (1, "Unrecognized svn action: '$action'."); } delete $g{svn_this_change}{path}{$p}; } # Validate add/delete lists: if a node is listed as both "A" and "D" # we can skip it altogether. Otherwise, "add" will fail because the # node can't be copied from svn workspace, it's been already deleted # by svn. if ( $r->{count_add} && $r->{count_delete} ) { my @a = keys (%{$r->{path_add}}); foreach my $p (@a) { if ( $r->{path_delete}->{$p} ) { tout (0, "Skipping redundant add/delete for node '$p'."); delete $r->{path_add}->{$p}; $r->{count_add}--; delete $r->{path_delete}->{$p}; $r->{count_delete}--; } } } # Final pass @a = keys (%{$r->{path_add}}); foreach my $p (@a) { if ( ! -e "$r->{path_add}->{$p}->{svn_local_path}" ) { finalize (1, "Cannot add '$r->{path_add}->{$p}->{p4_depot_path}', " . "svn node '$r->{path_add}->{$p}->{svn_local_path}' not found."); # TODO: Uncomment the below code to treat this condition as a warning. # #tout (2, "Skipping add for '$r->{path_add}->{$p}->{p4_depot_path}', " . # "svn node '$r->{path_add}->{$p}->{svn_local_path}' not found."); #delete $r->{path_add}->{$p}; #$r->{count_add}--; } } if ( $r->{count_path} != 0 ) { # Error condition, count_path is expected to be zero now. foreach my $k ( sort keys %{$r->{path}} ) { tout (1, "Problem interpreting svn change '$k'."); } finalize 1; } delete $g{svn_this_change}{path}; # Now to actual migration. if ( $r->{count_integ} > 0 ) { migrate_svn_change_integ (); } if ( $r->{count_add} > 0 ) { migrate_svn_change_add (); } if ( $r->{count_edit} > 0 ) { migrate_svn_change_edit (); } if ( $r->{count_delete} > 0 ) { migrate_svn_change_delete (); } if ( $r->{count_tag} > 0 ) { migrate_svn_change_tag (); } } sub migrate_svn_change_add { # Migrate add changes. my $r = $g{svn_this_change}{path_add}; my %dir = (); foreach my $p (sort keys (%{$r})) { my $svn_path = $r->{$p}->{svn_local_path}; my $p4_path = $r->{$p}->{p4_local_path}; my $p4_depot_path = $r->{$p}->{p4_depot_path}; if ( -d $svn_path ) { if ( ! -d $p4_path ) { md ($p4_path); } $dir{$p4_path} = $p4_depot_path; } elsif ( -f $svn_path ) { my $bdir = dirname ($p4_path); md ($bdir) if ( ! -d $bdir ); if ( -e $p4_path && ! -w $p4_path ) { chmod 0775, "$p4_path"; tout (2, "Copying to read-only file '$p4_path'."); } cp ($svn_path, $p4_path); run_p4_cmd ("add -c $g{p4_this_rev} \"$p4_depot_path\""); } else { unexpected_path_entry ($svn_path); } } foreach my $d (sort keys (%dir)) { if ( is_empty_dir ($d) ) { my $f = "$d/$g{p4_dummy_file}"; my $fd = "$dir{$d}/$g{p4_dummy_file}"; touch ($f); run_p4_cmd ("add -c $g{p4_this_rev} -t text \"$fd\""); } } } sub migrate_svn_change_delete { # Migrate delete changes. my $r = $g{svn_this_change}{path_delete}; foreach my $p (sort keys (%{$r})) { my $svn_path = $r->{$p}->{svn_local_path}; my $p4_path = $r->{$p}->{p4_local_path}; my $p4_depot_path = $r->{$p}->{p4_depot_path}; # svn_path location is already deleted in svn working copy # so use p4_path consistently. if ( -f $p4_path ) { my $ar = run_p4_cmd ("delete -c $g{p4_this_rev} \"$p4_depot_path\""); if ( $ar->[0] =~ /already opened/ ) { run_p4_cmd ("revert -c $g{p4_this_rev} \"$p4_depot_path\""); run_p4_cmd ("delete -c $g{p4_this_rev} \"$p4_depot_path\""); } } elsif ( -d $p4_path ) { # Recursively delete files in the directory. run_p4_cmd ("delete -c $g{p4_this_rev} \"$p4_depot_path/...\""); #$g{tmp_file_list} = []; #$g{tmp_dir_name} = $p4_path; #find ( \&wanted, $p4_path ); #foreach my $f (sort (@{$g{tmp_file_list}}) ) { # $f = "$p4_depot_path/$f"; # run_p4_cmd ("delete -c $g{p4_this_rev} \"$f\""); #} #delete $g{tmp_file_list}; #delete $g{tmp_dir_name}; } elsif ( ! -e $p4_path ) { tout (0, "No p4 action required: Path '$p4_path' has already been deleted."); next; } else { unexpected_path_entry ($p4_path); } } } sub migrate_svn_change_edit { my $r = $g{svn_this_change}{path_edit}; my %dir = (); foreach my $p (sort keys (%{$r})) { my $svn_path = $r->{$p}->{svn_local_path}; my $p4_path = $r->{$p}->{p4_local_path}; my $p4_depot_path = $r->{$p}->{p4_depot_path}; if ( -f $svn_path ) { run_p4_cmd ("edit -c $g{p4_this_rev} \"$p4_depot_path\""); cp ($svn_path, $p4_path); } elsif ( -d $svn_path ) { # Edit on directory is likely to change svn properties # that are not relevant to p4. tout (0, "Skipping edit on directory '$svn_path'."); next; } else { unexpected_path_entry ($svn_path); } } } sub migrate_svn_change_integ { # Migrate integrations. my $r = $g{svn_this_change}->{path_integ}; my @path = sort keys (%{$r}); for (my $i = 0; $i < scalar (@path); $i++) { my $p = $path[$i]; my $x = $p; if ( $r->{$p}->{action} eq "D" ) { # All changes already done. } elsif ( $r->{$p}->{action} eq "R" ) { if ( $r->{$p}->{source} ) { # Replace by integration. # Replacement can come from the same source (different revision) or # from other source and revision. my $so = $r->{$p}->{source}; my $so_rev_svn = $r->{$p}->{source_rev}; my $so_rev_p4 = $svn2p4_rev_map{$so_rev_svn}; my ($so_p4, $so_p4_local ) = get_p4_depot_and_local_path ($so); $r->{$p}->{source_rev_p4} = $so_rev_p4; $r->{$p}->{p4_depot_path_source} = $so_p4; $r->{$p}->{p4_local_path_source} = $so_p4_local; # Treat this change as integrate/edit. $g{svn_this_change}{path_edit}->{$p} = $r->{$p}; $g{svn_this_change}{count_edit}++; } else { # No source involved, emulate this action by simple edit. $g{svn_this_change}{path_edit}->{$p} = $r->{$p}; $g{svn_this_change}{count_edit}++; } } elsif ( $r->{$p}->{action} eq "A" ) { # Addition can come from the same source (different revision) or # from other source and revision. my $so = $r->{$p}->{source}; my $so_rev_svn = $r->{$p}->{source_rev}; my $so_rev_p4 = $svn2p4_rev_map{$so_rev_svn}; my ($so_p4, $so_p4_local ) = get_p4_depot_and_local_path ($so); $r->{$p}->{source_rev_p4} = $so_rev_p4; $r->{$p}->{p4_depot_path_source} = $so_p4; $r->{$p}->{p4_local_path_source} = $so_p4_local; # Treat this change as integrate/edit. $g{svn_this_change}{path_edit}->{$p} = $r->{$p}; $g{svn_this_change}{count_edit}++; } else { finalize (1, "Still need to implement integ actions other than 'add' and 'replace'."); } } for (my $i =0; $i < scalar (@path); $i++) { my $p = $path[$i]; if ( $r->{$p}->{action} eq "D" ) { # No action } elsif ( $r->{$p}->{action} eq "R" ) { if ( $r->{$p}->{source} ) { my $x = append_p4_wildcard ($r->{$p}->{p4_depot_path_source}, $r->{$p}->{p4_local_path_source}, $p); my $y = append_p4_wildcard ($r->{$p}->{p4_depot_path}, $r->{$p}->{p4_local_path}, $p); #run_p4_cmd ("revert -c $p4_change \"$y\""); my $ar = run_p4_cmd ("integrate -f -d -c $g{p4_this_rev} \"$x\@$r->{$p}->{source_rev_p4}\" \"$y\""); foreach my $s (@$ar) { if ( $s =~ /without -i flag/ ) { # Re-integrate with the -i flag. run_p4_cmd ("integrate -f -d -i -c $g{p4_this_rev} \"$x\@$r->{$p}->{source_rev_p4}\" \"$y\""); last; } } run_p4_cmd ("resolve -at \"$y\""); } else { # Same logic as edit. } } elsif ( $r->{$p}->{action} eq "A" ) { my $x = append_p4_wildcard ($r->{$p}->{p4_depot_path_source}, $r->{$p}->{p4_local_path_source}, $p); my $y = append_p4_wildcard ($r->{$p}->{p4_depot_path}, $r->{$p}->{p4_local_path}, $p); my $ar = run_p4_cmd ("integrate -f -d -c $g{p4_this_rev} \"$x\@$r->{$p}->{source_rev_p4}\" \"$y\""); if ( $ar->[0] =~ /already integrated/ ) { # We need to overlay tout (0, "This change requires forced integration."); run_p4_cmd ("integrate -f -d -c $g{p4_this_rev} \"$x\@$r->{$p}->{source_rev_p4}\" \"$y\""); } run_p4_cmd ("resolve -at \"$y\""); } else { finalize (1, "Still need to implement integ action '$r->{$p}->{action}'."); } } } sub migrate_svn_change_tag { # This sub handles rare cases pertaining to tags such as # tag patching (changing tag contents after it's been # created). The edge case of creating a top level /tags # folder in svn is also handled here. my $r = $g{svn_this_change}; foreach my $p (sort keys %{$r->{path_tag}}) { if ( is_new_tags_dir ($p) ) { tout (0, "Skipping a redundant step: " . "creation of the '$p' directory."); next; } # Handle any other tag-related special cases here. tout (2, "Skipping edits to the tag: migrate_svn_change_tag functionality not imlemented."); } } sub is_empty_dir { # Return true if the given directory is empty. my ($dir) = @_; opendir DIR, $dir; while (my $p = readdir DIR) { next if ( $p =~ /^\.\.?$/ ); closedir DIR; return false; } closedir DIR; return true; } sub is_new_tags_dir { # Return true if the given svn action creates a /tags directory. my ($p) = @_; my $r = $g{svn_this_change}->{path_tag}->{$p}; if ( $r->{action} eq "A" && !$r->{source} && ends_with($p,"/tags") ) { return true; } return false; } sub set_p4_depot_and_local_path { my $g = \%g; my $r = $g->{svn_this_change}; my @a = sort keys ( %{$r->{path}} ); foreach my $p (@a) { if ( $g{svn2p4_tag_migrate_target} eq "label" ) { # TODO: Move this code to migrate_svn_tag. if ( $p =~ /^.*\/tags\/.*/ ) { #if ( starts_with ($p, '\/tags\/') ) { # TODO: Won't work if there's a tags directory that # is not related to svn tags. $r->{path_tag}->{$p} = $r->{path}->{$p}; delete ($g{svn_this_change}{path}{$p}); $r->{count_tag}++; $r->{count_path}--; tout (0, "Added new entry to 'path_tag': '$p'.") if $g{very_verbose}; next; } } $r->{path}->{$p}->{svn_local_path} = "$g->{svn_working_copy}${p}"; ($r->{path}->{$p}->{p4_depot_path}, $r->{path}->{$p}->{p4_local_path}) = get_p4_depot_and_local_path ($p); } } sub signal_handler { # Handle any system inrerrupts. my $interrupt = shift; finalize (1, "Recieved signal: ${interrupt}."); } sub starts_with { # Return true if string $s starts with string $x. my ($s, $x) = @_; $x = quotemeta($x); return $s =~ m/^$x/; } sub touch { # Touch a file. my ($f) = @_; open (FILE, ">>$f") or finalize (1, "Problem touching file '$f'."); tout (0, "Touched file '$f'.") if $g{very_verbose}; close (FILE); } sub submit_p4_dummy_file { # Submit a dummy check-in for svn revision that does not require # p4 check-in so that each svn change always has a corresponding # p4 change. if ( !$g{p4_changespec_created} ) { $g{svn_this_change}->{rev_p4} = set_p4_changespec ($g{svn_this_change}->{log}); $g{p4_this_rev} = $g{svn_this_change}->{rev_p4}; $g{p4_changespec_created} = true; } my $p4_change = $g{svn_this_change}->{rev_p4}; if ( ! -f "$g{p4_dummy_file_default_path_local}" ) { touch "$g{p4_dummy_file_default_path_local}"; run_p4_cmd ("add -c $p4_change $g{p4_dummy_file_default_path_local}"); } else { run_p4_cmd ("edit -c $p4_change $g{p4_dummy_file_default_path_local}"); } submit_p4_change (); tout (0, "Submitted dummy change to '$g{p4_dummy_file_default_path}'.") } sub svn_checkout { my ($rev) = @_; #my $a = run_cmd ("svn co --ignore-externals -r $rev $g{svn_url}"); my $stem = $g{svn_this_change}->{common_stem}; my @a = (); if ( ref($stem) eq 'HASH' ) { @a = keys (%$stem); } else { @a = ( $stem ); } foreach $stem (@a) { my $ar = run_cmd ("svn update --ignore-externals -r $rev " . "'$g{svn_working_copy}$stem'"); if ( $g{very_verbose} ) { foreach my $entry (@$ar) { tout (0, "svn: $entry"); } } } } sub tout { # Print a time-stamped message and optionally increment # err_count and wrn_count for error and warning messages. # Include caller method id for debug messages. # Message types: 0-note, 1-error, 2-warning, 3-debug my ($a, $b) = @_; my @s = (); my $final = false; if ( $a < 0 ) { $a = -$a; $final = true; } if ($a eq '3' || $g{debug}) { @s = caller 1; $s[3] = $g{blank} if !$s[3]; $s[3] = "($s[3]):"; } else { $s[3] = $g{blank}; } if ($a eq "0") { $a = $g{blank}; } elsif ($a eq "1") { bump_err_count $final; $a = "ERROR: "; } elsif ($a eq "2") { bump_wrn_count $final; $a = "WARNING: "; } elsif ($a eq "3") { $a = "DEBUG: "; } $s[0] = localtime; $a = "$s[0] $s[3] $a$b\n"; out $a; return $a; } sub trim { # Trim whitespace in a string or array. This is a recursive procedure # that can process array references to any depth. my ($s) = @_; return undef if !defined($s); if ( ref($s) eq 'ARRAY' ) { for (my $i = 0; $i < scalar(@$s); $i++) { $s->[$i] = trim($s->[$i]); } } else { $s =~ s/^\s*(.*?)\s*$/$1/; } return $s; } sub unexpected_path_entry { # Report unexpected path entry and finalize. my ($p) = @_; my @a = stat ($p); if ( scalar (@a) == 0 ) { finalize (1, "Path '$p' does not exist."); } my $mode = scalar (@a) > 0 ? $a[2] : $g{blank}; finalize (1, "Expected file or directory, found '$p' ($a[2])."); } sub wanted { if ( -f ) { my $x = $File::Find::name; # Redundant assignment to suppress a spurious warning from # the module: my $y = $File::Find::name; $x =~ s/$g{tmp_dir_name}\///; push (@{$g{tmp_file_list}}, $x); } } # ------------ # --- Main --- # ------------ sub main { open_log (); greeting (); tout (0, "Opened process log '$g{log_file}'."); if ( ! $g{make_svn_repo_map_and_quit} ) { get_p4_info (); tout (0, "Obtained p4 server info."); get_p4_clientspec (); tout (0, "Obtained p4 clientspec '$g{p4_client}'."); set_p4_dummy_file_default_path_local (); } tout (0, "Accessing svn via the URL '$g{svn_url}'."); get_svn_info (); tout (0, "Obtained svn server and client info."); get_svn_conf_data (); get_svn_head_rev (); tout (0, "Head svn revision is '$g{svn_head_rev}'."); tout (0, "Starting the migration with the following properties."); prig (); if ( $g{make_svn_repo_map_and_quit} ) { tout (0, "Obtaining complete svn revision log."); get_svn_all_rev_log (); tout (0, "Generating svn repository tree dependency mapping."); # $g{silent} = true; get_svn_repo_map (); tout (0, "Generating the list of redundant nodes."); get_svn_repo_map_to_skip (); # $g{silent} = false; tout (0, "Writing the list of redundant nodes to " . "'$g{svn2p4_repo_map_to_skip_file}'."); save_svn_repo_map_to_skip (); finalize 0; } my $rev = get_svn_next_rev (); # Migration proper while ( $rev <= get_svn_head_rev () ) { tout (0, "Migrating svn revision '$rev'."); get_svn_rev_log ($rev); svn2p4_migrate ($rev); set_svn_last_rev ($rev); # Uncomment the below line to migrate one revision at a time # (useful for testing and debugging purposes). # # finalize 0; $rev = get_svn_next_rev (); # Uncomment the below line to stop migration at specific svn revision # (useful for testing and debugging purposes). # # if ( $rev > 5000 ) { finalize (0); } } finalize 0; } main ();