use File::Path qw/mkpath/;
use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/;
use File::Spec qw//;
+use File::Copy qw/copy/;
use POSIX qw/strftime/;
use IPC::Open3;
use Memoize;
memoize('get_commit_time');
my ($SVN_PATH, $SVN, $SVN_LOG, $_use_lib);
+
+sub nag_lib {
+ print STDERR <<EOF;
+! Please consider installing the SVN Perl libraries (version 1.1.0 or
+! newer). You will generally get better performance and fewer bugs,
+! especially if you:
+! 1) have a case-insensitive filesystem
+! 2) replace symlinks with files (and vice-versa) in commits
+
+EOF
+}
+
$_use_lib = 1 unless $ENV{GIT_SVN_NO_LIB};
libsvn_load();
+nag_lib() unless $_use_lib;
+
my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS};
my $sha1 = qr/[a-f\d]{40}/;
my $sha1_short = qr/[a-f\d]{4,40}/;
$_message, $_file, $_follow_parent, $_no_metadata,
$_template, $_shared, $_no_default_regex, $_no_graft_copy,
$_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
- $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m);
+ $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m,
+ $_merge, $_strategy, $_dry_run, $_ignore_nodate, $_non_recursive);
my (@_branch_from, %tree_map, %users, %rusers, %equiv);
my ($_svn_co_url_revs, $_svn_pg_peg_revs);
my @repo_path_split_cache;
'repack:i' => \$_repack,
'no-metadata' => \$_no_metadata,
'quiet|q' => \$_q,
+ 'ignore-nodate' => \$_ignore_nodate,
'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
my ($_trunk, $_tags, $_branches);
'copy-similarity|C=i'=> \$_cp_similarity
);
-# yes, 'native' sets "\n". Patches to fix this for non-*nix systems welcome:
-my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" );
-
my %cmd = (
fetch => [ \&fetch, "Download new revisions from SVN",
{ 'revision|r=s' => \$_revision, %fc_opts } ],
'incremental' => \$_incremental,
'oneline' => \$_oneline,
'show-commit' => \$_show_commit,
+ 'non-recursive' => \$_non_recursive,
'authors-file|A=s' => \$_authors,
} ],
'commit-diff' => [ \&commit_diff, 'Commit a diff between two trees',
{ 'message|m=s' => \$_message,
'file|F=s' => \$_file,
%cmt_opts } ],
+ dcommit => [ \&dcommit, 'Commit several diffs to merge with upstream',
+ { 'merge|m|M' => \$_merge,
+ 'strategy|s=s' => \$_strategy,
+ 'dry-run|n' => \$_dry_run,
+ %cmt_opts } ],
);
my $cmd;
load_authors() if $_authors;
load_all_refs() if $_branch_all_refs;
svn_compat_check() unless $_use_lib;
-migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init)$/;
+migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init|commit-diff)$/;
$cmd{$cmd}->[0]->(@ARGV);
exit 0;
foreach (sort keys %cmd) {
next if $cmd && $cmd ne $_;
- print $fd ' ',pack('A13',$_),$cmd{$_}->[1],"\n";
+ print $fd ' ',pack('A17',$_),$cmd{$_}->[1],"\n";
foreach (keys %{$cmd{$_}->[2]}) {
# prints out arguments as they should be passed:
my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
- print $fd ' ' x 17, join(', ', map { length $_ > 1 ?
+ print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
"--$_" : "-$_" }
split /\|/,$_)," $x\n";
}
my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
+ my $repo;
+ ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
set_svn_commit_env();
foreach my $c (@revs) {
my $log_msg = get_commit_message($c, $commit_msg);
# can't track down... (it's probably in the SVN code)
defined(my $pid = open my $fh, '-|') or croak $!;
if (!$pid) {
+ $SVN_LOG = libsvn_connect($repo);
+ $SVN = libsvn_connect($repo);
my $ed = SVN::Git::Editor->new(
{ r => $r_last,
- ra => $SVN,
+ ra => $SVN_LOG,
c => $c,
svn_path => $SVN_PATH
},
unlink $commit_msg;
}
+sub dcommit {
+ my $gs = "refs/remotes/$GIT_SVN";
+ chomp(my @refs = safe_qx(qw/git-rev-list --no-merges/, "$gs..HEAD"));
+ foreach my $d (reverse @refs) {
+ if ($_dry_run) {
+ print "diff-tree $d~1 $d\n";
+ } else {
+ commit_diff("$d~1", $d);
+ }
+ }
+ return if $_dry_run;
+ fetch();
+ my @diff = safe_qx(qw/git-diff-tree HEAD/, $gs);
+ my @finish;
+ if (@diff) {
+ @finish = qw/rebase/;
+ push @finish, qw/--merge/ if $_merge;
+ push @finish, "--strategy=$_strategy" if $_strategy;
+ print STDERR "W: HEAD and $gs differ, using @finish:\n", @diff;
+ } else {
+ print "No changes between current HEAD and $gs\n",
+ "Hard resetting to the latest $gs\n";
+ @finish = qw/reset --hard/;
+ }
+ sys('git', @finish, $gs);
+}
+
sub show_ignore {
$SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
$_use_lib ? show_ignore_lib() : show_ignore_cmd();
}
$_trunk = $url . $_trunk;
}
+ my $ch_id;
if ($GIT_SVN eq 'git-svn') {
- print "GIT_SVN_ID set to 'trunk' for $_trunk\n";
+ $ch_id = 1;
$GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
}
init_vars();
- init($_trunk);
+ unless (-d $GIT_SVN_DIR) {
+ print "GIT_SVN_ID set to 'trunk' for $_trunk\n" if $ch_id;
+ init($_trunk);
+ sys('git-repo-config', 'svn.trunk', $_trunk);
+ }
complete_url_ls_init($url, $_branches, '--branches/-b', '');
complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/');
}
# ignore
} elsif (/^:\d{6} \d{6} $sha1_short/o) {
push @{$c->{raw}}, $_;
+ } elsif (/^[ACRMDT]\t/) {
+ # we could add $SVN_PATH here, but that requires
+ # remote access at the moment (repo_path_split)...
+ s#^([ACRMDT])\t# $1 #;
+ push @{$c->{changed}}, $_;
} elsif (/^diff /) {
$d = 1;
push @{$c->{diff}}, $_;
} elsif ($d) {
push @{$c->{diff}}, $_;
} elsif (/^ (git-svn-id:.+)$/) {
- (undef, $c->{r}, undef) = extract_metadata($1);
+ ($c->{url}, $c->{r}, undef) = extract_metadata($1);
} elsif (s/^ //) {
push @{$c->{l}}, $_;
}
exit 1;
}
if (defined $_file) {
- $_message = file_to_s($_message);
+ $_message = file_to_s($_file);
} else {
$_message ||= get_commit_message($tb,
"$GIT_DIR/.svn-commit.tmp.$$")->{msg};
$SVN ||= libsvn_connect($repo);
my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
my $ed = SVN::Git::Editor->new({ r => $SVN->get_latest_revnum,
- ra => $SVN, c => $tb,
+ ra => $SVN_LOG, c => $tb,
svn_path => $SVN_PATH
},
$SVN->get_commit_editor($_message,
} else {
$ed->close_edit;
}
+ $_message = $_file = undef;
}
########################### utility functions #########################
my ($r_min, $r_max) = @_;
my @cmd = (qw/git-log --abbrev-commit --pretty=raw
--default/, "refs/remotes/$GIT_SVN");
- push @cmd, '--summary' if $_verbose;
+ push @cmd, '-r' unless $_non_recursive;
+ push @cmd, qw/--raw --name-status/ if $_verbose;
return @cmd unless defined $r_max;
if ($r_max == $r_min) {
push @cmd, '--max-count=1';
my ($c_min, $c_max);
$c_max = revdb_get($REVDB, $r_max);
$c_min = revdb_get($REVDB, $r_min);
- if ($c_min && $c_max) {
+ if (defined $c_min && defined $c_max) {
if ($r_max > $r_max) {
push @cmd, "$c_min..$c_max";
} else {
print STDERR "W: Unrecognized URL: $u\n";
die "This should never happen\n";
}
+ # don't try to init already existing refs
my $id = $pfx.$1;
- print "init $u => $id\n";
$GIT_SVN = $ENV{GIT_SVN_ID} = $id;
init_vars();
- init($u);
+ unless (-d $GIT_SVN_DIR) {
+ print "init $u => $id\n";
+ init($u);
+ }
}
exit 0;
}
waitpid $pid, 0;
croak $? if $?;
+ my ($n) = ($switch =~ /^--(\w+)/);
+ sys('git-repo-config', "svn.$n", $var);
}
sub common_prefix {
}
}
- my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i);
- $path =~ s#^/+##;
- my @paths = split(m#/+#, $path);
-
if ($_use_lib) {
- while (1) {
- $SVN = libsvn_connect($url);
- last if (defined $SVN &&
- defined eval { $SVN->get_latest_revnum });
- my $n = shift @paths || last;
- $url .= "/$n";
- }
+ my $tmp = libsvn_connect($full_url);
+ my $url = $tmp->get_repos_root;
+ $full_url =~ s#^\Q$url\E/*##;
+ push @repo_path_split_cache, qr/^(\Q$url\E)/;
+ return ($url, $full_url);
} else {
+ my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i);
+ $path =~ s#^/+##;
+ my @paths = split(m#/+#, $path);
while (quiet_run(qw/svn ls --non-interactive/, $url)) {
my $n = shift @paths || last;
$url .= "/$n";
}
+ push @repo_path_split_cache, qr/^(\Q$url\E)/;
+ $path = join('/',@paths);
+ return ($url, $path);
}
- push @repo_path_split_cache, qr/^(\Q$url\E)/;
- $path = join('/',@paths);
- return ($url, $path);
}
sub setup_git_svn {
}
my @status = grep(!/^Performing status on external/,(`svn status`));
@status = grep(!/^\s*$/,@status);
+ @status = grep(!/^X/,@status) if $_no_ignore_ext;
if (scalar @status) {
print STDERR "Tree ($SVN_WC) is not clean:\n";
print STDERR $_ foreach @status;
apply_mod_line_blob($m);
svn_check_prop_executable($m);
} elsif ($m->{chg} eq 'T') {
- sys(qw(svn rm --force),$m->{file_b});
- apply_mod_line_blob($m);
- sys(qw(svn add), $m->{file_b});
svn_check_prop_executable($m);
+ apply_mod_line_blob($m);
+ if ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
+ sys(qw(svn propdel svn:special), $m->{file_b});
+ } else {
+ sys(qw(svn propset svn:special *),$m->{file_b});
+ }
} elsif ($m->{chg} eq 'A') {
svn_ensure_parent_path( $m->{file_b} );
apply_mod_line_blob($m);
open my $msg, '>', $commit_msg or croak $!;
chomp(my $type = `git-cat-file -t $commit`);
- if ($type eq 'commit') {
+ if ($type eq 'commit' || $type eq 'tag') {
my $pid = open my $msg_fh, '-|';
defined $pid or croak $!;
if ($pid == 0) {
- exec(qw(git-cat-file commit), $commit) or croak $!;
+ exec('git-cat-file', $type, $commit) or croak $!;
}
my $in_msg = 0;
while (<$msg_fh>) {
my $rev = $1;
my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3);
($lines) = ($lines =~ /(\d+)/);
+ $date = '1970-01-01 00:00:00 +0000'
+ if ($_ignore_nodate && $date eq '(no date)');
my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
/(\d{4})\-(\d\d)\-(\d\d)\s
(\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
sub sys { system(@_) == 0 or croak $? }
-sub eol_cp {
- my ($from, $to) = @_;
- my $es = svn_propget_base('svn:eol-style', $to);
- open my $rfd, '<', $from or croak $!;
- binmode $rfd or croak $!;
- open my $wfd, '>', $to or croak $!;
- binmode $wfd or croak $!;
- eol_cp_fd($rfd, $wfd, $es);
- close $rfd or croak $!;
- close $wfd or croak $!;
-}
-
-sub eol_cp_fd {
- my ($rfd, $wfd, $es) = @_;
- my $eol = defined $es ? $EOL{$es} : undef;
- my $buf;
- use bytes;
- while (1) {
- my ($r, $w, $t);
- defined($r = sysread($rfd, $buf, 4096)) or croak $!;
- return unless $r;
- if ($eol) {
- if ($buf =~ /\015$/) {
- my $c;
- defined($r = sysread($rfd,$c,1)) or croak $!;
- $buf .= $c if $r > 0;
- }
- $buf =~ s/(?:\015\012|\015|\012)/$eol/gs;
- $r = length($buf);
- }
- for ($w = 0; $w < $r; $w += $t) {
- $t = syswrite($wfd, $buf, $r - $w, $w) or croak $!;
- }
- }
- no bytes;
-}
-
sub do_update_index {
my ($z_cmd, $cmd, $no_text_base) = @_;
'text-base',"$f.svn-base");
$tb =~ s#^/##;
}
+ my @s = stat($x);
unlink $x or croak $!;
- eol_cp($tb, $x);
+ copy($tb, $x);
chmod(($mode &~ umask), $x) or croak $!;
+ utime $s[8], $s[9], $x;
}
print $ui $x,"\0";
}
open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
while (<$authors>) {
chomp;
- next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
+ next unless /^(\S+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
my ($user, $name, $email) = ($1, $2, $3);
$users{$user} = [$name, $email];
}
\s([a-f\d\-]+)$/x);
if (!$rev || !$uuid || !$url) {
# some of the original repositories I made had
- # indentifiers like this:
+ # identifiers like this:
($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
}
return ($url, $rev, $uuid);
}
}
+sub show_commit_changed_paths {
+ my ($c) = @_;
+ return unless $c->{changed};
+ print "Changed paths:\n", @{$c->{changed}};
+}
+
sub show_commit_normal {
my ($c) = @_;
print '-' x72, "\nr$c->{r} | ";
my $nr_line = 0;
if (my $l = $c->{l}) {
- while ($l->[$#$l] eq "\n" && $l->[($#$l - 1)] eq "\n") {
+ while ($l->[$#$l] eq "\n" && $#$l > 0
+ && $l->[($#$l - 1)] eq "\n") {
pop @$l;
}
$nr_line = scalar @$l;
} else {
$nr_line .= ' lines';
}
- print $nr_line, "\n\n";
+ print $nr_line, "\n";
+ show_commit_changed_paths($c);
+ print "\n";
print $_ foreach @$l;
}
} else {
- print "1 line\n\n";
+ print "1 line\n";
+ show_commit_changed_paths($c);
+ print "\n";
}
foreach my $x (qw/raw diff/) {
sub libsvn_get_file {
my ($gui, $f, $rev) = @_;
my $p = $f;
- return unless ($p =~ s#^\Q$SVN_PATH\E/##);
+ if (length $SVN_PATH > 0) {
+ return unless ($p =~ s#^\Q$SVN_PATH\E/##);
+ }
my ($hash, $pid, $in, $out);
my $pool = SVN::Pool->new;
if (defined $_authors && ! defined $users{$author}) {
die "Author: $author not defined in $_authors file\n";
}
+ $msg = '' if ($rev == 0 && !defined $msg);
return { revision => $rev, date => "+0000 $Y-$m-$d $H:$M:$S",
author => $author, msg => $msg."\n", parents => $parents || [] }
}
} else {
die "Unrecognized action: $m, ($f r$rev)\n";
}
+ } elsif ($t == $SVN::Node::dir && $m =~ /^[AR]$/) {
+ my @traversed = ();
+ libsvn_traverse($gui, '', $f, $rev, \@traversed);
+ foreach (@traversed) {
+ push @amr, [ $m, $_ ]
+ }
}
$pool->clear;
}
}
sub libsvn_traverse {
- my ($gui, $pfx, $path, $rev) = @_;
+ my ($gui, $pfx, $path, $rev, $files) = @_;
my $cwd = "$pfx/$path";
my $pool = SVN::Pool->new;
$cwd =~ s#^/+##g;
foreach my $d (keys %$dirent) {
my $t = $dirent->{$d}->kind;
if ($t == $SVN::Node::dir) {
- libsvn_traverse($gui, $cwd, $d, $rev);
+ libsvn_traverse($gui, $cwd, $d, $rev, $files);
} elsif ($t == $SVN::Node::file) {
- print "\tA\t$cwd/$d\n" unless $_q;
- libsvn_get_file($gui, "$cwd/$d", $rev);
+ my $file = "$cwd/$d";
+ if (defined $files) {
+ push @$files, $file;
+ } else {
+ print "\tA\t$file\n" unless $_q;
+ libsvn_get_file($gui, $file, $rev);
+ }
}
}
$pool->clear;
}
my ($paths, $rev, $author, $date, $msg) = @_;
open my $gui, '| git-update-index -z --index-info' or croak $!;
- my $pool = SVN::Pool->new;
- libsvn_traverse($gui, '', $SVN_PATH, $rev, $pool);
- $pool->clear;
+ libsvn_traverse($gui, '', $SVN_PATH, $rev);
close $gui or croak $?;
return libsvn_log_entry($rev, $author, $date, $msg);
}
seek $fh, 0, 0 or croak $!;
my $exp = $md5->hexdigest;
- my $atd = $self->apply_textdelta($fbat, undef, $self->{pool});
- my $got = SVN::TxDelta::send_stream($fh, @$atd, $self->{pool});
+ my $pool = SVN::Pool->new;
+ my $atd = $self->apply_textdelta($fbat, undef, $pool);
+ my $got = SVN::TxDelta::send_stream($fh, @$atd, $pool);
die "Checksum mismatch\nexpected: $exp\ngot: $got\n" if ($got ne $exp);
+ $pool->clear;
close $fh or croak $!;
}