1#!/usr/bin/perl 2# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net> 3# License: GPL v2 or later 4use5.008; 5use warnings; 6use strict; 7use vars qw/$AUTHOR $VERSION 8$sha1 $sha1_short $_revision $_repository 9$_q $_authors $_authors_prog %users/; 10$AUTHOR='Eric Wong <normalperson@yhbt.net>'; 11$VERSION='@@GIT_VERSION@@'; 12 13# From which subdir have we been invoked? 14my$cmd_dir_prefix=eval{ 15 command_oneline([qw/rev-parse --show-prefix/], STDERR =>0) 16} ||''; 17 18my$git_dir_user_set=1ifdefined$ENV{GIT_DIR}; 19$ENV{GIT_DIR} ||='.git'; 20$Git::SVN::default_repo_id ='svn'; 21$Git::SVN::default_ref_id =$ENV{GIT_SVN_ID} ||'git-svn'; 22$Git::SVN::Ra::_log_window_size =100; 23$Git::SVN::_minimize_url ='unset'; 24 25if(!exists$ENV{SVN_SSH} &&exists$ENV{GIT_SSH}) { 26$ENV{SVN_SSH} =$ENV{GIT_SSH}; 27} 28 29if(exists$ENV{SVN_SSH} &&$^Oeq'msys') { 30$ENV{SVN_SSH} =~s/\\/\\\\/g; 31$ENV{SVN_SSH} =~s/(.*)/"$1"/; 32} 33 34$Git::SVN::Log::TZ =$ENV{TZ}; 35$ENV{TZ} ='UTC'; 36$| =1;# unbuffer STDOUT 37 38sub fatal (@) {print STDERR "@_\n";exit1} 39 40# All SVN commands do it. Otherwise we may die on SIGPIPE when the remote 41# repository decides to close the connection which we expect to be kept alive. 42$SIG{PIPE} ='IGNORE'; 43 44# Given a dot separated version number, "subtract" it from 45# the SVN::Core::VERSION; non-negaitive return means the SVN::Core 46# is at least at the version the caller asked for. 47sub compare_svn_version { 48my(@ours) =split(/\./,$SVN::Core::VERSION); 49my(@theirs) =split(/\./,$_[0]); 50my($i,$diff); 51 52for($i=0;$i<@ours&&$i<@theirs;$i++) { 53$diff=$ours[$i] -$theirs[$i]; 54return$diffif($diff); 55} 56return1if($i<@ours); 57return-1if($i<@theirs); 58return0; 59} 60 61sub _req_svn { 62require SVN::Core;# use()-ing this causes segfaults for me... *shrug* 63require SVN::Ra; 64require SVN::Delta; 65if(::compare_svn_version('1.1.0') <0) { 66 fatal "Need SVN::Core 1.1.0 or better (got$SVN::Core::VERSION)"; 67} 68} 69my$can_compress=eval{require Compress::Zlib;1}; 70push@Git::SVN::Ra::ISA,'SVN::Ra'; 71push@Git::SVN::Editor::ISA,'SVN::Delta::Editor'; 72use Carp qw/croak/; 73use Digest::MD5; 74use IO::File qw//; 75use File::Basename qw/dirname basename/; 76use File::Path qw/mkpath/; 77use File::Spec; 78use File::Find; 79use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; 80use IPC::Open3; 81use Git; 82use Git::SVN::Fetcher qw//; 83use Git::SVN::Prompt qw//; 84use Memoize;# core since 5.8.0, Jul 2002 85 86BEGIN{ 87# import functions from Git into our packages, en masse 88no strict 'refs'; 89foreach(qw/command command_oneline command_noisy command_output_pipe 90 command_input_pipe command_close_pipe 91 command_bidi_pipe command_close_bidi_pipe/) { 92formy$package(qw(Git::SVN::Editor 93 Git::SVN::Migration Git::SVN::Log Git::SVN), 94 __PACKAGE__) { 95*{"${package}::$_"} = \&{"Git::$_"}; 96} 97} 98 Memoize::memoize 'Git::config'; 99 Memoize::memoize 'Git::config_bool'; 100} 101 102my($SVN); 103 104$sha1=qr/[a-f\d]{40}/; 105$sha1_short=qr/[a-f\d]{4,40}/; 106my($_stdin,$_help,$_edit, 107$_message,$_file,$_branch_dest, 108$_template,$_shared, 109$_version,$_fetch_all,$_no_rebase,$_fetch_parent, 110$_merge,$_strategy,$_preserve_merges,$_dry_run,$_local, 111$_prefix,$_no_checkout,$_url,$_verbose, 112$_git_format,$_commit_url,$_tag,$_merge_info,$_interactive); 113$Git::SVN::_follow_parent =1; 114$Git::SVN::Fetcher::_placeholder_filename =".gitignore"; 115$_q||=0; 116my%remote_opts= ('username=s'=> \$Git::SVN::Prompt::_username, 117'config-dir=s'=> \$Git::SVN::Ra::config_dir, 118'no-auth-cache'=> \$Git::SVN::Prompt::_no_auth_cache, 119'ignore-paths=s'=> \$Git::SVN::Fetcher::_ignore_regex, 120'ignore-refs=s'=> \$Git::SVN::Ra::_ignore_refs_regex ); 121my%fc_opts= ('follow-parent|follow!'=> \$Git::SVN::_follow_parent, 122'authors-file|A=s'=> \$_authors, 123'authors-prog=s'=> \$_authors_prog, 124'repack:i'=> \$Git::SVN::_repack, 125'noMetadata'=> \$Git::SVN::_no_metadata, 126'useSvmProps'=> \$Git::SVN::_use_svm_props, 127'useSvnsyncProps'=> \$Git::SVN::_use_svnsync_props, 128'log-window-size=i'=> \$Git::SVN::Ra::_log_window_size, 129'no-checkout'=> \$_no_checkout, 130'quiet|q+'=> \$_q, 131'repack-flags|repack-args|repack-opts=s'=> 132 \$Git::SVN::_repack_flags, 133'use-log-author'=> \$Git::SVN::_use_log_author, 134'add-author-from'=> \$Git::SVN::_add_author_from, 135'localtime'=> \$Git::SVN::_localtime, 136%remote_opts); 137 138my($_trunk,@_tags,@_branches,$_stdlayout); 139my%icv; 140my%init_opts= ('template=s'=> \$_template,'shared:s'=> \$_shared, 141'trunk|T=s'=> \$_trunk,'tags|t=s@'=> \@_tags, 142'branches|b=s@'=> \@_branches,'prefix=s'=> \$_prefix, 143'stdlayout|s'=> \$_stdlayout, 144'minimize-url|m!'=> \$Git::SVN::_minimize_url, 145'no-metadata'=>sub{$icv{noMetadata} =1}, 146'use-svm-props'=>sub{$icv{useSvmProps} =1}, 147'use-svnsync-props'=>sub{$icv{useSvnsyncProps} =1}, 148'rewrite-root=s'=>sub{$icv{rewriteRoot} =$_[1] }, 149'rewrite-uuid=s'=>sub{$icv{rewriteUUID} =$_[1] }, 150%remote_opts); 151my%cmt_opts= ('edit|e'=> \$_edit, 152'rmdir'=> \$Git::SVN::Editor::_rmdir, 153'find-copies-harder'=> \$Git::SVN::Editor::_find_copies_harder, 154'l=i'=> \$Git::SVN::Editor::_rename_limit, 155'copy-similarity|C=i'=> \$Git::SVN::Editor::_cp_similarity 156); 157 158my%cmd= ( 159 fetch => [ \&cmd_fetch,"Download new revisions from SVN", 160{'revision|r=s'=> \$_revision, 161'fetch-all|all'=> \$_fetch_all, 162'parent|p'=> \$_fetch_parent, 163%fc_opts} ], 164 clone => [ \&cmd_clone,"Initialize and fetch revisions", 165{'revision|r=s'=> \$_revision, 166'preserve-empty-dirs'=> 167 \$Git::SVN::Fetcher::_preserve_empty_dirs, 168'placeholder-filename=s'=> 169 \$Git::SVN::Fetcher::_placeholder_filename, 170%fc_opts,%init_opts} ], 171 init => [ \&cmd_init,"Initialize a repo for tracking". 172" (requires URL argument)", 173 \%init_opts], 174'multi-init'=> [ \&cmd_multi_init, 175"Deprecated alias for ". 176"'$0init -T<trunk> -b<branches> -t<tags>'", 177 \%init_opts], 178 dcommit => [ \&cmd_dcommit, 179'Commit several diffs to merge with upstream', 180{'merge|m|M'=> \$_merge, 181'strategy|s=s'=> \$_strategy, 182'verbose|v'=> \$_verbose, 183'dry-run|n'=> \$_dry_run, 184'fetch-all|all'=> \$_fetch_all, 185'commit-url=s'=> \$_commit_url, 186'revision|r=i'=> \$_revision, 187'no-rebase'=> \$_no_rebase, 188'mergeinfo=s'=> \$_merge_info, 189'interactive|i'=> \$_interactive, 190%cmt_opts,%fc_opts} ], 191 branch => [ \&cmd_branch, 192'Create a branch in the SVN repository', 193{'message|m=s'=> \$_message, 194'destination|d=s'=> \$_branch_dest, 195'dry-run|n'=> \$_dry_run, 196'tag|t'=> \$_tag, 197'username=s'=> \$Git::SVN::Prompt::_username, 198'commit-url=s'=> \$_commit_url} ], 199 tag => [sub{$_tag=1; cmd_branch(@_) }, 200'Create a tag in the SVN repository', 201{'message|m=s'=> \$_message, 202'destination|d=s'=> \$_branch_dest, 203'dry-run|n'=> \$_dry_run, 204'username=s'=> \$Git::SVN::Prompt::_username, 205'commit-url=s'=> \$_commit_url} ], 206'set-tree'=> [ \&cmd_set_tree, 207"Set an SVN repository to a git tree-ish", 208{'stdin'=> \$_stdin,%cmt_opts,%fc_opts, } ], 209'create-ignore'=> [ \&cmd_create_ignore, 210'Create a .gitignore per svn:ignore', 211{'revision|r=i'=> \$_revision 212} ], 213'mkdirs'=> [ \&cmd_mkdirs , 214"recreate empty directories after a checkout", 215{'revision|r=i'=> \$_revision} ], 216'propget'=> [ \&cmd_propget, 217'Print the value of a property on a file or directory', 218{'revision|r=i'=> \$_revision} ], 219'proplist'=> [ \&cmd_proplist, 220'List all properties of a file or directory', 221{'revision|r=i'=> \$_revision} ], 222'show-ignore'=> [ \&cmd_show_ignore,"Show svn:ignore listings", 223{'revision|r=i'=> \$_revision 224} ], 225'show-externals'=> [ \&cmd_show_externals,"Show svn:externals listings", 226{'revision|r=i'=> \$_revision 227} ], 228'multi-fetch'=> [ \&cmd_multi_fetch, 229"Deprecated alias for$0fetch --all", 230{'revision|r=s'=> \$_revision,%fc_opts} ], 231'migrate'=> [sub{ }, 232# no-op, we automatically run this anyways, 233'Migrate configuration/metadata/layout from 234 previous versions of git-svn', 235{'minimize'=> \$Git::SVN::Migration::_minimize, 236%remote_opts} ], 237'log'=> [ \&Git::SVN::Log::cmd_show_log,'Show commit logs', 238{'limit=i'=> \$Git::SVN::Log::limit, 239'revision|r=s'=> \$_revision, 240'verbose|v'=> \$Git::SVN::Log::verbose, 241'incremental'=> \$Git::SVN::Log::incremental, 242'oneline'=> \$Git::SVN::Log::oneline, 243'show-commit'=> \$Git::SVN::Log::show_commit, 244'non-recursive'=> \$Git::SVN::Log::non_recursive, 245'authors-file|A=s'=> \$_authors, 246'color'=> \$Git::SVN::Log::color, 247'pager=s'=> \$Git::SVN::Log::pager 248} ], 249'find-rev'=> [ \&cmd_find_rev, 250"Translate between SVN revision numbers and tree-ish", 251{} ], 252'rebase'=> [ \&cmd_rebase,"Fetch and rebase your working directory", 253{'merge|m|M'=> \$_merge, 254'verbose|v'=> \$_verbose, 255'strategy|s=s'=> \$_strategy, 256'local|l'=> \$_local, 257'fetch-all|all'=> \$_fetch_all, 258'dry-run|n'=> \$_dry_run, 259'preserve-merges|p'=> \$_preserve_merges, 260%fc_opts} ], 261'commit-diff'=> [ \&cmd_commit_diff, 262'Commit a diff between two trees', 263{'message|m=s'=> \$_message, 264'file|F=s'=> \$_file, 265'revision|r=s'=> \$_revision, 266%cmt_opts} ], 267'info'=> [ \&cmd_info, 268"Show info about the latest SVN revision 269 on the current branch", 270{'url'=> \$_url, } ], 271'blame'=> [ \&Git::SVN::Log::cmd_blame, 272"Show what revision and author last modified each line of a file", 273{'git-format'=> \$_git_format} ], 274'reset'=> [ \&cmd_reset, 275"Undo fetches back to the specified SVN revision", 276{'revision|r=s'=> \$_revision, 277'parent|p'=> \$_fetch_parent} ], 278'gc'=> [ \&cmd_gc, 279"Compress unhandled.log files in .git/svn and remove ". 280"index files in .git/svn", 281{} ], 282); 283 284use Term::ReadLine; 285package FakeTerm; 286sub new { 287my($class,$reason) =@_; 288returnbless \$reason,shift; 289} 290subreadline{ 291my$self=shift; 292die"Cannot use readline on FakeTerm:$$self"; 293} 294package main; 295 296my$term=eval{ 297$ENV{"GIT_SVN_NOTTY"} 298? new Term::ReadLine 'git-svn', \*STDIN, \*STDOUT 299: new Term::ReadLine 'git-svn'; 300}; 301if($@) { 302$term= new FakeTerm "$@: going non-interactive"; 303} 304 305my$cmd; 306for(my$i=0;$i<@ARGV;$i++) { 307if(defined$cmd{$ARGV[$i]}) { 308$cmd=$ARGV[$i]; 309splice@ARGV,$i,1; 310last; 311}elsif($ARGV[$i]eq'help') { 312$cmd=$ARGV[$i+1]; 313 usage(0); 314} 315}; 316 317# make sure we're always running at the top-level working directory 318unless($cmd&&$cmd=~/(?:clone|init|multi-init)$/) { 319unless(-d $ENV{GIT_DIR}) { 320if($git_dir_user_set) { 321die"GIT_DIR=$ENV{GIT_DIR} explicitly set, ", 322"but it is not a directory\n"; 323} 324my$git_dir=delete$ENV{GIT_DIR}; 325my$cdup=undef; 326 git_cmd_try { 327$cdup= command_oneline(qw/rev-parse --show-cdup/); 328$git_dir='.'unless($cdup); 329chomp$cdupif($cdup); 330$cdup="."unless($cdup&&length$cdup); 331}"Already at toplevel, but$git_dirnot found\n"; 332chdir$cdupor die"Unable to chdir up to '$cdup'\n"; 333unless(-d $git_dir) { 334die"$git_dirstill not found after going to ", 335"'$cdup'\n"; 336} 337$ENV{GIT_DIR} =$git_dir; 338} 339$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 340} 341 342my%opts= %{$cmd{$cmd}->[2]}if(defined$cmd); 343 344read_git_config(\%opts); 345if($cmd&& ($cmdeq'log'||$cmdeq'blame')) { 346 Getopt::Long::Configure('pass_through'); 347} 348my$rv= GetOptions(%opts,'h|H'=> \$_help,'version|V'=> \$_version, 349'minimize-connections'=> \$Git::SVN::Migration::_minimize, 350'id|i=s'=> \$Git::SVN::default_ref_id, 351'svn-remote|remote|R=s'=>sub{ 352$Git::SVN::no_reuse_existing =1; 353$Git::SVN::default_repo_id =$_[1] }); 354exit1if(!$rv&&$cmd&&$cmdne'log'); 355 356usage(0)if$_help; 357version()if$_version; 358usage(1)unlessdefined$cmd; 359load_authors()if$_authors; 360if(defined$_authors_prog) { 361$_authors_prog="'". File::Spec->rel2abs($_authors_prog) ."'"; 362} 363 364unless($cmd=~/^(?:clone|init|multi-init|commit-diff)$/) { 365 Git::SVN::Migration::migration_check(); 366} 367Git::SVN::init_vars(); 368eval{ 369 Git::SVN::verify_remotes_sanity(); 370$cmd{$cmd}->[0]->(@ARGV); 371}; 372fatal $@if$@; 373post_fetch_checkout(); 374exit0; 375 376####################### primary functions ###################### 377sub usage { 378my$exit=shift||0; 379my$fd=$exit? \*STDERR : \*STDOUT; 380print$fd<<""; 381git-svn - bidirectional operations between a single Subversion tree and git 382Usage: git svn <command> [options] [arguments]\n 383 384print$fd"Available commands:\n"unless$cmd; 385 386foreach(sort keys%cmd) { 387next if$cmd&&$cmdne$_; 388next if/^multi-/;# don't show deprecated commands 389print$fd' ',pack('A17',$_),$cmd{$_}->[1],"\n"; 390foreach(sort keys%{$cmd{$_}->[2]}) { 391# mixed-case options are for .git/config only 392next if/[A-Z]/&&/^[a-z]+$/i; 393# prints out arguments as they should be passed: 394my$x= s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : ''; 395print$fd' ' x 21,join(', ',map{length$_>1? 396"--$_":"-$_"} 397split/\|/,$_),"$x\n"; 398} 399} 400print$fd<<""; 401\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an 402arbitrary identifier if you're tracking multiple SVN branches/repositories in 403one git repository and want to keep them separate. See git-svn(1) for more 404information. 405 406 exit$exit; 407} 408 409sub version { 410 ::_req_svn(); 411 print "git-svn version$VERSION(svn$SVN::Core::VERSION)\n"; 412 exit 0; 413} 414 415sub ask { 416 my ($prompt,%arg) =@_; 417 my$valid_re=$arg{valid_re}; 418 my$default=$arg{default}; 419 my$resp; 420 my$i= 0; 421 422 if ( !( defined($term->IN) 423 && defined( fileno($term->IN) ) 424 && defined($term->OUT ) 425 && defined( fileno($term->OUT) ) ) ){ 426 return defined($default) ?$default: undef; 427 } 428 429 while ($i++< 10) { 430$resp=$term->readline($prompt); 431 if (!defined$resp) { # EOF 432 print "\n"; 433 return defined$default?$default: undef; 434 } 435 if ($respeq '' and defined$default) { 436 return$default; 437 } 438 if (!defined$valid_reor$resp=~ /$valid_re/) { 439 return$resp; 440 } 441 } 442 return undef; 443} 444 445sub do_git_init_db { 446 unless (-d$ENV{GIT_DIR}) { 447 my@init_db= ('init'); 448 push@init_db, "--template=$_template" if defined$_template; 449 if (defined$_shared) { 450 if ($_shared=~ /[a-z]/) { 451 push@init_db, "--shared=$_shared"; 452 } else { 453 push@init_db, "--shared"; 454 } 455 } 456 command_noisy(@init_db); 457$_repository= Git->repository(Repository => ".git"); 458 } 459 my$set; 460 my$pfx= "svn-remote.$Git::SVN::default_repo_id"; 461 foreach my$i(keys%icv) { 462 die "'$set' and '$i' cannot both be set\n" if$set; 463 next unless defined$icv{$i}; 464 command_noisy('config', "$pfx.$i",$icv{$i}); 465$set=$i; 466 } 467 my$ignore_paths_regex= \$Git::SVN::Fetcher::_ignore_regex; 468 command_noisy('config', "$pfx.ignore-paths",$$ignore_paths_regex) 469 if defined$$ignore_paths_regex; 470 my$ignore_refs_regex= \$Git::SVN::Ra::_ignore_refs_regex; 471 command_noisy('config', "$pfx.ignore-refs",$$ignore_refs_regex) 472 if defined$$ignore_refs_regex; 473 474 if (defined$Git::SVN::Fetcher::_preserve_empty_dirs) { 475 my$fname= \$Git::SVN::Fetcher::_placeholder_filename; 476 command_noisy('config', "$pfx.preserve-empty-dirs", 'true'); 477 command_noisy('config', "$pfx.placeholder-filename",$$fname); 478 } 479} 480 481sub init_subdir { 482 my$repo_path= shift or return; 483 mkpath([$repo_path]) unless -d$repo_path; 484 chdir$repo_pathor die "Couldn't chdir to $repo_path:$!\n"; 485$ENV{GIT_DIR} = '.git'; 486$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 487} 488 489sub cmd_clone { 490 my ($url,$path) =@_; 491 if (!defined$path&& 492 (defined$_trunk||@_branches||@_tags|| 493 defined$_stdlayout) && 494$url!~ m#^[a-z\+]+://#) { 495$path=$url; 496 } 497$path= basename($url) if !defined$path|| !length$path; 498 my$authors_absolute=$_authors? File::Spec->rel2abs($_authors) : ""; 499 cmd_init($url,$path); 500 command_oneline('config', 'svn.authorsfile',$authors_absolute) 501 if$_authors; 502 Git::SVN::fetch_all($Git::SVN::default_repo_id); 503} 504 505sub cmd_init { 506 if (defined$_stdlayout) { 507$_trunk= 'trunk' if (!defined$_trunk); 508@_tags= 'tags' if (!@_tags); 509@_branches= 'branches' if (!@_branches); 510 } 511 if (defined$_trunk||@_branches||@_tags) { 512 return cmd_multi_init(@_); 513 } 514 my$url= shift or die "SVN repository location required ", 515 "as a command-line argument\n"; 516$url= canonicalize_url($url); 517 init_subdir(@_); 518 do_git_init_db(); 519 520 if ($Git::SVN::_minimize_url eq 'unset') { 521$Git::SVN::_minimize_url = 0; 522 } 523 524 Git::SVN->init($url); 525} 526 527sub cmd_fetch { 528 if (grep /^\d+=./,@_) { 529 die "'<rev>=<commit>' fetch arguments are ", 530 "no longer supported.\n"; 531 } 532 my ($remote) =@_; 533 if (@_> 1) { 534 die "Usage:$0 fetch [--all] [--parent] [svn-remote]\n"; 535 } 536$Git::SVN::no_reuse_existing = undef; 537 if ($_fetch_parent) { 538 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD'); 539 unless ($gs) { 540 die "Unable to determine upstream SVN information from ", 541 "working tree history\n"; 542 } 543 # just fetch, don't checkout. 544$_no_checkout= 'true'; 545$_fetch_all?$gs->fetch_all :$gs->fetch; 546 } elsif ($_fetch_all) { 547 cmd_multi_fetch(); 548 } else { 549$remote||=$Git::SVN::default_repo_id; 550 Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes()); 551 } 552} 553 554sub cmd_set_tree { 555 my (@commits) =@_; 556 if ($_stdin|| !@commits) { 557 print "Reading from stdin...\n"; 558@commits= (); 559 while (<STDIN>) { 560 if (/\b($sha1_short)\b/o) { 561 unshift@commits,$1; 562 } 563 } 564 } 565 my@revs; 566 foreach my$c(@commits) { 567 my@tmp= command('rev-parse',$c); 568 if (scalar@tmp== 1) { 569 push@revs,$tmp[0]; 570 } elsif (scalar@tmp> 1) { 571 push@revs, reverse(command('rev-list',@tmp)); 572 } else { 573 fatal "Failed to rev-parse $c"; 574 } 575 } 576 my$gs= Git::SVN->new; 577 my ($r_last,$cmt_last) =$gs->last_rev_commit; 578$gs->fetch; 579 if (defined$gs->{last_rev} &&$r_last!=$gs->{last_rev}) { 580 fatal "There are new revisions that were fetched ", 581 "and need to be merged (or acknowledged)", 582 "before committing.\nlast rev:$r_last\n", 583 " current:$gs->{last_rev}"; 584 } 585$gs->set_tree($_) foreach@revs; 586 print "Done committing ",scalar@revs," revisions to SVN\n"; 587 unlink$gs->{index}; 588} 589 590sub split_merge_info_range { 591 my ($range) =@_; 592 if ($range=~ /(\d+)-(\d+)/) { 593 return (int($1), int($2)); 594 } else { 595 return (int($range), int($range)); 596 } 597} 598 599sub combine_ranges { 600 my ($in) =@_; 601 602 my@fnums= (); 603 my@arr= split(/,/,$in); 604 for my$element(@arr) { 605 my ($start,$end) = split_merge_info_range($element); 606 push@fnums,$start; 607 } 608 609 my@sorted=@arr[ sort { 610$fnums[$a] <=>$fnums[$b] 611 } 0..$#arr]; 612 613 my@return= (); 614 my$last= -1; 615 my$first= -1; 616 for my$element(@sorted) { 617 my ($start,$end) = split_merge_info_range($element); 618 619 if ($last== -1) { 620$first=$start; 621$last=$end; 622 next; 623 } 624 if ($start<=$last+1) { 625 if ($end>$last) { 626$last=$end; 627 } 628 next; 629 } 630 if ($first==$last) { 631 push@return, "$first"; 632 } else { 633 push@return, "$first-$last"; 634 } 635$first=$start; 636$last=$end; 637 } 638 639 if ($first!= -1) { 640 if ($first==$last) { 641 push@return, "$first"; 642 } else { 643 push@return, "$first-$last"; 644 } 645 } 646 647 return join(',',@return); 648} 649 650sub merge_revs_into_hash { 651 my ($hash,$minfo) =@_; 652 my@lines= split(' ',$minfo); 653 654 for my$line(@lines) { 655 my ($branchpath,$revs) = split(/:/,$line); 656 657 if (exists($hash->{$branchpath})) { 658 # Merge the two revision sets 659 my$combined= "$hash->{$branchpath},$revs"; 660$hash->{$branchpath} = combine_ranges($combined); 661 } else { 662 # Just do range combining for consolidation 663$hash->{$branchpath} = combine_ranges($revs); 664 } 665 } 666} 667 668sub merge_merge_info { 669 my ($mergeinfo_one,$mergeinfo_two) =@_; 670 my%result_hash= (); 671 672 merge_revs_into_hash(\%result_hash,$mergeinfo_one); 673 merge_revs_into_hash(\%result_hash,$mergeinfo_two); 674 675 my$result= ''; 676 # Sort below is for consistency's sake 677 for my$branchname(sort keys(%result_hash)) { 678 my$revlist=$result_hash{$branchname}; 679$result.= "$branchname:$revlist\n" 680 } 681 return$result; 682} 683 684sub populate_merge_info { 685 my ($d,$gs,$uuid,$linear_refs,$rewritten_parent) =@_; 686 687 my%parentshash; 688 read_commit_parents(\%parentshash,$d); 689 my@parents= @{$parentshash{$d}}; 690 if ($#parents> 0) { 691 # Merge commit 692 my$all_parents_ok= 1; 693 my$aggregate_mergeinfo= ''; 694 my$rooturl=$gs->repos_root; 695 696 if (defined($rewritten_parent)) { 697 # Replace first parent with newly-rewritten version 698 shift@parents; 699 unshift@parents,$rewritten_parent; 700 } 701 702 foreach my$parent(@parents) { 703 my ($branchurl,$svnrev,$paruuid) = 704 cmt_metadata($parent); 705 706 unless (defined($svnrev)) { 707 # Should have been caught be preflight check 708 fatal "merge commit $dhas ancestor $parent, but that change " 709 ."doesnot have git-svn metadata!"; 710 } 711 unless ($branchurl=~ /^\Q$rooturl\E(.*)/) { 712 fatal "commit $parent git-svn metadata changed mid-run!"; 713 } 714 my$branchpath=$1; 715 716 my$ra= Git::SVN::Ra->new($branchurl); 717 my (undef, undef,$props) = 718$ra->get_dir(canonicalize_path("."),$svnrev); 719 my$par_mergeinfo=$props->{'svn:mergeinfo'}; 720 unless (defined$par_mergeinfo) { 721$par_mergeinfo= ''; 722 } 723 # Merge previous mergeinfo values 724$aggregate_mergeinfo= 725 merge_merge_info($aggregate_mergeinfo, 726$par_mergeinfo, 0); 727 728 next if$parenteq$parents[0]; # Skip first parent 729 # Add new changes being placed in tree by merge 730 my@cmd= (qw/rev-list --reverse/, 731$parent, qw/--not/); 732 foreach my$par(@parents) { 733 unless ($pareq$parent) { 734 push@cmd,$par; 735 } 736 } 737 my@revsin= (); 738 my ($revlist,$ctx) = command_output_pipe(@cmd); 739 while (<$revlist>) { 740 my$irev=$_; 741 chomp$irev; 742 my (undef,$csvnrev, undef) = 743 cmt_metadata($irev); 744 unless (defined$csvnrev) { 745 # A child is missing SVN annotations... 746 # this might be OK, or might not be. 747 warn "W:child $irevis merged into revision " 748 ."$d but doesnot have git-svn metadata." 749 ."This means git-svn cannot determine the " 750 ."svn revision numbers to place into the " 751 ."svn:mergeinfo property. You must ensure " 752 ."a branch is entirely committed to " 753 ."SVN before merging it in order for" 754 ."svn:mergeinfo population to function " 755 ."properly"; 756 } 757 push@revsin,$csvnrev; 758 } 759 command_close_pipe($revlist,$ctx); 760 761 last unless$all_parents_ok; 762 763 # We now have a list of all SVN revnos which are 764 # merged by this particular parent. Integrate them. 765 next if$#revsin== -1; 766 my$newmergeinfo= "$branchpath:" . join(',',@revsin); 767$aggregate_mergeinfo= 768 merge_merge_info($aggregate_mergeinfo, 769$newmergeinfo, 1); 770 } 771 if ($all_parents_okand$aggregate_mergeinfo) { 772 return$aggregate_mergeinfo; 773 } 774 } 775 776 return undef; 777} 778 779sub cmd_dcommit { 780 my$head= shift; 781 command_noisy(qw/update-index --refresh/); 782 git_cmd_try { command_oneline(qw/diff-index --quiet HEAD/) } 783 'Cannot dcommit with a dirty index. Commit your changes first, ' 784 . "or stash them with `git stash'.\n"; 785$head||= 'HEAD'; 786 787 my$old_head; 788 if ($headne 'HEAD') { 789$old_head= eval { 790 command_oneline([qw/symbolic-ref -q HEAD/]) 791 }; 792 if ($old_head) { 793$old_head=~ s{^refs/heads/}{}; 794 } else { 795$old_head= eval { command_oneline(qw/rev-parse HEAD/) }; 796 } 797 command(['checkout',$head], STDERR => 0); 798 } 799 800 my@refs; 801 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD', \@refs); 802 unless ($gs) { 803 die "Unable to determine upstream SVN information from ", 804 "$headhistory.\nPerhaps the repository is empty."; 805 } 806 807 if (defined$_commit_url) { 808$url=$_commit_url; 809 } else { 810$url= eval { command_oneline('config', '--get', 811 "svn-remote.$gs->{repo_id}.commiturl") }; 812 if (!$url) { 813$url=$gs->full_pushurl 814 } 815 } 816 817 my$last_rev=$_revisionif defined$_revision; 818 if ($url) { 819 print "Committing to$url...\n"; 820 } 821 my ($linear_refs,$parents) = linearize_history($gs, \@refs); 822 if ($_no_rebase&& scalar(@$linear_refs) > 1) { 823 warn "Attempting to commit more than one change while ", 824 "--no-rebase is enabled.\n", 825 "If these changes depend on each other, re-running ", 826 "without --no-rebase may be required." 827 } 828 829 if (defined$_interactive){ 830 my$ask_default= "y"; 831 foreach my$d(@$linear_refs){ 832 my ($fh,$ctx) = command_output_pipe(qw(show --summary),"$d"); 833while(<$fh>){ 834print$_; 835} 836 command_close_pipe($fh,$ctx); 837$_= ask("Commit this patch to SVN? ([y]es (default)|[n]o|[q]uit|[a]ll): ", 838 valid_re =>qr/^(?:yes|y|no|n|quit|q|all|a)/i, 839default=>$ask_default); 840die"Commit this patch reply required"unlessdefined$_; 841if(/^[nq]/i) { 842exit(0); 843}elsif(/^a/i) { 844last; 845} 846} 847} 848 849my$expect_url=$url; 850 851my$push_merge_info=eval{ 852 command_oneline(qw/config --get svn.pushmergeinfo/) 853}; 854if(not defined($push_merge_info) 855or$push_merge_infoeq"false" 856or$push_merge_infoeq"no" 857or$push_merge_infoeq"never") { 858$push_merge_info=0; 859} 860 861unless(defined($_merge_info) || !$push_merge_info) { 862# Preflight check of changes to ensure no issues with mergeinfo 863# This includes check for uncommitted-to-SVN parents 864# (other than the first parent, which we will handle), 865# information from different SVN repos, and paths 866# which are not underneath this repository root. 867my$rooturl=$gs->repos_root; 868foreachmy$d(@$linear_refs) { 869my%parentshash; 870 read_commit_parents(\%parentshash,$d); 871my@realparents= @{$parentshash{$d}}; 872if($#realparents>0) { 873# Merge commit 874shift@realparents;# Remove/ignore first parent 875foreachmy$parent(@realparents) { 876my($branchurl,$svnrev,$paruuid) = cmt_metadata($parent); 877unless(defined$paruuid) { 878# A parent is missing SVN annotations... 879# abort the whole operation. 880 fatal "$parentis merged into revision$d, " 881."but does not have git-svn metadata. " 882."Either dcommit the branch or use a " 883."local cherry-pick, FF merge, or rebase " 884."instead of an explicit merge commit."; 885} 886 887unless($paruuideq$uuid) { 888# Parent has SVN metadata from different repository 889 fatal "merge parent$parentfor change$dhas " 890."git-svn uuid$paruuid, while current change " 891."has uuid$uuid!"; 892} 893 894unless($branchurl=~/^\Q$rooturl\E(.*)/) { 895# This branch is very strange indeed. 896 fatal "merge parent$parentfor$dis on branch " 897."$branchurl, which is not under the " 898."git-svn root$rooturl!"; 899} 900} 901} 902} 903} 904 905my$rewritten_parent; 906 Git::SVN::remove_username($expect_url); 907if(defined($_merge_info)) { 908$_merge_info=~tr{ }{\n}; 909} 910while(1) { 911my$d=shift@$linear_refsorlast; 912unless(defined$last_rev) { 913(undef,$last_rev,undef) = cmt_metadata("$d~1"); 914unless(defined$last_rev) { 915 fatal "Unable to extract revision information ", 916"from commit$d~1"; 917} 918} 919if($_dry_run) { 920print"diff-tree$d~1$d\n"; 921}else{ 922my$cmt_rev; 923 924unless(defined($_merge_info) || !$push_merge_info) { 925$_merge_info= populate_merge_info($d,$gs, 926$uuid, 927$linear_refs, 928$rewritten_parent); 929} 930 931my%ed_opts= ( r =>$last_rev, 932log=> get_commit_entry($d)->{log}, 933 ra => Git::SVN::Ra->new($url), 934 config => SVN::Core::config_get_config( 935$Git::SVN::Ra::config_dir 936), 937 tree_a =>"$d~1", 938 tree_b =>$d, 939 editor_cb =>sub{ 940print"Committed r$_[0]\n"; 941$cmt_rev=$_[0]; 942}, 943 mergeinfo =>$_merge_info, 944 svn_path =>''); 945if(!Git::SVN::Editor->new(\%ed_opts)->apply_diff) { 946print"No changes\n$d~1==$d\n"; 947}elsif($parents->{$d} && @{$parents->{$d}}) { 948$gs->{inject_parents_dcommit}->{$cmt_rev} = 949$parents->{$d}; 950} 951$_fetch_all?$gs->fetch_all:$gs->fetch; 952$last_rev=$cmt_rev; 953next if$_no_rebase; 954 955# we always want to rebase against the current HEAD, 956# not any head that was passed to us 957my@diff= command('diff-tree',$d, 958$gs->refname,'--'); 959my@finish; 960if(@diff) { 961@finish= rebase_cmd(); 962print STDERR "W:$dand ",$gs->refname, 963" differ, using@finish:\n", 964join("\n",@diff),"\n"; 965}else{ 966print"No changes between current HEAD and ", 967$gs->refname, 968"\nResetting to the latest ", 969$gs->refname,"\n"; 970@finish= qw/reset --mixed/; 971} 972 command_noisy(@finish,$gs->refname); 973 974$rewritten_parent= command_oneline(qw/rev-parse HEAD/); 975 976if(@diff) { 977@refs= (); 978my($url_,$rev_,$uuid_,$gs_) = 979 working_head_info('HEAD', \@refs); 980my($linear_refs_,$parents_) = 981 linearize_history($gs_, \@refs); 982if(scalar(@$linear_refs) != 983scalar(@$linear_refs_)) { 984 fatal "# of revisions changed ", 985"\nbefore:\n", 986join("\n",@$linear_refs), 987"\n\nafter:\n", 988join("\n",@$linear_refs_),"\n", 989'If you are attempting to commit ', 990"merges, try running:\n\t", 991'git rebase --interactive', 992'--preserve-merges ', 993$gs->refname, 994"\nBefore dcommitting"; 995} 996if($url_ne$expect_url) { 997if($url_eq$gs->metadata_url) { 998print 999"Accepting rewritten URL:",1000"$url_\n";1001}else{1002 fatal1003"URL mismatch after rebase:",1004"$url_!=$expect_url";1005}1006}1007if($uuid_ne$uuid) {1008 fatal "uuid mismatch after rebase: ",1009"$uuid_!=$uuid";1010}1011# remap parents1012my(%p,@l,$i);1013for($i=0;$i<scalar@$linear_refs;$i++) {1014my$new=$linear_refs_->[$i]ornext;1015$p{$new} =1016$parents->{$linear_refs->[$i]};1017push@l,$new;1018}1019$parents= \%p;1020$linear_refs= \@l;1021}1022}1023}10241025if($old_head) {1026my$new_head= command_oneline(qw/rev-parse HEAD/);1027my$new_is_symbolic=eval{1028 command_oneline(qw/symbolic-ref -q HEAD/);1029};1030if($new_is_symbolic) {1031print"dcommitted the branch ",$head,"\n";1032}else{1033print"dcommitted on a detached HEAD because you gave ",1034"a revision argument.\n",1035"The rewritten commit is: ",$new_head,"\n";1036}1037 command(['checkout',$old_head], STDERR =>0);1038}10391040unlink$gs->{index};1041}10421043sub cmd_branch {1044my($branch_name,$head) =@_;10451046unless(defined$branch_name&&length$branch_name) {1047die(($_tag?"tag":"branch") ." name required\n");1048}1049$head||='HEAD';10501051my(undef,$rev,undef,$gs) = working_head_info($head);1052my$src=$gs->full_pushurl;10531054my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1055my$allglobs=$remote->{$_tag?'tags':'branches'};1056my$glob;1057if($#{$allglobs} ==0) {1058$glob=$allglobs->[0];1059}else{1060unless(defined$_branch_dest) {1061die"Multiple ",1062$_tag?"tag":"branch",1063" paths defined for Subversion repository.\n",1064"You must specify where you want to create the ",1065$_tag?"tag":"branch",1066" with the --destination argument.\n";1067}1068foreachmy$g(@{$allglobs}) {1069# Git::SVN::Editor could probably be moved to Git.pm..1070my$re= Git::SVN::Editor::glob2pat($g->{path}->{left});1071if($_branch_dest=~/$re/) {1072$glob=$g;1073last;1074}1075}1076unless(defined$glob) {1077my$dest_re=qr/\b\Q$_branch_dest\E\b/;1078foreachmy$g(@{$allglobs}) {1079$g->{path}->{left} =~/$dest_re/ornext;1080if(defined$glob) {1081die"Ambiguous destination: ",1082$_branch_dest,"\nmatches both '",1083$glob->{path}->{left},"' and '",1084$g->{path}->{left},"'\n";1085}1086$glob=$g;1087}1088unless(defined$glob) {1089die"Unknown ",1090$_tag?"tag":"branch",1091" destination$_branch_dest\n";1092}1093}1094}1095my($lft,$rgt) = @{$glob->{path} }{qw/left right/};1096my$url;1097if(defined$_commit_url) {1098$url=$_commit_url;1099}else{1100$url=eval{ command_oneline('config','--get',1101"svn-remote.$gs->{repo_id}.commiturl") };1102if(!$url) {1103$url=$remote->{pushurl} ||$remote->{url};1104}1105}1106my$dst=join'/',$url,$lft,$branch_name, ($rgt|| ());11071108if($dst=~/^https:/&&$src=~/^http:/) {1109$src=~s/^http:/https:/;1110}11111112::_req_svn();11131114my$ctx= SVN::Client->new(1115 auth => Git::SVN::Ra::_auth_providers(),1116 log_msg =>sub{1117${$_[0] } =defined$_message1118?$_message1119:'Create '. ($_tag?'tag ':'branch ')1120.$branch_name;1121},1122);11231124eval{1125$ctx->ls($dst,'HEAD',0);1126}and die"branch ${branch_name} already exists\n";11271128print"Copying ${src} at r${rev} to ${dst}...\n";1129$ctx->copy($src,$rev,$dst)1130unless$_dry_run;11311132$gs->fetch_all;1133}11341135sub cmd_find_rev {1136my$revision_or_hash=shift or die"SVN or git revision required ",1137"as a command-line argument\n";1138my$result;1139if($revision_or_hash=~/^r\d+$/) {1140my$head=shift;1141$head||='HEAD';1142my@refs;1143my(undef,undef,$uuid,$gs) = working_head_info($head, \@refs);1144unless($gs) {1145die"Unable to determine upstream SVN information from ",1146"$headhistory\n";1147}1148my$desired_revision=substr($revision_or_hash,1);1149$result=$gs->rev_map_get($desired_revision,$uuid);1150}else{1151my(undef,$rev,undef) = cmt_metadata($revision_or_hash);1152$result=$rev;1153}1154print"$result\n"if$result;1155}11561157sub auto_create_empty_directories {1158my($gs) =@_;1159my$var=eval{ command_oneline('config','--get','--bool',1160"svn-remote.$gs->{repo_id}.automkdirs") };1161# By default, create empty directories by consulting the unhandled log,1162# but allow setting it to 'false' to skip it.1163return!($var&&$vareq'false');1164}11651166sub cmd_rebase {1167 command_noisy(qw/update-index --refresh/);1168my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1169unless($gs) {1170die"Unable to determine upstream SVN information from ",1171"working tree history\n";1172}1173if($_dry_run) {1174print"Remote Branch: ".$gs->refname."\n";1175print"SVN URL: ".$url."\n";1176return;1177}1178if(command(qw/diff-index HEAD --/)) {1179print STDERR "Cannot rebase with uncommited changes:\n";1180 command_noisy('status');1181exit1;1182}1183unless($_local) {1184# rebase will checkout for us, so no need to do it explicitly1185$_no_checkout='true';1186$_fetch_all?$gs->fetch_all:$gs->fetch;1187}1188 command_noisy(rebase_cmd(),$gs->refname);1189if(auto_create_empty_directories($gs)) {1190$gs->mkemptydirs;1191}1192}11931194sub cmd_show_ignore {1195my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1196$gs||= Git::SVN->new;1197my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1198$gs->prop_walk($gs->{path},$r,sub{1199my($gs,$path,$props) =@_;1200print STDOUT "\n#$path\n";1201my$s=$props->{'svn:ignore'}orreturn;1202$s=~s/[\r\n]+/\n/g;1203$s=~s/^\n+//;1204chomp$s;1205$s=~ s#^#$path#gm;1206print STDOUT "$s\n";1207});1208}12091210sub cmd_show_externals {1211my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1212$gs||= Git::SVN->new;1213my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1214$gs->prop_walk($gs->{path},$r,sub{1215my($gs,$path,$props) =@_;1216print STDOUT "\n#$path\n";1217my$s=$props->{'svn:externals'}orreturn;1218$s=~s/[\r\n]+/\n/g;1219chomp$s;1220$s=~ s#^#$path#gm;1221print STDOUT "$s\n";1222});1223}12241225sub cmd_create_ignore {1226my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1227$gs||= Git::SVN->new;1228my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1229$gs->prop_walk($gs->{path},$r,sub{1230my($gs,$path,$props) =@_;1231# $path is of the form /path/to/dir/1232$path='.'.$path;1233# SVN can have attributes on empty directories,1234# which git won't track1235 mkpath([$path])unless-d $path;1236my$ignore=$path.'.gitignore';1237my$s=$props->{'svn:ignore'}orreturn;1238open(GITIGNORE,'>',$ignore)1239or fatal("Failed to open `$ignore' for writing:$!");1240$s=~s/[\r\n]+/\n/g;1241$s=~s/^\n+//;1242chomp$s;1243# Prefix all patterns so that the ignore doesn't apply1244# to sub-directories.1245$s=~ s#^#/#gm;1246print GITIGNORE "$s\n";1247close(GITIGNORE)1248or fatal("Failed to close `$ignore':$!");1249 command_noisy('add','-f',$ignore);1250});1251}12521253sub cmd_mkdirs {1254my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1255$gs||= Git::SVN->new;1256$gs->mkemptydirs($_revision);1257}12581259sub canonicalize_path {1260my($path) =@_;1261my$dot_slash_added=0;1262if(substr($path,0,1)ne"/") {1263$path="./".$path;1264$dot_slash_added=1;1265}1266# File::Spec->canonpath doesn't collapse x/../y into y (for a1267# good reason), so let's do this manually.1268$path=~ s#/+#/#g;1269$path=~ s#/\.(?:/|$)#/#g;1270$path=~ s#/[^/]+/\.\.##g;1271$path=~ s#/$##g;1272$path=~ s#^\./## if $dot_slash_added;1273$path=~ s#^/##;1274$path=~ s#^\.$##;1275return$path;1276}12771278sub canonicalize_url {1279my($url) =@_;1280$url=~ s#^([^:]+://[^/]*/)(.*)$#$1 . canonicalize_path($2)#e;1281return$url;1282}12831284# get_svnprops(PATH)1285# ------------------1286# Helper for cmd_propget and cmd_proplist below.1287sub get_svnprops {1288my$path=shift;1289my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1290$gs||= Git::SVN->new;12911292# prefix THE PATH by the sub-directory from which the user1293# invoked us.1294$path=$cmd_dir_prefix.$path;1295 fatal("No such file or directory:$path")unless-e $path;1296my$is_dir= -d $path?1:0;1297$path=$gs->{path} .'/'.$path;12981299# canonicalize the path (otherwise libsvn will abort or fail to1300# find the file)1301$path= canonicalize_path($path);13021303my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1304my$props;1305if($is_dir) {1306(undef,undef,$props) =$gs->ra->get_dir($path,$r);1307}1308else{1309(undef,$props) =$gs->ra->get_file($path,$r,undef);1310}1311return$props;1312}13131314# cmd_propget (PROP, PATH)1315# ------------------------1316# Print the SVN property PROP for PATH.1317sub cmd_propget {1318my($prop,$path) =@_;1319$path='.'ifnot defined$path;1320 usage(1)ifnot defined$prop;1321my$props= get_svnprops($path);1322if(not defined$props->{$prop}) {1323 fatal("`$path' does not have a `$prop' SVN property.");1324}1325print$props->{$prop} ."\n";1326}13271328# cmd_proplist (PATH)1329# -------------------1330# Print the list of SVN properties for PATH.1331sub cmd_proplist {1332my$path=shift;1333$path='.'ifnot defined$path;1334my$props= get_svnprops($path);1335print"Properties on '$path':\n";1336foreach(sort keys%{$props}) {1337print"$_\n";1338}1339}13401341sub cmd_multi_init {1342my$url=shift;1343unless(defined$_trunk||@_branches||@_tags) {1344 usage(1);1345}13461347$_prefix=''unlessdefined$_prefix;1348if(defined$url) {1349$url= canonicalize_url($url);1350 init_subdir(@_);1351}1352 do_git_init_db();1353if(defined$_trunk) {1354$_trunk=~ s#^/+##;1355my$trunk_ref='refs/remotes/'.$_prefix.'trunk';1356# try both old-style and new-style lookups:1357my$gs_trunk=eval{ Git::SVN->new($trunk_ref) };1358unless($gs_trunk) {1359my($trunk_url,$trunk_path) =1360 complete_svn_url($url,$_trunk);1361$gs_trunk= Git::SVN->init($trunk_url,$trunk_path,1362undef,$trunk_ref);1363}1364}1365return unless@_branches||@_tags;1366my$ra=$url? Git::SVN::Ra->new($url) :undef;1367foreachmy$path(@_branches) {1368 complete_url_ls_init($ra,$path,'--branches/-b',$_prefix);1369}1370foreachmy$path(@_tags) {1371 complete_url_ls_init($ra,$path,'--tags/-t',$_prefix.'tags/');1372}1373}13741375sub cmd_multi_fetch {1376$Git::SVN::no_reuse_existing =undef;1377my$remotes= Git::SVN::read_all_remotes();1378foreachmy$repo_id(sort keys%$remotes) {1379if($remotes->{$repo_id}->{url}) {1380 Git::SVN::fetch_all($repo_id,$remotes);1381}1382}1383}13841385# this command is special because it requires no metadata1386sub cmd_commit_diff {1387my($ta,$tb,$url) =@_;1388my$usage="Usage:$0commit-diff -r<revision> ".1389"<tree-ish> <tree-ish> [<URL>]";1390 fatal($usage)if(!defined$ta|| !defined$tb);1391my$svn_path='';1392if(!defined$url) {1393my$gs=eval{ Git::SVN->new};1394if(!$gs) {1395 fatal("Needed URL or usable git-svn --id in ",1396"the command-line\n",$usage);1397}1398$url=$gs->{url};1399$svn_path=$gs->{path};1400}1401unless(defined$_revision) {1402 fatal("-r|--revision is a required argument\n",$usage);1403}1404if(defined$_message&&defined$_file) {1405 fatal("Both --message/-m and --file/-F specified ",1406"for the commit message.\n",1407"I have no idea what you mean");1408}1409if(defined$_file) {1410$_message= file_to_s($_file);1411}else{1412$_message||= get_commit_entry($tb)->{log};1413}1414my$ra||= Git::SVN::Ra->new($url);1415my$r=$_revision;1416if($req'HEAD') {1417$r=$ra->get_latest_revnum;1418}elsif($r!~/^\d+$/) {1419die"revision argument:$rnot understood by git-svn\n";1420}1421my%ed_opts= ( r =>$r,1422log=>$_message,1423 ra =>$ra,1424 tree_a =>$ta,1425 tree_b =>$tb,1426 editor_cb =>sub{print"Committed r$_[0]\n"},1427 svn_path =>$svn_path);1428if(!Git::SVN::Editor->new(\%ed_opts)->apply_diff) {1429print"No changes\n$ta==$tb\n";1430}1431}14321433sub escape_uri_only {1434my($uri) =@_;1435my@tmp;1436foreach(splitm{/},$uri) {1437s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;1438push@tmp,$_;1439}1440join('/',@tmp);1441}14421443sub escape_url {1444my($url) =@_;1445if($url=~ m#^([^:]+)://([^/]*)(.*)$#) {1446my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));1447$url="$scheme://$domain$uri";1448}1449$url;1450}14511452sub cmd_info {1453my$path= canonicalize_path(defined($_[0]) ?$_[0] :".");1454my$fullpath= canonicalize_path($cmd_dir_prefix.$path);1455if(exists$_[1]) {1456die"Too many arguments specified\n";1457}14581459my($file_type,$diff_status) = find_file_type_and_diff_status($path);14601461if(!$file_type&& !$diff_status) {1462print STDERR "svn: '$path' is not under version control\n";1463exit1;1464}14651466my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1467unless($gs) {1468die"Unable to determine upstream SVN information from ",1469"working tree history\n";1470}14711472# canonicalize_path() will return "" to make libsvn 1.5.x happy,1473$path="."if$patheq"";14741475my$full_url=$url. ($fullpatheq""?"":"/$fullpath");14761477if($_url) {1478print escape_url($full_url),"\n";1479return;1480}14811482my$result="Path:$path\n";1483$result.="Name: ". basename($path) ."\n"if$file_typene"dir";1484$result.="URL: ". escape_url($full_url) ."\n";14851486eval{1487my$repos_root=$gs->repos_root;1488 Git::SVN::remove_username($repos_root);1489$result.="Repository Root: ". escape_url($repos_root) ."\n";1490};1491if($@) {1492$result.="Repository Root: (offline)\n";1493}1494::_req_svn();1495$result.="Repository UUID:$uuid\n"unless$diff_statuseq"A"&&1496(::compare_svn_version('1.5.4') <=0||$file_typene"dir");1497$result.="Revision: ". ($diff_statuseq"A"?0:$rev) ."\n";14981499$result.="Node Kind: ".1500($file_typeeq"dir"?"directory":"file") ."\n";15011502my$schedule=$diff_statuseq"A"1503?"add"1504: ($diff_statuseq"D"?"delete":"normal");1505$result.="Schedule:$schedule\n";15061507if($diff_statuseq"A") {1508print$result,"\n";1509return;1510}15111512my($lc_author,$lc_rev,$lc_date_utc);1513my@args= Git::SVN::Log::git_svn_log_cmd($rev,$rev,"--",$fullpath);1514my$log= command_output_pipe(@args);1515my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;1516while(<$log>) {1517if(/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {1518$lc_author=$1;1519$lc_date_utc= Git::SVN::Log::parse_git_date($2,$3);1520}elsif(/^${esc_color} (git-svn-id:.+)$/o) {1521(undef,$lc_rev,undef) = ::extract_metadata($1);1522}1523}1524close$log;15251526 Git::SVN::Log::set_local_timezone();15271528$result.="Last Changed Author:$lc_author\n";1529$result.="Last Changed Rev:$lc_rev\n";1530$result.="Last Changed Date: ".1531 Git::SVN::Log::format_svn_date($lc_date_utc) ."\n";15321533if($file_typene"dir") {1534my$text_last_updated_date=1535($diff_statuseq"D"?$lc_date_utc: (stat$path)[9]);1536$result.=1537"Text Last Updated: ".1538 Git::SVN::Log::format_svn_date($text_last_updated_date) .1539"\n";1540my$checksum;1541if($diff_statuseq"D") {1542my($fh,$ctx) =1543 command_output_pipe(qw(cat-file blob),"HEAD:$path");1544if($file_typeeq"link") {1545my$file_name= <$fh>;1546$checksum= md5sum("link$file_name");1547}else{1548$checksum= md5sum($fh);1549}1550 command_close_pipe($fh,$ctx);1551}elsif($file_typeeq"link") {1552my$file_name=1553 command(qw(cat-file blob),"HEAD:$path");1554$checksum=1555 md5sum("link ".$file_name);1556}else{1557open FILE,"<",$pathor die$!;1558$checksum= md5sum(\*FILE);1559close FILE or die$!;1560}1561$result.="Checksum: ".$checksum."\n";1562}15631564print$result,"\n";1565}15661567sub cmd_reset {1568my$target=shift||$_revisionor die"SVN revision required\n";1569$target=$1if$target=~/^r(\d+)$/;1570$target=~/^\d+$/or die"Numeric SVN revision expected\n";1571my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1572unless($gs) {1573die"Unable to determine upstream SVN information from ".1574"history\n";1575}1576my($r,$c) =$gs->find_rev_before($target,not$_fetch_parent);1577die"Cannot find SVN revision$target\n"unlessdefined($c);1578$gs->rev_map_set($r,$c,'reset',$uuid);1579print"r$r=$c($gs->{ref_id})\n";1580}15811582sub cmd_gc {1583if(!$can_compress) {1584warn"Compress::Zlib could not be found; unhandled.log ".1585"files will not be compressed.\n";1586}1587 find({ wanted => \&gc_directory, no_chdir =>1},"$ENV{GIT_DIR}/svn");1588}15891590########################### utility functions #########################15911592sub rebase_cmd {1593my@cmd= qw/rebase/;1594push@cmd,'-v'if$_verbose;1595push@cmd, qw/--merge/if$_merge;1596push@cmd,"--strategy=$_strategy"if$_strategy;1597push@cmd,"--preserve-merges"if$_preserve_merges;1598@cmd;1599}16001601sub post_fetch_checkout {1602return if$_no_checkout;1603my$gs=$Git::SVN::_head orreturn;1604return if verify_ref('refs/heads/master^0');16051606# look for "trunk" ref if it exists1607my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1608my$fetch=$remote->{fetch};1609if($fetch) {1610foreachmy$p(keys%$fetch) {1611 basename($fetch->{$p})eq'trunk'ornext;1612$gs= Git::SVN->new($fetch->{$p},$gs->{repo_id},$p);1613last;1614}1615}16161617my$valid_head= verify_ref('HEAD^0');1618 command_noisy(qw(update-ref refs/heads/master),$gs->refname);1619return if($valid_head|| !verify_ref('HEAD^0'));16201621return if$ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;1622my$index=$ENV{GIT_INDEX_FILE} ||"$ENV{GIT_DIR}/index";1623return if-f $index;16241625return if command_oneline(qw/rev-parse --is-inside-work-tree/)eq'false';1626return if command_oneline(qw/rev-parse --is-inside-git-dir/)eq'true';1627 command_noisy(qw/read-tree -m -u -v HEAD HEAD/);1628print STDERR "Checked out HEAD:\n",1629$gs->full_url," r",$gs->last_rev,"\n";1630if(auto_create_empty_directories($gs)) {1631$gs->mkemptydirs($gs->last_rev);1632}1633}16341635sub complete_svn_url {1636my($url,$path) =@_;1637$path=~ s#/+$##;1638if($path!~ m#^[a-z\+]+://#) {1639if(!defined$url||$url!~ m#^[a-z\+]+://#) {1640 fatal("E: '$path' is not a complete URL ",1641"and a separate URL is not specified");1642}1643return($url,$path);1644}1645return($path,'');1646}16471648sub complete_url_ls_init {1649my($ra,$repo_path,$switch,$pfx) =@_;1650unless($repo_path) {1651print STDERR "W:$switchnot specified\n";1652return;1653}1654$repo_path=~ s#/+$##;1655if($repo_path=~ m#^[a-z\+]+://#) {1656$ra= Git::SVN::Ra->new($repo_path);1657$repo_path='';1658}else{1659$repo_path=~ s#^/+##;1660unless($ra) {1661 fatal("E: '$repo_path' is not a complete URL ",1662"and a separate URL is not specified");1663}1664}1665my$url=$ra->{url};1666my$gs= Git::SVN->init($url,undef,undef,undef,1);1667my$k="svn-remote.$gs->{repo_id}.url";1668my$orig_url=eval{ command_oneline(qw/config --get/,$k) };1669if($orig_url&& ($orig_urlne$gs->{url})) {1670die"$kalready set:$orig_url\n",1671"wanted to set to:$gs->{url}\n";1672}1673 command_oneline('config',$k,$gs->{url})unless$orig_url;1674my$remote_path="$gs->{path}/$repo_path";1675$remote_path=~s{%([0-9A-F]{2})}{chr hex($1)}ieg;1676$remote_path=~ s#/+#/#g;1677$remote_path=~ s#^/##g;1678$remote_path.="/*"if$remote_path!~ /\*/;1679my($n) = ($switch=~/^--(\w+)/);1680if(length$pfx&&$pfx!~ m#/$#) {1681die"--prefix='$pfx' must have a trailing slash '/'\n";1682}1683 command_noisy('config',1684'--add',1685"svn-remote.$gs->{repo_id}.$n",1686"$remote_path:refs/remotes/$pfx*".1687('/*' x (($remote_path=~ tr/*/*/) -1)) );1688}16891690sub verify_ref {1691my($ref) =@_;1692eval{ command_oneline(['rev-parse','--verify',$ref],1693{ STDERR =>0}); };1694}16951696sub get_tree_from_treeish {1697my($treeish) =@_;1698# $treeish can be a symbolic ref, too:1699my$type= command_oneline(qw/cat-file -t/,$treeish);1700my$expected;1701while($typeeq'tag') {1702($treeish,$type) = command(qw/cat-file tag/,$treeish);1703}1704if($typeeq'commit') {1705$expected= (grep/^tree /, command(qw/cat-file commit/,1706$treeish))[0];1707($expected) = ($expected=~/^tree ($sha1)$/o);1708die"Unable to get tree from$treeish\n"unless$expected;1709}elsif($typeeq'tree') {1710$expected=$treeish;1711}else{1712die"$treeishis a$type, expected tree, tag or commit\n";1713}1714return$expected;1715}17161717sub get_commit_entry {1718my($treeish) =shift;1719my%log_entry= (log=>'', tree => get_tree_from_treeish($treeish) );1720my$commit_editmsg="$ENV{GIT_DIR}/COMMIT_EDITMSG";1721my$commit_msg="$ENV{GIT_DIR}/COMMIT_MSG";1722open my$log_fh,'>',$commit_editmsgor croak $!;17231724my$type= command_oneline(qw/cat-file -t/,$treeish);1725if($typeeq'commit'||$typeeq'tag') {1726my($msg_fh,$ctx) = command_output_pipe('cat-file',1727$type,$treeish);1728my$in_msg=0;1729my$author;1730my$saw_from=0;1731my$msgbuf="";1732while(<$msg_fh>) {1733if(!$in_msg) {1734$in_msg=1if(/^\s*$/);1735$author=$1if(/^author (.*>)/);1736}elsif(/^git-svn-id: /) {1737# skip this for now, we regenerate the1738# correct one on re-fetch anyways1739# TODO: set *:merge properties or like...1740}else{1741if(/^From:/||/^Signed-off-by:/) {1742$saw_from=1;1743}1744$msgbuf.=$_;1745}1746}1747$msgbuf=~s/\s+$//s;1748if($Git::SVN::_add_author_from &&defined($author)1749&& !$saw_from) {1750$msgbuf.="\n\nFrom:$author";1751}1752print$log_fh $msgbufor croak $!;1753 command_close_pipe($msg_fh,$ctx);1754}1755close$log_fhor croak $!;17561757if($_edit|| ($typeeq'tree')) {1758chomp(my$editor= command_oneline(qw(var GIT_EDITOR)));1759system('sh','-c',$editor.' "$@"',$editor,$commit_editmsg);1760}1761rename$commit_editmsg,$commit_msgor croak $!;1762{1763require Encode;1764# SVN requires messages to be UTF-8 when entering the repo1765local$/;1766open$log_fh,'<',$commit_msgor croak $!;1767binmode$log_fh;1768chomp($log_entry{log} = <$log_fh>);17691770my$enc= Git::config('i18n.commitencoding') ||'UTF-8';1771my$msg=$log_entry{log};17721773eval{$msg= Encode::decode($enc,$msg,1) };1774if($@) {1775die"Could not decode as$enc:\n",$msg,1776"\nPerhaps you need to set i18n.commitencoding\n";1777}17781779eval{$msg= Encode::encode('UTF-8',$msg,1) };1780die"Could not encode as UTF-8:\n$msg\n"if$@;17811782$log_entry{log} =$msg;17831784close$log_fhor croak $!;1785}1786unlink$commit_msg;1787 \%log_entry;1788}17891790sub s_to_file {1791my($str,$file,$mode) =@_;1792open my$fd,'>',$fileor croak $!;1793print$fd $str,"\n"or croak $!;1794close$fdor croak $!;1795chmod($mode&~umask,$file)if(defined$mode);1796}17971798sub file_to_s {1799my$file=shift;1800open my$fd,'<',$fileor croak "$!: file:$file\n";1801local$/;1802my$ret= <$fd>;1803close$fdor croak $!;1804$ret=~s/\s*$//s;1805return$ret;1806}18071808# '<svn username> = real-name <email address>' mapping based on git-svnimport:1809sub load_authors {1810open my$authors,'<',$_authorsor die"Can't open$_authors$!\n";1811my$log=$cmdeq'log';1812while(<$authors>) {1813chomp;1814next unless/^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;1815my($user,$name,$email) = ($1,$2,$3);1816if($log) {1817$Git::SVN::Log::rusers{"$name<$email>"} =$user;1818}else{1819$users{$user} = [$name,$email];1820}1821}1822close$authorsor croak $!;1823}18241825# convert GetOpt::Long specs for use by git-config1826sub read_git_config {1827my$opts=shift;1828my@config_only;1829foreachmy$o(keys%$opts) {1830# if we have mixedCase and a long option-only, then1831# it's a config-only variable that we don't need for1832# the command-line.1833push@config_only,$oif($o=~/[A-Z]/&&$o=~/^[a-z]+$/i);1834my$v=$opts->{$o};1835my($key) = ($o=~/^([a-zA-Z\-]+)/);1836$key=~s/-//g;1837my$arg='git config';1838$arg.=' --int'if($o=~/[:=]i$/);1839$arg.=' --bool'if($o!~/[:=][sfi]$/);1840if(ref$veq'ARRAY') {1841chomp(my@tmp=`$arg--get-all svn.$key`);1842@$v=@tmpif@tmp;1843 } else {1844 chomp(my$tmp= `$arg--get svn.$key`);1845if($tmp&& !($arg=~/ --bool/&&$tmpeq'false')) {1846$$v=$tmp;1847}1848}1849}1850delete@$opts{@config_only}if@config_only;1851}18521853sub extract_metadata {1854my$id=shift orreturn(undef,undef,undef);1855my($url,$rev,$uuid) = ($id=~ /^\s*git-svn-id:\s+(.*)\@(\d+)1856 \s([a-f\d\-]+)$/ix);1857if(!defined$rev|| !$uuid|| !$url) {1858# some of the original repositories I made had1859# identifiers like this:1860($rev,$uuid) = ($id=~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i);1861}1862return($url,$rev,$uuid);1863}18641865sub cmt_metadata {1866return extract_metadata((grep(/^git-svn-id: /,1867 command(qw/cat-file commit/,shift)))[-1]);1868}18691870sub cmt_sha2rev_batch {1871my%s2r;1872my($pid,$in,$out,$ctx) = command_bidi_pipe(qw/cat-file --batch/);1873my$list=shift;18741875foreachmy$sha(@{$list}) {1876my$first=1;1877my$size=0;1878print$out $sha,"\n";18791880while(my$line= <$in>) {1881if($first&&$line=~/^[[:xdigit:]]{40}\smissing$/) {1882last;1883}elsif($first&&1884$line=~/^[[:xdigit:]]{40}\scommit\s(\d+)$/) {1885$first=0;1886$size=$1;1887next;1888}elsif($line=~/^(git-svn-id: )/) {1889my(undef,$rev,undef) =1890 extract_metadata($line);1891$s2r{$sha} =$rev;1892}18931894$size-=length($line);1895last if($size==0);1896}1897}18981899 command_close_bidi_pipe($pid,$in,$out,$ctx);19001901return \%s2r;1902}19031904sub working_head_info {1905my($head,$refs) =@_;1906my@args= qw/rev-list --first-parent --pretty=medium/;1907my($fh,$ctx) = command_output_pipe(@args,$head);1908my$hash;1909my%max;1910while(<$fh>) {1911if(m{^commit ($::sha1)$}) {1912unshift@$refs,$hashif$hashand$refs;1913$hash=$1;1914next;1915}1916next unlesss{^\s*(git-svn-id:)}{$1};1917my($url,$rev,$uuid) = extract_metadata($_);1918if(defined$url&&defined$rev) {1919next if$max{$url}and$max{$url} <$rev;1920if(my$gs= Git::SVN->find_by_url($url)) {1921my$c=$gs->rev_map_get($rev,$uuid);1922if($c&&$ceq$hash) {1923close$fh;# break the pipe1924return($url,$rev,$uuid,$gs);1925}else{1926$max{$url} ||=$gs->rev_map_max;1927}1928}1929}1930}1931 command_close_pipe($fh,$ctx);1932(undef,undef,undef,undef);1933}19341935sub read_commit_parents {1936my($parents,$c) =@_;1937chomp(my$p= command_oneline(qw/rev-list --parents -1/,$c));1938$p=~s/^($c)\s*//or die"rev-list --parents -1$cfailed!\n";1939@{$parents->{$c}} =split(/ /,$p);1940}19411942sub linearize_history {1943my($gs,$refs) =@_;1944my%parents;1945foreachmy$c(@$refs) {1946 read_commit_parents(\%parents,$c);1947}19481949my@linear_refs;1950my%skip= ();1951my$last_svn_commit=$gs->last_commit;1952foreachmy$c(reverse@$refs) {1953next if$ceq$last_svn_commit;1954last if$skip{$c};19551956unshift@linear_refs,$c;1957$skip{$c} =1;19581959# we only want the first parent to diff against for linear1960# history, we save the rest to inject when we finalize the1961# svn commit1962my$fp_a= verify_ref("$c~1");1963my$fp_b=shift@{$parents{$c}}if$parents{$c};1964if(!$fp_a|| !$fp_b) {1965die"Commit$c\n",1966"has no parent commit, and therefore ",1967"nothing to diff against.\n",1968"You should be working from a repository ",1969"originally created by git-svn\n";1970}1971if($fp_ane$fp_b) {1972die"$c~1=$fp_a, however parsing commit$c",1973"revealed that:\n$c~1=$fp_b\nBUG!\n";1974}19751976foreachmy$p(@{$parents{$c}}) {1977$skip{$p} =1;1978}1979}1980(\@linear_refs, \%parents);1981}19821983sub find_file_type_and_diff_status {1984my($path) =@_;1985return('dir','')if$patheq'';19861987my$diff_output=1988 command_oneline(qw(diff --cached --name-status --),$path) ||"";1989my$diff_status= (split(' ',$diff_output))[0] ||"";19901991my$ls_tree= command_oneline(qw(ls-tree HEAD),$path) ||"";19921993return(undef,undef)if!$diff_status&& !$ls_tree;19941995if($diff_statuseq"A") {1996return("link",$diff_status)if-l $path;1997return("dir",$diff_status)if-d $path;1998return("file",$diff_status);1999}20002001my$mode= (split(' ',$ls_tree))[0] ||"";20022003return("link",$diff_status)if$modeeq"120000";2004return("dir",$diff_status)if$modeeq"040000";2005return("file",$diff_status);2006}20072008sub md5sum {2009my$arg=shift;2010my$ref=ref$arg;2011my$md5= Digest::MD5->new();2012if($refeq'GLOB'||$refeq'IO::File'||$refeq'File::Temp') {2013$md5->addfile($arg)or croak $!;2014}elsif($refeq'SCALAR') {2015$md5->add($$arg)or croak $!;2016}elsif(!$ref) {2017$md5->add($arg)or croak $!;2018}else{2019::fatal "Can't provide MD5 hash for unknown ref type: '",$ref,"'";2020}2021return$md5->hexdigest();2022}20232024sub gc_directory {2025if($can_compress&& -f $_&& basename($_)eq"unhandled.log") {2026my$out_filename=$_.".gz";2027open my$in_fh,"<",$_or die"Unable to open$_:$!\n";2028binmode$in_fh;2029my$gz= Compress::Zlib::gzopen($out_filename,"ab")or2030die"Unable to open$out_filename:$!\n";20312032my$res;2033while($res=sysread($in_fh,my$str,1024)) {2034$gz->gzwrite($str)or2035die"Unable to write: ".$gz->gzerror()."!\n";2036}2037unlink$_or die"unlink$File::Find::name:$!\n";2038}elsif(-f $_&& basename($_)eq"index") {2039unlink$_or die"unlink$_:$!\n";2040}2041}20422043package Git::SVN;2044use strict;2045use warnings;2046use Fcntl qw/:DEFAULT :seek/;2047useconstant rev_map_fmt =>'NH40';2048use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent2049$_repack $_repack_flags $_use_svm_props $_head2050$_use_svnsync_props $no_reuse_existing $_minimize_url2051$_use_log_author $_add_author_from $_localtime/;2052use Carp qw/croak/;2053use File::Path qw/mkpath/;2054use File::Copy qw/copy/;2055use IPC::Open3;2056use Time::Local;2057use Memoize;# core since 5.8.0, Jul 20022058use Memoize::Storable;2059use POSIX qw(:signal_h);20602061my($_gc_nr,$_gc_period);20622063# properties that we do not log:2064my%SKIP_PROP;2065BEGIN{2066%SKIP_PROP=map{$_=>1} qw/svn:wc:ra_dav:version-url2067 svn:special svn:executable2068 svn:entry:committed-rev2069 svn:entry:last-author2070 svn:entry:uuid2071 svn:entry:committed-date/;20722073# some options are read globally, but can be overridden locally2074# per [svn-remote "..."] section. Command-line options will *NOT*2075# override options set in an [svn-remote "..."] section2076no strict 'refs';2077formy$option(qw/follow_parent no_metadata use_svm_props2078 use_svnsync_props/) {2079my$key=$option;2080$key=~tr/_//d;2081my$prop="-$option";2082*$option=sub{2083my($self) =@_;2084return$self->{$prop}ifexists$self->{$prop};2085my$k="svn-remote.$self->{repo_id}.$key";2086eval{ command_oneline(qw/config --get/,$k) };2087if($@) {2088$self->{$prop} = ${"Git::SVN::_$option"};2089}else{2090my$v= command_oneline(qw/config --bool/,$k);2091$self->{$prop} =$veq'false'?0:1;2092}2093return$self->{$prop};2094}2095}2096}209720982099my(%LOCKFILES,%INDEX_FILES);2100END{2101unlink keys%LOCKFILESif%LOCKFILES;2102unlink keys%INDEX_FILESif%INDEX_FILES;2103}21042105sub resolve_local_globs {2106my($url,$fetch,$glob_spec) =@_;2107return unlessdefined$glob_spec;2108my$ref=$glob_spec->{ref};2109my$path=$glob_spec->{path};2110foreach(command(qw#for-each-ref --format=%(refname) refs/#)) {2111next unless m#^$ref->{regex}$#;2112my$p=$1;2113my$pathname= desanitize_refname($path->full_path($p));2114my$refname= desanitize_refname($ref->full_path($p));2115if(my$existing=$fetch->{$pathname}) {2116if($existingne$refname) {2117die"Refspec conflict:\n",2118"existing:$existing\n",2119" globbed:$refname\n";2120}2121my$u= (::cmt_metadata("$refname"))[0];2122$u=~s!^\Q$url\E(/|$)!!or die2123"$refname: '$url' not found in '$u'\n";2124if($pathnamene$u) {2125warn"W: Refspec glob conflict ",2126"(ref:$refname):\n",2127"expected path:$pathname\n",2128" real path:$u\n",2129"Continuing ahead with$u\n";2130next;2131}2132}else{2133$fetch->{$pathname} =$refname;2134}2135}2136}21372138sub parse_revision_argument {2139my($base,$head) =@_;2140if(!defined$::_revision || $::_revision eq'BASE:HEAD') {2141return($base,$head);2142}2143return($1,$2)if($::_revision =~/^(\d+):(\d+)$/);2144return($::_revision, $::_revision)if($::_revision =~/^\d+$/);2145return($head,$head)if($::_revision eq'HEAD');2146return($base,$1)if($::_revision =~/^BASE:(\d+)$/);2147return($1,$head)if($::_revision =~/^(\d+):HEAD$/);2148die"revision argument: $::_revision not understood by git-svn\n";2149}21502151sub fetch_all {2152my($repo_id,$remotes) =@_;2153if(ref$repo_id) {2154my$gs=$repo_id;2155$repo_id=undef;2156$repo_id=$gs->{repo_id};2157}2158$remotes||= read_all_remotes();2159my$remote=$remotes->{$repo_id}or2160die"[svn-remote\"$repo_id\"] unknown\n";2161my$fetch=$remote->{fetch};2162my$url=$remote->{url}or die"svn-remote.$repo_id.url not defined\n";2163my(@gs,@globs);2164my$ra= Git::SVN::Ra->new($url);2165my$uuid=$ra->get_uuid;2166my$head=$ra->get_latest_revnum;21672168# ignore errors, $head revision may not even exist anymore2169eval{$ra->get_log("",$head,0,1,0,1,sub{$head=$_[1] }) };2170warn"W:$@\n"if$@;21712172my$base=defined$fetch?$head:0;21732174# read the max revs for wildcard expansion (branches/*, tags/*)2175foreachmy$t(qw/branches tags/) {2176defined$remote->{$t}ornext;2177push@globs, @{$remote->{$t}};21782179my$max_rev=eval{ tmp_config(qw/--int --get/,2180"svn-remote.$repo_id.${t}-maxRev") };2181if(defined$max_rev&& ($max_rev<$base)) {2182$base=$max_rev;2183}elsif(!defined$max_rev) {2184$base=0;2185}2186}21872188if($fetch) {2189foreachmy$p(sort keys%$fetch) {2190my$gs= Git::SVN->new($fetch->{$p},$repo_id,$p);2191my$lr=$gs->rev_map_max;2192if(defined$lr) {2193$base=$lrif($lr<$base);2194}2195push@gs,$gs;2196}2197}21982199($base,$head) = parse_revision_argument($base,$head);2200$ra->gs_fetch_loop_common($base,$head, \@gs, \@globs);2201}22022203sub read_all_remotes {2204my$r= {};2205my$use_svm_props=eval{ command_oneline(qw/config --bool2206 svn.useSvmProps/) };2207$use_svm_props=$use_svm_propseq'true'if$use_svm_props;2208my$svn_refspec=qr{\s*(.*?)\s*:\s*(.+?)\s*};2209foreach(grep{s/^svn-remote\.//} command(qw/config -l/)) {2210if(m!^(.+)\.fetch=$svn_refspec$!) {2211my($remote,$local_ref,$remote_ref) = ($1,$2,$3);2212die("svn-remote.$remote: remote ref '$remote_ref' "2213."must start with 'refs/'\n")2214unless$remote_ref=~m{^refs/};2215$local_ref= uri_decode($local_ref);2216$r->{$remote}->{fetch}->{$local_ref} =$remote_ref;2217$r->{$remote}->{svm} = {}if$use_svm_props;2218}elsif(m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {2219$r->{$1}->{svm} = {};2220}elsif(m!^(.+)\.url=\s*(.*)\s*$!) {2221$r->{$1}->{url} =$2;2222}elsif(m!^(.+)\.pushurl=\s*(.*)\s*$!) {2223$r->{$1}->{pushurl} =$2;2224}elsif(m!^(.+)\.ignore-refs=\s*(.*)\s*$!) {2225$r->{$1}->{ignore_refs_regex} =$2;2226}elsif(m!^(.+)\.(branches|tags)=$svn_refspec$!) {2227my($remote,$t,$local_ref,$remote_ref) =2228($1,$2,$3,$4);2229die("svn-remote.$remote: remote ref '$remote_ref' ($t) "2230."must start with 'refs/'\n")2231unless$remote_ref=~m{^refs/};2232$local_ref= uri_decode($local_ref);2233my$rs= {2234 t =>$t,2235 remote =>$remote,2236 path => Git::SVN::GlobSpec->new($local_ref,1),2237ref=> Git::SVN::GlobSpec->new($remote_ref,0) };2238if(length($rs->{ref}->{right}) !=0) {2239die"The '*' glob character must be the last ",2240"character of '$remote_ref'\n";2241}2242push@{$r->{$remote}->{$t} },$rs;2243}2244}22452246map{2247if(defined$r->{$_}->{svm}) {2248my$svm;2249eval{2250my$section="svn-remote.$_";2251$svm= {2252 source => tmp_config('--get',2253"$section.svm-source"),2254 replace => tmp_config('--get',2255"$section.svm-replace"),2256}2257};2258$r->{$_}->{svm} =$svm;2259}2260}keys%$r;22612262foreachmy$remote(keys%$r) {2263foreach(grep{defined$_}2264map{$r->{$remote}->{$_} }qw(branches tags)) {2265foreachmy$rs(@$_) {2266$rs->{ignore_refs_regex} =2267$r->{$remote}->{ignore_refs_regex};2268}2269}2270}22712272$r;2273}22742275sub init_vars {2276$_gc_nr=$_gc_period=1000;2277if(defined$_repack||defined$_repack_flags) {2278warn"Repack options are obsolete; they have no effect.\n";2279}2280}22812282sub verify_remotes_sanity {2283return unless-d $ENV{GIT_DIR};2284my%seen;2285foreach(command(qw/config -l/)) {2286if(m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {2287if($seen{$1}) {2288die"Remote ref refs/remote/$1is tracked by",2289"\n \"$_\"\nand\n \"$seen{$1}\"\n",2290"Please resolve this ambiguity in ",2291"your git configuration file before ",2292"continuing\n";2293}2294$seen{$1} =$_;2295}2296}2297}22982299sub find_existing_remote {2300my($url,$remotes) =@_;2301returnundefif$no_reuse_existing;2302my$existing;2303foreachmy$repo_id(keys%$remotes) {2304my$u=$remotes->{$repo_id}->{url}ornext;2305next if$une$url;2306$existing=$repo_id;2307last;2308}2309$existing;2310}23112312sub init_remote_config {2313my($self,$url,$no_write) =@_;2314$url=~s!/+$!!;# strip trailing slash2315my$r= read_all_remotes();2316my$existing= find_existing_remote($url,$r);2317if($existing) {2318unless($no_write) {2319print STDERR "Using existing ",2320"[svn-remote\"$existing\"]\n";2321}2322$self->{repo_id} =$existing;2323}elsif($_minimize_url) {2324my$min_url= Git::SVN::Ra->new($url)->minimize_url;2325$existing= find_existing_remote($min_url,$r);2326if($existing) {2327unless($no_write) {2328print STDERR "Using existing ",2329"[svn-remote\"$existing\"]\n";2330}2331$self->{repo_id} =$existing;2332}2333if($min_urlne$url) {2334unless($no_write) {2335print STDERR "Using higher level of URL: ",2336"$url=>$min_url\n";2337}2338my$old_path=$self->{path};2339$self->{path} =$url;2340$self->{path} =~s!^\Q$min_url\E(/|$)!!;2341if(length$old_path) {2342$self->{path} .="/$old_path";2343}2344$url=$min_url;2345}2346}2347my$orig_url;2348if(!$existing) {2349# verify that we aren't overwriting anything:2350$orig_url=eval{2351 command_oneline('config','--get',2352"svn-remote.$self->{repo_id}.url")2353};2354if($orig_url&& ($orig_urlne$url)) {2355die"svn-remote.$self->{repo_id}.url already set: ",2356"$orig_url\nwanted to set to:$url\n";2357}2358}2359my($xrepo_id,$xpath) = find_ref($self->refname);2360if(!$no_write&&defined$xpath) {2361die"svn-remote.$xrepo_id.fetch already set to track ",2362"$xpath:",$self->refname,"\n";2363}2364unless($no_write) {2365 command_noisy('config',2366"svn-remote.$self->{repo_id}.url",$url);2367$self->{path} =~s{^/}{};2368$self->{path} =~s{%([0-9A-F]{2})}{chr hex($1)}ieg;2369 command_noisy('config','--add',2370"svn-remote.$self->{repo_id}.fetch",2371"$self->{path}:".$self->refname);2372}2373$self->{url} =$url;2374}23752376sub find_by_url {# repos_root and, path are optional2377my($class,$full_url,$repos_root,$path) =@_;23782379returnundefunlessdefined$full_url;2380 remove_username($full_url);2381 remove_username($repos_root)ifdefined$repos_root;2382my$remotes= read_all_remotes();2383if(defined$full_url&&defined$repos_root&& !defined$path) {2384$path=$full_url;2385$path=~ s#^\Q$repos_root\E(?:/|$)##;2386}2387foreachmy$repo_id(keys%$remotes) {2388my$u=$remotes->{$repo_id}->{url}ornext;2389 remove_username($u);2390next ifdefined$repos_root&&$repos_rootne$u;23912392my$fetch=$remotes->{$repo_id}->{fetch} || {};2393foreachmy$t(qw/branches tags/) {2394foreachmy$globspec(@{$remotes->{$repo_id}->{$t}}) {2395 resolve_local_globs($u,$fetch,$globspec);2396}2397}2398my$p=$path;2399my$rwr= rewrite_root({repo_id =>$repo_id});2400my$svm=$remotes->{$repo_id}->{svm}2401ifdefined$remotes->{$repo_id}->{svm};2402unless(defined$p) {2403$p=$full_url;2404my$z=$u;2405my$prefix='';2406if($rwr) {2407$z=$rwr;2408 remove_username($z);2409}elsif(defined$svm) {2410$z=$svm->{source};2411$prefix=$svm->{replace};2412$prefix=~ s#^\Q$u\E(?:/|$)##;2413$prefix=~ s#/$##;2414}2415$p=~ s#^\Q$z\E(?:/|$)#$prefix# or next;2416}2417foreachmy$f(keys%$fetch) {2418next if$fne$p;2419return Git::SVN->new($fetch->{$f},$repo_id,$f);2420}2421}2422undef;2423}24242425sub init {2426my($class,$url,$path,$repo_id,$ref_id,$no_write) =@_;2427my$self= _new($class,$repo_id,$ref_id,$path);2428if(defined$url) {2429$self->init_remote_config($url,$no_write);2430}2431$self;2432}24332434sub find_ref {2435my($ref_id) =@_;2436foreach(command(qw/config -l/)) {2437next unless m!^svn-remote\.(.+)\.fetch=2438 \s*(.*?)\s*:\s*(.+?)\s*$!x;2439my($repo_id,$path,$ref) = ($1,$2,$3);2440if($refeq$ref_id) {2441$path=''if($path=~ m#^\./?#);2442return($repo_id,$path);2443}2444}2445(undef,undef,undef);2446}24472448sub new {2449my($class,$ref_id,$repo_id,$path) =@_;2450if(defined$ref_id&& !defined$repo_id&& !defined$path) {2451($repo_id,$path) = find_ref($ref_id);2452if(!defined$repo_id) {2453die"Could not find a\"svn-remote.*.fetch\"key ",2454"in the repository configuration matching: ",2455"$ref_id\n";2456}2457}2458my$self= _new($class,$repo_id,$ref_id,$path);2459if(!defined$self->{path} || !length$self->{path}) {2460my$fetch= command_oneline('config','--get',2461"svn-remote.$repo_id.fetch",2462":$ref_id\$")or2463die"Failed to read\"svn-remote.$repo_id.fetch\"",2464"\":$ref_id\$\"in config\n";2465($self->{path},undef) =split(/\s*:\s*/,$fetch);2466}2467$self->{path} =~s{/+}{/}g;2468$self->{path} =~s{\A/}{};2469$self->{path} =~s{/\z}{};2470$self->{url} = command_oneline('config','--get',2471"svn-remote.$repo_id.url")or2472die"Failed to read\"svn-remote.$repo_id.url\"in config\n";2473$self->{pushurl} =eval{ command_oneline('config','--get',2474"svn-remote.$repo_id.pushurl") };2475$self->rebuild;2476$self;2477}24782479sub refname {2480my($refname) =$_[0]->{ref_id} ;24812482# It cannot end with a slash /, we'll throw up on this because2483# SVN can't have directories with a slash in their name, either:2484if($refname=~m{/$}) {2485die"ref: '$refname' ends with a trailing slash, this is ",2486"not permitted by git nor Subversion\n";2487}24882489# It cannot have ASCII control character space, tilde ~, caret ^,2490# colon :, question-mark ?, asterisk *, space, or open bracket [2491# anywhere.2492#2493# Additionally, % must be escaped because it is used for escaping2494# and we want our escaped refname to be reversible2495$refname=~s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;24962497# no slash-separated component can begin with a dot .2498# /.* becomes /%2E*2499$refname=~s{/\.}{/%2E}g;25002501# It cannot have two consecutive dots .. anywhere2502# .. becomes %2E%2E2503$refname=~s{\.\.}{%2E%2E}g;25042505# trailing dots and .lock are not allowed2506# .$ becomes %2E and .lock becomes %2Elock2507$refname=~s{\.(?=$|lock$)}{%2E};25082509# the sequence @{ is used to access the reflog2510# @{ becomes %40{2511$refname=~s{\@\{}{%40\{}g;25122513return$refname;2514}25152516sub desanitize_refname {2517my($refname) =@_;2518$refname=~s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;2519return$refname;2520}25212522sub svm_uuid {2523my($self) =@_;2524return$self->{svm}->{uuid}if$self->svm;2525$self->ra;2526unless($self->{svm}) {2527die"SVM UUID not cached, and reading remotely failed\n";2528}2529$self->{svm}->{uuid};2530}25312532sub svm {2533my($self) =@_;2534return$self->{svm}if$self->{svm};2535my$svm;2536# see if we have it in our config, first:2537eval{2538my$section="svn-remote.$self->{repo_id}";2539$svm= {2540 source => tmp_config('--get',"$section.svm-source"),2541 uuid => tmp_config('--get',"$section.svm-uuid"),2542 replace => tmp_config('--get',"$section.svm-replace"),2543}2544};2545if($svm&&$svm->{source} &&$svm->{uuid} &&$svm->{replace}) {2546$self->{svm} =$svm;2547}2548$self->{svm};2549}25502551sub _set_svm_vars {2552my($self,$ra) =@_;2553return$raif$self->svm;25542555my@err= ("useSvmProps set, but failed to read SVM properties\n",2556"(svm:source, svm:uuid) ",2557"from the following URLs:\n");2558sub read_svm_props {2559my($self,$ra,$path,$r) =@_;2560my$props= ($ra->get_dir($path,$r))[2];2561my$src=$props->{'svm:source'};2562my$uuid=$props->{'svm:uuid'};2563returnundefif(!$src|| !$uuid);25642565chomp($src,$uuid);25662567$uuid=~m{^[0-9a-f\-]{30,}$}i2568or die"doesn't look right - svm:uuid is '$uuid'\n";25692570# the '!' is used to mark the repos_root!/relative/path2571$src=~s{/?!/?}{/};2572$src=~s{/+$}{};# no trailing slashes please2573# username is of no interest2574$src=~s{(^[a-z\+]*://)[^/@]*@}{$1};25752576my$replace=$ra->{url};2577$replace.="/$path"iflength$path;25782579my$section="svn-remote.$self->{repo_id}";2580 tmp_config("$section.svm-source",$src);2581 tmp_config("$section.svm-replace",$replace);2582 tmp_config("$section.svm-uuid",$uuid);2583$self->{svm} = {2584 source =>$src,2585 uuid =>$uuid,2586 replace =>$replace2587};2588}25892590my$r=$ra->get_latest_revnum;2591my$path=$self->{path};2592my%tried;2593while(length$path) {2594unless($tried{"$self->{url}/$path"}) {2595return$raif$self->read_svm_props($ra,$path,$r);2596$tried{"$self->{url}/$path"} =1;2597}2598$path=~ s#/?[^/]+$##;2599}2600die"Path: '$path' should be ''\n"if$pathne'';2601return$raif$self->read_svm_props($ra,$path,$r);2602$tried{"$self->{url}/$path"} =1;26032604if($ra->{repos_root}eq$self->{url}) {2605die@err, (map{"$_\n"}keys%tried),"\n";2606}26072608# nope, make sure we're connected to the repository root:2609my$ok;2610my@tried_b;2611$path=$ra->{svn_path};2612$ra= Git::SVN::Ra->new($ra->{repos_root});2613while(length$path) {2614unless($tried{"$ra->{url}/$path"}) {2615$ok=$self->read_svm_props($ra,$path,$r);2616last if$ok;2617$tried{"$ra->{url}/$path"} =1;2618}2619$path=~ s#/?[^/]+$##;2620}2621die"Path: '$path' should be ''\n"if$pathne'';2622$ok||=$self->read_svm_props($ra,$path,$r);2623$tried{"$ra->{url}/$path"} =1;2624if(!$ok) {2625die@err, (map{"$_\n"}keys%tried),"\n";2626}2627 Git::SVN::Ra->new($self->{url});2628}26292630sub svnsync {2631my($self) =@_;2632return$self->{svnsync}if$self->{svnsync};26332634if($self->no_metadata) {2635die"Can't have both 'noMetadata' and ",2636"'useSvnsyncProps' options set!\n";2637}2638if($self->rewrite_root) {2639die"Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",2640"options set!\n";2641}2642if($self->rewrite_uuid) {2643die"Can't have both 'useSvnsyncProps' and 'rewriteUUID' ",2644"options set!\n";2645}26462647my$svnsync;2648# see if we have it in our config, first:2649eval{2650my$section="svn-remote.$self->{repo_id}";26512652my$url= tmp_config('--get',"$section.svnsync-url");2653($url) = ($url=~m{^([a-z\+]+://\S+)$})or2654die"doesn't look right - svn:sync-from-url is '$url'\n";26552656my$uuid= tmp_config('--get',"$section.svnsync-uuid");2657($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2658die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26592660$svnsync= { url =>$url, uuid =>$uuid}2661};2662if($svnsync&&$svnsync->{url} &&$svnsync->{uuid}) {2663return$self->{svnsync} =$svnsync;2664}26652666my$err="useSvnsyncProps set, but failed to read ".2667"svnsync property: svn:sync-from-";2668my$rp=$self->ra->rev_proplist(0);26692670my$url=$rp->{'svn:sync-from-url'}or die$err."url\n";2671($url) = ($url=~m{^([a-z\+]+://\S+)$})or2672die"doesn't look right - svn:sync-from-url is '$url'\n";26732674my$uuid=$rp->{'svn:sync-from-uuid'}or die$err."uuid\n";2675($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2676die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26772678my$section="svn-remote.$self->{repo_id}";2679 tmp_config('--add',"$section.svnsync-uuid",$uuid);2680 tmp_config('--add',"$section.svnsync-url",$url);2681return$self->{svnsync} = { url =>$url, uuid =>$uuid};2682}26832684# this allows us to memoize our SVN::Ra UUID locally and avoid a2685# remote lookup (useful for 'git svn log').2686sub ra_uuid {2687my($self) =@_;2688unless($self->{ra_uuid}) {2689my$key="svn-remote.$self->{repo_id}.uuid";2690my$uuid=eval{ tmp_config('--get',$key) };2691if(!$@&&$uuid&&$uuid=~/^([a-f\d\-]{30,})$/i) {2692$self->{ra_uuid} =$uuid;2693}else{2694die"ra_uuid called without URL\n"unless$self->{url};2695$self->{ra_uuid} =$self->ra->get_uuid;2696 tmp_config('--add',$key,$self->{ra_uuid});2697}2698}2699$self->{ra_uuid};2700}27012702sub _set_repos_root {2703my($self,$repos_root) =@_;2704my$k="svn-remote.$self->{repo_id}.reposRoot";2705$repos_root||=$self->ra->{repos_root};2706 tmp_config($k,$repos_root);2707$repos_root;2708}27092710sub repos_root {2711my($self) =@_;2712my$k="svn-remote.$self->{repo_id}.reposRoot";2713eval{ tmp_config('--get',$k) } ||$self->_set_repos_root;2714}27152716sub ra {2717my($self) =shift;2718my$ra= Git::SVN::Ra->new($self->{url});2719$self->_set_repos_root($ra->{repos_root});2720if($self->use_svm_props&& !$self->{svm}) {2721if($self->no_metadata) {2722die"Can't have both 'noMetadata' and ",2723"'useSvmProps' options set!\n";2724}elsif($self->use_svnsync_props) {2725die"Can't have both 'useSvnsyncProps' and ",2726"'useSvmProps' options set!\n";2727}2728$ra=$self->_set_svm_vars($ra);2729$self->{-want_revprops} =1;2730}2731$ra;2732}27332734# prop_walk(PATH, REV, SUB)2735# -------------------------2736# Recursively traverse PATH at revision REV and invoke SUB for each2737# directory that contains a SVN property. SUB will be invoked as2738# follows: &SUB(gs, path, props); where `gs' is this instance of2739# Git::SVN, `path' the path to the directory where the properties2740# `props' were found. The `path' will be relative to point of checkout,2741# that is, if url://repo/trunk is the current Git branch, and that2742# directory contains a sub-directory `d', SUB will be invoked with `/d/'2743# as `path' (note the trailing `/').2744sub prop_walk {2745my($self,$path,$rev,$sub) =@_;27462747$path=~ s#^/##;2748my($dirent,undef,$props) =$self->ra->get_dir($path,$rev);2749$path=~ s#^/*#/#g;2750my$p=$path;2751# Strip the irrelevant part of the path.2752$p=~ s#^/+\Q$self->{path}\E(/|$)#/#;2753# Ensure the path is terminated by a `/'.2754$p=~ s#/*$#/#;27552756# The properties contain all the internal SVN stuff nobody2757# (usually) cares about.2758my$interesting_props=0;2759foreach(keys%{$props}) {2760# If it doesn't start with `svn:', it must be a2761# user-defined property.2762++$interesting_propsandnext if$_!~/^svn:/;2763# FIXME: Fragile, if SVN adds new public properties,2764# this needs to be updated.2765++$interesting_propsif/^svn:(?:ignore|keywords|executable2766|eol-style|mime-type2767|externals|needs-lock)$/x;2768}2769&$sub($self,$p,$props)if$interesting_props;27702771foreach(sort keys%$dirent) {2772next if$dirent->{$_}->{kind} !=$SVN::Node::dir;2773$self->prop_walk($self->{path} .$p.$_,$rev,$sub);2774}2775}27762777sub last_rev { ($_[0]->last_rev_commit)[0] }2778sub last_commit { ($_[0]->last_rev_commit)[1] }27792780# returns the newest SVN revision number and newest commit SHA12781sub last_rev_commit {2782my($self) =@_;2783if(defined$self->{last_rev} &&defined$self->{last_commit}) {2784return($self->{last_rev},$self->{last_commit});2785}2786my$c= ::verify_ref($self->refname.'^0');2787if($c&& !$self->use_svm_props&& !$self->no_metadata) {2788my$rev= (::cmt_metadata($c))[1];2789if(defined$rev) {2790($self->{last_rev},$self->{last_commit}) = ($rev,$c);2791return($rev,$c);2792}2793}2794my$map_path=$self->map_path;2795unless(-e $map_path) {2796($self->{last_rev},$self->{last_commit}) = (undef,undef);2797return(undef,undef);2798}2799my($rev,$commit) =$self->rev_map_max(1);2800($self->{last_rev},$self->{last_commit}) = ($rev,$commit);2801return($rev,$commit);2802}28032804sub get_fetch_range {2805my($self,$min,$max) =@_;2806$max||=$self->ra->get_latest_revnum;2807$min||=$self->rev_map_max;2808(++$min,$max);2809}28102811sub tmp_config {2812my(@args) =@_;2813my$old_def_config="$ENV{GIT_DIR}/svn/config";2814my$config="$ENV{GIT_DIR}/svn/.metadata";2815if(! -f $config&& -f $old_def_config) {2816rename$old_def_config,$configor2817die"Failed rename$old_def_config=>$config:$!\n";2818}2819my$old_config=$ENV{GIT_CONFIG};2820$ENV{GIT_CONFIG} =$config;2821$@=undef;2822my@ret=eval{2823unless(-f $config) {2824 mkfile($config);2825open my$fh,'>',$configor2826die"Can't open$config:$!\n";2827print$fh"; This file is used internally by ",2828"git-svn\n"or die2829"Couldn't write to$config:$!\n";2830print$fh"; You should not have to edit it\n"or2831die"Couldn't write to$config:$!\n";2832close$fhor die"Couldn't close$config:$!\n";2833}2834 command('config',@args);2835};2836my$err=$@;2837if(defined$old_config) {2838$ENV{GIT_CONFIG} =$old_config;2839}else{2840delete$ENV{GIT_CONFIG};2841}2842die$errif$err;2843wantarray?@ret:$ret[0];2844}28452846sub tmp_index_do {2847my($self,$sub) =@_;2848my$old_index=$ENV{GIT_INDEX_FILE};2849$ENV{GIT_INDEX_FILE} =$self->{index};2850$@=undef;2851my@ret=eval{2852my($dir,$base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);2853 mkpath([$dir])unless-d $dir;2854&$sub;2855};2856my$err=$@;2857if(defined$old_index) {2858$ENV{GIT_INDEX_FILE} =$old_index;2859}else{2860delete$ENV{GIT_INDEX_FILE};2861}2862die$errif$err;2863wantarray?@ret:$ret[0];2864}28652866sub assert_index_clean {2867my($self,$treeish) =@_;28682869$self->tmp_index_do(sub{2870 command_noisy('read-tree',$treeish)unless-e $self->{index};2871my$x= command_oneline('write-tree');2872my($y) = (command(qw/cat-file commit/,$treeish) =~2873/^tree ($::sha1)/mo);2874return if$yeq$x;28752876warn"Index mismatch:$y!=$x\nrereading$treeish\n";2877unlink$self->{index}or die"unlink$self->{index}:$!\n";2878 command_noisy('read-tree',$treeish);2879$x= command_oneline('write-tree');2880if($yne$x) {2881::fatal "trees ($treeish)$y!=$x\n",2882"Something is seriously wrong...";2883}2884});2885}28862887sub get_commit_parents {2888my($self,$log_entry) =@_;2889my(%seen,@ret,@tmp);2890# legacy support for 'set-tree'; this is only used by set_tree_cb:2891if(my$ip=$self->{inject_parents}) {2892if(my$commit=delete$ip->{$log_entry->{revision}}) {2893push@tmp,$commit;2894}2895}2896if(my$cur= ::verify_ref($self->refname.'^0')) {2897push@tmp,$cur;2898}2899if(my$ipd=$self->{inject_parents_dcommit}) {2900if(my$commit=delete$ipd->{$log_entry->{revision}}) {2901push@tmp,@$commit;2902}2903}2904push@tmp,$_foreach(@{$log_entry->{parents}},@tmp);2905while(my$p=shift@tmp) {2906next if$seen{$p};2907$seen{$p} =1;2908push@ret,$p;2909}2910@ret;2911}29122913sub rewrite_root {2914my($self) =@_;2915return$self->{-rewrite_root}ifexists$self->{-rewrite_root};2916my$k="svn-remote.$self->{repo_id}.rewriteRoot";2917my$rwr=eval{ command_oneline(qw/config --get/,$k) };2918if($rwr) {2919$rwr=~ s#/+$##;2920if($rwr!~ m#^[a-z\+]+://#) {2921die"$rwris not a valid URL (key:$k)\n";2922}2923}2924$self->{-rewrite_root} =$rwr;2925}29262927sub rewrite_uuid {2928my($self) =@_;2929return$self->{-rewrite_uuid}ifexists$self->{-rewrite_uuid};2930my$k="svn-remote.$self->{repo_id}.rewriteUUID";2931my$rwid=eval{ command_oneline(qw/config --get/,$k) };2932if($rwid) {2933$rwid=~ s#/+$##;2934if($rwid!~ m#^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$#) {2935die"$rwidis not a valid UUID (key:$k)\n";2936}2937}2938$self->{-rewrite_uuid} =$rwid;2939}29402941sub metadata_url {2942my($self) =@_;2943($self->rewrite_root||$self->{url}) .2944(length$self->{path} ?'/'.$self->{path} :'');2945}29462947sub full_url {2948my($self) =@_;2949$self->{url} . (length$self->{path} ?'/'.$self->{path} :'');2950}29512952sub full_pushurl {2953my($self) =@_;2954if($self->{pushurl}) {2955return$self->{pushurl} . (length$self->{path} ?'/'.2956$self->{path} :'');2957}else{2958return$self->full_url;2959}2960}29612962sub set_commit_header_env {2963my($log_entry) =@_;2964my%env;2965foreachmy$ned(qw/NAME EMAIL DATE/) {2966foreachmy$ac(qw/AUTHOR COMMITTER/) {2967$env{"GIT_${ac}_${ned}"} =$ENV{"GIT_${ac}_${ned}"};2968}2969}29702971$ENV{GIT_AUTHOR_NAME} =$log_entry->{name};2972$ENV{GIT_AUTHOR_EMAIL} =$log_entry->{email};2973$ENV{GIT_AUTHOR_DATE} =$ENV{GIT_COMMITTER_DATE} =$log_entry->{date};29742975$ENV{GIT_COMMITTER_NAME} = (defined$log_entry->{commit_name})2976?$log_entry->{commit_name}2977:$log_entry->{name};2978$ENV{GIT_COMMITTER_EMAIL} = (defined$log_entry->{commit_email})2979?$log_entry->{commit_email}2980:$log_entry->{email};2981 \%env;2982}29832984sub restore_commit_header_env {2985my($env) =@_;2986foreachmy$ned(qw/NAME EMAIL DATE/) {2987foreachmy$ac(qw/AUTHOR COMMITTER/) {2988my$k="GIT_${ac}_${ned}";2989if(defined$env->{$k}) {2990$ENV{$k} =$env->{$k};2991}else{2992delete$ENV{$k};2993}2994}2995}2996}29972998sub gc {2999 command_noisy('gc','--auto');3000};30013002sub do_git_commit {3003my($self,$log_entry) =@_;3004my$lr=$self->last_rev;3005if(defined$lr&&$lr>=$log_entry->{revision}) {3006die"Last fetched revision of ",$self->refname,3007" was r$lr, but we are about to fetch: ",3008"r$log_entry->{revision}!\n";3009}3010if(my$c=$self->rev_map_get($log_entry->{revision})) {3011 croak "$log_entry->{revision} =$calready exists! ",3012"Why are we refetching it?\n";3013}3014my$old_env= set_commit_header_env($log_entry);3015my$tree=$log_entry->{tree};3016if(!defined$tree) {3017$tree=$self->tmp_index_do(sub{3018 command_oneline('write-tree') });3019}3020die"Tree is not a valid sha1:$tree\n"if$tree!~/^$::sha1$/o;30213022my@exec= ('git','commit-tree',$tree);3023foreach($self->get_commit_parents($log_entry)) {3024push@exec,'-p',$_;3025}3026defined(my$pid= open3(my$msg_fh,my$out_fh,'>&STDERR',@exec))3027or croak $!;3028binmode$msg_fh;30293030# we always get UTF-8 from SVN, but we may want our commits in3031# a different encoding.3032if(my$enc= Git::config('i18n.commitencoding')) {3033require Encode;3034 Encode::from_to($log_entry->{log},'UTF-8',$enc);3035}3036print$msg_fh $log_entry->{log}or croak $!;3037 restore_commit_header_env($old_env);3038unless($self->no_metadata) {3039print$msg_fh"\ngit-svn-id:$log_entry->{metadata}\n"3040or croak $!;3041}3042$msg_fh->flush==0or croak $!;3043close$msg_fhor croak $!;3044chomp(my$commit=do{local$/; <$out_fh> });3045close$out_fhor croak $!;3046waitpid$pid,0;3047 croak $?if$?;3048if($commit!~/^$::sha1$/o) {3049die"Failed to commit, invalid sha1:$commit\n";3050}30513052$self->rev_map_set($log_entry->{revision},$commit,1);30533054$self->{last_rev} =$log_entry->{revision};3055$self->{last_commit} =$commit;3056print"r$log_entry->{revision}"unless$::_q >1;3057if(defined$log_entry->{svm_revision}) {3058print" (\@$log_entry->{svm_revision})"unless$::_q >1;3059$self->rev_map_set($log_entry->{svm_revision},$commit,30600,$self->svm_uuid);3061}3062print" =$commit($self->{ref_id})\n"unless$::_q >1;3063if(--$_gc_nr==0) {3064$_gc_nr=$_gc_period;3065 gc();3066}3067return$commit;3068}30693070sub match_paths {3071my($self,$paths,$r) =@_;3072return1if$self->{path}eq'';3073if(my$path=$paths->{"/$self->{path}"}) {3074return($path->{action}eq'D') ?0:1;3075}3076$self->{path_regex} ||=qr/^\/\Q$self->{path}\E\//;3077if(grep/$self->{path_regex}/,keys%$paths) {3078return1;3079}3080my$c='';3081foreach(split m#/#, $self->{path}) {3082$c.="/$_";3083next unless($paths->{$c} &&3084($paths->{$c}->{action} =~/^[AR]$/));3085if($self->ra->check_path($self->{path},$r) ==3086$SVN::Node::dir) {3087return1;3088}3089}3090return0;3091}30923093sub find_parent_branch {3094my($self,$paths,$rev) =@_;3095returnundefunless$self->follow_parent;3096unless(defined$paths) {3097my$err_handler=$SVN::Error::handler;3098$SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;3099$self->ra->get_log([$self->{path}],$rev,$rev,0,1,1,3100sub{$paths=$_[0] });3101$SVN::Error::handler =$err_handler;3102}3103returnundefunlessdefined$paths;31043105# look for a parent from another branch:3106my@b_path_components=split m#/#, $self->{path};3107my@a_path_components;3108my$i;3109while(@b_path_components) {3110$i=$paths->{'/'.join('/',@b_path_components)};3111last if$i&&defined$i->{copyfrom_path};3112unshift(@a_path_components,pop(@b_path_components));3113}3114returnundefunlessdefined$i&&defined$i->{copyfrom_path};3115my$branch_from=$i->{copyfrom_path};3116if(@a_path_components) {3117print STDERR "branch_from:$branch_from=> ";3118$branch_from.='/'.join('/',@a_path_components);3119print STDERR $branch_from,"\n";3120}3121my$r=$i->{copyfrom_rev};3122my$repos_root=$self->ra->{repos_root};3123my$url=$self->ra->{url};3124my$new_url=$url.$branch_from;3125print STDERR "Found possible branch point: ",3126"$new_url=> ",$self->full_url,",$r\n"3127unless$::_q >1;3128$branch_from=~ s#^/##;3129my$gs=$self->other_gs($new_url,$url,3130$branch_from,$r,$self->{ref_id});3131my($r0,$parent) =$gs->find_rev_before($r,1);3132{3133my($base,$head);3134if(!defined$r0|| !defined$parent) {3135($base,$head) = parse_revision_argument(0,$r);3136}else{3137if($r0<$r) {3138$gs->ra->get_log([$gs->{path}],$r0+1,$r,1,31390,1,sub{$base=$_[1] -1});3140}3141}3142if(defined$base&&$base<=$r) {3143$gs->fetch($base,$r);3144}3145($r0,$parent) =$gs->find_rev_before($r,1);3146}3147if(defined$r0&&defined$parent) {3148print STDERR "Found branch parent: ($self->{ref_id})$parent\n"3149unless$::_q >1;3150my$ed;3151if($self->ra->can_do_switch) {3152$self->assert_index_clean($parent);3153print STDERR "Following parent with do_switch\n"3154unless$::_q >1;3155# do_switch works with svn/trunk >= r22312, but that3156# is not included with SVN 1.4.3 (the latest version3157# at the moment), so we can't rely on it3158$self->{last_rev} =$r0;3159$self->{last_commit} =$parent;3160$ed= Git::SVN::Fetcher->new($self,$gs->{path});3161$gs->ra->gs_do_switch($r0,$rev,$gs,3162$self->full_url,$ed)3163or die"SVN connection failed somewhere...\n";3164}elsif($self->ra->trees_match($new_url,$r0,3165$self->full_url,$rev)) {3166print STDERR "Trees match:\n",3167"$new_url\@$r0\n",3168" ${\$self->full_url}\@$rev\n",3169"Following parent with no changes\n"3170unless$::_q >1;3171$self->tmp_index_do(sub{3172 command_noisy('read-tree',$parent);3173});3174$self->{last_commit} =$parent;3175}else{3176print STDERR "Following parent with do_update\n"3177unless$::_q >1;3178$ed= Git::SVN::Fetcher->new($self);3179$self->ra->gs_do_update($rev,$rev,$self,$ed)3180or die"SVN connection failed somewhere...\n";3181}3182print STDERR "Successfully followed parent\n"unless$::_q >1;3183return$self->make_log_entry($rev, [$parent],$ed);3184}3185returnundef;3186}31873188sub do_fetch {3189my($self,$paths,$rev) =@_;3190my$ed;3191my($last_rev,@parents);3192if(my$lc=$self->last_commit) {3193# we can have a branch that was deleted, then re-added3194# under the same name but copied from another path, in3195# which case we'll have multiple parents (we don't3196# want to break the original ref, nor lose copypath info):3197if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3198push@{$log_entry->{parents}},$lc;3199return$log_entry;3200}3201$ed= Git::SVN::Fetcher->new($self);3202$last_rev=$self->{last_rev};3203$ed->{c} =$lc;3204@parents= ($lc);3205}else{3206$last_rev=$rev;3207if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3208return$log_entry;3209}3210$ed= Git::SVN::Fetcher->new($self);3211}3212unless($self->ra->gs_do_update($last_rev,$rev,$self,$ed)) {3213die"SVN connection failed somewhere...\n";3214}3215$self->make_log_entry($rev, \@parents,$ed);3216}32173218sub mkemptydirs {3219my($self,$r) =@_;32203221sub scan {3222my($r,$empty_dirs,$line) =@_;3223if(defined$r&&$line=~/^r(\d+)$/) {3224return0if$1>$r;3225}elsif($line=~/^ \+empty_dir: (.+)$/) {3226$empty_dirs->{$1} =1;3227}elsif($line=~/^ \-empty_dir: (.+)$/) {3228my@d=grep{m[^\Q$1\E(/|$)]} (keys%$empty_dirs);3229delete@$empty_dirs{@d};3230}32311;# continue3232};32333234my%empty_dirs= ();3235my$gz_file="$self->{dir}/unhandled.log.gz";3236if(-f $gz_file) {3237if(!$can_compress) {3238warn"Compress::Zlib could not be found; ",3239"empty directories in$gz_filewill not be read\n";3240}else{3241my$gz= Compress::Zlib::gzopen($gz_file,"rb")or3242die"Unable to open$gz_file:$!\n";3243my$line;3244while($gz->gzreadline($line) >0) {3245 scan($r, \%empty_dirs,$line)orlast;3246}3247$gz->gzclose;3248}3249}32503251if(open my$fh,'<',"$self->{dir}/unhandled.log") {3252binmode$fhor croak "binmode:$!";3253while(<$fh>) {3254 scan($r, \%empty_dirs,$_)orlast;3255}3256close$fh;3257}32583259my$strip=qr/\A\Q$self->{path}\E(?:\/|$)/;3260foreachmy$d(sort keys%empty_dirs) {3261$d= uri_decode($d);3262$d=~s/$strip//;3263next unlesslength($d);3264next if-d $d;3265if(-e $d) {3266warn"$dexists but is not a directory\n";3267}else{3268print"creating empty directory:$d\n";3269 mkpath([$d]);3270}3271}3272}32733274sub get_untracked {3275my($self,$ed) =@_;3276my@out;3277my$h=$ed->{empty};3278foreach(sort keys%$h) {3279my$act=$h->{$_} ?'+empty_dir':'-empty_dir';3280push@out,"$act: ". uri_encode($_);3281warn"W:$act:$_\n";3282}3283foreachmy$t(qw/dir_prop file_prop/) {3284$h=$ed->{$t}ornext;3285foreachmy$path(sort keys%$h) {3286my$ppath=$patheq''?'.':$path;3287foreachmy$prop(sort keys%{$h->{$path}}) {3288next if$SKIP_PROP{$prop};3289my$v=$h->{$path}->{$prop};3290my$t_ppath_prop="$t: ".3291 uri_encode($ppath) .' '.3292 uri_encode($prop);3293if(defined$v) {3294push@out," +$t_ppath_prop".3295 uri_encode($v);3296}else{3297push@out," -$t_ppath_prop";3298}3299}3300}3301}3302foreachmy$t(qw/absent_file absent_directory/) {3303$h=$ed->{$t}ornext;3304foreachmy$parent(sort keys%$h) {3305foreachmy$path(sort@{$h->{$parent}}) {3306push@out,"$t: ".3307 uri_encode("$parent/$path");3308warn"W:$t:$parent/$path",3309"Insufficient permissions?\n";3310}3311}3312}3313 \@out;3314}33153316sub get_tz {3317# some systmes don't handle or mishandle %z, so be creative.3318my$t=shift||time;3319my$gm= timelocal(gmtime($t));3320my$sign=qw( + + - )[$t<=>$gm];3321returnsprintf("%s%02d%02d",$sign, (gmtime(abs($t-$gm)))[2,1]);3322}33233324# parse_svn_date(DATE)3325# --------------------3326# Given a date (in UTC) from Subversion, return a string in the format3327# "<TZ Offset> <local date/time>" that Git will use.3328#3329# By default the parsed date will be in UTC; if $Git::SVN::_localtime3330# is true we'll convert it to the local timezone instead.3331sub parse_svn_date {3332my$date=shift||return'+0000 1970-01-01 00:00:00';3333my($Y,$m,$d,$H,$M,$S) = ($date=~ /^(\d{4})\-(\d\d)\-(\d\d)T3334(\d\d)\:(\d\d)\:(\d\d)\.\d*Z$/x)or3335 croak "Unable to parse date:$date\n";3336my$parsed_date;# Set next.33373338if($Git::SVN::_localtime) {3339# Translate the Subversion datetime to an epoch time.3340# Begin by switching ourselves to $date's timezone, UTC.3341my$old_env_TZ=$ENV{TZ};3342$ENV{TZ} ='UTC';33433344my$epoch_in_UTC=3345 POSIX::strftime('%s',$S,$M,$H,$d,$m-1,$Y-1900);33463347# Determine our local timezone (including DST) at the3348# time of $epoch_in_UTC. $Git::SVN::Log::TZ stored the3349# value of TZ, if any, at the time we were run.3350if(defined$Git::SVN::Log::TZ) {3351$ENV{TZ} =$Git::SVN::Log::TZ;3352}else{3353delete$ENV{TZ};3354}33553356my$our_TZ= get_tz();33573358# This converts $epoch_in_UTC into our local timezone.3359my($sec,$min,$hour,$mday,$mon,$year,3360$wday,$yday,$isdst) =localtime($epoch_in_UTC);33613362$parsed_date=sprintf('%s%04d-%02d-%02d%02d:%02d:%02d',3363$our_TZ,$year+1900,$mon+1,3364$mday,$hour,$min,$sec);33653366# Reset us to the timezone in effect when we entered3367# this routine.3368if(defined$old_env_TZ) {3369$ENV{TZ} =$old_env_TZ;3370}else{3371delete$ENV{TZ};3372}3373}else{3374$parsed_date="+0000$Y-$m-$d$H:$M:$S";3375}33763377return$parsed_date;3378}33793380sub other_gs {3381my($self,$new_url,$url,3382$branch_from,$r,$old_ref_id) =@_;3383my$gs= Git::SVN->find_by_url($new_url,$url,$branch_from);3384unless($gs) {3385my$ref_id=$old_ref_id;3386$ref_id=~s/\@\d+-*$//;3387$ref_id.="\@$r";3388# just grow a tail if we're not unique enough :x3389$ref_id.='-'while find_ref($ref_id);3390my($u,$p,$repo_id) = ($new_url,'',$ref_id);3391if($u=~ s#^\Q$url\E(/|$)##) {3392$p=$u;3393$u=$url;3394$repo_id=$self->{repo_id};3395}3396while(1) {3397# It is possible to tag two different subdirectories at3398# the same revision. If the url for an existing ref3399# does not match, we must either find a ref with a3400# matching url or create a new ref by growing a tail.3401$gs= Git::SVN->init($u,$p,$repo_id,$ref_id,1);3402my(undef,$max_commit) =$gs->rev_map_max(1);3403last if(!$max_commit);3404my($url) = ::cmt_metadata($max_commit);3405last if($urleq$gs->metadata_url);3406$ref_id.='-';3407}3408print STDERR "Initializing parent:$ref_id\n"unless$::_q >1;3409}3410$gs3411}34123413sub call_authors_prog {3414my($orig_author) =@_;3415$orig_author= command_oneline('rev-parse','--sq-quote',$orig_author);3416my$author=`$::_authors_prog$orig_author`;3417 if ($?!= 0) {3418 die "$::_authors_prog failed with exit code$?\n"3419 }3420 if ($author=~ /^\s*(.+?)\s*<(.*)>\s*$/) {3421 my ($name,$email) = ($1,$2);3422$email= undef if length$2== 0;3423 return [$name,$email];3424 } else {3425 die "Author:$orig_author: $::_authors_prog returned "3426 . "invalid author format:$author\n";3427 }3428}34293430sub check_author {3431 my ($author) =@_;3432 if (!defined$author|| length$author== 0) {3433$author= '(no author)';3434 }3435 if (!defined $::users{$author}) {3436 if (defined $::_authors_prog) {3437 $::users{$author} = call_authors_prog($author);3438 } elsif (defined $::_authors) {3439 die "Author:$authornot defined in $::_authors file\n";3440 }3441 }3442$author;3443}34443445sub find_extra_svk_parents {3446 my ($self,$ed,$tickets,$parents) =@_;3447 # aha! svk:merge property changed...3448 my@tickets= split "\n",$tickets;3449 my@known_parents;3450 for my$ticket(@tickets) {3451 my ($uuid,$path,$rev) = split /:/,$ticket;3452 if ($uuideq$self->ra_uuid ) {3453 my$url=$self->{url};3454 my$repos_root=$url;3455 my$branch_from=$path;3456$branch_from=~ s{^/}{};3457 my$gs=$self->other_gs($repos_root."/".$branch_from,3458$url,3459$branch_from,3460$rev,3461$self->{ref_id});3462 if ( my$commit=$gs->rev_map_get($rev,$uuid) ) {3463 # wahey! we found it, but it might be3464 # an old one (!)3465 push@known_parents, [$rev,$commit];3466 }3467 }3468 }3469 # Ordering matters; highest-numbered commit merge tickets3470 # first, as they may account for later merge ticket additions3471 # or changes.3472@known_parents= map {$_->[1]} sort {$b->[0] <=>$a->[0]}@known_parents;3473 for my$parent(@known_parents) {3474 my@cmd= ('rev-list',$parent, map { "^$_" }@$parents);3475 my ($msg_fh,$ctx) = command_output_pipe(@cmd);3476 my$new;3477 while ( <$msg_fh> ) {3478$new=1;last;3479 }3480 command_close_pipe($msg_fh,$ctx);3481 if ($new) {3482 print STDERR3483 "Found merge parent (svk:merge ticket):$parent\n";3484 push@$parents,$parent;3485 }3486 }3487}34883489sub lookup_svn_merge {3490 my$uuid= shift;3491 my$url= shift;3492 my$merge= shift;34933494 my ($source,$revs) = split ":",$merge;3495 my$path=$source;3496$path=~ s{^/}{};3497 my$gs= Git::SVN->find_by_url($url.$source,$url,$path);3498 if ( !$gs) {3499 warn "Couldn't find revmap for$url$source\n";3500 return;3501 }3502 my@ranges= split ",",$revs;3503 my ($tip,$tip_commit);3504 my@merged_commit_ranges;3505 # find the tip3506 for my$range(@ranges) {3507 my ($bottom,$top) = split "-",$range;3508$top||=$bottom;3509 my$bottom_commit=$gs->find_rev_after($bottom, 1,$top);3510 my$top_commit=$gs->find_rev_before($top, 1,$bottom);35113512 unless ($top_commitand$bottom_commit) {3513 warn "W:unknown path/rev in svn:mergeinfo "3514 ."dirprop:$source:$range\n";3515 next;3516 }35173518 if (scalar(command('rev-parse', "$bottom_commit^@"))) {3519 push@merged_commit_ranges,3520 "$bottom_commit^..$top_commit";3521 } else {3522 push@merged_commit_ranges, "$top_commit";3523 }35243525 if ( !defined$tipor$top>$tip) {3526$tip=$top;3527$tip_commit=$top_commit;3528 }3529 }3530 return ($tip_commit,@merged_commit_ranges);3531}35323533sub _rev_list {3534 my ($msg_fh,$ctx) = command_output_pipe(3535 "rev-list",@_,3536 );3537 my@rv;3538 while ( <$msg_fh> ) {3539 chomp;3540 push@rv,$_;3541 }3542 command_close_pipe($msg_fh,$ctx);3543@rv;3544}35453546sub check_cherry_pick {3547 my$base= shift;3548 my$tip= shift;3549 my$parents= shift;3550 my@ranges=@_;3551 my%commits= map {$_=> 1 }3552 _rev_list("--no-merges",$tip, "--not",$base,@$parents, "--");3553 for my$range(@ranges) {3554 delete@commits{_rev_list($range, "--")};3555 }3556 for my$commit(keys%commits) {3557 if (has_no_changes($commit)) {3558 delete$commits{$commit};3559 }3560 }3561 return (keys%commits);3562}35633564sub has_no_changes {3565 my$commit= shift;35663567 my@revs= split / /, command_oneline(3568 qw(rev-list --parents -1 -m),$commit);35693570# Commits with no parents, e.g. the start of a partial branch,3571# have changes by definition.3572return1if(@revs<2);35733574# Commits with multiple parents, e.g a merge, have no changes3575# by definition.3576return0if(@revs>2);35773578return(command_oneline("rev-parse","$commit^{tree}")eq3579 command_oneline("rev-parse","$commit~1^{tree}"));3580}35813582# The GIT_DIR environment variable is not always set until after the command3583# line arguments are processed, so we can't memoize in a BEGIN block.3584{3585my$memoized=0;35863587sub memoize_svn_mergeinfo_functions {3588return if$memoized;3589$memoized=1;35903591my$cache_path="$ENV{GIT_DIR}/svn/.caches/";3592 mkpath([$cache_path])unless-d $cache_path;35933594 tie my%lookup_svn_merge_cache=>'Memoize::Storable',3595"$cache_path/lookup_svn_merge.db",'nstore';3596 memoize 'lookup_svn_merge',3597 SCALAR_CACHE =>'FAULT',3598 LIST_CACHE => ['HASH'=> \%lookup_svn_merge_cache],3599;36003601 tie my%check_cherry_pick_cache=>'Memoize::Storable',3602"$cache_path/check_cherry_pick.db",'nstore';3603 memoize 'check_cherry_pick',3604 SCALAR_CACHE =>'FAULT',3605 LIST_CACHE => ['HASH'=> \%check_cherry_pick_cache],3606;36073608 tie my%has_no_changes_cache=>'Memoize::Storable',3609"$cache_path/has_no_changes.db",'nstore';3610 memoize 'has_no_changes',3611 SCALAR_CACHE => ['HASH'=> \%has_no_changes_cache],3612 LIST_CACHE =>'FAULT',3613;3614}36153616sub unmemoize_svn_mergeinfo_functions {3617return ifnot$memoized;3618$memoized=0;36193620 Memoize::unmemoize 'lookup_svn_merge';3621 Memoize::unmemoize 'check_cherry_pick';3622 Memoize::unmemoize 'has_no_changes';3623}36243625 Memoize::memoize 'Git::SVN::repos_root';3626}36273628END{3629# Force cache writeout explicitly instead of waiting for3630# global destruction to avoid segfault in Storable:3631# http://rt.cpan.org/Public/Bug/Display.html?id=360873632 unmemoize_svn_mergeinfo_functions();3633}36343635sub parents_exclude {3636my$parents=shift;3637my@commits=@_;3638return unless@commits;36393640my@excluded;3641my$excluded;3642do{3643my@cmd= ('rev-list',"-1",@commits,"--not",@$parents);3644$excluded= command_oneline(@cmd);3645if($excluded) {3646my@new;3647my$found;3648formy$commit(@commits) {3649if($commiteq$excluded) {3650push@excluded,$commit;3651$found++;3652last;3653}3654else{3655push@new,$commit;3656}3657}3658die"saw commit '$excluded' in rev-list output, "3659."but we didn't ask for that commit (wanted:@commits--not@$parents)"3660unless$found;3661@commits=@new;3662}3663}3664while($excludedand@commits);36653666return@excluded;3667}366836693670# note: this function should only be called if the various dirprops3671# have actually changed3672sub find_extra_svn_parents {3673my($self,$ed,$mergeinfo,$parents) =@_;3674# aha! svk:merge property changed...36753676 memoize_svn_mergeinfo_functions();36773678# We first search for merged tips which are not in our3679# history. Then, we figure out which git revisions are in3680# that tip, but not this revision. If all of those revisions3681# are now marked as merge, we can add the tip as a parent.3682my@merges=split"\n",$mergeinfo;3683my@merge_tips;3684my$url=$self->{url};3685my$uuid=$self->ra_uuid;3686my%ranges;3687formy$merge(@merges) {3688my($tip_commit,@ranges) =3689 lookup_svn_merge($uuid,$url,$merge);3690unless(!$tip_commitor3691grep{$_eq$tip_commit}@$parents) {3692push@merge_tips,$tip_commit;3693$ranges{$tip_commit} = \@ranges;3694}else{3695push@merge_tips,undef;3696}3697}36983699my%excluded=map{$_=>1}3700 parents_exclude($parents,grep{defined}@merge_tips);37013702# check merge tips for new parents3703my@new_parents;3704formy$merge_tip(@merge_tips) {3705my$spec=shift@merges;3706next unless$merge_tipand$excluded{$merge_tip};37073708my$ranges=$ranges{$merge_tip};37093710# check out 'new' tips3711my$merge_base;3712eval{3713$merge_base= command_oneline(3714"merge-base",3715@$parents,$merge_tip,3716);3717};3718if($@) {3719die"An error occurred during merge-base"3720unless$@->isa("Git::Error::Command");37213722warn"W: Cannot find common ancestor between ".3723"@$parentsand$merge_tip. Ignoring merge info.\n";3724next;3725}37263727# double check that there are no missing non-merge commits3728my(@incomplete) = check_cherry_pick(3729$merge_base,$merge_tip,3730$parents,3731@$ranges,3732);37333734if(@incomplete) {3735warn"W:svn cherry-pick ignored ($spec) - missing "3736.@incomplete." commit(s) (eg$incomplete[0])\n";3737}else{3738warn3739"Found merge parent (svn:mergeinfo prop): ",3740$merge_tip,"\n";3741push@new_parents,$merge_tip;3742}3743}37443745# cater for merges which merge commits from multiple branches3746if(@new_parents>1) {3747for(my$i=0;$i<=$#new_parents;$i++) {3748for(my$j=0;$j<=$#new_parents;$j++) {3749next if$i==$j;3750next unless$new_parents[$i];3751next unless$new_parents[$j];3752my$revs= command_oneline(3753"rev-list","-1",3754"$new_parents[$i]..$new_parents[$j]",3755);3756if( !$revs) {3757undef($new_parents[$j]);3758}3759}3760}3761}3762push@$parents,grep{defined}@new_parents;3763}37643765sub make_log_entry {3766my($self,$rev,$parents,$ed) =@_;3767my$untracked=$self->get_untracked($ed);37683769my@parents=@$parents;3770my$ps=$ed->{path_strip} ||"";3771formy$path(grep{m/$ps/} %{$ed->{dir_prop}} ) {3772my$props=$ed->{dir_prop}{$path};3773if($props->{"svk:merge"} ) {3774$self->find_extra_svk_parents3775($ed,$props->{"svk:merge"}, \@parents);3776}3777if($props->{"svn:mergeinfo"} ) {3778$self->find_extra_svn_parents3779($ed,3780$props->{"svn:mergeinfo"},3781 \@parents);3782}3783}37843785open my$un,'>>',"$self->{dir}/unhandled.log"or croak $!;3786print$un"r$rev\n"or croak $!;3787print$un $_,"\n"foreach@$untracked;3788my%log_entry= ( parents => \@parents, revision =>$rev,3789log=>'');37903791my$headrev;3792my$logged=delete$self->{logged_rev_props};3793if(!$logged||$self->{-want_revprops}) {3794my$rp=$self->ra->rev_proplist($rev);3795foreach(sort keys%$rp) {3796my$v=$rp->{$_};3797if(/^svn:(author|date|log)$/) {3798$log_entry{$1} =$v;3799}elsif($_eq'svm:headrev') {3800$headrev=$v;3801}else{3802print$un" rev_prop: ", uri_encode($_),' ',3803 uri_encode($v),"\n";3804}3805}3806}else{3807map{$log_entry{$_} =$logged->{$_} }keys%$logged;3808}3809close$unor croak $!;38103811$log_entry{date} = parse_svn_date($log_entry{date});3812$log_entry{log} .="\n";3813my$author=$log_entry{author} = check_author($log_entry{author});3814my($name,$email) =defined$::users{$author} ? @{$::users{$author}}3815: ($author,undef);38163817my($commit_name,$commit_email) = ($name,$email);3818if($_use_log_author) {3819my$name_field;3820if($log_entry{log} =~/From:\s+(.*\S)\s*\n/i) {3821$name_field=$1;3822}elsif($log_entry{log} =~/Signed-off-by:\s+(.*\S)\s*\n/i) {3823$name_field=$1;3824}3825if(!defined$name_field) {3826if(!defined$email) {3827$email=$name;3828}3829}elsif($name_field=~/(.*?)\s+<(.*)>/) {3830($name,$email) = ($1,$2);3831}elsif($name_field=~/(.*)@/) {3832($name,$email) = ($1,$name_field);3833}else{3834($name,$email) = ($name_field,$name_field);3835}3836}3837if(defined$headrev&&$self->use_svm_props) {3838if($self->rewrite_root) {3839die"Can't have both 'useSvmProps' and 'rewriteRoot' ",3840"options set!\n";3841}3842if($self->rewrite_uuid) {3843die"Can't have both 'useSvmProps' and 'rewriteUUID' ",3844"options set!\n";3845}3846my($uuid,$r) =$headrev=~m{^([a-f\d\-]{30,}):(\d+)$}i;3847# we don't want "SVM: initializing mirror for junk" ...3848returnundefif$r==0;3849my$svm=$self->svm;3850if($uuidne$svm->{uuid}) {3851die"UUID mismatch on SVM path:\n",3852"expected:$svm->{uuid}\n",3853" got:$uuid\n";3854}3855my$full_url=$self->full_url;3856$full_url=~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or3857die"Failed to replace '$svm->{replace}' with ",3858"'$svm->{source}' in$full_url\n";3859# throw away username for storing in records3860 remove_username($full_url);3861$log_entry{metadata} ="$full_url\@$r$uuid";3862$log_entry{svm_revision} =$r;3863$email||="$author\@$uuid";3864$commit_email||="$author\@$uuid";3865}elsif($self->use_svnsync_props) {3866my$full_url=$self->svnsync->{url};3867$full_url.="/$self->{path}"iflength$self->{path};3868 remove_username($full_url);3869my$uuid=$self->svnsync->{uuid};3870$log_entry{metadata} ="$full_url\@$rev$uuid";3871$email||="$author\@$uuid";3872$commit_email||="$author\@$uuid";3873}else{3874my$url=$self->metadata_url;3875 remove_username($url);3876my$uuid=$self->rewrite_uuid||$self->ra->get_uuid;3877$log_entry{metadata} ="$url\@$rev".$uuid;3878$email||="$author\@".$uuid;3879$commit_email||="$author\@".$uuid;3880}3881$log_entry{name} =$name;3882$log_entry{email} =$email;3883$log_entry{commit_name} =$commit_name;3884$log_entry{commit_email} =$commit_email;3885 \%log_entry;3886}38873888sub fetch {3889my($self,$min_rev,$max_rev,@parents) =@_;3890my($last_rev,$last_commit) =$self->last_rev_commit;3891my($base,$head) =$self->get_fetch_range($min_rev,$max_rev);3892$self->ra->gs_fetch_loop_common($base,$head, [$self]);3893}38943895sub set_tree_cb {3896my($self,$log_entry,$tree,$rev,$date,$author) =@_;3897$self->{inject_parents} = {$rev=>$tree};3898$self->fetch(undef,undef);3899}39003901sub set_tree {3902my($self,$tree) = (shift,shift);3903my$log_entry= ::get_commit_entry($tree);3904unless($self->{last_rev}) {3905::fatal("Must have an existing revision to commit");3906}3907my%ed_opts= ( r =>$self->{last_rev},3908log=>$log_entry->{log},3909 ra =>$self->ra,3910 tree_a =>$self->{last_commit},3911 tree_b =>$tree,3912 editor_cb =>sub{3913$self->set_tree_cb($log_entry,$tree,@_) },3914 svn_path =>$self->{path} );3915if(!Git::SVN::Editor->new(\%ed_opts)->apply_diff) {3916print"No changes\nr$self->{last_rev} =$tree\n";3917}3918}39193920sub rebuild_from_rev_db {3921my($self,$path) =@_;3922my$r= -1;3923open my$fh,'<',$pathor croak "open:$!";3924binmode$fhor croak "binmode:$!";3925while(<$fh>) {3926length($_) ==41or croak "inconsistent size in ($_) != 41";3927chomp($_);3928++$r;3929next if$_eq('0' x 40);3930$self->rev_map_set($r,$_);3931print"r$r=$_\n";3932}3933close$fhor croak "close:$!";3934unlink$pathor croak "unlink:$!";3935}39363937sub rebuild {3938my($self) =@_;3939my$map_path=$self->map_path;3940my$partial= (-e $map_path&& ! -z $map_path);3941return unless::verify_ref($self->refname.'^0');3942if(!$partial&& ($self->use_svm_props||$self->no_metadata)) {3943my$rev_db=$self->rev_db_path;3944$self->rebuild_from_rev_db($rev_db);3945if($self->use_svm_props) {3946my$svm_rev_db=$self->rev_db_path($self->svm_uuid);3947$self->rebuild_from_rev_db($svm_rev_db);3948}3949$self->unlink_rev_db_symlink;3950return;3951}3952print"Rebuilding$map_path...\n"if(!$partial);3953my($base_rev,$head) = ($partial?$self->rev_map_max_norebuild(1) :3954(undef,undef));3955my($log,$ctx) =3956 command_output_pipe(qw/rev-list --pretty=raw --reverse/,3957($head?"$head..":"") .$self->refname,3958'--');3959my$metadata_url=$self->metadata_url;3960 remove_username($metadata_url);3961my$svn_uuid=$self->rewrite_uuid||$self->ra_uuid;3962my$c;3963while(<$log>) {3964if(m{^commit ($::sha1)$}) {3965$c=$1;3966next;3967}3968next unlesss{^\s*(git-svn-id:)}{$1};3969my($url,$rev,$uuid) = ::extract_metadata($_);3970 remove_username($url);39713972# ignore merges (from set-tree)3973next if(!defined$rev|| !$uuid);39743975# if we merged or otherwise started elsewhere, this is3976# how we break out of it3977if(($uuidne$svn_uuid) ||3978($metadata_url&&$url&& ($urlne$metadata_url))) {3979next;3980}3981if($partial&&$head) {3982print"Partial-rebuilding$map_path...\n";3983print"Currently at$base_rev=$head\n";3984$head=undef;3985}39863987$self->rev_map_set($rev,$c);3988print"r$rev=$c\n";3989}3990 command_close_pipe($log,$ctx);3991print"Done rebuilding$map_path\n"if(!$partial|| !$head);3992my$rev_db_path=$self->rev_db_path;3993if(-f $self->rev_db_path) {3994unlink$self->rev_db_pathor croak "unlink:$!";3995}3996$self->unlink_rev_db_symlink;3997}39983999# rev_map:4000# Tie::File seems to be prone to offset errors if revisions get sparse,4001# it's not that fast, either. Tie::File is also not in Perl 5.6. So4002# one of my favorite modules is out :< Next up would be one of the DBM4003# modules, but I'm not sure which is most portable...4004#4005# This is the replacement for the rev_db format, which was too big4006# and inefficient for large repositories with a lot of sparse history4007# (mainly tags)4008#4009# The format is this:4010# - 24 bytes for every record,4011# * 4 bytes for the integer representing an SVN revision number4012# * 20 bytes representing the sha1 of a git commit4013# - No empty padding records like the old format4014# (except the last record, which can be overwritten)4015# - new records are written append-only since SVN revision numbers4016# increase monotonically4017# - lookups on SVN revision number are done via a binary search4018# - Piping the file to xxd -c24 is a good way of dumping it for4019# viewing or editing (piped back through xxd -r), should the need4020# ever arise.4021# - The last record can be padding revision with an all-zero sha14022# This is used to optimize fetch performance when using multiple4023# "fetch" directives in .git/config4024#4025# These files are disposable unless noMetadata or useSvmProps is set40264027sub _rev_map_set {4028my($fh,$rev,$commit) =@_;40294030binmode$fhor croak "binmode:$!";4031my$size= (stat($fh))[7];4032($size%24) ==0or croak "inconsistent size:$size";40334034my$wr_offset=0;4035if($size>0) {4036sysseek($fh, -24, SEEK_END)or croak "seek:$!";4037my$read=sysread($fh,my$buf,24)or croak "read:$!";4038$read==24or croak "read only$readbytes (!= 24)";4039my($last_rev,$last_commit) =unpack(rev_map_fmt,$buf);4040if($last_commiteq('0' x40)) {4041if($size>=48) {4042sysseek($fh, -48, SEEK_END)or croak "seek:$!";4043$read=sysread($fh,$buf,24)or4044 croak "read:$!";4045$read==24or4046 croak "read only$readbytes (!= 24)";4047($last_rev,$last_commit) =4048unpack(rev_map_fmt,$buf);4049if($last_commiteq('0' x40)) {4050 croak "inconsistent .rev_map\n";4051}4052}4053if($last_rev>=$rev) {4054 croak "last_rev is higher!:$last_rev>=$rev";4055}4056$wr_offset= -24;4057}4058}4059sysseek($fh,$wr_offset, SEEK_END)or croak "seek:$!";4060syswrite($fh,pack(rev_map_fmt,$rev,$commit),24) ==24or4061 croak "write:$!";4062}40634064sub _rev_map_reset {4065my($fh,$rev,$commit) =@_;4066my$c= _rev_map_get($fh,$rev);4067$ceq$commitor die"_rev_map_reset(@_) commit$cdoes not match!\n";4068my$offset=sysseek($fh,0, SEEK_CUR)or croak "seek:$!";4069truncate$fh,$offsetor croak "truncate:$!";4070}40714072sub mkfile {4073my($path) =@_;4074unless(-e $path) {4075my($dir,$base) = ($path=~ m#^(.*?)/?([^/]+)$#);4076 mkpath([$dir])unless-d $dir;4077open my$fh,'>>',$pathor die"Couldn't create$path:$!\n";4078close$fhor die"Couldn't close (create)$path:$!\n";4079}4080}40814082sub rev_map_set {4083my($self,$rev,$commit,$update_ref,$uuid) =@_;4084defined$commitor die"missing arg3\n";4085length$commit==40or die"arg3 must be a full SHA1 hexsum\n";4086my$db=$self->map_path($uuid);4087my$db_lock="$db.lock";4088my$sigmask;4089$update_ref||=0;4090if($update_ref) {4091$sigmask= POSIX::SigSet->new();4092my$signew= POSIX::SigSet->new(SIGINT, SIGHUP, SIGTERM,4093 SIGALRM, SIGUSR1, SIGUSR2);4094 sigprocmask(SIG_BLOCK,$signew,$sigmask)or4095 croak "Can't block signals:$!";4096}4097 mkfile($db);40984099$LOCKFILES{$db_lock} =1;4100my$sync;4101# both of these options make our .rev_db file very, very important4102# and we can't afford to lose it because rebuild() won't work4103if($self->use_svm_props||$self->no_metadata) {4104$sync=1;4105 copy($db,$db_lock)or die"rev_map_set(@_): ",4106"Failed to copy: ",4107"$db=>$db_lock($!)\n";4108}else{4109rename$db,$db_lockor die"rev_map_set(@_): ",4110"Failed to rename: ",4111"$db=>$db_lock($!)\n";4112}41134114sysopen(my$fh,$db_lock, O_RDWR | O_CREAT)4115or croak "Couldn't open$db_lock:$!\n";4116$update_refeq'reset'? _rev_map_reset($fh,$rev,$commit) :4117 _rev_map_set($fh,$rev,$commit);4118if($sync) {4119$fh->flushor die"Couldn't flush$db_lock:$!\n";4120$fh->syncor die"Couldn't sync$db_lock:$!\n";4121}4122close$fhor croak $!;4123if($update_ref) {4124$_head=$self;4125my$note="";4126$note=" ($update_ref)"if($update_ref!~/^\d*$/);4127 command_noisy('update-ref','-m',"r$rev$note",4128$self->refname,$commit);4129}4130rename$db_lock,$dbor die"rev_map_set(@_): ","Failed to rename: ",4131"$db_lock=>$db($!)\n";4132delete$LOCKFILES{$db_lock};4133if($update_ref) {4134 sigprocmask(SIG_SETMASK,$sigmask)or4135 croak "Can't restore signal mask:$!";4136}4137}41384139# If want_commit, this will return an array of (rev, commit) where4140# commit _must_ be a valid commit in the archive.4141# Otherwise, it'll return the max revision (whether or not the4142# commit is valid or just a 0x40 placeholder).4143sub rev_map_max {4144my($self,$want_commit) =@_;4145$self->rebuild;4146my($r,$c) =$self->rev_map_max_norebuild($want_commit);4147$want_commit? ($r,$c) :$r;4148}41494150sub rev_map_max_norebuild {4151my($self,$want_commit) =@_;4152my$map_path=$self->map_path;4153stat$map_pathorreturn$want_commit? (0,undef) :0;4154sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4155binmode$fhor croak "binmode:$!";4156my$size= (stat($fh))[7];4157($size%24) ==0or croak "inconsistent size:$size";41584159if($size==0) {4160close$fhor croak "close:$!";4161return$want_commit? (0,undef) :0;4162}41634164sysseek($fh, -24, SEEK_END)or croak "seek:$!";4165sysread($fh,my$buf,24) ==24or croak "read:$!";4166my($r,$c) =unpack(rev_map_fmt,$buf);4167if($want_commit&&$ceq('0' x40)) {4168if($size<48) {4169return$want_commit? (0,undef) :0;4170}4171sysseek($fh, -48, SEEK_END)or croak "seek:$!";4172sysread($fh,$buf,24) ==24or croak "read:$!";4173($r,$c) =unpack(rev_map_fmt,$buf);4174if($ceq('0'x40)) {4175 croak "Penultimate record is all-zeroes in$map_path";4176}4177}4178close$fhor croak "close:$!";4179$want_commit? ($r,$c) :$r;4180}41814182sub rev_map_get {4183my($self,$rev,$uuid) =@_;4184my$map_path=$self->map_path($uuid);4185returnundefunless-e $map_path;41864187sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4188my$c= _rev_map_get($fh,$rev);4189close($fh)or croak "close:$!";4190$c4191}41924193sub _rev_map_get {4194my($fh,$rev) =@_;41954196binmode$fhor croak "binmode:$!";4197my$size= (stat($fh))[7];4198($size%24) ==0or croak "inconsistent size:$size";41994200if($size==0) {4201returnundef;4202}42034204my($l,$u) = (0,$size-24);4205my($r,$c,$buf);42064207while($l<=$u) {4208my$i=int(($l/24+$u/24) /2) *24;4209sysseek($fh,$i, SEEK_SET)or croak "seek:$!";4210sysread($fh,my$buf,24) ==24or croak "read:$!";4211my($r,$c) =unpack(rev_map_fmt,$buf);42124213if($r<$rev) {4214$l=$i+24;4215}elsif($r>$rev) {4216$u=$i-24;4217}else{# $r == $rev4218return$ceq('0' x 40) ?undef:$c;4219}4220}4221undef;4222}42234224# Finds the first svn revision that exists on (if $eq_ok is true) or4225# before $rev for the current branch. It will not search any lower4226# than $min_rev. Returns the git commit hash and svn revision number4227# if found, else (undef, undef).4228sub find_rev_before {4229my($self,$rev,$eq_ok,$min_rev) =@_;4230--$revunless$eq_ok;4231$min_rev||=1;4232my$max_rev=$self->rev_map_max;4233$rev=$max_revif($rev>$max_rev);4234while($rev>=$min_rev) {4235if(my$c=$self->rev_map_get($rev)) {4236return($rev,$c);4237}4238--$rev;4239}4240return(undef,undef);4241}42424243# Finds the first svn revision that exists on (if $eq_ok is true) or4244# after $rev for the current branch. It will not search any higher4245# than $max_rev. Returns the git commit hash and svn revision number4246# if found, else (undef, undef).4247sub find_rev_after {4248my($self,$rev,$eq_ok,$max_rev) =@_;4249++$revunless$eq_ok;4250$max_rev||=$self->rev_map_max;4251while($rev<=$max_rev) {4252if(my$c=$self->rev_map_get($rev)) {4253return($rev,$c);4254}4255++$rev;4256}4257return(undef,undef);4258}42594260sub _new {4261my($class,$repo_id,$ref_id,$path) =@_;4262unless(defined$repo_id&&length$repo_id) {4263$repo_id=$Git::SVN::default_repo_id;4264}4265unless(defined$ref_id&&length$ref_id) {4266$_prefix=''unlessdefined($_prefix);4267$_[2] =$ref_id=4268"refs/remotes/$_prefix$Git::SVN::default_ref_id";4269}4270$_[1] =$repo_id;4271my$dir="$ENV{GIT_DIR}/svn/$ref_id";42724273# Older repos imported by us used $GIT_DIR/svn/foo instead of4274# $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo4275if($ref_id=~m{^refs/remotes/(.*)}) {4276my$old_dir="$ENV{GIT_DIR}/svn/$1";4277if(-d $old_dir&& ! -d $dir) {4278$dir=$old_dir;4279}4280}42814282$_[3] =$path=''unless(defined$path);4283 mkpath([$dir]);4284bless{4285 ref_id =>$ref_id, dir =>$dir,index=>"$dir/index",4286 path =>$path, config =>"$ENV{GIT_DIR}/svn/config",4287 map_root =>"$dir/.rev_map", repo_id =>$repo_id},$class;4288}42894290# for read-only access of old .rev_db formats4291sub unlink_rev_db_symlink {4292my($self) =@_;4293my$link=$self->rev_db_path;4294$link=~s/\.[\w-]+$//or croak "missing UUID at the end of$link";4295if(-l $link) {4296unlink$linkor croak "unlink:$linkfailed!";4297}4298}42994300sub rev_db_path {4301my($self,$uuid) =@_;4302my$db_path=$self->map_path($uuid);4303$db_path=~s{/\.rev_map\.}{/\.rev_db\.}4304or croak "map_path:$db_pathdoes not contain '/.rev_map.' !";4305$db_path;4306}43074308# the new replacement for .rev_db4309sub map_path {4310my($self,$uuid) =@_;4311$uuid||=$self->ra_uuid;4312"$self->{map_root}.$uuid";4313}43144315sub uri_encode {4316my($f) =@_;4317$f=~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;4318$f4319}43204321sub uri_decode {4322my($f) =@_;4323$f=~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;4324$f4325}43264327sub remove_username {4328$_[0] =~s{^([^:]*://)[^@]+@}{$1};4329}43304331package Git::SVN::Editor;4332use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;4333use strict;4334use warnings;4335use Carp qw/croak/;4336use IO::File;43374338sub new {4339my($class,$opts) =@_;4340foreach(qw/svn_path r ra tree_a tree_b log editor_cb/) {4341die"$_required!\n"unless(defined$opts->{$_});4342}43434344my$pool= SVN::Pool->new;4345my$mods= generate_diff($opts->{tree_a},$opts->{tree_b});4346my$types= check_diff_paths($opts->{ra},$opts->{svn_path},4347$opts->{r},$mods);43484349# $opts->{ra} functions should not be used after this:4350my@ce=$opts->{ra}->get_commit_editor($opts->{log},4351$opts->{editor_cb},$pool);4352my$self= SVN::Delta::Editor->new(@ce,$pool);4353bless$self,$class;4354foreach(qw/svn_path r tree_a tree_b/) {4355$self->{$_} =$opts->{$_};4356}4357$self->{url} =$opts->{ra}->{url};4358$self->{mods} =$mods;4359$self->{types} =$types;4360$self->{pool} =$pool;4361$self->{bat} = {''=>$self->open_root($self->{r},$self->{pool}) };4362$self->{rm} = { };4363$self->{path_prefix} =length$self->{svn_path} ?4364"$self->{svn_path}/":'';4365$self->{config} =$opts->{config};4366$self->{mergeinfo} =$opts->{mergeinfo};4367return$self;4368}43694370sub generate_diff {4371my($tree_a,$tree_b) =@_;4372my@diff_tree=qw(diff-tree -z -r);4373if($_cp_similarity) {4374push@diff_tree,"-C$_cp_similarity";4375}else{4376push@diff_tree,'-C';4377}4378push@diff_tree,'--find-copies-harder'if$_find_copies_harder;4379push@diff_tree,"-l$_rename_limit"ifdefined$_rename_limit;4380push@diff_tree,$tree_a,$tree_b;4381my($diff_fh,$ctx) = command_output_pipe(@diff_tree);4382local$/="\0";4383my$state='meta';4384my@mods;4385while(<$diff_fh>) {4386chomp$_;# this gets rid of the trailing "\0"4387if($stateeq'meta'&& /^:(\d{6})\s(\d{6})\s4388($::sha1)\s($::sha1)\s4389([MTCRAD])\d*$/xo) {4390push@mods, { mode_a =>$1, mode_b =>$2,4391 sha1_a =>$3, sha1_b =>$4,4392 chg =>$5};4393if($5=~/^(?:C|R)$/) {4394$state='file_a';4395}else{4396$state='file_b';4397}4398}elsif($stateeq'file_a') {4399my$x=$mods[$#mods]or croak "Empty array\n";4400if($x->{chg} !~/^(?:C|R)$/) {4401 croak "Error parsing$_,$x->{chg}\n";4402}4403$x->{file_a} =$_;4404$state='file_b';4405}elsif($stateeq'file_b') {4406my$x=$mods[$#mods]or croak "Empty array\n";4407if(exists$x->{file_a} &&$x->{chg} !~/^(?:C|R)$/) {4408 croak "Error parsing$_,$x->{chg}\n";4409}4410if(!exists$x->{file_a} &&$x->{chg} =~/^(?:C|R)$/) {4411 croak "Error parsing$_,$x->{chg}\n";4412}4413$x->{file_b} =$_;4414$state='meta';4415}else{4416 croak "Error parsing$_\n";4417}4418}4419 command_close_pipe($diff_fh,$ctx);4420 \@mods;4421}44224423sub check_diff_paths {4424my($ra,$pfx,$rev,$mods) =@_;4425my%types;4426$pfx.='/'iflength$pfx;44274428sub type_diff_paths {4429my($ra,$types,$path,$rev) =@_;4430my@p=split m#/+#, $path;4431my$c=shift@p;4432unless(defined$types->{$c}) {4433$types->{$c} =$ra->check_path($c,$rev);4434}4435while(@p) {4436$c.='/'.shift@p;4437next ifdefined$types->{$c};4438$types->{$c} =$ra->check_path($c,$rev);4439}4440}44414442foreachmy$m(@$mods) {4443foreachmy$f(qw/file_a file_b/) {4444next unlessdefined$m->{$f};4445my($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);4446if(length$pfx.$dir&& !defined$types{$dir}) {4447 type_diff_paths($ra, \%types,$pfx.$dir,$rev);4448}4449}4450}4451 \%types;4452}44534454sub split_path {4455return($_[0] =~ m#^(.*?)/?([^/]+)$#);4456}44574458sub repo_path {4459my($self,$path) =@_;4460if(my$enc=$self->{pathnameencoding}) {4461require Encode;4462 Encode::from_to($path,$enc,'UTF-8');4463}4464$self->{path_prefix}.(defined$path?$path:'');4465}44664467sub url_path {4468my($self,$path) =@_;4469if($self->{url} =~ m#^https?://#) {4470$path=~s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;4471}4472$self->{url} .'/'.$self->repo_path($path);4473}44744475sub rmdirs {4476my($self) =@_;4477my$rm=$self->{rm};4478delete$rm->{''};# we never delete the url we're tracking4479return unless%$rm;44804481foreach(keys%$rm) {4482my@d=split m#/#, $_;4483my$c=shift@d;4484$rm->{$c} =1;4485while(@d) {4486$c.='/'.shift@d;4487$rm->{$c} =1;4488}4489}4490delete$rm->{$self->{svn_path}};4491delete$rm->{''};# we never delete the url we're tracking4492return unless%$rm;44934494my($fh,$ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,4495$self->{tree_b});4496local$/="\0";4497while(<$fh>) {4498chomp;4499my@dn=split m#/#, $_;4500while(pop@dn) {4501delete$rm->{join'/',@dn};4502}4503unless(%$rm) {4504close$fh;4505return;4506}4507}4508 command_close_pipe($fh,$ctx);45094510my($r,$p,$bat) = ($self->{r},$self->{pool},$self->{bat});4511foreachmy$d(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {4512$self->close_directory($bat->{$d},$p);4513my($dn) = ($d=~ m#^(.*?)/?(?:[^/]+)$#);4514print"\tD+\t$d/\n"unless$::_q;4515$self->SUPER::delete_entry($d,$r,$bat->{$dn},$p);4516delete$bat->{$d};4517}4518}45194520sub open_or_add_dir {4521my($self,$full_path,$baton,$deletions) =@_;4522my$t=$self->{types}->{$full_path};4523if(!defined$t) {4524die"$full_pathnot known in r$self->{r} or we have a bug!\n";4525}4526{4527no warnings 'once';4528# SVN::Node::none and SVN::Node::file are used only once,4529# so we're shutting up Perl's warnings about them.4530if($t==$SVN::Node::none ||defined($deletions->{$full_path})) {4531return$self->add_directory($full_path,$baton,4532undef, -1,$self->{pool});4533}elsif($t==$SVN::Node::dir) {4534return$self->open_directory($full_path,$baton,4535$self->{r},$self->{pool});4536}# no warnings 'once'4537print STDERR "$full_pathalready exists in repository at ",4538"r$self->{r} and it is not a directory (",4539($t==$SVN::Node::file ?'file':'unknown'),"/$t)\n";4540}# no warnings 'once'4541exit1;4542}45434544sub ensure_path {4545my($self,$path,$deletions) =@_;4546my$bat=$self->{bat};4547my$repo_path=$self->repo_path($path);4548return$bat->{''}unless(length$repo_path);45494550my@p=split m#/+#, $repo_path;4551my$c=shift@p;4552$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{''},$deletions);4553while(@p) {4554my$c0=$c;4555$c.='/'.shift@p;4556$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{$c0},$deletions);4557}4558return$bat->{$c};4559}45604561# Subroutine to convert a globbing pattern to a regular expression.4562# From perl cookbook.4563sub glob2pat {4564my$globstr=shift;4565my%patmap= ('*'=>'.*','?'=>'.','['=>'[',']'=>']');4566$globstr=~s{(.)} { $patmap{$1}||"\Q$1"}ge;4567return'^'.$globstr.'$';4568}45694570sub check_autoprop {4571my($self,$pattern,$properties,$file,$fbat) =@_;4572# Convert the globbing pattern to a regular expression.4573my$regex= glob2pat($pattern);4574# Check if the pattern matches the file name.4575if($file=~m/($regex)/) {4576# Parse the list of properties to set.4577my@props=split(/;/,$properties);4578foreachmy$prop(@props) {4579# Parse 'name=value' syntax and set the property.4580if($prop=~/([^=]+)=(.*)/) {4581my($n,$v) = ($1,$2);4582for($n,$v) {4583s/^\s+//;s/\s+$//;4584}4585$self->change_file_prop($fbat,$n,$v);4586}4587}4588}4589}45904591sub apply_autoprops {4592my($self,$file,$fbat) =@_;4593my$conf_t= ${$self->{config}}{'config'};4594no warnings 'once';4595# Check [miscellany]/enable-auto-props in svn configuration.4596if(SVN::_Core::svn_config_get_bool(4597$conf_t,4598$SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY,4599$SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS,46000)) {4601# Auto-props are enabled. Enumerate them to look for matches.4602my$callback=sub{4603$self->check_autoprop($_[0],$_[1],$file,$fbat);4604};4605 SVN::_Core::svn_config_enumerate(4606$conf_t,4607$SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS,4608$callback);4609}4610}46114612sub A {4613my($self,$m,$deletions) =@_;4614my($dir,$file) = split_path($m->{file_b});4615my$pbat=$self->ensure_path($dir,$deletions);4616my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,4617undef, -1);4618print"\tA\t$m->{file_b}\n"unless$::_q;4619$self->apply_autoprops($file,$fbat);4620$self->chg_file($fbat,$m);4621$self->close_file($fbat,undef,$self->{pool});4622}46234624sub C {4625my($self,$m,$deletions) =@_;4626my($dir,$file) = split_path($m->{file_b});4627my$pbat=$self->ensure_path($dir,$deletions);4628my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,4629$self->url_path($m->{file_a}),$self->{r});4630print"\tC\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;4631$self->chg_file($fbat,$m);4632$self->close_file($fbat,undef,$self->{pool});4633}46344635sub delete_entry {4636my($self,$path,$pbat) =@_;4637my$rpath=$self->repo_path($path);4638my($dir,$file) = split_path($rpath);4639$self->{rm}->{$dir} =1;4640$self->SUPER::delete_entry($rpath,$self->{r},$pbat,$self->{pool});4641}46424643sub R {4644my($self,$m,$deletions) =@_;4645my($dir,$file) = split_path($m->{file_b});4646my$pbat=$self->ensure_path($dir,$deletions);4647my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,4648$self->url_path($m->{file_a}),$self->{r});4649print"\tR\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;4650$self->apply_autoprops($file,$fbat);4651$self->chg_file($fbat,$m);4652$self->close_file($fbat,undef,$self->{pool});46534654($dir,$file) = split_path($m->{file_a});4655$pbat=$self->ensure_path($dir,$deletions);4656$self->delete_entry($m->{file_a},$pbat);4657}46584659sub M {4660my($self,$m,$deletions) =@_;4661my($dir,$file) = split_path($m->{file_b});4662my$pbat=$self->ensure_path($dir,$deletions);4663my$fbat=$self->open_file($self->repo_path($m->{file_b}),4664$pbat,$self->{r},$self->{pool});4665print"\t$m->{chg}\t$m->{file_b}\n"unless$::_q;4666$self->chg_file($fbat,$m);4667$self->close_file($fbat,undef,$self->{pool});4668}46694670sub T { shift->M(@_) }46714672sub change_file_prop {4673my($self,$fbat,$pname,$pval) =@_;4674$self->SUPER::change_file_prop($fbat,$pname,$pval,$self->{pool});4675}46764677sub change_dir_prop {4678my($self,$pbat,$pname,$pval) =@_;4679$self->SUPER::change_dir_prop($pbat,$pname,$pval,$self->{pool});4680}46814682sub _chg_file_get_blob ($$$$) {4683my($self,$fbat,$m,$which) =@_;4684my$fh= $::_repository->temp_acquire("git_blob_$which");4685if($m->{"mode_$which"} =~/^120/) {4686print$fh'link 'or croak $!;4687$self->change_file_prop($fbat,'svn:special','*');4688}elsif($m->{mode_a} =~/^120/&&$m->{"mode_$which"} !~/^120/) {4689$self->change_file_prop($fbat,'svn:special',undef);4690}4691my$blob=$m->{"sha1_$which"};4692return($fh,)if($blob=~/^0{40}$/);4693my$size= $::_repository->cat_blob($blob,$fh);4694 croak "Failed to read object$blob"if($size<0);4695$fh->flush==0or croak $!;4696seek$fh,0,0or croak $!;46974698my$exp= ::md5sum($fh);4699seek$fh,0,0or croak $!;4700return($fh,$exp);4701}47024703sub chg_file {4704my($self,$fbat,$m) =@_;4705if($m->{mode_b} =~/755$/&&$m->{mode_a} !~/755$/) {4706$self->change_file_prop($fbat,'svn:executable','*');4707}elsif($m->{mode_b} !~/755$/&&$m->{mode_a} =~/755$/) {4708$self->change_file_prop($fbat,'svn:executable',undef);4709}4710my($fh_a,$exp_a) = _chg_file_get_blob $self,$fbat,$m,'a';4711my($fh_b,$exp_b) = _chg_file_get_blob $self,$fbat,$m,'b';4712my$pool= SVN::Pool->new;4713my$atd=$self->apply_textdelta($fbat,$exp_a,$pool);4714if(-s $fh_a) {4715my$txstream= SVN::TxDelta::new ($fh_a,$fh_b,$pool);4716my$res= SVN::TxDelta::send_txstream($txstream,@$atd,$pool);4717if(defined$res) {4718die"Unexpected result from send_txstream:$res\n",4719"(SVN::Core::VERSION:$SVN::Core::VERSION)\n";4720}4721}else{4722my$got= SVN::TxDelta::send_stream($fh_b,@$atd,$pool);4723die"Checksum mismatch\nexpected:$exp_b\ngot:$got\n"4724if($gotne$exp_b);4725}4726 Git::temp_release($fh_b,1);4727 Git::temp_release($fh_a,1);4728$pool->clear;4729}47304731sub D {4732my($self,$m,$deletions) =@_;4733my($dir,$file) = split_path($m->{file_b});4734my$pbat=$self->ensure_path($dir,$deletions);4735print"\tD\t$m->{file_b}\n"unless$::_q;4736$self->delete_entry($m->{file_b},$pbat);4737}47384739sub close_edit {4740my($self) =@_;4741my($p,$bat) = ($self->{pool},$self->{bat});4742foreach(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {4743next if$_eq'';4744$self->close_directory($bat->{$_},$p);4745}4746$self->close_directory($bat->{''},$p);4747$self->SUPER::close_edit($p);4748$p->clear;4749}47504751sub abort_edit {4752my($self) =@_;4753$self->SUPER::abort_edit($self->{pool});4754}47554756sub DESTROY {4757my$self=shift;4758$self->SUPER::DESTROY(@_);4759$self->{pool}->clear;4760}47614762# this drives the editor4763sub apply_diff {4764my($self) =@_;4765my$mods=$self->{mods};4766my%o= ( D =>0, C =>1, R =>2, A =>3, M =>4, T =>5);4767my%deletions;47684769foreachmy$m(@$mods) {4770if($m->{chg}eq"D") {4771$deletions{$m->{file_b}} =1;4772}4773}47744775foreachmy$m(sort{$o{$a->{chg}} <=>$o{$b->{chg}} }@$mods) {4776my$f=$m->{chg};4777if(defined$o{$f}) {4778$self->$f($m, \%deletions);4779}else{4780 fatal("Invalid change type:$f");4781}4782}47834784if(defined($self->{mergeinfo})) {4785$self->change_dir_prop($self->{bat}{''},"svn:mergeinfo",4786$self->{mergeinfo});4787}4788$self->rmdirsif$_rmdir;4789if(@$mods==0&& !defined($self->{mergeinfo})) {4790$self->abort_edit;4791}else{4792$self->close_edit;4793}4794returnscalar@$mods;4795}47964797package Git::SVN::Ra;4798use vars qw/@ISA $config_dir $_ignore_refs_regex $_log_window_size/;4799use strict;4800use warnings;4801my($ra_invalid,$can_do_switch,%ignored_err,$RA);48024803BEGIN{4804# enforce temporary pool usage for some simple functions4805no strict 'refs';4806formy$f(qw/rev_proplist get_latest_revnum get_uuid get_repos_root4807 get_file/) {4808my$SUPER="SUPER::$f";4809*$f=sub{4810my$self=shift;4811my$pool= SVN::Pool->new;4812my@ret=$self->$SUPER(@_,$pool);4813$pool->clear;4814wantarray?@ret:$ret[0];4815};4816}4817}48184819sub _auth_providers () {4820my@rv= (4821 SVN::Client::get_simple_provider(),4822 SVN::Client::get_ssl_server_trust_file_provider(),4823 SVN::Client::get_simple_prompt_provider(4824 \&Git::SVN::Prompt::simple,2),4825 SVN::Client::get_ssl_client_cert_file_provider(),4826 SVN::Client::get_ssl_client_cert_prompt_provider(4827 \&Git::SVN::Prompt::ssl_client_cert,2),4828 SVN::Client::get_ssl_client_cert_pw_file_provider(),4829 SVN::Client::get_ssl_client_cert_pw_prompt_provider(4830 \&Git::SVN::Prompt::ssl_client_cert_pw,2),4831 SVN::Client::get_username_provider(),4832 SVN::Client::get_ssl_server_trust_prompt_provider(4833 \&Git::SVN::Prompt::ssl_server_trust),4834 SVN::Client::get_username_prompt_provider(4835 \&Git::SVN::Prompt::username,2)4836);48374838# earlier 1.6.x versions would segfault, and <= 1.5.x didn't have4839# this function4840if(::compare_svn_version('1.6.12') >0) {4841my$config= SVN::Core::config_get_config($config_dir);4842my($p,@a);4843# config_get_config returns all config files from4844# ~/.subversion, auth_get_platform_specific_client_providers4845# just wants the config "file".4846@a= ($config->{'config'},undef);4847$p= SVN::Core::auth_get_platform_specific_client_providers(@a);4848# Insert the return value from4849# auth_get_platform_specific_providers4850unshift@rv,@$p;4851}4852 \@rv;4853}48544855sub escape_uri_only {4856my($uri) =@_;4857my@tmp;4858foreach(splitm{/},$uri) {4859s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;4860push@tmp,$_;4861}4862join('/',@tmp);4863}48644865sub escape_url {4866my($url) =@_;4867if($url=~ m#^(https?)://([^/]+)(.*)$#) {4868my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));4869$url="$scheme://$domain$uri";4870}4871$url;4872}48734874sub new {4875my($class,$url) =@_;4876$url=~s!/+$!!;4877return$RAif($RA&&$RA->{url}eq$url);48784879::_req_svn();48804881 SVN::_Core::svn_config_ensure($config_dir,undef);4882my($baton,$callbacks) = SVN::Core::auth_open_helper(_auth_providers);4883my$config= SVN::Core::config_get_config($config_dir);4884$RA=undef;4885my$dont_store_passwords=1;4886my$conf_t= ${$config}{'config'};4887{4888no warnings 'once';4889# The usage of $SVN::_Core::SVN_CONFIG_* variables4890# produces warnings that variables are used only once.4891# I had not found the better way to shut them up, so4892# the warnings of type 'once' are disabled in this block.4893if(SVN::_Core::svn_config_get_bool($conf_t,4894$SVN::_Core::SVN_CONFIG_SECTION_AUTH,4895$SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS,48961) ==0) {4897 SVN::_Core::svn_auth_set_parameter($baton,4898$SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS,4899bless(\$dont_store_passwords,"_p_void"));4900}4901if(SVN::_Core::svn_config_get_bool($conf_t,4902$SVN::_Core::SVN_CONFIG_SECTION_AUTH,4903$SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS,49041) ==0) {4905$Git::SVN::Prompt::_no_auth_cache =1;4906}4907}# no warnings 'once'4908my$self= SVN::Ra->new(url => escape_url($url), auth =>$baton,4909 config =>$config,4910 pool => SVN::Pool->new,4911 auth_provider_callbacks =>$callbacks);4912$self->{url} =$url;4913$self->{svn_path} =$url;4914$self->{repos_root} =$self->get_repos_root;4915$self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;4916$self->{cache} = { check_path => { r =>0, data => {} },4917 get_dir => { r =>0, data => {} } };4918$RA=bless$self,$class;4919}49204921sub check_path {4922my($self,$path,$r) =@_;4923my$cache=$self->{cache}->{check_path};4924if($r==$cache->{r} &&exists$cache->{data}->{$path}) {4925return$cache->{data}->{$path};4926}4927my$pool= SVN::Pool->new;4928my$t=$self->SUPER::check_path($path,$r,$pool);4929$pool->clear;4930if($r!=$cache->{r}) {4931%{$cache->{data}} = ();4932$cache->{r} =$r;4933}4934$cache->{data}->{$path} =$t;4935}49364937sub get_dir {4938my($self,$dir,$r) =@_;4939my$cache=$self->{cache}->{get_dir};4940if($r==$cache->{r}) {4941if(my$x=$cache->{data}->{$dir}) {4942returnwantarray?@$x:$x->[0];4943}4944}4945my$pool= SVN::Pool->new;4946my($d,undef,$props) =$self->SUPER::get_dir($dir,$r,$pool);4947my%dirents=map{$_=> { kind =>$d->{$_}->kind} }keys%$d;4948$pool->clear;4949if($r!=$cache->{r}) {4950%{$cache->{data}} = ();4951$cache->{r} =$r;4952}4953$cache->{data}->{$dir} = [ \%dirents,$r,$props];4954wantarray? (\%dirents,$r,$props) : \%dirents;4955}49564957sub DESTROY {4958# do not call the real DESTROY since we store ourselves in $RA4959}49604961# get_log(paths, start, end, limit,4962# discover_changed_paths, strict_node_history, receiver)4963sub get_log {4964my($self,@args) =@_;4965my$pool= SVN::Pool->new;49664967# svn_log_changed_path_t objects passed to get_log are likely to be4968# overwritten even if only the refs are copied to an external variable,4969# so we should dup the structures in their entirety. Using an4970# externally passed pool (instead of our temporary and quickly cleared4971# pool in Git::SVN::Ra) does not help matters at all...4972my$receiver=pop@args;4973my$prefix="/".$self->{svn_path};4974$prefix=~ s#/+($)##;4975my$prefix_regex= qr#^\Q$prefix\E#;4976push(@args,sub{4977my($paths) =$_[0];4978return&$receiver(@_)unless$paths;4979$_[0] = ();4980foreachmy$p(keys%$paths) {4981my$i=$paths->{$p};4982# Make path relative to our url, not repos_root4983$p=~s/$prefix_regex//;4984my%s=map{$_=>$i->$_; }4985 qw/copyfrom_path copyfrom_rev action/;4986if($s{'copyfrom_path'}) {4987$s{'copyfrom_path'} =~s/$prefix_regex//;4988}4989$_[0]{$p} = \%s;4990}4991&$receiver(@_);4992});499349944995# the limit parameter was not supported in SVN 1.1.x, so we4996# drop it. Therefore, the receiver callback passed to it4997# is made aware of this limitation by being wrapped if4998# the limit passed to is being wrapped.4999if(::compare_svn_version('1.2.0') <=0) {5000my$limit=splice(@args,3,1);5001if($limit>0) {5002my$receiver=pop@args;5003push(@args,sub{ &$receiver(@_)if(--$limit>=0) });5004}5005}5006my$ret=$self->SUPER::get_log(@args,$pool);5007$pool->clear;5008$ret;5009}50105011sub trees_match {5012my($self,$url1,$rev1,$url2,$rev2) =@_;5013my$ctx= SVN::Client->new(auth => _auth_providers);5014my$out= IO::File->new_tmpfile;50155016# older SVN (1.1.x) doesn't take $pool as the last parameter for5017# $ctx->diff(), so we'll create a default one5018my$pool= SVN::Pool->new_default_sub;50195020$ra_invalid=1;# this will open a new SVN::Ra connection to $url15021$ctx->diff([],$url1,$rev1,$url2,$rev2,1,1,0,$out,$out);5022$out->flush;5023my$ret= (($out->stat)[7] ==0);5024close$outor croak $!;50255026$ret;5027}50285029sub get_commit_editor {5030my($self,$log,$cb,$pool) =@_;50315032my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef,0) : ();5033$self->SUPER::get_commit_editor($log,$cb,@lock,$pool);5034}50355036sub gs_do_update {5037my($self,$rev_a,$rev_b,$gs,$editor) =@_;5038my$new= ($rev_a==$rev_b);5039my$path=$gs->{path};50405041if($new&& -e $gs->{index}) {5042unlink$gs->{index}or die5043"Couldn't unlink index:$gs->{index}:$!\n";5044}5045my$pool= SVN::Pool->new;5046$editor->set_path_strip($path);5047my(@pc) =split m#/#, $path;5048my$reporter=$self->do_update($rev_b, (@pc?shift@pc:''),50491,$editor,$pool);5050my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef) : ();50515052# Since we can't rely on svn_ra_reparent being available, we'll5053# just have to do some magic with set_path to make it so5054# we only want a partial path.5055my$sp='';5056my$final=join('/',@pc);5057while(@pc) {5058$reporter->set_path($sp,$rev_b,0,@lock,$pool);5059$sp.='/'iflength$sp;5060$sp.=shift@pc;5061}5062die"BUG: '$sp' != '$final'\n"if($spne$final);50635064$reporter->set_path($sp,$rev_a,$new,@lock,$pool);50655066$reporter->finish_report($pool);5067$pool->clear;5068$editor->{git_commit_ok};5069}50705071# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and5072# svn_ra_reparent didn't work before 1.4)5073sub gs_do_switch {5074my($self,$rev_a,$rev_b,$gs,$url_b,$editor) =@_;5075my$path=$gs->{path};5076my$pool= SVN::Pool->new;50775078my$full_url=$self->{url};5079my$old_url=$full_url;5080$full_url.='/'.$pathiflength$path;5081my($ra,$reparented);50825083if($old_url=~ m#^svn(\+ssh)?://# ||5084($full_url=~ m#^https?://# &&5085 escape_url($full_url)ne$full_url)) {5086$_[0] =undef;5087$self=undef;5088$RA=undef;5089$ra= Git::SVN::Ra->new($full_url);5090$ra_invalid=1;5091}elsif($old_urlne$full_url) {5092 SVN::_Ra::svn_ra_reparent($self->{session},$full_url,$pool);5093$self->{url} =$full_url;5094$reparented=1;5095}50965097$ra||=$self;5098$url_b= escape_url($url_b);5099my$reporter=$ra->do_switch($rev_b,'',1,$url_b,$editor,$pool);5100my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef) : ();5101$reporter->set_path('',$rev_a,0,@lock,$pool);5102$reporter->finish_report($pool);51035104if($reparented) {5105 SVN::_Ra::svn_ra_reparent($self->{session},$old_url,$pool);5106$self->{url} =$old_url;5107}51085109$pool->clear;5110$editor->{git_commit_ok};5111}51125113sub longest_common_path {5114my($gsv,$globs) =@_;5115my%common;5116my$common_max=scalar@$gsv;51175118foreachmy$gs(@$gsv) {5119my@tmp=split m#/#, $gs->{path};5120my$p='';5121foreach(@tmp) {5122$p.=length($p) ?"/$_":$_;5123$common{$p} ||=0;5124$common{$p}++;5125}5126}5127$globs||= [];5128$common_max+=scalar@$globs;5129foreachmy$glob(@$globs) {5130my@tmp=split m#/#, $glob->{path}->{left};5131my$p='';5132foreach(@tmp) {5133$p.=length($p) ?"/$_":$_;5134$common{$p} ||=0;5135$common{$p}++;5136}5137}51385139my$longest_path='';5140foreach(sort{length$b<=>length$a}keys%common) {5141if($common{$_} ==$common_max) {5142$longest_path=$_;5143last;5144}5145}5146$longest_path;5147}51485149sub gs_fetch_loop_common {5150my($self,$base,$head,$gsv,$globs) =@_;5151return if($base>$head);5152my$inc=$_log_window_size;5153my($min,$max) = ($base,$head<$base+$inc?$head:$base+$inc);5154my$longest_path= longest_common_path($gsv,$globs);5155my$ra_url=$self->{url};5156my$find_trailing_edge;5157while(1) {5158my%revs;5159my$err;5160my$err_handler=$SVN::Error::handler;5161$SVN::Error::handler =sub{5162($err) =@_;5163 skip_unknown_revs($err);5164};5165sub _cb {5166my($paths,$r,$author,$date,$log) =@_;5167[$paths,5168{ author =>$author, date =>$date,log=>$log} ];5169}5170$self->get_log([$longest_path],$min,$max,0,1,1,5171sub{$revs{$_[1]} = _cb(@_) });5172if($err) {5173print"Checked through r$max\r";5174}else{5175$find_trailing_edge=1;5176}5177if($errand$find_trailing_edge) {5178print STDERR "Path '$longest_path' ",5179"was probably deleted:\n",5180$err->expanded_message,5181"\nWill attempt to follow ",5182"revisions r$min.. r$max",5183"committed before the deletion\n";5184my$hi=$max;5185while(--$hi>=$min) {5186my$ok;5187$self->get_log([$longest_path],$min,$hi,51880,1,1,sub{5189$ok=$_[1];5190$revs{$_[1]} = _cb(@_) });5191if($ok) {5192print STDERR "r$min.. r$okOK\n";5193last;5194}5195}5196$find_trailing_edge=0;5197}5198$SVN::Error::handler =$err_handler;51995200my%exists=map{$_->{path} =>$_}@$gsv;5201foreachmy$r(sort{$a<=>$b}keys%revs) {5202my($paths,$logged) = @{$revs{$r}};52035204foreachmy$gs($self->match_globs(\%exists,$paths,5205$globs,$r)) {5206if($gs->rev_map_max>=$r) {5207next;5208}5209next unless$gs->match_paths($paths,$r);5210$gs->{logged_rev_props} =$logged;5211if(my$last_commit=$gs->last_commit) {5212$gs->assert_index_clean($last_commit);5213}5214my$log_entry=$gs->do_fetch($paths,$r);5215if($log_entry) {5216$gs->do_git_commit($log_entry);5217}5218$INDEX_FILES{$gs->{index}} =1;5219}5220foreachmy$g(@$globs) {5221my$k="svn-remote.$g->{remote}.".5222"$g->{t}-maxRev";5223 Git::SVN::tmp_config($k,$r);5224}5225if($ra_invalid) {5226$_[0] =undef;5227$self=undef;5228$RA=undef;5229$self= Git::SVN::Ra->new($ra_url);5230$ra_invalid=undef;5231}5232}5233# pre-fill the .rev_db since it'll eventually get filled in5234# with '0' x40 if something new gets committed5235foreachmy$gs(@$gsv) {5236next if$gs->rev_map_max>=$max;5237next ifdefined$gs->rev_map_get($max);5238$gs->rev_map_set($max,0 x40);5239}5240foreachmy$g(@$globs) {5241my$k="svn-remote.$g->{remote}.$g->{t}-maxRev";5242 Git::SVN::tmp_config($k,$max);5243}5244last if$max>=$head;5245$min=$max+1;5246$max+=$inc;5247$max=$headif($max>$head);5248}5249 Git::SVN::gc();5250}52515252sub get_dir_globbed {5253my($self,$left,$depth,$r) =@_;52545255my@x=eval{$self->get_dir($left,$r) };5256return unlessscalar@x==3;5257my$dirents=$x[0];5258my@finalents;5259foreachmy$de(keys%$dirents) {5260next if$dirents->{$de}->{kind} !=$SVN::Node::dir;5261if($depth>1) {5262my@args= ("$left/$de",$depth-1,$r);5263foreachmy$dir($self->get_dir_globbed(@args)) {5264push@finalents,"$de/$dir";5265}5266}else{5267push@finalents,$de;5268}5269}5270@finalents;5271}52725273# return value: 0 -- don't ignore, 1 -- ignore5274sub is_ref_ignored {5275my($g,$p) =@_;5276my$refname=$g->{ref}->full_path($p);5277return1ifdefined($g->{ignore_refs_regex}) &&5278$refname=~m!$g->{ignore_refs_regex}!;5279return0unlessdefined($_ignore_refs_regex);5280return1if$refname=~m!$_ignore_refs_regex!o;5281return0;5282}52835284sub match_globs {5285my($self,$exists,$paths,$globs,$r) =@_;52865287sub get_dir_check {5288my($self,$exists,$g,$r) =@_;52895290my@dirs=$self->get_dir_globbed($g->{path}->{left},5291$g->{path}->{depth},5292$r);52935294foreachmy$de(@dirs) {5295my$p=$g->{path}->full_path($de);5296next if$exists->{$p};5297next if(length$g->{path}->{right} &&5298($self->check_path($p,$r) !=5299$SVN::Node::dir));5300next unless$p=~/$g->{path}->{regex}/;5301$exists->{$p} = Git::SVN->init($self->{url},$p,undef,5302$g->{ref}->full_path($de),1);5303}5304}5305foreachmy$g(@$globs) {5306if(my$path=$paths->{"/$g->{path}->{left}"}) {5307if($path->{action} =~/^[AR]$/) {5308 get_dir_check($self,$exists,$g,$r);5309}5310}5311foreach(keys%$paths) {5312if(/$g->{path}->{left_regex}/&&5313!/$g->{path}->{regex}/) {5314next if$paths->{$_}->{action} !~/^[AR]$/;5315 get_dir_check($self,$exists,$g,$r);5316}5317next unless/$g->{path}->{regex}/;5318my$p=$1;5319my$pathname=$g->{path}->full_path($p);5320next if is_ref_ignored($g,$p);5321next if$exists->{$pathname};5322next if($self->check_path($pathname,$r) !=5323$SVN::Node::dir);5324$exists->{$pathname} = Git::SVN->init(5325$self->{url},$pathname,undef,5326$g->{ref}->full_path($p),1);5327}5328my$c='';5329foreach(split m#/#, $g->{path}->{left}) {5330$c.="/$_";5331next unless($paths->{$c} &&5332($paths->{$c}->{action} =~/^[AR]$/));5333 get_dir_check($self,$exists,$g,$r);5334}5335}5336values%$exists;5337}53385339sub minimize_url {5340my($self) =@_;5341return$self->{url}if($self->{url}eq$self->{repos_root});5342my$url=$self->{repos_root};5343my@components=split(m!/!,$self->{svn_path});5344my$c='';5345do{5346$url.="/$c"iflength$c;5347eval{5348my$ra= (ref$self)->new($url);5349my$latest=$ra->get_latest_revnum;5350$ra->get_log("",$latest,0,1,0,1,sub{});5351};5352}while($@&& ($c=shift@components));5353$url;5354}53555356sub can_do_switch {5357my$self=shift;5358unless(defined$can_do_switch) {5359my$pool= SVN::Pool->new;5360my$rep=eval{5361$self->do_switch(1,'',0,$self->{url},5362 SVN::Delta::Editor->new,$pool);5363};5364if($@) {5365$can_do_switch=0;5366}else{5367$rep->abort_report($pool);5368$can_do_switch=1;5369}5370$pool->clear;5371}5372$can_do_switch;5373}53745375sub skip_unknown_revs {5376my($err) =@_;5377my$errno=$err->apr_err();5378# Maybe the branch we're tracking didn't5379# exist when the repo started, so it's5380# not an error if it doesn't, just continue5381#5382# Wonderfully consistent library, eh?5383# 160013 - svn:// and file://5384# 175002 - http(s)://5385# 175007 - http(s):// (this repo required authorization, too...)5386# More codes may be discovered later...5387if($errno==175007||$errno==175002||$errno==160013) {5388my$err_key=$err->expanded_message;5389# revision numbers change every time, filter them out5390$err_key=~s/\d+/\0/g;5391$err_key="$errno\0$err_key";5392unless($ignored_err{$err_key}) {5393warn"W: Ignoring error from SVN, path probably ",5394"does not exist: ($errno): ",5395$err->expanded_message,"\n";5396warn"W: Do not be alarmed at the above message ",5397"git-svn is just searching aggressively for ",5398"old history.\n",5399"This may take a while on large repositories\n";5400$ignored_err{$err_key} =1;5401}5402return;5403}5404die"Error from SVN, ($errno): ",$err->expanded_message,"\n";5405}54065407package Git::SVN::Log;5408use strict;5409use warnings;5410use POSIX qw/strftime/;5411useconstant commit_log_separator => ('-' x 72) ."\n";5412use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline5413%rusers $show_commit $incremental/;5414my$l_fmt;54155416sub cmt_showable {5417my($c) =@_;5418return1ifdefined$c->{r};54195420# big commit message got truncated by the 16k pretty buffer in rev-list5421if($c->{l} &&$c->{l}->[-1]eq"...\n"&&5422$c->{a_raw} =~/\@([a-f\d\-]+)>$/) {5423@{$c->{l}} = ();5424my@log= command(qw/cat-file commit/,$c->{c});54255426# shift off the headers5427shift@logwhile($log[0]ne'');5428shift@log;54295430# TODO: make $c->{l} not have a trailing newline in the future5431@{$c->{l}} =map{"$_\n"}grep!/^git-svn-id: /,@log;54325433(undef,$c->{r},undef) = ::extract_metadata(5434(grep(/^git-svn-id: /,@log))[-1]);5435}5436returndefined$c->{r};5437}54385439sub log_use_color {5440return$color|| Git->repository->get_colorbool('color.diff');5441}54425443sub git_svn_log_cmd {5444my($r_min,$r_max,@args) =@_;5445my$head='HEAD';5446my(@files,@log_opts);5447foreachmy$x(@args) {5448if($xeq'--'||@files) {5449push@files,$x;5450}else{5451if(::verify_ref("$x^0")) {5452$head=$x;5453}else{5454push@log_opts,$x;5455}5456}5457}54585459my($url,$rev,$uuid,$gs) = ::working_head_info($head);5460$gs||= Git::SVN->_new;5461my@cmd= (qw/log --abbrev-commit --pretty=raw --default/,5462$gs->refname);5463push@cmd,'-r'unless$non_recursive;5464push@cmd, qw/--raw --name-status/if$verbose;5465push@cmd,'--color'if log_use_color();5466push@cmd,@log_opts;5467if(defined$r_max&&$r_max==$r_min) {5468push@cmd,'--max-count=1';5469if(my$c=$gs->rev_map_get($r_max)) {5470push@cmd,$c;5471}5472}elsif(defined$r_max) {5473if($r_max<$r_min) {5474($r_min,$r_max) = ($r_max,$r_min);5475}5476my(undef,$c_max) =$gs->find_rev_before($r_max,1,$r_min);5477my(undef,$c_min) =$gs->find_rev_after($r_min,1,$r_max);5478# If there are no commits in the range, both $c_max and $c_min5479# will be undefined. If there is at least 1 commit in the5480# range, both will be defined.5481return()if!defined$c_min|| !defined$c_max;5482if($c_mineq$c_max) {5483push@cmd,'--max-count=1',$c_min;5484}else{5485push@cmd,'--boundary',"$c_min..$c_max";5486}5487}5488return(@cmd,@files);5489}54905491# adapted from pager.c5492sub config_pager {5493if(! -t *STDOUT) {5494$ENV{GIT_PAGER_IN_USE} ='false';5495$pager=undef;5496return;5497}5498chomp($pager= command_oneline(qw(var GIT_PAGER)));5499if($pagereq'cat') {5500$pager=undef;5501}5502$ENV{GIT_PAGER_IN_USE} =defined($pager);5503}55045505sub run_pager {5506return unlessdefined$pager;5507pipe my($rfd,$wfd)orreturn;5508defined(my$pid=fork)or::fatal "Can't fork:$!";5509if(!$pid) {5510open STDOUT,'>&',$wfdor5511::fatal "Can't redirect to stdout:$!";5512return;5513}5514open STDIN,'<&',$rfdor::fatal "Can't redirect stdin:$!";5515$ENV{LESS} ||='FRSX';5516exec$pageror::fatal "Can't run pager:$!($pager)";5517}55185519sub format_svn_date {5520my$t=shift||time;5521my$gmoff= Git::SVN::get_tz($t);5522return strftime("%Y-%m-%d%H:%M:%S$gmoff(%a,%d%b%Y)",localtime($t));5523}55245525sub parse_git_date {5526my($t,$tz) =@_;5527# Date::Parse isn't in the standard Perl distro :(5528if($tz=~s/^\+//) {5529$t+= tz_to_s_offset($tz);5530}elsif($tz=~s/^\-//) {5531$t-= tz_to_s_offset($tz);5532}5533return$t;5534}55355536sub set_local_timezone {5537if(defined$TZ) {5538$ENV{TZ} =$TZ;5539}else{5540delete$ENV{TZ};5541}5542}55435544sub tz_to_s_offset {5545my($tz) =@_;5546$tz=~s/(\d\d)$//;5547return($1*60) + ($tz*3600);5548}55495550sub get_author_info {5551my($dest,$author,$t,$tz) =@_;5552$author=~s/(?:^\s*|\s*$)//g;5553$dest->{a_raw} =$author;5554my$au;5555if($::_authors) {5556$au=$rusers{$author} ||undef;5557}5558if(!$au) {5559($au) = ($author=~/<([^>]+)\@[^>]+>$/);5560}5561$dest->{t} =$t;5562$dest->{tz} =$tz;5563$dest->{a} =$au;5564$dest->{t_utc} = parse_git_date($t,$tz);5565}55665567sub process_commit {5568my($c,$r_min,$r_max,$defer) =@_;5569if(defined$r_min&&defined$r_max) {5570if($r_min==$c->{r} &&$r_min==$r_max) {5571 show_commit($c);5572return0;5573}5574return1if$r_min==$r_max;5575if($r_min<$r_max) {5576# we need to reverse the print order5577return0if(defined$limit&& --$limit<0);5578push@$defer,$c;5579return1;5580}5581if($r_min!=$r_max) {5582return1if($r_min<$c->{r});5583return1if($r_max>$c->{r});5584}5585}5586return0if(defined$limit&& --$limit<0);5587 show_commit($c);5588return1;5589}55905591sub show_commit {5592my$c=shift;5593if($oneline) {5594my$x="\n";5595if(my$l=$c->{l}) {5596while($l->[0] =~/^\s*$/) {shift@$l}5597$x=$l->[0];5598}5599$l_fmt||='A'.length($c->{r});5600print'r',pack($l_fmt,$c->{r}),' | ';5601print"$c->{c} | "if$show_commit;5602print$x;5603}else{5604 show_commit_normal($c);5605}5606}56075608sub show_commit_changed_paths {5609my($c) =@_;5610return unless$c->{changed};5611print"Changed paths:\n", @{$c->{changed}};5612}56135614sub show_commit_normal {5615my($c) =@_;5616print commit_log_separator,"r$c->{r} | ";5617print"$c->{c} | "if$show_commit;5618print"$c->{a} | ", format_svn_date($c->{t_utc}),' | ';5619my$nr_line=0;56205621if(my$l=$c->{l}) {5622while($l->[$#$l]eq"\n"&&$#$l>05623&&$l->[($#$l-1)]eq"\n") {5624pop@$l;5625}5626$nr_line=scalar@$l;5627if(!$nr_line) {5628print"1 line\n\n\n";5629}else{5630if($nr_line==1) {5631$nr_line='1 line';5632}else{5633$nr_line.=' lines';5634}5635print$nr_line,"\n";5636 show_commit_changed_paths($c);5637print"\n";5638print$_foreach@$l;5639}5640}else{5641print"1 line\n";5642 show_commit_changed_paths($c);5643print"\n";56445645}5646foreachmy$x(qw/raw stat diff/) {5647if($c->{$x}) {5648print"\n";5649print$_foreach@{$c->{$x}}5650}5651}5652}56535654sub cmd_show_log {5655my(@args) =@_;5656my($r_min,$r_max);5657my$r_last= -1;# prevent dupes5658 set_local_timezone();5659if(defined$::_revision) {5660if($::_revision =~/^(\d+):(\d+)$/) {5661($r_min,$r_max) = ($1,$2);5662}elsif($::_revision =~/^\d+$/) {5663$r_min=$r_max= $::_revision;5664}else{5665::fatal "-r$::_revision is not supported, use ",5666"standard 'git log' arguments instead";5667}5668}56695670 config_pager();5671@args= git_svn_log_cmd($r_min,$r_max,@args);5672if(!@args) {5673print commit_log_separator unless$incremental||$oneline;5674return;5675}5676my$log= command_output_pipe(@args);5677 run_pager();5678my(@k,$c,$d,$stat);5679my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;5680while(<$log>) {5681if(/^${esc_color}commit (?:- )?($::sha1_short)/o) {5682my$cmt=$1;5683if($c&& cmt_showable($c) &&$c->{r} !=$r_last) {5684$r_last=$c->{r};5685 process_commit($c,$r_min,$r_max, \@k)or5686goto out;5687}5688$d=undef;5689$c= { c =>$cmt};5690}elsif(/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {5691 get_author_info($c,$1,$2,$3);5692}elsif(/^${esc_color}(?:tree|parent|committer) /o) {5693# ignore5694}elsif(/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {5695push@{$c->{raw}},$_;5696}elsif(/^${esc_color}[ACRMDT]\t/) {5697# we could add $SVN->{svn_path} here, but that requires5698# remote access at the moment (repo_path_split)...5699 s#^(${esc_color})([ACRMDT])\t#$1 $2 #o;5700push@{$c->{changed}},$_;5701}elsif(/^${esc_color}diff /o) {5702$d=1;5703push@{$c->{diff}},$_;5704}elsif($d) {5705push@{$c->{diff}},$_;5706}elsif(/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*5707$esc_color*[\+\-]*$esc_color$/x) {5708$stat=1;5709push@{$c->{stat}},$_;5710}elsif($stat&&/^ \d+ files changed, \d+ insertions/) {5711push@{$c->{stat}},$_;5712$stat=undef;5713}elsif(/^${esc_color} (git-svn-id:.+)$/o) {5714($c->{url},$c->{r},undef) = ::extract_metadata($1);5715}elsif(s/^${esc_color} //o) {5716push@{$c->{l}},$_;5717}5718}5719if($c&&defined$c->{r} &&$c->{r} !=$r_last) {5720$r_last=$c->{r};5721 process_commit($c,$r_min,$r_max, \@k);5722}5723if(@k) {5724($r_min,$r_max) = ($r_max,$r_min);5725 process_commit($_,$r_min,$r_max)foreachreverse@k;5726}5727out:5728close$log;5729print commit_log_separator unless$incremental||$oneline;5730}57315732sub cmd_blame {5733my$path=pop;57345735 config_pager();5736 run_pager();57375738my($fh,$ctx,$rev);57395740if($_git_format) {5741($fh,$ctx) = command_output_pipe('blame',@_,$path);5742while(my$line= <$fh>) {5743if($line=~/^\^?([[:xdigit:]]+)\s/) {5744# Uncommitted edits show up as a rev ID of5745# all zeros, which we can't look up with5746# cmt_metadata5747if($1!~/^0+$/) {5748(undef,$rev,undef) =5749::cmt_metadata($1);5750$rev='0'if(!$rev);5751}else{5752$rev='0';5753}5754$rev=sprintf('%-10s',$rev);5755$line=~s/^\^?[[:xdigit:]]+(\s)/$rev$1/;5756}5757print$line;5758}5759}else{5760($fh,$ctx) = command_output_pipe('blame','-p',@_,'HEAD',5761'--',$path);5762my($sha1);5763my%authors;5764my@buffer;5765my%dsha;#distinct sha keys57665767while(my$line= <$fh>) {5768push@buffer,$line;5769if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {5770$dsha{$1} =1;5771}5772}57735774my$s2r= ::cmt_sha2rev_batch([keys%dsha]);57755776foreachmy$line(@buffer) {5777if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {5778$rev=$s2r->{$1};5779$rev='0'if(!$rev)5780}5781elsif($line=~/^author (.*)/) {5782$authors{$rev} =$1;5783$authors{$rev} =~s/\s/_/g;5784}5785elsif($line=~/^\t(.*)$/) {5786printf("%6s%10s%s\n",$rev,$authors{$rev},$1);5787}5788}5789}5790 command_close_pipe($fh,$ctx);5791}57925793package Git::SVN::Migration;5794# these version numbers do NOT correspond to actual version numbers5795# of git nor git-svn. They are just relative.5796#5797# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD5798#5799# v1 layout: .git/$id/info/url, refs/remotes/$id5800#5801# v2 layout: .git/svn/$id/info/url, refs/remotes/$id5802#5803# v3 layout: .git/svn/$id, refs/remotes/$id5804# - info/url may remain for backwards compatibility5805# - this is what we migrate up to this layout automatically,5806# - this will be used by git svn init on single branches5807# v3.1 layout (auto migrated):5808# - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink5809# for backwards compatibility5810#5811# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id5812# - this is only created for newly multi-init-ed5813# repositories. Similar in spirit to the5814# --use-separate-remotes option in git-clone (now default)5815# - we do not automatically migrate to this (following5816# the example set by core git)5817#5818# v5 layout: .rev_db.$UUID => .rev_map.$UUID5819# - newer, more-efficient format that uses 24-bytes per record5820# with no filler space.5821# - use xxd -c24 < .rev_map.$UUID to view and debug5822# - This is a one-way migration, repositories updated to the5823# new format will not be able to use old git-svn without5824# rebuilding the .rev_db. Rebuilding the rev_db is not5825# possible if noMetadata or useSvmProps are set; but should5826# be no problem for users that use the (sensible) defaults.5827use strict;5828use warnings;5829use Carp qw/croak/;5830use File::Path qw/mkpath/;5831use File::Basename qw/dirname basename/;5832use vars qw/$_minimize/;58335834sub migrate_from_v0 {5835my$git_dir=$ENV{GIT_DIR};5836returnundefunless-d $git_dir;5837my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);5838my$migrated=0;5839while(<$fh>) {5840chomp;5841my($id,$orig_ref) = ($_,$_);5842next unless$id=~ s#^refs/heads/(.+)-HEAD$#$1#;5843next unless-f "$git_dir/$id/info/url";5844my$new_ref="refs/remotes/$id";5845if(::verify_ref("$new_ref^0")) {5846print STDERR "W:$orig_refis probably an old ",5847"branch used by an ancient version of ",5848"git-svn.\n",5849"However,$new_refalso exists.\n",5850"We will not be able ",5851"to use this branch until this ",5852"ambiguity is resolved.\n";5853next;5854}5855print STDERR "Migrating from v0 layout...\n"if!$migrated;5856print STDERR "Renaming ref:$orig_ref=>$new_ref\n";5857 command_noisy('update-ref',$new_ref,$orig_ref);5858 command_noisy('update-ref','-d',$orig_ref,$orig_ref);5859$migrated++;5860}5861 command_close_pipe($fh,$ctx);5862print STDERR "Done migrating from v0 layout...\n"if$migrated;5863$migrated;5864}58655866sub migrate_from_v1 {5867my$git_dir=$ENV{GIT_DIR};5868my$migrated=0;5869return$migratedunless-d $git_dir;5870my$svn_dir="$git_dir/svn";58715872# just in case somebody used 'svn' as their $id at some point...5873return$migratedif-d $svn_dir&& ! -f "$svn_dir/info/url";58745875print STDERR "Migrating from a git-svn v1 layout...\n";5876 mkpath([$svn_dir]);5877print STDERR "Data from a previous version of git-svn exists, but\n\t",5878"$svn_dir\n\t(required for this version ",5879"($::VERSION) of git-svn) does not exist.\n";5880my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);5881while(<$fh>) {5882my$x=$_;5883next unless$x=~ s#^refs/remotes/##;5884chomp$x;5885next unless-f "$git_dir/$x/info/url";5886my$u=eval{ ::file_to_s("$git_dir/$x/info/url") };5887next unless$u;5888my$dn= dirname("$git_dir/svn/$x");5889 mkpath([$dn])unless-d $dn;5890if($xeq'svn') {# they used 'svn' as GIT_SVN_ID:5891 mkpath(["$git_dir/svn/svn"]);5892print STDERR " -$git_dir/$x/info=> ",5893"$git_dir/svn/$x/info\n";5894rename"$git_dir/$x/info","$git_dir/svn/$x/info"or5895 croak "$!:$x";5896# don't worry too much about these, they probably5897# don't exist with repos this old (save for index,5898# and we can easily regenerate that)5899foreachmy$f(qw/unhandled.log index .rev_db/) {5900rename"$git_dir/$x/$f","$git_dir/svn/$x/$f";5901}5902}else{5903print STDERR " -$git_dir/$x=>$git_dir/svn/$x\n";5904rename"$git_dir/$x","$git_dir/svn/$x"or5905 croak "$!:$x";5906}5907$migrated++;5908}5909 command_close_pipe($fh,$ctx);5910print STDERR "Done migrating from a git-svn v1 layout\n";5911$migrated;5912}59135914sub read_old_urls {5915my($l_map,$pfx,$path) =@_;5916my@dir;5917foreach(<$path/*>) {5918if(-r "$_/info/url") {5919$pfx.='/'if$pfx&&$pfx!~ m!/$!;5920my$ref_id=$pfx. basename $_;5921my$url= ::file_to_s("$_/info/url");5922$l_map->{$ref_id} =$url;5923}elsif(-d $_) {5924push@dir,$_;5925}5926}5927foreach(@dir) {5928my$x=$_;5929$x=~s!^\Q$ENV{GIT_DIR}\E/svn/!!o;5930 read_old_urls($l_map,$x,$_);5931}5932}59335934sub migrate_from_v2 {5935my@cfg= command(qw/config -l/);5936return ifgrep/^svn-remote\..+\.url=/,@cfg;5937my%l_map;5938 read_old_urls(\%l_map,'',"$ENV{GIT_DIR}/svn");5939my$migrated=0;59405941foreachmy$ref_id(sort keys%l_map) {5942eval{ Git::SVN->init($l_map{$ref_id},'',undef,$ref_id) };5943if($@) {5944 Git::SVN->init($l_map{$ref_id},'',$ref_id,$ref_id);5945}5946$migrated++;5947}5948$migrated;5949}59505951sub minimize_connections {5952my$r= Git::SVN::read_all_remotes();5953my$new_urls= {};5954my$root_repos= {};5955foreachmy$repo_id(keys%$r) {5956my$url=$r->{$repo_id}->{url}ornext;5957my$fetch=$r->{$repo_id}->{fetch}ornext;5958my$ra= Git::SVN::Ra->new($url);59595960# skip existing cases where we already connect to the root5961if(($ra->{url}eq$ra->{repos_root}) ||5962($ra->{repos_root}eq$repo_id)) {5963$root_repos->{$ra->{url}} =$repo_id;5964next;5965}59665967my$root_ra= Git::SVN::Ra->new($ra->{repos_root});5968my$root_path=$ra->{url};5969$root_path=~ s#^\Q$ra->{repos_root}\E(/|$)##;5970foreachmy$path(keys%$fetch) {5971my$ref_id=$fetch->{$path};5972my$gs= Git::SVN->new($ref_id,$repo_id,$path);59735974# make sure we can read when connecting to5975# a higher level of a repository5976my($last_rev,undef) =$gs->last_rev_commit;5977if(!defined$last_rev) {5978$last_rev=eval{5979$root_ra->get_latest_revnum;5980};5981next if$@;5982}5983my$new=$root_path;5984$new.=length$path?"/$path":'';5985eval{5986$root_ra->get_log([$new],$last_rev,$last_rev,59870,0,1,sub{ });5988};5989next if$@;5990$new_urls->{$ra->{repos_root}}->{$new} =5991{ ref_id =>$ref_id,5992 old_repo_id =>$repo_id,5993 old_path =>$path};5994}5995}59965997my@emptied;5998foreachmy$url(keys%$new_urls) {5999# see if we can re-use an existing [svn-remote "repo_id"]6000# instead of creating a(n ugly) new section:6001my$repo_id=$root_repos->{$url} ||$url;60026003my$fetch=$new_urls->{$url};6004foreachmy$path(keys%$fetch) {6005my$x=$fetch->{$path};6006 Git::SVN->init($url,$path,$repo_id,$x->{ref_id});6007my$pfx="svn-remote.$x->{old_repo_id}";60086009my$old_fetch=quotemeta("$x->{old_path}:".6010"$x->{ref_id}");6011 command_noisy(qw/config --unset/,6012"$pfx.fetch",'^'.$old_fetch.'$');6013delete$r->{$x->{old_repo_id}}->6014{fetch}->{$x->{old_path}};6015if(!keys%{$r->{$x->{old_repo_id}}->{fetch}}) {6016 command_noisy(qw/config --unset/,6017"$pfx.url");6018push@emptied,$x->{old_repo_id}6019}6020}6021}6022if(@emptied) {6023my$file=$ENV{GIT_CONFIG} ||"$ENV{GIT_DIR}/config";6024print STDERR <<EOF;6025The following [svn-remote] sections in your config file ($file) are empty6026and can be safely removed:6027EOF6028print STDERR "[svn-remote\"$_\"]\n"foreach@emptied;6029}6030}60316032sub migration_check {6033 migrate_from_v0();6034 migrate_from_v1();6035 migrate_from_v2();6036 minimize_connections()if$_minimize;6037}60386039package Git::IndexInfo;6040use strict;6041use warnings;6042use Git qw/command_input_pipe command_close_pipe/;60436044sub new {6045my($class) =@_;6046my($gui,$ctx) = command_input_pipe(qw/update-index -z --index-info/);6047bless{ gui =>$gui, ctx =>$ctx, nr =>0},$class;6048}60496050sub remove {6051my($self,$path) =@_;6052if(print{$self->{gui} }'0 ',0 x 40,"\t",$path,"\0") {6053return++$self->{nr};6054}6055undef;6056}60576058sub update {6059my($self,$mode,$hash,$path) =@_;6060if(print{$self->{gui} }$mode,' ',$hash,"\t",$path,"\0") {6061return++$self->{nr};6062}6063undef;6064}60656066sub DESTROY {6067my($self) =@_;6068 command_close_pipe($self->{gui},$self->{ctx});6069}60706071package Git::SVN::GlobSpec;6072use strict;6073use warnings;60746075sub new {6076my($class,$glob,$pattern_ok) =@_;6077my$re=$glob;6078$re=~s!/+$!!g;# no need for trailing slashes6079my(@left,@right,@patterns);6080my$state="left";6081my$die_msg="Only one set of wildcard directories ".6082"(e.g. '*' or '*/*/*') is supported: '$glob'\n";6083formy$part(split(m|/|,$glob)) {6084if($part=~/\*/&&$partne"*") {6085die"Invalid pattern in '$glob':$part\n";6086}elsif($pattern_ok&&$part=~/[{}]/&&6087$part!~/^\{[^{}]+\}/) {6088die"Invalid pattern in '$glob':$part\n";6089}6090if($parteq"*") {6091die$die_msgif$stateeq"right";6092$state="pattern";6093push(@patterns,"[^/]*");6094}elsif($pattern_ok&&$part=~/^\{(.*)\}$/) {6095die$die_msgif$stateeq"right";6096$state="pattern";6097my$p=quotemeta($1);6098$p=~s/\\,/|/g;6099push(@patterns,"(?:$p)");6100}else{6101if($stateeq"left") {6102push(@left,$part);6103}else{6104push(@right,$part);6105$state="right";6106}6107}6108}6109my$depth=@patterns;6110if($depth==0) {6111die"One '*' is needed in glob: '$glob'\n";6112}6113my$left=join('/',@left);6114my$right=join('/',@right);6115$re=join('/',@patterns);6116$re=join('\/',6117grep(length,quotemeta($left),"($re)",quotemeta($right)));6118my$left_re=qr/^\/\Q$left\E(\/|$)/;6119bless{ left =>$left, right =>$right, left_regex =>$left_re,6120 regex =>qr/$re/,glob=>$glob, depth =>$depth},$class;6121}61226123sub full_path {6124my($self,$path) =@_;6125return(length$self->{left} ?"$self->{left}/":'') .6126$path. (length$self->{right} ?"/$self->{right}":'');6127}61286129__END__61306131Data structures:613261336134$remotes= {# returned by read_all_remotes()6135'svn'=> {6136# svn-remote.svn.url=https://svn.musicpd.org6137 url =>'https://svn.musicpd.org',6138# svn-remote.svn.fetch=mpd/trunk:trunk6139 fetch => {6140'mpd/trunk'=>'trunk',6141},6142# svn-remote.svn.tags=mpd/tags/*:tags/*6143 tags => {6144 path => {6145 left =>'mpd/tags',6146 right =>'',6147 regex =>qr!mpd/tags/([^/]+)$!,6148glob=>'tags/*',6149},6150ref=> {6151 left =>'tags',6152 right =>'',6153 regex =>qr!tags/([^/]+)$!,6154glob=>'tags/*',6155},6156}6157}6158};61596160$log_entry hashref as returned by libsvn_log_entry()6161{6162log=>'whitespace-formatted log entry6163',# trailing newline is preserved6164 revision =>'8',# integer6165 date =>'2004-02-24T17:01:44.108345Z',# commit date6166 author =>'committer name'6167};616861696170# this is generated by generate_diff();6171@mods= array of diff-index line hashes,each element represents one line6172 of diff-index output61736174diff-index line ($m hash)6175{6176 mode_a => first column of diff-index output,no leading ':',6177 mode_b => second column of diff-index output,6178 sha1_b => sha1sum of the final blob,6179 chg => change type [MCRADT],6180 file_a => original file name of a file (iff chg is'C'or'R')6181 file_b => new/current file name of a file (any chg)6182}6183;61846185# retval of read_url_paths{,_all}();6186$l_map= {6187# repository root url6188'https://svn.musicpd.org'=> {6189# repository path # GIT_SVN_ID6190'mpd/trunk'=>'trunk',6191'mpd/tags/0.11.5'=>'tags/0.11.5',6192},6193}61946195Notes:6196 I don't trust the each() function on unless I created%hashmyself6197 because the internal iterator may not have started at base.