git

Форк
0
/
git-svn.perl 
2250 строк · 63.3 Кб
1
#!/usr/bin/perl
2
# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
3
# License: GPL v2 or later
4
use 5.008001;
5
use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : ();
6
use strict;
7
use vars qw/	$AUTHOR $VERSION
8
		$oid $oid_short $oid_length
9
		$_revision $_repository
10
		$_q $_authors $_authors_prog %users/;
11
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
12
$VERSION = '@@GIT_VERSION@@';
13

14
use Carp qw/croak/;
15
use File::Basename qw/dirname basename/;
16
use File::Path qw/mkpath/;
17
use File::Spec;
18
use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
19
use Memoize;
20

21
use Git::SVN;
22
use Git::SVN::Editor;
23
use Git::SVN::Fetcher;
24
use Git::SVN::Ra;
25
use Git::SVN::Prompt;
26
use Git::SVN::Log;
27
use Git::SVN::Migration;
28

29
use Git::SVN::Utils qw(
30
	fatal
31
	can_compress
32
	canonicalize_path
33
	canonicalize_url
34
	join_paths
35
	add_path_to_url
36
	join_paths
37
);
38

39
use Git qw(
40
	git_cmd_try
41
	command
42
	command_oneline
43
	command_noisy
44
	command_output_pipe
45
	command_close_pipe
46
	command_bidi_pipe
47
	command_close_bidi_pipe
48
	get_record
49
);
50

51
BEGIN {
52
	Memoize::memoize 'Git::config';
53
	Memoize::memoize 'Git::config_bool';
54
}
55

56

57
# From which subdir have we been invoked?
58
my $cmd_dir_prefix = eval {
59
	command_oneline([qw/rev-parse --show-prefix/], STDERR => 0)
60
} || '';
61

62
$Git::SVN::Ra::_log_window_size = 100;
63

64
if (! exists $ENV{SVN_SSH} && exists $ENV{GIT_SSH}) {
65
	$ENV{SVN_SSH} = $ENV{GIT_SSH};
66
}
67

68
if (exists $ENV{SVN_SSH} && $^O eq 'msys') {
69
	$ENV{SVN_SSH} =~ s/\\/\\\\/g;
70
	$ENV{SVN_SSH} =~ s/(.*)/"$1"/;
71
}
72

73
$Git::SVN::Log::TZ = $ENV{TZ};
74
$ENV{TZ} = 'UTC';
75
$| = 1; # unbuffer STDOUT
76

77
# All SVN commands do it.  Otherwise we may die on SIGPIPE when the remote
78
# repository decides to close the connection which we expect to be kept alive.
79
$SIG{PIPE} = 'IGNORE';
80

81
# Given a dot separated version number, "subtract" it from
82
# the SVN::Core::VERSION; non-negaitive return means the SVN::Core
83
# is at least at the version the caller asked for.
84
sub compare_svn_version {
85
	my (@ours) = split(/\./, $SVN::Core::VERSION);
86
	my (@theirs) = split(/\./, $_[0]);
87
	my ($i, $diff);
88

89
	for ($i = 0; $i < @ours && $i < @theirs; $i++) {
90
		$diff = $ours[$i] - $theirs[$i];
91
		return $diff if ($diff);
92
	}
93
	return 1 if ($i < @ours);
94
	return -1 if ($i < @theirs);
95
	return 0;
96
}
97

98
sub _req_svn {
99
	require SVN::Core; # use()-ing this causes segfaults for me... *shrug*
100
	require SVN::Ra;
101
	require SVN::Delta;
102
	if (::compare_svn_version('1.1.0') < 0) {
103
		fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)";
104
	}
105
}
106

107
$oid = qr/(?:[a-f\d]{40}(?:[a-f\d]{24})?)/;
108
$oid_short = qr/[a-f\d]{4,64}/;
109
$oid_length = 40;
110
my ($_stdin, $_help, $_edit,
111
	$_message, $_file, $_branch_dest,
112
	$_template, $_shared,
113
	$_version, $_fetch_all, $_no_rebase, $_fetch_parent,
114
	$_before, $_after,
115
	$_merge, $_strategy, $_rebase_merges, $_dry_run, $_parents, $_local,
116
	$_prefix, $_no_checkout, $_url, $_verbose,
117
	$_commit_url, $_tag, $_merge_info, $_interactive, $_set_svn_props);
118

119
# This is a refactoring artifact so Git::SVN can get at this git-svn switch.
120
sub opt_prefix { return $_prefix || '' }
121

122
$Git::SVN::Fetcher::_placeholder_filename = ".gitignore";
123
$_q ||= 0;
124
my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
125
                    'config-dir=s' => \$Git::SVN::Ra::config_dir,
126
                    'no-auth-cache' => \$Git::SVN::Prompt::_no_auth_cache,
127
                    'ignore-paths=s' => \$Git::SVN::Fetcher::_ignore_regex,
128
                    'include-paths=s' => \$Git::SVN::Fetcher::_include_regex,
129
                    'ignore-refs=s' => \$Git::SVN::Ra::_ignore_refs_regex );
130
my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent,
131
		'authors-file|A=s' => \$_authors,
132
		'authors-prog=s' => \$_authors_prog,
133
		'repack:i' => \$Git::SVN::_repack,
134
		'noMetadata' => \$Git::SVN::_no_metadata,
135
		'useSvmProps' => \$Git::SVN::_use_svm_props,
136
		'useSvnsyncProps' => \$Git::SVN::_use_svnsync_props,
137
		'log-window-size=i' => \$Git::SVN::Ra::_log_window_size,
138
		'no-checkout' => \$_no_checkout,
139
		'quiet|q+' => \$_q,
140
		'repack-flags|repack-args|repack-opts=s' =>
141
		   \$Git::SVN::_repack_flags,
142
		'use-log-author' => \$Git::SVN::_use_log_author,
143
		'add-author-from' => \$Git::SVN::_add_author_from,
144
		'localtime' => \$Git::SVN::_localtime,
145
		%remote_opts );
146

147
my ($_trunk, @_tags, @_branches, $_stdlayout);
148
my %icv;
149
my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared,
150
                  'trunk|T=s' => \$_trunk, 'tags|t=s@' => \@_tags,
151
                  'branches|b=s@' => \@_branches, 'prefix=s' => \$_prefix,
152
                  'stdlayout|s' => \$_stdlayout,
153
                  'minimize-url|m!' => \$Git::SVN::_minimize_url,
154
		  'no-metadata' => sub { $icv{noMetadata} = 1 },
155
		  'use-svm-props' => sub { $icv{useSvmProps} = 1 },
156
		  'use-svnsync-props' => sub { $icv{useSvnsyncProps} = 1 },
157
		  'rewrite-root=s' => sub { $icv{rewriteRoot} = $_[1] },
158
		  'rewrite-uuid=s' => sub { $icv{rewriteUUID} = $_[1] },
159
                  %remote_opts );
160
my %cmt_opts = ( 'edit|e' => \$_edit,
161
		'rmdir' => \$Git::SVN::Editor::_rmdir,
162
		'find-copies-harder' => \$Git::SVN::Editor::_find_copies_harder,
163
		'l=i' => \$Git::SVN::Editor::_rename_limit,
164
		'copy-similarity|C=i'=> \$Git::SVN::Editor::_cp_similarity
165
);
166

167
my %cmd = (
168
	fetch => [ \&cmd_fetch, "Download new revisions from SVN",
169
			{ 'revision|r=s' => \$_revision,
170
			  'fetch-all|all' => \$_fetch_all,
171
			  'parent|p' => \$_fetch_parent,
172
			   %fc_opts } ],
173
	clone => [ \&cmd_clone, "Initialize and fetch revisions",
174
			{ 'revision|r=s' => \$_revision,
175
			  'preserve-empty-dirs' =>
176
				\$Git::SVN::Fetcher::_preserve_empty_dirs,
177
			  'placeholder-filename=s' =>
178
				\$Git::SVN::Fetcher::_placeholder_filename,
179
			   %fc_opts, %init_opts } ],
180
	init => [ \&cmd_init, "Initialize a repo for tracking" .
181
			  " (requires URL argument)",
182
			  \%init_opts ],
183
	'multi-init' => [ \&cmd_multi_init,
184
	                  "Deprecated alias for ".
185
			  "'$0 init -T<trunk> -b<branches> -t<tags>'",
186
			  \%init_opts ],
187
	dcommit => [ \&cmd_dcommit,
188
	             'Commit several diffs to merge with upstream',
189
			{ 'merge|m|M' => \$_merge,
190
			  'strategy|s=s' => \$_strategy,
191
			  'verbose|v' => \$_verbose,
192
			  'dry-run|n' => \$_dry_run,
193
			  'fetch-all|all' => \$_fetch_all,
194
			  'commit-url=s' => \$_commit_url,
195
			  'set-svn-props=s' => \$_set_svn_props,
196
			  'revision|r=i' => \$_revision,
197
			  'no-rebase' => \$_no_rebase,
198
			  'mergeinfo=s' => \$_merge_info,
199
			  'interactive|i' => \$_interactive,
200
			%cmt_opts, %fc_opts } ],
201
	branch => [ \&cmd_branch,
202
	            'Create a branch in the SVN repository',
203
	            { 'message|m=s' => \$_message,
204
	              'destination|d=s' => \$_branch_dest,
205
	              'dry-run|n' => \$_dry_run,
206
	              'parents' => \$_parents,
207
	              'tag|t' => \$_tag,
208
	              'username=s' => \$Git::SVN::Prompt::_username,
209
	              'commit-url=s' => \$_commit_url } ],
210
	tag => [ sub { $_tag = 1; cmd_branch(@_) },
211
	         'Create a tag in the SVN repository',
212
	         { 'message|m=s' => \$_message,
213
	           'destination|d=s' => \$_branch_dest,
214
	           'dry-run|n' => \$_dry_run,
215
	           'parents' => \$_parents,
216
	           'username=s' => \$Git::SVN::Prompt::_username,
217
	           'commit-url=s' => \$_commit_url } ],
218
	'set-tree' => [ \&cmd_set_tree,
219
	                "Set an SVN repository to a git tree-ish",
220
			{ 'stdin' => \$_stdin, %cmt_opts, %fc_opts, } ],
221
	'create-ignore' => [ \&cmd_create_ignore,
222
			     "Create a .gitignore per directory with SVN ignore properties",
223
			     { 'revision|r=i' => \$_revision
224
			     } ],
225
	'mkdirs' => [ \&cmd_mkdirs ,
226
	              "Recreate empty directories after a checkout",
227
	              { 'revision|r=i' => \$_revision } ],
228
        'propget' => [ \&cmd_propget,
229
		       'Print the value of a property on a file or directory',
230
		       { 'revision|r=i' => \$_revision } ],
231
        'propset' => [ \&cmd_propset,
232
		       'Set the value of a property on a file or directory - will be set on commit',
233
		       {} ],
234
        'proplist' => [ \&cmd_proplist,
235
		       'List all properties of a file or directory',
236
		       { 'revision|r=i' => \$_revision } ],
237
	'show-ignore' => [ \&cmd_show_ignore, "Show .gitignore patterns from SVN ignore properties",
238
			{ 'revision|r=i' => \$_revision
239
			} ],
240
	'show-externals' => [ \&cmd_show_externals, "Show svn:externals listings",
241
			{ 'revision|r=i' => \$_revision
242
			} ],
243
	'multi-fetch' => [ \&cmd_multi_fetch,
244
	                   "Deprecated alias for $0 fetch --all",
245
			   { 'revision|r=s' => \$_revision, %fc_opts } ],
246
	'migrate' => [ sub { },
247
	               # no-op, we automatically run this anyways,
248
	               'Migrate configuration/metadata/layout from
249
		        previous versions of git-svn',
250
                       { 'minimize' => \$Git::SVN::Migration::_minimize,
251
			 %remote_opts } ],
252
	'log' => [ \&Git::SVN::Log::cmd_show_log, 'Show commit logs',
253
			{ 'limit=i' => \$Git::SVN::Log::limit,
254
			  'revision|r=s' => \$_revision,
255
			  'verbose|v' => \$Git::SVN::Log::verbose,
256
			  'incremental' => \$Git::SVN::Log::incremental,
257
			  'oneline' => \$Git::SVN::Log::oneline,
258
			  'show-commit' => \$Git::SVN::Log::show_commit,
259
			  'non-recursive' => \$Git::SVN::Log::non_recursive,
260
			  'authors-file|A=s' => \$_authors,
261
			  'color' => \$Git::SVN::Log::color,
262
			  'pager=s' => \$Git::SVN::Log::pager
263
			} ],
264
	'find-rev' => [ \&cmd_find_rev,
265
	                "Translate between SVN revision numbers and tree-ish",
266
			{ 'B|before' => \$_before,
267
			  'A|after' => \$_after } ],
268
	'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory",
269
			{ 'merge|m|M' => \$_merge,
270
			  'verbose|v' => \$_verbose,
271
			  'strategy|s=s' => \$_strategy,
272
			  'local|l' => \$_local,
273
			  'fetch-all|all' => \$_fetch_all,
274
			  'dry-run|n' => \$_dry_run,
275
			  'rebase-merges|p' => \$_rebase_merges,
276
			  %fc_opts } ],
