$GIT_SVN_INDEX $GIT_SVN
$GIT_DIR $GIT_SVN_DIR $REVDB
$_follow_parent $sha1 $sha1_short $_revision
- $_cp_remote $_upgrade/;
+ $_cp_remote $_upgrade $_rmdir $_q $_cp_similarity
+ $_find_copies_harder $_l $_authors %users/;
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
$VERSION = '@@GIT_VERSION@@';
$ENV{LC_ALL} = 'C';
$| = 1; # unbuffer STDOUT
-# properties that we do not log:
-my %SKIP = ( 'svn:wc:ra_dav:version-url' => 1,
- 'svn:special' => 1,
- 'svn:executable' => 1,
- 'svn:entry:committed-rev' => 1,
- 'svn:entry:last-author' => 1,
- 'svn:entry:uuid' => 1,
- 'svn:entry:committed-date' => 1,
-);
-
sub fatal (@) { print STDERR @_; exit 1 }
require SVN::Core; # use()-ing this causes segfaults for me... *shrug*
require SVN::Ra;
use File::Path qw/mkpath/;
use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/;
use IPC::Open3;
-use Memoize;
use Git;
-memoize('revisions_eq');
-memoize('cmt_metadata');
-memoize('get_commit_time');
BEGIN {
my $s;
my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS};
$sha1 = qr/[a-f\d]{40}/;
$sha1_short = qr/[a-f\d]{4,40}/;
-my ($_stdin,$_help,$_rmdir,$_edit,
- $_find_copies_harder, $_l, $_cp_similarity,
- $_repack, $_repack_nr, $_repack_flags, $_q,
+my ($_stdin, $_help, $_edit,
+ $_repack, $_repack_nr, $_repack_flags,
$_message, $_file, $_no_metadata,
- $_template, $_shared, $_no_default_regex, $_no_graft_copy,
- $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m,
+ $_template, $_shared,
+ $_version, $_upgrade,
$_merge, $_strategy, $_dry_run,
$_prefix);
-my (@_branch_from, %tree_map, %users);
my @repo_path_split_cache;
-my %fc_opts = ( 'branch|b=s' => \@_branch_from,
- 'follow-parent|follow' => \$_follow_parent,
- 'branch-all-refs|B' => \$_branch_all_refs,
+my %fc_opts = ( 'follow-parent|follow' => \$_follow_parent,
'authors-file|A=s' => \$_authors,
'repack:i' => \$_repack,
'no-metadata' => \$_no_metadata,
init => [ \&cmd_init, "Initialize a repo for tracking" .
" (requires URL argument)",
\%init_opts ],
- dcommit => [ \&dcommit, 'Commit several diffs to merge with upstream',
+ dcommit => [ \&cmd_dcommit,
+ 'Commit several diffs to merge with upstream',
{ 'merge|m|M' => \$_merge,
'strategy|s=s' => \$_strategy,
'dry-run|n' => \$_dry_run,
%cmt_opts, %fc_opts } ],
- 'set-tree' => [ \&commit, "Set an SVN repository to a git tree-ish",
- { 'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ],
+ 'set-tree' => [ \&cmd_set_tree,
+ "Set an SVN repository to a git tree-ish",
+ { 'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ],
'show-ignore' => [ \&cmd_show_ignore, "Show svn:ignore listings",
{ 'revision|r=i' => \$_revision } ],
rebuild => [ \&cmd_rebuild, "Rebuild git-svn metadata (after git clone)",
{ 'copy-remote|remote=s' => \$_cp_remote,
'upgrade' => \$_upgrade } ],
- 'graft-branches' => [ \&graft_branches,
- 'Detect merges/branches from already imported history',
- { 'merge-rx|m' => \@_opt_m,
- 'branch|b=s' => \@_branch_from,
- 'branch-all-refs|B' => \$_branch_all_refs,
- 'no-default-regex' => \$_no_default_regex,
- 'no-graft-copy' => \$_no_graft_copy } ],
'multi-init' => [ \&cmd_multi_init,
'Initialize multiple trees (like git-svnimport)',
{ %multi_opts, %init_opts,
'no-auth-cache' => \$Git::SVN::Prompt::_no_auth_cache,
'prefix=s' => \$_prefix,
} ],
- 'multi-fetch' => [ \&multi_fetch,
+ 'multi-fetch' => [ \&cmd_multi_fetch,
'Fetch multiple trees (like git-svnimport)',
\%fc_opts ],
'log' => [ \&Git::SVN::Log::cmd_show_log, 'Show commit logs',
'color' => \$Git::SVN::Log::color,
'pager=s' => \$Git::SVN::Log::pager,
} ],
- 'commit-diff' => [ \&commit_diff, 'Commit a diff between two trees',
+ 'commit-diff' => [ \&cmd_commit_diff,
+ 'Commit a diff between two trees',
{ 'message|m=s' => \$_message,
'file|F=s' => \$_file,
'revision|r=s' => \$_revision,
'id|i=s' => \$GIT_SVN);
exit 1 if (!$rv && $cmd ne 'log');
-set_default_vals();
usage(0) if $_help;
version() if $_version;
usage(1) unless defined $cmd;
init_vars();
load_authors() if $_authors;
-load_all_refs() if $_branch_all_refs;
migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init|commit-diff)$/;
$cmd{$cmd}->[0]->(@ARGV);
exit 0;
}
sub cmd_fetch {
- fetch_child_id($GIT_SVN, @_);
-}
-
-sub fetch {
- check_upgrade_needed();
- $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
- my $ret = fetch_lib(@_);
- if ($ret->{commit} && !verify_ref('refs/heads/master^0')) {
- command_noisy(qw(update-ref refs/heads/master),$ret->{commit});
- }
- return $ret;
-}
-
-sub fetch_lib {
- my (@parents) = @_;
- $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
- $SVN ||= Git::SVN::Ra->new($SVN_URL);
- my ($last_rev, $last_commit) = svn_grab_base_rev();
- my ($base, $head) = libsvn_parse_revision($last_rev);
- if ($base > $head) {
- return { revision => $last_rev, commit => $last_commit }
- }
- my $index = set_index($GIT_SVN_INDEX);
-
- # limit ourselves and also fork() since get_log won't release memory
- # after processing a revision and SVN stuff seems to leak
- my $inc = 1000;
- my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
- if (defined $last_commit) {
- unless (-e $GIT_SVN_INDEX) {
- command_noisy('read-tree', $last_commit);
- }
- my $x = command_oneline('write-tree');
- my ($y) = (command(qw/cat-file commit/, $last_commit)
- =~ /^tree ($sha1)/m);
- if ($y ne $x) {
- unlink $GIT_SVN_INDEX or croak $!;
- command_noisy('read-tree', $last_commit);
- }
- $x = command_oneline('write-tree');
- if ($y ne $x) {
- print STDERR "trees ($last_commit) $y != $x\n",
- "Something is seriously wrong...\n";
- }
- }
- while (1) {
- # fork, because using SVN::Pool with get_log() still doesn't
- # seem to help enough to keep memory usage down.
- defined(my $pid = fork) or croak $!;
- if (!$pid) {
- $SVN::Error::handler = \&libsvn_skip_unknown_revs;
-
- # Yes I'm perfectly aware that the fourth argument
- # below is the limit revisions number. Unfortunately
- # performance sucks with it enabled, so it's much
- # faster to fetch revision ranges instead of relying
- # on the limiter.
- $SVN->dup->get_log([''], $min, $max, 0, 1, 1,
- sub {
- my $log_msg;
- if ($last_commit) {
- $log_msg = libsvn_fetch(
- $last_commit, @_);
- $last_commit = git_commit(
- $log_msg,
- $last_commit,
- @parents);
- } else {
- $log_msg = libsvn_new_tree(@_);
- $last_commit = git_commit(
- $log_msg, @parents);
- }
- });
- exit 0;
- }
- waitpid $pid, 0;
- croak $? if $?;
- ($last_rev, $last_commit) = svn_grab_base_rev();
- last if ($max >= $head);
- $min = $max + 1;
- $max += $inc;
- $max = $head if ($max > $head);
- $SVN = Git::SVN::Ra->new($SVN_URL);
+ my $gs = Git::SVN->new;
+ $gs->fetch(@_);
+ if ($gs->{last_commit} && !verify_ref('refs/heads/master^0')) {
+ command_noisy(qw(update-ref refs/heads/master),
+ $gs->{last_commit});
}
- restore_index($index);
- return { revision => $last_rev, commit => $last_commit };
}
-sub commit {
+sub cmd_set_tree {
my (@commits) = @_;
- check_upgrade_needed();
if ($_stdin || !@commits) {
print "Reading from stdin...\n";
@commits = ();
} elsif (scalar @tmp > 1) {
push @revs, reverse(command('rev-list',@tmp));
} else {
- die "Failed to rev-parse $c\n";
+ fatal "Failed to rev-parse $c\n";
}
}
- commit_lib(@revs);
+ my $gs = Git::SVN->new;
+ my ($r_last, $cmt_last) = $gs->last_rev_commit;
+ $gs->fetch;
+ if ($r_last != $gs->{last_rev}) {
+ fatal "There are new revisions that were fetched ",
+ "and need to be merged (or acknowledged) ",
+ "before committing.\nlast rev: $r_last\n",
+ " current: $gs->{last_rev}\n";
+ }
+ $gs->set_tree($_) foreach @revs;
print "Done committing ",scalar @revs," revisions to SVN\n";
}
-sub commit_lib {
- my (@revs) = @_;
- my ($r_last, $cmt_last) = svn_grab_base_rev();
- defined $r_last or die "Must have an existing revision to commit\n";
- my $fetched = fetch();
- if ($r_last != $fetched->{revision}) {
- print STDERR "There are new revisions that were fetched ",
- "and need to be merged (or acknowledged) ",
- "before committing.\n",
- "last rev: $r_last\n",
- " current: $fetched->{revision}\n";
- exit 1;
- }
- my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
-
- my $repo;
- set_svn_commit_env();
- foreach my $c (@revs) {
- my $log_msg = get_commit_message($c, $commit_msg);
-
- # fork for each commit because there's a memory leak I
- # can't track down... (it's probably in the SVN code)
- defined(my $pid = open my $fh, '-|') or croak $!;
- if (!$pid) {
- my $pool = SVN::Pool->new;
- my $ed = SVN::Git::Editor->new(
- { r => $r_last,
- ra => $SVN->dup,
- c => $c,
- svn_path => $SVN->{svn_path},
- },
- $SVN->get_commit_editor(
- $log_msg->{msg},
- sub {
- libsvn_commit_cb(
- @_, $c,
- $log_msg->{msg},
- $r_last,
- $cmt_last)
- }, $pool)
- );
- my $mods = libsvn_checkout_tree($cmt_last, $c, $ed);
- if (@$mods == 0) {
- print "No changes\nr$r_last = $cmt_last\n";
- $ed->abort_edit;
- } else {
- $ed->close_edit;
- }
- $pool->clear;
- exit 0;
- }
- my ($r_new, $cmt_new, $no);
- while (<$fh>) {
- print $_;
- chomp;
- if (/^r(\d+) = ($sha1)$/o) {
- ($r_new, $cmt_new) = ($1, $2);
- } elsif ($_ eq 'No changes') {
- $no = 1;
- }
- }
- close $fh or exit 1;
- if (! defined $r_new && ! defined $cmt_new) {
- unless ($no) {
- die "Failed to parse revision information\n";
- }
- } else {
- ($r_last, $cmt_last) = ($r_new, $cmt_new);
- }
- }
- $ENV{LC_ALL} = 'C';
- unlink $commit_msg;
-}
-
-sub dcommit {
- my $head = shift || 'HEAD';
- my $gs = "refs/remotes/$GIT_SVN";
- my @refs = command(qw/rev-list --no-merges/, "$gs..$head");
+sub cmd_dcommit {
+ my $head = shift;
+ my $gs = Git::SVN->new;
+ $head ||= 'HEAD';
+ my @refs = command(qw/rev-list --no-merges/, $gs->refname."..$head");
my $last_rev;
foreach my $d (reverse @refs) {
if (!verify_ref("$d~1")) {
- die "Commit $d\n",
- "has no parent commit, and therefore ",
- "nothing to diff against.\n",
- "You should be working from a repository ",
- "originally created by git-svn\n";
+ fatal "Commit $d\n",
+ "has no parent commit, and therefore ",
+ "nothing to diff against.\n",
+ "You should be working from a repository ",
+ "originally created by git-svn\n";
}
unless (defined $last_rev) {
(undef, $last_rev, undef) = cmt_metadata("$d~1");
unless (defined $last_rev) {
- die "Unable to extract revision information ",
- "from commit $d~1\n";
+ fatal "Unable to extract revision information ",
+ "from commit $d~1\n";
}
}
if ($_dry_run) {
print "diff-tree $d~1 $d\n";
} else {
- if (my $r = commit_diff("$d~1", $d, undef, $last_rev)) {
- $last_rev = $r;
- } # else: no changes, same $last_rev
+ my $ra = $gs->ra;
+ my $pool = SVN::Pool->new;
+ my %ed_opts = ( r => $last_rev,
+ ra => $ra->dup,
+ svn_path => $ra->{svn_path} );
+ my $ed = SVN::Git::Editor->new(\%ed_opts,
+ $ra->get_commit_editor($::_message,
+ sub { print "Committed r$_[0]\n";
+ $last_rev = $_[0]; }),
+ $pool);
+ my $mods = $ed->apply_diff("$d~1", $d);
+ if (@$mods == 0) {
+ print "No changes\n$d~1 == $d\n";
+ }
}
}
return if $_dry_run;
- fetch();
- my @diff = command('diff-tree', 'HEAD', $gs, '--');
+ $gs->fetch;
+ # we always want to rebase against the current HEAD, not any
+ # head that was passed to us
+ my @diff = command('diff-tree', 'HEAD', $gs->refname, '--');
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;
+ print STDERR "W: HEAD and ", $gs->refname, " differ, ",
+ "using @finish:\n", "@diff";
} else {
- print "No changes between current HEAD and $gs\n",
- "Resetting to the latest $gs\n";
+ print "No changes between current HEAD and ",
+ $gs->refname, "\nResetting to the latest ",
+ $gs->refname, "\n";
@finish = qw/reset --mixed/;
}
- command_noisy(@finish, $gs);
+ command_noisy(@finish, $gs->refname);
}
sub cmd_show_ignore {
$gs->traverse_ignore(\*STDOUT, '', $r);
}
-sub graft_branches {
- my $gr_file = "$GIT_DIR/info/grafts";
- my ($grafts, $comments) = read_grafts($gr_file);
- my $gr_sha1;
-
- if (%$grafts) {
- # temporarily disable our grafts file to make this idempotent
- chomp($gr_sha1 = command(qw/hash-object -w/,$gr_file));
- rename $gr_file, "$gr_file~$gr_sha1" or croak $!;
- }
-
- my $l_map = read_url_paths();
- my @re = map { qr/$_/is } @_opt_m if @_opt_m;
- unless ($_no_default_regex) {
- push @re, (qr/\b(?:merge|merging|merged)\s+with\s+([\w\.\-]+)/i,
- qr/\b(?:merge|merging|merged)\s+([\w\.\-]+)/i,
- qr/\b(?:from|of)\s+([\w\.\-]+)/i );
- }
- foreach my $u (keys %$l_map) {
- if (@re) {
- foreach my $p (keys %{$l_map->{$u}}) {
- graft_merge_msg($grafts,$l_map,$u,$p,@re);
- }
- }
- unless ($_no_graft_copy) {
- graft_file_copy_lib($grafts,$l_map,$u);
- }
- }
- graft_tree_joins($grafts);
-
- write_grafts($grafts, $comments, $gr_file);
- unlink "$gr_file~$gr_sha1" if $gr_sha1;
-}
-
sub cmd_multi_init {
my $url = shift;
unless (defined $_trunk || defined $_branches || defined $_tags) {
complete_url_ls_init($ra, $_tags, '--tags/-t', $_prefix . 'tags/');
}
-sub multi_fetch {
+sub cmd_multi_fetch {
# try to do trunk first, since branches/tags
# may be descended from it.
- if (-e "$GIT_DIR/svn/trunk/info/url") {
- fetch_child_id('trunk', @_);
+ if (-e "$ENV{GIT_DIR}/svn/trunk/info/url") {
+ my $gs = Git::SVN->new('trunk');
+ $gs->fetch(@_);
}
- rec_fetch('', "$GIT_DIR/svn", @_);
-}
-
-sub commit_diff_usage {
- print STDERR "Usage: $0 commit-diff <tree-ish> <tree-ish> [<URL>]\n";
- exit 1
+ rec_fetch('', "$ENV{GIT_DIR}/svn", @_);
}
-sub commit_diff {
- my $ta = shift or commit_diff_usage();
- my $tb = shift or commit_diff_usage();
- if (!eval { $SVN_URL = shift || file_to_s("$GIT_SVN_DIR/info/url") }) {
- print STDERR "Needed URL or usable git-svn id command-line\n";
- commit_diff_usage();
- }
- my $r = shift;
- unless (defined $r) {
- if (defined $_revision) {
- $r = $_revision
- } else {
- die "-r|--revision is a required argument\n";
+# this command is special because it requires no metadata
+sub cmd_commit_diff {
+ my ($ta, $tb, $url) = @_;
+ my $usage = "Usage: $0 commit-diff -r<revision> ".
+ "<tree-ish> <tree-ish> [<URL>]\n";
+ fatal($usage) if (!defined $ta || !defined $tb);
+ if (!defined $url) {
+ my $gs = eval { Git::SVN->new };
+ if (!$gs) {
+ fatal("Needed URL or usable git-svn --id in ",
+ "the command-line\n", $usage);
}
+ $url = $gs->{url};
+ }
+ unless (defined $_revision) {
+ fatal("-r|--revision is a required argument\n", $usage);
}
if (defined $_message && defined $_file) {
- print STDERR "Both --message/-m and --file/-F specified ",
- "for the commit message.\n",
- "I have no idea what you mean\n";
- exit 1;
+ fatal("Both --message/-m and --file/-F specified ",
+ "for the commit message.\n",
+ "I have no idea what you mean\n");
}
if (defined $_file) {
$_message = file_to_s($_file);
} else {
- $_message ||= get_commit_message($tb,
- "$GIT_DIR/.svn-commit.tmp.$$")->{msg};
+ $_message ||= get_commit_entry($tb)->{log};
}
- $SVN ||= Git::SVN::Ra->new($SVN_URL);
+ my $ra ||= Git::SVN::Ra->new($url);
+ my $r = $_revision;
if ($r eq 'HEAD') {
- $r = $SVN->get_latest_revnum;
+ $r = $ra->get_latest_revnum;
} elsif ($r !~ /^\d+$/) {
die "revision argument: $r not understood by git-svn\n";
}
- my $rev_committed;
my $pool = SVN::Pool->new;
- my $ed = SVN::Git::Editor->new({ r => $r,
- ra => $SVN->dup,
- c => $tb,
- svn_path => $SVN->{svn_path}
- },
- $SVN->get_commit_editor($_message,
- sub {
- $rev_committed = $_[0];
- print "Committed $_[0]\n";
- },
- $pool)
- );
- eval {
- my $mods = libsvn_checkout_tree($ta, $tb, $ed);
- if (@$mods == 0) {
- print "No changes\n$ta == $tb\n";
- $ed->abort_edit;
- } else {
- $ed->close_edit;
- }
- };
+ my %ed_opts = ( r => $r,
+ ra => $ra->dup,
+ svn_path => $ra->{svn_path} );
+ my $ed = SVN::Git::Editor->new(\%ed_opts,
+ $ra->get_commit_editor($_message,
+ sub { print "Committed r$_[0]\n" }),
+ $pool);
+ my $mods = $ed->apply_diff($ta, $tb);
+ if (@$mods == 0) {
+ print "No changes\n$ta == $tb\n";
+ }
$pool->clear;
- fatal "$@\n" if $@;
- $_message = $_file = undef;
- return $rev_committed;
}
########################### utility functions #########################
-sub fetch_child_id {
- my $id = shift;
- print "Fetching $id\n";
- my $ref = "$GIT_DIR/refs/remotes/$id";
- defined(my $pid = open my $fh, '-|') or croak $!;
- if (!$pid) {
- $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
- init_vars();
- fetch(@_);
- exit 0;
- }
- while (<$fh>) {
- print $_;
- check_repack() if (/^r\d+ = $sha1/o);
- }
- close $fh or croak $?;
-}
-
sub rec_fetch {
my ($pfx, $p, @args) = @_;
my @dir;
$pfx .= '/' if $pfx && $pfx !~ m!/$!;
my $id = $pfx . basename $_;
next if $id eq 'trunk';
- fetch_child_id($id, @args);
+ my $gs = Git::SVN->new($id);
+ $gs->fetch(@args);
} elsif (-d $_) {
push @dir, $_;
}
}
foreach (@dir) {
my $x = $_;
- $x =~ s!^\Q$GIT_DIR\E/svn/!!;
- rec_fetch($x, $_);
+ $x =~ s!^\Q$ENV{GIT_DIR}\E/svn/!!o;
+ rec_fetch($x, $_, @args);
}
}
return '';
}
-# grafts set here are 'stronger' in that they're based on actual tree
-# matches, and won't be deleted from merge-base checking in write_grafts()
-sub graft_tree_joins {
- my $grafts = shift;
- map_tree_joins() if (@_branch_from && !%tree_map);
- return unless %tree_map;
-
- git_svn_each(sub {
- my $i = shift;
- my @args = (qw/rev-list --pretty=raw/, "refs/remotes/$i");
- my ($fh, $ctx) = command_output_pipe(@args);
- while (<$fh>) {
- next unless /^commit ($sha1)$/o;
- my $c = $1;
- my ($t) = (<$fh> =~ /^tree ($sha1)$/o);
- next unless $tree_map{$t};
-
- my $l;
- do {
- $l = readline $fh;
- } until ($l =~ /^committer (?:.+) (\d+) ([\-\+]?\d+)$/);
-
- my ($s, $tz) = ($1, $2);
- if ($tz =~ s/^\+//) {
- $s += tz_to_s_offset($tz);
- } elsif ($tz =~ s/^\-//) {
- $s -= tz_to_s_offset($tz);
- }
-
- my ($url_a, $r_a, $uuid_a) = cmt_metadata($c);
-
- foreach my $p (@{$tree_map{$t}}) {
- next if $p eq $c;
- my $mb = eval { command('merge-base', $c, $p) };
- next unless ($@ || $?);
- if (defined $r_a) {
- # see if SVN says it's a relative
- my ($url_b, $r_b, $uuid_b) =
- cmt_metadata($p);
- next if (defined $url_b &&
- defined $url_a &&
- ($url_a eq $url_b) &&
- ($uuid_a eq $uuid_b));
- if ($uuid_a eq $uuid_b) {
- if ($r_b < $r_a) {
- $grafts->{$c}->{$p} = 2;
- next;
- } elsif ($r_b > $r_a) {
- $grafts->{$p}->{$c} = 2;
- next;
- }
- }
- }
- my $ct = get_commit_time($p);
- if ($ct < $s) {
- $grafts->{$c}->{$p} = 2;
- } elsif ($ct > $s) {
- $grafts->{$p}->{$c} = 2;
- }
- # what should we do when $ct == $s ?
- }
- }
- command_close_pipe($fh, $ctx);
- });
-}
-
-sub graft_file_copy_lib {
- my ($grafts, $l_map, $u) = @_;
- my $tree_paths = $l_map->{$u};
- my $pfx = common_prefix([keys %$tree_paths]);
- my ($repo, $path) = repo_path_split($u.$pfx);
- $SVN = Git::SVN::Ra->new($repo);
-
- my ($base, $head) = libsvn_parse_revision();
- my $inc = 1000;
- my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
- my $eh = $SVN::Error::handler;
- $SVN::Error::handler = \&libsvn_skip_unknown_revs;
- while (1) {
- $SVN->dup->get_log([$path], $min, $max, 0, 2, 1,
- sub {
- libsvn_graft_file_copies($grafts, $tree_paths,
- $path, @_);
- });
- last if ($max >= $head);
- $min = $max + 1;
- $max += $inc;
- $max = $head if ($max > $head);
- }
- $SVN::Error::handler = $eh;
-}
-
-sub process_merge_msg_matches {
- my ($grafts, $l_map, $u, $p, $c, @matches) = @_;
- my (@strong, @weak);
- foreach (@matches) {
- # merging with ourselves is not interesting
- next if $_ eq $p;
- if ($l_map->{$u}->{$_}) {
- push @strong, $_;
- } else {
- push @weak, $_;
- }
- }
- foreach my $w (@weak) {
- last if @strong;
- # no exact match, use branch name as regexp.
- my $re = qr/\Q$w\E/i;
- foreach (keys %{$l_map->{$u}}) {
- if (/$re/) {
- push @strong, $l_map->{$u}->{$_};
- last;
- }
- }
- last if @strong;
- $w = basename($w);
- $re = qr/\Q$w\E/i;
- foreach (keys %{$l_map->{$u}}) {
- if (/$re/) {
- push @strong, $l_map->{$u}->{$_};
- last;
- }
- }
- }
- my ($rev) = ($c->{m} =~ /^git-svn-id:\s(?:\S+?)\@(\d+)
- \s(?:[a-f\d\-]+)$/xsm);
- unless (defined $rev) {
- ($rev) = ($c->{m} =~/^git-svn-id:\s(\d+)
- \@(?:[a-f\d\-]+)/xsm);
- return unless defined $rev;
- }
- foreach my $m (@strong) {
- my ($r0, $s0) = find_rev_before($rev, $m, 1);
- $grafts->{$c->{c}}->{$s0} = 1 if defined $s0;
- }
-}
-
-sub graft_merge_msg {
- my ($grafts, $l_map, $u, $p, @re) = @_;
-
- my $x = $l_map->{$u}->{$p};
- my $rl = rev_list_raw("refs/remotes/$x");
- while (my $c = next_rev_list_entry($rl)) {
- foreach my $re (@re) {
- my (@br) = ($c->{m} =~ /$re/g);
- next unless @br;
- process_merge_msg_matches($grafts,$l_map,$u,$p,$c,@br);
- }
- }
-}
-
sub verify_ref {
my ($ref) = @_;
eval { command_oneline([ 'rev-parse', '--verify', $ref ],
sub get_tree_from_treeish {
my ($treeish) = @_;
- croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
+ # $treeish can be a symbolic ref, too:
my $type = command_oneline(qw/cat-file -t/, $treeish);
my $expected;
while ($type eq 'tag') {
if ($type eq 'commit') {
$expected = (grep /^tree /, command(qw/cat-file commit/,
$treeish))[0];
- ($expected) = ($expected =~ /^tree ($sha1)$/);
+ ($expected) = ($expected =~ /^tree ($sha1)$/o);
die "Unable to get tree from $treeish\n" unless $expected;
} elsif ($type eq 'tree') {
$expected = $treeish;
return $expected;
}
-sub get_diff {
- my ($from, $treeish) = @_;
- print "diff-tree $from $treeish\n";
- my @diff_tree = qw(diff-tree -z -r);
- if ($_cp_similarity) {
- push @diff_tree, "-C$_cp_similarity";
- } else {
- push @diff_tree, '-C';
- }
- push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
- push @diff_tree, "-l$_l" if defined $_l;
- push @diff_tree, $from, $treeish;
- my ($diff_fh, $ctx) = command_output_pipe(@diff_tree);
- local $/ = "\0";
- my $state = 'meta';
- my @mods;
- while (<$diff_fh>) {
- chomp $_; # this gets rid of the trailing "\0"
- if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
- $sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
- push @mods, { mode_a => $1, mode_b => $2,
- sha1_b => $3, chg => $4 };
- if ($4 =~ /^(?:C|R)$/) {
- $state = 'file_a';
- } else {
- $state = 'file_b';
- }
- } elsif ($state eq 'file_a') {
- my $x = $mods[$#mods] or croak "Empty array\n";
- if ($x->{chg} !~ /^(?:C|R)$/) {
- croak "Error parsing $_, $x->{chg}\n";
- }
- $x->{file_a} = $_;
- $state = 'file_b';
- } elsif ($state eq 'file_b') {
- my $x = $mods[$#mods] or croak "Empty array\n";
- if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
- croak "Error parsing $_, $x->{chg}\n";
- }
- if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
- croak "Error parsing $_, $x->{chg}\n";
- }
- $x->{file_b} = $_;
- $state = 'meta';
- } else {
- croak "Error parsing $_\n";
- }
- }
- command_close_pipe($diff_fh, $ctx);
- return \@mods;
-}
-
-sub libsvn_checkout_tree {
- my ($from, $treeish, $ed) = @_;
- my $mods = get_diff($from, $treeish);
- return $mods unless (scalar @$mods);
- my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
- foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
- my $f = $m->{chg};
- if (defined $o{$f}) {
- $ed->$f($m, $_q);
- } else {
- croak "Invalid change type: $f\n";
- }
- }
- $ed->rmdirs($_q) if $_rmdir;
- return $mods;
-}
-
-sub get_commit_message {
- my ($commit, $commit_msg) = (@_);
- my %log_msg = ( msg => '' );
- open my $msg, '>', $commit_msg or croak $!;
+sub get_commit_entry {
+ my ($treeish) = shift;
+ my %log_entry = ( log => '', tree => get_tree_from_treeish($treeish) );
+ my $commit_editmsg = "$ENV{GIT_DIR}/COMMIT_EDITMSG";
+ my $commit_msg = "$ENV{GIT_DIR}/COMMIT_MSG";
+ open my $log_fh, '>', $commit_editmsg or croak $!;
- my $type = command_oneline(qw/cat-file -t/, $commit);
+ my $type = command_oneline(qw/cat-file -t/, $treeish);
if ($type eq 'commit' || $type eq 'tag') {
my ($msg_fh, $ctx) = command_output_pipe('cat-file',
- $type, $commit);
+ $type, $treeish);
my $in_msg = 0;
while (<$msg_fh>) {
if (!$in_msg) {
$in_msg = 1 if (/^\s*$/);
} elsif (/^git-svn-id: /) {
- # skip this, we regenerate the correct one
- # on re-fetch anyways
+ # skip this for now, we regenerate the
+ # correct one on re-fetch anyways
+ # TODO: set *:merge properties or like...
} else {
- print $msg $_ or croak $!;
+ print $log_fh $_ or croak $!;
}
}
command_close_pipe($msg_fh, $ctx);
}
- close $msg or croak $!;
+ close $log_fh or croak $!;
if ($_edit || ($type eq 'tree')) {
my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
- system($editor, $commit_msg);
- }
-
- # file_to_s removes all trailing newlines, so just use chomp() here:
- open $msg, '<', $commit_msg or croak $!;
- { local $/; chomp($log_msg{msg} = <$msg>); }
- close $msg or croak $!;
-
- return \%log_msg;
-}
-
-sub set_svn_commit_env {
- if (defined $LC_ALL) {
- $ENV{LC_ALL} = $LC_ALL;
- } else {
- delete $ENV{LC_ALL};
- }
-}
-
-sub rev_list_raw {
- my ($fh, $c) = command_output_pipe(qw/rev-list --pretty=raw/, @_);
- return { fh => $fh, ctx => $c, t => { } };
-}
-
-sub next_rev_list_entry {
- my $rl = shift;
- my $fh = $rl->{fh};
- my $x = $rl->{t};
- while (<$fh>) {
- if (/^commit ($sha1)$/o) {
- if ($x->{c}) {
- $rl->{t} = { c => $1 };
- return $x;
- } else {
- $x->{c} = $1;
- }
- } elsif (/^parent ($sha1)$/o) {
- $x->{p}->{$1} = 1;
- } elsif (s/^ //) {
- $x->{m} ||= '';
- $x->{m} .= $_;
- }
+ # TODO: strip out spaces, comments, like git-commit.sh
+ system($editor, $commit_editmsg);
}
- command_close_pipe($fh, $rl->{ctx});
- return ($x != $rl->{t}) ? $x : undef;
+ rename $commit_editmsg, $commit_msg or croak $!;
+ open $log_fh, '<', $commit_msg or croak $!;
+ { local $/; chomp($log_entry{log} = <$log_fh>); }
+ close $log_fh or croak $!;
+ unlink $commit_msg;
+ \%log_entry;
}
sub s_to_file {
return $ret;
}
-sub assert_revision_unknown {
- my $r = shift;
- if (my $c = revdb_get($REVDB, $r)) {
- croak "$r = $c already exists! Why are we refetching it?";
- }
-}
-
-sub git_commit {
- my ($log_msg, @parents) = @_;
- assert_revision_unknown($log_msg->{revision});
- map_tree_joins() if (@_branch_from && !%tree_map);
-
- my (@tmp_parents, @exec_parents, %seen_parent);
- if (my $lparents = $log_msg->{parents}) {
- @tmp_parents = @$lparents
- }
- # commit parents can be conditionally bound to a particular
- # svn revision via: "svn_revno=commit_sha1", filter them out here:
- foreach my $p (@parents) {
- next unless defined $p;
- if ($p =~ /^(\d+)=($sha1_short)$/o) {
- if ($1 == $log_msg->{revision}) {
- push @tmp_parents, $2;
- }
- } else {
- push @tmp_parents, $p if $p =~ /$sha1_short/o;
- }
- }
- my $tree = $log_msg->{tree};
- if (!defined $tree) {
- my $index = set_index($GIT_SVN_INDEX);
- $tree = command_oneline('write-tree');
- croak $? if $?;
- restore_index($index);
- }
- # just in case we clobber the existing ref, we still want that ref
- # as our parent:
- if (my $cur = verify_ref("refs/remotes/$GIT_SVN^0")) {
- chomp $cur;
- push @tmp_parents, $cur;
- }
-
- if (exists $tree_map{$tree}) {
- foreach my $p (@{$tree_map{$tree}}) {
- my $skip;
- foreach (@tmp_parents) {
- # see if a common parent is found
- my $mb = eval { command('merge-base', $_, $p) };
- next if ($@ || $?);
- $skip = 1;
- last;
- }
- next if $skip;
- my ($url_p, $r_p, $uuid_p) = cmt_metadata($p);
- next if (($SVN->uuid eq $uuid_p) &&
- ($log_msg->{revision} > $r_p));
- next if (defined $url_p && defined $SVN_URL &&
- ($SVN->uuid eq $uuid_p) &&
- ($url_p eq $SVN_URL));
- push @tmp_parents, $p;
- }
- }
- foreach (@tmp_parents) {
- next if $seen_parent{$_};
- $seen_parent{$_} = 1;
- push @exec_parents, $_;
- # MAXPARENT is defined to 16 in commit-tree.c:
- last if @exec_parents > 16;
- }
-
- set_commit_env($log_msg);
- my @exec = ('git-commit-tree', $tree);
- push @exec, '-p', $_ foreach @exec_parents;
- defined(my $pid = open3(my $msg_fh, my $out_fh, '>&STDERR', @exec))
- or croak $!;
- print $msg_fh $log_msg->{msg} or croak $!;
- unless ($_no_metadata) {
- print $msg_fh "\ngit-svn-id: $SVN_URL\@$log_msg->{revision} ",
- $SVN->uuid,"\n" or croak $!;
- }
- $msg_fh->flush == 0 or croak $!;
- close $msg_fh or croak $!;
- chomp(my $commit = do { local $/; <$out_fh> });
- close $out_fh or croak $!;
- waitpid $pid, 0;
- croak $? if $?;
- if ($commit !~ /^$sha1$/o) {
- die "Failed to commit, invalid sha1: $commit\n";
- }
- command_noisy('update-ref',"refs/remotes/$GIT_SVN",$commit);
- revdb_set($REVDB, $log_msg->{revision}, $commit);
-
- # this output is read via pipe, do not change:
- print "r$log_msg->{revision} = $commit\n";
- return $commit;
-}
-
-sub check_repack {
- if ($_repack && (--$_repack_nr == 0)) {
- $_repack_nr = $_repack;
- # repack doesn't use any arguments with spaces in them, does it?
- command_noisy('repack', split(/\s+/, $_repack_flags));
- }
-}
-
-sub set_commit_env {
- my ($log_msg) = @_;
- my $author = $log_msg->{author};
- if (!defined $author || length $author == 0) {
- $author = '(no author)';
- }
- my ($name,$email) = defined $users{$author} ? @{$users{$author}}
- : ($author,$author . '@' . $SVN->uuid);
- $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
- $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email;
- $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date};
-}
-
sub check_upgrade_needed {
if (!-r $REVDB) {
-d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
}
}
-# fills %tree_map with a reverse mapping of trees to commits. Useful
-# for finding parents to commit on.
-sub map_tree_joins {
- my %seen;
- foreach my $br (@_branch_from) {
- my $pipe = command_output_pipe(qw/rev-list
- --topo-order --pretty=raw/, $br);
- while (<$pipe>) {
- if (/^commit ($sha1)$/o) {
- my $commit = $1;
-
- # if we've seen a commit,
- # we've seen its parents
- last if $seen{$commit};
- my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
- unless (defined $tree) {
- die "Failed to parse commit $commit\n";
- }
- push @{$tree_map{$tree}}, $commit;
- $seen{$commit} = 1;
- }
- }
- close $pipe;
- }
-}
-
-sub load_all_refs {
- if (@_branch_from) {
- print STDERR '--branch|-b parameters are ignored when ',
- "--branch-all-refs|-B is passed\n";
- }
-
- # don't worry about rev-list on non-commit objects/tags,
- # it shouldn't blow up if a ref is a blob or tree...
- @_branch_from = command(qw/rev-parse --symbolic --all/);
-}
-
# '<svn username> = real-name <email address>' mapping based on git-svnimport:
sub load_authors {
open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
print "Done upgrading.\n";
}
-sub find_rev_before {
- my ($r, $id, $eq_ok) = @_;
- my $f = "$GIT_DIR/svn/$id/.rev_db";
- return (undef,undef) unless -r $f;
- --$r unless $eq_ok;
- while ($r > 0) {
- if (my $c = revdb_get($f, $r)) {
- return ($r, $c);
- }
- --$r;
- }
- return (undef, undef);
-}
-
sub init_vars {
$GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
$Git::SVN::default = $GIT_SVN;
$REVDB = "$GIT_SVN_DIR/.rev_db";
$GIT_SVN_INDEX = "$GIT_SVN_DIR/index";
$SVN_URL = undef;
- %tree_map = ();
}
# convert GetOpt::Long specs for use by git-config
}
}
-sub set_default_vals {
- if (defined $_repack) {
- $_repack = 1000 if ($_repack <= 0);
- $_repack_nr = $_repack;
- $_repack_flags ||= '-d';
- }
-}
-
-sub read_grafts {
- my $gr_file = shift;
- my ($grafts, $comments) = ({}, {});
- if (open my $fh, '<', $gr_file) {
- my @tmp;
- while (<$fh>) {
- if (/^($sha1)\s+/) {
- my $c = $1;
- if (@tmp) {
- @{$comments->{$c}} = @tmp;
- @tmp = ();
- }
- foreach my $p (split /\s+/, $_) {
- $grafts->{$c}->{$p} = 1;
- }
- } else {
- push @tmp, $_;
- }
- }
- close $fh or croak $!;
- @{$comments->{'END'}} = @tmp if @tmp;
- }
- return ($grafts, $comments);
-}
-
-sub write_grafts {
- my ($grafts, $comments, $gr_file) = @_;
-
- open my $fh, '>', $gr_file or croak $!;
- foreach my $c (sort keys %$grafts) {
- if ($comments->{$c}) {
- print $fh $_ foreach @{$comments->{$c}};
- }
- my $p = $grafts->{$c};
- my %x; # real parents
- delete $p->{$c}; # commits are not self-reproducing...
- my $ch = command_output_pipe(qw/cat-file commit/, $c);
- while (<$ch>) {
- if (/^parent ($sha1)/) {
- $x{$1} = $p->{$1} = 1;
- } else {
- last unless /^\S/;
- }
- }
- close $ch; # breaking the pipe
-
- # if real parents are the only ones in the grafts, drop it
- next if join(' ',sort keys %$p) eq join(' ',sort keys %x);
-
- my (@ip, @jp, $mb);
- my %del = %x;
- @ip = @jp = keys %$p;
- foreach my $i (@ip) {
- next if $del{$i} || $p->{$i} == 2;
- foreach my $j (@jp) {
- next if $i eq $j || $del{$j} || $p->{$j} == 2;
- $mb = eval { command('merge-base', $i, $j) };
- next unless $mb;
- chomp $mb;
- next if $x{$mb};
- if ($mb eq $j) {
- delete $p->{$i};
- $del{$i} = 1;
- } elsif ($mb eq $i) {
- delete $p->{$j};
- $del{$j} = 1;
- }
- }
- }
-
- # if real parents are the only ones in the grafts, drop it
- next if join(' ',sort keys %$p) eq join(' ',sort keys %x);
-
- print $fh $c, ' ', join(' ', sort keys %$p),"\n";
- }
- if ($comments->{'END'}) {
- print $fh $_ foreach @{$comments->{'END'}};
- }
- close $fh or croak $!;
-}
-
sub read_url_paths_all {
my ($l_map, $pfx, $p) = @_;
my @dir;
}
sub get_commit_parents {
- my ($self, $log_msg, @parents) = @_;
+ my ($self, $log_entry, @parents) = @_;
my (%seen, @ret, @tmp);
# commit parents can be conditionally bound to a particular
# svn revision via: "svn_revno=commit_sha1", filter them out here:
foreach my $p (@parents) {
next unless defined $p;
if ($p =~ /^(\d+)=($::sha1_short)$/o) {
- push @tmp, $2 if $1 == $log_msg->{revision};
+ push @tmp, $2 if $1 == $log_entry->{revision};
} else {
push @tmp, $p if $p =~ /^$::sha1_short$/o;
}
if (my $cur = ::verify_ref($self->refname.'^0')) {
push @tmp, $cur;
}
- push @tmp, $_ foreach (@{$log_msg->{parents}}, @tmp);
+ push @tmp, $_ foreach (@{$log_entry->{parents}}, @tmp);
while (my $p = shift @tmp) {
next if $seen{$p};
$seen{$p} = 1;
last if @ret >= 16;
}
if (@tmp) {
- die "r$log_msg->{revision}: No room for parents:\n\t",
+ die "r$log_entry->{revision}: No room for parents:\n\t",
join("\n\t", @tmp), "\n";
}
@ret;
}
sub do_git_commit {
- my ($self, $log_msg, @parents) = @_;
- if (my $c = $self->rev_db_get($log_msg->{revision})) {
- croak "$log_msg->{revision} = $c already exists! ",
+ my ($self, $log_entry, @parents) = @_;
+ if (my $c = $self->rev_db_get($log_entry->{revision})) {
+ croak "$log_entry->{revision} = $c already exists! ",
"Why are we refetching it?\n";
}
- my ($name, $email) = ::author_name_email($log_msg->{author}, $self->ra);
+ my $author = $log_entry->{author};
+ my ($name, $email) = (defined $::users{$author} ? @{$::users{$author}}
+ : ($author, "$author\@".$self->ra->uuid));
$ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
$ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email;
- $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date};
+ $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_entry->{date};
- my $tree = $log_msg->{tree};
+ my $tree = $log_entry->{tree};
if (!defined $tree) {
$tree = $self->tmp_index_do(sub {
command_oneline('write-tree') });
die "Tree is not a valid sha1: $tree\n" if $tree !~ /^$::sha1$/o;
my @exec = ('git-commit-tree', $tree);
- foreach ($self->get_commit_parents($log_msg, @parents)) {
+ foreach ($self->get_commit_parents($log_entry, @parents)) {
push @exec, '-p', $_;
}
defined(my $pid = open3(my $msg_fh, my $out_fh, '>&STDERR', @exec))
or croak $!;
- print $msg_fh $log_msg->{log} or croak $!;
- print $msg_fh "\ngit-svn-id: $self->{ra}->{url}\@$log_msg->{revision}",
- " ", $self->ra->uuid,"\n" or croak $!;
+ print $msg_fh $log_entry->{log} or croak $!;
+ print $msg_fh "\ngit-svn-id: ", $self->ra->{url}, '@',
+ $log_entry->{revision}, ' ',
+ $self->ra->uuid, "\n" or croak $!;
$msg_fh->flush == 0 or croak $!;
close $msg_fh or croak $!;
chomp(my $commit = do { local $/; <$out_fh> });
}
command_noisy('update-ref',$self->refname, $commit);
- $self->rev_db_set($log_msg->{revision}, $commit);
+ $self->rev_db_set($log_entry->{revision}, $commit);
- $self->{last_rev} = $log_msg->{revision};
+ $self->{last_rev} = $log_entry->{revision};
$self->{last_commit} = $commit;
- print "r$log_msg->{revision} = $commit\n";
+ print "r$log_entry->{revision} = $commit\n";
return $commit;
}
sub do_fetch {
- my ($self, $paths, $rev) = @_; #, $author, $date, $msg) = @_;
+ my ($self, $paths, $rev) = @_; #, $author, $date, $log) = @_;
my $ed = SVN::Git::Fetcher->new($self);
my ($last_rev, @parents);
if ($self->{last_commit}) {
} else {
$last_rev = $rev;
}
- unless ($self->ra->do_update($last_rev, $rev, '', 1, $ed)) {
+ unless ($self->ra->gs_do_update($last_rev, $rev, '', 1, $ed)) {
die "SVN connection failed somewhere...\n";
}
$self->make_log_entry($rev, \@parents, $ed);
foreach my $path (sort keys %$h) {
my $ppath = $path eq '' ? '.' : $path;
foreach my $prop (sort keys %{$h->{$path}}) {
- next if $SKIP{$prop};
+ next if $SKIP_PROP{$prop};
my $v = $h->{$path}->{$prop};
if (defined $v) {
print $fh " +$t: ",
}
}
+sub parse_svn_date {
+ my $date = shift || return '+0000 1970-01-01 00:00:00';
+ my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
+ (\d\d)\:(\d\d)\:(\d\d).\d+Z$/x) or
+ croak "Unable to parse date: $date\n";
+ "+0000 $Y-$m-$d $H:$M:$S";
+}
+
+sub check_author {
+ my ($author) = @_;
+ if (!defined $author || length $author == 0) {
+ $author = '(no author)';
+ }
+ if (defined $::_authors && ! defined $::users{$author}) {
+ die "Author: $author not defined in $::_authors file\n";
+ }
+ $author;
+}
+
sub make_log_entry {
my ($self, $rev, $parents, $untracked) = @_;
my $rp = $self->ra->rev_proplist($rev);
while (1) {
my @revs;
$self->ra->get_log([''], $min, $max, 0, 1, 1, sub {
- my ($paths, $rev, $author, $date, $msg) = @_;
+ my ($paths, $rev, $author, $date, $log) = @_;
push @revs, $rev });
foreach (@revs) {
my $log_entry = $self->do_fetch(undef, $_);
sub set_tree {
my ($self, $tree) = (shift, shift);
- my $log_entry = get_commit_entry($tree);
+ my $log_entry = ::get_commit_entry($tree);
unless ($self->{last_rev}) {
fatal("Must have an existing revision to commit\n");
}
my $pool = SVN::Pool->new;
my $ed = SVN::Git::Editor->new({ r => $self->{last_rev},
ra => $self->ra->dup,
- c => $tree,
svn_path => $self->ra->{svn_path}
},
$self->ra->get_commit_editor(
db_path => "$dir/.rev_db" }, $class;
}
+sub uri_encode {
+ my ($f) = @_;
+ $f =~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;
+ $f
+}
package Git::SVN::Prompt;
use strict;
$f
}
-sub libsvn_log_entry {
- my ($rev, $author, $date, $msg, $parents, $untracked) = @_;
- my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
- (\d\d)\:(\d\d)\:(\d\d).\d+Z$/x)
- or die "Unable to parse date: $date\n";
- if (defined $author && length $author > 0 &&
- defined $_authors && ! defined $users{$author}) {
- die "Author: $author not defined in $_authors file\n";
- }
- $msg = '' if ($rev == 0 && !defined $msg);
-
- open my $un, '>>', "$GIT_SVN_DIR/unhandled.log" or croak $!;
- my $h;
- print $un "r$rev\n" or croak $!;
- $h = $untracked->{empty};
- foreach (sort keys %$h) {
- my $act = $h->{$_} ? '+empty_dir' : '-empty_dir';
- print $un " $act: ", uri_encode($_), "\n" or croak $!;
- warn "W: $act: $_\n";
- }
- foreach my $t (qw/dir_prop file_prop/) {
- $h = $untracked->{$t} or next;
- foreach my $path (sort keys %$h) {
- my $ppath = $path eq '' ? '.' : $path;
- foreach my $prop (sort keys %{$h->{$path}}) {
- next if $SKIP{$prop};
- my $v = $h->{$path}->{$prop};
- if (defined $v) {
- print $un " +$t: ",
- uri_encode($ppath), ' ',
- uri_encode($prop), ' ',
- uri_encode($v), "\n"
- or croak $!;
- } else {
- print $un " -$t: ",
- uri_encode($ppath), ' ',
- uri_encode($prop), "\n"
- or croak $!;
- }
- }
- }
- }
- foreach my $t (qw/absent_file absent_directory/) {
- $h = $untracked->{$t} or next;
- foreach my $parent (sort keys %$h) {
- foreach my $path (sort @{$h->{$parent}}) {
- print $un " $t: ",
- uri_encode("$parent/$path"), "\n"
- or croak $!;
- warn "W: $t: $parent/$path ",
- "Insufficient permissions?\n";
- }
- }
- }
-
- # revprops (make this optional? it's an extra network trip...)
- my $rp = $SVN->rev_proplist($rev);
- foreach (sort keys %$rp) {
- next if /^svn:(?:author|date|log)$/;
- print $un " rev_prop: ", uri_encode($_), ' ',
- uri_encode($rp->{$_}), "\n";
- }
- close $un or croak $!;
-
- { revision => $rev, date => "+0000 $Y-$m-$d $H:$M:$S",
- author => $author, msg => $msg."\n", parents => $parents || [],
- revprops => $rp }
-}
-
-sub libsvn_fetch {
- my ($last_commit, $paths, $rev, $author, $date, $msg) = @_;
- my $ed = SVN::Git::Fetcher->new({ c => $last_commit, q => $_q });
- my (undef, $last_rev, undef) = cmt_metadata($last_commit);
- unless ($SVN->gs_do_update($last_rev, $rev, '', 1, $ed)) {
- die "SVN connection failed somewhere...\n";
- }
- libsvn_log_entry($rev, $author, $date, $msg, [$last_commit], $ed);
-}
-
-sub svn_grab_base_rev {
- my $c = eval { command_oneline([qw/rev-parse --verify/,
- "refs/remotes/$GIT_SVN^0"],
- { STDERR => 0 }) };
- if (defined $c && length $c) {
- my ($url, $rev, $uuid) = cmt_metadata($c);
- return ($rev, $c) if defined $rev;
- }
- if ($_no_metadata) {
- my $offset = -41; # from tail
- my $rl;
- open my $fh, '<', $REVDB or
- die "--no-metadata specified and $REVDB not readable\n";
- seek $fh, $offset, 2;
- $rl = readline $fh;
- defined $rl or return (undef, undef);
- chomp $rl;
- while ($c ne $rl && tell $fh != 0) {
- $offset -= 41;
- seek $fh, $offset, 2;
- $rl = readline $fh;
- defined $rl or return (undef, undef);
- chomp $rl;
- }
- my $rev = tell $fh;
- croak $! if ($rev < -1);
- $rev = ($rev - 41) / 41;
- close $fh or croak $!;
- return ($rev, $c);
- }
- return (undef, undef);
-}
-
-sub libsvn_parse_revision {
- my $base = shift;
- my $head = $SVN->get_latest_revnum();
- if (!defined $_revision || $_revision eq 'BASE:HEAD') {
- return ($base + 1, $head) if (defined $base);
- return (0, $head);
- }
- return ($1, $2) if ($_revision =~ /^(\d+):(\d+)$/);
- return ($_revision, $_revision) if ($_revision =~ /^\d+$/);
- if ($_revision =~ /^BASE:(\d+)$/) {
- return ($base + 1, $1) if (defined $base);
- return (0, $head);
- }
- return ($1, $head) if ($_revision =~ /^(\d+):HEAD$/);
- die "revision argument: $_revision not understood by git-svn\n",
- "Try using the command-line svn client instead\n";
-}
-
-sub libsvn_traverse_ignore {
- my ($fh, $path, $r) = @_;
- $path =~ s#^/+##g;
- my ($dirent, undef, $props) = $SVN->get_dir($path, $r);
- my $p = $path;
- $p =~ s#^\Q$SVN->{svn_path}\E/##;
- print $fh length $p ? "\n# $p\n" : "\n# /\n";
- if (my $s = $props->{'svn:ignore'}) {
- $s =~ s/[\r\n]+/\n/g;
- chomp $s;
- if (length $p == 0) {
- $s =~ s#\n#\n/$p#g;
- print $fh "/$s\n";
- } else {
- $s =~ s#\n#\n/$p/#g;
- print $fh "/$p/$s\n";
- }
- }
- foreach (sort keys %$dirent) {
- next if $dirent->{$_}->kind != $SVN::Node::dir;
- libsvn_traverse_ignore($fh, "$path/$_", $r);
- }
-}
-
sub revisions_eq {
my ($path, $r0, $r1) = @_;
return 1 if $r0 == $r1;
}
sub libsvn_find_parent_branch {
- my ($paths, $rev, $author, $date, $msg) = @_;
+ my ($paths, $rev, $author, $date, $log) = @_;
my $svn_path = '/'.$SVN->{svn_path};
# look for a parent from another branch:
command_noisy('read-tree', $parent);
unless ($SVN->can_do_switch) {
return _libsvn_new_tree($paths, $rev, $author, $date,
- $msg, [$parent]);
+ $log, [$parent]);
}
# do_switch works with svn/trunk >= r22312, but that is not
# included with SVN 1.4.2 (the latest version at the moment),
my $ed = SVN::Git::Fetcher->new({c => $parent, q => $_q });
$ra->gs_do_switch($r0, $rev, '', 1, $SVN->{url}, $ed) or
die "SVN connection failed somewhere...\n";
- return libsvn_log_entry($rev, $author, $date, $msg, [$parent]);
+ return libsvn_log_entry($rev, $author, $date, $log, [$parent]);
}
print STDERR "Nope, branch point not imported or unknown\n";
return undef;
}
-sub libsvn_new_tree {
- if (my $log_entry = libsvn_find_parent_branch(@_)) {
- return $log_entry;
- }
- my ($paths, $rev, $author, $date, $msg) = @_; # $pool is last
- _libsvn_new_tree($paths, $rev, $author, $date, $msg, []);
-}
-
sub _libsvn_new_tree {
- my ($paths, $rev, $author, $date, $msg, $parents) = @_;
+ my ($paths, $rev, $author, $date, $log, $parents) = @_;
my $ed = SVN::Git::Fetcher->new({q => $_q});
unless ($SVN->gs_do_update($rev, $rev, '', 1, $ed)) {
die "SVN connection failed somewhere...\n";
}
- libsvn_log_entry($rev, $author, $date, $msg, $parents, $ed);
-}
-
-sub find_graft_path_commit {
- my ($tree_paths, $p1, $r1) = @_;
- foreach my $x (keys %$tree_paths) {
- next unless ($p1 =~ /^\Q$x\E/);
- my $i = $tree_paths->{$x};
- my ($r0, $parent) = find_rev_before($r1,$i,1);
- return $parent if (defined $r0 && $r0 == $r1);
- print STDERR "r$r1 of $i not imported\n";
- next;
- }
- return undef;
-}
-
-sub find_graft_path_parents {
- my ($grafts, $tree_paths, $c, $p0, $r0) = @_;
- foreach my $x (keys %$tree_paths) {
- next unless ($p0 =~ /^\Q$x\E/);
- my $i = $tree_paths->{$x};
- my ($r, $parent) = find_rev_before($r0, $i, 1);
- if (defined $r && defined $parent && revisions_eq($x,$r,$r0)) {
- my ($url_b, undef, $uuid_b) = cmt_metadata($c);
- my ($url_a, undef, $uuid_a) = cmt_metadata($parent);
- next if ($url_a && $url_b && $url_a eq $url_b &&
- $uuid_b eq $uuid_a);
- $grafts->{$c}->{$parent} = 1;
- }
- }
-}
-
-sub libsvn_graft_file_copies {
- my ($grafts, $tree_paths, $path, $paths, $rev) = @_;
- foreach (keys %$paths) {
- my $i = $paths->{$_};
- my ($m, $p0, $r0) = ($i->action, $i->copyfrom_path,
- $i->copyfrom_rev);
- next unless (defined $p0 && defined $r0);
-
- my $p1 = $_;
- $p1 =~ s#^/##;
- $p0 =~ s#^/##;
- my $c = find_graft_path_commit($tree_paths, $p1, $rev);
- next unless $c;
- find_graft_path_parents($grafts, $tree_paths, $c, $p0, $r0);
- }
-}
-
-sub set_index {
- my $old = $ENV{GIT_INDEX_FILE};
- $ENV{GIT_INDEX_FILE} = shift;
- return $old;
-}
-
-sub restore_index {
- my ($old) = @_;
- if (defined $old) {
- $ENV{GIT_INDEX_FILE} = $old;
- } else {
- delete $ENV{GIT_INDEX_FILE};
- }
-}
-
-sub libsvn_commit_cb {
- my ($rev, $date, $committer, $c, $msg, $r_last, $cmt_last) = @_;
- if ($_optimize_commits && $rev == ($r_last + 1)) {
- my $log = libsvn_log_entry($rev,$committer,$date,$msg);
- $log->{tree} = get_tree_from_treeish($c);
- my $cmt = git_commit($log, $cmt_last, $c);
- my @diff = command('diff-tree', $cmt, $c);
- if (@diff) {
- print STDERR "Trees differ: $cmt $c\n",
- join('',@diff),"\n";
- exit 1;
- }
- } else {
- fetch("$rev=$c");
- }
-}
-
-sub libsvn_skip_unknown_revs {
- my $err = shift;
- my $errno = $err->apr_err();
- # Maybe the branch we're tracking didn't
- # exist when the repo started, so it's
- # not an error if it doesn't, just continue
- #
- # Wonderfully consistent library, eh?
- # 160013 - svn:// and file://
- # 175002 - http(s)://
- # 175007 - http(s):// (this repo required authorization, too...)
- # More codes may be discovered later...
- if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
- return;
- }
- croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";
-};
-
-# Tie::File seems to be prone to offset errors if revisions get sparse,
-# it's not that fast, either. Tie::File is also not in Perl 5.6. So
-# one of my favorite modules is out :< Next up would be one of the DBM
-# modules, but I'm not sure which is most portable... So I'll just
-# go with something that's plain-text, but still capable of
-# being randomly accessed. So here's my ultra-simple fixed-width
-# database. All records are 40 characters + "\n", so it's easy to seek
-# to a revision: (41 * rev) is the byte offset.
-# A record of 40 0s denotes an empty revision.
-# And yes, it's still pretty fast (faster than Tie::File).
-sub revdb_set {
- my ($file, $rev, $commit) = @_;
- length $commit == 40 or croak "arg3 must be a full SHA1 hexsum\n";
- open my $fh, '+<', $file or croak $!;
- my $offset = $rev * 41;
- # assume that append is the common case:
- seek $fh, 0, 2 or croak $!;
- my $pos = tell $fh;
- if ($pos < $offset) {
- print $fh (('0' x 40),"\n") x (($offset - $pos) / 41);
- }
- seek $fh, $offset, 0 or croak $!;
- print $fh $commit,"\n";
- close $fh or croak $!;
-}
-
-sub revdb_get {
- my ($file, $rev) = @_;
- my $ret;
- my $offset = $rev * 41;
- open my $fh, '<', $file or croak $!;
- seek $fh, $offset, 0;
- if (tell $fh == $offset) {
- $ret = readline $fh;
- if (defined $ret) {
- chomp $ret;
- $ret = undef if ($ret =~ /^0{40}$/);
- }
- }
- close $fh or croak $!;
- return $ret;
+ libsvn_log_entry($rev, $author, $date, $log, $parents, $ed);
}
{
my ($class, $git_svn) = @_;
my $self = SVN::Delta::Editor->new;
bless $self, $class;
- $self->{c} = $git_svn->{c} if exists $git_svn->{c};
- $self->{q} = $git_svn->{q};
+ $self->{c} = $git_svn->{last_commit} if exists $git_svn->{last_commit};
$self->{empty} = {};
$self->{dir_prop} = {};
$self->{file_prop} = {};
$self->{absent_dir} = {};
$self->{absent_file} = {};
- ($self->{gui}, $self->{ctx}) = command_input_pipe(
- qw/update-index -z --index-info/);
+ ($self->{gui}, $self->{ctx}) = $git_svn->tmp_index_do(
+ sub { command_input_pipe(qw/update-index -z --index-info/) } );
require Digest::MD5;
$self;
}
my $git_svn = shift;
my $self = SVN::Delta::Editor->new(@_);
bless $self, $class;
- foreach (qw/svn_path c r ra /) {
+ foreach (qw/svn_path r ra/) {
die "$_ required!\n" unless (defined $git_svn->{$_});
$self->{$_} = $git_svn->{$_};
}
}
sub rmdirs {
- my ($self, $q) = @_;
+ my ($self, $tree_b) = @_;
my $rm = $self->{rm};
delete $rm->{''}; # we never delete the url we're tracking
return unless %$rm;
return unless %$rm;
my ($fh, $ctx) = command_output_pipe(
- qw/ls-tree --name-only -r -z/, $self->{c});
+ qw/ls-tree --name-only -r -z/, $tree_b);
local $/ = "\0";
while (<$fh>) {
chomp;
foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
$self->close_directory($bat->{$d}, $p);
my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#);
- print "\tD+\t$d/\n" unless $q;
+ print "\tD+\t$d/\n" unless $::_q;
$self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p);
delete $bat->{$d};
}
}
sub A {
- my ($self, $m, $q) = @_;
+ my ($self, $m) = @_;
my ($dir, $file) = split_path($m->{file_b});
my $pbat = $self->ensure_path($dir);
my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
undef, -1);
- print "\tA\t$m->{file_b}\n" unless $q;
+ print "\tA\t$m->{file_b}\n" unless $::_q;
$self->chg_file($fbat, $m);
$self->close_file($fbat,undef,$self->{pool});
}
sub C {
- my ($self, $m, $q) = @_;
+ my ($self, $m) = @_;
my ($dir, $file) = split_path($m->{file_b});
my $pbat = $self->ensure_path($dir);
my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
$self->url_path($m->{file_a}), $self->{r});
- print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $q;
+ print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
$self->chg_file($fbat, $m);
$self->close_file($fbat,undef,$self->{pool});
}
}
sub R {
- my ($self, $m, $q) = @_;
+ my ($self, $m) = @_;
my ($dir, $file) = split_path($m->{file_b});
my $pbat = $self->ensure_path($dir);
my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
$self->url_path($m->{file_a}), $self->{r});
- print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $q;
+ print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
$self->chg_file($fbat, $m);
$self->close_file($fbat,undef,$self->{pool});
}
sub M {
- my ($self, $m, $q) = @_;
+ my ($self, $m) = @_;
my ($dir, $file) = split_path($m->{file_b});
my $pbat = $self->ensure_path($dir);
my $fbat = $self->open_file($self->repo_path($m->{file_b}),
$pbat,$self->{r},$self->{pool});
- print "\t$m->{chg}\t$m->{file_b}\n" unless $q;
+ print "\t$m->{chg}\t$m->{file_b}\n" unless $::_q;
$self->chg_file($fbat, $m);
$self->close_file($fbat,undef,$self->{pool});
}
}
sub D {
- my ($self, $m, $q) = @_;
+ my ($self, $m) = @_;
my ($dir, $file) = split_path($m->{file_b});
my $pbat = $self->ensure_path($dir);
- print "\tD\t$m->{file_b}\n" unless $q;
+ print "\tD\t$m->{file_b}\n" unless $::_q;
$self->delete_entry($m->{file_b}, $pbat);
}
$self->{pool}->clear;
}
+# this drives the editor
+sub apply_diff {
+ my ($self, $tree_a, $tree_b) = @_;
+ my @diff_tree = qw(diff-tree -z -r);
+ if ($::_cp_similarity) {
+ push @diff_tree, "-C$::_cp_similarity";
+ } else {
+ push @diff_tree, '-C';
+ }
+ push @diff_tree, '--find-copies-harder' if $::_find_copies_harder;
+ push @diff_tree, "-l$::_l" if defined $::_l;
+ push @diff_tree, $tree_a, $tree_b;
+ my ($diff_fh, $ctx) = command_output_pipe(@diff_tree);
+ my $nl = $/;
+ local $/ = "\0";
+ my $state = 'meta';
+ my @mods;
+ while (<$diff_fh>) {
+ chomp $_; # this gets rid of the trailing "\0"
+ if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
+ $::sha1\s($::sha1)\s
+ ([MTCRAD])\d*$/xo) {
+ push @mods, { mode_a => $1, mode_b => $2,
+ sha1_b => $3, chg => $4 };
+ if ($4 =~ /^(?:C|R)$/) {
+ $state = 'file_a';
+ } else {
+ $state = 'file_b';
+ }
+ } elsif ($state eq 'file_a') {
+ my $x = $mods[$#mods] or croak "Empty array\n";
+ if ($x->{chg} !~ /^(?:C|R)$/) {
+ croak "Error parsing $_, $x->{chg}\n";
+ }
+ $x->{file_a} = $_;
+ $state = 'file_b';
+ } elsif ($state eq 'file_b') {
+ my $x = $mods[$#mods] or croak "Empty array\n";
+ if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
+ croak "Error parsing $_, $x->{chg}\n";
+ }
+ if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
+ croak "Error parsing $_, $x->{chg}\n";
+ }
+ $x->{file_b} = $_;
+ $state = 'meta';
+ } else {
+ croak "Error parsing $_\n";
+ }
+ }
+ command_close_pipe($diff_fh, $ctx);
+ $/ = $nl;
+
+ my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
+ foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @mods) {
+ my $f = $m->{chg};
+ if (defined $o{$f}) {
+ $self->$f($m);
+ } else {
+ fatal("Invalid change type: $f\n");
+ }
+ }
+ $self->rmdirs($tree_b) if $::_rmdir;
+ if (@mods == 0) {
+ $self->abort_edit;
+ } else {
+ $self->close_edit;
+ }
+ \@mods;
+}
+
package Git::SVN::Ra;
use vars qw/@ISA $config_dir/;
use strict;
}
sub get_commit_editor {
- my ($self, $msg, $cb, $pool) = @_;
+ my ($self, $log, $cb, $pool) = @_;
my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
- $self->SUPER::get_commit_editor($msg, $cb, @lock, $pool);
+ $self->SUPER::get_commit_editor($log, $cb, @lock, $pool);
}
sub uuid {
return 1 if defined $c->{r};
if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
$c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
- my @msg = command(qw/cat-file commit/, $c->{c});
- shift @msg while ($msg[0] ne "\n");
- shift @msg;
- @{$c->{l}} = grep !/^git-svn-id: /, @msg;
+ my @log = command(qw/cat-file commit/, $c->{c});
+ shift @log while ($log[0] ne "\n");
+ shift @log;
+ @{$c->{l}} = grep !/^git-svn-id: /, @log;
(undef, $c->{r}, undef) = ::extract_metadata(
- (grep(/^git-svn-id: /, @msg))[-1]);
+ (grep(/^git-svn-id: /, @log))[-1]);
}
return defined $c->{r};
}
$author =~ s/(?:^\s*|\s*$)//g;
$dest->{a_raw} = $author;
my $au;
- if ($_authors) {
+ if ($::_authors) {
$au = $rusers{$author} || undef;
}
if (!$au) {
Data structures:
-$log_msg hashref as returned by libsvn_log_entry()
+$log_entry hashref as returned by libsvn_log_entry()
{
- msg => 'whitespace-formatted log entry
+ log => 'whitespace-formatted log entry
', # trailing newline is preserved
revision => '8', # integer
date => '2004-02-24T17:01:44.108345Z', # commit date