Git fork
at reftables-rust 2250 lines 65 kB view raw
1#!/usr/bin/perl 2# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net> 3# License: GPL v2 or later 4require v5.26; 5use warnings $ENV{GIT_PERL_FATAL_WARNINGS} ? qw(FATAL all) : (); 6use strict; 7use 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 14use Carp qw/croak/; 15use File::Basename qw/dirname basename/; 16use File::Path qw/mkpath/; 17use File::Spec; 18use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; 19use Memoize; 20 21use Git::SVN; 22use Git::SVN::Editor; 23use Git::SVN::Fetcher; 24use Git::SVN::Ra; 25use Git::SVN::Prompt; 26use Git::SVN::Log; 27use Git::SVN::Migration; 28 29use 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 39use 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 51BEGIN { 52 Memoize::memoize 'Git::config'; 53 Memoize::memoize 'Git::config_bool'; 54} 55 56 57# From which subdir have we been invoked? 58my $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 64if (! exists $ENV{SVN_SSH} && exists $ENV{GIT_SSH}) { 65 $ENV{SVN_SSH} = $ENV{GIT_SSH}; 66} 67 68if (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. 84sub 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 98sub _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; 110my ($_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. 120sub opt_prefix { return $_prefix || '' } 121 122$Git::SVN::Fetcher::_placeholder_filename = ".gitignore"; 123$_q ||= 0; 124my %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 ); 130my %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 147my ($_trunk, @_tags, @_branches, $_stdlayout); 148my %icv; 149my %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 ); 160my %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 167my %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 300my $term; 301sub 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 308my $cmd; 309for (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 321if ($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 344my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd); 345 346read_git_config(\%opts) if $ENV{GIT_DIR}; 347if ($cmd && ($cmd eq 'log' || $cmd eq 'blame')) { 348 Getopt::Long::Configure('pass_through'); 349} 350my $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] }); 356exit 1 if (!$rv && $cmd && $cmd ne 'log'); 357 358usage(0) if $_help; 359version() if $_version; 360usage(1) unless defined $cmd; 361load_authors() if $_authors; 362if (defined $_authors_prog) { 363 my $abs_file = File::Spec->rel2abs($_authors_prog); 364 $_authors_prog = "'" . $abs_file . "'" if -x $abs_file; 365} 366 367unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) { 368 Git::SVN::Migration::migration_check(); 369} 370Git::SVN::init_vars(); 371eval { 372 Git::SVN::verify_remotes_sanity(); 373 $cmd{$cmd}->[0]->(@ARGV); 374 post_fetch_checkout(); 375}; 376fatal $@ if $@; 377exit 0; 378 379####################### primary functions ###################### 380sub usage { 381 my $exit = shift || 0; 382 my $fd = $exit ? \*STDERR : \*STDOUT; 383 print $fd <<""; 384git-svn - bidirectional operations between a single Subversion tree and git 385usage: 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 405arbitrary identifier if you're tracking multiple SVN branches/repositories in 406one git repository and want to keep them separate. See git-svn(1) for more 407information. 408 409 exit $exit; 410} 411 412sub version { 413 ::_req_svn(); 414 print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n"; 415 exit 0; 416} 417 418sub 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 449sub 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 489sub 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 497sub 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 516sub 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 538sub 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 565sub 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 601sub 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 610sub 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 661sub 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 679sub 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 697sub 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 795sub 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 833sub 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 1096sub 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 1199sub 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 1210sub 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 1238sub 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 1247sub 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 1275sub 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 1299sub 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 1314sub 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 1351sub 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. 1360sub 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. 1390sub 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. 1404sub 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. 1447sub 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 1457sub 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 1491sub 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 1502sub 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 1549sub 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 1692sub 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 1707sub 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 1719sub 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 1728sub 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 1761sub 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 1777sub 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 1820sub verify_ref { 1821 my ($ref) = @_; 1822 eval { command_oneline([ 'rev-parse', '--verify', $ref ], 1823 { STDERR => 0 }); }; 1824} 1825 1826sub 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 1847sub 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 1921sub 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 1929sub 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: 1940sub 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 1957sub 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 1985sub load_object_format { 1986 chomp(my $hash = `git config --get extensions.objectformat`); 1987 $::oid_length = 64 if $hash eq 'sha256'; 1988} 1989 1990sub 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 2002sub cmt_metadata { 2003 return extract_metadata((grep(/^git-svn-id: /, 2004 command(qw/cat-file commit/, shift)))[-1]); 2005} 2006 2007sub 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 2041sub 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 2072sub 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 2079sub 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 2120sub 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 2145sub 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 2162sub 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 2184Data 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 2227diff-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 2248Notes: 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.