277
	'commit-diff' => [ \&cmd_commit_diff,
278
	                   'Commit a diff between two trees',
279
			{ 'message|m=s' => \$_message,
280
			  'file|F=s' => \$_file,
281
			  'revision|r=s' => \$_revision,
282
			%cmt_opts } ],
283
	'info' => [ \&cmd_info,
284
		    "Show info about the latest SVN revision
285
		     on the current branch",
286
		    { 'url' => \$_url, } ],
287
	'blame' => [ \&Git::SVN::Log::cmd_blame,
288
	            "Show what revision and author last modified each line of a file",
289
		    { 'git-format' => \$Git::SVN::Log::_git_format } ],
290
	'reset' => [ \&cmd_reset,
291
		     "Undo fetches back to the specified SVN revision",
292
		     { 'revision|r=s' => \$_revision,
293
		       'parent|p' => \$_fetch_parent } ],
294
	'gc' => [ \&cmd_gc,
295
		  "Compress unhandled.log files in .git/svn and remove " .
296
		  "index files in .git/svn",
297
		{} ],
298
);
299

300
my $term;
301
sub term_init {
302
	require Term::ReadLine;
303
	$term = $ENV{"GIT_SVN_NOTTY"}
304
			? new Term::ReadLine 'git-svn', \*STDIN, \*STDOUT
305
			: new Term::ReadLine 'git-svn';
306
}
307

308
my $cmd;
309
for (my $i = 0; $i < @ARGV; $i++) {
310
	if (defined $cmd{$ARGV[$i]}) {
311
		$cmd = $ARGV[$i];
312
		splice @ARGV, $i, 1;
313
		last;
314
	} elsif ($ARGV[$i] eq 'help') {
315
		$cmd = $ARGV[$i+1];
316
		usage(0);
317
	}
318
};
319

320
# make sure we're always running at the top-level working directory
321
if ($cmd && $cmd =~ /(?:clone|init|multi-init)$/) {
322
	$ENV{GIT_DIR} ||= ".git";
323
	# catch the submodule case
324
	if (-f $ENV{GIT_DIR}) {
325
		open(my $fh, '<', $ENV{GIT_DIR}) or
326
			die "failed to open $ENV{GIT_DIR}: $!\n";
327
		$ENV{GIT_DIR} = $1 if <$fh> =~ /^gitdir: (.+)$/;
328
	}
329
} elsif ($cmd) {
330
	my ($git_dir, $cdup);
331
	git_cmd_try {
332
		$git_dir = command_oneline([qw/rev-parse --git-dir/]);
333
	} "Unable to find .git directory\n";
334
	git_cmd_try {
335
		$cdup = command_oneline(qw/rev-parse --show-cdup/);
336
		chomp $cdup if ($cdup);
337
		$cdup = "." unless ($cdup && length $cdup);
338
	} "Already at toplevel, but $git_dir not found\n";
339
	$ENV{GIT_DIR} = $git_dir;
340
	chdir $cdup or die "Unable to chdir up to '$cdup'\n";
341
	$_repository = Git->repository(Repository => $ENV{GIT_DIR});
342
}
343

344
my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
345

346
read_git_config(\%opts) if $ENV{GIT_DIR};
347
if ($cmd && ($cmd eq 'log' || $cmd eq 'blame')) {
348
	Getopt::Long::Configure('pass_through');
349
}
350
my $rv = GetOptions(%opts, 'h|H' => \$_help, 'version|V' => \$_version,
351
                    'minimize-connections' => \$Git::SVN::Migration::_minimize,
352
                    'id|i=s' => \$Git::SVN::default_ref_id,
353
                    'svn-remote|remote|R=s' => sub {
354
                       $Git::SVN::no_reuse_existing = 1;
355
                       $Git::SVN::default_repo_id = $_[1] });
356
exit 1 if (!$rv && $cmd && $cmd ne 'log');
357

358
usage(0) if $_help;
359
version() if $_version;
360
usage(1) unless defined $cmd;
361
load_authors() if $_authors;
362
if (defined $_authors_prog) {
363
	my $abs_file = File::Spec->rel2abs($_authors_prog);
364
	$_authors_prog = "'" . $abs_file . "'" if -x $abs_file;
365
}
366

367
unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) {
368
	Git::SVN::Migration::migration_check();
369
}
370
Git::SVN::init_vars();
371
eval {
372
	Git::SVN::verify_remotes_sanity();
373
	$cmd{$cmd}->[0]->(@ARGV);
374
	post_fetch_checkout();
375
};
376
fatal $@ if $@;
377
exit 0;
378

379
####################### primary functions ######################
380
sub usage {
381
	my $exit = shift || 0;
382
	my $fd = $exit ? \*STDERR : \*STDOUT;
383
	print $fd <<"";
384
git-svn - bidirectional operations between a single Subversion tree and git
385
usage: git svn <command> [options] [arguments]\n
386

387
	print $fd "Available commands:\n" unless $cmd;
388

389
	foreach (sort keys %cmd) {
390
		next if $cmd && $cmd ne $_;
391
		next if /^multi-/; # don't show deprecated commands
392
		print $fd '  ',pack('A17',$_),$cmd{$_}->[1],"\n";
393
		foreach (sort keys %{$cmd{$_}->[2]}) {
394
			# mixed-case options are for .git/config only
395
			next if /[A-Z]/ && /^[a-z]+$/i;
396
			# prints out arguments as they should be passed:
397
			my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
398
			print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
399
							"--$_" : "-$_" }
400
						split /\|/,$_)," $x\n";
401
		}
402
	}
403
	print $fd <<"";
404
\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an
405
arbitrary identifier if you're tracking multiple SVN branches/repositories in
406
one git repository and want to keep them separate.  See git-svn(1) for more
407
information.
408

409
	exit $exit;
410
}
411

412
sub version {
413
	::_req_svn();
414
	print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n";
415
	exit 0;
416
}
417

418
sub ask {
419
	my ($prompt, %arg) = @_;
420
	my $valid_re = $arg{valid_re};
421
	my $default = $arg{default};
422
	my $resp;
423
	my $i = 0;
424
	term_init() unless $term;
425

426
	if ( !( defined($term->IN)
427
            && defined( fileno($term->IN) )
428
            && defined( $term->OUT )
429
            && defined( fileno($term->OUT) ) ) ){
430
		return defined($default) ? $default : undef;
431
	}
432

433
	while ($i++ < 10) {
434
		$resp = $term->readline($prompt);
435
		if (!defined $resp) { # EOF
436
			print "\n";
437
			return defined $default ? $default : undef;
438
		}
439
		if ($resp eq '' and defined $default) {
440
			return $default;
441
		}
442
		if (!defined $valid_re or $resp =~ /$valid_re/) {
443
			return $resp;
444
		}
445
	}
446
	return undef;
447
}
448

449
sub do_git_init_db {
450
	unless (-d $ENV{GIT_DIR}) {
451
		my @init_db = ('init');
452
		push @init_db, "--template=$_template" if defined $_template;
453
		if (defined $_shared) {
454
			if ($_shared =~ /[a-z]/) {
455
				push @init_db, "--shared=$_shared";
456
			} else {
457
				push @init_db, "--shared";
458
			}
459
		}
460
		command_noisy(@init_db);
461
		$_repository = Git->repository(Repository => ".git");
462
	}
463
	my $set;
464
	my $pfx = "svn-remote.$Git::SVN::default_repo_id";
465
	foreach my $i (keys %icv) {
466
		die "'$set' and '$i' cannot both be set\n" if $set;
467
		next unless defined $icv{$i};
468
		command_noisy('config', "$pfx.$i", $icv{$i});
469
		$set = $i;
470
	}
471
	my $ignore_paths_regex = \$Git::SVN::Fetcher::_ignore_regex;
472
	command_noisy('config', "$pfx.ignore-paths", $$ignore_paths_regex)
473
		if defined $$ignore_paths_regex;
474
	my $include_paths_regex = \$Git::SVN::Fetcher::_include_regex;
475
	command_noisy('config', "$pfx.include-paths", $$include_paths_regex)
476
		if defined $$include_paths_regex;
477
	my $ignore_refs_regex = \$Git::SVN::Ra::_ignore_refs_regex;
478
	command_noisy('config', "$pfx.ignore-refs", $$ignore_refs_regex)
479
		if defined $$ignore_refs_regex;
480

481
	if (defined $Git::SVN::Fetcher::_preserve_empty_dirs) {
482
		my $fname = \$Git::SVN::Fetcher::_placeholder_filename;
483
		command_noisy('config', "$pfx.preserve-empty-dirs", 'true');
484
		command_noisy('config', "$pfx.placeholder-filename", $$fname);
485
	}
486
	load_object_format();
487
}
488

489
sub init_subdir {
490
	my $repo_path = shift or return;
491
	mkpath([$repo_path]) unless -d $repo_path;
492
	chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n";
493
	$ENV{GIT_DIR} = '.git';
494
	$_repository = Git->repository(Repository => $ENV{GIT_DIR});
495
}
496

497
sub cmd_clone {
498
	my ($url, $path) = @_;
499
	if (!$url) {
500
		die "SVN repository location required ",
501
		    "as a command-line argument\n";
502
	} elsif (!defined $path &&
503
	    (defined $_trunk || @_branches || @_tags ||
504
	     defined $_stdlayout) &&
505
	    $url !~ m#^[a-z\+]+://#) {
506
		$path = $url;
507
	}
508
	$path = basename($url) if !defined $path || !length $path;
509
	my $authors_absolute = $_authors ? File::Spec->rel2abs($_authors) : "";
510
	cmd_init($url, $path);
511
	command_oneline('config', 'svn.authorsfile', $authors_absolute)
512
	    if $_authors;
513
	Git::SVN::fetch_all($Git::SVN::default_repo_id);
514
}
515

