#!/usr/bin/perl -w #line 4 my $Id = q$Id: apc2svn 201 2006-02-25 06:29:11Z k $; warn "WARNING: script is not maintained anymore; please file a feature request on rt.cpan.org if you want to continue to use it and indicate if you can take it over\n"; sleep 3; use strict; use File::Path qw(rmtree); use Getopt::Long; use File::Basename; use File::Spec (); use File::Temp (); use Perl::Repository::APC2SVN qw(url_latest_change get_dirs_to_add get_dirs_to_delete delete_empty_dirs); our $Signal = 0; our $MPV; $SIG{INT} = $SIG{TERM} = sub { print "Caught SIG$_[0]; please stand by, I'm leaving as soon as possible...\n"; $Signal++; }; our %Opt; sub Usage (){ qq{Usage: $0 OPTIONS --apc # APC base directory --debug # be more verbose --h # this help page --password # password (not needed for file: URLs) --q # quiet --singlestep # feed the patches one at a time to perlpatch2svn --sw_or_co # set to "co" (default) to prefer checkout over switch # set to "sw" otherwise --url # SVN repository (default: http://localhost/svn/perl) --wc # directory of the working copy }; } GetOptions(\%Opt, "apc=s", "bounds=s", "debug!", "h!", "password=s", "q!", "singlestep!", "sw_or_co=s", "url=s", "wc=s", ) or die Usage; if ($Opt{h}) { print Usage; exit; } sub mysystem (@); sub contains_cr ($); sub svn_mkdir_minus_p ($$); sub makepatch_version (); sub myls ($); # $Opt{url} ||= "http://localhost/svn/perl"; $Opt{url} ||= "file:///usr/local/svn/perl"; $Opt{wc} ||= "perl-wc"; my @passwordarg; $Opt{password} and @passwordarg = "--password=$Opt{password}"; $Opt{apc} ||= "APC"; for my $option (qw(apc wc)) { if (File::Spec->file_name_is_absolute($Opt{$option})) { $Opt{$option} = File::Spec->abs2rel($Opt{$option}); } } $Opt{singlestep} = 0 unless defined $Opt{singlestep}; $Opt{sw_or_co} ||= "co"; # svn 0.17.1 seems to have problems with switch unless ($Opt{sw_or_co} =~ /^(co|sw)$/) { die "Illegal value for sw_or_co[$Opt{sw_or_co}]: only 'co' or 'sw' are allowed"; } if ($Opt{debug}) { our $DEBUG = 1; } my $owning_wc = 0; my(%latest_change); use Perl::Repository::APC; my $apc_repo = Perl::Repository::APC->new($Opt{apc}); APCDIR: for my $apcdir ($apc_repo->apcdirs){ my($apc_branch,$pver,@patches) = @$apcdir; exit if $Signal; if ( $latest_change{$apc_branch} && $latest_change{$apc_branch} > $patches[-1] # if ==, we might # still need # branching and # tagging ) { next APCDIR; } my($work_branch, $park_branch_parent); my $tag_branch_parent = "tags/branchpoints"; my $rel_branch_parent = "tags/releases"; if ($apc_branch eq "perl") { $work_branch = "trunk"; $park_branch_parent = "branches/perl"; } else { $work_branch = "branches/$apc_branch/mbranch"; $park_branch_parent = "branches/$apc_branch/rel"; if ($pver eq "5.004_00") { # special case: starts empty } elsif (myls "$Opt{url}/$work_branch") { # if this branch already exists, continue there } else { # else create it with cp from the tagged branch-point svn_mkdir_minus_p $Opt{url}, "branches/$apc_branch"; my $from = $pver; # before we had maint-5.6/perl-5.6.2, we had only to care for if (index($apc_branch, "/") > 0) { if ($apc_branch eq "maint-5.6/perl-5.6.2") { $from = "5.6.1"; } else { die "PANIC ($0): Unknown apc_branch[$apc_branch]"; } } else { # 5.6.1 => 5.6.0, 5.004_01 => 5.004_00 $from =~ s/1$/0/; } mysystem svn => "cp", @passwordarg, "-m", "Generating maint branch $apc_branch from $from for $pver", "$Opt{url}/tags/branchpoints/$from", "$Opt{url}/branches/$apc_branch/mbranch"; } } exit if $Signal; $latest_change{$apc_branch} = url_latest_change("$Opt{url}/$work_branch"); if ($latest_change{$apc_branch} > $patches[-1]) { next APCDIR; } for my $dir ($work_branch, $park_branch_parent, $tag_branch_parent, $rel_branch_parent) { die "dir empty value" unless $dir; svn_mkdir_minus_p $Opt{url}, $dir; } warn sprintf "#### %-15s %10s %6d %6d ####\n", $apc_branch, $pver, $patches[0], $patches[-1]; # APPLY PATCHES if ($latest_change{$apc_branch} < $patches[-1]) { if ($owning_wc && $Opt{sw_or_co} eq "sw") { # the first time # through we don't own # it chdir $Opt{wc}; mysystem svn => "switch", "-q", "$Opt{url}/$work_branch" or die; chdir ".."; } else { rmtree $Opt{wc}; mysystem svn => "co", "-q", @passwordarg, "$Opt{url}/$work_branch", $Opt{wc} or die; $owning_wc=1; } exit if $Signal; { chdir $Opt{wc} or die "Could not chdir to $Opt{wc}: $!"; my $brancharg; if ($apc_branch eq "perl") { $brancharg = ""; } elsif (index($apc_branch, "/") > 0) { # special case for maint-5.6/perl-5.6.2 $brancharg = " --branch $apc_branch"; } else { $brancharg = " --branch $apc_branch/perl"; } our $DEBUG; my $debugarg = $DEBUG ? " --debug" : ""; my $want_singlestep; if ( $Opt{singlestep} || # "open" branches that have or may have changed a little since # we were here last time ( $latest_change{$apc_branch} >= $patches[0] && $latest_change{$apc_branch} < $patches[-1] ) ) { $want_singlestep = 1; } if ($want_singlestep) { PATCH: for my $patch (0..$#patches){ my $nr = $patches[$patch]; next PATCH if $latest_change{$apc_branch} >= $nr; my $gz = File::Spec->catfile($Opt{apc}, $pver, "diffs", "$nr.gz"); my $upgz = File::Spec->catfile(File::Spec->updir,$gz); if ($Opt{bounds}) { die "Illegal arguments[$Opt{bounds}] to bounds" unless $Opt{bounds} =~ /^(\d+)-(\d+)$/; my($lower,$upper) = ($1,$2); next PATCH if $nr < $lower or $nr > $upper; } printf "Trying %s (%sb)\n", $gz, -s $upgz; my($n) = $nr; $n = sprintf "%05d", $n; # -f is less verbose and faster mysystem("zcat $upgz | perlpatch2svn -f$brancharg$debugarg") or die; exit if $Signal; } } else { mysystem("zcat ../$Opt{apc}/$pver/diffs/*.gz | ". "perlpatch2svn$brancharg$debugarg") or die; } printf "Finished checkin of %s\n", $pver; $latest_change{$apc_branch} = $patches[-1]; chdir ".."; exit if $Signal; } } # DETERMINE TARBALL (we ignore RCs [RELEASE CANDIDATES]) opendir my $DIR, "$Opt{apc}/$pver" or die; my(@dirent) = grep !/RC|TRIAL/, grep /^perl.*\.tar\.gz$/, readdir $DIR; closedir $DIR; die "\aALERT: (\@dirent > 1: @dirent) in $Opt{apc}/$pver" if @dirent>1; if (@dirent) { # SVN TAGGING AND BRANCHING unless (myls "$Opt{url}/$park_branch_parent/$pver") { chdir $Opt{wc} or die "Could not chdir to $Opt{wc}: $!"; mysystem svn => "cp", @passwordarg, "-m", "Branching $pver", "$Opt{url}/$work_branch", "$Opt{url}/$park_branch_parent/$pver"; print "Branched $pver\n"; chdir ".."; } unless (myls "$Opt{url}/$tag_branch_parent/$pver") { chdir $Opt{wc} or die "Could not chdir to $Opt{wc}: $!"; mysystem svn => "cp", @passwordarg, "-m", "Tagging branching point $pver", "$Opt{url}/$work_branch", "$Opt{url}/$tag_branch_parent/$pver"; print "Tagged branching point $pver\n"; chdir ".."; } # DETERMINE TARBALL's ROOT DIRECTORY my $tarball = $dirent[0]; if (myls "$Opt{url}/$rel_branch_parent/$tarball") { next APCDIR; } open my $TAR, "tar tzf $Opt{apc}/$pver/$tarball |" or die; my $tardir = <$TAR>; chomp $tardir; $tardir =~ s|^\./||; $tardir =~ s|/.*$||; close $TAR; print "dirent[@dirent]tardir[$tardir]\n"; # $tardir/ is the directory that we get if we untar the ball now # and "$Opt{wc}/ is the directory we have to compare against. Note # that we have to eliminate CR in tardir/ # HANDLE TARBALL COMPARISON AND PATCH SVN's COPY # MAKEPATCH rmtree $tardir; mysystem tar => "xzf", "$Opt{apc}/$pver/$tarball" or die "Could not run tar"; my @ccr = mani_unCR($tardir); # must be nomanifest because either manifest may be wrong unless ($MPV) { $MPV = makepatch_version; die "Your version of makepatch ($MPV) is not recent enough, 2.00 is needed" unless $MPV >= 2.00; } my(undef,$mpfile) = File::Temp::tempfile; $mpfile = File::Spec->rel2abs($mpfile); mysystem("makepatch '-diff=diff -u' -nomanifest ". "-description '$park_branch_parent/$pver vs. $tardir' ". "-exclude .svn ". "$Opt{wc} $tardir > $mpfile") or die "Could not run makepatch"; print "Makepatch $pver done\n"; rmtree $tardir; # APPLYPATCH { if ($Opt{sw_or_co} eq "co") { rmtree $Opt{wc}; mysystem svn => "co", "-q", "$Opt{url}/$park_branch_parent/$pver", $Opt{wc} or die "Could not co"; chdir $Opt{wc}; } else { chdir $Opt{wc}; mysystem svn => "switch", "-q", "$Opt{url}/$park_branch_parent/$pver" or die "Could not switch"; } # applypatch is at the mercy of patch and sometimes exits with # error code although we're fine:-( So no check for the return # value here: mysystem "applypatch $mpfile"; #### svn add/delete: my($adds,$deletes) = parse_applypatch_data($mpfile); unlink $mpfile; if (@$adds){ unshift @$adds, get_dirs_to_add(@$adds) ; mysystem svn => "add", @$adds; } mysystem svn => "rm", @$deletes if @$deletes; delete_empty_dirs(@$deletes); # so that commit always has something to do: mysystem svn => "propset", "perl:release", $pver, "."; # why native? so that Windows people get what they need. # why not CRLF? so that even Unix people can patch the file. mysystem svn => "propset", "svn:eol-style", "native", @ccr if @ccr; mysystem svn => "ci", "-m", "Released as $tarball with rootdir $tardir branched at $pver"; mysystem svn => "cp", @passwordarg, "-m", "Release", "$Opt{url}/$work_branch", "$Opt{url}/$rel_branch_parent/$tarball"; chdir ".."; } } else { print "For $pver there is no tarfile to check in; nothing left to do.\n"; } exit if $Signal; } sub svn_mkdir_minus_p ($$) { my($root,$mkdir) = @_; die "mkdir no value" unless $mkdir; my $ipath = ""; for my $idir (split m|/|, $mkdir) { $ipath = $ipath ? "$ipath/$idir" : $idir; my $urlipath = "$root/$ipath"; unless (myls $urlipath) { mysystem(svn => "mkdir", "-m" => "mkdir $ipath", $urlipath) or die; } } } sub myls ($) { my $ls = shift; die "myls() called with illegal argument [$ls]: must be a URL" unless index($ls,"/") > -1; my($parent,$child) = $ls =~ m|^(.+/)([^/]+)$|; open my $fh, "svn ls $parent|" or return 0; while (<$fh>) { chomp; if (m|^\Q$child\E/?$|){ # warn "Info ls: $ls exists\n"; return 1; } } close $fh; return 0; } sub contains_cr ($) { my($file) = shift; open my $fh, $file or die "Couldn't open $file: $!"; local($/) = "\n"; my $firstline = <$fh>; defined $firstline && $firstline =~ /\cM/; } sub mysystem (@) { my @system = @_; warn sprintf("%s: Running (%s)\n", scalar(localtime), join(",",map {"\"$_\""} @system), ) unless $Opt{"q"}; system(@system)==0; } sub parse_applypatch_data { my $file = shift; my(@crea, @remo); open my $fh, $file or die "Could not open $file: $!"; while (<$fh>) { next unless / ^ \#\#\#\# \s ApplyPatch \s data \s follows /x; last; } while (<$fh>) { last if / ^ \#\#\#\# \s End \s of \s ApplyPatch \s data /x; next unless / ^ \# \s ([cr]) \s (.*) /x; my $spec1 = $1; my $spec2 = $2; require Text::ParseWords; my(@spec2) = Text::ParseWords::shellwords($spec2); if ($spec1 eq "c") { push @crea, $spec2[0]; } else { push @remo, $spec2[0]; } } (\@crea,\@remo); } sub mani_unCR { my($tardir) = @_; my @ccr; my $mani = "$tardir/MANIFEST"; open my $fh, $mani or die "Could not open $mani: $!"; while (<$fh>) { my($file) = /(\S+)/ or next; my $intar_file = $file; $intar_file =~ s|^|$tardir/|; next unless contains_cr $intar_file; push @ccr, $file; @ARGV = $intar_file; $^I=""; while (<>) { # in 5.7.1 we had files that contained 0x0d0d0a on line endings s/[\r\n]+\z/\n/; print; } } close $fh; @ccr; } sub makepatch_version () { open my $fh, "makepatch --version 2>&1 |" or die "Could not run makepatch"; local $/ = "\n"; my $v; while (<$fh>) { next unless /^This is makepatch version ([\d\.]+)/; $v = $1; } close $fh; # cannot check return value, --version exits with error # code (at least with makepatch 1.16) $v; } __END__ =head1 NAME apc2svn - Import APC into subversion with resume-where-it-left-off =head1 SYNOPSIS apc2svn --h # describes all options =head1 DESCRIPTION Apply Rafael's perlpatch2svn to all diffs in APC skipping already applied patches. An initial run of this script took on my 1 GHz Athlon about 12 hours. Update 2003-09: 17 hours with my Athlon 2800. Yes, subversion is getting slower. We assume, our current directory can be used as a working directory with only one subdirectory: APC. This is a mirror of rsync://ftp.linux.activestate.com/all-of-the-APC-for-mirrors-only/ We further assume that the --url parameter is either an empty Subversion repository or one left over from a previous session. It should provide space for several GB of data. Note that after C<svnadmin create> you need to tweak the db/DB_CONFIG file: increase the values of the three lock variables to 8000. Run an C<svnadmin recover> afterwards. The latter may be redundant, but better safe than sorry. The partition holding the svn repository should either have about 12 GB or you must remove BerkeleyDB log files while the script is running. See C<db_archive> how to do that. If the log files are removed, subversion needs hardly more space than the pure compressed patches. The current directory is assumed to be a working directory for our own work. This script will create the subdirectory perl-wc/ (configurable via the --wc option), and we'll untar all historic perl distributions within the current directory, so we will create temporary directories like C<perl5.004_52> etc. B<Note http URLs:> performance was catastrophic for me via http/DAV. B<Note file URLs:> User must be same group as owner, umask should be 002. File URLs did not work for me for unknown reasons with subversion before 0.17.1. They worked fine with 0.17.1 and BerkeleyDB 4.0.14 =head1 REPOSITORY LAYOUT The following file system layout is realized in the repository: trunk/ branches/ perl/ 5.004_50/ ... 5.6.0/ ... 5.8.0/ ... 5.9.0/ not before there is a perl-5.9.0.tar.gz maint-5.004/ mbranch/ rel/ 5.004_00/ 5.004_01/ 5.004_02/ 5.004_03/ 5.004_04/ 5.004_05/ maint-5.005/ mbranch/ rel/ 5.005_01/ 5.005_02/ 5.005_03/ 5.005_04/ not before there is a perl-5.005_04.tar.gz maint-5.6/ mbranch/ rel/ 5.6.1/ 5.6.2/ not before there is a perl-5.6.2.tar.gz maint-5.8/ mbranch/ rel/ 5.8.1/ not before there is a perl-5.8.1.tar.gz tags/ branchpoints/ 5.004_00/ 5.004_01/ ... 5.004_50/ ... releases/ perl5.004.tar.gz/ perl5.004_01.tar.gz/ perl5.004_02.tar.gz/ perl5.004_03.tar.gz/ perl5.004_04.tar.gz/ perl5.004_05.tar.gz/ perl5.004_50.tar.gz/ perl5.004_51.tar.gz/ perl5.004_52.tar.gz/ perl5.004_53.tar.gz/ ... The final layout is still subject to change and will be discussed on the Perl5-porters mailing list. The directory branches/perl/ contains perl releases made from the trunk. There is already integrated the diff between the branchpoint and the final release. One can find the branchpoint in tags/branchpoints/, too, and the final release in tags/releases/. Apc2svn sets a property "perl:release" as soon as a release is integrated. From that point in time they should be regarded as read-only. All branches/maint-*/mbranch/ directories contain the respective maintainance branches themselves. If new patches for any of them turn up, they are integrated there. branches/maint-*/rel/ contains releases from the corresponding maintainance track. As with the trunk we find the branchpoints in tags/branchpoints/ too and the final releases are duplicated in tags/releases/. All that implies that only the trunk/ directory and the branches/maint-*/mbranch/ directories should be regarded read-write. =head1 BUGS Error checking of the many calls to external programs need some more work. Currently many failures of external programs are tolerated. =head1 PREREQUISITES Same prerequisites as mentioned in patchaperlup. Additionally makepatch and applypatch by Johan Vromans (CPAN author JV), svn, zcat, tar. =head1 AUTHOR andreas.koenig@anima.de and Rafael Garcia-Suarez =cut