git-svn: cleanup: avoid re-use()ing Git.pm in sub-packages
[git/git.git] / git-svn.perl
CommitLineData
3397f9df 1#!/usr/bin/env perl
551ce28f
EW
2# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
3# License: GPL v2 or later
3397f9df
EW
4use warnings;
5use strict;
6use vars qw/ $AUTHOR $VERSION
d81bf827 7 $SVN_URL
3397f9df 8 $GIT_SVN_INDEX $GIT_SVN
42d32870 9 $GIT_DIR $GIT_SVN_DIR $REVDB/;
3397f9df 10$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
60d02ccc 11$VERSION = '@@GIT_VERSION@@';
13ccd6d4
EW
12
13use Cwd qw/abs_path/;
14$GIT_DIR = abs_path($ENV{GIT_DIR} || '.git');
15$ENV{GIT_DIR} = $GIT_DIR;
16
ce475dfc 17my $LC_ALL = $ENV{LC_ALL};
79bb8d88 18my $TZ = $ENV{TZ};
3397f9df
EW
19# make sure the svn binary gives consistent output between locales and TZs:
20$ENV{TZ} = 'UTC';
21$ENV{LC_ALL} = 'C';
a00439ac 22$| = 1; # unbuffer STDOUT
3397f9df 23
d2a9a87b
EW
24# properties that we do not log:
25my %SKIP = ( 'svn:wc:ra_dav:version-url' => 1,
26 'svn:special' => 1,
27 'svn:executable' => 1,
28 'svn:entry:committed-rev' => 1,
29 'svn:entry:last-author' => 1,
30 'svn:entry:uuid' => 1,
31 'svn:entry:committed-date' => 1,
32);
33
6fda05ae 34sub fatal (@) { print STDERR @_; exit 1 }
b9c85187
EW
35require SVN::Core; # use()-ing this causes segfaults for me... *shrug*
36require SVN::Ra;
37require SVN::Delta;
38if ($SVN::Core::VERSION lt '1.1.0') {
39 fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)\n";
40}
d81bf827 41push @Git::SVN::Ra::ISA, 'SVN::Ra';
b9c85187
EW
42push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
43push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor';
3397f9df
EW
44use Carp qw/croak/;
45use IO::File qw//;
46use File::Basename qw/dirname basename/;
47use File::Path qw/mkpath/;
79bb8d88 48use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/;
e17512f3 49use POSIX qw/strftime/;
968bdf1f 50use IPC::Open3;
42d32870 51use Memoize;
336f1714 52use Git;
42d32870 53memoize('revisions_eq');
c1927a85
EW
54memoize('cmt_metadata');
55memoize('get_commit_time');
a5e0cedc 56
336f1714
EW
57BEGIN {
58 my $s;
59 foreach (qw/command command_oneline command_noisy command_output_pipe
60 command_input_pipe command_close_pipe/) {
61 $s .= "*SVN::Git::Editor::$_ = *SVN::Git::Fetcher::$_ = ".
62 "*$_ = *Git::$_; ";
63 }
64 eval $s;
65}
66
b9c85187 67my ($SVN);
83e9940a 68
42d32870 69my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS};
3397f9df 70my $sha1 = qr/[a-f\d]{40}/;
ac8e0b91 71my $sha1_short = qr/[a-f\d]{4,40}/;
9aca0258 72my $_esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
336f1714 73my ($_revision,$_stdin,$_help,$_rmdir,$_edit,
1a82e793 74 $_find_copies_harder, $_l, $_cp_similarity, $_cp_remote,
80f50749 75 $_repack, $_repack_nr, $_repack_flags, $_q,
d81bf827 76 $_message, $_file, $_no_metadata,
9d55b41a 77 $_template, $_shared, $_no_default_regex, $_no_graft_copy,
79bb8d88 78 $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
b22d4497 79 $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m,
30d055aa 80 $_merge, $_strategy, $_dry_run, $_ignore_nodate, $_non_recursive,
ae410987 81 $_pager, $_color, $_prefix);
336f1714 82my (@_branch_from, %tree_map, %users, %rusers);
883d0a78 83my @repo_path_split_cache;
d81bf827 84use vars qw/$_follow_parent/;
3397f9df 85
336f1714 86my %fc_opts = ( 'branch|b=s' => \@_branch_from,
a00439ac 87 'follow-parent|follow' => \$_follow_parent,
bf78b1d8 88 'branch-all-refs|B' => \$_branch_all_refs,
dc5869c0
EW
89 'authors-file|A=s' => \$_authors,
90 'repack:i' => \$_repack,
a00439ac 91 'no-metadata' => \$_no_metadata,
80f50749 92 'quiet|q' => \$_q,
d976acfd 93 'username=s' => \$Git::SVN::Prompt::_username,
d81bf827 94 'config-dir=s' => \$Git::SVN::Ra::config_dir,
d976acfd 95 'no-auth-cache' => \$Git::SVN::Prompt::_no_auth_cache,
f7bae37f 96 'ignore-nodate' => \$_ignore_nodate,
dc5869c0 97 'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
36f5b1f0 98
9d55b41a
EW
99my ($_trunk, $_tags, $_branches);
100my %multi_opts = ( 'trunk|T=s' => \$_trunk,
101 'tags|t=s' => \$_tags,
102 'branches|b=s' => \$_branches );
103my %init_opts = ( 'template=s' => \$_template, 'shared' => \$_shared );
27e9fb8d
EW
104my %cmt_opts = ( 'edit|e' => \$_edit,
105 'rmdir' => \$_rmdir,
106 'find-copies-harder' => \$_find_copies_harder,
107 'l=i' => \$_l,
108 'copy-similarity|C=i'=> \$_cp_similarity
109);
9d55b41a 110
3397f9df 111my %cmd = (
2a3240be 112 fetch => [ \&cmd_fetch, "Download new revisions from SVN",
eeb0abe0 113 { 'revision|r=s' => \$_revision, %fc_opts } ],
81c5a0e6 114 init => [ \&init, "Initialize a repo for tracking" .
f8ab6b73 115 " (requires URL argument)",
9d55b41a 116 \%init_opts ],
3289e86e
EW
117 dcommit => [ \&dcommit, 'Commit several diffs to merge with upstream',
118 { 'merge|m|M' => \$_merge,
119 'strategy|s=s' => \$_strategy,
120 'dry-run|n' => \$_dry_run,
4b155223 121 %cmt_opts, %fc_opts } ],
3289e86e 122 'set-tree' => [ \&commit, "Set an SVN repository to a git tree-ish",
27e9fb8d 123 { 'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ],
a5e0cedc
EW
124 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings",
125 { 'revision|r=i' => \$_revision } ],
eeb0abe0 126 rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)",
336f1714 127 { 'copy-remote|remote=s' => \$_cp_remote,
eeb0abe0 128 'upgrade' => \$_upgrade } ],
9d55b41a
EW
129 'graft-branches' => [ \&graft_branches,
130 'Detect merges/branches from already imported history',
131 { 'merge-rx|m' => \@_opt_m,
c1927a85
EW
132 'branch|b=s' => \@_branch_from,
133 'branch-all-refs|B' => \$_branch_all_refs,
9d55b41a
EW
134 'no-default-regex' => \$_no_default_regex,
135 'no-graft-copy' => \$_no_graft_copy } ],
136 'multi-init' => [ \&multi_init,
137 'Initialize multiple trees (like git-svnimport)',
1ca7558d
EW
138 { %multi_opts, %init_opts,
139 'revision|r=i' => \$_revision,
d976acfd 140 'username=s' => \$Git::SVN::Prompt::_username,
d81bf827 141 'config-dir=s' => \$Git::SVN::Ra::config_dir,
d976acfd 142 'no-auth-cache' => \$Git::SVN::Prompt::_no_auth_cache,
ae410987 143 'prefix=s' => \$_prefix,
1ca7558d 144 } ],
9d55b41a
EW
145 'multi-fetch' => [ \&multi_fetch,
146 'Fetch multiple trees (like git-svnimport)',
147 \%fc_opts ],
79bb8d88
EW
148 'log' => [ \&show_log, 'Show commit logs',
149 { 'limit=i' => \$_limit,
150 'revision|r=s' => \$_revision,
151 'verbose|v' => \$_verbose,
152 'incremental' => \$_incremental,
153 'oneline' => \$_oneline,
154 'show-commit' => \$_show_commit,
74a31a10 155 'non-recursive' => \$_non_recursive,
79bb8d88 156 'authors-file|A=s' => \$_authors,
9aca0258
EW
157 'color' => \$_color,
158 'pager=s' => \$_pager,
79bb8d88 159 } ],
27e9fb8d
EW
160 'commit-diff' => [ \&commit_diff, 'Commit a diff between two trees',
161 { 'message|m=s' => \$_message,
162 'file|F=s' => \$_file,
45bf473a 163 'revision|r=s' => \$_revision,
27e9fb8d 164 %cmt_opts } ],
3397f9df 165);
9d55b41a 166
3397f9df
EW
167my $cmd;
168for (my $i = 0; $i < @ARGV; $i++) {
169 if (defined $cmd{$ARGV[$i]}) {
170 $cmd = $ARGV[$i];
171 splice @ARGV, $i, 1;
172 last;
173 }
174};
175
448c81b4 176my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
a9612be2 177
b8c92cad 178read_repo_config(\%opts);
79bb8d88
EW
179my $rv = GetOptions(%opts, 'help|H|h' => \$_help,
180 'version|V' => \$_version,
181 'id|i=s' => \$GIT_SVN);
182exit 1 if (!$rv && $cmd ne 'log');
6f0783cf 183
dc5869c0 184set_default_vals();
3397f9df 185usage(0) if $_help;
551ce28f 186version() if $_version;
eeb0abe0 187usage(1) unless defined $cmd;
b8c92cad 188init_vars();
eeb0abe0 189load_authors() if $_authors;
bf78b1d8 190load_all_refs() if $_branch_all_refs;
7b520e62 191migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init|commit-diff)$/;
3397f9df
EW
192$cmd{$cmd}->[0]->(@ARGV);
193exit 0;
194
195####################### primary functions ######################
196sub usage {
197 my $exit = shift || 0;
198 my $fd = $exit ? \*STDERR : \*STDOUT;
199 print $fd <<"";
200git-svn - bidirectional operations between a single Subversion tree and git
201Usage: $0 <command> [options] [arguments]\n
448c81b4
EW
202
203 print $fd "Available commands:\n" unless $cmd;
3397f9df
EW
204
205 foreach (sort keys %cmd) {
448c81b4 206 next if $cmd && $cmd ne $_;
b203b769 207 print $fd ' ',pack('A17',$_),$cmd{$_}->[1],"\n";
448c81b4
EW
208 foreach (keys %{$cmd{$_}->[2]}) {
209 # prints out arguments as they should be passed:
b8c92cad 210 my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
b203b769 211 print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
448c81b4
EW
212 "--$_" : "-$_" }
213 split /\|/,$_)," $x\n";
214 }
3397f9df
EW
215 }
216 print $fd <<"";
448c81b4
EW
217\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an
218arbitrary identifier if you're tracking multiple SVN branches/repositories in
219one git repository and want to keep them separate. See git-svn(1) for more
220information.
3397f9df
EW
221
222 exit $exit;
223}
224
551ce28f 225sub version {
7d60ab2c 226 print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n";
551ce28f
EW
227 exit 0;
228}
229
3397f9df 230sub rebuild {
aef4e921 231 if (!verify_ref("refs/remotes/$GIT_SVN^0")) {
1a82e793
EW
232 copy_remote_ref();
233 }
3397f9df 234 $SVN_URL = shift or undef;
3397f9df 235 my $newest_rev = 0;
2beb3cdd 236 if ($_upgrade) {
aef4e921
EW
237 command_noisy('update-ref',"refs/remotes/$GIT_SVN","
238 $GIT_SVN-HEAD");
2beb3cdd
EW
239 } else {
240 check_upgrade_needed();
241 }
3397f9df 242
aef4e921
EW
243 my ($rev_list, $ctx) = command_output_pipe("rev-list",
244 "refs/remotes/$GIT_SVN");
2beb3cdd 245 my $latest;
d81bf827 246 my $svn_uuid;
3397f9df
EW
247 while (<$rev_list>) {
248 chomp;
249 my $c = $_;
250 croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o;
aef4e921
EW
251 my @commit = grep(/^git-svn-id: /,
252 command(qw/cat-file commit/, $c));
3397f9df 253 next if (!@commit); # skip merges
79bb8d88 254 my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]);
e70dc780 255 if (!defined $rev || !$uuid) {
79bb8d88
EW
256 croak "Unable to extract revision or UUID from ",
257 "$c, $commit[$#commit]\n";
3397f9df 258 }
2beb3cdd
EW
259
260 # if we merged or otherwise started elsewhere, this is
261 # how we break out of it
d81bf827 262 next if (defined $svn_uuid && ($uuid ne $svn_uuid));
779b1446 263 next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL));
2beb3cdd 264
2beb3cdd 265 unless (defined $latest) {
3397f9df
EW
266 if (!$SVN_URL && !$url) {
267 croak "SVN repository location required: $url\n";
268 }
269 $SVN_URL ||= $url;
d81bf827 270 $svn_uuid ||= $uuid;
1d52aba8 271 setup_git_svn();
2beb3cdd 272 $latest = $rev;
3397f9df 273 }
42d32870
EW
274 revdb_set($REVDB, $rev, $c);
275 print "r$rev = $c\n";
3397f9df
EW
276 $newest_rev = $rev if ($rev > $newest_rev);
277 }
aef4e921 278 command_close_pipe($rev_list, $ctx);
3397f9df
EW
279}
280
281sub init {
03e0ea87 282 my $url = shift or die "SVN repository location required " .
81c5a0e6 283 "as a command-line argument\n";
03e0ea87
EW
284 $url =~ s!/+$!!; # strip trailing slash
285
286 if (my $repo_path = shift) {
287 unless (-d $repo_path) {
288 mkpath([$repo_path]);
289 }
290 $GIT_DIR = $ENV{GIT_DIR} = $repo_path . "/.git";
291 init_vars();
292 }
293
294 $SVN_URL = $url;
3397f9df 295 unless (-d $GIT_DIR) {
5c94f87e 296 my @init_db = ('init');
f8ab6b73
EW
297 push @init_db, "--template=$_template" if defined $_template;
298 push @init_db, "--shared" if defined $_shared;
aef4e921 299 command_noisy(@init_db);
3397f9df
EW
300 }
301 setup_git_svn();
302}
303
2a3240be
EW
304sub cmd_fetch {
305 fetch_child_id($GIT_SVN, @_);
306}
307
3397f9df 308sub fetch {
2beb3cdd 309 check_upgrade_needed();
883d0a78 310 $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
b9c85187 311 my $ret = fetch_lib(@_);
aef4e921
EW
312 if ($ret->{commit} && !verify_ref('refs/heads/master^0')) {
313 command_noisy(qw(update-ref refs/heads/master),$ret->{commit});
a5e0cedc
EW
314 }
315 return $ret;
316}
317
a5e0cedc
EW
318sub fetch_lib {
319 my (@parents) = @_;
320 $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
d81bf827 321 $SVN ||= Git::SVN::Ra->new($SVN_URL);
a5e0cedc
EW
322 my ($last_rev, $last_commit) = svn_grab_base_rev();
323 my ($base, $head) = libsvn_parse_revision($last_rev);
324 if ($base > $head) {
325 return { revision => $last_rev, commit => $last_commit }
326 }
327 my $index = set_index($GIT_SVN_INDEX);
328
329 # limit ourselves and also fork() since get_log won't release memory
330 # after processing a revision and SVN stuff seems to leak
331 my $inc = 1000;
332 my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
a5e0cedc
EW
333 if (defined $last_commit) {
334 unless (-e $GIT_SVN_INDEX) {
aef4e921 335 command_noisy('read-tree', $last_commit);
a5e0cedc 336 }
aef4e921
EW
337 my $x = command_oneline('write-tree');
338 my ($y) = (command(qw/cat-file commit/, $last_commit)
a5e0cedc
EW
339 =~ /^tree ($sha1)/m);
340 if ($y ne $x) {
341 unlink $GIT_SVN_INDEX or croak $!;
aef4e921 342 command_noisy('read-tree', $last_commit);
a5e0cedc 343 }
aef4e921 344 $x = command_oneline('write-tree');
a5e0cedc
EW
345 if ($y ne $x) {
346 print STDERR "trees ($last_commit) $y != $x\n",
347 "Something is seriously wrong...\n";
348 }
349 }
350 while (1) {
351 # fork, because using SVN::Pool with get_log() still doesn't
352 # seem to help enough to keep memory usage down.
353 defined(my $pid = fork) or croak $!;
354 if (!$pid) {
355 $SVN::Error::handler = \&libsvn_skip_unknown_revs;
a5e0cedc
EW
356
357 # Yes I'm perfectly aware that the fourth argument
358 # below is the limit revisions number. Unfortunately
359 # performance sucks with it enabled, so it's much
360 # faster to fetch revision ranges instead of relying
361 # on the limiter.
d81bf827 362 $SVN->dup->get_log([''], $min, $max, 0, 1, 1,
a5e0cedc
EW
363 sub {
364 my $log_msg;
365 if ($last_commit) {
366 $log_msg = libsvn_fetch(
367 $last_commit, @_);
368 $last_commit = git_commit(
369 $log_msg,
370 $last_commit,
371 @parents);
372 } else {
373 $log_msg = libsvn_new_tree(@_);
374 $last_commit = git_commit(
375 $log_msg, @parents);
376 }
377 });
a5e0cedc
EW
378 exit 0;
379 }
380 waitpid $pid, 0;
381 croak $? if $?;
382 ($last_rev, $last_commit) = svn_grab_base_rev();
383 last if ($max >= $head);
384 $min = $max + 1;
385 $max += $inc;
386 $max = $head if ($max > $head);
d81bf827 387 $SVN = Git::SVN::Ra->new($SVN_URL);
a5e0cedc
EW
388 }
389 restore_index($index);
390 return { revision => $last_rev, commit => $last_commit };
391}
392
3397f9df
EW
393sub commit {
394 my (@commits) = @_;
2beb3cdd 395 check_upgrade_needed();
3397f9df
EW
396 if ($_stdin || !@commits) {
397 print "Reading from stdin...\n";
398 @commits = ();
399 while (<STDIN>) {
1ca72aef 400 if (/\b($sha1_short)\b/o) {
3397f9df
EW
401 unshift @commits, $1;
402 }
403 }
404 }
405 my @revs;
8de010ad 406 foreach my $c (@commits) {
aef4e921 407 my @tmp = command('rev-parse',$c);
8de010ad
EW
408 if (scalar @tmp == 1) {
409 push @revs, $tmp[0];
410 } elsif (scalar @tmp > 1) {
aef4e921 411 push @revs, reverse(command('rev-list',@tmp));
8de010ad
EW
412 } else {
413 die "Failed to rev-parse $c\n";
414 }
3397f9df 415 }
b9c85187 416 commit_lib(@revs);
a5e0cedc
EW
417 print "Done committing ",scalar @revs," revisions to SVN\n";
418}
419
a5e0cedc
EW
420sub commit_lib {
421 my (@revs) = @_;
422 my ($r_last, $cmt_last) = svn_grab_base_rev();
423 defined $r_last or die "Must have an existing revision to commit\n";
cf7424b0 424 my $fetched = fetch();
a5e0cedc
EW
425 if ($r_last != $fetched->{revision}) {
426 print STDERR "There are new revisions that were fetched ",
427 "and need to be merged (or acknowledged) ",
428 "before committing.\n",
429 "last rev: $r_last\n",
430 " current: $fetched->{revision}\n";
431 exit 1;
432 }
a5e0cedc
EW
433 my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
434
5a990e45 435 my $repo;
27e9fb8d 436 set_svn_commit_env();
a5e0cedc 437 foreach my $c (@revs) {
ec9d00d0
EW
438 my $log_msg = get_commit_message($c, $commit_msg);
439
a5e0cedc
EW
440 # fork for each commit because there's a memory leak I
441 # can't track down... (it's probably in the SVN code)
442 defined(my $pid = open my $fh, '-|') or croak $!;
443 if (!$pid) {
d81bf827 444 my $pool = SVN::Pool->new;
a5e0cedc
EW
445 my $ed = SVN::Git::Editor->new(
446 { r => $r_last,
d81bf827 447 ra => $SVN->dup,
a5e0cedc 448 c => $c,
747fa12c 449 svn_path => $SVN->{svn_path},
a5e0cedc
EW
450 },
451 $SVN->get_commit_editor(
452 $log_msg->{msg},
453 sub {
454 libsvn_commit_cb(
455 @_, $c,
456 $log_msg->{msg},
457 $r_last,
458 $cmt_last)
d81bf827 459 }, $pool)
a5e0cedc 460 );
42d32870 461 my $mods = libsvn_checkout_tree($cmt_last, $c, $ed);
a5e0cedc
EW
462 if (@$mods == 0) {
463 print "No changes\nr$r_last = $cmt_last\n";
464 $ed->abort_edit;
465 } else {
466 $ed->close_edit;
467 }
d81bf827 468 $pool->clear;
a5e0cedc
EW
469 exit 0;
470 }
471 my ($r_new, $cmt_new, $no);
472 while (<$fh>) {
473 print $_;
474 chomp;
475 if (/^r(\d+) = ($sha1)$/o) {
476 ($r_new, $cmt_new) = ($1, $2);
477 } elsif ($_ eq 'No changes') {
478 $no = 1;
479 }
480 }
d25c26e7 481 close $fh or exit 1;
a5e0cedc
EW
482 if (! defined $r_new && ! defined $cmt_new) {
483 unless ($no) {
484 die "Failed to parse revision information\n";
485 }
486 } else {
487 ($r_last, $cmt_last) = ($r_new, $cmt_new);
488 }
489 }
ec9d00d0 490 $ENV{LC_ALL} = 'C';
a5e0cedc
EW
491 unlink $commit_msg;
492}
8f22562c 493
b22d4497 494sub dcommit {
dd31da2f 495 my $head = shift || 'HEAD';
b22d4497 496 my $gs = "refs/remotes/$GIT_SVN";
aef4e921 497 my @refs = command(qw/rev-list --no-merges/, "$gs..$head");
45bf473a 498 my $last_rev;
b22d4497 499 foreach my $d (reverse @refs) {
aef4e921 500 if (!verify_ref("$d~1")) {
48d044b5
EW
501 die "Commit $d\n",
502 "has no parent commit, and therefore ",
503 "nothing to diff against.\n",
504 "You should be working from a repository ",
505 "originally created by git-svn\n";
506 }
45bf473a
EW
507 unless (defined $last_rev) {
508 (undef, $last_rev, undef) = cmt_metadata("$d~1");
509 unless (defined $last_rev) {
510 die "Unable to extract revision information ",
511 "from commit $d~1\n";
512 }
513 }
b22d4497
EW
514 if ($_dry_run) {
515 print "diff-tree $d~1 $d\n";
516 } else {
45bf473a
EW
517 if (my $r = commit_diff("$d~1", $d, undef, $last_rev)) {
518 $last_rev = $r;
519 } # else: no changes, same $last_rev
b22d4497
EW
520 }
521 }
522 return if $_dry_run;
523 fetch();
c3a41037 524 my @diff = command('diff-tree', 'HEAD', $gs, '--');
b22d4497
EW
525 my @finish;
526 if (@diff) {
527 @finish = qw/rebase/;
528 push @finish, qw/--merge/ if $_merge;
529 push @finish, "--strategy=$_strategy" if $_strategy;
c3a41037 530 print STDERR "W: HEAD and $gs differ, using @finish:\n", @diff;
b22d4497 531 } else {
c3a41037 532 print "No changes between current HEAD and $gs\n",
dd31da2f 533 "Resetting to the latest $gs\n";
4769489a 534 @finish = qw/reset --mixed/;
b22d4497 535 }
aef4e921 536 command_noisy(@finish, $gs);
b22d4497
EW
537}
538
a5e0cedc 539sub show_ignore {
883d0a78 540 $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
a5e0cedc 541 my $repo;
d81bf827 542 $SVN ||= Git::SVN::Ra->new($SVN_URL);
a5e0cedc 543 my $r = defined $_revision ? $_revision : $SVN->get_latest_revnum;
fffe694d 544 libsvn_traverse_ignore(\*STDOUT, '', $r);
a5e0cedc
EW
545}
546
9d55b41a
EW
547sub graft_branches {
548 my $gr_file = "$GIT_DIR/info/grafts";
549 my ($grafts, $comments) = read_grafts($gr_file);
550 my $gr_sha1;
551
552 if (%$grafts) {
553 # temporarily disable our grafts file to make this idempotent
aef4e921 554 chomp($gr_sha1 = command(qw/hash-object -w/,$gr_file));
9d55b41a
EW
555 rename $gr_file, "$gr_file~$gr_sha1" or croak $!;
556 }
557
558 my $l_map = read_url_paths();
559 my @re = map { qr/$_/is } @_opt_m if @_opt_m;
560 unless ($_no_default_regex) {
c1927a85
EW
561 push @re, (qr/\b(?:merge|merging|merged)\s+with\s+([\w\.\-]+)/i,
562 qr/\b(?:merge|merging|merged)\s+([\w\.\-]+)/i,
563 qr/\b(?:from|of)\s+([\w\.\-]+)/i );
9d55b41a
EW
564 }
565 foreach my $u (keys %$l_map) {
566 if (@re) {
567 foreach my $p (keys %{$l_map->{$u}}) {
c1927a85 568 graft_merge_msg($grafts,$l_map,$u,$p,@re);
9d55b41a
EW
569 }
570 }
a5e0cedc 571 unless ($_no_graft_copy) {
b9c85187 572 graft_file_copy_lib($grafts,$l_map,$u);
a5e0cedc 573 }
9d55b41a 574 }
c1927a85 575 graft_tree_joins($grafts);
9d55b41a
EW
576
577 write_grafts($grafts, $comments, $gr_file);
578 unlink "$gr_file~$gr_sha1" if $gr_sha1;
579}
580
581sub multi_init {
582 my $url = shift;
98327e58
EW
583 unless (defined $_trunk || defined $_branches || defined $_tags) {
584 usage(1);
9d55b41a 585 }
98327e58
EW
586 if (defined $_trunk) {
587 my $trunk_url = complete_svn_url($url, $_trunk);
588 my $ch_id;
589 if ($GIT_SVN eq 'git-svn') {
590 $ch_id = 1;
591 $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
592 }
593 init_vars();
594 unless (-d $GIT_SVN_DIR) {
595 if ($ch_id) {
596 print "GIT_SVN_ID set to 'trunk' for ",
597 "$trunk_url ($_trunk)\n";
598 }
599 init($trunk_url);
e0d10e1c 600 command_noisy('config', 'svn.trunk', $trunk_url);
98327e58 601 }
c35b96e7 602 }
ae410987
EW
603 $_prefix = '' unless defined $_prefix;
604 complete_url_ls_init($url, $_branches, '--branches/-b', $_prefix);
605 complete_url_ls_init($url, $_tags, '--tags/-t', $_prefix . 'tags/');
9d55b41a
EW
606}
607
608sub multi_fetch {
609 # try to do trunk first, since branches/tags
610 # may be descended from it.
cf7424b0
EW
611 if (-e "$GIT_DIR/svn/trunk/info/url") {
612 fetch_child_id('trunk', @_);
9d55b41a
EW
613 }
614 rec_fetch('', "$GIT_DIR/svn", @_);
615}
616
79bb8d88
EW
617sub show_log {
618 my (@args) = @_;
619 my ($r_min, $r_max);
620 my $r_last = -1; # prevent dupes
621 rload_authors() if $_authors;
622 if (defined $TZ) {
623 $ENV{TZ} = $TZ;
624 } else {
625 delete $ENV{TZ};
626 }
627 if (defined $_revision) {
628 if ($_revision =~ /^(\d+):(\d+)$/) {
629 ($r_min, $r_max) = ($1, $2);
630 } elsif ($_revision =~ /^\d+$/) {
631 $r_min = $r_max = $_revision;
632 } else {
633 print STDERR "-r$_revision is not supported, use ",
634 "standard \'git log\' arguments instead\n";
635 exit 1;
636 }
637 }
638
9aca0258 639 config_pager();
aef4e921
EW
640 @args = (git_svn_log_cmd($r_min, $r_max), @args);
641 my $log = command_output_pipe(@args);
9aca0258 642 run_pager();
79bb8d88 643 my (@k, $c, $d);
c0d48222 644
79bb8d88 645 while (<$log>) {
9aca0258 646 if (/^${_esc_color}commit ($sha1_short)/o) {
79bb8d88 647 my $cmt = $1;
c0d48222 648 if ($c && cmt_showable($c) && $c->{r} != $r_last) {
79bb8d88
EW
649 $r_last = $c->{r};
650 process_commit($c, $r_min, $r_max, \@k) or
651 goto out;
652 }
653 $d = undef;
654 $c = { c => $cmt };
9aca0258 655 } elsif (/^${_esc_color}author (.+) (\d+) ([\-\+]?\d+)$/) {
79bb8d88 656 get_author_info($c, $1, $2, $3);
9aca0258 657 } elsif (/^${_esc_color}(?:tree|parent|committer) /) {
79bb8d88 658 # ignore
9aca0258 659 } elsif (/^${_esc_color}:\d{6} \d{6} $sha1_short/o) {
79bb8d88 660 push @{$c->{raw}}, $_;
9aca0258 661 } elsif (/^${_esc_color}[ACRMDT]\t/) {
747fa12c 662 # we could add $SVN->{svn_path} here, but that requires
74a31a10 663 # remote access at the moment (repo_path_split)...
9aca0258 664 s#^(${_esc_color})([ACRMDT])\t#$1 $2 #;
74a31a10 665 push @{$c->{changed}}, $_;
9aca0258 666 } elsif (/^${_esc_color}diff /) {
79bb8d88
EW
667 $d = 1;
668 push @{$c->{diff}}, $_;
669 } elsif ($d) {
670 push @{$c->{diff}}, $_;
9aca0258 671 } elsif (/^${_esc_color} (git-svn-id:.+)$/) {
74a31a10 672 ($c->{url}, $c->{r}, undef) = extract_metadata($1);
9aca0258 673 } elsif (s/^${_esc_color} //) {
79bb8d88
EW
674 push @{$c->{l}}, $_;
675 }
676 }
677 if ($c && defined $c->{r} && $c->{r} != $r_last) {
678 $r_last = $c->{r};
679 process_commit($c, $r_min, $r_max, \@k);
680 }
681 if (@k) {
682 my $swap = $r_max;
683 $r_max = $r_min;
684 $r_min = $swap;
685 process_commit($_, $r_min, $r_max) foreach reverse @k;
686 }
687out:
22600a25 688 close $log;
79bb8d88
EW
689 print '-' x72,"\n" unless $_incremental || $_oneline;
690}
691
27e9fb8d
EW
692sub commit_diff_usage {
693 print STDERR "Usage: $0 commit-diff <tree-ish> <tree-ish> [<URL>]\n";
694 exit 1
695}
696
697sub commit_diff {
27e9fb8d
EW
698 my $ta = shift or commit_diff_usage();
699 my $tb = shift or commit_diff_usage();
700 if (!eval { $SVN_URL = shift || file_to_s("$GIT_SVN_DIR/info/url") }) {
701 print STDERR "Needed URL or usable git-svn id command-line\n";
702 commit_diff_usage();
703 }
e70dc780
EW
704 my $r = shift;
705 unless (defined $r) {
706 if (defined $_revision) {
707 $r = $_revision
708 } else {
709 die "-r|--revision is a required argument\n";
710 }
711 }
27e9fb8d
EW
712 if (defined $_message && defined $_file) {
713 print STDERR "Both --message/-m and --file/-F specified ",
714 "for the commit message.\n",
715 "I have no idea what you mean\n";
716 exit 1;
717 }
718 if (defined $_file) {
4ad4515d 719 $_message = file_to_s($_file);
27e9fb8d
EW
720 } else {
721 $_message ||= get_commit_message($tb,
722 "$GIT_DIR/.svn-commit.tmp.$$")->{msg};
723 }
d81bf827 724 $SVN ||= Git::SVN::Ra->new($SVN_URL);
45bf473a
EW
725 if ($r eq 'HEAD') {
726 $r = $SVN->get_latest_revnum;
727 } elsif ($r !~ /^\d+$/) {
728 die "revision argument: $r not understood by git-svn\n";
729 }
45bf473a 730 my $rev_committed;
d81bf827 731 my $pool = SVN::Pool->new;
45bf473a 732 my $ed = SVN::Git::Editor->new({ r => $r,
d81bf827 733 ra => $SVN->dup,
747fa12c
EW
734 c => $tb,
735 svn_path => $SVN->{svn_path}
27e9fb8d
EW
736 },
737 $SVN->get_commit_editor($_message,
45bf473a
EW
738 sub {
739 $rev_committed = $_[0];
740 print "Committed $_[0]\n";
d81bf827
EW
741 },
742 $pool)
27e9fb8d 743 );
d25c26e7
EW
744 eval {
745 my $mods = libsvn_checkout_tree($ta, $tb, $ed);
746 if (@$mods == 0) {
747 print "No changes\n$ta == $tb\n";
748 $ed->abort_edit;
749 } else {
750 $ed->close_edit;
751 }
752 };
d81bf827 753 $pool->clear;
d25c26e7 754 fatal "$@\n" if $@;
5f641ccc 755 $_message = $_file = undef;
45bf473a 756 return $rev_committed;
27e9fb8d
EW
757}
758
3397f9df
EW
759########################### utility functions #########################
760
c0d48222
EW
761sub cmt_showable {
762 my ($c) = @_;
763 return 1 if defined $c->{r};
764 if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
765 $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
aef4e921 766 my @msg = command(qw/cat-file commit/, $c->{c});
c0d48222
EW
767 shift @msg while ($msg[0] ne "\n");
768 shift @msg;
769 @{$c->{l}} = grep !/^git-svn-id: /, @msg;
770
771 (undef, $c->{r}, undef) = extract_metadata(
772 (grep(/^git-svn-id: /, @msg))[-1]);
773 }
774 return defined $c->{r};
775}
776
9aca0258
EW
777sub log_use_color {
778 return 1 if $_color;
62e1aeab
JH
779 my ($dc, $dcvar);
780 $dcvar = 'color.diff';
e0d10e1c 781 $dc = `git-config --get $dcvar`;
62e1aeab
JH
782 if ($dc eq '') {
783 # nothing at all; fallback to "diff.color"
784 $dcvar = 'diff.color';
e0d10e1c 785 $dc = `git-config --get $dcvar`;
62e1aeab
JH
786 }
787 chomp($dc);
9aca0258 788 if ($dc eq 'auto') {
62e1aeab 789 my $pc;
e0d10e1c 790 $pc = `git-config --get color.pager`;
62e1aeab
JH
791 if ($pc eq '') {
792 # does not have it -- fallback to pager.color
e0d10e1c 793 $pc = `git-config --bool --get pager.color`;
62e1aeab
JH
794 }
795 else {
e0d10e1c 796 $pc = `git-config --bool --get color.pager`;
62e1aeab
JH
797 if ($?) {
798 $pc = 'false';
799 }
800 }
801 chomp($pc);
802 if (-t *STDOUT || (defined $_pager && $pc eq 'true')) {
9aca0258
EW
803 return ($ENV{TERM} && $ENV{TERM} ne 'dumb');
804 }
805 return 0;
806 }
807 return 0 if $dc eq 'never';
808 return 1 if $dc eq 'always';
e0d10e1c 809 chomp($dc = `git-config --bool --get $dcvar`);
62e1aeab 810 return ($dc eq 'true');
9aca0258
EW
811}
812
c0d48222
EW
813sub git_svn_log_cmd {
814 my ($r_min, $r_max) = @_;
aef4e921 815 my @cmd = (qw/log --abbrev-commit --pretty=raw
c0d48222 816 --default/, "refs/remotes/$GIT_SVN");
74a31a10
EW
817 push @cmd, '-r' unless $_non_recursive;
818 push @cmd, qw/--raw --name-status/ if $_verbose;
9aca0258 819 push @cmd, '--color' if log_use_color();
c0d48222
EW
820 return @cmd unless defined $r_max;
821 if ($r_max == $r_min) {
822 push @cmd, '--max-count=1';
823 if (my $c = revdb_get($REVDB, $r_max)) {
824 push @cmd, $c;
825 }
826 } else {
827 my ($c_min, $c_max);
828 $c_max = revdb_get($REVDB, $r_max);
829 $c_min = revdb_get($REVDB, $r_min);
74a31a10 830 if (defined $c_min && defined $c_max) {
c0d48222
EW
831 if ($r_max > $r_max) {
832 push @cmd, "$c_min..$c_max";
833 } else {
834 push @cmd, "$c_max..$c_min";
835 }
836 } elsif ($r_max > $r_min) {
837 push @cmd, $c_max;
838 } else {
839 push @cmd, $c_min;
840 }
841 }
842 return @cmd;
843}
844
cf7424b0
EW
845sub fetch_child_id {
846 my $id = shift;
847 print "Fetching $id\n";
848 my $ref = "$GIT_DIR/refs/remotes/$id";
a00439ac 849 defined(my $pid = open my $fh, '-|') or croak $!;
cf7424b0
EW
850 if (!$pid) {
851 $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
852 init_vars();
853 fetch(@_);
854 exit 0;
855 }
cf7424b0 856 while (<$fh>) {
a00439ac 857 print $_;
2a3240be 858 check_repack() if (/^r\d+ = $sha1/o);
cf7424b0 859 }
a00439ac 860 close $fh or croak $?;
cf7424b0
EW
861}
862
9d55b41a
EW
863sub rec_fetch {
864 my ($pfx, $p, @args) = @_;
865 my @dir;
866 foreach (sort <$p/*>) {
867 if (-r "$_/info/url") {
868 $pfx .= '/' if $pfx && $pfx !~ m!/$!;
869 my $id = $pfx . basename $_;
870 next if $id eq 'trunk';
cf7424b0 871 fetch_child_id($id, @args);
9d55b41a
EW
872 } elsif (-d $_) {
873 push @dir, $_;
874 }
875 }
876 foreach (@dir) {
877 my $x = $_;
878 $x =~ s!^\Q$GIT_DIR\E/svn/!!;
879 rec_fetch($x, $_);
880 }
881}
882
98327e58
EW
883sub complete_svn_url {
884 my ($url, $path) = @_;
885 $path =~ s#/+$##;
886 $url =~ s#/+$## if $url;
887 if ($path !~ m#^[a-z\+]+://#) {
888 $path = '/' . $path if ($path !~ m#^/#);
889 if (!defined $url || $url !~ m#^[a-z\+]+://#) {
890 fatal("E: '$path' is not a complete URL ",
891 "and a separate URL is not specified\n");
892 }
893 $path = $url . $path;
894 }
895 return $path;
896}
897
9d55b41a 898sub complete_url_ls_init {
98327e58
EW
899 my ($url, $path, $switch, $pfx) = @_;
900 unless ($path) {
9d55b41a
EW
901 print STDERR "W: $switch not specified\n";
902 return;
903 }
98327e58
EW
904 my $full_url = complete_svn_url($url, $path);
905 my @ls = libsvn_ls_fullurl($full_url);
9d55b41a
EW
906 defined(my $pid = fork) or croak $!;
907 if (!$pid) {
98327e58 908 foreach my $u (map { "$full_url/$_" } (grep m!/$!, @ls)) {
9d55b41a 909 $u =~ s#/+$##;
98327e58 910 if ($u !~ m!\Q$full_url\E/(.+)$!) {
9d55b41a
EW
911 print STDERR "W: Unrecognized URL: $u\n";
912 die "This should never happen\n";
913 }
c35b96e7 914 # don't try to init already existing refs
9d55b41a 915 my $id = $pfx.$1;
9d55b41a
EW
916 $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
917 init_vars();
c35b96e7
EW
918 unless (-d $GIT_SVN_DIR) {
919 print "init $u => $id\n";
920 init($u);
921 }
9d55b41a
EW
922 }
923 exit 0;
924 }
925 waitpid $pid, 0;
926 croak $? if $?;
c35b96e7 927 my ($n) = ($switch =~ /^--(\w+)/);
e0d10e1c 928 command_noisy('config', "svn.$n", $full_url);
9d55b41a
EW
929}
930
931sub common_prefix {
932 my $paths = shift;
933 my %common;
934 foreach (@$paths) {
935 my @tmp = split m#/#, $_;
936 my $p = '';
937 while (my $x = shift @tmp) {
938 $p .= "/$x";
939 $common{$p} ||= 0;
940 $common{$p}++;
941 }
942 }
943 foreach (sort {length $b <=> length $a} keys %common) {
944 if ($common{$_} == @$paths) {
945 return $_;
946 }
947 }
948 return '';
949}
950
c1927a85
EW
951# grafts set here are 'stronger' in that they're based on actual tree
952# matches, and won't be deleted from merge-base checking in write_grafts()
953sub graft_tree_joins {
954 my $grafts = shift;
955 map_tree_joins() if (@_branch_from && !%tree_map);
956 return unless %tree_map;
957
958 git_svn_each(sub {
959 my $i = shift;
aef4e921
EW
960 my @args = (qw/rev-list --pretty=raw/, "refs/remotes/$i");
961 my ($fh, $ctx) = command_output_pipe(@args);
c1927a85
EW
962 while (<$fh>) {
963 next unless /^commit ($sha1)$/o;
964 my $c = $1;
965 my ($t) = (<$fh> =~ /^tree ($sha1)$/o);
966 next unless $tree_map{$t};
967
968 my $l;
969 do {
970 $l = readline $fh;
971 } until ($l =~ /^committer (?:.+) (\d+) ([\-\+]?\d+)$/);
972
973 my ($s, $tz) = ($1, $2);
974 if ($tz =~ s/^\+//) {
975 $s += tz_to_s_offset($tz);
976 } elsif ($tz =~ s/^\-//) {
977 $s -= tz_to_s_offset($tz);
978 }
979
980 my ($url_a, $r_a, $uuid_a) = cmt_metadata($c);
981
982 foreach my $p (@{$tree_map{$t}}) {
983 next if $p eq $c;
aef4e921 984 my $mb = eval { command('merge-base', $c, $p) };
c1927a85
EW
985 next unless ($@ || $?);
986 if (defined $r_a) {
987 # see if SVN says it's a relative
988 my ($url_b, $r_b, $uuid_b) =
989 cmt_metadata($p);
990 next if (defined $url_b &&
991 defined $url_a &&
992 ($url_a eq $url_b) &&
993 ($uuid_a eq $uuid_b));
994 if ($uuid_a eq $uuid_b) {
995 if ($r_b < $r_a) {
996 $grafts->{$c}->{$p} = 2;
997 next;
998 } elsif ($r_b > $r_a) {
999 $grafts->{$p}->{$c} = 2;
1000 next;
1001 }
1002 }
1003 }
1004 my $ct = get_commit_time($p);
1005 if ($ct < $s) {
1006 $grafts->{$c}->{$p} = 2;
1007 } elsif ($ct > $s) {
1008 $grafts->{$p}->{$c} = 2;
1009 }
1010 # what should we do when $ct == $s ?
1011 }
1012 }
aef4e921 1013 command_close_pipe($fh, $ctx);
c1927a85
EW
1014 });
1015}
1016
a5e0cedc
EW
1017sub graft_file_copy_lib {
1018 my ($grafts, $l_map, $u) = @_;
1019 my $tree_paths = $l_map->{$u};
1020 my $pfx = common_prefix([keys %$tree_paths]);
1021 my ($repo, $path) = repo_path_split($u.$pfx);
d81bf827 1022 $SVN = Git::SVN::Ra->new($repo);
a5e0cedc
EW
1023
1024 my ($base, $head) = libsvn_parse_revision();
1025 my $inc = 1000;
1026 my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
42d32870
EW
1027 my $eh = $SVN::Error::handler;
1028 $SVN::Error::handler = \&libsvn_skip_unknown_revs;
a5e0cedc 1029 while (1) {
d81bf827 1030 $SVN->dup->get_log([$path], $min, $max, 0, 2, 1,
a5e0cedc
EW
1031 sub {
1032 libsvn_graft_file_copies($grafts, $tree_paths,
1033 $path, @_);
d81bf827 1034 });
a5e0cedc
EW
1035 last if ($max >= $head);
1036 $min = $max + 1;
1037 $max += $inc;
1038 $max = $head if ($max > $head);
1039 }
42d32870 1040 $SVN::Error::handler = $eh;
a5e0cedc
EW
1041}
1042
9d55b41a
EW
1043sub process_merge_msg_matches {
1044 my ($grafts, $l_map, $u, $p, $c, @matches) = @_;
1045 my (@strong, @weak);
1046 foreach (@matches) {
1047 # merging with ourselves is not interesting
1048 next if $_ eq $p;
1049 if ($l_map->{$u}->{$_}) {
1050 push @strong, $_;
1051 } else {
1052 push @weak, $_;
1053 }
1054 }
1055 foreach my $w (@weak) {
1056 last if @strong;
1057 # no exact match, use branch name as regexp.
1058 my $re = qr/\Q$w\E/i;
1059 foreach (keys %{$l_map->{$u}}) {
1060 if (/$re/) {
c1927a85 1061 push @strong, $l_map->{$u}->{$_};
9d55b41a
EW
1062 last;
1063 }
1064 }
1065 last if @strong;
1066 $w = basename($w);
1067 $re = qr/\Q$w\E/i;
1068 foreach (keys %{$l_map->{$u}}) {
1069 if (/$re/) {
c1927a85 1070 push @strong, $l_map->{$u}->{$_};
9d55b41a
EW
1071 last;
1072 }
1073 }
1074 }
1075 my ($rev) = ($c->{m} =~ /^git-svn-id:\s(?:\S+?)\@(\d+)
1076 \s(?:[a-f\d\-]+)$/xsm);
1077 unless (defined $rev) {
1078 ($rev) = ($c->{m} =~/^git-svn-id:\s(\d+)
1079 \@(?:[a-f\d\-]+)/xsm);
1080 return unless defined $rev;
1081 }
1082 foreach my $m (@strong) {
c1927a85 1083 my ($r0, $s0) = find_rev_before($rev, $m, 1);
9d55b41a
EW
1084 $grafts->{$c->{c}}->{$s0} = 1 if defined $s0;
1085 }
1086}
1087
1088sub graft_merge_msg {
1089 my ($grafts, $l_map, $u, $p, @re) = @_;
1090
1091 my $x = $l_map->{$u}->{$p};
9a5e4075 1092 my $rl = rev_list_raw("refs/remotes/$x");
9d55b41a
EW
1093 while (my $c = next_rev_list_entry($rl)) {
1094 foreach my $re (@re) {
1095 my (@br) = ($c->{m} =~ /$re/g);
1096 next unless @br;
1097 process_merge_msg_matches($grafts,$l_map,$u,$p,$c,@br);
1098 }
1099 }
1100}
1101
aef4e921
EW
1102sub verify_ref {
1103 my ($ref) = @_;
2c5c1d53
EW
1104 eval { command_oneline([ 'rev-parse', '--verify', $ref ],
1105 { STDERR => 0 }); };
aef4e921
EW
1106}
1107
883d0a78
EW
1108sub repo_path_split {
1109 my $full_url = shift;
1110 $full_url =~ s#/+$##;
1111
1112 foreach (@repo_path_split_cache) {
1113 if ($full_url =~ s#$_##) {
1114 my $u = $1;
1115 $full_url =~ s#^/+##;
1116 return ($u, $full_url);
1117 }
1118 }
d81bf827 1119 my $tmp = Git::SVN::Ra->new($full_url);
b9c85187 1120 return ($tmp->{repos_root}, $tmp->{svn_path});
1d52aba8
EW
1121}
1122
3397f9df
EW
1123sub setup_git_svn {
1124 defined $SVN_URL or croak "SVN repository location required\n";
1125 unless (-d $GIT_DIR) {
1126 croak "GIT_DIR=$GIT_DIR does not exist!\n";
1127 }
883d0a78
EW
1128 mkpath([$GIT_SVN_DIR]);
1129 mkpath(["$GIT_SVN_DIR/info"]);
42d32870
EW
1130 open my $fh, '>>',$REVDB or croak $!;
1131 close $fh;
883d0a78 1132 s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url");
3397f9df 1133
3397f9df
EW
1134}
1135
a5e0cedc 1136sub get_tree_from_treeish {
cf52b8f0
EW
1137 my ($treeish) = @_;
1138 croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
aef4e921 1139 my $type = command_oneline(qw/cat-file -t/, $treeish);
cf52b8f0
EW
1140 my $expected;
1141 while ($type eq 'tag') {
aef4e921 1142 ($treeish, $type) = command(qw/cat-file tag/, $treeish);
cf52b8f0
EW
1143 }
1144 if ($type eq 'commit') {
aef4e921
EW
1145 $expected = (grep /^tree /, command(qw/cat-file commit/,
1146 $treeish))[0];
cf52b8f0
EW
1147 ($expected) = ($expected =~ /^tree ($sha1)$/);
1148 die "Unable to get tree from $treeish\n" unless $expected;
1149 } elsif ($type eq 'tree') {
1150 $expected = $treeish;
1151 } else {
1152 die "$treeish is a $type, expected tree, tag or commit\n";
1153 }
a5e0cedc
EW
1154 return $expected;
1155}
1156
aef4e921
EW
1157sub get_diff {
1158 my ($from, $treeish) = @_;
aef4e921
EW
1159 print "diff-tree $from $treeish\n";
1160 my @diff_tree = qw(diff-tree -z -r);
1161 if ($_cp_similarity) {
1162 push @diff_tree, "-C$_cp_similarity";
1163 } else {
1164 push @diff_tree, '-C';
1165 }
1166 push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
1167 push @diff_tree, "-l$_l" if defined $_l;
1168 push @diff_tree, $from, $treeish;
1169 my ($diff_fh, $ctx) = command_output_pipe(@diff_tree);
3397f9df
EW
1170 local $/ = "\0";
1171 my $state = 'meta';
1172 my @mods;
1173 while (<$diff_fh>) {
1174 chomp $_; # this gets rid of the trailing "\0"
3397f9df
EW
1175 if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
1176 $sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
1177 push @mods, { mode_a => $1, mode_b => $2,
1178 sha1_b => $3, chg => $4 };
1179 if ($4 =~ /^(?:C|R)$/) {
1180 $state = 'file_a';
1181 } else {
1182 $state = 'file_b';
1183 }
1184 } elsif ($state eq 'file_a') {
cf52b8f0 1185 my $x = $mods[$#mods] or croak "Empty array\n";
3397f9df 1186 if ($x->{chg} !~ /^(?:C|R)$/) {
cf52b8f0 1187 croak "Error parsing $_, $x->{chg}\n";
3397f9df
EW
1188 }
1189 $x->{file_a} = $_;
1190 $state = 'file_b';
1191 } elsif ($state eq 'file_b') {
cf52b8f0 1192 my $x = $mods[$#mods] or croak "Empty array\n";
3397f9df 1193 if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
cf52b8f0 1194 croak "Error parsing $_, $x->{chg}\n";
3397f9df
EW
1195 }
1196 if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
cf52b8f0 1197 croak "Error parsing $_, $x->{chg}\n";
3397f9df
EW
1198 }
1199 $x->{file_b} = $_;
1200 $state = 'meta';
1201 } else {
cf52b8f0 1202 croak "Error parsing $_\n";
3397f9df
EW
1203 }
1204 }
aef4e921 1205 command_close_pipe($diff_fh, $ctx);
3397f9df
EW
1206 return \@mods;
1207}
1208
a5e0cedc 1209sub libsvn_checkout_tree {
42d32870
EW
1210 my ($from, $treeish, $ed) = @_;
1211 my $mods = get_diff($from, $treeish);
a5e0cedc
EW
1212 return $mods unless (scalar @$mods);
1213 my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
1214 foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
1215 my $f = $m->{chg};
1216 if (defined $o{$f}) {
80f50749 1217 $ed->$f($m, $_q);
a5e0cedc
EW
1218 } else {
1219 croak "Invalid change type: $f\n";
1220 }
1221 }
80f50749 1222 $ed->rmdirs($_q) if $_rmdir;
a5e0cedc
EW
1223 return $mods;
1224}
1225
a5e0cedc
EW
1226sub get_commit_message {
1227 my ($commit, $commit_msg) = (@_);
e17512f3 1228 my %log_msg = ( msg => '' );
ac8e0b91 1229 open my $msg, '>', $commit_msg or croak $!;
3397f9df 1230
aef4e921 1231 my $type = command_oneline(qw/cat-file -t/, $commit);
4ad4515d 1232 if ($type eq 'commit' || $type eq 'tag') {
aef4e921
EW
1233 my ($msg_fh, $ctx) = command_output_pipe('cat-file',
1234 $type, $commit);
3397f9df
EW
1235 my $in_msg = 0;
1236 while (<$msg_fh>) {
1237 if (!$in_msg) {
1238 $in_msg = 1 if (/^\s*$/);
df746c5a
EW
1239 } elsif (/^git-svn-id: /) {
1240 # skip this, we regenerate the correct one
1241 # on re-fetch anyways
3397f9df
EW
1242 } else {
1243 print $msg $_ or croak $!;
1244 }
1245 }
aef4e921 1246 command_close_pipe($msg_fh, $ctx);
3397f9df
EW
1247 }
1248 close $msg or croak $!;
1249
1250 if ($_edit || ($type eq 'tree')) {
1251 my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
1252 system($editor, $commit_msg);
1253 }
ac8e0b91
EW
1254
1255 # file_to_s removes all trailing newlines, so just use chomp() here:
1256 open $msg, '<', $commit_msg or croak $!;
1257 { local $/; chomp($log_msg{msg} = <$msg>); }
1258 close $msg or croak $!;
1259
a5e0cedc
EW
1260 return \%log_msg;
1261}
1262
27e9fb8d
EW
1263sub set_svn_commit_env {
1264 if (defined $LC_ALL) {
1265 $ENV{LC_ALL} = $LC_ALL;
1266 } else {
1267 delete $ENV{LC_ALL};
1268 }
1269}
1270
9d55b41a 1271sub rev_list_raw {
aef4e921
EW
1272 my ($fh, $c) = command_output_pipe(qw/rev-list --pretty=raw/, @_);
1273 return { fh => $fh, ctx => $c, t => { } };
9d55b41a
EW
1274}
1275
1276sub next_rev_list_entry {
1277 my $rl = shift;
1278 my $fh = $rl->{fh};
1279 my $x = $rl->{t};
1280 while (<$fh>) {
1281 if (/^commit ($sha1)$/o) {
1282 if ($x->{c}) {
1283 $rl->{t} = { c => $1 };
1284 return $x;
1285 } else {
1286 $x->{c} = $1;
1287 }
1288 } elsif (/^parent ($sha1)$/o) {
1289 $x->{p}->{$1} = 1;
1290 } elsif (s/^ //) {
1291 $x->{m} ||= '';
1292 $x->{m} .= $_;
1293 }
1294 }
aef4e921 1295 command_close_pipe($fh, $rl->{ctx});
9d55b41a
EW
1296 return ($x != $rl->{t}) ? $x : undef;
1297}
1298
3397f9df
EW
1299sub s_to_file {
1300 my ($str, $file, $mode) = @_;
1301 open my $fd,'>',$file or croak $!;
1302 print $fd $str,"\n" or croak $!;
1303 close $fd or croak $!;
1304 chmod ($mode &~ umask, $file) if (defined $mode);
1305}
1306
1307sub file_to_s {
1308 my $file = shift;
1309 open my $fd,'<',$file or croak "$!: file: $file\n";
1310 local $/;
1311 my $ret = <$fd>;
1312 close $fd or croak $!;
1313 $ret =~ s/\s*$//s;
1314 return $ret;
1315}
1316
1317sub assert_revision_unknown {
42d32870
EW
1318 my $r = shift;
1319 if (my $c = revdb_get($REVDB, $r)) {
1320 croak "$r = $c already exists! Why are we refetching it?";
3397f9df
EW
1321 }
1322}
1323
3397f9df
EW
1324sub git_commit {
1325 my ($log_msg, @parents) = @_;
1326 assert_revision_unknown($log_msg->{revision});
69f0d91e
EW
1327 map_tree_joins() if (@_branch_from && !%tree_map);
1328
a5e0cedc
EW
1329 my (@tmp_parents, @exec_parents, %seen_parent);
1330 if (my $lparents = $log_msg->{parents}) {
1331 @tmp_parents = @$lparents
1332 }
3397f9df
EW
1333 # commit parents can be conditionally bound to a particular
1334 # svn revision via: "svn_revno=commit_sha1", filter them out here:
3397f9df
EW
1335 foreach my $p (@parents) {
1336 next unless defined $p;
1337 if ($p =~ /^(\d+)=($sha1_short)$/o) {
1338 if ($1 == $log_msg->{revision}) {
a5e0cedc 1339 push @tmp_parents, $2;
3397f9df
EW
1340 }
1341 } else {
a5e0cedc 1342 push @tmp_parents, $p if $p =~ /$sha1_short/o;
3397f9df
EW
1343 }
1344 }
a5e0cedc
EW
1345 my $tree = $log_msg->{tree};
1346 if (!defined $tree) {
1347 my $index = set_index($GIT_SVN_INDEX);
aef4e921 1348 $tree = command_oneline('write-tree');
b8c92cad 1349 croak $? if $?;
a5e0cedc
EW
1350 restore_index($index);
1351 }
a00439ac
EW
1352 # just in case we clobber the existing ref, we still want that ref
1353 # as our parent:
aef4e921 1354 if (my $cur = verify_ref("refs/remotes/$GIT_SVN^0")) {
c53d696b 1355 chomp $cur;
a00439ac
EW
1356 push @tmp_parents, $cur;
1357 }
1358
a5e0cedc 1359 if (exists $tree_map{$tree}) {
c1927a85
EW
1360 foreach my $p (@{$tree_map{$tree}}) {
1361 my $skip;
1362 foreach (@tmp_parents) {
1363 # see if a common parent is found
aef4e921 1364 my $mb = eval { command('merge-base', $_, $p) };
c1927a85
EW
1365 next if ($@ || $?);
1366 $skip = 1;
1367 last;
1368 }
1369 next if $skip;
1370 my ($url_p, $r_p, $uuid_p) = cmt_metadata($p);
d81bf827 1371 next if (($SVN->uuid eq $uuid_p) &&
c1927a85
EW
1372 ($log_msg->{revision} > $r_p));
1373 next if (defined $url_p && defined $SVN_URL &&
d81bf827 1374 ($SVN->uuid eq $uuid_p) &&
c1927a85
EW
1375 ($url_p eq $SVN_URL));
1376 push @tmp_parents, $p;
1377 }
a5e0cedc
EW
1378 }
1379 foreach (@tmp_parents) {
1380 next if $seen_parent{$_};
1381 $seen_parent{$_} = 1;
1382 push @exec_parents, $_;
1383 # MAXPARENT is defined to 16 in commit-tree.c:
1384 last if @exec_parents > 16;
1385 }
1386
a00439ac
EW
1387 set_commit_env($log_msg);
1388 my @exec = ('git-commit-tree', $tree);
1389 push @exec, '-p', $_ foreach @exec_parents;
1390 defined(my $pid = open3(my $msg_fh, my $out_fh, '>&STDERR', @exec))
1391 or croak $!;
1392 print $msg_fh $log_msg->{msg} or croak $!;
1393 unless ($_no_metadata) {
d81bf827
EW
1394 print $msg_fh "\ngit-svn-id: $SVN_URL\@$log_msg->{revision} ",
1395 $SVN->uuid,"\n" or croak $!;
3397f9df 1396 }
a00439ac
EW
1397 $msg_fh->flush == 0 or croak $!;
1398 close $msg_fh or croak $!;
3397f9df 1399 chomp(my $commit = do { local $/; <$out_fh> });
a00439ac
EW
1400 close $out_fh or croak $!;
1401 waitpid $pid, 0;
1402 croak $? if $?;
3397f9df 1403 if ($commit !~ /^$sha1$/o) {
a00439ac 1404 die "Failed to commit, invalid sha1: $commit\n";
3397f9df 1405 }
aef4e921 1406 command_noisy('update-ref',"refs/remotes/$GIT_SVN",$commit);
42d32870
EW
1407 revdb_set($REVDB, $log_msg->{revision}, $commit);
1408
a5e0cedc 1409 # this output is read via pipe, do not change:
3397f9df 1410 print "r$log_msg->{revision} = $commit\n";
cf7424b0
EW
1411 return $commit;
1412}
1413
1414sub check_repack {
dc5869c0
EW
1415 if ($_repack && (--$_repack_nr == 0)) {
1416 $_repack_nr = $_repack;
aef4e921
EW
1417 # repack doesn't use any arguments with spaces in them, does it?
1418 command_noisy('repack', split(/\s+/, $_repack_flags));
dc5869c0 1419 }
3397f9df
EW
1420}
1421
a9612be2 1422sub set_commit_env {
1ca72aef 1423 my ($log_msg) = @_;
a9612be2 1424 my $author = $log_msg->{author};
a5e0cedc
EW
1425 if (!defined $author || length $author == 0) {
1426 $author = '(no author)';
1427 }
a9612be2 1428 my ($name,$email) = defined $users{$author} ? @{$users{$author}}
d81bf827 1429 : ($author,$author . '@' . $SVN->uuid);
a9612be2
EW
1430 $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
1431 $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email;
1432 $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date};
1433}
1434
2beb3cdd 1435sub check_upgrade_needed {
cf7424b0 1436 if (!-r $REVDB) {
1a82e793 1437 -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
cf7424b0
EW
1438 open my $fh, '>>',$REVDB or croak $!;
1439 close $fh;
1440 }
aef4e921
EW
1441 return unless eval {
1442 command([qw/rev-parse --verify/,"$GIT_SVN-HEAD^0"],
1443 {STDERR => 0});
2beb3cdd 1444 };
aef4e921 1445 my $head = eval { command('rev-parse',"refs/remotes/$GIT_SVN") };
2beb3cdd
EW
1446 if ($@ || !$head) {
1447 print STDERR "Please run: $0 rebuild --upgrade\n";
1448 exit 1;
1449 }
1450}
1451
69f0d91e
EW
1452# fills %tree_map with a reverse mapping of trees to commits. Useful
1453# for finding parents to commit on.
1454sub map_tree_joins {
098749d9 1455 my %seen;
69f0d91e 1456 foreach my $br (@_branch_from) {
aef4e921
EW
1457 my $pipe = command_output_pipe(qw/rev-list
1458 --topo-order --pretty=raw/, $br);
69f0d91e
EW
1459 while (<$pipe>) {
1460 if (/^commit ($sha1)$/o) {
1461 my $commit = $1;
098749d9
EW
1462
1463 # if we've seen a commit,
1464 # we've seen its parents
1465 last if $seen{$commit};
69f0d91e
EW
1466 my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
1467 unless (defined $tree) {
1468 die "Failed to parse commit $commit\n";
1469 }
1470 push @{$tree_map{$tree}}, $commit;
098749d9 1471 $seen{$commit} = 1;
69f0d91e
EW
1472 }
1473 }
22600a25 1474 close $pipe;
69f0d91e
EW
1475 }
1476}
1477
bf78b1d8
EW
1478sub load_all_refs {
1479 if (@_branch_from) {
1480 print STDERR '--branch|-b parameters are ignored when ',
1481 "--branch-all-refs|-B is passed\n";
1482 }
1483
1484 # don't worry about rev-list on non-commit objects/tags,
1485 # it shouldn't blow up if a ref is a blob or tree...
aef4e921 1486 @_branch_from = command(qw/rev-parse --symbolic --all/);
bf78b1d8
EW
1487}
1488
eeb0abe0
EW
1489# '<svn username> = real-name <email address>' mapping based on git-svnimport:
1490sub load_authors {
1491 open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
1492 while (<$authors>) {
1493 chomp;
8815788e 1494 next unless /^(\S+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
eeb0abe0
EW
1495 my ($user, $name, $email) = ($1, $2, $3);
1496 $users{$user} = [$name, $email];
1497 }
1498 close $authors or croak $!;
1499}
1500
79bb8d88
EW
1501sub rload_authors {
1502 open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
1503 while (<$authors>) {
1504 chomp;
1505 next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
1506 my ($user, $name, $email) = ($1, $2, $3);
1507 $rusers{"$name <$email>"} = $user;
1508 }
1509 close $authors or croak $!;
1510}
1511
9d55b41a
EW
1512sub git_svn_each {
1513 my $sub = shift;
aef4e921 1514 foreach (command(qw/rev-parse --symbolic --all/)) {
9d55b41a
EW
1515 next unless s#^refs/remotes/##;
1516 chomp $_;
1517 next unless -f "$GIT_DIR/svn/$_/info/url";
1518 &$sub($_);
1519 }
1520}
1521
42d32870
EW
1522sub migrate_revdb {
1523 git_svn_each(sub {
1524 my $id = shift;
1525 defined(my $pid = fork) or croak $!;
1526 if (!$pid) {
1527 $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
1528 init_vars();
1529 exit 0 if -r $REVDB;
1530 print "Upgrading svn => git mapping...\n";
1a82e793 1531 -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
42d32870
EW
1532 open my $fh, '>>',$REVDB or croak $!;
1533 close $fh;
1534 rebuild();
1535 print "Done upgrading. You may now delete the ",
1536 "deprecated $GIT_SVN_DIR/revs directory\n";
1537 exit 0;
1538 }
1539 waitpid $pid, 0;
1540 croak $? if $?;
1541 });
1542}
1543
883d0a78 1544sub migration_check {
42d32870 1545 migrate_revdb() unless (-e $REVDB);
883d0a78
EW
1546 return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR);
1547 print "Upgrading repository...\n";
1548 unless (-d "$GIT_DIR/svn") {
1549 mkdir "$GIT_DIR/svn" or croak $!;
1550 }
1551 print "Data from a previous version of git-svn exists, but\n\t",
1552 "$GIT_SVN_DIR\n\t(required for this version ",
1553 "($VERSION) of git-svn) does not.\n";
1554
aef4e921 1555 foreach my $x (command(qw/rev-parse --symbolic --all/)) {
883d0a78
EW
1556 next unless $x =~ s#^refs/remotes/##;
1557 chomp $x;
1558 next unless -f "$GIT_DIR/$x/info/url";
1559 my $u = eval { file_to_s("$GIT_DIR/$x/info/url") };
1560 next unless $u;
1561 my $dn = dirname("$GIT_DIR/svn/$x");
1562 mkpath([$dn]) unless -d $dn;
1563 rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x";
883d0a78 1564 }
42d32870 1565 migrate_revdb() if (-d $GIT_SVN_DIR && !-w $REVDB);
883d0a78
EW
1566 print "Done upgrading.\n";
1567}
1568
9d55b41a 1569sub find_rev_before {
42d32870
EW
1570 my ($r, $id, $eq_ok) = @_;
1571 my $f = "$GIT_DIR/svn/$id/.rev_db";
cf7424b0
EW
1572 return (undef,undef) unless -r $f;
1573 --$r unless $eq_ok;
42d32870
EW
1574 while ($r > 0) {
1575 if (my $c = revdb_get($f, $r)) {
1576 return ($r, $c);
1577 }
1578 --$r;
9d55b41a
EW
1579 }
1580 return (undef, undef);
1581}
1582
b8c92cad
EW
1583sub init_vars {
1584 $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
1585 $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN";
42d32870 1586 $REVDB = "$GIT_SVN_DIR/.rev_db";
b8c92cad
EW
1587 $GIT_SVN_INDEX = "$GIT_SVN_DIR/index";
1588 $SVN_URL = undef;
c1927a85 1589 %tree_map = ();
b8c92cad
EW
1590}
1591
e0d10e1c 1592# convert GetOpt::Long specs for use by git-config
b8c92cad
EW
1593sub read_repo_config {
1594 return unless -d $GIT_DIR;
1595 my $opts = shift;
1596 foreach my $o (keys %$opts) {
1597 my $v = $opts->{$o};
1598 my ($key) = ($o =~ /^([a-z\-]+)/);
1599 $key =~ s/-//g;
e0d10e1c 1600 my $arg = 'git-config';
b8c92cad
EW
1601 $arg .= ' --int' if ($o =~ /[:=]i$/);
1602 $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
1603 if (ref $v eq 'ARRAY') {
1604 chomp(my @tmp = `$arg --get-all svn.$key`);
1605 @$v = @tmp if @tmp;
1606 } else {
1607 chomp(my $tmp = `$arg --get svn.$key`);
7774284a 1608 if ($tmp && !($arg =~ / --bool/ && $tmp eq 'false')) {
b8c92cad
EW
1609 $$v = $tmp;
1610 }
1611 }
1612 }
1613}
1614
dc5869c0
EW
1615sub set_default_vals {
1616 if (defined $_repack) {
1617 $_repack = 1000 if ($_repack <= 0);
1618 $_repack_nr = $_repack;
cf7424b0 1619 $_repack_flags ||= '-d';
dc5869c0
EW
1620 }
1621}
1622
9d55b41a
EW
1623sub read_grafts {
1624 my $gr_file = shift;
1625 my ($grafts, $comments) = ({}, {});
1626 if (open my $fh, '<', $gr_file) {
1627 my @tmp;
1628 while (<$fh>) {
1629 if (/^($sha1)\s+/) {
1630 my $c = $1;
1631 if (@tmp) {
1632 @{$comments->{$c}} = @tmp;
1633 @tmp = ();
1634 }
1635 foreach my $p (split /\s+/, $_) {
1636 $grafts->{$c}->{$p} = 1;
1637 }
1638 } else {
1639 push @tmp, $_;
1640 }
1641 }
1642 close $fh or croak $!;
1643 @{$comments->{'END'}} = @tmp if @tmp;
1644 }
1645 return ($grafts, $comments);
1646}
1647
1648sub write_grafts {
1649 my ($grafts, $comments, $gr_file) = @_;
1650
1651 open my $fh, '>', $gr_file or croak $!;
1652 foreach my $c (sort keys %$grafts) {
1653 if ($comments->{$c}) {
1654 print $fh $_ foreach @{$comments->{$c}};
1655 }
1656 my $p = $grafts->{$c};
c1927a85 1657 my %x; # real parents
9d55b41a 1658 delete $p->{$c}; # commits are not self-reproducing...
aef4e921 1659 my $ch = command_output_pipe(qw/cat-file commit/, $c);
9d55b41a 1660 while (<$ch>) {
c1927a85
EW
1661 if (/^parent ($sha1)/) {
1662 $x{$1} = $p->{$1} = 1;
9d55b41a 1663 } else {
c1927a85 1664 last unless /^\S/;
9d55b41a
EW
1665 }
1666 }
22600a25 1667 close $ch; # breaking the pipe
c1927a85
EW
1668
1669 # if real parents are the only ones in the grafts, drop it
1670 next if join(' ',sort keys %$p) eq join(' ',sort keys %x);
1671
1672 my (@ip, @jp, $mb);
1673 my %del = %x;
1674 @ip = @jp = keys %$p;
1675 foreach my $i (@ip) {
1676 next if $del{$i} || $p->{$i} == 2;
1677 foreach my $j (@jp) {
1678 next if $i eq $j || $del{$j} || $p->{$j} == 2;
aef4e921 1679 $mb = eval { command('merge-base', $i, $j) };
c1927a85
EW
1680 next unless $mb;
1681 chomp $mb;
1682 next if $x{$mb};
1683 if ($mb eq $j) {
1684 delete $p->{$i};
1685 $del{$i} = 1;
1686 } elsif ($mb eq $i) {
1687 delete $p->{$j};
1688 $del{$j} = 1;
1689 }
1690 }
1691 }
1692
1693 # if real parents are the only ones in the grafts, drop it
1694 next if join(' ',sort keys %$p) eq join(' ',sort keys %x);
1695
9d55b41a
EW
1696 print $fh $c, ' ', join(' ', sort keys %$p),"\n";
1697 }
1698 if ($comments->{'END'}) {
1699 print $fh $_ foreach @{$comments->{'END'}};
1700 }
1701 close $fh or croak $!;
1702}
1703
a00439ac
EW
1704sub read_url_paths_all {
1705 my ($l_map, $pfx, $p) = @_;
1706 my @dir;
1707 foreach (<$p/*>) {
1708 if (-r "$_/info/url") {
1709 $pfx .= '/' if $pfx && $pfx !~ m!/$!;
1710 my $id = $pfx . basename $_;
1711 my $url = file_to_s("$_/info/url");
1712 my ($u, $p) = repo_path_split($url);
1713 $l_map->{$u}->{$p} = $id;
1714 } elsif (-d $_) {
1715 push @dir, $_;
1716 }
1717 }
1718 foreach (@dir) {
1719 my $x = $_;
1720 $x =~ s!^\Q$GIT_DIR\E/svn/!!o;
1721 read_url_paths_all($l_map, $x, $_);
1722 }
1723}
1724
1725# this one only gets ids that have been imported, not new ones
9d55b41a
EW
1726sub read_url_paths {
1727 my $l_map = {};
1728 git_svn_each(sub { my $x = shift;
6c5cda89
EW
1729 my $url = file_to_s("$GIT_DIR/svn/$x/info/url");
1730 my ($u, $p) = repo_path_split($url);
9d55b41a
EW
1731 $l_map->{$u}->{$p} = $x;
1732 });
1733 return $l_map;
1734}
1735
79bb8d88 1736sub extract_metadata {
c1927a85 1737 my $id = shift or return (undef, undef, undef);
79bb8d88
EW
1738 my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
1739 \s([a-f\d\-]+)$/x);
e70dc780 1740 if (!defined $rev || !$uuid || !$url) {
79bb8d88 1741 # some of the original repositories I made had
82e5a82f 1742 # identifiers like this:
79bb8d88
EW
1743 ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
1744 }
1745 return ($url, $rev, $uuid);
1746}
1747
c1927a85
EW
1748sub cmt_metadata {
1749 return extract_metadata((grep(/^git-svn-id: /,
aef4e921 1750 command(qw/cat-file commit/, shift)))[-1]);
c1927a85
EW
1751}
1752
1753sub get_commit_time {
1754 my $cmt = shift;
aef4e921 1755 my $fh = command_output_pipe(qw/rev-list --pretty=raw -n1/, $cmt);
c1927a85
EW
1756 while (<$fh>) {
1757 /^committer\s(?:.+) (\d+) ([\-\+]?\d+)$/ or next;
1758 my ($s, $tz) = ($1, $2);
1759 if ($tz =~ s/^\+//) {
1760 $s += tz_to_s_offset($tz);
1761 } elsif ($tz =~ s/^\-//) {
1762 $s -= tz_to_s_offset($tz);
1763 }
22600a25 1764 close $fh;
c1927a85
EW
1765 return $s;
1766 }
1767 die "Can't get commit time for commit: $cmt\n";
1768}
1769
79bb8d88
EW
1770sub tz_to_s_offset {
1771 my ($tz) = @_;
1772 $tz =~ s/(\d\d)$//;
1773 return ($1 * 60) + ($tz * 3600);
1774}
1775
9aca0258
EW
1776# adapted from pager.c
1777sub config_pager {
1778 $_pager ||= $ENV{GIT_PAGER} || $ENV{PAGER};
1779 if (!defined $_pager) {
1780 $_pager = 'less';
1781 } elsif (length $_pager == 0 || $_pager eq 'cat') {
1782 $_pager = undef;
79bb8d88 1783 }
9aca0258
EW
1784}
1785
1786sub run_pager {
1787 return unless -t *STDOUT;
79bb8d88
EW
1788 pipe my $rfd, my $wfd or return;
1789 defined(my $pid = fork) or croak $!;
1790 if (!$pid) {
1791 open STDOUT, '>&', $wfd or croak $!;
1792 return;
1793 }
1794 open STDIN, '<&', $rfd or croak $!;
9aca0258
EW
1795 $ENV{LESS} ||= 'FRSX';
1796 exec $_pager or croak "Can't run pager: $! ($_pager)\n";
79bb8d88
EW
1797}
1798
1799sub get_author_info {
1800 my ($dest, $author, $t, $tz) = @_;
1801 $author =~ s/(?:^\s*|\s*$)//g;
c0d48222 1802 $dest->{a_raw} = $author;
79bb8d88
EW
1803 my $_a;
1804 if ($_authors) {
1805 $_a = $rusers{$author} || undef;
1806 }
1807 if (!$_a) {
1808 ($_a) = ($author =~ /<([^>]+)\@[^>]+>$/);
1809 }
1810 $dest->{t} = $t;
1811 $dest->{tz} = $tz;
1812 $dest->{a} = $_a;
1813 # Date::Parse isn't in the standard Perl distro :(
1814 if ($tz =~ s/^\+//) {
1815 $t += tz_to_s_offset($tz);
1816 } elsif ($tz =~ s/^\-//) {
1817 $t -= tz_to_s_offset($tz);
1818 }
1819 $dest->{t_utc} = $t;
1820}
1821
1822sub process_commit {
1823 my ($c, $r_min, $r_max, $defer) = @_;
1824 if (defined $r_min && defined $r_max) {
1825 if ($r_min == $c->{r} && $r_min == $r_max) {
1826 show_commit($c);
1827 return 0;
1828 }
1829 return 1 if $r_min == $r_max;
1830 if ($r_min < $r_max) {
1831 # we need to reverse the print order
1832 return 0 if (defined $_limit && --$_limit < 0);
1833 push @$defer, $c;
1834 return 1;
1835 }
1836 if ($r_min != $r_max) {
1837 return 1 if ($r_min < $c->{r});
1838 return 1 if ($r_max > $c->{r});
1839 }
1840 }
1841 return 0 if (defined $_limit && --$_limit < 0);
1842 show_commit($c);
1843 return 1;
1844}
1845
1846sub show_commit {
1847 my $c = shift;
1848 if ($_oneline) {
1849 my $x = "\n";
1850 if (my $l = $c->{l}) {
1851 while ($l->[0] =~ /^\s*$/) { shift @$l }
1852 $x = $l->[0];
1853 }
1854 $_l_fmt ||= 'A' . length($c->{r});
1855 print 'r',pack($_l_fmt, $c->{r}),' | ';
1856 print "$c->{c} | " if $_show_commit;
1857 print $x;
1858 } else {
1859 show_commit_normal($c);
1860 }
1861}
1862
74a31a10
EW
1863sub show_commit_changed_paths {
1864 my ($c) = @_;
1865 return unless $c->{changed};
1866 print "Changed paths:\n", @{$c->{changed}};
1867}
1868
79bb8d88
EW
1869sub show_commit_normal {
1870 my ($c) = @_;
1871 print '-' x72, "\nr$c->{r} | ";
1872 print "$c->{c} | " if $_show_commit;
1873 print "$c->{a} | ", strftime("%Y-%m-%d %H:%M:%S %z (%a, %d %b %Y)",
1874 localtime($c->{t_utc})), ' | ';
1875 my $nr_line = 0;
1876
1877 if (my $l = $c->{l}) {
74a31a10
EW
1878 while ($l->[$#$l] eq "\n" && $#$l > 0
1879 && $l->[($#$l - 1)] eq "\n") {
79bb8d88
EW
1880 pop @$l;
1881 }
1882 $nr_line = scalar @$l;
1883 if (!$nr_line) {
1884 print "1 line\n\n\n";
1885 } else {
1886 if ($nr_line == 1) {
1887 $nr_line = '1 line';
1888 } else {
1889 $nr_line .= ' lines';
1890 }
74a31a10
EW
1891 print $nr_line, "\n";
1892 show_commit_changed_paths($c);
1893 print "\n";
79bb8d88
EW
1894 print $_ foreach @$l;
1895 }
1896 } else {
74a31a10
EW
1897 print "1 line\n";
1898 show_commit_changed_paths($c);
1899 print "\n";
79bb8d88
EW
1900
1901 }
1902 foreach my $x (qw/raw diff/) {
1903 if ($c->{$x}) {
1904 print "\n";
1905 print $_ foreach @{$c->{$x}}
1906 }
1907 }
1908}
1909
d976acfd
EW
1910package Git::SVN::Prompt;
1911use strict;
1912use warnings;
1913require SVN::Core;
1914use vars qw/$_no_auth_cache $_username/;
1915
1916sub simple {
30d055aa
EW
1917 my ($cred, $realm, $default_username, $may_save, $pool) = @_;
1918 $may_save = undef if $_no_auth_cache;
1919 $default_username = $_username if defined $_username;
1920 if (defined $default_username && length $default_username) {
1921 if (defined $realm && length $realm) {
6f729591
EW
1922 print STDERR "Authentication realm: $realm\n";
1923 STDERR->flush;
30d055aa
EW
1924 }
1925 $cred->username($default_username);
1926 } else {
d976acfd 1927 username($cred, $realm, $may_save, $pool);
30d055aa
EW
1928 }
1929 $cred->password(_read_password("Password for '" .
1930 $cred->username . "': ", $realm));
1931 $cred->may_save($may_save);
1932 $SVN::_Core::SVN_NO_ERROR;
1933}
1934
d976acfd 1935sub ssl_server_trust {
30d055aa
EW
1936 my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_;
1937 $may_save = undef if $_no_auth_cache;
6f729591 1938 print STDERR "Error validating server certificate for '$realm':\n";
30d055aa 1939 if ($failures & $SVN::Auth::SSL::UNKNOWNCA) {
6f729591 1940 print STDERR " - The certificate is not issued by a trusted ",
30d055aa
EW
1941 "authority. Use the\n",
1942 " fingerprint to validate the certificate manually!\n";
1943 }
1944 if ($failures & $SVN::Auth::SSL::CNMISMATCH) {
6f729591 1945 print STDERR " - The certificate hostname does not match.\n";
30d055aa
EW
1946 }
1947 if ($failures & $SVN::Auth::SSL::NOTYETVALID) {
6f729591 1948 print STDERR " - The certificate is not yet valid.\n";
30d055aa
EW
1949 }
1950 if ($failures & $SVN::Auth::SSL::EXPIRED) {
6f729591 1951 print STDERR " - The certificate has expired.\n";
30d055aa
EW
1952 }
1953 if ($failures & $SVN::Auth::SSL::OTHER) {
6f729591 1954 print STDERR " - The certificate has an unknown error.\n";
30d055aa 1955 }
6f729591
EW
1956 printf STDERR
1957 "Certificate information:\n".
30d055aa
EW
1958 " - Hostname: %s\n".
1959 " - Valid: from %s until %s\n".
1960 " - Issuer: %s\n".
1961 " - Fingerprint: %s\n",
1962 map $cert_info->$_, qw(hostname valid_from valid_until
6f729591 1963 issuer_dname fingerprint);
30d055aa
EW
1964 my $choice;
1965prompt:
6f729591 1966 print STDERR $may_save ?
30d055aa
EW
1967 "(R)eject, accept (t)emporarily or accept (p)ermanently? " :
1968 "(R)eject or accept (t)emporarily? ";
6f729591 1969 STDERR->flush;
30d055aa
EW
1970 $choice = lc(substr(<STDIN> || 'R', 0, 1));
1971 if ($choice =~ /^t$/i) {
1972 $cred->may_save(undef);
1973 } elsif ($choice =~ /^r$/i) {
1974 return -1;
1975 } elsif ($may_save && $choice =~ /^p$/i) {
1976 $cred->may_save($may_save);
1977 } else {
1978 goto prompt;
1979 }
1980 $cred->accepted_failures($failures);
1981 $SVN::_Core::SVN_NO_ERROR;
1982}
1983
d976acfd 1984sub ssl_client_cert {
30d055aa
EW
1985 my ($cred, $realm, $may_save, $pool) = @_;
1986 $may_save = undef if $_no_auth_cache;
6f729591
EW
1987 print STDERR "Client certificate filename: ";
1988 STDERR->flush;
30d055aa
EW
1989 chomp(my $filename = <STDIN>);
1990 $cred->cert_file($filename);
1991 $cred->may_save($may_save);
1992 $SVN::_Core::SVN_NO_ERROR;
1993}
1994
d976acfd 1995sub ssl_client_cert_pw {
30d055aa
EW
1996 my ($cred, $realm, $may_save, $pool) = @_;
1997 $may_save = undef if $_no_auth_cache;
1998 $cred->password(_read_password("Password: ", $realm));
1999 $cred->may_save($may_save);
2000 $SVN::_Core::SVN_NO_ERROR;
2001}
2002
d976acfd 2003sub username {
30d055aa
EW
2004 my ($cred, $realm, $may_save, $pool) = @_;
2005 $may_save = undef if $_no_auth_cache;
2006 if (defined $realm && length $realm) {
6f729591 2007 print STDERR "Authentication realm: $realm\n";
30d055aa
EW
2008 }
2009 my $username;
2010 if (defined $_username) {
2011 $username = $_username;
2012 } else {
6f729591
EW
2013 print STDERR "Username: ";
2014 STDERR->flush;
30d055aa
EW
2015 chomp($username = <STDIN>);
2016 }
2017 $cred->username($username);
2018 $cred->may_save($may_save);
2019 $SVN::_Core::SVN_NO_ERROR;
2020}
2021
2022sub _read_password {
2023 my ($prompt, $realm) = @_;
6f729591
EW
2024 print STDERR $prompt;
2025 STDERR->flush;
30d055aa
EW
2026 require Term::ReadKey;
2027 Term::ReadKey::ReadMode('noecho');
2028 my $password = '';
2029 while (defined(my $key = Term::ReadKey::ReadKey(0))) {
2030 last if $key =~ /[\012\015]/; # \n\r
2031 $password .= $key;
2032 }
2033 Term::ReadKey::ReadMode('restore');
6f729591
EW
2034 print STDERR "\n";
2035 STDERR->flush;
30d055aa
EW
2036 $password;
2037}
2038
d976acfd
EW
2039package main;
2040
d2a9a87b
EW
2041sub uri_encode {
2042 my ($f) = @_;
2043 $f =~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;
2044 $f
2045}
2046
2047sub uri_decode {
2048 my ($f) = @_;
2049 $f =~ tr/+/ /;
2050 $f =~ s/%([A-F0-9]{2})/chr hex($1)/ge;
2051 $f
2052}
2053
a5e0cedc 2054sub libsvn_log_entry {
d2a9a87b 2055 my ($rev, $author, $date, $msg, $parents, $untracked) = @_;
a5e0cedc
EW
2056 my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
2057 (\d\d)\:(\d\d)\:(\d\d).\d+Z$/x)
2058 or die "Unable to parse date: $date\n";
35985004
EW
2059 if (defined $author && length $author > 0 &&
2060 defined $_authors && ! defined $users{$author}) {
a5e0cedc
EW
2061 die "Author: $author not defined in $_authors file\n";
2062 }
308906fa 2063 $msg = '' if ($rev == 0 && !defined $msg);
d2a9a87b
EW
2064
2065 open my $un, '>>', "$GIT_SVN_DIR/unhandled.log" or croak $!;
2066 my $h;
2067 print $un "r$rev\n" or croak $!;
2068 $h = $untracked->{empty};
2069 foreach (sort keys %$h) {
2070 my $act = $h->{$_} ? '+empty_dir' : '-empty_dir';
2071 print $un " $act: ", uri_encode($_), "\n" or croak $!;
2072 warn "W: $act: $_\n";
2073 }
2074 foreach my $t (qw/dir_prop file_prop/) {
2075 $h = $untracked->{$t} or next;
2076 foreach my $path (sort keys %$h) {
2077 my $ppath = $path eq '' ? '.' : $path;
2078 foreach my $prop (sort keys %{$h->{$path}}) {
2079 next if $SKIP{$prop};
2080 my $v = $h->{$path}->{$prop};
2081 if (defined $v) {
2082 print $un " +$t: ",
2083 uri_encode($ppath), ' ',
2084 uri_encode($prop), ' ',
2085 uri_encode($v), "\n"
2086 or croak $!;
2087 } else {
2088 print $un " -$t: ",
2089 uri_encode($ppath), ' ',
2090 uri_encode($prop), "\n"
2091 or croak $!;
2092 }
2093 }
2094 }
2095 }
2096 foreach my $t (qw/absent_file absent_directory/) {
2097 $h = $untracked->{$t} or next;
2098 foreach my $parent (sort keys %$h) {
2099 foreach my $path (sort @{$h->{$parent}}) {
2100 print $un " $t: ",
2101 uri_encode("$parent/$path"), "\n"
2102 or croak $!;
2103 warn "W: $t: $parent/$path ",
2104 "Insufficient permissions?\n";
2105 }
2106 }
2107 }
2108
2109 # revprops (make this optional? it's an extra network trip...)
d81bf827 2110 my $rp = $SVN->rev_proplist($rev);
d2a9a87b
EW
2111 foreach (sort keys %$rp) {
2112 next if /^svn:(?:author|date|log)$/;
2113 print $un " rev_prop: ", uri_encode($_), ' ',
2114 uri_encode($rp->{$_}), "\n";
2115 }
d2a9a87b
EW
2116 close $un or croak $!;
2117
2118 { revision => $rev, date => "+0000 $Y-$m-$d $H:$M:$S",
2119 author => $author, msg => $msg."\n", parents => $parents || [],
2120 revprops => $rp }
a5e0cedc
EW
2121}
2122
a5e0cedc 2123sub libsvn_fetch {
27a1a801 2124 my ($last_commit, $paths, $rev, $author, $date, $msg) = @_;
0864e3ba 2125 my $ed = SVN::Git::Fetcher->new({ c => $last_commit, q => $_q });
27a1a801 2126 my (undef, $last_rev, undef) = cmt_metadata($last_commit);
d81bf827 2127 unless ($SVN->gs_do_update($last_rev, $rev, '', 1, $ed)) {
dad73c0b
EW
2128 die "SVN connection failed somewhere...\n";
2129 }
d2a9a87b 2130 libsvn_log_entry($rev, $author, $date, $msg, [$last_commit], $ed);
27a1a801
EW
2131}
2132
a5e0cedc 2133sub svn_grab_base_rev {
aef4e921
EW
2134 my $c = eval { command_oneline([qw/rev-parse --verify/,
2135 "refs/remotes/$GIT_SVN^0"],
2136 { STDERR => 0 }) };
a5e0cedc 2137 if (defined $c && length $c) {
c1927a85 2138 my ($url, $rev, $uuid) = cmt_metadata($c);
a00439ac
EW
2139 return ($rev, $c) if defined $rev;
2140 }
2141 if ($_no_metadata) {
2142 my $offset = -41; # from tail
2143 my $rl;
2144 open my $fh, '<', $REVDB or
2145 die "--no-metadata specified and $REVDB not readable\n";
2146 seek $fh, $offset, 2;
2147 $rl = readline $fh;
2148 defined $rl or return (undef, undef);
2149 chomp $rl;
2150 while ($c ne $rl && tell $fh != 0) {
2151 $offset -= 41;
2152 seek $fh, $offset, 2;
2153 $rl = readline $fh;
2154 defined $rl or return (undef, undef);
2155 chomp $rl;
2156 }
2157 my $rev = tell $fh;
2158 croak $! if ($rev < -1);
2159 $rev = ($rev - 41) / 41;
2160 close $fh or croak $!;
a5e0cedc
EW
2161 return ($rev, $c);
2162 }
2163 return (undef, undef);
2164}
2165
2166sub libsvn_parse_revision {
2167 my $base = shift;
2168 my $head = $SVN->get_latest_revnum();
2169 if (!defined $_revision || $_revision eq 'BASE:HEAD') {
2170 return ($base + 1, $head) if (defined $base);
2171 return (0, $head);
2172 }
2173 return ($1, $2) if ($_revision =~ /^(\d+):(\d+)$/);
2174 return ($_revision, $_revision) if ($_revision =~ /^\d+$/);
2175 if ($_revision =~ /^BASE:(\d+)$/) {
2176 return ($base + 1, $1) if (defined $base);
2177 return (0, $head);
2178 }
2179 return ($1, $head) if ($_revision =~ /^(\d+):HEAD$/);
2180 die "revision argument: $_revision not understood by git-svn\n",
2181 "Try using the command-line svn client instead\n";
2182}
2183
a5e0cedc
EW
2184sub libsvn_traverse_ignore {
2185 my ($fh, $path, $r) = @_;
2186 $path =~ s#^/+##g;
d81bf827 2187 my ($dirent, undef, $props) = $SVN->get_dir($path, $r);
a5e0cedc 2188 my $p = $path;
747fa12c 2189 $p =~ s#^\Q$SVN->{svn_path}\E/##;
a5e0cedc
EW
2190 print $fh length $p ? "\n# $p\n" : "\n# /\n";
2191 if (my $s = $props->{'svn:ignore'}) {
2192 $s =~ s/[\r\n]+/\n/g;
2193 chomp $s;
2194 if (length $p == 0) {
2195 $s =~ s#\n#\n/$p#g;
2196 print $fh "/$s\n";
2197 } else {
2198 $s =~ s#\n#\n/$p/#g;
2199 print $fh "/$p/$s\n";
2200 }
2201 }
2202 foreach (sort keys %$dirent) {
2203 next if $dirent->{$_}->kind != $SVN::Node::dir;
2204 libsvn_traverse_ignore($fh, "$path/$_", $r);
2205 }
a5e0cedc
EW
2206}
2207
42d32870
EW
2208sub revisions_eq {
2209 my ($path, $r0, $r1) = @_;
2210 return 1 if $r0 == $r1;
2211 my $nr = 0;
b9c85187 2212 # should be OK to use Pool here (r1 - r0) should be small
d81bf827 2213 $SVN->get_log([$path], $r0, $r1, 0, 0, 1, sub {$nr++});
42d32870
EW
2214 return 0 if ($nr > 1);
2215 return 1;
2216}
2217
2218sub libsvn_find_parent_branch {
a5e0cedc 2219 my ($paths, $rev, $author, $date, $msg) = @_;
747fa12c 2220 my $svn_path = '/'.$SVN->{svn_path};
a5e0cedc
EW
2221
2222 # look for a parent from another branch:
cf7424b0
EW
2223 my $i = $paths->{$svn_path} or return;
2224 my $branch_from = $i->copyfrom_path or return;
2225 my $r = $i->copyfrom_rev;
2226 print STDERR "Found possible branch point: ",
2227 "$branch_from => $svn_path, $r\n";
2228 $branch_from =~ s#^/##;
a00439ac
EW
2229 my $l_map = {};
2230 read_url_paths_all($l_map, '', "$GIT_DIR/svn");
747fa12c 2231 my $url = $SVN->{repos_root};
cf7424b0 2232 defined $l_map->{$url} or return;
a00439ac
EW
2233 my $id = $l_map->{$url}->{$branch_from};
2234 if (!defined $id && $_follow_parent) {
2235 print STDERR "Following parent: $branch_from\@$r\n";
2236 # auto create a new branch and follow it
2237 $id = basename($branch_from);
2238 $id .= '@'.$r if -r "$GIT_DIR/svn/$id";
2239 while (-r "$GIT_DIR/svn/$id") {
2240 # just grow a tail if we're not unique enough :x
2241 $id .= '-';
2242 }
2243 }
2244 return unless defined $id;
2245
cf7424b0 2246 my ($r0, $parent) = find_rev_before($r,$id,1);
a00439ac
EW
2247 if ($_follow_parent && (!defined $r0 || !defined $parent)) {
2248 defined(my $pid = fork) or croak $!;
2249 if (!$pid) {
2250 $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
2251 init_vars();
2252 $SVN_URL = "$url/$branch_from";
747fa12c 2253 $SVN = undef;
a00439ac
EW
2254 setup_git_svn();
2255 # we can't assume SVN_URL exists at r+1:
2256 $_revision = "0:$r";
2257 fetch_lib();
2258 exit 0;
2259 }
2260 waitpid $pid, 0;
2261 croak $? if $?;
2262 ($r0, $parent) = find_rev_before($r,$id,1);
2263 }
cf7424b0
EW
2264 return unless (defined $r0 && defined $parent);
2265 if (revisions_eq($branch_from, $r0, $r)) {
2266 unlink $GIT_SVN_INDEX;
a00439ac 2267 print STDERR "Found branch parent: ($GIT_SVN) $parent\n";
aef4e921 2268 command_noisy('read-tree', $parent);
d81bf827 2269 unless ($SVN->can_do_switch) {
ed92f170
EW
2270 return _libsvn_new_tree($paths, $rev, $author, $date,
2271 $msg, [$parent]);
a552db3a
EW
2272 }
2273 # do_switch works with svn/trunk >= r22312, but that is not
2274 # included with SVN 1.4.2 (the latest version at the moment),
2275 # so we can't rely on it.
d81bf827 2276 my $ra = Git::SVN::Ra->new("$url/$branch_from");
aef4e921 2277 my $ed = SVN::Git::Fetcher->new({c => $parent, q => $_q });
d81bf827
EW
2278 $ra->gs_do_switch($r0, $rev, '', 1, $SVN->{url}, $ed) or
2279 die "SVN connection failed somewhere...\n";
a552db3a 2280 return libsvn_log_entry($rev, $author, $date, $msg, [$parent]);
cf7424b0
EW
2281 }
2282 print STDERR "Nope, branch point not imported or unknown\n";
42d32870
EW
2283 return undef;
2284}
2285
2286sub libsvn_new_tree {
2287 if (my $log_entry = libsvn_find_parent_branch(@_)) {
2288 return $log_entry;
2289 }
ed92f170
EW
2290 my ($paths, $rev, $author, $date, $msg) = @_; # $pool is last
2291 _libsvn_new_tree($paths, $rev, $author, $date, $msg, []);
2292}
2293
2294sub _libsvn_new_tree {
2295 my ($paths, $rev, $author, $date, $msg, $parents) = @_;
ed92f170 2296 my $ed = SVN::Git::Fetcher->new({q => $_q});
d81bf827 2297 unless ($SVN->gs_do_update($rev, $rev, '', 1, $ed)) {
ed92f170 2298 die "SVN connection failed somewhere...\n";
27a1a801 2299 }
ed92f170 2300 libsvn_log_entry($rev, $author, $date, $msg, $parents, $ed);
a5e0cedc
EW
2301}
2302
2303sub find_graft_path_commit {
2304 my ($tree_paths, $p1, $r1) = @_;
2305 foreach my $x (keys %$tree_paths) {
2306 next unless ($p1 =~ /^\Q$x\E/);
2307 my $i = $tree_paths->{$x};
42d32870
EW
2308 my ($r0, $parent) = find_rev_before($r1,$i,1);
2309 return $parent if (defined $r0 && $r0 == $r1);
a5e0cedc
EW
2310 print STDERR "r$r1 of $i not imported\n";
2311 next;
2312 }
2313 return undef;
2314}
2315
2316sub find_graft_path_parents {
2317 my ($grafts, $tree_paths, $c, $p0, $r0) = @_;
2318 foreach my $x (keys %$tree_paths) {
2319 next unless ($p0 =~ /^\Q$x\E/);
2320 my $i = $tree_paths->{$x};
42d32870
EW
2321 my ($r, $parent) = find_rev_before($r0, $i, 1);
2322 if (defined $r && defined $parent && revisions_eq($x,$r,$r0)) {
c1927a85
EW
2323 my ($url_b, undef, $uuid_b) = cmt_metadata($c);
2324 my ($url_a, undef, $uuid_a) = cmt_metadata($parent);
2325 next if ($url_a && $url_b && $url_a eq $url_b &&
2326 $uuid_b eq $uuid_a);
42d32870 2327 $grafts->{$c}->{$parent} = 1;
a5e0cedc 2328 }
a5e0cedc
EW
2329 }
2330}
2331
2332sub libsvn_graft_file_copies {
2333 my ($grafts, $tree_paths, $path, $paths, $rev) = @_;
2334 foreach (keys %$paths) {
2335 my $i = $paths->{$_};
2336 my ($m, $p0, $r0) = ($i->action, $i->copyfrom_path,
2337 $i->copyfrom_rev);
2338 next unless (defined $p0 && defined $r0);
2339
2340 my $p1 = $_;
2341 $p1 =~ s#^/##;
2342 $p0 =~ s#^/##;
2343 my $c = find_graft_path_commit($tree_paths, $p1, $rev);
2344 next unless $c;
2345 find_graft_path_parents($grafts, $tree_paths, $c, $p0, $r0);
2346 }
2347}
2348
2349sub set_index {
2350 my $old = $ENV{GIT_INDEX_FILE};
2351 $ENV{GIT_INDEX_FILE} = shift;
2352 return $old;
2353}
2354
2355sub restore_index {
2356 my ($old) = @_;
2357 if (defined $old) {
2358 $ENV{GIT_INDEX_FILE} = $old;
2359 } else {
2360 delete $ENV{GIT_INDEX_FILE};
2361 }
2362}
2363
2364sub libsvn_commit_cb {
2365 my ($rev, $date, $committer, $c, $msg, $r_last, $cmt_last) = @_;
42d32870 2366 if ($_optimize_commits && $rev == ($r_last + 1)) {
a5e0cedc
EW
2367 my $log = libsvn_log_entry($rev,$committer,$date,$msg);
2368 $log->{tree} = get_tree_from_treeish($c);
2369 my $cmt = git_commit($log, $cmt_last, $c);
aef4e921 2370 my @diff = command('diff-tree', $cmt, $c);
a5e0cedc
EW
2371 if (@diff) {
2372 print STDERR "Trees differ: $cmt $c\n",
2373 join('',@diff),"\n";
2374 exit 1;
2375 }
2376 } else {
cf7424b0 2377 fetch("$rev=$c");
a5e0cedc
EW
2378 }
2379}
2380
2381sub libsvn_ls_fullurl {
2382 my $fullurl = shift;
d81bf827 2383 my $ra = Git::SVN::Ra->new($fullurl);
a5e0cedc 2384 my @ret;
1ca7558d 2385 my $r = defined $_revision ? $_revision : $ra->get_latest_revnum;
d81bf827 2386 my ($dirent, undef, undef) = $ra->get_dir('', $r);
4a4d94b2 2387 foreach my $d (sort keys %$dirent) {
a5e0cedc
EW
2388 if ($dirent->{$d}->kind == $SVN::Node::dir) {
2389 push @ret, "$d/"; # add '/' for compat with cli svn
2390 }
2391 }
a5e0cedc
EW
2392 return @ret;
2393}
2394
a5e0cedc
EW
2395sub libsvn_skip_unknown_revs {
2396 my $err = shift;
2397 my $errno = $err->apr_err();
2398 # Maybe the branch we're tracking didn't
2399 # exist when the repo started, so it's
2400 # not an error if it doesn't, just continue
2401 #
2402 # Wonderfully consistent library, eh?
2403 # 160013 - svn:// and file://
2404 # 175002 - http(s)://
747fa12c 2405 # 175007 - http(s):// (this repo required authorization, too...)
a5e0cedc 2406 # More codes may be discovered later...
747fa12c 2407 if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
a5e0cedc
EW
2408 return;
2409 }
2410 croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";
2411};
2412
42d32870
EW
2413# Tie::File seems to be prone to offset errors if revisions get sparse,
2414# it's not that fast, either. Tie::File is also not in Perl 5.6. So
2415# one of my favorite modules is out :< Next up would be one of the DBM
2416# modules, but I'm not sure which is most portable... So I'll just
2417# go with something that's plain-text, but still capable of
2418# being randomly accessed. So here's my ultra-simple fixed-width
2419# database. All records are 40 characters + "\n", so it's easy to seek
2420# to a revision: (41 * rev) is the byte offset.
2421# A record of 40 0s denotes an empty revision.
2422# And yes, it's still pretty fast (faster than Tie::File).
2423sub revdb_set {
2424 my ($file, $rev, $commit) = @_;
2425 length $commit == 40 or croak "arg3 must be a full SHA1 hexsum\n";
2426 open my $fh, '+<', $file or croak $!;
2427 my $offset = $rev * 41;
2428 # assume that append is the common case:
2429 seek $fh, 0, 2 or croak $!;
2430 my $pos = tell $fh;
2431 if ($pos < $offset) {
2432 print $fh (('0' x 40),"\n") x (($offset - $pos) / 41);
2433 }
2434 seek $fh, $offset, 0 or croak $!;
2435 print $fh $commit,"\n";
2436 close $fh or croak $!;
2437}
2438
2439sub revdb_get {
2440 my ($file, $rev) = @_;
2441 my $ret;
2442 my $offset = $rev * 41;
2443 open my $fh, '<', $file or croak $!;
2444 seek $fh, $offset, 0;
2445 if (tell $fh == $offset) {
2446 $ret = readline $fh;
2447 if (defined $ret) {
2448 chomp $ret;
2449 $ret = undef if ($ret =~ /^0{40}$/);
2450 }
2451 }
2452 close $fh or croak $!;
2453 return $ret;
2454}
2455
1a82e793
EW
2456sub copy_remote_ref {
2457 my $origin = $_cp_remote ? $_cp_remote : 'origin';
2458 my $ref = "refs/remotes/$GIT_SVN";
aef4e921
EW
2459 if (command('ls-remote', $origin, $ref)) {
2460 command_noisy('fetch', $origin, "$ref:$ref");
a35a0458 2461 } elsif ($_cp_remote && !$_upgrade) {
1a82e793
EW
2462 die "Unable to find remote reference: ",
2463 "refs/remotes/$GIT_SVN on $origin\n";
2464 }
2465}
b9c85187
EW
2466
2467{
2468 my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file.
2469 $SVN::Node::dir.$SVN::Node::unknown.
2470 $SVN::Node::none.$SVN::Node::file.
2471 $SVN::Node::dir.$SVN::Node::unknown.
2472 $SVN::Auth::SSL::CNMISMATCH.
2473 $SVN::Auth::SSL::NOTYETVALID.
2474 $SVN::Auth::SSL::EXPIRED.
2475 $SVN::Auth::SSL::UNKNOWNCA.
2476 $SVN::Auth::SSL::OTHER;
2477}
2478
27a1a801
EW
2479package SVN::Git::Fetcher;
2480use vars qw/@ISA/;
2481use strict;
2482use warnings;
2483use Carp qw/croak/;
2484use IO::File qw//;
2485
2486# file baton members: path, mode_a, mode_b, pool, fh, blob, base
2487sub new {
2488 my ($class, $git_svn) = @_;
2489 my $self = SVN::Delta::Editor->new;
2490 bless $self, $class;
27a1a801 2491 $self->{c} = $git_svn->{c} if exists $git_svn->{c};
0864e3ba 2492 $self->{q} = $git_svn->{q};
d2a9a87b
EW
2493 $self->{empty} = {};
2494 $self->{dir_prop} = {};
2495 $self->{file_prop} = {};
2496 $self->{absent_dir} = {};
2497 $self->{absent_file} = {};
aef4e921
EW
2498 ($self->{gui}, $self->{ctx}) = command_input_pipe(
2499 qw/update-index -z --index-info/);
27a1a801
EW
2500 require Digest::MD5;
2501 $self;
2502}
2503
d2a9a87b
EW
2504sub open_root {
2505 { path => '' };
2506}
2507
2508sub open_directory {
2509 my ($self, $path, $pb, $rev) = @_;
2510 { path => $path };
2511}
2512
27a1a801
EW
2513sub delete_entry {
2514 my ($self, $path, $rev, $pb) = @_;
4a87db0e
EW
2515 my $gui = $self->{gui};
2516
2517 # remove entire directories.
2518 if (command('ls-tree', $self->{c}, '--', $path) =~ /^040000 tree/) {
2519 my ($ls, $ctx) = command_output_pipe(qw/ls-tree
2520 -r --name-only -z/,
2521 $self->{c}, '--', $path);
2522 local $/ = "\0";
2523 while (<$ls>) {
2524 print $gui '0 ',0 x 40,"\t",$_ or croak $!;
2525 print "\tD\t$_\n" unless $self->{q};
2526 }
2527 print "\tD\t$path/\n" unless $self->{q};
2528 command_close_pipe($ls, $ctx);
2529 $self->{empty}->{$path} = 0
2530 } else {
2531 print $gui '0 ',0 x 40,"\t",$path,"\0" or croak $!;
2532 print "\tD\t$path\n" unless $self->{q};
2533 }
27a1a801
EW
2534 undef;
2535}
2536
2537sub open_file {
2538 my ($self, $path, $pb, $rev) = @_;
aef4e921 2539 my ($mode, $blob) = (command('ls-tree', $self->{c}, '--',$path)
27a1a801 2540 =~ /^(\d{6}) blob ([a-f\d]{40})\t/);
006ede5e
EW
2541 unless (defined $mode && defined $blob) {
2542 die "$path was not found in commit $self->{c} (r$rev)\n";
2543 }
27a1a801 2544 { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob,
0864e3ba 2545 pool => SVN::Pool->new, action => 'M' };
27a1a801
EW
2546}
2547
2548sub add_file {
2549 my ($self, $path, $pb, $cp_path, $cp_rev) = @_;
d2a9a87b
EW
2550 my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
2551 delete $self->{empty}->{$dir};
27a1a801 2552 { path => $path, mode_a => 100644, mode_b => 100644,
0864e3ba 2553 pool => SVN::Pool->new, action => 'A' };
27a1a801
EW
2554}
2555
d2a9a87b
EW
2556sub add_directory {
2557 my ($self, $path, $cp_path, $cp_rev) = @_;
2558 my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
2559 delete $self->{empty}->{$dir};
2560 $self->{empty}->{$path} = 1;
2561 { path => $path };
2562}
2563
2564sub change_dir_prop {
2565 my ($self, $db, $prop, $value) = @_;
2566 $self->{dir_prop}->{$db->{path}} ||= {};
2567 $self->{dir_prop}->{$db->{path}}->{$prop} = $value;
2568 undef;
2569}
2570
2571sub absent_directory {
2572 my ($self, $path, $pb) = @_;
2573 $self->{absent_dir}->{$pb->{path}} ||= [];
2574 push @{$self->{absent_dir}->{$pb->{path}}}, $path;
2575 undef;
2576}
2577
2578sub absent_file {
2579 my ($self, $path, $pb) = @_;
2580 $self->{absent_file}->{$pb->{path}} ||= [];
2581 push @{$self->{absent_file}->{$pb->{path}}}, $path;
2582 undef;
2583}
2584
27a1a801
EW
2585sub change_file_prop {
2586 my ($self, $fb, $prop, $value) = @_;
2587 if ($prop eq 'svn:executable') {
2588 if ($fb->{mode_b} != 120000) {
2589 $fb->{mode_b} = defined $value ? 100755 : 100644;
2590 }
2591 } elsif ($prop eq 'svn:special') {
2592 $fb->{mode_b} = defined $value ? 120000 : 100644;
d2a9a87b
EW
2593 } else {
2594 $self->{file_prop}->{$fb->{path}} ||= {};
2595 $self->{file_prop}->{$fb->{path}}->{$prop} = $value;
27a1a801
EW
2596 }
2597 undef;
2598}
2599
2600sub apply_textdelta {
2601 my ($self, $fb, $exp) = @_;
2602 my $fh = IO::File->new_tmpfile;
2603 $fh->autoflush(1);
2604 # $fh gets auto-closed() by SVN::TxDelta::apply(),
2605 # (but $base does not,) so dup() it for reading in close_file
2606 open my $dup, '<&', $fh or croak $!;
2607 my $base = IO::File->new_tmpfile;
2608 $base->autoflush(1);
2609 if ($fb->{blob}) {
2610 defined (my $pid = fork) or croak $!;
2611 if (!$pid) {
2612 open STDOUT, '>&', $base or croak $!;
2613 print STDOUT 'link ' if ($fb->{mode_a} == 120000);
2614 exec qw/git-cat-file blob/, $fb->{blob} or croak $!;
2615 }
2616 waitpid $pid, 0;
2617 croak $? if $?;
2618
2619 if (defined $exp) {
2620 seek $base, 0, 0 or croak $!;
2621 my $md5 = Digest::MD5->new;
2622 $md5->addfile($base);
2623 my $got = $md5->hexdigest;
2624 die "Checksum mismatch: $fb->{path} $fb->{blob}\n",
2625 "expected: $exp\n",
2626 " got: $got\n" if ($got ne $exp);
2627 }
2628 }
2629 seek $base, 0, 0 or croak $!;
2630 $fb->{fh} = $dup;
2631 $fb->{base} = $base;
2632 [ SVN::TxDelta::apply($base, $fh, undef, $fb->{path}, $fb->{pool}) ];
2633}
2634
2635sub close_file {
2636 my ($self, $fb, $exp) = @_;
2637 my $hash;
2638 my $path = $fb->{path};
2639 if (my $fh = $fb->{fh}) {
2640 seek($fh, 0, 0) or croak $!;
2641 my $md5 = Digest::MD5->new;
2642 $md5->addfile($fh);
2643 my $got = $md5->hexdigest;
2644 die "Checksum mismatch: $path\n",
2645 "expected: $exp\n got: $got\n" if ($got ne $exp);
2646 seek($fh, 0, 0) or croak $!;
2647 if ($fb->{mode_b} == 120000) {
2648 read($fh, my $buf, 5) == 5 or croak $!;
2649 $buf eq 'link ' or die "$path has mode 120000",
2650 "but is not a link\n";
2651 }
2652 defined(my $pid = open my $out,'-|') or die "Can't fork: $!\n";
2653 if (!$pid) {
2654 open STDIN, '<&', $fh or croak $!;
2655 exec qw/git-hash-object -w --stdin/ or croak $!;
2656 }
2657 chomp($hash = do { local $/; <$out> });
2658 close $out or croak $!;
2659 close $fh or croak $!;
2660 $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
2661 close $fb->{base} or croak $!;
2662 } else {
2663 $hash = $fb->{blob} or die "no blob information\n";
2664 }
2665 $fb->{pool}->clear;
2666 my $gui = $self->{gui};
2667 print $gui "$fb->{mode_b} $hash\t$path\0" or croak $!;
0864e3ba 2668 print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $self->{q};
27a1a801
EW
2669 undef;
2670}
2671
2672sub abort_edit {
2673 my $self = shift;
aef4e921 2674 eval { command_close_pipe($self->{gui}, $self->{ctx}) };
27a1a801
EW
2675 $self->SUPER::abort_edit(@_);
2676}
2677
2678sub close_edit {
2679 my $self = shift;
aef4e921 2680 command_close_pipe($self->{gui}, $self->{ctx});
dad73c0b 2681 $self->{git_commit_ok} = 1;
27a1a801
EW
2682 $self->SUPER::close_edit(@_);
2683}
1a82e793 2684
a5e0cedc
EW
2685package SVN::Git::Editor;
2686use vars qw/@ISA/;
2687use strict;
2688use warnings;
2689use Carp qw/croak/;
2690use IO::File;
2691
2692sub new {
2693 my $class = shift;
2694 my $git_svn = shift;
2695 my $self = SVN::Delta::Editor->new(@_);
2696 bless $self, $class;
2697 foreach (qw/svn_path c r ra /) {
2698 die "$_ required!\n" unless (defined $git_svn->{$_});
2699 $self->{$_} = $git_svn->{$_};
2700 }
2701 $self->{pool} = SVN::Pool->new;
2702 $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
2703 $self->{rm} = { };
2704 require Digest::MD5;
2705 return $self;
2706}
2707
2708sub split_path {
2709 return ($_[0] =~ m#^(.*?)/?([^/]+)$#);
2710}
2711
2712sub repo_path {
747fa12c 2713 (defined $_[1] && length $_[1]) ? $_[1] : ''
a5e0cedc
EW
2714}
2715
2716sub url_path {
2717 my ($self, $path) = @_;
2718 $self->{ra}->{url} . '/' . $self->repo_path($path);
2719}
2720
2721sub rmdirs {
80f50749 2722 my ($self, $q) = @_;
a5e0cedc
EW
2723 my $rm = $self->{rm};
2724 delete $rm->{''}; # we never delete the url we're tracking
2725 return unless %$rm;
2726
2727 foreach (keys %$rm) {
2728 my @d = split m#/#, $_;
2729 my $c = shift @d;
2730 $rm->{$c} = 1;
2731 while (@d) {
2732 $c .= '/' . shift @d;
2733 $rm->{$c} = 1;
2734 }
2735 }
2736 delete $rm->{$self->{svn_path}};
2737 delete $rm->{''}; # we never delete the url we're tracking
2738 return unless %$rm;
2739
aef4e921
EW
2740 my ($fh, $ctx) = command_output_pipe(
2741 qw/ls-tree --name-only -r -z/, $self->{c});
a5e0cedc
EW
2742 local $/ = "\0";
2743 while (<$fh>) {
2744 chomp;
747fa12c 2745 my @dn = split m#/#, $_;
c07eee1f
EW
2746 while (pop @dn) {
2747 delete $rm->{join '/', @dn};
2748 }
2749 unless (%$rm) {
22600a25 2750 close $fh;
c07eee1f
EW
2751 return;
2752 }
a5e0cedc 2753 }
aef4e921 2754 command_close_pipe($fh, $ctx);
c07eee1f 2755
a5e0cedc
EW
2756 my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat});
2757 foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
2758 $self->close_directory($bat->{$d}, $p);
2759 my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#);
cea70cf8 2760 print "\tD+\t$d/\n" unless $q;
a5e0cedc
EW
2761 $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p);
2762 delete $bat->{$d};
2763 }
2764}
2765
2766sub open_or_add_dir {
2767 my ($self, $full_path, $baton) = @_;
d81bf827 2768 my $t = $self->{ra}->check_path($full_path, $self->{r});
a5e0cedc
EW
2769 if ($t == $SVN::Node::none) {
2770 return $self->add_directory($full_path, $baton,
2771 undef, -1, $self->{pool});
2772 } elsif ($t == $SVN::Node::dir) {
2773 return $self->open_directory($full_path, $baton,
2774 $self->{r}, $self->{pool});
2775 }
2776 print STDERR "$full_path already exists in repository at ",
2777 "r$self->{r} and it is not a directory (",
2778 ($t == $SVN::Node::file ? 'file' : 'unknown'),"/$t)\n";
2779 exit 1;
2780}
2781
2782sub ensure_path {
2783 my ($self, $path) = @_;
2784 my $bat = $self->{bat};
2785 $path = $self->repo_path($path);
2786 return $bat->{''} unless (length $path);
2787 my @p = split m#/+#, $path;
2788 my $c = shift @p;
2789 $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''});
2790 while (@p) {
2791 my $c0 = $c;
2792 $c .= '/' . shift @p;
2793 $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0});
2794 }
2795 return $bat->{$c};
2796}
2797
2798sub A {
80f50749 2799 my ($self, $m, $q) = @_;
a5e0cedc
EW
2800 my ($dir, $file) = split_path($m->{file_b});
2801 my $pbat = $self->ensure_path($dir);
2802 my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
2803 undef, -1);
80f50749 2804 print "\tA\t$m->{file_b}\n" unless $q;
a5e0cedc
EW
2805 $self->chg_file($fbat, $m);
2806 $self->close_file($fbat,undef,$self->{pool});
2807}
2808
2809sub C {
80f50749 2810 my ($self, $m, $q) = @_;
a5e0cedc
EW
2811 my ($dir, $file) = split_path($m->{file_b});
2812 my $pbat = $self->ensure_path($dir);
2813 my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
2814 $self->url_path($m->{file_a}), $self->{r});
80f50749 2815 print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $q;
a5e0cedc
EW
2816 $self->chg_file($fbat, $m);
2817 $self->close_file($fbat,undef,$self->{pool});
2818}
2819
2820sub delete_entry {
2821 my ($self, $path, $pbat) = @_;
2822 my $rpath = $self->repo_path($path);
2823 my ($dir, $file) = split_path($rpath);
2824 $self->{rm}->{$dir} = 1;
2825 $self->SUPER::delete_entry($rpath, $self->{r}, $pbat, $self->{pool});
2826}
2827
2828sub R {
80f50749 2829 my ($self, $m, $q) = @_;
a5e0cedc
EW
2830 my ($dir, $file) = split_path($m->{file_b});
2831 my $pbat = $self->ensure_path($dir);
2832 my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
2833 $self->url_path($m->{file_a}), $self->{r});
80f50749 2834 print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $q;
a5e0cedc
EW
2835 $self->chg_file($fbat, $m);
2836 $self->close_file($fbat,undef,$self->{pool});
2837
2838 ($dir, $file) = split_path($m->{file_a});
2839 $pbat = $self->ensure_path($dir);
2840 $self->delete_entry($m->{file_a}, $pbat);
2841}
2842
2843sub M {
80f50749 2844 my ($self, $m, $q) = @_;
a5e0cedc
EW
2845 my ($dir, $file) = split_path($m->{file_b});
2846 my $pbat = $self->ensure_path($dir);
2847 my $fbat = $self->open_file($self->repo_path($m->{file_b}),
2848 $pbat,$self->{r},$self->{pool});
80f50749 2849 print "\t$m->{chg}\t$m->{file_b}\n" unless $q;
a5e0cedc
EW
2850 $self->chg_file($fbat, $m);
2851 $self->close_file($fbat,undef,$self->{pool});
2852}
2853
2854sub T { shift->M(@_) }
2855
2856sub change_file_prop {
2857 my ($self, $fbat, $pname, $pval) = @_;
2858 $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool});
2859}
2860
2861sub chg_file {
2862 my ($self, $fbat, $m) = @_;
2863 if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
2864 $self->change_file_prop($fbat,'svn:executable','*');
2865 } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
2866 $self->change_file_prop($fbat,'svn:executable',undef);
2867 }
2868 my $fh = IO::File->new_tmpfile or croak $!;
2869 if ($m->{mode_b} =~ /^120/) {
2870 print $fh 'link ' or croak $!;
2871 $self->change_file_prop($fbat,'svn:special','*');
2872 } elsif ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
2873 $self->change_file_prop($fbat,'svn:special',undef);
2874 }
2875 defined(my $pid = fork) or croak $!;
2876 if (!$pid) {
2877 open STDOUT, '>&', $fh or croak $!;
2878 exec qw/git-cat-file blob/, $m->{sha1_b} or croak $!;
2879 }
2880 waitpid $pid, 0;
2881 croak $? if $?;
2882 $fh->flush == 0 or croak $!;
2883 seek $fh, 0, 0 or croak $!;
2884
2885 my $md5 = Digest::MD5->new;
2886 $md5->addfile($fh) or croak $!;
2887 seek $fh, 0, 0 or croak $!;
2888
2889 my $exp = $md5->hexdigest;
f7197dff
EW
2890 my $pool = SVN::Pool->new;
2891 my $atd = $self->apply_textdelta($fbat, undef, $pool);
2892 my $got = SVN::TxDelta::send_stream($fh, @$atd, $pool);
a5e0cedc 2893 die "Checksum mismatch\nexpected: $exp\ngot: $got\n" if ($got ne $exp);
f7197dff 2894 $pool->clear;
a5e0cedc
EW
2895
2896 close $fh or croak $!;
2897}
2898
2899sub D {
80f50749 2900 my ($self, $m, $q) = @_;
a5e0cedc
EW
2901 my ($dir, $file) = split_path($m->{file_b});
2902 my $pbat = $self->ensure_path($dir);
80f50749 2903 print "\tD\t$m->{file_b}\n" unless $q;
a5e0cedc
EW
2904 $self->delete_entry($m->{file_b}, $pbat);
2905}
2906
2907sub close_edit {
2908 my ($self) = @_;
2909 my ($p,$bat) = ($self->{pool}, $self->{bat});
2910 foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {
2911 $self->close_directory($bat->{$_}, $p);
2912 }
2913 $self->SUPER::close_edit($p);
2914 $p->clear;
2915}
2916
2917sub abort_edit {
2918 my ($self) = @_;
2919 $self->SUPER::abort_edit($self->{pool});
2920 $self->{pool}->clear;
2921}
2922
d81bf827
EW
2923package Git::SVN::Ra;
2924use vars qw/@ISA $config_dir/;
2925use strict;
2926use warnings;
2927my ($can_do_switch);
2928
2929BEGIN {
2930 # enforce temporary pool usage for some simple functions
2931 my $e;
2932 foreach (qw/get_latest_revnum rev_proplist get_file
2933 check_path get_dir get_uuid get_repos_root/) {
2934 $e .= "sub $_ {
2935 my \$self = shift;
2936 my \$pool = SVN::Pool->new;
2937 my \@ret = \$self->SUPER::$_(\@_,\$pool);
2938 \$pool->clear;
2939 wantarray ? \@ret : \$ret[0]; }\n";
2940 }
2941 eval $e;
2942}
2943
2944sub new {
2945 my ($class, $url) = @_;
2946 SVN::_Core::svn_config_ensure($config_dir, undef);
2947 my ($baton, $callbacks) = SVN::Core::auth_open_helper([
2948 SVN::Client::get_simple_provider(),
2949 SVN::Client::get_ssl_server_trust_file_provider(),
2950 SVN::Client::get_simple_prompt_provider(
2951 \&Git::SVN::Prompt::simple, 2),
2952 SVN::Client::get_ssl_client_cert_prompt_provider(
2953 \&Git::SVN::Prompt::ssl_client_cert, 2),
2954 SVN::Client::get_ssl_client_cert_pw_prompt_provider(
2955 \&Git::SVN::Prompt::ssl_client_cert_pw, 2),
2956 SVN::Client::get_username_provider(),
2957 SVN::Client::get_ssl_server_trust_prompt_provider(
2958 \&Git::SVN::Prompt::ssl_server_trust),
2959 SVN::Client::get_username_prompt_provider(
2960 \&Git::SVN::Prompt::username, 2),
2961 ]);
2962 my $config = SVN::Core::config_get_config($config_dir);
2963 my $self = SVN::Ra->new(url => $url, auth => $baton,
2964 config => $config,
2965 pool => SVN::Pool->new,
2966 auth_provider_callbacks => $callbacks);
2967 $self->{svn_path} = $url;
2968 $self->{repos_root} = $self->get_repos_root;
2969 $self->{svn_path} =~ s#^\Q$self->{repos_root}\E/*##;
2970 bless $self, $class;
2971}
2972
2973sub DESTROY {
2974 my $self = shift;
2975 $self->{pool}->clear if $self->{pool};
2976 $self->SUPER::DESTROY(@_);
2977}
2978
2979sub dup {
2980 my ($self) = @_;
2981 my $dup = SVN::Ra->new(pool => SVN::Pool->new,
2982 map { $_ => $self->{$_} } qw/config url
2983 auth auth_provider_callbacks repos_root svn_path/);
2984 bless $dup, ref $self;
2985}
2986
2987sub get_log {
2988 my ($self, @args) = @_;
2989 my $pool = SVN::Pool->new;
2990 $args[4]-- if $args[4] && ! $::_follow_parent;
2991 splice(@args, 3, 1) if ($SVN::Core::VERSION le '1.2.0');
2992 my $ret = $self->SUPER::get_log(@args, $pool);
2993 $pool->clear;
2994 $ret;
2995}
2996
2997sub get_commit_editor {
2998 my ($self, $msg, $cb, $pool) = @_;
2999 my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
3000 $self->SUPER::get_commit_editor($msg, $cb, @lock, $pool);
3001}
3002
3003sub uuid {
3004 my ($self) = @_;
3005 $self->{uuid} ||= $self->get_uuid;
3006}
3007
3008sub gs_do_update {
3009 my ($self, $rev_a, $rev_b, $path, $recurse, $editor) = @_;
3010 my $pool = SVN::Pool->new;
3011 my $reporter = $self->do_update($rev_b, $path, $recurse,
3012 $editor, $pool);
3013 my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
3014 my $new = ($rev_a == $rev_b);
3015 $reporter->set_path($path, $rev_a, $new, @lock, $pool);
3016 $reporter->finish_report($pool);
3017 $pool->clear;
3018 $editor->{git_commit_ok};
3019}
3020
3021sub gs_do_switch {
3022 my ($self, $rev_a, $rev_b, $path, $recurse, $url_b, $editor) = @_;
3023 my $pool = SVN::Pool->new;
3024 my $reporter = $self->do_switch($rev_b, $path, $recurse,
3025 $url_b, $editor, $pool);
3026 my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
3027 $reporter->set_path($path, $rev_a, 0, @lock, $pool);
3028 $reporter->finish_report($pool);
3029 $pool->clear;
3030 $editor->{git_commit_ok};
3031}
3032
3033sub can_do_switch {
3034 my $self = shift;
3035 unless (defined $can_do_switch) {
3036 my $pool = SVN::Pool->new;
3037 my $rep = eval {
3038 $self->do_switch(1, '', 0, $self->{url},
3039 SVN::Delta::Editor->new, $pool);
3040 };
3041 if ($@) {
3042 $can_do_switch = 0;
3043 } else {
3044 $rep->abort_report($pool);
3045 $can_do_switch = 1;
3046 }
3047 $pool->clear;
3048 }
3049 $can_do_switch;
3050}
3051
3397f9df
EW
3052__END__
3053
3054Data structures:
3055
b9c85187 3056$log_msg hashref as returned by libsvn_log_entry()
3397f9df
EW
3057{
3058 msg => 'whitespace-formatted log entry
3059', # trailing newline is preserved
3060 revision => '8', # integer
3061 date => '2004-02-24T17:01:44.108345Z', # commit date
3062 author => 'committer name'
3063};
3064
3397f9df
EW
3065@mods = array of diff-index line hashes, each element represents one line
3066 of diff-index output
3067
3068diff-index line ($m hash)
3069{
3070 mode_a => first column of diff-index output, no leading ':',
3071 mode_b => second column of diff-index output,
3072 sha1_b => sha1sum of the final blob,
ac8e0b91 3073 chg => change type [MCRADT],
3397f9df
EW
3074 file_a => original file name of a file (iff chg is 'C' or 'R')
3075 file_b => new/current file name of a file (any chg)
3076}
3077;
a5e0cedc 3078
a00439ac
EW
3079# retval of read_url_paths{,_all}();
3080$l_map = {
3081 # repository root url
3082 'https://svn.musicpd.org' => {
3083 # repository path # GIT_SVN_ID
3084 'mpd/trunk' => 'trunk',
3085 'mpd/tags/0.11.5' => 'tags/0.11.5',
3086 },
3087}
3088
a5e0cedc
EW
3089Notes:
3090 I don't trust the each() function on unless I created %hash myself
3091 because the internal iterator may not have started at base.