516
sub cmd_init {
517
	if (defined $_stdlayout) {
518
		$_trunk = 'trunk' if (!defined $_trunk);
519
		@_tags = 'tags' if (! @_tags);
520
		@_branches = 'branches' if (! @_branches);
521
	}
522
	if (defined $_trunk || @_branches || @_tags) {
523
		return cmd_multi_init(@_);
524
	}
525
	my $url = shift or die "SVN repository location required ",
526
	                       "as a command-line argument\n";
527
	$url = canonicalize_url($url);
528
	init_subdir(@_);
529
	do_git_init_db();
530

531
	if ($Git::SVN::_minimize_url eq 'unset') {
532
		$Git::SVN::_minimize_url = 0;
533
	}
534

535
	Git::SVN->init($url);
536
}
537

538
sub cmd_fetch {
539
	if (grep /^\d+=./, @_) {
540
		die "'<rev>=<commit>' fetch arguments are ",
541
		    "no longer supported.\n";
542
	}
543
	my ($remote) = @_;
544
	if (@_ > 1) {
545
		die "usage: $0 fetch [--all] [--parent] [svn-remote]\n";
546
	}
547
	$Git::SVN::no_reuse_existing = undef;
548
	if ($_fetch_parent) {
549
		my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
550
		unless ($gs) {
551
			die "Unable to determine upstream SVN information from ",
552
			    "working tree history\n";
553
		}
554
	        # just fetch, don't checkout.
555
		$_no_checkout = 'true';
556
		$_fetch_all ? $gs->fetch_all : $gs->fetch;
557
	} elsif ($_fetch_all) {
558
		cmd_multi_fetch();
559
	} else {
560
		$remote ||= $Git::SVN::default_repo_id;
561
		Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes());
562
	}
563
}
564

565
sub cmd_set_tree {
566
	my (@commits) = @_;
567
	if ($_stdin || !@commits) {
568
		print "Reading from stdin...\n";
569
		@commits = ();
570
		while (<STDIN>) {
571
			if (/\b($oid_short)\b/o) {
572
				unshift @commits, $1;
573
			}
574
		}
575
	}
576
	my @revs;
577
	foreach my $c (@commits) {
578
		my @tmp = command('rev-parse',$c);
579
		if (scalar @tmp == 1) {
580
			push @revs, $tmp[0];
581
		} elsif (scalar @tmp > 1) {
582
			push @revs, reverse(command('rev-list',@tmp));
583
		} else {
584
			fatal "Failed to rev-parse $c";
585
		}
586
	}
587
	my $gs = Git::SVN->new;
588
	my ($r_last, $cmt_last) = $gs->last_rev_commit;
589
	$gs->fetch;
590
	if (defined $gs->{last_rev} && $r_last != $gs->{last_rev}) {
591
		fatal "There are new revisions that were fetched ",
592
		      "and need to be merged (or acknowledged) ",
593
		      "before committing.\nlast rev: $r_last\n",
594
		      " current: $gs->{last_rev}";
595
	}
596
	$gs->set_tree($_) foreach @revs;
597
	print "Done committing ",scalar @revs," revisions to SVN\n";
598
	unlink $gs->{index};
599
}
600

601
sub split_merge_info_range {
602
	my ($range) = @_;
603
	if ($range =~ /(\d+)-(\d+)/) {
604
		return (int($1), int($2));
605
	} else {
606
		return (int($range), int($range));
607
	}
608
}
609

