| #!/usr/bin/perl |
| |
| # This tool is copyright (c) 2005, Matthias Urlichs. |
| # It is released under the Gnu Public License, version 2. |
| # |
| # The basic idea is to pull and analyze SVN changes. |
| # |
| # Checking out the files is done by a single long-running SVN connection. |
| # |
| # The head revision is on branch "origin" by default. |
| # You can change that with the '-o' option. |
| |
| use strict; |
| use warnings; |
| use Getopt::Std; |
| use File::Copy; |
| use File::Spec; |
| use File::Temp qw(tempfile); |
| use File::Path qw(mkpath); |
| use File::Basename qw(basename dirname); |
| use Time::Local; |
| use IO::Pipe; |
| use POSIX qw(strftime dup2); |
| use IPC::Open2; |
| use SVN::Core; |
| use SVN::Ra; |
| |
| die "Need SVN:Core 1.2.1 or better" if $SVN::Core::VERSION lt "1.2.1"; |
| |
| $SIG{'PIPE'}="IGNORE"; |
| $ENV{'TZ'}="UTC"; |
| |
| our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T, |
| $opt_b,$opt_r,$opt_I,$opt_A,$opt_s,$opt_l,$opt_d,$opt_D,$opt_S,$opt_F, |
| $opt_P,$opt_R); |
| |
| sub usage() { |
| print STDERR <<END; |
| usage: ${\basename $0} # fetch/update GIT from SVN |
| [-o branch-for-HEAD] [-h] [-v] [-l max_rev] [-R repack_each_revs] |
| [-C GIT_repository] [-t tagname] [-T trunkname] [-b branchname] |
| [-d|-D] [-i] [-u] [-r] [-I ignorefilename] [-s start_chg] |
| [-m] [-M regex] [-A author_file] [-S] [-F] [-P project_name] [SVN_URL] |
| END |
| exit(1); |
| } |
| |
| getopts("A:b:C:dDFhiI:l:mM:o:rs:t:T:SP:R:uv") or usage(); |
| usage if $opt_h; |
| |
| my $tag_name = $opt_t || "tags"; |
| my $trunk_name = defined $opt_T ? $opt_T : "trunk"; |
| my $branch_name = $opt_b || "branches"; |
| my $project_name = $opt_P || ""; |
| $project_name = "/" . $project_name if ($project_name); |
| my $repack_after = $opt_R || 1000; |
| my $root_pool = SVN::Pool->new_default; |
| |
| @ARGV == 1 or @ARGV == 2 or usage(); |
| |
| $opt_o ||= "origin"; |
| $opt_s ||= 1; |
| my $git_tree = $opt_C; |
| $git_tree ||= "."; |
| |
| my $svn_url = $ARGV[0]; |
| my $svn_dir = $ARGV[1]; |
| |
| our @mergerx = (); |
| if ($opt_m) { |
| my $branch_esc = quotemeta ($branch_name); |
| my $trunk_esc = quotemeta ($trunk_name); |
| @mergerx = |
| ( |
| qr!\b(?:merg(?:ed?|ing))\b.*?\b((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i, |
| qr!\b(?:from|of)\W+((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i, |
| qr!\b(?:from|of)\W+(?:the )?([\w\.\-]+)[-\s]branch\b!i |
| ); |
| } |
| if ($opt_M) { |
| unshift (@mergerx, qr/$opt_M/); |
| } |
| |
| # Absolutize filename now, since we will have chdir'ed by the time we |
| # get around to opening it. |
| $opt_A = File::Spec->rel2abs($opt_A) if $opt_A; |
| |
| our %users = (); |
| our $users_file = undef; |
| sub read_users($) { |
| $users_file = File::Spec->rel2abs(@_); |
| die "Cannot open $users_file\n" unless -f $users_file; |
| open(my $authors,$users_file); |
| while(<$authors>) { |
| chomp; |
| next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/; |
| (my $user,my $name,my $email) = ($1,$2,$3); |
| $users{$user} = [$name,$email]; |
| } |
| close($authors); |
| } |
| |
| select(STDERR); $|=1; select(STDOUT); |
| |
| |
| package SVNconn; |
| # Basic SVN connection. |
| # We're only interested in connecting and downloading, so ... |
| |
| use File::Spec; |
| use File::Temp qw(tempfile); |
| use POSIX qw(strftime dup2); |
| use Fcntl qw(SEEK_SET); |
| |
| sub new { |
| my($what,$repo) = @_; |
| $what=ref($what) if ref($what); |
| |
| my $self = {}; |
| $self->{'buffer'} = ""; |
| bless($self,$what); |
| |
| $repo =~ s#/+$##; |
| $self->{'fullrep'} = $repo; |
| $self->conn(); |
| |
| return $self; |
| } |
| |
| sub conn { |
| my $self = shift; |
| my $repo = $self->{'fullrep'}; |
| my $auth = SVN::Core::auth_open ([SVN::Client::get_simple_provider, |
| SVN::Client::get_ssl_server_trust_file_provider, |
| SVN::Client::get_username_provider]); |
| my $s = SVN::Ra->new(url => $repo, auth => $auth, pool => $root_pool); |
| die "SVN connection to $repo: $!\n" unless defined $s; |
| $self->{'svn'} = $s; |
| $self->{'repo'} = $repo; |
| $self->{'maxrev'} = $s->get_latest_revnum(); |
| } |
| |
| sub file { |
| my($self,$path,$rev) = @_; |
| |
| my ($fh, $name) = tempfile('gitsvn.XXXXXX', |
| DIR => File::Spec->tmpdir(), UNLINK => 1); |
| |
| print "... $rev $path ...\n" if $opt_v; |
| my (undef, $properties); |
| $path =~ s#^/*##; |
| my $subpool = SVN::Pool::new_default_sub; |
| eval { (undef, $properties) |
| = $self->{'svn'}->get_file($path,$rev,$fh); }; |
| if($@) { |
| return undef if $@ =~ /Attempted to get checksum/; |
| die $@; |
| } |
| my $mode; |
| if (exists $properties->{'svn:executable'}) { |
| $mode = '100755'; |
| } elsif (exists $properties->{'svn:special'}) { |
| my ($special_content, $filesize); |
| $filesize = tell $fh; |
| seek $fh, 0, SEEK_SET; |
| read $fh, $special_content, $filesize; |
| if ($special_content =~ s/^link //) { |
| $mode = '120000'; |
| seek $fh, 0, SEEK_SET; |
| truncate $fh, 0; |
| print $fh $special_content; |
| } else { |
| die "unexpected svn:special file encountered"; |
| } |
| } else { |
| $mode = '100644'; |
| } |
| close ($fh); |
| |
| return ($name, $mode); |
| } |
| |
| sub ignore { |
| my($self,$path,$rev) = @_; |
| |
| print "... $rev $path ...\n" if $opt_v; |
| $path =~ s#^/*##; |
| my $subpool = SVN::Pool::new_default_sub; |
| my (undef,undef,$properties) |
| = $self->{'svn'}->get_dir($path,$rev,undef); |
| if (exists $properties->{'svn:ignore'}) { |
| my ($fh, $name) = tempfile('gitsvn.XXXXXX', |
| DIR => File::Spec->tmpdir(), |
| UNLINK => 1); |
| print $fh $properties->{'svn:ignore'}; |
| close($fh); |
| return $name; |
| } else { |
| return undef; |
| } |
| } |
| |
| sub dir_list { |
| my($self,$path,$rev) = @_; |
| $path =~ s#^/*##; |
| my $subpool = SVN::Pool::new_default_sub; |
| my ($dirents,undef,$properties) |
| = $self->{'svn'}->get_dir($path,$rev,undef); |
| return $dirents; |
| } |
| |
| package main; |
| use URI; |
| |
| our $svn = $svn_url; |
| $svn .= "/$svn_dir" if defined $svn_dir; |
| my $svn2 = SVNconn->new($svn); |
| $svn = SVNconn->new($svn); |
| |
| my $lwp_ua; |
| if($opt_d or $opt_D) { |
| $svn_url = URI->new($svn_url)->canonical; |
| if($opt_D) { |
| $svn_dir =~ s#/*$#/#; |
| } else { |
| $svn_dir = ""; |
| } |
| if ($svn_url->scheme eq "http") { |
| use LWP::UserAgent; |
| $lwp_ua = LWP::UserAgent->new(keep_alive => 1, requests_redirectable => []); |
| } else { |
| print STDERR "Warning: not HTTP; turning off direct file access\n"; |
| $opt_d=0; |
| } |
| } |
| |
| sub pdate($) { |
| my($d) = @_; |
| $d =~ m#(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)# |
| or die "Unparseable date: $d\n"; |
| my $y=$1; $y-=1900 if $y>1900; |
| return timegm($6||0,$5,$4,$3,$2-1,$y); |
| } |
| |
| sub getwd() { |
| my $pwd = `pwd`; |
| chomp $pwd; |
| return $pwd; |
| } |
| |
| |
| sub get_headref($$) { |
| my $name = shift; |
| my $git_dir = shift; |
| my $sha; |
| |
| if (open(C,"$git_dir/refs/heads/$name")) { |
| chomp($sha = <C>); |
| close(C); |
| length($sha) == 40 |
| or die "Cannot get head id for $name ($sha): $!\n"; |
| } |
| return $sha; |
| } |
| |
| |
| -d $git_tree |
| or mkdir($git_tree,0777) |
| or die "Could not create $git_tree: $!"; |
| chdir($git_tree); |
| |
| my $orig_branch = ""; |
| my $forward_master = 0; |
| my %branches; |
| |
| my $git_dir = $ENV{"GIT_DIR"} || ".git"; |
| $git_dir = getwd()."/".$git_dir unless $git_dir =~ m#^/#; |
| $ENV{"GIT_DIR"} = $git_dir; |
| my $orig_git_index; |
| $orig_git_index = $ENV{GIT_INDEX_FILE} if exists $ENV{GIT_INDEX_FILE}; |
| my ($git_ih, $git_index) = tempfile('gitXXXXXX', SUFFIX => '.idx', |
| DIR => File::Spec->tmpdir()); |
| close ($git_ih); |
| $ENV{GIT_INDEX_FILE} = $git_index; |
| my $maxnum = 0; |
| my $last_rev = ""; |
| my $last_branch; |
| my $current_rev = $opt_s || 1; |
| unless(-d $git_dir) { |
| system("git init"); |
| die "Cannot init the GIT db at $git_tree: $?\n" if $?; |
| system("git read-tree --empty"); |
| die "Cannot init an empty tree: $?\n" if $?; |
| |
| $last_branch = $opt_o; |
| $orig_branch = ""; |
| } else { |
| -f "$git_dir/refs/heads/$opt_o" |
| or die "Branch '$opt_o' does not exist.\n". |
| "Either use the correct '-o branch' option,\n". |
| "or import to a new repository.\n"; |
| |
| -f "$git_dir/svn2git" |
| or die "'$git_dir/svn2git' does not exist.\n". |
| "You need that file for incremental imports.\n"; |
| open(F, "git symbolic-ref HEAD |") or |
| die "Cannot run git-symbolic-ref: $!\n"; |
| chomp ($last_branch = <F>); |
| $last_branch = basename($last_branch); |
| close(F); |
| unless($last_branch) { |
| warn "Cannot read the last branch name: $! -- assuming 'master'\n"; |
| $last_branch = "master"; |
| } |
| $orig_branch = $last_branch; |
| $last_rev = get_headref($orig_branch, $git_dir); |
| if (-f "$git_dir/SVN2GIT_HEAD") { |
| die <<EOM; |
| SVN2GIT_HEAD exists. |
| Make sure your working directory corresponds to HEAD and remove SVN2GIT_HEAD. |
| You may need to run |
| |
| git-read-tree -m -u SVN2GIT_HEAD HEAD |
| EOM |
| } |
| system('cp', "$git_dir/HEAD", "$git_dir/SVN2GIT_HEAD"); |
| |
| $forward_master = |
| $opt_o ne 'master' && -f "$git_dir/refs/heads/master" && |
| system('cmp', '-s', "$git_dir/refs/heads/master", |
| "$git_dir/refs/heads/$opt_o") == 0; |
| |
| # populate index |
| system('git', 'read-tree', $last_rev); |
| die "read-tree failed: $?\n" if $?; |
| |
| # Get the last import timestamps |
| open my $B,"<", "$git_dir/svn2git"; |
| while(<$B>) { |
| chomp; |
| my($num,$branch,$ref) = split; |
| $branches{$branch}{$num} = $ref; |
| $branches{$branch}{"LAST"} = $ref; |
| $current_rev = $num+1 if $current_rev <= $num; |
| } |
| close($B); |
| } |
| -d $git_dir |
| or die "Could not create git subdir ($git_dir).\n"; |
| |
| my $default_authors = "$git_dir/svn-authors"; |
| if ($opt_A) { |
| read_users($opt_A); |
| copy($opt_A,$default_authors) or die "Copy failed: $!"; |
| } else { |
| read_users($default_authors) if -f $default_authors; |
| } |
| |
| open BRANCHES,">>", "$git_dir/svn2git"; |
| |
| sub node_kind($$) { |
| my ($svnpath, $revision) = @_; |
| $svnpath =~ s#^/*##; |
| my $subpool = SVN::Pool::new_default_sub; |
| my $kind = $svn->{'svn'}->check_path($svnpath,$revision); |
| return $kind; |
| } |
| |
| sub get_file($$$) { |
| my($svnpath,$rev,$path) = @_; |
| |
| # now get it |
| my ($name,$mode); |
| if($opt_d) { |
| my($req,$res); |
| |
| # /svn/!svn/bc/2/django/trunk/django-docs/build.py |
| my $url=$svn_url->clone(); |
| $url->path($url->path."/!svn/bc/$rev/$svn_dir$svnpath"); |
| print "... $path...\n" if $opt_v; |
| $req = HTTP::Request->new(GET => $url); |
| $res = $lwp_ua->request($req); |
| if ($res->is_success) { |
| my $fh; |
| ($fh, $name) = tempfile('gitsvn.XXXXXX', |
| DIR => File::Spec->tmpdir(), UNLINK => 1); |
| print $fh $res->content; |
| close($fh) or die "Could not write $name: $!\n"; |
| } else { |
| return undef if $res->code == 301; # directory? |
| die $res->status_line." at $url\n"; |
| } |
| $mode = '0644'; # can't obtain mode via direct http request? |
| } else { |
| ($name,$mode) = $svn->file("$svnpath",$rev); |
| return undef unless defined $name; |
| } |
| |
| my $pid = open(my $F, '-|'); |
| die $! unless defined $pid; |
| if (!$pid) { |
| exec("git", "hash-object", "-w", $name) |
| or die "Cannot create object: $!\n"; |
| } |
| my $sha = <$F>; |
| chomp $sha; |
| close $F; |
| unlink $name; |
| return [$mode, $sha, $path]; |
| } |
| |
| sub get_ignore($$$$$) { |
| my($new,$old,$rev,$path,$svnpath) = @_; |
| |
| return unless $opt_I; |
| my $name = $svn->ignore("$svnpath",$rev); |
| if ($path eq '/') { |
| $path = $opt_I; |
| } else { |
| $path = File::Spec->catfile($path,$opt_I); |
| } |
| if (defined $name) { |
| my $pid = open(my $F, '-|'); |
| die $! unless defined $pid; |
| if (!$pid) { |
| exec("git", "hash-object", "-w", $name) |
| or die "Cannot create object: $!\n"; |
| } |
| my $sha = <$F>; |
| chomp $sha; |
| close $F; |
| unlink $name; |
| push(@$new,['0644',$sha,$path]); |
| } elsif (defined $old) { |
| push(@$old,$path); |
| } |
| } |
| |
| sub project_path($$) |
| { |
| my ($path, $project) = @_; |
| |
| $path = "/".$path unless ($path =~ m#^\/#) ; |
| return $1 if ($path =~ m#^$project\/(.*)$#); |
| |
| $path =~ s#\.#\\\.#g; |
| $path =~ s#\+#\\\+#g; |
| return "/" if ($project =~ m#^$path.*$#); |
| |
| return undef; |
| } |
| |
| sub split_path($$) { |
| my($rev,$path) = @_; |
| my $branch; |
| |
| if($path =~ s#^/\Q$tag_name\E/([^/]+)/?##) { |
| $branch = "/$1"; |
| } elsif($path =~ s#^/\Q$trunk_name\E/?##) { |
| $branch = "/"; |
| } elsif($path =~ s#^/\Q$branch_name\E/([^/]+)/?##) { |
| $branch = $1; |
| } else { |
| my %no_error = ( |
| "/" => 1, |
| "/$tag_name" => 1, |
| "/$branch_name" => 1 |
| ); |
| print STDERR "$rev: Unrecognized path: $path\n" unless (defined $no_error{$path}); |
| return () |
| } |
| if ($path eq "") { |
| $path = "/"; |
| } elsif ($project_name) { |
| $path = project_path($path, $project_name); |
| } |
| return ($branch,$path); |
| } |
| |
| sub branch_rev($$) { |
| |
| my ($srcbranch,$uptorev) = @_; |
| |
| my $bbranches = $branches{$srcbranch}; |
| my @revs = reverse sort { ($a eq 'LAST' ? 0 : $a) <=> ($b eq 'LAST' ? 0 : $b) } keys %$bbranches; |
| my $therev; |
| foreach my $arev(@revs) { |
| next if ($arev eq 'LAST'); |
| if ($arev <= $uptorev) { |
| $therev = $arev; |
| last; |
| } |
| } |
| return $therev; |
| } |
| |
| sub expand_svndir($$$); |
| |
| sub expand_svndir($$$) |
| { |
| my ($svnpath, $rev, $path) = @_; |
| my @list; |
| get_ignore(\@list, undef, $rev, $path, $svnpath); |
| my $dirents = $svn->dir_list($svnpath, $rev); |
| foreach my $p(keys %$dirents) { |
| my $kind = node_kind($svnpath.'/'.$p, $rev); |
| if ($kind eq $SVN::Node::file) { |
| my $f = get_file($svnpath.'/'.$p, $rev, $path.'/'.$p); |
| push(@list, $f) if $f; |
| } elsif ($kind eq $SVN::Node::dir) { |
| push(@list, |
| expand_svndir($svnpath.'/'.$p, $rev, $path.'/'.$p)); |
| } |
| } |
| return @list; |
| } |
| |
| sub copy_path($$$$$$$$) { |
| # Somebody copied a whole subdirectory. |
| # We need to find the index entries from the old version which the |
| # SVN log entry points to, and add them to the new place. |
| |
| my($newrev,$newbranch,$path,$oldpath,$rev,$node_kind,$new,$parents) = @_; |
| |
| my($srcbranch,$srcpath) = split_path($rev,$oldpath); |
| unless(defined $srcbranch && defined $srcpath) { |
| print "Path not found when copying from $oldpath @ $rev.\n". |
| "Will try to copy from original SVN location...\n" |
| if $opt_v; |
| push (@$new, expand_svndir($oldpath, $rev, $path)); |
| return; |
| } |
| my $therev = branch_rev($srcbranch, $rev); |
| my $gitrev = $branches{$srcbranch}{$therev}; |
| unless($gitrev) { |
| print STDERR "$newrev:$newbranch: could not find $oldpath \@ $rev\n"; |
| return; |
| } |
| if ($srcbranch ne $newbranch) { |
| push(@$parents, $branches{$srcbranch}{'LAST'}); |
| } |
| print "$newrev:$newbranch:$path: copying from $srcbranch:$srcpath @ $rev\n" if $opt_v; |
| if ($node_kind eq $SVN::Node::dir) { |
| $srcpath =~ s#/*$#/#; |
| } |
| |
| my $pid = open my $f,'-|'; |
| die $! unless defined $pid; |
| if (!$pid) { |
| exec("git","ls-tree","-r","-z",$gitrev,$srcpath) |
| or die $!; |
| } |
| local $/ = "\0"; |
| while(<$f>) { |
| chomp; |
| my($m,$p) = split(/\t/,$_,2); |
| my($mode,$type,$sha1) = split(/ /,$m); |
| next if $type ne "blob"; |
| if ($node_kind eq $SVN::Node::dir) { |
| $p = $path . substr($p,length($srcpath)-1); |
| } else { |
| $p = $path; |
| } |
| push(@$new,[$mode,$sha1,$p]); |
| } |
| close($f) or |
| print STDERR "$newrev:$newbranch: could not list files in $oldpath \@ $rev\n"; |
| } |
| |
| sub commit { |
| my($branch, $changed_paths, $revision, $author, $date, $message) = @_; |
| my($committer_name,$committer_email,$dest); |
| my($author_name,$author_email); |
| my(@old,@new,@parents); |
| |
| if (not defined $author or $author eq "") { |
| $committer_name = $committer_email = "unknown"; |
| } elsif (defined $users_file) { |
| die "User $author is not listed in $users_file\n" |
| unless exists $users{$author}; |
| ($committer_name,$committer_email) = @{$users{$author}}; |
| } elsif ($author =~ /^(.*?)\s+<(.*)>$/) { |
| ($committer_name, $committer_email) = ($1, $2); |
| } else { |
| $author =~ s/^<(.*)>$/$1/; |
| $committer_name = $committer_email = $author; |
| } |
| |
| if ($opt_F && $message =~ /From:\s+(.*?)\s+<(.*)>\s*\n/) { |
| ($author_name, $author_email) = ($1, $2); |
| print "Author from From: $1 <$2>\n" if ($opt_v);; |
| } elsif ($opt_S && $message =~ /Signed-off-by:\s+(.*?)\s+<(.*)>\s*\n/) { |
| ($author_name, $author_email) = ($1, $2); |
| print "Author from Signed-off-by: $1 <$2>\n" if ($opt_v);; |
| } else { |
| $author_name = $committer_name; |
| $author_email = $committer_email; |
| } |
| |
| $date = pdate($date); |
| |
| my $tag; |
| my $parent; |
| if($branch eq "/") { # trunk |
| $parent = $opt_o; |
| } elsif($branch =~ m#^/(.+)#) { # tag |
| $tag = 1; |
| $parent = $1; |
| } else { # "normal" branch |
| # nothing to do |
| $parent = $branch; |
| } |
| $dest = $parent; |
| |
| my $prev = $changed_paths->{"/"}; |
| if($prev and $prev->[0] eq "A") { |
| delete $changed_paths->{"/"}; |
| my $oldpath = $prev->[1]; |
| my $rev; |
| if(defined $oldpath) { |
| my $p; |
| ($parent,$p) = split_path($revision,$oldpath); |
| if(defined $parent) { |
| if($parent eq "/") { |
| $parent = $opt_o; |
| } else { |
| $parent =~ s#^/##; # if it's a tag |
| } |
| } |
| } else { |
| $parent = undef; |
| } |
| } |
| |
| my $rev; |
| if($revision > $opt_s and defined $parent) { |
| open(H,'-|',"git","rev-parse","--verify",$parent); |
| $rev = <H>; |
| close(H) or do { |
| print STDERR "$revision: cannot find commit '$parent'!\n"; |
| return; |
| }; |
| chop $rev; |
| if(length($rev) != 40) { |
| print STDERR "$revision: cannot find commit '$parent'!\n"; |
| return; |
| } |
| $rev = $branches{($parent eq $opt_o) ? "/" : $parent}{"LAST"}; |
| if($revision != $opt_s and not $rev) { |
| print STDERR "$revision: do not know ancestor for '$parent'!\n"; |
| return; |
| } |
| } else { |
| $rev = undef; |
| } |
| |
| # if($prev and $prev->[0] eq "A") { |
| # if(not $tag) { |
| # unless(open(H,"> $git_dir/refs/heads/$branch")) { |
| # print STDERR "$revision: Could not create branch $branch: $!\n"; |
| # $state=11; |
| # next; |
| # } |
| # print H "$rev\n" |
| # or die "Could not write branch $branch: $!"; |
| # close(H) |
| # or die "Could not write branch $branch: $!"; |
| # } |
| # } |
| if(not defined $rev) { |
| unlink($git_index); |
| } elsif ($rev ne $last_rev) { |
| print "Switching from $last_rev to $rev ($branch)\n" if $opt_v; |
| system("git", "read-tree", $rev); |
| die "read-tree failed for $rev: $?\n" if $?; |
| $last_rev = $rev; |
| } |
| |
| push (@parents, $rev) if defined $rev; |
| |
| my $cid; |
| if($tag and not %$changed_paths) { |
| $cid = $rev; |
| } else { |
| my @paths = sort keys %$changed_paths; |
| foreach my $path(@paths) { |
| my $action = $changed_paths->{$path}; |
| |
| if ($action->[0] eq "R") { |
| # refer to a file/tree in an earlier commit |
| push(@old,$path); # remove any old stuff |
| } |
| if(($action->[0] eq "A") || ($action->[0] eq "R")) { |
| my $node_kind = node_kind($action->[3], $revision); |
| if ($node_kind eq $SVN::Node::file) { |
| my $f = get_file($action->[3], |
| $revision, $path); |
| if ($f) { |
| push(@new,$f) if $f; |
| } else { |
| my $opath = $action->[3]; |
| print STDERR "$revision: $branch: could not fetch '$opath'\n"; |
| } |
| } elsif ($node_kind eq $SVN::Node::dir) { |
| if($action->[1]) { |
| copy_path($revision, $branch, |
| $path, $action->[1], |
| $action->[2], $node_kind, |
| \@new, \@parents); |
| } else { |
| get_ignore(\@new, \@old, $revision, |
| $path, $action->[3]); |
| } |
| } |
| } elsif ($action->[0] eq "D") { |
| push(@old,$path); |
| } elsif ($action->[0] eq "M") { |
| my $node_kind = node_kind($action->[3], $revision); |
| if ($node_kind eq $SVN::Node::file) { |
| my $f = get_file($action->[3], |
| $revision, $path); |
| push(@new,$f) if $f; |
| } elsif ($node_kind eq $SVN::Node::dir) { |
| get_ignore(\@new, \@old, $revision, |
| $path, $action->[3]); |
| } |
| } else { |
| die "$revision: unknown action '".$action->[0]."' for $path\n"; |
| } |
| } |
| |
| while(@old) { |
| my @o1; |
| if(@old > 55) { |
| @o1 = splice(@old,0,50); |
| } else { |
| @o1 = @old; |
| @old = (); |
| } |
| my $pid = open my $F, "-|"; |
| die "$!" unless defined $pid; |
| if (!$pid) { |
| exec("git", "ls-files", "-z", @o1) or die $!; |
| } |
| @o1 = (); |
| local $/ = "\0"; |
| while(<$F>) { |
| chomp; |
| push(@o1,$_); |
| } |
| close($F); |
| |
| while(@o1) { |
| my @o2; |
| if(@o1 > 55) { |
| @o2 = splice(@o1,0,50); |
| } else { |
| @o2 = @o1; |
| @o1 = (); |
| } |
| system("git","update-index","--force-remove","--",@o2); |
| die "Cannot remove files: $?\n" if $?; |
| } |
| } |
| while(@new) { |
| my @n2; |
| if(@new > 12) { |
| @n2 = splice(@new,0,10); |
| } else { |
| @n2 = @new; |
| @new = (); |
| } |
| system("git","update-index","--add", |
| (map { ('--cacheinfo', @$_) } @n2)); |
| die "Cannot add files: $?\n" if $?; |
| } |
| |
| my $pid = open(C,"-|"); |
| die "Cannot fork: $!" unless defined $pid; |
| unless($pid) { |
| exec("git","write-tree"); |
| die "Cannot exec git-write-tree: $!\n"; |
| } |
| chomp(my $tree = <C>); |
| length($tree) == 40 |
| or die "Cannot get tree id ($tree): $!\n"; |
| close(C) |
| or die "Error running git-write-tree: $?\n"; |
| print "Tree ID $tree\n" if $opt_v; |
| |
| my $pr = IO::Pipe->new() or die "Cannot open pipe: $!\n"; |
| my $pw = IO::Pipe->new() or die "Cannot open pipe: $!\n"; |
| $pid = fork(); |
| die "Fork: $!\n" unless defined $pid; |
| unless($pid) { |
| $pr->writer(); |
| $pw->reader(); |
| open(OUT,">&STDOUT"); |
| dup2($pw->fileno(),0); |
| dup2($pr->fileno(),1); |
| $pr->close(); |
| $pw->close(); |
| |
| my @par = (); |
| |
| # loose detection of merges |
| # based on the commit msg |
| foreach my $rx (@mergerx) { |
| if ($message =~ $rx) { |
| my $mparent = $1; |
| if ($mparent eq 'HEAD') { $mparent = $opt_o }; |
| if ( -e "$git_dir/refs/heads/$mparent") { |
| $mparent = get_headref($mparent, $git_dir); |
| push (@parents, $mparent); |
| print OUT "Merge parent branch: $mparent\n" if $opt_v; |
| } |
| } |
| } |
| my %seen_parents = (); |
| my @unique_parents = grep { ! $seen_parents{$_} ++ } @parents; |
| foreach my $bparent (@unique_parents) { |
| push @par, '-p', $bparent; |
| print OUT "Merge parent branch: $bparent\n" if $opt_v; |
| } |
| |
| exec("env", |
| "GIT_AUTHOR_NAME=$author_name", |
| "GIT_AUTHOR_EMAIL=$author_email", |
| "GIT_AUTHOR_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)), |
| "GIT_COMMITTER_NAME=$committer_name", |
| "GIT_COMMITTER_EMAIL=$committer_email", |
| "GIT_COMMITTER_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)), |
| "git", "commit-tree", $tree,@par); |
| die "Cannot exec git-commit-tree: $!\n"; |
| } |
| $pw->writer(); |
| $pr->reader(); |
| |
| $message =~ s/[\s\n]+\z//; |
| $message = "r$revision: $message" if $opt_r; |
| |
| print $pw "$message\n" |
| or die "Error writing to git-commit-tree: $!\n"; |
| $pw->close(); |
| |
| print "Committed change $revision:$branch ".strftime("%Y-%m-%d %H:%M:%S",gmtime($date)).")\n" if $opt_v; |
| chomp($cid = <$pr>); |
| length($cid) == 40 |
| or die "Cannot get commit id ($cid): $!\n"; |
| print "Commit ID $cid\n" if $opt_v; |
| $pr->close(); |
| |
| waitpid($pid,0); |
| die "Error running git-commit-tree: $?\n" if $?; |
| } |
| |
| if (not defined $cid) { |
| $cid = $branches{"/"}{"LAST"}; |
| } |
| |
| if(not defined $dest) { |
| print "... no known parent\n" if $opt_v; |
| } elsif(not $tag) { |
| print "Writing to refs/heads/$dest\n" if $opt_v; |
| open(C,">$git_dir/refs/heads/$dest") and |
| print C ("$cid\n") and |
| close(C) |
| or die "Cannot write branch $dest for update: $!\n"; |
| } |
| |
| if ($tag) { |
| $last_rev = "-" if %$changed_paths; |
| # the tag was 'complex', i.e. did not refer to a "real" revision |
| |
| $dest =~ tr/_/\./ if $opt_u; |
| |
| system('git', 'tag', '-f', $dest, $cid) == 0 |
| or die "Cannot create tag $dest: $!\n"; |
| |
| print "Created tag '$dest' on '$branch'\n" if $opt_v; |
| } |
| $branches{$branch}{"LAST"} = $cid; |
| $branches{$branch}{$revision} = $cid; |
| $last_rev = $cid; |
| print BRANCHES "$revision $branch $cid\n"; |
| print "DONE: $revision $dest $cid\n" if $opt_v; |
| } |
| |
| sub commit_all { |
| # Recursive use of the SVN connection does not work |
| local $svn = $svn2; |
| |
| my ($changed_paths, $revision, $author, $date, $message) = @_; |
| my %p; |
| while(my($path,$action) = each %$changed_paths) { |
| $p{$path} = [ $action->action,$action->copyfrom_path, $action->copyfrom_rev, $path ]; |
| } |
| $changed_paths = \%p; |
| |
| my %done; |
| my @col; |
| my $pref; |
| my $branch; |
| |
| while(my($path,$action) = each %$changed_paths) { |
| ($branch,$path) = split_path($revision,$path); |
| next if not defined $branch; |
| next if not defined $path; |
| $done{$branch}{$path} = $action; |
| } |
| while(($branch,$changed_paths) = each %done) { |
| commit($branch, $changed_paths, $revision, $author, $date, $message); |
| } |
| } |
| |
| $opt_l = $svn->{'maxrev'} if not defined $opt_l or $opt_l > $svn->{'maxrev'}; |
| |
| if ($opt_l < $current_rev) { |
| print "Up to date: no new revisions to fetch!\n" if $opt_v; |
| unlink("$git_dir/SVN2GIT_HEAD"); |
| exit; |
| } |
| |
| print "Processing from $current_rev to $opt_l ...\n" if $opt_v; |
| |
| my $from_rev; |
| my $to_rev = $current_rev - 1; |
| |
| my $subpool = SVN::Pool::new_default_sub; |
| while ($to_rev < $opt_l) { |
| $subpool->clear; |
| $from_rev = $to_rev + 1; |
| $to_rev = $from_rev + $repack_after; |
| $to_rev = $opt_l if $opt_l < $to_rev; |
| print "Fetching from $from_rev to $to_rev ...\n" if $opt_v; |
| $svn->{'svn'}->get_log("",$from_rev,$to_rev,0,1,1,\&commit_all); |
| my $pid = fork(); |
| die "Fork: $!\n" unless defined $pid; |
| unless($pid) { |
| exec("git", "repack", "-d") |
| or die "Cannot repack: $!\n"; |
| } |
| waitpid($pid, 0); |
| } |
| |
| |
| unlink($git_index); |
| |
| if (defined $orig_git_index) { |
| $ENV{GIT_INDEX_FILE} = $orig_git_index; |
| } else { |
| delete $ENV{GIT_INDEX_FILE}; |
| } |
| |
| # Now switch back to the branch we were in before all of this happened |
| if($orig_branch) { |
| print "DONE\n" if $opt_v and (not defined $opt_l or $opt_l > 0); |
| system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master") |
| if $forward_master; |
| unless ($opt_i) { |
| system('git', 'read-tree', '-m', '-u', 'SVN2GIT_HEAD', 'HEAD'); |
| die "read-tree failed: $?\n" if $?; |
| } |
| } else { |
| $orig_branch = "master"; |
| print "DONE; creating $orig_branch branch\n" if $opt_v and (not defined $opt_l or $opt_l > 0); |
| system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master") |
| unless -f "$git_dir/refs/heads/master"; |
| system('git', 'update-ref', 'HEAD', "$orig_branch"); |
| unless ($opt_i) { |
| system('git checkout'); |
| die "checkout failed: $?\n" if $?; |
| } |
| } |
| unlink("$git_dir/SVN2GIT_HEAD"); |
| close(BRANCHES); |