git-svn: respect commiturl option for branch/tag
[git] / git-svn.perl
1 #!/usr/bin/env perl
2 # Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
3 # License: GPL v2 or later
4 use warnings;
5 use strict;
6 use vars qw/    $AUTHOR $VERSION
7                 $sha1 $sha1_short $_revision $_repository
8                 $_q $_authors $_authors_prog %users/;
9 $AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
10 $VERSION = '@@GIT_VERSION@@';
11
12 # From which subdir have we been invoked?
13 my $cmd_dir_prefix = eval {
14         command_oneline([qw/rev-parse --show-prefix/], STDERR => 0)
15 } || '';
16
17 my $git_dir_user_set = 1 if defined $ENV{GIT_DIR};
18 $ENV{GIT_DIR} ||= '.git';
19 $Git::SVN::default_repo_id = 'svn';
20 $Git::SVN::default_ref_id = $ENV{GIT_SVN_ID} || 'git-svn';
21 $Git::SVN::Ra::_log_window_size = 100;
22 $Git::SVN::_minimize_url = 'unset';
23
24 if (! exists $ENV{SVN_SSH}) {
25         if (exists $ENV{GIT_SSH}) {
26                 $ENV{SVN_SSH} = $ENV{GIT_SSH};
27                 if ($^O eq 'msys') {
28                         $ENV{SVN_SSH} =~ s/\\/\\\\/g;
29                 }
30         }
31 }
32
33 $Git::SVN::Log::TZ = $ENV{TZ};
34 $ENV{TZ} = 'UTC';
35 $| = 1; # unbuffer STDOUT
36
37 sub fatal (@) { print STDERR "@_\n"; exit 1 }
38 require SVN::Core; # use()-ing this causes segfaults for me... *shrug*
39 require SVN::Ra;
40 require SVN::Delta;
41 if ($SVN::Core::VERSION lt '1.1.0') {
42         fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)";
43 }
44 my $can_compress = eval { require Compress::Zlib; 1};
45 push @Git::SVN::Ra::ISA, 'SVN::Ra';
46 push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
47 push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor';
48 use Carp qw/croak/;
49 use Digest::MD5;
50 use IO::File qw//;
51 use File::Basename qw/dirname basename/;
52 use File::Path qw/mkpath/;
53 use File::Spec;
54 use File::Find;
55 use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
56 use IPC::Open3;
57 use Git;
58
59 BEGIN {
60         # import functions from Git into our packages, en masse
61         no strict 'refs';
62         foreach (qw/command command_oneline command_noisy command_output_pipe
63                     command_input_pipe command_close_pipe
64                     command_bidi_pipe command_close_bidi_pipe/) {
65                 for my $package ( qw(SVN::Git::Editor SVN::Git::Fetcher
66                         Git::SVN::Migration Git::SVN::Log Git::SVN),
67                         __PACKAGE__) {
68                         *{"${package}::$_"} = \&{"Git::$_"};
69                 }
70         }
71 }
72
73 my ($SVN);
74
75 $sha1 = qr/[a-f\d]{40}/;
76 $sha1_short = qr/[a-f\d]{4,40}/;
77 my ($_stdin, $_help, $_edit,
78         $_message, $_file, $_branch_dest,
79         $_template, $_shared,
80         $_version, $_fetch_all, $_no_rebase, $_fetch_parent,
81         $_merge, $_strategy, $_dry_run, $_local,
82         $_prefix, $_no_checkout, $_url, $_verbose,
83         $_git_format, $_commit_url, $_tag);
84 $Git::SVN::_follow_parent = 1;
85 $_q ||= 0;
86 my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
87                     'config-dir=s' => \$Git::SVN::Ra::config_dir,
88                     'no-auth-cache' => \$Git::SVN::Prompt::_no_auth_cache,
89                     'ignore-paths=s' => \$SVN::Git::Fetcher::_ignore_regex );
90 my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent,
91                 'authors-file|A=s' => \$_authors,
92                 'authors-prog=s' => \$_authors_prog,
93                 'repack:i' => \$Git::SVN::_repack,
94                 'noMetadata' => \$Git::SVN::_no_metadata,
95                 'useSvmProps' => \$Git::SVN::_use_svm_props,
96                 'useSvnsyncProps' => \$Git::SVN::_use_svnsync_props,
97                 'log-window-size=i' => \$Git::SVN::Ra::_log_window_size,
98                 'no-checkout' => \$_no_checkout,
99                 'quiet|q+' => \$_q,
100                 'repack-flags|repack-args|repack-opts=s' =>
101                    \$Git::SVN::_repack_flags,
102                 'use-log-author' => \$Git::SVN::_use_log_author,
103                 'add-author-from' => \$Git::SVN::_add_author_from,
104                 'localtime' => \$Git::SVN::_localtime,
105                 %remote_opts );
106
107 my ($_trunk, @_tags, @_branches, $_stdlayout);
108 my %icv;
109 my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared,
110                   'trunk|T=s' => \$_trunk, 'tags|t=s@' => \@_tags,
111                   'branches|b=s@' => \@_branches, 'prefix=s' => \$_prefix,
112                   'stdlayout|s' => \$_stdlayout,
113                   'minimize-url|m!' => \$Git::SVN::_minimize_url,
114                   'no-metadata' => sub { $icv{noMetadata} = 1 },
115                   'use-svm-props' => sub { $icv{useSvmProps} = 1 },
116                   'use-svnsync-props' => sub { $icv{useSvnsyncProps} = 1 },
117                   'rewrite-root=s' => sub { $icv{rewriteRoot} = $_[1] },
118                   %remote_opts );
119 my %cmt_opts = ( 'edit|e' => \$_edit,
120                 'rmdir' => \$SVN::Git::Editor::_rmdir,
121                 'find-copies-harder' => \$SVN::Git::Editor::_find_copies_harder,
122                 'l=i' => \$SVN::Git::Editor::_rename_limit,
123                 'copy-similarity|C=i'=> \$SVN::Git::Editor::_cp_similarity
124 );
125
126 my %cmd = (
127         fetch => [ \&cmd_fetch, "Download new revisions from SVN",
128                         { 'revision|r=s' => \$_revision,
129                           'fetch-all|all' => \$_fetch_all,
130                           'parent|p' => \$_fetch_parent,
131                            %fc_opts } ],
132         clone => [ \&cmd_clone, "Initialize and fetch revisions",
133                         { 'revision|r=s' => \$_revision,
134                            %fc_opts, %init_opts } ],
135         init => [ \&cmd_init, "Initialize a repo for tracking" .
136                           " (requires URL argument)",
137                           \%init_opts ],
138         'multi-init' => [ \&cmd_multi_init,
139                           "Deprecated alias for ".
140                           "'$0 init -T<trunk> -b<branches> -t<tags>'",
141                           \%init_opts ],
142         dcommit => [ \&cmd_dcommit,
143                      'Commit several diffs to merge with upstream',
144                         { 'merge|m|M' => \$_merge,
145                           'strategy|s=s' => \$_strategy,
146                           'verbose|v' => \$_verbose,
147                           'dry-run|n' => \$_dry_run,
148                           'fetch-all|all' => \$_fetch_all,
149                           'commit-url=s' => \$_commit_url,
150                           'revision|r=i' => \$_revision,
151                           'no-rebase' => \$_no_rebase,
152                         %cmt_opts, %fc_opts } ],
153         branch => [ \&cmd_branch,
154                     'Create a branch in the SVN repository',
155                     { 'message|m=s' => \$_message,
156                       'destination|d=s' => \$_branch_dest,
157                       'dry-run|n' => \$_dry_run,
158                       'tag|t' => \$_tag } ],
159         tag => [ sub { $_tag = 1; cmd_branch(@_) },
160                  'Create a tag in the SVN repository',
161                  { 'message|m=s' => \$_message,
162                    'destination|d=s' => \$_branch_dest,
163                    'dry-run|n' => \$_dry_run } ],
164         'set-tree' => [ \&cmd_set_tree,
165                         "Set an SVN repository to a git tree-ish",
166                         { 'stdin' => \$_stdin, %cmt_opts, %fc_opts, } ],
167         'create-ignore' => [ \&cmd_create_ignore,
168                              'Create a .gitignore per svn:ignore',
169                              { 'revision|r=i' => \$_revision
170                              } ],
171         'mkdirs' => [ \&cmd_mkdirs ,
172                       "recreate empty directories after a checkout",
173                       { 'revision|r=i' => \$_revision } ],
174         'propget' => [ \&cmd_propget,
175                        'Print the value of a property on a file or directory',
176                        { 'revision|r=i' => \$_revision } ],
177         'proplist' => [ \&cmd_proplist,
178                        'List all properties of a file or directory',
179                        { 'revision|r=i' => \$_revision } ],
180         'show-ignore' => [ \&cmd_show_ignore, "Show svn:ignore listings",
181                         { 'revision|r=i' => \$_revision
182                         } ],
183         'show-externals' => [ \&cmd_show_externals, "Show svn:externals listings",
184                         { 'revision|r=i' => \$_revision
185                         } ],
186         'multi-fetch' => [ \&cmd_multi_fetch,
187                            "Deprecated alias for $0 fetch --all",
188                            { 'revision|r=s' => \$_revision, %fc_opts } ],
189         'migrate' => [ sub { },
190                        # no-op, we automatically run this anyways,
191                        'Migrate configuration/metadata/layout from
192                         previous versions of git-svn',
193                        { 'minimize' => \$Git::SVN::Migration::_minimize,
194                          %remote_opts } ],
195         'log' => [ \&Git::SVN::Log::cmd_show_log, 'Show commit logs',
196                         { 'limit=i' => \$Git::SVN::Log::limit,
197                           'revision|r=s' => \$_revision,
198                           'verbose|v' => \$Git::SVN::Log::verbose,
199                           'incremental' => \$Git::SVN::Log::incremental,
200                           'oneline' => \$Git::SVN::Log::oneline,
201                           'show-commit' => \$Git::SVN::Log::show_commit,
202                           'non-recursive' => \$Git::SVN::Log::non_recursive,
203                           'authors-file|A=s' => \$_authors,
204                           'color' => \$Git::SVN::Log::color,
205                           'pager=s' => \$Git::SVN::Log::pager
206                         } ],
207         'find-rev' => [ \&cmd_find_rev,
208                         "Translate between SVN revision numbers and tree-ish",
209                         {} ],
210         'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory",
211                         { 'merge|m|M' => \$_merge,
212                           'verbose|v' => \$_verbose,
213                           'strategy|s=s' => \$_strategy,
214                           'local|l' => \$_local,
215                           'fetch-all|all' => \$_fetch_all,
216                           'dry-run|n' => \$_dry_run,
217                           %fc_opts } ],
218         'commit-diff' => [ \&cmd_commit_diff,
219                            'Commit a diff between two trees',
220                         { 'message|m=s' => \$_message,
221                           'file|F=s' => \$_file,
222                           'revision|r=s' => \$_revision,
223                         %cmt_opts } ],
224         'info' => [ \&cmd_info,
225                     "Show info about the latest SVN revision
226                      on the current branch",
227                     { 'url' => \$_url, } ],
228         'blame' => [ \&Git::SVN::Log::cmd_blame,
229                     "Show what revision and author last modified each line of a file",
230                     { 'git-format' => \$_git_format } ],
231         'reset' => [ \&cmd_reset,
232                      "Undo fetches back to the specified SVN revision",
233                      { 'revision|r=s' => \$_revision,
234                        'parent|p' => \$_fetch_parent } ],
235         'gc' => [ \&cmd_gc,
236                   "Compress unhandled.log files in .git/svn and remove " .
237                   "index files in .git/svn",
238                 {} ],
239 );
240
241 my $cmd;
242 for (my $i = 0; $i < @ARGV; $i++) {
243         if (defined $cmd{$ARGV[$i]}) {
244                 $cmd = $ARGV[$i];
245                 splice @ARGV, $i, 1;
246                 last;
247         } elsif ($ARGV[$i] eq 'help') {
248                 $cmd = $ARGV[$i+1];
249                 usage(0);
250         }
251 };
252
253 # make sure we're always running at the top-level working directory
254 unless ($cmd && $cmd =~ /(?:clone|init|multi-init)$/) {
255         unless (-d $ENV{GIT_DIR}) {
256                 if ($git_dir_user_set) {
257                         die "GIT_DIR=$ENV{GIT_DIR} explicitly set, ",
258                             "but it is not a directory\n";
259                 }
260                 my $git_dir = delete $ENV{GIT_DIR};
261                 my $cdup = undef;
262                 git_cmd_try {
263                         $cdup = command_oneline(qw/rev-parse --show-cdup/);
264                         $git_dir = '.' unless ($cdup);
265                         chomp $cdup if ($cdup);
266                         $cdup = "." unless ($cdup && length $cdup);
267                 } "Already at toplevel, but $git_dir not found\n";
268                 chdir $cdup or die "Unable to chdir up to '$cdup'\n";
269                 unless (-d $git_dir) {
270                         die "$git_dir still not found after going to ",
271                             "'$cdup'\n";
272                 }
273                 $ENV{GIT_DIR} = $git_dir;
274         }
275         $_repository = Git->repository(Repository => $ENV{GIT_DIR});
276 }
277
278 my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
279
280 read_git_config(\%opts);
281 if ($cmd && ($cmd eq 'log' || $cmd eq 'blame')) {
282         Getopt::Long::Configure('pass_through');
283 }
284 my $rv = GetOptions(%opts, 'help|H|h' => \$_help, 'version|V' => \$_version,
285                     'minimize-connections' => \$Git::SVN::Migration::_minimize,
286                     'id|i=s' => \$Git::SVN::default_ref_id,
287                     'svn-remote|remote|R=s' => sub {
288                        $Git::SVN::no_reuse_existing = 1;
289                        $Git::SVN::default_repo_id = $_[1] });
290 exit 1 if (!$rv && $cmd && $cmd ne 'log');
291
292 usage(0) if $_help;
293 version() if $_version;
294 usage(1) unless defined $cmd;
295 load_authors() if $_authors;
296 if (defined $_authors_prog) {
297         $_authors_prog = "'" . File::Spec->rel2abs($_authors_prog) . "'";
298 }
299
300 unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) {
301         Git::SVN::Migration::migration_check();
302 }
303 Git::SVN::init_vars();
304 eval {
305         Git::SVN::verify_remotes_sanity();
306         $cmd{$cmd}->[0]->(@ARGV);
307 };
308 fatal $@ if $@;
309 post_fetch_checkout();
310 exit 0;
311
312 ####################### primary functions ######################
313 sub usage {
314         my $exit = shift || 0;
315         my $fd = $exit ? \*STDERR : \*STDOUT;
316         print $fd <<"";
317 git-svn - bidirectional operations between a single Subversion tree and git
318 Usage: git svn <command> [options] [arguments]\n
319
320         print $fd "Available commands:\n" unless $cmd;
321
322         foreach (sort keys %cmd) {
323                 next if $cmd && $cmd ne $_;
324                 next if /^multi-/; # don't show deprecated commands
325                 print $fd '  ',pack('A17',$_),$cmd{$_}->[1],"\n";
326                 foreach (sort keys %{$cmd{$_}->[2]}) {
327                         # mixed-case options are for .git/config only
328                         next if /[A-Z]/ && /^[a-z]+$/i;
329                         # prints out arguments as they should be passed:
330                         my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
331                         print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
332                                                         "--$_" : "-$_" }
333                                                 split /\|/,$_)," $x\n";
334                 }
335         }
336         print $fd <<"";
337 \nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an
338 arbitrary identifier if you're tracking multiple SVN branches/repositories in
339 one git repository and want to keep them separate.  See git-svn(1) for more
340 information.
341
342         exit $exit;
343 }
344
345 sub version {
346         print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n";
347         exit 0;
348 }
349
350 sub do_git_init_db {
351         unless (-d $ENV{GIT_DIR}) {
352                 my @init_db = ('init');
353                 push @init_db, "--template=$_template" if defined $_template;
354                 if (defined $_shared) {
355                         if ($_shared =~ /[a-z]/) {
356                                 push @init_db, "--shared=$_shared";
357                         } else {
358                                 push @init_db, "--shared";
359                         }
360                 }
361                 command_noisy(@init_db);
362                 $_repository = Git->repository(Repository => ".git");
363         }
364         command_noisy('config', 'core.autocrlf', 'false');
365         my $set;
366         my $pfx = "svn-remote.$Git::SVN::default_repo_id";
367         foreach my $i (keys %icv) {
368                 die "'$set' and '$i' cannot both be set\n" if $set;
369                 next unless defined $icv{$i};
370                 command_noisy('config', "$pfx.$i", $icv{$i});
371                 $set = $i;
372         }
373         my $ignore_regex = \$SVN::Git::Fetcher::_ignore_regex;
374         command_noisy('config', "$pfx.ignore-paths", $$ignore_regex)
375                 if defined $$ignore_regex;
376 }
377
378 sub init_subdir {
379         my $repo_path = shift or return;
380         mkpath([$repo_path]) unless -d $repo_path;
381         chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n";
382         $ENV{GIT_DIR} = '.git';
383         $_repository = Git->repository(Repository => $ENV{GIT_DIR});
384 }
385
386 sub cmd_clone {
387         my ($url, $path) = @_;
388         if (!defined $path &&
389             (defined $_trunk || @_branches || @_tags ||
390              defined $_stdlayout) &&
391             $url !~ m#^[a-z\+]+://#) {
392                 $path = $url;
393         }
394         $path = basename($url) if !defined $path || !length $path;
395         my $authors_absolute = $_authors ? File::Spec->rel2abs($_authors) : "";
396         cmd_init($url, $path);
397         command_oneline('config', 'svn.authorsfile', $authors_absolute)
398             if $_authors;
399         Git::SVN::fetch_all($Git::SVN::default_repo_id);
400 }
401
402 sub cmd_init {
403         if (defined $_stdlayout) {
404                 $_trunk = 'trunk' if (!defined $_trunk);
405                 @_tags = 'tags' if (! @_tags);
406                 @_branches = 'branches' if (! @_branches);
407         }
408         if (defined $_trunk || @_branches || @_tags) {
409                 return cmd_multi_init(@_);
410         }
411         my $url = shift or die "SVN repository location required ",
412                                "as a command-line argument\n";
413         $url = canonicalize_url($url);
414         init_subdir(@_);
415         do_git_init_db();
416
417         if ($Git::SVN::_minimize_url eq 'unset') {
418                 $Git::SVN::_minimize_url = 0;
419         }
420
421         Git::SVN->init($url);
422 }
423
424 sub cmd_fetch {
425         if (grep /^\d+=./, @_) {
426                 die "'<rev>=<commit>' fetch arguments are ",
427                     "no longer supported.\n";
428         }
429         my ($remote) = @_;
430         if (@_ > 1) {
431                 die "Usage: $0 fetch [--all] [--parent] [svn-remote]\n";
432         }
433         $Git::SVN::no_reuse_existing = undef;
434         if ($_fetch_parent) {
435                 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
436                 unless ($gs) {
437                         die "Unable to determine upstream SVN information from ",
438                             "working tree history\n";
439                 }
440                 # just fetch, don't checkout.
441                 $_no_checkout = 'true';
442                 $_fetch_all ? $gs->fetch_all : $gs->fetch;
443         } elsif ($_fetch_all) {
444                 cmd_multi_fetch();
445         } else {
446                 $remote ||= $Git::SVN::default_repo_id;
447                 Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes());
448         }
449 }
450
451 sub cmd_set_tree {
452         my (@commits) = @_;
453         if ($_stdin || !@commits) {
454                 print "Reading from stdin...\n";
455                 @commits = ();
456                 while (<STDIN>) {
457                         if (/\b($sha1_short)\b/o) {
458                                 unshift @commits, $1;
459                         }
460                 }
461         }
462         my @revs;
463         foreach my $c (@commits) {
464                 my @tmp = command('rev-parse',$c);
465                 if (scalar @tmp == 1) {
466                         push @revs, $tmp[0];
467                 } elsif (scalar @tmp > 1) {
468                         push @revs, reverse(command('rev-list',@tmp));
469                 } else {
470                         fatal "Failed to rev-parse $c";
471                 }
472         }
473         my $gs = Git::SVN->new;
474         my ($r_last, $cmt_last) = $gs->last_rev_commit;
475         $gs->fetch;
476         if (defined $gs->{last_rev} && $r_last != $gs->{last_rev}) {
477                 fatal "There are new revisions that were fetched ",
478                       "and need to be merged (or acknowledged) ",
479                       "before committing.\nlast rev: $r_last\n",
480                       " current: $gs->{last_rev}";
481         }
482         $gs->set_tree($_) foreach @revs;
483         print "Done committing ",scalar @revs," revisions to SVN\n";
484         unlink $gs->{index};
485 }
486
487 sub cmd_dcommit {
488         my $head = shift;
489         git_cmd_try { command_oneline(qw/diff-index --quiet HEAD/) }
490                 'Cannot dcommit with a dirty index.  Commit your changes first, '
491                 . "or stash them with `git stash'.\n";
492         $head ||= 'HEAD';
493
494         my $old_head;
495         if ($head ne 'HEAD') {
496                 $old_head = eval {
497                         command_oneline([qw/symbolic-ref -q HEAD/])
498                 };
499                 if ($old_head) {
500                         $old_head =~ s{^refs/heads/}{};
501                 } else {
502                         $old_head = eval { command_oneline(qw/rev-parse HEAD/) };
503                 }
504                 command(['checkout', $head], STDERR => 0);
505         }
506
507         my @refs;
508         my ($url, $rev, $uuid, $gs) = working_head_info('HEAD', \@refs);
509         unless ($gs) {
510                 die "Unable to determine upstream SVN information from ",
511                     "$head history.\nPerhaps the repository is empty.";
512         }
513
514         if (defined $_commit_url) {
515                 $url = $_commit_url;
516         } else {
517                 $url = eval { command_oneline('config', '--get',
518                               "svn-remote.$gs->{repo_id}.commiturl") };
519                 if (!$url) {
520                         $url = $gs->full_url
521                 }
522         }
523
524         my $last_rev = $_revision if defined $_revision;
525         if ($url) {
526                 print "Committing to $url ...\n";
527         }
528         my ($linear_refs, $parents) = linearize_history($gs, \@refs);
529         if ($_no_rebase && scalar(@$linear_refs) > 1) {
530                 warn "Attempting to commit more than one change while ",
531                      "--no-rebase is enabled.\n",
532                      "If these changes depend on each other, re-running ",
533                      "without --no-rebase may be required."
534         }
535         my $expect_url = $url;
536         Git::SVN::remove_username($expect_url);
537         while (1) {
538                 my $d = shift @$linear_refs or last;
539                 unless (defined $last_rev) {
540                         (undef, $last_rev, undef) = cmt_metadata("$d~1");
541                         unless (defined $last_rev) {
542                                 fatal "Unable to extract revision information ",
543                                       "from commit $d~1";
544                         }
545                 }
546                 if ($_dry_run) {
547                         print "diff-tree $d~1 $d\n";
548                 } else {
549                         my $cmt_rev;
550                         my %ed_opts = ( r => $last_rev,
551                                         log => get_commit_entry($d)->{log},
552                                         ra => Git::SVN::Ra->new($url),
553                                         config => SVN::Core::config_get_config(
554                                                 $Git::SVN::Ra::config_dir
555                                         ),
556                                         tree_a => "$d~1",
557                                         tree_b => $d,
558                                         editor_cb => sub {
559                                                print "Committed r$_[0]\n";
560                                                $cmt_rev = $_[0];
561                                         },
562                                         svn_path => '');
563                         if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
564                                 print "No changes\n$d~1 == $d\n";
565                         } elsif ($parents->{$d} && @{$parents->{$d}}) {
566                                 $gs->{inject_parents_dcommit}->{$cmt_rev} =
567                                                                $parents->{$d};
568                         }
569                         $_fetch_all ? $gs->fetch_all : $gs->fetch;
570                         $last_rev = $cmt_rev;
571                         next if $_no_rebase;
572
573                         # we always want to rebase against the current HEAD,
574                         # not any head that was passed to us
575                         my @diff = command('diff-tree', $d,
576                                            $gs->refname, '--');
577                         my @finish;
578                         if (@diff) {
579                                 @finish = rebase_cmd();
580                                 print STDERR "W: $d and ", $gs->refname,
581                                              " differ, using @finish:\n",
582                                              join("\n", @diff), "\n";
583                         } else {
584                                 print "No changes between current HEAD and ",
585                                       $gs->refname,
586                                       "\nResetting to the latest ",
587                                       $gs->refname, "\n";
588                                 @finish = qw/reset --mixed/;
589                         }
590                         command_noisy(@finish, $gs->refname);
591                         if (@diff) {
592                                 @refs = ();
593                                 my ($url_, $rev_, $uuid_, $gs_) =
594                                               working_head_info('HEAD', \@refs);
595                                 my ($linear_refs_, $parents_) =
596                                               linearize_history($gs_, \@refs);
597                                 if (scalar(@$linear_refs) !=
598                                     scalar(@$linear_refs_)) {
599                                         fatal "# of revisions changed ",
600                                           "\nbefore:\n",
601                                           join("\n", @$linear_refs),
602                                           "\n\nafter:\n",
603                                           join("\n", @$linear_refs_), "\n",
604                                           'If you are attempting to commit ',
605                                           "merges, try running:\n\t",
606                                           'git rebase --interactive',
607                                           '--preserve-merges ',
608                                           $gs->refname,
609                                           "\nBefore dcommitting";
610                                 }
611                                 if ($url_ ne $expect_url) {
612                                         if ($url_ eq $gs->metadata_url) {
613                                                 print
614                                                   "Accepting rewritten URL:",
615                                                   " $url_\n";
616                                         } else {
617                                                 fatal
618                                                   "URL mismatch after rebase:",
619                                                   " $url_ != $expect_url";
620                                         }
621                                 }
622                                 if ($uuid_ ne $uuid) {
623                                         fatal "uuid mismatch after rebase: ",
624                                               "$uuid_ != $uuid";
625                                 }
626                                 # remap parents
627                                 my (%p, @l, $i);
628                                 for ($i = 0; $i < scalar @$linear_refs; $i++) {
629                                         my $new = $linear_refs_->[$i] or next;
630                                         $p{$new} =
631                                                 $parents->{$linear_refs->[$i]};
632                                         push @l, $new;
633                                 }
634                                 $parents = \%p;
635                                 $linear_refs = \@l;
636                         }
637                 }
638         }
639
640         if ($old_head) {
641                 my $new_head = command_oneline(qw/rev-parse HEAD/);
642                 my $new_is_symbolic = eval {
643                         command_oneline(qw/symbolic-ref -q HEAD/);
644                 };
645                 if ($new_is_symbolic) {
646                         print "dcommitted the branch ", $head, "\n";
647                 } else {
648                         print "dcommitted on a detached HEAD because you gave ",
649                               "a revision argument.\n",
650                               "The rewritten commit is: ", $new_head, "\n";
651                 }
652                 command(['checkout', $old_head], STDERR => 0);
653         }
654
655         unlink $gs->{index};
656 }
657
658 sub cmd_branch {
659         my ($branch_name, $head) = @_;
660
661         unless (defined $branch_name && length $branch_name) {
662                 die(($_tag ? "tag" : "branch") . " name required\n");
663         }
664         $head ||= 'HEAD';
665
666         my (undef, $rev, undef, $gs) = working_head_info($head);
667         my $src = $gs->full_url;
668
669         my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}};
670         my $allglobs = $remote->{ $_tag ? 'tags' : 'branches' };
671         my $glob;
672         if ($#{$allglobs} == 0) {
673                 $glob = $allglobs->[0];
674         } else {
675                 unless(defined $_branch_dest) {
676                         die "Multiple ",
677                             $_tag ? "tag" : "branch",
678                             " paths defined for Subversion repository.\n",
679                             "You must specify where you want to create the ",
680                             $_tag ? "tag" : "branch",
681                             " with the --destination argument.\n";
682                 }
683                 foreach my $g (@{$allglobs}) {
684                         # SVN::Git::Editor could probably be moved to Git.pm..
685                         my $re = SVN::Git::Editor::glob2pat($g->{path}->{left});
686                         if ($_branch_dest =~ /$re/) {
687                                 $glob = $g;
688                                 last;
689                         }
690                 }
691                 unless (defined $glob) {
692                         my $dest_re = qr/\b\Q$_branch_dest\E\b/;
693                         foreach my $g (@{$allglobs}) {
694                                 $g->{path}->{left} =~ /$dest_re/ or next;
695                                 if (defined $glob) {
696                                         die "Ambiguous destination: ",
697                                             $_branch_dest, "\nmatches both '",
698                                             $glob->{path}->{left}, "' and '",
699                                             $g->{path}->{left}, "'\n";
700                                 }
701                                 $glob = $g;
702                         }
703                         unless (defined $glob) {
704                                 die "Unknown ",
705                                     $_tag ? "tag" : "branch",
706                                     " destination $_branch_dest\n";
707                         }
708                 }
709         }
710         my ($lft, $rgt) = @{ $glob->{path} }{qw/left right/};
711         my $url;
712         if (defined $_commit_url) {
713                 $url = $_commit_url;
714         } else {
715                 $url = eval { command_oneline('config', '--get',
716                         "svn-remote.$gs->{repo_id}.commiturl") };
717                 if (!$url) {
718                         $url = $remote->{url};
719                 }
720         }
721         my $dst = join '/', $url, $lft, $branch_name, ($rgt || ());
722
723         if ($dst =~ /^https:/ && $src =~ /^http:/) {
724                 $src=~s/^http:/https:/;
725         }
726
727         my $ctx = SVN::Client->new(
728                 auth    => Git::SVN::Ra::_auth_providers(),
729                 log_msg => sub {
730                         ${ $_[0] } = defined $_message
731                                 ? $_message
732                                 : 'Create ' . ($_tag ? 'tag ' : 'branch ' )
733                                 . $branch_name;
734                 },
735         );
736
737         eval {
738                 $ctx->ls($dst, 'HEAD', 0);
739         } and die "branch ${branch_name} already exists\n";
740
741         print "Copying ${src} at r${rev} to ${dst}...\n";
742         $ctx->copy($src, $rev, $dst)
743                 unless $_dry_run;
744
745         $gs->fetch_all;
746 }
747
748 sub cmd_find_rev {
749         my $revision_or_hash = shift or die "SVN or git revision required ",
750                                             "as a command-line argument\n";
751         my $result;
752         if ($revision_or_hash =~ /^r\d+$/) {
753                 my $head = shift;
754                 $head ||= 'HEAD';
755                 my @refs;
756                 my (undef, undef, $uuid, $gs) = working_head_info($head, \@refs);
757                 unless ($gs) {
758                         die "Unable to determine upstream SVN information from ",
759                             "$head history\n";
760                 }
761                 my $desired_revision = substr($revision_or_hash, 1);
762                 $result = $gs->rev_map_get($desired_revision, $uuid);
763         } else {
764                 my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
765                 $result = $rev;
766         }
767         print "$result\n" if $result;
768 }
769
770 sub cmd_rebase {
771         command_noisy(qw/update-index --refresh/);
772         my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
773         unless ($gs) {
774                 die "Unable to determine upstream SVN information from ",
775                     "working tree history\n";
776         }
777         if ($_dry_run) {
778                 print "Remote Branch: " . $gs->refname . "\n";
779                 print "SVN URL: " . $url . "\n";
780                 return;
781         }
782         if (command(qw/diff-index HEAD --/)) {
783                 print STDERR "Cannot rebase with uncommited changes:\n";
784                 command_noisy('status');
785                 exit 1;
786         }
787         unless ($_local) {
788                 # rebase will checkout for us, so no need to do it explicitly
789                 $_no_checkout = 'true';
790                 $_fetch_all ? $gs->fetch_all : $gs->fetch;
791         }
792         command_noisy(rebase_cmd(), $gs->refname);
793         $gs->mkemptydirs;
794 }
795
796 sub cmd_show_ignore {
797         my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
798         $gs ||= Git::SVN->new;
799         my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
800         $gs->prop_walk($gs->{path}, $r, sub {
801                 my ($gs, $path, $props) = @_;
802                 print STDOUT "\n# $path\n";
803                 my $s = $props->{'svn:ignore'} or return;
804                 $s =~ s/[\r\n]+/\n/g;
805                 $s =~ s/^\n+//;
806                 chomp $s;
807                 $s =~ s#^#$path#gm;
808                 print STDOUT "$s\n";
809         });
810 }
811
812 sub cmd_show_externals {
813         my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
814         $gs ||= Git::SVN->new;
815         my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
816         $gs->prop_walk($gs->{path}, $r, sub {
817                 my ($gs, $path, $props) = @_;
818                 print STDOUT "\n# $path\n";
819                 my $s = $props->{'svn:externals'} or return;
820                 $s =~ s/[\r\n]+/\n/g;
821                 chomp $s;
822                 $s =~ s#^#$path#gm;
823                 print STDOUT "$s\n";
824         });
825 }
826
827 sub cmd_create_ignore {
828         my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
829         $gs ||= Git::SVN->new;
830         my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
831         $gs->prop_walk($gs->{path}, $r, sub {
832                 my ($gs, $path, $props) = @_;
833                 # $path is of the form /path/to/dir/
834                 $path = '.' . $path;
835                 # SVN can have attributes on empty directories,
836                 # which git won't track
837                 mkpath([$path]) unless -d $path;
838                 my $ignore = $path . '.gitignore';
839                 my $s = $props->{'svn:ignore'} or return;
840                 open(GITIGNORE, '>', $ignore)
841                   or fatal("Failed to open `$ignore' for writing: $!");
842                 $s =~ s/[\r\n]+/\n/g;
843                 $s =~ s/^\n+//;
844                 chomp $s;
845                 # Prefix all patterns so that the ignore doesn't apply
846                 # to sub-directories.
847                 $s =~ s#^#/#gm;
848                 print GITIGNORE "$s\n";
849                 close(GITIGNORE)
850                   or fatal("Failed to close `$ignore': $!");
851                 command_noisy('add', '-f', $ignore);
852         });
853 }
854
855 sub cmd_mkdirs {
856         my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
857         $gs ||= Git::SVN->new;
858         $gs->mkemptydirs($_revision);
859 }
860
861 sub canonicalize_path {
862         my ($path) = @_;
863         my $dot_slash_added = 0;
864         if (substr($path, 0, 1) ne "/") {
865                 $path = "./" . $path;
866                 $dot_slash_added = 1;
867         }
868         # File::Spec->canonpath doesn't collapse x/../y into y (for a
869         # good reason), so let's do this manually.
870         $path =~ s#/+#/#g;
871         $path =~ s#/\.(?:/|$)#/#g;
872         $path =~ s#/[^/]+/\.\.##g;
873         $path =~ s#/$##g;
874         $path =~ s#^\./## if $dot_slash_added;
875         $path =~ s#^/##;
876         $path =~ s#^\.$##;
877         return $path;
878 }
879
880 sub canonicalize_url {
881         my ($url) = @_;
882         $url =~ s#^([^:]+://[^/]*/)(.*)$#$1 . canonicalize_path($2)#e;
883         return $url;
884 }
885
886 # get_svnprops(PATH)
887 # ------------------
888 # Helper for cmd_propget and cmd_proplist below.
889 sub get_svnprops {
890         my $path = shift;
891         my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
892         $gs ||= Git::SVN->new;
893
894         # prefix THE PATH by the sub-directory from which the user
895         # invoked us.
896         $path = $cmd_dir_prefix . $path;
897         fatal("No such file or directory: $path") unless -e $path;
898         my $is_dir = -d $path ? 1 : 0;
899         $path = $gs->{path} . '/' . $path;
900
901         # canonicalize the path (otherwise libsvn will abort or fail to
902         # find the file)
903         $path = canonicalize_path($path);
904
905         my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
906         my $props;
907         if ($is_dir) {
908                 (undef, undef, $props) = $gs->ra->get_dir($path, $r);
909         }
910         else {
911                 (undef, $props) = $gs->ra->get_file($path, $r, undef);
912         }
913         return $props;
914 }
915
916 # cmd_propget (PROP, PATH)
917 # ------------------------
918 # Print the SVN property PROP for PATH.
919 sub cmd_propget {
920         my ($prop, $path) = @_;
921         $path = '.' if not defined $path;
922         usage(1) if not defined $prop;
923         my $props = get_svnprops($path);
924         if (not defined $props->{$prop}) {
925                 fatal("`$path' does not have a `$prop' SVN property.");
926         }
927         print $props->{$prop} . "\n";
928 }
929
930 # cmd_proplist (PATH)
931 # -------------------
932 # Print the list of SVN properties for PATH.
933 sub cmd_proplist {
934         my $path = shift;
935         $path = '.' if not defined $path;
936         my $props = get_svnprops($path);
937         print "Properties on '$path':\n";
938         foreach (sort keys %{$props}) {
939                 print "  $_\n";
940         }
941 }
942
943 sub cmd_multi_init {
944         my $url = shift;
945         unless (defined $_trunk || @_branches || @_tags) {
946                 usage(1);
947         }
948
949         $_prefix = '' unless defined $_prefix;
950         if (defined $url) {
951                 $url = canonicalize_url($url);
952                 init_subdir(@_);
953         }
954         do_git_init_db();
955         if (defined $_trunk) {
956                 my $trunk_ref = 'refs/remotes/' . $_prefix . 'trunk';
957                 # try both old-style and new-style lookups:
958                 my $gs_trunk = eval { Git::SVN->new($trunk_ref) };
959                 unless ($gs_trunk) {
960                         my ($trunk_url, $trunk_path) =
961                                               complete_svn_url($url, $_trunk);
962                         $gs_trunk = Git::SVN->init($trunk_url, $trunk_path,
963                                                    undef, $trunk_ref);
964                 }
965         }
966         return unless @_branches || @_tags;
967         my $ra = $url ? Git::SVN::Ra->new($url) : undef;
968         foreach my $path (@_branches) {
969                 complete_url_ls_init($ra, $path, '--branches/-b', $_prefix);
970         }
971         foreach my $path (@_tags) {
972                 complete_url_ls_init($ra, $path, '--tags/-t', $_prefix.'tags/');
973         }
974 }
975
976 sub cmd_multi_fetch {
977         $Git::SVN::no_reuse_existing = undef;
978         my $remotes = Git::SVN::read_all_remotes();
979         foreach my $repo_id (sort keys %$remotes) {
980                 if ($remotes->{$repo_id}->{url}) {
981                         Git::SVN::fetch_all($repo_id, $remotes);
982                 }
983         }
984 }
985
986 # this command is special because it requires no metadata
987 sub cmd_commit_diff {
988         my ($ta, $tb, $url) = @_;
989         my $usage = "Usage: $0 commit-diff -r<revision> ".
990                     "<tree-ish> <tree-ish> [<URL>]";
991         fatal($usage) if (!defined $ta || !defined $tb);
992         my $svn_path = '';
993         if (!defined $url) {
994                 my $gs = eval { Git::SVN->new };
995                 if (!$gs) {
996                         fatal("Needed URL or usable git-svn --id in ",
997                               "the command-line\n", $usage);
998                 }
999                 $url = $gs->{url};
1000                 $svn_path = $gs->{path};
1001         }
1002         unless (defined $_revision) {
1003                 fatal("-r|--revision is a required argument\n", $usage);
1004         }
1005         if (defined $_message && defined $_file) {
1006                 fatal("Both --message/-m and --file/-F specified ",
1007                       "for the commit message.\n",
1008                       "I have no idea what you mean");
1009         }
1010         if (defined $_file) {
1011                 $_message = file_to_s($_file);
1012         } else {
1013                 $_message ||= get_commit_entry($tb)->{log};
1014         }
1015         my $ra ||= Git::SVN::Ra->new($url);
1016         my $r = $_revision;
1017         if ($r eq 'HEAD') {
1018                 $r = $ra->get_latest_revnum;
1019         } elsif ($r !~ /^\d+$/) {
1020                 die "revision argument: $r not understood by git-svn\n";
1021         }
1022         my %ed_opts = ( r => $r,
1023                         log => $_message,
1024                         ra => $ra,
1025                         tree_a => $ta,
1026                         tree_b => $tb,
1027                         editor_cb => sub { print "Committed r$_[0]\n" },
1028                         svn_path => $svn_path );
1029         if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
1030                 print "No changes\n$ta == $tb\n";
1031         }
1032 }
1033
1034 sub escape_uri_only {
1035         my ($uri) = @_;
1036         my @tmp;
1037         foreach (split m{/}, $uri) {
1038                 s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;
1039                 push @tmp, $_;
1040         }
1041         join('/', @tmp);
1042 }
1043
1044 sub escape_url {
1045         my ($url) = @_;
1046         if ($url =~ m#^([^:]+)://([^/]*)(.*)$#) {
1047                 my ($scheme, $domain, $uri) = ($1, $2, escape_uri_only($3));
1048                 $url = "$scheme://$domain$uri";
1049         }
1050         $url;
1051 }
1052
1053 sub cmd_info {
1054         my $path = canonicalize_path(defined($_[0]) ? $_[0] : ".");
1055         my $fullpath = canonicalize_path($cmd_dir_prefix . $path);
1056         if (exists $_[1]) {
1057                 die "Too many arguments specified\n";
1058         }
1059
1060         my ($file_type, $diff_status) = find_file_type_and_diff_status($path);
1061
1062         if (!$file_type && !$diff_status) {
1063                 print STDERR "svn: '$path' is not under version control\n";
1064                 exit 1;
1065         }
1066
1067         my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
1068         unless ($gs) {
1069                 die "Unable to determine upstream SVN information from ",
1070                     "working tree history\n";
1071         }
1072
1073         # canonicalize_path() will return "" to make libsvn 1.5.x happy,
1074         $path = "." if $path eq "";
1075
1076         my $full_url = $url . ($fullpath eq "" ? "" : "/$fullpath");
1077
1078         if ($_url) {
1079                 print escape_url($full_url), "\n";
1080                 return;
1081         }
1082
1083         my $result = "Path: $path\n";
1084         $result .= "Name: " . basename($path) . "\n" if $file_type ne "dir";
1085         $result .= "URL: " . escape_url($full_url) . "\n";
1086
1087         eval {
1088                 my $repos_root = $gs->repos_root;
1089                 Git::SVN::remove_username($repos_root);
1090                 $result .= "Repository Root: " . escape_url($repos_root) . "\n";
1091         };
1092         if ($@) {
1093                 $result .= "Repository Root: (offline)\n";
1094         }
1095         $result .= "Repository UUID: $uuid\n" unless $diff_status eq "A" &&
1096                 ($SVN::Core::VERSION le '1.5.4' || $file_type ne "dir");
1097         $result .= "Revision: " . ($diff_status eq "A" ? 0 : $rev) . "\n";
1098
1099         $result .= "Node Kind: " .
1100                    ($file_type eq "dir" ? "directory" : "file") . "\n";
1101
1102         my $schedule = $diff_status eq "A"
1103                        ? "add"
1104                        : ($diff_status eq "D" ? "delete" : "normal");
1105         $result .= "Schedule: $schedule\n";
1106
1107         if ($diff_status eq "A") {
1108                 print $result, "\n";
1109                 return;
1110         }
1111
1112         my ($lc_author, $lc_rev, $lc_date_utc);
1113         my @args = Git::SVN::Log::git_svn_log_cmd($rev, $rev, "--", $fullpath);
1114         my $log = command_output_pipe(@args);
1115         my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
1116         while (<$log>) {
1117                 if (/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {
1118                         $lc_author = $1;
1119                         $lc_date_utc = Git::SVN::Log::parse_git_date($2, $3);
1120                 } elsif (/^${esc_color}    (git-svn-id:.+)$/o) {
1121                         (undef, $lc_rev, undef) = ::extract_metadata($1);
1122                 }
1123         }
1124         close $log;
1125
1126         Git::SVN::Log::set_local_timezone();
1127
1128         $result .= "Last Changed Author: $lc_author\n";
1129         $result .= "Last Changed Rev: $lc_rev\n";
1130         $result .= "Last Changed Date: " .
1131                    Git::SVN::Log::format_svn_date($lc_date_utc) . "\n";
1132
1133         if ($file_type ne "dir") {
1134                 my $text_last_updated_date =
1135                     ($diff_status eq "D" ? $lc_date_utc : (stat $path)[9]);
1136                 $result .=
1137                     "Text Last Updated: " .
1138                     Git::SVN::Log::format_svn_date($text_last_updated_date) .
1139                     "\n";
1140                 my $checksum;
1141                 if ($diff_status eq "D") {
1142                         my ($fh, $ctx) =
1143                             command_output_pipe(qw(cat-file blob), "HEAD:$path");
1144                         if ($file_type eq "link") {
1145                                 my $file_name = <$fh>;
1146                                 $checksum = md5sum("link $file_name");
1147                         } else {
1148                                 $checksum = md5sum($fh);
1149                         }
1150                         command_close_pipe($fh, $ctx);
1151                 } elsif ($file_type eq "link") {
1152                         my $file_name =
1153                             command(qw(cat-file blob), "HEAD:$path");
1154                         $checksum =
1155                             md5sum("link " . $file_name);
1156                 } else {
1157                         open FILE, "<", $path or die $!;
1158                         $checksum = md5sum(\*FILE);
1159                         close FILE or die $!;
1160                 }
1161                 $result .= "Checksum: " . $checksum . "\n";
1162         }
1163
1164         print $result, "\n";
1165 }
1166
1167 sub cmd_reset {
1168         my $target = shift || $_revision or die "SVN revision required\n";
1169         $target = $1 if $target =~ /^r(\d+)$/;
1170         $target =~ /^\d+$/ or die "Numeric SVN revision expected\n";
1171         my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
1172         unless ($gs) {
1173                 die "Unable to determine upstream SVN information from ".
1174                     "history\n";
1175         }
1176         my ($r, $c) = $gs->find_rev_before($target, not $_fetch_parent);
1177         $gs->rev_map_set($r, $c, 'reset', $uuid);
1178         print "r$r = $c ($gs->{ref_id})\n";
1179 }
1180
1181 sub cmd_gc {
1182         if (!$can_compress) {
1183                 warn "Compress::Zlib could not be found; unhandled.log " .
1184                      "files will not be compressed.\n";
1185         }
1186         find({ wanted => \&gc_directory, no_chdir => 1}, "$ENV{GIT_DIR}/svn");
1187 }
1188
1189 ########################### utility functions #########################
1190
1191 sub rebase_cmd {
1192         my @cmd = qw/rebase/;
1193         push @cmd, '-v' if $_verbose;
1194         push @cmd, qw/--merge/ if $_merge;
1195         push @cmd, "--strategy=$_strategy" if $_strategy;
1196         @cmd;
1197 }
1198
1199 sub post_fetch_checkout {
1200         return if $_no_checkout;
1201         my $gs = $Git::SVN::_head or return;
1202         return if verify_ref('refs/heads/master^0');
1203
1204         # look for "trunk" ref if it exists
1205         my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}};
1206         my $fetch = $remote->{fetch};
1207         if ($fetch) {
1208                 foreach my $p (keys %$fetch) {
1209                         basename($fetch->{$p}) eq 'trunk' or next;
1210                         $gs = Git::SVN->new($fetch->{$p}, $gs->{repo_id}, $p);
1211                         last;
1212                 }
1213         }
1214
1215         my $valid_head = verify_ref('HEAD^0');
1216         command_noisy(qw(update-ref refs/heads/master), $gs->refname);
1217         return if ($valid_head || !verify_ref('HEAD^0'));
1218
1219         return if $ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;
1220         my $index = $ENV{GIT_INDEX_FILE} || "$ENV{GIT_DIR}/index";
1221         return if -f $index;
1222
1223         return if command_oneline(qw/rev-parse --is-inside-work-tree/) eq 'false';
1224         return if command_oneline(qw/rev-parse --is-inside-git-dir/) eq 'true';
1225         command_noisy(qw/read-tree -m -u -v HEAD HEAD/);
1226         print STDERR "Checked out HEAD:\n  ",
1227                      $gs->full_url, " r", $gs->last_rev, "\n";
1228         $gs->mkemptydirs($gs->last_rev);
1229 }
1230
1231 sub complete_svn_url {
1232         my ($url, $path) = @_;
1233         $path =~ s#/+$##;
1234         if ($path !~ m#^[a-z\+]+://#) {
1235                 if (!defined $url || $url !~ m#^[a-z\+]+://#) {
1236                         fatal("E: '$path' is not a complete URL ",
1237                               "and a separate URL is not specified");
1238                 }
1239                 return ($url, $path);
1240         }
1241         return ($path, '');
1242 }
1243
1244 sub complete_url_ls_init {
1245         my ($ra, $repo_path, $switch, $pfx) = @_;
1246         unless ($repo_path) {
1247                 print STDERR "W: $switch not specified\n";
1248                 return;
1249         }
1250         $repo_path =~ s#/+$##;
1251         if ($repo_path =~ m#^[a-z\+]+://#) {
1252                 $ra = Git::SVN::Ra->new($repo_path);
1253                 $repo_path = '';
1254         } else {
1255                 $repo_path =~ s#^/+##;
1256                 unless ($ra) {
1257                         fatal("E: '$repo_path' is not a complete URL ",
1258                               "and a separate URL is not specified");
1259                 }
1260         }
1261         my $url = $ra->{url};
1262         my $gs = Git::SVN->init($url, undef, undef, undef, 1);
1263         my $k = "svn-remote.$gs->{repo_id}.url";
1264         my $orig_url = eval { command_oneline(qw/config --get/, $k) };
1265         if ($orig_url && ($orig_url ne $gs->{url})) {
1266                 die "$k already set: $orig_url\n",
1267                     "wanted to set to: $gs->{url}\n";
1268         }
1269         command_oneline('config', $k, $gs->{url}) unless $orig_url;
1270         my $remote_path = "$gs->{path}/$repo_path";
1271         $remote_path =~ s{%([0-9A-F]{2})}{chr hex($1)}ieg;
1272         $remote_path =~ s#/+#/#g;
1273         $remote_path =~ s#^/##g;
1274         $remote_path .= "/*" if $remote_path !~ /\*/;
1275         my ($n) = ($switch =~ /^--(\w+)/);
1276         if (length $pfx && $pfx !~ m#/$#) {
1277                 die "--prefix='$pfx' must have a trailing slash '/'\n";
1278         }
1279         command_noisy('config',
1280                       '--add',
1281                       "svn-remote.$gs->{repo_id}.$n",
1282                       "$remote_path:refs/remotes/$pfx*" .
1283                         ('/*' x (($remote_path =~ tr/*/*/) - 1)) );
1284 }
1285
1286 sub verify_ref {
1287         my ($ref) = @_;
1288         eval { command_oneline([ 'rev-parse', '--verify', $ref ],
1289                                { STDERR => 0 }); };
1290 }
1291
1292 sub get_tree_from_treeish {
1293         my ($treeish) = @_;
1294         # $treeish can be a symbolic ref, too:
1295         my $type = command_oneline(qw/cat-file -t/, $treeish);
1296         my $expected;
1297         while ($type eq 'tag') {
1298                 ($treeish, $type) = command(qw/cat-file tag/, $treeish);
1299         }
1300         if ($type eq 'commit') {
1301                 $expected = (grep /^tree /, command(qw/cat-file commit/,
1302                                                     $treeish))[0];
1303                 ($expected) = ($expected =~ /^tree ($sha1)$/o);
1304                 die "Unable to get tree from $treeish\n" unless $expected;
1305         } elsif ($type eq 'tree') {
1306                 $expected = $treeish;
1307         } else {
1308                 die "$treeish is a $type, expected tree, tag or commit\n";
1309         }
1310         return $expected;
1311 }
1312
1313 sub get_commit_entry {
1314         my ($treeish) = shift;
1315         my %log_entry = ( log => '', tree => get_tree_from_treeish($treeish) );
1316         my $commit_editmsg = "$ENV{GIT_DIR}/COMMIT_EDITMSG";
1317         my $commit_msg = "$ENV{GIT_DIR}/COMMIT_MSG";
1318         open my $log_fh, '>', $commit_editmsg or croak $!;
1319
1320         my $type = command_oneline(qw/cat-file -t/, $treeish);
1321         if ($type eq 'commit' || $type eq 'tag') {
1322                 my ($msg_fh, $ctx) = command_output_pipe('cat-file',
1323                                                          $type, $treeish);
1324                 my $in_msg = 0;
1325                 my $author;
1326                 my $saw_from = 0;
1327                 my $msgbuf = "";
1328                 while (<$msg_fh>) {
1329                         if (!$in_msg) {
1330                                 $in_msg = 1 if (/^\s*$/);
1331                                 $author = $1 if (/^author (.*>)/);
1332                         } elsif (/^git-svn-id: /) {
1333                                 # skip this for now, we regenerate the
1334                                 # correct one on re-fetch anyways
1335                                 # TODO: set *:merge properties or like...
1336                         } else {
1337                                 if (/^From:/ || /^Signed-off-by:/) {
1338                                         $saw_from = 1;
1339                                 }
1340                                 $msgbuf .= $_;
1341                         }
1342                 }
1343                 $msgbuf =~ s/\s+$//s;
1344                 if ($Git::SVN::_add_author_from && defined($author)
1345                     && !$saw_from) {
1346                         $msgbuf .= "\n\nFrom: $author";
1347                 }
1348                 print $log_fh $msgbuf or croak $!;
1349                 command_close_pipe($msg_fh, $ctx);
1350         }
1351         close $log_fh or croak $!;
1352
1353         if ($_edit || ($type eq 'tree')) {
1354                 chomp(my $editor = command_oneline(qw(var GIT_EDITOR)));
1355                 system('sh', '-c', $editor.' "$@"', $editor, $commit_editmsg);
1356         }
1357         rename $commit_editmsg, $commit_msg or croak $!;
1358         {
1359                 require Encode;
1360                 # SVN requires messages to be UTF-8 when entering the repo
1361                 local $/;
1362                 open $log_fh, '<', $commit_msg or croak $!;
1363                 binmode $log_fh;
1364                 chomp($log_entry{log} = <$log_fh>);
1365
1366                 my $enc = Git::config('i18n.commitencoding') || 'UTF-8';
1367                 my $msg = $log_entry{log};
1368
1369                 eval { $msg = Encode::decode($enc, $msg, 1) };
1370                 if ($@) {
1371                         die "Could not decode as $enc:\n", $msg,
1372                             "\nPerhaps you need to set i18n.commitencoding\n";
1373                 }
1374
1375                 eval { $msg = Encode::encode('UTF-8', $msg, 1) };
1376                 die "Could not encode as UTF-8:\n$msg\n" if $@;
1377
1378                 $log_entry{log} = $msg;
1379
1380                 close $log_fh or croak $!;
1381         }
1382         unlink $commit_msg;
1383         \%log_entry;
1384 }
1385
1386 sub s_to_file {
1387         my ($str, $file, $mode) = @_;
1388         open my $fd,'>',$file or croak $!;
1389         print $fd $str,"\n" or croak $!;
1390         close $fd or croak $!;
1391         chmod ($mode &~ umask, $file) if (defined $mode);
1392 }
1393
1394 sub file_to_s {
1395         my $file = shift;
1396         open my $fd,'<',$file or croak "$!: file: $file\n";
1397         local $/;
1398         my $ret = <$fd>;
1399         close $fd or croak $!;
1400         $ret =~ s/\s*$//s;
1401         return $ret;
1402 }
1403
1404 # '<svn username> = real-name <email address>' mapping based on git-svnimport:
1405 sub load_authors {
1406         open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
1407         my $log = $cmd eq 'log';
1408         while (<$authors>) {
1409                 chomp;
1410                 next unless /^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;
1411                 my ($user, $name, $email) = ($1, $2, $3);
1412                 if ($log) {
1413                         $Git::SVN::Log::rusers{"$name <$email>"} = $user;
1414                 } else {
1415                         $users{$user} = [$name, $email];
1416                 }
1417         }
1418         close $authors or croak $!;
1419 }
1420
1421 # convert GetOpt::Long specs for use by git-config
1422 sub read_git_config {
1423         my $opts = shift;
1424         my @config_only;
1425         foreach my $o (keys %$opts) {
1426                 # if we have mixedCase and a long option-only, then
1427                 # it's a config-only variable that we don't need for
1428                 # the command-line.
1429                 push @config_only, $o if ($o =~ /[A-Z]/ && $o =~ /^[a-z]+$/i);
1430                 my $v = $opts->{$o};
1431                 my ($key) = ($o =~ /^([a-zA-Z\-]+)/);
1432                 $key =~ s/-//g;
1433                 my $arg = 'git config';
1434                 $arg .= ' --int' if ($o =~ /[:=]i$/);
1435                 $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
1436                 if (ref $v eq 'ARRAY') {
1437                         chomp(my @tmp = `$arg --get-all svn.$key`);
1438                         @$v = @tmp if @tmp;
1439                 } else {
1440                         chomp(my $tmp = `$arg --get svn.$key`);
1441                         if ($tmp && !($arg =~ / --bool/ && $tmp eq 'false')) {
1442                                 $$v = $tmp;
1443                         }
1444                 }
1445         }
1446         delete @$opts{@config_only} if @config_only;
1447 }
1448
1449 sub extract_metadata {
1450         my $id = shift or return (undef, undef, undef);
1451         my ($url, $rev, $uuid) = ($id =~ /^\s*git-svn-id:\s+(.*)\@(\d+)
1452                                                         \s([a-f\d\-]+)$/ix);
1453         if (!defined $rev || !$uuid || !$url) {
1454                 # some of the original repositories I made had
1455                 # identifiers like this:
1456                 ($rev, $uuid) = ($id =~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i);
1457         }
1458         return ($url, $rev, $uuid);
1459 }
1460
1461 sub cmt_metadata {
1462         return extract_metadata((grep(/^git-svn-id: /,
1463                 command(qw/cat-file commit/, shift)))[-1]);
1464 }
1465
1466 sub cmt_sha2rev_batch {
1467         my %s2r;
1468         my ($pid, $in, $out, $ctx) = command_bidi_pipe(qw/cat-file --batch/);
1469         my $list = shift;
1470
1471         foreach my $sha (@{$list}) {
1472                 my $first = 1;
1473                 my $size = 0;
1474                 print $out $sha, "\n";
1475
1476                 while (my $line = <$in>) {
1477                         if ($first && $line =~ /^[[:xdigit:]]{40}\smissing$/) {
1478                                 last;
1479                         } elsif ($first &&
1480                                $line =~ /^[[:xdigit:]]{40}\scommit\s(\d+)$/) {
1481                                 $first = 0;
1482                                 $size = $1;
1483                                 next;
1484                         } elsif ($line =~ /^(git-svn-id: )/) {
1485                                 my (undef, $rev, undef) =
1486                                                       extract_metadata($line);
1487                                 $s2r{$sha} = $rev;
1488                         }
1489
1490                         $size -= length($line);
1491                         last if ($size == 0);
1492                 }
1493         }
1494
1495         command_close_bidi_pipe($pid, $in, $out, $ctx);
1496
1497         return \%s2r;
1498 }
1499
1500 sub working_head_info {
1501         my ($head, $refs) = @_;
1502         my @args = ('log', '--no-color', '--first-parent', '--pretty=medium');
1503         my ($fh, $ctx) = command_output_pipe(@args, $head);
1504         my $hash;
1505         my %max;
1506         while (<$fh>) {
1507                 if ( m{^commit ($::sha1)$} ) {
1508                         unshift @$refs, $hash if $hash and $refs;
1509                         $hash = $1;
1510                         next;
1511                 }
1512                 next unless s{^\s*(git-svn-id:)}{$1};
1513                 my ($url, $rev, $uuid) = extract_metadata($_);
1514                 if (defined $url && defined $rev) {
1515                         next if $max{$url} and $max{$url} < $rev;
1516                         if (my $gs = Git::SVN->find_by_url($url)) {
1517                                 my $c = $gs->rev_map_get($rev, $uuid);
1518                                 if ($c && $c eq $hash) {
1519                                         close $fh; # break the pipe
1520                                         return ($url, $rev, $uuid, $gs);
1521                                 } else {
1522                                         $max{$url} ||= $gs->rev_map_max;
1523                                 }
1524                         }
1525                 }
1526         }
1527         command_close_pipe($fh, $ctx);
1528         (undef, undef, undef, undef);
1529 }
1530
1531 sub read_commit_parents {
1532         my ($parents, $c) = @_;
1533         chomp(my $p = command_oneline(qw/rev-list --parents -1/, $c));
1534         $p =~ s/^($c)\s*// or die "rev-list --parents -1 $c failed!\n";
1535         @{$parents->{$c}} = split(/ /, $p);
1536 }
1537
1538 sub linearize_history {
1539         my ($gs, $refs) = @_;
1540         my %parents;
1541         foreach my $c (@$refs) {
1542                 read_commit_parents(\%parents, $c);
1543         }
1544
1545         my @linear_refs;
1546         my %skip = ();
1547         my $last_svn_commit = $gs->last_commit;
1548         foreach my $c (reverse @$refs) {
1549                 next if $c eq $last_svn_commit;
1550                 last if $skip{$c};
1551
1552                 unshift @linear_refs, $c;
1553                 $skip{$c} = 1;
1554
1555                 # we only want the first parent to diff against for linear
1556                 # history, we save the rest to inject when we finalize the
1557                 # svn commit
1558                 my $fp_a = verify_ref("$c~1");
1559                 my $fp_b = shift @{$parents{$c}} if $parents{$c};
1560                 if (!$fp_a || !$fp_b) {
1561                         die "Commit $c\n",
1562                             "has no parent commit, and therefore ",
1563                             "nothing to diff against.\n",
1564                             "You should be working from a repository ",
1565                             "originally created by git-svn\n";
1566                 }
1567                 if ($fp_a ne $fp_b) {
1568                         die "$c~1 = $fp_a, however parsing commit $c ",
1569                             "revealed that:\n$c~1 = $fp_b\nBUG!\n";
1570                 }
1571
1572                 foreach my $p (@{$parents{$c}}) {
1573                         $skip{$p} = 1;
1574                 }
1575         }
1576         (\@linear_refs, \%parents);
1577 }
1578
1579 sub find_file_type_and_diff_status {
1580         my ($path) = @_;
1581         return ('dir', '') if $path eq '';
1582
1583         my $diff_output =
1584             command_oneline(qw(diff --cached --name-status --), $path) || "";
1585         my $diff_status = (split(' ', $diff_output))[0] || "";
1586
1587         my $ls_tree = command_oneline(qw(ls-tree HEAD), $path) || "";
1588
1589         return (undef, undef) if !$diff_status && !$ls_tree;
1590
1591         if ($diff_status eq "A") {
1592                 return ("link", $diff_status) if -l $path;
1593                 return ("dir", $diff_status) if -d $path;
1594                 return ("file", $diff_status);
1595         }
1596
1597         my $mode = (split(' ', $ls_tree))[0] || "";
1598
1599         return ("link", $diff_status) if $mode eq "120000";
1600         return ("dir", $diff_status) if $mode eq "040000";
1601         return ("file", $diff_status);
1602 }
1603
1604 sub md5sum {
1605         my $arg = shift;
1606         my $ref = ref $arg;
1607         my $md5 = Digest::MD5->new();
1608         if ($ref eq 'GLOB' || $ref eq 'IO::File' || $ref eq 'File::Temp') {
1609                 $md5->addfile($arg) or croak $!;
1610         } elsif ($ref eq 'SCALAR') {
1611                 $md5->add($$arg) or croak $!;
1612         } elsif (!$ref) {
1613                 $md5->add($arg) or croak $!;
1614         } else {
1615                 ::fatal "Can't provide MD5 hash for unknown ref type: '", $ref, "'";
1616         }
1617         return $md5->hexdigest();
1618 }
1619
1620 sub gc_directory {
1621         if ($can_compress && -f $_ && basename($_) eq "unhandled.log") {
1622                 my $out_filename = $_ . ".gz";
1623                 open my $in_fh, "<", $_ or die "Unable to open $_: $!\n";
1624                 binmode $in_fh;
1625                 my $gz = Compress::Zlib::gzopen($out_filename, "ab") or
1626                                 die "Unable to open $out_filename: $!\n";
1627
1628                 my $res;
1629                 while ($res = sysread($in_fh, my $str, 1024)) {
1630                         $gz->gzwrite($str) or
1631                                 die "Unable to write: ".$gz->gzerror()."!\n";
1632                 }
1633                 unlink $_ or die "unlink $File::Find::name: $!\n";
1634         } elsif (-f $_ && basename($_) eq "index") {
1635                 unlink $_ or die "unlink $_: $!\n";
1636         }
1637 }
1638
1639 package Git::SVN;
1640 use strict;
1641 use warnings;
1642 use Fcntl qw/:DEFAULT :seek/;
1643 use constant rev_map_fmt => 'NH40';
1644 use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent
1645             $_repack $_repack_flags $_use_svm_props $_head
1646             $_use_svnsync_props $no_reuse_existing $_minimize_url
1647             $_use_log_author $_add_author_from $_localtime/;
1648 use Carp qw/croak/;
1649 use File::Path qw/mkpath/;
1650 use File::Copy qw/copy/;
1651 use IPC::Open3;
1652 use Memoize;  # core since 5.8.0, Jul 2002
1653
1654 my ($_gc_nr, $_gc_period);
1655
1656 # properties that we do not log:
1657 my %SKIP_PROP;
1658 BEGIN {
1659         %SKIP_PROP = map { $_ => 1 } qw/svn:wc:ra_dav:version-url
1660                                         svn:special svn:executable
1661                                         svn:entry:committed-rev
1662                                         svn:entry:last-author
1663                                         svn:entry:uuid
1664                                         svn:entry:committed-date/;
1665
1666         # some options are read globally, but can be overridden locally
1667         # per [svn-remote "..."] section.  Command-line options will *NOT*
1668         # override options set in an [svn-remote "..."] section
1669         no strict 'refs';
1670         for my $option (qw/follow_parent no_metadata use_svm_props
1671                            use_svnsync_props/) {
1672                 my $key = $option;
1673                 $key =~ tr/_//d;
1674                 my $prop = "-$option";
1675                 *$option = sub {
1676                         my ($self) = @_;
1677                         return $self->{$prop} if exists $self->{$prop};
1678                         my $k = "svn-remote.$self->{repo_id}.$key";
1679                         eval { command_oneline(qw/config --get/, $k) };
1680                         if ($@) {
1681                                 $self->{$prop} = ${"Git::SVN::_$option"};
1682                         } else {
1683                                 my $v = command_oneline(qw/config --bool/,$k);
1684                                 $self->{$prop} = $v eq 'false' ? 0 : 1;
1685                         }
1686                         return $self->{$prop};
1687                 }
1688         }
1689 }
1690
1691
1692 my (%LOCKFILES, %INDEX_FILES);
1693 END {
1694         unlink keys %LOCKFILES if %LOCKFILES;
1695         unlink keys %INDEX_FILES if %INDEX_FILES;
1696 }
1697
1698 sub resolve_local_globs {
1699         my ($url, $fetch, $glob_spec) = @_;
1700         return unless defined $glob_spec;
1701         my $ref = $glob_spec->{ref};
1702         my $path = $glob_spec->{path};
1703         foreach (command(qw#for-each-ref --format=%(refname) refs/#)) {
1704                 next unless m#^$ref->{regex}$#;
1705                 my $p = $1;
1706                 my $pathname = desanitize_refname($path->full_path($p));
1707                 my $refname = desanitize_refname($ref->full_path($p));
1708                 if (my $existing = $fetch->{$pathname}) {
1709                         if ($existing ne $refname) {
1710                                 die "Refspec conflict:\n",
1711                                     "existing: $existing\n",
1712                                     " globbed: $refname\n";
1713                         }
1714                         my $u = (::cmt_metadata("$refname"))[0];
1715                         $u =~ s!^\Q$url\E(/|$)!! or die
1716                           "$refname: '$url' not found in '$u'\n";
1717                         if ($pathname ne $u) {
1718                                 warn "W: Refspec glob conflict ",
1719                                      "(ref: $refname):\n",
1720                                      "expected path: $pathname\n",
1721                                      "    real path: $u\n",
1722                                      "Continuing ahead with $u\n";
1723                                 next;
1724                         }
1725                 } else {
1726                         $fetch->{$pathname} = $refname;
1727                 }
1728         }
1729 }
1730
1731 sub parse_revision_argument {
1732         my ($base, $head) = @_;
1733         if (!defined $::_revision || $::_revision eq 'BASE:HEAD') {
1734                 return ($base, $head);
1735         }
1736         return ($1, $2) if ($::_revision =~ /^(\d+):(\d+)$/);
1737         return ($::_revision, $::_revision) if ($::_revision =~ /^\d+$/);
1738         return ($head, $head) if ($::_revision eq 'HEAD');
1739         return ($base, $1) if ($::_revision =~ /^BASE:(\d+)$/);
1740         return ($1, $head) if ($::_revision =~ /^(\d+):HEAD$/);
1741         die "revision argument: $::_revision not understood by git-svn\n";
1742 }
1743
1744 sub fetch_all {
1745         my ($repo_id, $remotes) = @_;
1746         if (ref $repo_id) {
1747                 my $gs = $repo_id;
1748                 $repo_id = undef;
1749                 $repo_id = $gs->{repo_id};
1750         }
1751         $remotes ||= read_all_remotes();
1752         my $remote = $remotes->{$repo_id} or
1753                      die "[svn-remote \"$repo_id\"] unknown\n";
1754         my $fetch = $remote->{fetch};
1755         my $url = $remote->{url} or die "svn-remote.$repo_id.url not defined\n";
1756         my (@gs, @globs);
1757         my $ra = Git::SVN::Ra->new($url);
1758         my $uuid = $ra->get_uuid;
1759         my $head = $ra->get_latest_revnum;
1760
1761         # ignore errors, $head revision may not even exist anymore
1762         eval { $ra->get_log("", $head, 0, 1, 0, 1, sub { $head = $_[1] }) };
1763         warn "W: $@\n" if $@;
1764
1765         my $base = defined $fetch ? $head : 0;
1766
1767         # read the max revs for wildcard expansion (branches/*, tags/*)
1768         foreach my $t (qw/branches tags/) {
1769                 defined $remote->{$t} or next;
1770                 push @globs, @{$remote->{$t}};
1771
1772                 my $max_rev = eval { tmp_config(qw/--int --get/,
1773                                          "svn-remote.$repo_id.${t}-maxRev") };
1774                 if (defined $max_rev && ($max_rev < $base)) {
1775                         $base = $max_rev;
1776                 } elsif (!defined $max_rev) {
1777                         $base = 0;
1778                 }
1779         }
1780
1781         if ($fetch) {
1782                 foreach my $p (sort keys %$fetch) {
1783                         my $gs = Git::SVN->new($fetch->{$p}, $repo_id, $p);
1784                         my $lr = $gs->rev_map_max;
1785                         if (defined $lr) {
1786                                 $base = $lr if ($lr < $base);
1787                         }
1788                         push @gs, $gs;
1789                 }
1790         }
1791
1792         ($base, $head) = parse_revision_argument($base, $head);
1793         $ra->gs_fetch_loop_common($base, $head, \@gs, \@globs);
1794 }
1795
1796 sub read_all_remotes {
1797         my $r = {};
1798         my $use_svm_props = eval { command_oneline(qw/config --bool
1799             svn.useSvmProps/) };
1800         $use_svm_props = $use_svm_props eq 'true' if $use_svm_props;
1801         my $svn_refspec = qr{\s*(.*?)\s*:\s*(.+?)\s*};
1802         foreach (grep { s/^svn-remote\.// } command(qw/config -l/)) {
1803                 if (m!^(.+)\.fetch=$svn_refspec$!) {
1804                         my ($remote, $local_ref, $remote_ref) = ($1, $2, $3);
1805                         die("svn-remote.$remote: remote ref '$remote_ref' "
1806                             . "must start with 'refs/'\n")
1807                                 unless $remote_ref =~ m{^refs/};
1808                         $r->{$remote}->{fetch}->{$local_ref} = $remote_ref;
1809                         $r->{$remote}->{svm} = {} if $use_svm_props;
1810                 } elsif (m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {
1811                         $r->{$1}->{svm} = {};
1812                 } elsif (m!^(.+)\.url=\s*(.*)\s*$!) {
1813                         $r->{$1}->{url} = $2;
1814                 } elsif (m!^(.+)\.(branches|tags)=$svn_refspec$!) {
1815                         my ($remote, $t, $local_ref, $remote_ref) =
1816                                                              ($1, $2, $3, $4);
1817                         die("svn-remote.$remote: remote ref '$remote_ref' ($t) "
1818                             . "must start with 'refs/'\n")
1819                                 unless $remote_ref =~ m{^refs/};
1820                         my $rs = {
1821                             t => $t,
1822                             remote => $remote,
1823                             path => Git::SVN::GlobSpec->new($local_ref),
1824                             ref => Git::SVN::GlobSpec->new($remote_ref) };
1825                         if (length($rs->{ref}->{right}) != 0) {
1826                                 die "The '*' glob character must be the last ",
1827                                     "character of '$remote_ref'\n";
1828                         }
1829                         push @{ $r->{$remote}->{$t} }, $rs;
1830                 }
1831         }
1832
1833         map {
1834                 if (defined $r->{$_}->{svm}) {
1835                         my $svm;
1836                         eval {
1837                                 my $section = "svn-remote.$_";
1838                                 $svm = {
1839                                         source => tmp_config('--get',
1840                                             "$section.svm-source"),
1841                                         replace => tmp_config('--get',
1842                                             "$section.svm-replace"),
1843                                 }
1844                         };
1845                         $r->{$_}->{svm} = $svm;
1846                 }
1847         } keys %$r;
1848
1849         $r;
1850 }
1851
1852 sub init_vars {
1853         $_gc_nr = $_gc_period = 1000;
1854         if (defined $_repack || defined $_repack_flags) {
1855                warn "Repack options are obsolete; they have no effect.\n";
1856         }
1857 }
1858
1859 sub verify_remotes_sanity {
1860         return unless -d $ENV{GIT_DIR};
1861         my %seen;
1862         foreach (command(qw/config -l/)) {
1863                 if (m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {
1864                         if ($seen{$1}) {
1865                                 die "Remote ref refs/remote/$1 is tracked by",
1866                                     "\n  \"$_\"\nand\n  \"$seen{$1}\"\n",
1867                                     "Please resolve this ambiguity in ",
1868                                     "your git configuration file before ",
1869                                     "continuing\n";
1870                         }
1871                         $seen{$1} = $_;
1872                 }
1873         }
1874 }
1875
1876 sub find_existing_remote {
1877         my ($url, $remotes) = @_;
1878         return undef if $no_reuse_existing;
1879         my $existing;
1880         foreach my $repo_id (keys %$remotes) {
1881                 my $u = $remotes->{$repo_id}->{url} or next;
1882                 next if $u ne $url;
1883                 $existing = $repo_id;
1884                 last;
1885         }
1886         $existing;
1887 }
1888
1889 sub init_remote_config {
1890         my ($self, $url, $no_write) = @_;
1891         $url =~ s!/+$!!; # strip trailing slash
1892         my $r = read_all_remotes();
1893         my $existing = find_existing_remote($url, $r);
1894         if ($existing) {
1895                 unless ($no_write) {
1896                         print STDERR "Using existing ",
1897                                      "[svn-remote \"$existing\"]\n";
1898                 }
1899                 $self->{repo_id} = $existing;
1900         } elsif ($_minimize_url) {
1901                 my $min_url = Git::SVN::Ra->new($url)->minimize_url;
1902                 $existing = find_existing_remote($min_url, $r);
1903                 if ($existing) {
1904                         unless ($no_write) {
1905                                 print STDERR "Using existing ",
1906                                              "[svn-remote \"$existing\"]\n";
1907                         }
1908                         $self->{repo_id} = $existing;
1909                 }
1910                 if ($min_url ne $url) {
1911                         unless ($no_write) {
1912                                 print STDERR "Using higher level of URL: ",
1913                                              "$url => $min_url\n";
1914                         }
1915                         my $old_path = $self->{path};
1916                         $self->{path} = $url;
1917                         $self->{path} =~ s!^\Q$min_url\E(/|$)!!;
1918                         if (length $old_path) {
1919                                 $self->{path} .= "/$old_path";
1920                         }
1921                         $url = $min_url;
1922                 }
1923         }
1924         my $orig_url;
1925         if (!$existing) {
1926                 # verify that we aren't overwriting anything:
1927                 $orig_url = eval {
1928                         command_oneline('config', '--get',
1929                                         "svn-remote.$self->{repo_id}.url")
1930                 };
1931                 if ($orig_url && ($orig_url ne $url)) {
1932                         die "svn-remote.$self->{repo_id}.url already set: ",
1933                             "$orig_url\nwanted to set to: $url\n";
1934                 }
1935         }
1936         my ($xrepo_id, $xpath) = find_ref($self->refname);
1937         if (!$no_write && defined $xpath) {
1938                 die "svn-remote.$xrepo_id.fetch already set to track ",
1939                     "$xpath:", $self->refname, "\n";
1940         }
1941         unless ($no_write) {
1942                 command_noisy('config',
1943                               "svn-remote.$self->{repo_id}.url", $url);
1944                 $self->{path} =~ s{^/}{};
1945                 $self->{path} =~ s{%([0-9A-F]{2})}{chr hex($1)}ieg;
1946                 command_noisy('config', '--add',
1947                               "svn-remote.$self->{repo_id}.fetch",
1948                               "$self->{path}:".$self->refname);
1949         }
1950         $self->{url} = $url;
1951 }
1952
1953 sub find_by_url { # repos_root and, path are optional
1954         my ($class, $full_url, $repos_root, $path) = @_;
1955
1956         return undef unless defined $full_url;
1957         remove_username($full_url);
1958         remove_username($repos_root) if defined $repos_root;
1959         my $remotes = read_all_remotes();
1960         if (defined $full_url && defined $repos_root && !defined $path) {
1961                 $path = $full_url;
1962                 $path =~ s#^\Q$repos_root\E(?:/|$)##;
1963         }
1964         foreach my $repo_id (keys %$remotes) {
1965                 my $u = $remotes->{$repo_id}->{url} or next;
1966                 remove_username($u);
1967                 next if defined $repos_root && $repos_root ne $u;
1968
1969                 my $fetch = $remotes->{$repo_id}->{fetch} || {};
1970                 foreach my $t (qw/branches tags/) {
1971                         foreach my $globspec (@{$remotes->{$repo_id}->{$t}}) {
1972                                 resolve_local_globs($u, $fetch, $globspec);
1973                         }
1974                 }
1975                 my $p = $path;
1976                 my $rwr = rewrite_root({repo_id => $repo_id});
1977                 my $svm = $remotes->{$repo_id}->{svm}
1978                         if defined $remotes->{$repo_id}->{svm};
1979                 unless (defined $p) {
1980                         $p = $full_url;
1981                         my $z = $u;
1982                         my $prefix = '';
1983                         if ($rwr) {
1984                                 $z = $rwr;
1985                                 remove_username($z);
1986                         } elsif (defined $svm) {
1987                                 $z = $svm->{source};
1988                                 $prefix = $svm->{replace};
1989                                 $prefix =~ s#^\Q$u\E(?:/|$)##;
1990                                 $prefix =~ s#/$##;
1991                         }
1992                         $p =~ s#^\Q$z\E(?:/|$)#$prefix# or next;
1993                 }
1994                 foreach my $f (keys %$fetch) {
1995                         next if $f ne $p;
1996                         return Git::SVN->new($fetch->{$f}, $repo_id, $f);
1997                 }
1998         }
1999         undef;
2000 }
2001
2002 sub init {
2003         my ($class, $url, $path, $repo_id, $ref_id, $no_write) = @_;
2004         my $self = _new($class, $repo_id, $ref_id, $path);
2005         if (defined $url) {
2006                 $self->init_remote_config($url, $no_write);
2007         }
2008         $self;
2009 }
2010
2011 sub find_ref {
2012         my ($ref_id) = @_;
2013         foreach (command(qw/config -l/)) {
2014                 next unless m!^svn-remote\.(.+)\.fetch=
2015                               \s*(.*?)\s*:\s*(.+?)\s*$!x;
2016                 my ($repo_id, $path, $ref) = ($1, $2, $3);
2017                 if ($ref eq $ref_id) {
2018                         $path = '' if ($path =~ m#^\./?#);
2019                         return ($repo_id, $path);
2020                 }
2021         }
2022         (undef, undef, undef);
2023 }
2024
2025 sub new {
2026         my ($class, $ref_id, $repo_id, $path) = @_;
2027         if (defined $ref_id && !defined $repo_id && !defined $path) {
2028                 ($repo_id, $path) = find_ref($ref_id);
2029                 if (!defined $repo_id) {
2030                         die "Could not find a \"svn-remote.*.fetch\" key ",
2031                             "in the repository configuration matching: ",
2032                             "$ref_id\n";
2033                 }
2034         }
2035         my $self = _new($class, $repo_id, $ref_id, $path);
2036         if (!defined $self->{path} || !length $self->{path}) {
2037                 my $fetch = command_oneline('config', '--get',
2038                                             "svn-remote.$repo_id.fetch",
2039                                             ":$ref_id\$") or
2040                      die "Failed to read \"svn-remote.$repo_id.fetch\" ",
2041                          "\":$ref_id\$\" in config\n";
2042                 ($self->{path}, undef) = split(/\s*:\s*/, $fetch);
2043         }
2044         $self->{url} = command_oneline('config', '--get',
2045                                        "svn-remote.$repo_id.url") or
2046                   die "Failed to read \"svn-remote.$repo_id.url\" in config\n";
2047         $self->rebuild;
2048         $self;
2049 }
2050
2051 sub refname {
2052         my ($refname) = $_[0]->{ref_id} ;
2053
2054         # It cannot end with a slash /, we'll throw up on this because
2055         # SVN can't have directories with a slash in their name, either:
2056         if ($refname =~ m{/$}) {
2057                 die "ref: '$refname' ends with a trailing slash, this is ",
2058                     "not permitted by git nor Subversion\n";
2059         }
2060
2061         # It cannot have ASCII control character space, tilde ~, caret ^,
2062         # colon :, question-mark ?, asterisk *, space, or open bracket [
2063         # anywhere.
2064         #
2065         # Additionally, % must be escaped because it is used for escaping
2066         # and we want our escaped refname to be reversible
2067         $refname =~ s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;
2068
2069         # no slash-separated component can begin with a dot .
2070         # /.* becomes /%2E*
2071         $refname =~ s{/\.}{/%2E}g;
2072
2073         # It cannot have two consecutive dots .. anywhere
2074         # .. becomes %2E%2E
2075         $refname =~ s{\.\.}{%2E%2E}g;
2076
2077         return $refname;
2078 }
2079
2080 sub desanitize_refname {
2081         my ($refname) = @_;
2082         $refname =~ s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;
2083         return $refname;
2084 }
2085
2086 sub svm_uuid {
2087         my ($self) = @_;
2088         return $self->{svm}->{uuid} if $self->svm;
2089         $self->ra;
2090         unless ($self->{svm}) {
2091                 die "SVM UUID not cached, and reading remotely failed\n";
2092         }
2093         $self->{svm}->{uuid};
2094 }
2095
2096 sub svm {
2097         my ($self) = @_;
2098         return $self->{svm} if $self->{svm};
2099         my $svm;
2100         # see if we have it in our config, first:
2101         eval {
2102                 my $section = "svn-remote.$self->{repo_id}";
2103                 $svm = {
2104                   source => tmp_config('--get', "$section.svm-source"),
2105                   uuid => tmp_config('--get', "$section.svm-uuid"),
2106                   replace => tmp_config('--get', "$section.svm-replace"),
2107                 }
2108         };
2109         if ($svm && $svm->{source} && $svm->{uuid} && $svm->{replace}) {
2110                 $self->{svm} = $svm;
2111         }
2112         $self->{svm};
2113 }
2114
2115 sub _set_svm_vars {
2116         my ($self, $ra) = @_;
2117         return $ra if $self->svm;
2118
2119         my @err = ( "useSvmProps set, but failed to read SVM properties\n",
2120                     "(svm:source, svm:uuid) ",
2121                     "from the following URLs:\n" );
2122         sub read_svm_props {
2123                 my ($self, $ra, $path, $r) = @_;
2124                 my $props = ($ra->get_dir($path, $r))[2];
2125                 my $src = $props->{'svm:source'};
2126                 my $uuid = $props->{'svm:uuid'};
2127                 return undef if (!$src || !$uuid);
2128
2129                 chomp($src, $uuid);
2130
2131                 $uuid =~ m{^[0-9a-f\-]{30,}$}i
2132                     or die "doesn't look right - svm:uuid is '$uuid'\n";
2133
2134                 # the '!' is used to mark the repos_root!/relative/path
2135                 $src =~ s{/?!/?}{/};
2136                 $src =~ s{/+$}{}; # no trailing slashes please
2137                 # username is of no interest
2138                 $src =~ s{(^[a-z\+]*://)[^/@]*@}{$1};
2139
2140                 my $replace = $ra->{url};
2141                 $replace .= "/$path" if length $path;
2142
2143                 my $section = "svn-remote.$self->{repo_id}";
2144                 tmp_config("$section.svm-source", $src);
2145                 tmp_config("$section.svm-replace", $replace);
2146                 tmp_config("$section.svm-uuid", $uuid);
2147                 $self->{svm} = {
2148                         source => $src,
2149                         uuid => $uuid,
2150                         replace => $replace
2151                 };
2152         }
2153
2154         my $r = $ra->get_latest_revnum;
2155         my $path = $self->{path};
2156         my %tried;
2157         while (length $path) {
2158                 unless ($tried{"$self->{url}/$path"}) {
2159                         return $ra if $self->read_svm_props($ra, $path, $r);
2160                         $tried{"$self->{url}/$path"} = 1;
2161                 }
2162                 $path =~ s#/?[^/]+$##;
2163         }
2164         die "Path: '$path' should be ''\n" if $path ne '';
2165         return $ra if $self->read_svm_props($ra, $path, $r);
2166         $tried{"$self->{url}/$path"} = 1;
2167
2168         if ($ra->{repos_root} eq $self->{url}) {
2169                 die @err, (map { "  $_\n" } keys %tried), "\n";
2170         }
2171
2172         # nope, make sure we're connected to the repository root:
2173         my $ok;
2174         my @tried_b;
2175         $path = $ra->{svn_path};
2176         $ra = Git::SVN::Ra->new($ra->{repos_root});
2177         while (length $path) {
2178                 unless ($tried{"$ra->{url}/$path"}) {
2179                         $ok = $self->read_svm_props($ra, $path, $r);
2180                         last if $ok;
2181                         $tried{"$ra->{url}/$path"} = 1;
2182                 }
2183                 $path =~ s#/?[^/]+$##;
2184         }
2185         die "Path: '$path' should be ''\n" if $path ne '';
2186         $ok ||= $self->read_svm_props($ra, $path, $r);
2187         $tried{"$ra->{url}/$path"} = 1;
2188         if (!$ok) {
2189                 die @err, (map { "  $_\n" } keys %tried), "\n";
2190         }
2191         Git::SVN::Ra->new($self->{url});
2192 }
2193
2194 sub svnsync {
2195         my ($self) = @_;
2196         return $self->{svnsync} if $self->{svnsync};
2197
2198         if ($self->no_metadata) {
2199                 die "Can't have both 'noMetadata' and ",
2200                     "'useSvnsyncProps' options set!\n";
2201         }
2202         if ($self->rewrite_root) {
2203                 die "Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",
2204                     "options set!\n";
2205         }
2206
2207         my $svnsync;
2208         # see if we have it in our config, first:
2209         eval {
2210                 my $section = "svn-remote.$self->{repo_id}";
2211
2212                 my $url = tmp_config('--get', "$section.svnsync-url");
2213                 ($url) = ($url =~ m{^([a-z\+]+://\S+)$}) or
2214                    die "doesn't look right - svn:sync-from-url is '$url'\n";
2215
2216                 my $uuid = tmp_config('--get', "$section.svnsync-uuid");
2217                 ($uuid) = ($uuid =~ m{^([0-9a-f\-]{30,})$}i) or
2218                    die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
2219
2220                 $svnsync = { url => $url, uuid => $uuid }
2221         };
2222         if ($svnsync && $svnsync->{url} && $svnsync->{uuid}) {
2223                 return $self->{svnsync} = $svnsync;
2224         }
2225
2226         my $err = "useSvnsyncProps set, but failed to read " .
2227                   "svnsync property: svn:sync-from-";
2228         my $rp = $self->ra->rev_proplist(0);
2229
2230         my $url = $rp->{'svn:sync-from-url'} or die $err . "url\n";
2231         ($url) = ($url =~ m{^([a-z\+]+://\S+)$}) or
2232                    die "doesn't look right - svn:sync-from-url is '$url'\n";
2233
2234         my $uuid = $rp->{'svn:sync-from-uuid'} or die $err . "uuid\n";
2235         ($uuid) = ($uuid =~ m{^([0-9a-f\-]{30,})$}i) or
2236                    die "doesn't look right - svn:sync-from-uuid is '$uuid'\n";
2237
2238         my $section = "svn-remote.$self->{repo_id}";
2239         tmp_config('--add', "$section.svnsync-uuid", $uuid);
2240         tmp_config('--add', "$section.svnsync-url", $url);
2241         return $self->{svnsync} = { url => $url, uuid => $uuid };
2242 }
2243
2244 # this allows us to memoize our SVN::Ra UUID locally and avoid a
2245 # remote lookup (useful for 'git svn log').
2246 sub ra_uuid {
2247         my ($self) = @_;
2248         unless ($self->{ra_uuid}) {
2249                 my $key = "svn-remote.$self->{repo_id}.uuid";
2250                 my $uuid = eval { tmp_config('--get', $key) };
2251                 if (!$@ && $uuid && $uuid =~ /^([a-f\d\-]{30,})$/i) {
2252                         $self->{ra_uuid} = $uuid;
2253                 } else {
2254                         die "ra_uuid called without URL\n" unless $self->{url};
2255                         $self->{ra_uuid} = $self->ra->get_uuid;
2256                         tmp_config('--add', $key, $self->{ra_uuid});
2257                 }
2258         }
2259         $self->{ra_uuid};
2260 }
2261
2262 sub _set_repos_root {
2263         my ($self, $repos_root) = @_;
2264         my $k = "svn-remote.$self->{repo_id}.reposRoot";
2265         $repos_root ||= $self->ra->{repos_root};
2266         tmp_config($k, $repos_root);
2267         $repos_root;
2268 }
2269
2270 sub repos_root {
2271         my ($self) = @_;
2272         my $k = "svn-remote.$self->{repo_id}.reposRoot";
2273         eval { tmp_config('--get', $k) } || $self->_set_repos_root;
2274 }
2275
2276 sub ra {
2277         my ($self) = shift;
2278         my $ra = Git::SVN::Ra->new($self->{url});
2279         $self->_set_repos_root($ra->{repos_root});
2280         if ($self->use_svm_props && !$self->{svm}) {
2281                 if ($self->no_metadata) {
2282                         die "Can't have both 'noMetadata' and ",
2283                             "'useSvmProps' options set!\n";
2284                 } elsif ($self->use_svnsync_props) {
2285                         die "Can't have both 'useSvnsyncProps' and ",
2286                             "'useSvmProps' options set!\n";
2287                 }
2288                 $ra = $self->_set_svm_vars($ra);
2289                 $self->{-want_revprops} = 1;
2290         }
2291         $ra;
2292 }
2293
2294 # prop_walk(PATH, REV, SUB)
2295 # -------------------------
2296 # Recursively traverse PATH at revision REV and invoke SUB for each
2297 # directory that contains a SVN property.  SUB will be invoked as
2298 # follows:  &SUB(gs, path, props);  where `gs' is this instance of
2299 # Git::SVN, `path' the path to the directory where the properties
2300 # `props' were found.  The `path' will be relative to point of checkout,
2301 # that is, if url://repo/trunk is the current Git branch, and that
2302 # directory contains a sub-directory `d', SUB will be invoked with `/d/'
2303 # as `path' (note the trailing `/').
2304 sub prop_walk {
2305         my ($self, $path, $rev, $sub) = @_;
2306
2307         $path =~ s#^/##;
2308         my ($dirent, undef, $props) = $self->ra->get_dir($path, $rev);
2309         $path =~ s#^/*#/#g;
2310         my $p = $path;
2311         # Strip the irrelevant part of the path.
2312         $p =~ s#^/+\Q$self->{path}\E(/|$)#/#;
2313         # Ensure the path is terminated by a `/'.
2314         $p =~ s#/*$#/#;
2315
2316         # The properties contain all the internal SVN stuff nobody
2317         # (usually) cares about.
2318         my $interesting_props = 0;
2319         foreach (keys %{$props}) {
2320                 # If it doesn't start with `svn:', it must be a
2321                 # user-defined property.
2322                 ++$interesting_props and next if $_ !~ /^svn:/;
2323                 # FIXME: Fragile, if SVN adds new public properties,
2324                 # this needs to be updated.
2325                 ++$interesting_props if /^svn:(?:ignore|keywords|executable
2326                                                  |eol-style|mime-type
2327                                                  |externals|needs-lock)$/x;
2328         }
2329         &$sub($self, $p, $props) if $interesting_props;
2330
2331         foreach (sort keys %$dirent) {
2332                 next if $dirent->{$_}->{kind} != $SVN::Node::dir;
2333                 $self->prop_walk($self->{path} . $p . $_, $rev, $sub);
2334         }
2335 }
2336
2337 sub last_rev { ($_[0]->last_rev_commit)[0] }
2338 sub last_commit { ($_[0]->last_rev_commit)[1] }
2339
2340 # returns the newest SVN revision number and newest commit SHA1
2341 sub last_rev_commit {
2342         my ($self) = @_;
2343         if (defined $self->{last_rev} && defined $self->{last_commit}) {
2344                 return ($self->{last_rev}, $self->{last_commit});
2345         }
2346         my $c = ::verify_ref($self->refname.'^0');
2347         if ($c && !$self->use_svm_props && !$self->no_metadata) {
2348                 my $rev = (::cmt_metadata($c))[1];
2349                 if (defined $rev) {
2350                         ($self->{last_rev}, $self->{last_commit}) = ($rev, $c);
2351                         return ($rev, $c);
2352                 }
2353         }
2354         my $map_path = $self->map_path;
2355         unless (-e $map_path) {
2356                 ($self->{last_rev}, $self->{last_commit}) = (undef, undef);
2357                 return (undef, undef);
2358         }
2359         my ($rev, $commit) = $self->rev_map_max(1);
2360         ($self->{last_rev}, $self->{last_commit}) = ($rev, $commit);
2361         return ($rev, $commit);
2362 }
2363
2364 sub get_fetch_range {
2365         my ($self, $min, $max) = @_;
2366         $max ||= $self->ra->get_latest_revnum;
2367         $min ||= $self->rev_map_max;
2368         (++$min, $max);
2369 }
2370
2371 sub tmp_config {
2372         my (@args) = @_;
2373         my $old_def_config = "$ENV{GIT_DIR}/svn/config";
2374         my $config = "$ENV{GIT_DIR}/svn/.metadata";
2375         if (! -f $config && -f $old_def_config) {
2376                 rename $old_def_config, $config or
2377                        die "Failed rename $old_def_config => $config: $!\n";
2378         }
2379         my $old_config = $ENV{GIT_CONFIG};
2380         $ENV{GIT_CONFIG} = $config;
2381         $@ = undef;
2382         my @ret = eval {
2383                 unless (-f $config) {
2384                         mkfile($config);
2385                         open my $fh, '>', $config or
2386                             die "Can't open $config: $!\n";
2387                         print $fh "; This file is used internally by ",
2388                                   "git-svn\n" or die
2389                                   "Couldn't write to $config: $!\n";
2390                         print $fh "; You should not have to edit it\n" or
2391                               die "Couldn't write to $config: $!\n";
2392                         close $fh or die "Couldn't close $config: $!\n";
2393                 }
2394                 command('config', @args);
2395         };
2396         my $err = $@;
2397         if (defined $old_config) {
2398                 $ENV{GIT_CONFIG} = $old_config;
2399         } else {
2400                 delete $ENV{GIT_CONFIG};
2401         }
2402         die $err if $err;
2403         wantarray ? @ret : $ret[0];
2404 }
2405
2406 sub tmp_index_do {
2407         my ($self, $sub) = @_;
2408         my $old_index = $ENV{GIT_INDEX_FILE};
2409         $ENV{GIT_INDEX_FILE} = $self->{index};
2410         $@ = undef;
2411         my @ret = eval {
2412                 my ($dir, $base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);
2413                 mkpath([$dir]) unless -d $dir;
2414                 &$sub;
2415         };
2416         my $err = $@;
2417         if (defined $old_index) {
2418                 $ENV{GIT_INDEX_FILE} = $old_index;
2419         } else {
2420                 delete $ENV{GIT_INDEX_FILE};
2421         }
2422         die $err if $err;
2423         wantarray ? @ret : $ret[0];
2424 }
2425
2426 sub assert_index_clean {
2427         my ($self, $treeish) = @_;
2428
2429         $self->tmp_index_do(sub {
2430                 command_noisy('read-tree', $treeish) unless -e $self->{index};
2431                 my $x = command_oneline('write-tree');
2432                 my ($y) = (command(qw/cat-file commit/, $treeish) =~
2433                            /^tree ($::sha1)/mo);
2434                 return if $y eq $x;
2435
2436                 warn "Index mismatch: $y != $x\nrereading $treeish\n";
2437                 unlink $self->{index} or die "unlink $self->{index}: $!\n";
2438                 command_noisy('read-tree', $treeish);
2439                 $x = command_oneline('write-tree');
2440                 if ($y ne $x) {
2441                         ::fatal "trees ($treeish) $y != $x\n",
2442                                 "Something is seriously wrong...";
2443                 }
2444         });
2445 }
2446
2447 sub get_commit_parents {
2448         my ($self, $log_entry) = @_;
2449         my (%seen, @ret, @tmp);
2450         # legacy support for 'set-tree'; this is only used by set_tree_cb:
2451         if (my $ip = $self->{inject_parents}) {
2452                 if (my $commit = delete $ip->{$log_entry->{revision}}) {
2453                         push @tmp, $commit;
2454                 }
2455         }
2456         if (my $cur = ::verify_ref($self->refname.'^0')) {
2457                 push @tmp, $cur;
2458         }
2459         if (my $ipd = $self->{inject_parents_dcommit}) {
2460                 if (my $commit = delete $ipd->{$log_entry->{revision}}) {
2461                         push @tmp, @$commit;
2462                 }
2463         }
2464         push @tmp, $_ foreach (@{$log_entry->{parents}}, @tmp);
2465         while (my $p = shift @tmp) {
2466                 next if $seen{$p};
2467                 $seen{$p} = 1;
2468                 push @ret, $p;
2469         }
2470         @ret;
2471 }
2472
2473 sub rewrite_root {
2474         my ($self) = @_;
2475         return $self->{-rewrite_root} if exists $self->{-rewrite_root};
2476         my $k = "svn-remote.$self->{repo_id}.rewriteRoot";
2477         my $rwr = eval { command_oneline(qw/config --get/, $k) };
2478         if ($rwr) {
2479                 $rwr =~ s#/+$##;
2480                 if ($rwr !~ m#^[a-z\+]+://#) {
2481                         die "$rwr is not a valid URL (key: $k)\n";
2482                 }
2483         }
2484         $self->{-rewrite_root} = $rwr;
2485 }
2486
2487 sub metadata_url {
2488         my ($self) = @_;
2489         ($self->rewrite_root || $self->{url}) .
2490            (length $self->{path} ? '/' . $self->{path} : '');
2491 }
2492
2493 sub full_url {
2494         my ($self) = @_;
2495         $self->{url} . (length $self->{path} ? '/' . $self->{path} : '');
2496 }
2497
2498
2499 sub set_commit_header_env {
2500         my ($log_entry) = @_;
2501         my %env;
2502         foreach my $ned (qw/NAME EMAIL DATE/) {
2503                 foreach my $ac (qw/AUTHOR COMMITTER/) {
2504                         $env{"GIT_${ac}_${ned}"} = $ENV{"GIT_${ac}_${ned}"};
2505                 }
2506         }
2507
2508         $ENV{GIT_AUTHOR_NAME} = $log_entry->{name};
2509         $ENV{GIT_AUTHOR_EMAIL} = $log_entry->{email};
2510         $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_entry->{date};
2511
2512         $ENV{GIT_COMMITTER_NAME} = (defined $log_entry->{commit_name})
2513                                                 ? $log_entry->{commit_name}
2514                                                 : $log_entry->{name};
2515         $ENV{GIT_COMMITTER_EMAIL} = (defined $log_entry->{commit_email})
2516                                                 ? $log_entry->{commit_email}
2517                                                 : $log_entry->{email};
2518         \%env;
2519 }
2520
2521 sub restore_commit_header_env {
2522         my ($env) = @_;
2523         foreach my $ned (qw/NAME EMAIL DATE/) {
2524                 foreach my $ac (qw/AUTHOR COMMITTER/) {
2525                         my $k = "GIT_${ac}_${ned}";
2526                         if (defined $env->{$k}) {
2527                                 $ENV{$k} = $env->{$k};
2528                         } else {
2529                                 delete $ENV{$k};
2530                         }
2531                 }
2532         }
2533 }
2534
2535 sub gc {
2536         command_noisy('gc', '--auto');
2537 };
2538
2539 sub do_git_commit {
2540         my ($self, $log_entry) = @_;
2541         my $lr = $self->last_rev;
2542         if (defined $lr && $lr >= $log_entry->{revision}) {
2543                 die "Last fetched revision of ", $self->refname,
2544                     " was r$lr, but we are about to fetch: ",
2545                     "r$log_entry->{revision}!\n";
2546         }
2547         if (my $c = $self->rev_map_get($log_entry->{revision})) {
2548                 croak "$log_entry->{revision} = $c already exists! ",
2549                       "Why are we refetching it?\n";
2550         }
2551         my $old_env = set_commit_header_env($log_entry);
2552         my $tree = $log_entry->{tree};
2553         if (!defined $tree) {
2554                 $tree = $self->tmp_index_do(sub {
2555                                             command_oneline('write-tree') });
2556         }
2557         die "Tree is not a valid sha1: $tree\n" if $tree !~ /^$::sha1$/o;
2558
2559         my @exec = ('git', 'commit-tree', $tree);
2560         foreach ($self->get_commit_parents($log_entry)) {
2561                 push @exec, '-p', $_;
2562         }
2563         defined(my $pid = open3(my $msg_fh, my $out_fh, '>&STDERR', @exec))
2564                                                                    or croak $!;
2565         binmode $msg_fh;
2566
2567         # we always get UTF-8 from SVN, but we may want our commits in
2568         # a different encoding.
2569         if (my $enc = Git::config('i18n.commitencoding')) {
2570                 require Encode;
2571                 Encode::from_to($log_entry->{log}, 'UTF-8', $enc);
2572         }
2573         print $msg_fh $log_entry->{log} or croak $!;
2574         restore_commit_header_env($old_env);
2575         unless ($self->no_metadata) {
2576                 print $msg_fh "\ngit-svn-id: $log_entry->{metadata}\n"
2577                               or croak $!;
2578         }
2579         $msg_fh->flush == 0 or croak $!;
2580         close $msg_fh or croak $!;
2581         chomp(my $commit = do { local $/; <$out_fh> });
2582         close $out_fh or croak $!;
2583         waitpid $pid, 0;
2584         croak $? if $?;
2585         if ($commit !~ /^$::sha1$/o) {
2586                 die "Failed to commit, invalid sha1: $commit\n";
2587         }
2588
2589         $self->rev_map_set($log_entry->{revision}, $commit, 1);
2590
2591         $self->{last_rev} = $log_entry->{revision};
2592         $self->{last_commit} = $commit;
2593         print "r$log_entry->{revision}" unless $::_q > 1;
2594         if (defined $log_entry->{svm_revision}) {
2595                  print " (\@$log_entry->{svm_revision})" unless $::_q > 1;
2596                  $self->rev_map_set($log_entry->{svm_revision}, $commit,
2597                                    0, $self->svm_uuid);
2598         }
2599         print " = $commit ($self->{ref_id})\n" unless $::_q > 1;
2600         if (--$_gc_nr == 0) {
2601                 $_gc_nr = $_gc_period;
2602                 gc();
2603         }
2604         return $commit;
2605 }
2606
2607 sub match_paths {
2608         my ($self, $paths, $r) = @_;
2609         return 1 if $self->{path} eq '';
2610         if (my $path = $paths->{"/$self->{path}"}) {
2611                 return ($path->{action} eq 'D') ? 0 : 1;
2612         }
2613         $self->{path_regex} ||= qr/^\/\Q$self->{path}\E\//;
2614         if (grep /$self->{path_regex}/, keys %$paths) {
2615                 return 1;
2616         }
2617         my $c = '';
2618         foreach (split m#/#, $self->{path}) {
2619                 $c .= "/$_";
2620                 next unless ($paths->{$c} &&
2621                              ($paths->{$c}->{action} =~ /^[AR]$/));
2622                 if ($self->ra->check_path($self->{path}, $r) ==
2623                     $SVN::Node::dir) {
2624                         return 1;
2625                 }
2626         }
2627         return 0;
2628 }
2629
2630 sub find_parent_branch {
2631         my ($self, $paths, $rev) = @_;
2632         return undef unless $self->follow_parent;
2633         unless (defined $paths) {
2634                 my $err_handler = $SVN::Error::handler;
2635                 $SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;
2636                 $self->ra->get_log([$self->{path}], $rev, $rev, 0, 1, 1,
2637                                    sub { $paths = $_[0] });
2638                 $SVN::Error::handler = $err_handler;
2639         }
2640         return undef unless defined $paths;
2641
2642         # look for a parent from another branch:
2643         my @b_path_components = split m#/#, $self->{path};
2644         my @a_path_components;
2645         my $i;
2646         while (@b_path_components) {
2647                 $i = $paths->{'/'.join('/', @b_path_components)};
2648                 last if $i && defined $i->{copyfrom_path};
2649                 unshift(@a_path_components, pop(@b_path_components));
2650         }
2651         return undef unless defined $i && defined $i->{copyfrom_path};
2652         my $branch_from = $i->{copyfrom_path};
2653         if (@a_path_components) {
2654                 print STDERR "branch_from: $branch_from => ";
2655                 $branch_from .= '/'.join('/', @a_path_components);
2656                 print STDERR $branch_from, "\n";
2657         }
2658         my $r = $i->{copyfrom_rev};
2659         my $repos_root = $self->ra->{repos_root};
2660         my $url = $self->ra->{url};
2661         my $new_url = $url . $branch_from;
2662         print STDERR  "Found possible branch point: ",
2663                       "$new_url => ", $self->full_url, ", $r\n"
2664                       unless $::_q > 1;
2665         $branch_from =~ s#^/##;
2666         my $gs = $self->other_gs($new_url, $url,
2667                                  $branch_from, $r, $self->{ref_id});
2668         my ($r0, $parent) = $gs->find_rev_before($r, 1);
2669         {
2670                 my ($base, $head);
2671                 if (!defined $r0 || !defined $parent) {
2672                         ($base, $head) = parse_revision_argument(0, $r);
2673                 } else {
2674                         if ($r0 < $r) {
2675                                 $gs->ra->get_log([$gs->{path}], $r0 + 1, $r, 1,
2676                                         0, 1, sub { $base = $_[1] - 1 });
2677                         }
2678                 }
2679                 if (defined $base && $base <= $r) {
2680                         $gs->fetch($base, $r);
2681                 }
2682                 ($r0, $parent) = $gs->find_rev_before($r, 1);
2683         }
2684         if (defined $r0 && defined $parent) {
2685                 print STDERR "Found branch parent: ($self->{ref_id}) $parent\n"
2686                              unless $::_q > 1;
2687                 my $ed;
2688                 if ($self->ra->can_do_switch) {
2689                         $self->assert_index_clean($parent);
2690                         print STDERR "Following parent with do_switch\n"
2691                                      unless $::_q > 1;
2692                         # do_switch works with svn/trunk >= r22312, but that
2693                         # is not included with SVN 1.4.3 (the latest version
2694                         # at the moment), so we can't rely on it
2695                         $self->{last_rev} = $r0;
2696                         $self->{last_commit} = $parent;
2697                         $ed = SVN::Git::Fetcher->new($self, $gs->{path});
2698                         $gs->ra->gs_do_switch($r0, $rev, $gs,
2699                                               $self->full_url, $ed)
2700                           or die "SVN connection failed somewhere...\n";
2701                 } elsif ($self->ra->trees_match($new_url, $r0,
2702                                                 $self->full_url, $rev)) {
2703                         print STDERR "Trees match:\n",
2704                                      "  $new_url\@$r0\n",
2705                                      "  ${\$self->full_url}\@$rev\n",
2706                                      "Following parent with no changes\n"
2707                                      unless $::_q > 1;
2708                         $self->tmp_index_do(sub {
2709                             command_noisy('read-tree', $parent);
2710                         });
2711                         $self->{last_commit} = $parent;
2712                 } else {
2713                         print STDERR "Following parent with do_update\n"
2714                                      unless $::_q > 1;
2715                         $ed = SVN::Git::Fetcher->new($self);
2716                         $self->ra->gs_do_update($rev, $rev, $self, $ed)
2717                           or die "SVN connection failed somewhere...\n";
2718                 }
2719                 print STDERR "Successfully followed parent\n" unless $::_q > 1;
2720                 return $self->make_log_entry($rev, [$parent], $ed);
2721         }
2722         return undef;
2723 }
2724
2725 sub do_fetch {
2726         my ($self, $paths, $rev) = @_;
2727         my $ed;
2728         my ($last_rev, @parents);
2729         if (my $lc = $self->last_commit) {
2730                 # we can have a branch that was deleted, then re-added
2731                 # under the same name but copied from another path, in
2732                 # which case we'll have multiple parents (we don't
2733                 # want to break the original ref, nor lose copypath info):
2734                 if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
2735                         push @{$log_entry->{parents}}, $lc;
2736                         return $log_entry;
2737                 }
2738                 $ed = SVN::Git::Fetcher->new($self);
2739                 $last_rev = $self->{last_rev};
2740                 $ed->{c} = $lc;
2741                 @parents = ($lc);
2742         } else {
2743                 $last_rev = $rev;
2744                 if (my $log_entry = $self->find_parent_branch($paths, $rev)) {
2745                         return $log_entry;
2746                 }
2747                 $ed = SVN::Git::Fetcher->new($self);
2748         }
2749         unless ($self->ra->gs_do_update($last_rev, $rev, $self, $ed)) {
2750                 die "SVN connection failed somewhere...\n";
2751         }
2752         $self->make_log_entry($rev, \@parents, $ed);
2753 }
2754
2755 sub mkemptydirs {
2756         my ($self, $r) = @_;
2757
2758         sub scan {
2759                 my ($r, $empty_dirs, $line) = @_;
2760                 if (defined $r && $line =~ /^r(\d+)$/) {
2761                         return 0 if $1 > $r;
2762                 } elsif ($line =~ /^  \+empty_dir: (.+)$/) {
2763                         $empty_dirs->{$1} = 1;
2764                 } elsif ($line =~ /^  \-empty_dir: (.+)$/) {
2765                         my @d = grep {m[^\Q$1\E(/|$)]} (keys %$empty_dirs);
2766                         delete @$empty_dirs{@d};
2767                 }
2768                 1; # continue
2769         };
2770
2771         my %empty_dirs = ();
2772         my $gz_file = "$self->{dir}/unhandled.log.gz";
2773         if (-f $gz_file) {
2774                 if (!$can_compress) {
2775                         warn "Compress::Zlib could not be found; ",
2776                              "empty directories in $gz_file will not be read\n";
2777                 } else {
2778                         my $gz = Compress::Zlib::gzopen($gz_file, "rb") or
2779                                 die "Unable to open $gz_file: $!\n";
2780                         my $line;
2781                         while ($gz->gzreadline($line) > 0) {
2782                                 scan($r, \%empty_dirs, $line) or last;
2783                         }
2784                         $gz->gzclose;
2785                 }
2786         }
2787
2788         if (open my $fh, '<', "$self->{dir}/unhandled.log") {
2789                 binmode $fh or croak "binmode: $!";
2790                 while (<$fh>) {
2791                         scan($r, \%empty_dirs, $_) or last;
2792                 }
2793                 close $fh;
2794         }
2795
2796         my $strip = qr/\A\Q$self->{path}\E(?:\/|$)/;
2797         foreach my $d (sort keys %empty_dirs) {
2798                 $d = uri_decode($d);
2799                 $d =~ s/$strip//;
2800                 next if -d $d;
2801                 if (-e _) {
2802                         warn "$d exists but is not a directory\n";
2803                 } else {
2804                         print "creating empty directory: $d\n";
2805                         mkpath([$d]);
2806                 }
2807         }
2808 }
2809
2810 sub get_untracked {
2811         my ($self, $ed) = @_;
2812         my @out;
2813         my $h = $ed->{empty};
2814         foreach (sort keys %$h) {
2815                 my $act = $h->{$_} ? '+empty_dir' : '-empty_dir';
2816                 push @out, "  $act: " . uri_encode($_);
2817                 warn "W: $act: $_\n";
2818         }
2819         foreach my $t (qw/dir_prop file_prop/) {
2820                 $h = $ed->{$t} or next;
2821                 foreach my $path (sort keys %$h) {
2822                         my $ppath = $path eq '' ? '.' : $path;
2823                         foreach my $prop (sort keys %{$h->{$path}}) {
2824                                 next if $SKIP_PROP{$prop};
2825                                 my $v = $h->{$path}->{$prop};
2826                                 my $t_ppath_prop = "$t: " .
2827                                                     uri_encode($ppath) . ' ' .
2828                                                     uri_encode($prop);
2829                                 if (defined $v) {
2830                                         push @out, "  +$t_ppath_prop " .
2831                                                    uri_encode($v);
2832                                 } else {
2833                                         push @out, "  -$t_ppath_prop";
2834                                 }
2835                         }
2836                 }
2837         }
2838         foreach my $t (qw/absent_file absent_directory/) {
2839                 $h = $ed->{$t} or next;
2840                 foreach my $parent (sort keys %$h) {
2841                         foreach my $path (sort @{$h->{$parent}}) {
2842                                 push @out, "  $t: " .
2843                                            uri_encode("$parent/$path");
2844                                 warn "W: $t: $parent/$path ",
2845                                      "Insufficient permissions?\n";
2846                         }
2847                 }
2848         }
2849         \@out;
2850 }
2851
2852 # parse_svn_date(DATE)
2853 # --------------------
2854 # Given a date (in UTC) from Subversion, return a string in the format
2855 # "<TZ Offset> <local date/time>" that Git will use.
2856 #
2857 # By default the parsed date will be in UTC; if $Git::SVN::_localtime
2858 # is true we'll convert it to the local timezone instead.
2859 sub parse_svn_date {
2860         my $date = shift || return '+0000 1970-01-01 00:00:00';
2861         my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
2862                                             (\d\d)\:(\d\d)\:(\d\d)\.\d*Z$/x) or
2863                                          croak "Unable to parse date: $date\n";
2864         my $parsed_date;    # Set next.
2865
2866         if ($Git::SVN::_localtime) {
2867                 # Translate the Subversion datetime to an epoch time.
2868                 # Begin by switching ourselves to $date's timezone, UTC.
2869                 my $old_env_TZ = $ENV{TZ};
2870                 $ENV{TZ} = 'UTC';
2871
2872                 my $epoch_in_UTC =
2873                     POSIX::strftime('%s', $S, $M, $H, $d, $m - 1, $Y - 1900);
2874
2875                 # Determine our local timezone (including DST) at the
2876                 # time of $epoch_in_UTC.  $Git::SVN::Log::TZ stored the
2877                 # value of TZ, if any, at the time we were run.
2878                 if (defined $Git::SVN::Log::TZ) {
2879                         $ENV{TZ} = $Git::SVN::Log::TZ;
2880                 } else {
2881                         delete $ENV{TZ};
2882                 }
2883
2884                 my $our_TZ =
2885                     POSIX::strftime('%Z', $S, $M, $H, $d, $m - 1, $Y - 1900);
2886
2887                 # This converts $epoch_in_UTC into our local timezone.
2888                 my ($sec, $min, $hour, $mday, $mon, $year,
2889                     $wday, $yday, $isdst) = localtime($epoch_in_UTC);
2890
2891                 $parsed_date = sprintf('%s %04d-%02d-%02d %02d:%02d:%02d',
2892                                        $our_TZ, $year + 1900, $mon + 1,
2893                                        $mday, $hour, $min, $sec);
2894
2895                 # Reset us to the timezone in effect when we entered
2896                 # this routine.
2897                 if (defined $old_env_TZ) {
2898                         $ENV{TZ} = $old_env_TZ;
2899                 } else {
2900                         delete $ENV{TZ};
2901                 }
2902         } else {
2903                 $parsed_date = "+0000 $Y-$m-$d $H:$M:$S";
2904         }
2905
2906         return $parsed_date;
2907 }
2908
2909 sub other_gs {
2910         my ($self, $new_url, $url,
2911             $branch_from, $r, $old_ref_id) = @_;
2912         my $gs = Git::SVN->find_by_url($new_url, $url, $branch_from);
2913         unless ($gs) {
2914                 my $ref_id = $old_ref_id;
2915                 $ref_id =~ s/\@\d+$//;
2916                 $ref_id .= "\@$r";
2917                 # just grow a tail if we're not unique enough :x
2918                 $ref_id .= '-' while find_ref($ref_id);
2919                 print STDERR "Initializing parent: $ref_id\n" unless $::_q > 1;
2920                 my ($u, $p, $repo_id) = ($new_url, '', $ref_id);
2921                 if ($u =~ s#^\Q$url\E(/|$)##) {
2922                         $p = $u;
2923                         $u = $url;
2924                         $repo_id = $self->{repo_id};
2925                 }
2926                 $gs = Git::SVN->init($u, $p, $repo_id, $ref_id, 1);
2927         }
2928         $gs
2929 }
2930
2931 sub call_authors_prog {
2932         my ($orig_author) = @_;
2933         $orig_author = command_oneline('rev-parse', '--sq-quote', $orig_author);
2934         my $author = `$::_authors_prog $orig_author`;
2935         if ($? != 0) {
2936                 die "$::_authors_prog failed with exit code $?\n"
2937         }
2938         if ($author =~ /^\s*(.+?)\s*<(.*)>\s*$/) {
2939                 my ($name, $email) = ($1, $2);
2940                 $email = undef if length $2 == 0;
2941                 return [$name, $email];
2942         } else {
2943                 die "Author: $orig_author: $::_authors_prog returned "
2944                         . "invalid author format: $author\n";
2945         }
2946 }
2947
2948 sub check_author {
2949         my ($author) = @_;
2950         if (!defined $author || length $author == 0) {
2951                 $author = '(no author)';
2952         }
2953         if (!defined $::users{$author}) {
2954                 if (defined $::_authors_prog) {
2955                         $::users{$author} = call_authors_prog($author);
2956                 } elsif (defined $::_authors) {
2957                         die "Author: $author not defined in $::_authors file\n";
2958                 }
2959         }
2960         $author;
2961 }
2962
2963 sub find_extra_svk_parents {
2964         my ($self, $ed, $tickets, $parents) = @_;
2965         # aha!  svk:merge property changed...
2966         my @tickets = split "\n", $tickets;
2967         my @known_parents;
2968         for my $ticket ( @tickets ) {
2969                 my ($uuid, $path, $rev) = split /:/, $ticket;
2970                 if ( $uuid eq $self->ra_uuid ) {
2971                         my $url = $self->rewrite_root || $self->{url};
2972                         my $repos_root = $url;
2973                         my $branch_from = $path;
2974                         $branch_from =~ s{^/}{};
2975                         my $gs = $self->other_gs($repos_root."/".$branch_from,
2976                                                  $url,
2977                                                  $branch_from,
2978                                                  $rev,
2979                                                  $self->{ref_id});
2980                         if ( my $commit = $gs->rev_map_get($rev, $uuid) ) {
2981                                 # wahey!  we found it, but it might be
2982                                 # an old one (!)
2983                                 push @known_parents, [ $rev, $commit ];
2984                         }
2985                 }
2986         }
2987         # Ordering matters; highest-numbered commit merge tickets
2988         # first, as they may account for later merge ticket additions
2989         # or changes.
2990         @known_parents = map {$_->[1]} sort {$b->[0] <=> $a->[0]} @known_parents;
2991         for my $parent ( @known_parents ) {
2992                 my @cmd = ('rev-list', $parent, map { "^$_" } @$parents );
2993                 my ($msg_fh, $ctx) = command_output_pipe(@cmd);
2994                 my $new;
2995                 while ( <$msg_fh> ) {
2996                         $new=1;last;
2997                 }
2998                 command_close_pipe($msg_fh, $ctx);
2999                 if ( $new ) {
3000                         print STDERR
3001                             "Found merge parent (svk:merge ticket): $parent\n";
3002                         push @$parents, $parent;
3003                 }
3004         }
3005 }
3006
3007 sub lookup_svn_merge {
3008         my $uuid = shift;
3009         my $url = shift;
3010         my $merge = shift;
3011
3012         my ($source, $revs) = split ":", $merge;
3013         my $path = $source;
3014         $path =~ s{^/}{};
3015         my $gs = Git::SVN->find_by_url($url.$source, $url, $path);
3016         if ( !$gs ) {
3017                 warn "Couldn't find revmap for $url$source\n";
3018                 return;
3019         }
3020         my @ranges = split ",", $revs;
3021         my ($tip, $tip_commit);
3022         my @merged_commit_ranges;
3023         # find the tip
3024         for my $range ( @ranges ) {
3025                 my ($bottom, $top) = split "-", $range;
3026                 $top ||= $bottom;
3027                 my $bottom_commit = $gs->find_rev_after( $bottom, 1, $top );
3028                 my $top_commit = $gs->find_rev_before( $top, 1, $bottom );
3029
3030                 unless ($top_commit and $bottom_commit) {
3031                         warn "W:unknown path/rev in svn:mergeinfo "
3032                                 ."dirprop: $source:$range\n";
3033                         next;
3034                 }
3035
3036                 push @merged_commit_ranges,
3037                         "$bottom_commit^..$top_commit";
3038
3039                 if ( !defined $tip or $top > $tip ) {
3040                         $tip = $top;
3041                         $tip_commit = $top_commit;
3042                 }
3043         }
3044         return ($tip_commit, @merged_commit_ranges);
3045 }
3046
3047 sub _rev_list {
3048         my ($msg_fh, $ctx) = command_output_pipe(
3049                 "rev-list", @_,
3050                );
3051         my @rv;
3052         while ( <$msg_fh> ) {
3053                 chomp;
3054                 push @rv, $_;
3055         }
3056         command_close_pipe($msg_fh, $ctx);
3057         @rv;
3058 }
3059
3060 sub check_cherry_pick {
3061         my $base = shift;
3062         my $tip = shift;
3063         my @ranges = @_;
3064         my %commits = map { $_ => 1 }
3065                 _rev_list("--no-merges", $tip, "--not", $base);
3066         for my $range ( @ranges ) {
3067                 delete @commits{_rev_list($range)};
3068         }
3069         for my $commit (keys %commits) {
3070                 if (has_no_changes($commit)) {
3071                         delete $commits{$commit};
3072                 }
3073         }
3074         return (keys %commits);
3075 }
3076
3077 sub has_no_changes {
3078         my $commit = shift;
3079
3080         my @revs = split / /, command_oneline(
3081                 qw(rev-list --parents -1 -m), $commit);
3082
3083         # Commits with no parents, e.g. the start of a partial branch,
3084         # have changes by definition.
3085         return 1 if (@revs < 2);
3086
3087         # Commits with multiple parents, e.g a merge, have no changes
3088         # by definition.
3089         return 0 if (@revs > 2);
3090
3091         return (command_oneline("rev-parse", "$commit^{tree}") eq
3092                 command_oneline("rev-parse", "$commit~1^{tree}"));
3093 }
3094
3095 BEGIN {
3096         memoize 'lookup_svn_merge';
3097         memoize 'check_cherry_pick';
3098         memoize 'has_no_changes';
3099 }
3100
3101 sub parents_exclude {
3102         my $parents = shift;
3103         my @commits = @_;
3104         return unless @commits;
3105
3106         my @excluded;
3107         my $excluded;
3108         do {
3109                 my @cmd = ('rev-list', "-1", @commits, "--not", @$parents );
3110                 $excluded = command_oneline(@cmd);
3111                 if ( $excluded ) {
3112                         my @new;
3113                         my $found;
3114                         for my $commit ( @commits ) {
3115                                 if ( $commit eq $excluded ) {
3116                                         push @excluded, $commit;
3117                                         $found++;
3118                                         last;
3119                                 }
3120                                 else {
3121                                         push @new, $commit;
3122                                 }
3123                         }
3124                         die "saw commit '$excluded' in rev-list output, "
3125                                 ."but we didn't ask for that commit (wanted: @commits --not @$parents)"
3126                                         unless $found;
3127                         @commits = @new;
3128                 }
3129         }
3130                 while ($excluded and @commits);
3131
3132         return @excluded;
3133 }
3134
3135
3136 # note: this function should only be called if the various dirprops
3137 # have actually changed
3138 sub find_extra_svn_parents {
3139         my ($self, $ed, $mergeinfo, $parents) = @_;
3140         # aha!  svk:merge property changed...
3141
3142         # We first search for merged tips which are not in our
3143         # history.  Then, we figure out which git revisions are in
3144         # that tip, but not this revision.  If all of those revisions
3145         # are now marked as merge, we can add the tip as a parent.
3146         my @merges = split "\n", $mergeinfo;
3147         my @merge_tips;
3148         my $url = $self->rewrite_root || $self->{url};
3149         my $uuid = $self->ra_uuid;
3150         my %ranges;
3151         for my $merge ( @merges ) {
3152                 my ($tip_commit, @ranges) =
3153                         lookup_svn_merge( $uuid, $url, $merge );
3154                 unless (!$tip_commit or
3155                                 grep { $_ eq $tip_commit } @$parents ) {
3156                         push @merge_tips, $tip_commit;
3157                         $ranges{$tip_commit} = \@ranges;
3158                 } else {
3159                         push @merge_tips, undef;
3160                 }
3161         }
3162
3163         my %excluded = map { $_ => 1 }
3164                 parents_exclude($parents, grep { defined } @merge_tips);
3165
3166         # check merge tips for new parents
3167         my @new_parents;
3168         for my $merge_tip ( @merge_tips ) {
3169                 my $spec = shift @merges;
3170                 next unless $merge_tip and $excluded{$merge_tip};
3171
3172                 my $ranges = $ranges{$merge_tip};
3173
3174                 # check out 'new' tips
3175                 my $merge_base;
3176                 eval {
3177                         $merge_base = command_oneline(
3178                                 "merge-base",
3179                                 @$parents, $merge_tip,
3180                         );
3181                 };
3182                 if ($@) {
3183                         die "An error occurred during merge-base"
3184                                 unless $@->isa("Git::Error::Command");
3185
3186                         warn "W: Cannot find common ancestor between ".
3187                              "@$parents and $merge_tip. Ignoring merge info.\n";
3188                         next;
3189                 }
3190
3191                 # double check that there are no missing non-merge commits
3192                 my (@incomplete) = check_cherry_pick(
3193                         $merge_base, $merge_tip,
3194                         @$ranges,
3195                        );
3196
3197                 if ( @incomplete ) {
3198                         warn "W:svn cherry-pick ignored ($spec) - missing "
3199                                 .@incomplete." commit(s) (eg $incomplete[0])\n";
3200                 } else {
3201                         warn
3202                                 "Found merge parent (svn:mergeinfo prop): ",
3203                                         $merge_tip, "\n";
3204                         push @new_parents, $merge_tip;
3205                 }
3206         }
3207
3208         # cater for merges which merge commits from multiple branches
3209         if ( @new_parents > 1 ) {
3210                 for ( my $i = 0; $i <= $#new_parents; $i++ ) {
3211                         for ( my $j = 0; $j <= $#new_parents; $j++ ) {
3212                                 next if $i == $j;
3213                                 next unless $new_parents[$i];
3214                                 next unless $new_parents[$j];
3215                                 my $revs = command_oneline(
3216                                         "rev-list", "-1",
3217                                         "$new_parents[$i]..$new_parents[$j]",
3218                                        );
3219                                 if ( !$revs ) {
3220                                         undef($new_parents[$i]);
3221                                 }
3222                         }
3223                 }
3224         }
3225         push @$parents, grep { defined } @new_parents;
3226 }
3227
3228 sub make_log_entry {
3229         my ($self, $rev, $parents, $ed) = @_;
3230         my $untracked = $self->get_untracked($ed);
3231
3232         my @parents = @$parents;
3233         my $ps = $ed->{path_strip} || "";
3234         for my $path ( grep { m/$ps/ } %{$ed->{dir_prop}} ) {
3235                 my $props = $ed->{dir_prop}{$path};
3236                 if ( $props->{"svk:merge"} ) {
3237                         $self->find_extra_svk_parents
3238                                 ($ed, $props->{"svk:merge"}, \@parents);
3239                 }
3240                 if ( $props->{"svn:mergeinfo"} ) {
3241                         $self->find_extra_svn_parents
3242                                 ($ed,
3243                                  $props->{"svn:mergeinfo"},
3244                                  \@parents);
3245                 }
3246         }
3247
3248         open my $un, '>>', "$self->{dir}/unhandled.log" or croak $!;
3249         print $un "r$rev\n" or croak $!;
3250         print $un $_, "\n" foreach @$untracked;
3251         my %log_entry = ( parents => \@parents, revision => $rev,
3252                           log => '');
3253
3254         my $headrev;
3255         my $logged = delete $self->{logged_rev_props};
3256         if (!$logged || $self->{-want_revprops}) {
3257                 my $rp = $self->ra->rev_proplist($rev);
3258                 foreach (sort keys %$rp) {
3259                         my $v = $rp->{$_};
3260                         if (/^svn:(author|date|log)$/) {
3261                                 $log_entry{$1} = $v;
3262                         } elsif ($_ eq 'svm:headrev') {
3263                                 $headrev = $v;
3264                         } else {
3265                                 print $un "  rev_prop: ", uri_encode($_), ' ',
3266                                           uri_encode($v), "\n";
3267                         }
3268                 }
3269         } else {
3270                 map { $log_entry{$_} = $logged->{$_} } keys %$logged;
3271         }
3272         close $un or croak $!;
3273
3274         $log_entry{date} = parse_svn_date($log_entry{date});
3275         $log_entry{log} .= "\n";
3276         my $author = $log_entry{author} = check_author($log_entry{author});
3277         my ($name, $email) = defined $::users{$author} ? @{$::users{$author}}
3278                                                        : ($author, undef);
3279
3280         my ($commit_name, $commit_email) = ($name, $email);
3281         if ($_use_log_author) {
3282                 my $name_field;
3283                 if ($log_entry{log} =~ /From:\s+(.*\S)\s*\n/i) {
3284                         $name_field = $1;
3285                 } elsif ($log_entry{log} =~ /Signed-off-by:\s+(.*\S)\s*\n/i) {
3286                         $name_field = $1;
3287                 }
3288                 if (!defined $name_field) {
3289                         if (!defined $email) {
3290                                 $email = $name;
3291                         }
3292                 } elsif ($name_field =~ /(.*?)\s+<(.*)>/) {
3293                         ($name, $email) = ($1, $2);
3294                 } elsif ($name_field =~ /(.*)@/) {
3295                         ($name, $email) = ($1, $name_field);
3296                 } else {
3297                         ($name, $email) = ($name_field, $name_field);
3298                 }
3299         }
3300         if (defined $headrev && $self->use_svm_props) {
3301                 if ($self->rewrite_root) {
3302                         die "Can't have both 'useSvmProps' and 'rewriteRoot' ",
3303                             "options set!\n";
3304                 }
3305                 my ($uuid, $r) = $headrev =~ m{^([a-f\d\-]{30,}):(\d+)$}i;
3306                 # we don't want "SVM: initializing mirror for junk" ...
3307                 return undef if $r == 0;
3308                 my $svm = $self->svm;
3309                 if ($uuid ne $svm->{uuid}) {
3310                         die "UUID mismatch on SVM path:\n",
3311                             "expected: $svm->{uuid}\n",
3312                             "     got: $uuid\n";
3313                 }
3314                 my $full_url = $self->full_url;
3315                 $full_url =~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or
3316                              die "Failed to replace '$svm->{replace}' with ",
3317                                  "'$svm->{source}' in $full_url\n";
3318                 # throw away username for storing in records
3319                 remove_username($full_url);
3320                 $log_entry{metadata} = "$full_url\@$r $uuid";
3321                 $log_entry{svm_revision} = $r;
3322                 $email ||= "$author\@$uuid";
3323                 $commit_email ||= "$author\@$uuid";
3324         } elsif ($self->use_svnsync_props) {
3325                 my $full_url = $self->svnsync->{url};
3326                 $full_url .= "/$self->{path}" if length $self->{path};
3327                 remove_username($full_url);
3328                 my $uuid = $self->svnsync->{uuid};
3329                 $log_entry{metadata} = "$full_url\@$rev $uuid";
3330                 $email ||= "$author\@$uuid";
3331                 $commit_email ||= "$author\@$uuid";
3332         } else {
3333                 my $url = $self->metadata_url;
3334                 remove_username($url);
3335                 $log_entry{metadata} = "$url\@$rev " .
3336                                        $self->ra->get_uuid;
3337                 $email ||= "$author\@" . $self->ra->get_uuid;
3338                 $commit_email ||= "$author\@" . $self->ra->get_uuid;
3339         }
3340         $log_entry{name} = $name;
3341         $log_entry{email} = $email;
3342         $log_entry{commit_name} = $commit_name;
3343         $log_entry{commit_email} = $commit_email;
3344         \%log_entry;
3345 }
3346
3347 sub fetch {
3348         my ($self, $min_rev, $max_rev, @parents) = @_;
3349         my ($last_rev, $last_commit) = $self->last_rev_commit;
3350         my ($base, $head) = $self->get_fetch_range($min_rev, $max_rev);
3351         $self->ra->gs_fetch_loop_common($base, $head, [$self]);
3352 }
3353
3354 sub set_tree_cb {
3355         my ($self, $log_entry, $tree, $rev, $date, $author) = @_;
3356         $self->{inject_parents} = { $rev => $tree };
3357         $self->fetch(undef, undef);
3358 }
3359
3360 sub set_tree {
3361         my ($self, $tree) = (shift, shift);
3362         my $log_entry = ::get_commit_entry($tree);
3363         unless ($self->{last_rev}) {
3364                 ::fatal("Must have an existing revision to commit");
3365         }
3366         my %ed_opts = ( r => $self->{last_rev},
3367                         log => $log_entry->{log},
3368                         ra => $self->ra,
3369                         tree_a => $self->{last_commit},
3370                         tree_b => $tree,
3371                         editor_cb => sub {
3372                                $self->set_tree_cb($log_entry, $tree, @_) },
3373                         svn_path => $self->{path} );
3374         if (!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {
3375                 print "No changes\nr$self->{last_rev} = $tree\n";
3376         }
3377 }
3378
3379 sub rebuild_from_rev_db {
3380         my ($self, $path) = @_;
3381         my $r = -1;
3382         open my $fh, '<', $path or croak "open: $!";
3383         binmode $fh or croak "binmode: $!";
3384         while (<$fh>) {
3385                 length($_) == 41 or croak "inconsistent size in ($_) != 41";
3386                 chomp($_);
3387                 ++$r;
3388                 next if $_ eq ('0' x 40);
3389                 $self->rev_map_set($r, $_);
3390                 print "r$r = $_\n";
3391         }
3392         close $fh or croak "close: $!";
3393         unlink $path or croak "unlink: $!";
3394 }
3395
3396 sub rebuild {
3397         my ($self) = @_;
3398         my $map_path = $self->map_path;
3399         my $partial = (-e $map_path && ! -z $map_path);
3400         return unless ::verify_ref($self->refname.'^0');
3401         if (!$partial && ($self->use_svm_props || $self->no_metadata)) {
3402                 my $rev_db = $self->rev_db_path;
3403                 $self->rebuild_from_rev_db($rev_db);
3404                 if ($self->use_svm_props) {
3405                         my $svm_rev_db = $self->rev_db_path($self->svm_uuid);
3406                         $self->rebuild_from_rev_db($svm_rev_db);
3407                 }
3408                 $self->unlink_rev_db_symlink;
3409                 return;
3410         }
3411         print "Rebuilding $map_path ...\n" if (!$partial);
3412         my ($base_rev, $head) = ($partial ? $self->rev_map_max_norebuild(1) :
3413                 (undef, undef));
3414         my ($log, $ctx) =
3415             command_output_pipe(qw/rev-list --pretty=raw --no-color --reverse/,
3416                                 ($head ? "$head.." : "") . $self->refname,
3417                                 '--');
3418         my $metadata_url = $self->metadata_url;
3419         remove_username($metadata_url);
3420         my $svn_uuid = $self->ra_uuid;
3421         my $c;
3422         while (<$log>) {
3423                 if ( m{^commit ($::sha1)$} ) {
3424                         $c = $1;
3425                         next;
3426                 }
3427                 next unless s{^\s*(git-svn-id:)}{$1};
3428                 my ($url, $rev, $uuid) = ::extract_metadata($_);
3429                 remove_username($url);
3430
3431                 # ignore merges (from set-tree)
3432                 next if (!defined $rev || !$uuid);
3433
3434                 # if we merged or otherwise started elsewhere, this is
3435                 # how we break out of it
3436                 if (($uuid ne $svn_uuid) ||
3437                     ($metadata_url && $url && ($url ne $metadata_url))) {
3438                         next;
3439                 }
3440                 if ($partial && $head) {
3441                         print "Partial-rebuilding $map_path ...\n";
3442                         print "Currently at $base_rev = $head\n";
3443                         $head = undef;
3444                 }
3445
3446                 $self->rev_map_set($rev, $c);
3447                 print "r$rev = $c\n";
3448         }
3449         command_close_pipe($log, $ctx);
3450         print "Done rebuilding $map_path\n" if (!$partial || !$head);
3451         my $rev_db_path = $self->rev_db_path;
3452         if (-f $self->rev_db_path) {
3453                 unlink $self->rev_db_path or croak "unlink: $!";
3454         }
3455         $self->unlink_rev_db_symlink;
3456 }
3457
3458 # rev_map:
3459 # Tie::File seems to be prone to offset errors if revisions get sparse,
3460 # it's not that fast, either.  Tie::File is also not in Perl 5.6.  So
3461 # one of my favorite modules is out :<  Next up would be one of the DBM
3462 # modules, but I'm not sure which is most portable...
3463 #
3464 # This is the replacement for the rev_db format, which was too big
3465 # and inefficient for large repositories with a lot of sparse history
3466 # (mainly tags)
3467 #
3468 # The format is this:
3469 #   - 24 bytes for every record,
3470 #     * 4 bytes for the integer representing an SVN revision number
3471 #     * 20 bytes representing the sha1 of a git commit
3472 #   - No empty padding records like the old format
3473 #     (except the last record, which can be overwritten)
3474 #   - new records are written append-only since SVN revision numbers
3475 #     increase monotonically
3476 #   - lookups on SVN revision number are done via a binary search
3477 #   - Piping the file to xxd -c24 is a good way of dumping it for
3478 #     viewing or editing (piped back through xxd -r), should the need
3479 #     ever arise.
3480 #   - The last record can be padding revision with an all-zero sha1
3481 #     This is used to optimize fetch performance when using multiple
3482 #     "fetch" directives in .git/config
3483 #
3484 # These files are disposable unless noMetadata or useSvmProps is set
3485
3486 sub _rev_map_set {
3487         my ($fh, $rev, $commit) = @_;
3488
3489         binmode $fh or croak "binmode: $!";
3490         my $size = (stat($fh))[7];
3491         ($size % 24) == 0 or croak "inconsistent size: $size";
3492
3493         my $wr_offset = 0;
3494         if ($size > 0) {
3495                 sysseek($fh, -24, SEEK_END) or croak "seek: $!";
3496                 my $read = sysread($fh, my $buf, 24) or croak "read: $!";
3497                 $read == 24 or croak "read only $read bytes (!= 24)";
3498                 my ($last_rev, $last_commit) = unpack(rev_map_fmt, $buf);
3499                 if ($last_commit eq ('0' x40)) {
3500                         if ($size >= 48) {
3501                                 sysseek($fh, -48, SEEK_END) or croak "seek: $!";
3502                                 $read = sysread($fh, $buf, 24) or
3503                                     croak "read: $!";
3504                                 $read == 24 or
3505                                     croak "read only $read bytes (!= 24)";
3506                                 ($last_rev, $last_commit) =
3507                                     unpack(rev_map_fmt, $buf);
3508                                 if ($last_commit eq ('0' x40)) {
3509                                         croak "inconsistent .rev_map\n";
3510                                 }
3511                         }
3512                         if ($last_rev >= $rev) {
3513                                 croak "last_rev is higher!: $last_rev >= $rev";
3514                         }
3515                         $wr_offset = -24;
3516                 }
3517         }
3518         sysseek($fh, $wr_offset, SEEK_END) or croak "seek: $!";
3519         syswrite($fh, pack(rev_map_fmt, $rev, $commit), 24) == 24 or
3520           croak "write: $!";
3521 }
3522
3523 sub _rev_map_reset {
3524         my ($fh, $rev, $commit) = @_;
3525         my $c = _rev_map_get($fh, $rev);
3526         $c eq $commit or die "_rev_map_reset(@_) commit $c does not match!\n";
3527         my $offset = sysseek($fh, 0, SEEK_CUR) or croak "seek: $!";
3528         truncate $fh, $offset or croak "truncate: $!";
3529 }
3530
3531 sub mkfile {
3532         my ($path) = @_;
3533         unless (-e $path) {
3534                 my ($dir, $base) = ($path =~ m#^(.*?)/?([^/]+)$#);
3535                 mkpath([$dir]) unless -d $dir;
3536                 open my $fh, '>>', $path or die "Couldn't create $path: $!\n";
3537                 close $fh or die "Couldn't close (create) $path: $!\n";
3538         }
3539 }
3540
3541 sub rev_map_set {
3542         my ($self, $rev, $commit, $update_ref, $uuid) = @_;
3543         length $commit == 40 or die "arg3 must be a full SHA1 hexsum\n";
3544         my $db = $self->map_path($uuid);
3545         my $db_lock = "$db.lock";
3546         my $sig;
3547         $update_ref ||= 0;
3548         if ($update_ref) {
3549                 $SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} =
3550                             $SIG{USR1} = $SIG{USR2} = sub { $sig = $_[0] };
3551         }
3552         mkfile($db);
3553
3554         $LOCKFILES{$db_lock} = 1;
3555         my $sync;
3556         # both of these options make our .rev_db file very, very important
3557         # and we can't afford to lose it because rebuild() won't work
3558         if ($self->use_svm_props || $self->no_metadata) {
3559                 $sync = 1;
3560                 copy($db, $db_lock) or die "rev_map_set(@_): ",
3561                                            "Failed to copy: ",
3562                                            "$db => $db_lock ($!)\n";
3563         } else {
3564                 rename $db, $db_lock or die "rev_map_set(@_): ",
3565                                             "Failed to rename: ",
3566                                             "$db => $db_lock ($!)\n";
3567         }
3568
3569         sysopen(my $fh, $db_lock, O_RDWR | O_CREAT)
3570              or croak "Couldn't open $db_lock: $!\n";
3571         $update_ref eq 'reset' ? _rev_map_reset($fh, $rev, $commit) :
3572                                  _rev_map_set($fh, $rev, $commit);
3573         if ($sync) {
3574                 $fh->flush or die "Couldn't flush $db_lock: $!\n";
3575                 $fh->sync or die "Couldn't sync $db_lock: $!\n";
3576         }
3577         close $fh or croak $!;
3578         if ($update_ref) {
3579                 $_head = $self;
3580                 my $note = "";
3581                 $note = " ($update_ref)" if ($update_ref !~ /^\d*$/);
3582                 command_noisy('update-ref', '-m', "r$rev$note",
3583                               $self->refname, $commit);
3584         }
3585         rename $db_lock, $db or die "rev_map_set(@_): ", "Failed to rename: ",
3586                                     "$db_lock => $db ($!)\n";
3587         delete $LOCKFILES{$db_lock};
3588         if ($update_ref) {
3589                 $SIG{INT} = $SIG{HUP} = $SIG{TERM} = $SIG{ALRM} = $SIG{PIPE} =
3590                             $SIG{USR1} = $SIG{USR2} = 'DEFAULT';
3591                 kill $sig, $$ if defined $sig;
3592         }
3593 }
3594
3595 # If want_commit, this will return an array of (rev, commit) where
3596 # commit _must_ be a valid commit in the archive.
3597 # Otherwise, it'll return the max revision (whether or not the
3598 # commit is valid or just a 0x40 placeholder).
3599 sub rev_map_max {
3600         my ($self, $want_commit) = @_;
3601         $self->rebuild;
3602         my ($r, $c) = $self->rev_map_max_norebuild($want_commit);
3603         $want_commit ? ($r, $c) : $r;
3604 }
3605
3606 sub rev_map_max_norebuild {
3607         my ($self, $want_commit) = @_;
3608         my $map_path = $self->map_path;
3609         stat $map_path or return $want_commit ? (0, undef) : 0;
3610         sysopen(my $fh, $map_path, O_RDONLY) or croak "open: $!";
3611         binmode $fh or croak "binmode: $!";
3612         my $size = (stat($fh))[7];
3613         ($size % 24) == 0 or croak "inconsistent size: $size";
3614
3615         if ($size == 0) {
3616                 close $fh or croak "close: $!";
3617                 return $want_commit ? (0, undef) : 0;
3618         }
3619
3620         sysseek($fh, -24, SEEK_END) or croak "seek: $!";
3621         sysread($fh, my $buf, 24) == 24 or croak "read: $!";
3622         my ($r, $c) = unpack(rev_map_fmt, $buf);
3623         if ($want_commit && $c eq ('0' x40)) {
3624                 if ($size < 48) {
3625                         return $want_commit ? (0, undef) : 0;
3626                 }
3627                 sysseek($fh, -48, SEEK_END) or croak "seek: $!";
3628                 sysread($fh, $buf, 24) == 24 or croak "read: $!";
3629                 ($r, $c) = unpack(rev_map_fmt, $buf);
3630                 if ($c eq ('0'x40)) {
3631                         croak "Penultimate record is all-zeroes in $map_path";
3632                 }
3633         }
3634         close $fh or croak "close: $!";
3635         $want_commit ? ($r, $c) : $r;
3636 }
3637
3638 sub rev_map_get {
3639         my ($self, $rev, $uuid) = @_;
3640         my $map_path = $self->map_path($uuid);
3641         return undef unless -e $map_path;
3642
3643         sysopen(my $fh, $map_path, O_RDONLY) or croak "open: $!";
3644         my $c = _rev_map_get($fh, $rev);
3645         close($fh) or croak "close: $!";
3646         $c
3647 }
3648
3649 sub _rev_map_get {
3650         my ($fh, $rev) = @_;
3651
3652         binmode $fh or croak "binmode: $!";
3653         my $size = (stat($fh))[7];
3654         ($size % 24) == 0 or croak "inconsistent size: $size";
3655
3656         if ($size == 0) {
3657                 return undef;
3658         }
3659
3660         my ($l, $u) = (0, $size - 24);
3661         my ($r, $c, $buf);
3662
3663         while ($l <= $u) {
3664                 my $i = int(($l/24 + $u/24) / 2) * 24;
3665                 sysseek($fh, $i, SEEK_SET) or croak "seek: $!";
3666                 sysread($fh, my $buf, 24) == 24 or croak "read: $!";
3667                 my ($r, $c) = unpack(rev_map_fmt, $buf);
3668
3669                 if ($r < $rev) {
3670                         $l = $i + 24;
3671                 } elsif ($r > $rev) {
3672                         $u = $i - 24;
3673                 } else { # $r == $rev
3674                         return $c eq ('0' x 40) ? undef : $c;
3675                 }
3676         }
3677         undef;
3678 }
3679
3680 # Finds the first svn revision that exists on (if $eq_ok is true) or
3681 # before $rev for the current branch.  It will not search any lower
3682 # than $min_rev.  Returns the git commit hash and svn revision number
3683 # if found, else (undef, undef).
3684 sub find_rev_before {
3685         my ($self, $rev, $eq_ok, $min_rev) = @_;
3686         --$rev unless $eq_ok;
3687         $min_rev ||= 1;
3688         my $max_rev = $self->rev_map_max;
3689         $rev = $max_rev if ($rev > $max_rev);
3690         while ($rev >= $min_rev) {
3691                 if (my $c = $self->rev_map_get($rev)) {
3692                         return ($rev, $c);
3693                 }
3694                 --$rev;
3695         }
3696         return (undef, undef);
3697 }
3698
3699 # Finds the first svn revision that exists on (if $eq_ok is true) or
3700 # after $rev for the current branch.  It will not search any higher
3701 # than $max_rev.  Returns the git commit hash and svn revision number
3702 # if found, else (undef, undef).
3703 sub find_rev_after {
3704         my ($self, $rev, $eq_ok, $max_rev) = @_;
3705         ++$rev unless $eq_ok;
3706         $max_rev ||= $self->rev_map_max;
3707         while ($rev <= $max_rev) {
3708                 if (my $c = $self->rev_map_get($rev)) {
3709                         return ($rev, $c);
3710                 }
3711                 ++$rev;
3712         }
3713         return (undef, undef);
3714 }
3715
3716 sub _new {
3717         my ($class, $repo_id, $ref_id, $path) = @_;
3718         unless (defined $repo_id && length $repo_id) {
3719                 $repo_id = $Git::SVN::default_repo_id;
3720         }
3721         unless (defined $ref_id && length $ref_id) {
3722                 $_prefix = '' unless defined($_prefix);
3723                 $_[2] = $ref_id =
3724                              "refs/remotes/$_prefix$Git::SVN::default_ref_id";
3725         }
3726         $_[1] = $repo_id;
3727         my $dir = "$ENV{GIT_DIR}/svn/$ref_id";
3728
3729         # Older repos imported by us used $GIT_DIR/svn/foo instead of
3730         # $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo
3731         if ($ref_id =~ m{^refs/remotes/(.*)}) {
3732                 my $old_dir = "$ENV{GIT_DIR}/svn/$1";
3733                 if (-d $old_dir && ! -d $dir) {
3734                         $dir = $old_dir;
3735                 }
3736         }
3737
3738         $_[3] = $path = '' unless (defined $path);
3739         mkpath([$dir]);
3740         bless {
3741                 ref_id => $ref_id, dir => $dir, index => "$dir/index",
3742                 path => $path, config => "$ENV{GIT_DIR}/svn/config",
3743                 map_root => "$dir/.rev_map", repo_id => $repo_id }, $class;
3744 }
3745
3746 # for read-only access of old .rev_db formats
3747 sub unlink_rev_db_symlink {
3748         my ($self) = @_;
3749         my $link = $self->rev_db_path;
3750         $link =~ s/\.[\w-]+$// or croak "missing UUID at the end of $link";
3751         if (-l $link) {
3752                 unlink $link or croak "unlink: $link failed!";
3753         }
3754 }
3755
3756 sub rev_db_path {
3757         my ($self, $uuid) = @_;
3758         my $db_path = $self->map_path($uuid);
3759         $db_path =~ s{/\.rev_map\.}{/\.rev_db\.}
3760             or croak "map_path: $db_path does not contain '/.rev_map.' !";
3761         $db_path;
3762 }
3763
3764 # the new replacement for .rev_db
3765 sub map_path {
3766         my ($self, $uuid) = @_;
3767         $uuid ||= $self->ra_uuid;
3768         "$self->{map_root}.$uuid";
3769 }
3770
3771 sub uri_encode {
3772         my ($f) = @_;
3773         $f =~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;
3774         $f
3775 }
3776
3777 sub uri_decode {
3778         my ($f) = @_;
3779         $f =~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;
3780         $f
3781 }
3782
3783 sub remove_username {
3784         $_[0] =~ s{^([^:]*://)[^@]+@}{$1};
3785 }
3786
3787 package Git::SVN::Prompt;
3788 use strict;
3789 use warnings;
3790 require SVN::Core;
3791 use vars qw/$_no_auth_cache $_username/;
3792
3793 sub simple {
3794         my ($cred, $realm, $default_username, $may_save, $pool) = @_;
3795         $may_save = undef if $_no_auth_cache;
3796         $default_username = $_username if defined $_username;
3797         if (defined $default_username && length $default_username) {
3798                 if (defined $realm && length $realm) {
3799                         print STDERR "Authentication realm: $realm\n";
3800                         STDERR->flush;
3801                 }
3802                 $cred->username($default_username);
3803         } else {
3804                 username($cred, $realm, $may_save, $pool);
3805         }
3806         $cred->password(_read_password("Password for '" .
3807                                        $cred->username . "': ", $realm));
3808         $cred->may_save($may_save);
3809         $SVN::_Core::SVN_NO_ERROR;
3810 }
3811
3812 sub ssl_server_trust {
3813         my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_;
3814         $may_save = undef if $_no_auth_cache;
3815         print STDERR "Error validating server certificate for '$realm':\n";
3816         {
3817                 no warnings 'once';
3818                 # All variables SVN::Auth::SSL::* are used only once,
3819                 # so we're shutting up Perl warnings about this.
3820                 if ($failures & $SVN::Auth::SSL::UNKNOWNCA) {
3821                         print STDERR " - The certificate is not issued ",
3822                             "by a trusted authority. Use the\n",
3823                             "   fingerprint to validate ",
3824                             "the certificate manually!\n";
3825                 }
3826                 if ($failures & $SVN::Auth::SSL::CNMISMATCH) {
3827                         print STDERR " - The certificate hostname ",
3828                             "does not match.\n";
3829                 }
3830                 if ($failures & $SVN::Auth::SSL::NOTYETVALID) {
3831                         print STDERR " - The certificate is not yet valid.\n";
3832                 }
3833                 if ($failures & $SVN::Auth::SSL::EXPIRED) {
3834                         print STDERR " - The certificate has expired.\n";
3835                 }
3836                 if ($failures & $SVN::Auth::SSL::OTHER) {
3837                         print STDERR " - The certificate has ",
3838                             "an unknown error.\n";
3839                 }
3840         } # no warnings 'once'
3841         printf STDERR
3842                 "Certificate information:\n".
3843                 " - Hostname: %s\n".
3844                 " - Valid: from %s until %s\n".
3845                 " - Issuer: %s\n".
3846                 " - Fingerprint: %s\n",
3847                 map $cert_info->$_, qw(hostname valid_from valid_until
3848                                        issuer_dname fingerprint);
3849         my $choice;
3850 prompt:
3851         print STDERR $may_save ?
3852               "(R)eject, accept (t)emporarily or accept (p)ermanently? " :
3853               "(R)eject or accept (t)emporarily? ";
3854         STDERR->flush;
3855         $choice = lc(substr(<STDIN> || 'R', 0, 1));
3856         if ($choice =~ /^t$/i) {
3857                 $cred->may_save(undef);
3858         } elsif ($choice =~ /^r$/i) {
3859                 return -1;
3860         } elsif ($may_save && $choice =~ /^p$/i) {
3861                 $cred->may_save($may_save);
3862         } else {
3863                 goto prompt;
3864         }
3865         $cred->accepted_failures($failures);
3866         $SVN::_Core::SVN_NO_ERROR;
3867 }
3868
3869 sub ssl_client_cert {
3870         my ($cred, $realm, $may_save, $pool) = @_;
3871         $may_save = undef if $_no_auth_cache;
3872         print STDERR "Client certificate filename: ";
3873         STDERR->flush;
3874         chomp(my $filename = <STDIN>);
3875         $cred->cert_file($filename);
3876         $cred->may_save($may_save);
3877         $SVN::_Core::SVN_NO_ERROR;
3878 }
3879
3880 sub ssl_client_cert_pw {
3881         my ($cred, $realm, $may_save, $pool) = @_;
3882         $may_save = undef if $_no_auth_cache;
3883         $cred->password(_read_password("Password: ", $realm));
3884         $cred->may_save($may_save);
3885         $SVN::_Core::SVN_NO_ERROR;
3886 }
3887
3888 sub username {
3889         my ($cred, $realm, $may_save, $pool) = @_;
3890         $may_save = undef if $_no_auth_cache;
3891         if (defined $realm && length $realm) {
3892                 print STDERR "Authentication realm: $realm\n";
3893         }
3894         my $username;
3895         if (defined $_username) {
3896                 $username = $_username;
3897         } else {
3898                 print STDERR "Username: ";
3899                 STDERR->flush;
3900                 chomp($username = <STDIN>);
3901         }
3902         $cred->username($username);
3903         $cred->may_save($may_save);
3904         $SVN::_Core::SVN_NO_ERROR;
3905 }
3906
3907 sub _read_password {
3908         my ($prompt, $realm) = @_;
3909         print STDERR $prompt;
3910         STDERR->flush;
3911         require Term::ReadKey;
3912         Term::ReadKey::ReadMode('noecho');
3913         my $password = '';
3914         while (defined(my $key = Term::ReadKey::ReadKey(0))) {
3915                 last if $key =~ /[\012\015]/; # \n\r
3916                 $password .= $key;
3917         }
3918         Term::ReadKey::ReadMode('restore');
3919         print STDERR "\n";
3920         STDERR->flush;
3921         $password;
3922 }
3923
3924 package SVN::Git::Fetcher;
3925 use vars qw/@ISA/;
3926 use strict;
3927 use warnings;
3928 use Carp qw/croak/;
3929 use File::Temp qw/tempfile/;
3930 use IO::File qw//;
3931 use vars qw/$_ignore_regex/;
3932
3933 # file baton members: path, mode_a, mode_b, pool, fh, blob, base
3934 sub new {
3935         my ($class, $git_svn, $switch_path) = @_;
3936         my $self = SVN::Delta::Editor->new;
3937         bless $self, $class;
3938         if (exists $git_svn->{last_commit}) {
3939                 $self->{c} = $git_svn->{last_commit};
3940                 $self->{empty_symlinks} =
3941                                   _mark_empty_symlinks($git_svn, $switch_path);
3942         }
3943         $self->{ignore_regex} = eval { command_oneline('config', '--get',
3944                              "svn-remote.$git_svn->{repo_id}.ignore-paths") };
3945         $self->{empty} = {};
3946         $self->{dir_prop} = {};
3947         $self->{file_prop} = {};
3948         $self->{absent_dir} = {};
3949         $self->{absent_file} = {};
3950         $self->{gii} = $git_svn->tmp_index_do(sub { Git::IndexInfo->new });
3951         $self;
3952 }
3953
3954 # this uses the Ra object, so it must be called before do_{switch,update},
3955 # not inside them (when the Git::SVN::Fetcher object is passed) to
3956 # do_{switch,update}
3957 sub _mark_empty_symlinks {
3958         my ($git_svn, $switch_path) = @_;
3959         my $bool = Git::config_bool('svn.brokenSymlinkWorkaround');
3960         return {} if (!defined($bool)) || (defined($bool) && ! $bool);
3961
3962         my %ret;
3963         my ($rev, $cmt) = $git_svn->last_rev_commit;
3964         return {} unless ($rev && $cmt);
3965
3966         # allow the warning to be printed for each revision we fetch to
3967         # ensure the user sees it.  The user can also disable the workaround
3968         # on the repository even while git svn is running and the next
3969         # revision fetched will skip this expensive function.
3970         my $printed_warning;
3971         chomp(my $empty_blob = `git hash-object -t blob --stdin < /dev/null`);
3972         my ($ls, $ctx) = command_output_pipe(qw/ls-tree -r -z/, $cmt);
3973         local $/ = "\0";
3974         my $pfx = defined($switch_path) ? $switch_path : $git_svn->{path};
3975         $pfx .= '/' if length($pfx);
3976         while (<$ls>) {
3977                 chomp;
3978                 s/\A100644 blob $empty_blob\t//o or next;
3979                 unless ($printed_warning) {
3980                         print STDERR "Scanning for empty symlinks, ",
3981                                      "this may take a while if you have ",
3982                                      "many empty files\n",
3983                                      "You may disable this with `",
3984                                      "git config svn.brokenSymlinkWorkaround ",
3985                                      "false'.\n",
3986                                      "This may be done in a different ",
3987                                      "terminal without restarting ",
3988                                      "git svn\n";
3989                         $printed_warning = 1;
3990                 }
3991                 my $path = $_;
3992                 my (undef, $props) =
3993                                $git_svn->ra->get_file($pfx.$path, $rev, undef);
3994                 if ($props->{'svn:special'}) {
3995                         $ret{$path} = 1;
3996                 }
3997         }
3998         command_close_pipe($ls, $ctx);
3999         \%ret;
4000 }
4001
4002 # returns true if a given path is inside a ".git" directory
4003 sub in_dot_git {
4004         $_[0] =~ m{(?:^|/)\.git(?:/|$)};
4005 }
4006
4007 # return value: 0 -- don't ignore, 1 -- ignore
4008 sub is_path_ignored {
4009         my ($self, $path) = @_;
4010         return 1 if in_dot_git($path);
4011         return 1 if defined($self->{ignore_regex}) &&
4012                     $path =~ m!$self->{ignore_regex}!;
4013         return 0 unless defined($_ignore_regex);
4014         return 1 if $path =~ m!$_ignore_regex!o;
4015         return 0;
4016 }
4017
4018 sub set_path_strip {
4019         my ($self, $path) = @_;
4020         $self->{path_strip} = qr/^\Q$path\E(\/|$)/ if length $path;
4021 }
4022
4023 sub open_root {
4024         { path => '' };
4025 }
4026
4027 sub open_directory {
4028         my ($self, $path, $pb, $rev) = @_;
4029         { path => $path };
4030 }
4031
4032 sub git_path {
4033         my ($self, $path) = @_;
4034         if ($self->{path_strip}) {
4035                 $path =~ s!$self->{path_strip}!! or
4036                   die "Failed to strip path '$path' ($self->{path_strip})\n";
4037         }
4038         $path;
4039 }
4040
4041 sub delete_entry {
4042         my ($self, $path, $rev, $pb) = @_;
4043         return undef if $self->is_path_ignored($path);
4044
4045         my $gpath = $self->git_path($path);
4046         return undef if ($gpath eq '');
4047
4048         # remove entire directories.
4049         my ($tree) = (command('ls-tree', '-z', $self->{c}, "./$gpath")
4050                          =~ /\A040000 tree ([a-f\d]{40})\t\Q$gpath\E\0/);
4051         if ($tree) {
4052                 my ($ls, $ctx) = command_output_pipe(qw/ls-tree
4053                                                      -r --name-only -z/,
4054                                                      $tree);
4055                 local $/ = "\0";
4056                 while (<$ls>) {
4057                         chomp;
4058                         my $rmpath = "$gpath/$_";
4059                         $self->{gii}->remove($rmpath);
4060                         print "\tD\t$rmpath\n" unless $::_q;
4061                 }
4062                 print "\tD\t$gpath/\n" unless $::_q;
4063                 command_close_pipe($ls, $ctx);
4064         } else {
4065                 $self->{gii}->remove($gpath);
4066                 print "\tD\t$gpath\n" unless $::_q;
4067         }
4068         $self->{empty}->{$path} = 0;
4069         undef;
4070 }
4071
4072 sub open_file {
4073         my ($self, $path, $pb, $rev) = @_;
4074         my ($mode, $blob);
4075
4076         goto out if $self->is_path_ignored($path);
4077
4078         my $gpath = $self->git_path($path);
4079         ($mode, $blob) = (command('ls-tree', '-z', $self->{c}, "./$gpath")
4080                              =~ /\A(\d{6}) blob ([a-f\d]{40})\t\Q$gpath\E\0/);
4081         unless (defined $mode && defined $blob) {
4082                 die "$path was not found in commit $self->{c} (r$rev)\n";
4083         }
4084         if ($mode eq '100644' && $self->{empty_symlinks}->{$path}) {
4085                 $mode = '120000';
4086         }
4087 out:
4088         { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob,
4089           pool => SVN::Pool->new, action => 'M' };
4090 }
4091
4092 sub add_file {
4093         my ($self, $path, $pb, $cp_path, $cp_rev) = @_;
4094         my $mode;
4095
4096         if (!$self->is_path_ignored($path)) {
4097                 my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
4098                 delete $self->{empty}->{$dir};
4099                 $mode = '100644';
4100         }
4101         { path => $path, mode_a => $mode, mode_b => $mode,
4102           pool => SVN::Pool->new, action => 'A' };
4103 }
4104
4105 sub add_directory {
4106         my ($self, $path, $cp_path, $cp_rev) = @_;
4107         goto out if $self->is_path_ignored($path);
4108         my $gpath = $self->git_path($path);
4109         if ($gpath eq '') {
4110                 my ($ls, $ctx) = command_output_pipe(qw/ls-tree
4111                                                      -r --name-only -z/,
4112                                                      $self->{c});
4113                 local $/ = "\0";
4114                 while (<$ls>) {
4115                         chomp;
4116                         $self->{gii}->remove($_);
4117                         print "\tD\t$_\n" unless $::_q;
4118                 }
4119                 command_close_pipe($ls, $ctx);
4120                 $self->{empty}->{$path} = 0;
4121         }
4122         my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
4123         delete $self->{empty}->{$dir};
4124         $self->{empty}->{$path} = 1;
4125 out:
4126         { path => $path };
4127 }
4128
4129 sub change_dir_prop {
4130         my ($self, $db, $prop, $value) = @_;
4131         return undef if $self->is_path_ignored($db->{path});
4132         $self->{dir_prop}->{$db->{path}} ||= {};
4133         $self->{dir_prop}->{$db->{path}}->{$prop} = $value;
4134         undef;
4135 }
4136
4137 sub absent_directory {
4138         my ($self, $path, $pb) = @_;
4139         return undef if $self->is_path_ignored($path);
4140         $self->{absent_dir}->{$pb->{path}} ||= [];
4141         push @{$self->{absent_dir}->{$pb->{path}}}, $path;
4142         undef;
4143 }
4144
4145 sub absent_file {
4146         my ($self, $path, $pb) = @_;
4147         return undef if $self->is_path_ignored($path);
4148         $self->{absent_file}->{$pb->{path}} ||= [];
4149         push @{$self->{absent_file}->{$pb->{path}}}, $path;
4150         undef;
4151 }
4152
4153 sub change_file_prop {
4154         my ($self, $fb, $prop, $value) = @_;
4155         return undef if $self->is_path_ignored($fb->{path});
4156         if ($prop eq 'svn:executable') {
4157                 if ($fb->{mode_b} != 120000) {
4158                         $fb->{mode_b} = defined $value ? 100755 : 100644;
4159                 }
4160         } elsif ($prop eq 'svn:special') {
4161                 $fb->{mode_b} = defined $value ? 120000 : 100644;
4162         } else {
4163                 $self->{file_prop}->{$fb->{path}} ||= {};
4164                 $self->{file_prop}->{$fb->{path}}->{$prop} = $value;
4165         }
4166         undef;
4167 }
4168
4169 sub apply_textdelta {
4170         my ($self, $fb, $exp) = @_;
4171         return undef if $self->is_path_ignored($fb->{path});
4172         my $fh = $::_repository->temp_acquire('svn_delta');
4173         # $fh gets auto-closed() by SVN::TxDelta::apply(),
4174         # (but $base does not,) so dup() it for reading in close_file
4175         open my $dup, '<&', $fh or croak $!;
4176         my $base = $::_repository->temp_acquire('git_blob');
4177
4178         if ($fb->{blob}) {
4179                 my ($base_is_link, $size);
4180
4181                 if ($fb->{mode_a} eq '120000' &&
4182                     ! $self->{empty_symlinks}->{$fb->{path}}) {
4183                         print $base 'link ' or die "print $!\n";
4184                         $base_is_link = 1;
4185                 }
4186         retry:
4187                 $size = $::_repository->cat_blob($fb->{blob}, $base);
4188                 die "Failed to read object $fb->{blob}" if ($size < 0);
4189
4190                 if (defined $exp) {
4191                         seek $base, 0, 0 or croak $!;
4192                         my $got = ::md5sum($base);
4193                         if ($got ne $exp) {
4194                                 my $err = "Checksum mismatch: ".
4195                                        "$fb->{path} $fb->{blob}\n" .
4196                                        "expected: $exp\n" .
4197                                        "     got: $got\n";
4198                                 if ($base_is_link) {
4199                                         warn $err,
4200                                              "Retrying... (possibly ",
4201                                              "a bad symlink from SVN)\n";
4202                                         $::_repository->temp_reset($base);
4203                                         $base_is_link = 0;
4204                                         goto retry;
4205                                 }
4206                                 die $err;
4207                         }
4208                 }
4209         }
4210         seek $base, 0, 0 or croak $!;
4211         $fb->{fh} = $fh;
4212         $fb->{base} = $base;
4213         [ SVN::TxDelta::apply($base, $dup, undef, $fb->{path}, $fb->{pool}) ];
4214 }
4215
4216 sub close_file {
4217         my ($self, $fb, $exp) = @_;
4218         return undef if $self->is_path_ignored($fb->{path});
4219
4220         my $hash;
4221         my $path = $self->git_path($fb->{path});
4222         if (my $fh = $fb->{fh}) {
4223                 if (defined $exp) {
4224                         seek($fh, 0, 0) or croak $!;
4225                         my $got = ::md5sum($fh);
4226                         if ($got ne $exp) {
4227                                 die "Checksum mismatch: $path\n",
4228                                     "expected: $exp\n    got: $got\n";
4229                         }
4230                 }
4231                 if ($fb->{mode_b} == 120000) {
4232                         sysseek($fh, 0, 0) or croak $!;
4233                         my $rd = sysread($fh, my $buf, 5);
4234
4235                         if (!defined $rd) {
4236                                 croak "sysread: $!\n";
4237                         } elsif ($rd == 0) {
4238                                 warn "$path has mode 120000",
4239                                      " but it points to nothing\n",
4240                                      "converting to an empty file with mode",
4241                                      " 100644\n";
4242                                 $fb->{mode_b} = '100644';
4243                         } elsif ($buf ne 'link ') {
4244                                 warn "$path has mode 120000",
4245                                      " but is not a link\n";
4246                         } else {
4247                                 my $tmp_fh = $::_repository->temp_acquire(
4248                                         'svn_hash');
4249                                 my $res;
4250                                 while ($res = sysread($fh, my $str, 1024)) {
4251                                         my $out = syswrite($tmp_fh, $str, $res);
4252                                         defined($out) && $out == $res
4253                                                 or croak("write ",
4254                                                         Git::temp_path($tmp_fh),
4255                                                         ": $!\n");
4256                                 }
4257                                 defined $res or croak $!;
4258
4259                                 ($fh, $tmp_fh) = ($tmp_fh, $fh);
4260                                 Git::temp_release($tmp_fh, 1);
4261                         }
4262                 }
4263
4264                 $hash = $::_repository->hash_and_insert_object(
4265                                 Git::temp_path($fh));
4266                 $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
4267
4268                 Git::temp_release($fb->{base}, 1);
4269                 Git::temp_release($fh, 1);
4270         } else {
4271                 $hash = $fb->{blob} or die "no blob information\n";
4272         }
4273         $fb->{pool}->clear;
4274         $self->{gii}->update($fb->{mode_b}, $hash, $path) or croak $!;
4275         print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $::_q;
4276         undef;
4277 }
4278
4279 sub abort_edit {
4280         my $self = shift;
4281         $self->{nr} = $self->{gii}->{nr};
4282         delete $self->{gii};
4283         $self->SUPER::abort_edit(@_);
4284 }
4285
4286 sub close_edit {
4287         my $self = shift;
4288         $self->{git_commit_ok} = 1;
4289         $self->{nr} = $self->{gii}->{nr};
4290         delete $self->{gii};
4291         $self->SUPER::close_edit(@_);
4292 }
4293
4294 package SVN::Git::Editor;
4295 use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;
4296 use strict;
4297 use warnings;
4298 use Carp qw/croak/;
4299 use IO::File;
4300
4301 sub new {
4302         my ($class, $opts) = @_;
4303         foreach (qw/svn_path r ra tree_a tree_b log editor_cb/) {
4304                 die "$_ required!\n" unless (defined $opts->{$_});
4305         }
4306
4307         my $pool = SVN::Pool->new;
4308         my $mods = generate_diff($opts->{tree_a}, $opts->{tree_b});
4309         my $types = check_diff_paths($opts->{ra}, $opts->{svn_path},
4310                                      $opts->{r}, $mods);
4311
4312         # $opts->{ra} functions should not be used after this:
4313         my @ce  = $opts->{ra}->get_commit_editor($opts->{log},
4314                                                 $opts->{editor_cb}, $pool);
4315         my $self = SVN::Delta::Editor->new(@ce, $pool);
4316         bless $self, $class;
4317         foreach (qw/svn_path r tree_a tree_b/) {
4318                 $self->{$_} = $opts->{$_};
4319         }
4320         $self->{url} = $opts->{ra}->{url};
4321         $self->{mods} = $mods;
4322         $self->{types} = $types;
4323         $self->{pool} = $pool;
4324         $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
4325         $self->{rm} = { };
4326         $self->{path_prefix} = length $self->{svn_path} ?
4327                                "$self->{svn_path}/" : '';
4328         $self->{config} = $opts->{config};
4329         return $self;
4330 }
4331
4332 sub generate_diff {
4333         my ($tree_a, $tree_b) = @_;
4334         my @diff_tree = qw(diff-tree -z -r);
4335         if ($_cp_similarity) {
4336                 push @diff_tree, "-C$_cp_similarity";
4337         } else {
4338                 push @diff_tree, '-C';
4339         }
4340         push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
4341         push @diff_tree, "-l$_rename_limit" if defined $_rename_limit;
4342         push @diff_tree, $tree_a, $tree_b;
4343         my ($diff_fh, $ctx) = command_output_pipe(@diff_tree);
4344         local $/ = "\0";
4345         my $state = 'meta';
4346         my @mods;
4347         while (<$diff_fh>) {
4348                 chomp $_; # this gets rid of the trailing "\0"
4349                 if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
4350                                         ($::sha1)\s($::sha1)\s
4351                                         ([MTCRAD])\d*$/xo) {
4352                         push @mods, {   mode_a => $1, mode_b => $2,
4353                                         sha1_a => $3, sha1_b => $4,
4354                                         chg => $5 };
4355                         if ($5 =~ /^(?:C|R)$/) {
4356                                 $state = 'file_a';
4357                         } else {
4358                                 $state = 'file_b';
4359                         }
4360                 } elsif ($state eq 'file_a') {
4361                         my $x = $mods[$#mods] or croak "Empty array\n";
4362                         if ($x->{chg} !~ /^(?:C|R)$/) {
4363                                 croak "Error parsing $_, $x->{chg}\n";
4364                         }
4365                         $x->{file_a} = $_;
4366                         $state = 'file_b';
4367                 } elsif ($state eq 'file_b') {
4368                         my $x = $mods[$#mods] or croak "Empty array\n";
4369                         if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
4370                                 croak "Error parsing $_, $x->{chg}\n";
4371                         }
4372                         if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
4373                                 croak "Error parsing $_, $x->{chg}\n";
4374                         }
4375                         $x->{file_b} = $_;
4376                         $state = 'meta';
4377                 } else {
4378                         croak "Error parsing $_\n";
4379                 }
4380         }
4381         command_close_pipe($diff_fh, $ctx);
4382         \@mods;
4383 }
4384
4385 sub check_diff_paths {
4386         my ($ra, $pfx, $rev, $mods) = @_;
4387         my %types;
4388         $pfx .= '/' if length $pfx;
4389
4390         sub type_diff_paths {
4391                 my ($ra, $types, $path, $rev) = @_;
4392                 my @p = split m#/+#, $path;
4393                 my $c = shift @p;
4394                 unless (defined $types->{$c}) {
4395                         $types->{$c} = $ra->check_path($c, $rev);
4396                 }
4397                 while (@p) {
4398                         $c .= '/' . shift @p;
4399                         next if defined $types->{$c};
4400                         $types->{$c} = $ra->check_path($c, $rev);
4401                 }
4402         }
4403
4404         foreach my $m (@$mods) {
4405                 foreach my $f (qw/file_a file_b/) {
4406                         next unless defined $m->{$f};
4407                         my ($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);
4408                         if (length $pfx.$dir && ! defined $types{$dir}) {
4409                                 type_diff_paths($ra, \%types, $pfx.$dir, $rev);
4410                         }
4411                 }
4412         }
4413         \%types;
4414 }
4415
4416 sub split_path {
4417         return ($_[0] =~ m#^(.*?)/?([^/]+)$#);
4418 }
4419
4420 sub repo_path {
4421         my ($self, $path) = @_;
4422         $self->{path_prefix}.(defined $path ? $path : '');
4423 }
4424
4425 sub url_path {
4426         my ($self, $path) = @_;
4427         if ($self->{url} =~ m#^https?://#) {
4428                 $path =~ s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;
4429         }
4430         $self->{url} . '/' . $self->repo_path($path);
4431 }
4432
4433 sub rmdirs {
4434         my ($self) = @_;
4435         my $rm = $self->{rm};
4436         delete $rm->{''}; # we never delete the url we're tracking
4437         return unless %$rm;
4438
4439         foreach (keys %$rm) {
4440                 my @d = split m#/#, $_;
4441                 my $c = shift @d;
4442                 $rm->{$c} = 1;
4443                 while (@d) {
4444                         $c .= '/' . shift @d;
4445                         $rm->{$c} = 1;
4446                 }
4447         }
4448         delete $rm->{$self->{svn_path}};
4449         delete $rm->{''}; # we never delete the url we're tracking
4450         return unless %$rm;
4451
4452         my ($fh, $ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,
4453                                              $self->{tree_b});
4454         local $/ = "\0";
4455         while (<$fh>) {
4456                 chomp;
4457                 my @dn = split m#/#, $_;
4458                 while (pop @dn) {
4459                         delete $rm->{join '/', @dn};
4460                 }
4461                 unless (%$rm) {
4462                         close $fh;
4463                         return;
4464                 }
4465         }
4466         command_close_pipe($fh, $ctx);
4467
4468         my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat});
4469         foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
4470                 $self->close_directory($bat->{$d}, $p);
4471                 my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#);
4472                 print "\tD+\t$d/\n" unless $::_q;
4473                 $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p);
4474                 delete $bat->{$d};
4475         }
4476 }
4477
4478 sub open_or_add_dir {
4479         my ($self, $full_path, $baton) = @_;
4480         my $t = $self->{types}->{$full_path};
4481         if (!defined $t) {
4482                 die "$full_path not known in r$self->{r} or we have a bug!\n";
4483         }
4484         {
4485                 no warnings 'once';
4486                 # SVN::Node::none and SVN::Node::file are used only once,
4487                 # so we're shutting up Perl's warnings about them.
4488                 if ($t == $SVN::Node::none) {
4489                         return $self->add_directory($full_path, $baton,
4490                             undef, -1, $self->{pool});
4491                 } elsif ($t == $SVN::Node::dir) {
4492                         return $self->open_directory($full_path, $baton,
4493                             $self->{r}, $self->{pool});
4494                 } # no warnings 'once'
4495                 print STDERR "$full_path already exists in repository at ",
4496                     "r$self->{r} and it is not a directory (",
4497                     ($t == $SVN::Node::file ? 'file' : 'unknown'),"/$t)\n";
4498         } # no warnings 'once'
4499         exit 1;
4500 }
4501
4502 sub ensure_path {
4503         my ($self, $path) = @_;
4504         my $bat = $self->{bat};
4505         my $repo_path = $self->repo_path($path);
4506         return $bat->{''} unless (length $repo_path);
4507         my @p = split m#/+#, $repo_path;
4508         my $c = shift @p;
4509         $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''});
4510         while (@p) {
4511                 my $c0 = $c;
4512                 $c .= '/' . shift @p;
4513                 $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0});
4514         }
4515         return $bat->{$c};
4516 }
4517
4518 # Subroutine to convert a globbing pattern to a regular expression.
4519 # From perl cookbook.
4520 sub glob2pat {
4521         my $globstr = shift;
4522         my %patmap = ('*' => '.*', '?' => '.', '[' => '[', ']' => ']');
4523         $globstr =~ s{(.)} { $patmap{$1} || "\Q$1" }ge;
4524         return '^' . $globstr . '$';
4525 }
4526
4527 sub check_autoprop {
4528         my ($self, $pattern, $properties, $file, $fbat) = @_;
4529         # Convert the globbing pattern to a regular expression.
4530         my $regex = glob2pat($pattern);
4531         # Check if the pattern matches the file name.
4532         if($file =~ m/($regex)/) {
4533                 # Parse the list of properties to set.
4534                 my @props = split(/;/, $properties);
4535                 foreach my $prop (@props) {
4536                         # Parse 'name=value' syntax and set the property.
4537                         if ($prop =~ /([^=]+)=(.*)/) {
4538                                 my ($n,$v) = ($1,$2);
4539                                 for ($n, $v) {
4540                                         s/^\s+//; s/\s+$//;
4541                                 }
4542                                 $self->change_file_prop($fbat, $n, $v);
4543                         }
4544                 }
4545         }
4546 }
4547
4548 sub apply_autoprops {
4549         my ($self, $file, $fbat) = @_;
4550         my $conf_t = ${$self->{config}}{'config'};
4551         no warnings 'once';
4552         # Check [miscellany]/enable-auto-props in svn configuration.
4553         if (SVN::_Core::svn_config_get_bool(
4554                 $conf_t,
4555                 $SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY,
4556                 $SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS,
4557                 0)) {
4558                 # Auto-props are enabled.  Enumerate them to look for matches.
4559                 my $callback = sub {
4560                         $self->check_autoprop($_[0], $_[1], $file, $fbat);
4561                 };
4562                 SVN::_Core::svn_config_enumerate(
4563                         $conf_t,
4564                         $SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS,
4565                         $callback);
4566         }
4567 }
4568
4569 sub A {
4570         my ($self, $m) = @_;
4571         my ($dir, $file) = split_path($m->{file_b});
4572         my $pbat = $self->ensure_path($dir);
4573         my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
4574                                         undef, -1);
4575         print "\tA\t$m->{file_b}\n" unless $::_q;
4576         $self->apply_autoprops($file, $fbat);
4577         $self->chg_file($fbat, $m);
4578         $self->close_file($fbat,undef,$self->{pool});
4579 }
4580
4581 sub C {
4582         my ($self, $m) = @_;
4583         my ($dir, $file) = split_path($m->{file_b});
4584         my $pbat = $self->ensure_path($dir);
4585         my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
4586                                 $self->url_path($m->{file_a}), $self->{r});
4587         print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
4588         $self->chg_file($fbat, $m);
4589         $self->close_file($fbat,undef,$self->{pool});
4590 }
4591
4592 sub delete_entry {
4593         my ($self, $path, $pbat) = @_;
4594         my $rpath = $self->repo_path($path);
4595         my ($dir, $file) = split_path($rpath);
4596         $self->{rm}->{$dir} = 1;
4597         $self->SUPER::delete_entry($rpath, $self->{r}, $pbat, $self->{pool});
4598 }
4599
4600 sub R {
4601         my ($self, $m) = @_;
4602         my ($dir, $file) = split_path($m->{file_b});
4603         my $pbat = $self->ensure_path($dir);
4604         my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
4605                                 $self->url_path($m->{file_a}), $self->{r});
4606         print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
4607         $self->apply_autoprops($file, $fbat);
4608         $self->chg_file($fbat, $m);
4609         $self->close_file($fbat,undef,$self->{pool});
4610
4611         ($dir, $file) = split_path($m->{file_a});
4612         $pbat = $self->ensure_path($dir);
4613         $self->delete_entry($m->{file_a}, $pbat);
4614 }
4615
4616 sub M {
4617         my ($self, $m) = @_;
4618         my ($dir, $file) = split_path($m->{file_b});
4619         my $pbat = $self->ensure_path($dir);
4620         my $fbat = $self->open_file($self->repo_path($m->{file_b}),
4621                                 $pbat,$self->{r},$self->{pool});
4622         print "\t$m->{chg}\t$m->{file_b}\n" unless $::_q;
4623         $self->chg_file($fbat, $m);
4624         $self->close_file($fbat,undef,$self->{pool});
4625 }
4626
4627 sub T { shift->M(@_) }
4628
4629 sub change_file_prop {
4630         my ($self, $fbat, $pname, $pval) = @_;
4631         $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool});
4632 }
4633
4634 sub _chg_file_get_blob ($$$$) {
4635         my ($self, $fbat, $m, $which) = @_;
4636         my $fh = $::_repository->temp_acquire("git_blob_$which");
4637         if ($m->{"mode_$which"} =~ /^120/) {
4638                 print $fh 'link ' or croak $!;
4639                 $self->change_file_prop($fbat,'svn:special','*');
4640         } elsif ($m->{mode_a} =~ /^120/ && $m->{"mode_$which"} !~ /^120/) {
4641                 $self->change_file_prop($fbat,'svn:special',undef);
4642         }
4643         my $blob = $m->{"sha1_$which"};
4644         return ($fh,) if ($blob =~ /^0{40}$/);
4645         my $size = $::_repository->cat_blob($blob, $fh);
4646         croak "Failed to read object $blob" if ($size < 0);
4647         $fh->flush == 0 or croak $!;
4648         seek $fh, 0, 0 or croak $!;
4649
4650         my $exp = ::md5sum($fh);
4651         seek $fh, 0, 0 or croak $!;
4652         return ($fh, $exp);
4653 }
4654
4655 sub chg_file {
4656         my ($self, $fbat, $m) = @_;
4657         if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
4658                 $self->change_file_prop($fbat,'svn:executable','*');
4659         } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
4660                 $self->change_file_prop($fbat,'svn:executable',undef);
4661         }
4662         my ($fh_a, $exp_a) = _chg_file_get_blob $self, $fbat, $m, 'a';
4663         my ($fh_b, $exp_b) = _chg_file_get_blob $self, $fbat, $m, 'b';
4664         my $pool = SVN::Pool->new;
4665         my $atd = $self->apply_textdelta($fbat, $exp_a, $pool);
4666         if (-s $fh_a) {
4667                 my $txstream = SVN::TxDelta::new ($fh_a, $fh_b, $pool);
4668                 my $res = SVN::TxDelta::send_txstream($txstream, @$atd, $pool);
4669                 if (defined $res) {
4670                         die "Unexpected result from send_txstream: $res\n",
4671                             "(SVN::Core::VERSION: $SVN::Core::VERSION)\n";
4672                 }
4673         } else {
4674                 my $got = SVN::TxDelta::send_stream($fh_b, @$atd, $pool);
4675                 die "Checksum mismatch\nexpected: $exp_b\ngot: $got\n"
4676                     if ($got ne $exp_b);
4677         }
4678         Git::temp_release($fh_b, 1);
4679         Git::temp_release($fh_a, 1);
4680         $pool->clear;
4681 }
4682
4683 sub D {
4684         my ($self, $m) = @_;
4685         my ($dir, $file) = split_path($m->{file_b});
4686         my $pbat = $self->ensure_path($dir);
4687         print "\tD\t$m->{file_b}\n" unless $::_q;
4688         $self->delete_entry($m->{file_b}, $pbat);
4689 }
4690
4691 sub close_edit {
4692         my ($self) = @_;
4693         my ($p,$bat) = ($self->{pool}, $self->{bat});
4694         foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {
4695                 next if $_ eq '';
4696                 $self->close_directory($bat->{$_}, $p);
4697         }
4698         $self->close_directory($bat->{''}, $p);
4699         $self->SUPER::close_edit($p);
4700         $p->clear;
4701 }
4702
4703 sub abort_edit {
4704         my ($self) = @_;
4705         $self->SUPER::abort_edit($self->{pool});
4706 }
4707
4708 sub DESTROY {
4709         my $self = shift;
4710         $self->SUPER::DESTROY(@_);
4711         $self->{pool}->clear;
4712 }
4713
4714 # this drives the editor
4715 sub apply_diff {
4716         my ($self) = @_;
4717         my $mods = $self->{mods};
4718         my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
4719         foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
4720                 my $f = $m->{chg};
4721                 if (defined $o{$f}) {
4722                         $self->$f($m);
4723                 } else {
4724                         fatal("Invalid change type: $f");
4725                 }
4726         }
4727         $self->rmdirs if $_rmdir;
4728         if (@$mods == 0) {
4729                 $self->abort_edit;
4730         } else {
4731                 $self->close_edit;
4732         }
4733         return scalar @$mods;
4734 }
4735
4736 package Git::SVN::Ra;
4737 use vars qw/@ISA $config_dir $_log_window_size/;
4738 use strict;
4739 use warnings;
4740 my ($ra_invalid, $can_do_switch, %ignored_err, $RA);
4741
4742 BEGIN {
4743         # enforce temporary pool usage for some simple functions
4744         no strict 'refs';
4745         for my $f (qw/rev_proplist get_latest_revnum get_uuid get_repos_root
4746                       get_file/) {
4747                 my $SUPER = "SUPER::$f";
4748                 *$f = sub {
4749                         my $self = shift;
4750                         my $pool = SVN::Pool->new;
4751                         my @ret = $self->$SUPER(@_,$pool);
4752                         $pool->clear;
4753                         wantarray ? @ret : $ret[0];
4754                 };
4755         }
4756 }
4757
4758 sub _auth_providers () {
4759         [
4760           SVN::Client::get_simple_provider(),
4761           SVN::Client::get_ssl_server_trust_file_provider(),
4762           SVN::Client::get_simple_prompt_provider(
4763             \&Git::SVN::Prompt::simple, 2),
4764           SVN::Client::get_ssl_client_cert_file_provider(),
4765           SVN::Client::get_ssl_client_cert_prompt_provider(
4766             \&Git::SVN::Prompt::ssl_client_cert, 2),
4767           SVN::Client::get_ssl_client_cert_pw_file_provider(),
4768           SVN::Client::get_ssl_client_cert_pw_prompt_provider(
4769             \&Git::SVN::Prompt::ssl_client_cert_pw, 2),
4770           SVN::Client::get_username_provider(),
4771           SVN::Client::get_ssl_server_trust_prompt_provider(
4772             \&Git::SVN::Prompt::ssl_server_trust),
4773           SVN::Client::get_username_prompt_provider(
4774             \&Git::SVN::Prompt::username, 2)
4775         ]
4776 }
4777
4778 sub escape_uri_only {
4779         my ($uri) = @_;
4780         my @tmp;
4781         foreach (split m{/}, $uri) {
4782                 s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;
4783                 push @tmp, $_;
4784         }
4785         join('/', @tmp);
4786 }
4787
4788 sub escape_url {
4789         my ($url) = @_;
4790         if ($url =~ m#^(https?)://([^/]+)(.*)$#) {
4791                 my ($scheme, $domain, $uri) = ($1, $2, escape_uri_only($3));
4792                 $url = "$scheme://$domain$uri";
4793         }
4794         $url;
4795 }
4796
4797 sub new {
4798         my ($class, $url) = @_;
4799         $url =~ s!/+$!!;
4800         return $RA if ($RA && $RA->{url} eq $url);
4801
4802         SVN::_Core::svn_config_ensure($config_dir, undef);
4803         my ($baton, $callbacks) = SVN::Core::auth_open_helper(_auth_providers);
4804         my $config = SVN::Core::config_get_config($config_dir);
4805         $RA = undef;
4806         my $dont_store_passwords = 1;
4807         my $conf_t = ${$config}{'config'};
4808         {
4809                 no warnings 'once';
4810                 # The usage of $SVN::_Core::SVN_CONFIG_* variables
4811                 # produces warnings that variables are used only once.
4812                 # I had not found the better way to shut them up, so
4813                 # the warnings of type 'once' are disabled in this block.
4814                 if (SVN::_Core::svn_config_get_bool($conf_t,
4815                     $SVN::_Core::SVN_CONFIG_SECTION_AUTH,
4816                     $SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS,
4817                     1) == 0) {
4818                         SVN::_Core::svn_auth_set_parameter($baton,
4819                             $SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS,
4820                             bless (\$dont_store_passwords, "_p_void"));
4821                 }
4822                 if (SVN::_Core::svn_config_get_bool($conf_t,
4823                     $SVN::_Core::SVN_CONFIG_SECTION_AUTH,
4824                     $SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS,
4825                     1) == 0) {
4826                         $Git::SVN::Prompt::_no_auth_cache = 1;
4827                 }
4828         } # no warnings 'once'
4829         my $self = SVN::Ra->new(url => escape_url($url), auth => $baton,
4830                               config => $config,
4831                               pool => SVN::Pool->new,
4832                               auth_provider_callbacks => $callbacks);
4833         $self->{url} = $url;
4834         $self->{svn_path} = $url;
4835         $self->{repos_root} = $self->get_repos_root;
4836         $self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;
4837         $self->{cache} = { check_path => { r => 0, data => {} },
4838                            get_dir => { r => 0, data => {} } };
4839         $RA = bless $self, $class;
4840 }
4841
4842 sub check_path {
4843         my ($self, $path, $r) = @_;
4844         my $cache = $self->{cache}->{check_path};
4845         if ($r == $cache->{r} && exists $cache->{data}->{$path}) {
4846                 return $cache->{data}->{$path};
4847         }
4848         my $pool = SVN::Pool->new;
4849         my $t = $self->SUPER::check_path($path, $r, $pool);
4850         $pool->clear;
4851         if ($r != $cache->{r}) {
4852                 %{$cache->{data}} = ();
4853                 $cache->{r} = $r;
4854         }
4855         $cache->{data}->{$path} = $t;
4856 }
4857
4858 sub get_dir {
4859         my ($self, $dir, $r) = @_;
4860         my $cache = $self->{cache}->{get_dir};
4861         if ($r == $cache->{r}) {
4862                 if (my $x = $cache->{data}->{$dir}) {
4863                         return wantarray ? @$x : $x->[0];
4864                 }
4865         }
4866         my $pool = SVN::Pool->new;
4867         my ($d, undef, $props) = $self->SUPER::get_dir($dir, $r, $pool);
4868         my %dirents = map { $_ => { kind => $d->{$_}->kind } } keys %$d;
4869         $pool->clear;
4870         if ($r != $cache->{r}) {
4871                 %{$cache->{data}} = ();
4872                 $cache->{r} = $r;
4873         }
4874         $cache->{data}->{$dir} = [ \%dirents, $r, $props ];
4875         wantarray ? (\%dirents, $r, $props) : \%dirents;
4876 }
4877
4878 sub DESTROY {
4879         # do not call the real DESTROY since we store ourselves in $RA
4880 }
4881
4882 # get_log(paths, start, end, limit,
4883 #         discover_changed_paths, strict_node_history, receiver)
4884 sub get_log {
4885         my ($self, @args) = @_;
4886         my $pool = SVN::Pool->new;
4887
4888         # svn_log_changed_path_t objects passed to get_log are likely to be
4889         # overwritten even if only the refs are copied to an external variable,
4890         # so we should dup the structures in their entirety.  Using an
4891         # externally passed pool (instead of our temporary and quickly cleared
4892         # pool in Git::SVN::Ra) does not help matters at all...
4893         my $receiver = pop @args;
4894         my $prefix = "/".$self->{svn_path};
4895         $prefix =~ s#/+($)##;
4896         my $prefix_regex = qr#^\Q$prefix\E#;
4897         push(@args, sub {
4898                 my ($paths) = $_[0];
4899                 return &$receiver(@_) unless $paths;
4900                 $_[0] = ();
4901                 foreach my $p (keys %$paths) {
4902                         my $i = $paths->{$p};
4903                         # Make path relative to our url, not repos_root
4904                         $p =~ s/$prefix_regex//;
4905                         my %s = map { $_ => $i->$_; }
4906                                 qw/copyfrom_path copyfrom_rev action/;
4907                         if ($s{'copyfrom_path'}) {
4908                                 $s{'copyfrom_path'} =~ s/$prefix_regex//;
4909                         }
4910                         $_[0]{$p} = \%s;
4911                 }
4912                 &$receiver(@_);
4913         });
4914
4915
4916         # the limit parameter was not supported in SVN 1.1.x, so we
4917         # drop it.  Therefore, the receiver callback passed to it
4918         # is made aware of this limitation by being wrapped if
4919         # the limit passed to is being wrapped.
4920         if ($SVN::Core::VERSION le '1.2.0') {
4921                 my $limit = splice(@args, 3, 1);
4922                 if ($limit > 0) {
4923                         my $receiver = pop @args;
4924                         push(@args, sub { &$receiver(@_) if (--$limit >= 0) });
4925                 }
4926         }
4927         my $ret = $self->SUPER::get_log(@args, $pool);
4928         $pool->clear;
4929         $ret;
4930 }
4931
4932 sub trees_match {
4933         my ($self, $url1, $rev1, $url2, $rev2) = @_;
4934         my $ctx = SVN::Client->new(auth => _auth_providers);
4935         my $out = IO::File->new_tmpfile;
4936
4937         # older SVN (1.1.x) doesn't take $pool as the last parameter for
4938         # $ctx->diff(), so we'll create a default one
4939         my $pool = SVN::Pool->new_default_sub;
4940
4941         $ra_invalid = 1; # this will open a new SVN::Ra connection to $url1
4942         $ctx->diff([], $url1, $rev1, $url2, $rev2, 1, 1, 0, $out, $out);
4943         $out->flush;
4944         my $ret = (($out->stat)[7] == 0);
4945         close $out or croak $!;
4946
4947         $ret;
4948 }
4949
4950 sub get_commit_editor {
4951         my ($self, $log, $cb, $pool) = @_;
4952         my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
4953         $self->SUPER::get_commit_editor($log, $cb, @lock, $pool);
4954 }
4955
4956 sub gs_do_update {
4957         my ($self, $rev_a, $rev_b, $gs, $editor) = @_;
4958         my $new = ($rev_a == $rev_b);
4959         my $path = $gs->{path};
4960
4961         if ($new && -e $gs->{index}) {
4962                 unlink $gs->{index} or die
4963                   "Couldn't unlink index: $gs->{index}: $!\n";
4964         }
4965         my $pool = SVN::Pool->new;
4966         $editor->set_path_strip($path);
4967         my (@pc) = split m#/#, $path;
4968         my $reporter = $self->do_update($rev_b, (@pc ? shift @pc : ''),
4969                                         1, $editor, $pool);
4970         my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
4971
4972         # Since we can't rely on svn_ra_reparent being available, we'll
4973         # just have to do some magic with set_path to make it so
4974         # we only want a partial path.
4975         my $sp = '';
4976         my $final = join('/', @pc);
4977         while (@pc) {
4978                 $reporter->set_path($sp, $rev_b, 0, @lock, $pool);
4979                 $sp .= '/' if length $sp;
4980                 $sp .= shift @pc;
4981         }
4982         die "BUG: '$sp' != '$final'\n" if ($sp ne $final);
4983
4984         $reporter->set_path($sp, $rev_a, $new, @lock, $pool);
4985
4986         $reporter->finish_report($pool);
4987         $pool->clear;
4988         $editor->{git_commit_ok};
4989 }
4990
4991 # this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and
4992 # svn_ra_reparent didn't work before 1.4)
4993 sub gs_do_switch {
4994         my ($self, $rev_a, $rev_b, $gs, $url_b, $editor) = @_;
4995         my $path = $gs->{path};
4996         my $pool = SVN::Pool->new;
4997
4998         my $full_url = $self->{url};
4999         my $old_url = $full_url;
5000         $full_url .= '/' . $path if length $path;
5001         my ($ra, $reparented);
5002
5003         if ($old_url =~ m#^svn(\+ssh)?://# ||
5004             ($full_url =~ m#^https?://# &&
5005              escape_url($full_url) ne $full_url)) {
5006                 $_[0] = undef;
5007                 $self = undef;
5008                 $RA = undef;
5009                 $ra = Git::SVN::Ra->new($full_url);
5010                 $ra_invalid = 1;
5011         } elsif ($old_url ne $full_url) {
5012                 SVN::_Ra::svn_ra_reparent($self->{session}, $full_url, $pool);
5013                 $self->{url} = $full_url;
5014                 $reparented = 1;
5015         }
5016
5017         $ra ||= $self;
5018         $url_b = escape_url($url_b);
5019         my $reporter = $ra->do_switch($rev_b, '', 1, $url_b, $editor, $pool);
5020         my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : ();
5021         $reporter->set_path('', $rev_a, 0, @lock, $pool);
5022         $reporter->finish_report($pool);
5023
5024         if ($reparented) {
5025                 SVN::_Ra::svn_ra_reparent($self->{session}, $old_url, $pool);
5026                 $self->{url} = $old_url;
5027         }
5028
5029         $pool->clear;
5030         $editor->{git_commit_ok};
5031 }
5032
5033 sub longest_common_path {
5034         my ($gsv, $globs) = @_;
5035         my %common;
5036         my $common_max = scalar @$gsv;
5037
5038         foreach my $gs (@$gsv) {
5039                 my @tmp = split m#/#, $gs->{path};
5040                 my $p = '';
5041                 foreach (@tmp) {
5042                         $p .= length($p) ? "/$_" : $_;
5043                         $common{$p} ||= 0;
5044                         $common{$p}++;
5045                 }
5046         }
5047         $globs ||= [];
5048         $common_max += scalar @$globs;
5049         foreach my $glob (@$globs) {
5050                 my @tmp = split m#/#, $glob->{path}->{left};
5051                 my $p = '';
5052                 foreach (@tmp) {
5053                         $p .= length($p) ? "/$_" : $_;
5054                         $common{$p} ||= 0;
5055                         $common{$p}++;
5056                 }
5057         }
5058
5059         my $longest_path = '';
5060         foreach (sort {length $b <=> length $a} keys %common) {
5061                 if ($common{$_} == $common_max) {
5062                         $longest_path = $_;
5063                         last;
5064                 }
5065         }
5066         $longest_path;
5067 }
5068
5069 sub gs_fetch_loop_common {
5070         my ($self, $base, $head, $gsv, $globs) = @_;
5071         return if ($base > $head);
5072         my $inc = $_log_window_size;
5073         my ($min, $max) = ($base, $head < $base + $inc ? $head : $base + $inc);
5074         my $longest_path = longest_common_path($gsv, $globs);
5075         my $ra_url = $self->{url};
5076         my $find_trailing_edge;
5077         while (1) {
5078                 my %revs;
5079                 my $err;
5080                 my $err_handler = $SVN::Error::handler;
5081                 $SVN::Error::handler = sub {
5082                         ($err) = @_;
5083                         skip_unknown_revs($err);
5084                 };
5085                 sub _cb {
5086                         my ($paths, $r, $author, $date, $log) = @_;
5087                         [ $paths,
5088                           { author => $author, date => $date, log => $log } ];
5089                 }
5090                 $self->get_log([$longest_path], $min, $max, 0, 1, 1,
5091                                sub { $revs{$_[1]} = _cb(@_) });
5092                 if ($err) {
5093                         print "Checked through r$max\r";
5094                 } else {
5095                         $find_trailing_edge = 1;
5096                 }
5097                 if ($err and $find_trailing_edge) {
5098                         print STDERR "Path '$longest_path' ",
5099                                      "was probably deleted:\n",
5100                                      $err->expanded_message,
5101                                      "\nWill attempt to follow ",
5102                                      "revisions r$min .. r$max ",
5103                                      "committed before the deletion\n";
5104                         my $hi = $max;
5105                         while (--$hi >= $min) {
5106                                 my $ok;
5107                                 $self->get_log([$longest_path], $min, $hi,
5108                                                0, 1, 1, sub {
5109                                                $ok = $_[1];
5110                                                $revs{$_[1]} = _cb(@_) });
5111                                 if ($ok) {
5112                                         print STDERR "r$min .. r$ok OK\n";
5113                                         last;
5114                                 }
5115                         }
5116                         $find_trailing_edge = 0;
5117                 }
5118                 $SVN::Error::handler = $err_handler;
5119
5120                 my %exists = map { $_->{path} => $_ } @$gsv;
5121                 foreach my $r (sort {$a <=> $b} keys %revs) {
5122                         my ($paths, $logged) = @{$revs{$r}};
5123
5124                         foreach my $gs ($self->match_globs(\%exists, $paths,
5125                                                            $globs, $r)) {
5126                                 if ($gs->rev_map_max >= $r) {
5127                                         next;
5128                                 }
5129                                 next unless $gs->match_paths($paths, $r);
5130                                 $gs->{logged_rev_props} = $logged;
5131                                 if (my $last_commit = $gs->last_commit) {
5132                                         $gs->assert_index_clean($last_commit);
5133                                 }
5134                                 my $log_entry = $gs->do_fetch($paths, $r);
5135                                 if ($log_entry) {
5136                                         $gs->do_git_commit($log_entry);
5137                                 }
5138                                 $INDEX_FILES{$gs->{index}} = 1;
5139                         }
5140                         foreach my $g (@$globs) {
5141                                 my $k = "svn-remote.$g->{remote}." .
5142                                         "$g->{t}-maxRev";
5143                                 Git::SVN::tmp_config($k, $r);
5144                         }
5145                         if ($ra_invalid) {
5146                                 $_[0] = undef;
5147                                 $self = undef;
5148                                 $RA = undef;
5149                                 $self = Git::SVN::Ra->new($ra_url);
5150                                 $ra_invalid = undef;
5151                         }
5152                 }
5153                 # pre-fill the .rev_db since it'll eventually get filled in
5154                 # with '0' x40 if something new gets committed
5155                 foreach my $gs (@$gsv) {
5156                         next if $gs->rev_map_max >= $max;
5157                         next if defined $gs->rev_map_get($max);
5158                         $gs->rev_map_set($max, 0 x40);
5159                 }
5160                 foreach my $g (@$globs) {
5161                         my $k = "svn-remote.$g->{remote}.$g->{t}-maxRev";
5162                         Git::SVN::tmp_config($k, $max);
5163                 }
5164                 last if $max >= $head;
5165                 $min = $max + 1;
5166                 $max += $inc;
5167                 $max = $head if ($max > $head);
5168         }
5169         Git::SVN::gc();
5170 }
5171
5172 sub get_dir_globbed {
5173         my ($self, $left, $depth, $r) = @_;
5174
5175         my @x = eval { $self->get_dir($left, $r) };
5176         return unless scalar @x == 3;
5177         my $dirents = $x[0];
5178         my @finalents;
5179         foreach my $de (keys %$dirents) {
5180                 next if $dirents->{$de}->{kind} != $SVN::Node::dir;
5181                 if ($depth > 1) {
5182                         my @args = ("$left/$de", $depth - 1, $r);
5183                         foreach my $dir ($self->get_dir_globbed(@args)) {
5184                                 push @finalents, "$de/$dir";
5185                         }
5186                 } else {
5187                         push @finalents, $de;
5188                 }
5189         }
5190         @finalents;
5191 }
5192
5193 sub match_globs {
5194         my ($self, $exists, $paths, $globs, $r) = @_;
5195
5196         sub get_dir_check {
5197                 my ($self, $exists, $g, $r) = @_;
5198
5199                 my @dirs = $self->get_dir_globbed($g->{path}->{left},
5200                                                   $g->{path}->{depth},
5201                                                   $r);
5202
5203                 foreach my $de (@dirs) {
5204                         my $p = $g->{path}->full_path($de);
5205                         next if $exists->{$p};
5206                         next if (length $g->{path}->{right} &&
5207                                  ($self->check_path($p, $r) !=
5208                                   $SVN::Node::dir));
5209                         $exists->{$p} = Git::SVN->init($self->{url}, $p, undef,
5210                                          $g->{ref}->full_path($de), 1);
5211                 }
5212         }
5213         foreach my $g (@$globs) {
5214                 if (my $path = $paths->{"/$g->{path}->{left}"}) {
5215                         if ($path->{action} =~ /^[AR]$/) {
5216                                 get_dir_check($self, $exists, $g, $r);
5217                         }
5218                 }
5219                 foreach (keys %$paths) {
5220                         if (/$g->{path}->{left_regex}/ &&
5221                             !/$g->{path}->{regex}/) {
5222                                 next if $paths->{$_}->{action} !~ /^[AR]$/;
5223                                 get_dir_check($self, $exists, $g, $r);
5224                         }
5225                         next unless /$g->{path}->{regex}/;
5226                         my $p = $1;
5227                         my $pathname = $g->{path}->full_path($p);
5228                         next if $exists->{$pathname};
5229                         next if ($self->check_path($pathname, $r) !=
5230                                  $SVN::Node::dir);
5231                         $exists->{$pathname} = Git::SVN->init(
5232                                               $self->{url}, $pathname, undef,
5233                                               $g->{ref}->full_path($p), 1);
5234                 }
5235                 my $c = '';
5236                 foreach (split m#/#, $g->{path}->{left}) {
5237                         $c .= "/$_";
5238                         next unless ($paths->{$c} &&
5239                                      ($paths->{$c}->{action} =~ /^[AR]$/));
5240                         get_dir_check($self, $exists, $g, $r);
5241                 }
5242         }
5243         values %$exists;
5244 }
5245
5246 sub minimize_url {
5247         my ($self) = @_;
5248         return $self->{url} if ($self->{url} eq $self->{repos_root});
5249         my $url = $self->{repos_root};
5250         my @components = split(m!/!, $self->{svn_path});
5251         my $c = '';
5252         do {
5253                 $url .= "/$c" if length $c;
5254                 eval {
5255                         my $ra = (ref $self)->new($url);
5256                         my $latest = $ra->get_latest_revnum;
5257                         $ra->get_log("", $latest, 0, 1, 0, 1, sub {});
5258                 };
5259         } while ($@ && ($c = shift @components));
5260         $url;
5261 }
5262
5263 sub can_do_switch {
5264         my $self = shift;
5265         unless (defined $can_do_switch) {
5266                 my $pool = SVN::Pool->new;
5267                 my $rep = eval {
5268                         $self->do_switch(1, '', 0, $self->{url},
5269                                          SVN::Delta::Editor->new, $pool);
5270                 };
5271                 if ($@) {
5272                         $can_do_switch = 0;
5273                 } else {
5274                         $rep->abort_report($pool);
5275                         $can_do_switch = 1;
5276                 }
5277                 $pool->clear;
5278         }
5279         $can_do_switch;
5280 }
5281
5282 sub skip_unknown_revs {
5283         my ($err) = @_;
5284         my $errno = $err->apr_err();
5285         # Maybe the branch we're tracking didn't
5286         # exist when the repo started, so it's
5287         # not an error if it doesn't, just continue
5288         #
5289         # Wonderfully consistent library, eh?
5290         # 160013 - svn:// and file://
5291         # 175002 - http(s)://
5292         # 175007 - http(s):// (this repo required authorization, too...)
5293         #   More codes may be discovered later...
5294         if ($errno == 175007 || $errno == 175002 || $errno == 160013) {
5295                 my $err_key = $err->expanded_message;
5296                 # revision numbers change every time, filter them out
5297                 $err_key =~ s/\d+/\0/g;
5298                 $err_key = "$errno\0$err_key";
5299                 unless ($ignored_err{$err_key}) {
5300                         warn "W: Ignoring error from SVN, path probably ",
5301                              "does not exist: ($errno): ",
5302                              $err->expanded_message,"\n";
5303                         warn "W: Do not be alarmed at the above message ",
5304                              "git-svn is just searching aggressively for ",
5305                              "old history.\n",
5306                              "This may take a while on large repositories\n";
5307                         $ignored_err{$err_key} = 1;
5308                 }
5309                 return;
5310         }
5311         die "Error from SVN, ($errno): ", $err->expanded_message,"\n";
5312 }
5313
5314 package Git::SVN::Log;
5315 use strict;
5316 use warnings;
5317 use POSIX qw/strftime/;
5318 use Time::Local;
5319 use constant commit_log_separator => ('-' x 72) . "\n";
5320 use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline
5321             %rusers $show_commit $incremental/;
5322 my $l_fmt;
5323
5324 sub cmt_showable {
5325         my ($c) = @_;
5326         return 1 if defined $c->{r};
5327
5328         # big commit message got truncated by the 16k pretty buffer in rev-list
5329         if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
5330                                 $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
5331                 @{$c->{l}} = ();
5332                 my @log = command(qw/cat-file commit/, $c->{c});
5333
5334                 # shift off the headers
5335                 shift @log while ($log[0] ne '');
5336                 shift @log;
5337
5338                 # TODO: make $c->{l} not have a trailing newline in the future
5339                 @{$c->{l}} = map { "$_\n" } grep !/^git-svn-id: /, @log;
5340
5341                 (undef, $c->{r}, undef) = ::extract_metadata(
5342                                 (grep(/^git-svn-id: /, @log))[-1]);
5343         }
5344         return defined $c->{r};
5345 }
5346
5347 sub log_use_color {
5348         return $color || Git->repository->get_colorbool('color.diff');
5349 }
5350
5351 sub git_svn_log_cmd {
5352         my ($r_min, $r_max, @args) = @_;
5353         my $head = 'HEAD';
5354         my (@files, @log_opts);
5355         foreach my $x (@args) {
5356                 if ($x eq '--' || @files) {
5357                         push @files, $x;
5358                 } else {
5359                         if (::verify_ref("$x^0")) {
5360                                 $head = $x;
5361                         } else {
5362                                 push @log_opts, $x;
5363                         }
5364                 }
5365         }
5366
5367         my ($url, $rev, $uuid, $gs) = ::working_head_info($head);
5368         $gs ||= Git::SVN->_new;
5369         my @cmd = (qw/log --abbrev-commit --pretty=raw --default/,
5370                    $gs->refname);
5371         push @cmd, '-r' unless $non_recursive;
5372         push @cmd, qw/--raw --name-status/ if $verbose;
5373         push @cmd, '--color' if log_use_color();
5374         push @cmd, @log_opts;
5375         if (defined $r_max && $r_max == $r_min) {
5376                 push @cmd, '--max-count=1';
5377                 if (my $c = $gs->rev_map_get($r_max)) {
5378                         push @cmd, $c;
5379                 }
5380         } elsif (defined $r_max) {
5381                 if ($r_max < $r_min) {
5382                         ($r_min, $r_max) = ($r_max, $r_min);
5383                 }
5384                 my (undef, $c_max) = $gs->find_rev_before($r_max, 1, $r_min);
5385                 my (undef, $c_min) = $gs->find_rev_after($r_min, 1, $r_max);
5386                 # If there are no commits in the range, both $c_max and $c_min
5387                 # will be undefined.  If there is at least 1 commit in the
5388                 # range, both will be defined.
5389                 return () if !defined $c_min || !defined $c_max;
5390                 if ($c_min eq $c_max) {
5391                         push @cmd, '--max-count=1', $c_min;
5392                 } else {
5393                         push @cmd, '--boundary', "$c_min..$c_max";
5394                 }
5395         }
5396         return (@cmd, @files);
5397 }
5398
5399 # adapted from pager.c
5400 sub config_pager {
5401         chomp(my $pager = command_oneline(qw(var GIT_PAGER)));
5402         if ($pager eq 'cat') {
5403                 $pager = undef;
5404         }
5405         $ENV{GIT_PAGER_IN_USE} = defined($pager);
5406 }
5407
5408 sub run_pager {
5409         return unless -t *STDOUT && defined $pager;
5410         pipe my ($rfd, $wfd) or return;
5411         defined(my $pid = fork) or ::fatal "Can't fork: $!";
5412         if (!$pid) {
5413                 open STDOUT, '>&', $wfd or
5414                                      ::fatal "Can't redirect to stdout: $!";
5415                 return;
5416         }
5417         open STDIN, '<&', $rfd or ::fatal "Can't redirect stdin: $!";
5418         $ENV{LESS} ||= 'FRSX';
5419         exec $pager or ::fatal "Can't run pager: $! ($pager)";
5420 }
5421
5422 sub format_svn_date {
5423         # some systmes don't handle or mishandle %z, so be creative.
5424         my $t = shift || time;
5425         my $gm = timelocal(gmtime($t));
5426         my $sign = qw( + + - )[ $t <=> $gm ];
5427         my $gmoff = sprintf("%s%02d%02d", $sign, (gmtime(abs($t - $gm)))[2,1]);
5428         return strftime("%Y-%m-%d %H:%M:%S $gmoff (%a, %d %b %Y)", localtime($t));
5429 }
5430
5431 sub parse_git_date {
5432         my ($t, $tz) = @_;
5433         # Date::Parse isn't in the standard Perl distro :(
5434         if ($tz =~ s/^\+//) {
5435                 $t += tz_to_s_offset($tz);
5436         } elsif ($tz =~ s/^\-//) {
5437                 $t -= tz_to_s_offset($tz);
5438         }
5439         return $t;
5440 }
5441
5442 sub set_local_timezone {
5443         if (defined $TZ) {
5444                 $ENV{TZ} = $TZ;
5445         } else {
5446                 delete $ENV{TZ};
5447         }
5448 }
5449
5450 sub tz_to_s_offset {
5451         my ($tz) = @_;
5452         $tz =~ s/(\d\d)$//;
5453         return ($1 * 60) + ($tz * 3600);
5454 }
5455
5456 sub get_author_info {
5457         my ($dest, $author, $t, $tz) = @_;
5458         $author =~ s/(?:^\s*|\s*$)//g;
5459         $dest->{a_raw} = $author;
5460         my $au;
5461         if ($::_authors) {
5462                 $au = $rusers{$author} || undef;
5463         }
5464         if (!$au) {
5465                 ($au) = ($author =~ /<([^>]+)\@[^>]+>$/);
5466         }
5467         $dest->{t} = $t;
5468         $dest->{tz} = $tz;
5469         $dest->{a} = $au;
5470         $dest->{t_utc} = parse_git_date($t, $tz);
5471 }
5472
5473 sub process_commit {
5474         my ($c, $r_min, $r_max, $defer) = @_;
5475         if (defined $r_min && defined $r_max) {
5476                 if ($r_min == $c->{r} && $r_min == $r_max) {
5477                         show_commit($c);
5478                         return 0;
5479                 }
5480                 return 1 if $r_min == $r_max;
5481                 if ($r_min < $r_max) {
5482                         # we need to reverse the print order
5483                         return 0 if (defined $limit && --$limit < 0);
5484                         push @$defer, $c;
5485                         return 1;
5486                 }
5487                 if ($r_min != $r_max) {
5488                         return 1 if ($r_min < $c->{r});
5489                         return 1 if ($r_max > $c->{r});
5490                 }
5491         }
5492         return 0 if (defined $limit && --$limit < 0);
5493         show_commit($c);
5494         return 1;
5495 }
5496
5497 sub show_commit {
5498         my $c = shift;
5499         if ($oneline) {
5500                 my $x = "\n";
5501                 if (my $l = $c->{l}) {
5502                         while ($l->[0] =~ /^\s*$/) { shift @$l }
5503                         $x = $l->[0];
5504                 }
5505                 $l_fmt ||= 'A' . length($c->{r});
5506                 print 'r',pack($l_fmt, $c->{r}),' | ';
5507                 print "$c->{c} | " if $show_commit;
5508                 print $x;
5509         } else {
5510                 show_commit_normal($c);
5511         }
5512 }
5513
5514 sub show_commit_changed_paths {
5515         my ($c) = @_;
5516         return unless $c->{changed};
5517         print "Changed paths:\n", @{$c->{changed}};
5518 }
5519
5520 sub show_commit_normal {
5521         my ($c) = @_;
5522         print commit_log_separator, "r$c->{r} | ";
5523         print "$c->{c} | " if $show_commit;
5524         print "$c->{a} | ", format_svn_date($c->{t_utc}), ' | ';
5525         my $nr_line = 0;
5526
5527         if (my $l = $c->{l}) {
5528                 while ($l->[$#$l] eq "\n" && $#$l > 0
5529                                           && $l->[($#$l - 1)] eq "\n") {
5530                         pop @$l;
5531                 }
5532                 $nr_line = scalar @$l;
5533                 if (!$nr_line) {
5534                         print "1 line\n\n\n";
5535                 } else {
5536                         if ($nr_line == 1) {
5537                                 $nr_line = '1 line';
5538                         } else {
5539                                 $nr_line .= ' lines';
5540                         }
5541                         print $nr_line, "\n";
5542                         show_commit_changed_paths($c);
5543                         print "\n";
5544                         print $_ foreach @$l;
5545                 }
5546         } else {
5547                 print "1 line\n";
5548                 show_commit_changed_paths($c);
5549                 print "\n";
5550
5551         }
5552         foreach my $x (qw/raw stat diff/) {
5553                 if ($c->{$x}) {
5554                         print "\n";
5555                         print $_ foreach @{$c->{$x}}
5556                 }
5557         }
5558 }
5559
5560 sub cmd_show_log {
5561         my (@args) = @_;
5562         my ($r_min, $r_max);
5563         my $r_last = -1; # prevent dupes
5564         set_local_timezone();
5565         if (defined $::_revision) {
5566                 if ($::_revision =~ /^(\d+):(\d+)$/) {
5567                         ($r_min, $r_max) = ($1, $2);
5568                 } elsif ($::_revision =~ /^\d+$/) {
5569                         $r_min = $r_max = $::_revision;
5570                 } else {
5571                         ::fatal "-r$::_revision is not supported, use ",
5572                                 "standard 'git log' arguments instead";
5573                 }
5574         }
5575
5576         config_pager();
5577         @args = git_svn_log_cmd($r_min, $r_max, @args);
5578         if (!@args) {
5579                 print commit_log_separator unless $incremental || $oneline;
5580                 return;
5581         }
5582         my $log = command_output_pipe(@args);
5583         run_pager();
5584         my (@k, $c, $d, $stat);
5585         my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
5586         while (<$log>) {
5587                 if (/^${esc_color}commit -?($::sha1_short)/o) {
5588                         my $cmt = $1;
5589                         if ($c && cmt_showable($c) && $c->{r} != $r_last) {
5590                                 $r_last = $c->{r};
5591                                 process_commit($c, $r_min, $r_max, \@k) or
5592                                                                 goto out;
5593                         }
5594                         $d = undef;
5595                         $c = { c => $cmt };
5596                 } elsif (/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {
5597                         get_author_info($c, $1, $2, $3);
5598                 } elsif (/^${esc_color}(?:tree|parent|committer) /o) {
5599                         # ignore
5600                 } elsif (/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {
5601                         push @{$c->{raw}}, $_;
5602                 } elsif (/^${esc_color}[ACRMDT]\t/) {
5603                         # we could add $SVN->{svn_path} here, but that requires
5604                         # remote access at the moment (repo_path_split)...
5605                         s#^(${esc_color})([ACRMDT])\t#$1   $2 #o;
5606                         push @{$c->{changed}}, $_;
5607                 } elsif (/^${esc_color}diff /o) {
5608                         $d = 1;
5609                         push @{$c->{diff}}, $_;
5610                 } elsif ($d) {
5611                         push @{$c->{diff}}, $_;
5612                 } elsif (/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*
5613                           $esc_color*[\+\-]*$esc_color$/x) {
5614                         $stat = 1;
5615                         push @{$c->{stat}}, $_;
5616                 } elsif ($stat && /^ \d+ files changed, \d+ insertions/) {
5617                         push @{$c->{stat}}, $_;
5618                         $stat = undef;
5619                 } elsif (/^${esc_color}    (git-svn-id:.+)$/o) {
5620                         ($c->{url}, $c->{r}, undef) = ::extract_metadata($1);
5621                 } elsif (s/^${esc_color}    //o) {
5622                         push @{$c->{l}}, $_;
5623                 }
5624         }
5625         if ($c && defined $c->{r} && $c->{r} != $r_last) {
5626                 $r_last = $c->{r};
5627                 process_commit($c, $r_min, $r_max, \@k);
5628         }
5629         if (@k) {
5630                 ($r_min, $r_max) = ($r_max, $r_min);
5631                 process_commit($_, $r_min, $r_max) foreach reverse @k;
5632         }
5633 out:
5634         close $log;
5635         print commit_log_separator unless $incremental || $oneline;
5636 }
5637
5638 sub cmd_blame {
5639         my $path = pop;
5640
5641         config_pager();
5642         run_pager();
5643
5644         my ($fh, $ctx, $rev);
5645
5646         if ($_git_format) {
5647                 ($fh, $ctx) = command_output_pipe('blame', @_, $path);
5648                 while (my $line = <$fh>) {
5649                         if ($line =~ /^\^?([[:xdigit:]]+)\s/) {
5650                                 # Uncommitted edits show up as a rev ID of
5651                                 # all zeros, which we can't look up with
5652                                 # cmt_metadata
5653                                 if ($1 !~ /^0+$/) {
5654                                         (undef, $rev, undef) =
5655                                                 ::cmt_metadata($1);
5656                                         $rev = '0' if (!$rev);
5657                                 } else {
5658                                         $rev = '0';
5659                                 }
5660                                 $rev = sprintf('%-10s', $rev);
5661                                 $line =~ s/^\^?[[:xdigit:]]+(\s)/$rev$1/;
5662                         }
5663                         print $line;
5664                 }
5665         } else {
5666                 ($fh, $ctx) = command_output_pipe('blame', '-p', @_, 'HEAD',
5667                                                   '--', $path);
5668                 my ($sha1);
5669                 my %authors;
5670                 my @buffer;
5671                 my %dsha; #distinct sha keys
5672
5673                 while (my $line = <$fh>) {
5674                         push @buffer, $line;
5675                         if ($line =~ /^([[:xdigit:]]{40})\s\d+\s\d+/) {
5676                                 $dsha{$1} = 1;
5677                         }
5678                 }
5679
5680                 my $s2r = ::cmt_sha2rev_batch([keys %dsha]);
5681
5682                 foreach my $line (@buffer) {
5683                         if ($line =~ /^([[:xdigit:]]{40})\s\d+\s\d+/) {
5684                                 $rev = $s2r->{$1};
5685                                 $rev = '0' if (!$rev)
5686                         }
5687                         elsif ($line =~ /^author (.*)/) {
5688                                 $authors{$rev} = $1;
5689                                 $authors{$rev} =~ s/\s/_/g;
5690                         }
5691                         elsif ($line =~ /^\t(.*)$/) {
5692                                 printf("%6s %10s %s\n", $rev, $authors{$rev}, $1);
5693                         }
5694                 }
5695         }
5696         command_close_pipe($fh, $ctx);
5697 }
5698
5699 package Git::SVN::Migration;
5700 # these version numbers do NOT correspond to actual version numbers
5701 # of git nor git-svn.  They are just relative.
5702 #
5703 # v0 layout: .git/$id/info/url, refs/heads/$id-HEAD
5704 #
5705 # v1 layout: .git/$id/info/url, refs/remotes/$id
5706 #
5707 # v2 layout: .git/svn/$id/info/url, refs/remotes/$id
5708 #
5709 # v3 layout: .git/svn/$id, refs/remotes/$id
5710 #            - info/url may remain for backwards compatibility
5711 #            - this is what we migrate up to this layout automatically,
5712 #            - this will be used by git svn init on single branches
5713 # v3.1 layout (auto migrated):
5714 #            - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink
5715 #              for backwards compatibility
5716 #
5717 # v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id
5718 #            - this is only created for newly multi-init-ed
5719 #              repositories.  Similar in spirit to the
5720 #              --use-separate-remotes option in git-clone (now default)
5721 #            - we do not automatically migrate to this (following
5722 #              the example set by core git)
5723 #
5724 # v5 layout: .rev_db.$UUID => .rev_map.$UUID
5725 #            - newer, more-efficient format that uses 24-bytes per record
5726 #              with no filler space.
5727 #            - use xxd -c24 < .rev_map.$UUID to view and debug
5728 #            - This is a one-way migration, repositories updated to the
5729 #              new format will not be able to use old git-svn without
5730 #              rebuilding the .rev_db.  Rebuilding the rev_db is not
5731 #              possible if noMetadata or useSvmProps are set; but should
5732 #              be no problem for users that use the (sensible) defaults.
5733 use strict;
5734 use warnings;
5735 use Carp qw/croak/;
5736 use File::Path qw/mkpath/;
5737 use File::Basename qw/dirname basename/;
5738 use vars qw/$_minimize/;
5739
5740 sub migrate_from_v0 {
5741         my $git_dir = $ENV{GIT_DIR};
5742         return undef unless -d $git_dir;
5743         my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);
5744         my $migrated = 0;
5745         while (<$fh>) {
5746                 chomp;
5747                 my ($id, $orig_ref) = ($_, $_);
5748                 next unless $id =~ s#^refs/heads/(.+)-HEAD$#$1#;
5749                 next unless -f "$git_dir/$id/info/url";
5750                 my $new_ref = "refs/remotes/$id";
5751                 if (::verify_ref("$new_ref^0")) {
5752                         print STDERR "W: $orig_ref is probably an old ",
5753                                      "branch used by an ancient version of ",
5754                                      "git-svn.\n",
5755                                      "However, $new_ref also exists.\n",
5756                                      "We will not be able ",
5757                                      "to use this branch until this ",
5758                                      "ambiguity is resolved.\n";
5759                         next;
5760                 }
5761                 print STDERR "Migrating from v0 layout...\n" if !$migrated;
5762                 print STDERR "Renaming ref: $orig_ref => $new_ref\n";
5763                 command_noisy('update-ref', $new_ref, $orig_ref);
5764                 command_noisy('update-ref', '-d', $orig_ref, $orig_ref);
5765                 $migrated++;
5766         }
5767         command_close_pipe($fh, $ctx);
5768         print STDERR "Done migrating from v0 layout...\n" if $migrated;
5769         $migrated;
5770 }
5771
5772 sub migrate_from_v1 {
5773         my $git_dir = $ENV{GIT_DIR};
5774         my $migrated = 0;
5775         return $migrated unless -d $git_dir;
5776         my $svn_dir = "$git_dir/svn";
5777
5778         # just in case somebody used 'svn' as their $id at some point...
5779         return $migrated if -d $svn_dir && ! -f "$svn_dir/info/url";
5780
5781         print STDERR "Migrating from a git-svn v1 layout...\n";
5782         mkpath([$svn_dir]);
5783         print STDERR "Data from a previous version of git-svn exists, but\n\t",
5784                      "$svn_dir\n\t(required for this version ",
5785                      "($::VERSION) of git-svn) does not exist.\n";
5786         my ($fh, $ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);
5787         while (<$fh>) {
5788                 my $x = $_;
5789                 next unless $x =~ s#^refs/remotes/##;
5790                 chomp $x;
5791                 next unless -f "$git_dir/$x/info/url";
5792                 my $u = eval { ::file_to_s("$git_dir/$x/info/url") };
5793                 next unless $u;
5794                 my $dn = dirname("$git_dir/svn/$x");
5795                 mkpath([$dn]) unless -d $dn;
5796                 if ($x eq 'svn') { # they used 'svn' as GIT_SVN_ID:
5797                         mkpath(["$git_dir/svn/svn"]);
5798                         print STDERR " - $git_dir/$x/info => ",
5799                                         "$git_dir/svn/$x/info\n";
5800                         rename "$git_dir/$x/info", "$git_dir/svn/$x/info" or
5801                                croak "$!: $x";
5802                         # don't worry too much about these, they probably
5803                         # don't exist with repos this old (save for index,
5804                         # and we can easily regenerate that)
5805                         foreach my $f (qw/unhandled.log index .rev_db/) {
5806                                 rename "$git_dir/$x/$f", "$git_dir/svn/$x/$f";
5807                         }
5808                 } else {
5809                         print STDERR " - $git_dir/$x => $git_dir/svn/$x\n";
5810                         rename "$git_dir/$x", "$git_dir/svn/$x" or
5811                                croak "$!: $x";
5812                 }
5813                 $migrated++;
5814         }
5815         command_close_pipe($fh, $ctx);
5816         print STDERR "Done migrating from a git-svn v1 layout\n";
5817         $migrated;
5818 }
5819
5820 sub read_old_urls {
5821         my ($l_map, $pfx, $path) = @_;
5822         my @dir;
5823         foreach (<$path/*>) {
5824                 if (-r "$_/info/url") {
5825                         $pfx .= '/' if $pfx && $pfx !~ m!/$!;
5826                         my $ref_id = $pfx . basename $_;
5827                         my $url = ::file_to_s("$_/info/url");
5828                         $l_map->{$ref_id} = $url;
5829                 } elsif (-d $_) {
5830                         push @dir, $_;
5831                 }
5832         }
5833         foreach (@dir) {
5834                 my $x = $_;
5835                 $x =~ s!^\Q$ENV{GIT_DIR}\E/svn/!!o;
5836                 read_old_urls($l_map, $x, $_);
5837         }
5838 }
5839
5840 sub migrate_from_v2 {
5841         my @cfg = command(qw/config -l/);
5842         return if grep /^svn-remote\..+\.url=/, @cfg;
5843         my %l_map;
5844         read_old_urls(\%l_map, '', "$ENV{GIT_DIR}/svn");
5845         my $migrated = 0;
5846
5847         foreach my $ref_id (sort keys %l_map) {
5848                 eval { Git::SVN->init($l_map{$ref_id}, '', undef, $ref_id) };
5849                 if ($@) {
5850                         Git::SVN->init($l_map{$ref_id}, '', $ref_id, $ref_id);
5851                 }
5852                 $migrated++;
5853         }
5854         $migrated;
5855 }
5856
5857 sub minimize_connections {
5858         my $r = Git::SVN::read_all_remotes();
5859         my $new_urls = {};
5860         my $root_repos = {};
5861         foreach my $repo_id (keys %$r) {
5862                 my $url = $r->{$repo_id}->{url} or next;
5863                 my $fetch = $r->{$repo_id}->{fetch} or next;
5864                 my $ra = Git::SVN::Ra->new($url);
5865
5866                 # skip existing cases where we already connect to the root
5867                 if (($ra->{url} eq $ra->{repos_root}) ||
5868                     ($ra->{repos_root} eq $repo_id)) {
5869                         $root_repos->{$ra->{url}} = $repo_id;
5870                         next;
5871                 }
5872
5873                 my $root_ra = Git::SVN::Ra->new($ra->{repos_root});
5874                 my $root_path = $ra->{url};
5875                 $root_path =~ s#^\Q$ra->{repos_root}\E(/|$)##;
5876                 foreach my $path (keys %$fetch) {
5877                         my $ref_id = $fetch->{$path};
5878                         my $gs = Git::SVN->new($ref_id, $repo_id, $path);
5879
5880                         # make sure we can read when connecting to
5881                         # a higher level of a repository
5882                         my ($last_rev, undef) = $gs->last_rev_commit;
5883                         if (!defined $last_rev) {
5884                                 $last_rev = eval {
5885                                         $root_ra->get_latest_revnum;
5886                                 };
5887                                 next if $@;
5888                         }
5889                         my $new = $root_path;
5890                         $new .= length $path ? "/$path" : '';
5891                         eval {
5892                                 $root_ra->get_log([$new], $last_rev, $last_rev,
5893                                                   0, 0, 1, sub { });
5894                         };
5895                         next if $@;
5896                         $new_urls->{$ra->{repos_root}}->{$new} =
5897                                 { ref_id => $ref_id,
5898                                   old_repo_id => $repo_id,
5899                                   old_path => $path };
5900                 }
5901         }
5902
5903         my @emptied;
5904         foreach my $url (keys %$new_urls) {
5905                 # see if we can re-use an existing [svn-remote "repo_id"]
5906                 # instead of creating a(n ugly) new section:
5907                 my $repo_id = $root_repos->{$url} || $url;
5908
5909                 my $fetch = $new_urls->{$url};
5910                 foreach my $path (keys %$fetch) {
5911                         my $x = $fetch->{$path};
5912                         Git::SVN->init($url, $path, $repo_id, $x->{ref_id});
5913                         my $pfx = "svn-remote.$x->{old_repo_id}";
5914
5915                         my $old_fetch = quotemeta("$x->{old_path}:".
5916                                                   "$x->{ref_id}");
5917                         command_noisy(qw/config --unset/,
5918                                       "$pfx.fetch", '^'. $old_fetch . '$');
5919                         delete $r->{$x->{old_repo_id}}->
5920                                {fetch}->{$x->{old_path}};
5921                         if (!keys %{$r->{$x->{old_repo_id}}->{fetch}}) {
5922                                 command_noisy(qw/config --unset/,
5923                                               "$pfx.url");
5924                                 push @emptied, $x->{old_repo_id}
5925                         }
5926                 }
5927         }
5928         if (@emptied) {
5929                 my $file = $ENV{GIT_CONFIG} || "$ENV{GIT_DIR}/config";
5930                 print STDERR <<EOF;
5931 The following [svn-remote] sections in your config file ($file) are empty
5932 and can be safely removed:
5933 EOF
5934                 print STDERR "[svn-remote \"$_\"]\n" foreach @emptied;
5935         }
5936 }
5937
5938 sub migration_check {
5939         migrate_from_v0();
5940         migrate_from_v1();
5941         migrate_from_v2();
5942         minimize_connections() if $_minimize;
5943 }
5944
5945 package Git::IndexInfo;
5946 use strict;
5947 use warnings;
5948 use Git qw/command_input_pipe command_close_pipe/;
5949
5950 sub new {
5951         my ($class) = @_;
5952         my ($gui, $ctx) = command_input_pipe(qw/update-index -z --index-info/);
5953         bless { gui => $gui, ctx => $ctx, nr => 0}, $class;
5954 }
5955
5956 sub remove {
5957         my ($self, $path) = @_;
5958         if (print { $self->{gui} } '0 ', 0 x 40, "\t", $path, "\0") {
5959                 return ++$self->{nr};
5960         }
5961         undef;
5962 }
5963
5964 sub update {
5965         my ($self, $mode, $hash, $path) = @_;
5966         if (print { $self->{gui} } $mode, ' ', $hash, "\t", $path, "\0") {
5967                 return ++$self->{nr};
5968         }
5969         undef;
5970 }
5971
5972 sub DESTROY {
5973         my ($self) = @_;
5974         command_close_pipe($self->{gui}, $self->{ctx});
5975 }
5976
5977 package Git::SVN::GlobSpec;
5978 use strict;
5979 use warnings;
5980
5981 sub new {
5982         my ($class, $glob) = @_;
5983         my $re = $glob;
5984         $re =~ s!/+$!!g; # no need for trailing slashes
5985         $re =~ m!^([^*]*)(\*(?:/\*)*)(.*)$!;
5986         my $temp = $re;
5987         my ($left, $right) = ($1, $3);
5988         $re = $2;
5989         my $depth = $re =~ tr/*/*/;
5990         if ($depth != $temp =~ tr/*/*/) {
5991                 die "Only one set of wildcard directories " .
5992                         "(e.g. '*' or '*/*/*') is supported: '$glob'\n";
5993         }
5994         if ($depth == 0) {
5995                 die "One '*' is needed for glob: '$glob'\n";
5996         }
5997         $re =~ s!\*!\[^/\]*!g;
5998         $re = quotemeta($left) . "($re)" . quotemeta($right);
5999         if (length $left && !($left =~ s!/+$!!g)) {
6000                 die "Missing trailing '/' on left side of: '$glob' ($left)\n";
6001         }
6002         if (length $right && !($right =~ s!^/+!!g)) {
6003                 die "Missing leading '/' on right side of: '$glob' ($right)\n";
6004         }
6005         my $left_re = qr/^\/\Q$left\E(\/|$)/;
6006         bless { left => $left, right => $right, left_regex => $left_re,
6007                 regex => qr/$re/, glob => $glob, depth => $depth }, $class;
6008 }
6009
6010 sub full_path {
6011         my ($self, $path) = @_;
6012         return (length $self->{left} ? "$self->{left}/" : '') .
6013                $path . (length $self->{right} ? "/$self->{right}" : '');
6014 }
6015
6016 __END__
6017
6018 Data structures:
6019
6020
6021 $remotes = { # returned by read_all_remotes()
6022         'svn' => {
6023                 # svn-remote.svn.url=https://svn.musicpd.org
6024                 url => 'https://svn.musicpd.org',
6025                 # svn-remote.svn.fetch=mpd/trunk:trunk
6026                 fetch => {
6027                         'mpd/trunk' => 'trunk',
6028                 },
6029                 # svn-remote.svn.tags=mpd/tags/*:tags/*
6030                 tags => {
6031                         path => {
6032                                 left => 'mpd/tags',
6033                                 right => '',
6034                                 regex => qr!mpd/tags/([^/]+)$!,
6035                                 glob => 'tags/*',
6036                         },
6037                         ref => {
6038                                 left => 'tags',
6039                                 right => '',
6040                                 regex => qr!tags/([^/]+)$!,
6041                                 glob => 'tags/*',
6042                         },
6043                 }
6044         }
6045 };
6046
6047 $log_entry hashref as returned by libsvn_log_entry()
6048 {
6049         log => 'whitespace-formatted log entry
6050 ',                                              # trailing newline is preserved
6051         revision => '8',                        # integer
6052         date => '2004-02-24T17:01:44.108345Z',  # commit date
6053         author => 'committer name'
6054 };
6055
6056
6057 # this is generated by generate_diff();
6058 @mods = array of diff-index line hashes, each element represents one line
6059         of diff-index output
6060
6061 diff-index line ($m hash)
6062 {
6063         mode_a => first column of diff-index output, no leading ':',
6064         mode_b => second column of diff-index output,
6065         sha1_b => sha1sum of the final blob,
6066         chg => change type [MCRADT],
6067         file_a => original file name of a file (iff chg is 'C' or 'R')
6068         file_b => new/current file name of a file (any chg)
6069 }
6070 ;
6071
6072 # retval of read_url_paths{,_all}();
6073 $l_map = {
6074         # repository root url
6075         'https://svn.musicpd.org' => {
6076                 # repository path               # GIT_SVN_ID
6077                 'mpd/trunk'             =>      'trunk',
6078                 'mpd/tags/0.11.5'       =>      'tags/0.11.5',
6079         },
6080 }
6081
6082 Notes:
6083         I don't trust the each() function on unless I created %hash myself
6084         because the internal iterator may not have started at base.