610
sub combine_ranges {
611
	my ($in) = @_;
612

613
	my @fnums = ();
614
	my @arr = split(/,/, $in);
615
	for my $element (@arr) {
616
		my ($start, $end) = split_merge_info_range($element);
617
		push @fnums, $start;
618
	}
619

620
	my @sorted = @arr [ sort {
621
		$fnums[$a] <=> $fnums[$b]
622
	} 0..$#arr ];
623

624
	my @return = ();
625
	my $last = -1;
626
	my $first = -1;
627
	for my $element (@sorted) {
628
		my ($start, $end) = split_merge_info_range($element);
629

630
		if ($last == -1) {
631
			$first = $start;
632
			$last = $end;
633
			next;
634
		}
635
		if ($start <= $last+1) {
636
			if ($end > $last) {
637
				$last = $end;
638
			}
639
			next;
640
		}
641
		if ($first == $last) {
642
			push @return, "$first";
643
		} else {
644
			push @return, "$first-$last";
645
		}
646
		$first = $start;
647
		$last = $end;
648
	}
649

650
	if ($first != -1) {
651
		if ($first == $last) {
652
			push @return, "$first";
653
		} else {
654
			push @return, "$first-$last";
655
		}
656
	}
657

658
	return join(',', @return);
659
}
660

661
sub merge_revs_into_hash {
662
	my ($hash, $minfo) = @_;
663
	my @lines = split(' ', $minfo);
664

665
	for my $line (@lines) {
666
		my ($branchpath, $revs) = split(/:/, $line);
667

668
		if (exists($hash->{$branchpath})) {
669
			# Merge the two revision sets
670
			my $combined = "$hash->{$branchpath},$revs";
671
			$hash->{$branchpath} = combine_ranges($combined);
672
		} else {
673
			# Just do range combining for consolidation
674
			$hash->{$branchpath} = combine_ranges($revs);
675
		}
676
	}
677
}
678

679
sub merge_merge_info {
680
	my ($mergeinfo_one, $mergeinfo_two, $ignore_branch) = @_;
681
	my %result_hash = ();
682

683
	merge_revs_into_hash(\%result_hash, $mergeinfo_one);
684
	merge_revs_into_hash(\%result_hash, $mergeinfo_two);
685

686
	delete $result_hash{$ignore_branch} if $ignore_branch;
687

688
	my $result = '';
689
	# Sort below is for consistency's sake
690
	for my $branchname (sort keys(%result_hash)) {
691
		my $revlist = $result_hash{$branchname};
692
		$result .= "$branchname:$revlist\n"
693
	}
694
	return $result;
695
}
696

697
sub populate_merge_info {
698
	my ($d, $gs, $uuid, $linear_refs, $rewritten_parent) = @_;
699

700
	my %parentshash;
701
	read_commit_parents(\%parentshash, $d);
702
	my @parents = @{$parentshash{$d}};
703
	if ($#parents > 0) {
704
		# Merge commit
705
		my $all_parents_ok = 1;
706
		my $aggregate_mergeinfo = '';
707
		my $rooturl = $gs->repos_root;
708
		my ($target_branch) = $gs->full_pushurl =~ /^\Q$rooturl\E(.*)/;
709

710
		if (defined($rewritten_parent)) {
711
			# Replace first parent with newly-rewritten version
712
			shift @parents;
713
			unshift @parents, $rewritten_parent;
714
		}
715

716
		foreach my $parent (@parents) {
717
			my ($branchurl, $svnrev, $paruuid) =
718
				cmt_metadata($parent);
719

720
			unless (defined($svnrev)) {
721
				# Should have been caught be preflight check
722
				fatal "merge commit $d has ancestor $parent, but that change "
723
                     ."does not have git-svn metadata!";
724
			}
725
			unless ($branchurl =~ /^\Q$rooturl\E(.*)/) {
726
				fatal "commit $parent git-svn metadata changed mid-run!";
727
			}
728
			my $branchpath = $1;
729

730
			my $ra = Git::SVN::Ra->new($branchurl);
731
			my (undef, undef, $props) =
732
				$ra->get_dir(canonicalize_path("."), $svnrev);
733
			my $par_mergeinfo = $props->{'svn:mergeinfo'};
734
			unless (defined $par_mergeinfo) {
735
				$par_mergeinfo = '';
736
			}
737
			# Merge previous mergeinfo values
738
			$aggregate_mergeinfo =
739
				merge_merge_info($aggregate_mergeinfo,
740
								$par_mergeinfo,
741
								$target_branch);
742

743
			next if $parent eq $parents[0]; # Skip first parent
744
			# Add new changes being placed in tree by merge
745
			my @cmd = (qw/rev-list --reverse/,
746
					   $parent, qw/--not/);
747
			foreach my $par (@parents) {
748
				unless ($par eq $parent) {
749
					push @cmd, $par;
750
				}
751
			}
752
			my @revsin = ();
753
			my ($revlist, $ctx) = command_output_pipe(@cmd);
754
			while (<$revlist>) {
755
				my $irev = $_;
756
				chomp $irev;
757
				my (undef, $csvnrev, undef) =
758
					cmt_metadata($irev);
759
				unless (defined $csvnrev) {
760
					# A child is missing SVN annotations...
761
					# this might be OK, or might not be.
762
					warn "W:child $irev is merged into revision "
763
						 ."$d but does not have git-svn metadata. "
764
						 ."This means git-svn cannot determine the "
765
						 ."svn revision numbers to place into the "
766
						 ."svn:mergeinfo property. You must ensure "
767
						 ."a branch is entirely committed to "
768
						 ."SVN before merging it in order for "
769
						 ."svn:mergeinfo population to function "
770
						 ."properly";
771
				}
772
				push @revsin, $csvnrev;
773
			}
774
			command_close_pipe($revlist, $ctx);
775

776
			last unless $all_parents_ok;
777

778
			# We now have a list of all SVN revnos which are
779
			# merged by this particular parent. Integrate them.
780
			next if $#revsin == -1;
781
			my $newmergeinfo = "$branchpath:" . join(',', @revsin);
782
			$aggregate_mergeinfo =
783
				merge_merge_info($aggregate_mergeinfo,
784
								$newmergeinfo,
785
								$target_branch);
786
		}
787
		if ($all_parents_ok and $aggregate_mergeinfo) {
788
			return $aggregate_mergeinfo;
789
		}
790
	}
791

792
	return undef;
793
}
794

795
sub dcommit_rebase {
796
	my ($is_last, $current, $fetched_ref, $svn_error) = @_;
797
	my @diff;
798

799
	if ($svn_error) {
800
		print STDERR "\nERROR from SVN:\n",
801
				$svn_error->expanded_message, "\n";
802
	}
803
	unless ($_no_rebase) {
804
		# we always want to rebase against the current HEAD,
805
		# not any head that was passed to us
806
		@diff = command('diff-tree', $current,
807
	                   $fetched_ref, '--');
808
		my @finish;
809
		if (@diff) {
810
			@finish = rebase_cmd();
811
			print STDERR "W: $current and ", $fetched_ref,
812
			             " differ, using @finish:\n",
813
			             join("\n", @diff), "\n";
814
		} elsif ($is_last) {
815
			print "No changes between ", $current, " and ",
816
			      $fetched_ref,
817
			      "\nResetting to the latest ",
818
			      $fetched_ref, "\n";
819
			@finish = qw/reset --mixed/;
820
		}
821
		command_noisy(@finish, $fetched_ref) if @finish;
822
	}
823
	if ($svn_error) {
824
		die "ERROR: Not all changes have been committed into SVN"
825
			.($_no_rebase ? ".\n" : ", however the committed\n"
826
			."ones (if any) seem to be successfully integrated "
827
			."into the working tree.\n")
828
			."Please see the above messages for details.\n";
829
	}
830
	return @diff;
831
}
832

833
sub cmd_dcommit {
834
	my $head = shift;
835
	command_noisy(qw/update-index --refresh/);
836
	git_cmd_try { command_oneline(qw/diff-index --quiet HEAD --/) }
837
		'Cannot dcommit with a dirty index.  Commit your changes first, '
838
		. "or stash them with `git stash'.\n";
839
	$head ||= 'HEAD';
840

841
	my $old_head;
842
	if ($head ne 'HEAD') {
843
		$old_head = eval {
844
			command_oneline([qw/symbolic-ref -q HEAD/])
845
		};
846
		if ($old_head) {
847
			$old_head =~ s{^refs/heads/}{};
848
		} else {
849
			$old_head = eval { command_oneline(qw/rev-parse HEAD/) };
850
		}
851
		command(['checkout', $head], STDERR => 0);
852
	}
853

854
	my @refs;
855
	my ($url, $rev, $uuid, $gs) = working_head_info('HEAD', \@refs);
856
	unless ($gs) {
857
		die "Unable to determine upstream SVN information from ",
858
		    "$head history.\nPerhaps the repository is empty.";
859
	}
860

861
	if (defined $_commit_url) {
862
		$url = $_commit_url;
863
	} else {
864
		$url = eval { command_oneline('config', '--get',
865
			      "svn-remote.$gs->{repo_id}.commiturl") };
866
		if (!$url) {
867
			$url = $gs->full_pushurl
868
		}
869
	}
870

871
	my $last_rev = $_revision if defined $_revision;
872
	if ($url) {
873
		print "Committing to $url ...\n";
874
	}
875
	my ($linear_refs, $parents) = linearize_history($gs, \@refs);
876
	if ($_no_rebase && scalar(@$linear_refs) > 1) {
877
		warn "Attempting to commit more than one change while ",
878
		     "--no-rebase is enabled.\n",
879
		     "If these changes depend on each other, re-running ",
880
		     "without --no-rebase may be required."
881
	}
882

883
	if (defined $_interactive){
884
		my $ask_default = "y";
885
		foreach my $d (@$linear_refs){
886
			my ($fh, $ctx) = command_output_pipe(qw(show --summary), "$d");
887
			while (<$fh>){
888
				print $_;
889
			}
890
			command_close_pipe($fh, $ctx);
891
			$_ = ask("Commit this patch to SVN? ([y]es (default)|[n]o|[q]uit|[a]ll): ",
892
			         valid_re => qr/^(?:yes|y|no|n|quit|q|all|a)/i,
893
			         default => $ask_default);
894
			die "Commit this patch reply required" unless defined $_;
895
			if (/^[nq]/i) {
896
				exit(0);
897
			} elsif (/^a/i) {
898
				last;
899
			}
900
		}
901
	}
902

903
	my $expect_url = $url;
904

905
	my $push_merge_info = eval {
906
		command_oneline(qw/config --get svn.pushmergeinfo/)
907
		};
908
	if (not defined($push_merge_info)
909
			or $push_merge_info eq "false"
910
			or $push_merge_info eq "no"
911
			or $push_merge_info eq "never") {
912
		$push_merge_info = 0;
913
	}
914

915
	unless (defined($_merge_info) || ! $push_merge_info) {
916
		# Preflight check of changes to ensure no issues with mergeinfo
917
		# This includes check for uncommitted-to-SVN parents
918
		# (other than the first parent, which we will handle),
919
		# information from different SVN repos, and paths
920
		# which are not underneath this repository root.
921
		my $rooturl = $gs->repos_root;
922
	        Git::SVN::remove_username($rooturl);
923
		foreach my $d (@$linear_refs) {
924
			my %parentshash;
925
			read_commit_parents(\%parentshash, $d);
926
			my @realparents = @{$parentshash{$d}};
927
			if ($#realparents > 0) {
928
				# Merge commit
929
				shift @realparents; # Remove/ignore first parent
930
				foreach my $parent (@realparents) {
931
					my ($branchurl, $svnrev, $paruuid) = cmt_metadata($parent);
932
					unless (defined $paruuid) {
933
						# A parent is missing SVN annotations...
934
						# abort the whole operation.
935
						fatal "$parent is merged into revision $d, "
936
							 ."but does not have git-svn metadata. "
937
							 ."Either dcommit the branch or use a "
938
							 ."local cherry-pick, FF merge, or rebase "
939
							 ."instead of an explicit merge commit.";
940
					}
941

942
					unless ($paruuid eq $uuid) {
943
						# Parent has SVN metadata from different repository
944
						fatal "merge parent $parent for change $d has "
945
							 ."git-svn uuid $paruuid, while current change "
946
							 ."has uuid $uuid!";
947
					}
948

949
					unless ($branchurl =~ /^\Q$rooturl\E(.*)/) {
950
						# This branch is very strange indeed.
951
						fatal "merge parent $parent for $d is on branch "
952
							 ."$branchurl, which is not under the "
953
							 ."git-svn root $rooturl!";
954
					}
955
				}
956
			}
957
		}
958
	}
959

960
	my $rewritten_parent;
961
	my $current_head = command_oneline(qw/rev-parse HEAD/);
962
	Git::SVN::remove_username($expect_url);
963
	if (defined($_merge_info)) {
964
		$_merge_info =~ tr{ }{\n};
965
	}
966
	while (1) {
967
		my $d = shift @$linear_refs or last;
968
		unless (defined $last_rev) {
969
			(undef, $last_rev, undef) = cmt_metadata("$d~1");
970
			unless (defined $last_rev) {
971
				fatal "Unable to extract revision information ",
972
				      "from commit $d~1";
973
			}
974
		}
975
		if ($_dry_run) {
976
			print "diff-tree $d~1 $d\n";
977
		} else {
978
			my $cmt_rev;
979

980
			unless (defined($_merge_info) || ! $push_merge_info) {
981
				$_merge_info = populate_merge_info($d, $gs,
982
				                             $uuid,
983
				                             $linear_refs,
984
				                             $rewritten_parent);
985
			}
986

987
			my %ed_opts = ( r => $last_rev,
988
			                log => get_commit_entry($d)->{log},
989
			                ra => Git::SVN::Ra->new($url),
990
			                config => SVN::Core::config_get_config(
991
			                        $Git::SVN::Ra::config_dir
992
			                ),
993
			                tree_a => "$d~1",
994
			                tree_b => $d,
995
			                editor_cb => sub {
996
			                       print "Committed r$_[0]\n";
997
			                       $cmt_rev = $_[0];
998
			                },
999
					mergeinfo => $_merge_info,
1000
			                svn_path => '');
1001

1002
			my $err_handler = $SVN::Error::handler;
1003
			$SVN::Error::handler = sub {
1004
				my $err = shift;
1005
				dcommit_rebase(1, $current_head, $gs->refname,
1006
					$err);
1007
			};
1008

1009
			if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) {
1010
				print "No changes\n$d~1 == $d\n";
1011
			} elsif ($parents->{$d} && @{$parents->{$d}}) {
1012
				$gs->{inject_parents_dcommit}->{$cmt_rev} =
1013
				                               $parents->{$d};
1014
			}
1015
			$_fetch_all ? $gs->fetch_all : $gs->fetch;
1016
			$SVN::Error::handler = $err_handler;
1017
			$last_rev = $cmt_rev;
1018
			next if $_no_rebase;
1019

1020
			my @diff = dcommit_rebase(@$linear_refs == 0, $d,
1021
						$gs->refname, undef);
1022

1023
			$rewritten_parent = command_oneline(qw/rev-parse/,
1024
							$gs->refname);
1025

1026
			if (@diff) {
1027
				$current_head = command_oneline(qw/rev-parse
1028
								HEAD/);
1029
				@refs = ();
1030
				my ($url_, $rev_, $uuid_, $gs_) =
1031
				              working_head_info('HEAD', \@refs);
1032
				my ($linear_refs_, $parents_) =
1033
				              linearize_history($gs_, \@refs);
1034
				if (scalar(@$linear_refs) !=
1035
				    scalar(@$linear_refs_)) {
1036
					fatal "# of revisions changed ",
1037
					  "\nbefore:\n",
1038
					  join("\n", @$linear_refs),
1039
					  "\n\nafter:\n",
1040
					  join("\n", @$linear_refs_), "\n",
1041
					  'If you are attempting to commit ',
1042
					  "merges, try running:\n\t",
1043
					  'git rebase --interactive',
1044
					  '--rebase-merges ',
1045
					  $gs->refname,
1046
					  "\nBefore dcommitting";
1047
				}
1048
				if ($url_ ne $expect_url) {
1049
					if ($url_ eq $gs->metadata_url) {
1050
						print
1051
						  "Accepting rewritten URL:",
1052
						  " $url_\n";
1053
					} else {
1054
						fatal
1055
						  "URL mismatch after rebase:",
1056
						  " $url_ != $expect_url";
1057
					}
1058
				}
1059
				if ($uuid_ ne $uuid) {
1060
					fatal "uuid mismatch after rebase: ",
1061
					      "$uuid_ != $uuid";
1062
				}
1063
				# remap parents
1064
				my (%p, @l, $i);
1065
				for ($i = 0; $i < scalar @$linear_refs; $i++) {
1066
					my $new = $linear_refs_->[$i] or next;
1067
					$p{$new} =
1068
						$parents->{$linear_refs->[$i]};
1069
					push @l, $new;
1070
				}
1071
				$parents = \%p;
1072
				$linear_refs = \@l;
1073
				undef $last_rev;
1074
			}
1075
		}
1076
	}
1077

1078
	if ($old_head) {
1079
		my $new_head = command_oneline(qw/rev-parse HEAD/);
1080
		my $new_is_symbolic = eval {
1081
			command_oneline(qw/symbolic-ref -q HEAD/);
1082
		};
1083
		if ($new_is_symbolic) {
1084
			print "dcommitted the branch ", $head, "\n";
1085
		} else {
1086
			print "dcommitted on a detached HEAD because you gave ",
1087
			      "a revision argument.\n",
1088
			      "The rewritten commit is: ", $new_head, "\n";
1089
		}
1090
		command(['checkout', $old_head], STDERR => 0);
1091
	}
1092

1093
	unlink $gs->{index};
1094
}
1095

1096
sub cmd_branch {
1097
	my ($branch_name, $head) = @_;
1098

1099
	unless (defined $branch_name && length $branch_name) {
1100
		die(($_tag ? "tag" : "branch") . " name required\n");
1101
	}
1102
	$head ||= 'HEAD';
1103

1104
	my (undef, $rev, undef, $gs) = working_head_info($head);
1105
	my $src = $gs->full_pushurl;
1106

1107
	my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}};
1108
	my $allglobs = $remote->{ $_tag ? 'tags' : 'branches' };
1109
	my $glob;
1110
	if ($#{$allglobs} == 0) {
1111
		$glob = $allglobs->[0];
1112
	} else {
1113
		unless(defined $_branch_dest) {
1114
			die "Multiple ",
1115
			    $_tag ? "tag" : "branch",
1116
			    " paths defined for Subversion repository.\n",
1117
		            "You must specify where you want to create the ",
1118
		            $_tag ? "tag" : "branch",
1119
		            " with the --destination argument.\n";
1120
		}
1121
		foreach my $g (@{$allglobs}) {
1122
			my $re = Git::SVN::Editor::glob2pat($g->{path}->{left});
1123
			if ($_branch_dest =~ /$re/) {
1124
				$glob = $g;
1125
				last;
1126
			}
1127
		}
1128
		unless (defined $glob) {
1129
			my $dest_re = qr/\b\Q$_branch_dest\E\b/;
1130
			foreach my $g (@{$allglobs}) {
1131
				$g->{path}->{left} =~ /$dest_re/ or next;
1132
				if (defined $glob) {
1133
					die "Ambiguous destination: ",
1134
					    $_branch_dest, "\nmatches both '",
1135
					    $glob->{path}->{left}, "' and '",
1136
					    $g->{path}->{left}, "'\n";
1137
				}
1138
				$glob = $g;
1139
			}
1140
			unless (defined $glob) {
1141
				die "Unknown ",
1142
				    $_tag ? "tag" : "branch",
1143
				    " destination $_branch_dest\n";
1144
			}
1145
		}
1146
	}
