65160b8b04
The regexes detecting merges (while still relying on the commit messages, though) have been improved to catch saner (and hopefully more) messages. The old regex was so generic that it often matched something else and missed the actual merge-message. Also, the regex given with the `-M' commandline-option is checked first: Explicitely given regexes should be considered better than the builtin ones, and should therefore be given a chance to match a message first. Signed-off-by: Junio C Hamano <junkio@cox.net>
925 lines
23 KiB
Perl
Executable File
925 lines
23 KiB
Perl
Executable File
#!/usr/bin/perl -w
|
|
|
|
# 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);
|
|
|
|
sub usage() {
|
|
print STDERR <<END;
|
|
Usage: ${\basename $0} # fetch/update GIT from SVN
|
|
[-o branch-for-HEAD] [-h] [-v] [-l max_rev]
|
|
[-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] [SVN_URL]
|
|
END
|
|
exit(1);
|
|
}
|
|
|
|
getopts("A:b:C:dDhiI:l:mM:o:rs:t:T:uv") or usage();
|
|
usage if $opt_h;
|
|
|
|
my $tag_name = $opt_t || "tags";
|
|
my $trunk_name = $opt_T || "trunk";
|
|
my $branch_name = $opt_b || "branches";
|
|
|
|
@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);
|
|
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);
|
|
my $pool = SVN::Pool->new();
|
|
eval { (undef, $properties)
|
|
= $self->{'svn'}->get_file($path,$rev,$fh,$pool); };
|
|
$pool->clear;
|
|
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;
|
|
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;
|
|
}
|
|
}
|
|
|
|
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-db");
|
|
die "Cannot init the GIT db at $git_tree: $?\n" if $?;
|
|
system("git-read-tree");
|
|
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 ($branch, $path, $revision) = @_;
|
|
my $pool=SVN::Pool->new;
|
|
my $kind = $svn->{'svn'}->check_path(revert_split_path($branch,$path),$revision,$pool);
|
|
$pool->clear;
|
|
return $kind;
|
|
}
|
|
|
|
sub revert_split_path($$) {
|
|
my($branch,$path) = @_;
|
|
|
|
my $svnpath;
|
|
$path = "" if $path eq "/"; # this should not happen, but ...
|
|
if($branch eq "/") {
|
|
$svnpath = "$trunk_name/$path";
|
|
} elsif($branch =~ m#^/#) {
|
|
$svnpath = "$tag_name$branch/$path";
|
|
} else {
|
|
$svnpath = "$branch_name/$branch/$path";
|
|
}
|
|
|
|
$svnpath =~ s#/+$##;
|
|
return $svnpath;
|
|
}
|
|
|
|
sub get_file($$$) {
|
|
my($rev,$branch,$path) = @_;
|
|
|
|
my $svnpath = revert_split_path($branch,$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,$branch,$path) = @_;
|
|
|
|
return unless $opt_I;
|
|
my $svnpath = revert_split_path($branch,$path);
|
|
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]);
|
|
} else {
|
|
push(@$old,$path);
|
|
}
|
|
}
|
|
|
|
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 ()
|
|
}
|
|
$path = "/" if $path eq "";
|
|
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 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) {
|
|
print "Path not found when copying from $oldpath @ $rev\n";
|
|
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($author_name,$author_email,$dest);
|
|
my(@old,@new,@parents);
|
|
|
|
if (not defined $author) {
|
|
$author_name = $author_email = "unknown";
|
|
} elsif (defined $users_file) {
|
|
die "User $author is not listed in $users_file\n"
|
|
unless exists $users{$author};
|
|
($author_name,$author_email) = @{$users{$author}};
|
|
} elsif ($author =~ /^(.*?)\s+<(.*)>$/) {
|
|
($author_name, $author_email) = ($1, $2);
|
|
} else {
|
|
$author =~ s/^<(.*)>$/$1/;
|
|
$author_name = $author_email = $author;
|
|
}
|
|
$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($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($branch,$path,$revision);
|
|
if ($node_kind eq $SVN::Node::file) {
|
|
my $f = get_file($revision,$branch,$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,
|
|
$branch, $path);
|
|
}
|
|
}
|
|
} elsif ($action->[0] eq "D") {
|
|
push(@old,$path);
|
|
} elsif ($action->[0] eq "M") {
|
|
my $node_kind = node_kind($branch,$path,$revision);
|
|
if ($node_kind eq $SVN::Node::file) {
|
|
my $f = get_file($revision,$branch,$path);
|
|
push(@new,$f) if $f;
|
|
} elsif ($node_kind eq $SVN::Node::dir) {
|
|
get_ignore(\@new, \@old, $revision,
|
|
$branch,$path);
|
|
}
|
|
} 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=$author_name",
|
|
"GIT_COMMITTER_EMAIL=$author_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) {
|
|
my($in, $out) = ('','');
|
|
$last_rev = "-" if %$changed_paths;
|
|
# the tag was 'complex', i.e. did not refer to a "real" revision
|
|
|
|
$dest =~ tr/_/\./ if $opt_u;
|
|
$branch = $dest;
|
|
|
|
my $pid = open2($in, $out, 'git-mktag');
|
|
print $out ("object $cid\n".
|
|
"type commit\n".
|
|
"tag $dest\n".
|
|
"tagger $author_name <$author_email>\n") and
|
|
close($out)
|
|
or die "Cannot create tag object $dest: $!\n";
|
|
|
|
my $tagobj = <$in>;
|
|
chomp $tagobj;
|
|
|
|
if ( !close($in) or waitpid($pid, 0) != $pid or
|
|
$? != 0 or $tagobj !~ /^[0123456789abcdef]{40}$/ ) {
|
|
die "Cannot create tag object $dest: $!\n";
|
|
}
|
|
|
|
open(C,">$git_dir/refs/tags/$dest") and
|
|
print C ("$tagobj\n") and
|
|
close(C)
|
|
or die "Cannot create tag $branch: $!\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, $pool) = @_;
|
|
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;
|
|
$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 "Fetching from $current_rev to $opt_l ...\n" if $opt_v;
|
|
|
|
my $pool=SVN::Pool->new;
|
|
$svn->{'svn'}->get_log("/",$current_rev,$opt_l,0,1,1,\&commit_all,$pool);
|
|
$pool->clear;
|
|
|
|
|
|
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);
|