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@SVN::Git::Editor::ISA,'SVN::Delta::Editor'; 72push@SVN::Git::Fetcher::ISA,'SVN::Delta::Editor'; 73use Carp qw/croak/; 74use Digest::MD5; 75use IO::File qw//; 76use File::Basename qw/dirname basename/; 77use File::Path qw/mkpath/; 78use File::Spec; 79use File::Find; 80use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; 81use IPC::Open3; 82use Git; 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(SVN::Git::Editor SVN::Git::Fetcher 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$SVN::Git::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'=> \$SVN::Git::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'=> \$SVN::Git::Editor::_rmdir, 153'find-copies-harder'=> \$SVN::Git::Editor::_find_copies_harder, 154'l=i'=> \$SVN::Git::Editor::_rename_limit, 155'copy-similarity|C=i'=> \$SVN::Git::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 \$SVN::Git::Fetcher::_preserve_empty_dirs, 168'placeholder-filename=s'=> 169 \$SVN::Git::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= \$SVN::Git::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$SVN::Git::Fetcher::_preserve_empty_dirs) { 475 my$fname= \$SVN::Git::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(!SVN::Git::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# SVN::Git::Editor could probably be moved to Git.pm..1070my$re= SVN::Git::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(!SVN::Git::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= SVN::Git::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= SVN::Git::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= SVN::Git::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= SVN::Git::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(!SVN::Git::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 SVN::Git::Fetcher;4332use vars qw/@ISA $_ignore_regex $_preserve_empty_dirs $_placeholder_filename4333@deleted_gpath %added_placeholder $repo_id/;4334use strict;4335use warnings;4336use Carp qw/croak/;4337use File::Basename qw/dirname/;4338use IO::File qw//;43394340# file baton members: path, mode_a, mode_b, pool, fh, blob, base4341sub new {4342my($class,$git_svn,$switch_path) =@_;4343my$self= SVN::Delta::Editor->new;4344bless$self,$class;4345if(exists$git_svn->{last_commit}) {4346$self->{c} =$git_svn->{last_commit};4347$self->{empty_symlinks} =4348 _mark_empty_symlinks($git_svn,$switch_path);4349}43504351# some options are read globally, but can be overridden locally4352# per [svn-remote "..."] section. Command-line options will *NOT*4353# override options set in an [svn-remote "..."] section4354$repo_id=$git_svn->{repo_id};4355my$k="svn-remote.$repo_id.ignore-paths";4356my$v=eval{ command_oneline('config','--get',$k) };4357$self->{ignore_regex} =$v;43584359$k="svn-remote.$repo_id.preserve-empty-dirs";4360$v=eval{ command_oneline('config','--get','--bool',$k) };4361if($v&&$veq'true') {4362$_preserve_empty_dirs=1;4363$k="svn-remote.$repo_id.placeholder-filename";4364$v=eval{ command_oneline('config','--get',$k) };4365$_placeholder_filename=$v;4366}43674368# Load the list of placeholder files added during previous invocations.4369$k="svn-remote.$repo_id.added-placeholder";4370$v=eval{ command_oneline('config','--get-all',$k) };4371if($_preserve_empty_dirs&&$v) {4372# command() prints errors to stderr, so we only call it if4373# command_oneline() succeeded.4374my@v= command('config','--get-all',$k);4375$added_placeholder{ dirname($_) } =$_foreach@v;4376}43774378$self->{empty} = {};4379$self->{dir_prop} = {};4380$self->{file_prop} = {};4381$self->{absent_dir} = {};4382$self->{absent_file} = {};4383$self->{gii} =$git_svn->tmp_index_do(sub{ Git::IndexInfo->new});4384$self->{pathnameencoding} = Git::config('svn.pathnameencoding');4385$self;4386}43874388# this uses the Ra object, so it must be called before do_{switch,update},4389# not inside them (when the Git::SVN::Fetcher object is passed) to4390# do_{switch,update}4391sub _mark_empty_symlinks {4392my($git_svn,$switch_path) =@_;4393my$bool= Git::config_bool('svn.brokenSymlinkWorkaround');4394return{}if(!defined($bool)) || (defined($bool) && !$bool);43954396my%ret;4397my($rev,$cmt) =$git_svn->last_rev_commit;4398return{}unless($rev&&$cmt);43994400# allow the warning to be printed for each revision we fetch to4401# ensure the user sees it. The user can also disable the workaround4402# on the repository even while git svn is running and the next4403# revision fetched will skip this expensive function.4404my$printed_warning;4405chomp(my$empty_blob=`git hash-object -t blob --stdin < /dev/null`);4406my($ls,$ctx) = command_output_pipe(qw/ls-tree -r -z/,$cmt);4407local$/="\0";4408my$pfx=defined($switch_path) ?$switch_path:$git_svn->{path};4409$pfx.='/'iflength($pfx);4410while(<$ls>) {4411chomp;4412s/\A100644 blob $empty_blob\t//oornext;4413unless($printed_warning) {4414print STDERR "Scanning for empty symlinks, ",4415"this may take a while if you have ",4416"many empty files\n",4417"You may disable this with `",4418"git config svn.brokenSymlinkWorkaround ",4419"false'.\n",4420"This may be done in a different ",4421"terminal without restarting ",4422"git svn\n";4423$printed_warning=1;4424}4425my$path=$_;4426my(undef,$props) =4427$git_svn->ra->get_file($pfx.$path,$rev,undef);4428if($props->{'svn:special'}) {4429$ret{$path} =1;4430}4431}4432 command_close_pipe($ls,$ctx);4433 \%ret;4434}44354436# returns true if a given path is inside a ".git" directory4437sub in_dot_git {4438$_[0] =~m{(?:^|/)\.git(?:/|$)};4439}44404441# return value: 0 -- don't ignore, 1 -- ignore4442sub is_path_ignored {4443my($self,$path) =@_;4444return1if in_dot_git($path);4445return1ifdefined($self->{ignore_regex}) &&4446$path=~m!$self->{ignore_regex}!;4447return0unlessdefined($_ignore_regex);4448return1if$path=~m!$_ignore_regex!o;4449return0;4450}44514452sub set_path_strip {4453my($self,$path) =@_;4454$self->{path_strip} =qr/^\Q$path\E(\/|$)/iflength$path;4455}44564457sub open_root {4458{ path =>''};4459}44604461sub open_directory {4462my($self,$path,$pb,$rev) =@_;4463{ path =>$path};4464}44654466sub git_path {4467my($self,$path) =@_;4468if(my$enc=$self->{pathnameencoding}) {4469require Encode;4470 Encode::from_to($path,'UTF-8',$enc);4471}4472if($self->{path_strip}) {4473$path=~s!$self->{path_strip}!!or4474die"Failed to strip path '$path' ($self->{path_strip})\n";4475}4476$path;4477}44784479sub delete_entry {4480my($self,$path,$rev,$pb) =@_;4481returnundefif$self->is_path_ignored($path);44824483my$gpath=$self->git_path($path);4484returnundefif($gpatheq'');44854486# remove entire directories.4487my($tree) = (command('ls-tree','-z',$self->{c},"./$gpath")4488=~/\A040000 tree ([a-f\d]{40})\t\Q$gpath\E\0/);4489if($tree) {4490my($ls,$ctx) = command_output_pipe(qw/ls-tree4491-r --name-only -z/,4492$tree);4493local$/="\0";4494while(<$ls>) {4495chomp;4496my$rmpath="$gpath/$_";4497$self->{gii}->remove($rmpath);4498print"\tD\t$rmpath\n"unless$::_q;4499}4500print"\tD\t$gpath/\n"unless$::_q;4501 command_close_pipe($ls,$ctx);4502}else{4503$self->{gii}->remove($gpath);4504print"\tD\t$gpath\n"unless$::_q;4505}4506# Don't add to @deleted_gpath if we're deleting a placeholder file.4507push@deleted_gpath,$gpathunless$added_placeholder{dirname($path)};4508$self->{empty}->{$path} =0;4509undef;4510}45114512sub open_file {4513my($self,$path,$pb,$rev) =@_;4514my($mode,$blob);45154516goto out if$self->is_path_ignored($path);45174518my$gpath=$self->git_path($path);4519($mode,$blob) = (command('ls-tree','-z',$self->{c},"./$gpath")4520=~/\A(\d{6}) blob ([a-f\d]{40})\t\Q$gpath\E\0/);4521unless(defined$mode&&defined$blob) {4522die"$pathwas not found in commit$self->{c} (r$rev)\n";4523}4524if($modeeq'100644'&&$self->{empty_symlinks}->{$path}) {4525$mode='120000';4526}4527out:4528{ path =>$path, mode_a =>$mode, mode_b =>$mode, blob =>$blob,4529 pool => SVN::Pool->new, action =>'M'};4530}45314532sub add_file {4533my($self,$path,$pb,$cp_path,$cp_rev) =@_;4534my$mode;45354536if(!$self->is_path_ignored($path)) {4537my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4538delete$self->{empty}->{$dir};4539$mode='100644';45404541if($added_placeholder{$dir}) {4542# Remove our placeholder file, if we created one.4543 delete_entry($self,$added_placeholder{$dir})4544unless$patheq$added_placeholder{$dir};4545delete$added_placeholder{$dir}4546}4547}45484549{ path =>$path, mode_a =>$mode, mode_b =>$mode,4550 pool => SVN::Pool->new, action =>'A'};4551}45524553sub add_directory {4554my($self,$path,$cp_path,$cp_rev) =@_;4555goto out if$self->is_path_ignored($path);4556my$gpath=$self->git_path($path);4557if($gpatheq'') {4558my($ls,$ctx) = command_output_pipe(qw/ls-tree4559-r --name-only -z/,4560$self->{c});4561local$/="\0";4562while(<$ls>) {4563chomp;4564$self->{gii}->remove($_);4565print"\tD\t$_\n"unless$::_q;4566push@deleted_gpath,$gpath;4567}4568 command_close_pipe($ls,$ctx);4569$self->{empty}->{$path} =0;4570}4571my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4572delete$self->{empty}->{$dir};4573$self->{empty}->{$path} =1;45744575if($added_placeholder{$dir}) {4576# Remove our placeholder file, if we created one.4577 delete_entry($self,$added_placeholder{$dir});4578delete$added_placeholder{$dir}4579}45804581out:4582{ path =>$path};4583}45844585sub change_dir_prop {4586my($self,$db,$prop,$value) =@_;4587returnundefif$self->is_path_ignored($db->{path});4588$self->{dir_prop}->{$db->{path}} ||= {};4589$self->{dir_prop}->{$db->{path}}->{$prop} =$value;4590undef;4591}45924593sub absent_directory {4594my($self,$path,$pb) =@_;4595returnundefif$self->is_path_ignored($path);4596$self->{absent_dir}->{$pb->{path}} ||= [];4597push@{$self->{absent_dir}->{$pb->{path}}},$path;4598undef;4599}46004601sub absent_file {4602my($self,$path,$pb) =@_;4603returnundefif$self->is_path_ignored($path);4604$self->{absent_file}->{$pb->{path}} ||= [];4605push@{$self->{absent_file}->{$pb->{path}}},$path;4606undef;4607}46084609sub change_file_prop {4610my($self,$fb,$prop,$value) =@_;4611returnundefif$self->is_path_ignored($fb->{path});4612if($propeq'svn:executable') {4613if($fb->{mode_b} !=120000) {4614$fb->{mode_b} =defined$value?100755:100644;4615}4616}elsif($propeq'svn:special') {4617$fb->{mode_b} =defined$value?120000:100644;4618}else{4619$self->{file_prop}->{$fb->{path}} ||= {};4620$self->{file_prop}->{$fb->{path}}->{$prop} =$value;4621}4622undef;4623}46244625sub apply_textdelta {4626my($self,$fb,$exp) =@_;4627returnundefif$self->is_path_ignored($fb->{path});4628my$fh= $::_repository->temp_acquire('svn_delta');4629# $fh gets auto-closed() by SVN::TxDelta::apply(),4630# (but $base does not,) so dup() it for reading in close_file4631open my$dup,'<&',$fhor croak $!;4632my$base= $::_repository->temp_acquire('git_blob');46334634if($fb->{blob}) {4635my($base_is_link,$size);46364637if($fb->{mode_a}eq'120000'&&4638!$self->{empty_symlinks}->{$fb->{path}}) {4639print$base'link 'or die"print$!\n";4640$base_is_link=1;4641}4642 retry:4643$size= $::_repository->cat_blob($fb->{blob},$base);4644die"Failed to read object$fb->{blob}"if($size<0);46454646if(defined$exp) {4647seek$base,0,0or croak $!;4648my$got= ::md5sum($base);4649if($gotne$exp) {4650my$err="Checksum mismatch: ".4651"$fb->{path}$fb->{blob}\n".4652"expected:$exp\n".4653" got:$got\n";4654if($base_is_link) {4655warn$err,4656"Retrying... (possibly ",4657"a bad symlink from SVN)\n";4658$::_repository->temp_reset($base);4659$base_is_link=0;4660goto retry;4661}4662die$err;4663}4664}4665}4666seek$base,0,0or croak $!;4667$fb->{fh} =$fh;4668$fb->{base} =$base;4669[ SVN::TxDelta::apply($base,$dup,undef,$fb->{path},$fb->{pool}) ];4670}46714672sub close_file {4673my($self,$fb,$exp) =@_;4674returnundefif$self->is_path_ignored($fb->{path});46754676my$hash;4677my$path=$self->git_path($fb->{path});4678if(my$fh=$fb->{fh}) {4679if(defined$exp) {4680seek($fh,0,0)or croak $!;4681my$got= ::md5sum($fh);4682if($gotne$exp) {4683die"Checksum mismatch:$path\n",4684"expected:$exp\ngot:$got\n";4685}4686}4687if($fb->{mode_b} ==120000) {4688sysseek($fh,0,0)or croak $!;4689my$rd=sysread($fh,my$buf,5);46904691if(!defined$rd) {4692 croak "sysread:$!\n";4693}elsif($rd==0) {4694warn"$pathhas mode 120000",4695" but it points to nothing\n",4696"converting to an empty file with mode",4697" 100644\n";4698$fb->{mode_b} ='100644';4699}elsif($bufne'link ') {4700warn"$pathhas mode 120000",4701" but is not a link\n";4702}else{4703my$tmp_fh= $::_repository->temp_acquire(4704'svn_hash');4705my$res;4706while($res=sysread($fh,my$str,1024)) {4707my$out=syswrite($tmp_fh,$str,$res);4708defined($out) &&$out==$res4709or croak("write ",4710 Git::temp_path($tmp_fh),4711":$!\n");4712}4713defined$resor croak $!;47144715($fh,$tmp_fh) = ($tmp_fh,$fh);4716 Git::temp_release($tmp_fh,1);4717}4718}47194720$hash= $::_repository->hash_and_insert_object(4721 Git::temp_path($fh));4722$hash=~/^[a-f\d]{40}$/or die"not a sha1:$hash\n";47234724 Git::temp_release($fb->{base},1);4725 Git::temp_release($fh,1);4726}else{4727$hash=$fb->{blob}or die"no blob information\n";4728}4729$fb->{pool}->clear;4730$self->{gii}->update($fb->{mode_b},$hash,$path)or croak $!;4731print"\t$fb->{action}\t$path\n"if$fb->{action} && ! $::_q;4732undef;4733}47344735sub abort_edit {4736my$self=shift;4737$self->{nr} =$self->{gii}->{nr};4738delete$self->{gii};4739$self->SUPER::abort_edit(@_);4740}47414742sub close_edit {4743my$self=shift;47444745if($_preserve_empty_dirs) {4746my@empty_dirs;47474748# Any entry flagged as empty that also has an associated4749# dir_prop represents a newly created empty directory.4750foreachmy$i(keys%{$self->{empty}}) {4751push@empty_dirs,$iifexists$self->{dir_prop}->{$i};4752}47534754# Search for directories that have become empty due subsequent4755# file deletes.4756push@empty_dirs,$self->find_empty_directories();47574758# Finally, add a placeholder file to each empty directory.4759$self->add_placeholder_file($_)foreach(@empty_dirs);47604761$self->stash_placeholder_list();4762}47634764$self->{git_commit_ok} =1;4765$self->{nr} =$self->{gii}->{nr};4766delete$self->{gii};4767$self->SUPER::close_edit(@_);4768}47694770sub find_empty_directories {4771my($self) =@_;4772my@empty_dirs;4773my%dirs=map{ dirname($_) =>1}@deleted_gpath;47744775foreachmy$dir(sort keys%dirs) {4776next if$direq".";47774778# If there have been any additions to this directory, there is4779# no reason to check if it is empty.4780my$skip_added=0;4781foreachmy$t(qw/dir_prop file_prop/) {4782foreachmy$path(keys%{$self->{$t} }) {4783if(exists$self->{$t}->{dirname($path)}) {4784$skip_added=1;4785last;4786}4787}4788last if$skip_added;4789}4790next if$skip_added;47914792# Use `git ls-tree` to get the filenames of this directory4793# that existed prior to this particular commit.4794my$ls= command('ls-tree','-z','--name-only',4795$self->{c},"$dir/");4796my%files=map{$_=>1}split(/\0/,$ls);47974798# Remove the filenames that were deleted during this commit.4799delete$files{$_}foreach(@deleted_gpath);48004801# Report the directory if there are no filenames left.4802push@empty_dirs,$dirunless(scalar%files);4803}4804@empty_dirs;4805}48064807sub add_placeholder_file {4808my($self,$dir) =@_;4809my$path="$dir/$_placeholder_filename";4810my$gpath=$self->git_path($path);48114812my$fh= $::_repository->temp_acquire($gpath);4813my$hash= $::_repository->hash_and_insert_object(Git::temp_path($fh));4814 Git::temp_release($fh,1);4815$self->{gii}->update('100644',$hash,$gpath)or croak $!;48164817# The directory should no longer be considered empty.4818delete$self->{empty}->{$dir}ifexists$self->{empty}->{$dir};48194820# Keep track of any placeholder files we create.4821$added_placeholder{$dir} =$path;4822}48234824sub stash_placeholder_list {4825my($self) =@_;4826my$k="svn-remote.$repo_id.added-placeholder";4827my$v=eval{ command_oneline('config','--get-all',$k) };4828 command_noisy('config','--unset-all',$k)if$v;4829foreach(values%added_placeholder) {4830 command_noisy('config','--add',$k,$_);4831}4832}48334834package SVN::Git::Editor;4835use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;4836use strict;4837use warnings;4838use Carp qw/croak/;4839use IO::File;48404841sub new {4842my($class,$opts) =@_;4843foreach(qw/svn_path r ra tree_a tree_b log editor_cb/) {4844die"$_required!\n"unless(defined$opts->{$_});4845}48464847my$pool= SVN::Pool->new;4848my$mods= generate_diff($opts->{tree_a},$opts->{tree_b});4849my$types= check_diff_paths($opts->{ra},$opts->{svn_path},4850$opts->{r},$mods);48514852# $opts->{ra} functions should not be used after this:4853my@ce=$opts->{ra}->get_commit_editor($opts->{log},4854$opts->{editor_cb},$pool);4855my$self= SVN::Delta::Editor->new(@ce,$pool);4856bless$self,$class;4857foreach(qw/svn_path r tree_a tree_b/) {4858$self->{$_} =$opts->{$_};4859}4860$self->{url} =$opts->{ra}->{url};4861$self->{mods} =$mods;4862$self->{types} =$types;4863$self->{pool} =$pool;4864$self->{bat} = {''=>$self->open_root($self->{r},$self->{pool}) };4865$self->{rm} = { };4866$self->{path_prefix} =length$self->{svn_path} ?4867"$self->{svn_path}/":'';4868$self->{config} =$opts->{config};4869$self->{mergeinfo} =$opts->{mergeinfo};4870return$self;4871}48724873sub generate_diff {4874my($tree_a,$tree_b) =@_;4875my@diff_tree=qw(diff-tree -z -r);4876if($_cp_similarity) {4877push@diff_tree,"-C$_cp_similarity";4878}else{4879push@diff_tree,'-C';4880}4881push@diff_tree,'--find-copies-harder'if$_find_copies_harder;4882push@diff_tree,"-l$_rename_limit"ifdefined$_rename_limit;4883push@diff_tree,$tree_a,$tree_b;4884my($diff_fh,$ctx) = command_output_pipe(@diff_tree);4885local$/="\0";4886my$state='meta';4887my@mods;4888while(<$diff_fh>) {4889chomp$_;# this gets rid of the trailing "\0"4890if($stateeq'meta'&& /^:(\d{6})\s(\d{6})\s4891($::sha1)\s($::sha1)\s4892([MTCRAD])\d*$/xo) {4893push@mods, { mode_a =>$1, mode_b =>$2,4894 sha1_a =>$3, sha1_b =>$4,4895 chg =>$5};4896if($5=~/^(?:C|R)$/) {4897$state='file_a';4898}else{4899$state='file_b';4900}4901}elsif($stateeq'file_a') {4902my$x=$mods[$#mods]or croak "Empty array\n";4903if($x->{chg} !~/^(?:C|R)$/) {4904 croak "Error parsing$_,$x->{chg}\n";4905}4906$x->{file_a} =$_;4907$state='file_b';4908}elsif($stateeq'file_b') {4909my$x=$mods[$#mods]or croak "Empty array\n";4910if(exists$x->{file_a} &&$x->{chg} !~/^(?:C|R)$/) {4911 croak "Error parsing$_,$x->{chg}\n";4912}4913if(!exists$x->{file_a} &&$x->{chg} =~/^(?:C|R)$/) {4914 croak "Error parsing$_,$x->{chg}\n";4915}4916$x->{file_b} =$_;4917$state='meta';4918}else{4919 croak "Error parsing$_\n";4920}4921}4922 command_close_pipe($diff_fh,$ctx);4923 \@mods;4924}49254926sub check_diff_paths {4927my($ra,$pfx,$rev,$mods) =@_;4928my%types;4929$pfx.='/'iflength$pfx;49304931sub type_diff_paths {4932my($ra,$types,$path,$rev) =@_;4933my@p=split m#/+#, $path;4934my$c=shift@p;4935unless(defined$types->{$c}) {4936$types->{$c} =$ra->check_path($c,$rev);4937}4938while(@p) {4939$c.='/'.shift@p;4940next ifdefined$types->{$c};4941$types->{$c} =$ra->check_path($c,$rev);4942}4943}49444945foreachmy$m(@$mods) {4946foreachmy$f(qw/file_a file_b/) {4947next unlessdefined$m->{$f};4948my($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);4949if(length$pfx.$dir&& !defined$types{$dir}) {4950 type_diff_paths($ra, \%types,$pfx.$dir,$rev);4951}4952}4953}4954 \%types;4955}49564957sub split_path {4958return($_[0] =~ m#^(.*?)/?([^/]+)$#);4959}49604961sub repo_path {4962my($self,$path) =@_;4963if(my$enc=$self->{pathnameencoding}) {4964require Encode;4965 Encode::from_to($path,$enc,'UTF-8');4966}4967$self->{path_prefix}.(defined$path?$path:'');4968}49694970sub url_path {4971my($self,$path) =@_;4972if($self->{url} =~ m#^https?://#) {4973$path=~s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;4974}4975$self->{url} .'/'.$self->repo_path($path);4976}49774978sub rmdirs {4979my($self) =@_;4980my$rm=$self->{rm};4981delete$rm->{''};# we never delete the url we're tracking4982return unless%$rm;49834984foreach(keys%$rm) {4985my@d=split m#/#, $_;4986my$c=shift@d;4987$rm->{$c} =1;4988while(@d) {4989$c.='/'.shift@d;4990$rm->{$c} =1;4991}4992}4993delete$rm->{$self->{svn_path}};4994delete$rm->{''};# we never delete the url we're tracking4995return unless%$rm;49964997my($fh,$ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,4998$self->{tree_b});4999local$/="\0";5000while(<$fh>) {5001chomp;5002my@dn=split m#/#, $_;5003while(pop@dn) {5004delete$rm->{join'/',@dn};5005}5006unless(%$rm) {5007close$fh;5008return;5009}5010}5011 command_close_pipe($fh,$ctx);50125013my($r,$p,$bat) = ($self->{r},$self->{pool},$self->{bat});5014foreachmy$d(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {5015$self->close_directory($bat->{$d},$p);5016my($dn) = ($d=~ m#^(.*?)/?(?:[^/]+)$#);5017print"\tD+\t$d/\n"unless$::_q;5018$self->SUPER::delete_entry($d,$r,$bat->{$dn},$p);5019delete$bat->{$d};5020}5021}50225023sub open_or_add_dir {5024my($self,$full_path,$baton,$deletions) =@_;5025my$t=$self->{types}->{$full_path};5026if(!defined$t) {5027die"$full_pathnot known in r$self->{r} or we have a bug!\n";5028}5029{5030no warnings 'once';5031# SVN::Node::none and SVN::Node::file are used only once,5032# so we're shutting up Perl's warnings about them.5033if($t==$SVN::Node::none ||defined($deletions->{$full_path})) {5034return$self->add_directory($full_path,$baton,5035undef, -1,$self->{pool});5036}elsif($t==$SVN::Node::dir) {5037return$self->open_directory($full_path,$baton,5038$self->{r},$self->{pool});5039}# no warnings 'once'5040print STDERR "$full_pathalready exists in repository at ",5041"r$self->{r} and it is not a directory (",5042($t==$SVN::Node::file ?'file':'unknown'),"/$t)\n";5043}# no warnings 'once'5044exit1;5045}50465047sub ensure_path {5048my($self,$path,$deletions) =@_;5049my$bat=$self->{bat};5050my$repo_path=$self->repo_path($path);5051return$bat->{''}unless(length$repo_path);50525053my@p=split m#/+#, $repo_path;5054my$c=shift@p;5055$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{''},$deletions);5056while(@p) {5057my$c0=$c;5058$c.='/'.shift@p;5059$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{$c0},$deletions);5060}5061return$bat->{$c};5062}50635064# Subroutine to convert a globbing pattern to a regular expression.5065# From perl cookbook.5066sub glob2pat {5067my$globstr=shift;5068my%patmap= ('*'=>'.*','?'=>'.','['=>'[',']'=>']');5069$globstr=~s{(.)} { $patmap{$1}||"\Q$1"}ge;5070return'^'.$globstr.'$';5071}50725073sub check_autoprop {5074my($self,$pattern,$properties,$file,$fbat) =@_;5075# Convert the globbing pattern to a regular expression.5076my$regex= glob2pat($pattern);5077# Check if the pattern matches the file name.5078if($file=~m/($regex)/) {5079# Parse the list of properties to set.5080my@props=split(/;/,$properties);5081foreachmy$prop(@props) {5082# Parse 'name=value' syntax and set the property.5083if($prop=~/([^=]+)=(.*)/) {5084my($n,$v) = ($1,$2);5085for($n,$v) {5086s/^\s+//;s/\s+$//;5087}5088$self->change_file_prop($fbat,$n,$v);5089}5090}5091}5092}50935094sub apply_autoprops {5095my($self,$file,$fbat) =@_;5096my$conf_t= ${$self->{config}}{'config'};5097no warnings 'once';5098# Check [miscellany]/enable-auto-props in svn configuration.5099if(SVN::_Core::svn_config_get_bool(5100$conf_t,5101$SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY,5102$SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS,51030)) {5104# Auto-props are enabled. Enumerate them to look for matches.5105my$callback=sub{5106$self->check_autoprop($_[0],$_[1],$file,$fbat);5107};5108 SVN::_Core::svn_config_enumerate(5109$conf_t,5110$SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS,5111$callback);5112}5113}51145115sub A {5116my($self,$m,$deletions) =@_;5117my($dir,$file) = split_path($m->{file_b});5118my$pbat=$self->ensure_path($dir,$deletions);5119my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5120undef, -1);5121print"\tA\t$m->{file_b}\n"unless$::_q;5122$self->apply_autoprops($file,$fbat);5123$self->chg_file($fbat,$m);5124$self->close_file($fbat,undef,$self->{pool});5125}51265127sub C {5128my($self,$m,$deletions) =@_;5129my($dir,$file) = split_path($m->{file_b});5130my$pbat=$self->ensure_path($dir,$deletions);5131my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5132$self->url_path($m->{file_a}),$self->{r});5133print"\tC\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5134$self->chg_file($fbat,$m);5135$self->close_file($fbat,undef,$self->{pool});5136}51375138sub delete_entry {5139my($self,$path,$pbat) =@_;5140my$rpath=$self->repo_path($path);5141my($dir,$file) = split_path($rpath);5142$self->{rm}->{$dir} =1;5143$self->SUPER::delete_entry($rpath,$self->{r},$pbat,$self->{pool});5144}51455146sub R {5147my($self,$m,$deletions) =@_;5148my($dir,$file) = split_path($m->{file_b});5149my$pbat=$self->ensure_path($dir,$deletions);5150my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5151$self->url_path($m->{file_a}),$self->{r});5152print"\tR\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5153$self->apply_autoprops($file,$fbat);5154$self->chg_file($fbat,$m);5155$self->close_file($fbat,undef,$self->{pool});51565157($dir,$file) = split_path($m->{file_a});5158$pbat=$self->ensure_path($dir,$deletions);5159$self->delete_entry($m->{file_a},$pbat);5160}51615162sub M {5163my($self,$m,$deletions) =@_;5164my($dir,$file) = split_path($m->{file_b});5165my$pbat=$self->ensure_path($dir,$deletions);5166my$fbat=$self->open_file($self->repo_path($m->{file_b}),5167$pbat,$self->{r},$self->{pool});5168print"\t$m->{chg}\t$m->{file_b}\n"unless$::_q;5169$self->chg_file($fbat,$m);5170$self->close_file($fbat,undef,$self->{pool});5171}51725173sub T { shift->M(@_) }51745175sub change_file_prop {5176my($self,$fbat,$pname,$pval) =@_;5177$self->SUPER::change_file_prop($fbat,$pname,$pval,$self->{pool});5178}51795180sub change_dir_prop {5181my($self,$pbat,$pname,$pval) =@_;5182$self->SUPER::change_dir_prop($pbat,$pname,$pval,$self->{pool});5183}51845185sub _chg_file_get_blob ($$$$) {5186my($self,$fbat,$m,$which) =@_;5187my$fh= $::_repository->temp_acquire("git_blob_$which");5188if($m->{"mode_$which"} =~/^120/) {5189print$fh'link 'or croak $!;5190$self->change_file_prop($fbat,'svn:special','*');5191}elsif($m->{mode_a} =~/^120/&&$m->{"mode_$which"} !~/^120/) {5192$self->change_file_prop($fbat,'svn:special',undef);5193}5194my$blob=$m->{"sha1_$which"};5195return($fh,)if($blob=~/^0{40}$/);5196my$size= $::_repository->cat_blob($blob,$fh);5197 croak "Failed to read object$blob"if($size<0);5198$fh->flush==0or croak $!;5199seek$fh,0,0or croak $!;52005201my$exp= ::md5sum($fh);5202seek$fh,0,0or croak $!;5203return($fh,$exp);5204}52055206sub chg_file {5207my($self,$fbat,$m) =@_;5208if($m->{mode_b} =~/755$/&&$m->{mode_a} !~/755$/) {5209$self->change_file_prop($fbat,'svn:executable','*');5210}elsif($m->{mode_b} !~/755$/&&$m->{mode_a} =~/755$/) {5211$self->change_file_prop($fbat,'svn:executable',undef);5212}5213my($fh_a,$exp_a) = _chg_file_get_blob $self,$fbat,$m,'a';5214my($fh_b,$exp_b) = _chg_file_get_blob $self,$fbat,$m,'b';5215my$pool= SVN::Pool->new;5216my$atd=$self->apply_textdelta($fbat,$exp_a,$pool);5217if(-s $fh_a) {5218my$txstream= SVN::TxDelta::new ($fh_a,$fh_b,$pool);5219my$res= SVN::TxDelta::send_txstream($txstream,@$atd,$pool);5220if(defined$res) {5221die"Unexpected result from send_txstream:$res\n",5222"(SVN::Core::VERSION:$SVN::Core::VERSION)\n";5223}5224}else{5225my$got= SVN::TxDelta::send_stream($fh_b,@$atd,$pool);5226die"Checksum mismatch\nexpected:$exp_b\ngot:$got\n"5227if($gotne$exp_b);5228}5229 Git::temp_release($fh_b,1);5230 Git::temp_release($fh_a,1);5231$pool->clear;5232}52335234sub D {5235my($self,$m,$deletions) =@_;5236my($dir,$file) = split_path($m->{file_b});5237my$pbat=$self->ensure_path($dir,$deletions);5238print"\tD\t$m->{file_b}\n"unless$::_q;5239$self->delete_entry($m->{file_b},$pbat);5240}52415242sub close_edit {5243my($self) =@_;5244my($p,$bat) = ($self->{pool},$self->{bat});5245foreach(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {5246next if$_eq'';5247$self->close_directory($bat->{$_},$p);5248}5249$self->close_directory($bat->{''},$p);5250$self->SUPER::close_edit($p);5251$p->clear;5252}52535254sub abort_edit {5255my($self) =@_;5256$self->SUPER::abort_edit($self->{pool});5257}52585259sub DESTROY {5260my$self=shift;5261$self->SUPER::DESTROY(@_);5262$self->{pool}->clear;5263}52645265# this drives the editor5266sub apply_diff {5267my($self) =@_;5268my$mods=$self->{mods};5269my%o= ( D =>0, C =>1, R =>2, A =>3, M =>4, T =>5);5270my%deletions;52715272foreachmy$m(@$mods) {5273if($m->{chg}eq"D") {5274$deletions{$m->{file_b}} =1;5275}5276}52775278foreachmy$m(sort{$o{$a->{chg}} <=>$o{$b->{chg}} }@$mods) {5279my$f=$m->{chg};5280if(defined$o{$f}) {5281$self->$f($m, \%deletions);5282}else{5283 fatal("Invalid change type:$f");5284}5285}52865287if(defined($self->{mergeinfo})) {5288$self->change_dir_prop($self->{bat}{''},"svn:mergeinfo",5289$self->{mergeinfo});5290}5291$self->rmdirsif$_rmdir;5292if(@$mods==0&& !defined($self->{mergeinfo})) {5293$self->abort_edit;5294}else{5295$self->close_edit;5296}5297returnscalar@$mods;5298}52995300package Git::SVN::Ra;5301use vars qw/@ISA $config_dir $_ignore_refs_regex $_log_window_size/;5302use strict;5303use warnings;5304my($ra_invalid,$can_do_switch,%ignored_err,$RA);53055306BEGIN{5307# enforce temporary pool usage for some simple functions5308no strict 'refs';5309formy$f(qw/rev_proplist get_latest_revnum get_uuid get_repos_root5310 get_file/) {5311my$SUPER="SUPER::$f";5312*$f=sub{5313my$self=shift;5314my$pool= SVN::Pool->new;5315my@ret=$self->$SUPER(@_,$pool);5316$pool->clear;5317wantarray?@ret:$ret[0];5318};5319}5320}53215322sub _auth_providers () {5323my@rv= (5324 SVN::Client::get_simple_provider(),5325 SVN::Client::get_ssl_server_trust_file_provider(),5326 SVN::Client::get_simple_prompt_provider(5327 \&Git::SVN::Prompt::simple,2),5328 SVN::Client::get_ssl_client_cert_file_provider(),5329 SVN::Client::get_ssl_client_cert_prompt_provider(5330 \&Git::SVN::Prompt::ssl_client_cert,2),5331 SVN::Client::get_ssl_client_cert_pw_file_provider(),5332 SVN::Client::get_ssl_client_cert_pw_prompt_provider(5333 \&Git::SVN::Prompt::ssl_client_cert_pw,2),5334 SVN::Client::get_username_provider(),5335 SVN::Client::get_ssl_server_trust_prompt_provider(5336 \&Git::SVN::Prompt::ssl_server_trust),5337 SVN::Client::get_username_prompt_provider(5338 \&Git::SVN::Prompt::username,2)5339);53405341# earlier 1.6.x versions would segfault, and <= 1.5.x didn't have5342# this function5343if(::compare_svn_version('1.6.12') >0) {5344my$config= SVN::Core::config_get_config($config_dir);5345my($p,@a);5346# config_get_config returns all config files from5347# ~/.subversion, auth_get_platform_specific_client_providers5348# just wants the config "file".5349@a= ($config->{'config'},undef);5350$p= SVN::Core::auth_get_platform_specific_client_providers(@a);5351# Insert the return value from5352# auth_get_platform_specific_providers5353unshift@rv,@$p;5354}5355 \@rv;5356}53575358sub escape_uri_only {5359my($uri) =@_;5360my@tmp;5361foreach(splitm{/},$uri) {5362s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;5363push@tmp,$_;5364}5365join('/',@tmp);5366}53675368sub escape_url {5369my($url) =@_;5370if($url=~ m#^(https?)://([^/]+)(.*)$#) {5371my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));5372$url="$scheme://$domain$uri";5373}5374$url;5375}53765377sub new {5378my($class,$url) =@_;5379$url=~s!/+$!!;5380return$RAif($RA&&$RA->{url}eq$url);53815382::_req_svn();53835384 SVN::_Core::svn_config_ensure($config_dir,undef);5385my($baton,$callbacks) = SVN::Core::auth_open_helper(_auth_providers);5386my$config= SVN::Core::config_get_config($config_dir);5387$RA=undef;5388my$dont_store_passwords=1;5389my$conf_t= ${$config}{'config'};5390{5391no warnings 'once';5392# The usage of $SVN::_Core::SVN_CONFIG_* variables5393# produces warnings that variables are used only once.5394# I had not found the better way to shut them up, so5395# the warnings of type 'once' are disabled in this block.5396if(SVN::_Core::svn_config_get_bool($conf_t,5397$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5398$SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS,53991) ==0) {5400 SVN::_Core::svn_auth_set_parameter($baton,5401$SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS,5402bless(\$dont_store_passwords,"_p_void"));5403}5404if(SVN::_Core::svn_config_get_bool($conf_t,5405$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5406$SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS,54071) ==0) {5408$Git::SVN::Prompt::_no_auth_cache =1;5409}5410}# no warnings 'once'5411my$self= SVN::Ra->new(url => escape_url($url), auth =>$baton,5412 config =>$config,5413 pool => SVN::Pool->new,5414 auth_provider_callbacks =>$callbacks);5415$self->{url} =$url;5416$self->{svn_path} =$url;5417$self->{repos_root} =$self->get_repos_root;5418$self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;5419$self->{cache} = { check_path => { r =>0, data => {} },5420 get_dir => { r =>0, data => {} } };5421$RA=bless$self,$class;5422}54235424sub check_path {5425my($self,$path,$r) =@_;5426my$cache=$self->{cache}->{check_path};5427if($r==$cache->{r} &&exists$cache->{data}->{$path}) {5428return$cache->{data}->{$path};5429}5430my$pool= SVN::Pool->new;5431my$t=$self->SUPER::check_path($path,$r,$pool);5432$pool->clear;5433if($r!=$cache->{r}) {5434%{$cache->{data}} = ();5435$cache->{r} =$r;5436}5437$cache->{data}->{$path} =$t;5438}54395440sub get_dir {5441my($self,$dir,$r) =@_;5442my$cache=$self->{cache}->{get_dir};5443if($r==$cache->{r}) {5444if(my$x=$cache->{data}->{$dir}) {5445returnwantarray?@$x:$x->[0];5446}5447}5448my$pool= SVN::Pool->new;5449my($d,undef,$props) =$self->SUPER::get_dir($dir,$r,$pool);5450my%dirents=map{$_=> { kind =>$d->{$_}->kind} }keys%$d;5451$pool->clear;5452if($r!=$cache->{r}) {5453%{$cache->{data}} = ();5454$cache->{r} =$r;5455}5456$cache->{data}->{$dir} = [ \%dirents,$r,$props];5457wantarray? (\%dirents,$r,$props) : \%dirents;5458}54595460sub DESTROY {5461# do not call the real DESTROY since we store ourselves in $RA5462}54635464# get_log(paths, start, end, limit,5465# discover_changed_paths, strict_node_history, receiver)5466sub get_log {5467my($self,@args) =@_;5468my$pool= SVN::Pool->new;54695470# svn_log_changed_path_t objects passed to get_log are likely to be5471# overwritten even if only the refs are copied to an external variable,5472# so we should dup the structures in their entirety. Using an5473# externally passed pool (instead of our temporary and quickly cleared5474# pool in Git::SVN::Ra) does not help matters at all...5475my$receiver=pop@args;5476my$prefix="/".$self->{svn_path};5477$prefix=~ s#/+($)##;5478my$prefix_regex= qr#^\Q$prefix\E#;5479push(@args,sub{5480my($paths) =$_[0];5481return&$receiver(@_)unless$paths;5482$_[0] = ();5483foreachmy$p(keys%$paths) {5484my$i=$paths->{$p};5485# Make path relative to our url, not repos_root5486$p=~s/$prefix_regex//;5487my%s=map{$_=>$i->$_; }5488 qw/copyfrom_path copyfrom_rev action/;5489if($s{'copyfrom_path'}) {5490$s{'copyfrom_path'} =~s/$prefix_regex//;5491}5492$_[0]{$p} = \%s;5493}5494&$receiver(@_);5495});549654975498# the limit parameter was not supported in SVN 1.1.x, so we5499# drop it. Therefore, the receiver callback passed to it5500# is made aware of this limitation by being wrapped if5501# the limit passed to is being wrapped.5502if(::compare_svn_version('1.2.0') <=0) {5503my$limit=splice(@args,3,1);5504if($limit>0) {5505my$receiver=pop@args;5506push(@args,sub{ &$receiver(@_)if(--$limit>=0) });5507}5508}5509my$ret=$self->SUPER::get_log(@args,$pool);5510$pool->clear;5511$ret;5512}55135514sub trees_match {5515my($self,$url1,$rev1,$url2,$rev2) =@_;5516my$ctx= SVN::Client->new(auth => _auth_providers);5517my$out= IO::File->new_tmpfile;55185519# older SVN (1.1.x) doesn't take $pool as the last parameter for5520# $ctx->diff(), so we'll create a default one5521my$pool= SVN::Pool->new_default_sub;55225523$ra_invalid=1;# this will open a new SVN::Ra connection to $url15524$ctx->diff([],$url1,$rev1,$url2,$rev2,1,1,0,$out,$out);5525$out->flush;5526my$ret= (($out->stat)[7] ==0);5527close$outor croak $!;55285529$ret;5530}55315532sub get_commit_editor {5533my($self,$log,$cb,$pool) =@_;55345535my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef,0) : ();5536$self->SUPER::get_commit_editor($log,$cb,@lock,$pool);5537}55385539sub gs_do_update {5540my($self,$rev_a,$rev_b,$gs,$editor) =@_;5541my$new= ($rev_a==$rev_b);5542my$path=$gs->{path};55435544if($new&& -e $gs->{index}) {5545unlink$gs->{index}or die5546"Couldn't unlink index:$gs->{index}:$!\n";5547}5548my$pool= SVN::Pool->new;5549$editor->set_path_strip($path);5550my(@pc) =split m#/#, $path;5551my$reporter=$self->do_update($rev_b, (@pc?shift@pc:''),55521,$editor,$pool);5553my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef) : ();55545555# Since we can't rely on svn_ra_reparent being available, we'll5556# just have to do some magic with set_path to make it so5557# we only want a partial path.5558my$sp='';5559my$final=join('/',@pc);5560while(@pc) {5561$reporter->set_path($sp,$rev_b,0,@lock,$pool);5562$sp.='/'iflength$sp;5563$sp.=shift@pc;5564}5565die"BUG: '$sp' != '$final'\n"if($spne$final);55665567$reporter->set_path($sp,$rev_a,$new,@lock,$pool);55685569$reporter->finish_report($pool);5570$pool->clear;5571$editor->{git_commit_ok};5572}55735574# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and5575# svn_ra_reparent didn't work before 1.4)5576sub gs_do_switch {5577my($self,$rev_a,$rev_b,$gs,$url_b,$editor) =@_;5578my$path=$gs->{path};5579my$pool= SVN::Pool->new;55805581my$full_url=$self->{url};5582my$old_url=$full_url;5583$full_url.='/'.$pathiflength$path;5584my($ra,$reparented);55855586if($old_url=~ m#^svn(\+ssh)?://# ||5587($full_url=~ m#^https?://# &&5588 escape_url($full_url)ne$full_url)) {5589$_[0] =undef;5590$self=undef;5591$RA=undef;5592$ra= Git::SVN::Ra->new($full_url);5593$ra_invalid=1;5594}elsif($old_urlne$full_url) {5595 SVN::_Ra::svn_ra_reparent($self->{session},$full_url,$pool);5596$self->{url} =$full_url;5597$reparented=1;5598}55995600$ra||=$self;5601$url_b= escape_url($url_b);5602my$reporter=$ra->do_switch($rev_b,'',1,$url_b,$editor,$pool);5603my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef) : ();5604$reporter->set_path('',$rev_a,0,@lock,$pool);5605$reporter->finish_report($pool);56065607if($reparented) {5608 SVN::_Ra::svn_ra_reparent($self->{session},$old_url,$pool);5609$self->{url} =$old_url;5610}56115612$pool->clear;5613$editor->{git_commit_ok};5614}56155616sub longest_common_path {5617my($gsv,$globs) =@_;5618my%common;5619my$common_max=scalar@$gsv;56205621foreachmy$gs(@$gsv) {5622my@tmp=split m#/#, $gs->{path};5623my$p='';5624foreach(@tmp) {5625$p.=length($p) ?"/$_":$_;5626$common{$p} ||=0;5627$common{$p}++;5628}5629}5630$globs||= [];5631$common_max+=scalar@$globs;5632foreachmy$glob(@$globs) {5633my@tmp=split m#/#, $glob->{path}->{left};5634my$p='';5635foreach(@tmp) {5636$p.=length($p) ?"/$_":$_;5637$common{$p} ||=0;5638$common{$p}++;5639}5640}56415642my$longest_path='';5643foreach(sort{length$b<=>length$a}keys%common) {5644if($common{$_} ==$common_max) {5645$longest_path=$_;5646last;5647}5648}5649$longest_path;5650}56515652sub gs_fetch_loop_common {5653my($self,$base,$head,$gsv,$globs) =@_;5654return if($base>$head);5655my$inc=$_log_window_size;5656my($min,$max) = ($base,$head<$base+$inc?$head:$base+$inc);5657my$longest_path= longest_common_path($gsv,$globs);5658my$ra_url=$self->{url};5659my$find_trailing_edge;5660while(1) {5661my%revs;5662my$err;5663my$err_handler=$SVN::Error::handler;5664$SVN::Error::handler =sub{5665($err) =@_;5666 skip_unknown_revs($err);5667};5668sub _cb {5669my($paths,$r,$author,$date,$log) =@_;5670[$paths,5671{ author =>$author, date =>$date,log=>$log} ];5672}5673$self->get_log([$longest_path],$min,$max,0,1,1,5674sub{$revs{$_[1]} = _cb(@_) });5675if($err) {5676print"Checked through r$max\r";5677}else{5678$find_trailing_edge=1;5679}5680if($errand$find_trailing_edge) {5681print STDERR "Path '$longest_path' ",5682"was probably deleted:\n",5683$err->expanded_message,5684"\nWill attempt to follow ",5685"revisions r$min.. r$max",5686"committed before the deletion\n";5687my$hi=$max;5688while(--$hi>=$min) {5689my$ok;5690$self->get_log([$longest_path],$min,$hi,56910,1,1,sub{5692$ok=$_[1];5693$revs{$_[1]} = _cb(@_) });5694if($ok) {5695print STDERR "r$min.. r$okOK\n";5696last;5697}5698}5699$find_trailing_edge=0;5700}5701$SVN::Error::handler =$err_handler;57025703my%exists=map{$_->{path} =>$_}@$gsv;5704foreachmy$r(sort{$a<=>$b}keys%revs) {5705my($paths,$logged) = @{$revs{$r}};57065707foreachmy$gs($self->match_globs(\%exists,$paths,5708$globs,$r)) {5709if($gs->rev_map_max>=$r) {5710next;5711}5712next unless$gs->match_paths($paths,$r);5713$gs->{logged_rev_props} =$logged;5714if(my$last_commit=$gs->last_commit) {5715$gs->assert_index_clean($last_commit);5716}5717my$log_entry=$gs->do_fetch($paths,$r);5718if($log_entry) {5719$gs->do_git_commit($log_entry);5720}5721$INDEX_FILES{$gs->{index}} =1;5722}5723foreachmy$g(@$globs) {5724my$k="svn-remote.$g->{remote}.".5725"$g->{t}-maxRev";5726 Git::SVN::tmp_config($k,$r);5727}5728if($ra_invalid) {5729$_[0] =undef;5730$self=undef;5731$RA=undef;5732$self= Git::SVN::Ra->new($ra_url);5733$ra_invalid=undef;5734}5735}5736# pre-fill the .rev_db since it'll eventually get filled in5737# with '0' x40 if something new gets committed5738foreachmy$gs(@$gsv) {5739next if$gs->rev_map_max>=$max;5740next ifdefined$gs->rev_map_get($max);5741$gs->rev_map_set($max,0 x40);5742}5743foreachmy$g(@$globs) {5744my$k="svn-remote.$g->{remote}.$g->{t}-maxRev";5745 Git::SVN::tmp_config($k,$max);5746}5747last if$max>=$head;5748$min=$max+1;5749$max+=$inc;5750$max=$headif($max>$head);5751}5752 Git::SVN::gc();5753}57545755sub get_dir_globbed {5756my($self,$left,$depth,$r) =@_;57575758my@x=eval{$self->get_dir($left,$r) };5759return unlessscalar@x==3;5760my$dirents=$x[0];5761my@finalents;5762foreachmy$de(keys%$dirents) {5763next if$dirents->{$de}->{kind} !=$SVN::Node::dir;5764if($depth>1) {5765my@args= ("$left/$de",$depth-1,$r);5766foreachmy$dir($self->get_dir_globbed(@args)) {5767push@finalents,"$de/$dir";5768}5769}else{5770push@finalents,$de;5771}5772}5773@finalents;5774}57755776# return value: 0 -- don't ignore, 1 -- ignore5777sub is_ref_ignored {5778my($g,$p) =@_;5779my$refname=$g->{ref}->full_path($p);5780return1ifdefined($g->{ignore_refs_regex}) &&5781$refname=~m!$g->{ignore_refs_regex}!;5782return0unlessdefined($_ignore_refs_regex);5783return1if$refname=~m!$_ignore_refs_regex!o;5784return0;5785}57865787sub match_globs {5788my($self,$exists,$paths,$globs,$r) =@_;57895790sub get_dir_check {5791my($self,$exists,$g,$r) =@_;57925793my@dirs=$self->get_dir_globbed($g->{path}->{left},5794$g->{path}->{depth},5795$r);57965797foreachmy$de(@dirs) {5798my$p=$g->{path}->full_path($de);5799next if$exists->{$p};5800next if(length$g->{path}->{right} &&5801($self->check_path($p,$r) !=5802$SVN::Node::dir));5803next unless$p=~/$g->{path}->{regex}/;5804$exists->{$p} = Git::SVN->init($self->{url},$p,undef,5805$g->{ref}->full_path($de),1);5806}5807}5808foreachmy$g(@$globs) {5809if(my$path=$paths->{"/$g->{path}->{left}"}) {5810if($path->{action} =~/^[AR]$/) {5811 get_dir_check($self,$exists,$g,$r);5812}5813}5814foreach(keys%$paths) {5815if(/$g->{path}->{left_regex}/&&5816!/$g->{path}->{regex}/) {5817next if$paths->{$_}->{action} !~/^[AR]$/;5818 get_dir_check($self,$exists,$g,$r);5819}5820next unless/$g->{path}->{regex}/;5821my$p=$1;5822my$pathname=$g->{path}->full_path($p);5823next if is_ref_ignored($g,$p);5824next if$exists->{$pathname};5825next if($self->check_path($pathname,$r) !=5826$SVN::Node::dir);5827$exists->{$pathname} = Git::SVN->init(5828$self->{url},$pathname,undef,5829$g->{ref}->full_path($p),1);5830}5831my$c='';5832foreach(split m#/#, $g->{path}->{left}) {5833$c.="/$_";5834next unless($paths->{$c} &&5835($paths->{$c}->{action} =~/^[AR]$/));5836 get_dir_check($self,$exists,$g,$r);5837}5838}5839values%$exists;5840}58415842sub minimize_url {5843my($self) =@_;5844return$self->{url}if($self->{url}eq$self->{repos_root});5845my$url=$self->{repos_root};5846my@components=split(m!/!,$self->{svn_path});5847my$c='';5848do{5849$url.="/$c"iflength$c;5850eval{5851my$ra= (ref$self)->new($url);5852my$latest=$ra->get_latest_revnum;5853$ra->get_log("",$latest,0,1,0,1,sub{});5854};5855}while($@&& ($c=shift@components));5856$url;5857}58585859sub can_do_switch {5860my$self=shift;5861unless(defined$can_do_switch) {5862my$pool= SVN::Pool->new;5863my$rep=eval{5864$self->do_switch(1,'',0,$self->{url},5865 SVN::Delta::Editor->new,$pool);5866};5867if($@) {5868$can_do_switch=0;5869}else{5870$rep->abort_report($pool);5871$can_do_switch=1;5872}5873$pool->clear;5874}5875$can_do_switch;5876}58775878sub skip_unknown_revs {5879my($err) =@_;5880my$errno=$err->apr_err();5881# Maybe the branch we're tracking didn't5882# exist when the repo started, so it's5883# not an error if it doesn't, just continue5884#5885# Wonderfully consistent library, eh?5886# 160013 - svn:// and file://5887# 175002 - http(s)://5888# 175007 - http(s):// (this repo required authorization, too...)5889# More codes may be discovered later...5890if($errno==175007||$errno==175002||$errno==160013) {5891my$err_key=$err->expanded_message;5892# revision numbers change every time, filter them out5893$err_key=~s/\d+/\0/g;5894$err_key="$errno\0$err_key";5895unless($ignored_err{$err_key}) {5896warn"W: Ignoring error from SVN, path probably ",5897"does not exist: ($errno): ",5898$err->expanded_message,"\n";5899warn"W: Do not be alarmed at the above message ",5900"git-svn is just searching aggressively for ",5901"old history.\n",5902"This may take a while on large repositories\n";5903$ignored_err{$err_key} =1;5904}5905return;5906}5907die"Error from SVN, ($errno): ",$err->expanded_message,"\n";5908}59095910package Git::SVN::Log;5911use strict;5912use warnings;5913use POSIX qw/strftime/;5914useconstant commit_log_separator => ('-' x 72) ."\n";5915use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline5916%rusers $show_commit $incremental/;5917my$l_fmt;59185919sub cmt_showable {5920my($c) =@_;5921return1ifdefined$c->{r};59225923# big commit message got truncated by the 16k pretty buffer in rev-list5924if($c->{l} &&$c->{l}->[-1]eq"...\n"&&5925$c->{a_raw} =~/\@([a-f\d\-]+)>$/) {5926@{$c->{l}} = ();5927my@log= command(qw/cat-file commit/,$c->{c});59285929# shift off the headers5930shift@logwhile($log[0]ne'');5931shift@log;59325933# TODO: make $c->{l} not have a trailing newline in the future5934@{$c->{l}} =map{"$_\n"}grep!/^git-svn-id: /,@log;59355936(undef,$c->{r},undef) = ::extract_metadata(5937(grep(/^git-svn-id: /,@log))[-1]);5938}5939returndefined$c->{r};5940}59415942sub log_use_color {5943return$color|| Git->repository->get_colorbool('color.diff');5944}59455946sub git_svn_log_cmd {5947my($r_min,$r_max,@args) =@_;5948my$head='HEAD';5949my(@files,@log_opts);5950foreachmy$x(@args) {5951if($xeq'--'||@files) {5952push@files,$x;5953}else{5954if(::verify_ref("$x^0")) {5955$head=$x;5956}else{5957push@log_opts,$x;5958}5959}5960}59615962my($url,$rev,$uuid,$gs) = ::working_head_info($head);5963$gs||= Git::SVN->_new;5964my@cmd= (qw/log --abbrev-commit --pretty=raw --default/,5965$gs->refname);5966push@cmd,'-r'unless$non_recursive;5967push@cmd, qw/--raw --name-status/if$verbose;5968push@cmd,'--color'if log_use_color();5969push@cmd,@log_opts;5970if(defined$r_max&&$r_max==$r_min) {5971push@cmd,'--max-count=1';5972if(my$c=$gs->rev_map_get($r_max)) {5973push@cmd,$c;5974}5975}elsif(defined$r_max) {5976if($r_max<$r_min) {5977($r_min,$r_max) = ($r_max,$r_min);5978}5979my(undef,$c_max) =$gs->find_rev_before($r_max,1,$r_min);5980my(undef,$c_min) =$gs->find_rev_after($r_min,1,$r_max);5981# If there are no commits in the range, both $c_max and $c_min5982# will be undefined. If there is at least 1 commit in the5983# range, both will be defined.5984return()if!defined$c_min|| !defined$c_max;5985if($c_mineq$c_max) {5986push@cmd,'--max-count=1',$c_min;5987}else{5988push@cmd,'--boundary',"$c_min..$c_max";5989}5990}5991return(@cmd,@files);5992}59935994# adapted from pager.c5995sub config_pager {5996if(! -t *STDOUT) {5997$ENV{GIT_PAGER_IN_USE} ='false';5998$pager=undef;5999return;6000}6001chomp($pager= command_oneline(qw(var GIT_PAGER)));6002if($pagereq'cat') {6003$pager=undef;6004}6005$ENV{GIT_PAGER_IN_USE} =defined($pager);6006}60076008sub run_pager {6009return unlessdefined$pager;6010pipe my($rfd,$wfd)orreturn;6011defined(my$pid=fork)or::fatal "Can't fork:$!";6012if(!$pid) {6013open STDOUT,'>&',$wfdor6014::fatal "Can't redirect to stdout:$!";6015return;6016}6017open STDIN,'<&',$rfdor::fatal "Can't redirect stdin:$!";6018$ENV{LESS} ||='FRSX';6019exec$pageror::fatal "Can't run pager:$!($pager)";6020}60216022sub format_svn_date {6023my$t=shift||time;6024my$gmoff= Git::SVN::get_tz($t);6025return strftime("%Y-%m-%d%H:%M:%S$gmoff(%a,%d%b%Y)",localtime($t));6026}60276028sub parse_git_date {6029my($t,$tz) =@_;6030# Date::Parse isn't in the standard Perl distro :(6031if($tz=~s/^\+//) {6032$t+= tz_to_s_offset($tz);6033}elsif($tz=~s/^\-//) {6034$t-= tz_to_s_offset($tz);6035}6036return$t;6037}60386039sub set_local_timezone {6040if(defined$TZ) {6041$ENV{TZ} =$TZ;6042}else{6043delete$ENV{TZ};6044}6045}60466047sub tz_to_s_offset {6048my($tz) =@_;6049$tz=~s/(\d\d)$//;6050return($1*60) + ($tz*3600);6051}60526053sub get_author_info {6054my($dest,$author,$t,$tz) =@_;6055$author=~s/(?:^\s*|\s*$)//g;6056$dest->{a_raw} =$author;6057my$au;6058if($::_authors) {6059$au=$rusers{$author} ||undef;6060}6061if(!$au) {6062($au) = ($author=~/<([^>]+)\@[^>]+>$/);6063}6064$dest->{t} =$t;6065$dest->{tz} =$tz;6066$dest->{a} =$au;6067$dest->{t_utc} = parse_git_date($t,$tz);6068}60696070sub process_commit {6071my($c,$r_min,$r_max,$defer) =@_;6072if(defined$r_min&&defined$r_max) {6073if($r_min==$c->{r} &&$r_min==$r_max) {6074 show_commit($c);6075return0;6076}6077return1if$r_min==$r_max;6078if($r_min<$r_max) {6079# we need to reverse the print order6080return0if(defined$limit&& --$limit<0);6081push@$defer,$c;6082return1;6083}6084if($r_min!=$r_max) {6085return1if($r_min<$c->{r});6086return1if($r_max>$c->{r});6087}6088}6089return0if(defined$limit&& --$limit<0);6090 show_commit($c);6091return1;6092}60936094sub show_commit {6095my$c=shift;6096if($oneline) {6097my$x="\n";6098if(my$l=$c->{l}) {6099while($l->[0] =~/^\s*$/) {shift@$l}6100$x=$l->[0];6101}6102$l_fmt||='A'.length($c->{r});6103print'r',pack($l_fmt,$c->{r}),' | ';6104print"$c->{c} | "if$show_commit;6105print$x;6106}else{6107 show_commit_normal($c);6108}6109}61106111sub show_commit_changed_paths {6112my($c) =@_;6113return unless$c->{changed};6114print"Changed paths:\n", @{$c->{changed}};6115}61166117sub show_commit_normal {6118my($c) =@_;6119print commit_log_separator,"r$c->{r} | ";6120print"$c->{c} | "if$show_commit;6121print"$c->{a} | ", format_svn_date($c->{t_utc}),' | ';6122my$nr_line=0;61236124if(my$l=$c->{l}) {6125while($l->[$#$l]eq"\n"&&$#$l>06126&&$l->[($#$l-1)]eq"\n") {6127pop@$l;6128}6129$nr_line=scalar@$l;6130if(!$nr_line) {6131print"1 line\n\n\n";6132}else{6133if($nr_line==1) {6134$nr_line='1 line';6135}else{6136$nr_line.=' lines';6137}6138print$nr_line,"\n";6139 show_commit_changed_paths($c);6140print"\n";6141print$_foreach@$l;6142}6143}else{6144print"1 line\n";6145 show_commit_changed_paths($c);6146print"\n";61476148}6149foreachmy$x(qw/raw stat diff/) {6150if($c->{$x}) {6151print"\n";6152print$_foreach@{$c->{$x}}6153}6154}6155}61566157sub cmd_show_log {6158my(@args) =@_;6159my($r_min,$r_max);6160my$r_last= -1;# prevent dupes6161 set_local_timezone();6162if(defined$::_revision) {6163if($::_revision =~/^(\d+):(\d+)$/) {6164($r_min,$r_max) = ($1,$2);6165}elsif($::_revision =~/^\d+$/) {6166$r_min=$r_max= $::_revision;6167}else{6168::fatal "-r$::_revision is not supported, use ",6169"standard 'git log' arguments instead";6170}6171}61726173 config_pager();6174@args= git_svn_log_cmd($r_min,$r_max,@args);6175if(!@args) {6176print commit_log_separator unless$incremental||$oneline;6177return;6178}6179my$log= command_output_pipe(@args);6180 run_pager();6181my(@k,$c,$d,$stat);6182my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;6183while(<$log>) {6184if(/^${esc_color}commit (?:- )?($::sha1_short)/o) {6185my$cmt=$1;6186if($c&& cmt_showable($c) &&$c->{r} !=$r_last) {6187$r_last=$c->{r};6188 process_commit($c,$r_min,$r_max, \@k)or6189goto out;6190}6191$d=undef;6192$c= { c =>$cmt};6193}elsif(/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {6194 get_author_info($c,$1,$2,$3);6195}elsif(/^${esc_color}(?:tree|parent|committer) /o) {6196# ignore6197}elsif(/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {6198push@{$c->{raw}},$_;6199}elsif(/^${esc_color}[ACRMDT]\t/) {6200# we could add $SVN->{svn_path} here, but that requires6201# remote access at the moment (repo_path_split)...6202 s#^(${esc_color})([ACRMDT])\t#$1 $2 #o;6203push@{$c->{changed}},$_;6204}elsif(/^${esc_color}diff /o) {6205$d=1;6206push@{$c->{diff}},$_;6207}elsif($d) {6208push@{$c->{diff}},$_;6209}elsif(/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*6210$esc_color*[\+\-]*$esc_color$/x) {6211$stat=1;6212push@{$c->{stat}},$_;6213}elsif($stat&&/^ \d+ files changed, \d+ insertions/) {6214push@{$c->{stat}},$_;6215$stat=undef;6216}elsif(/^${esc_color} (git-svn-id:.+)$/o) {6217($c->{url},$c->{r},undef) = ::extract_metadata($1);6218}elsif(s/^${esc_color} //o) {6219push@{$c->{l}},$_;6220}6221}6222if($c&&defined$c->{r} &&$c->{r} !=$r_last) {6223$r_last=$c->{r};6224 process_commit($c,$r_min,$r_max, \@k);6225}6226if(@k) {6227($r_min,$r_max) = ($r_max,$r_min);6228 process_commit($_,$r_min,$r_max)foreachreverse@k;6229}6230out:6231close$log;6232print commit_log_separator unless$incremental||$oneline;6233}62346235sub cmd_blame {6236my$path=pop;62376238 config_pager();6239 run_pager();62406241my($fh,$ctx,$rev);62426243if($_git_format) {6244($fh,$ctx) = command_output_pipe('blame',@_,$path);6245while(my$line= <$fh>) {6246if($line=~/^\^?([[:xdigit:]]+)\s/) {6247# Uncommitted edits show up as a rev ID of6248# all zeros, which we can't look up with6249# cmt_metadata6250if($1!~/^0+$/) {6251(undef,$rev,undef) =6252::cmt_metadata($1);6253$rev='0'if(!$rev);6254}else{6255$rev='0';6256}6257$rev=sprintf('%-10s',$rev);6258$line=~s/^\^?[[:xdigit:]]+(\s)/$rev$1/;6259}6260print$line;6261}6262}else{6263($fh,$ctx) = command_output_pipe('blame','-p',@_,'HEAD',6264'--',$path);6265my($sha1);6266my%authors;6267my@buffer;6268my%dsha;#distinct sha keys62696270while(my$line= <$fh>) {6271push@buffer,$line;6272if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6273$dsha{$1} =1;6274}6275}62766277my$s2r= ::cmt_sha2rev_batch([keys%dsha]);62786279foreachmy$line(@buffer) {6280if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6281$rev=$s2r->{$1};6282$rev='0'if(!$rev)6283}6284elsif($line=~/^author (.*)/) {6285$authors{$rev} =$1;6286$authors{$rev} =~s/\s/_/g;6287}6288elsif($line=~/^\t(.*)$/) {6289printf("%6s%10s%s\n",$rev,$authors{$rev},$1);6290}6291}6292}6293 command_close_pipe($fh,$ctx);6294}62956296package Git::SVN::Migration;6297# these version numbers do NOT correspond to actual version numbers6298# of git nor git-svn. They are just relative.6299#6300# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD6301#6302# v1 layout: .git/$id/info/url, refs/remotes/$id6303#6304# v2 layout: .git/svn/$id/info/url, refs/remotes/$id6305#6306# v3 layout: .git/svn/$id, refs/remotes/$id6307# - info/url may remain for backwards compatibility6308# - this is what we migrate up to this layout automatically,6309# - this will be used by git svn init on single branches6310# v3.1 layout (auto migrated):6311# - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink6312# for backwards compatibility6313#6314# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id6315# - this is only created for newly multi-init-ed6316# repositories. Similar in spirit to the6317# --use-separate-remotes option in git-clone (now default)6318# - we do not automatically migrate to this (following6319# the example set by core git)6320#6321# v5 layout: .rev_db.$UUID => .rev_map.$UUID6322# - newer, more-efficient format that uses 24-bytes per record6323# with no filler space.6324# - use xxd -c24 < .rev_map.$UUID to view and debug6325# - This is a one-way migration, repositories updated to the6326# new format will not be able to use old git-svn without6327# rebuilding the .rev_db. Rebuilding the rev_db is not6328# possible if noMetadata or useSvmProps are set; but should6329# be no problem for users that use the (sensible) defaults.6330use strict;6331use warnings;6332use Carp qw/croak/;6333use File::Path qw/mkpath/;6334use File::Basename qw/dirname basename/;6335use vars qw/$_minimize/;63366337sub migrate_from_v0 {6338my$git_dir=$ENV{GIT_DIR};6339returnundefunless-d $git_dir;6340my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6341my$migrated=0;6342while(<$fh>) {6343chomp;6344my($id,$orig_ref) = ($_,$_);6345next unless$id=~ s#^refs/heads/(.+)-HEAD$#$1#;6346next unless-f "$git_dir/$id/info/url";6347my$new_ref="refs/remotes/$id";6348if(::verify_ref("$new_ref^0")) {6349print STDERR "W:$orig_refis probably an old ",6350"branch used by an ancient version of ",6351"git-svn.\n",6352"However,$new_refalso exists.\n",6353"We will not be able ",6354"to use this branch until this ",6355"ambiguity is resolved.\n";6356next;6357}6358print STDERR "Migrating from v0 layout...\n"if!$migrated;6359print STDERR "Renaming ref:$orig_ref=>$new_ref\n";6360 command_noisy('update-ref',$new_ref,$orig_ref);6361 command_noisy('update-ref','-d',$orig_ref,$orig_ref);6362$migrated++;6363}6364 command_close_pipe($fh,$ctx);6365print STDERR "Done migrating from v0 layout...\n"if$migrated;6366$migrated;6367}63686369sub migrate_from_v1 {6370my$git_dir=$ENV{GIT_DIR};6371my$migrated=0;6372return$migratedunless-d $git_dir;6373my$svn_dir="$git_dir/svn";63746375# just in case somebody used 'svn' as their $id at some point...6376return$migratedif-d $svn_dir&& ! -f "$svn_dir/info/url";63776378print STDERR "Migrating from a git-svn v1 layout...\n";6379 mkpath([$svn_dir]);6380print STDERR "Data from a previous version of git-svn exists, but\n\t",6381"$svn_dir\n\t(required for this version ",6382"($::VERSION) of git-svn) does not exist.\n";6383my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6384while(<$fh>) {6385my$x=$_;6386next unless$x=~ s#^refs/remotes/##;6387chomp$x;6388next unless-f "$git_dir/$x/info/url";6389my$u=eval{ ::file_to_s("$git_dir/$x/info/url") };6390next unless$u;6391my$dn= dirname("$git_dir/svn/$x");6392 mkpath([$dn])unless-d $dn;6393if($xeq'svn') {# they used 'svn' as GIT_SVN_ID:6394 mkpath(["$git_dir/svn/svn"]);6395print STDERR " -$git_dir/$x/info=> ",6396"$git_dir/svn/$x/info\n";6397rename"$git_dir/$x/info","$git_dir/svn/$x/info"or6398 croak "$!:$x";6399# don't worry too much about these, they probably6400# don't exist with repos this old (save for index,6401# and we can easily regenerate that)6402foreachmy$f(qw/unhandled.log index .rev_db/) {6403rename"$git_dir/$x/$f","$git_dir/svn/$x/$f";6404}6405}else{6406print STDERR " -$git_dir/$x=>$git_dir/svn/$x\n";6407rename"$git_dir/$x","$git_dir/svn/$x"or6408 croak "$!:$x";6409}6410$migrated++;6411}6412 command_close_pipe($fh,$ctx);6413print STDERR "Done migrating from a git-svn v1 layout\n";6414$migrated;6415}64166417sub read_old_urls {6418my($l_map,$pfx,$path) =@_;6419my@dir;6420foreach(<$path/*>) {6421if(-r "$_/info/url") {6422$pfx.='/'if$pfx&&$pfx!~ m!/$!;6423my$ref_id=$pfx. basename $_;6424my$url= ::file_to_s("$_/info/url");6425$l_map->{$ref_id} =$url;6426}elsif(-d $_) {6427push@dir,$_;6428}6429}6430foreach(@dir) {6431my$x=$_;6432$x=~s!^\Q$ENV{GIT_DIR}\E/svn/!!o;6433 read_old_urls($l_map,$x,$_);6434}6435}64366437sub migrate_from_v2 {6438my@cfg= command(qw/config -l/);6439return ifgrep/^svn-remote\..+\.url=/,@cfg;6440my%l_map;6441 read_old_urls(\%l_map,'',"$ENV{GIT_DIR}/svn");6442my$migrated=0;64436444foreachmy$ref_id(sort keys%l_map) {6445eval{ Git::SVN->init($l_map{$ref_id},'',undef,$ref_id) };6446if($@) {6447 Git::SVN->init($l_map{$ref_id},'',$ref_id,$ref_id);6448}6449$migrated++;6450}6451$migrated;6452}64536454sub minimize_connections {6455my$r= Git::SVN::read_all_remotes();6456my$new_urls= {};6457my$root_repos= {};6458foreachmy$repo_id(keys%$r) {6459my$url=$r->{$repo_id}->{url}ornext;6460my$fetch=$r->{$repo_id}->{fetch}ornext;6461my$ra= Git::SVN::Ra->new($url);64626463# skip existing cases where we already connect to the root6464if(($ra->{url}eq$ra->{repos_root}) ||6465($ra->{repos_root}eq$repo_id)) {6466$root_repos->{$ra->{url}} =$repo_id;6467next;6468}64696470my$root_ra= Git::SVN::Ra->new($ra->{repos_root});6471my$root_path=$ra->{url};6472$root_path=~ s#^\Q$ra->{repos_root}\E(/|$)##;6473foreachmy$path(keys%$fetch) {6474my$ref_id=$fetch->{$path};6475my$gs= Git::SVN->new($ref_id,$repo_id,$path);64766477# make sure we can read when connecting to6478# a higher level of a repository6479my($last_rev,undef) =$gs->last_rev_commit;6480if(!defined$last_rev) {6481$last_rev=eval{6482$root_ra->get_latest_revnum;6483};6484next if$@;6485}6486my$new=$root_path;6487$new.=length$path?"/$path":'';6488eval{6489$root_ra->get_log([$new],$last_rev,$last_rev,64900,0,1,sub{ });6491};6492next if$@;6493$new_urls->{$ra->{repos_root}}->{$new} =6494{ ref_id =>$ref_id,6495 old_repo_id =>$repo_id,6496 old_path =>$path};6497}6498}64996500my@emptied;6501foreachmy$url(keys%$new_urls) {6502# see if we can re-use an existing [svn-remote "repo_id"]6503# instead of creating a(n ugly) new section:6504my$repo_id=$root_repos->{$url} ||$url;65056506my$fetch=$new_urls->{$url};6507foreachmy$path(keys%$fetch) {6508my$x=$fetch->{$path};6509 Git::SVN->init($url,$path,$repo_id,$x->{ref_id});6510my$pfx="svn-remote.$x->{old_repo_id}";65116512my$old_fetch=quotemeta("$x->{old_path}:".6513"$x->{ref_id}");6514 command_noisy(qw/config --unset/,6515"$pfx.fetch",'^'.$old_fetch.'$');6516delete$r->{$x->{old_repo_id}}->6517{fetch}->{$x->{old_path}};6518if(!keys%{$r->{$x->{old_repo_id}}->{fetch}}) {6519 command_noisy(qw/config --unset/,6520"$pfx.url");6521push@emptied,$x->{old_repo_id}6522}6523}6524}6525if(@emptied) {6526my$file=$ENV{GIT_CONFIG} ||"$ENV{GIT_DIR}/config";6527print STDERR <<EOF;6528The following [svn-remote] sections in your config file ($file) are empty6529and can be safely removed:6530EOF6531print STDERR "[svn-remote\"$_\"]\n"foreach@emptied;6532}6533}65346535sub migration_check {6536 migrate_from_v0();6537 migrate_from_v1();6538 migrate_from_v2();6539 minimize_connections()if$_minimize;6540}65416542package Git::IndexInfo;6543use strict;6544use warnings;6545use Git qw/command_input_pipe command_close_pipe/;65466547sub new {6548my($class) =@_;6549my($gui,$ctx) = command_input_pipe(qw/update-index -z --index-info/);6550bless{ gui =>$gui, ctx =>$ctx, nr =>0},$class;6551}65526553sub remove {6554my($self,$path) =@_;6555if(print{$self->{gui} }'0 ',0 x 40,"\t",$path,"\0") {6556return++$self->{nr};6557}6558undef;6559}65606561sub update {6562my($self,$mode,$hash,$path) =@_;6563if(print{$self->{gui} }$mode,' ',$hash,"\t",$path,"\0") {6564return++$self->{nr};6565}6566undef;6567}65686569sub DESTROY {6570my($self) =@_;6571 command_close_pipe($self->{gui},$self->{ctx});6572}65736574package Git::SVN::GlobSpec;6575use strict;6576use warnings;65776578sub new {6579my($class,$glob,$pattern_ok) =@_;6580my$re=$glob;6581$re=~s!/+$!!g;# no need for trailing slashes6582my(@left,@right,@patterns);6583my$state="left";6584my$die_msg="Only one set of wildcard directories ".6585"(e.g. '*' or '*/*/*') is supported: '$glob'\n";6586formy$part(split(m|/|,$glob)) {6587if($part=~/\*/&&$partne"*") {6588die"Invalid pattern in '$glob':$part\n";6589}elsif($pattern_ok&&$part=~/[{}]/&&6590$part!~/^\{[^{}]+\}/) {6591die"Invalid pattern in '$glob':$part\n";6592}6593if($parteq"*") {6594die$die_msgif$stateeq"right";6595$state="pattern";6596push(@patterns,"[^/]*");6597}elsif($pattern_ok&&$part=~/^\{(.*)\}$/) {6598die$die_msgif$stateeq"right";6599$state="pattern";6600my$p=quotemeta($1);6601$p=~s/\\,/|/g;6602push(@patterns,"(?:$p)");6603}else{6604if($stateeq"left") {6605push(@left,$part);6606}else{6607push(@right,$part);6608$state="right";6609}6610}6611}6612my$depth=@patterns;6613if($depth==0) {6614die"One '*' is needed in glob: '$glob'\n";6615}6616my$left=join('/',@left);6617my$right=join('/',@right);6618$re=join('/',@patterns);6619$re=join('\/',6620grep(length,quotemeta($left),"($re)",quotemeta($right)));6621my$left_re=qr/^\/\Q$left\E(\/|$)/;6622bless{ left =>$left, right =>$right, left_regex =>$left_re,6623 regex =>qr/$re/,glob=>$glob, depth =>$depth},$class;6624}66256626sub full_path {6627my($self,$path) =@_;6628return(length$self->{left} ?"$self->{left}/":'') .6629$path. (length$self->{right} ?"/$self->{right}":'');6630}66316632__END__66336634Data structures:663566366637$remotes= {# returned by read_all_remotes()6638'svn'=> {6639# svn-remote.svn.url=https://svn.musicpd.org6640 url =>'https://svn.musicpd.org',6641# svn-remote.svn.fetch=mpd/trunk:trunk6642 fetch => {6643'mpd/trunk'=>'trunk',6644},6645# svn-remote.svn.tags=mpd/tags/*:tags/*6646 tags => {6647 path => {6648 left =>'mpd/tags',6649 right =>'',6650 regex =>qr!mpd/tags/([^/]+)$!,6651glob=>'tags/*',6652},6653ref=> {6654 left =>'tags',6655 right =>'',6656 regex =>qr!tags/([^/]+)$!,6657glob=>'tags/*',6658},6659}6660}6661};66626663$log_entry hashref as returned by libsvn_log_entry()6664{6665log=>'whitespace-formatted log entry6666',# trailing newline is preserved6667 revision =>'8',# integer6668 date =>'2004-02-24T17:01:44.108345Z',# commit date6669 author =>'committer name'6670};667166726673# this is generated by generate_diff();6674@mods= array of diff-index line hashes,each element represents one line6675 of diff-index output66766677diff-index line ($m hash)6678{6679 mode_a => first column of diff-index output,no leading ':',6680 mode_b => second column of diff-index output,6681 sha1_b => sha1sum of the final blob,6682 chg => change type [MCRADT],6683 file_a => original file name of a file (iff chg is'C'or'R')6684 file_b => new/current file name of a file (any chg)6685}6686;66876688# retval of read_url_paths{,_all}();6689$l_map= {6690# repository root url6691'https://svn.musicpd.org'=> {6692# repository path # GIT_SVN_ID6693'mpd/trunk'=>'trunk',6694'mpd/tags/0.11.5'=>'tags/0.11.5',6695},6696}66976698Notes:6699 I don't trust the each() function on unless I created%hashmyself6700 because the internal iterator may not have started at base.