1147
	my ($lft, $rgt) = @{ $glob->{path} }{qw/left right/};
1148
	my $url;
1149
	if (defined $_commit_url) {
1150
		$url = $_commit_url;
1151
	} else {
1152
		$url = eval { command_oneline('config', '--get',
1153
			"svn-remote.$gs->{repo_id}.commiturl") };
1154
		if (!$url) {
1155
			$url = $remote->{pushurl} || $remote->{url};
1156
		}
1157
	}
1158
	my $dst = join '/', $url, $lft, $branch_name, ($rgt || ());
1159

1160
	if ($dst =~ /^https:/ && $src =~ /^http:/) {
1161
		$src=~s/^http:/https:/;
1162
	}
1163

1164
	::_req_svn();
1165
	require SVN::Client;
1166

1167
	my ($config, $baton, undef) = Git::SVN::Ra::prepare_config_once();
1168
	my $ctx = SVN::Client->new(
1169
		auth => $baton,
1170
		config => $config,
1171
		log_msg => sub {
1172
			${ $_[0] } = defined $_message
1173
				? $_message
1174
				: 'Create ' . ($_tag ? 'tag ' : 'branch ' )
1175
				. $branch_name;
1176
		},
1177
	);
1178

1179
	eval {
1180
		$ctx->ls($dst, 'HEAD', 0);
1181
	} and die "branch ${branch_name} already exists\n";
1182

1183
	if ($_parents) {
1184
		mk_parent_dirs($ctx, $dst);
1185
	}
1186

1187
	print "Copying ${src} at r${rev} to ${dst}...\n";
1188
	$ctx->copy($src, $rev, $dst)
1189
		unless $_dry_run;
1190

1191
	# Release resources held by ctx before creating another SVN::Ra
1192
	# so destruction is orderly.  This seems necessary with SVN 1.9.5
1193
	# to avoid segfaults.
1194
	$ctx = undef;
1195

1196
	$gs->fetch_all;
1197
}
1198

1199
sub mk_parent_dirs {
1200
	my ($ctx, $parent) = @_;
1201
	$parent =~ s{/[^/]*$}{};
1202

1203
	if (!eval{$ctx->ls($parent, 'HEAD', 0)}) {
1204
		mk_parent_dirs($ctx, $parent);
1205
		print "Creating parent folder ${parent} ...\n";
1206
		$ctx->mkdir($parent) unless $_dry_run;
1207
	}
1208
}
1209

1210
sub cmd_find_rev {
1211
	my $revision_or_hash = shift or die "SVN or git revision required ",
1212
	                                    "as a command-line argument\n";
1213
	my $result;
1214
	if ($revision_or_hash =~ /^r\d+$/) {
1215
		my $head = shift;
1216
		$head ||= 'HEAD';
1217
		my @refs;
1218
		my (undef, undef, $uuid, $gs) = working_head_info($head, \@refs);
1219
		unless ($gs) {
1220
			die "Unable to determine upstream SVN information from ",
1221
			    "$head history\n";
1222
		}
1223
		my $desired_revision = substr($revision_or_hash, 1);
1224
		if ($_before) {
1225
			$result = $gs->find_rev_before($desired_revision, 1);
1226
		} elsif ($_after) {
1227
			$result = $gs->find_rev_after($desired_revision, 1);
1228
		} else {
1229
			$result = $gs->rev_map_get($desired_revision, $uuid);
1230
		}
1231
	} else {
1232
		my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
1233
		$result = $rev;
1234
	}
1235
	print "$result\n" if $result;
1236
}
1237

1238
sub auto_create_empty_directories {
1239
	my ($gs) = @_;
1240
	my $var = eval { command_oneline('config', '--get', '--bool',
1241
					 "svn-remote.$gs->{repo_id}.automkdirs") };
1242
	# By default, create empty directories by consulting the unhandled log,
1243
	# but allow setting it to 'false' to skip it.
1244
	return !($var && $var eq 'false');
1245
}
1246

1247
sub cmd_rebase {
1248
	command_noisy(qw/update-index --refresh/);
1249
	my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
1250
	unless ($gs) {
1251
		die "Unable to determine upstream SVN information from ",
1252
		    "working tree history\n";
1253
	}
1254
	if ($_dry_run) {
1255
		print "Remote Branch: " . $gs->refname . "\n";
1256
		print "SVN URL: " . $url . "\n";
1257
		return;
1258
	}
1259
	if (command(qw/diff-index HEAD --/)) {
1260
		print STDERR "Cannot rebase with uncommitted changes:\n";
1261
		command_noisy('status');
1262
		exit 1;
1263
	}
1264
	unless ($_local) {
1265
		# rebase will checkout for us, so no need to do it explicitly
1266
		$_no_checkout = 'true';
1267
		$_fetch_all ? $gs->fetch_all : $gs->fetch;
1268
	}
1269
	command_noisy(rebase_cmd(), $gs->refname);
1270
	if (auto_create_empty_directories($gs)) {
1271
		$gs->mkemptydirs;
1272
	}
1273
}
1274

1275
sub cmd_show_ignore {
1276
	my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
1277
	$gs ||= Git::SVN->new;
1278
	my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
1279
	$gs->prop_walk($gs->path, $r, sub {
1280
		my ($gs, $path, $props) = @_;
1281
		print STDOUT "\n# $path\n";
1282
		if (my $s = $props->{'svn:ignore'}) {
1283
			$s =~ s/[\r\n]+/\n/g;
1284
			$s =~ s/^\n+//;
1285
			chomp $s;
1286
			$s =~ s#^#$path#gm;
1287
			print STDOUT "$s\n";
1288
		}
1289
		if (my $s = $props->{'svn:global-ignores'}) {
1290
			$s =~ s/[\r\n]+/\n/g;
1291
			$s =~ s/^\n+//;
1292
			chomp $s;
1293
			$s =~ s#^#$path**/#gm;
1294
			print STDOUT "$s\n";
1295
		}
1296
	});
1297
}
1298

1299
sub cmd_show_externals {
1300
	my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
1301
	$gs ||= Git::SVN->new;
1302
	my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
1303
	$gs->prop_walk($gs->path, $r, sub {
1304
		my ($gs, $path, $props) = @_;
1305
		print STDOUT "\n# $path\n";
1306
		my $s = $props->{'svn:externals'} or return;
1307
		$s =~ s/[\r\n]+/\n/g;
1308
		chomp $s;
1309
		$s =~ s#^#$path#gm;
1310
		print STDOUT "$s\n";
1311
	});
1312
}
1313

1314
sub cmd_create_ignore {
1315
	my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
1316
	$gs ||= Git::SVN->new;
1317
	my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
1318
	$gs->prop_walk($gs->path, $r, sub {
1319
		my ($gs, $path, $props) = @_;
1320
		# $path is of the form /path/to/dir/
1321
		$path = '.' . $path;
1322
		# SVN can have attributes on empty directories,
1323
		# which git won't track
1324
		mkpath([$path]) unless -d $path;
1325
		my $ignore = $path . '.gitignore';
1326
		open(GITIGNORE, '>', $ignore)
1327
		  or fatal("Failed to open `$ignore' for writing: $!");
1328
		if (my $s = $props->{'svn:ignore'}) {
1329
			$s =~ s/[\r\n]+/\n/g;
1330
			$s =~ s/^\n+//;
1331
			chomp $s;
1332
			# Prefix all patterns so that the ignore doesn't apply
1333
			# to sub-directories.
1334
			$s =~ s#^#/#gm;
1335
			print GITIGNORE "$s\n";
1336
		}
1337
		if (my $s = $props->{'svn:global-ignores'}) {
1338
			$s =~ s/[\r\n]+/\n/g;
1339
			$s =~ s/^\n+//;
1340
			chomp $s;
1341
			# Global ignores apply to sub-directories, so they are
1342
			# not prefixed.
1343
			print GITIGNORE "$s\n";
1344
		}
1345
		close(GITIGNORE)
1346
		  or fatal("Failed to close `$ignore': $!");
1347
		command_noisy('add', '-f', $ignore);
1348
	});
1349
}
1350

1351
sub cmd_mkdirs {
1352
	my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
1353
	$gs ||= Git::SVN->new;
1354
	$gs->mkemptydirs($_revision);
1355
}
1356

1357
# get_svnprops(PATH)
1358
# ------------------
1359
# Helper for cmd_propget and cmd_proplist below.
1360
sub get_svnprops {
1361
	my $path = shift;
1362
	my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
1363
	$gs ||= Git::SVN->new;
1364

1365
	# prefix THE PATH by the sub-directory from which the user
1366
	# invoked us.
1367
	$path = $cmd_dir_prefix . $path;
1368
	fatal("No such file or directory: $path") unless -e $path;
1369
	my $is_dir = -d $path ? 1 : 0;
1370
	$path = join_paths($gs->path, $path);
1371

1372
	# canonicalize the path (otherwise libsvn will abort or fail to
1373
	# find the file)
1374
	$path = canonicalize_path($path);
1375

1376
	my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
1377
	my $props;
1378
	if ($is_dir) {
1379
		(undef, undef, $props) = $gs->ra->get_dir($path, $r);
1380
	}
1381
	else {
1382
		(undef, $props) = $gs->ra->get_file($path, $r, undef);
1383
	}
1384
	return $props;
1385
}
1386

1387
# cmd_propget (PROP, PATH)
1388
# ------------------------
1389
# Print the SVN property PROP for PATH.
1390
sub cmd_propget {
1391
	my ($prop, $path) = @_;
1392
	$path = '.' if not defined $path;
1393
	usage(1) if not defined $prop;
1394
	my $props = get_svnprops($path);
1395
	if (not defined $props->{$prop}) {
1396
		fatal("`$path' does not have a `$prop' SVN property.");
1397
	}
1398
	print $props->{$prop} . "\n";
1399
}
1400

