1#!/usr/local/bin/perl 2# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net> 3# License: GPL v2 or later 4use 5.008; 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 svn:ignore', 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 svn:ignore listings", 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 300package FakeTerm; 301sub new { 302 my ($class, $reason) = @_; 303 return bless \$reason, shift; 304} 305sub readline { 306 my $self = shift; 307 die "Cannot use readline on FakeTerm: $$self"; 308} 309package main; 310 311my $term; 312sub term_init { 313 $term = eval { 314 require Term::ReadLine; 315 $ENV{"GIT_SVN_NOTTY"} 316 ? new Term::ReadLine 'git-svn', \*STDIN, \*STDOUT 317 : new Term::ReadLine 'git-svn'; 318 }; 319 if ($@) { 320 $term = new FakeTerm "$@: going non-interactive"; 321 } 322} 323 324my $cmd; 325for (my $i = 0; $i < @ARGV; $i++) { 326 if (defined $cmd{$ARGV[$i]}) { 327 $cmd = $ARGV[$i]; 328 splice @ARGV, $i, 1; 329 last; 330 } elsif ($ARGV[$i] eq 'help') { 331 $cmd = $ARGV[$i+1]; 332 usage(0); 333 } 334}; 335 336# make sure we're always running at the top-level working directory 337if ($cmd && $cmd =~ /(?:clone|init|multi-init)$/) { 338 $ENV{GIT_DIR} ||= ".git"; 339 # catch the submodule case 340 if (-f $ENV{GIT_DIR}) { 341 open(my $fh, '<', $ENV{GIT_DIR}) or 342 die "failed to open $ENV{GIT_DIR}: $!\n"; 343 $ENV{GIT_DIR} = $1 if <$fh> =~ /^gitdir: (.+)$/; 344 } 345} elsif ($cmd) { 346 my ($git_dir, $cdup); 347 git_cmd_try { 348 $git_dir = command_oneline([qw/rev-parse --git-dir/]); 349 } "Unable to find .git directory\n"; 350 git_cmd_try { 351 $cdup = command_oneline(qw/rev-parse --show-cdup/); 352 chomp $cdup if ($cdup); 353 $cdup = "." unless ($cdup && length $cdup); 354 } "Already at toplevel, but $git_dir not found\n"; 355 $ENV{GIT_DIR} = $git_dir; 356 chdir $cdup or die "Unable to chdir up to '$cdup'\n"; 357 $_repository = Git->repository(Repository => $ENV{GIT_DIR}); 358} 359 360my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd); 361 362read_git_config(\%opts) if $ENV{GIT_DIR}; 363if ($cmd && ($cmd eq 'log' || $cmd eq 'blame')) { 364 Getopt::Long::Configure('pass_through'); 365} 366my $rv = GetOptions(%opts, 'h|H' => \$_help, 'version|V' => \$_version, 367 'minimize-connections' => \$Git::SVN::Migration::_minimize, 368 'id|i=s' => \$Git::SVN::default_ref_id, 369 'svn-remote|remote|R=s' => sub { 370 $Git::SVN::no_reuse_existing = 1; 371 $Git::SVN::default_repo_id = $_[1] }); 372exit 1 if (!$rv && $cmd && $cmd ne 'log'); 373 374usage(0) if $_help; 375version() if $_version; 376usage(1) unless defined $cmd; 377load_authors() if $_authors; 378if (defined $_authors_prog) { 379 my $abs_file = File::Spec->rel2abs($_authors_prog); 380 $_authors_prog = "'" . $abs_file . "'" if -x $abs_file; 381} 382 383unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) { 384 Git::SVN::Migration::migration_check(); 385} 386Git::SVN::init_vars(); 387eval { 388 Git::SVN::verify_remotes_sanity(); 389 $cmd{$cmd}->[0]->(@ARGV); 390 post_fetch_checkout(); 391}; 392fatal $@ if $@; 393exit 0; 394 395####################### primary functions ###################### 396sub usage { 397 my $exit = shift || 0; 398 my $fd = $exit ? \*STDERR : \*STDOUT; 399 print $fd <<""; 400git-svn - bidirectional operations between a single Subversion tree and git 401usage: git svn <command> [options] [arguments]\n 402 403 print $fd "Available commands:\n" unless $cmd; 404 405 foreach (sort keys %cmd) { 406 next if $cmd && $cmd ne $_; 407 next if /^multi-/; # don't show deprecated commands 408 print $fd ' ',pack('A17',$_),$cmd{$_}->[1],"\n"; 409 foreach (sort keys %{$cmd{$_}->[2]}) { 410 # mixed-case options are for .git/config only 411 next if /[A-Z]/ && /^[a-z]+$/i; 412 # prints out arguments as they should be passed: 413 my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : ''; 414 print $fd ' ' x 21, join(', ', map { length $_ > 1 ? 415 "--$_" : "-$_" } 416 split /\|/,$_)," $x\n"; 417 } 418 } 419 print $fd <<""; 420\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an 421arbitrary identifier if you're tracking multiple SVN branches/repositories in 422one git repository and want to keep them separate. See git-svn(1) for more 423information. 424 425 exit $exit; 426} 427 428sub version { 429 ::_req_svn(); 430 print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n"; 431 exit 0; 432} 433 434sub ask { 435 my ($prompt, %arg) = @_; 436 my $valid_re = $arg{valid_re}; 437 my $default = $arg{default}; 438 my $resp; 439 my $i = 0; 440 term_init() unless $term; 441 442 if ( !( defined($term->IN) 443 && defined( fileno($term->IN) ) 444 && defined( $term->OUT ) 445 && defined( fileno($term->OUT) ) ) ){ 446 return defined($default) ? $default : undef; 447 } 448 449 while ($i++ < 10) { 450 $resp = $term->readline($prompt); 451 if (!defined $resp) { # EOF 452 print "\n"; 453 return defined $default ? $default : undef; 454 } 455 if ($resp eq '' and defined $default) { 456 return $default; 457 } 458 if (!defined $valid_re or $resp =~ /$valid_re/) { 459 return $resp; 460 } 461 } 462 return undef; 463} 464 465sub do_git_init_db { 466 unless (-d $ENV{GIT_DIR}) { 467 my @init_db = ('init'); 468 push @init_db, "--template=$_template" if defined $_template; 469 if (defined $_shared) { 470 if ($_shared =~ /[a-z]/) { 471 push @init_db, "--shared=$_shared"; 472 } else { 473 push @init_db, "--shared"; 474 } 475 } 476 command_noisy(@init_db); 477 $_repository = Git->repository(Repository => ".git"); 478 } 479 my $set; 480 my $pfx = "svn-remote.$Git::SVN::default_repo_id"; 481 foreach my $i (keys %icv) { 482 die "'$set' and '$i' cannot both be set\n" if $set; 483 next unless defined $icv{$i}; 484 command_noisy('config', "$pfx.$i", $icv{$i}); 485 $set = $i; 486 } 487 my $ignore_paths_regex = \$Git::SVN::Fetcher::_ignore_regex; 488 command_noisy('config', "$pfx.ignore-paths", $$ignore_paths_regex) 489 if defined $$ignore_paths_regex; 490 my $include_paths_regex = \$Git::SVN::Fetcher::_include_regex; 491 command_noisy('config', "$pfx.include-paths", $$include_paths_regex) 492 if defined $$include_paths_regex; 493 my $ignore_refs_regex = \$Git::SVN::Ra::_ignore_refs_regex; 494 command_noisy('config', "$pfx.ignore-refs", $$ignore_refs_regex) 495 if defined $$ignore_refs_regex; 496 497 if (defined $Git::SVN::Fetcher::_preserve_empty_dirs) { 498 my $fname = \$Git::SVN::Fetcher::_placeholder_filename; 499 command_noisy('config', "$pfx.preserve-empty-dirs", 'true'); 500 command_noisy('config', "$pfx.placeholder-filename", $$fname); 501 } 502 load_object_format(); 503} 504 505sub init_subdir { 506 my $repo_path = shift or return; 507 mkpath([$repo_path]) unless -d $repo_path; 508 chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n"; 509 $ENV{GIT_DIR} = '.git'; 510 $_repository = Git->repository(Repository => $ENV{GIT_DIR}); 511} 512 513sub cmd_clone { 514 my ($url, $path) = @_; 515 if (!$url) { 516 die "SVN repository location required ", 517 "as a command-line argument\n"; 518 } elsif (!defined $path && 519 (defined $_trunk || @_branches || @_tags || 520 defined $_stdlayout) && 521 $url !~ m#^[a-z\+]+://#) { 522 $path = $url; 523 } 524 $path = basename($url) if !defined $path || !length $path; 525 my $authors_absolute = $_authors ? File::Spec->rel2abs($_authors) : ""; 526 cmd_init($url, $path); 527 command_oneline('config', 'svn.authorsfile', $authors_absolute) 528 if $_authors; 529 Git::SVN::fetch_all($Git::SVN::default_repo_id); 530} 531 532sub cmd_init { 533 if (defined $_stdlayout) { 534 $_trunk = 'trunk' if (!defined $_trunk); 535 @_tags = 'tags' if (! @_tags); 536 @_branches = 'branches' if (! @_branches); 537 } 538 if (defined $_trunk || @_branches || @_tags) { 539 return cmd_multi_init(@_); 540 } 541 my $url = shift or die "SVN repository location required ", 542 "as a command-line argument\n"; 543 $url = canonicalize_url($url); 544 init_subdir(@_); 545 do_git_init_db(); 546 547 if ($Git::SVN::_minimize_url eq 'unset') { 548 $Git::SVN::_minimize_url = 0; 549 } 550 551 Git::SVN->init($url); 552} 553 554sub cmd_fetch { 555 if (grep /^\d+=./, @_) { 556 die "'<rev>=<commit>' fetch arguments are ", 557 "no longer supported.\n"; 558 } 559 my ($remote) = @_; 560 if (@_ > 1) { 561 die "usage: $0 fetch [--all] [--parent] [svn-remote]\n"; 562 } 563 $Git::SVN::no_reuse_existing = undef; 564 if ($_fetch_parent) { 565 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); 566 unless ($gs) { 567 die "Unable to determine upstream SVN information from ", 568 "working tree history\n"; 569 } 570 # just fetch, don't checkout. 571 $_no_checkout = 'true'; 572 $_fetch_all ? $gs->fetch_all : $gs->fetch; 573 } elsif ($_fetch_all) { 574 cmd_multi_fetch(); 575 } else { 576 $remote ||= $Git::SVN::default_repo_id; 577 Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes()); 578 } 579} 580 581sub cmd_set_tree { 582 my (@commits) = @_; 583 if ($_stdin || !@commits) { 584 print "Reading from stdin...\n"; 585 @commits = (); 586 while (<STDIN>) { 587 if (/\b($oid_short)\b/o) { 588 unshift @commits, $1; 589 } 590 } 591 } 592 my @revs; 593 foreach my $c (@commits) { 594 my @tmp = command('rev-parse',$c); 595 if (scalar @tmp == 1) { 596 push @revs, $tmp[0]; 597 } elsif (scalar @tmp > 1) { 598 push @revs, reverse(command('rev-list',@tmp)); 599 } else { 600 fatal "Failed to rev-parse $c"; 601 } 602 } 603 my $gs = Git::SVN->new; 604 my ($r_last, $cmt_last) = $gs->last_rev_commit; 605 $gs->fetch; 606 if (defined $gs->{last_rev} && $r_last != $gs->{last_rev}) { 607 fatal "There are new revisions that were fetched ", 608 "and need to be merged (or acknowledged) ", 609 "before committing.\nlast rev: $r_last\n", 610 " current: $gs->{last_rev}"; 611 } 612 $gs->set_tree($_) foreach @revs; 613 print "Done committing ",scalar @revs," revisions to SVN\n"; 614 unlink $gs->{index}; 615} 616 617sub split_merge_info_range { 618 my ($range) = @_; 619 if ($range =~ /(\d+)-(\d+)/) { 620 return (int($1), int($2)); 621 } else { 622 return (int($range), int($range)); 623 } 624} 625 626sub combine_ranges { 627 my ($in) = @_; 628 629 my @fnums = (); 630 my @arr = split(/,/, $in); 631 for my $element (@arr) { 632 my ($start, $end) = split_merge_info_range($element); 633 push @fnums, $start; 634 } 635 636 my @sorted = @arr [ sort { 637 $fnums[$a] <=> $fnums[$b] 638 } 0..$#arr ]; 639 640 my @return = (); 641 my $last = -1; 642 my $first = -1; 643 for my $element (@sorted) { 644 my ($start, $end) = split_merge_info_range($element); 645 646 if ($last == -1) { 647 $first = $start; 648 $last = $end; 649 next; 650 } 651 if ($start <= $last+1) { 652 if ($end > $last) { 653 $last = $end; 654 } 655 next; 656 } 657 if ($first == $last) { 658 push @return, "$first"; 659 } else { 660 push @return, "$first-$last"; 661 } 662 $first = $start; 663 $last = $end; 664 } 665 666 if ($first != -1) { 667 if ($first == $last) { 668 push @return, "$first"; 669 } else { 670 push @return, "$first-$last"; 671 } 672 } 673 674 return join(',', @return); 675} 676 677sub merge_revs_into_hash { 678 my ($hash, $minfo) = @_; 679 my @lines = split(' ', $minfo); 680 681 for my $line (@lines) { 682 my ($branchpath, $revs) = split(/:/, $line); 683 684 if (exists($hash->{$branchpath})) { 685 # Merge the two revision sets 686 my $combined = "$hash->{$branchpath},$revs"; 687 $hash->{$branchpath} = combine_ranges($combined); 688 } else { 689 # Just do range combining for consolidation 690 $hash->{$branchpath} = combine_ranges($revs); 691 } 692 } 693} 694 695sub merge_merge_info { 696 my ($mergeinfo_one, $mergeinfo_two, $ignore_branch) = @_; 697 my %result_hash = (); 698 699 merge_revs_into_hash(\%result_hash, $mergeinfo_one); 700 merge_revs_into_hash(\%result_hash, $mergeinfo_two); 701 702 delete $result_hash{$ignore_branch} if $ignore_branch; 703 704 my $result = ''; 705 # Sort below is for consistency's sake 706 for my $branchname (sort keys(%result_hash)) { 707 my $revlist = $result_hash{$branchname}; 708 $result .= "$branchname:$revlist\n" 709 } 710 return $result; 711} 712 713sub populate_merge_info { 714 my ($d, $gs, $uuid, $linear_refs, $rewritten_parent) = @_; 715 716 my %parentshash; 717 read_commit_parents(\%parentshash, $d); 718 my @parents = @{$parentshash{$d}}; 719 if ($#parents > 0) { 720 # Merge commit 721 my $all_parents_ok = 1; 722 my $aggregate_mergeinfo = ''; 723 my $rooturl = $gs->repos_root; 724 my ($target_branch) = $gs->full_pushurl =~ /^\Q$rooturl\E(.*)/; 725 726 if (defined($rewritten_parent)) { 727 # Replace first parent with newly-rewritten version 728 shift @parents; 729 unshift @parents, $rewritten_parent; 730 } 731 732 foreach my $parent (@parents) { 733 my ($branchurl, $svnrev, $paruuid) = 734 cmt_metadata($parent); 735 736 unless (defined($svnrev)) { 737 # Should have been caught be preflight check 738 fatal "merge commit $d has ancestor $parent, but that change " 739 ."does not have git-svn metadata!"; 740 } 741 unless ($branchurl =~ /^\Q$rooturl\E(.*)/) { 742 fatal "commit $parent git-svn metadata changed mid-run!"; 743 } 744 my $branchpath = $1; 745 746 my $ra = Git::SVN::Ra->new($branchurl); 747 my (undef, undef, $props) = 748 $ra->get_dir(canonicalize_path("."), $svnrev); 749 my $par_mergeinfo = $props->{'svn:mergeinfo'}; 750 unless (defined $par_mergeinfo) { 751 $par_mergeinfo = ''; 752 } 753 # Merge previous mergeinfo values 754 $aggregate_mergeinfo = 755 merge_merge_info($aggregate_mergeinfo, 756 $par_mergeinfo, 757 $target_branch); 758 759 next if $parent eq $parents[0]; # Skip first parent 760 # Add new changes being placed in tree by merge 761 my @cmd = (qw/rev-list --reverse/, 762 $parent, qw/--not/); 763 foreach my $par (@parents) { 764 unless ($par eq $parent) { 765 push @cmd, $par; 766 } 767 } 768 my @revsin = (); 769 my ($revlist, $ctx) = command_output_pipe(@cmd); 770 while (<$revlist>) { 771 my $irev = $_; 772 chomp $irev; 773 my (undef, $csvnrev, undef) = 774 cmt_metadata($irev); 775 unless (defined $csvnrev) { 776 # A child is missing SVN annotations... 777 # this might be OK, or might not be. 778 warn "W:child $irev is merged into revision " 779 ."$d but does not have git-svn metadata. " 780 ."This means git-svn cannot determine the " 781 ."svn revision numbers to place into the " 782 ."svn:mergeinfo property. You must ensure " 783 ."a branch is entirely committed to " 784 ."SVN before merging it in order for " 785 ."svn:mergeinfo population to function " 786 ."properly"; 787 } 788 push @revsin, $csvnrev; 789 } 790 command_close_pipe($revlist, $ctx); 791 792 last unless $all_parents_ok; 793 794 # We now have a list of all SVN revnos which are 795 # merged by this particular parent. Integrate them. 796 next if $#revsin == -1; 797 my $newmergeinfo = "$branchpath:" . join(',', @revsin); 798 $aggregate_mergeinfo = 799 merge_merge_info($aggregate_mergeinfo, 800 $newmergeinfo, 801 $target_branch); 802 } 803 if ($all_parents_ok and $aggregate_mergeinfo) { 804 return $aggregate_mergeinfo; 805 } 806 } 807 808 return undef; 809} 810 811sub dcommit_rebase { 812 my ($is_last, $current, $fetched_ref, $svn_error) = @_; 813 my @diff; 814 815 if ($svn_error) { 816 print STDERR "\nERROR from SVN:\n", 817 $svn_error->expanded_message, "\n"; 818 } 819 unless ($_no_rebase) { 820 # we always want to rebase against the current HEAD, 821 # not any head that was passed to us 822 @diff = command('diff-tree', $current, 823 $fetched_ref, '--'); 824 my @finish; 825 if (@diff) { 826 @finish = rebase_cmd(); 827 print STDERR "W: $current and ", $fetched_ref, 828 " differ, using @finish:\n", 829 join("\n", @diff), "\n"; 830 } elsif ($is_last) { 831 print "No changes between ", $current, " and ", 832 $fetched_ref, 833 "\nResetting to the latest ", 834 $fetched_ref, "\n"; 835 @finish = qw/reset --mixed/; 836 } 837 command_noisy(@finish, $fetched_ref) if @finish; 838 } 839 if ($svn_error) { 840 die "ERROR: Not all changes have been committed into SVN" 841 .($_no_rebase ? ".\n" : ", however the committed\n" 842 ."ones (if any) seem to be successfully integrated " 843 ."into the working tree.\n") 844 ."Please see the above messages for details.\n"; 845 } 846 return @diff; 847} 848 849sub cmd_dcommit { 850 my $head = shift; 851 command_noisy(qw/update-index --refresh/); 852 git_cmd_try { command_oneline(qw/diff-index --quiet HEAD --/) } 853 'Cannot dcommit with a dirty index. Commit your changes first, ' 854 . "or stash them with `git stash'.\n"; 855 $head ||= 'HEAD'; 856 857 my $old_head; 858 if ($head ne 'HEAD') { 859 $old_head = eval { 860 command_oneline([qw/symbolic-ref -q HEAD/]) 861 }; 862 if ($old_head) { 863 $old_head =~ s{^refs/heads/}{}; 864 } else { 865 $old_head = eval { command_oneline(qw/rev-parse HEAD/) }; 866 } 867 command(['checkout', $head], STDERR => 0); 868 } 869 870 my @refs; 871 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD', \@refs); 872 unless ($gs) { 873 die "Unable to determine upstream SVN information from ", 874 "$head history.\nPerhaps the repository is empty."; 875 } 876 877 if (defined $_commit_url) { 878 $url = $_commit_url; 879 } else { 880 $url = eval { command_oneline('config', '--get', 881 "svn-remote.$gs->{repo_id}.commiturl") }; 882 if (!$url) { 883 $url = $gs->full_pushurl 884 } 885 } 886 887 my $last_rev = $_revision if defined $_revision; 888 if ($url) { 889 print "Committing to $url ...\n"; 890 } 891 my ($linear_refs, $parents) = linearize_history($gs, \@refs); 892 if ($_no_rebase && scalar(@$linear_refs) > 1) { 893 warn "Attempting to commit more than one change while ", 894 "--no-rebase is enabled.\n", 895 "If these changes depend on each other, re-running ", 896 "without --no-rebase may be required." 897 } 898 899 if (defined $_interactive){ 900 my $ask_default = "y"; 901 foreach my $d (@$linear_refs){ 902 my ($fh, $ctx) = command_output_pipe(qw(show --summary), "$d"); 903 while (<$fh>){ 904 print $_; 905 } 906 command_close_pipe($fh, $ctx); 907 $_ = ask("Commit this patch to SVN? ([y]es (default)|[n]o|[q]uit|[a]ll): ", 908 valid_re => qr/^(?:yes|y|no|n|quit|q|all|a)/i, 909 default => $ask_default); 910 die "Commit this patch reply required" unless defined $_; 911 if (/^[nq]/i) { 912 exit(0); 913 } elsif (/^a/i) { 914 last; 915 } 916 } 917 } 918 919 my $expect_url = $url; 920 921 my $push_merge_info = eval { 922 command_oneline(qw/config --get svn.pushmergeinfo/) 923 }; 924 if (not defined($push_merge_info) 925 or $push_merge_info eq "false" 926 or $push_merge_info eq "no" 927 or $push_merge_info eq "never") { 928 $push_merge_info = 0; 929 } 930 931 unless (defined($_merge_info) || ! $push_merge_info) { 932 # Preflight check of changes to ensure no issues with mergeinfo 933 # This includes check for uncommitted-to-SVN parents 934 # (other than the first parent, which we will handle), 935 # information from different SVN repos, and paths 936 # which are not underneath this repository root. 937 my $rooturl = $gs->repos_root; 938 Git::SVN::remove_username($rooturl); 939 foreach my $d (@$linear_refs) { 940 my %parentshash; 941 read_commit_parents(\%parentshash, $d); 942 my @realparents = @{$parentshash{$d}}; 943 if ($#realparents > 0) { 944 # Merge commit 945 shift @realparents; # Remove/ignore first parent 946 foreach my $parent (@realparents) { 947 my ($branchurl, $svnrev, $paruuid) = cmt_metadata($parent); 948 unless (defined $paruuid) { 949 # A parent is missing SVN annotations... 950 # abort the whole operation. 951 fatal "$parent is merged into revision $d, " 952 ."but does not have git-svn metadata. " 953 ."Either dcommit the branch or use a " 954 ."local cherry-pick, FF merge, or rebase " 955 ."instead of an explicit merge commit."; 956 } 957 958 unless ($paruuid eq $uuid) { 959 # Parent has SVN metadata from different repository 960 fatal "merge parent $parent for change $d has " 961 ."git-svn uuid $paruuid, while current change " 962 ."has uuid $uuid!"; 963 } 964 965 unless ($branchurl =~ /^\Q$rooturl\E(.*)/) { 966 # This branch is very strange indeed. 967 fatal "merge parent $parent for $d is on branch " 968 ."$branchurl, which is not under the " 969 ."git-svn root $rooturl!"; 970 } 971 } 972 } 973 } 974 } 975 976 my $rewritten_parent; 977 my $current_head = command_oneline(qw/rev-parse HEAD/); 978 Git::SVN::remove_username($expect_url); 979 if (defined($_merge_info)) { 980 $_merge_info =~ tr{ }{\n}; 981 } 982 while (1) { 983 my $d = shift @$linear_refs or last; 984 unless (defined $last_rev) { 985 (undef, $last_rev, undef) = cmt_metadata("$d~1"); 986 unless (defined $last_rev) { 987 fatal "Unable to extract revision information ", 988 "from commit $d~1"; 989 } 990 } 991 if ($_dry_run) { 992 print "diff-tree $d~1 $d\n"; 993 } else { 994 my $cmt_rev; 995 996 unless (defined($_merge_info) || ! $push_merge_info) { 997 $_merge_info = populate_merge_info($d, $gs, 998 $uuid, 999 $linear_refs, 1000 $rewritten_parent); 1001 } 1002 1003 my %ed_opts = ( r => $last_rev, 1004 log => get_commit_entry($d)->{log}, 1005 ra => Git::SVN::Ra->new($url), 1006 config => SVN::Core::config_get_config( 1007 $Git::SVN::Ra::config_dir 1008 ), 1009 tree_a => "$d~1", 1010 tree_b => $d, 1011 editor_cb => sub { 1012 print "Committed r$_[0]\n"; 1013 $cmt_rev = $_[0]; 1014 }, 1015 mergeinfo => $_merge_info, 1016 svn_path => ''); 1017 1018 my $err_handler = $SVN::Error::handler; 1019 $SVN::Error::handler = sub { 1020 my $err = shift; 1021 dcommit_rebase(1, $current_head, $gs->refname, 1022 $err); 1023 }; 1024 1025 if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) { 1026 print "No changes\n$d~1 == $d\n"; 1027 } elsif ($parents->{$d} && @{$parents->{$d}}) { 1028 $gs->{inject_parents_dcommit}->{$cmt_rev} = 1029 $parents->{$d}; 1030 } 1031 $_fetch_all ? $gs->fetch_all : $gs->fetch; 1032 $SVN::Error::handler = $err_handler; 1033 $last_rev = $cmt_rev; 1034 next if $_no_rebase; 1035 1036 my @diff = dcommit_rebase(@$linear_refs == 0, $d, 1037 $gs->refname, undef); 1038 1039 $rewritten_parent = command_oneline(qw/rev-parse/, 1040 $gs->refname); 1041 1042 if (@diff) { 1043 $current_head = command_oneline(qw/rev-parse 1044 HEAD/); 1045 @refs = (); 1046 my ($url_, $rev_, $uuid_, $gs_) = 1047 working_head_info('HEAD', \@refs); 1048 my ($linear_refs_, $parents_) = 1049 linearize_history($gs_, \@refs); 1050 if (scalar(@$linear_refs) != 1051 scalar(@$linear_refs_)) { 1052 fatal "# of revisions changed ", 1053 "\nbefore:\n", 1054 join("\n", @$linear_refs), 1055 "\n\nafter:\n", 1056 join("\n", @$linear_refs_), "\n", 1057 'If you are attempting to commit ', 1058 "merges, try running:\n\t", 1059 'git rebase --interactive', 1060 '--rebase-merges ', 1061 $gs->refname, 1062 "\nBefore dcommitting"; 1063 } 1064 if ($url_ ne $expect_url) { 1065 if ($url_ eq $gs->metadata_url) { 1066 print 1067 "Accepting rewritten URL:", 1068 " $url_\n"; 1069 } else { 1070 fatal 1071 "URL mismatch after rebase:", 1072 " $url_ != $expect_url"; 1073 } 1074 } 1075 if ($uuid_ ne $uuid) { 1076 fatal "uuid mismatch after rebase: ", 1077 "$uuid_ != $uuid"; 1078 } 1079 # remap parents 1080 my (%p, @l, $i); 1081 for ($i = 0; $i < scalar @$linear_refs; $i++) { 1082 my $new = $linear_refs_->[$i] or next; 1083 $p{$new} = 1084 $parents->{$linear_refs->[$i]}; 1085 push @l, $new; 1086 } 1087 $parents = \%p; 1088 $linear_refs = \@l; 1089 undef $last_rev; 1090 } 1091 } 1092 } 1093 1094 if ($old_head) { 1095 my $new_head = command_oneline(qw/rev-parse HEAD/); 1096 my $new_is_symbolic = eval { 1097 command_oneline(qw/symbolic-ref -q HEAD/); 1098 }; 1099 if ($new_is_symbolic) { 1100 print "dcommitted the branch ", $head, "\n"; 1101 } else { 1102 print "dcommitted on a detached HEAD because you gave ", 1103 "a revision argument.\n", 1104 "The rewritten commit is: ", $new_head, "\n"; 1105 } 1106 command(['checkout', $old_head], STDERR => 0); 1107 } 1108 1109 unlink $gs->{index}; 1110} 1111 1112sub cmd_branch { 1113 my ($branch_name, $head) = @_; 1114 1115 unless (defined $branch_name && length $branch_name) { 1116 die(($_tag ? "tag" : "branch") . " name required\n"); 1117 } 1118 $head ||= 'HEAD'; 1119 1120 my (undef, $rev, undef, $gs) = working_head_info($head); 1121 my $src = $gs->full_pushurl; 1122 1123 my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}}; 1124 my $allglobs = $remote->{ $_tag ? 'tags' : 'branches' }; 1125 my $glob; 1126 if ($#{$allglobs} == 0) { 1127 $glob = $allglobs->[0]; 1128 } else { 1129 unless(defined $_branch_dest) { 1130 die "Multiple ", 1131 $_tag ? "tag" : "branch", 1132 " paths defined for Subversion repository.\n", 1133 "You must specify where you want to create the ", 1134 $_tag ? "tag" : "branch", 1135 " with the --destination argument.\n"; 1136 } 1137 foreach my $g (@{$allglobs}) { 1138 my $re = Git::SVN::Editor::glob2pat($g->{path}->{left}); 1139 if ($_branch_dest =~ /$re/) { 1140 $glob = $g; 1141 last; 1142 } 1143 } 1144 unless (defined $glob) { 1145 my $dest_re = qr/\b\Q$_branch_dest\E\b/; 1146 foreach my $g (@{$allglobs}) { 1147 $g->{path}->{left} =~ /$dest_re/ or next; 1148 if (defined $glob) { 1149 die "Ambiguous destination: ", 1150 $_branch_dest, "\nmatches both '", 1151 $glob->{path}->{left}, "' and '", 1152 $g->{path}->{left}, "'\n"; 1153 } 1154 $glob = $g; 1155 } 1156 unless (defined $glob) { 1157 die "Unknown ", 1158 $_tag ? "tag" : "branch", 1159 " destination $_branch_dest\n"; 1160 } 1161 } 1162 } 1163 my ($lft, $rgt) = @{ $glob->{path} }{qw/left right/}; 1164 my $url; 1165 if (defined $_commit_url) { 1166 $url = $_commit_url; 1167 } else { 1168 $url = eval { command_oneline('config', '--get', 1169 "svn-remote.$gs->{repo_id}.commiturl") }; 1170 if (!$url) { 1171 $url = $remote->{pushurl} || $remote->{url}; 1172 } 1173 } 1174 my $dst = join '/', $url, $lft, $branch_name, ($rgt || ()); 1175 1176 if ($dst =~ /^https:/ && $src =~ /^http:/) { 1177 $src=~s/^http:/https:/; 1178 } 1179 1180 ::_req_svn(); 1181 require SVN::Client; 1182 1183 my ($config, $baton, undef) = Git::SVN::Ra::prepare_config_once(); 1184 my $ctx = SVN::Client->new( 1185 auth => $baton, 1186 config => $config, 1187 log_msg => sub { 1188 ${ $_[0] } = defined $_message 1189 ? $_message 1190 : 'Create ' . ($_tag ? 'tag ' : 'branch ' ) 1191 . $branch_name; 1192 }, 1193 ); 1194 1195 eval { 1196 $ctx->ls($dst, 'HEAD', 0); 1197 } and die "branch ${branch_name} already exists\n"; 1198 1199 if ($_parents) { 1200 mk_parent_dirs($ctx, $dst); 1201 } 1202 1203 print "Copying ${src} at r${rev} to ${dst}...\n"; 1204 $ctx->copy($src, $rev, $dst) 1205 unless $_dry_run; 1206 1207 # Release resources held by ctx before creating another SVN::Ra 1208 # so destruction is orderly. This seems necessary with SVN 1.9.5 1209 # to avoid segfaults. 1210 $ctx = undef; 1211 1212 $gs->fetch_all; 1213} 1214 1215sub mk_parent_dirs { 1216 my ($ctx, $parent) = @_; 1217 $parent =~ s{/[^/]*$}{}; 1218 1219 if (!eval{$ctx->ls($parent, 'HEAD', 0)}) { 1220 mk_parent_dirs($ctx, $parent); 1221 print "Creating parent folder ${parent} ...\n"; 1222 $ctx->mkdir($parent) unless $_dry_run; 1223 } 1224} 1225 1226sub cmd_find_rev { 1227 my $revision_or_hash = shift or die "SVN or git revision required ", 1228 "as a command-line argument\n"; 1229 my $result; 1230 if ($revision_or_hash =~ /^r\d+$/) { 1231 my $head = shift; 1232 $head ||= 'HEAD'; 1233 my @refs; 1234 my (undef, undef, $uuid, $gs) = working_head_info($head, \@refs); 1235 unless ($gs) { 1236 die "Unable to determine upstream SVN information from ", 1237 "$head history\n"; 1238 } 1239 my $desired_revision = substr($revision_or_hash, 1); 1240 if ($_before) { 1241 $result = $gs->find_rev_before($desired_revision, 1); 1242 } elsif ($_after) { 1243 $result = $gs->find_rev_after($desired_revision, 1); 1244 } else { 1245 $result = $gs->rev_map_get($desired_revision, $uuid); 1246 } 1247 } else { 1248 my (undef, $rev, undef) = cmt_metadata($revision_or_hash); 1249 $result = $rev; 1250 } 1251 print "$result\n" if $result; 1252} 1253 1254sub auto_create_empty_directories { 1255 my ($gs) = @_; 1256 my $var = eval { command_oneline('config', '--get', '--bool', 1257 "svn-remote.$gs->{repo_id}.automkdirs") }; 1258 # By default, create empty directories by consulting the unhandled log, 1259 # but allow setting it to 'false' to skip it. 1260 return !($var && $var eq 'false'); 1261} 1262 1263sub cmd_rebase { 1264 command_noisy(qw/update-index --refresh/); 1265 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); 1266 unless ($gs) { 1267 die "Unable to determine upstream SVN information from ", 1268 "working tree history\n"; 1269 } 1270 if ($_dry_run) { 1271 print "Remote Branch: " . $gs->refname . "\n"; 1272 print "SVN URL: " . $url . "\n"; 1273 return; 1274 } 1275 if (command(qw/diff-index HEAD --/)) { 1276 print STDERR "Cannot rebase with uncommitted changes:\n"; 1277 command_noisy('status'); 1278 exit 1; 1279 } 1280 unless ($_local) { 1281 # rebase will checkout for us, so no need to do it explicitly 1282 $_no_checkout = 'true'; 1283 $_fetch_all ? $gs->fetch_all : $gs->fetch; 1284 } 1285 command_noisy(rebase_cmd(), $gs->refname); 1286 if (auto_create_empty_directories($gs)) { 1287 $gs->mkemptydirs; 1288 } 1289} 1290 1291sub cmd_show_ignore { 1292 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); 1293 $gs ||= Git::SVN->new; 1294 my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum); 1295 $gs->prop_walk($gs->path, $r, sub { 1296 my ($gs, $path, $props) = @_; 1297 print STDOUT "\n# $path\n"; 1298 my $s = $props->{'svn:ignore'} or return; 1299 $s =~ s/[\r\n]+/\n/g; 1300 $s =~ s/^\n+//; 1301 chomp $s; 1302 $s =~ s#^#$path#gm; 1303 print STDOUT "$s\n"; 1304 }); 1305} 1306 1307sub cmd_show_externals { 1308 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); 1309 $gs ||= Git::SVN->new; 1310 my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum); 1311 $gs->prop_walk($gs->path, $r, sub { 1312 my ($gs, $path, $props) = @_; 1313 print STDOUT "\n# $path\n"; 1314 my $s = $props->{'svn:externals'} or return; 1315 $s =~ s/[\r\n]+/\n/g; 1316 chomp $s; 1317 $s =~ s#^#$path#gm; 1318 print STDOUT "$s\n"; 1319 }); 1320} 1321 1322sub cmd_create_ignore { 1323 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); 1324 $gs ||= Git::SVN->new; 1325 my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum); 1326 $gs->prop_walk($gs->path, $r, sub { 1327 my ($gs, $path, $props) = @_; 1328 # $path is of the form /path/to/dir/ 1329 $path = '.' . $path; 1330 # SVN can have attributes on empty directories, 1331 # which git won't track 1332 mkpath([$path]) unless -d $path; 1333 my $ignore = $path . '.gitignore'; 1334 my $s = $props->{'svn:ignore'} or return; 1335 open(GITIGNORE, '>', $ignore) 1336 or fatal("Failed to open `$ignore' for writing: $!"); 1337 $s =~ s/[\r\n]+/\n/g; 1338 $s =~ s/^\n+//; 1339 chomp $s; 1340 # Prefix all patterns so that the ignore doesn't apply 1341 # to sub-directories. 1342 $s =~ s#^#/#gm; 1343 print GITIGNORE "$s\n"; 1344 close(GITIGNORE) 1345 or fatal("Failed to close `$ignore': $!"); 1346 command_noisy('add', '-f', $ignore); 1347 }); 1348} 1349 1350sub cmd_mkdirs { 1351 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); 1352 $gs ||= Git::SVN->new; 1353 $gs->mkemptydirs($_revision); 1354} 1355 1356# get_svnprops(PATH) 1357# ------------------ 1358# Helper for cmd_propget and cmd_proplist below. 1359sub get_svnprops { 1360 my $path = shift; 1361 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); 1362 $gs ||= Git::SVN->new; 1363 1364 # prefix THE PATH by the sub-directory from which the user 1365 # invoked us. 1366 $path = $cmd_dir_prefix . $path; 1367 fatal("No such file or directory: $path") unless -e $path; 1368 my $is_dir = -d $path ? 1 : 0; 1369 $path = join_paths($gs->path, $path); 1370 1371 # canonicalize the path (otherwise libsvn will abort or fail to 1372 # find the file) 1373 $path = canonicalize_path($path); 1374 1375 my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum); 1376 my $props; 1377 if ($is_dir) { 1378 (undef, undef, $props) = $gs->ra->get_dir($path, $r); 1379 } 1380 else { 1381 (undef, $props) = $gs->ra->get_file($path, $r, undef); 1382 } 1383 return $props; 1384} 1385 1386# cmd_propget (PROP, PATH) 1387# ------------------------ 1388# Print the SVN property PROP for PATH. 1389sub cmd_propget { 1390 my ($prop, $path) = @_; 1391 $path = '.' if not defined $path; 1392 usage(1) if not defined $prop; 1393 my $props = get_svnprops($path); 1394 if (not defined $props->{$prop}) { 1395 fatal("`$path' does not have a `$prop' SVN property."); 1396 } 1397 print $props->{$prop} . "\n"; 1398} 1399 1400# cmd_propset (PROPNAME, PROPVAL, PATH) 1401# ------------------------ 1402# Adjust the SVN property PROPNAME to PROPVAL for PATH. 1403sub cmd_propset { 1404 my ($propname, $propval, $path) = @_; 1405 $path = '.' if not defined $path; 1406 $path = $cmd_dir_prefix . $path; 1407 usage(1) if not defined $propname; 1408 usage(1) if not defined $propval; 1409 my $file = basename($path); 1410 my $dn = dirname($path); 1411 my $cur_props = Git::SVN::Editor::check_attr( "svn-properties", $path ); 1412 my @new_props; 1413 if (!$cur_props || $cur_props eq "unset" || $cur_props eq "" || $cur_props eq "set") { 1414 push @new_props, "$propname=$propval"; 1415 } else { 1416 # TODO: handle combining properties better 1417 my @props = split(/;/, $cur_props); 1418 my $replaced_prop; 1419 foreach my $prop (@props) { 1420 # Parse 'name=value' syntax and set the property. 1421 if ($prop =~ /([^=]+)=(.*)/) { 1422 my ($n,$v) = ($1,$2); 1423 if ($n eq $propname) { 1424 $v = $propval; 1425 $replaced_prop = 1; 1426 } 1427 push @new_props, "$n=$v"; 1428 } 1429 } 1430 if (!$replaced_prop) { 1431 push @new_props, "$propname=$propval"; 1432 } 1433 } 1434 my $attrfile = "$dn/.gitattributes"; 1435 open my $attrfh, '>>', $attrfile or die "Can't open $attrfile: $!\n"; 1436 # TODO: don't simply append here if $file already has svn-properties 1437 my $new_props = join(';', @new_props); 1438 print $attrfh "$file svn-properties=$new_props\n" or 1439 die "write to $attrfile: $!\n"; 1440 close $attrfh or die "close $attrfile: $!\n"; 1441} 1442 1443# cmd_proplist (PATH) 1444# ------------------- 1445# Print the list of SVN properties for PATH. 1446sub cmd_proplist { 1447 my $path = shift; 1448 $path = '.' if not defined $path; 1449 my $props = get_svnprops($path); 1450 print "Properties on '$path':\n"; 1451 foreach (sort keys %{$props}) { 1452 print " $_\n"; 1453 } 1454} 1455 1456sub cmd_multi_init { 1457 my $url = shift; 1458 unless (defined $_trunk || @_branches || @_tags) { 1459 usage(1); 1460 } 1461 1462 $_prefix = 'origin/' unless defined $_prefix; 1463 if (defined $url) { 1464 $url = canonicalize_url($url); 1465 init_subdir(@_); 1466 } 1467 do_git_init_db(); 1468 if (defined $_trunk) { 1469 $_trunk =~ s#^/+##; 1470 my $trunk_ref = 'refs/remotes/' . $_prefix . 'trunk'; 1471 # try both old-style and new-style lookups: 1472 my $gs_trunk = eval { Git::SVN->new($trunk_ref) }; 1473 unless ($gs_trunk) { 1474 my ($trunk_url, $trunk_path) = 1475 complete_svn_url($url, $_trunk); 1476 $gs_trunk = Git::SVN->init($trunk_url, $trunk_path, 1477 undef, $trunk_ref); 1478 } 1479 } 1480 return unless @_branches || @_tags; 1481 my $ra = $url ? Git::SVN::Ra->new($url) : undef; 1482 foreach my $path (@_branches) { 1483 complete_url_ls_init($ra, $path, '--branches/-b', $_prefix); 1484 } 1485 foreach my $path (@_tags) { 1486 complete_url_ls_init($ra, $path, '--tags/-t', $_prefix.'tags/'); 1487 } 1488} 1489 1490sub cmd_multi_fetch { 1491 $Git::SVN::no_reuse_existing = undef; 1492 my $remotes = Git::SVN::read_all_remotes(); 1493 foreach my $repo_id (sort keys %$remotes) { 1494 if ($remotes->{$repo_id}->{url}) { 1495 Git::SVN::fetch_all($repo_id, $remotes); 1496 } 1497 } 1498} 1499 1500# this command is special because it requires no metadata 1501sub cmd_commit_diff { 1502 my ($ta, $tb, $url) = @_; 1503 my $usage = "usage: $0 commit-diff -r<revision> ". 1504 "<tree-ish> <tree-ish> [<URL>]"; 1505 fatal($usage) if (!defined $ta || !defined $tb); 1506 my $svn_path = ''; 1507 if (!defined $url) { 1508 my $gs = eval { Git::SVN->new }; 1509 if (!$gs) { 1510 fatal("Needed URL or usable git-svn --id in ", 1511 "the command-line\n", $usage); 1512 } 1513 $url = $gs->url; 1514 $svn_path = $gs->path; 1515 } 1516 unless (defined $_revision) { 1517 fatal("-r|--revision is a required argument\n", $usage); 1518 } 1519 if (defined $_message && defined $_file) { 1520 fatal("Both --message/-m and --file/-F specified ", 1521 "for the commit message.\n", 1522 "I have no idea what you mean"); 1523 } 1524 if (defined $_file) { 1525 $_message = file_to_s($_file); 1526 } else { 1527 $_message ||= get_commit_entry($tb)->{log}; 1528 } 1529 my $ra ||= Git::SVN::Ra->new($url); 1530 my $r = $_revision; 1531 if ($r eq 'HEAD') { 1532 $r = $ra->get_latest_revnum; 1533 } elsif ($r !~ /^\d+$/) { 1534 die "revision argument: $r not understood by git-svn\n"; 1535 } 1536 my %ed_opts = ( r => $r, 1537 log => $_message, 1538 ra => $ra, 1539 tree_a => $ta, 1540 tree_b => $tb, 1541 editor_cb => sub { print "Committed r$_[0]\n" }, 1542 svn_path => $svn_path ); 1543 if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) { 1544 print "No changes\n$ta == $tb\n"; 1545 } 1546} 1547 1548sub cmd_info { 1549 my $path_arg = defined($_[0]) ? $_[0] : '.'; 1550 my $path = $path_arg; 1551 if (File::Spec->file_name_is_absolute($path)) { 1552 $path = canonicalize_path($path); 1553 1554 my $toplevel = eval { 1555 my @cmd = qw/rev-parse --show-toplevel/; 1556 command_oneline(\@cmd, STDERR => 0); 1557 }; 1558 1559 # remove $toplevel from the absolute path: 1560 my ($vol, $dirs, $file) = File::Spec->splitpath($path); 1561 my (undef, $tdirs, $tfile) = File::Spec->splitpath($toplevel); 1562 my @dirs = File::Spec->splitdir($dirs); 1563 my @tdirs = File::Spec->splitdir($tdirs); 1564 pop @dirs if $dirs[-1] eq ''; 1565 pop @tdirs if $tdirs[-1] eq ''; 1566 push @dirs, $file; 1567 push @tdirs, $tfile; 1568 while (@tdirs && @dirs && $tdirs[0] eq $dirs[0]) { 1569 shift @dirs; 1570 shift @tdirs; 1571 } 1572 $dirs = File::Spec->catdir(@dirs); 1573 $path = File::Spec->catpath($vol, $dirs); 1574 1575 $path = canonicalize_path($path); 1576 } else { 1577 $path = canonicalize_path($cmd_dir_prefix . $path); 1578 } 1579 if (exists $_[1]) { 1580 die "Too many arguments specified\n"; 1581 } 1582 1583 my ($file_type, $diff_status) = find_file_type_and_diff_status($path); 1584 1585 if (!$file_type && !$diff_status) { 1586 print STDERR "svn: '$path' is not under version control\n"; 1587 exit 1; 1588 } 1589 1590 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); 1591 unless ($gs) { 1592 die "Unable to determine upstream SVN information from ", 1593 "working tree history\n"; 1594 } 1595 1596 # canonicalize_path() will return "" to make libsvn 1.5.x happy, 1597 $path = "." if $path eq ""; 1598 1599 my $full_url = canonicalize_url( add_path_to_url( $url, $path ) ); 1600 1601 if ($_url) { 1602 print "$full_url\n"; 1603 return; 1604 } 1605 1606 my $result = "Path: $path_arg\n"; 1607 $result .= "Name: " . basename($path) . "\n" if $file_type ne "dir"; 1608 $result .= "URL: $full_url\n"; 1609 1610 eval { 1611 my $repos_root = $gs->repos_root; 1612 Git::SVN::remove_username($repos_root); 1613 $result .= "Repository Root: " . canonicalize_url($repos_root) . "\n"; 1614 }; 1615 if ($@) { 1616 $result .= "Repository Root: (offline)\n"; 1617 } 1618 ::_req_svn(); 1619 $result .= "Repository UUID: $uuid\n" unless $diff_status eq "A" && 1620 (::compare_svn_version('1.5.4') <= 0 || $file_type ne "dir"); 1621 $result .= "Revision: " . ($diff_status eq "A" ? 0 : $rev) . "\n"; 1622 1623 $result .= "Node Kind: " . 1624 ($file_type eq "dir" ? "directory" : "file") . "\n"; 1625 1626 my $schedule = $diff_status eq "A" 1627 ? "add" 1628 : ($diff_status eq "D" ? "delete" : "normal"); 1629 $result .= "Schedule: $schedule\n"; 1630 1631 if ($diff_status eq "A") { 1632 print $result, "\n"; 1633 return; 1634 } 1635 1636 my ($lc_author, $lc_rev, $lc_date_utc); 1637 my @args = Git::SVN::Log::git_svn_log_cmd($rev, $rev, "--", $path); 1638 my $log = command_output_pipe(@args); 1639 my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/; 1640 while (<$log>) { 1641 if (/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) { 1642 $lc_author = $1; 1643 $lc_date_utc = Git::SVN::Log::parse_git_date($2, $3); 1644 } elsif (/^${esc_color} (git-svn-id:.+)$/o) { 1645 (undef, $lc_rev, undef) = ::extract_metadata($1); 1646 } 1647 } 1648 close $log; 1649 1650 Git::SVN::Log::set_local_timezone(); 1651 1652 $result .= "Last Changed Author: $lc_author\n"; 1653 $result .= "Last Changed Rev: $lc_rev\n"; 1654 $result .= "Last Changed Date: " . 1655 Git::SVN::Log::format_svn_date($lc_date_utc) . "\n"; 1656 1657 if ($file_type ne "dir") { 1658 my $text_last_updated_date = 1659 ($diff_status eq "D" ? $lc_date_utc : (stat $path)[9]); 1660 $result .= 1661 "Text Last Updated: " . 1662 Git::SVN::Log::format_svn_date($text_last_updated_date) . 1663 "\n"; 1664 my $checksum; 1665 if ($diff_status eq "D") { 1666 my ($fh, $ctx) = 1667 command_output_pipe(qw(cat-file blob), "HEAD:$path"); 1668 if ($file_type eq "link") { 1669 my $file_name = <$fh>; 1670 $checksum = md5sum("link $file_name"); 1671 } else { 1672 $checksum = md5sum($fh); 1673 } 1674 command_close_pipe($fh, $ctx); 1675 } elsif ($file_type eq "link") { 1676 my $file_name = 1677 command(qw(cat-file blob), "HEAD:$path"); 1678 $checksum = 1679 md5sum("link " . $file_name); 1680 } else { 1681 open FILE, "<", $path or die $!; 1682 $checksum = md5sum(\*FILE); 1683 close FILE or die $!; 1684 } 1685 $result .= "Checksum: " . $checksum . "\n"; 1686 } 1687 1688 print $result, "\n"; 1689} 1690 1691sub cmd_reset { 1692 my $target = shift || $_revision or die "SVN revision required\n"; 1693 $target = $1 if $target =~ /^r(\d+)$/; 1694 $target =~ /^\d+$/ or die "Numeric SVN revision expected\n"; 1695 my ($url, $rev, $uuid, $gs) = working_head_info('HEAD'); 1696 unless ($gs) { 1697 die "Unable to determine upstream SVN information from ". 1698 "history\n"; 1699 } 1700 my ($r, $c) = $gs->find_rev_before($target, not $_fetch_parent); 1701 die "Cannot find SVN revision $target\n" unless defined($c); 1702 $gs->rev_map_set($r, $c, 'reset', $uuid); 1703 print "r$r = $c ($gs->{ref_id})\n"; 1704} 1705 1706sub cmd_gc { 1707 require File::Find; 1708 if (!can_compress()) { 1709 warn "Compress::Zlib could not be found; unhandled.log " . 1710 "files will not be compressed.\n"; 1711 } 1712 File::Find::find({ wanted => \&gc_directory, no_chdir => 1}, 1713 Git::SVN::svn_dir()); 1714} 1715 1716########################### utility functions ######################### 1717 1718sub rebase_cmd { 1719 my @cmd = qw/rebase/; 1720 push @cmd, '-v' if $_verbose; 1721 push @cmd, qw/--merge/ if $_merge; 1722 push @cmd, "--strategy=$_strategy" if $_strategy; 1723 push @cmd, "--rebase-merges" if $_rebase_merges; 1724 @cmd; 1725} 1726 1727sub post_fetch_checkout { 1728 return if $_no_checkout; 1729 return if verify_ref('HEAD^0'); 1730 my $gs = $Git::SVN::_head or return; 1731 1732 # look for "trunk" ref if it exists 1733 my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}}; 1734 my $fetch = $remote->{fetch}; 1735 if ($fetch) { 1736 foreach my $p (keys %$fetch) { 1737 basename($fetch->{$p}) eq 'trunk' or next; 1738 $gs = Git::SVN->new($fetch->{$p}, $gs->{repo_id}, $p); 1739 last; 1740 } 1741 } 1742 1743 command_noisy(qw(update-ref HEAD), $gs->refname); 1744 return unless verify_ref('HEAD^0'); 1745 1746 return if $ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#; 1747 my $index = command_oneline(qw(rev-parse --git-path index)); 1748 return if -f $index; 1749 1750 return if command_oneline(qw/rev-parse --is-inside-work-tree/) eq 'false'; 1751 return if command_oneline(qw/rev-parse --is-inside-git-dir/) eq 'true'; 1752 command_noisy(qw/read-tree -m -u -v HEAD HEAD/); 1753 print STDERR "Checked out HEAD:\n ", 1754 $gs->full_url, " r", $gs->last_rev, "\n"; 1755 if (auto_create_empty_directories($gs)) { 1756 $gs->mkemptydirs($gs->last_rev); 1757 } 1758} 1759 1760sub complete_svn_url { 1761 my ($url, $path) = @_; 1762 1763 if ($path =~ m#^[a-z\+]+://#i) { # path is a URL 1764 $path = canonicalize_url($path); 1765 } else { 1766 $path = canonicalize_path($path); 1767 if (!defined $url || $url !~ m#^[a-z\+]+://#i) { 1768 fatal("E: '$path' is not a complete URL ", 1769 "and a separate URL is not specified"); 1770 } 1771 return ($url, $path); 1772 } 1773 return ($path, ''); 1774} 1775 1776sub complete_url_ls_init { 1777 my ($ra, $repo_path, $switch, $pfx) = @_; 1778 unless ($repo_path) { 1779 print STDERR "W: $switch not specified\n"; 1780 return; 1781 } 1782 if ($repo_path =~ m#^[a-z\+]+://#i) { 1783 $repo_path = canonicalize_url($repo_path); 1784 $ra = Git::SVN::Ra->new($repo_path); 1785 $repo_path = ''; 1786 } else { 1787 $repo_path = canonicalize_path($repo_path); 1788 $repo_path =~ s#^/+##; 1789 unless ($ra) { 1790 fatal("E: '$repo_path' is not a complete URL ", 1791 "and a separate URL is not specified"); 1792 } 1793 } 1794 my $url = $ra->url; 1795 my $gs = Git::SVN->init($url, undef, undef, undef, 1); 1796 my $k = "svn-remote.$gs->{repo_id}.url"; 1797 my $orig_url = eval { command_oneline(qw/config --get/, $k) }; 1798 if ($orig_url && ($orig_url ne $gs->url)) { 1799 die "$k already set: $orig_url\n", 1800 "wanted to set to: $gs->url\n"; 1801 } 1802 command_oneline('config', $k, $gs->url) unless $orig_url; 1803 1804 my $remote_path = join_paths( $gs->path, $repo_path ); 1805 $remote_path =~ s{%([0-9A-F]{2})}{chr hex($1)}ieg; 1806 $remote_path =~ s#^/##g; 1807 $remote_path .= "/*" if $remote_path !~ /\*/; 1808 my ($n) = ($switch =~ /^--(\w+)/); 1809 if (length $pfx && $pfx !~ m#/$#) { 1810 die "--prefix='$pfx' must have a trailing slash '/'\n"; 1811 } 1812 command_noisy('config', 1813 '--add', 1814 "svn-remote.$gs->{repo_id}.$n", 1815 "$remote_path:refs/remotes/$pfx*" . 1816 ('/*' x (($remote_path =~ tr/*/*/) - 1)) ); 1817} 1818 1819sub verify_ref { 1820 my ($ref) = @_; 1821 eval { command_oneline([ 'rev-parse', '--verify', $ref ], 1822 { STDERR => 0 }); }; 1823} 1824 1825sub get_tree_from_treeish { 1826 my ($treeish) = @_; 1827 # $treeish can be a symbolic ref, too: 1828 my $type = command_oneline(qw/cat-file -t/, $treeish); 1829 my $expected; 1830 while ($type eq 'tag') { 1831 ($treeish, $type) = command(qw/cat-file tag/, $treeish); 1832 } 1833 if ($type eq 'commit') { 1834 $expected = (grep /^tree /, command(qw/cat-file commit/, 1835 $treeish))[0]; 1836 ($expected) = ($expected =~ /^tree ($oid)$/o); 1837 die "Unable to get tree from $treeish\n" unless $expected; 1838 } elsif ($type eq 'tree') { 1839 $expected = $treeish; 1840 } else { 1841 die "$treeish is a $type, expected tree, tag or commit\n"; 1842 } 1843 return $expected; 1844} 1845 1846sub get_commit_entry { 1847 my ($treeish) = shift; 1848 my %log_entry = ( log => '', tree => get_tree_from_treeish($treeish) ); 1849 my @git_path = qw(rev-parse --git-path); 1850 my $commit_editmsg = command_oneline(@git_path, 'COMMIT_EDITMSG'); 1851 my $commit_msg = command_oneline(@git_path, 'COMMIT_MSG'); 1852 open my $log_fh, '>', $commit_editmsg or croak $!; 1853 1854 my $type = command_oneline(qw/cat-file -t/, $treeish); 1855 if ($type eq 'commit' || $type eq 'tag') { 1856 my ($msg_fh, $ctx) = command_output_pipe('cat-file', 1857 $type, $treeish); 1858 my $in_msg = 0; 1859 my $author; 1860 my $saw_from = 0; 1861 my $msgbuf = ""; 1862 while (<$msg_fh>) { 1863 if (!$in_msg) { 1864 $in_msg = 1 if (/^$/); 1865 $author = $1 if (/^author (.*>)/); 1866 } elsif (/^git-svn-id: /) { 1867 # skip this for now, we regenerate the 1868 # correct one on re-fetch anyways 1869 # TODO: set *:merge properties or like... 1870 } else { 1871 if (/^From:/ || /^Signed-off-by:/) { 1872 $saw_from = 1; 1873 } 1874 $msgbuf .= $_; 1875 } 1876 } 1877 $msgbuf =~ s/\s+$//s; 1878 $msgbuf =~ s/\r\n/\n/sg; # SVN 1.6+ disallows CRLF 1879 if ($Git::SVN::_add_author_from && defined($author) 1880 && !$saw_from) { 1881 $msgbuf .= "\n\nFrom: $author"; 1882 } 1883 print $log_fh $msgbuf or croak $!; 1884 command_close_pipe($msg_fh, $ctx); 1885 } 1886 close $log_fh or croak $!; 1887 1888 if ($_edit || ($type eq 'tree')) { 1889 chomp(my $editor = command_oneline(qw(var GIT_EDITOR))); 1890 system('sh', '-c', $editor.' "$@"', $editor, $commit_editmsg); 1891 } 1892 rename $commit_editmsg, $commit_msg or croak $!; 1893 { 1894 require Encode; 1895 # SVN requires messages to be UTF-8 when entering the repo 1896 open $log_fh, '<', $commit_msg or croak $!; 1897 binmode $log_fh; 1898 chomp($log_entry{log} = get_record($log_fh, undef)); 1899 1900 my $enc = Git::config('i18n.commitencoding') || 'UTF-8'; 1901 my $msg = $log_entry{log}; 1902 1903 eval { $msg = Encode::decode($enc, $msg, 1) }; 1904 if ($@) { 1905 die "Could not decode as $enc:\n", $msg, 1906 "\nPerhaps you need to set i18n.commitencoding\n"; 1907 } 1908 1909 eval { $msg = Encode::encode('UTF-8', $msg, 1) }; 1910 die "Could not encode as UTF-8:\n$msg\n" if $@; 1911 1912 $log_entry{log} = $msg; 1913 1914 close $log_fh or croak $!; 1915 } 1916 unlink $commit_msg; 1917 \%log_entry; 1918} 1919 1920sub s_to_file { 1921 my ($str, $file, $mode) = @_; 1922 open my $fd,'>',$file or croak $!; 1923 print $fd $str,"\n" or croak $!; 1924 close $fd or croak $!; 1925 chmod ($mode &~ umask, $file) if (defined $mode); 1926} 1927 1928sub file_to_s { 1929 my $file = shift; 1930 open my $fd,'<',$file or croak "$!: file: $file\n"; 1931 local $/; 1932 my $ret = <$fd>; 1933 close $fd or croak $!; 1934 $ret =~ s/\s*$//s; 1935 return $ret; 1936} 1937 1938# '<svn username> = real-name <email address>' mapping based on git-svnimport: 1939sub load_authors { 1940 open my $authors, '<', $_authors or die "Can't open $_authors $!\n"; 1941 my $log = $cmd eq 'log'; 1942 while (<$authors>) { 1943 chomp; 1944 next unless /^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.*)>\s*$/; 1945 my ($user, $name, $email) = ($1, $2, $3); 1946 if ($log) { 1947 $Git::SVN::Log::rusers{"$name <$email>"} = $user; 1948 } else { 1949 $users{$user} = [$name, $email]; 1950 } 1951 } 1952 close $authors or croak $!; 1953} 1954 1955# convert GetOpt::Long specs for use by git-config 1956sub read_git_config { 1957 my $opts = shift; 1958 my @config_only; 1959 foreach my $o (keys %$opts) { 1960 # if we have mixedCase and a long option-only, then 1961 # it's a config-only variable that we don't need for 1962 # the command-line. 1963 push @config_only, $o if ($o =~ /[A-Z]/ && $o =~ /^[a-z]+$/i); 1964 my $v = $opts->{$o}; 1965 my ($key) = ($o =~ /^([a-zA-Z\-]+)/); 1966 $key =~ s/-//g; 1967 my $arg = 'git config'; 1968 $arg .= ' --int' if ($o =~ /[:=]i$/); 1969 $arg .= ' --bool' if ($o !~ /[:=][sfi]$/); 1970 if (ref $v eq 'ARRAY') { 1971 chomp(my @tmp = `$arg --get-all svn.$key`); 1972 @$v = @tmp if @tmp; 1973 } else { 1974 chomp(my $tmp = `$arg --get svn.$key`); 1975 if ($tmp && !($arg =~ / --bool/ && $tmp eq 'false')) { 1976 $$v = $tmp; 1977 } 1978 } 1979 } 1980 load_object_format(); 1981 delete @$opts{@config_only} if @config_only; 1982} 1983 1984sub load_object_format { 1985 chomp(my $hash = `git config --get extensions.objectformat`); 1986 $::oid_length = 64 if $hash eq 'sha256'; 1987} 1988 1989sub extract_metadata { 1990 my $id = shift or return (undef, undef, undef); 1991 my ($url, $rev, $uuid) = ($id =~ /^\s*git-svn-id:\s+(.*)\@(\d+) 1992 \s([a-f\d\-]+)$/ix); 1993 if (!defined $rev || !$uuid || !$url) { 1994 # some of the original repositories I made had 1995 # identifiers like this: 1996 ($rev, $uuid) = ($id =~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i); 1997 } 1998 return ($url, $rev, $uuid); 1999} 2000 2001sub cmt_metadata { 2002 return extract_metadata((grep(/^git-svn-id: /, 2003 command(qw/cat-file commit/, shift)))[-1]); 2004} 2005 2006sub cmt_sha2rev_batch { 2007 my %s2r; 2008 my ($pid, $in, $out, $ctx) = command_bidi_pipe(qw/cat-file --batch/); 2009 my $list = shift; 2010 2011 foreach my $sha (@{$list}) { 2012 my $first = 1; 2013 my $size = 0; 2014 print $out $sha, "\n"; 2015 2016 while (my $line = <$in>) { 2017 if ($first && $line =~ /^$::oid\smissing$/) { 2018 last; 2019 } elsif ($first && 2020 $line =~ /^$::oid\scommit\s(\d+)$/) { 2021 $first = 0; 2022 $size = $1; 2023 next; 2024 } elsif ($line =~ /^(git-svn-id: )/) { 2025 my (undef, $rev, undef) = 2026 extract_metadata($line); 2027 $s2r{$sha} = $rev; 2028 } 2029 2030 $size -= length($line); 2031 last if ($size == 0); 2032 } 2033 } 2034 2035 command_close_bidi_pipe($pid, $in, $out, $ctx); 2036 2037 return \%s2r; 2038} 2039 2040sub working_head_info { 2041 my ($head, $refs) = @_; 2042 my @args = qw/rev-list --first-parent --pretty=medium/; 2043 my ($fh, $ctx) = command_output_pipe(@args, $head, "--"); 2044 my $hash; 2045 my %max; 2046 while (<$fh>) { 2047 if ( m{^commit ($::oid)$} ) { 2048 unshift @$refs, $hash if $hash and $refs; 2049 $hash = $1; 2050 next; 2051 } 2052 next unless s{^\s*(git-svn-id:)}{$1}; 2053 my ($url, $rev, $uuid) = extract_metadata($_); 2054 if (defined $url && defined $rev) { 2055 next if $max{$url} and $max{$url} < $rev; 2056 if (my $gs = Git::SVN->find_by_url($url)) { 2057 my $c = $gs->rev_map_get($rev, $uuid); 2058 if ($c && $c eq $hash) { 2059 close $fh; # break the pipe 2060 return ($url, $rev, $uuid, $gs); 2061 } else { 2062 $max{$url} ||= $gs->rev_map_max; 2063 } 2064 } 2065 } 2066 } 2067 command_close_pipe($fh, $ctx); 2068 (undef, undef, undef, undef); 2069} 2070 2071sub read_commit_parents { 2072 my ($parents, $c) = @_; 2073 chomp(my $p = command_oneline(qw/rev-list --parents -1/, $c)); 2074 $p =~ s/^($c)\s*// or die "rev-list --parents -1 $c failed!\n"; 2075 @{$parents->{$c}} = split(/ /, $p); 2076} 2077 2078sub linearize_history { 2079 my ($gs, $refs) = @_; 2080 my %parents; 2081 foreach my $c (@$refs) { 2082 read_commit_parents(\%parents, $c); 2083 } 2084 2085 my @linear_refs; 2086 my %skip = (); 2087 my $last_svn_commit = $gs->last_commit; 2088 foreach my $c (reverse @$refs) { 2089 next if $c eq $last_svn_commit; 2090 last if $skip{$c}; 2091 2092 unshift @linear_refs, $c; 2093 $skip{$c} = 1; 2094 2095 # we only want the first parent to diff against for linear 2096 # history, we save the rest to inject when we finalize the 2097 # svn commit 2098 my $fp_a = verify_ref("$c~1"); 2099 my $fp_b = shift @{$parents{$c}} if $parents{$c}; 2100 if (!$fp_a || !$fp_b) { 2101 die "Commit $c\n", 2102 "has no parent commit, and therefore ", 2103 "nothing to diff against.\n", 2104 "You should be working from a repository ", 2105 "originally created by git-svn\n"; 2106 } 2107 if ($fp_a ne $fp_b) { 2108 die "$c~1 = $fp_a, however parsing commit $c ", 2109 "revealed that:\n$c~1 = $fp_b\nBUG!\n"; 2110 } 2111 2112 foreach my $p (@{$parents{$c}}) { 2113 $skip{$p} = 1; 2114 } 2115 } 2116 (\@linear_refs, \%parents); 2117} 2118 2119sub find_file_type_and_diff_status { 2120 my ($path) = @_; 2121 return ('dir', '') if $path eq ''; 2122 2123 my $diff_output = 2124 command_oneline(qw(diff --cached --name-status --), $path) || ""; 2125 my $diff_status = (split(' ', $diff_output))[0] || ""; 2126 2127 my $ls_tree = command_oneline(qw(ls-tree HEAD), $path) || ""; 2128 2129 return (undef, undef) if !$diff_status && !$ls_tree; 2130 2131 if ($diff_status eq "A") { 2132 return ("link", $diff_status) if -l $path; 2133 return ("dir", $diff_status) if -d $path; 2134 return ("file", $diff_status); 2135 } 2136 2137 my $mode = (split(' ', $ls_tree))[0] || ""; 2138 2139 return ("link", $diff_status) if $mode eq "120000"; 2140 return ("dir", $diff_status) if $mode eq "040000"; 2141 return ("file", $diff_status); 2142} 2143 2144sub md5sum { 2145 my $arg = shift; 2146 my $ref = ref $arg; 2147 require Digest::MD5; 2148 my $md5 = Digest::MD5->new(); 2149 if ($ref eq 'GLOB' || $ref eq 'IO::File' || $ref eq 'File::Temp') { 2150 $md5->addfile($arg) or croak $!; 2151 } elsif ($ref eq 'SCALAR') { 2152 $md5->add($$arg) or croak $!; 2153 } elsif (!$ref) { 2154 $md5->add($arg) or croak $!; 2155 } else { 2156 fatal "Can't provide MD5 hash for unknown ref type: '", $ref, "'"; 2157 } 2158 return $md5->hexdigest(); 2159} 2160 2161sub gc_directory { 2162 if (can_compress() && -f $_ && basename($_) eq "unhandled.log") { 2163 my $out_filename = $_ . ".gz"; 2164 open my $in_fh, "<", $_ or die "Unable to open $_: $!\n"; 2165 binmode $in_fh; 2166 my $gz = Compress::Zlib::gzopen($out_filename, "ab") or 2167 die "Unable to open $out_filename: $!\n"; 2168 2169 my $res; 2170 while ($res = sysread($in_fh, my $str, 1024)) { 2171 $gz->gzwrite($str) or 2172 die "Unable to write: ".$gz->gzerror()."!\n"; 2173 } 2174 no warnings 'once'; # $File::Find::name would warn 2175 unlink $_ or die "unlink $File::Find::name: $!\n"; 2176 } elsif (-f $_ && basename($_) eq "index") { 2177 unlink $_ or die "unlink $_: $!\n"; 2178 } 2179} 2180 2181__END__ 2182 2183Data structures: 2184 2185 2186$remotes = { # returned by read_all_remotes() 2187 'svn' => { 2188 # svn-remote.svn.url=https://svn.musicpd.org 2189 url => 'https://svn.musicpd.org', 2190 # svn-remote.svn.fetch=mpd/trunk:trunk 2191 fetch => { 2192 'mpd/trunk' => 'trunk', 2193 }, 2194 # svn-remote.svn.tags=mpd/tags/*:tags/* 2195 tags => { 2196 path => { 2197 left => 'mpd/tags', 2198 right => '', 2199 regex => qr!mpd/tags/([^/]+)$!, 2200 glob => 'tags/*', 2201 }, 2202 ref => { 2203 left => 'tags', 2204 right => '', 2205 regex => qr!tags/([^/]+)$!, 2206 glob => 'tags/*', 2207 }, 2208 } 2209 } 2210}; 2211 2212$log_entry hashref as returned by libsvn_log_entry() 2213{ 2214 log => 'whitespace-formatted log entry 2215', # trailing newline is preserved 2216 revision => '8', # integer 2217 date => '2004-02-24T17:01:44.108345Z', # commit date 2218 author => 'committer name' 2219}; 2220 2221 2222# this is generated by generate_diff(); 2223@mods = array of diff-index line hashes, each element represents one line 2224 of diff-index output 2225 2226diff-index line ($m hash) 2227{ 2228 mode_a => first column of diff-index output, no leading ':', 2229 mode_b => second column of diff-index output, 2230 sha1_b => sha1sum of the final blob, 2231 chg => change type [MCRADT], 2232 file_a => original file name of a file (iff chg is 'C' or 'R') 2233 file_b => new/current file name of a file (any chg) 2234} 2235; 2236 2237# retval of read_url_paths{,_all}(); 2238$l_map = { 2239 # repository root url 2240 'https://svn.musicpd.org' => { 2241 # repository path # GIT_SVN_ID 2242 'mpd/trunk' => 'trunk', 2243 'mpd/tags/0.11.5' => 'tags/0.11.5', 2244 }, 2245} 2246 2247Notes: 2248 I don't trust the each() function on unless I created %hash myself 2249 because the internal iterator may not have started at base. 2250