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 Memoize;# core since 5.8.0, Jul 2002 84 85BEGIN{ 86# import functions from Git into our packages, en masse 87no strict 'refs'; 88foreach(qw/command command_oneline command_noisy command_output_pipe 89 command_input_pipe command_close_pipe 90 command_bidi_pipe command_close_bidi_pipe/) { 91formy$package(qw(SVN::Git::Editor SVN::Git::Fetcher 92 Git::SVN::Migration Git::SVN::Log Git::SVN), 93 __PACKAGE__) { 94*{"${package}::$_"} = \&{"Git::$_"}; 95} 96} 97 Memoize::memoize 'Git::config'; 98 Memoize::memoize 'Git::config_bool'; 99} 100 101my($SVN); 102 103$sha1=qr/[a-f\d]{40}/; 104$sha1_short=qr/[a-f\d]{4,40}/; 105my($_stdin,$_help,$_edit, 106$_message,$_file,$_branch_dest, 107$_template,$_shared, 108$_version,$_fetch_all,$_no_rebase,$_fetch_parent, 109$_merge,$_strategy,$_dry_run,$_local, 110$_prefix,$_no_checkout,$_url,$_verbose, 111$_git_format,$_commit_url,$_tag,$_merge_info,$_interactive); 112$Git::SVN::_follow_parent =1; 113$SVN::Git::Fetcher::_placeholder_filename =".gitignore"; 114$_q||=0; 115my%remote_opts= ('username=s'=> \$Git::SVN::Prompt::_username, 116'config-dir=s'=> \$Git::SVN::Ra::config_dir, 117'no-auth-cache'=> \$Git::SVN::Prompt::_no_auth_cache, 118'ignore-paths=s'=> \$SVN::Git::Fetcher::_ignore_regex, 119'ignore-refs=s'=> \$Git::SVN::Ra::_ignore_refs_regex ); 120my%fc_opts= ('follow-parent|follow!'=> \$Git::SVN::_follow_parent, 121'authors-file|A=s'=> \$_authors, 122'authors-prog=s'=> \$_authors_prog, 123'repack:i'=> \$Git::SVN::_repack, 124'noMetadata'=> \$Git::SVN::_no_metadata, 125'useSvmProps'=> \$Git::SVN::_use_svm_props, 126'useSvnsyncProps'=> \$Git::SVN::_use_svnsync_props, 127'log-window-size=i'=> \$Git::SVN::Ra::_log_window_size, 128'no-checkout'=> \$_no_checkout, 129'quiet|q+'=> \$_q, 130'repack-flags|repack-args|repack-opts=s'=> 131 \$Git::SVN::_repack_flags, 132'use-log-author'=> \$Git::SVN::_use_log_author, 133'add-author-from'=> \$Git::SVN::_add_author_from, 134'localtime'=> \$Git::SVN::_localtime, 135%remote_opts); 136 137my($_trunk,@_tags,@_branches,$_stdlayout); 138my%icv; 139my%init_opts= ('template=s'=> \$_template,'shared:s'=> \$_shared, 140'trunk|T=s'=> \$_trunk,'tags|t=s@'=> \@_tags, 141'branches|b=s@'=> \@_branches,'prefix=s'=> \$_prefix, 142'stdlayout|s'=> \$_stdlayout, 143'minimize-url|m!'=> \$Git::SVN::_minimize_url, 144'no-metadata'=>sub{$icv{noMetadata} =1}, 145'use-svm-props'=>sub{$icv{useSvmProps} =1}, 146'use-svnsync-props'=>sub{$icv{useSvnsyncProps} =1}, 147'rewrite-root=s'=>sub{$icv{rewriteRoot} =$_[1] }, 148'rewrite-uuid=s'=>sub{$icv{rewriteUUID} =$_[1] }, 149%remote_opts); 150my%cmt_opts= ('edit|e'=> \$_edit, 151'rmdir'=> \$SVN::Git::Editor::_rmdir, 152'find-copies-harder'=> \$SVN::Git::Editor::_find_copies_harder, 153'l=i'=> \$SVN::Git::Editor::_rename_limit, 154'copy-similarity|C=i'=> \$SVN::Git::Editor::_cp_similarity 155); 156 157my%cmd= ( 158 fetch => [ \&cmd_fetch,"Download new revisions from SVN", 159{'revision|r=s'=> \$_revision, 160'fetch-all|all'=> \$_fetch_all, 161'parent|p'=> \$_fetch_parent, 162%fc_opts} ], 163 clone => [ \&cmd_clone,"Initialize and fetch revisions", 164{'revision|r=s'=> \$_revision, 165'preserve-empty-dirs'=> 166 \$SVN::Git::Fetcher::_preserve_empty_dirs, 167'placeholder-filename=s'=> 168 \$SVN::Git::Fetcher::_placeholder_filename, 169%fc_opts,%init_opts} ], 170 init => [ \&cmd_init,"Initialize a repo for tracking". 171" (requires URL argument)", 172 \%init_opts], 173'multi-init'=> [ \&cmd_multi_init, 174"Deprecated alias for ". 175"'$0init -T<trunk> -b<branches> -t<tags>'", 176 \%init_opts], 177 dcommit => [ \&cmd_dcommit, 178'Commit several diffs to merge with upstream', 179{'merge|m|M'=> \$_merge, 180'strategy|s=s'=> \$_strategy, 181'verbose|v'=> \$_verbose, 182'dry-run|n'=> \$_dry_run, 183'fetch-all|all'=> \$_fetch_all, 184'commit-url=s'=> \$_commit_url, 185'revision|r=i'=> \$_revision, 186'no-rebase'=> \$_no_rebase, 187'mergeinfo=s'=> \$_merge_info, 188'interactive|i'=> \$_interactive, 189%cmt_opts,%fc_opts} ], 190 branch => [ \&cmd_branch, 191'Create a branch in the SVN repository', 192{'message|m=s'=> \$_message, 193'destination|d=s'=> \$_branch_dest, 194'dry-run|n'=> \$_dry_run, 195'tag|t'=> \$_tag, 196'username=s'=> \$Git::SVN::Prompt::_username, 197'commit-url=s'=> \$_commit_url} ], 198 tag => [sub{$_tag=1; cmd_branch(@_) }, 199'Create a tag in the SVN repository', 200{'message|m=s'=> \$_message, 201'destination|d=s'=> \$_branch_dest, 202'dry-run|n'=> \$_dry_run, 203'username=s'=> \$Git::SVN::Prompt::_username, 204'commit-url=s'=> \$_commit_url} ], 205'set-tree'=> [ \&cmd_set_tree, 206"Set an SVN repository to a git tree-ish", 207{'stdin'=> \$_stdin,%cmt_opts,%fc_opts, } ], 208'create-ignore'=> [ \&cmd_create_ignore, 209'Create a .gitignore per svn:ignore', 210{'revision|r=i'=> \$_revision 211} ], 212'mkdirs'=> [ \&cmd_mkdirs , 213"recreate empty directories after a checkout", 214{'revision|r=i'=> \$_revision} ], 215'propget'=> [ \&cmd_propget, 216'Print the value of a property on a file or directory', 217{'revision|r=i'=> \$_revision} ], 218'proplist'=> [ \&cmd_proplist, 219'List all properties of a file or directory', 220{'revision|r=i'=> \$_revision} ], 221'show-ignore'=> [ \&cmd_show_ignore,"Show svn:ignore listings", 222{'revision|r=i'=> \$_revision 223} ], 224'show-externals'=> [ \&cmd_show_externals,"Show svn:externals listings", 225{'revision|r=i'=> \$_revision 226} ], 227'multi-fetch'=> [ \&cmd_multi_fetch, 228"Deprecated alias for$0fetch --all", 229{'revision|r=s'=> \$_revision,%fc_opts} ], 230'migrate'=> [sub{ }, 231# no-op, we automatically run this anyways, 232'Migrate configuration/metadata/layout from 233 previous versions of git-svn', 234{'minimize'=> \$Git::SVN::Migration::_minimize, 235%remote_opts} ], 236'log'=> [ \&Git::SVN::Log::cmd_show_log,'Show commit logs', 237{'limit=i'=> \$Git::SVN::Log::limit, 238'revision|r=s'=> \$_revision, 239'verbose|v'=> \$Git::SVN::Log::verbose, 240'incremental'=> \$Git::SVN::Log::incremental, 241'oneline'=> \$Git::SVN::Log::oneline, 242'show-commit'=> \$Git::SVN::Log::show_commit, 243'non-recursive'=> \$Git::SVN::Log::non_recursive, 244'authors-file|A=s'=> \$_authors, 245'color'=> \$Git::SVN::Log::color, 246'pager=s'=> \$Git::SVN::Log::pager 247} ], 248'find-rev'=> [ \&cmd_find_rev, 249"Translate between SVN revision numbers and tree-ish", 250{} ], 251'rebase'=> [ \&cmd_rebase,"Fetch and rebase your working directory", 252{'merge|m|M'=> \$_merge, 253'verbose|v'=> \$_verbose, 254'strategy|s=s'=> \$_strategy, 255'local|l'=> \$_local, 256'fetch-all|all'=> \$_fetch_all, 257'dry-run|n'=> \$_dry_run, 258%fc_opts} ], 259'commit-diff'=> [ \&cmd_commit_diff, 260'Commit a diff between two trees', 261{'message|m=s'=> \$_message, 262'file|F=s'=> \$_file, 263'revision|r=s'=> \$_revision, 264%cmt_opts} ], 265'info'=> [ \&cmd_info, 266"Show info about the latest SVN revision 267 on the current branch", 268{'url'=> \$_url, } ], 269'blame'=> [ \&Git::SVN::Log::cmd_blame, 270"Show what revision and author last modified each line of a file", 271{'git-format'=> \$_git_format} ], 272'reset'=> [ \&cmd_reset, 273"Undo fetches back to the specified SVN revision", 274{'revision|r=s'=> \$_revision, 275'parent|p'=> \$_fetch_parent} ], 276'gc'=> [ \&cmd_gc, 277"Compress unhandled.log files in .git/svn and remove ". 278"index files in .git/svn", 279{} ], 280); 281 282use Term::ReadLine; 283package FakeTerm; 284sub new { 285my($class,$reason) =@_; 286returnbless \$reason,shift; 287} 288subreadline{ 289my$self=shift; 290die"Cannot use readline on FakeTerm:$$self"; 291} 292package main; 293 294my$term=eval{ 295$ENV{"GIT_SVN_NOTTY"} 296? new Term::ReadLine 'git-svn', \*STDIN, \*STDOUT 297: new Term::ReadLine 'git-svn'; 298}; 299if($@) { 300$term= new FakeTerm "$@: going non-interactive"; 301} 302 303my$cmd; 304for(my$i=0;$i<@ARGV;$i++) { 305if(defined$cmd{$ARGV[$i]}) { 306$cmd=$ARGV[$i]; 307splice@ARGV,$i,1; 308last; 309}elsif($ARGV[$i]eq'help') { 310$cmd=$ARGV[$i+1]; 311 usage(0); 312} 313}; 314 315# make sure we're always running at the top-level working directory 316unless($cmd&&$cmd=~/(?:clone|init|multi-init)$/) { 317unless(-d $ENV{GIT_DIR}) { 318if($git_dir_user_set) { 319die"GIT_DIR=$ENV{GIT_DIR} explicitly set, ", 320"but it is not a directory\n"; 321} 322my$git_dir=delete$ENV{GIT_DIR}; 323my$cdup=undef; 324 git_cmd_try { 325$cdup= command_oneline(qw/rev-parse --show-cdup/); 326$git_dir='.'unless($cdup); 327chomp$cdupif($cdup); 328$cdup="."unless($cdup&&length$cdup); 329}"Already at toplevel, but$git_dirnot found\n"; 330chdir$cdupor die"Unable to chdir up to '$cdup'\n"; 331unless(-d $git_dir) { 332die"$git_dirstill not found after going to ", 333"'$cdup'\n"; 334} 335$ENV{GIT_DIR} =$git_dir; 336} 337$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 338} 339 340my%opts= %{$cmd{$cmd}->[2]}if(defined$cmd); 341 342read_git_config(\%opts); 343if($cmd&& ($cmdeq'log'||$cmdeq'blame')) { 344 Getopt::Long::Configure('pass_through'); 345} 346my$rv= GetOptions(%opts,'h|H'=> \$_help,'version|V'=> \$_version, 347'minimize-connections'=> \$Git::SVN::Migration::_minimize, 348'id|i=s'=> \$Git::SVN::default_ref_id, 349'svn-remote|remote|R=s'=>sub{ 350$Git::SVN::no_reuse_existing =1; 351$Git::SVN::default_repo_id =$_[1] }); 352exit1if(!$rv&&$cmd&&$cmdne'log'); 353 354usage(0)if$_help; 355version()if$_version; 356usage(1)unlessdefined$cmd; 357load_authors()if$_authors; 358if(defined$_authors_prog) { 359$_authors_prog="'". File::Spec->rel2abs($_authors_prog) ."'"; 360} 361 362unless($cmd=~/^(?:clone|init|multi-init|commit-diff)$/) { 363 Git::SVN::Migration::migration_check(); 364} 365Git::SVN::init_vars(); 366eval{ 367 Git::SVN::verify_remotes_sanity(); 368$cmd{$cmd}->[0]->(@ARGV); 369}; 370fatal $@if$@; 371post_fetch_checkout(); 372exit0; 373 374####################### primary functions ###################### 375sub usage { 376my$exit=shift||0; 377my$fd=$exit? \*STDERR : \*STDOUT; 378print$fd<<""; 379git-svn - bidirectional operations between a single Subversion tree and git 380Usage: git svn <command> [options] [arguments]\n 381 382print$fd"Available commands:\n"unless$cmd; 383 384foreach(sort keys%cmd) { 385next if$cmd&&$cmdne$_; 386next if/^multi-/;# don't show deprecated commands 387print$fd' ',pack('A17',$_),$cmd{$_}->[1],"\n"; 388foreach(sort keys%{$cmd{$_}->[2]}) { 389# mixed-case options are for .git/config only 390next if/[A-Z]/&&/^[a-z]+$/i; 391# prints out arguments as they should be passed: 392my$x= s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : ''; 393print$fd' ' x 21,join(', ',map{length$_>1? 394"--$_":"-$_"} 395split/\|/,$_),"$x\n"; 396} 397} 398print$fd<<""; 399\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an 400arbitrary identifier if you're tracking multiple SVN branches/repositories in 401one git repository and want to keep them separate. See git-svn(1) for more 402information. 403 404 exit$exit; 405} 406 407sub version { 408 ::_req_svn(); 409 print "git-svn version$VERSION(svn$SVN::Core::VERSION)\n"; 410 exit 0; 411} 412 413sub ask { 414 my ($prompt,%arg) =@_; 415 my$valid_re=$arg{valid_re}; 416 my$default=$arg{default}; 417 my$resp; 418 my$i= 0; 419 420 if ( !( defined($term->IN) 421 && defined( fileno($term->IN) ) 422 && defined($term->OUT ) 423 && defined( fileno($term->OUT) ) ) ){ 424 return defined($default) ?$default: undef; 425 } 426 427 while ($i++< 10) { 428$resp=$term->readline($prompt); 429 if (!defined$resp) { # EOF 430 print "\n"; 431 return defined$default?$default: undef; 432 } 433 if ($respeq '' and defined$default) { 434 return$default; 435 } 436 if (!defined$valid_reor$resp=~ /$valid_re/) { 437 return$resp; 438 } 439 } 440 return undef; 441} 442 443sub do_git_init_db { 444 unless (-d$ENV{GIT_DIR}) { 445 my@init_db= ('init'); 446 push@init_db, "--template=$_template" if defined$_template; 447 if (defined$_shared) { 448 if ($_shared=~ /[a-z]/) { 449 push@init_db, "--shared=$_shared"; 450 } else { 451 push@init_db, "--shared"; 452 } 453 } 454 command_noisy(@init_db); 455$_repository= Git->repository(Repository => ".git"); 456 } 457 my$set; 458 my$pfx= "svn-remote.$Git::SVN::default_repo_id"; 459 foreach my$i(keys%icv) { 460 die "'$set' and '$i' cannot both be set\n" if$set; 461 next unless defined$icv{$i}; 462 command_noisy('config', "$pfx.$i",$icv{$i}); 463$set=$i; 464 } 465 my$ignore_paths_regex= \$SVN::Git::Fetcher::_ignore_regex; 466 command_noisy('config', "$pfx.ignore-paths",$$ignore_paths_regex) 467 if defined$$ignore_paths_regex; 468 my$ignore_refs_regex= \$Git::SVN::Ra::_ignore_refs_regex; 469 command_noisy('config', "$pfx.ignore-refs",$$ignore_refs_regex) 470 if defined$$ignore_refs_regex; 471 472 if (defined$SVN::Git::Fetcher::_preserve_empty_dirs) { 473 my$fname= \$SVN::Git::Fetcher::_placeholder_filename; 474 command_noisy('config', "$pfx.preserve-empty-dirs", 'true'); 475 command_noisy('config', "$pfx.placeholder-filename",$$fname); 476 } 477} 478 479sub init_subdir { 480 my$repo_path= shift or return; 481 mkpath([$repo_path]) unless -d$repo_path; 482 chdir$repo_pathor die "Couldn't chdir to $repo_path:$!\n"; 483$ENV{GIT_DIR} = '.git'; 484$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 485} 486 487sub cmd_clone { 488 my ($url,$path) =@_; 489 if (!defined$path&& 490 (defined$_trunk||@_branches||@_tags|| 491 defined$_stdlayout) && 492$url!~ m#^[a-z\+]+://#) { 493$path=$url; 494 } 495$path= basename($url) if !defined$path|| !length$path; 496 my$authors_absolute=$_authors? File::Spec->rel2abs($_authors) : ""; 497 cmd_init($url,$path); 498 command_oneline('config', 'svn.authorsfile',$authors_absolute) 499 if$_authors; 500 Git::SVN::fetch_all($Git::SVN::default_repo_id); 501} 502 503sub cmd_init { 504 if (defined$_stdlayout) { 505$_trunk= 'trunk' if (!defined$_trunk); 506@_tags= 'tags' if (!@_tags); 507@_branches= 'branches' if (!@_branches); 508 } 509 if (defined$_trunk||@_branches||@_tags) { 510 return cmd_multi_init(@_); 511 } 512 my$url= shift or die "SVN repository location required ", 513 "as a command-line argument\n"; 514$url= canonicalize_url($url); 515 init_subdir(@_); 516 do_git_init_db(); 517 518 if ($Git::SVN::_minimize_url eq 'unset') { 519$Git::SVN::_minimize_url = 0; 520 } 521 522 Git::SVN->init($url); 523} 524 525sub cmd_fetch { 526 if (grep /^\d+=./,@_) { 527 die "'<rev>=<commit>' fetch arguments are ", 528 "no longer supported.\n"; 529 } 530 my ($remote) =@_; 531 if (@_> 1) { 532 die "Usage:$0 fetch [--all] [--parent] [svn-remote]\n"; 533 } 534$Git::SVN::no_reuse_existing = undef; 535 if ($_fetch_parent) { 536 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD'); 537 unless ($gs) { 538 die "Unable to determine upstream SVN information from ", 539 "working tree history\n"; 540 } 541 # just fetch, don't checkout. 542$_no_checkout= 'true'; 543$_fetch_all?$gs->fetch_all :$gs->fetch; 544 } elsif ($_fetch_all) { 545 cmd_multi_fetch(); 546 } else { 547$remote||=$Git::SVN::default_repo_id; 548 Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes()); 549 } 550} 551 552sub cmd_set_tree { 553 my (@commits) =@_; 554 if ($_stdin|| !@commits) { 555 print "Reading from stdin...\n"; 556@commits= (); 557 while (<STDIN>) { 558 if (/\b($sha1_short)\b/o) { 559 unshift@commits,$1; 560 } 561 } 562 } 563 my@revs; 564 foreach my$c(@commits) { 565 my@tmp= command('rev-parse',$c); 566 if (scalar@tmp== 1) { 567 push@revs,$tmp[0]; 568 } elsif (scalar@tmp> 1) { 569 push@revs, reverse(command('rev-list',@tmp)); 570 } else { 571 fatal "Failed to rev-parse $c"; 572 } 573 } 574 my$gs= Git::SVN->new; 575 my ($r_last,$cmt_last) =$gs->last_rev_commit; 576$gs->fetch; 577 if (defined$gs->{last_rev} &&$r_last!=$gs->{last_rev}) { 578 fatal "There are new revisions that were fetched ", 579 "and need to be merged (or acknowledged)", 580 "before committing.\nlast rev:$r_last\n", 581 " current:$gs->{last_rev}"; 582 } 583$gs->set_tree($_) foreach@revs; 584 print "Done committing ",scalar@revs," revisions to SVN\n"; 585 unlink$gs->{index}; 586} 587 588sub split_merge_info_range { 589 my ($range) =@_; 590 if ($range=~ /(\d+)-(\d+)/) { 591 return (int($1), int($2)); 592 } else { 593 return (int($range), int($range)); 594 } 595} 596 597sub combine_ranges { 598 my ($in) =@_; 599 600 my@fnums= (); 601 my@arr= split(/,/,$in); 602 for my$element(@arr) { 603 my ($start,$end) = split_merge_info_range($element); 604 push@fnums,$start; 605 } 606 607 my@sorted=@arr[ sort { 608$fnums[$a] <=>$fnums[$b] 609 } 0..$#arr]; 610 611 my@return= (); 612 my$last= -1; 613 my$first= -1; 614 for my$element(@sorted) { 615 my ($start,$end) = split_merge_info_range($element); 616 617 if ($last== -1) { 618$first=$start; 619$last=$end; 620 next; 621 } 622 if ($start<=$last+1) { 623 if ($end>$last) { 624$last=$end; 625 } 626 next; 627 } 628 if ($first==$last) { 629 push@return, "$first"; 630 } else { 631 push@return, "$first-$last"; 632 } 633$first=$start; 634$last=$end; 635 } 636 637 if ($first!= -1) { 638 if ($first==$last) { 639 push@return, "$first"; 640 } else { 641 push@return, "$first-$last"; 642 } 643 } 644 645 return join(',',@return); 646} 647 648sub merge_revs_into_hash { 649 my ($hash,$minfo) =@_; 650 my@lines= split(' ',$minfo); 651 652 for my$line(@lines) { 653 my ($branchpath,$revs) = split(/:/,$line); 654 655 if (exists($hash->{$branchpath})) { 656 # Merge the two revision sets 657 my$combined= "$hash->{$branchpath},$revs"; 658$hash->{$branchpath} = combine_ranges($combined); 659 } else { 660 # Just do range combining for consolidation 661$hash->{$branchpath} = combine_ranges($revs); 662 } 663 } 664} 665 666sub merge_merge_info { 667 my ($mergeinfo_one,$mergeinfo_two) =@_; 668 my%result_hash= (); 669 670 merge_revs_into_hash(\%result_hash,$mergeinfo_one); 671 merge_revs_into_hash(\%result_hash,$mergeinfo_two); 672 673 my$result= ''; 674 # Sort below is for consistency's sake 675 for my$branchname(sort keys(%result_hash)) { 676 my$revlist=$result_hash{$branchname}; 677$result.= "$branchname:$revlist\n" 678 } 679 return$result; 680} 681 682sub populate_merge_info { 683 my ($d,$gs,$uuid,$linear_refs,$rewritten_parent) =@_; 684 685 my%parentshash; 686 read_commit_parents(\%parentshash,$d); 687 my@parents= @{$parentshash{$d}}; 688 if ($#parents> 0) { 689 # Merge commit 690 my$all_parents_ok= 1; 691 my$aggregate_mergeinfo= ''; 692 my$rooturl=$gs->repos_root; 693 694 if (defined($rewritten_parent)) { 695 # Replace first parent with newly-rewritten version 696 shift@parents; 697 unshift@parents,$rewritten_parent; 698 } 699 700 foreach my$parent(@parents) { 701 my ($branchurl,$svnrev,$paruuid) = 702 cmt_metadata($parent); 703 704 unless (defined($svnrev)) { 705 # Should have been caught be preflight check 706 fatal "merge commit $dhas ancestor $parent, but that change " 707 ."doesnot have git-svn metadata!"; 708 } 709 unless ($branchurl=~ /^\Q$rooturl\E(.*)/) { 710 fatal "commit $parent git-svn metadata changed mid-run!"; 711 } 712 my$branchpath=$1; 713 714 my$ra= Git::SVN::Ra->new($branchurl); 715 my (undef, undef,$props) = 716$ra->get_dir(canonicalize_path("."),$svnrev); 717 my$par_mergeinfo=$props->{'svn:mergeinfo'}; 718 unless (defined$par_mergeinfo) { 719$par_mergeinfo= ''; 720 } 721 # Merge previous mergeinfo values 722$aggregate_mergeinfo= 723 merge_merge_info($aggregate_mergeinfo, 724$par_mergeinfo, 0); 725 726 next if$parenteq$parents[0]; # Skip first parent 727 # Add new changes being placed in tree by merge 728 my@cmd= (qw/rev-list --reverse/, 729$parent, qw/--not/); 730 foreach my$par(@parents) { 731 unless ($pareq$parent) { 732 push@cmd,$par; 733 } 734 } 735 my@revsin= (); 736 my ($revlist,$ctx) = command_output_pipe(@cmd); 737 while (<$revlist>) { 738 my$irev=$_; 739 chomp$irev; 740 my (undef,$csvnrev, undef) = 741 cmt_metadata($irev); 742 unless (defined$csvnrev) { 743 # A child is missing SVN annotations... 744 # this might be OK, or might not be. 745 warn "W:child $irevis merged into revision " 746 ."$d but doesnot have git-svn metadata." 747 ."This means git-svn cannot determine the " 748 ."svn revision numbers to place into the " 749 ."svn:mergeinfo property. You must ensure " 750 ."a branch is entirely committed to " 751 ."SVN before merging it in order for" 752 ."svn:mergeinfo population to function " 753 ."properly"; 754 } 755 push@revsin,$csvnrev; 756 } 757 command_close_pipe($revlist,$ctx); 758 759 last unless$all_parents_ok; 760 761 # We now have a list of all SVN revnos which are 762 # merged by this particular parent. Integrate them. 763 next if$#revsin== -1; 764 my$newmergeinfo= "$branchpath:" . join(',',@revsin); 765$aggregate_mergeinfo= 766 merge_merge_info($aggregate_mergeinfo, 767$newmergeinfo, 1); 768 } 769 if ($all_parents_okand$aggregate_mergeinfo) { 770 return$aggregate_mergeinfo; 771 } 772 } 773 774 return undef; 775} 776 777sub cmd_dcommit { 778 my$head= shift; 779 command_noisy(qw/update-index --refresh/); 780 git_cmd_try { command_oneline(qw/diff-index --quiet HEAD/) } 781 'Cannot dcommit with a dirty index. Commit your changes first, ' 782 . "or stash them with `git stash'.\n"; 783$head||= 'HEAD'; 784 785 my$old_head; 786 if ($headne 'HEAD') { 787$old_head= eval { 788 command_oneline([qw/symbolic-ref -q HEAD/]) 789 }; 790 if ($old_head) { 791$old_head=~ s{^refs/heads/}{}; 792 } else { 793$old_head= eval { command_oneline(qw/rev-parse HEAD/) }; 794 } 795 command(['checkout',$head], STDERR => 0); 796 } 797 798 my@refs; 799 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD', \@refs); 800 unless ($gs) { 801 die "Unable to determine upstream SVN information from ", 802 "$headhistory.\nPerhaps the repository is empty."; 803 } 804 805 if (defined$_commit_url) { 806$url=$_commit_url; 807 } else { 808$url= eval { command_oneline('config', '--get', 809 "svn-remote.$gs->{repo_id}.commiturl") }; 810 if (!$url) { 811$url=$gs->full_pushurl 812 } 813 } 814 815 my$last_rev=$_revisionif defined$_revision; 816 if ($url) { 817 print "Committing to$url...\n"; 818 } 819 my ($linear_refs,$parents) = linearize_history($gs, \@refs); 820 if ($_no_rebase&& scalar(@$linear_refs) > 1) { 821 warn "Attempting to commit more than one change while ", 822 "--no-rebase is enabled.\n", 823 "If these changes depend on each other, re-running ", 824 "without --no-rebase may be required." 825 } 826 827 if (defined$_interactive){ 828 my$ask_default= "y"; 829 foreach my$d(@$linear_refs){ 830 my ($fh,$ctx) = command_output_pipe(qw(show --summary),"$d"); 831while(<$fh>){ 832print$_; 833} 834 command_close_pipe($fh,$ctx); 835$_= ask("Commit this patch to SVN? ([y]es (default)|[n]o|[q]uit|[a]ll): ", 836 valid_re =>qr/^(?:yes|y|no|n|quit|q|all|a)/i, 837default=>$ask_default); 838die"Commit this patch reply required"unlessdefined$_; 839if(/^[nq]/i) { 840exit(0); 841}elsif(/^a/i) { 842last; 843} 844} 845} 846 847my$expect_url=$url; 848 849my$push_merge_info=eval{ 850 command_oneline(qw/config --get svn.pushmergeinfo/) 851}; 852if(not defined($push_merge_info) 853or$push_merge_infoeq"false" 854or$push_merge_infoeq"no" 855or$push_merge_infoeq"never") { 856$push_merge_info=0; 857} 858 859unless(defined($_merge_info) || !$push_merge_info) { 860# Preflight check of changes to ensure no issues with mergeinfo 861# This includes check for uncommitted-to-SVN parents 862# (other than the first parent, which we will handle), 863# information from different SVN repos, and paths 864# which are not underneath this repository root. 865my$rooturl=$gs->repos_root; 866foreachmy$d(@$linear_refs) { 867my%parentshash; 868 read_commit_parents(\%parentshash,$d); 869my@realparents= @{$parentshash{$d}}; 870if($#realparents>0) { 871# Merge commit 872shift@realparents;# Remove/ignore first parent 873foreachmy$parent(@realparents) { 874my($branchurl,$svnrev,$paruuid) = cmt_metadata($parent); 875unless(defined$paruuid) { 876# A parent is missing SVN annotations... 877# abort the whole operation. 878 fatal "$parentis merged into revision$d, " 879."but does not have git-svn metadata. " 880."Either dcommit the branch or use a " 881."local cherry-pick, FF merge, or rebase " 882."instead of an explicit merge commit."; 883} 884 885unless($paruuideq$uuid) { 886# Parent has SVN metadata from different repository 887 fatal "merge parent$parentfor change$dhas " 888."git-svn uuid$paruuid, while current change " 889."has uuid$uuid!"; 890} 891 892unless($branchurl=~/^\Q$rooturl\E(.*)/) { 893# This branch is very strange indeed. 894 fatal "merge parent$parentfor$dis on branch " 895."$branchurl, which is not under the " 896."git-svn root$rooturl!"; 897} 898} 899} 900} 901} 902 903my$rewritten_parent; 904 Git::SVN::remove_username($expect_url); 905if(defined($_merge_info)) { 906$_merge_info=~tr{ }{\n}; 907} 908while(1) { 909my$d=shift@$linear_refsorlast; 910unless(defined$last_rev) { 911(undef,$last_rev,undef) = cmt_metadata("$d~1"); 912unless(defined$last_rev) { 913 fatal "Unable to extract revision information ", 914"from commit$d~1"; 915} 916} 917if($_dry_run) { 918print"diff-tree$d~1$d\n"; 919}else{ 920my$cmt_rev; 921 922unless(defined($_merge_info) || !$push_merge_info) { 923$_merge_info= populate_merge_info($d,$gs, 924$uuid, 925$linear_refs, 926$rewritten_parent); 927} 928 929my%ed_opts= ( r =>$last_rev, 930log=> get_commit_entry($d)->{log}, 931 ra => Git::SVN::Ra->new($url), 932 config => SVN::Core::config_get_config( 933$Git::SVN::Ra::config_dir 934), 935 tree_a =>"$d~1", 936 tree_b =>$d, 937 editor_cb =>sub{ 938print"Committed r$_[0]\n"; 939$cmt_rev=$_[0]; 940}, 941 mergeinfo =>$_merge_info, 942 svn_path =>''); 943if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) { 944print"No changes\n$d~1==$d\n"; 945}elsif($parents->{$d} && @{$parents->{$d}}) { 946$gs->{inject_parents_dcommit}->{$cmt_rev} = 947$parents->{$d}; 948} 949$_fetch_all?$gs->fetch_all:$gs->fetch; 950$last_rev=$cmt_rev; 951next if$_no_rebase; 952 953# we always want to rebase against the current HEAD, 954# not any head that was passed to us 955my@diff= command('diff-tree',$d, 956$gs->refname,'--'); 957my@finish; 958if(@diff) { 959@finish= rebase_cmd(); 960print STDERR "W:$dand ",$gs->refname, 961" differ, using@finish:\n", 962join("\n",@diff),"\n"; 963}else{ 964print"No changes between current HEAD and ", 965$gs->refname, 966"\nResetting to the latest ", 967$gs->refname,"\n"; 968@finish= qw/reset --mixed/; 969} 970 command_noisy(@finish,$gs->refname); 971 972$rewritten_parent= command_oneline(qw/rev-parse HEAD/); 973 974if(@diff) { 975@refs= (); 976my($url_,$rev_,$uuid_,$gs_) = 977 working_head_info('HEAD', \@refs); 978my($linear_refs_,$parents_) = 979 linearize_history($gs_, \@refs); 980if(scalar(@$linear_refs) != 981scalar(@$linear_refs_)) { 982 fatal "# of revisions changed ", 983"\nbefore:\n", 984join("\n",@$linear_refs), 985"\n\nafter:\n", 986join("\n",@$linear_refs_),"\n", 987'If you are attempting to commit ', 988"merges, try running:\n\t", 989'git rebase --interactive', 990'--preserve-merges ', 991$gs->refname, 992"\nBefore dcommitting"; 993} 994if($url_ne$expect_url) { 995if($url_eq$gs->metadata_url) { 996print 997"Accepting rewritten URL:", 998"$url_\n"; 999}else{1000 fatal1001"URL mismatch after rebase:",1002"$url_!=$expect_url";1003}1004}1005if($uuid_ne$uuid) {1006 fatal "uuid mismatch after rebase: ",1007"$uuid_!=$uuid";1008}1009# remap parents1010my(%p,@l,$i);1011for($i=0;$i<scalar@$linear_refs;$i++) {1012my$new=$linear_refs_->[$i]ornext;1013$p{$new} =1014$parents->{$linear_refs->[$i]};1015push@l,$new;1016}1017$parents= \%p;1018$linear_refs= \@l;1019}1020}1021}10221023if($old_head) {1024my$new_head= command_oneline(qw/rev-parse HEAD/);1025my$new_is_symbolic=eval{1026 command_oneline(qw/symbolic-ref -q HEAD/);1027};1028if($new_is_symbolic) {1029print"dcommitted the branch ",$head,"\n";1030}else{1031print"dcommitted on a detached HEAD because you gave ",1032"a revision argument.\n",1033"The rewritten commit is: ",$new_head,"\n";1034}1035 command(['checkout',$old_head], STDERR =>0);1036}10371038unlink$gs->{index};1039}10401041sub cmd_branch {1042my($branch_name,$head) =@_;10431044unless(defined$branch_name&&length$branch_name) {1045die(($_tag?"tag":"branch") ." name required\n");1046}1047$head||='HEAD';10481049my(undef,$rev,undef,$gs) = working_head_info($head);1050my$src=$gs->full_pushurl;10511052my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1053my$allglobs=$remote->{$_tag?'tags':'branches'};1054my$glob;1055if($#{$allglobs} ==0) {1056$glob=$allglobs->[0];1057}else{1058unless(defined$_branch_dest) {1059die"Multiple ",1060$_tag?"tag":"branch",1061" paths defined for Subversion repository.\n",1062"You must specify where you want to create the ",1063$_tag?"tag":"branch",1064" with the --destination argument.\n";1065}1066foreachmy$g(@{$allglobs}) {1067# SVN::Git::Editor could probably be moved to Git.pm..1068my$re= SVN::Git::Editor::glob2pat($g->{path}->{left});1069if($_branch_dest=~/$re/) {1070$glob=$g;1071last;1072}1073}1074unless(defined$glob) {1075my$dest_re=qr/\b\Q$_branch_dest\E\b/;1076foreachmy$g(@{$allglobs}) {1077$g->{path}->{left} =~/$dest_re/ornext;1078if(defined$glob) {1079die"Ambiguous destination: ",1080$_branch_dest,"\nmatches both '",1081$glob->{path}->{left},"' and '",1082$g->{path}->{left},"'\n";1083}1084$glob=$g;1085}1086unless(defined$glob) {1087die"Unknown ",1088$_tag?"tag":"branch",1089" destination$_branch_dest\n";1090}1091}1092}1093my($lft,$rgt) = @{$glob->{path} }{qw/left right/};1094my$url;1095if(defined$_commit_url) {1096$url=$_commit_url;1097}else{1098$url=eval{ command_oneline('config','--get',1099"svn-remote.$gs->{repo_id}.commiturl") };1100if(!$url) {1101$url=$remote->{pushurl} ||$remote->{url};1102}1103}1104my$dst=join'/',$url,$lft,$branch_name, ($rgt|| ());11051106if($dst=~/^https:/&&$src=~/^http:/) {1107$src=~s/^http:/https:/;1108}11091110::_req_svn();11111112my$ctx= SVN::Client->new(1113 auth => Git::SVN::Ra::_auth_providers(),1114 log_msg =>sub{1115${$_[0] } =defined$_message1116?$_message1117:'Create '. ($_tag?'tag ':'branch ')1118.$branch_name;1119},1120);11211122eval{1123$ctx->ls($dst,'HEAD',0);1124}and die"branch ${branch_name} already exists\n";11251126print"Copying ${src} at r${rev} to ${dst}...\n";1127$ctx->copy($src,$rev,$dst)1128unless$_dry_run;11291130$gs->fetch_all;1131}11321133sub cmd_find_rev {1134my$revision_or_hash=shift or die"SVN or git revision required ",1135"as a command-line argument\n";1136my$result;1137if($revision_or_hash=~/^r\d+$/) {1138my$head=shift;1139$head||='HEAD';1140my@refs;1141my(undef,undef,$uuid,$gs) = working_head_info($head, \@refs);1142unless($gs) {1143die"Unable to determine upstream SVN information from ",1144"$headhistory\n";1145}1146my$desired_revision=substr($revision_or_hash,1);1147$result=$gs->rev_map_get($desired_revision,$uuid);1148}else{1149my(undef,$rev,undef) = cmt_metadata($revision_or_hash);1150$result=$rev;1151}1152print"$result\n"if$result;1153}11541155sub auto_create_empty_directories {1156my($gs) =@_;1157my$var=eval{ command_oneline('config','--get','--bool',1158"svn-remote.$gs->{repo_id}.automkdirs") };1159# By default, create empty directories by consulting the unhandled log,1160# but allow setting it to 'false' to skip it.1161return!($var&&$vareq'false');1162}11631164sub cmd_rebase {1165 command_noisy(qw/update-index --refresh/);1166my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1167unless($gs) {1168die"Unable to determine upstream SVN information from ",1169"working tree history\n";1170}1171if($_dry_run) {1172print"Remote Branch: ".$gs->refname."\n";1173print"SVN URL: ".$url."\n";1174return;1175}1176if(command(qw/diff-index HEAD --/)) {1177print STDERR "Cannot rebase with uncommited changes:\n";1178 command_noisy('status');1179exit1;1180}1181unless($_local) {1182# rebase will checkout for us, so no need to do it explicitly1183$_no_checkout='true';1184$_fetch_all?$gs->fetch_all:$gs->fetch;1185}1186 command_noisy(rebase_cmd(),$gs->refname);1187if(auto_create_empty_directories($gs)) {1188$gs->mkemptydirs;1189}1190}11911192sub cmd_show_ignore {1193my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1194$gs||= Git::SVN->new;1195my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1196$gs->prop_walk($gs->{path},$r,sub{1197my($gs,$path,$props) =@_;1198print STDOUT "\n#$path\n";1199my$s=$props->{'svn:ignore'}orreturn;1200$s=~s/[\r\n]+/\n/g;1201$s=~s/^\n+//;1202chomp$s;1203$s=~ s#^#$path#gm;1204print STDOUT "$s\n";1205});1206}12071208sub cmd_show_externals {1209my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1210$gs||= Git::SVN->new;1211my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1212$gs->prop_walk($gs->{path},$r,sub{1213my($gs,$path,$props) =@_;1214print STDOUT "\n#$path\n";1215my$s=$props->{'svn:externals'}orreturn;1216$s=~s/[\r\n]+/\n/g;1217chomp$s;1218$s=~ s#^#$path#gm;1219print STDOUT "$s\n";1220});1221}12221223sub cmd_create_ignore {1224my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1225$gs||= Git::SVN->new;1226my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1227$gs->prop_walk($gs->{path},$r,sub{1228my($gs,$path,$props) =@_;1229# $path is of the form /path/to/dir/1230$path='.'.$path;1231# SVN can have attributes on empty directories,1232# which git won't track1233 mkpath([$path])unless-d $path;1234my$ignore=$path.'.gitignore';1235my$s=$props->{'svn:ignore'}orreturn;1236open(GITIGNORE,'>',$ignore)1237or fatal("Failed to open `$ignore' for writing:$!");1238$s=~s/[\r\n]+/\n/g;1239$s=~s/^\n+//;1240chomp$s;1241# Prefix all patterns so that the ignore doesn't apply1242# to sub-directories.1243$s=~ s#^#/#gm;1244print GITIGNORE "$s\n";1245close(GITIGNORE)1246or fatal("Failed to close `$ignore':$!");1247 command_noisy('add','-f',$ignore);1248});1249}12501251sub cmd_mkdirs {1252my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1253$gs||= Git::SVN->new;1254$gs->mkemptydirs($_revision);1255}12561257sub canonicalize_path {1258my($path) =@_;1259my$dot_slash_added=0;1260if(substr($path,0,1)ne"/") {1261$path="./".$path;1262$dot_slash_added=1;1263}1264# File::Spec->canonpath doesn't collapse x/../y into y (for a1265# good reason), so let's do this manually.1266$path=~ s#/+#/#g;1267$path=~ s#/\.(?:/|$)#/#g;1268$path=~ s#/[^/]+/\.\.##g;1269$path=~ s#/$##g;1270$path=~ s#^\./## if $dot_slash_added;1271$path=~ s#^/##;1272$path=~ s#^\.$##;1273return$path;1274}12751276sub canonicalize_url {1277my($url) =@_;1278$url=~ s#^([^:]+://[^/]*/)(.*)$#$1 . canonicalize_path($2)#e;1279return$url;1280}12811282# get_svnprops(PATH)1283# ------------------1284# Helper for cmd_propget and cmd_proplist below.1285sub get_svnprops {1286my$path=shift;1287my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1288$gs||= Git::SVN->new;12891290# prefix THE PATH by the sub-directory from which the user1291# invoked us.1292$path=$cmd_dir_prefix.$path;1293 fatal("No such file or directory:$path")unless-e $path;1294my$is_dir= -d $path?1:0;1295$path=$gs->{path} .'/'.$path;12961297# canonicalize the path (otherwise libsvn will abort or fail to1298# find the file)1299$path= canonicalize_path($path);13001301my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1302my$props;1303if($is_dir) {1304(undef,undef,$props) =$gs->ra->get_dir($path,$r);1305}1306else{1307(undef,$props) =$gs->ra->get_file($path,$r,undef);1308}1309return$props;1310}13111312# cmd_propget (PROP, PATH)1313# ------------------------1314# Print the SVN property PROP for PATH.1315sub cmd_propget {1316my($prop,$path) =@_;1317$path='.'ifnot defined$path;1318 usage(1)ifnot defined$prop;1319my$props= get_svnprops($path);1320if(not defined$props->{$prop}) {1321 fatal("`$path' does not have a `$prop' SVN property.");1322}1323print$props->{$prop} ."\n";1324}13251326# cmd_proplist (PATH)1327# -------------------1328# Print the list of SVN properties for PATH.1329sub cmd_proplist {1330my$path=shift;1331$path='.'ifnot defined$path;1332my$props= get_svnprops($path);1333print"Properties on '$path':\n";1334foreach(sort keys%{$props}) {1335print"$_\n";1336}1337}13381339sub cmd_multi_init {1340my$url=shift;1341unless(defined$_trunk||@_branches||@_tags) {1342 usage(1);1343}13441345$_prefix=''unlessdefined$_prefix;1346if(defined$url) {1347$url= canonicalize_url($url);1348 init_subdir(@_);1349}1350 do_git_init_db();1351if(defined$_trunk) {1352$_trunk=~ s#^/+##;1353my$trunk_ref='refs/remotes/'.$_prefix.'trunk';1354# try both old-style and new-style lookups:1355my$gs_trunk=eval{ Git::SVN->new($trunk_ref) };1356unless($gs_trunk) {1357my($trunk_url,$trunk_path) =1358 complete_svn_url($url,$_trunk);1359$gs_trunk= Git::SVN->init($trunk_url,$trunk_path,1360undef,$trunk_ref);1361}1362}1363return unless@_branches||@_tags;1364my$ra=$url? Git::SVN::Ra->new($url) :undef;1365foreachmy$path(@_branches) {1366 complete_url_ls_init($ra,$path,'--branches/-b',$_prefix);1367}1368foreachmy$path(@_tags) {1369 complete_url_ls_init($ra,$path,'--tags/-t',$_prefix.'tags/');1370}1371}13721373sub cmd_multi_fetch {1374$Git::SVN::no_reuse_existing =undef;1375my$remotes= Git::SVN::read_all_remotes();1376foreachmy$repo_id(sort keys%$remotes) {1377if($remotes->{$repo_id}->{url}) {1378 Git::SVN::fetch_all($repo_id,$remotes);1379}1380}1381}13821383# this command is special because it requires no metadata1384sub cmd_commit_diff {1385my($ta,$tb,$url) =@_;1386my$usage="Usage:$0commit-diff -r<revision> ".1387"<tree-ish> <tree-ish> [<URL>]";1388 fatal($usage)if(!defined$ta|| !defined$tb);1389my$svn_path='';1390if(!defined$url) {1391my$gs=eval{ Git::SVN->new};1392if(!$gs) {1393 fatal("Needed URL or usable git-svn --id in ",1394"the command-line\n",$usage);1395}1396$url=$gs->{url};1397$svn_path=$gs->{path};1398}1399unless(defined$_revision) {1400 fatal("-r|--revision is a required argument\n",$usage);1401}1402if(defined$_message&&defined$_file) {1403 fatal("Both --message/-m and --file/-F specified ",1404"for the commit message.\n",1405"I have no idea what you mean");1406}1407if(defined$_file) {1408$_message= file_to_s($_file);1409}else{1410$_message||= get_commit_entry($tb)->{log};1411}1412my$ra||= Git::SVN::Ra->new($url);1413my$r=$_revision;1414if($req'HEAD') {1415$r=$ra->get_latest_revnum;1416}elsif($r!~/^\d+$/) {1417die"revision argument:$rnot understood by git-svn\n";1418}1419my%ed_opts= ( r =>$r,1420log=>$_message,1421 ra =>$ra,1422 tree_a =>$ta,1423 tree_b =>$tb,1424 editor_cb =>sub{print"Committed r$_[0]\n"},1425 svn_path =>$svn_path);1426if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {1427print"No changes\n$ta==$tb\n";1428}1429}14301431sub escape_uri_only {1432my($uri) =@_;1433my@tmp;1434foreach(splitm{/},$uri) {1435s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;1436push@tmp,$_;1437}1438join('/',@tmp);1439}14401441sub escape_url {1442my($url) =@_;1443if($url=~ m#^([^:]+)://([^/]*)(.*)$#) {1444my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));1445$url="$scheme://$domain$uri";1446}1447$url;1448}14491450sub cmd_info {1451my$path= canonicalize_path(defined($_[0]) ?$_[0] :".");1452my$fullpath= canonicalize_path($cmd_dir_prefix.$path);1453if(exists$_[1]) {1454die"Too many arguments specified\n";1455}14561457my($file_type,$diff_status) = find_file_type_and_diff_status($path);14581459if(!$file_type&& !$diff_status) {1460print STDERR "svn: '$path' is not under version control\n";1461exit1;1462}14631464my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1465unless($gs) {1466die"Unable to determine upstream SVN information from ",1467"working tree history\n";1468}14691470# canonicalize_path() will return "" to make libsvn 1.5.x happy,1471$path="."if$patheq"";14721473my$full_url=$url. ($fullpatheq""?"":"/$fullpath");14741475if($_url) {1476print escape_url($full_url),"\n";1477return;1478}14791480my$result="Path:$path\n";1481$result.="Name: ". basename($path) ."\n"if$file_typene"dir";1482$result.="URL: ". escape_url($full_url) ."\n";14831484eval{1485my$repos_root=$gs->repos_root;1486 Git::SVN::remove_username($repos_root);1487$result.="Repository Root: ". escape_url($repos_root) ."\n";1488};1489if($@) {1490$result.="Repository Root: (offline)\n";1491}1492::_req_svn();1493$result.="Repository UUID:$uuid\n"unless$diff_statuseq"A"&&1494(::compare_svn_version('1.5.4') <=0||$file_typene"dir");1495$result.="Revision: ". ($diff_statuseq"A"?0:$rev) ."\n";14961497$result.="Node Kind: ".1498($file_typeeq"dir"?"directory":"file") ."\n";14991500my$schedule=$diff_statuseq"A"1501?"add"1502: ($diff_statuseq"D"?"delete":"normal");1503$result.="Schedule:$schedule\n";15041505if($diff_statuseq"A") {1506print$result,"\n";1507return;1508}15091510my($lc_author,$lc_rev,$lc_date_utc);1511my@args= Git::SVN::Log::git_svn_log_cmd($rev,$rev,"--",$fullpath);1512my$log= command_output_pipe(@args);1513my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;1514while(<$log>) {1515if(/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {1516$lc_author=$1;1517$lc_date_utc= Git::SVN::Log::parse_git_date($2,$3);1518}elsif(/^${esc_color} (git-svn-id:.+)$/o) {1519(undef,$lc_rev,undef) = ::extract_metadata($1);1520}1521}1522close$log;15231524 Git::SVN::Log::set_local_timezone();15251526$result.="Last Changed Author:$lc_author\n";1527$result.="Last Changed Rev:$lc_rev\n";1528$result.="Last Changed Date: ".1529 Git::SVN::Log::format_svn_date($lc_date_utc) ."\n";15301531if($file_typene"dir") {1532my$text_last_updated_date=1533($diff_statuseq"D"?$lc_date_utc: (stat$path)[9]);1534$result.=1535"Text Last Updated: ".1536 Git::SVN::Log::format_svn_date($text_last_updated_date) .1537"\n";1538my$checksum;1539if($diff_statuseq"D") {1540my($fh,$ctx) =1541 command_output_pipe(qw(cat-file blob),"HEAD:$path");1542if($file_typeeq"link") {1543my$file_name= <$fh>;1544$checksum= md5sum("link$file_name");1545}else{1546$checksum= md5sum($fh);1547}1548 command_close_pipe($fh,$ctx);1549}elsif($file_typeeq"link") {1550my$file_name=1551 command(qw(cat-file blob),"HEAD:$path");1552$checksum=1553 md5sum("link ".$file_name);1554}else{1555open FILE,"<",$pathor die$!;1556$checksum= md5sum(\*FILE);1557close FILE or die$!;1558}1559$result.="Checksum: ".$checksum."\n";1560}15611562print$result,"\n";1563}15641565sub cmd_reset {1566my$target=shift||$_revisionor die"SVN revision required\n";1567$target=$1if$target=~/^r(\d+)$/;1568$target=~/^\d+$/or die"Numeric SVN revision expected\n";1569my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1570unless($gs) {1571die"Unable to determine upstream SVN information from ".1572"history\n";1573}1574my($r,$c) =$gs->find_rev_before($target,not$_fetch_parent);1575die"Cannot find SVN revision$target\n"unlessdefined($c);1576$gs->rev_map_set($r,$c,'reset',$uuid);1577print"r$r=$c($gs->{ref_id})\n";1578}15791580sub cmd_gc {1581if(!$can_compress) {1582warn"Compress::Zlib could not be found; unhandled.log ".1583"files will not be compressed.\n";1584}1585 find({ wanted => \&gc_directory, no_chdir =>1},"$ENV{GIT_DIR}/svn");1586}15871588########################### utility functions #########################15891590sub rebase_cmd {1591my@cmd= qw/rebase/;1592push@cmd,'-v'if$_verbose;1593push@cmd, qw/--merge/if$_merge;1594push@cmd,"--strategy=$_strategy"if$_strategy;1595@cmd;1596}15971598sub post_fetch_checkout {1599return if$_no_checkout;1600my$gs=$Git::SVN::_head orreturn;1601return if verify_ref('refs/heads/master^0');16021603# look for "trunk" ref if it exists1604my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1605my$fetch=$remote->{fetch};1606if($fetch) {1607foreachmy$p(keys%$fetch) {1608 basename($fetch->{$p})eq'trunk'ornext;1609$gs= Git::SVN->new($fetch->{$p},$gs->{repo_id},$p);1610last;1611}1612}16131614my$valid_head= verify_ref('HEAD^0');1615 command_noisy(qw(update-ref refs/heads/master),$gs->refname);1616return if($valid_head|| !verify_ref('HEAD^0'));16171618return if$ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;1619my$index=$ENV{GIT_INDEX_FILE} ||"$ENV{GIT_DIR}/index";1620return if-f $index;16211622return if command_oneline(qw/rev-parse --is-inside-work-tree/)eq'false';1623return if command_oneline(qw/rev-parse --is-inside-git-dir/)eq'true';1624 command_noisy(qw/read-tree -m -u -v HEAD HEAD/);1625print STDERR "Checked out HEAD:\n",1626$gs->full_url," r",$gs->last_rev,"\n";1627if(auto_create_empty_directories($gs)) {1628$gs->mkemptydirs($gs->last_rev);1629}1630}16311632sub complete_svn_url {1633my($url,$path) =@_;1634$path=~ s#/+$##;1635if($path!~ m#^[a-z\+]+://#) {1636if(!defined$url||$url!~ m#^[a-z\+]+://#) {1637 fatal("E: '$path' is not a complete URL ",1638"and a separate URL is not specified");1639}1640return($url,$path);1641}1642return($path,'');1643}16441645sub complete_url_ls_init {1646my($ra,$repo_path,$switch,$pfx) =@_;1647unless($repo_path) {1648print STDERR "W:$switchnot specified\n";1649return;1650}1651$repo_path=~ s#/+$##;1652if($repo_path=~ m#^[a-z\+]+://#) {1653$ra= Git::SVN::Ra->new($repo_path);1654$repo_path='';1655}else{1656$repo_path=~ s#^/+##;1657unless($ra) {1658 fatal("E: '$repo_path' is not a complete URL ",1659"and a separate URL is not specified");1660}1661}1662my$url=$ra->{url};1663my$gs= Git::SVN->init($url,undef,undef,undef,1);1664my$k="svn-remote.$gs->{repo_id}.url";1665my$orig_url=eval{ command_oneline(qw/config --get/,$k) };1666if($orig_url&& ($orig_urlne$gs->{url})) {1667die"$kalready set:$orig_url\n",1668"wanted to set to:$gs->{url}\n";1669}1670 command_oneline('config',$k,$gs->{url})unless$orig_url;1671my$remote_path="$gs->{path}/$repo_path";1672$remote_path=~s{%([0-9A-F]{2})}{chr hex($1)}ieg;1673$remote_path=~ s#/+#/#g;1674$remote_path=~ s#^/##g;1675$remote_path.="/*"if$remote_path!~ /\*/;1676my($n) = ($switch=~/^--(\w+)/);1677if(length$pfx&&$pfx!~ m#/$#) {1678die"--prefix='$pfx' must have a trailing slash '/'\n";1679}1680 command_noisy('config',1681'--add',1682"svn-remote.$gs->{repo_id}.$n",1683"$remote_path:refs/remotes/$pfx*".1684('/*' x (($remote_path=~ tr/*/*/) -1)) );1685}16861687sub verify_ref {1688my($ref) =@_;1689eval{ command_oneline(['rev-parse','--verify',$ref],1690{ STDERR =>0}); };1691}16921693sub get_tree_from_treeish {1694my($treeish) =@_;1695# $treeish can be a symbolic ref, too:1696my$type= command_oneline(qw/cat-file -t/,$treeish);1697my$expected;1698while($typeeq'tag') {1699($treeish,$type) = command(qw/cat-file tag/,$treeish);1700}1701if($typeeq'commit') {1702$expected= (grep/^tree /, command(qw/cat-file commit/,1703$treeish))[0];1704($expected) = ($expected=~/^tree ($sha1)$/o);1705die"Unable to get tree from$treeish\n"unless$expected;1706}elsif($typeeq'tree') {1707$expected=$treeish;1708}else{1709die"$treeishis a$type, expected tree, tag or commit\n";1710}1711return$expected;1712}17131714sub get_commit_entry {1715my($treeish) =shift;1716my%log_entry= (log=>'', tree => get_tree_from_treeish($treeish) );1717my$commit_editmsg="$ENV{GIT_DIR}/COMMIT_EDITMSG";1718my$commit_msg="$ENV{GIT_DIR}/COMMIT_MSG";1719open my$log_fh,'>',$commit_editmsgor croak $!;17201721my$type= command_oneline(qw/cat-file -t/,$treeish);1722if($typeeq'commit'||$typeeq'tag') {1723my($msg_fh,$ctx) = command_output_pipe('cat-file',1724$type,$treeish);1725my$in_msg=0;1726my$author;1727my$saw_from=0;1728my$msgbuf="";1729while(<$msg_fh>) {1730if(!$in_msg) {1731$in_msg=1if(/^\s*$/);1732$author=$1if(/^author (.*>)/);1733}elsif(/^git-svn-id: /) {1734# skip this for now, we regenerate the1735# correct one on re-fetch anyways1736# TODO: set *:merge properties or like...1737}else{1738if(/^From:/||/^Signed-off-by:/) {1739$saw_from=1;1740}1741$msgbuf.=$_;1742}1743}1744$msgbuf=~s/\s+$//s;1745if($Git::SVN::_add_author_from &&defined($author)1746&& !$saw_from) {1747$msgbuf.="\n\nFrom:$author";1748}1749print$log_fh $msgbufor croak $!;1750 command_close_pipe($msg_fh,$ctx);1751}1752close$log_fhor croak $!;17531754if($_edit|| ($typeeq'tree')) {1755chomp(my$editor= command_oneline(qw(var GIT_EDITOR)));1756system('sh','-c',$editor.' "$@"',$editor,$commit_editmsg);1757}1758rename$commit_editmsg,$commit_msgor croak $!;1759{1760require Encode;1761# SVN requires messages to be UTF-8 when entering the repo1762local$/;1763open$log_fh,'<',$commit_msgor croak $!;1764binmode$log_fh;1765chomp($log_entry{log} = <$log_fh>);17661767my$enc= Git::config('i18n.commitencoding') ||'UTF-8';1768my$msg=$log_entry{log};17691770eval{$msg= Encode::decode($enc,$msg,1) };1771if($@) {1772die"Could not decode as$enc:\n",$msg,1773"\nPerhaps you need to set i18n.commitencoding\n";1774}17751776eval{$msg= Encode::encode('UTF-8',$msg,1) };1777die"Could not encode as UTF-8:\n$msg\n"if$@;17781779$log_entry{log} =$msg;17801781close$log_fhor croak $!;1782}1783unlink$commit_msg;1784 \%log_entry;1785}17861787sub s_to_file {1788my($str,$file,$mode) =@_;1789open my$fd,'>',$fileor croak $!;1790print$fd $str,"\n"or croak $!;1791close$fdor croak $!;1792chmod($mode&~umask,$file)if(defined$mode);1793}17941795sub file_to_s {1796my$file=shift;1797open my$fd,'<',$fileor croak "$!: file:$file\n";1798local$/;1799my$ret= <$fd>;1800close$fdor croak $!;1801$ret=~s/\s*$//s;1802return$ret;1803}18041805# '<svn username> = real-name <email address>' mapping based on git-svnimport:1806sub load_authors {1807open my$authors,'<',$_authorsor die"Can't open$_authors$!\n";1808my$log=$cmdeq'log';1809while(<$authors>) {1810chomp;1811next unless/^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;1812my($user,$name,$email) = ($1,$2,$3);1813if($log) {1814$Git::SVN::Log::rusers{"$name<$email>"} =$user;1815}else{1816$users{$user} = [$name,$email];1817}1818}1819close$authorsor croak $!;1820}18211822# convert GetOpt::Long specs for use by git-config1823sub read_git_config {1824my$opts=shift;1825my@config_only;1826foreachmy$o(keys%$opts) {1827# if we have mixedCase and a long option-only, then1828# it's a config-only variable that we don't need for1829# the command-line.1830push@config_only,$oif($o=~/[A-Z]/&&$o=~/^[a-z]+$/i);1831my$v=$opts->{$o};1832my($key) = ($o=~/^([a-zA-Z\-]+)/);1833$key=~s/-//g;1834my$arg='git config';1835$arg.=' --int'if($o=~/[:=]i$/);1836$arg.=' --bool'if($o!~/[:=][sfi]$/);1837if(ref$veq'ARRAY') {1838chomp(my@tmp=`$arg--get-all svn.$key`);1839@$v=@tmpif@tmp;1840 } else {1841 chomp(my$tmp= `$arg--get svn.$key`);1842if($tmp&& !($arg=~/ --bool/&&$tmpeq'false')) {1843$$v=$tmp;1844}1845}1846}1847delete@$opts{@config_only}if@config_only;1848}18491850sub extract_metadata {1851my$id=shift orreturn(undef,undef,undef);1852my($url,$rev,$uuid) = ($id=~ /^\s*git-svn-id:\s+(.*)\@(\d+)1853 \s([a-f\d\-]+)$/ix);1854if(!defined$rev|| !$uuid|| !$url) {1855# some of the original repositories I made had1856# identifiers like this:1857($rev,$uuid) = ($id=~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i);1858}1859return($url,$rev,$uuid);1860}18611862sub cmt_metadata {1863return extract_metadata((grep(/^git-svn-id: /,1864 command(qw/cat-file commit/,shift)))[-1]);1865}18661867sub cmt_sha2rev_batch {1868my%s2r;1869my($pid,$in,$out,$ctx) = command_bidi_pipe(qw/cat-file --batch/);1870my$list=shift;18711872foreachmy$sha(@{$list}) {1873my$first=1;1874my$size=0;1875print$out $sha,"\n";18761877while(my$line= <$in>) {1878if($first&&$line=~/^[[:xdigit:]]{40}\smissing$/) {1879last;1880}elsif($first&&1881$line=~/^[[:xdigit:]]{40}\scommit\s(\d+)$/) {1882$first=0;1883$size=$1;1884next;1885}elsif($line=~/^(git-svn-id: )/) {1886my(undef,$rev,undef) =1887 extract_metadata($line);1888$s2r{$sha} =$rev;1889}18901891$size-=length($line);1892last if($size==0);1893}1894}18951896 command_close_bidi_pipe($pid,$in,$out,$ctx);18971898return \%s2r;1899}19001901sub working_head_info {1902my($head,$refs) =@_;1903my@args= qw/rev-list --first-parent --pretty=medium/;1904my($fh,$ctx) = command_output_pipe(@args,$head);1905my$hash;1906my%max;1907while(<$fh>) {1908if(m{^commit ($::sha1)$}) {1909unshift@$refs,$hashif$hashand$refs;1910$hash=$1;1911next;1912}1913next unlesss{^\s*(git-svn-id:)}{$1};1914my($url,$rev,$uuid) = extract_metadata($_);1915if(defined$url&&defined$rev) {1916next if$max{$url}and$max{$url} <$rev;1917if(my$gs= Git::SVN->find_by_url($url)) {1918my$c=$gs->rev_map_get($rev,$uuid);1919if($c&&$ceq$hash) {1920close$fh;# break the pipe1921return($url,$rev,$uuid,$gs);1922}else{1923$max{$url} ||=$gs->rev_map_max;1924}1925}1926}1927}1928 command_close_pipe($fh,$ctx);1929(undef,undef,undef,undef);1930}19311932sub read_commit_parents {1933my($parents,$c) =@_;1934chomp(my$p= command_oneline(qw/rev-list --parents -1/,$c));1935$p=~s/^($c)\s*//or die"rev-list --parents -1$cfailed!\n";1936@{$parents->{$c}} =split(/ /,$p);1937}19381939sub linearize_history {1940my($gs,$refs) =@_;1941my%parents;1942foreachmy$c(@$refs) {1943 read_commit_parents(\%parents,$c);1944}19451946my@linear_refs;1947my%skip= ();1948my$last_svn_commit=$gs->last_commit;1949foreachmy$c(reverse@$refs) {1950next if$ceq$last_svn_commit;1951last if$skip{$c};19521953unshift@linear_refs,$c;1954$skip{$c} =1;19551956# we only want the first parent to diff against for linear1957# history, we save the rest to inject when we finalize the1958# svn commit1959my$fp_a= verify_ref("$c~1");1960my$fp_b=shift@{$parents{$c}}if$parents{$c};1961if(!$fp_a|| !$fp_b) {1962die"Commit$c\n",1963"has no parent commit, and therefore ",1964"nothing to diff against.\n",1965"You should be working from a repository ",1966"originally created by git-svn\n";1967}1968if($fp_ane$fp_b) {1969die"$c~1=$fp_a, however parsing commit$c",1970"revealed that:\n$c~1=$fp_b\nBUG!\n";1971}19721973foreachmy$p(@{$parents{$c}}) {1974$skip{$p} =1;1975}1976}1977(\@linear_refs, \%parents);1978}19791980sub find_file_type_and_diff_status {1981my($path) =@_;1982return('dir','')if$patheq'';19831984my$diff_output=1985 command_oneline(qw(diff --cached --name-status --),$path) ||"";1986my$diff_status= (split(' ',$diff_output))[0] ||"";19871988my$ls_tree= command_oneline(qw(ls-tree HEAD),$path) ||"";19891990return(undef,undef)if!$diff_status&& !$ls_tree;19911992if($diff_statuseq"A") {1993return("link",$diff_status)if-l $path;1994return("dir",$diff_status)if-d $path;1995return("file",$diff_status);1996}19971998my$mode= (split(' ',$ls_tree))[0] ||"";19992000return("link",$diff_status)if$modeeq"120000";2001return("dir",$diff_status)if$modeeq"040000";2002return("file",$diff_status);2003}20042005sub md5sum {2006my$arg=shift;2007my$ref=ref$arg;2008my$md5= Digest::MD5->new();2009if($refeq'GLOB'||$refeq'IO::File'||$refeq'File::Temp') {2010$md5->addfile($arg)or croak $!;2011}elsif($refeq'SCALAR') {2012$md5->add($$arg)or croak $!;2013}elsif(!$ref) {2014$md5->add($arg)or croak $!;2015}else{2016::fatal "Can't provide MD5 hash for unknown ref type: '",$ref,"'";2017}2018return$md5->hexdigest();2019}20202021sub gc_directory {2022if($can_compress&& -f $_&& basename($_)eq"unhandled.log") {2023my$out_filename=$_.".gz";2024open my$in_fh,"<",$_or die"Unable to open$_:$!\n";2025binmode$in_fh;2026my$gz= Compress::Zlib::gzopen($out_filename,"ab")or2027die"Unable to open$out_filename:$!\n";20282029my$res;2030while($res=sysread($in_fh,my$str,1024)) {2031$gz->gzwrite($str)or2032die"Unable to write: ".$gz->gzerror()."!\n";2033}2034unlink$_or die"unlink$File::Find::name:$!\n";2035}elsif(-f $_&& basename($_)eq"index") {2036unlink$_or die"unlink$_:$!\n";2037}2038}20392040package Git::SVN;2041use strict;2042use warnings;2043use Fcntl qw/:DEFAULT :seek/;2044useconstant rev_map_fmt =>'NH40';2045use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent2046$_repack $_repack_flags $_use_svm_props $_head2047$_use_svnsync_props $no_reuse_existing $_minimize_url2048$_use_log_author $_add_author_from $_localtime/;2049use Carp qw/croak/;2050use File::Path qw/mkpath/;2051use File::Copy qw/copy/;2052use IPC::Open3;2053use Time::Local;2054use Memoize;# core since 5.8.0, Jul 20022055use Memoize::Storable;2056use POSIX qw(:signal_h);20572058my($_gc_nr,$_gc_period);20592060# properties that we do not log:2061my%SKIP_PROP;2062BEGIN{2063%SKIP_PROP=map{$_=>1} qw/svn:wc:ra_dav:version-url2064 svn:special svn:executable2065 svn:entry:committed-rev2066 svn:entry:last-author2067 svn:entry:uuid2068 svn:entry:committed-date/;20692070# some options are read globally, but can be overridden locally2071# per [svn-remote "..."] section. Command-line options will *NOT*2072# override options set in an [svn-remote "..."] section2073no strict 'refs';2074formy$option(qw/follow_parent no_metadata use_svm_props2075 use_svnsync_props/) {2076my$key=$option;2077$key=~tr/_//d;2078my$prop="-$option";2079*$option=sub{2080my($self) =@_;2081return$self->{$prop}ifexists$self->{$prop};2082my$k="svn-remote.$self->{repo_id}.$key";2083eval{ command_oneline(qw/config --get/,$k) };2084if($@) {2085$self->{$prop} = ${"Git::SVN::_$option"};2086}else{2087my$v= command_oneline(qw/config --bool/,$k);2088$self->{$prop} =$veq'false'?0:1;2089}2090return$self->{$prop};2091}2092}2093}209420952096my(%LOCKFILES,%INDEX_FILES);2097END{2098unlink keys%LOCKFILESif%LOCKFILES;2099unlink keys%INDEX_FILESif%INDEX_FILES;2100}21012102sub resolve_local_globs {2103my($url,$fetch,$glob_spec) =@_;2104return unlessdefined$glob_spec;2105my$ref=$glob_spec->{ref};2106my$path=$glob_spec->{path};2107foreach(command(qw#for-each-ref --format=%(refname) refs/#)) {2108next unless m#^$ref->{regex}$#;2109my$p=$1;2110my$pathname= desanitize_refname($path->full_path($p));2111my$refname= desanitize_refname($ref->full_path($p));2112if(my$existing=$fetch->{$pathname}) {2113if($existingne$refname) {2114die"Refspec conflict:\n",2115"existing:$existing\n",2116" globbed:$refname\n";2117}2118my$u= (::cmt_metadata("$refname"))[0];2119$u=~s!^\Q$url\E(/|$)!!or die2120"$refname: '$url' not found in '$u'\n";2121if($pathnamene$u) {2122warn"W: Refspec glob conflict ",2123"(ref:$refname):\n",2124"expected path:$pathname\n",2125" real path:$u\n",2126"Continuing ahead with$u\n";2127next;2128}2129}else{2130$fetch->{$pathname} =$refname;2131}2132}2133}21342135sub parse_revision_argument {2136my($base,$head) =@_;2137if(!defined$::_revision || $::_revision eq'BASE:HEAD') {2138return($base,$head);2139}2140return($1,$2)if($::_revision =~/^(\d+):(\d+)$/);2141return($::_revision, $::_revision)if($::_revision =~/^\d+$/);2142return($head,$head)if($::_revision eq'HEAD');2143return($base,$1)if($::_revision =~/^BASE:(\d+)$/);2144return($1,$head)if($::_revision =~/^(\d+):HEAD$/);2145die"revision argument: $::_revision not understood by git-svn\n";2146}21472148sub fetch_all {2149my($repo_id,$remotes) =@_;2150if(ref$repo_id) {2151my$gs=$repo_id;2152$repo_id=undef;2153$repo_id=$gs->{repo_id};2154}2155$remotes||= read_all_remotes();2156my$remote=$remotes->{$repo_id}or2157die"[svn-remote\"$repo_id\"] unknown\n";2158my$fetch=$remote->{fetch};2159my$url=$remote->{url}or die"svn-remote.$repo_id.url not defined\n";2160my(@gs,@globs);2161my$ra= Git::SVN::Ra->new($url);2162my$uuid=$ra->get_uuid;2163my$head=$ra->get_latest_revnum;21642165# ignore errors, $head revision may not even exist anymore2166eval{$ra->get_log("",$head,0,1,0,1,sub{$head=$_[1] }) };2167warn"W:$@\n"if$@;21682169my$base=defined$fetch?$head:0;21702171# read the max revs for wildcard expansion (branches/*, tags/*)2172foreachmy$t(qw/branches tags/) {2173defined$remote->{$t}ornext;2174push@globs, @{$remote->{$t}};21752176my$max_rev=eval{ tmp_config(qw/--int --get/,2177"svn-remote.$repo_id.${t}-maxRev") };2178if(defined$max_rev&& ($max_rev<$base)) {2179$base=$max_rev;2180}elsif(!defined$max_rev) {2181$base=0;2182}2183}21842185if($fetch) {2186foreachmy$p(sort keys%$fetch) {2187my$gs= Git::SVN->new($fetch->{$p},$repo_id,$p);2188my$lr=$gs->rev_map_max;2189if(defined$lr) {2190$base=$lrif($lr<$base);2191}2192push@gs,$gs;2193}2194}21952196($base,$head) = parse_revision_argument($base,$head);2197$ra->gs_fetch_loop_common($base,$head, \@gs, \@globs);2198}21992200sub read_all_remotes {2201my$r= {};2202my$use_svm_props=eval{ command_oneline(qw/config --bool2203 svn.useSvmProps/) };2204$use_svm_props=$use_svm_propseq'true'if$use_svm_props;2205my$svn_refspec=qr{\s*(.*?)\s*:\s*(.+?)\s*};2206foreach(grep{s/^svn-remote\.//} command(qw/config -l/)) {2207if(m!^(.+)\.fetch=$svn_refspec$!) {2208my($remote,$local_ref,$remote_ref) = ($1,$2,$3);2209die("svn-remote.$remote: remote ref '$remote_ref' "2210."must start with 'refs/'\n")2211unless$remote_ref=~m{^refs/};2212$local_ref= uri_decode($local_ref);2213$r->{$remote}->{fetch}->{$local_ref} =$remote_ref;2214$r->{$remote}->{svm} = {}if$use_svm_props;2215}elsif(m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {2216$r->{$1}->{svm} = {};2217}elsif(m!^(.+)\.url=\s*(.*)\s*$!) {2218$r->{$1}->{url} =$2;2219}elsif(m!^(.+)\.pushurl=\s*(.*)\s*$!) {2220$r->{$1}->{pushurl} =$2;2221}elsif(m!^(.+)\.ignore-refs=\s*(.*)\s*$!) {2222$r->{$1}->{ignore_refs_regex} =$2;2223}elsif(m!^(.+)\.(branches|tags)=$svn_refspec$!) {2224my($remote,$t,$local_ref,$remote_ref) =2225($1,$2,$3,$4);2226die("svn-remote.$remote: remote ref '$remote_ref' ($t) "2227."must start with 'refs/'\n")2228unless$remote_ref=~m{^refs/};2229$local_ref= uri_decode($local_ref);2230my$rs= {2231 t =>$t,2232 remote =>$remote,2233 path => Git::SVN::GlobSpec->new($local_ref,1),2234ref=> Git::SVN::GlobSpec->new($remote_ref,0) };2235if(length($rs->{ref}->{right}) !=0) {2236die"The '*' glob character must be the last ",2237"character of '$remote_ref'\n";2238}2239push@{$r->{$remote}->{$t} },$rs;2240}2241}22422243map{2244if(defined$r->{$_}->{svm}) {2245my$svm;2246eval{2247my$section="svn-remote.$_";2248$svm= {2249 source => tmp_config('--get',2250"$section.svm-source"),2251 replace => tmp_config('--get',2252"$section.svm-replace"),2253}2254};2255$r->{$_}->{svm} =$svm;2256}2257}keys%$r;22582259foreachmy$remote(keys%$r) {2260foreach(grep{defined$_}2261map{$r->{$remote}->{$_} }qw(branches tags)) {2262foreachmy$rs(@$_) {2263$rs->{ignore_refs_regex} =2264$r->{$remote}->{ignore_refs_regex};2265}2266}2267}22682269$r;2270}22712272sub init_vars {2273$_gc_nr=$_gc_period=1000;2274if(defined$_repack||defined$_repack_flags) {2275warn"Repack options are obsolete; they have no effect.\n";2276}2277}22782279sub verify_remotes_sanity {2280return unless-d $ENV{GIT_DIR};2281my%seen;2282foreach(command(qw/config -l/)) {2283if(m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {2284if($seen{$1}) {2285die"Remote ref refs/remote/$1is tracked by",2286"\n \"$_\"\nand\n \"$seen{$1}\"\n",2287"Please resolve this ambiguity in ",2288"your git configuration file before ",2289"continuing\n";2290}2291$seen{$1} =$_;2292}2293}2294}22952296sub find_existing_remote {2297my($url,$remotes) =@_;2298returnundefif$no_reuse_existing;2299my$existing;2300foreachmy$repo_id(keys%$remotes) {2301my$u=$remotes->{$repo_id}->{url}ornext;2302next if$une$url;2303$existing=$repo_id;2304last;2305}2306$existing;2307}23082309sub init_remote_config {2310my($self,$url,$no_write) =@_;2311$url=~s!/+$!!;# strip trailing slash2312my$r= read_all_remotes();2313my$existing= find_existing_remote($url,$r);2314if($existing) {2315unless($no_write) {2316print STDERR "Using existing ",2317"[svn-remote\"$existing\"]\n";2318}2319$self->{repo_id} =$existing;2320}elsif($_minimize_url) {2321my$min_url= Git::SVN::Ra->new($url)->minimize_url;2322$existing= find_existing_remote($min_url,$r);2323if($existing) {2324unless($no_write) {2325print STDERR "Using existing ",2326"[svn-remote\"$existing\"]\n";2327}2328$self->{repo_id} =$existing;2329}2330if($min_urlne$url) {2331unless($no_write) {2332print STDERR "Using higher level of URL: ",2333"$url=>$min_url\n";2334}2335my$old_path=$self->{path};2336$self->{path} =$url;2337$self->{path} =~s!^\Q$min_url\E(/|$)!!;2338if(length$old_path) {2339$self->{path} .="/$old_path";2340}2341$url=$min_url;2342}2343}2344my$orig_url;2345if(!$existing) {2346# verify that we aren't overwriting anything:2347$orig_url=eval{2348 command_oneline('config','--get',2349"svn-remote.$self->{repo_id}.url")2350};2351if($orig_url&& ($orig_urlne$url)) {2352die"svn-remote.$self->{repo_id}.url already set: ",2353"$orig_url\nwanted to set to:$url\n";2354}2355}2356my($xrepo_id,$xpath) = find_ref($self->refname);2357if(!$no_write&&defined$xpath) {2358die"svn-remote.$xrepo_id.fetch already set to track ",2359"$xpath:",$self->refname,"\n";2360}2361unless($no_write) {2362 command_noisy('config',2363"svn-remote.$self->{repo_id}.url",$url);2364$self->{path} =~s{^/}{};2365$self->{path} =~s{%([0-9A-F]{2})}{chr hex($1)}ieg;2366 command_noisy('config','--add',2367"svn-remote.$self->{repo_id}.fetch",2368"$self->{path}:".$self->refname);2369}2370$self->{url} =$url;2371}23722373sub find_by_url {# repos_root and, path are optional2374my($class,$full_url,$repos_root,$path) =@_;23752376returnundefunlessdefined$full_url;2377 remove_username($full_url);2378 remove_username($repos_root)ifdefined$repos_root;2379my$remotes= read_all_remotes();2380if(defined$full_url&&defined$repos_root&& !defined$path) {2381$path=$full_url;2382$path=~ s#^\Q$repos_root\E(?:/|$)##;2383}2384foreachmy$repo_id(keys%$remotes) {2385my$u=$remotes->{$repo_id}->{url}ornext;2386 remove_username($u);2387next ifdefined$repos_root&&$repos_rootne$u;23882389my$fetch=$remotes->{$repo_id}->{fetch} || {};2390foreachmy$t(qw/branches tags/) {2391foreachmy$globspec(@{$remotes->{$repo_id}->{$t}}) {2392 resolve_local_globs($u,$fetch,$globspec);2393}2394}2395my$p=$path;2396my$rwr= rewrite_root({repo_id =>$repo_id});2397my$svm=$remotes->{$repo_id}->{svm}2398ifdefined$remotes->{$repo_id}->{svm};2399unless(defined$p) {2400$p=$full_url;2401my$z=$u;2402my$prefix='';2403if($rwr) {2404$z=$rwr;2405 remove_username($z);2406}elsif(defined$svm) {2407$z=$svm->{source};2408$prefix=$svm->{replace};2409$prefix=~ s#^\Q$u\E(?:/|$)##;2410$prefix=~ s#/$##;2411}2412$p=~ s#^\Q$z\E(?:/|$)#$prefix# or next;2413}2414foreachmy$f(keys%$fetch) {2415next if$fne$p;2416return Git::SVN->new($fetch->{$f},$repo_id,$f);2417}2418}2419undef;2420}24212422sub init {2423my($class,$url,$path,$repo_id,$ref_id,$no_write) =@_;2424my$self= _new($class,$repo_id,$ref_id,$path);2425if(defined$url) {2426$self->init_remote_config($url,$no_write);2427}2428$self;2429}24302431sub find_ref {2432my($ref_id) =@_;2433foreach(command(qw/config -l/)) {2434next unless m!^svn-remote\.(.+)\.fetch=2435 \s*(.*?)\s*:\s*(.+?)\s*$!x;2436my($repo_id,$path,$ref) = ($1,$2,$3);2437if($refeq$ref_id) {2438$path=''if($path=~ m#^\./?#);2439return($repo_id,$path);2440}2441}2442(undef,undef,undef);2443}24442445sub new {2446my($class,$ref_id,$repo_id,$path) =@_;2447if(defined$ref_id&& !defined$repo_id&& !defined$path) {2448($repo_id,$path) = find_ref($ref_id);2449if(!defined$repo_id) {2450die"Could not find a\"svn-remote.*.fetch\"key ",2451"in the repository configuration matching: ",2452"$ref_id\n";2453}2454}2455my$self= _new($class,$repo_id,$ref_id,$path);2456if(!defined$self->{path} || !length$self->{path}) {2457my$fetch= command_oneline('config','--get',2458"svn-remote.$repo_id.fetch",2459":$ref_id\$")or2460die"Failed to read\"svn-remote.$repo_id.fetch\"",2461"\":$ref_id\$\"in config\n";2462($self->{path},undef) =split(/\s*:\s*/,$fetch);2463}2464$self->{path} =~s{/+}{/}g;2465$self->{path} =~s{\A/}{};2466$self->{path} =~s{/\z}{};2467$self->{url} = command_oneline('config','--get',2468"svn-remote.$repo_id.url")or2469die"Failed to read\"svn-remote.$repo_id.url\"in config\n";2470$self->{pushurl} =eval{ command_oneline('config','--get',2471"svn-remote.$repo_id.pushurl") };2472$self->rebuild;2473$self;2474}24752476sub refname {2477my($refname) =$_[0]->{ref_id} ;24782479# It cannot end with a slash /, we'll throw up on this because2480# SVN can't have directories with a slash in their name, either:2481if($refname=~m{/$}) {2482die"ref: '$refname' ends with a trailing slash, this is ",2483"not permitted by git nor Subversion\n";2484}24852486# It cannot have ASCII control character space, tilde ~, caret ^,2487# colon :, question-mark ?, asterisk *, space, or open bracket [2488# anywhere.2489#2490# Additionally, % must be escaped because it is used for escaping2491# and we want our escaped refname to be reversible2492$refname=~s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;24932494# no slash-separated component can begin with a dot .2495# /.* becomes /%2E*2496$refname=~s{/\.}{/%2E}g;24972498# It cannot have two consecutive dots .. anywhere2499# .. becomes %2E%2E2500$refname=~s{\.\.}{%2E%2E}g;25012502# trailing dots and .lock are not allowed2503# .$ becomes %2E and .lock becomes %2Elock2504$refname=~s{\.(?=$|lock$)}{%2E};25052506# the sequence @{ is used to access the reflog2507# @{ becomes %40{2508$refname=~s{\@\{}{%40\{}g;25092510return$refname;2511}25122513sub desanitize_refname {2514my($refname) =@_;2515$refname=~s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;2516return$refname;2517}25182519sub svm_uuid {2520my($self) =@_;2521return$self->{svm}->{uuid}if$self->svm;2522$self->ra;2523unless($self->{svm}) {2524die"SVM UUID not cached, and reading remotely failed\n";2525}2526$self->{svm}->{uuid};2527}25282529sub svm {2530my($self) =@_;2531return$self->{svm}if$self->{svm};2532my$svm;2533# see if we have it in our config, first:2534eval{2535my$section="svn-remote.$self->{repo_id}";2536$svm= {2537 source => tmp_config('--get',"$section.svm-source"),2538 uuid => tmp_config('--get',"$section.svm-uuid"),2539 replace => tmp_config('--get',"$section.svm-replace"),2540}2541};2542if($svm&&$svm->{source} &&$svm->{uuid} &&$svm->{replace}) {2543$self->{svm} =$svm;2544}2545$self->{svm};2546}25472548sub _set_svm_vars {2549my($self,$ra) =@_;2550return$raif$self->svm;25512552my@err= ("useSvmProps set, but failed to read SVM properties\n",2553"(svm:source, svm:uuid) ",2554"from the following URLs:\n");2555sub read_svm_props {2556my($self,$ra,$path,$r) =@_;2557my$props= ($ra->get_dir($path,$r))[2];2558my$src=$props->{'svm:source'};2559my$uuid=$props->{'svm:uuid'};2560returnundefif(!$src|| !$uuid);25612562chomp($src,$uuid);25632564$uuid=~m{^[0-9a-f\-]{30,}$}i2565or die"doesn't look right - svm:uuid is '$uuid'\n";25662567# the '!' is used to mark the repos_root!/relative/path2568$src=~s{/?!/?}{/};2569$src=~s{/+$}{};# no trailing slashes please2570# username is of no interest2571$src=~s{(^[a-z\+]*://)[^/@]*@}{$1};25722573my$replace=$ra->{url};2574$replace.="/$path"iflength$path;25752576my$section="svn-remote.$self->{repo_id}";2577 tmp_config("$section.svm-source",$src);2578 tmp_config("$section.svm-replace",$replace);2579 tmp_config("$section.svm-uuid",$uuid);2580$self->{svm} = {2581 source =>$src,2582 uuid =>$uuid,2583 replace =>$replace2584};2585}25862587my$r=$ra->get_latest_revnum;2588my$path=$self->{path};2589my%tried;2590while(length$path) {2591unless($tried{"$self->{url}/$path"}) {2592return$raif$self->read_svm_props($ra,$path,$r);2593$tried{"$self->{url}/$path"} =1;2594}2595$path=~ s#/?[^/]+$##;2596}2597die"Path: '$path' should be ''\n"if$pathne'';2598return$raif$self->read_svm_props($ra,$path,$r);2599$tried{"$self->{url}/$path"} =1;26002601if($ra->{repos_root}eq$self->{url}) {2602die@err, (map{"$_\n"}keys%tried),"\n";2603}26042605# nope, make sure we're connected to the repository root:2606my$ok;2607my@tried_b;2608$path=$ra->{svn_path};2609$ra= Git::SVN::Ra->new($ra->{repos_root});2610while(length$path) {2611unless($tried{"$ra->{url}/$path"}) {2612$ok=$self->read_svm_props($ra,$path,$r);2613last if$ok;2614$tried{"$ra->{url}/$path"} =1;2615}2616$path=~ s#/?[^/]+$##;2617}2618die"Path: '$path' should be ''\n"if$pathne'';2619$ok||=$self->read_svm_props($ra,$path,$r);2620$tried{"$ra->{url}/$path"} =1;2621if(!$ok) {2622die@err, (map{"$_\n"}keys%tried),"\n";2623}2624 Git::SVN::Ra->new($self->{url});2625}26262627sub svnsync {2628my($self) =@_;2629return$self->{svnsync}if$self->{svnsync};26302631if($self->no_metadata) {2632die"Can't have both 'noMetadata' and ",2633"'useSvnsyncProps' options set!\n";2634}2635if($self->rewrite_root) {2636die"Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",2637"options set!\n";2638}2639if($self->rewrite_uuid) {2640die"Can't have both 'useSvnsyncProps' and 'rewriteUUID' ",2641"options set!\n";2642}26432644my$svnsync;2645# see if we have it in our config, first:2646eval{2647my$section="svn-remote.$self->{repo_id}";26482649my$url= tmp_config('--get',"$section.svnsync-url");2650($url) = ($url=~m{^([a-z\+]+://\S+)$})or2651die"doesn't look right - svn:sync-from-url is '$url'\n";26522653my$uuid= tmp_config('--get',"$section.svnsync-uuid");2654($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2655die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26562657$svnsync= { url =>$url, uuid =>$uuid}2658};2659if($svnsync&&$svnsync->{url} &&$svnsync->{uuid}) {2660return$self->{svnsync} =$svnsync;2661}26622663my$err="useSvnsyncProps set, but failed to read ".2664"svnsync property: svn:sync-from-";2665my$rp=$self->ra->rev_proplist(0);26662667my$url=$rp->{'svn:sync-from-url'}or die$err."url\n";2668($url) = ($url=~m{^([a-z\+]+://\S+)$})or2669die"doesn't look right - svn:sync-from-url is '$url'\n";26702671my$uuid=$rp->{'svn:sync-from-uuid'}or die$err."uuid\n";2672($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2673die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26742675my$section="svn-remote.$self->{repo_id}";2676 tmp_config('--add',"$section.svnsync-uuid",$uuid);2677 tmp_config('--add',"$section.svnsync-url",$url);2678return$self->{svnsync} = { url =>$url, uuid =>$uuid};2679}26802681# this allows us to memoize our SVN::Ra UUID locally and avoid a2682# remote lookup (useful for 'git svn log').2683sub ra_uuid {2684my($self) =@_;2685unless($self->{ra_uuid}) {2686my$key="svn-remote.$self->{repo_id}.uuid";2687my$uuid=eval{ tmp_config('--get',$key) };2688if(!$@&&$uuid&&$uuid=~/^([a-f\d\-]{30,})$/i) {2689$self->{ra_uuid} =$uuid;2690}else{2691die"ra_uuid called without URL\n"unless$self->{url};2692$self->{ra_uuid} =$self->ra->get_uuid;2693 tmp_config('--add',$key,$self->{ra_uuid});2694}2695}2696$self->{ra_uuid};2697}26982699sub _set_repos_root {2700my($self,$repos_root) =@_;2701my$k="svn-remote.$self->{repo_id}.reposRoot";2702$repos_root||=$self->ra->{repos_root};2703 tmp_config($k,$repos_root);2704$repos_root;2705}27062707sub repos_root {2708my($self) =@_;2709my$k="svn-remote.$self->{repo_id}.reposRoot";2710eval{ tmp_config('--get',$k) } ||$self->_set_repos_root;2711}27122713sub ra {2714my($self) =shift;2715my$ra= Git::SVN::Ra->new($self->{url});2716$self->_set_repos_root($ra->{repos_root});2717if($self->use_svm_props&& !$self->{svm}) {2718if($self->no_metadata) {2719die"Can't have both 'noMetadata' and ",2720"'useSvmProps' options set!\n";2721}elsif($self->use_svnsync_props) {2722die"Can't have both 'useSvnsyncProps' and ",2723"'useSvmProps' options set!\n";2724}2725$ra=$self->_set_svm_vars($ra);2726$self->{-want_revprops} =1;2727}2728$ra;2729}27302731# prop_walk(PATH, REV, SUB)2732# -------------------------2733# Recursively traverse PATH at revision REV and invoke SUB for each2734# directory that contains a SVN property. SUB will be invoked as2735# follows: &SUB(gs, path, props); where `gs' is this instance of2736# Git::SVN, `path' the path to the directory where the properties2737# `props' were found. The `path' will be relative to point of checkout,2738# that is, if url://repo/trunk is the current Git branch, and that2739# directory contains a sub-directory `d', SUB will be invoked with `/d/'2740# as `path' (note the trailing `/').2741sub prop_walk {2742my($self,$path,$rev,$sub) =@_;27432744$path=~ s#^/##;2745my($dirent,undef,$props) =$self->ra->get_dir($path,$rev);2746$path=~ s#^/*#/#g;2747my$p=$path;2748# Strip the irrelevant part of the path.2749$p=~ s#^/+\Q$self->{path}\E(/|$)#/#;2750# Ensure the path is terminated by a `/'.2751$p=~ s#/*$#/#;27522753# The properties contain all the internal SVN stuff nobody2754# (usually) cares about.2755my$interesting_props=0;2756foreach(keys%{$props}) {2757# If it doesn't start with `svn:', it must be a2758# user-defined property.2759++$interesting_propsandnext if$_!~/^svn:/;2760# FIXME: Fragile, if SVN adds new public properties,2761# this needs to be updated.2762++$interesting_propsif/^svn:(?:ignore|keywords|executable2763|eol-style|mime-type2764|externals|needs-lock)$/x;2765}2766&$sub($self,$p,$props)if$interesting_props;27672768foreach(sort keys%$dirent) {2769next if$dirent->{$_}->{kind} !=$SVN::Node::dir;2770$self->prop_walk($self->{path} .$p.$_,$rev,$sub);2771}2772}27732774sub last_rev { ($_[0]->last_rev_commit)[0] }2775sub last_commit { ($_[0]->last_rev_commit)[1] }27762777# returns the newest SVN revision number and newest commit SHA12778sub last_rev_commit {2779my($self) =@_;2780if(defined$self->{last_rev} &&defined$self->{last_commit}) {2781return($self->{last_rev},$self->{last_commit});2782}2783my$c= ::verify_ref($self->refname.'^0');2784if($c&& !$self->use_svm_props&& !$self->no_metadata) {2785my$rev= (::cmt_metadata($c))[1];2786if(defined$rev) {2787($self->{last_rev},$self->{last_commit}) = ($rev,$c);2788return($rev,$c);2789}2790}2791my$map_path=$self->map_path;2792unless(-e $map_path) {2793($self->{last_rev},$self->{last_commit}) = (undef,undef);2794return(undef,undef);2795}2796my($rev,$commit) =$self->rev_map_max(1);2797($self->{last_rev},$self->{last_commit}) = ($rev,$commit);2798return($rev,$commit);2799}28002801sub get_fetch_range {2802my($self,$min,$max) =@_;2803$max||=$self->ra->get_latest_revnum;2804$min||=$self->rev_map_max;2805(++$min,$max);2806}28072808sub tmp_config {2809my(@args) =@_;2810my$old_def_config="$ENV{GIT_DIR}/svn/config";2811my$config="$ENV{GIT_DIR}/svn/.metadata";2812if(! -f $config&& -f $old_def_config) {2813rename$old_def_config,$configor2814die"Failed rename$old_def_config=>$config:$!\n";2815}2816my$old_config=$ENV{GIT_CONFIG};2817$ENV{GIT_CONFIG} =$config;2818$@=undef;2819my@ret=eval{2820unless(-f $config) {2821 mkfile($config);2822open my$fh,'>',$configor2823die"Can't open$config:$!\n";2824print$fh"; This file is used internally by ",2825"git-svn\n"or die2826"Couldn't write to$config:$!\n";2827print$fh"; You should not have to edit it\n"or2828die"Couldn't write to$config:$!\n";2829close$fhor die"Couldn't close$config:$!\n";2830}2831 command('config',@args);2832};2833my$err=$@;2834if(defined$old_config) {2835$ENV{GIT_CONFIG} =$old_config;2836}else{2837delete$ENV{GIT_CONFIG};2838}2839die$errif$err;2840wantarray?@ret:$ret[0];2841}28422843sub tmp_index_do {2844my($self,$sub) =@_;2845my$old_index=$ENV{GIT_INDEX_FILE};2846$ENV{GIT_INDEX_FILE} =$self->{index};2847$@=undef;2848my@ret=eval{2849my($dir,$base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);2850 mkpath([$dir])unless-d $dir;2851&$sub;2852};2853my$err=$@;2854if(defined$old_index) {2855$ENV{GIT_INDEX_FILE} =$old_index;2856}else{2857delete$ENV{GIT_INDEX_FILE};2858}2859die$errif$err;2860wantarray?@ret:$ret[0];2861}28622863sub assert_index_clean {2864my($self,$treeish) =@_;28652866$self->tmp_index_do(sub{2867 command_noisy('read-tree',$treeish)unless-e $self->{index};2868my$x= command_oneline('write-tree');2869my($y) = (command(qw/cat-file commit/,$treeish) =~2870/^tree ($::sha1)/mo);2871return if$yeq$x;28722873warn"Index mismatch:$y!=$x\nrereading$treeish\n";2874unlink$self->{index}or die"unlink$self->{index}:$!\n";2875 command_noisy('read-tree',$treeish);2876$x= command_oneline('write-tree');2877if($yne$x) {2878::fatal "trees ($treeish)$y!=$x\n",2879"Something is seriously wrong...";2880}2881});2882}28832884sub get_commit_parents {2885my($self,$log_entry) =@_;2886my(%seen,@ret,@tmp);2887# legacy support for 'set-tree'; this is only used by set_tree_cb:2888if(my$ip=$self->{inject_parents}) {2889if(my$commit=delete$ip->{$log_entry->{revision}}) {2890push@tmp,$commit;2891}2892}2893if(my$cur= ::verify_ref($self->refname.'^0')) {2894push@tmp,$cur;2895}2896if(my$ipd=$self->{inject_parents_dcommit}) {2897if(my$commit=delete$ipd->{$log_entry->{revision}}) {2898push@tmp,@$commit;2899}2900}2901push@tmp,$_foreach(@{$log_entry->{parents}},@tmp);2902while(my$p=shift@tmp) {2903next if$seen{$p};2904$seen{$p} =1;2905push@ret,$p;2906}2907@ret;2908}29092910sub rewrite_root {2911my($self) =@_;2912return$self->{-rewrite_root}ifexists$self->{-rewrite_root};2913my$k="svn-remote.$self->{repo_id}.rewriteRoot";2914my$rwr=eval{ command_oneline(qw/config --get/,$k) };2915if($rwr) {2916$rwr=~ s#/+$##;2917if($rwr!~ m#^[a-z\+]+://#) {2918die"$rwris not a valid URL (key:$k)\n";2919}2920}2921$self->{-rewrite_root} =$rwr;2922}29232924sub rewrite_uuid {2925my($self) =@_;2926return$self->{-rewrite_uuid}ifexists$self->{-rewrite_uuid};2927my$k="svn-remote.$self->{repo_id}.rewriteUUID";2928my$rwid=eval{ command_oneline(qw/config --get/,$k) };2929if($rwid) {2930$rwid=~ s#/+$##;2931if($rwid!~ m#^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$#) {2932die"$rwidis not a valid UUID (key:$k)\n";2933}2934}2935$self->{-rewrite_uuid} =$rwid;2936}29372938sub metadata_url {2939my($self) =@_;2940($self->rewrite_root||$self->{url}) .2941(length$self->{path} ?'/'.$self->{path} :'');2942}29432944sub full_url {2945my($self) =@_;2946$self->{url} . (length$self->{path} ?'/'.$self->{path} :'');2947}29482949sub full_pushurl {2950my($self) =@_;2951if($self->{pushurl}) {2952return$self->{pushurl} . (length$self->{path} ?'/'.2953$self->{path} :'');2954}else{2955return$self->full_url;2956}2957}29582959sub set_commit_header_env {2960my($log_entry) =@_;2961my%env;2962foreachmy$ned(qw/NAME EMAIL DATE/) {2963foreachmy$ac(qw/AUTHOR COMMITTER/) {2964$env{"GIT_${ac}_${ned}"} =$ENV{"GIT_${ac}_${ned}"};2965}2966}29672968$ENV{GIT_AUTHOR_NAME} =$log_entry->{name};2969$ENV{GIT_AUTHOR_EMAIL} =$log_entry->{email};2970$ENV{GIT_AUTHOR_DATE} =$ENV{GIT_COMMITTER_DATE} =$log_entry->{date};29712972$ENV{GIT_COMMITTER_NAME} = (defined$log_entry->{commit_name})2973?$log_entry->{commit_name}2974:$log_entry->{name};2975$ENV{GIT_COMMITTER_EMAIL} = (defined$log_entry->{commit_email})2976?$log_entry->{commit_email}2977:$log_entry->{email};2978 \%env;2979}29802981sub restore_commit_header_env {2982my($env) =@_;2983foreachmy$ned(qw/NAME EMAIL DATE/) {2984foreachmy$ac(qw/AUTHOR COMMITTER/) {2985my$k="GIT_${ac}_${ned}";2986if(defined$env->{$k}) {2987$ENV{$k} =$env->{$k};2988}else{2989delete$ENV{$k};2990}2991}2992}2993}29942995sub gc {2996 command_noisy('gc','--auto');2997};29982999sub do_git_commit {3000my($self,$log_entry) =@_;3001my$lr=$self->last_rev;3002if(defined$lr&&$lr>=$log_entry->{revision}) {3003die"Last fetched revision of ",$self->refname,3004" was r$lr, but we are about to fetch: ",3005"r$log_entry->{revision}!\n";3006}3007if(my$c=$self->rev_map_get($log_entry->{revision})) {3008 croak "$log_entry->{revision} =$calready exists! ",3009"Why are we refetching it?\n";3010}3011my$old_env= set_commit_header_env($log_entry);3012my$tree=$log_entry->{tree};3013if(!defined$tree) {3014$tree=$self->tmp_index_do(sub{3015 command_oneline('write-tree') });3016}3017die"Tree is not a valid sha1:$tree\n"if$tree!~/^$::sha1$/o;30183019my@exec= ('git','commit-tree',$tree);3020foreach($self->get_commit_parents($log_entry)) {3021push@exec,'-p',$_;3022}3023defined(my$pid= open3(my$msg_fh,my$out_fh,'>&STDERR',@exec))3024or croak $!;3025binmode$msg_fh;30263027# we always get UTF-8 from SVN, but we may want our commits in3028# a different encoding.3029if(my$enc= Git::config('i18n.commitencoding')) {3030require Encode;3031 Encode::from_to($log_entry->{log},'UTF-8',$enc);3032}3033print$msg_fh $log_entry->{log}or croak $!;3034 restore_commit_header_env($old_env);3035unless($self->no_metadata) {3036print$msg_fh"\ngit-svn-id:$log_entry->{metadata}\n"3037or croak $!;3038}3039$msg_fh->flush==0or croak $!;3040close$msg_fhor croak $!;3041chomp(my$commit=do{local$/; <$out_fh> });3042close$out_fhor croak $!;3043waitpid$pid,0;3044 croak $?if$?;3045if($commit!~/^$::sha1$/o) {3046die"Failed to commit, invalid sha1:$commit\n";3047}30483049$self->rev_map_set($log_entry->{revision},$commit,1);30503051$self->{last_rev} =$log_entry->{revision};3052$self->{last_commit} =$commit;3053print"r$log_entry->{revision}"unless$::_q >1;3054if(defined$log_entry->{svm_revision}) {3055print" (\@$log_entry->{svm_revision})"unless$::_q >1;3056$self->rev_map_set($log_entry->{svm_revision},$commit,30570,$self->svm_uuid);3058}3059print" =$commit($self->{ref_id})\n"unless$::_q >1;3060if(--$_gc_nr==0) {3061$_gc_nr=$_gc_period;3062 gc();3063}3064return$commit;3065}30663067sub match_paths {3068my($self,$paths,$r) =@_;3069return1if$self->{path}eq'';3070if(my$path=$paths->{"/$self->{path}"}) {3071return($path->{action}eq'D') ?0:1;3072}3073$self->{path_regex} ||=qr/^\/\Q$self->{path}\E\//;3074if(grep/$self->{path_regex}/,keys%$paths) {3075return1;3076}3077my$c='';3078foreach(split m#/#, $self->{path}) {3079$c.="/$_";3080next unless($paths->{$c} &&3081($paths->{$c}->{action} =~/^[AR]$/));3082if($self->ra->check_path($self->{path},$r) ==3083$SVN::Node::dir) {3084return1;3085}3086}3087return0;3088}30893090sub find_parent_branch {3091my($self,$paths,$rev) =@_;3092returnundefunless$self->follow_parent;3093unless(defined$paths) {3094my$err_handler=$SVN::Error::handler;3095$SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;3096$self->ra->get_log([$self->{path}],$rev,$rev,0,1,1,3097sub{$paths=$_[0] });3098$SVN::Error::handler =$err_handler;3099}3100returnundefunlessdefined$paths;31013102# look for a parent from another branch:3103my@b_path_components=split m#/#, $self->{path};3104my@a_path_components;3105my$i;3106while(@b_path_components) {3107$i=$paths->{'/'.join('/',@b_path_components)};3108last if$i&&defined$i->{copyfrom_path};3109unshift(@a_path_components,pop(@b_path_components));3110}3111returnundefunlessdefined$i&&defined$i->{copyfrom_path};3112my$branch_from=$i->{copyfrom_path};3113if(@a_path_components) {3114print STDERR "branch_from:$branch_from=> ";3115$branch_from.='/'.join('/',@a_path_components);3116print STDERR $branch_from,"\n";3117}3118my$r=$i->{copyfrom_rev};3119my$repos_root=$self->ra->{repos_root};3120my$url=$self->ra->{url};3121my$new_url=$url.$branch_from;3122print STDERR "Found possible branch point: ",3123"$new_url=> ",$self->full_url,",$r\n"3124unless$::_q >1;3125$branch_from=~ s#^/##;3126my$gs=$self->other_gs($new_url,$url,3127$branch_from,$r,$self->{ref_id});3128my($r0,$parent) =$gs->find_rev_before($r,1);3129{3130my($base,$head);3131if(!defined$r0|| !defined$parent) {3132($base,$head) = parse_revision_argument(0,$r);3133}else{3134if($r0<$r) {3135$gs->ra->get_log([$gs->{path}],$r0+1,$r,1,31360,1,sub{$base=$_[1] -1});3137}3138}3139if(defined$base&&$base<=$r) {3140$gs->fetch($base,$r);3141}3142($r0,$parent) =$gs->find_rev_before($r,1);3143}3144if(defined$r0&&defined$parent) {3145print STDERR "Found branch parent: ($self->{ref_id})$parent\n"3146unless$::_q >1;3147my$ed;3148if($self->ra->can_do_switch) {3149$self->assert_index_clean($parent);3150print STDERR "Following parent with do_switch\n"3151unless$::_q >1;3152# do_switch works with svn/trunk >= r22312, but that3153# is not included with SVN 1.4.3 (the latest version3154# at the moment), so we can't rely on it3155$self->{last_rev} =$r0;3156$self->{last_commit} =$parent;3157$ed= SVN::Git::Fetcher->new($self,$gs->{path});3158$gs->ra->gs_do_switch($r0,$rev,$gs,3159$self->full_url,$ed)3160or die"SVN connection failed somewhere...\n";3161}elsif($self->ra->trees_match($new_url,$r0,3162$self->full_url,$rev)) {3163print STDERR "Trees match:\n",3164"$new_url\@$r0\n",3165" ${\$self->full_url}\@$rev\n",3166"Following parent with no changes\n"3167unless$::_q >1;3168$self->tmp_index_do(sub{3169 command_noisy('read-tree',$parent);3170});3171$self->{last_commit} =$parent;3172}else{3173print STDERR "Following parent with do_update\n"3174unless$::_q >1;3175$ed= SVN::Git::Fetcher->new($self);3176$self->ra->gs_do_update($rev,$rev,$self,$ed)3177or die"SVN connection failed somewhere...\n";3178}3179print STDERR "Successfully followed parent\n"unless$::_q >1;3180return$self->make_log_entry($rev, [$parent],$ed);3181}3182returnundef;3183}31843185sub do_fetch {3186my($self,$paths,$rev) =@_;3187my$ed;3188my($last_rev,@parents);3189if(my$lc=$self->last_commit) {3190# we can have a branch that was deleted, then re-added3191# under the same name but copied from another path, in3192# which case we'll have multiple parents (we don't3193# want to break the original ref, nor lose copypath info):3194if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3195push@{$log_entry->{parents}},$lc;3196return$log_entry;3197}3198$ed= SVN::Git::Fetcher->new($self);3199$last_rev=$self->{last_rev};3200$ed->{c} =$lc;3201@parents= ($lc);3202}else{3203$last_rev=$rev;3204if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3205return$log_entry;3206}3207$ed= SVN::Git::Fetcher->new($self);3208}3209unless($self->ra->gs_do_update($last_rev,$rev,$self,$ed)) {3210die"SVN connection failed somewhere...\n";3211}3212$self->make_log_entry($rev, \@parents,$ed);3213}32143215sub mkemptydirs {3216my($self,$r) =@_;32173218sub scan {3219my($r,$empty_dirs,$line) =@_;3220if(defined$r&&$line=~/^r(\d+)$/) {3221return0if$1>$r;3222}elsif($line=~/^ \+empty_dir: (.+)$/) {3223$empty_dirs->{$1} =1;3224}elsif($line=~/^ \-empty_dir: (.+)$/) {3225my@d=grep{m[^\Q$1\E(/|$)]} (keys%$empty_dirs);3226delete@$empty_dirs{@d};3227}32281;# continue3229};32303231my%empty_dirs= ();3232my$gz_file="$self->{dir}/unhandled.log.gz";3233if(-f $gz_file) {3234if(!$can_compress) {3235warn"Compress::Zlib could not be found; ",3236"empty directories in$gz_filewill not be read\n";3237}else{3238my$gz= Compress::Zlib::gzopen($gz_file,"rb")or3239die"Unable to open$gz_file:$!\n";3240my$line;3241while($gz->gzreadline($line) >0) {3242 scan($r, \%empty_dirs,$line)orlast;3243}3244$gz->gzclose;3245}3246}32473248if(open my$fh,'<',"$self->{dir}/unhandled.log") {3249binmode$fhor croak "binmode:$!";3250while(<$fh>) {3251 scan($r, \%empty_dirs,$_)orlast;3252}3253close$fh;3254}32553256my$strip=qr/\A\Q$self->{path}\E(?:\/|$)/;3257foreachmy$d(sort keys%empty_dirs) {3258$d= uri_decode($d);3259$d=~s/$strip//;3260next unlesslength($d);3261next if-d $d;3262if(-e $d) {3263warn"$dexists but is not a directory\n";3264}else{3265print"creating empty directory:$d\n";3266 mkpath([$d]);3267}3268}3269}32703271sub get_untracked {3272my($self,$ed) =@_;3273my@out;3274my$h=$ed->{empty};3275foreach(sort keys%$h) {3276my$act=$h->{$_} ?'+empty_dir':'-empty_dir';3277push@out,"$act: ". uri_encode($_);3278warn"W:$act:$_\n";3279}3280foreachmy$t(qw/dir_prop file_prop/) {3281$h=$ed->{$t}ornext;3282foreachmy$path(sort keys%$h) {3283my$ppath=$patheq''?'.':$path;3284foreachmy$prop(sort keys%{$h->{$path}}) {3285next if$SKIP_PROP{$prop};3286my$v=$h->{$path}->{$prop};3287my$t_ppath_prop="$t: ".3288 uri_encode($ppath) .' '.3289 uri_encode($prop);3290if(defined$v) {3291push@out," +$t_ppath_prop".3292 uri_encode($v);3293}else{3294push@out," -$t_ppath_prop";3295}3296}3297}3298}3299foreachmy$t(qw/absent_file absent_directory/) {3300$h=$ed->{$t}ornext;3301foreachmy$parent(sort keys%$h) {3302foreachmy$path(sort@{$h->{$parent}}) {3303push@out,"$t: ".3304 uri_encode("$parent/$path");3305warn"W:$t:$parent/$path",3306"Insufficient permissions?\n";3307}3308}3309}3310 \@out;3311}33123313sub get_tz {3314# some systmes don't handle or mishandle %z, so be creative.3315my$t=shift||time;3316my$gm= timelocal(gmtime($t));3317my$sign=qw( + + - )[$t<=>$gm];3318returnsprintf("%s%02d%02d",$sign, (gmtime(abs($t-$gm)))[2,1]);3319}33203321# parse_svn_date(DATE)3322# --------------------3323# Given a date (in UTC) from Subversion, return a string in the format3324# "<TZ Offset> <local date/time>" that Git will use.3325#3326# By default the parsed date will be in UTC; if $Git::SVN::_localtime3327# is true we'll convert it to the local timezone instead.3328sub parse_svn_date {3329my$date=shift||return'+0000 1970-01-01 00:00:00';3330my($Y,$m,$d,$H,$M,$S) = ($date=~ /^(\d{4})\-(\d\d)\-(\d\d)T3331(\d\d)\:(\d\d)\:(\d\d)\.\d*Z$/x)or3332 croak "Unable to parse date:$date\n";3333my$parsed_date;# Set next.33343335if($Git::SVN::_localtime) {3336# Translate the Subversion datetime to an epoch time.3337# Begin by switching ourselves to $date's timezone, UTC.3338my$old_env_TZ=$ENV{TZ};3339$ENV{TZ} ='UTC';33403341my$epoch_in_UTC=3342 POSIX::strftime('%s',$S,$M,$H,$d,$m-1,$Y-1900);33433344# Determine our local timezone (including DST) at the3345# time of $epoch_in_UTC. $Git::SVN::Log::TZ stored the3346# value of TZ, if any, at the time we were run.3347if(defined$Git::SVN::Log::TZ) {3348$ENV{TZ} =$Git::SVN::Log::TZ;3349}else{3350delete$ENV{TZ};3351}33523353my$our_TZ= get_tz();33543355# This converts $epoch_in_UTC into our local timezone.3356my($sec,$min,$hour,$mday,$mon,$year,3357$wday,$yday,$isdst) =localtime($epoch_in_UTC);33583359$parsed_date=sprintf('%s%04d-%02d-%02d%02d:%02d:%02d',3360$our_TZ,$year+1900,$mon+1,3361$mday,$hour,$min,$sec);33623363# Reset us to the timezone in effect when we entered3364# this routine.3365if(defined$old_env_TZ) {3366$ENV{TZ} =$old_env_TZ;3367}else{3368delete$ENV{TZ};3369}3370}else{3371$parsed_date="+0000$Y-$m-$d$H:$M:$S";3372}33733374return$parsed_date;3375}33763377sub other_gs {3378my($self,$new_url,$url,3379$branch_from,$r,$old_ref_id) =@_;3380my$gs= Git::SVN->find_by_url($new_url,$url,$branch_from);3381unless($gs) {3382my$ref_id=$old_ref_id;3383$ref_id=~s/\@\d+-*$//;3384$ref_id.="\@$r";3385# just grow a tail if we're not unique enough :x3386$ref_id.='-'while find_ref($ref_id);3387my($u,$p,$repo_id) = ($new_url,'',$ref_id);3388if($u=~ s#^\Q$url\E(/|$)##) {3389$p=$u;3390$u=$url;3391$repo_id=$self->{repo_id};3392}3393while(1) {3394# It is possible to tag two different subdirectories at3395# the same revision. If the url for an existing ref3396# does not match, we must either find a ref with a3397# matching url or create a new ref by growing a tail.3398$gs= Git::SVN->init($u,$p,$repo_id,$ref_id,1);3399my(undef,$max_commit) =$gs->rev_map_max(1);3400last if(!$max_commit);3401my($url) = ::cmt_metadata($max_commit);3402last if($urleq$gs->metadata_url);3403$ref_id.='-';3404}3405print STDERR "Initializing parent:$ref_id\n"unless$::_q >1;3406}3407$gs3408}34093410sub call_authors_prog {3411my($orig_author) =@_;3412$orig_author= command_oneline('rev-parse','--sq-quote',$orig_author);3413my$author=`$::_authors_prog$orig_author`;3414 if ($?!= 0) {3415 die "$::_authors_prog failed with exit code$?\n"3416 }3417 if ($author=~ /^\s*(.+?)\s*<(.*)>\s*$/) {3418 my ($name,$email) = ($1,$2);3419$email= undef if length$2== 0;3420 return [$name,$email];3421 } else {3422 die "Author:$orig_author: $::_authors_prog returned "3423 . "invalid author format:$author\n";3424 }3425}34263427sub check_author {3428 my ($author) =@_;3429 if (!defined$author|| length$author== 0) {3430$author= '(no author)';3431 }3432 if (!defined $::users{$author}) {3433 if (defined $::_authors_prog) {3434 $::users{$author} = call_authors_prog($author);3435 } elsif (defined $::_authors) {3436 die "Author:$authornot defined in $::_authors file\n";3437 }3438 }3439$author;3440}34413442sub find_extra_svk_parents {3443 my ($self,$ed,$tickets,$parents) =@_;3444 # aha! svk:merge property changed...3445 my@tickets= split "\n",$tickets;3446 my@known_parents;3447 for my$ticket(@tickets) {3448 my ($uuid,$path,$rev) = split /:/,$ticket;3449 if ($uuideq$self->ra_uuid ) {3450 my$url=$self->{url};3451 my$repos_root=$url;3452 my$branch_from=$path;3453$branch_from=~ s{^/}{};3454 my$gs=$self->other_gs($repos_root."/".$branch_from,3455$url,3456$branch_from,3457$rev,3458$self->{ref_id});3459 if ( my$commit=$gs->rev_map_get($rev,$uuid) ) {3460 # wahey! we found it, but it might be3461 # an old one (!)3462 push@known_parents, [$rev,$commit];3463 }3464 }3465 }3466 # Ordering matters; highest-numbered commit merge tickets3467 # first, as they may account for later merge ticket additions3468 # or changes.3469@known_parents= map {$_->[1]} sort {$b->[0] <=>$a->[0]}@known_parents;3470 for my$parent(@known_parents) {3471 my@cmd= ('rev-list',$parent, map { "^$_" }@$parents);3472 my ($msg_fh,$ctx) = command_output_pipe(@cmd);3473 my$new;3474 while ( <$msg_fh> ) {3475$new=1;last;3476 }3477 command_close_pipe($msg_fh,$ctx);3478 if ($new) {3479 print STDERR3480 "Found merge parent (svk:merge ticket):$parent\n";3481 push@$parents,$parent;3482 }3483 }3484}34853486sub lookup_svn_merge {3487 my$uuid= shift;3488 my$url= shift;3489 my$merge= shift;34903491 my ($source,$revs) = split ":",$merge;3492 my$path=$source;3493$path=~ s{^/}{};3494 my$gs= Git::SVN->find_by_url($url.$source,$url,$path);3495 if ( !$gs) {3496 warn "Couldn't find revmap for$url$source\n";3497 return;3498 }3499 my@ranges= split ",",$revs;3500 my ($tip,$tip_commit);3501 my@merged_commit_ranges;3502 # find the tip3503 for my$range(@ranges) {3504 my ($bottom,$top) = split "-",$range;3505$top||=$bottom;3506 my$bottom_commit=$gs->find_rev_after($bottom, 1,$top);3507 my$top_commit=$gs->find_rev_before($top, 1,$bottom);35083509 unless ($top_commitand$bottom_commit) {3510 warn "W:unknown path/rev in svn:mergeinfo "3511 ."dirprop:$source:$range\n";3512 next;3513 }35143515 if (scalar(command('rev-parse', "$bottom_commit^@"))) {3516 push@merged_commit_ranges,3517 "$bottom_commit^..$top_commit";3518 } else {3519 push@merged_commit_ranges, "$top_commit";3520 }35213522 if ( !defined$tipor$top>$tip) {3523$tip=$top;3524$tip_commit=$top_commit;3525 }3526 }3527 return ($tip_commit,@merged_commit_ranges);3528}35293530sub _rev_list {3531 my ($msg_fh,$ctx) = command_output_pipe(3532 "rev-list",@_,3533 );3534 my@rv;3535 while ( <$msg_fh> ) {3536 chomp;3537 push@rv,$_;3538 }3539 command_close_pipe($msg_fh,$ctx);3540@rv;3541}35423543sub check_cherry_pick {3544 my$base= shift;3545 my$tip= shift;3546 my$parents= shift;3547 my@ranges=@_;3548 my%commits= map {$_=> 1 }3549 _rev_list("--no-merges",$tip, "--not",$base,@$parents, "--");3550 for my$range(@ranges) {3551 delete@commits{_rev_list($range, "--")};3552 }3553 for my$commit(keys%commits) {3554 if (has_no_changes($commit)) {3555 delete$commits{$commit};3556 }3557 }3558 return (keys%commits);3559}35603561sub has_no_changes {3562 my$commit= shift;35633564 my@revs= split / /, command_oneline(3565 qw(rev-list --parents -1 -m),$commit);35663567# Commits with no parents, e.g. the start of a partial branch,3568# have changes by definition.3569return1if(@revs<2);35703571# Commits with multiple parents, e.g a merge, have no changes3572# by definition.3573return0if(@revs>2);35743575return(command_oneline("rev-parse","$commit^{tree}")eq3576 command_oneline("rev-parse","$commit~1^{tree}"));3577}35783579# The GIT_DIR environment variable is not always set until after the command3580# line arguments are processed, so we can't memoize in a BEGIN block.3581{3582my$memoized=0;35833584sub memoize_svn_mergeinfo_functions {3585return if$memoized;3586$memoized=1;35873588my$cache_path="$ENV{GIT_DIR}/svn/.caches/";3589 mkpath([$cache_path])unless-d $cache_path;35903591 tie my%lookup_svn_merge_cache=>'Memoize::Storable',3592"$cache_path/lookup_svn_merge.db",'nstore';3593 memoize 'lookup_svn_merge',3594 SCALAR_CACHE =>'FAULT',3595 LIST_CACHE => ['HASH'=> \%lookup_svn_merge_cache],3596;35973598 tie my%check_cherry_pick_cache=>'Memoize::Storable',3599"$cache_path/check_cherry_pick.db",'nstore';3600 memoize 'check_cherry_pick',3601 SCALAR_CACHE =>'FAULT',3602 LIST_CACHE => ['HASH'=> \%check_cherry_pick_cache],3603;36043605 tie my%has_no_changes_cache=>'Memoize::Storable',3606"$cache_path/has_no_changes.db",'nstore';3607 memoize 'has_no_changes',3608 SCALAR_CACHE => ['HASH'=> \%has_no_changes_cache],3609 LIST_CACHE =>'FAULT',3610;3611}36123613sub unmemoize_svn_mergeinfo_functions {3614return ifnot$memoized;3615$memoized=0;36163617 Memoize::unmemoize 'lookup_svn_merge';3618 Memoize::unmemoize 'check_cherry_pick';3619 Memoize::unmemoize 'has_no_changes';3620}36213622 Memoize::memoize 'Git::SVN::repos_root';3623}36243625END{3626# Force cache writeout explicitly instead of waiting for3627# global destruction to avoid segfault in Storable:3628# http://rt.cpan.org/Public/Bug/Display.html?id=360873629 unmemoize_svn_mergeinfo_functions();3630}36313632sub parents_exclude {3633my$parents=shift;3634my@commits=@_;3635return unless@commits;36363637my@excluded;3638my$excluded;3639do{3640my@cmd= ('rev-list',"-1",@commits,"--not",@$parents);3641$excluded= command_oneline(@cmd);3642if($excluded) {3643my@new;3644my$found;3645formy$commit(@commits) {3646if($commiteq$excluded) {3647push@excluded,$commit;3648$found++;3649last;3650}3651else{3652push@new,$commit;3653}3654}3655die"saw commit '$excluded' in rev-list output, "3656."but we didn't ask for that commit (wanted:@commits--not@$parents)"3657unless$found;3658@commits=@new;3659}3660}3661while($excludedand@commits);36623663return@excluded;3664}366536663667# note: this function should only be called if the various dirprops3668# have actually changed3669sub find_extra_svn_parents {3670my($self,$ed,$mergeinfo,$parents) =@_;3671# aha! svk:merge property changed...36723673 memoize_svn_mergeinfo_functions();36743675# We first search for merged tips which are not in our3676# history. Then, we figure out which git revisions are in3677# that tip, but not this revision. If all of those revisions3678# are now marked as merge, we can add the tip as a parent.3679my@merges=split"\n",$mergeinfo;3680my@merge_tips;3681my$url=$self->{url};3682my$uuid=$self->ra_uuid;3683my%ranges;3684formy$merge(@merges) {3685my($tip_commit,@ranges) =3686 lookup_svn_merge($uuid,$url,$merge);3687unless(!$tip_commitor3688grep{$_eq$tip_commit}@$parents) {3689push@merge_tips,$tip_commit;3690$ranges{$tip_commit} = \@ranges;3691}else{3692push@merge_tips,undef;3693}3694}36953696my%excluded=map{$_=>1}3697 parents_exclude($parents,grep{defined}@merge_tips);36983699# check merge tips for new parents3700my@new_parents;3701formy$merge_tip(@merge_tips) {3702my$spec=shift@merges;3703next unless$merge_tipand$excluded{$merge_tip};37043705my$ranges=$ranges{$merge_tip};37063707# check out 'new' tips3708my$merge_base;3709eval{3710$merge_base= command_oneline(3711"merge-base",3712@$parents,$merge_tip,3713);3714};3715if($@) {3716die"An error occurred during merge-base"3717unless$@->isa("Git::Error::Command");37183719warn"W: Cannot find common ancestor between ".3720"@$parentsand$merge_tip. Ignoring merge info.\n";3721next;3722}37233724# double check that there are no missing non-merge commits3725my(@incomplete) = check_cherry_pick(3726$merge_base,$merge_tip,3727$parents,3728@$ranges,3729);37303731if(@incomplete) {3732warn"W:svn cherry-pick ignored ($spec) - missing "3733.@incomplete." commit(s) (eg$incomplete[0])\n";3734}else{3735warn3736"Found merge parent (svn:mergeinfo prop): ",3737$merge_tip,"\n";3738push@new_parents,$merge_tip;3739}3740}37413742# cater for merges which merge commits from multiple branches3743if(@new_parents>1) {3744for(my$i=0;$i<=$#new_parents;$i++) {3745for(my$j=0;$j<=$#new_parents;$j++) {3746next if$i==$j;3747next unless$new_parents[$i];3748next unless$new_parents[$j];3749my$revs= command_oneline(3750"rev-list","-1",3751"$new_parents[$i]..$new_parents[$j]",3752);3753if( !$revs) {3754undef($new_parents[$j]);3755}3756}3757}3758}3759push@$parents,grep{defined}@new_parents;3760}37613762sub make_log_entry {3763my($self,$rev,$parents,$ed) =@_;3764my$untracked=$self->get_untracked($ed);37653766my@parents=@$parents;3767my$ps=$ed->{path_strip} ||"";3768formy$path(grep{m/$ps/} %{$ed->{dir_prop}} ) {3769my$props=$ed->{dir_prop}{$path};3770if($props->{"svk:merge"} ) {3771$self->find_extra_svk_parents3772($ed,$props->{"svk:merge"}, \@parents);3773}3774if($props->{"svn:mergeinfo"} ) {3775$self->find_extra_svn_parents3776($ed,3777$props->{"svn:mergeinfo"},3778 \@parents);3779}3780}37813782open my$un,'>>',"$self->{dir}/unhandled.log"or croak $!;3783print$un"r$rev\n"or croak $!;3784print$un $_,"\n"foreach@$untracked;3785my%log_entry= ( parents => \@parents, revision =>$rev,3786log=>'');37873788my$headrev;3789my$logged=delete$self->{logged_rev_props};3790if(!$logged||$self->{-want_revprops}) {3791my$rp=$self->ra->rev_proplist($rev);3792foreach(sort keys%$rp) {3793my$v=$rp->{$_};3794if(/^svn:(author|date|log)$/) {3795$log_entry{$1} =$v;3796}elsif($_eq'svm:headrev') {3797$headrev=$v;3798}else{3799print$un" rev_prop: ", uri_encode($_),' ',3800 uri_encode($v),"\n";3801}3802}3803}else{3804map{$log_entry{$_} =$logged->{$_} }keys%$logged;3805}3806close$unor croak $!;38073808$log_entry{date} = parse_svn_date($log_entry{date});3809$log_entry{log} .="\n";3810my$author=$log_entry{author} = check_author($log_entry{author});3811my($name,$email) =defined$::users{$author} ? @{$::users{$author}}3812: ($author,undef);38133814my($commit_name,$commit_email) = ($name,$email);3815if($_use_log_author) {3816my$name_field;3817if($log_entry{log} =~/From:\s+(.*\S)\s*\n/i) {3818$name_field=$1;3819}elsif($log_entry{log} =~/Signed-off-by:\s+(.*\S)\s*\n/i) {3820$name_field=$1;3821}3822if(!defined$name_field) {3823if(!defined$email) {3824$email=$name;3825}3826}elsif($name_field=~/(.*?)\s+<(.*)>/) {3827($name,$email) = ($1,$2);3828}elsif($name_field=~/(.*)@/) {3829($name,$email) = ($1,$name_field);3830}else{3831($name,$email) = ($name_field,$name_field);3832}3833}3834if(defined$headrev&&$self->use_svm_props) {3835if($self->rewrite_root) {3836die"Can't have both 'useSvmProps' and 'rewriteRoot' ",3837"options set!\n";3838}3839if($self->rewrite_uuid) {3840die"Can't have both 'useSvmProps' and 'rewriteUUID' ",3841"options set!\n";3842}3843my($uuid,$r) =$headrev=~m{^([a-f\d\-]{30,}):(\d+)$}i;3844# we don't want "SVM: initializing mirror for junk" ...3845returnundefif$r==0;3846my$svm=$self->svm;3847if($uuidne$svm->{uuid}) {3848die"UUID mismatch on SVM path:\n",3849"expected:$svm->{uuid}\n",3850" got:$uuid\n";3851}3852my$full_url=$self->full_url;3853$full_url=~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or3854die"Failed to replace '$svm->{replace}' with ",3855"'$svm->{source}' in$full_url\n";3856# throw away username for storing in records3857 remove_username($full_url);3858$log_entry{metadata} ="$full_url\@$r$uuid";3859$log_entry{svm_revision} =$r;3860$email||="$author\@$uuid";3861$commit_email||="$author\@$uuid";3862}elsif($self->use_svnsync_props) {3863my$full_url=$self->svnsync->{url};3864$full_url.="/$self->{path}"iflength$self->{path};3865 remove_username($full_url);3866my$uuid=$self->svnsync->{uuid};3867$log_entry{metadata} ="$full_url\@$rev$uuid";3868$email||="$author\@$uuid";3869$commit_email||="$author\@$uuid";3870}else{3871my$url=$self->metadata_url;3872 remove_username($url);3873my$uuid=$self->rewrite_uuid||$self->ra->get_uuid;3874$log_entry{metadata} ="$url\@$rev".$uuid;3875$email||="$author\@".$uuid;3876$commit_email||="$author\@".$uuid;3877}3878$log_entry{name} =$name;3879$log_entry{email} =$email;3880$log_entry{commit_name} =$commit_name;3881$log_entry{commit_email} =$commit_email;3882 \%log_entry;3883}38843885sub fetch {3886my($self,$min_rev,$max_rev,@parents) =@_;3887my($last_rev,$last_commit) =$self->last_rev_commit;3888my($base,$head) =$self->get_fetch_range($min_rev,$max_rev);3889$self->ra->gs_fetch_loop_common($base,$head, [$self]);3890}38913892sub set_tree_cb {3893my($self,$log_entry,$tree,$rev,$date,$author) =@_;3894$self->{inject_parents} = {$rev=>$tree};3895$self->fetch(undef,undef);3896}38973898sub set_tree {3899my($self,$tree) = (shift,shift);3900my$log_entry= ::get_commit_entry($tree);3901unless($self->{last_rev}) {3902::fatal("Must have an existing revision to commit");3903}3904my%ed_opts= ( r =>$self->{last_rev},3905log=>$log_entry->{log},3906 ra =>$self->ra,3907 tree_a =>$self->{last_commit},3908 tree_b =>$tree,3909 editor_cb =>sub{3910$self->set_tree_cb($log_entry,$tree,@_) },3911 svn_path =>$self->{path} );3912if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {3913print"No changes\nr$self->{last_rev} =$tree\n";3914}3915}39163917sub rebuild_from_rev_db {3918my($self,$path) =@_;3919my$r= -1;3920open my$fh,'<',$pathor croak "open:$!";3921binmode$fhor croak "binmode:$!";3922while(<$fh>) {3923length($_) ==41or croak "inconsistent size in ($_) != 41";3924chomp($_);3925++$r;3926next if$_eq('0' x 40);3927$self->rev_map_set($r,$_);3928print"r$r=$_\n";3929}3930close$fhor croak "close:$!";3931unlink$pathor croak "unlink:$!";3932}39333934sub rebuild {3935my($self) =@_;3936my$map_path=$self->map_path;3937my$partial= (-e $map_path&& ! -z $map_path);3938return unless::verify_ref($self->refname.'^0');3939if(!$partial&& ($self->use_svm_props||$self->no_metadata)) {3940my$rev_db=$self->rev_db_path;3941$self->rebuild_from_rev_db($rev_db);3942if($self->use_svm_props) {3943my$svm_rev_db=$self->rev_db_path($self->svm_uuid);3944$self->rebuild_from_rev_db($svm_rev_db);3945}3946$self->unlink_rev_db_symlink;3947return;3948}3949print"Rebuilding$map_path...\n"if(!$partial);3950my($base_rev,$head) = ($partial?$self->rev_map_max_norebuild(1) :3951(undef,undef));3952my($log,$ctx) =3953 command_output_pipe(qw/rev-list --pretty=raw --reverse/,3954($head?"$head..":"") .$self->refname,3955'--');3956my$metadata_url=$self->metadata_url;3957 remove_username($metadata_url);3958my$svn_uuid=$self->rewrite_uuid||$self->ra_uuid;3959my$c;3960while(<$log>) {3961if(m{^commit ($::sha1)$}) {3962$c=$1;3963next;3964}3965next unlesss{^\s*(git-svn-id:)}{$1};3966my($url,$rev,$uuid) = ::extract_metadata($_);3967 remove_username($url);39683969# ignore merges (from set-tree)3970next if(!defined$rev|| !$uuid);39713972# if we merged or otherwise started elsewhere, this is3973# how we break out of it3974if(($uuidne$svn_uuid) ||3975($metadata_url&&$url&& ($urlne$metadata_url))) {3976next;3977}3978if($partial&&$head) {3979print"Partial-rebuilding$map_path...\n";3980print"Currently at$base_rev=$head\n";3981$head=undef;3982}39833984$self->rev_map_set($rev,$c);3985print"r$rev=$c\n";3986}3987 command_close_pipe($log,$ctx);3988print"Done rebuilding$map_path\n"if(!$partial|| !$head);3989my$rev_db_path=$self->rev_db_path;3990if(-f $self->rev_db_path) {3991unlink$self->rev_db_pathor croak "unlink:$!";3992}3993$self->unlink_rev_db_symlink;3994}39953996# rev_map:3997# Tie::File seems to be prone to offset errors if revisions get sparse,3998# it's not that fast, either. Tie::File is also not in Perl 5.6. So3999# one of my favorite modules is out :< Next up would be one of the DBM4000# modules, but I'm not sure which is most portable...4001#4002# This is the replacement for the rev_db format, which was too big4003# and inefficient for large repositories with a lot of sparse history4004# (mainly tags)4005#4006# The format is this:4007# - 24 bytes for every record,4008# * 4 bytes for the integer representing an SVN revision number4009# * 20 bytes representing the sha1 of a git commit4010# - No empty padding records like the old format4011# (except the last record, which can be overwritten)4012# - new records are written append-only since SVN revision numbers4013# increase monotonically4014# - lookups on SVN revision number are done via a binary search4015# - Piping the file to xxd -c24 is a good way of dumping it for4016# viewing or editing (piped back through xxd -r), should the need4017# ever arise.4018# - The last record can be padding revision with an all-zero sha14019# This is used to optimize fetch performance when using multiple4020# "fetch" directives in .git/config4021#4022# These files are disposable unless noMetadata or useSvmProps is set40234024sub _rev_map_set {4025my($fh,$rev,$commit) =@_;40264027binmode$fhor croak "binmode:$!";4028my$size= (stat($fh))[7];4029($size%24) ==0or croak "inconsistent size:$size";40304031my$wr_offset=0;4032if($size>0) {4033sysseek($fh, -24, SEEK_END)or croak "seek:$!";4034my$read=sysread($fh,my$buf,24)or croak "read:$!";4035$read==24or croak "read only$readbytes (!= 24)";4036my($last_rev,$last_commit) =unpack(rev_map_fmt,$buf);4037if($last_commiteq('0' x40)) {4038if($size>=48) {4039sysseek($fh, -48, SEEK_END)or croak "seek:$!";4040$read=sysread($fh,$buf,24)or4041 croak "read:$!";4042$read==24or4043 croak "read only$readbytes (!= 24)";4044($last_rev,$last_commit) =4045unpack(rev_map_fmt,$buf);4046if($last_commiteq('0' x40)) {4047 croak "inconsistent .rev_map\n";4048}4049}4050if($last_rev>=$rev) {4051 croak "last_rev is higher!:$last_rev>=$rev";4052}4053$wr_offset= -24;4054}4055}4056sysseek($fh,$wr_offset, SEEK_END)or croak "seek:$!";4057syswrite($fh,pack(rev_map_fmt,$rev,$commit),24) ==24or4058 croak "write:$!";4059}40604061sub _rev_map_reset {4062my($fh,$rev,$commit) =@_;4063my$c= _rev_map_get($fh,$rev);4064$ceq$commitor die"_rev_map_reset(@_) commit$cdoes not match!\n";4065my$offset=sysseek($fh,0, SEEK_CUR)or croak "seek:$!";4066truncate$fh,$offsetor croak "truncate:$!";4067}40684069sub mkfile {4070my($path) =@_;4071unless(-e $path) {4072my($dir,$base) = ($path=~ m#^(.*?)/?([^/]+)$#);4073 mkpath([$dir])unless-d $dir;4074open my$fh,'>>',$pathor die"Couldn't create$path:$!\n";4075close$fhor die"Couldn't close (create)$path:$!\n";4076}4077}40784079sub rev_map_set {4080my($self,$rev,$commit,$update_ref,$uuid) =@_;4081defined$commitor die"missing arg3\n";4082length$commit==40or die"arg3 must be a full SHA1 hexsum\n";4083my$db=$self->map_path($uuid);4084my$db_lock="$db.lock";4085my$sigmask;4086$update_ref||=0;4087if($update_ref) {4088$sigmask= POSIX::SigSet->new();4089my$signew= POSIX::SigSet->new(SIGINT, SIGHUP, SIGTERM,4090 SIGALRM, SIGUSR1, SIGUSR2);4091 sigprocmask(SIG_BLOCK,$signew,$sigmask)or4092 croak "Can't block signals:$!";4093}4094 mkfile($db);40954096$LOCKFILES{$db_lock} =1;4097my$sync;4098# both of these options make our .rev_db file very, very important4099# and we can't afford to lose it because rebuild() won't work4100if($self->use_svm_props||$self->no_metadata) {4101$sync=1;4102 copy($db,$db_lock)or die"rev_map_set(@_): ",4103"Failed to copy: ",4104"$db=>$db_lock($!)\n";4105}else{4106rename$db,$db_lockor die"rev_map_set(@_): ",4107"Failed to rename: ",4108"$db=>$db_lock($!)\n";4109}41104111sysopen(my$fh,$db_lock, O_RDWR | O_CREAT)4112or croak "Couldn't open$db_lock:$!\n";4113$update_refeq'reset'? _rev_map_reset($fh,$rev,$commit) :4114 _rev_map_set($fh,$rev,$commit);4115if($sync) {4116$fh->flushor die"Couldn't flush$db_lock:$!\n";4117$fh->syncor die"Couldn't sync$db_lock:$!\n";4118}4119close$fhor croak $!;4120if($update_ref) {4121$_head=$self;4122my$note="";4123$note=" ($update_ref)"if($update_ref!~/^\d*$/);4124 command_noisy('update-ref','-m',"r$rev$note",4125$self->refname,$commit);4126}4127rename$db_lock,$dbor die"rev_map_set(@_): ","Failed to rename: ",4128"$db_lock=>$db($!)\n";4129delete$LOCKFILES{$db_lock};4130if($update_ref) {4131 sigprocmask(SIG_SETMASK,$sigmask)or4132 croak "Can't restore signal mask:$!";4133}4134}41354136# If want_commit, this will return an array of (rev, commit) where4137# commit _must_ be a valid commit in the archive.4138# Otherwise, it'll return the max revision (whether or not the4139# commit is valid or just a 0x40 placeholder).4140sub rev_map_max {4141my($self,$want_commit) =@_;4142$self->rebuild;4143my($r,$c) =$self->rev_map_max_norebuild($want_commit);4144$want_commit? ($r,$c) :$r;4145}41464147sub rev_map_max_norebuild {4148my($self,$want_commit) =@_;4149my$map_path=$self->map_path;4150stat$map_pathorreturn$want_commit? (0,undef) :0;4151sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4152binmode$fhor croak "binmode:$!";4153my$size= (stat($fh))[7];4154($size%24) ==0or croak "inconsistent size:$size";41554156if($size==0) {4157close$fhor croak "close:$!";4158return$want_commit? (0,undef) :0;4159}41604161sysseek($fh, -24, SEEK_END)or croak "seek:$!";4162sysread($fh,my$buf,24) ==24or croak "read:$!";4163my($r,$c) =unpack(rev_map_fmt,$buf);4164if($want_commit&&$ceq('0' x40)) {4165if($size<48) {4166return$want_commit? (0,undef) :0;4167}4168sysseek($fh, -48, SEEK_END)or croak "seek:$!";4169sysread($fh,$buf,24) ==24or croak "read:$!";4170($r,$c) =unpack(rev_map_fmt,$buf);4171if($ceq('0'x40)) {4172 croak "Penultimate record is all-zeroes in$map_path";4173}4174}4175close$fhor croak "close:$!";4176$want_commit? ($r,$c) :$r;4177}41784179sub rev_map_get {4180my($self,$rev,$uuid) =@_;4181my$map_path=$self->map_path($uuid);4182returnundefunless-e $map_path;41834184sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4185my$c= _rev_map_get($fh,$rev);4186close($fh)or croak "close:$!";4187$c4188}41894190sub _rev_map_get {4191my($fh,$rev) =@_;41924193binmode$fhor croak "binmode:$!";4194my$size= (stat($fh))[7];4195($size%24) ==0or croak "inconsistent size:$size";41964197if($size==0) {4198returnundef;4199}42004201my($l,$u) = (0,$size-24);4202my($r,$c,$buf);42034204while($l<=$u) {4205my$i=int(($l/24+$u/24) /2) *24;4206sysseek($fh,$i, SEEK_SET)or croak "seek:$!";4207sysread($fh,my$buf,24) ==24or croak "read:$!";4208my($r,$c) =unpack(rev_map_fmt,$buf);42094210if($r<$rev) {4211$l=$i+24;4212}elsif($r>$rev) {4213$u=$i-24;4214}else{# $r == $rev4215return$ceq('0' x 40) ?undef:$c;4216}4217}4218undef;4219}42204221# Finds the first svn revision that exists on (if $eq_ok is true) or4222# before $rev for the current branch. It will not search any lower4223# than $min_rev. Returns the git commit hash and svn revision number4224# if found, else (undef, undef).4225sub find_rev_before {4226my($self,$rev,$eq_ok,$min_rev) =@_;4227--$revunless$eq_ok;4228$min_rev||=1;4229my$max_rev=$self->rev_map_max;4230$rev=$max_revif($rev>$max_rev);4231while($rev>=$min_rev) {4232if(my$c=$self->rev_map_get($rev)) {4233return($rev,$c);4234}4235--$rev;4236}4237return(undef,undef);4238}42394240# Finds the first svn revision that exists on (if $eq_ok is true) or4241# after $rev for the current branch. It will not search any higher4242# than $max_rev. Returns the git commit hash and svn revision number4243# if found, else (undef, undef).4244sub find_rev_after {4245my($self,$rev,$eq_ok,$max_rev) =@_;4246++$revunless$eq_ok;4247$max_rev||=$self->rev_map_max;4248while($rev<=$max_rev) {4249if(my$c=$self->rev_map_get($rev)) {4250return($rev,$c);4251}4252++$rev;4253}4254return(undef,undef);4255}42564257sub _new {4258my($class,$repo_id,$ref_id,$path) =@_;4259unless(defined$repo_id&&length$repo_id) {4260$repo_id=$Git::SVN::default_repo_id;4261}4262unless(defined$ref_id&&length$ref_id) {4263$_prefix=''unlessdefined($_prefix);4264$_[2] =$ref_id=4265"refs/remotes/$_prefix$Git::SVN::default_ref_id";4266}4267$_[1] =$repo_id;4268my$dir="$ENV{GIT_DIR}/svn/$ref_id";42694270# Older repos imported by us used $GIT_DIR/svn/foo instead of4271# $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo4272if($ref_id=~m{^refs/remotes/(.*)}) {4273my$old_dir="$ENV{GIT_DIR}/svn/$1";4274if(-d $old_dir&& ! -d $dir) {4275$dir=$old_dir;4276}4277}42784279$_[3] =$path=''unless(defined$path);4280 mkpath([$dir]);4281bless{4282 ref_id =>$ref_id, dir =>$dir,index=>"$dir/index",4283 path =>$path, config =>"$ENV{GIT_DIR}/svn/config",4284 map_root =>"$dir/.rev_map", repo_id =>$repo_id},$class;4285}42864287# for read-only access of old .rev_db formats4288sub unlink_rev_db_symlink {4289my($self) =@_;4290my$link=$self->rev_db_path;4291$link=~s/\.[\w-]+$//or croak "missing UUID at the end of$link";4292if(-l $link) {4293unlink$linkor croak "unlink:$linkfailed!";4294}4295}42964297sub rev_db_path {4298my($self,$uuid) =@_;4299my$db_path=$self->map_path($uuid);4300$db_path=~s{/\.rev_map\.}{/\.rev_db\.}4301or croak "map_path:$db_pathdoes not contain '/.rev_map.' !";4302$db_path;4303}43044305# the new replacement for .rev_db4306sub map_path {4307my($self,$uuid) =@_;4308$uuid||=$self->ra_uuid;4309"$self->{map_root}.$uuid";4310}43114312sub uri_encode {4313my($f) =@_;4314$f=~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;4315$f4316}43174318sub uri_decode {4319my($f) =@_;4320$f=~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;4321$f4322}43234324sub remove_username {4325$_[0] =~s{^([^:]*://)[^@]+@}{$1};4326}43274328package Git::SVN::Prompt;4329use strict;4330use warnings;4331require SVN::Core;4332use vars qw/$_no_auth_cache $_username/;43334334sub simple {4335my($cred,$realm,$default_username,$may_save,$pool) =@_;4336$may_save=undefif$_no_auth_cache;4337$default_username=$_usernameifdefined$_username;4338if(defined$default_username&&length$default_username) {4339if(defined$realm&&length$realm) {4340print STDERR "Authentication realm:$realm\n";4341 STDERR->flush;4342}4343$cred->username($default_username);4344}else{4345 username($cred,$realm,$may_save,$pool);4346}4347$cred->password(_read_password("Password for '".4348$cred->username."': ",$realm));4349$cred->may_save($may_save);4350$SVN::_Core::SVN_NO_ERROR;4351}43524353sub ssl_server_trust {4354my($cred,$realm,$failures,$cert_info,$may_save,$pool) =@_;4355$may_save=undefif$_no_auth_cache;4356print STDERR "Error validating server certificate for '$realm':\n";4357{4358no warnings 'once';4359# All variables SVN::Auth::SSL::* are used only once,4360# so we're shutting up Perl warnings about this.4361if($failures&$SVN::Auth::SSL::UNKNOWNCA) {4362print STDERR " - The certificate is not issued ",4363"by a trusted authority. Use the\n",4364" fingerprint to validate ",4365"the certificate manually!\n";4366}4367if($failures&$SVN::Auth::SSL::CNMISMATCH) {4368print STDERR " - The certificate hostname ",4369"does not match.\n";4370}4371if($failures&$SVN::Auth::SSL::NOTYETVALID) {4372print STDERR " - The certificate is not yet valid.\n";4373}4374if($failures&$SVN::Auth::SSL::EXPIRED) {4375print STDERR " - The certificate has expired.\n";4376}4377if($failures&$SVN::Auth::SSL::OTHER) {4378print STDERR " - The certificate has ",4379"an unknown error.\n";4380}4381}# no warnings 'once'4382printf STDERR4383"Certificate information:\n".4384" - Hostname:%s\n".4385" - Valid: from%suntil%s\n".4386" - Issuer:%s\n".4387" - Fingerprint:%s\n",4388map$cert_info->$_,qw(hostname valid_from valid_until4389 issuer_dname fingerprint);4390my$choice;4391prompt:4392print STDERR $may_save?4393"(R)eject, accept (t)emporarily or accept (p)ermanently? ":4394"(R)eject or accept (t)emporarily? ";4395 STDERR->flush;4396$choice=lc(substr(<STDIN> ||'R',0,1));4397if($choice=~/^t$/i) {4398$cred->may_save(undef);4399}elsif($choice=~/^r$/i) {4400return-1;4401}elsif($may_save&&$choice=~/^p$/i) {4402$cred->may_save($may_save);4403}else{4404goto prompt;4405}4406$cred->accepted_failures($failures);4407$SVN::_Core::SVN_NO_ERROR;4408}44094410sub ssl_client_cert {4411my($cred,$realm,$may_save,$pool) =@_;4412$may_save=undefif$_no_auth_cache;4413print STDERR "Client certificate filename: ";4414 STDERR->flush;4415chomp(my$filename= <STDIN>);4416$cred->cert_file($filename);4417$cred->may_save($may_save);4418$SVN::_Core::SVN_NO_ERROR;4419}44204421sub ssl_client_cert_pw {4422my($cred,$realm,$may_save,$pool) =@_;4423$may_save=undefif$_no_auth_cache;4424$cred->password(_read_password("Password: ",$realm));4425$cred->may_save($may_save);4426$SVN::_Core::SVN_NO_ERROR;4427}44284429sub username {4430my($cred,$realm,$may_save,$pool) =@_;4431$may_save=undefif$_no_auth_cache;4432if(defined$realm&&length$realm) {4433print STDERR "Authentication realm:$realm\n";4434}4435my$username;4436if(defined$_username) {4437$username=$_username;4438}else{4439print STDERR "Username: ";4440 STDERR->flush;4441chomp($username= <STDIN>);4442}4443$cred->username($username);4444$cred->may_save($may_save);4445$SVN::_Core::SVN_NO_ERROR;4446}44474448sub _read_password {4449my($prompt,$realm) =@_;4450my$password='';4451if(exists$ENV{GIT_ASKPASS}) {4452open(PH,"-|",$ENV{GIT_ASKPASS},$prompt);4453$password= <PH>;4454$password=~s/[\012\015]//;# \n\r4455close(PH);4456}else{4457print STDERR $prompt;4458 STDERR->flush;4459require Term::ReadKey;4460 Term::ReadKey::ReadMode('noecho');4461while(defined(my$key= Term::ReadKey::ReadKey(0))) {4462last if$key=~/[\012\015]/;# \n\r4463$password.=$key;4464}4465 Term::ReadKey::ReadMode('restore');4466print STDERR "\n";4467 STDERR->flush;4468}4469$password;4470}44714472package SVN::Git::Fetcher;4473use vars qw/@ISA $_ignore_regex $_preserve_empty_dirs $_placeholder_filename4474@deleted_gpath %added_placeholder $repo_id/;4475use strict;4476use warnings;4477use Carp qw/croak/;4478use File::Basename qw/dirname/;4479use IO::File qw//;44804481# file baton members: path, mode_a, mode_b, pool, fh, blob, base4482sub new {4483my($class,$git_svn,$switch_path) =@_;4484my$self= SVN::Delta::Editor->new;4485bless$self,$class;4486if(exists$git_svn->{last_commit}) {4487$self->{c} =$git_svn->{last_commit};4488$self->{empty_symlinks} =4489 _mark_empty_symlinks($git_svn,$switch_path);4490}44914492# some options are read globally, but can be overridden locally4493# per [svn-remote "..."] section. Command-line options will *NOT*4494# override options set in an [svn-remote "..."] section4495$repo_id=$git_svn->{repo_id};4496my$k="svn-remote.$repo_id.ignore-paths";4497my$v=eval{ command_oneline('config','--get',$k) };4498$self->{ignore_regex} =$v;44994500$k="svn-remote.$repo_id.preserve-empty-dirs";4501$v=eval{ command_oneline('config','--get','--bool',$k) };4502if($v&&$veq'true') {4503$_preserve_empty_dirs=1;4504$k="svn-remote.$repo_id.placeholder-filename";4505$v=eval{ command_oneline('config','--get',$k) };4506$_placeholder_filename=$v;4507}45084509# Load the list of placeholder files added during previous invocations.4510$k="svn-remote.$repo_id.added-placeholder";4511$v=eval{ command_oneline('config','--get-all',$k) };4512if($_preserve_empty_dirs&&$v) {4513# command() prints errors to stderr, so we only call it if4514# command_oneline() succeeded.4515my@v= command('config','--get-all',$k);4516$added_placeholder{ dirname($_) } =$_foreach@v;4517}45184519$self->{empty} = {};4520$self->{dir_prop} = {};4521$self->{file_prop} = {};4522$self->{absent_dir} = {};4523$self->{absent_file} = {};4524$self->{gii} =$git_svn->tmp_index_do(sub{ Git::IndexInfo->new});4525$self->{pathnameencoding} = Git::config('svn.pathnameencoding');4526$self;4527}45284529# this uses the Ra object, so it must be called before do_{switch,update},4530# not inside them (when the Git::SVN::Fetcher object is passed) to4531# do_{switch,update}4532sub _mark_empty_symlinks {4533my($git_svn,$switch_path) =@_;4534my$bool= Git::config_bool('svn.brokenSymlinkWorkaround');4535return{}if(!defined($bool)) || (defined($bool) && !$bool);45364537my%ret;4538my($rev,$cmt) =$git_svn->last_rev_commit;4539return{}unless($rev&&$cmt);45404541# allow the warning to be printed for each revision we fetch to4542# ensure the user sees it. The user can also disable the workaround4543# on the repository even while git svn is running and the next4544# revision fetched will skip this expensive function.4545my$printed_warning;4546chomp(my$empty_blob=`git hash-object -t blob --stdin < /dev/null`);4547my($ls,$ctx) = command_output_pipe(qw/ls-tree -r -z/,$cmt);4548local$/="\0";4549my$pfx=defined($switch_path) ?$switch_path:$git_svn->{path};4550$pfx.='/'iflength($pfx);4551while(<$ls>) {4552chomp;4553s/\A100644 blob $empty_blob\t//oornext;4554unless($printed_warning) {4555print STDERR "Scanning for empty symlinks, ",4556"this may take a while if you have ",4557"many empty files\n",4558"You may disable this with `",4559"git config svn.brokenSymlinkWorkaround ",4560"false'.\n",4561"This may be done in a different ",4562"terminal without restarting ",4563"git svn\n";4564$printed_warning=1;4565}4566my$path=$_;4567my(undef,$props) =4568$git_svn->ra->get_file($pfx.$path,$rev,undef);4569if($props->{'svn:special'}) {4570$ret{$path} =1;4571}4572}4573 command_close_pipe($ls,$ctx);4574 \%ret;4575}45764577# returns true if a given path is inside a ".git" directory4578sub in_dot_git {4579$_[0] =~m{(?:^|/)\.git(?:/|$)};4580}45814582# return value: 0 -- don't ignore, 1 -- ignore4583sub is_path_ignored {4584my($self,$path) =@_;4585return1if in_dot_git($path);4586return1ifdefined($self->{ignore_regex}) &&4587$path=~m!$self->{ignore_regex}!;4588return0unlessdefined($_ignore_regex);4589return1if$path=~m!$_ignore_regex!o;4590return0;4591}45924593sub set_path_strip {4594my($self,$path) =@_;4595$self->{path_strip} =qr/^\Q$path\E(\/|$)/iflength$path;4596}45974598sub open_root {4599{ path =>''};4600}46014602sub open_directory {4603my($self,$path,$pb,$rev) =@_;4604{ path =>$path};4605}46064607sub git_path {4608my($self,$path) =@_;4609if(my$enc=$self->{pathnameencoding}) {4610require Encode;4611 Encode::from_to($path,'UTF-8',$enc);4612}4613if($self->{path_strip}) {4614$path=~s!$self->{path_strip}!!or4615die"Failed to strip path '$path' ($self->{path_strip})\n";4616}4617$path;4618}46194620sub delete_entry {4621my($self,$path,$rev,$pb) =@_;4622returnundefif$self->is_path_ignored($path);46234624my$gpath=$self->git_path($path);4625returnundefif($gpatheq'');46264627# remove entire directories.4628my($tree) = (command('ls-tree','-z',$self->{c},"./$gpath")4629=~/\A040000 tree ([a-f\d]{40})\t\Q$gpath\E\0/);4630if($tree) {4631my($ls,$ctx) = command_output_pipe(qw/ls-tree4632-r --name-only -z/,4633$tree);4634local$/="\0";4635while(<$ls>) {4636chomp;4637my$rmpath="$gpath/$_";4638$self->{gii}->remove($rmpath);4639print"\tD\t$rmpath\n"unless$::_q;4640}4641print"\tD\t$gpath/\n"unless$::_q;4642 command_close_pipe($ls,$ctx);4643}else{4644$self->{gii}->remove($gpath);4645print"\tD\t$gpath\n"unless$::_q;4646}4647# Don't add to @deleted_gpath if we're deleting a placeholder file.4648push@deleted_gpath,$gpathunless$added_placeholder{dirname($path)};4649$self->{empty}->{$path} =0;4650undef;4651}46524653sub open_file {4654my($self,$path,$pb,$rev) =@_;4655my($mode,$blob);46564657goto out if$self->is_path_ignored($path);46584659my$gpath=$self->git_path($path);4660($mode,$blob) = (command('ls-tree','-z',$self->{c},"./$gpath")4661=~/\A(\d{6}) blob ([a-f\d]{40})\t\Q$gpath\E\0/);4662unless(defined$mode&&defined$blob) {4663die"$pathwas not found in commit$self->{c} (r$rev)\n";4664}4665if($modeeq'100644'&&$self->{empty_symlinks}->{$path}) {4666$mode='120000';4667}4668out:4669{ path =>$path, mode_a =>$mode, mode_b =>$mode, blob =>$blob,4670 pool => SVN::Pool->new, action =>'M'};4671}46724673sub add_file {4674my($self,$path,$pb,$cp_path,$cp_rev) =@_;4675my$mode;46764677if(!$self->is_path_ignored($path)) {4678my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4679delete$self->{empty}->{$dir};4680$mode='100644';46814682if($added_placeholder{$dir}) {4683# Remove our placeholder file, if we created one.4684 delete_entry($self,$added_placeholder{$dir})4685unless$patheq$added_placeholder{$dir};4686delete$added_placeholder{$dir}4687}4688}46894690{ path =>$path, mode_a =>$mode, mode_b =>$mode,4691 pool => SVN::Pool->new, action =>'A'};4692}46934694sub add_directory {4695my($self,$path,$cp_path,$cp_rev) =@_;4696goto out if$self->is_path_ignored($path);4697my$gpath=$self->git_path($path);4698if($gpatheq'') {4699my($ls,$ctx) = command_output_pipe(qw/ls-tree4700-r --name-only -z/,4701$self->{c});4702local$/="\0";4703while(<$ls>) {4704chomp;4705$self->{gii}->remove($_);4706print"\tD\t$_\n"unless$::_q;4707push@deleted_gpath,$gpath;4708}4709 command_close_pipe($ls,$ctx);4710$self->{empty}->{$path} =0;4711}4712my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4713delete$self->{empty}->{$dir};4714$self->{empty}->{$path} =1;47154716if($added_placeholder{$dir}) {4717# Remove our placeholder file, if we created one.4718 delete_entry($self,$added_placeholder{$dir});4719delete$added_placeholder{$dir}4720}47214722out:4723{ path =>$path};4724}47254726sub change_dir_prop {4727my($self,$db,$prop,$value) =@_;4728returnundefif$self->is_path_ignored($db->{path});4729$self->{dir_prop}->{$db->{path}} ||= {};4730$self->{dir_prop}->{$db->{path}}->{$prop} =$value;4731undef;4732}47334734sub absent_directory {4735my($self,$path,$pb) =@_;4736returnundefif$self->is_path_ignored($path);4737$self->{absent_dir}->{$pb->{path}} ||= [];4738push@{$self->{absent_dir}->{$pb->{path}}},$path;4739undef;4740}47414742sub absent_file {4743my($self,$path,$pb) =@_;4744returnundefif$self->is_path_ignored($path);4745$self->{absent_file}->{$pb->{path}} ||= [];4746push@{$self->{absent_file}->{$pb->{path}}},$path;4747undef;4748}47494750sub change_file_prop {4751my($self,$fb,$prop,$value) =@_;4752returnundefif$self->is_path_ignored($fb->{path});4753if($propeq'svn:executable') {4754if($fb->{mode_b} !=120000) {4755$fb->{mode_b} =defined$value?100755:100644;4756}4757}elsif($propeq'svn:special') {4758$fb->{mode_b} =defined$value?120000:100644;4759}else{4760$self->{file_prop}->{$fb->{path}} ||= {};4761$self->{file_prop}->{$fb->{path}}->{$prop} =$value;4762}4763undef;4764}47654766sub apply_textdelta {4767my($self,$fb,$exp) =@_;4768returnundefif$self->is_path_ignored($fb->{path});4769my$fh= $::_repository->temp_acquire('svn_delta');4770# $fh gets auto-closed() by SVN::TxDelta::apply(),4771# (but $base does not,) so dup() it for reading in close_file4772open my$dup,'<&',$fhor croak $!;4773my$base= $::_repository->temp_acquire('git_blob');47744775if($fb->{blob}) {4776my($base_is_link,$size);47774778if($fb->{mode_a}eq'120000'&&4779!$self->{empty_symlinks}->{$fb->{path}}) {4780print$base'link 'or die"print$!\n";4781$base_is_link=1;4782}4783 retry:4784$size= $::_repository->cat_blob($fb->{blob},$base);4785die"Failed to read object$fb->{blob}"if($size<0);47864787if(defined$exp) {4788seek$base,0,0or croak $!;4789my$got= ::md5sum($base);4790if($gotne$exp) {4791my$err="Checksum mismatch: ".4792"$fb->{path}$fb->{blob}\n".4793"expected:$exp\n".4794" got:$got\n";4795if($base_is_link) {4796warn$err,4797"Retrying... (possibly ",4798"a bad symlink from SVN)\n";4799$::_repository->temp_reset($base);4800$base_is_link=0;4801goto retry;4802}4803die$err;4804}4805}4806}4807seek$base,0,0or croak $!;4808$fb->{fh} =$fh;4809$fb->{base} =$base;4810[ SVN::TxDelta::apply($base,$dup,undef,$fb->{path},$fb->{pool}) ];4811}48124813sub close_file {4814my($self,$fb,$exp) =@_;4815returnundefif$self->is_path_ignored($fb->{path});48164817my$hash;4818my$path=$self->git_path($fb->{path});4819if(my$fh=$fb->{fh}) {4820if(defined$exp) {4821seek($fh,0,0)or croak $!;4822my$got= ::md5sum($fh);4823if($gotne$exp) {4824die"Checksum mismatch:$path\n",4825"expected:$exp\ngot:$got\n";4826}4827}4828if($fb->{mode_b} ==120000) {4829sysseek($fh,0,0)or croak $!;4830my$rd=sysread($fh,my$buf,5);48314832if(!defined$rd) {4833 croak "sysread:$!\n";4834}elsif($rd==0) {4835warn"$pathhas mode 120000",4836" but it points to nothing\n",4837"converting to an empty file with mode",4838" 100644\n";4839$fb->{mode_b} ='100644';4840}elsif($bufne'link ') {4841warn"$pathhas mode 120000",4842" but is not a link\n";4843}else{4844my$tmp_fh= $::_repository->temp_acquire(4845'svn_hash');4846my$res;4847while($res=sysread($fh,my$str,1024)) {4848my$out=syswrite($tmp_fh,$str,$res);4849defined($out) &&$out==$res4850or croak("write ",4851 Git::temp_path($tmp_fh),4852":$!\n");4853}4854defined$resor croak $!;48554856($fh,$tmp_fh) = ($tmp_fh,$fh);4857 Git::temp_release($tmp_fh,1);4858}4859}48604861$hash= $::_repository->hash_and_insert_object(4862 Git::temp_path($fh));4863$hash=~/^[a-f\d]{40}$/or die"not a sha1:$hash\n";48644865 Git::temp_release($fb->{base},1);4866 Git::temp_release($fh,1);4867}else{4868$hash=$fb->{blob}or die"no blob information\n";4869}4870$fb->{pool}->clear;4871$self->{gii}->update($fb->{mode_b},$hash,$path)or croak $!;4872print"\t$fb->{action}\t$path\n"if$fb->{action} && ! $::_q;4873undef;4874}48754876sub abort_edit {4877my$self=shift;4878$self->{nr} =$self->{gii}->{nr};4879delete$self->{gii};4880$self->SUPER::abort_edit(@_);4881}48824883sub close_edit {4884my$self=shift;48854886if($_preserve_empty_dirs) {4887my@empty_dirs;48884889# Any entry flagged as empty that also has an associated4890# dir_prop represents a newly created empty directory.4891foreachmy$i(keys%{$self->{empty}}) {4892push@empty_dirs,$iifexists$self->{dir_prop}->{$i};4893}48944895# Search for directories that have become empty due subsequent4896# file deletes.4897push@empty_dirs,$self->find_empty_directories();48984899# Finally, add a placeholder file to each empty directory.4900$self->add_placeholder_file($_)foreach(@empty_dirs);49014902$self->stash_placeholder_list();4903}49044905$self->{git_commit_ok} =1;4906$self->{nr} =$self->{gii}->{nr};4907delete$self->{gii};4908$self->SUPER::close_edit(@_);4909}49104911sub find_empty_directories {4912my($self) =@_;4913my@empty_dirs;4914my%dirs=map{ dirname($_) =>1}@deleted_gpath;49154916foreachmy$dir(sort keys%dirs) {4917next if$direq".";49184919# If there have been any additions to this directory, there is4920# no reason to check if it is empty.4921my$skip_added=0;4922foreachmy$t(qw/dir_prop file_prop/) {4923foreachmy$path(keys%{$self->{$t} }) {4924if(exists$self->{$t}->{dirname($path)}) {4925$skip_added=1;4926last;4927}4928}4929last if$skip_added;4930}4931next if$skip_added;49324933# Use `git ls-tree` to get the filenames of this directory4934# that existed prior to this particular commit.4935my$ls= command('ls-tree','-z','--name-only',4936$self->{c},"$dir/");4937my%files=map{$_=>1}split(/\0/,$ls);49384939# Remove the filenames that were deleted during this commit.4940delete$files{$_}foreach(@deleted_gpath);49414942# Report the directory if there are no filenames left.4943push@empty_dirs,$dirunless(scalar%files);4944}4945@empty_dirs;4946}49474948sub add_placeholder_file {4949my($self,$dir) =@_;4950my$path="$dir/$_placeholder_filename";4951my$gpath=$self->git_path($path);49524953my$fh= $::_repository->temp_acquire($gpath);4954my$hash= $::_repository->hash_and_insert_object(Git::temp_path($fh));4955 Git::temp_release($fh,1);4956$self->{gii}->update('100644',$hash,$gpath)or croak $!;49574958# The directory should no longer be considered empty.4959delete$self->{empty}->{$dir}ifexists$self->{empty}->{$dir};49604961# Keep track of any placeholder files we create.4962$added_placeholder{$dir} =$path;4963}49644965sub stash_placeholder_list {4966my($self) =@_;4967my$k="svn-remote.$repo_id.added-placeholder";4968my$v=eval{ command_oneline('config','--get-all',$k) };4969 command_noisy('config','--unset-all',$k)if$v;4970foreach(values%added_placeholder) {4971 command_noisy('config','--add',$k,$_);4972}4973}49744975package SVN::Git::Editor;4976use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;4977use strict;4978use warnings;4979use Carp qw/croak/;4980use IO::File;49814982sub new {4983my($class,$opts) =@_;4984foreach(qw/svn_path r ra tree_a tree_b log editor_cb/) {4985die"$_required!\n"unless(defined$opts->{$_});4986}49874988my$pool= SVN::Pool->new;4989my$mods= generate_diff($opts->{tree_a},$opts->{tree_b});4990my$types= check_diff_paths($opts->{ra},$opts->{svn_path},4991$opts->{r},$mods);49924993# $opts->{ra} functions should not be used after this:4994my@ce=$opts->{ra}->get_commit_editor($opts->{log},4995$opts->{editor_cb},$pool);4996my$self= SVN::Delta::Editor->new(@ce,$pool);4997bless$self,$class;4998foreach(qw/svn_path r tree_a tree_b/) {4999$self->{$_} =$opts->{$_};5000}5001$self->{url} =$opts->{ra}->{url};5002$self->{mods} =$mods;5003$self->{types} =$types;5004$self->{pool} =$pool;5005$self->{bat} = {''=>$self->open_root($self->{r},$self->{pool}) };5006$self->{rm} = { };5007$self->{path_prefix} =length$self->{svn_path} ?5008"$self->{svn_path}/":'';5009$self->{config} =$opts->{config};5010$self->{mergeinfo} =$opts->{mergeinfo};5011return$self;5012}50135014sub generate_diff {5015my($tree_a,$tree_b) =@_;5016my@diff_tree=qw(diff-tree -z -r);5017if($_cp_similarity) {5018push@diff_tree,"-C$_cp_similarity";5019}else{5020push@diff_tree,'-C';5021}5022push@diff_tree,'--find-copies-harder'if$_find_copies_harder;5023push@diff_tree,"-l$_rename_limit"ifdefined$_rename_limit;5024push@diff_tree,$tree_a,$tree_b;5025my($diff_fh,$ctx) = command_output_pipe(@diff_tree);5026local$/="\0";5027my$state='meta';5028my@mods;5029while(<$diff_fh>) {5030chomp$_;# this gets rid of the trailing "\0"5031if($stateeq'meta'&& /^:(\d{6})\s(\d{6})\s5032($::sha1)\s($::sha1)\s5033([MTCRAD])\d*$/xo) {5034push@mods, { mode_a =>$1, mode_b =>$2,5035 sha1_a =>$3, sha1_b =>$4,5036 chg =>$5};5037if($5=~/^(?:C|R)$/) {5038$state='file_a';5039}else{5040$state='file_b';5041}5042}elsif($stateeq'file_a') {5043my$x=$mods[$#mods]or croak "Empty array\n";5044if($x->{chg} !~/^(?:C|R)$/) {5045 croak "Error parsing$_,$x->{chg}\n";5046}5047$x->{file_a} =$_;5048$state='file_b';5049}elsif($stateeq'file_b') {5050my$x=$mods[$#mods]or croak "Empty array\n";5051if(exists$x->{file_a} &&$x->{chg} !~/^(?:C|R)$/) {5052 croak "Error parsing$_,$x->{chg}\n";5053}5054if(!exists$x->{file_a} &&$x->{chg} =~/^(?:C|R)$/) {5055 croak "Error parsing$_,$x->{chg}\n";5056}5057$x->{file_b} =$_;5058$state='meta';5059}else{5060 croak "Error parsing$_\n";5061}5062}5063 command_close_pipe($diff_fh,$ctx);5064 \@mods;5065}50665067sub check_diff_paths {5068my($ra,$pfx,$rev,$mods) =@_;5069my%types;5070$pfx.='/'iflength$pfx;50715072sub type_diff_paths {5073my($ra,$types,$path,$rev) =@_;5074my@p=split m#/+#, $path;5075my$c=shift@p;5076unless(defined$types->{$c}) {5077$types->{$c} =$ra->check_path($c,$rev);5078}5079while(@p) {5080$c.='/'.shift@p;5081next ifdefined$types->{$c};5082$types->{$c} =$ra->check_path($c,$rev);5083}5084}50855086foreachmy$m(@$mods) {5087foreachmy$f(qw/file_a file_b/) {5088next unlessdefined$m->{$f};5089my($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);5090if(length$pfx.$dir&& !defined$types{$dir}) {5091 type_diff_paths($ra, \%types,$pfx.$dir,$rev);5092}5093}5094}5095 \%types;5096}50975098sub split_path {5099return($_[0] =~ m#^(.*?)/?([^/]+)$#);5100}51015102sub repo_path {5103my($self,$path) =@_;5104if(my$enc=$self->{pathnameencoding}) {5105require Encode;5106 Encode::from_to($path,$enc,'UTF-8');5107}5108$self->{path_prefix}.(defined$path?$path:'');5109}51105111sub url_path {5112my($self,$path) =@_;5113if($self->{url} =~ m#^https?://#) {5114$path=~s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;5115}5116$self->{url} .'/'.$self->repo_path($path);5117}51185119sub rmdirs {5120my($self) =@_;5121my$rm=$self->{rm};5122delete$rm->{''};# we never delete the url we're tracking5123return unless%$rm;51245125foreach(keys%$rm) {5126my@d=split m#/#, $_;5127my$c=shift@d;5128$rm->{$c} =1;5129while(@d) {5130$c.='/'.shift@d;5131$rm->{$c} =1;5132}5133}5134delete$rm->{$self->{svn_path}};5135delete$rm->{''};# we never delete the url we're tracking5136return unless%$rm;51375138my($fh,$ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,5139$self->{tree_b});5140local$/="\0";5141while(<$fh>) {5142chomp;5143my@dn=split m#/#, $_;5144while(pop@dn) {5145delete$rm->{join'/',@dn};5146}5147unless(%$rm) {5148close$fh;5149return;5150}5151}5152 command_close_pipe($fh,$ctx);51535154my($r,$p,$bat) = ($self->{r},$self->{pool},$self->{bat});5155foreachmy$d(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {5156$self->close_directory($bat->{$d},$p);5157my($dn) = ($d=~ m#^(.*?)/?(?:[^/]+)$#);5158print"\tD+\t$d/\n"unless$::_q;5159$self->SUPER::delete_entry($d,$r,$bat->{$dn},$p);5160delete$bat->{$d};5161}5162}51635164sub open_or_add_dir {5165my($self,$full_path,$baton,$deletions) =@_;5166my$t=$self->{types}->{$full_path};5167if(!defined$t) {5168die"$full_pathnot known in r$self->{r} or we have a bug!\n";5169}5170{5171no warnings 'once';5172# SVN::Node::none and SVN::Node::file are used only once,5173# so we're shutting up Perl's warnings about them.5174if($t==$SVN::Node::none ||defined($deletions->{$full_path})) {5175return$self->add_directory($full_path,$baton,5176undef, -1,$self->{pool});5177}elsif($t==$SVN::Node::dir) {5178return$self->open_directory($full_path,$baton,5179$self->{r},$self->{pool});5180}# no warnings 'once'5181print STDERR "$full_pathalready exists in repository at ",5182"r$self->{r} and it is not a directory (",5183($t==$SVN::Node::file ?'file':'unknown'),"/$t)\n";5184}# no warnings 'once'5185exit1;5186}51875188sub ensure_path {5189my($self,$path,$deletions) =@_;5190my$bat=$self->{bat};5191my$repo_path=$self->repo_path($path);5192return$bat->{''}unless(length$repo_path);51935194my@p=split m#/+#, $repo_path;5195my$c=shift@p;5196$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{''},$deletions);5197while(@p) {5198my$c0=$c;5199$c.='/'.shift@p;5200$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{$c0},$deletions);5201}5202return$bat->{$c};5203}52045205# Subroutine to convert a globbing pattern to a regular expression.5206# From perl cookbook.5207sub glob2pat {5208my$globstr=shift;5209my%patmap= ('*'=>'.*','?'=>'.','['=>'[',']'=>']');5210$globstr=~s{(.)} { $patmap{$1}||"\Q$1"}ge;5211return'^'.$globstr.'$';5212}52135214sub check_autoprop {5215my($self,$pattern,$properties,$file,$fbat) =@_;5216# Convert the globbing pattern to a regular expression.5217my$regex= glob2pat($pattern);5218# Check if the pattern matches the file name.5219if($file=~m/($regex)/) {5220# Parse the list of properties to set.5221my@props=split(/;/,$properties);5222foreachmy$prop(@props) {5223# Parse 'name=value' syntax and set the property.5224if($prop=~/([^=]+)=(.*)/) {5225my($n,$v) = ($1,$2);5226for($n,$v) {5227s/^\s+//;s/\s+$//;5228}5229$self->change_file_prop($fbat,$n,$v);5230}5231}5232}5233}52345235sub apply_autoprops {5236my($self,$file,$fbat) =@_;5237my$conf_t= ${$self->{config}}{'config'};5238no warnings 'once';5239# Check [miscellany]/enable-auto-props in svn configuration.5240if(SVN::_Core::svn_config_get_bool(5241$conf_t,5242$SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY,5243$SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS,52440)) {5245# Auto-props are enabled. Enumerate them to look for matches.5246my$callback=sub{5247$self->check_autoprop($_[0],$_[1],$file,$fbat);5248};5249 SVN::_Core::svn_config_enumerate(5250$conf_t,5251$SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS,5252$callback);5253}5254}52555256sub A {5257my($self,$m,$deletions) =@_;5258my($dir,$file) = split_path($m->{file_b});5259my$pbat=$self->ensure_path($dir,$deletions);5260my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5261undef, -1);5262print"\tA\t$m->{file_b}\n"unless$::_q;5263$self->apply_autoprops($file,$fbat);5264$self->chg_file($fbat,$m);5265$self->close_file($fbat,undef,$self->{pool});5266}52675268sub C {5269my($self,$m,$deletions) =@_;5270my($dir,$file) = split_path($m->{file_b});5271my$pbat=$self->ensure_path($dir,$deletions);5272my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5273$self->url_path($m->{file_a}),$self->{r});5274print"\tC\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5275$self->chg_file($fbat,$m);5276$self->close_file($fbat,undef,$self->{pool});5277}52785279sub delete_entry {5280my($self,$path,$pbat) =@_;5281my$rpath=$self->repo_path($path);5282my($dir,$file) = split_path($rpath);5283$self->{rm}->{$dir} =1;5284$self->SUPER::delete_entry($rpath,$self->{r},$pbat,$self->{pool});5285}52865287sub R {5288my($self,$m,$deletions) =@_;5289my($dir,$file) = split_path($m->{file_b});5290my$pbat=$self->ensure_path($dir,$deletions);5291my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5292$self->url_path($m->{file_a}),$self->{r});5293print"\tR\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5294$self->apply_autoprops($file,$fbat);5295$self->chg_file($fbat,$m);5296$self->close_file($fbat,undef,$self->{pool});52975298($dir,$file) = split_path($m->{file_a});5299$pbat=$self->ensure_path($dir,$deletions);5300$self->delete_entry($m->{file_a},$pbat);5301}53025303sub M {5304my($self,$m,$deletions) =@_;5305my($dir,$file) = split_path($m->{file_b});5306my$pbat=$self->ensure_path($dir,$deletions);5307my$fbat=$self->open_file($self->repo_path($m->{file_b}),5308$pbat,$self->{r},$self->{pool});5309print"\t$m->{chg}\t$m->{file_b}\n"unless$::_q;5310$self->chg_file($fbat,$m);5311$self->close_file($fbat,undef,$self->{pool});5312}53135314sub T { shift->M(@_) }53155316sub change_file_prop {5317my($self,$fbat,$pname,$pval) =@_;5318$self->SUPER::change_file_prop($fbat,$pname,$pval,$self->{pool});5319}53205321sub change_dir_prop {5322my($self,$pbat,$pname,$pval) =@_;5323$self->SUPER::change_dir_prop($pbat,$pname,$pval,$self->{pool});5324}53255326sub _chg_file_get_blob ($$$$) {5327my($self,$fbat,$m,$which) =@_;5328my$fh= $::_repository->temp_acquire("git_blob_$which");5329if($m->{"mode_$which"} =~/^120/) {5330print$fh'link 'or croak $!;5331$self->change_file_prop($fbat,'svn:special','*');5332}elsif($m->{mode_a} =~/^120/&&$m->{"mode_$which"} !~/^120/) {5333$self->change_file_prop($fbat,'svn:special',undef);5334}5335my$blob=$m->{"sha1_$which"};5336return($fh,)if($blob=~/^0{40}$/);5337my$size= $::_repository->cat_blob($blob,$fh);5338 croak "Failed to read object$blob"if($size<0);5339$fh->flush==0or croak $!;5340seek$fh,0,0or croak $!;53415342my$exp= ::md5sum($fh);5343seek$fh,0,0or croak $!;5344return($fh,$exp);5345}53465347sub chg_file {5348my($self,$fbat,$m) =@_;5349if($m->{mode_b} =~/755$/&&$m->{mode_a} !~/755$/) {5350$self->change_file_prop($fbat,'svn:executable','*');5351}elsif($m->{mode_b} !~/755$/&&$m->{mode_a} =~/755$/) {5352$self->change_file_prop($fbat,'svn:executable',undef);5353}5354my($fh_a,$exp_a) = _chg_file_get_blob $self,$fbat,$m,'a';5355my($fh_b,$exp_b) = _chg_file_get_blob $self,$fbat,$m,'b';5356my$pool= SVN::Pool->new;5357my$atd=$self->apply_textdelta($fbat,$exp_a,$pool);5358if(-s $fh_a) {5359my$txstream= SVN::TxDelta::new ($fh_a,$fh_b,$pool);5360my$res= SVN::TxDelta::send_txstream($txstream,@$atd,$pool);5361if(defined$res) {5362die"Unexpected result from send_txstream:$res\n",5363"(SVN::Core::VERSION:$SVN::Core::VERSION)\n";5364}5365}else{5366my$got= SVN::TxDelta::send_stream($fh_b,@$atd,$pool);5367die"Checksum mismatch\nexpected:$exp_b\ngot:$got\n"5368if($gotne$exp_b);5369}5370 Git::temp_release($fh_b,1);5371 Git::temp_release($fh_a,1);5372$pool->clear;5373}53745375sub D {5376my($self,$m,$deletions) =@_;5377my($dir,$file) = split_path($m->{file_b});5378my$pbat=$self->ensure_path($dir,$deletions);5379print"\tD\t$m->{file_b}\n"unless$::_q;5380$self->delete_entry($m->{file_b},$pbat);5381}53825383sub close_edit {5384my($self) =@_;5385my($p,$bat) = ($self->{pool},$self->{bat});5386foreach(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {5387next if$_eq'';5388$self->close_directory($bat->{$_},$p);5389}5390$self->close_directory($bat->{''},$p);5391$self->SUPER::close_edit($p);5392$p->clear;5393}53945395sub abort_edit {5396my($self) =@_;5397$self->SUPER::abort_edit($self->{pool});5398}53995400sub DESTROY {5401my$self=shift;5402$self->SUPER::DESTROY(@_);5403$self->{pool}->clear;5404}54055406# this drives the editor5407sub apply_diff {5408my($self) =@_;5409my$mods=$self->{mods};5410my%o= ( D =>0, C =>1, R =>2, A =>3, M =>4, T =>5);5411my%deletions;54125413foreachmy$m(@$mods) {5414if($m->{chg}eq"D") {5415$deletions{$m->{file_b}} =1;5416}5417}54185419foreachmy$m(sort{$o{$a->{chg}} <=>$o{$b->{chg}} }@$mods) {5420my$f=$m->{chg};5421if(defined$o{$f}) {5422$self->$f($m, \%deletions);5423}else{5424 fatal("Invalid change type:$f");5425}5426}54275428if(defined($self->{mergeinfo})) {5429$self->change_dir_prop($self->{bat}{''},"svn:mergeinfo",5430$self->{mergeinfo});5431}5432$self->rmdirsif$_rmdir;5433if(@$mods==0&& !defined($self->{mergeinfo})) {5434$self->abort_edit;5435}else{5436$self->close_edit;5437}5438returnscalar@$mods;5439}54405441package Git::SVN::Ra;5442use vars qw/@ISA $config_dir $_ignore_refs_regex $_log_window_size/;5443use strict;5444use warnings;5445my($ra_invalid,$can_do_switch,%ignored_err,$RA);54465447BEGIN{5448# enforce temporary pool usage for some simple functions5449no strict 'refs';5450formy$f(qw/rev_proplist get_latest_revnum get_uuid get_repos_root5451 get_file/) {5452my$SUPER="SUPER::$f";5453*$f=sub{5454my$self=shift;5455my$pool= SVN::Pool->new;5456my@ret=$self->$SUPER(@_,$pool);5457$pool->clear;5458wantarray?@ret:$ret[0];5459};5460}5461}54625463sub _auth_providers () {5464my@rv= (5465 SVN::Client::get_simple_provider(),5466 SVN::Client::get_ssl_server_trust_file_provider(),5467 SVN::Client::get_simple_prompt_provider(5468 \&Git::SVN::Prompt::simple,2),5469 SVN::Client::get_ssl_client_cert_file_provider(),5470 SVN::Client::get_ssl_client_cert_prompt_provider(5471 \&Git::SVN::Prompt::ssl_client_cert,2),5472 SVN::Client::get_ssl_client_cert_pw_file_provider(),5473 SVN::Client::get_ssl_client_cert_pw_prompt_provider(5474 \&Git::SVN::Prompt::ssl_client_cert_pw,2),5475 SVN::Client::get_username_provider(),5476 SVN::Client::get_ssl_server_trust_prompt_provider(5477 \&Git::SVN::Prompt::ssl_server_trust),5478 SVN::Client::get_username_prompt_provider(5479 \&Git::SVN::Prompt::username,2)5480);54815482# earlier 1.6.x versions would segfault, and <= 1.5.x didn't have5483# this function5484if(::compare_svn_version('1.6.12') >0) {5485my$config= SVN::Core::config_get_config($config_dir);5486my($p,@a);5487# config_get_config returns all config files from5488# ~/.subversion, auth_get_platform_specific_client_providers5489# just wants the config "file".5490@a= ($config->{'config'},undef);5491$p= SVN::Core::auth_get_platform_specific_client_providers(@a);5492# Insert the return value from5493# auth_get_platform_specific_providers5494unshift@rv,@$p;5495}5496 \@rv;5497}54985499sub escape_uri_only {5500my($uri) =@_;5501my@tmp;5502foreach(splitm{/},$uri) {5503s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;5504push@tmp,$_;5505}5506join('/',@tmp);5507}55085509sub escape_url {5510my($url) =@_;5511if($url=~ m#^(https?)://([^/]+)(.*)$#) {5512my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));5513$url="$scheme://$domain$uri";5514}5515$url;5516}55175518sub new {5519my($class,$url) =@_;5520$url=~s!/+$!!;5521return$RAif($RA&&$RA->{url}eq$url);55225523::_req_svn();55245525 SVN::_Core::svn_config_ensure($config_dir,undef);5526my($baton,$callbacks) = SVN::Core::auth_open_helper(_auth_providers);5527my$config= SVN::Core::config_get_config($config_dir);5528$RA=undef;5529my$dont_store_passwords=1;5530my$conf_t= ${$config}{'config'};5531{5532no warnings 'once';5533# The usage of $SVN::_Core::SVN_CONFIG_* variables5534# produces warnings that variables are used only once.5535# I had not found the better way to shut them up, so5536# the warnings of type 'once' are disabled in this block.5537if(SVN::_Core::svn_config_get_bool($conf_t,5538$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5539$SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS,55401) ==0) {5541 SVN::_Core::svn_auth_set_parameter($baton,5542$SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS,5543bless(\$dont_store_passwords,"_p_void"));5544}5545if(SVN::_Core::svn_config_get_bool($conf_t,5546$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5547$SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS,55481) ==0) {5549$Git::SVN::Prompt::_no_auth_cache =1;5550}5551}# no warnings 'once'5552my$self= SVN::Ra->new(url => escape_url($url), auth =>$baton,5553 config =>$config,5554 pool => SVN::Pool->new,5555 auth_provider_callbacks =>$callbacks);5556$self->{url} =$url;5557$self->{svn_path} =$url;5558$self->{repos_root} =$self->get_repos_root;5559$self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;5560$self->{cache} = { check_path => { r =>0, data => {} },5561 get_dir => { r =>0, data => {} } };5562$RA=bless$self,$class;5563}55645565sub check_path {5566my($self,$path,$r) =@_;5567my$cache=$self->{cache}->{check_path};5568if($r==$cache->{r} &&exists$cache->{data}->{$path}) {5569return$cache->{data}->{$path};5570}5571my$pool= SVN::Pool->new;5572my$t=$self->SUPER::check_path($path,$r,$pool);5573$pool->clear;5574if($r!=$cache->{r}) {5575%{$cache->{data}} = ();5576$cache->{r} =$r;5577}5578$cache->{data}->{$path} =$t;5579}55805581sub get_dir {5582my($self,$dir,$r) =@_;5583my$cache=$self->{cache}->{get_dir};5584if($r==$cache->{r}) {5585if(my$x=$cache->{data}->{$dir}) {5586returnwantarray?@$x:$x->[0];5587}5588}5589my$pool= SVN::Pool->new;5590my($d,undef,$props) =$self->SUPER::get_dir($dir,$r,$pool);5591my%dirents=map{$_=> { kind =>$d->{$_}->kind} }keys%$d;5592$pool->clear;5593if($r!=$cache->{r}) {5594%{$cache->{data}} = ();5595$cache->{r} =$r;5596}5597$cache->{data}->{$dir} = [ \%dirents,$r,$props];5598wantarray? (\%dirents,$r,$props) : \%dirents;5599}56005601sub DESTROY {5602# do not call the real DESTROY since we store ourselves in $RA5603}56045605# get_log(paths, start, end, limit,5606# discover_changed_paths, strict_node_history, receiver)5607sub get_log {5608my($self,@args) =@_;5609my$pool= SVN::Pool->new;56105611# svn_log_changed_path_t objects passed to get_log are likely to be5612# overwritten even if only the refs are copied to an external variable,5613# so we should dup the structures in their entirety. Using an5614# externally passed pool (instead of our temporary and quickly cleared5615# pool in Git::SVN::Ra) does not help matters at all...5616my$receiver=pop@args;5617my$prefix="/".$self->{svn_path};5618$prefix=~ s#/+($)##;5619my$prefix_regex= qr#^\Q$prefix\E#;5620push(@args,sub{5621my($paths) =$_[0];5622return&$receiver(@_)unless$paths;5623$_[0] = ();5624foreachmy$p(keys%$paths) {5625my$i=$paths->{$p};5626# Make path relative to our url, not repos_root5627$p=~s/$prefix_regex//;5628my%s=map{$_=>$i->$_; }5629 qw/copyfrom_path copyfrom_rev action/;5630if($s{'copyfrom_path'}) {5631$s{'copyfrom_path'} =~s/$prefix_regex//;5632}5633$_[0]{$p} = \%s;5634}5635&$receiver(@_);5636});563756385639# the limit parameter was not supported in SVN 1.1.x, so we5640# drop it. Therefore, the receiver callback passed to it5641# is made aware of this limitation by being wrapped if5642# the limit passed to is being wrapped.5643if(::compare_svn_version('1.2.0') <=0) {5644my$limit=splice(@args,3,1);5645if($limit>0) {5646my$receiver=pop@args;5647push(@args,sub{ &$receiver(@_)if(--$limit>=0) });5648}5649}5650my$ret=$self->SUPER::get_log(@args,$pool);5651$pool->clear;5652$ret;5653}56545655sub trees_match {5656my($self,$url1,$rev1,$url2,$rev2) =@_;5657my$ctx= SVN::Client->new(auth => _auth_providers);5658my$out= IO::File->new_tmpfile;56595660# older SVN (1.1.x) doesn't take $pool as the last parameter for5661# $ctx->diff(), so we'll create a default one5662my$pool= SVN::Pool->new_default_sub;56635664$ra_invalid=1;# this will open a new SVN::Ra connection to $url15665$ctx->diff([],$url1,$rev1,$url2,$rev2,1,1,0,$out,$out);5666$out->flush;5667my$ret= (($out->stat)[7] ==0);5668close$outor croak $!;56695670$ret;5671}56725673sub get_commit_editor {5674my($self,$log,$cb,$pool) =@_;56755676my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef,0) : ();5677$self->SUPER::get_commit_editor($log,$cb,@lock,$pool);5678}56795680sub gs_do_update {5681my($self,$rev_a,$rev_b,$gs,$editor) =@_;5682my$new= ($rev_a==$rev_b);5683my$path=$gs->{path};56845685if($new&& -e $gs->{index}) {5686unlink$gs->{index}or die5687"Couldn't unlink index:$gs->{index}:$!\n";5688}5689my$pool= SVN::Pool->new;5690$editor->set_path_strip($path);5691my(@pc) =split m#/#, $path;5692my$reporter=$self->do_update($rev_b, (@pc?shift@pc:''),56931,$editor,$pool);5694my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef) : ();56955696# Since we can't rely on svn_ra_reparent being available, we'll5697# just have to do some magic with set_path to make it so5698# we only want a partial path.5699my$sp='';5700my$final=join('/',@pc);5701while(@pc) {5702$reporter->set_path($sp,$rev_b,0,@lock,$pool);5703$sp.='/'iflength$sp;5704$sp.=shift@pc;5705}5706die"BUG: '$sp' != '$final'\n"if($spne$final);57075708$reporter->set_path($sp,$rev_a,$new,@lock,$pool);57095710$reporter->finish_report($pool);5711$pool->clear;5712$editor->{git_commit_ok};5713}57145715# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and5716# svn_ra_reparent didn't work before 1.4)5717sub gs_do_switch {5718my($self,$rev_a,$rev_b,$gs,$url_b,$editor) =@_;5719my$path=$gs->{path};5720my$pool= SVN::Pool->new;57215722my$full_url=$self->{url};5723my$old_url=$full_url;5724$full_url.='/'.$pathiflength$path;5725my($ra,$reparented);57265727if($old_url=~ m#^svn(\+ssh)?://# ||5728($full_url=~ m#^https?://# &&5729 escape_url($full_url)ne$full_url)) {5730$_[0] =undef;5731$self=undef;5732$RA=undef;5733$ra= Git::SVN::Ra->new($full_url);5734$ra_invalid=1;5735}elsif($old_urlne$full_url) {5736 SVN::_Ra::svn_ra_reparent($self->{session},$full_url,$pool);5737$self->{url} =$full_url;5738$reparented=1;5739}57405741$ra||=$self;5742$url_b= escape_url($url_b);5743my$reporter=$ra->do_switch($rev_b,'',1,$url_b,$editor,$pool);5744my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef) : ();5745$reporter->set_path('',$rev_a,0,@lock,$pool);5746$reporter->finish_report($pool);57475748if($reparented) {5749 SVN::_Ra::svn_ra_reparent($self->{session},$old_url,$pool);5750$self->{url} =$old_url;5751}57525753$pool->clear;5754$editor->{git_commit_ok};5755}57565757sub longest_common_path {5758my($gsv,$globs) =@_;5759my%common;5760my$common_max=scalar@$gsv;57615762foreachmy$gs(@$gsv) {5763my@tmp=split m#/#, $gs->{path};5764my$p='';5765foreach(@tmp) {5766$p.=length($p) ?"/$_":$_;5767$common{$p} ||=0;5768$common{$p}++;5769}5770}5771$globs||= [];5772$common_max+=scalar@$globs;5773foreachmy$glob(@$globs) {5774my@tmp=split m#/#, $glob->{path}->{left};5775my$p='';5776foreach(@tmp) {5777$p.=length($p) ?"/$_":$_;5778$common{$p} ||=0;5779$common{$p}++;5780}5781}57825783my$longest_path='';5784foreach(sort{length$b<=>length$a}keys%common) {5785if($common{$_} ==$common_max) {5786$longest_path=$_;5787last;5788}5789}5790$longest_path;5791}57925793sub gs_fetch_loop_common {5794my($self,$base,$head,$gsv,$globs) =@_;5795return if($base>$head);5796my$inc=$_log_window_size;5797my($min,$max) = ($base,$head<$base+$inc?$head:$base+$inc);5798my$longest_path= longest_common_path($gsv,$globs);5799my$ra_url=$self->{url};5800my$find_trailing_edge;5801while(1) {5802my%revs;5803my$err;5804my$err_handler=$SVN::Error::handler;5805$SVN::Error::handler =sub{5806($err) =@_;5807 skip_unknown_revs($err);5808};5809sub _cb {5810my($paths,$r,$author,$date,$log) =@_;5811[$paths,5812{ author =>$author, date =>$date,log=>$log} ];5813}5814$self->get_log([$longest_path],$min,$max,0,1,1,5815sub{$revs{$_[1]} = _cb(@_) });5816if($err) {5817print"Checked through r$max\r";5818}else{5819$find_trailing_edge=1;5820}5821if($errand$find_trailing_edge) {5822print STDERR "Path '$longest_path' ",5823"was probably deleted:\n",5824$err->expanded_message,5825"\nWill attempt to follow ",5826"revisions r$min.. r$max",5827"committed before the deletion\n";5828my$hi=$max;5829while(--$hi>=$min) {5830my$ok;5831$self->get_log([$longest_path],$min,$hi,58320,1,1,sub{5833$ok=$_[1];5834$revs{$_[1]} = _cb(@_) });5835if($ok) {5836print STDERR "r$min.. r$okOK\n";5837last;5838}5839}5840$find_trailing_edge=0;5841}5842$SVN::Error::handler =$err_handler;58435844my%exists=map{$_->{path} =>$_}@$gsv;5845foreachmy$r(sort{$a<=>$b}keys%revs) {5846my($paths,$logged) = @{$revs{$r}};58475848foreachmy$gs($self->match_globs(\%exists,$paths,5849$globs,$r)) {5850if($gs->rev_map_max>=$r) {5851next;5852}5853next unless$gs->match_paths($paths,$r);5854$gs->{logged_rev_props} =$logged;5855if(my$last_commit=$gs->last_commit) {5856$gs->assert_index_clean($last_commit);5857}5858my$log_entry=$gs->do_fetch($paths,$r);5859if($log_entry) {5860$gs->do_git_commit($log_entry);5861}5862$INDEX_FILES{$gs->{index}} =1;5863}5864foreachmy$g(@$globs) {5865my$k="svn-remote.$g->{remote}.".5866"$g->{t}-maxRev";5867 Git::SVN::tmp_config($k,$r);5868}5869if($ra_invalid) {5870$_[0] =undef;5871$self=undef;5872$RA=undef;5873$self= Git::SVN::Ra->new($ra_url);5874$ra_invalid=undef;5875}5876}5877# pre-fill the .rev_db since it'll eventually get filled in5878# with '0' x40 if something new gets committed5879foreachmy$gs(@$gsv) {5880next if$gs->rev_map_max>=$max;5881next ifdefined$gs->rev_map_get($max);5882$gs->rev_map_set($max,0 x40);5883}5884foreachmy$g(@$globs) {5885my$k="svn-remote.$g->{remote}.$g->{t}-maxRev";5886 Git::SVN::tmp_config($k,$max);5887}5888last if$max>=$head;5889$min=$max+1;5890$max+=$inc;5891$max=$headif($max>$head);5892}5893 Git::SVN::gc();5894}58955896sub get_dir_globbed {5897my($self,$left,$depth,$r) =@_;58985899my@x=eval{$self->get_dir($left,$r) };5900return unlessscalar@x==3;5901my$dirents=$x[0];5902my@finalents;5903foreachmy$de(keys%$dirents) {5904next if$dirents->{$de}->{kind} !=$SVN::Node::dir;5905if($depth>1) {5906my@args= ("$left/$de",$depth-1,$r);5907foreachmy$dir($self->get_dir_globbed(@args)) {5908push@finalents,"$de/$dir";5909}5910}else{5911push@finalents,$de;5912}5913}5914@finalents;5915}59165917# return value: 0 -- don't ignore, 1 -- ignore5918sub is_ref_ignored {5919my($g,$p) =@_;5920my$refname=$g->{ref}->full_path($p);5921return1ifdefined($g->{ignore_refs_regex}) &&5922$refname=~m!$g->{ignore_refs_regex}!;5923return0unlessdefined($_ignore_refs_regex);5924return1if$refname=~m!$_ignore_refs_regex!o;5925return0;5926}59275928sub match_globs {5929my($self,$exists,$paths,$globs,$r) =@_;59305931sub get_dir_check {5932my($self,$exists,$g,$r) =@_;59335934my@dirs=$self->get_dir_globbed($g->{path}->{left},5935$g->{path}->{depth},5936$r);59375938foreachmy$de(@dirs) {5939my$p=$g->{path}->full_path($de);5940next if$exists->{$p};5941next if(length$g->{path}->{right} &&5942($self->check_path($p,$r) !=5943$SVN::Node::dir));5944next unless$p=~/$g->{path}->{regex}/;5945$exists->{$p} = Git::SVN->init($self->{url},$p,undef,5946$g->{ref}->full_path($de),1);5947}5948}5949foreachmy$g(@$globs) {5950if(my$path=$paths->{"/$g->{path}->{left}"}) {5951if($path->{action} =~/^[AR]$/) {5952 get_dir_check($self,$exists,$g,$r);5953}5954}5955foreach(keys%$paths) {5956if(/$g->{path}->{left_regex}/&&5957!/$g->{path}->{regex}/) {5958next if$paths->{$_}->{action} !~/^[AR]$/;5959 get_dir_check($self,$exists,$g,$r);5960}5961next unless/$g->{path}->{regex}/;5962my$p=$1;5963my$pathname=$g->{path}->full_path($p);5964next if is_ref_ignored($g,$p);5965next if$exists->{$pathname};5966next if($self->check_path($pathname,$r) !=5967$SVN::Node::dir);5968$exists->{$pathname} = Git::SVN->init(5969$self->{url},$pathname,undef,5970$g->{ref}->full_path($p),1);5971}5972my$c='';5973foreach(split m#/#, $g->{path}->{left}) {5974$c.="/$_";5975next unless($paths->{$c} &&5976($paths->{$c}->{action} =~/^[AR]$/));5977 get_dir_check($self,$exists,$g,$r);5978}5979}5980values%$exists;5981}59825983sub minimize_url {5984my($self) =@_;5985return$self->{url}if($self->{url}eq$self->{repos_root});5986my$url=$self->{repos_root};5987my@components=split(m!/!,$self->{svn_path});5988my$c='';5989do{5990$url.="/$c"iflength$c;5991eval{5992my$ra= (ref$self)->new($url);5993my$latest=$ra->get_latest_revnum;5994$ra->get_log("",$latest,0,1,0,1,sub{});5995};5996}while($@&& ($c=shift@components));5997$url;5998}59996000sub can_do_switch {6001my$self=shift;6002unless(defined$can_do_switch) {6003my$pool= SVN::Pool->new;6004my$rep=eval{6005$self->do_switch(1,'',0,$self->{url},6006 SVN::Delta::Editor->new,$pool);6007};6008if($@) {6009$can_do_switch=0;6010}else{6011$rep->abort_report($pool);6012$can_do_switch=1;6013}6014$pool->clear;6015}6016$can_do_switch;6017}60186019sub skip_unknown_revs {6020my($err) =@_;6021my$errno=$err->apr_err();6022# Maybe the branch we're tracking didn't6023# exist when the repo started, so it's6024# not an error if it doesn't, just continue6025#6026# Wonderfully consistent library, eh?6027# 160013 - svn:// and file://6028# 175002 - http(s)://6029# 175007 - http(s):// (this repo required authorization, too...)6030# More codes may be discovered later...6031if($errno==175007||$errno==175002||$errno==160013) {6032my$err_key=$err->expanded_message;6033# revision numbers change every time, filter them out6034$err_key=~s/\d+/\0/g;6035$err_key="$errno\0$err_key";6036unless($ignored_err{$err_key}) {6037warn"W: Ignoring error from SVN, path probably ",6038"does not exist: ($errno): ",6039$err->expanded_message,"\n";6040warn"W: Do not be alarmed at the above message ",6041"git-svn is just searching aggressively for ",6042"old history.\n",6043"This may take a while on large repositories\n";6044$ignored_err{$err_key} =1;6045}6046return;6047}6048die"Error from SVN, ($errno): ",$err->expanded_message,"\n";6049}60506051package Git::SVN::Log;6052use strict;6053use warnings;6054use POSIX qw/strftime/;6055useconstant commit_log_separator => ('-' x 72) ."\n";6056use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline6057%rusers $show_commit $incremental/;6058my$l_fmt;60596060sub cmt_showable {6061my($c) =@_;6062return1ifdefined$c->{r};60636064# big commit message got truncated by the 16k pretty buffer in rev-list6065if($c->{l} &&$c->{l}->[-1]eq"...\n"&&6066$c->{a_raw} =~/\@([a-f\d\-]+)>$/) {6067@{$c->{l}} = ();6068my@log= command(qw/cat-file commit/,$c->{c});60696070# shift off the headers6071shift@logwhile($log[0]ne'');6072shift@log;60736074# TODO: make $c->{l} not have a trailing newline in the future6075@{$c->{l}} =map{"$_\n"}grep!/^git-svn-id: /,@log;60766077(undef,$c->{r},undef) = ::extract_metadata(6078(grep(/^git-svn-id: /,@log))[-1]);6079}6080returndefined$c->{r};6081}60826083sub log_use_color {6084return$color|| Git->repository->get_colorbool('color.diff');6085}60866087sub git_svn_log_cmd {6088my($r_min,$r_max,@args) =@_;6089my$head='HEAD';6090my(@files,@log_opts);6091foreachmy$x(@args) {6092if($xeq'--'||@files) {6093push@files,$x;6094}else{6095if(::verify_ref("$x^0")) {6096$head=$x;6097}else{6098push@log_opts,$x;6099}6100}6101}61026103my($url,$rev,$uuid,$gs) = ::working_head_info($head);6104$gs||= Git::SVN->_new;6105my@cmd= (qw/log --abbrev-commit --pretty=raw --default/,6106$gs->refname);6107push@cmd,'-r'unless$non_recursive;6108push@cmd, qw/--raw --name-status/if$verbose;6109push@cmd,'--color'if log_use_color();6110push@cmd,@log_opts;6111if(defined$r_max&&$r_max==$r_min) {6112push@cmd,'--max-count=1';6113if(my$c=$gs->rev_map_get($r_max)) {6114push@cmd,$c;6115}6116}elsif(defined$r_max) {6117if($r_max<$r_min) {6118($r_min,$r_max) = ($r_max,$r_min);6119}6120my(undef,$c_max) =$gs->find_rev_before($r_max,1,$r_min);6121my(undef,$c_min) =$gs->find_rev_after($r_min,1,$r_max);6122# If there are no commits in the range, both $c_max and $c_min6123# will be undefined. If there is at least 1 commit in the6124# range, both will be defined.6125return()if!defined$c_min|| !defined$c_max;6126if($c_mineq$c_max) {6127push@cmd,'--max-count=1',$c_min;6128}else{6129push@cmd,'--boundary',"$c_min..$c_max";6130}6131}6132return(@cmd,@files);6133}61346135# adapted from pager.c6136sub config_pager {6137if(! -t *STDOUT) {6138$ENV{GIT_PAGER_IN_USE} ='false';6139$pager=undef;6140return;6141}6142chomp($pager= command_oneline(qw(var GIT_PAGER)));6143if($pagereq'cat') {6144$pager=undef;6145}6146$ENV{GIT_PAGER_IN_USE} =defined($pager);6147}61486149sub run_pager {6150return unlessdefined$pager;6151pipe my($rfd,$wfd)orreturn;6152defined(my$pid=fork)or::fatal "Can't fork:$!";6153if(!$pid) {6154open STDOUT,'>&',$wfdor6155::fatal "Can't redirect to stdout:$!";6156return;6157}6158open STDIN,'<&',$rfdor::fatal "Can't redirect stdin:$!";6159$ENV{LESS} ||='FRSX';6160exec$pageror::fatal "Can't run pager:$!($pager)";6161}61626163sub format_svn_date {6164my$t=shift||time;6165my$gmoff= Git::SVN::get_tz($t);6166return strftime("%Y-%m-%d%H:%M:%S$gmoff(%a,%d%b%Y)",localtime($t));6167}61686169sub parse_git_date {6170my($t,$tz) =@_;6171# Date::Parse isn't in the standard Perl distro :(6172if($tz=~s/^\+//) {6173$t+= tz_to_s_offset($tz);6174}elsif($tz=~s/^\-//) {6175$t-= tz_to_s_offset($tz);6176}6177return$t;6178}61796180sub set_local_timezone {6181if(defined$TZ) {6182$ENV{TZ} =$TZ;6183}else{6184delete$ENV{TZ};6185}6186}61876188sub tz_to_s_offset {6189my($tz) =@_;6190$tz=~s/(\d\d)$//;6191return($1*60) + ($tz*3600);6192}61936194sub get_author_info {6195my($dest,$author,$t,$tz) =@_;6196$author=~s/(?:^\s*|\s*$)//g;6197$dest->{a_raw} =$author;6198my$au;6199if($::_authors) {6200$au=$rusers{$author} ||undef;6201}6202if(!$au) {6203($au) = ($author=~/<([^>]+)\@[^>]+>$/);6204}6205$dest->{t} =$t;6206$dest->{tz} =$tz;6207$dest->{a} =$au;6208$dest->{t_utc} = parse_git_date($t,$tz);6209}62106211sub process_commit {6212my($c,$r_min,$r_max,$defer) =@_;6213if(defined$r_min&&defined$r_max) {6214if($r_min==$c->{r} &&$r_min==$r_max) {6215 show_commit($c);6216return0;6217}6218return1if$r_min==$r_max;6219if($r_min<$r_max) {6220# we need to reverse the print order6221return0if(defined$limit&& --$limit<0);6222push@$defer,$c;6223return1;6224}6225if($r_min!=$r_max) {6226return1if($r_min<$c->{r});6227return1if($r_max>$c->{r});6228}6229}6230return0if(defined$limit&& --$limit<0);6231 show_commit($c);6232return1;6233}62346235sub show_commit {6236my$c=shift;6237if($oneline) {6238my$x="\n";6239if(my$l=$c->{l}) {6240while($l->[0] =~/^\s*$/) {shift@$l}6241$x=$l->[0];6242}6243$l_fmt||='A'.length($c->{r});6244print'r',pack($l_fmt,$c->{r}),' | ';6245print"$c->{c} | "if$show_commit;6246print$x;6247}else{6248 show_commit_normal($c);6249}6250}62516252sub show_commit_changed_paths {6253my($c) =@_;6254return unless$c->{changed};6255print"Changed paths:\n", @{$c->{changed}};6256}62576258sub show_commit_normal {6259my($c) =@_;6260print commit_log_separator,"r$c->{r} | ";6261print"$c->{c} | "if$show_commit;6262print"$c->{a} | ", format_svn_date($c->{t_utc}),' | ';6263my$nr_line=0;62646265if(my$l=$c->{l}) {6266while($l->[$#$l]eq"\n"&&$#$l>06267&&$l->[($#$l-1)]eq"\n") {6268pop@$l;6269}6270$nr_line=scalar@$l;6271if(!$nr_line) {6272print"1 line\n\n\n";6273}else{6274if($nr_line==1) {6275$nr_line='1 line';6276}else{6277$nr_line.=' lines';6278}6279print$nr_line,"\n";6280 show_commit_changed_paths($c);6281print"\n";6282print$_foreach@$l;6283}6284}else{6285print"1 line\n";6286 show_commit_changed_paths($c);6287print"\n";62886289}6290foreachmy$x(qw/raw stat diff/) {6291if($c->{$x}) {6292print"\n";6293print$_foreach@{$c->{$x}}6294}6295}6296}62976298sub cmd_show_log {6299my(@args) =@_;6300my($r_min,$r_max);6301my$r_last= -1;# prevent dupes6302 set_local_timezone();6303if(defined$::_revision) {6304if($::_revision =~/^(\d+):(\d+)$/) {6305($r_min,$r_max) = ($1,$2);6306}elsif($::_revision =~/^\d+$/) {6307$r_min=$r_max= $::_revision;6308}else{6309::fatal "-r$::_revision is not supported, use ",6310"standard 'git log' arguments instead";6311}6312}63136314 config_pager();6315@args= git_svn_log_cmd($r_min,$r_max,@args);6316if(!@args) {6317print commit_log_separator unless$incremental||$oneline;6318return;6319}6320my$log= command_output_pipe(@args);6321 run_pager();6322my(@k,$c,$d,$stat);6323my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;6324while(<$log>) {6325if(/^${esc_color}commit (?:- )?($::sha1_short)/o) {6326my$cmt=$1;6327if($c&& cmt_showable($c) &&$c->{r} !=$r_last) {6328$r_last=$c->{r};6329 process_commit($c,$r_min,$r_max, \@k)or6330goto out;6331}6332$d=undef;6333$c= { c =>$cmt};6334}elsif(/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {6335 get_author_info($c,$1,$2,$3);6336}elsif(/^${esc_color}(?:tree|parent|committer) /o) {6337# ignore6338}elsif(/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {6339push@{$c->{raw}},$_;6340}elsif(/^${esc_color}[ACRMDT]\t/) {6341# we could add $SVN->{svn_path} here, but that requires6342# remote access at the moment (repo_path_split)...6343 s#^(${esc_color})([ACRMDT])\t#$1 $2 #o;6344push@{$c->{changed}},$_;6345}elsif(/^${esc_color}diff /o) {6346$d=1;6347push@{$c->{diff}},$_;6348}elsif($d) {6349push@{$c->{diff}},$_;6350}elsif(/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*6351$esc_color*[\+\-]*$esc_color$/x) {6352$stat=1;6353push@{$c->{stat}},$_;6354}elsif($stat&&/^ \d+ files changed, \d+ insertions/) {6355push@{$c->{stat}},$_;6356$stat=undef;6357}elsif(/^${esc_color} (git-svn-id:.+)$/o) {6358($c->{url},$c->{r},undef) = ::extract_metadata($1);6359}elsif(s/^${esc_color} //o) {6360push@{$c->{l}},$_;6361}6362}6363if($c&&defined$c->{r} &&$c->{r} !=$r_last) {6364$r_last=$c->{r};6365 process_commit($c,$r_min,$r_max, \@k);6366}6367if(@k) {6368($r_min,$r_max) = ($r_max,$r_min);6369 process_commit($_,$r_min,$r_max)foreachreverse@k;6370}6371out:6372close$log;6373print commit_log_separator unless$incremental||$oneline;6374}63756376sub cmd_blame {6377my$path=pop;63786379 config_pager();6380 run_pager();63816382my($fh,$ctx,$rev);63836384if($_git_format) {6385($fh,$ctx) = command_output_pipe('blame',@_,$path);6386while(my$line= <$fh>) {6387if($line=~/^\^?([[:xdigit:]]+)\s/) {6388# Uncommitted edits show up as a rev ID of6389# all zeros, which we can't look up with6390# cmt_metadata6391if($1!~/^0+$/) {6392(undef,$rev,undef) =6393::cmt_metadata($1);6394$rev='0'if(!$rev);6395}else{6396$rev='0';6397}6398$rev=sprintf('%-10s',$rev);6399$line=~s/^\^?[[:xdigit:]]+(\s)/$rev$1/;6400}6401print$line;6402}6403}else{6404($fh,$ctx) = command_output_pipe('blame','-p',@_,'HEAD',6405'--',$path);6406my($sha1);6407my%authors;6408my@buffer;6409my%dsha;#distinct sha keys64106411while(my$line= <$fh>) {6412push@buffer,$line;6413if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6414$dsha{$1} =1;6415}6416}64176418my$s2r= ::cmt_sha2rev_batch([keys%dsha]);64196420foreachmy$line(@buffer) {6421if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6422$rev=$s2r->{$1};6423$rev='0'if(!$rev)6424}6425elsif($line=~/^author (.*)/) {6426$authors{$rev} =$1;6427$authors{$rev} =~s/\s/_/g;6428}6429elsif($line=~/^\t(.*)$/) {6430printf("%6s%10s%s\n",$rev,$authors{$rev},$1);6431}6432}6433}6434 command_close_pipe($fh,$ctx);6435}64366437package Git::SVN::Migration;6438# these version numbers do NOT correspond to actual version numbers6439# of git nor git-svn. They are just relative.6440#6441# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD6442#6443# v1 layout: .git/$id/info/url, refs/remotes/$id6444#6445# v2 layout: .git/svn/$id/info/url, refs/remotes/$id6446#6447# v3 layout: .git/svn/$id, refs/remotes/$id6448# - info/url may remain for backwards compatibility6449# - this is what we migrate up to this layout automatically,6450# - this will be used by git svn init on single branches6451# v3.1 layout (auto migrated):6452# - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink6453# for backwards compatibility6454#6455# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id6456# - this is only created for newly multi-init-ed6457# repositories. Similar in spirit to the6458# --use-separate-remotes option in git-clone (now default)6459# - we do not automatically migrate to this (following6460# the example set by core git)6461#6462# v5 layout: .rev_db.$UUID => .rev_map.$UUID6463# - newer, more-efficient format that uses 24-bytes per record6464# with no filler space.6465# - use xxd -c24 < .rev_map.$UUID to view and debug6466# - This is a one-way migration, repositories updated to the6467# new format will not be able to use old git-svn without6468# rebuilding the .rev_db. Rebuilding the rev_db is not6469# possible if noMetadata or useSvmProps are set; but should6470# be no problem for users that use the (sensible) defaults.6471use strict;6472use warnings;6473use Carp qw/croak/;6474use File::Path qw/mkpath/;6475use File::Basename qw/dirname basename/;6476use vars qw/$_minimize/;64776478sub migrate_from_v0 {6479my$git_dir=$ENV{GIT_DIR};6480returnundefunless-d $git_dir;6481my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6482my$migrated=0;6483while(<$fh>) {6484chomp;6485my($id,$orig_ref) = ($_,$_);6486next unless$id=~ s#^refs/heads/(.+)-HEAD$#$1#;6487next unless-f "$git_dir/$id/info/url";6488my$new_ref="refs/remotes/$id";6489if(::verify_ref("$new_ref^0")) {6490print STDERR "W:$orig_refis probably an old ",6491"branch used by an ancient version of ",6492"git-svn.\n",6493"However,$new_refalso exists.\n",6494"We will not be able ",6495"to use this branch until this ",6496"ambiguity is resolved.\n";6497next;6498}6499print STDERR "Migrating from v0 layout...\n"if!$migrated;6500print STDERR "Renaming ref:$orig_ref=>$new_ref\n";6501 command_noisy('update-ref',$new_ref,$orig_ref);6502 command_noisy('update-ref','-d',$orig_ref,$orig_ref);6503$migrated++;6504}6505 command_close_pipe($fh,$ctx);6506print STDERR "Done migrating from v0 layout...\n"if$migrated;6507$migrated;6508}65096510sub migrate_from_v1 {6511my$git_dir=$ENV{GIT_DIR};6512my$migrated=0;6513return$migratedunless-d $git_dir;6514my$svn_dir="$git_dir/svn";65156516# just in case somebody used 'svn' as their $id at some point...6517return$migratedif-d $svn_dir&& ! -f "$svn_dir/info/url";65186519print STDERR "Migrating from a git-svn v1 layout...\n";6520 mkpath([$svn_dir]);6521print STDERR "Data from a previous version of git-svn exists, but\n\t",6522"$svn_dir\n\t(required for this version ",6523"($::VERSION) of git-svn) does not exist.\n";6524my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6525while(<$fh>) {6526my$x=$_;6527next unless$x=~ s#^refs/remotes/##;6528chomp$x;6529next unless-f "$git_dir/$x/info/url";6530my$u=eval{ ::file_to_s("$git_dir/$x/info/url") };6531next unless$u;6532my$dn= dirname("$git_dir/svn/$x");6533 mkpath([$dn])unless-d $dn;6534if($xeq'svn') {# they used 'svn' as GIT_SVN_ID:6535 mkpath(["$git_dir/svn/svn"]);6536print STDERR " -$git_dir/$x/info=> ",6537"$git_dir/svn/$x/info\n";6538rename"$git_dir/$x/info","$git_dir/svn/$x/info"or6539 croak "$!:$x";6540# don't worry too much about these, they probably6541# don't exist with repos this old (save for index,6542# and we can easily regenerate that)6543foreachmy$f(qw/unhandled.log index .rev_db/) {6544rename"$git_dir/$x/$f","$git_dir/svn/$x/$f";6545}6546}else{6547print STDERR " -$git_dir/$x=>$git_dir/svn/$x\n";6548rename"$git_dir/$x","$git_dir/svn/$x"or6549 croak "$!:$x";6550}6551$migrated++;6552}6553 command_close_pipe($fh,$ctx);6554print STDERR "Done migrating from a git-svn v1 layout\n";6555$migrated;6556}65576558sub read_old_urls {6559my($l_map,$pfx,$path) =@_;6560my@dir;6561foreach(<$path/*>) {6562if(-r "$_/info/url") {6563$pfx.='/'if$pfx&&$pfx!~ m!/$!;6564my$ref_id=$pfx. basename $_;6565my$url= ::file_to_s("$_/info/url");6566$l_map->{$ref_id} =$url;6567}elsif(-d $_) {6568push@dir,$_;6569}6570}6571foreach(@dir) {6572my$x=$_;6573$x=~s!^\Q$ENV{GIT_DIR}\E/svn/!!o;6574 read_old_urls($l_map,$x,$_);6575}6576}65776578sub migrate_from_v2 {6579my@cfg= command(qw/config -l/);6580return ifgrep/^svn-remote\..+\.url=/,@cfg;6581my%l_map;6582 read_old_urls(\%l_map,'',"$ENV{GIT_DIR}/svn");6583my$migrated=0;65846585foreachmy$ref_id(sort keys%l_map) {6586eval{ Git::SVN->init($l_map{$ref_id},'',undef,$ref_id) };6587if($@) {6588 Git::SVN->init($l_map{$ref_id},'',$ref_id,$ref_id);6589}6590$migrated++;6591}6592$migrated;6593}65946595sub minimize_connections {6596my$r= Git::SVN::read_all_remotes();6597my$new_urls= {};6598my$root_repos= {};6599foreachmy$repo_id(keys%$r) {6600my$url=$r->{$repo_id}->{url}ornext;6601my$fetch=$r->{$repo_id}->{fetch}ornext;6602my$ra= Git::SVN::Ra->new($url);66036604# skip existing cases where we already connect to the root6605if(($ra->{url}eq$ra->{repos_root}) ||6606($ra->{repos_root}eq$repo_id)) {6607$root_repos->{$ra->{url}} =$repo_id;6608next;6609}66106611my$root_ra= Git::SVN::Ra->new($ra->{repos_root});6612my$root_path=$ra->{url};6613$root_path=~ s#^\Q$ra->{repos_root}\E(/|$)##;6614foreachmy$path(keys%$fetch) {6615my$ref_id=$fetch->{$path};6616my$gs= Git::SVN->new($ref_id,$repo_id,$path);66176618# make sure we can read when connecting to6619# a higher level of a repository6620my($last_rev,undef) =$gs->last_rev_commit;6621if(!defined$last_rev) {6622$last_rev=eval{6623$root_ra->get_latest_revnum;6624};6625next if$@;6626}6627my$new=$root_path;6628$new.=length$path?"/$path":'';6629eval{6630$root_ra->get_log([$new],$last_rev,$last_rev,66310,0,1,sub{ });6632};6633next if$@;6634$new_urls->{$ra->{repos_root}}->{$new} =6635{ ref_id =>$ref_id,6636 old_repo_id =>$repo_id,6637 old_path =>$path};6638}6639}66406641my@emptied;6642foreachmy$url(keys%$new_urls) {6643# see if we can re-use an existing [svn-remote "repo_id"]6644# instead of creating a(n ugly) new section:6645my$repo_id=$root_repos->{$url} ||$url;66466647my$fetch=$new_urls->{$url};6648foreachmy$path(keys%$fetch) {6649my$x=$fetch->{$path};6650 Git::SVN->init($url,$path,$repo_id,$x->{ref_id});6651my$pfx="svn-remote.$x->{old_repo_id}";66526653my$old_fetch=quotemeta("$x->{old_path}:".6654"$x->{ref_id}");6655 command_noisy(qw/config --unset/,6656"$pfx.fetch",'^'.$old_fetch.'$');6657delete$r->{$x->{old_repo_id}}->6658{fetch}->{$x->{old_path}};6659if(!keys%{$r->{$x->{old_repo_id}}->{fetch}}) {6660 command_noisy(qw/config --unset/,6661"$pfx.url");6662push@emptied,$x->{old_repo_id}6663}6664}6665}6666if(@emptied) {6667my$file=$ENV{GIT_CONFIG} ||"$ENV{GIT_DIR}/config";6668print STDERR <<EOF;6669The following [svn-remote] sections in your config file ($file) are empty6670and can be safely removed:6671EOF6672print STDERR "[svn-remote\"$_\"]\n"foreach@emptied;6673}6674}66756676sub migration_check {6677 migrate_from_v0();6678 migrate_from_v1();6679 migrate_from_v2();6680 minimize_connections()if$_minimize;6681}66826683package Git::IndexInfo;6684use strict;6685use warnings;6686use Git qw/command_input_pipe command_close_pipe/;66876688sub new {6689my($class) =@_;6690my($gui,$ctx) = command_input_pipe(qw/update-index -z --index-info/);6691bless{ gui =>$gui, ctx =>$ctx, nr =>0},$class;6692}66936694sub remove {6695my($self,$path) =@_;6696if(print{$self->{gui} }'0 ',0 x 40,"\t",$path,"\0") {6697return++$self->{nr};6698}6699undef;6700}67016702sub update {6703my($self,$mode,$hash,$path) =@_;6704if(print{$self->{gui} }$mode,' ',$hash,"\t",$path,"\0") {6705return++$self->{nr};6706}6707undef;6708}67096710sub DESTROY {6711my($self) =@_;6712 command_close_pipe($self->{gui},$self->{ctx});6713}67146715package Git::SVN::GlobSpec;6716use strict;6717use warnings;67186719sub new {6720my($class,$glob,$pattern_ok) =@_;6721my$re=$glob;6722$re=~s!/+$!!g;# no need for trailing slashes6723my(@left,@right,@patterns);6724my$state="left";6725my$die_msg="Only one set of wildcard directories ".6726"(e.g. '*' or '*/*/*') is supported: '$glob'\n";6727formy$part(split(m|/|,$glob)) {6728if($part=~/\*/&&$partne"*") {6729die"Invalid pattern in '$glob':$part\n";6730}elsif($pattern_ok&&$part=~/[{}]/&&6731$part!~/^\{[^{}]+\}/) {6732die"Invalid pattern in '$glob':$part\n";6733}6734if($parteq"*") {6735die$die_msgif$stateeq"right";6736$state="pattern";6737push(@patterns,"[^/]*");6738}elsif($pattern_ok&&$part=~/^\{(.*)\}$/) {6739die$die_msgif$stateeq"right";6740$state="pattern";6741my$p=quotemeta($1);6742$p=~s/\\,/|/g;6743push(@patterns,"(?:$p)");6744}else{6745if($stateeq"left") {6746push(@left,$part);6747}else{6748push(@right,$part);6749$state="right";6750}6751}6752}6753my$depth=@patterns;6754if($depth==0) {6755die"One '*' is needed in glob: '$glob'\n";6756}6757my$left=join('/',@left);6758my$right=join('/',@right);6759$re=join('/',@patterns);6760$re=join('\/',6761grep(length,quotemeta($left),"($re)",quotemeta($right)));6762my$left_re=qr/^\/\Q$left\E(\/|$)/;6763bless{ left =>$left, right =>$right, left_regex =>$left_re,6764 regex =>qr/$re/,glob=>$glob, depth =>$depth},$class;6765}67666767sub full_path {6768my($self,$path) =@_;6769return(length$self->{left} ?"$self->{left}/":'') .6770$path. (length$self->{right} ?"/$self->{right}":'');6771}67726773__END__67746775Data structures:677667776778$remotes= {# returned by read_all_remotes()6779'svn'=> {6780# svn-remote.svn.url=https://svn.musicpd.org6781 url =>'https://svn.musicpd.org',6782# svn-remote.svn.fetch=mpd/trunk:trunk6783 fetch => {6784'mpd/trunk'=>'trunk',6785},6786# svn-remote.svn.tags=mpd/tags/*:tags/*6787 tags => {6788 path => {6789 left =>'mpd/tags',6790 right =>'',6791 regex =>qr!mpd/tags/([^/]+)$!,6792glob=>'tags/*',6793},6794ref=> {6795 left =>'tags',6796 right =>'',6797 regex =>qr!tags/([^/]+)$!,6798glob=>'tags/*',6799},6800}6801}6802};68036804$log_entry hashref as returned by libsvn_log_entry()6805{6806log=>'whitespace-formatted log entry6807',# trailing newline is preserved6808 revision =>'8',# integer6809 date =>'2004-02-24T17:01:44.108345Z',# commit date6810 author =>'committer name'6811};681268136814# this is generated by generate_diff();6815@mods= array of diff-index line hashes,each element represents one line6816 of diff-index output68176818diff-index line ($m hash)6819{6820 mode_a => first column of diff-index output,no leading ':',6821 mode_b => second column of diff-index output,6822 sha1_b => sha1sum of the final blob,6823 chg => change type [MCRADT],6824 file_a => original file name of a file (iff chg is'C'or'R')6825 file_b => new/current file name of a file (any chg)6826}6827;68286829# retval of read_url_paths{,_all}();6830$l_map= {6831# repository root url6832'https://svn.musicpd.org'=> {6833# repository path # GIT_SVN_ID6834'mpd/trunk'=>'trunk',6835'mpd/tags/0.11.5'=>'tags/0.11.5',6836},6837}68386839Notes:6840 I don't trust the each() function on unless I created%hashmyself6841 because the internal iterator may not have started at base.