1401
# cmd_propset (PROPNAME, PROPVAL, PATH)
1402
# ------------------------
1403
# Adjust the SVN property PROPNAME to PROPVAL for PATH.
1404
sub cmd_propset {
1405
	my ($propname, $propval, $path) = @_;
1406
	$path = '.' if not defined $path;
1407
	$path = $cmd_dir_prefix . $path;
1408
	usage(1) if not defined $propname;
1409
	usage(1) if not defined $propval;
1410
	my $file = basename($path);
1411
	my $dn = dirname($path);
1412
	my $cur_props = Git::SVN::Editor::check_attr( "svn-properties", $path );
1413
	my @new_props;
1414
	if (!$cur_props || $cur_props eq "unset" || $cur_props eq "" || $cur_props eq "set") {
1415
		push @new_props, "$propname=$propval";
1416
	} else {
1417
		# TODO: handle combining properties better
1418
		my @props = split(/;/, $cur_props);
1419
		my $replaced_prop;
1420
		foreach my $prop (@props) {
1421
			# Parse 'name=value' syntax and set the property.
1422
			if ($prop =~ /([^=]+)=(.*)/) {
1423
				my ($n,$v) = ($1,$2);
1424
				if ($n eq $propname) {
1425
					$v = $propval;
1426
					$replaced_prop = 1;
1427
				}
1428
				push @new_props, "$n=$v";
1429
			}
1430
		}
1431
		if (!$replaced_prop) {
1432
			push @new_props, "$propname=$propval";
1433
		}
1434
	}
1435
	my $attrfile = "$dn/.gitattributes";
1436
	open my $attrfh, '>>', $attrfile or die "Can't open $attrfile: $!\n";
1437
	# TODO: don't simply append here if $file already has svn-properties
1438
	my $new_props = join(';', @new_props);
1439
	print $attrfh "$file svn-properties=$new_props\n" or
1440
		die "write to $attrfile: $!\n";
1441
	close $attrfh or die "close $attrfile: $!\n";
1442
}
1443

1444
# cmd_proplist (PATH)
1445
# -------------------
1446
# Print the list of SVN properties for PATH.
1447
sub cmd_proplist {
1448
	my $path = shift;
1449
	$path = '.' if not defined $path;
1450
	my $props = get_svnprops($path);
1451
	print "Properties on '$path':\n";
1452
	foreach (sort keys %{$props}) {
1453
		print "  $_\n";
1454
	}
1455
}
1456

1457
sub cmd_multi_init {
1458
	my $url = shift;
1459
	unless (defined $_trunk || @_branches || @_tags) {
1460
		usage(1);
1461
	}
1462

1463
	$_prefix = 'origin/' unless defined $_prefix;
1464
	if (defined $url) {
1465
		$url = canonicalize_url($url);
1466
		init_subdir(@_);
1467
	}
1468
	do_git_init_db();
1469
	if (defined $_trunk) {
1470
		$_trunk =~ s#^/+##;
1471
		my $trunk_ref = 'refs/remotes/' . $_prefix . 'trunk';
1472
		# try both old-style and new-style lookups:
1473
		my $gs_trunk = eval { Git::SVN->new($trunk_ref) };
1474
		unless ($gs_trunk) {
1475
			my ($trunk_url, $trunk_path) =
1476
			                      complete_svn_url($url, $_trunk);
1477
			$gs_trunk = Git::SVN->init($trunk_url, $trunk_path,
1478
						   undef, $trunk_ref);
1479
		}
1480
	}
1481
	return unless @_branches || @_tags;
1482
	my $ra = $url ? Git::SVN::Ra->new($url) : undef;
1483
	foreach my $path (@_branches) {
1484
		complete_url_ls_init($ra, $path, '--branches/-b', $_prefix);
1485
	}
1486
	foreach my $path (@_tags) {
1487
		complete_url_ls_init($ra, $path, '--tags/-t', $_prefix.'tags/');
1488
	}
1489
}
1490

1491
sub cmd_multi_fetch {
1492
	$Git::SVN::no_reuse_existing = undef;
1493
	my $remotes = Git::SVN::read_all_remotes();
1494
	foreach my $repo_id (sort keys %$remotes) {
1495
		if ($remotes->{$repo_id}->{url}) {
1496
			Git::SVN::fetch_all($repo_id, $remotes);
1497
		}
1498
	}
1499
}
1500

1501
# this command is special because it requires no metadata
1502
sub cmd_commit_diff {
1503
	my ($ta, $tb, $url) = @_;
1504
	my $usage = "usage: $0 commit-diff -r<revision> ".
1505
	            "<tree-ish> <tree-ish> [<URL>]";
1506
	fatal($usage) if (!defined $ta || !defined $tb);
1507
	my $svn_path = '';
1508
	if (!defined $url) {
1509
		my $gs = eval { Git::SVN->new };
1510
		if (!$gs) {
1511
			fatal("Needed URL or usable git-svn --id in ",
1512
			      "the command-line\n", $usage);
1513
		}
1514
		$url = $gs->url;
1515
		$svn_path = $gs->path;
1516
	}
1517
	unless (defined $_revision) {
1518
		fatal("-r|--revision is a required argument\n", $usage);
1519
	}
1520
	if (defined $_message && defined $_file) {
1521
		fatal("Both --message/-m and --file/-F specified ",
1522
		      "for the commit message.\n",
1523
		      "I have no idea what you mean");
1524
	}
1525
	if (defined $_file) {
1526
		$_message = file_to_s($_file);
1527
	} else {
1528
		$_message ||= get_commit_entry($tb)->{log};
1529
	}
1530
	my $ra ||= Git::SVN::Ra->new($url);
1531
	my $r = $_revision;
1532
	if ($r eq 'HEAD') {
1533
		$r = $ra->get_latest_revnum;
1534
	} elsif ($r !~ /^\d+$/) {
1535
		die "revision argument: $r not understood by git-svn\n";
1536
	}
1537
	my %ed_opts = ( r => $r,
1538
	                log => $_message,
1539
	                ra => $ra,
1540
	                tree_a => $ta,
1541
	                tree_b => $tb,
1542
	                editor_cb => sub { print "Committed r$_[0]\n" },
1543
	                svn_path => $svn_path );
1544
	if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) {
1545
		print "No changes\n$ta == $tb\n";
1546
	}
1547
}
1548

1549
sub cmd_info {
1550
	my $path_arg = defined($_[0]) ? $_[0] : '.';
1551
	my $path = $path_arg;
1552
	if (File::Spec->file_name_is_absolute($path)) {
1553
		$path = canonicalize_path($path);
1554

1555
		my $toplevel = eval {
1556
			my @cmd = qw/rev-parse --show-toplevel/;
1557
			command_oneline(\@cmd, STDERR => 0);
1558
		};
1559

1560
		# remove $toplevel from the absolute path:
1561
		my ($vol, $dirs, $file) = File::Spec->splitpath($path);
1562
		my (undef, $tdirs, $tfile) = File::Spec->splitpath($toplevel);
1563
		my @dirs = File::Spec->splitdir($dirs);
1564
		my @tdirs = File::Spec->splitdir($tdirs);
1565
		pop @dirs if $dirs[-1] eq '';
1566
		pop @tdirs if $tdirs[-1] eq '';
1567
		push @dirs, $file;
1568
		push @tdirs, $tfile;
1569
		while (@tdirs && @dirs && $tdirs[0] eq $dirs[0]) {
1570
			shift @dirs;
1571
			shift @tdirs;
1572
		}
1573
		$dirs = File::Spec->catdir(@dirs);
1574
		$path = File::Spec->catpath($vol, $dirs);
1575

1576
		$path = canonicalize_path($path);
1577
	} else {
1578
		$path = canonicalize_path($cmd_dir_prefix . $path);
1579
	}
1580
	if (exists $_[1]) {
1581
		die "Too many arguments specified\n";
1582
	}
1583

1584
	my ($file_type, $diff_status) = find_file_type_and_diff_status($path);
1585

1586
	if (!$file_type && !$diff_status) {
1587
		print STDERR "svn: '$path' is not under version control\n";
1588
		exit 1;
1589
	}
1590

1591
	my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
1592
	unless ($gs) {
1593
		die "Unable to determine upstream SVN information from ",
1594
		    "working tree history\n";
1595
	}
1596

1597
	# canonicalize_path() will return "" to make libsvn 1.5.x happy,
1598
	$path = "." if $path eq "";
1599

1600
	my $full_url = canonicalize_url( add_path_to_url( $url, $path ) );
1601

1602
	if ($_url) {
1603
		print "$full_url\n";
1604
		return;
1605
	}
1606

1607
	my $result = "Path: $path_arg\n";
1608
	$result .= "Name: " . basename($path) . "\n" if $file_type ne "dir";
1609
	$result .= "URL: $full_url\n";
1610

1611
	eval {
1612
		my $repos_root = $gs->repos_root;
1613
		Git::SVN::remove_username($repos_root);
1614
		$result .= "Repository Root: " . canonicalize_url($repos_root) . "\n";
1615
	};
1616
	if ($@) {
1617
		$result .= "Repository Root: (offline)\n";
1618
	}
1619
	::_req_svn();
1620
	$result .= "Repository UUID: $uuid\n" unless $diff_status eq "A" &&
1621
		(::compare_svn_version('1.5.4') <= 0 || $file_type ne "dir");
1622
	$result .= "Revision: " . ($diff_status eq "A" ? 0 : $rev) . "\n";
1623

1624
	$result .= "Node Kind: " .
1625
		   ($file_type eq "dir" ? "directory" : "file") . "\n";
1626

1627
	my $schedule = $diff_status eq "A"
1628
		       ? "add"
1629
		       : ($diff_status eq "D" ? "delete" : "normal");
1630
	$result .= "Schedule: $schedule\n";
1631

1632
	if ($diff_status eq "A") {
1633
		print $result, "\n";
1634
		return;
1635
	}
1636

1637
	my ($lc_author, $lc_rev, $lc_date_utc);
1638
	my @args = Git::SVN::Log::git_svn_log_cmd($rev, $rev, "--", $path);
1639
	my $log = command_output_pipe(@args);
1640
	my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
1641
	while (<$log>) {
1642
		if (/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {
1643
			$lc_author = $1;
1644
			$lc_date_utc = Git::SVN::Log::parse_git_date($2, $3);
1645
		} elsif (/^${esc_color}    (git-svn-id:.+)$/o) {
1646
			(undef, $lc_rev, undef) = ::extract_metadata($1);
1647
		}
1648
	}
1649
	close $log;
1650

1651
	Git::SVN::Log::set_local_timezone();
1652

1653
	$result .= "Last Changed Author: $lc_author\n";
1654
	$result .= "Last Changed Rev: $lc_rev\n";
1655
	$result .= "Last Changed Date: " .
1656
		   Git::SVN::Log::format_svn_date($lc_date_utc) . "\n";
1657

1658
	if ($file_type ne "dir") {
1659
		my $text_last_updated_date =
1660
		    ($diff_status eq "D" ? $lc_date_utc : (stat $path)[9]);
1661
		$result .=
1662
		    "Text Last Updated: " .
1663
		    Git::SVN::Log::format_svn_date($text_last_updated_date) .
1664
		    "\n";
1665
		my $checksum;
1666
		if ($diff_status eq "D") {
1667
			my ($fh, $ctx) =
1668
			    command_output_pipe(qw(cat-file blob), "HEAD:$path");
1669
			if ($file_type eq "link") {
1670
				my $file_name = <$fh>;
1671
				$checksum = md5sum("link $file_name");
1672
			} else {
1673
				$checksum = md5sum($fh);
1674
			}
1675
			command_close_pipe($fh, $ctx);
1676
		} elsif ($file_type eq "link") {
1677
			my $file_name =
1678
			    command(qw(cat-file blob), "HEAD:$path");
1679
			$checksum =
1680
			    md5sum("link " . $file_name);
1681
		} else {
1682
			open FILE, "<", $path or die $!;
1683
			$checksum = md5sum(\*FILE);
1684
			close FILE or die $!;
1685
		}
1686
		$result .= "Checksum: " . $checksum . "\n";
1687
	}
1688

1689
	print $result, "\n";
1690
}
1691

1692
sub cmd_reset {
1693
	my $target = shift || $_revision or die "SVN revision required\n";
1694
	$target = $1 if $target =~ /^r(\d+)$/;
1695
	$target =~ /^\d+$/ or die "Numeric SVN revision expected\n";
1696
	my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
1697
	unless ($gs) {
1698
		die "Unable to determine upstream SVN information from ".
1699
		    "history\n";
1700
	}
1701
	my ($r, $c) = $gs->find_rev_before($target, not $_fetch_parent);
1702
	die "Cannot find SVN revision $target\n" unless defined($c);
1703
	$gs->rev_map_set($r, $c, 'reset', $uuid);
1704
	print "r$r = $c ($gs->{ref_id})\n";
1705
}
1706

1707
sub cmd_gc {
1708
	require File::Find;
1709
	if (!can_compress()) {
1710
		warn "Compress::Zlib could not be found; unhandled.log " .
1711
		     "files will not be compressed.\n";
1712
	}
1713
	File::Find::find({ wanted => \&gc_directory, no_chdir => 1},
1714
			 Git::SVN::svn_dir());
1715
}
1716

1717
########################### utility functions #########################
1718

1719
sub rebase_cmd {
1720
	my @cmd = qw/rebase/;
1721
	push @cmd, '-v' if $_verbose;
1722
	push @cmd, qw/--merge/ if $_merge;
1723
	push @cmd, "--strategy=$_strategy" if $_strategy;
1724
	push @cmd, "--rebase-merges" if $_rebase_merges;
1725
	@cmd;
1726
}
1727

1728
sub post_fetch_checkout {
1729
	return if $_no_checkout;
1730
	return if verify_ref('HEAD^0');
1731
	my $gs = $Git::SVN::_head or return;
1732

1733
	# look for "trunk" ref if it exists
1734
	my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}};
1735
	my $fetch = $remote->{fetch};
1736
	if ($fetch) {
1737
		foreach my $p (keys %$fetch) {
1738
			basename($fetch->{$p}) eq 'trunk' or next;
1739
			$gs = Git::SVN->new($fetch->{$p}, $gs->{repo_id}, $p);
1740
			last;
1741
		}
1742
	}
1743

1744
	command_noisy(qw(update-ref HEAD), $gs->refname);
1745
	return unless verify_ref('HEAD^0');
1746

1747
	return if $ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;
1748
	my $index = command_oneline(qw(rev-parse --git-path index));
1749
	return if -f $index;
1750

1751
	return if command_oneline(qw/rev-parse --is-inside-work-tree/) eq 'false';
1752
	return if command_oneline(qw/rev-parse --is-inside-git-dir/) eq 'true';
1753
	command_noisy(qw/read-tree -m -u -v HEAD HEAD/);
1754
	print STDERR "Checked out HEAD:\n  ",
1755
	             $gs->full_url, " r", $gs->last_rev, "\n";
1756
	if (auto_create_empty_directories($gs)) {
1757
		$gs->mkemptydirs($gs->last_rev);
1758
	}
1759
}
1760

1761
sub complete_svn_url {
1762
	my ($url, $path) = @_;
1763

1764
	if ($path =~ m#^[a-z\+]+://#i) { # path is a URL
1765
		$path = canonicalize_url($path);
1766
	} else {
1767
		$path = canonicalize_path($path);
1768
		if (!defined $url || $url !~ m#^[a-z\+]+://#i) {
1769
			fatal("E: '$path' is not a complete URL ",
1770
			      "and a separate URL is not specified");
1771
		}
1772
		return ($url, $path);
1773
	}
1774
	return ($path, '');
1775
}
1776

1777
sub complete_url_ls_init {
1778
	my ($ra, $repo_path, $switch, $pfx) = @_;
1779
	unless ($repo_path) {
1780
		print STDERR "W: $switch not specified\n";
1781
		return;
1782
	}
1783
	if ($repo_path =~ m#^[a-z\+]+://#i) {
1784
		$repo_path = canonicalize_url($repo_path);
1785
		$ra = Git::SVN::Ra->new($repo_path);
1786
		$repo_path = '';
1787
	} else {
1788
		$repo_path = canonicalize_path($repo_path);
1789
		$repo_path =~ s#^/+##;
1790
		unless ($ra) {
1791
			fatal("E: '$repo_path' is not a complete URL ",
1792
			      "and a separate URL is not specified");
1793
		}
1794
	}
1795
	my $url = $ra->url;
1796
	my $gs = Git::SVN->init($url, undef, undef, undef, 1);
1797
	my $k = "svn-remote.$gs->{repo_id}.url";
1798
	my $orig_url = eval { command_oneline(qw/config --get/, $k) };
1799
	if ($orig_url && ($orig_url ne $gs->url)) {
1800
		die "$k already set: $orig_url\n",
1801
		    "wanted to set to: $gs->url\n";
1802
	}
1803
	command_oneline('config', $k, $gs->url) unless $orig_url;
1804

1805
	my $remote_path = join_paths( $gs->path, $repo_path );
1806
	$remote_path =~ s{%([0-9A-F]{2})}{chr hex($1)}ieg;
1807
	$remote_path =~ s#^/##g;
1808
	$remote_path .= "/*" if $remote_path !~ /\*/;
1809
	my ($n) = ($switch =~ /^--(\w+)/);
1810
	if (length $pfx && $pfx !~ m#/$#) {
1811
		die "--prefix='$pfx' must have a trailing slash '/'\n";
1812
	}
1813
	command_noisy('config',
1814
		      '--add',
1815
	              "svn-remote.$gs->{repo_id}.$n",
1816
	              "$remote_path:refs/remotes/$pfx*" .
1817
	                ('/*' x (($remote_path =~ tr/*/*/) - 1)) );
1818
}
1819

1820
sub verify_ref {
1821
	my ($ref) = @_;
1822
	eval { command_oneline([ 'rev-parse', '--verify', $ref ],
1823
	                       { STDERR => 0 }); };
1824
}
1825

1826
sub get_tree_from_treeish {
1827
	my ($treeish) = @_;
1828
	# $treeish can be a symbolic ref, too:
1829
	my $type = command_oneline(qw/cat-file -t/, $treeish);
1830
	my $expected;
1831
	while ($type eq 'tag') {
1832
		($treeish, $type) = command(qw/cat-file tag/, $treeish);
1833
	}
1834
	if ($type eq 'commit') {
1835
		$expected = (grep /^tree /, command(qw/cat-file commit/,
1836
		                                    $treeish))[0];
1837
		($expected) = ($expected =~ /^tree ($oid)$/o);
1838
		die "Unable to get tree from $treeish\n" unless $expected;
1839
	} elsif ($type eq 'tree') {
1840
		$expected = $treeish;
1841
	} else {
1842
		die "$treeish is a $type, expected tree, tag or commit\n";
1843
	}
1844
	return $expected;
1845
}
1846

1847
sub get_commit_entry {
1848
	my ($treeish) = shift;
1849
	my %log_entry = ( log => '', tree => get_tree_from_treeish($treeish) );
1850
	my @git_path = qw(rev-parse --git-path);
1851
	my $commit_editmsg = command_oneline(@git_path, 'COMMIT_EDITMSG');
1852
	my $commit_msg = command_oneline(@git_path, 'COMMIT_MSG');
1853
	open my $log_fh, '>', $commit_editmsg or croak $!;
1854

1855
	my $type = command_oneline(qw/cat-file -t/, $treeish);
1856
	if ($type eq 'commit' || $type eq 'tag') {
1857
		my ($msg_fh, $ctx) = command_output_pipe('cat-file',
1858
		                                         $type, $treeish);
1859
		my $in_msg = 0;
1860
		my $author;
1861
		my $saw_from = 0;
1862
		my $msgbuf = "";
1863
		while (<$msg_fh>) {
1864
			if (!$in_msg) {
1865
				$in_msg = 1 if (/^$/);
1866
				$author = $1 if (/^author (.*>)/);
1867
			} elsif (/^git-svn-id: /) {
1868
				# skip this for now, we regenerate the
1869
				# correct one on re-fetch anyways
1870
				# TODO: set *:merge properties or like...
1871
			} else {
1872
				if (/^From:/ || /^Signed-off-by:/) {
1873
					$saw_from = 1;
1874
				}
1875
				$msgbuf .= $_;
1876
			}
1877
		}
1878
		$msgbuf =~ s/\s+$//s;
1879
		$msgbuf =~ s/\r\n/\n/sg; # SVN 1.6+ disallows CRLF
1880
		if ($Git::SVN::_add_author_from && defined($author)
1881
		    && !$saw_from) {
1882
			$msgbuf .= "\n\nFrom: $author";
1883
		}
1884
		print $log_fh $msgbuf or croak $!;
1885
		command_close_pipe($msg_fh, $ctx);
1886
	}
1887
	close $log_fh or croak $!;
1888

1889
	if ($_edit || ($type eq 'tree')) {
1890
		chomp(my $editor = command_oneline(qw(var GIT_EDITOR)));
1891
		system('sh', '-c', $editor.' "$@"', $editor, $commit_editmsg);
1892
	}
1893
	rename $commit_editmsg, $commit_msg or croak $!;
1894
	{
1895
		require Encode;
1896
		# SVN requires messages to be UTF-8 when entering the repo
1897
		open $log_fh, '<', $commit_msg or croak $!;
1898
		binmode $log_fh;
1899
		chomp($log_entry{log} = get_record($log_fh, undef));
1900

1901
		my $enc = Git::config('i18n.commitencoding') || 'UTF-8';
1902
		my $msg = $log_entry{log};
1903

1904
		eval { $msg = Encode::decode($enc, $msg, 1) };
1905
		if ($@) {
1906
			die "Could not decode as $enc:\n", $msg,
1907
			    "\nPerhaps you need to set i18n.commitencoding\n";
1908
		}
1909

1910
		eval { $msg = Encode::encode('UTF-8', $msg, 1) };
1911
		die "Could not encode as UTF-8:\n$msg\n" if $@;
1912

1913
		$log_entry{log} = $msg;
1914

1915
		close $log_fh or croak $!;
1916
	}
1917
	unlink $commit_msg;
1918
	\%log_entry;
1919
}
1920

1921
sub s_to_file {
1922
	my ($str, $file, $mode) = @_;
1923
	open my $fd,'>',$file or croak $!;
1924
	print $fd $str,"\n" or croak $!;
1925
	close $fd or croak $!;
1926
	chmod ($mode &~ umask, $file) if (defined $mode);
1927
}
1928

1929
sub file_to_s {
1930
	my $file = shift;
1931
	open my $fd,'<',$file or croak "$!: file: $file\n";
1932
	local $/;
1933
	my $ret = <$fd>;
1934
	close $fd or croak $!;
1935
	$ret =~ s/\s*$//s;
1936
	return $ret;
1937
}
1938

1939
# '<svn username> = real-name <email address>' mapping based on git-svnimport:
1940
sub load_authors {
1941
	open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
1942
	my $log = $cmd eq 'log';
1943
	while (<$authors>) {
1944
		chomp;
1945
		next unless /^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.*)>\s*$/;
1946
		my ($user, $name, $email) = ($1, $2, $3);
1947
		if ($log) {
1948
			$Git::SVN::Log::rusers{"$name <$email>"} = $user;
1949
		} else {
1950
			$users{$user} = [$name, $email];
1951
		}
1952
	}
1953
	close $authors or croak $!;
1954
}
1955

1956
# convert GetOpt::Long specs for use by git-config
1957
sub read_git_config {
1958
	my $opts = shift;
1959
	my @config_only;
1960
	foreach my $o (keys %$opts) {
1961
		# if we have mixedCase and a long option-only, then
1962
		# it's a config-only variable that we don't need for
1963
		# the command-line.
1964
		push @config_only, $o if ($o =~ /[A-Z]/ && $o =~ /^[a-z]+$/i);
1965
		my $v = $opts->{$o};
1966
		my ($key) = ($o =~ /^([a-zA-Z\-]+)/);
1967
		$key =~ s/-//g;
1968
		my $arg = 'git config';
1969
		$arg .= ' --int' if ($o =~ /[:=]i$/);
1970
		$arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
1971
		if (ref $v eq 'ARRAY') {
1972
			chomp(my @tmp = `$arg --get-all svn.$key`);
1973
			@$v = @tmp if @tmp;
1974
		} else {
1975
			chomp(my $tmp = `$arg --get svn.$key`);
1976
			if ($tmp && !($arg =~ / --bool/ && $tmp eq 'false')) {
1977
				$$v = $tmp;
1978
			}
1979
		}
1980
	}
1981
	load_object_format();
1982
	delete @$opts{@config_only} if @config_only;
1983
}
1984

1985
sub load_object_format {
1986
	chomp(my $hash = `git config --get extensions.objectformat`);
1987
	$::oid_length = 64 if $hash eq 'sha256';
1988
}
1989

1990
sub extract_metadata {
1991
	my $id = shift or return (undef, undef, undef);
1992
	my ($url, $rev, $uuid) = ($id =~ /^\s*git-svn-id:\s+(.*)\@(\d+)
1993
							\s([a-f\d\-]+)$/ix);
1994
	if (!defined $rev || !$uuid || !$url) {
1995
		# some of the original repositories I made had
1996
		# identifiers like this:
1997
		($rev, $uuid) = ($id =~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i);
1998
	}
1999
	return ($url, $rev, $uuid);
2000
}
2001

2002
sub cmt_metadata {
2003
	return extract_metadata((grep(/^git-svn-id: /,
2004
		command(qw/cat-file commit/, shift)))[-1]);
2005
}
2006

2007
sub cmt_sha2rev_batch {
2008
	my %s2r;
2009
	my ($pid, $in, $out, $ctx) = command_bidi_pipe(qw/cat-file --batch/);
2010
	my $list = shift;
2011

2012
	foreach my $sha (@{$list}) {
2013
		my $first = 1;
2014
		my $size = 0;
2015
		print $out $sha, "\n";
2016

2017
		while (my $line = <$in>) {
2018
			if ($first && $line =~ /^$::oid\smissing$/) {
2019
				last;
2020
			} elsif ($first &&
2021
			       $line =~ /^$::oid\scommit\s(\d+)$/) {
2022
				$first = 0;
2023
				$size = $1;
2024
				next;
2025
			} elsif ($line =~ /^(git-svn-id: )/) {
2026
				my (undef, $rev, undef) =
2027
				                      extract_metadata($line);
2028
				$s2r{$sha} = $rev;
2029
			}
2030

2031
			$size -= length($line);
2032
			last if ($size == 0);
2033
		}
2034
	}
2035

2036
	command_close_bidi_pipe($pid, $in, $out, $ctx);
2037

2038
	return \%s2r;
2039
}
2040

2041
sub working_head_info {
2042
	my ($head, $refs) = @_;
2043
	my @args = qw/rev-list --first-parent --pretty=medium/;
2044
	my ($fh, $ctx) = command_output_pipe(@args, $head, "--");
2045
	my $hash;
2046
	my %max;
2047
	while (<$fh>) {
2048
		if ( m{^commit ($::oid)$} ) {
2049
			unshift @$refs, $hash if $hash and $refs;
2050
			$hash = $1;
2051
			next;
2052
		}
2053
		next unless s{^\s*(git-svn-id:)}{$1};
2054
		my ($url, $rev, $uuid) = extract_metadata($_);
2055
		if (defined $url && defined $rev) {
2056
			next if $max{$url} and $max{$url} < $rev;
2057
			if (my $gs = Git::SVN->find_by_url($url)) {
2058
				my $c = $gs->rev_map_get($rev, $uuid);
2059
				if ($c && $c eq $hash) {
2060
					close $fh; # break the pipe
2061
					return ($url, $rev, $uuid, $gs);
2062
				} else {
2063
					$max{$url} ||= $gs->rev_map_max;
2064
				}
2065
			}
2066
		}
2067
	}
2068
	command_close_pipe($fh, $ctx);
2069
	(undef, undef, undef, undef);
2070
}
2071

2072
sub read_commit_parents {
2073
	my ($parents, $c) = @_;
2074
	chomp(my $p = command_oneline(qw/rev-list --parents -1/, $c));
2075
	$p =~ s/^($c)\s*// or die "rev-list --parents -1 $c failed!\n";
2076
	@{$parents->{$c}} = split(/ /, $p);
2077
}
2078

2079
sub linearize_history {
2080
	my ($gs, $refs) = @_;
2081
	my %parents;
2082
	foreach my $c (@$refs) {
2083
		read_commit_parents(\%parents, $c);
2084
	}
2085

2086
	my @linear_refs;
2087
	my %skip = ();
2088
	my $last_svn_commit = $gs->last_commit;
2089
	foreach my $c (reverse @$refs) {
2090
		next if $c eq $last_svn_commit;
2091
		last if $skip{$c};
2092

2093
		unshift @linear_refs, $c;
2094
		$skip{$c} = 1;
2095

2096
		# we only want the first parent to diff against for linear
2097
		# history, we save the rest to inject when we finalize the
2098
		# svn commit
2099
		my $fp_a = verify_ref("$c~1");
2100
		my $fp_b = shift @{$parents{$c}} if $parents{$c};
2101
		if (!$fp_a || !$fp_b) {
2102
			die "Commit $c\n",
2103
			    "has no parent commit, and therefore ",
2104
			    "nothing to diff against.\n",
2105
			    "You should be working from a repository ",
2106
			    "originally created by git-svn\n";
2107
		}
2108
		if ($fp_a ne $fp_b) {
2109
			die "$c~1 = $fp_a, however parsing commit $c ",
2110
			    "revealed that:\n$c~1 = $fp_b\nBUG!\n";
2111
		}
2112

2113
		foreach my $p (@{$parents{$c}}) {
2114
			$skip{$p} = 1;
2115
		}
2116
	}
2117
	(\@linear_refs, \%parents);
2118
}
2119

2120
sub find_file_type_and_diff_status {
2121
	my ($path) = @_;
2122
	return ('dir', '') if $path eq '';
2123

2124
	my $diff_output =
2125
	    command_oneline(qw(diff --cached --name-status --), $path) || "";
2126
	my $diff_status = (split(' ', $diff_output))[0] || "";
2127

2128
	my $ls_tree = command_oneline(qw(ls-tree HEAD), $path) || "";
2129

2130
	return (undef, undef) if !$diff_status && !$ls_tree;
2131

2132
	if ($diff_status eq "A") {
2133
		return ("link", $diff_status) if -l $path;
2134
		return ("dir", $diff_status) if -d $path;
2135
		return ("file", $diff_status);
2136
	}
2137

2138
	my $mode = (split(' ', $ls_tree))[0] || "";
2139

2140
	return ("link", $diff_status) if $mode eq "120000";
2141
	return ("dir", $diff_status) if $mode eq "040000";
2142
	return ("file", $diff_status);
2143
}
2144

2145
sub md5sum {
2146
	my $arg = shift;
2147
	my $ref = ref $arg;
2148
	require Digest::MD5;
2149
	my $md5 = Digest::MD5->new();
2150
        if ($ref eq 'GLOB' || $ref eq 'IO::File' || $ref eq 'File::Temp') {
2151
		$md5->addfile($arg) or croak $!;
2152
	} elsif ($ref eq 'SCALAR') {
2153
		$md5->add($$arg) or croak $!;
2154
	} elsif (!$ref) {
2155
		$md5->add($arg) or croak $!;
2156
	} else {
2157
		fatal "Can't provide MD5 hash for unknown ref type: '", $ref, "'";
2158
	}
2159
	return $md5->hexdigest();
2160
}
2161

2162
sub gc_directory {
2163
	if (can_compress() && -f $_ && basename($_) eq "unhandled.log") {
2164
		my $out_filename = $_ . ".gz";
2165
		open my $in_fh, "<", $_ or die "Unable to open $_: $!\n";
2166
		binmode $in_fh;
2167
		my $gz = Compress::Zlib::gzopen($out_filename, "ab") or
2168
				die "Unable to open $out_filename: $!\n";
2169

2170
		my $res;
2171
		while ($res = sysread($in_fh, my $str, 1024)) {
2172
			$gz->gzwrite($str) or
2173
				die "Unable to write: ".$gz->gzerror()."!\n";
2174
		}
2175
		no warnings 'once'; # $File::Find::name would warn
2176
		unlink $_ or die "unlink $File::Find::name: $!\n";
2177
	} elsif (-f $_ && basename($_) eq "index") {
2178
		unlink $_ or die "unlink $_: $!\n";
2179
	}
2180
}
2181

2182
__END__
2183

2184
Data structures:
2185

2186

2187
$remotes = { # returned by read_all_remotes()
2188
	'svn' => {
2189
		# svn-remote.svn.url=https://svn.musicpd.org
2190
		url => 'https://svn.musicpd.org',
2191
		# svn-remote.svn.fetch=mpd/trunk:trunk
2192
		fetch => {
2193
			'mpd/trunk' => 'trunk',
2194
		},
2195
		# svn-remote.svn.tags=mpd/tags/*:tags/*
2196
		tags => {
2197
			path => {
2198
				left => 'mpd/tags',
2199
				right => '',
2200
				regex => qr!mpd/tags/([^/]+)$!,
2201
				glob => 'tags/*',
2202
			},
2203
			ref => {
2204
				left => 'tags',
2205
				right => '',
2206
				regex => qr!tags/([^/]+)$!,
2207
				glob => 'tags/*',
2208
			},
2209
		}
2210
	}
2211
};
2212

2213
$log_entry hashref as returned by libsvn_log_entry()
2214
{
2215
	log => 'whitespace-formatted log entry
2216
',						# trailing newline is preserved
2217
	revision => '8',			# integer
2218
	date => '2004-02-24T17:01:44.108345Z',	# commit date
2219
	author => 'committer name'
2220
};
2221

2222

2223
# this is generated by generate_diff();
2224
@mods = array of diff-index line hashes, each element represents one line
2225
	of diff-index output
2226

2227
diff-index line ($m hash)
2228
{
2229
	mode_a => first column of diff-index output, no leading ':',
2230
	mode_b => second column of diff-index output,
2231
	sha1_b => sha1sum of the final blob,
2232
	chg => change type [MCRADT],
2233
	file_a => original file name of a file (iff chg is 'C' or 'R')
2234
	file_b => new/current file name of a file (any chg)
2235
}
2236
;
2237

2238
# retval of read_url_paths{,_all}();
2239
$l_map = {
2240
	# repository root url
2241
	'https://svn.musicpd.org' => {
2242
		# repository path 		# GIT_SVN_ID
2243
		'mpd/trunk'		=>	'trunk',
2244
		'mpd/tags/0.11.5'	=>	'tags/0.11.5',
2245
	},
2246
}
2247

2248
Notes:
2249
	I don't trust the each() function on unless I created %hash myself
2250
	because the internal iterator may not have started at base.
2251

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.