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,$_preserve_merges,$_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'preserve-merges|p'=> \$_preserve_merges, 259%fc_opts} ], 260'commit-diff'=> [ \&cmd_commit_diff, 261'Commit a diff between two trees', 262{'message|m=s'=> \$_message, 263'file|F=s'=> \$_file, 264'revision|r=s'=> \$_revision, 265%cmt_opts} ], 266'info'=> [ \&cmd_info, 267"Show info about the latest SVN revision 268 on the current branch", 269{'url'=> \$_url, } ], 270'blame'=> [ \&Git::SVN::Log::cmd_blame, 271"Show what revision and author last modified each line of a file", 272{'git-format'=> \$_git_format} ], 273'reset'=> [ \&cmd_reset, 274"Undo fetches back to the specified SVN revision", 275{'revision|r=s'=> \$_revision, 276'parent|p'=> \$_fetch_parent} ], 277'gc'=> [ \&cmd_gc, 278"Compress unhandled.log files in .git/svn and remove ". 279"index files in .git/svn", 280{} ], 281); 282 283use Term::ReadLine; 284package FakeTerm; 285sub new { 286my($class,$reason) =@_; 287returnbless \$reason,shift; 288} 289subreadline{ 290my$self=shift; 291die"Cannot use readline on FakeTerm:$$self"; 292} 293package main; 294 295my$term=eval{ 296$ENV{"GIT_SVN_NOTTY"} 297? new Term::ReadLine 'git-svn', \*STDIN, \*STDOUT 298: new Term::ReadLine 'git-svn'; 299}; 300if($@) { 301$term= new FakeTerm "$@: going non-interactive"; 302} 303 304my$cmd; 305for(my$i=0;$i<@ARGV;$i++) { 306if(defined$cmd{$ARGV[$i]}) { 307$cmd=$ARGV[$i]; 308splice@ARGV,$i,1; 309last; 310}elsif($ARGV[$i]eq'help') { 311$cmd=$ARGV[$i+1]; 312 usage(0); 313} 314}; 315 316# make sure we're always running at the top-level working directory 317unless($cmd&&$cmd=~/(?:clone|init|multi-init)$/) { 318unless(-d $ENV{GIT_DIR}) { 319if($git_dir_user_set) { 320die"GIT_DIR=$ENV{GIT_DIR} explicitly set, ", 321"but it is not a directory\n"; 322} 323my$git_dir=delete$ENV{GIT_DIR}; 324my$cdup=undef; 325 git_cmd_try { 326$cdup= command_oneline(qw/rev-parse --show-cdup/); 327$git_dir='.'unless($cdup); 328chomp$cdupif($cdup); 329$cdup="."unless($cdup&&length$cdup); 330}"Already at toplevel, but$git_dirnot found\n"; 331chdir$cdupor die"Unable to chdir up to '$cdup'\n"; 332unless(-d $git_dir) { 333die"$git_dirstill not found after going to ", 334"'$cdup'\n"; 335} 336$ENV{GIT_DIR} =$git_dir; 337} 338$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 339} 340 341my%opts= %{$cmd{$cmd}->[2]}if(defined$cmd); 342 343read_git_config(\%opts); 344if($cmd&& ($cmdeq'log'||$cmdeq'blame')) { 345 Getopt::Long::Configure('pass_through'); 346} 347my$rv= GetOptions(%opts,'h|H'=> \$_help,'version|V'=> \$_version, 348'minimize-connections'=> \$Git::SVN::Migration::_minimize, 349'id|i=s'=> \$Git::SVN::default_ref_id, 350'svn-remote|remote|R=s'=>sub{ 351$Git::SVN::no_reuse_existing =1; 352$Git::SVN::default_repo_id =$_[1] }); 353exit1if(!$rv&&$cmd&&$cmdne'log'); 354 355usage(0)if$_help; 356version()if$_version; 357usage(1)unlessdefined$cmd; 358load_authors()if$_authors; 359if(defined$_authors_prog) { 360$_authors_prog="'". File::Spec->rel2abs($_authors_prog) ."'"; 361} 362 363unless($cmd=~/^(?:clone|init|multi-init|commit-diff)$/) { 364 Git::SVN::Migration::migration_check(); 365} 366Git::SVN::init_vars(); 367eval{ 368 Git::SVN::verify_remotes_sanity(); 369$cmd{$cmd}->[0]->(@ARGV); 370}; 371fatal $@if$@; 372post_fetch_checkout(); 373exit0; 374 375####################### primary functions ###################### 376sub usage { 377my$exit=shift||0; 378my$fd=$exit? \*STDERR : \*STDOUT; 379print$fd<<""; 380git-svn - bidirectional operations between a single Subversion tree and git 381Usage: git svn <command> [options] [arguments]\n 382 383print$fd"Available commands:\n"unless$cmd; 384 385foreach(sort keys%cmd) { 386next if$cmd&&$cmdne$_; 387next if/^multi-/;# don't show deprecated commands 388print$fd' ',pack('A17',$_),$cmd{$_}->[1],"\n"; 389foreach(sort keys%{$cmd{$_}->[2]}) { 390# mixed-case options are for .git/config only 391next if/[A-Z]/&&/^[a-z]+$/i; 392# prints out arguments as they should be passed: 393my$x= s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : ''; 394print$fd' ' x 21,join(', ',map{length$_>1? 395"--$_":"-$_"} 396split/\|/,$_),"$x\n"; 397} 398} 399print$fd<<""; 400\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an 401arbitrary identifier if you're tracking multiple SVN branches/repositories in 402one git repository and want to keep them separate. See git-svn(1) for more 403information. 404 405 exit$exit; 406} 407 408sub version { 409 ::_req_svn(); 410 print "git-svn version$VERSION(svn$SVN::Core::VERSION)\n"; 411 exit 0; 412} 413 414sub ask { 415 my ($prompt,%arg) =@_; 416 my$valid_re=$arg{valid_re}; 417 my$default=$arg{default}; 418 my$resp; 419 my$i= 0; 420 421 if ( !( defined($term->IN) 422 && defined( fileno($term->IN) ) 423 && defined($term->OUT ) 424 && defined( fileno($term->OUT) ) ) ){ 425 return defined($default) ?$default: undef; 426 } 427 428 while ($i++< 10) { 429$resp=$term->readline($prompt); 430 if (!defined$resp) { # EOF 431 print "\n"; 432 return defined$default?$default: undef; 433 } 434 if ($respeq '' and defined$default) { 435 return$default; 436 } 437 if (!defined$valid_reor$resp=~ /$valid_re/) { 438 return$resp; 439 } 440 } 441 return undef; 442} 443 444sub do_git_init_db { 445 unless (-d$ENV{GIT_DIR}) { 446 my@init_db= ('init'); 447 push@init_db, "--template=$_template" if defined$_template; 448 if (defined$_shared) { 449 if ($_shared=~ /[a-z]/) { 450 push@init_db, "--shared=$_shared"; 451 } else { 452 push@init_db, "--shared"; 453 } 454 } 455 command_noisy(@init_db); 456$_repository= Git->repository(Repository => ".git"); 457 } 458 my$set; 459 my$pfx= "svn-remote.$Git::SVN::default_repo_id"; 460 foreach my$i(keys%icv) { 461 die "'$set' and '$i' cannot both be set\n" if$set; 462 next unless defined$icv{$i}; 463 command_noisy('config', "$pfx.$i",$icv{$i}); 464$set=$i; 465 } 466 my$ignore_paths_regex= \$SVN::Git::Fetcher::_ignore_regex; 467 command_noisy('config', "$pfx.ignore-paths",$$ignore_paths_regex) 468 if defined$$ignore_paths_regex; 469 my$ignore_refs_regex= \$Git::SVN::Ra::_ignore_refs_regex; 470 command_noisy('config', "$pfx.ignore-refs",$$ignore_refs_regex) 471 if defined$$ignore_refs_regex; 472 473 if (defined$SVN::Git::Fetcher::_preserve_empty_dirs) { 474 my$fname= \$SVN::Git::Fetcher::_placeholder_filename; 475 command_noisy('config', "$pfx.preserve-empty-dirs", 'true'); 476 command_noisy('config', "$pfx.placeholder-filename",$$fname); 477 } 478} 479 480sub init_subdir { 481 my$repo_path= shift or return; 482 mkpath([$repo_path]) unless -d$repo_path; 483 chdir$repo_pathor die "Couldn't chdir to $repo_path:$!\n"; 484$ENV{GIT_DIR} = '.git'; 485$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 486} 487 488sub cmd_clone { 489 my ($url,$path) =@_; 490 if (!defined$path&& 491 (defined$_trunk||@_branches||@_tags|| 492 defined$_stdlayout) && 493$url!~ m#^[a-z\+]+://#) { 494$path=$url; 495 } 496$path= basename($url) if !defined$path|| !length$path; 497 my$authors_absolute=$_authors? File::Spec->rel2abs($_authors) : ""; 498 cmd_init($url,$path); 499 command_oneline('config', 'svn.authorsfile',$authors_absolute) 500 if$_authors; 501 Git::SVN::fetch_all($Git::SVN::default_repo_id); 502} 503 504sub cmd_init { 505 if (defined$_stdlayout) { 506$_trunk= 'trunk' if (!defined$_trunk); 507@_tags= 'tags' if (!@_tags); 508@_branches= 'branches' if (!@_branches); 509 } 510 if (defined$_trunk||@_branches||@_tags) { 511 return cmd_multi_init(@_); 512 } 513 my$url= shift or die "SVN repository location required ", 514 "as a command-line argument\n"; 515$url= canonicalize_url($url); 516 init_subdir(@_); 517 do_git_init_db(); 518 519 if ($Git::SVN::_minimize_url eq 'unset') { 520$Git::SVN::_minimize_url = 0; 521 } 522 523 Git::SVN->init($url); 524} 525 526sub cmd_fetch { 527 if (grep /^\d+=./,@_) { 528 die "'<rev>=<commit>' fetch arguments are ", 529 "no longer supported.\n"; 530 } 531 my ($remote) =@_; 532 if (@_> 1) { 533 die "Usage:$0 fetch [--all] [--parent] [svn-remote]\n"; 534 } 535$Git::SVN::no_reuse_existing = undef; 536 if ($_fetch_parent) { 537 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD'); 538 unless ($gs) { 539 die "Unable to determine upstream SVN information from ", 540 "working tree history\n"; 541 } 542 # just fetch, don't checkout. 543$_no_checkout= 'true'; 544$_fetch_all?$gs->fetch_all :$gs->fetch; 545 } elsif ($_fetch_all) { 546 cmd_multi_fetch(); 547 } else { 548$remote||=$Git::SVN::default_repo_id; 549 Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes()); 550 } 551} 552 553sub cmd_set_tree { 554 my (@commits) =@_; 555 if ($_stdin|| !@commits) { 556 print "Reading from stdin...\n"; 557@commits= (); 558 while (<STDIN>) { 559 if (/\b($sha1_short)\b/o) { 560 unshift@commits,$1; 561 } 562 } 563 } 564 my@revs; 565 foreach my$c(@commits) { 566 my@tmp= command('rev-parse',$c); 567 if (scalar@tmp== 1) { 568 push@revs,$tmp[0]; 569 } elsif (scalar@tmp> 1) { 570 push@revs, reverse(command('rev-list',@tmp)); 571 } else { 572 fatal "Failed to rev-parse $c"; 573 } 574 } 575 my$gs= Git::SVN->new; 576 my ($r_last,$cmt_last) =$gs->last_rev_commit; 577$gs->fetch; 578 if (defined$gs->{last_rev} &&$r_last!=$gs->{last_rev}) { 579 fatal "There are new revisions that were fetched ", 580 "and need to be merged (or acknowledged)", 581 "before committing.\nlast rev:$r_last\n", 582 " current:$gs->{last_rev}"; 583 } 584$gs->set_tree($_) foreach@revs; 585 print "Done committing ",scalar@revs," revisions to SVN\n"; 586 unlink$gs->{index}; 587} 588 589sub split_merge_info_range { 590 my ($range) =@_; 591 if ($range=~ /(\d+)-(\d+)/) { 592 return (int($1), int($2)); 593 } else { 594 return (int($range), int($range)); 595 } 596} 597 598sub combine_ranges { 599 my ($in) =@_; 600 601 my@fnums= (); 602 my@arr= split(/,/,$in); 603 for my$element(@arr) { 604 my ($start,$end) = split_merge_info_range($element); 605 push@fnums,$start; 606 } 607 608 my@sorted=@arr[ sort { 609$fnums[$a] <=>$fnums[$b] 610 } 0..$#arr]; 611 612 my@return= (); 613 my$last= -1; 614 my$first= -1; 615 for my$element(@sorted) { 616 my ($start,$end) = split_merge_info_range($element); 617 618 if ($last== -1) { 619$first=$start; 620$last=$end; 621 next; 622 } 623 if ($start<=$last+1) { 624 if ($end>$last) { 625$last=$end; 626 } 627 next; 628 } 629 if ($first==$last) { 630 push@return, "$first"; 631 } else { 632 push@return, "$first-$last"; 633 } 634$first=$start; 635$last=$end; 636 } 637 638 if ($first!= -1) { 639 if ($first==$last) { 640 push@return, "$first"; 641 } else { 642 push@return, "$first-$last"; 643 } 644 } 645 646 return join(',',@return); 647} 648 649sub merge_revs_into_hash { 650 my ($hash,$minfo) =@_; 651 my@lines= split(' ',$minfo); 652 653 for my$line(@lines) { 654 my ($branchpath,$revs) = split(/:/,$line); 655 656 if (exists($hash->{$branchpath})) { 657 # Merge the two revision sets 658 my$combined= "$hash->{$branchpath},$revs"; 659$hash->{$branchpath} = combine_ranges($combined); 660 } else { 661 # Just do range combining for consolidation 662$hash->{$branchpath} = combine_ranges($revs); 663 } 664 } 665} 666 667sub merge_merge_info { 668 my ($mergeinfo_one,$mergeinfo_two) =@_; 669 my%result_hash= (); 670 671 merge_revs_into_hash(\%result_hash,$mergeinfo_one); 672 merge_revs_into_hash(\%result_hash,$mergeinfo_two); 673 674 my$result= ''; 675 # Sort below is for consistency's sake 676 for my$branchname(sort keys(%result_hash)) { 677 my$revlist=$result_hash{$branchname}; 678$result.= "$branchname:$revlist\n" 679 } 680 return$result; 681} 682 683sub populate_merge_info { 684 my ($d,$gs,$uuid,$linear_refs,$rewritten_parent) =@_; 685 686 my%parentshash; 687 read_commit_parents(\%parentshash,$d); 688 my@parents= @{$parentshash{$d}}; 689 if ($#parents> 0) { 690 # Merge commit 691 my$all_parents_ok= 1; 692 my$aggregate_mergeinfo= ''; 693 my$rooturl=$gs->repos_root; 694 695 if (defined($rewritten_parent)) { 696 # Replace first parent with newly-rewritten version 697 shift@parents; 698 unshift@parents,$rewritten_parent; 699 } 700 701 foreach my$parent(@parents) { 702 my ($branchurl,$svnrev,$paruuid) = 703 cmt_metadata($parent); 704 705 unless (defined($svnrev)) { 706 # Should have been caught be preflight check 707 fatal "merge commit $dhas ancestor $parent, but that change " 708 ."doesnot have git-svn metadata!"; 709 } 710 unless ($branchurl=~ /^\Q$rooturl\E(.*)/) { 711 fatal "commit $parent git-svn metadata changed mid-run!"; 712 } 713 my$branchpath=$1; 714 715 my$ra= Git::SVN::Ra->new($branchurl); 716 my (undef, undef,$props) = 717$ra->get_dir(canonicalize_path("."),$svnrev); 718 my$par_mergeinfo=$props->{'svn:mergeinfo'}; 719 unless (defined$par_mergeinfo) { 720$par_mergeinfo= ''; 721 } 722 # Merge previous mergeinfo values 723$aggregate_mergeinfo= 724 merge_merge_info($aggregate_mergeinfo, 725$par_mergeinfo, 0); 726 727 next if$parenteq$parents[0]; # Skip first parent 728 # Add new changes being placed in tree by merge 729 my@cmd= (qw/rev-list --reverse/, 730$parent, qw/--not/); 731 foreach my$par(@parents) { 732 unless ($pareq$parent) { 733 push@cmd,$par; 734 } 735 } 736 my@revsin= (); 737 my ($revlist,$ctx) = command_output_pipe(@cmd); 738 while (<$revlist>) { 739 my$irev=$_; 740 chomp$irev; 741 my (undef,$csvnrev, undef) = 742 cmt_metadata($irev); 743 unless (defined$csvnrev) { 744 # A child is missing SVN annotations... 745 # this might be OK, or might not be. 746 warn "W:child $irevis merged into revision " 747 ."$d but doesnot have git-svn metadata." 748 ."This means git-svn cannot determine the " 749 ."svn revision numbers to place into the " 750 ."svn:mergeinfo property. You must ensure " 751 ."a branch is entirely committed to " 752 ."SVN before merging it in order for" 753 ."svn:mergeinfo population to function " 754 ."properly"; 755 } 756 push@revsin,$csvnrev; 757 } 758 command_close_pipe($revlist,$ctx); 759 760 last unless$all_parents_ok; 761 762 # We now have a list of all SVN revnos which are 763 # merged by this particular parent. Integrate them. 764 next if$#revsin== -1; 765 my$newmergeinfo= "$branchpath:" . join(',',@revsin); 766$aggregate_mergeinfo= 767 merge_merge_info($aggregate_mergeinfo, 768$newmergeinfo, 1); 769 } 770 if ($all_parents_okand$aggregate_mergeinfo) { 771 return$aggregate_mergeinfo; 772 } 773 } 774 775 return undef; 776} 777 778sub cmd_dcommit { 779 my$head= shift; 780 command_noisy(qw/update-index --refresh/); 781 git_cmd_try { command_oneline(qw/diff-index --quiet HEAD/) } 782 'Cannot dcommit with a dirty index. Commit your changes first, ' 783 . "or stash them with `git stash'.\n"; 784$head||= 'HEAD'; 785 786 my$old_head; 787 if ($headne 'HEAD') { 788$old_head= eval { 789 command_oneline([qw/symbolic-ref -q HEAD/]) 790 }; 791 if ($old_head) { 792$old_head=~ s{^refs/heads/}{}; 793 } else { 794$old_head= eval { command_oneline(qw/rev-parse HEAD/) }; 795 } 796 command(['checkout',$head], STDERR => 0); 797 } 798 799 my@refs; 800 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD', \@refs); 801 unless ($gs) { 802 die "Unable to determine upstream SVN information from ", 803 "$headhistory.\nPerhaps the repository is empty."; 804 } 805 806 if (defined$_commit_url) { 807$url=$_commit_url; 808 } else { 809$url= eval { command_oneline('config', '--get', 810 "svn-remote.$gs->{repo_id}.commiturl") }; 811 if (!$url) { 812$url=$gs->full_pushurl 813 } 814 } 815 816 my$last_rev=$_revisionif defined$_revision; 817 if ($url) { 818 print "Committing to$url...\n"; 819 } 820 my ($linear_refs,$parents) = linearize_history($gs, \@refs); 821 if ($_no_rebase&& scalar(@$linear_refs) > 1) { 822 warn "Attempting to commit more than one change while ", 823 "--no-rebase is enabled.\n", 824 "If these changes depend on each other, re-running ", 825 "without --no-rebase may be required." 826 } 827 828 if (defined$_interactive){ 829 my$ask_default= "y"; 830 foreach my$d(@$linear_refs){ 831 my ($fh,$ctx) = command_output_pipe(qw(show --summary),"$d"); 832while(<$fh>){ 833print$_; 834} 835 command_close_pipe($fh,$ctx); 836$_= ask("Commit this patch to SVN? ([y]es (default)|[n]o|[q]uit|[a]ll): ", 837 valid_re =>qr/^(?:yes|y|no|n|quit|q|all|a)/i, 838default=>$ask_default); 839die"Commit this patch reply required"unlessdefined$_; 840if(/^[nq]/i) { 841exit(0); 842}elsif(/^a/i) { 843last; 844} 845} 846} 847 848my$expect_url=$url; 849 850my$push_merge_info=eval{ 851 command_oneline(qw/config --get svn.pushmergeinfo/) 852}; 853if(not defined($push_merge_info) 854or$push_merge_infoeq"false" 855or$push_merge_infoeq"no" 856or$push_merge_infoeq"never") { 857$push_merge_info=0; 858} 859 860unless(defined($_merge_info) || !$push_merge_info) { 861# Preflight check of changes to ensure no issues with mergeinfo 862# This includes check for uncommitted-to-SVN parents 863# (other than the first parent, which we will handle), 864# information from different SVN repos, and paths 865# which are not underneath this repository root. 866my$rooturl=$gs->repos_root; 867foreachmy$d(@$linear_refs) { 868my%parentshash; 869 read_commit_parents(\%parentshash,$d); 870my@realparents= @{$parentshash{$d}}; 871if($#realparents>0) { 872# Merge commit 873shift@realparents;# Remove/ignore first parent 874foreachmy$parent(@realparents) { 875my($branchurl,$svnrev,$paruuid) = cmt_metadata($parent); 876unless(defined$paruuid) { 877# A parent is missing SVN annotations... 878# abort the whole operation. 879 fatal "$parentis merged into revision$d, " 880."but does not have git-svn metadata. " 881."Either dcommit the branch or use a " 882."local cherry-pick, FF merge, or rebase " 883."instead of an explicit merge commit."; 884} 885 886unless($paruuideq$uuid) { 887# Parent has SVN metadata from different repository 888 fatal "merge parent$parentfor change$dhas " 889."git-svn uuid$paruuid, while current change " 890."has uuid$uuid!"; 891} 892 893unless($branchurl=~/^\Q$rooturl\E(.*)/) { 894# This branch is very strange indeed. 895 fatal "merge parent$parentfor$dis on branch " 896."$branchurl, which is not under the " 897."git-svn root$rooturl!"; 898} 899} 900} 901} 902} 903 904my$rewritten_parent; 905 Git::SVN::remove_username($expect_url); 906if(defined($_merge_info)) { 907$_merge_info=~tr{ }{\n}; 908} 909while(1) { 910my$d=shift@$linear_refsorlast; 911unless(defined$last_rev) { 912(undef,$last_rev,undef) = cmt_metadata("$d~1"); 913unless(defined$last_rev) { 914 fatal "Unable to extract revision information ", 915"from commit$d~1"; 916} 917} 918if($_dry_run) { 919print"diff-tree$d~1$d\n"; 920}else{ 921my$cmt_rev; 922 923unless(defined($_merge_info) || !$push_merge_info) { 924$_merge_info= populate_merge_info($d,$gs, 925$uuid, 926$linear_refs, 927$rewritten_parent); 928} 929 930my%ed_opts= ( r =>$last_rev, 931log=> get_commit_entry($d)->{log}, 932 ra => Git::SVN::Ra->new($url), 933 config => SVN::Core::config_get_config( 934$Git::SVN::Ra::config_dir 935), 936 tree_a =>"$d~1", 937 tree_b =>$d, 938 editor_cb =>sub{ 939print"Committed r$_[0]\n"; 940$cmt_rev=$_[0]; 941}, 942 mergeinfo =>$_merge_info, 943 svn_path =>''); 944if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) { 945print"No changes\n$d~1==$d\n"; 946}elsif($parents->{$d} && @{$parents->{$d}}) { 947$gs->{inject_parents_dcommit}->{$cmt_rev} = 948$parents->{$d}; 949} 950$_fetch_all?$gs->fetch_all:$gs->fetch; 951$last_rev=$cmt_rev; 952next if$_no_rebase; 953 954# we always want to rebase against the current HEAD, 955# not any head that was passed to us 956my@diff= command('diff-tree',$d, 957$gs->refname,'--'); 958my@finish; 959if(@diff) { 960@finish= rebase_cmd(); 961print STDERR "W:$dand ",$gs->refname, 962" differ, using@finish:\n", 963join("\n",@diff),"\n"; 964}else{ 965print"No changes between current HEAD and ", 966$gs->refname, 967"\nResetting to the latest ", 968$gs->refname,"\n"; 969@finish= qw/reset --mixed/; 970} 971 command_noisy(@finish,$gs->refname); 972 973$rewritten_parent= command_oneline(qw/rev-parse HEAD/); 974 975if(@diff) { 976@refs= (); 977my($url_,$rev_,$uuid_,$gs_) = 978 working_head_info('HEAD', \@refs); 979my($linear_refs_,$parents_) = 980 linearize_history($gs_, \@refs); 981if(scalar(@$linear_refs) != 982scalar(@$linear_refs_)) { 983 fatal "# of revisions changed ", 984"\nbefore:\n", 985join("\n",@$linear_refs), 986"\n\nafter:\n", 987join("\n",@$linear_refs_),"\n", 988'If you are attempting to commit ', 989"merges, try running:\n\t", 990'git rebase --interactive', 991'--preserve-merges ', 992$gs->refname, 993"\nBefore dcommitting"; 994} 995if($url_ne$expect_url) { 996if($url_eq$gs->metadata_url) { 997print 998"Accepting rewritten URL:", 999"$url_\n";1000}else{1001 fatal1002"URL mismatch after rebase:",1003"$url_!=$expect_url";1004}1005}1006if($uuid_ne$uuid) {1007 fatal "uuid mismatch after rebase: ",1008"$uuid_!=$uuid";1009}1010# remap parents1011my(%p,@l,$i);1012for($i=0;$i<scalar@$linear_refs;$i++) {1013my$new=$linear_refs_->[$i]ornext;1014$p{$new} =1015$parents->{$linear_refs->[$i]};1016push@l,$new;1017}1018$parents= \%p;1019$linear_refs= \@l;1020}1021}1022}10231024if($old_head) {1025my$new_head= command_oneline(qw/rev-parse HEAD/);1026my$new_is_symbolic=eval{1027 command_oneline(qw/symbolic-ref -q HEAD/);1028};1029if($new_is_symbolic) {1030print"dcommitted the branch ",$head,"\n";1031}else{1032print"dcommitted on a detached HEAD because you gave ",1033"a revision argument.\n",1034"The rewritten commit is: ",$new_head,"\n";1035}1036 command(['checkout',$old_head], STDERR =>0);1037}10381039unlink$gs->{index};1040}10411042sub cmd_branch {1043my($branch_name,$head) =@_;10441045unless(defined$branch_name&&length$branch_name) {1046die(($_tag?"tag":"branch") ." name required\n");1047}1048$head||='HEAD';10491050my(undef,$rev,undef,$gs) = working_head_info($head);1051my$src=$gs->full_pushurl;10521053my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1054my$allglobs=$remote->{$_tag?'tags':'branches'};1055my$glob;1056if($#{$allglobs} ==0) {1057$glob=$allglobs->[0];1058}else{1059unless(defined$_branch_dest) {1060die"Multiple ",1061$_tag?"tag":"branch",1062" paths defined for Subversion repository.\n",1063"You must specify where you want to create the ",1064$_tag?"tag":"branch",1065" with the --destination argument.\n";1066}1067foreachmy$g(@{$allglobs}) {1068# SVN::Git::Editor could probably be moved to Git.pm..1069my$re= SVN::Git::Editor::glob2pat($g->{path}->{left});1070if($_branch_dest=~/$re/) {1071$glob=$g;1072last;1073}1074}1075unless(defined$glob) {1076my$dest_re=qr/\b\Q$_branch_dest\E\b/;1077foreachmy$g(@{$allglobs}) {1078$g->{path}->{left} =~/$dest_re/ornext;1079if(defined$glob) {1080die"Ambiguous destination: ",1081$_branch_dest,"\nmatches both '",1082$glob->{path}->{left},"' and '",1083$g->{path}->{left},"'\n";1084}1085$glob=$g;1086}1087unless(defined$glob) {1088die"Unknown ",1089$_tag?"tag":"branch",1090" destination$_branch_dest\n";1091}1092}1093}1094my($lft,$rgt) = @{$glob->{path} }{qw/left right/};1095my$url;1096if(defined$_commit_url) {1097$url=$_commit_url;1098}else{1099$url=eval{ command_oneline('config','--get',1100"svn-remote.$gs->{repo_id}.commiturl") };1101if(!$url) {1102$url=$remote->{pushurl} ||$remote->{url};1103}1104}1105my$dst=join'/',$url,$lft,$branch_name, ($rgt|| ());11061107if($dst=~/^https:/&&$src=~/^http:/) {1108$src=~s/^http:/https:/;1109}11101111::_req_svn();11121113my$ctx= SVN::Client->new(1114 auth => Git::SVN::Ra::_auth_providers(),1115 log_msg =>sub{1116${$_[0] } =defined$_message1117?$_message1118:'Create '. ($_tag?'tag ':'branch ')1119.$branch_name;1120},1121);11221123eval{1124$ctx->ls($dst,'HEAD',0);1125}and die"branch ${branch_name} already exists\n";11261127print"Copying ${src} at r${rev} to ${dst}...\n";1128$ctx->copy($src,$rev,$dst)1129unless$_dry_run;11301131$gs->fetch_all;1132}11331134sub cmd_find_rev {1135my$revision_or_hash=shift or die"SVN or git revision required ",1136"as a command-line argument\n";1137my$result;1138if($revision_or_hash=~/^r\d+$/) {1139my$head=shift;1140$head||='HEAD';1141my@refs;1142my(undef,undef,$uuid,$gs) = working_head_info($head, \@refs);1143unless($gs) {1144die"Unable to determine upstream SVN information from ",1145"$headhistory\n";1146}1147my$desired_revision=substr($revision_or_hash,1);1148$result=$gs->rev_map_get($desired_revision,$uuid);1149}else{1150my(undef,$rev,undef) = cmt_metadata($revision_or_hash);1151$result=$rev;1152}1153print"$result\n"if$result;1154}11551156sub auto_create_empty_directories {1157my($gs) =@_;1158my$var=eval{ command_oneline('config','--get','--bool',1159"svn-remote.$gs->{repo_id}.automkdirs") };1160# By default, create empty directories by consulting the unhandled log,1161# but allow setting it to 'false' to skip it.1162return!($var&&$vareq'false');1163}11641165sub cmd_rebase {1166 command_noisy(qw/update-index --refresh/);1167my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1168unless($gs) {1169die"Unable to determine upstream SVN information from ",1170"working tree history\n";1171}1172if($_dry_run) {1173print"Remote Branch: ".$gs->refname."\n";1174print"SVN URL: ".$url."\n";1175return;1176}1177if(command(qw/diff-index HEAD --/)) {1178print STDERR "Cannot rebase with uncommited changes:\n";1179 command_noisy('status');1180exit1;1181}1182unless($_local) {1183# rebase will checkout for us, so no need to do it explicitly1184$_no_checkout='true';1185$_fetch_all?$gs->fetch_all:$gs->fetch;1186}1187 command_noisy(rebase_cmd(),$gs->refname);1188if(auto_create_empty_directories($gs)) {1189$gs->mkemptydirs;1190}1191}11921193sub cmd_show_ignore {1194my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1195$gs||= Git::SVN->new;1196my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1197$gs->prop_walk($gs->{path},$r,sub{1198my($gs,$path,$props) =@_;1199print STDOUT "\n#$path\n";1200my$s=$props->{'svn:ignore'}orreturn;1201$s=~s/[\r\n]+/\n/g;1202$s=~s/^\n+//;1203chomp$s;1204$s=~ s#^#$path#gm;1205print STDOUT "$s\n";1206});1207}12081209sub cmd_show_externals {1210my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1211$gs||= Git::SVN->new;1212my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1213$gs->prop_walk($gs->{path},$r,sub{1214my($gs,$path,$props) =@_;1215print STDOUT "\n#$path\n";1216my$s=$props->{'svn:externals'}orreturn;1217$s=~s/[\r\n]+/\n/g;1218chomp$s;1219$s=~ s#^#$path#gm;1220print STDOUT "$s\n";1221});1222}12231224sub cmd_create_ignore {1225my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1226$gs||= Git::SVN->new;1227my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1228$gs->prop_walk($gs->{path},$r,sub{1229my($gs,$path,$props) =@_;1230# $path is of the form /path/to/dir/1231$path='.'.$path;1232# SVN can have attributes on empty directories,1233# which git won't track1234 mkpath([$path])unless-d $path;1235my$ignore=$path.'.gitignore';1236my$s=$props->{'svn:ignore'}orreturn;1237open(GITIGNORE,'>',$ignore)1238or fatal("Failed to open `$ignore' for writing:$!");1239$s=~s/[\r\n]+/\n/g;1240$s=~s/^\n+//;1241chomp$s;1242# Prefix all patterns so that the ignore doesn't apply1243# to sub-directories.1244$s=~ s#^#/#gm;1245print GITIGNORE "$s\n";1246close(GITIGNORE)1247or fatal("Failed to close `$ignore':$!");1248 command_noisy('add','-f',$ignore);1249});1250}12511252sub cmd_mkdirs {1253my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1254$gs||= Git::SVN->new;1255$gs->mkemptydirs($_revision);1256}12571258sub canonicalize_path {1259my($path) =@_;1260my$dot_slash_added=0;1261if(substr($path,0,1)ne"/") {1262$path="./".$path;1263$dot_slash_added=1;1264}1265# File::Spec->canonpath doesn't collapse x/../y into y (for a1266# good reason), so let's do this manually.1267$path=~ s#/+#/#g;1268$path=~ s#/\.(?:/|$)#/#g;1269$path=~ s#/[^/]+/\.\.##g;1270$path=~ s#/$##g;1271$path=~ s#^\./## if $dot_slash_added;1272$path=~ s#^/##;1273$path=~ s#^\.$##;1274return$path;1275}12761277sub canonicalize_url {1278my($url) =@_;1279$url=~ s#^([^:]+://[^/]*/)(.*)$#$1 . canonicalize_path($2)#e;1280return$url;1281}12821283# get_svnprops(PATH)1284# ------------------1285# Helper for cmd_propget and cmd_proplist below.1286sub get_svnprops {1287my$path=shift;1288my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1289$gs||= Git::SVN->new;12901291# prefix THE PATH by the sub-directory from which the user1292# invoked us.1293$path=$cmd_dir_prefix.$path;1294 fatal("No such file or directory:$path")unless-e $path;1295my$is_dir= -d $path?1:0;1296$path=$gs->{path} .'/'.$path;12971298# canonicalize the path (otherwise libsvn will abort or fail to1299# find the file)1300$path= canonicalize_path($path);13011302my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1303my$props;1304if($is_dir) {1305(undef,undef,$props) =$gs->ra->get_dir($path,$r);1306}1307else{1308(undef,$props) =$gs->ra->get_file($path,$r,undef);1309}1310return$props;1311}13121313# cmd_propget (PROP, PATH)1314# ------------------------1315# Print the SVN property PROP for PATH.1316sub cmd_propget {1317my($prop,$path) =@_;1318$path='.'ifnot defined$path;1319 usage(1)ifnot defined$prop;1320my$props= get_svnprops($path);1321if(not defined$props->{$prop}) {1322 fatal("`$path' does not have a `$prop' SVN property.");1323}1324print$props->{$prop} ."\n";1325}13261327# cmd_proplist (PATH)1328# -------------------1329# Print the list of SVN properties for PATH.1330sub cmd_proplist {1331my$path=shift;1332$path='.'ifnot defined$path;1333my$props= get_svnprops($path);1334print"Properties on '$path':\n";1335foreach(sort keys%{$props}) {1336print"$_\n";1337}1338}13391340sub cmd_multi_init {1341my$url=shift;1342unless(defined$_trunk||@_branches||@_tags) {1343 usage(1);1344}13451346$_prefix=''unlessdefined$_prefix;1347if(defined$url) {1348$url= canonicalize_url($url);1349 init_subdir(@_);1350}1351 do_git_init_db();1352if(defined$_trunk) {1353$_trunk=~ s#^/+##;1354my$trunk_ref='refs/remotes/'.$_prefix.'trunk';1355# try both old-style and new-style lookups:1356my$gs_trunk=eval{ Git::SVN->new($trunk_ref) };1357unless($gs_trunk) {1358my($trunk_url,$trunk_path) =1359 complete_svn_url($url,$_trunk);1360$gs_trunk= Git::SVN->init($trunk_url,$trunk_path,1361undef,$trunk_ref);1362}1363}1364return unless@_branches||@_tags;1365my$ra=$url? Git::SVN::Ra->new($url) :undef;1366foreachmy$path(@_branches) {1367 complete_url_ls_init($ra,$path,'--branches/-b',$_prefix);1368}1369foreachmy$path(@_tags) {1370 complete_url_ls_init($ra,$path,'--tags/-t',$_prefix.'tags/');1371}1372}13731374sub cmd_multi_fetch {1375$Git::SVN::no_reuse_existing =undef;1376my$remotes= Git::SVN::read_all_remotes();1377foreachmy$repo_id(sort keys%$remotes) {1378if($remotes->{$repo_id}->{url}) {1379 Git::SVN::fetch_all($repo_id,$remotes);1380}1381}1382}13831384# this command is special because it requires no metadata1385sub cmd_commit_diff {1386my($ta,$tb,$url) =@_;1387my$usage="Usage:$0commit-diff -r<revision> ".1388"<tree-ish> <tree-ish> [<URL>]";1389 fatal($usage)if(!defined$ta|| !defined$tb);1390my$svn_path='';1391if(!defined$url) {1392my$gs=eval{ Git::SVN->new};1393if(!$gs) {1394 fatal("Needed URL or usable git-svn --id in ",1395"the command-line\n",$usage);1396}1397$url=$gs->{url};1398$svn_path=$gs->{path};1399}1400unless(defined$_revision) {1401 fatal("-r|--revision is a required argument\n",$usage);1402}1403if(defined$_message&&defined$_file) {1404 fatal("Both --message/-m and --file/-F specified ",1405"for the commit message.\n",1406"I have no idea what you mean");1407}1408if(defined$_file) {1409$_message= file_to_s($_file);1410}else{1411$_message||= get_commit_entry($tb)->{log};1412}1413my$ra||= Git::SVN::Ra->new($url);1414my$r=$_revision;1415if($req'HEAD') {1416$r=$ra->get_latest_revnum;1417}elsif($r!~/^\d+$/) {1418die"revision argument:$rnot understood by git-svn\n";1419}1420my%ed_opts= ( r =>$r,1421log=>$_message,1422 ra =>$ra,1423 tree_a =>$ta,1424 tree_b =>$tb,1425 editor_cb =>sub{print"Committed r$_[0]\n"},1426 svn_path =>$svn_path);1427if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {1428print"No changes\n$ta==$tb\n";1429}1430}14311432sub escape_uri_only {1433my($uri) =@_;1434my@tmp;1435foreach(splitm{/},$uri) {1436s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;1437push@tmp,$_;1438}1439join('/',@tmp);1440}14411442sub escape_url {1443my($url) =@_;1444if($url=~ m#^([^:]+)://([^/]*)(.*)$#) {1445my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));1446$url="$scheme://$domain$uri";1447}1448$url;1449}14501451sub cmd_info {1452my$path= canonicalize_path(defined($_[0]) ?$_[0] :".");1453my$fullpath= canonicalize_path($cmd_dir_prefix.$path);1454if(exists$_[1]) {1455die"Too many arguments specified\n";1456}14571458my($file_type,$diff_status) = find_file_type_and_diff_status($path);14591460if(!$file_type&& !$diff_status) {1461print STDERR "svn: '$path' is not under version control\n";1462exit1;1463}14641465my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1466unless($gs) {1467die"Unable to determine upstream SVN information from ",1468"working tree history\n";1469}14701471# canonicalize_path() will return "" to make libsvn 1.5.x happy,1472$path="."if$patheq"";14731474my$full_url=$url. ($fullpatheq""?"":"/$fullpath");14751476if($_url) {1477print escape_url($full_url),"\n";1478return;1479}14801481my$result="Path:$path\n";1482$result.="Name: ". basename($path) ."\n"if$file_typene"dir";1483$result.="URL: ". escape_url($full_url) ."\n";14841485eval{1486my$repos_root=$gs->repos_root;1487 Git::SVN::remove_username($repos_root);1488$result.="Repository Root: ". escape_url($repos_root) ."\n";1489};1490if($@) {1491$result.="Repository Root: (offline)\n";1492}1493::_req_svn();1494$result.="Repository UUID:$uuid\n"unless$diff_statuseq"A"&&1495(::compare_svn_version('1.5.4') <=0||$file_typene"dir");1496$result.="Revision: ". ($diff_statuseq"A"?0:$rev) ."\n";14971498$result.="Node Kind: ".1499($file_typeeq"dir"?"directory":"file") ."\n";15001501my$schedule=$diff_statuseq"A"1502?"add"1503: ($diff_statuseq"D"?"delete":"normal");1504$result.="Schedule:$schedule\n";15051506if($diff_statuseq"A") {1507print$result,"\n";1508return;1509}15101511my($lc_author,$lc_rev,$lc_date_utc);1512my@args= Git::SVN::Log::git_svn_log_cmd($rev,$rev,"--",$fullpath);1513my$log= command_output_pipe(@args);1514my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;1515while(<$log>) {1516if(/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {1517$lc_author=$1;1518$lc_date_utc= Git::SVN::Log::parse_git_date($2,$3);1519}elsif(/^${esc_color} (git-svn-id:.+)$/o) {1520(undef,$lc_rev,undef) = ::extract_metadata($1);1521}1522}1523close$log;15241525 Git::SVN::Log::set_local_timezone();15261527$result.="Last Changed Author:$lc_author\n";1528$result.="Last Changed Rev:$lc_rev\n";1529$result.="Last Changed Date: ".1530 Git::SVN::Log::format_svn_date($lc_date_utc) ."\n";15311532if($file_typene"dir") {1533my$text_last_updated_date=1534($diff_statuseq"D"?$lc_date_utc: (stat$path)[9]);1535$result.=1536"Text Last Updated: ".1537 Git::SVN::Log::format_svn_date($text_last_updated_date) .1538"\n";1539my$checksum;1540if($diff_statuseq"D") {1541my($fh,$ctx) =1542 command_output_pipe(qw(cat-file blob),"HEAD:$path");1543if($file_typeeq"link") {1544my$file_name= <$fh>;1545$checksum= md5sum("link$file_name");1546}else{1547$checksum= md5sum($fh);1548}1549 command_close_pipe($fh,$ctx);1550}elsif($file_typeeq"link") {1551my$file_name=1552 command(qw(cat-file blob),"HEAD:$path");1553$checksum=1554 md5sum("link ".$file_name);1555}else{1556open FILE,"<",$pathor die$!;1557$checksum= md5sum(\*FILE);1558close FILE or die$!;1559}1560$result.="Checksum: ".$checksum."\n";1561}15621563print$result,"\n";1564}15651566sub cmd_reset {1567my$target=shift||$_revisionor die"SVN revision required\n";1568$target=$1if$target=~/^r(\d+)$/;1569$target=~/^\d+$/or die"Numeric SVN revision expected\n";1570my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1571unless($gs) {1572die"Unable to determine upstream SVN information from ".1573"history\n";1574}1575my($r,$c) =$gs->find_rev_before($target,not$_fetch_parent);1576die"Cannot find SVN revision$target\n"unlessdefined($c);1577$gs->rev_map_set($r,$c,'reset',$uuid);1578print"r$r=$c($gs->{ref_id})\n";1579}15801581sub cmd_gc {1582if(!$can_compress) {1583warn"Compress::Zlib could not be found; unhandled.log ".1584"files will not be compressed.\n";1585}1586 find({ wanted => \&gc_directory, no_chdir =>1},"$ENV{GIT_DIR}/svn");1587}15881589########################### utility functions #########################15901591sub rebase_cmd {1592my@cmd= qw/rebase/;1593push@cmd,'-v'if$_verbose;1594push@cmd, qw/--merge/if$_merge;1595push@cmd,"--strategy=$_strategy"if$_strategy;1596push@cmd,"--preserve-merges"if$_preserve_merges;1597@cmd;1598}15991600sub post_fetch_checkout {1601return if$_no_checkout;1602my$gs=$Git::SVN::_head orreturn;1603return if verify_ref('refs/heads/master^0');16041605# look for "trunk" ref if it exists1606my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1607my$fetch=$remote->{fetch};1608if($fetch) {1609foreachmy$p(keys%$fetch) {1610 basename($fetch->{$p})eq'trunk'ornext;1611$gs= Git::SVN->new($fetch->{$p},$gs->{repo_id},$p);1612last;1613}1614}16151616my$valid_head= verify_ref('HEAD^0');1617 command_noisy(qw(update-ref refs/heads/master),$gs->refname);1618return if($valid_head|| !verify_ref('HEAD^0'));16191620return if$ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;1621my$index=$ENV{GIT_INDEX_FILE} ||"$ENV{GIT_DIR}/index";1622return if-f $index;16231624return if command_oneline(qw/rev-parse --is-inside-work-tree/)eq'false';1625return if command_oneline(qw/rev-parse --is-inside-git-dir/)eq'true';1626 command_noisy(qw/read-tree -m -u -v HEAD HEAD/);1627print STDERR "Checked out HEAD:\n",1628$gs->full_url," r",$gs->last_rev,"\n";1629if(auto_create_empty_directories($gs)) {1630$gs->mkemptydirs($gs->last_rev);1631}1632}16331634sub complete_svn_url {1635my($url,$path) =@_;1636$path=~ s#/+$##;1637if($path!~ m#^[a-z\+]+://#) {1638if(!defined$url||$url!~ m#^[a-z\+]+://#) {1639 fatal("E: '$path' is not a complete URL ",1640"and a separate URL is not specified");1641}1642return($url,$path);1643}1644return($path,'');1645}16461647sub complete_url_ls_init {1648my($ra,$repo_path,$switch,$pfx) =@_;1649unless($repo_path) {1650print STDERR "W:$switchnot specified\n";1651return;1652}1653$repo_path=~ s#/+$##;1654if($repo_path=~ m#^[a-z\+]+://#) {1655$ra= Git::SVN::Ra->new($repo_path);1656$repo_path='';1657}else{1658$repo_path=~ s#^/+##;1659unless($ra) {1660 fatal("E: '$repo_path' is not a complete URL ",1661"and a separate URL is not specified");1662}1663}1664my$url=$ra->{url};1665my$gs= Git::SVN->init($url,undef,undef,undef,1);1666my$k="svn-remote.$gs->{repo_id}.url";1667my$orig_url=eval{ command_oneline(qw/config --get/,$k) };1668if($orig_url&& ($orig_urlne$gs->{url})) {1669die"$kalready set:$orig_url\n",1670"wanted to set to:$gs->{url}\n";1671}1672 command_oneline('config',$k,$gs->{url})unless$orig_url;1673my$remote_path="$gs->{path}/$repo_path";1674$remote_path=~s{%([0-9A-F]{2})}{chr hex($1)}ieg;1675$remote_path=~ s#/+#/#g;1676$remote_path=~ s#^/##g;1677$remote_path.="/*"if$remote_path!~ /\*/;1678my($n) = ($switch=~/^--(\w+)/);1679if(length$pfx&&$pfx!~ m#/$#) {1680die"--prefix='$pfx' must have a trailing slash '/'\n";1681}1682 command_noisy('config',1683'--add',1684"svn-remote.$gs->{repo_id}.$n",1685"$remote_path:refs/remotes/$pfx*".1686('/*' x (($remote_path=~ tr/*/*/) -1)) );1687}16881689sub verify_ref {1690my($ref) =@_;1691eval{ command_oneline(['rev-parse','--verify',$ref],1692{ STDERR =>0}); };1693}16941695sub get_tree_from_treeish {1696my($treeish) =@_;1697# $treeish can be a symbolic ref, too:1698my$type= command_oneline(qw/cat-file -t/,$treeish);1699my$expected;1700while($typeeq'tag') {1701($treeish,$type) = command(qw/cat-file tag/,$treeish);1702}1703if($typeeq'commit') {1704$expected= (grep/^tree /, command(qw/cat-file commit/,1705$treeish))[0];1706($expected) = ($expected=~/^tree ($sha1)$/o);1707die"Unable to get tree from$treeish\n"unless$expected;1708}elsif($typeeq'tree') {1709$expected=$treeish;1710}else{1711die"$treeishis a$type, expected tree, tag or commit\n";1712}1713return$expected;1714}17151716sub get_commit_entry {1717my($treeish) =shift;1718my%log_entry= (log=>'', tree => get_tree_from_treeish($treeish) );1719my$commit_editmsg="$ENV{GIT_DIR}/COMMIT_EDITMSG";1720my$commit_msg="$ENV{GIT_DIR}/COMMIT_MSG";1721open my$log_fh,'>',$commit_editmsgor croak $!;17221723my$type= command_oneline(qw/cat-file -t/,$treeish);1724if($typeeq'commit'||$typeeq'tag') {1725my($msg_fh,$ctx) = command_output_pipe('cat-file',1726$type,$treeish);1727my$in_msg=0;1728my$author;1729my$saw_from=0;1730my$msgbuf="";1731while(<$msg_fh>) {1732if(!$in_msg) {1733$in_msg=1if(/^\s*$/);1734$author=$1if(/^author (.*>)/);1735}elsif(/^git-svn-id: /) {1736# skip this for now, we regenerate the1737# correct one on re-fetch anyways1738# TODO: set *:merge properties or like...1739}else{1740if(/^From:/||/^Signed-off-by:/) {1741$saw_from=1;1742}1743$msgbuf.=$_;1744}1745}1746$msgbuf=~s/\s+$//s;1747if($Git::SVN::_add_author_from &&defined($author)1748&& !$saw_from) {1749$msgbuf.="\n\nFrom:$author";1750}1751print$log_fh $msgbufor croak $!;1752 command_close_pipe($msg_fh,$ctx);1753}1754close$log_fhor croak $!;17551756if($_edit|| ($typeeq'tree')) {1757chomp(my$editor= command_oneline(qw(var GIT_EDITOR)));1758system('sh','-c',$editor.' "$@"',$editor,$commit_editmsg);1759}1760rename$commit_editmsg,$commit_msgor croak $!;1761{1762require Encode;1763# SVN requires messages to be UTF-8 when entering the repo1764local$/;1765open$log_fh,'<',$commit_msgor croak $!;1766binmode$log_fh;1767chomp($log_entry{log} = <$log_fh>);17681769my$enc= Git::config('i18n.commitencoding') ||'UTF-8';1770my$msg=$log_entry{log};17711772eval{$msg= Encode::decode($enc,$msg,1) };1773if($@) {1774die"Could not decode as$enc:\n",$msg,1775"\nPerhaps you need to set i18n.commitencoding\n";1776}17771778eval{$msg= Encode::encode('UTF-8',$msg,1) };1779die"Could not encode as UTF-8:\n$msg\n"if$@;17801781$log_entry{log} =$msg;17821783close$log_fhor croak $!;1784}1785unlink$commit_msg;1786 \%log_entry;1787}17881789sub s_to_file {1790my($str,$file,$mode) =@_;1791open my$fd,'>',$fileor croak $!;1792print$fd $str,"\n"or croak $!;1793close$fdor croak $!;1794chmod($mode&~umask,$file)if(defined$mode);1795}17961797sub file_to_s {1798my$file=shift;1799open my$fd,'<',$fileor croak "$!: file:$file\n";1800local$/;1801my$ret= <$fd>;1802close$fdor croak $!;1803$ret=~s/\s*$//s;1804return$ret;1805}18061807# '<svn username> = real-name <email address>' mapping based on git-svnimport:1808sub load_authors {1809open my$authors,'<',$_authorsor die"Can't open$_authors$!\n";1810my$log=$cmdeq'log';1811while(<$authors>) {1812chomp;1813next unless/^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;1814my($user,$name,$email) = ($1,$2,$3);1815if($log) {1816$Git::SVN::Log::rusers{"$name<$email>"} =$user;1817}else{1818$users{$user} = [$name,$email];1819}1820}1821close$authorsor croak $!;1822}18231824# convert GetOpt::Long specs for use by git-config1825sub read_git_config {1826my$opts=shift;1827my@config_only;1828foreachmy$o(keys%$opts) {1829# if we have mixedCase and a long option-only, then1830# it's a config-only variable that we don't need for1831# the command-line.1832push@config_only,$oif($o=~/[A-Z]/&&$o=~/^[a-z]+$/i);1833my$v=$opts->{$o};1834my($key) = ($o=~/^([a-zA-Z\-]+)/);1835$key=~s/-//g;1836my$arg='git config';1837$arg.=' --int'if($o=~/[:=]i$/);1838$arg.=' --bool'if($o!~/[:=][sfi]$/);1839if(ref$veq'ARRAY') {1840chomp(my@tmp=`$arg--get-all svn.$key`);1841@$v=@tmpif@tmp;1842 } else {1843 chomp(my$tmp= `$arg--get svn.$key`);1844if($tmp&& !($arg=~/ --bool/&&$tmpeq'false')) {1845$$v=$tmp;1846}1847}1848}1849delete@$opts{@config_only}if@config_only;1850}18511852sub extract_metadata {1853my$id=shift orreturn(undef,undef,undef);1854my($url,$rev,$uuid) = ($id=~ /^\s*git-svn-id:\s+(.*)\@(\d+)1855 \s([a-f\d\-]+)$/ix);1856if(!defined$rev|| !$uuid|| !$url) {1857# some of the original repositories I made had1858# identifiers like this:1859($rev,$uuid) = ($id=~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i);1860}1861return($url,$rev,$uuid);1862}18631864sub cmt_metadata {1865return extract_metadata((grep(/^git-svn-id: /,1866 command(qw/cat-file commit/,shift)))[-1]);1867}18681869sub cmt_sha2rev_batch {1870my%s2r;1871my($pid,$in,$out,$ctx) = command_bidi_pipe(qw/cat-file --batch/);1872my$list=shift;18731874foreachmy$sha(@{$list}) {1875my$first=1;1876my$size=0;1877print$out $sha,"\n";18781879while(my$line= <$in>) {1880if($first&&$line=~/^[[:xdigit:]]{40}\smissing$/) {1881last;1882}elsif($first&&1883$line=~/^[[:xdigit:]]{40}\scommit\s(\d+)$/) {1884$first=0;1885$size=$1;1886next;1887}elsif($line=~/^(git-svn-id: )/) {1888my(undef,$rev,undef) =1889 extract_metadata($line);1890$s2r{$sha} =$rev;1891}18921893$size-=length($line);1894last if($size==0);1895}1896}18971898 command_close_bidi_pipe($pid,$in,$out,$ctx);18991900return \%s2r;1901}19021903sub working_head_info {1904my($head,$refs) =@_;1905my@args= qw/rev-list --first-parent --pretty=medium/;1906my($fh,$ctx) = command_output_pipe(@args,$head);1907my$hash;1908my%max;1909while(<$fh>) {1910if(m{^commit ($::sha1)$}) {1911unshift@$refs,$hashif$hashand$refs;1912$hash=$1;1913next;1914}1915next unlesss{^\s*(git-svn-id:)}{$1};1916my($url,$rev,$uuid) = extract_metadata($_);1917if(defined$url&&defined$rev) {1918next if$max{$url}and$max{$url} <$rev;1919if(my$gs= Git::SVN->find_by_url($url)) {1920my$c=$gs->rev_map_get($rev,$uuid);1921if($c&&$ceq$hash) {1922close$fh;# break the pipe1923return($url,$rev,$uuid,$gs);1924}else{1925$max{$url} ||=$gs->rev_map_max;1926}1927}1928}1929}1930 command_close_pipe($fh,$ctx);1931(undef,undef,undef,undef);1932}19331934sub read_commit_parents {1935my($parents,$c) =@_;1936chomp(my$p= command_oneline(qw/rev-list --parents -1/,$c));1937$p=~s/^($c)\s*//or die"rev-list --parents -1$cfailed!\n";1938@{$parents->{$c}} =split(/ /,$p);1939}19401941sub linearize_history {1942my($gs,$refs) =@_;1943my%parents;1944foreachmy$c(@$refs) {1945 read_commit_parents(\%parents,$c);1946}19471948my@linear_refs;1949my%skip= ();1950my$last_svn_commit=$gs->last_commit;1951foreachmy$c(reverse@$refs) {1952next if$ceq$last_svn_commit;1953last if$skip{$c};19541955unshift@linear_refs,$c;1956$skip{$c} =1;19571958# we only want the first parent to diff against for linear1959# history, we save the rest to inject when we finalize the1960# svn commit1961my$fp_a= verify_ref("$c~1");1962my$fp_b=shift@{$parents{$c}}if$parents{$c};1963if(!$fp_a|| !$fp_b) {1964die"Commit$c\n",1965"has no parent commit, and therefore ",1966"nothing to diff against.\n",1967"You should be working from a repository ",1968"originally created by git-svn\n";1969}1970if($fp_ane$fp_b) {1971die"$c~1=$fp_a, however parsing commit$c",1972"revealed that:\n$c~1=$fp_b\nBUG!\n";1973}19741975foreachmy$p(@{$parents{$c}}) {1976$skip{$p} =1;1977}1978}1979(\@linear_refs, \%parents);1980}19811982sub find_file_type_and_diff_status {1983my($path) =@_;1984return('dir','')if$patheq'';19851986my$diff_output=1987 command_oneline(qw(diff --cached --name-status --),$path) ||"";1988my$diff_status= (split(' ',$diff_output))[0] ||"";19891990my$ls_tree= command_oneline(qw(ls-tree HEAD),$path) ||"";19911992return(undef,undef)if!$diff_status&& !$ls_tree;19931994if($diff_statuseq"A") {1995return("link",$diff_status)if-l $path;1996return("dir",$diff_status)if-d $path;1997return("file",$diff_status);1998}19992000my$mode= (split(' ',$ls_tree))[0] ||"";20012002return("link",$diff_status)if$modeeq"120000";2003return("dir",$diff_status)if$modeeq"040000";2004return("file",$diff_status);2005}20062007sub md5sum {2008my$arg=shift;2009my$ref=ref$arg;2010my$md5= Digest::MD5->new();2011if($refeq'GLOB'||$refeq'IO::File'||$refeq'File::Temp') {2012$md5->addfile($arg)or croak $!;2013}elsif($refeq'SCALAR') {2014$md5->add($$arg)or croak $!;2015}elsif(!$ref) {2016$md5->add($arg)or croak $!;2017}else{2018::fatal "Can't provide MD5 hash for unknown ref type: '",$ref,"'";2019}2020return$md5->hexdigest();2021}20222023sub gc_directory {2024if($can_compress&& -f $_&& basename($_)eq"unhandled.log") {2025my$out_filename=$_.".gz";2026open my$in_fh,"<",$_or die"Unable to open$_:$!\n";2027binmode$in_fh;2028my$gz= Compress::Zlib::gzopen($out_filename,"ab")or2029die"Unable to open$out_filename:$!\n";20302031my$res;2032while($res=sysread($in_fh,my$str,1024)) {2033$gz->gzwrite($str)or2034die"Unable to write: ".$gz->gzerror()."!\n";2035}2036unlink$_or die"unlink$File::Find::name:$!\n";2037}elsif(-f $_&& basename($_)eq"index") {2038unlink$_or die"unlink$_:$!\n";2039}2040}20412042package Git::SVN;2043use strict;2044use warnings;2045use Fcntl qw/:DEFAULT :seek/;2046useconstant rev_map_fmt =>'NH40';2047use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent2048$_repack $_repack_flags $_use_svm_props $_head2049$_use_svnsync_props $no_reuse_existing $_minimize_url2050$_use_log_author $_add_author_from $_localtime/;2051use Carp qw/croak/;2052use File::Path qw/mkpath/;2053use File::Copy qw/copy/;2054use IPC::Open3;2055use Time::Local;2056use Memoize;# core since 5.8.0, Jul 20022057use Memoize::Storable;2058use POSIX qw(:signal_h);20592060my($_gc_nr,$_gc_period);20612062# properties that we do not log:2063my%SKIP_PROP;2064BEGIN{2065%SKIP_PROP=map{$_=>1} qw/svn:wc:ra_dav:version-url2066 svn:special svn:executable2067 svn:entry:committed-rev2068 svn:entry:last-author2069 svn:entry:uuid2070 svn:entry:committed-date/;20712072# some options are read globally, but can be overridden locally2073# per [svn-remote "..."] section. Command-line options will *NOT*2074# override options set in an [svn-remote "..."] section2075no strict 'refs';2076formy$option(qw/follow_parent no_metadata use_svm_props2077 use_svnsync_props/) {2078my$key=$option;2079$key=~tr/_//d;2080my$prop="-$option";2081*$option=sub{2082my($self) =@_;2083return$self->{$prop}ifexists$self->{$prop};2084my$k="svn-remote.$self->{repo_id}.$key";2085eval{ command_oneline(qw/config --get/,$k) };2086if($@) {2087$self->{$prop} = ${"Git::SVN::_$option"};2088}else{2089my$v= command_oneline(qw/config --bool/,$k);2090$self->{$prop} =$veq'false'?0:1;2091}2092return$self->{$prop};2093}2094}2095}209620972098my(%LOCKFILES,%INDEX_FILES);2099END{2100unlink keys%LOCKFILESif%LOCKFILES;2101unlink keys%INDEX_FILESif%INDEX_FILES;2102}21032104sub resolve_local_globs {2105my($url,$fetch,$glob_spec) =@_;2106return unlessdefined$glob_spec;2107my$ref=$glob_spec->{ref};2108my$path=$glob_spec->{path};2109foreach(command(qw#for-each-ref --format=%(refname) refs/#)) {2110next unless m#^$ref->{regex}$#;2111my$p=$1;2112my$pathname= desanitize_refname($path->full_path($p));2113my$refname= desanitize_refname($ref->full_path($p));2114if(my$existing=$fetch->{$pathname}) {2115if($existingne$refname) {2116die"Refspec conflict:\n",2117"existing:$existing\n",2118" globbed:$refname\n";2119}2120my$u= (::cmt_metadata("$refname"))[0];2121$u=~s!^\Q$url\E(/|$)!!or die2122"$refname: '$url' not found in '$u'\n";2123if($pathnamene$u) {2124warn"W: Refspec glob conflict ",2125"(ref:$refname):\n",2126"expected path:$pathname\n",2127" real path:$u\n",2128"Continuing ahead with$u\n";2129next;2130}2131}else{2132$fetch->{$pathname} =$refname;2133}2134}2135}21362137sub parse_revision_argument {2138my($base,$head) =@_;2139if(!defined$::_revision || $::_revision eq'BASE:HEAD') {2140return($base,$head);2141}2142return($1,$2)if($::_revision =~/^(\d+):(\d+)$/);2143return($::_revision, $::_revision)if($::_revision =~/^\d+$/);2144return($head,$head)if($::_revision eq'HEAD');2145return($base,$1)if($::_revision =~/^BASE:(\d+)$/);2146return($1,$head)if($::_revision =~/^(\d+):HEAD$/);2147die"revision argument: $::_revision not understood by git-svn\n";2148}21492150sub fetch_all {2151my($repo_id,$remotes) =@_;2152if(ref$repo_id) {2153my$gs=$repo_id;2154$repo_id=undef;2155$repo_id=$gs->{repo_id};2156}2157$remotes||= read_all_remotes();2158my$remote=$remotes->{$repo_id}or2159die"[svn-remote\"$repo_id\"] unknown\n";2160my$fetch=$remote->{fetch};2161my$url=$remote->{url}or die"svn-remote.$repo_id.url not defined\n";2162my(@gs,@globs);2163my$ra= Git::SVN::Ra->new($url);2164my$uuid=$ra->get_uuid;2165my$head=$ra->get_latest_revnum;21662167# ignore errors, $head revision may not even exist anymore2168eval{$ra->get_log("",$head,0,1,0,1,sub{$head=$_[1] }) };2169warn"W:$@\n"if$@;21702171my$base=defined$fetch?$head:0;21722173# read the max revs for wildcard expansion (branches/*, tags/*)2174foreachmy$t(qw/branches tags/) {2175defined$remote->{$t}ornext;2176push@globs, @{$remote->{$t}};21772178my$max_rev=eval{ tmp_config(qw/--int --get/,2179"svn-remote.$repo_id.${t}-maxRev") };2180if(defined$max_rev&& ($max_rev<$base)) {2181$base=$max_rev;2182}elsif(!defined$max_rev) {2183$base=0;2184}2185}21862187if($fetch) {2188foreachmy$p(sort keys%$fetch) {2189my$gs= Git::SVN->new($fetch->{$p},$repo_id,$p);2190my$lr=$gs->rev_map_max;2191if(defined$lr) {2192$base=$lrif($lr<$base);2193}2194push@gs,$gs;2195}2196}21972198($base,$head) = parse_revision_argument($base,$head);2199$ra->gs_fetch_loop_common($base,$head, \@gs, \@globs);2200}22012202sub read_all_remotes {2203my$r= {};2204my$use_svm_props=eval{ command_oneline(qw/config --bool2205 svn.useSvmProps/) };2206$use_svm_props=$use_svm_propseq'true'if$use_svm_props;2207my$svn_refspec=qr{\s*(.*?)\s*:\s*(.+?)\s*};2208foreach(grep{s/^svn-remote\.//} command(qw/config -l/)) {2209if(m!^(.+)\.fetch=$svn_refspec$!) {2210my($remote,$local_ref,$remote_ref) = ($1,$2,$3);2211die("svn-remote.$remote: remote ref '$remote_ref' "2212."must start with 'refs/'\n")2213unless$remote_ref=~m{^refs/};2214$local_ref= uri_decode($local_ref);2215$r->{$remote}->{fetch}->{$local_ref} =$remote_ref;2216$r->{$remote}->{svm} = {}if$use_svm_props;2217}elsif(m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {2218$r->{$1}->{svm} = {};2219}elsif(m!^(.+)\.url=\s*(.*)\s*$!) {2220$r->{$1}->{url} =$2;2221}elsif(m!^(.+)\.pushurl=\s*(.*)\s*$!) {2222$r->{$1}->{pushurl} =$2;2223}elsif(m!^(.+)\.ignore-refs=\s*(.*)\s*$!) {2224$r->{$1}->{ignore_refs_regex} =$2;2225}elsif(m!^(.+)\.(branches|tags)=$svn_refspec$!) {2226my($remote,$t,$local_ref,$remote_ref) =2227($1,$2,$3,$4);2228die("svn-remote.$remote: remote ref '$remote_ref' ($t) "2229."must start with 'refs/'\n")2230unless$remote_ref=~m{^refs/};2231$local_ref= uri_decode($local_ref);2232my$rs= {2233 t =>$t,2234 remote =>$remote,2235 path => Git::SVN::GlobSpec->new($local_ref,1),2236ref=> Git::SVN::GlobSpec->new($remote_ref,0) };2237if(length($rs->{ref}->{right}) !=0) {2238die"The '*' glob character must be the last ",2239"character of '$remote_ref'\n";2240}2241push@{$r->{$remote}->{$t} },$rs;2242}2243}22442245map{2246if(defined$r->{$_}->{svm}) {2247my$svm;2248eval{2249my$section="svn-remote.$_";2250$svm= {2251 source => tmp_config('--get',2252"$section.svm-source"),2253 replace => tmp_config('--get',2254"$section.svm-replace"),2255}2256};2257$r->{$_}->{svm} =$svm;2258}2259}keys%$r;22602261foreachmy$remote(keys%$r) {2262foreach(grep{defined$_}2263map{$r->{$remote}->{$_} }qw(branches tags)) {2264foreachmy$rs(@$_) {2265$rs->{ignore_refs_regex} =2266$r->{$remote}->{ignore_refs_regex};2267}2268}2269}22702271$r;2272}22732274sub init_vars {2275$_gc_nr=$_gc_period=1000;2276if(defined$_repack||defined$_repack_flags) {2277warn"Repack options are obsolete; they have no effect.\n";2278}2279}22802281sub verify_remotes_sanity {2282return unless-d $ENV{GIT_DIR};2283my%seen;2284foreach(command(qw/config -l/)) {2285if(m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {2286if($seen{$1}) {2287die"Remote ref refs/remote/$1is tracked by",2288"\n \"$_\"\nand\n \"$seen{$1}\"\n",2289"Please resolve this ambiguity in ",2290"your git configuration file before ",2291"continuing\n";2292}2293$seen{$1} =$_;2294}2295}2296}22972298sub find_existing_remote {2299my($url,$remotes) =@_;2300returnundefif$no_reuse_existing;2301my$existing;2302foreachmy$repo_id(keys%$remotes) {2303my$u=$remotes->{$repo_id}->{url}ornext;2304next if$une$url;2305$existing=$repo_id;2306last;2307}2308$existing;2309}23102311sub init_remote_config {2312my($self,$url,$no_write) =@_;2313$url=~s!/+$!!;# strip trailing slash2314my$r= read_all_remotes();2315my$existing= find_existing_remote($url,$r);2316if($existing) {2317unless($no_write) {2318print STDERR "Using existing ",2319"[svn-remote\"$existing\"]\n";2320}2321$self->{repo_id} =$existing;2322}elsif($_minimize_url) {2323my$min_url= Git::SVN::Ra->new($url)->minimize_url;2324$existing= find_existing_remote($min_url,$r);2325if($existing) {2326unless($no_write) {2327print STDERR "Using existing ",2328"[svn-remote\"$existing\"]\n";2329}2330$self->{repo_id} =$existing;2331}2332if($min_urlne$url) {2333unless($no_write) {2334print STDERR "Using higher level of URL: ",2335"$url=>$min_url\n";2336}2337my$old_path=$self->{path};2338$self->{path} =$url;2339$self->{path} =~s!^\Q$min_url\E(/|$)!!;2340if(length$old_path) {2341$self->{path} .="/$old_path";2342}2343$url=$min_url;2344}2345}2346my$orig_url;2347if(!$existing) {2348# verify that we aren't overwriting anything:2349$orig_url=eval{2350 command_oneline('config','--get',2351"svn-remote.$self->{repo_id}.url")2352};2353if($orig_url&& ($orig_urlne$url)) {2354die"svn-remote.$self->{repo_id}.url already set: ",2355"$orig_url\nwanted to set to:$url\n";2356}2357}2358my($xrepo_id,$xpath) = find_ref($self->refname);2359if(!$no_write&&defined$xpath) {2360die"svn-remote.$xrepo_id.fetch already set to track ",2361"$xpath:",$self->refname,"\n";2362}2363unless($no_write) {2364 command_noisy('config',2365"svn-remote.$self->{repo_id}.url",$url);2366$self->{path} =~s{^/}{};2367$self->{path} =~s{%([0-9A-F]{2})}{chr hex($1)}ieg;2368 command_noisy('config','--add',2369"svn-remote.$self->{repo_id}.fetch",2370"$self->{path}:".$self->refname);2371}2372$self->{url} =$url;2373}23742375sub find_by_url {# repos_root and, path are optional2376my($class,$full_url,$repos_root,$path) =@_;23772378returnundefunlessdefined$full_url;2379 remove_username($full_url);2380 remove_username($repos_root)ifdefined$repos_root;2381my$remotes= read_all_remotes();2382if(defined$full_url&&defined$repos_root&& !defined$path) {2383$path=$full_url;2384$path=~ s#^\Q$repos_root\E(?:/|$)##;2385}2386foreachmy$repo_id(keys%$remotes) {2387my$u=$remotes->{$repo_id}->{url}ornext;2388 remove_username($u);2389next ifdefined$repos_root&&$repos_rootne$u;23902391my$fetch=$remotes->{$repo_id}->{fetch} || {};2392foreachmy$t(qw/branches tags/) {2393foreachmy$globspec(@{$remotes->{$repo_id}->{$t}}) {2394 resolve_local_globs($u,$fetch,$globspec);2395}2396}2397my$p=$path;2398my$rwr= rewrite_root({repo_id =>$repo_id});2399my$svm=$remotes->{$repo_id}->{svm}2400ifdefined$remotes->{$repo_id}->{svm};2401unless(defined$p) {2402$p=$full_url;2403my$z=$u;2404my$prefix='';2405if($rwr) {2406$z=$rwr;2407 remove_username($z);2408}elsif(defined$svm) {2409$z=$svm->{source};2410$prefix=$svm->{replace};2411$prefix=~ s#^\Q$u\E(?:/|$)##;2412$prefix=~ s#/$##;2413}2414$p=~ s#^\Q$z\E(?:/|$)#$prefix# or next;2415}2416foreachmy$f(keys%$fetch) {2417next if$fne$p;2418return Git::SVN->new($fetch->{$f},$repo_id,$f);2419}2420}2421undef;2422}24232424sub init {2425my($class,$url,$path,$repo_id,$ref_id,$no_write) =@_;2426my$self= _new($class,$repo_id,$ref_id,$path);2427if(defined$url) {2428$self->init_remote_config($url,$no_write);2429}2430$self;2431}24322433sub find_ref {2434my($ref_id) =@_;2435foreach(command(qw/config -l/)) {2436next unless m!^svn-remote\.(.+)\.fetch=2437 \s*(.*?)\s*:\s*(.+?)\s*$!x;2438my($repo_id,$path,$ref) = ($1,$2,$3);2439if($refeq$ref_id) {2440$path=''if($path=~ m#^\./?#);2441return($repo_id,$path);2442}2443}2444(undef,undef,undef);2445}24462447sub new {2448my($class,$ref_id,$repo_id,$path) =@_;2449if(defined$ref_id&& !defined$repo_id&& !defined$path) {2450($repo_id,$path) = find_ref($ref_id);2451if(!defined$repo_id) {2452die"Could not find a\"svn-remote.*.fetch\"key ",2453"in the repository configuration matching: ",2454"$ref_id\n";2455}2456}2457my$self= _new($class,$repo_id,$ref_id,$path);2458if(!defined$self->{path} || !length$self->{path}) {2459my$fetch= command_oneline('config','--get',2460"svn-remote.$repo_id.fetch",2461":$ref_id\$")or2462die"Failed to read\"svn-remote.$repo_id.fetch\"",2463"\":$ref_id\$\"in config\n";2464($self->{path},undef) =split(/\s*:\s*/,$fetch);2465}2466$self->{path} =~s{/+}{/}g;2467$self->{path} =~s{\A/}{};2468$self->{path} =~s{/\z}{};2469$self->{url} = command_oneline('config','--get',2470"svn-remote.$repo_id.url")or2471die"Failed to read\"svn-remote.$repo_id.url\"in config\n";2472$self->{pushurl} =eval{ command_oneline('config','--get',2473"svn-remote.$repo_id.pushurl") };2474$self->rebuild;2475$self;2476}24772478sub refname {2479my($refname) =$_[0]->{ref_id} ;24802481# It cannot end with a slash /, we'll throw up on this because2482# SVN can't have directories with a slash in their name, either:2483if($refname=~m{/$}) {2484die"ref: '$refname' ends with a trailing slash, this is ",2485"not permitted by git nor Subversion\n";2486}24872488# It cannot have ASCII control character space, tilde ~, caret ^,2489# colon :, question-mark ?, asterisk *, space, or open bracket [2490# anywhere.2491#2492# Additionally, % must be escaped because it is used for escaping2493# and we want our escaped refname to be reversible2494$refname=~s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;24952496# no slash-separated component can begin with a dot .2497# /.* becomes /%2E*2498$refname=~s{/\.}{/%2E}g;24992500# It cannot have two consecutive dots .. anywhere2501# .. becomes %2E%2E2502$refname=~s{\.\.}{%2E%2E}g;25032504# trailing dots and .lock are not allowed2505# .$ becomes %2E and .lock becomes %2Elock2506$refname=~s{\.(?=$|lock$)}{%2E};25072508# the sequence @{ is used to access the reflog2509# @{ becomes %40{2510$refname=~s{\@\{}{%40\{}g;25112512return$refname;2513}25142515sub desanitize_refname {2516my($refname) =@_;2517$refname=~s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;2518return$refname;2519}25202521sub svm_uuid {2522my($self) =@_;2523return$self->{svm}->{uuid}if$self->svm;2524$self->ra;2525unless($self->{svm}) {2526die"SVM UUID not cached, and reading remotely failed\n";2527}2528$self->{svm}->{uuid};2529}25302531sub svm {2532my($self) =@_;2533return$self->{svm}if$self->{svm};2534my$svm;2535# see if we have it in our config, first:2536eval{2537my$section="svn-remote.$self->{repo_id}";2538$svm= {2539 source => tmp_config('--get',"$section.svm-source"),2540 uuid => tmp_config('--get',"$section.svm-uuid"),2541 replace => tmp_config('--get',"$section.svm-replace"),2542}2543};2544if($svm&&$svm->{source} &&$svm->{uuid} &&$svm->{replace}) {2545$self->{svm} =$svm;2546}2547$self->{svm};2548}25492550sub _set_svm_vars {2551my($self,$ra) =@_;2552return$raif$self->svm;25532554my@err= ("useSvmProps set, but failed to read SVM properties\n",2555"(svm:source, svm:uuid) ",2556"from the following URLs:\n");2557sub read_svm_props {2558my($self,$ra,$path,$r) =@_;2559my$props= ($ra->get_dir($path,$r))[2];2560my$src=$props->{'svm:source'};2561my$uuid=$props->{'svm:uuid'};2562returnundefif(!$src|| !$uuid);25632564chomp($src,$uuid);25652566$uuid=~m{^[0-9a-f\-]{30,}$}i2567or die"doesn't look right - svm:uuid is '$uuid'\n";25682569# the '!' is used to mark the repos_root!/relative/path2570$src=~s{/?!/?}{/};2571$src=~s{/+$}{};# no trailing slashes please2572# username is of no interest2573$src=~s{(^[a-z\+]*://)[^/@]*@}{$1};25742575my$replace=$ra->{url};2576$replace.="/$path"iflength$path;25772578my$section="svn-remote.$self->{repo_id}";2579 tmp_config("$section.svm-source",$src);2580 tmp_config("$section.svm-replace",$replace);2581 tmp_config("$section.svm-uuid",$uuid);2582$self->{svm} = {2583 source =>$src,2584 uuid =>$uuid,2585 replace =>$replace2586};2587}25882589my$r=$ra->get_latest_revnum;2590my$path=$self->{path};2591my%tried;2592while(length$path) {2593unless($tried{"$self->{url}/$path"}) {2594return$raif$self->read_svm_props($ra,$path,$r);2595$tried{"$self->{url}/$path"} =1;2596}2597$path=~ s#/?[^/]+$##;2598}2599die"Path: '$path' should be ''\n"if$pathne'';2600return$raif$self->read_svm_props($ra,$path,$r);2601$tried{"$self->{url}/$path"} =1;26022603if($ra->{repos_root}eq$self->{url}) {2604die@err, (map{"$_\n"}keys%tried),"\n";2605}26062607# nope, make sure we're connected to the repository root:2608my$ok;2609my@tried_b;2610$path=$ra->{svn_path};2611$ra= Git::SVN::Ra->new($ra->{repos_root});2612while(length$path) {2613unless($tried{"$ra->{url}/$path"}) {2614$ok=$self->read_svm_props($ra,$path,$r);2615last if$ok;2616$tried{"$ra->{url}/$path"} =1;2617}2618$path=~ s#/?[^/]+$##;2619}2620die"Path: '$path' should be ''\n"if$pathne'';2621$ok||=$self->read_svm_props($ra,$path,$r);2622$tried{"$ra->{url}/$path"} =1;2623if(!$ok) {2624die@err, (map{"$_\n"}keys%tried),"\n";2625}2626 Git::SVN::Ra->new($self->{url});2627}26282629sub svnsync {2630my($self) =@_;2631return$self->{svnsync}if$self->{svnsync};26322633if($self->no_metadata) {2634die"Can't have both 'noMetadata' and ",2635"'useSvnsyncProps' options set!\n";2636}2637if($self->rewrite_root) {2638die"Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",2639"options set!\n";2640}2641if($self->rewrite_uuid) {2642die"Can't have both 'useSvnsyncProps' and 'rewriteUUID' ",2643"options set!\n";2644}26452646my$svnsync;2647# see if we have it in our config, first:2648eval{2649my$section="svn-remote.$self->{repo_id}";26502651my$url= tmp_config('--get',"$section.svnsync-url");2652($url) = ($url=~m{^([a-z\+]+://\S+)$})or2653die"doesn't look right - svn:sync-from-url is '$url'\n";26542655my$uuid= tmp_config('--get',"$section.svnsync-uuid");2656($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2657die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26582659$svnsync= { url =>$url, uuid =>$uuid}2660};2661if($svnsync&&$svnsync->{url} &&$svnsync->{uuid}) {2662return$self->{svnsync} =$svnsync;2663}26642665my$err="useSvnsyncProps set, but failed to read ".2666"svnsync property: svn:sync-from-";2667my$rp=$self->ra->rev_proplist(0);26682669my$url=$rp->{'svn:sync-from-url'}or die$err."url\n";2670($url) = ($url=~m{^([a-z\+]+://\S+)$})or2671die"doesn't look right - svn:sync-from-url is '$url'\n";26722673my$uuid=$rp->{'svn:sync-from-uuid'}or die$err."uuid\n";2674($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2675die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26762677my$section="svn-remote.$self->{repo_id}";2678 tmp_config('--add',"$section.svnsync-uuid",$uuid);2679 tmp_config('--add',"$section.svnsync-url",$url);2680return$self->{svnsync} = { url =>$url, uuid =>$uuid};2681}26822683# this allows us to memoize our SVN::Ra UUID locally and avoid a2684# remote lookup (useful for 'git svn log').2685sub ra_uuid {2686my($self) =@_;2687unless($self->{ra_uuid}) {2688my$key="svn-remote.$self->{repo_id}.uuid";2689my$uuid=eval{ tmp_config('--get',$key) };2690if(!$@&&$uuid&&$uuid=~/^([a-f\d\-]{30,})$/i) {2691$self->{ra_uuid} =$uuid;2692}else{2693die"ra_uuid called without URL\n"unless$self->{url};2694$self->{ra_uuid} =$self->ra->get_uuid;2695 tmp_config('--add',$key,$self->{ra_uuid});2696}2697}2698$self->{ra_uuid};2699}27002701sub _set_repos_root {2702my($self,$repos_root) =@_;2703my$k="svn-remote.$self->{repo_id}.reposRoot";2704$repos_root||=$self->ra->{repos_root};2705 tmp_config($k,$repos_root);2706$repos_root;2707}27082709sub repos_root {2710my($self) =@_;2711my$k="svn-remote.$self->{repo_id}.reposRoot";2712eval{ tmp_config('--get',$k) } ||$self->_set_repos_root;2713}27142715sub ra {2716my($self) =shift;2717my$ra= Git::SVN::Ra->new($self->{url});2718$self->_set_repos_root($ra->{repos_root});2719if($self->use_svm_props&& !$self->{svm}) {2720if($self->no_metadata) {2721die"Can't have both 'noMetadata' and ",2722"'useSvmProps' options set!\n";2723}elsif($self->use_svnsync_props) {2724die"Can't have both 'useSvnsyncProps' and ",2725"'useSvmProps' options set!\n";2726}2727$ra=$self->_set_svm_vars($ra);2728$self->{-want_revprops} =1;2729}2730$ra;2731}27322733# prop_walk(PATH, REV, SUB)2734# -------------------------2735# Recursively traverse PATH at revision REV and invoke SUB for each2736# directory that contains a SVN property. SUB will be invoked as2737# follows: &SUB(gs, path, props); where `gs' is this instance of2738# Git::SVN, `path' the path to the directory where the properties2739# `props' were found. The `path' will be relative to point of checkout,2740# that is, if url://repo/trunk is the current Git branch, and that2741# directory contains a sub-directory `d', SUB will be invoked with `/d/'2742# as `path' (note the trailing `/').2743sub prop_walk {2744my($self,$path,$rev,$sub) =@_;27452746$path=~ s#^/##;2747my($dirent,undef,$props) =$self->ra->get_dir($path,$rev);2748$path=~ s#^/*#/#g;2749my$p=$path;2750# Strip the irrelevant part of the path.2751$p=~ s#^/+\Q$self->{path}\E(/|$)#/#;2752# Ensure the path is terminated by a `/'.2753$p=~ s#/*$#/#;27542755# The properties contain all the internal SVN stuff nobody2756# (usually) cares about.2757my$interesting_props=0;2758foreach(keys%{$props}) {2759# If it doesn't start with `svn:', it must be a2760# user-defined property.2761++$interesting_propsandnext if$_!~/^svn:/;2762# FIXME: Fragile, if SVN adds new public properties,2763# this needs to be updated.2764++$interesting_propsif/^svn:(?:ignore|keywords|executable2765|eol-style|mime-type2766|externals|needs-lock)$/x;2767}2768&$sub($self,$p,$props)if$interesting_props;27692770foreach(sort keys%$dirent) {2771next if$dirent->{$_}->{kind} !=$SVN::Node::dir;2772$self->prop_walk($self->{path} .$p.$_,$rev,$sub);2773}2774}27752776sub last_rev { ($_[0]->last_rev_commit)[0] }2777sub last_commit { ($_[0]->last_rev_commit)[1] }27782779# returns the newest SVN revision number and newest commit SHA12780sub last_rev_commit {2781my($self) =@_;2782if(defined$self->{last_rev} &&defined$self->{last_commit}) {2783return($self->{last_rev},$self->{last_commit});2784}2785my$c= ::verify_ref($self->refname.'^0');2786if($c&& !$self->use_svm_props&& !$self->no_metadata) {2787my$rev= (::cmt_metadata($c))[1];2788if(defined$rev) {2789($self->{last_rev},$self->{last_commit}) = ($rev,$c);2790return($rev,$c);2791}2792}2793my$map_path=$self->map_path;2794unless(-e $map_path) {2795($self->{last_rev},$self->{last_commit}) = (undef,undef);2796return(undef,undef);2797}2798my($rev,$commit) =$self->rev_map_max(1);2799($self->{last_rev},$self->{last_commit}) = ($rev,$commit);2800return($rev,$commit);2801}28022803sub get_fetch_range {2804my($self,$min,$max) =@_;2805$max||=$self->ra->get_latest_revnum;2806$min||=$self->rev_map_max;2807(++$min,$max);2808}28092810sub tmp_config {2811my(@args) =@_;2812my$old_def_config="$ENV{GIT_DIR}/svn/config";2813my$config="$ENV{GIT_DIR}/svn/.metadata";2814if(! -f $config&& -f $old_def_config) {2815rename$old_def_config,$configor2816die"Failed rename$old_def_config=>$config:$!\n";2817}2818my$old_config=$ENV{GIT_CONFIG};2819$ENV{GIT_CONFIG} =$config;2820$@=undef;2821my@ret=eval{2822unless(-f $config) {2823 mkfile($config);2824open my$fh,'>',$configor2825die"Can't open$config:$!\n";2826print$fh"; This file is used internally by ",2827"git-svn\n"or die2828"Couldn't write to$config:$!\n";2829print$fh"; You should not have to edit it\n"or2830die"Couldn't write to$config:$!\n";2831close$fhor die"Couldn't close$config:$!\n";2832}2833 command('config',@args);2834};2835my$err=$@;2836if(defined$old_config) {2837$ENV{GIT_CONFIG} =$old_config;2838}else{2839delete$ENV{GIT_CONFIG};2840}2841die$errif$err;2842wantarray?@ret:$ret[0];2843}28442845sub tmp_index_do {2846my($self,$sub) =@_;2847my$old_index=$ENV{GIT_INDEX_FILE};2848$ENV{GIT_INDEX_FILE} =$self->{index};2849$@=undef;2850my@ret=eval{2851my($dir,$base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);2852 mkpath([$dir])unless-d $dir;2853&$sub;2854};2855my$err=$@;2856if(defined$old_index) {2857$ENV{GIT_INDEX_FILE} =$old_index;2858}else{2859delete$ENV{GIT_INDEX_FILE};2860}2861die$errif$err;2862wantarray?@ret:$ret[0];2863}28642865sub assert_index_clean {2866my($self,$treeish) =@_;28672868$self->tmp_index_do(sub{2869 command_noisy('read-tree',$treeish)unless-e $self->{index};2870my$x= command_oneline('write-tree');2871my($y) = (command(qw/cat-file commit/,$treeish) =~2872/^tree ($::sha1)/mo);2873return if$yeq$x;28742875warn"Index mismatch:$y!=$x\nrereading$treeish\n";2876unlink$self->{index}or die"unlink$self->{index}:$!\n";2877 command_noisy('read-tree',$treeish);2878$x= command_oneline('write-tree');2879if($yne$x) {2880::fatal "trees ($treeish)$y!=$x\n",2881"Something is seriously wrong...";2882}2883});2884}28852886sub get_commit_parents {2887my($self,$log_entry) =@_;2888my(%seen,@ret,@tmp);2889# legacy support for 'set-tree'; this is only used by set_tree_cb:2890if(my$ip=$self->{inject_parents}) {2891if(my$commit=delete$ip->{$log_entry->{revision}}) {2892push@tmp,$commit;2893}2894}2895if(my$cur= ::verify_ref($self->refname.'^0')) {2896push@tmp,$cur;2897}2898if(my$ipd=$self->{inject_parents_dcommit}) {2899if(my$commit=delete$ipd->{$log_entry->{revision}}) {2900push@tmp,@$commit;2901}2902}2903push@tmp,$_foreach(@{$log_entry->{parents}},@tmp);2904while(my$p=shift@tmp) {2905next if$seen{$p};2906$seen{$p} =1;2907push@ret,$p;2908}2909@ret;2910}29112912sub rewrite_root {2913my($self) =@_;2914return$self->{-rewrite_root}ifexists$self->{-rewrite_root};2915my$k="svn-remote.$self->{repo_id}.rewriteRoot";2916my$rwr=eval{ command_oneline(qw/config --get/,$k) };2917if($rwr) {2918$rwr=~ s#/+$##;2919if($rwr!~ m#^[a-z\+]+://#) {2920die"$rwris not a valid URL (key:$k)\n";2921}2922}2923$self->{-rewrite_root} =$rwr;2924}29252926sub rewrite_uuid {2927my($self) =@_;2928return$self->{-rewrite_uuid}ifexists$self->{-rewrite_uuid};2929my$k="svn-remote.$self->{repo_id}.rewriteUUID";2930my$rwid=eval{ command_oneline(qw/config --get/,$k) };2931if($rwid) {2932$rwid=~ s#/+$##;2933if($rwid!~ m#^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$#) {2934die"$rwidis not a valid UUID (key:$k)\n";2935}2936}2937$self->{-rewrite_uuid} =$rwid;2938}29392940sub metadata_url {2941my($self) =@_;2942($self->rewrite_root||$self->{url}) .2943(length$self->{path} ?'/'.$self->{path} :'');2944}29452946sub full_url {2947my($self) =@_;2948$self->{url} . (length$self->{path} ?'/'.$self->{path} :'');2949}29502951sub full_pushurl {2952my($self) =@_;2953if($self->{pushurl}) {2954return$self->{pushurl} . (length$self->{path} ?'/'.2955$self->{path} :'');2956}else{2957return$self->full_url;2958}2959}29602961sub set_commit_header_env {2962my($log_entry) =@_;2963my%env;2964foreachmy$ned(qw/NAME EMAIL DATE/) {2965foreachmy$ac(qw/AUTHOR COMMITTER/) {2966$env{"GIT_${ac}_${ned}"} =$ENV{"GIT_${ac}_${ned}"};2967}2968}29692970$ENV{GIT_AUTHOR_NAME} =$log_entry->{name};2971$ENV{GIT_AUTHOR_EMAIL} =$log_entry->{email};2972$ENV{GIT_AUTHOR_DATE} =$ENV{GIT_COMMITTER_DATE} =$log_entry->{date};29732974$ENV{GIT_COMMITTER_NAME} = (defined$log_entry->{commit_name})2975?$log_entry->{commit_name}2976:$log_entry->{name};2977$ENV{GIT_COMMITTER_EMAIL} = (defined$log_entry->{commit_email})2978?$log_entry->{commit_email}2979:$log_entry->{email};2980 \%env;2981}29822983sub restore_commit_header_env {2984my($env) =@_;2985foreachmy$ned(qw/NAME EMAIL DATE/) {2986foreachmy$ac(qw/AUTHOR COMMITTER/) {2987my$k="GIT_${ac}_${ned}";2988if(defined$env->{$k}) {2989$ENV{$k} =$env->{$k};2990}else{2991delete$ENV{$k};2992}2993}2994}2995}29962997sub gc {2998 command_noisy('gc','--auto');2999};30003001sub do_git_commit {3002my($self,$log_entry) =@_;3003my$lr=$self->last_rev;3004if(defined$lr&&$lr>=$log_entry->{revision}) {3005die"Last fetched revision of ",$self->refname,3006" was r$lr, but we are about to fetch: ",3007"r$log_entry->{revision}!\n";3008}3009if(my$c=$self->rev_map_get($log_entry->{revision})) {3010 croak "$log_entry->{revision} =$calready exists! ",3011"Why are we refetching it?\n";3012}3013my$old_env= set_commit_header_env($log_entry);3014my$tree=$log_entry->{tree};3015if(!defined$tree) {3016$tree=$self->tmp_index_do(sub{3017 command_oneline('write-tree') });3018}3019die"Tree is not a valid sha1:$tree\n"if$tree!~/^$::sha1$/o;30203021my@exec= ('git','commit-tree',$tree);3022foreach($self->get_commit_parents($log_entry)) {3023push@exec,'-p',$_;3024}3025defined(my$pid= open3(my$msg_fh,my$out_fh,'>&STDERR',@exec))3026or croak $!;3027binmode$msg_fh;30283029# we always get UTF-8 from SVN, but we may want our commits in3030# a different encoding.3031if(my$enc= Git::config('i18n.commitencoding')) {3032require Encode;3033 Encode::from_to($log_entry->{log},'UTF-8',$enc);3034}3035print$msg_fh $log_entry->{log}or croak $!;3036 restore_commit_header_env($old_env);3037unless($self->no_metadata) {3038print$msg_fh"\ngit-svn-id:$log_entry->{metadata}\n"3039or croak $!;3040}3041$msg_fh->flush==0or croak $!;3042close$msg_fhor croak $!;3043chomp(my$commit=do{local$/; <$out_fh> });3044close$out_fhor croak $!;3045waitpid$pid,0;3046 croak $?if$?;3047if($commit!~/^$::sha1$/o) {3048die"Failed to commit, invalid sha1:$commit\n";3049}30503051$self->rev_map_set($log_entry->{revision},$commit,1);30523053$self->{last_rev} =$log_entry->{revision};3054$self->{last_commit} =$commit;3055print"r$log_entry->{revision}"unless$::_q >1;3056if(defined$log_entry->{svm_revision}) {3057print" (\@$log_entry->{svm_revision})"unless$::_q >1;3058$self->rev_map_set($log_entry->{svm_revision},$commit,30590,$self->svm_uuid);3060}3061print" =$commit($self->{ref_id})\n"unless$::_q >1;3062if(--$_gc_nr==0) {3063$_gc_nr=$_gc_period;3064 gc();3065}3066return$commit;3067}30683069sub match_paths {3070my($self,$paths,$r) =@_;3071return1if$self->{path}eq'';3072if(my$path=$paths->{"/$self->{path}"}) {3073return($path->{action}eq'D') ?0:1;3074}3075$self->{path_regex} ||=qr/^\/\Q$self->{path}\E\//;3076if(grep/$self->{path_regex}/,keys%$paths) {3077return1;3078}3079my$c='';3080foreach(split m#/#, $self->{path}) {3081$c.="/$_";3082next unless($paths->{$c} &&3083($paths->{$c}->{action} =~/^[AR]$/));3084if($self->ra->check_path($self->{path},$r) ==3085$SVN::Node::dir) {3086return1;3087}3088}3089return0;3090}30913092sub find_parent_branch {3093my($self,$paths,$rev) =@_;3094returnundefunless$self->follow_parent;3095unless(defined$paths) {3096my$err_handler=$SVN::Error::handler;3097$SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;3098$self->ra->get_log([$self->{path}],$rev,$rev,0,1,1,3099sub{$paths=$_[0] });3100$SVN::Error::handler =$err_handler;3101}3102returnundefunlessdefined$paths;31033104# look for a parent from another branch:3105my@b_path_components=split m#/#, $self->{path};3106my@a_path_components;3107my$i;3108while(@b_path_components) {3109$i=$paths->{'/'.join('/',@b_path_components)};3110last if$i&&defined$i->{copyfrom_path};3111unshift(@a_path_components,pop(@b_path_components));3112}3113returnundefunlessdefined$i&&defined$i->{copyfrom_path};3114my$branch_from=$i->{copyfrom_path};3115if(@a_path_components) {3116print STDERR "branch_from:$branch_from=> ";3117$branch_from.='/'.join('/',@a_path_components);3118print STDERR $branch_from,"\n";3119}3120my$r=$i->{copyfrom_rev};3121my$repos_root=$self->ra->{repos_root};3122my$url=$self->ra->{url};3123my$new_url=$url.$branch_from;3124print STDERR "Found possible branch point: ",3125"$new_url=> ",$self->full_url,",$r\n"3126unless$::_q >1;3127$branch_from=~ s#^/##;3128my$gs=$self->other_gs($new_url,$url,3129$branch_from,$r,$self->{ref_id});3130my($r0,$parent) =$gs->find_rev_before($r,1);3131{3132my($base,$head);3133if(!defined$r0|| !defined$parent) {3134($base,$head) = parse_revision_argument(0,$r);3135}else{3136if($r0<$r) {3137$gs->ra->get_log([$gs->{path}],$r0+1,$r,1,31380,1,sub{$base=$_[1] -1});3139}3140}3141if(defined$base&&$base<=$r) {3142$gs->fetch($base,$r);3143}3144($r0,$parent) =$gs->find_rev_before($r,1);3145}3146if(defined$r0&&defined$parent) {3147print STDERR "Found branch parent: ($self->{ref_id})$parent\n"3148unless$::_q >1;3149my$ed;3150if($self->ra->can_do_switch) {3151$self->assert_index_clean($parent);3152print STDERR "Following parent with do_switch\n"3153unless$::_q >1;3154# do_switch works with svn/trunk >= r22312, but that3155# is not included with SVN 1.4.3 (the latest version3156# at the moment), so we can't rely on it3157$self->{last_rev} =$r0;3158$self->{last_commit} =$parent;3159$ed= SVN::Git::Fetcher->new($self,$gs->{path});3160$gs->ra->gs_do_switch($r0,$rev,$gs,3161$self->full_url,$ed)3162or die"SVN connection failed somewhere...\n";3163}elsif($self->ra->trees_match($new_url,$r0,3164$self->full_url,$rev)) {3165print STDERR "Trees match:\n",3166"$new_url\@$r0\n",3167" ${\$self->full_url}\@$rev\n",3168"Following parent with no changes\n"3169unless$::_q >1;3170$self->tmp_index_do(sub{3171 command_noisy('read-tree',$parent);3172});3173$self->{last_commit} =$parent;3174}else{3175print STDERR "Following parent with do_update\n"3176unless$::_q >1;3177$ed= SVN::Git::Fetcher->new($self);3178$self->ra->gs_do_update($rev,$rev,$self,$ed)3179or die"SVN connection failed somewhere...\n";3180}3181print STDERR "Successfully followed parent\n"unless$::_q >1;3182return$self->make_log_entry($rev, [$parent],$ed);3183}3184returnundef;3185}31863187sub do_fetch {3188my($self,$paths,$rev) =@_;3189my$ed;3190my($last_rev,@parents);3191if(my$lc=$self->last_commit) {3192# we can have a branch that was deleted, then re-added3193# under the same name but copied from another path, in3194# which case we'll have multiple parents (we don't3195# want to break the original ref, nor lose copypath info):3196if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3197push@{$log_entry->{parents}},$lc;3198return$log_entry;3199}3200$ed= SVN::Git::Fetcher->new($self);3201$last_rev=$self->{last_rev};3202$ed->{c} =$lc;3203@parents= ($lc);3204}else{3205$last_rev=$rev;3206if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3207return$log_entry;3208}3209$ed= SVN::Git::Fetcher->new($self);3210}3211unless($self->ra->gs_do_update($last_rev,$rev,$self,$ed)) {3212die"SVN connection failed somewhere...\n";3213}3214$self->make_log_entry($rev, \@parents,$ed);3215}32163217sub mkemptydirs {3218my($self,$r) =@_;32193220sub scan {3221my($r,$empty_dirs,$line) =@_;3222if(defined$r&&$line=~/^r(\d+)$/) {3223return0if$1>$r;3224}elsif($line=~/^ \+empty_dir: (.+)$/) {3225$empty_dirs->{$1} =1;3226}elsif($line=~/^ \-empty_dir: (.+)$/) {3227my@d=grep{m[^\Q$1\E(/|$)]} (keys%$empty_dirs);3228delete@$empty_dirs{@d};3229}32301;# continue3231};32323233my%empty_dirs= ();3234my$gz_file="$self->{dir}/unhandled.log.gz";3235if(-f $gz_file) {3236if(!$can_compress) {3237warn"Compress::Zlib could not be found; ",3238"empty directories in$gz_filewill not be read\n";3239}else{3240my$gz= Compress::Zlib::gzopen($gz_file,"rb")or3241die"Unable to open$gz_file:$!\n";3242my$line;3243while($gz->gzreadline($line) >0) {3244 scan($r, \%empty_dirs,$line)orlast;3245}3246$gz->gzclose;3247}3248}32493250if(open my$fh,'<',"$self->{dir}/unhandled.log") {3251binmode$fhor croak "binmode:$!";3252while(<$fh>) {3253 scan($r, \%empty_dirs,$_)orlast;3254}3255close$fh;3256}32573258my$strip=qr/\A\Q$self->{path}\E(?:\/|$)/;3259foreachmy$d(sort keys%empty_dirs) {3260$d= uri_decode($d);3261$d=~s/$strip//;3262next unlesslength($d);3263next if-d $d;3264if(-e $d) {3265warn"$dexists but is not a directory\n";3266}else{3267print"creating empty directory:$d\n";3268 mkpath([$d]);3269}3270}3271}32723273sub get_untracked {3274my($self,$ed) =@_;3275my@out;3276my$h=$ed->{empty};3277foreach(sort keys%$h) {3278my$act=$h->{$_} ?'+empty_dir':'-empty_dir';3279push@out,"$act: ". uri_encode($_);3280warn"W:$act:$_\n";3281}3282foreachmy$t(qw/dir_prop file_prop/) {3283$h=$ed->{$t}ornext;3284foreachmy$path(sort keys%$h) {3285my$ppath=$patheq''?'.':$path;3286foreachmy$prop(sort keys%{$h->{$path}}) {3287next if$SKIP_PROP{$prop};3288my$v=$h->{$path}->{$prop};3289my$t_ppath_prop="$t: ".3290 uri_encode($ppath) .' '.3291 uri_encode($prop);3292if(defined$v) {3293push@out," +$t_ppath_prop".3294 uri_encode($v);3295}else{3296push@out," -$t_ppath_prop";3297}3298}3299}3300}3301foreachmy$t(qw/absent_file absent_directory/) {3302$h=$ed->{$t}ornext;3303foreachmy$parent(sort keys%$h) {3304foreachmy$path(sort@{$h->{$parent}}) {3305push@out,"$t: ".3306 uri_encode("$parent/$path");3307warn"W:$t:$parent/$path",3308"Insufficient permissions?\n";3309}3310}3311}3312 \@out;3313}33143315sub get_tz {3316# some systmes don't handle or mishandle %z, so be creative.3317my$t=shift||time;3318my$gm= timelocal(gmtime($t));3319my$sign=qw( + + - )[$t<=>$gm];3320returnsprintf("%s%02d%02d",$sign, (gmtime(abs($t-$gm)))[2,1]);3321}33223323# parse_svn_date(DATE)3324# --------------------3325# Given a date (in UTC) from Subversion, return a string in the format3326# "<TZ Offset> <local date/time>" that Git will use.3327#3328# By default the parsed date will be in UTC; if $Git::SVN::_localtime3329# is true we'll convert it to the local timezone instead.3330sub parse_svn_date {3331my$date=shift||return'+0000 1970-01-01 00:00:00';3332my($Y,$m,$d,$H,$M,$S) = ($date=~ /^(\d{4})\-(\d\d)\-(\d\d)T3333(\d\d)\:(\d\d)\:(\d\d)\.\d*Z$/x)or3334 croak "Unable to parse date:$date\n";3335my$parsed_date;# Set next.33363337if($Git::SVN::_localtime) {3338# Translate the Subversion datetime to an epoch time.3339# Begin by switching ourselves to $date's timezone, UTC.3340my$old_env_TZ=$ENV{TZ};3341$ENV{TZ} ='UTC';33423343my$epoch_in_UTC=3344 POSIX::strftime('%s',$S,$M,$H,$d,$m-1,$Y-1900);33453346# Determine our local timezone (including DST) at the3347# time of $epoch_in_UTC. $Git::SVN::Log::TZ stored the3348# value of TZ, if any, at the time we were run.3349if(defined$Git::SVN::Log::TZ) {3350$ENV{TZ} =$Git::SVN::Log::TZ;3351}else{3352delete$ENV{TZ};3353}33543355my$our_TZ= get_tz();33563357# This converts $epoch_in_UTC into our local timezone.3358my($sec,$min,$hour,$mday,$mon,$year,3359$wday,$yday,$isdst) =localtime($epoch_in_UTC);33603361$parsed_date=sprintf('%s%04d-%02d-%02d%02d:%02d:%02d',3362$our_TZ,$year+1900,$mon+1,3363$mday,$hour,$min,$sec);33643365# Reset us to the timezone in effect when we entered3366# this routine.3367if(defined$old_env_TZ) {3368$ENV{TZ} =$old_env_TZ;3369}else{3370delete$ENV{TZ};3371}3372}else{3373$parsed_date="+0000$Y-$m-$d$H:$M:$S";3374}33753376return$parsed_date;3377}33783379sub other_gs {3380my($self,$new_url,$url,3381$branch_from,$r,$old_ref_id) =@_;3382my$gs= Git::SVN->find_by_url($new_url,$url,$branch_from);3383unless($gs) {3384my$ref_id=$old_ref_id;3385$ref_id=~s/\@\d+-*$//;3386$ref_id.="\@$r";3387# just grow a tail if we're not unique enough :x3388$ref_id.='-'while find_ref($ref_id);3389my($u,$p,$repo_id) = ($new_url,'',$ref_id);3390if($u=~ s#^\Q$url\E(/|$)##) {3391$p=$u;3392$u=$url;3393$repo_id=$self->{repo_id};3394}3395while(1) {3396# It is possible to tag two different subdirectories at3397# the same revision. If the url for an existing ref3398# does not match, we must either find a ref with a3399# matching url or create a new ref by growing a tail.3400$gs= Git::SVN->init($u,$p,$repo_id,$ref_id,1);3401my(undef,$max_commit) =$gs->rev_map_max(1);3402last if(!$max_commit);3403my($url) = ::cmt_metadata($max_commit);3404last if($urleq$gs->metadata_url);3405$ref_id.='-';3406}3407print STDERR "Initializing parent:$ref_id\n"unless$::_q >1;3408}3409$gs3410}34113412sub call_authors_prog {3413my($orig_author) =@_;3414$orig_author= command_oneline('rev-parse','--sq-quote',$orig_author);3415my$author=`$::_authors_prog$orig_author`;3416 if ($?!= 0) {3417 die "$::_authors_prog failed with exit code$?\n"3418 }3419 if ($author=~ /^\s*(.+?)\s*<(.*)>\s*$/) {3420 my ($name,$email) = ($1,$2);3421$email= undef if length$2== 0;3422 return [$name,$email];3423 } else {3424 die "Author:$orig_author: $::_authors_prog returned "3425 . "invalid author format:$author\n";3426 }3427}34283429sub check_author {3430 my ($author) =@_;3431 if (!defined$author|| length$author== 0) {3432$author= '(no author)';3433 }3434 if (!defined $::users{$author}) {3435 if (defined $::_authors_prog) {3436 $::users{$author} = call_authors_prog($author);3437 } elsif (defined $::_authors) {3438 die "Author:$authornot defined in $::_authors file\n";3439 }3440 }3441$author;3442}34433444sub find_extra_svk_parents {3445 my ($self,$ed,$tickets,$parents) =@_;3446 # aha! svk:merge property changed...3447 my@tickets= split "\n",$tickets;3448 my@known_parents;3449 for my$ticket(@tickets) {3450 my ($uuid,$path,$rev) = split /:/,$ticket;3451 if ($uuideq$self->ra_uuid ) {3452 my$url=$self->{url};3453 my$repos_root=$url;3454 my$branch_from=$path;3455$branch_from=~ s{^/}{};3456 my$gs=$self->other_gs($repos_root."/".$branch_from,3457$url,3458$branch_from,3459$rev,3460$self->{ref_id});3461 if ( my$commit=$gs->rev_map_get($rev,$uuid) ) {3462 # wahey! we found it, but it might be3463 # an old one (!)3464 push@known_parents, [$rev,$commit];3465 }3466 }3467 }3468 # Ordering matters; highest-numbered commit merge tickets3469 # first, as they may account for later merge ticket additions3470 # or changes.3471@known_parents= map {$_->[1]} sort {$b->[0] <=>$a->[0]}@known_parents;3472 for my$parent(@known_parents) {3473 my@cmd= ('rev-list',$parent, map { "^$_" }@$parents);3474 my ($msg_fh,$ctx) = command_output_pipe(@cmd);3475 my$new;3476 while ( <$msg_fh> ) {3477$new=1;last;3478 }3479 command_close_pipe($msg_fh,$ctx);3480 if ($new) {3481 print STDERR3482 "Found merge parent (svk:merge ticket):$parent\n";3483 push@$parents,$parent;3484 }3485 }3486}34873488sub lookup_svn_merge {3489 my$uuid= shift;3490 my$url= shift;3491 my$merge= shift;34923493 my ($source,$revs) = split ":",$merge;3494 my$path=$source;3495$path=~ s{^/}{};3496 my$gs= Git::SVN->find_by_url($url.$source,$url,$path);3497 if ( !$gs) {3498 warn "Couldn't find revmap for$url$source\n";3499 return;3500 }3501 my@ranges= split ",",$revs;3502 my ($tip,$tip_commit);3503 my@merged_commit_ranges;3504 # find the tip3505 for my$range(@ranges) {3506 my ($bottom,$top) = split "-",$range;3507$top||=$bottom;3508 my$bottom_commit=$gs->find_rev_after($bottom, 1,$top);3509 my$top_commit=$gs->find_rev_before($top, 1,$bottom);35103511 unless ($top_commitand$bottom_commit) {3512 warn "W:unknown path/rev in svn:mergeinfo "3513 ."dirprop:$source:$range\n";3514 next;3515 }35163517 if (scalar(command('rev-parse', "$bottom_commit^@"))) {3518 push@merged_commit_ranges,3519 "$bottom_commit^..$top_commit";3520 } else {3521 push@merged_commit_ranges, "$top_commit";3522 }35233524 if ( !defined$tipor$top>$tip) {3525$tip=$top;3526$tip_commit=$top_commit;3527 }3528 }3529 return ($tip_commit,@merged_commit_ranges);3530}35313532sub _rev_list {3533 my ($msg_fh,$ctx) = command_output_pipe(3534 "rev-list",@_,3535 );3536 my@rv;3537 while ( <$msg_fh> ) {3538 chomp;3539 push@rv,$_;3540 }3541 command_close_pipe($msg_fh,$ctx);3542@rv;3543}35443545sub check_cherry_pick {3546 my$base= shift;3547 my$tip= shift;3548 my$parents= shift;3549 my@ranges=@_;3550 my%commits= map {$_=> 1 }3551 _rev_list("--no-merges",$tip, "--not",$base,@$parents, "--");3552 for my$range(@ranges) {3553 delete@commits{_rev_list($range, "--")};3554 }3555 for my$commit(keys%commits) {3556 if (has_no_changes($commit)) {3557 delete$commits{$commit};3558 }3559 }3560 return (keys%commits);3561}35623563sub has_no_changes {3564 my$commit= shift;35653566 my@revs= split / /, command_oneline(3567 qw(rev-list --parents -1 -m),$commit);35683569# Commits with no parents, e.g. the start of a partial branch,3570# have changes by definition.3571return1if(@revs<2);35723573# Commits with multiple parents, e.g a merge, have no changes3574# by definition.3575return0if(@revs>2);35763577return(command_oneline("rev-parse","$commit^{tree}")eq3578 command_oneline("rev-parse","$commit~1^{tree}"));3579}35803581# The GIT_DIR environment variable is not always set until after the command3582# line arguments are processed, so we can't memoize in a BEGIN block.3583{3584my$memoized=0;35853586sub memoize_svn_mergeinfo_functions {3587return if$memoized;3588$memoized=1;35893590my$cache_path="$ENV{GIT_DIR}/svn/.caches/";3591 mkpath([$cache_path])unless-d $cache_path;35923593 tie my%lookup_svn_merge_cache=>'Memoize::Storable',3594"$cache_path/lookup_svn_merge.db",'nstore';3595 memoize 'lookup_svn_merge',3596 SCALAR_CACHE =>'FAULT',3597 LIST_CACHE => ['HASH'=> \%lookup_svn_merge_cache],3598;35993600 tie my%check_cherry_pick_cache=>'Memoize::Storable',3601"$cache_path/check_cherry_pick.db",'nstore';3602 memoize 'check_cherry_pick',3603 SCALAR_CACHE =>'FAULT',3604 LIST_CACHE => ['HASH'=> \%check_cherry_pick_cache],3605;36063607 tie my%has_no_changes_cache=>'Memoize::Storable',3608"$cache_path/has_no_changes.db",'nstore';3609 memoize 'has_no_changes',3610 SCALAR_CACHE => ['HASH'=> \%has_no_changes_cache],3611 LIST_CACHE =>'FAULT',3612;3613}36143615sub unmemoize_svn_mergeinfo_functions {3616return ifnot$memoized;3617$memoized=0;36183619 Memoize::unmemoize 'lookup_svn_merge';3620 Memoize::unmemoize 'check_cherry_pick';3621 Memoize::unmemoize 'has_no_changes';3622}36233624 Memoize::memoize 'Git::SVN::repos_root';3625}36263627END{3628# Force cache writeout explicitly instead of waiting for3629# global destruction to avoid segfault in Storable:3630# http://rt.cpan.org/Public/Bug/Display.html?id=360873631 unmemoize_svn_mergeinfo_functions();3632}36333634sub parents_exclude {3635my$parents=shift;3636my@commits=@_;3637return unless@commits;36383639my@excluded;3640my$excluded;3641do{3642my@cmd= ('rev-list',"-1",@commits,"--not",@$parents);3643$excluded= command_oneline(@cmd);3644if($excluded) {3645my@new;3646my$found;3647formy$commit(@commits) {3648if($commiteq$excluded) {3649push@excluded,$commit;3650$found++;3651last;3652}3653else{3654push@new,$commit;3655}3656}3657die"saw commit '$excluded' in rev-list output, "3658."but we didn't ask for that commit (wanted:@commits--not@$parents)"3659unless$found;3660@commits=@new;3661}3662}3663while($excludedand@commits);36643665return@excluded;3666}366736683669# note: this function should only be called if the various dirprops3670# have actually changed3671sub find_extra_svn_parents {3672my($self,$ed,$mergeinfo,$parents) =@_;3673# aha! svk:merge property changed...36743675 memoize_svn_mergeinfo_functions();36763677# We first search for merged tips which are not in our3678# history. Then, we figure out which git revisions are in3679# that tip, but not this revision. If all of those revisions3680# are now marked as merge, we can add the tip as a parent.3681my@merges=split"\n",$mergeinfo;3682my@merge_tips;3683my$url=$self->{url};3684my$uuid=$self->ra_uuid;3685my%ranges;3686formy$merge(@merges) {3687my($tip_commit,@ranges) =3688 lookup_svn_merge($uuid,$url,$merge);3689unless(!$tip_commitor3690grep{$_eq$tip_commit}@$parents) {3691push@merge_tips,$tip_commit;3692$ranges{$tip_commit} = \@ranges;3693}else{3694push@merge_tips,undef;3695}3696}36973698my%excluded=map{$_=>1}3699 parents_exclude($parents,grep{defined}@merge_tips);37003701# check merge tips for new parents3702my@new_parents;3703formy$merge_tip(@merge_tips) {3704my$spec=shift@merges;3705next unless$merge_tipand$excluded{$merge_tip};37063707my$ranges=$ranges{$merge_tip};37083709# check out 'new' tips3710my$merge_base;3711eval{3712$merge_base= command_oneline(3713"merge-base",3714@$parents,$merge_tip,3715);3716};3717if($@) {3718die"An error occurred during merge-base"3719unless$@->isa("Git::Error::Command");37203721warn"W: Cannot find common ancestor between ".3722"@$parentsand$merge_tip. Ignoring merge info.\n";3723next;3724}37253726# double check that there are no missing non-merge commits3727my(@incomplete) = check_cherry_pick(3728$merge_base,$merge_tip,3729$parents,3730@$ranges,3731);37323733if(@incomplete) {3734warn"W:svn cherry-pick ignored ($spec) - missing "3735.@incomplete." commit(s) (eg$incomplete[0])\n";3736}else{3737warn3738"Found merge parent (svn:mergeinfo prop): ",3739$merge_tip,"\n";3740push@new_parents,$merge_tip;3741}3742}37433744# cater for merges which merge commits from multiple branches3745if(@new_parents>1) {3746for(my$i=0;$i<=$#new_parents;$i++) {3747for(my$j=0;$j<=$#new_parents;$j++) {3748next if$i==$j;3749next unless$new_parents[$i];3750next unless$new_parents[$j];3751my$revs= command_oneline(3752"rev-list","-1",3753"$new_parents[$i]..$new_parents[$j]",3754);3755if( !$revs) {3756undef($new_parents[$j]);3757}3758}3759}3760}3761push@$parents,grep{defined}@new_parents;3762}37633764sub make_log_entry {3765my($self,$rev,$parents,$ed) =@_;3766my$untracked=$self->get_untracked($ed);37673768my@parents=@$parents;3769my$ps=$ed->{path_strip} ||"";3770formy$path(grep{m/$ps/} %{$ed->{dir_prop}} ) {3771my$props=$ed->{dir_prop}{$path};3772if($props->{"svk:merge"} ) {3773$self->find_extra_svk_parents3774($ed,$props->{"svk:merge"}, \@parents);3775}3776if($props->{"svn:mergeinfo"} ) {3777$self->find_extra_svn_parents3778($ed,3779$props->{"svn:mergeinfo"},3780 \@parents);3781}3782}37833784open my$un,'>>',"$self->{dir}/unhandled.log"or croak $!;3785print$un"r$rev\n"or croak $!;3786print$un $_,"\n"foreach@$untracked;3787my%log_entry= ( parents => \@parents, revision =>$rev,3788log=>'');37893790my$headrev;3791my$logged=delete$self->{logged_rev_props};3792if(!$logged||$self->{-want_revprops}) {3793my$rp=$self->ra->rev_proplist($rev);3794foreach(sort keys%$rp) {3795my$v=$rp->{$_};3796if(/^svn:(author|date|log)$/) {3797$log_entry{$1} =$v;3798}elsif($_eq'svm:headrev') {3799$headrev=$v;3800}else{3801print$un" rev_prop: ", uri_encode($_),' ',3802 uri_encode($v),"\n";3803}3804}3805}else{3806map{$log_entry{$_} =$logged->{$_} }keys%$logged;3807}3808close$unor croak $!;38093810$log_entry{date} = parse_svn_date($log_entry{date});3811$log_entry{log} .="\n";3812my$author=$log_entry{author} = check_author($log_entry{author});3813my($name,$email) =defined$::users{$author} ? @{$::users{$author}}3814: ($author,undef);38153816my($commit_name,$commit_email) = ($name,$email);3817if($_use_log_author) {3818my$name_field;3819if($log_entry{log} =~/From:\s+(.*\S)\s*\n/i) {3820$name_field=$1;3821}elsif($log_entry{log} =~/Signed-off-by:\s+(.*\S)\s*\n/i) {3822$name_field=$1;3823}3824if(!defined$name_field) {3825if(!defined$email) {3826$email=$name;3827}3828}elsif($name_field=~/(.*?)\s+<(.*)>/) {3829($name,$email) = ($1,$2);3830}elsif($name_field=~/(.*)@/) {3831($name,$email) = ($1,$name_field);3832}else{3833($name,$email) = ($name_field,$name_field);3834}3835}3836if(defined$headrev&&$self->use_svm_props) {3837if($self->rewrite_root) {3838die"Can't have both 'useSvmProps' and 'rewriteRoot' ",3839"options set!\n";3840}3841if($self->rewrite_uuid) {3842die"Can't have both 'useSvmProps' and 'rewriteUUID' ",3843"options set!\n";3844}3845my($uuid,$r) =$headrev=~m{^([a-f\d\-]{30,}):(\d+)$}i;3846# we don't want "SVM: initializing mirror for junk" ...3847returnundefif$r==0;3848my$svm=$self->svm;3849if($uuidne$svm->{uuid}) {3850die"UUID mismatch on SVM path:\n",3851"expected:$svm->{uuid}\n",3852" got:$uuid\n";3853}3854my$full_url=$self->full_url;3855$full_url=~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or3856die"Failed to replace '$svm->{replace}' with ",3857"'$svm->{source}' in$full_url\n";3858# throw away username for storing in records3859 remove_username($full_url);3860$log_entry{metadata} ="$full_url\@$r$uuid";3861$log_entry{svm_revision} =$r;3862$email||="$author\@$uuid";3863$commit_email||="$author\@$uuid";3864}elsif($self->use_svnsync_props) {3865my$full_url=$self->svnsync->{url};3866$full_url.="/$self->{path}"iflength$self->{path};3867 remove_username($full_url);3868my$uuid=$self->svnsync->{uuid};3869$log_entry{metadata} ="$full_url\@$rev$uuid";3870$email||="$author\@$uuid";3871$commit_email||="$author\@$uuid";3872}else{3873my$url=$self->metadata_url;3874 remove_username($url);3875my$uuid=$self->rewrite_uuid||$self->ra->get_uuid;3876$log_entry{metadata} ="$url\@$rev".$uuid;3877$email||="$author\@".$uuid;3878$commit_email||="$author\@".$uuid;3879}3880$log_entry{name} =$name;3881$log_entry{email} =$email;3882$log_entry{commit_name} =$commit_name;3883$log_entry{commit_email} =$commit_email;3884 \%log_entry;3885}38863887sub fetch {3888my($self,$min_rev,$max_rev,@parents) =@_;3889my($last_rev,$last_commit) =$self->last_rev_commit;3890my($base,$head) =$self->get_fetch_range($min_rev,$max_rev);3891$self->ra->gs_fetch_loop_common($base,$head, [$self]);3892}38933894sub set_tree_cb {3895my($self,$log_entry,$tree,$rev,$date,$author) =@_;3896$self->{inject_parents} = {$rev=>$tree};3897$self->fetch(undef,undef);3898}38993900sub set_tree {3901my($self,$tree) = (shift,shift);3902my$log_entry= ::get_commit_entry($tree);3903unless($self->{last_rev}) {3904::fatal("Must have an existing revision to commit");3905}3906my%ed_opts= ( r =>$self->{last_rev},3907log=>$log_entry->{log},3908 ra =>$self->ra,3909 tree_a =>$self->{last_commit},3910 tree_b =>$tree,3911 editor_cb =>sub{3912$self->set_tree_cb($log_entry,$tree,@_) },3913 svn_path =>$self->{path} );3914if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {3915print"No changes\nr$self->{last_rev} =$tree\n";3916}3917}39183919sub rebuild_from_rev_db {3920my($self,$path) =@_;3921my$r= -1;3922open my$fh,'<',$pathor croak "open:$!";3923binmode$fhor croak "binmode:$!";3924while(<$fh>) {3925length($_) ==41or croak "inconsistent size in ($_) != 41";3926chomp($_);3927++$r;3928next if$_eq('0' x 40);3929$self->rev_map_set($r,$_);3930print"r$r=$_\n";3931}3932close$fhor croak "close:$!";3933unlink$pathor croak "unlink:$!";3934}39353936sub rebuild {3937my($self) =@_;3938my$map_path=$self->map_path;3939my$partial= (-e $map_path&& ! -z $map_path);3940return unless::verify_ref($self->refname.'^0');3941if(!$partial&& ($self->use_svm_props||$self->no_metadata)) {3942my$rev_db=$self->rev_db_path;3943$self->rebuild_from_rev_db($rev_db);3944if($self->use_svm_props) {3945my$svm_rev_db=$self->rev_db_path($self->svm_uuid);3946$self->rebuild_from_rev_db($svm_rev_db);3947}3948$self->unlink_rev_db_symlink;3949return;3950}3951print"Rebuilding$map_path...\n"if(!$partial);3952my($base_rev,$head) = ($partial?$self->rev_map_max_norebuild(1) :3953(undef,undef));3954my($log,$ctx) =3955 command_output_pipe(qw/rev-list --pretty=raw --reverse/,3956($head?"$head..":"") .$self->refname,3957'--');3958my$metadata_url=$self->metadata_url;3959 remove_username($metadata_url);3960my$svn_uuid=$self->rewrite_uuid||$self->ra_uuid;3961my$c;3962while(<$log>) {3963if(m{^commit ($::sha1)$}) {3964$c=$1;3965next;3966}3967next unlesss{^\s*(git-svn-id:)}{$1};3968my($url,$rev,$uuid) = ::extract_metadata($_);3969 remove_username($url);39703971# ignore merges (from set-tree)3972next if(!defined$rev|| !$uuid);39733974# if we merged or otherwise started elsewhere, this is3975# how we break out of it3976if(($uuidne$svn_uuid) ||3977($metadata_url&&$url&& ($urlne$metadata_url))) {3978next;3979}3980if($partial&&$head) {3981print"Partial-rebuilding$map_path...\n";3982print"Currently at$base_rev=$head\n";3983$head=undef;3984}39853986$self->rev_map_set($rev,$c);3987print"r$rev=$c\n";3988}3989 command_close_pipe($log,$ctx);3990print"Done rebuilding$map_path\n"if(!$partial|| !$head);3991my$rev_db_path=$self->rev_db_path;3992if(-f $self->rev_db_path) {3993unlink$self->rev_db_pathor croak "unlink:$!";3994}3995$self->unlink_rev_db_symlink;3996}39973998# rev_map:3999# Tie::File seems to be prone to offset errors if revisions get sparse,4000# it's not that fast, either. Tie::File is also not in Perl 5.6. So4001# one of my favorite modules is out :< Next up would be one of the DBM4002# modules, but I'm not sure which is most portable...4003#4004# This is the replacement for the rev_db format, which was too big4005# and inefficient for large repositories with a lot of sparse history4006# (mainly tags)4007#4008# The format is this:4009# - 24 bytes for every record,4010# * 4 bytes for the integer representing an SVN revision number4011# * 20 bytes representing the sha1 of a git commit4012# - No empty padding records like the old format4013# (except the last record, which can be overwritten)4014# - new records are written append-only since SVN revision numbers4015# increase monotonically4016# - lookups on SVN revision number are done via a binary search4017# - Piping the file to xxd -c24 is a good way of dumping it for4018# viewing or editing (piped back through xxd -r), should the need4019# ever arise.4020# - The last record can be padding revision with an all-zero sha14021# This is used to optimize fetch performance when using multiple4022# "fetch" directives in .git/config4023#4024# These files are disposable unless noMetadata or useSvmProps is set40254026sub _rev_map_set {4027my($fh,$rev,$commit) =@_;40284029binmode$fhor croak "binmode:$!";4030my$size= (stat($fh))[7];4031($size%24) ==0or croak "inconsistent size:$size";40324033my$wr_offset=0;4034if($size>0) {4035sysseek($fh, -24, SEEK_END)or croak "seek:$!";4036my$read=sysread($fh,my$buf,24)or croak "read:$!";4037$read==24or croak "read only$readbytes (!= 24)";4038my($last_rev,$last_commit) =unpack(rev_map_fmt,$buf);4039if($last_commiteq('0' x40)) {4040if($size>=48) {4041sysseek($fh, -48, SEEK_END)or croak "seek:$!";4042$read=sysread($fh,$buf,24)or4043 croak "read:$!";4044$read==24or4045 croak "read only$readbytes (!= 24)";4046($last_rev,$last_commit) =4047unpack(rev_map_fmt,$buf);4048if($last_commiteq('0' x40)) {4049 croak "inconsistent .rev_map\n";4050}4051}4052if($last_rev>=$rev) {4053 croak "last_rev is higher!:$last_rev>=$rev";4054}4055$wr_offset= -24;4056}4057}4058sysseek($fh,$wr_offset, SEEK_END)or croak "seek:$!";4059syswrite($fh,pack(rev_map_fmt,$rev,$commit),24) ==24or4060 croak "write:$!";4061}40624063sub _rev_map_reset {4064my($fh,$rev,$commit) =@_;4065my$c= _rev_map_get($fh,$rev);4066$ceq$commitor die"_rev_map_reset(@_) commit$cdoes not match!\n";4067my$offset=sysseek($fh,0, SEEK_CUR)or croak "seek:$!";4068truncate$fh,$offsetor croak "truncate:$!";4069}40704071sub mkfile {4072my($path) =@_;4073unless(-e $path) {4074my($dir,$base) = ($path=~ m#^(.*?)/?([^/]+)$#);4075 mkpath([$dir])unless-d $dir;4076open my$fh,'>>',$pathor die"Couldn't create$path:$!\n";4077close$fhor die"Couldn't close (create)$path:$!\n";4078}4079}40804081sub rev_map_set {4082my($self,$rev,$commit,$update_ref,$uuid) =@_;4083defined$commitor die"missing arg3\n";4084length$commit==40or die"arg3 must be a full SHA1 hexsum\n";4085my$db=$self->map_path($uuid);4086my$db_lock="$db.lock";4087my$sigmask;4088$update_ref||=0;4089if($update_ref) {4090$sigmask= POSIX::SigSet->new();4091my$signew= POSIX::SigSet->new(SIGINT, SIGHUP, SIGTERM,4092 SIGALRM, SIGUSR1, SIGUSR2);4093 sigprocmask(SIG_BLOCK,$signew,$sigmask)or4094 croak "Can't block signals:$!";4095}4096 mkfile($db);40974098$LOCKFILES{$db_lock} =1;4099my$sync;4100# both of these options make our .rev_db file very, very important4101# and we can't afford to lose it because rebuild() won't work4102if($self->use_svm_props||$self->no_metadata) {4103$sync=1;4104 copy($db,$db_lock)or die"rev_map_set(@_): ",4105"Failed to copy: ",4106"$db=>$db_lock($!)\n";4107}else{4108rename$db,$db_lockor die"rev_map_set(@_): ",4109"Failed to rename: ",4110"$db=>$db_lock($!)\n";4111}41124113sysopen(my$fh,$db_lock, O_RDWR | O_CREAT)4114or croak "Couldn't open$db_lock:$!\n";4115$update_refeq'reset'? _rev_map_reset($fh,$rev,$commit) :4116 _rev_map_set($fh,$rev,$commit);4117if($sync) {4118$fh->flushor die"Couldn't flush$db_lock:$!\n";4119$fh->syncor die"Couldn't sync$db_lock:$!\n";4120}4121close$fhor croak $!;4122if($update_ref) {4123$_head=$self;4124my$note="";4125$note=" ($update_ref)"if($update_ref!~/^\d*$/);4126 command_noisy('update-ref','-m',"r$rev$note",4127$self->refname,$commit);4128}4129rename$db_lock,$dbor die"rev_map_set(@_): ","Failed to rename: ",4130"$db_lock=>$db($!)\n";4131delete$LOCKFILES{$db_lock};4132if($update_ref) {4133 sigprocmask(SIG_SETMASK,$sigmask)or4134 croak "Can't restore signal mask:$!";4135}4136}41374138# If want_commit, this will return an array of (rev, commit) where4139# commit _must_ be a valid commit in the archive.4140# Otherwise, it'll return the max revision (whether or not the4141# commit is valid or just a 0x40 placeholder).4142sub rev_map_max {4143my($self,$want_commit) =@_;4144$self->rebuild;4145my($r,$c) =$self->rev_map_max_norebuild($want_commit);4146$want_commit? ($r,$c) :$r;4147}41484149sub rev_map_max_norebuild {4150my($self,$want_commit) =@_;4151my$map_path=$self->map_path;4152stat$map_pathorreturn$want_commit? (0,undef) :0;4153sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4154binmode$fhor croak "binmode:$!";4155my$size= (stat($fh))[7];4156($size%24) ==0or croak "inconsistent size:$size";41574158if($size==0) {4159close$fhor croak "close:$!";4160return$want_commit? (0,undef) :0;4161}41624163sysseek($fh, -24, SEEK_END)or croak "seek:$!";4164sysread($fh,my$buf,24) ==24or croak "read:$!";4165my($r,$c) =unpack(rev_map_fmt,$buf);4166if($want_commit&&$ceq('0' x40)) {4167if($size<48) {4168return$want_commit? (0,undef) :0;4169}4170sysseek($fh, -48, SEEK_END)or croak "seek:$!";4171sysread($fh,$buf,24) ==24or croak "read:$!";4172($r,$c) =unpack(rev_map_fmt,$buf);4173if($ceq('0'x40)) {4174 croak "Penultimate record is all-zeroes in$map_path";4175}4176}4177close$fhor croak "close:$!";4178$want_commit? ($r,$c) :$r;4179}41804181sub rev_map_get {4182my($self,$rev,$uuid) =@_;4183my$map_path=$self->map_path($uuid);4184returnundefunless-e $map_path;41854186sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4187my$c= _rev_map_get($fh,$rev);4188close($fh)or croak "close:$!";4189$c4190}41914192sub _rev_map_get {4193my($fh,$rev) =@_;41944195binmode$fhor croak "binmode:$!";4196my$size= (stat($fh))[7];4197($size%24) ==0or croak "inconsistent size:$size";41984199if($size==0) {4200returnundef;4201}42024203my($l,$u) = (0,$size-24);4204my($r,$c,$buf);42054206while($l<=$u) {4207my$i=int(($l/24+$u/24) /2) *24;4208sysseek($fh,$i, SEEK_SET)or croak "seek:$!";4209sysread($fh,my$buf,24) ==24or croak "read:$!";4210my($r,$c) =unpack(rev_map_fmt,$buf);42114212if($r<$rev) {4213$l=$i+24;4214}elsif($r>$rev) {4215$u=$i-24;4216}else{# $r == $rev4217return$ceq('0' x 40) ?undef:$c;4218}4219}4220undef;4221}42224223# Finds the first svn revision that exists on (if $eq_ok is true) or4224# before $rev for the current branch. It will not search any lower4225# than $min_rev. Returns the git commit hash and svn revision number4226# if found, else (undef, undef).4227sub find_rev_before {4228my($self,$rev,$eq_ok,$min_rev) =@_;4229--$revunless$eq_ok;4230$min_rev||=1;4231my$max_rev=$self->rev_map_max;4232$rev=$max_revif($rev>$max_rev);4233while($rev>=$min_rev) {4234if(my$c=$self->rev_map_get($rev)) {4235return($rev,$c);4236}4237--$rev;4238}4239return(undef,undef);4240}42414242# Finds the first svn revision that exists on (if $eq_ok is true) or4243# after $rev for the current branch. It will not search any higher4244# than $max_rev. Returns the git commit hash and svn revision number4245# if found, else (undef, undef).4246sub find_rev_after {4247my($self,$rev,$eq_ok,$max_rev) =@_;4248++$revunless$eq_ok;4249$max_rev||=$self->rev_map_max;4250while($rev<=$max_rev) {4251if(my$c=$self->rev_map_get($rev)) {4252return($rev,$c);4253}4254++$rev;4255}4256return(undef,undef);4257}42584259sub _new {4260my($class,$repo_id,$ref_id,$path) =@_;4261unless(defined$repo_id&&length$repo_id) {4262$repo_id=$Git::SVN::default_repo_id;4263}4264unless(defined$ref_id&&length$ref_id) {4265$_prefix=''unlessdefined($_prefix);4266$_[2] =$ref_id=4267"refs/remotes/$_prefix$Git::SVN::default_ref_id";4268}4269$_[1] =$repo_id;4270my$dir="$ENV{GIT_DIR}/svn/$ref_id";42714272# Older repos imported by us used $GIT_DIR/svn/foo instead of4273# $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo4274if($ref_id=~m{^refs/remotes/(.*)}) {4275my$old_dir="$ENV{GIT_DIR}/svn/$1";4276if(-d $old_dir&& ! -d $dir) {4277$dir=$old_dir;4278}4279}42804281$_[3] =$path=''unless(defined$path);4282 mkpath([$dir]);4283bless{4284 ref_id =>$ref_id, dir =>$dir,index=>"$dir/index",4285 path =>$path, config =>"$ENV{GIT_DIR}/svn/config",4286 map_root =>"$dir/.rev_map", repo_id =>$repo_id},$class;4287}42884289# for read-only access of old .rev_db formats4290sub unlink_rev_db_symlink {4291my($self) =@_;4292my$link=$self->rev_db_path;4293$link=~s/\.[\w-]+$//or croak "missing UUID at the end of$link";4294if(-l $link) {4295unlink$linkor croak "unlink:$linkfailed!";4296}4297}42984299sub rev_db_path {4300my($self,$uuid) =@_;4301my$db_path=$self->map_path($uuid);4302$db_path=~s{/\.rev_map\.}{/\.rev_db\.}4303or croak "map_path:$db_pathdoes not contain '/.rev_map.' !";4304$db_path;4305}43064307# the new replacement for .rev_db4308sub map_path {4309my($self,$uuid) =@_;4310$uuid||=$self->ra_uuid;4311"$self->{map_root}.$uuid";4312}43134314sub uri_encode {4315my($f) =@_;4316$f=~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;4317$f4318}43194320sub uri_decode {4321my($f) =@_;4322$f=~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;4323$f4324}43254326sub remove_username {4327$_[0] =~s{^([^:]*://)[^@]+@}{$1};4328}43294330package Git::SVN::Prompt;4331use strict;4332use warnings;4333require SVN::Core;4334use vars qw/$_no_auth_cache $_username/;43354336sub simple {4337my($cred,$realm,$default_username,$may_save,$pool) =@_;4338$may_save=undefif$_no_auth_cache;4339$default_username=$_usernameifdefined$_username;4340if(defined$default_username&&length$default_username) {4341if(defined$realm&&length$realm) {4342print STDERR "Authentication realm:$realm\n";4343 STDERR->flush;4344}4345$cred->username($default_username);4346}else{4347 username($cred,$realm,$may_save,$pool);4348}4349$cred->password(_read_password("Password for '".4350$cred->username."': ",$realm));4351$cred->may_save($may_save);4352$SVN::_Core::SVN_NO_ERROR;4353}43544355sub ssl_server_trust {4356my($cred,$realm,$failures,$cert_info,$may_save,$pool) =@_;4357$may_save=undefif$_no_auth_cache;4358print STDERR "Error validating server certificate for '$realm':\n";4359{4360no warnings 'once';4361# All variables SVN::Auth::SSL::* are used only once,4362# so we're shutting up Perl warnings about this.4363if($failures&$SVN::Auth::SSL::UNKNOWNCA) {4364print STDERR " - The certificate is not issued ",4365"by a trusted authority. Use the\n",4366" fingerprint to validate ",4367"the certificate manually!\n";4368}4369if($failures&$SVN::Auth::SSL::CNMISMATCH) {4370print STDERR " - The certificate hostname ",4371"does not match.\n";4372}4373if($failures&$SVN::Auth::SSL::NOTYETVALID) {4374print STDERR " - The certificate is not yet valid.\n";4375}4376if($failures&$SVN::Auth::SSL::EXPIRED) {4377print STDERR " - The certificate has expired.\n";4378}4379if($failures&$SVN::Auth::SSL::OTHER) {4380print STDERR " - The certificate has ",4381"an unknown error.\n";4382}4383}# no warnings 'once'4384printf STDERR4385"Certificate information:\n".4386" - Hostname:%s\n".4387" - Valid: from%suntil%s\n".4388" - Issuer:%s\n".4389" - Fingerprint:%s\n",4390map$cert_info->$_,qw(hostname valid_from valid_until4391 issuer_dname fingerprint);4392my$choice;4393prompt:4394print STDERR $may_save?4395"(R)eject, accept (t)emporarily or accept (p)ermanently? ":4396"(R)eject or accept (t)emporarily? ";4397 STDERR->flush;4398$choice=lc(substr(<STDIN> ||'R',0,1));4399if($choice=~/^t$/i) {4400$cred->may_save(undef);4401}elsif($choice=~/^r$/i) {4402return-1;4403}elsif($may_save&&$choice=~/^p$/i) {4404$cred->may_save($may_save);4405}else{4406goto prompt;4407}4408$cred->accepted_failures($failures);4409$SVN::_Core::SVN_NO_ERROR;4410}44114412sub ssl_client_cert {4413my($cred,$realm,$may_save,$pool) =@_;4414$may_save=undefif$_no_auth_cache;4415print STDERR "Client certificate filename: ";4416 STDERR->flush;4417chomp(my$filename= <STDIN>);4418$cred->cert_file($filename);4419$cred->may_save($may_save);4420$SVN::_Core::SVN_NO_ERROR;4421}44224423sub ssl_client_cert_pw {4424my($cred,$realm,$may_save,$pool) =@_;4425$may_save=undefif$_no_auth_cache;4426$cred->password(_read_password("Password: ",$realm));4427$cred->may_save($may_save);4428$SVN::_Core::SVN_NO_ERROR;4429}44304431sub username {4432my($cred,$realm,$may_save,$pool) =@_;4433$may_save=undefif$_no_auth_cache;4434if(defined$realm&&length$realm) {4435print STDERR "Authentication realm:$realm\n";4436}4437my$username;4438if(defined$_username) {4439$username=$_username;4440}else{4441print STDERR "Username: ";4442 STDERR->flush;4443chomp($username= <STDIN>);4444}4445$cred->username($username);4446$cred->may_save($may_save);4447$SVN::_Core::SVN_NO_ERROR;4448}44494450sub _read_password {4451my($prompt,$realm) =@_;4452my$password='';4453if(exists$ENV{GIT_ASKPASS}) {4454open(PH,"-|",$ENV{GIT_ASKPASS},$prompt);4455$password= <PH>;4456$password=~s/[\012\015]//;# \n\r4457close(PH);4458}else{4459print STDERR $prompt;4460 STDERR->flush;4461require Term::ReadKey;4462 Term::ReadKey::ReadMode('noecho');4463while(defined(my$key= Term::ReadKey::ReadKey(0))) {4464last if$key=~/[\012\015]/;# \n\r4465$password.=$key;4466}4467 Term::ReadKey::ReadMode('restore');4468print STDERR "\n";4469 STDERR->flush;4470}4471$password;4472}44734474package SVN::Git::Fetcher;4475use vars qw/@ISA $_ignore_regex $_preserve_empty_dirs $_placeholder_filename4476@deleted_gpath %added_placeholder $repo_id/;4477use strict;4478use warnings;4479use Carp qw/croak/;4480use File::Basename qw/dirname/;4481use IO::File qw//;44824483# file baton members: path, mode_a, mode_b, pool, fh, blob, base4484sub new {4485my($class,$git_svn,$switch_path) =@_;4486my$self= SVN::Delta::Editor->new;4487bless$self,$class;4488if(exists$git_svn->{last_commit}) {4489$self->{c} =$git_svn->{last_commit};4490$self->{empty_symlinks} =4491 _mark_empty_symlinks($git_svn,$switch_path);4492}44934494# some options are read globally, but can be overridden locally4495# per [svn-remote "..."] section. Command-line options will *NOT*4496# override options set in an [svn-remote "..."] section4497$repo_id=$git_svn->{repo_id};4498my$k="svn-remote.$repo_id.ignore-paths";4499my$v=eval{ command_oneline('config','--get',$k) };4500$self->{ignore_regex} =$v;45014502$k="svn-remote.$repo_id.preserve-empty-dirs";4503$v=eval{ command_oneline('config','--get','--bool',$k) };4504if($v&&$veq'true') {4505$_preserve_empty_dirs=1;4506$k="svn-remote.$repo_id.placeholder-filename";4507$v=eval{ command_oneline('config','--get',$k) };4508$_placeholder_filename=$v;4509}45104511# Load the list of placeholder files added during previous invocations.4512$k="svn-remote.$repo_id.added-placeholder";4513$v=eval{ command_oneline('config','--get-all',$k) };4514if($_preserve_empty_dirs&&$v) {4515# command() prints errors to stderr, so we only call it if4516# command_oneline() succeeded.4517my@v= command('config','--get-all',$k);4518$added_placeholder{ dirname($_) } =$_foreach@v;4519}45204521$self->{empty} = {};4522$self->{dir_prop} = {};4523$self->{file_prop} = {};4524$self->{absent_dir} = {};4525$self->{absent_file} = {};4526$self->{gii} =$git_svn->tmp_index_do(sub{ Git::IndexInfo->new});4527$self->{pathnameencoding} = Git::config('svn.pathnameencoding');4528$self;4529}45304531# this uses the Ra object, so it must be called before do_{switch,update},4532# not inside them (when the Git::SVN::Fetcher object is passed) to4533# do_{switch,update}4534sub _mark_empty_symlinks {4535my($git_svn,$switch_path) =@_;4536my$bool= Git::config_bool('svn.brokenSymlinkWorkaround');4537return{}if(!defined($bool)) || (defined($bool) && !$bool);45384539my%ret;4540my($rev,$cmt) =$git_svn->last_rev_commit;4541return{}unless($rev&&$cmt);45424543# allow the warning to be printed for each revision we fetch to4544# ensure the user sees it. The user can also disable the workaround4545# on the repository even while git svn is running and the next4546# revision fetched will skip this expensive function.4547my$printed_warning;4548chomp(my$empty_blob=`git hash-object -t blob --stdin < /dev/null`);4549my($ls,$ctx) = command_output_pipe(qw/ls-tree -r -z/,$cmt);4550local$/="\0";4551my$pfx=defined($switch_path) ?$switch_path:$git_svn->{path};4552$pfx.='/'iflength($pfx);4553while(<$ls>) {4554chomp;4555s/\A100644 blob $empty_blob\t//oornext;4556unless($printed_warning) {4557print STDERR "Scanning for empty symlinks, ",4558"this may take a while if you have ",4559"many empty files\n",4560"You may disable this with `",4561"git config svn.brokenSymlinkWorkaround ",4562"false'.\n",4563"This may be done in a different ",4564"terminal without restarting ",4565"git svn\n";4566$printed_warning=1;4567}4568my$path=$_;4569my(undef,$props) =4570$git_svn->ra->get_file($pfx.$path,$rev,undef);4571if($props->{'svn:special'}) {4572$ret{$path} =1;4573}4574}4575 command_close_pipe($ls,$ctx);4576 \%ret;4577}45784579# returns true if a given path is inside a ".git" directory4580sub in_dot_git {4581$_[0] =~m{(?:^|/)\.git(?:/|$)};4582}45834584# return value: 0 -- don't ignore, 1 -- ignore4585sub is_path_ignored {4586my($self,$path) =@_;4587return1if in_dot_git($path);4588return1ifdefined($self->{ignore_regex}) &&4589$path=~m!$self->{ignore_regex}!;4590return0unlessdefined($_ignore_regex);4591return1if$path=~m!$_ignore_regex!o;4592return0;4593}45944595sub set_path_strip {4596my($self,$path) =@_;4597$self->{path_strip} =qr/^\Q$path\E(\/|$)/iflength$path;4598}45994600sub open_root {4601{ path =>''};4602}46034604sub open_directory {4605my($self,$path,$pb,$rev) =@_;4606{ path =>$path};4607}46084609sub git_path {4610my($self,$path) =@_;4611if(my$enc=$self->{pathnameencoding}) {4612require Encode;4613 Encode::from_to($path,'UTF-8',$enc);4614}4615if($self->{path_strip}) {4616$path=~s!$self->{path_strip}!!or4617die"Failed to strip path '$path' ($self->{path_strip})\n";4618}4619$path;4620}46214622sub delete_entry {4623my($self,$path,$rev,$pb) =@_;4624returnundefif$self->is_path_ignored($path);46254626my$gpath=$self->git_path($path);4627returnundefif($gpatheq'');46284629# remove entire directories.4630my($tree) = (command('ls-tree','-z',$self->{c},"./$gpath")4631=~/\A040000 tree ([a-f\d]{40})\t\Q$gpath\E\0/);4632if($tree) {4633my($ls,$ctx) = command_output_pipe(qw/ls-tree4634-r --name-only -z/,4635$tree);4636local$/="\0";4637while(<$ls>) {4638chomp;4639my$rmpath="$gpath/$_";4640$self->{gii}->remove($rmpath);4641print"\tD\t$rmpath\n"unless$::_q;4642}4643print"\tD\t$gpath/\n"unless$::_q;4644 command_close_pipe($ls,$ctx);4645}else{4646$self->{gii}->remove($gpath);4647print"\tD\t$gpath\n"unless$::_q;4648}4649# Don't add to @deleted_gpath if we're deleting a placeholder file.4650push@deleted_gpath,$gpathunless$added_placeholder{dirname($path)};4651$self->{empty}->{$path} =0;4652undef;4653}46544655sub open_file {4656my($self,$path,$pb,$rev) =@_;4657my($mode,$blob);46584659goto out if$self->is_path_ignored($path);46604661my$gpath=$self->git_path($path);4662($mode,$blob) = (command('ls-tree','-z',$self->{c},"./$gpath")4663=~/\A(\d{6}) blob ([a-f\d]{40})\t\Q$gpath\E\0/);4664unless(defined$mode&&defined$blob) {4665die"$pathwas not found in commit$self->{c} (r$rev)\n";4666}4667if($modeeq'100644'&&$self->{empty_symlinks}->{$path}) {4668$mode='120000';4669}4670out:4671{ path =>$path, mode_a =>$mode, mode_b =>$mode, blob =>$blob,4672 pool => SVN::Pool->new, action =>'M'};4673}46744675sub add_file {4676my($self,$path,$pb,$cp_path,$cp_rev) =@_;4677my$mode;46784679if(!$self->is_path_ignored($path)) {4680my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4681delete$self->{empty}->{$dir};4682$mode='100644';46834684if($added_placeholder{$dir}) {4685# Remove our placeholder file, if we created one.4686 delete_entry($self,$added_placeholder{$dir})4687unless$patheq$added_placeholder{$dir};4688delete$added_placeholder{$dir}4689}4690}46914692{ path =>$path, mode_a =>$mode, mode_b =>$mode,4693 pool => SVN::Pool->new, action =>'A'};4694}46954696sub add_directory {4697my($self,$path,$cp_path,$cp_rev) =@_;4698goto out if$self->is_path_ignored($path);4699my$gpath=$self->git_path($path);4700if($gpatheq'') {4701my($ls,$ctx) = command_output_pipe(qw/ls-tree4702-r --name-only -z/,4703$self->{c});4704local$/="\0";4705while(<$ls>) {4706chomp;4707$self->{gii}->remove($_);4708print"\tD\t$_\n"unless$::_q;4709push@deleted_gpath,$gpath;4710}4711 command_close_pipe($ls,$ctx);4712$self->{empty}->{$path} =0;4713}4714my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4715delete$self->{empty}->{$dir};4716$self->{empty}->{$path} =1;47174718if($added_placeholder{$dir}) {4719# Remove our placeholder file, if we created one.4720 delete_entry($self,$added_placeholder{$dir});4721delete$added_placeholder{$dir}4722}47234724out:4725{ path =>$path};4726}47274728sub change_dir_prop {4729my($self,$db,$prop,$value) =@_;4730returnundefif$self->is_path_ignored($db->{path});4731$self->{dir_prop}->{$db->{path}} ||= {};4732$self->{dir_prop}->{$db->{path}}->{$prop} =$value;4733undef;4734}47354736sub absent_directory {4737my($self,$path,$pb) =@_;4738returnundefif$self->is_path_ignored($path);4739$self->{absent_dir}->{$pb->{path}} ||= [];4740push@{$self->{absent_dir}->{$pb->{path}}},$path;4741undef;4742}47434744sub absent_file {4745my($self,$path,$pb) =@_;4746returnundefif$self->is_path_ignored($path);4747$self->{absent_file}->{$pb->{path}} ||= [];4748push@{$self->{absent_file}->{$pb->{path}}},$path;4749undef;4750}47514752sub change_file_prop {4753my($self,$fb,$prop,$value) =@_;4754returnundefif$self->is_path_ignored($fb->{path});4755if($propeq'svn:executable') {4756if($fb->{mode_b} !=120000) {4757$fb->{mode_b} =defined$value?100755:100644;4758}4759}elsif($propeq'svn:special') {4760$fb->{mode_b} =defined$value?120000:100644;4761}else{4762$self->{file_prop}->{$fb->{path}} ||= {};4763$self->{file_prop}->{$fb->{path}}->{$prop} =$value;4764}4765undef;4766}47674768sub apply_textdelta {4769my($self,$fb,$exp) =@_;4770returnundefif$self->is_path_ignored($fb->{path});4771my$fh= $::_repository->temp_acquire('svn_delta');4772# $fh gets auto-closed() by SVN::TxDelta::apply(),4773# (but $base does not,) so dup() it for reading in close_file4774open my$dup,'<&',$fhor croak $!;4775my$base= $::_repository->temp_acquire('git_blob');47764777if($fb->{blob}) {4778my($base_is_link,$size);47794780if($fb->{mode_a}eq'120000'&&4781!$self->{empty_symlinks}->{$fb->{path}}) {4782print$base'link 'or die"print$!\n";4783$base_is_link=1;4784}4785 retry:4786$size= $::_repository->cat_blob($fb->{blob},$base);4787die"Failed to read object$fb->{blob}"if($size<0);47884789if(defined$exp) {4790seek$base,0,0or croak $!;4791my$got= ::md5sum($base);4792if($gotne$exp) {4793my$err="Checksum mismatch: ".4794"$fb->{path}$fb->{blob}\n".4795"expected:$exp\n".4796" got:$got\n";4797if($base_is_link) {4798warn$err,4799"Retrying... (possibly ",4800"a bad symlink from SVN)\n";4801$::_repository->temp_reset($base);4802$base_is_link=0;4803goto retry;4804}4805die$err;4806}4807}4808}4809seek$base,0,0or croak $!;4810$fb->{fh} =$fh;4811$fb->{base} =$base;4812[ SVN::TxDelta::apply($base,$dup,undef,$fb->{path},$fb->{pool}) ];4813}48144815sub close_file {4816my($self,$fb,$exp) =@_;4817returnundefif$self->is_path_ignored($fb->{path});48184819my$hash;4820my$path=$self->git_path($fb->{path});4821if(my$fh=$fb->{fh}) {4822if(defined$exp) {4823seek($fh,0,0)or croak $!;4824my$got= ::md5sum($fh);4825if($gotne$exp) {4826die"Checksum mismatch:$path\n",4827"expected:$exp\ngot:$got\n";4828}4829}4830if($fb->{mode_b} ==120000) {4831sysseek($fh,0,0)or croak $!;4832my$rd=sysread($fh,my$buf,5);48334834if(!defined$rd) {4835 croak "sysread:$!\n";4836}elsif($rd==0) {4837warn"$pathhas mode 120000",4838" but it points to nothing\n",4839"converting to an empty file with mode",4840" 100644\n";4841$fb->{mode_b} ='100644';4842}elsif($bufne'link ') {4843warn"$pathhas mode 120000",4844" but is not a link\n";4845}else{4846my$tmp_fh= $::_repository->temp_acquire(4847'svn_hash');4848my$res;4849while($res=sysread($fh,my$str,1024)) {4850my$out=syswrite($tmp_fh,$str,$res);4851defined($out) &&$out==$res4852or croak("write ",4853 Git::temp_path($tmp_fh),4854":$!\n");4855}4856defined$resor croak $!;48574858($fh,$tmp_fh) = ($tmp_fh,$fh);4859 Git::temp_release($tmp_fh,1);4860}4861}48624863$hash= $::_repository->hash_and_insert_object(4864 Git::temp_path($fh));4865$hash=~/^[a-f\d]{40}$/or die"not a sha1:$hash\n";48664867 Git::temp_release($fb->{base},1);4868 Git::temp_release($fh,1);4869}else{4870$hash=$fb->{blob}or die"no blob information\n";4871}4872$fb->{pool}->clear;4873$self->{gii}->update($fb->{mode_b},$hash,$path)or croak $!;4874print"\t$fb->{action}\t$path\n"if$fb->{action} && ! $::_q;4875undef;4876}48774878sub abort_edit {4879my$self=shift;4880$self->{nr} =$self->{gii}->{nr};4881delete$self->{gii};4882$self->SUPER::abort_edit(@_);4883}48844885sub close_edit {4886my$self=shift;48874888if($_preserve_empty_dirs) {4889my@empty_dirs;48904891# Any entry flagged as empty that also has an associated4892# dir_prop represents a newly created empty directory.4893foreachmy$i(keys%{$self->{empty}}) {4894push@empty_dirs,$iifexists$self->{dir_prop}->{$i};4895}48964897# Search for directories that have become empty due subsequent4898# file deletes.4899push@empty_dirs,$self->find_empty_directories();49004901# Finally, add a placeholder file to each empty directory.4902$self->add_placeholder_file($_)foreach(@empty_dirs);49034904$self->stash_placeholder_list();4905}49064907$self->{git_commit_ok} =1;4908$self->{nr} =$self->{gii}->{nr};4909delete$self->{gii};4910$self->SUPER::close_edit(@_);4911}49124913sub find_empty_directories {4914my($self) =@_;4915my@empty_dirs;4916my%dirs=map{ dirname($_) =>1}@deleted_gpath;49174918foreachmy$dir(sort keys%dirs) {4919next if$direq".";49204921# If there have been any additions to this directory, there is4922# no reason to check if it is empty.4923my$skip_added=0;4924foreachmy$t(qw/dir_prop file_prop/) {4925foreachmy$path(keys%{$self->{$t} }) {4926if(exists$self->{$t}->{dirname($path)}) {4927$skip_added=1;4928last;4929}4930}4931last if$skip_added;4932}4933next if$skip_added;49344935# Use `git ls-tree` to get the filenames of this directory4936# that existed prior to this particular commit.4937my$ls= command('ls-tree','-z','--name-only',4938$self->{c},"$dir/");4939my%files=map{$_=>1}split(/\0/,$ls);49404941# Remove the filenames that were deleted during this commit.4942delete$files{$_}foreach(@deleted_gpath);49434944# Report the directory if there are no filenames left.4945push@empty_dirs,$dirunless(scalar%files);4946}4947@empty_dirs;4948}49494950sub add_placeholder_file {4951my($self,$dir) =@_;4952my$path="$dir/$_placeholder_filename";4953my$gpath=$self->git_path($path);49544955my$fh= $::_repository->temp_acquire($gpath);4956my$hash= $::_repository->hash_and_insert_object(Git::temp_path($fh));4957 Git::temp_release($fh,1);4958$self->{gii}->update('100644',$hash,$gpath)or croak $!;49594960# The directory should no longer be considered empty.4961delete$self->{empty}->{$dir}ifexists$self->{empty}->{$dir};49624963# Keep track of any placeholder files we create.4964$added_placeholder{$dir} =$path;4965}49664967sub stash_placeholder_list {4968my($self) =@_;4969my$k="svn-remote.$repo_id.added-placeholder";4970my$v=eval{ command_oneline('config','--get-all',$k) };4971 command_noisy('config','--unset-all',$k)if$v;4972foreach(values%added_placeholder) {4973 command_noisy('config','--add',$k,$_);4974}4975}49764977package SVN::Git::Editor;4978use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;4979use strict;4980use warnings;4981use Carp qw/croak/;4982use IO::File;49834984sub new {4985my($class,$opts) =@_;4986foreach(qw/svn_path r ra tree_a tree_b log editor_cb/) {4987die"$_required!\n"unless(defined$opts->{$_});4988}49894990my$pool= SVN::Pool->new;4991my$mods= generate_diff($opts->{tree_a},$opts->{tree_b});4992my$types= check_diff_paths($opts->{ra},$opts->{svn_path},4993$opts->{r},$mods);49944995# $opts->{ra} functions should not be used after this:4996my@ce=$opts->{ra}->get_commit_editor($opts->{log},4997$opts->{editor_cb},$pool);4998my$self= SVN::Delta::Editor->new(@ce,$pool);4999bless$self,$class;5000foreach(qw/svn_path r tree_a tree_b/) {5001$self->{$_} =$opts->{$_};5002}5003$self->{url} =$opts->{ra}->{url};5004$self->{mods} =$mods;5005$self->{types} =$types;5006$self->{pool} =$pool;5007$self->{bat} = {''=>$self->open_root($self->{r},$self->{pool}) };5008$self->{rm} = { };5009$self->{path_prefix} =length$self->{svn_path} ?5010"$self->{svn_path}/":'';5011$self->{config} =$opts->{config};5012$self->{mergeinfo} =$opts->{mergeinfo};5013return$self;5014}50155016sub generate_diff {5017my($tree_a,$tree_b) =@_;5018my@diff_tree=qw(diff-tree -z -r);5019if($_cp_similarity) {5020push@diff_tree,"-C$_cp_similarity";5021}else{5022push@diff_tree,'-C';5023}5024push@diff_tree,'--find-copies-harder'if$_find_copies_harder;5025push@diff_tree,"-l$_rename_limit"ifdefined$_rename_limit;5026push@diff_tree,$tree_a,$tree_b;5027my($diff_fh,$ctx) = command_output_pipe(@diff_tree);5028local$/="\0";5029my$state='meta';5030my@mods;5031while(<$diff_fh>) {5032chomp$_;# this gets rid of the trailing "\0"5033if($stateeq'meta'&& /^:(\d{6})\s(\d{6})\s5034($::sha1)\s($::sha1)\s5035([MTCRAD])\d*$/xo) {5036push@mods, { mode_a =>$1, mode_b =>$2,5037 sha1_a =>$3, sha1_b =>$4,5038 chg =>$5};5039if($5=~/^(?:C|R)$/) {5040$state='file_a';5041}else{5042$state='file_b';5043}5044}elsif($stateeq'file_a') {5045my$x=$mods[$#mods]or croak "Empty array\n";5046if($x->{chg} !~/^(?:C|R)$/) {5047 croak "Error parsing$_,$x->{chg}\n";5048}5049$x->{file_a} =$_;5050$state='file_b';5051}elsif($stateeq'file_b') {5052my$x=$mods[$#mods]or croak "Empty array\n";5053if(exists$x->{file_a} &&$x->{chg} !~/^(?:C|R)$/) {5054 croak "Error parsing$_,$x->{chg}\n";5055}5056if(!exists$x->{file_a} &&$x->{chg} =~/^(?:C|R)$/) {5057 croak "Error parsing$_,$x->{chg}\n";5058}5059$x->{file_b} =$_;5060$state='meta';5061}else{5062 croak "Error parsing$_\n";5063}5064}5065 command_close_pipe($diff_fh,$ctx);5066 \@mods;5067}50685069sub check_diff_paths {5070my($ra,$pfx,$rev,$mods) =@_;5071my%types;5072$pfx.='/'iflength$pfx;50735074sub type_diff_paths {5075my($ra,$types,$path,$rev) =@_;5076my@p=split m#/+#, $path;5077my$c=shift@p;5078unless(defined$types->{$c}) {5079$types->{$c} =$ra->check_path($c,$rev);5080}5081while(@p) {5082$c.='/'.shift@p;5083next ifdefined$types->{$c};5084$types->{$c} =$ra->check_path($c,$rev);5085}5086}50875088foreachmy$m(@$mods) {5089foreachmy$f(qw/file_a file_b/) {5090next unlessdefined$m->{$f};5091my($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);5092if(length$pfx.$dir&& !defined$types{$dir}) {5093 type_diff_paths($ra, \%types,$pfx.$dir,$rev);5094}5095}5096}5097 \%types;5098}50995100sub split_path {5101return($_[0] =~ m#^(.*?)/?([^/]+)$#);5102}51035104sub repo_path {5105my($self,$path) =@_;5106if(my$enc=$self->{pathnameencoding}) {5107require Encode;5108 Encode::from_to($path,$enc,'UTF-8');5109}5110$self->{path_prefix}.(defined$path?$path:'');5111}51125113sub url_path {5114my($self,$path) =@_;5115if($self->{url} =~ m#^https?://#) {5116$path=~s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;5117}5118$self->{url} .'/'.$self->repo_path($path);5119}51205121sub rmdirs {5122my($self) =@_;5123my$rm=$self->{rm};5124delete$rm->{''};# we never delete the url we're tracking5125return unless%$rm;51265127foreach(keys%$rm) {5128my@d=split m#/#, $_;5129my$c=shift@d;5130$rm->{$c} =1;5131while(@d) {5132$c.='/'.shift@d;5133$rm->{$c} =1;5134}5135}5136delete$rm->{$self->{svn_path}};5137delete$rm->{''};# we never delete the url we're tracking5138return unless%$rm;51395140my($fh,$ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,5141$self->{tree_b});5142local$/="\0";5143while(<$fh>) {5144chomp;5145my@dn=split m#/#, $_;5146while(pop@dn) {5147delete$rm->{join'/',@dn};5148}5149unless(%$rm) {5150close$fh;5151return;5152}5153}5154 command_close_pipe($fh,$ctx);51555156my($r,$p,$bat) = ($self->{r},$self->{pool},$self->{bat});5157foreachmy$d(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {5158$self->close_directory($bat->{$d},$p);5159my($dn) = ($d=~ m#^(.*?)/?(?:[^/]+)$#);5160print"\tD+\t$d/\n"unless$::_q;5161$self->SUPER::delete_entry($d,$r,$bat->{$dn},$p);5162delete$bat->{$d};5163}5164}51655166sub open_or_add_dir {5167my($self,$full_path,$baton,$deletions) =@_;5168my$t=$self->{types}->{$full_path};5169if(!defined$t) {5170die"$full_pathnot known in r$self->{r} or we have a bug!\n";5171}5172{5173no warnings 'once';5174# SVN::Node::none and SVN::Node::file are used only once,5175# so we're shutting up Perl's warnings about them.5176if($t==$SVN::Node::none ||defined($deletions->{$full_path})) {5177return$self->add_directory($full_path,$baton,5178undef, -1,$self->{pool});5179}elsif($t==$SVN::Node::dir) {5180return$self->open_directory($full_path,$baton,5181$self->{r},$self->{pool});5182}# no warnings 'once'5183print STDERR "$full_pathalready exists in repository at ",5184"r$self->{r} and it is not a directory (",5185($t==$SVN::Node::file ?'file':'unknown'),"/$t)\n";5186}# no warnings 'once'5187exit1;5188}51895190sub ensure_path {5191my($self,$path,$deletions) =@_;5192my$bat=$self->{bat};5193my$repo_path=$self->repo_path($path);5194return$bat->{''}unless(length$repo_path);51955196my@p=split m#/+#, $repo_path;5197my$c=shift@p;5198$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{''},$deletions);5199while(@p) {5200my$c0=$c;5201$c.='/'.shift@p;5202$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{$c0},$deletions);5203}5204return$bat->{$c};5205}52065207# Subroutine to convert a globbing pattern to a regular expression.5208# From perl cookbook.5209sub glob2pat {5210my$globstr=shift;5211my%patmap= ('*'=>'.*','?'=>'.','['=>'[',']'=>']');5212$globstr=~s{(.)} { $patmap{$1}||"\Q$1"}ge;5213return'^'.$globstr.'$';5214}52155216sub check_autoprop {5217my($self,$pattern,$properties,$file,$fbat) =@_;5218# Convert the globbing pattern to a regular expression.5219my$regex= glob2pat($pattern);5220# Check if the pattern matches the file name.5221if($file=~m/($regex)/) {5222# Parse the list of properties to set.5223my@props=split(/;/,$properties);5224foreachmy$prop(@props) {5225# Parse 'name=value' syntax and set the property.5226if($prop=~/([^=]+)=(.*)/) {5227my($n,$v) = ($1,$2);5228for($n,$v) {5229s/^\s+//;s/\s+$//;5230}5231$self->change_file_prop($fbat,$n,$v);5232}5233}5234}5235}52365237sub apply_autoprops {5238my($self,$file,$fbat) =@_;5239my$conf_t= ${$self->{config}}{'config'};5240no warnings 'once';5241# Check [miscellany]/enable-auto-props in svn configuration.5242if(SVN::_Core::svn_config_get_bool(5243$conf_t,5244$SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY,5245$SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS,52460)) {5247# Auto-props are enabled. Enumerate them to look for matches.5248my$callback=sub{5249$self->check_autoprop($_[0],$_[1],$file,$fbat);5250};5251 SVN::_Core::svn_config_enumerate(5252$conf_t,5253$SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS,5254$callback);5255}5256}52575258sub A {5259my($self,$m,$deletions) =@_;5260my($dir,$file) = split_path($m->{file_b});5261my$pbat=$self->ensure_path($dir,$deletions);5262my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5263undef, -1);5264print"\tA\t$m->{file_b}\n"unless$::_q;5265$self->apply_autoprops($file,$fbat);5266$self->chg_file($fbat,$m);5267$self->close_file($fbat,undef,$self->{pool});5268}52695270sub C {5271my($self,$m,$deletions) =@_;5272my($dir,$file) = split_path($m->{file_b});5273my$pbat=$self->ensure_path($dir,$deletions);5274my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5275$self->url_path($m->{file_a}),$self->{r});5276print"\tC\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5277$self->chg_file($fbat,$m);5278$self->close_file($fbat,undef,$self->{pool});5279}52805281sub delete_entry {5282my($self,$path,$pbat) =@_;5283my$rpath=$self->repo_path($path);5284my($dir,$file) = split_path($rpath);5285$self->{rm}->{$dir} =1;5286$self->SUPER::delete_entry($rpath,$self->{r},$pbat,$self->{pool});5287}52885289sub R {5290my($self,$m,$deletions) =@_;5291my($dir,$file) = split_path($m->{file_b});5292my$pbat=$self->ensure_path($dir,$deletions);5293my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5294$self->url_path($m->{file_a}),$self->{r});5295print"\tR\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5296$self->apply_autoprops($file,$fbat);5297$self->chg_file($fbat,$m);5298$self->close_file($fbat,undef,$self->{pool});52995300($dir,$file) = split_path($m->{file_a});5301$pbat=$self->ensure_path($dir,$deletions);5302$self->delete_entry($m->{file_a},$pbat);5303}53045305sub M {5306my($self,$m,$deletions) =@_;5307my($dir,$file) = split_path($m->{file_b});5308my$pbat=$self->ensure_path($dir,$deletions);5309my$fbat=$self->open_file($self->repo_path($m->{file_b}),5310$pbat,$self->{r},$self->{pool});5311print"\t$m->{chg}\t$m->{file_b}\n"unless$::_q;5312$self->chg_file($fbat,$m);5313$self->close_file($fbat,undef,$self->{pool});5314}53155316sub T { shift->M(@_) }53175318sub change_file_prop {5319my($self,$fbat,$pname,$pval) =@_;5320$self->SUPER::change_file_prop($fbat,$pname,$pval,$self->{pool});5321}53225323sub change_dir_prop {5324my($self,$pbat,$pname,$pval) =@_;5325$self->SUPER::change_dir_prop($pbat,$pname,$pval,$self->{pool});5326}53275328sub _chg_file_get_blob ($$$$) {5329my($self,$fbat,$m,$which) =@_;5330my$fh= $::_repository->temp_acquire("git_blob_$which");5331if($m->{"mode_$which"} =~/^120/) {5332print$fh'link 'or croak $!;5333$self->change_file_prop($fbat,'svn:special','*');5334}elsif($m->{mode_a} =~/^120/&&$m->{"mode_$which"} !~/^120/) {5335$self->change_file_prop($fbat,'svn:special',undef);5336}5337my$blob=$m->{"sha1_$which"};5338return($fh,)if($blob=~/^0{40}$/);5339my$size= $::_repository->cat_blob($blob,$fh);5340 croak "Failed to read object$blob"if($size<0);5341$fh->flush==0or croak $!;5342seek$fh,0,0or croak $!;53435344my$exp= ::md5sum($fh);5345seek$fh,0,0or croak $!;5346return($fh,$exp);5347}53485349sub chg_file {5350my($self,$fbat,$m) =@_;5351if($m->{mode_b} =~/755$/&&$m->{mode_a} !~/755$/) {5352$self->change_file_prop($fbat,'svn:executable','*');5353}elsif($m->{mode_b} !~/755$/&&$m->{mode_a} =~/755$/) {5354$self->change_file_prop($fbat,'svn:executable',undef);5355}5356my($fh_a,$exp_a) = _chg_file_get_blob $self,$fbat,$m,'a';5357my($fh_b,$exp_b) = _chg_file_get_blob $self,$fbat,$m,'b';5358my$pool= SVN::Pool->new;5359my$atd=$self->apply_textdelta($fbat,$exp_a,$pool);5360if(-s $fh_a) {5361my$txstream= SVN::TxDelta::new ($fh_a,$fh_b,$pool);5362my$res= SVN::TxDelta::send_txstream($txstream,@$atd,$pool);5363if(defined$res) {5364die"Unexpected result from send_txstream:$res\n",5365"(SVN::Core::VERSION:$SVN::Core::VERSION)\n";5366}5367}else{5368my$got= SVN::TxDelta::send_stream($fh_b,@$atd,$pool);5369die"Checksum mismatch\nexpected:$exp_b\ngot:$got\n"5370if($gotne$exp_b);5371}5372 Git::temp_release($fh_b,1);5373 Git::temp_release($fh_a,1);5374$pool->clear;5375}53765377sub D {5378my($self,$m,$deletions) =@_;5379my($dir,$file) = split_path($m->{file_b});5380my$pbat=$self->ensure_path($dir,$deletions);5381print"\tD\t$m->{file_b}\n"unless$::_q;5382$self->delete_entry($m->{file_b},$pbat);5383}53845385sub close_edit {5386my($self) =@_;5387my($p,$bat) = ($self->{pool},$self->{bat});5388foreach(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {5389next if$_eq'';5390$self->close_directory($bat->{$_},$p);5391}5392$self->close_directory($bat->{''},$p);5393$self->SUPER::close_edit($p);5394$p->clear;5395}53965397sub abort_edit {5398my($self) =@_;5399$self->SUPER::abort_edit($self->{pool});5400}54015402sub DESTROY {5403my$self=shift;5404$self->SUPER::DESTROY(@_);5405$self->{pool}->clear;5406}54075408# this drives the editor5409sub apply_diff {5410my($self) =@_;5411my$mods=$self->{mods};5412my%o= ( D =>0, C =>1, R =>2, A =>3, M =>4, T =>5);5413my%deletions;54145415foreachmy$m(@$mods) {5416if($m->{chg}eq"D") {5417$deletions{$m->{file_b}} =1;5418}5419}54205421foreachmy$m(sort{$o{$a->{chg}} <=>$o{$b->{chg}} }@$mods) {5422my$f=$m->{chg};5423if(defined$o{$f}) {5424$self->$f($m, \%deletions);5425}else{5426 fatal("Invalid change type:$f");5427}5428}54295430if(defined($self->{mergeinfo})) {5431$self->change_dir_prop($self->{bat}{''},"svn:mergeinfo",5432$self->{mergeinfo});5433}5434$self->rmdirsif$_rmdir;5435if(@$mods==0&& !defined($self->{mergeinfo})) {5436$self->abort_edit;5437}else{5438$self->close_edit;5439}5440returnscalar@$mods;5441}54425443package Git::SVN::Ra;5444use vars qw/@ISA $config_dir $_ignore_refs_regex $_log_window_size/;5445use strict;5446use warnings;5447my($ra_invalid,$can_do_switch,%ignored_err,$RA);54485449BEGIN{5450# enforce temporary pool usage for some simple functions5451no strict 'refs';5452formy$f(qw/rev_proplist get_latest_revnum get_uuid get_repos_root5453 get_file/) {5454my$SUPER="SUPER::$f";5455*$f=sub{5456my$self=shift;5457my$pool= SVN::Pool->new;5458my@ret=$self->$SUPER(@_,$pool);5459$pool->clear;5460wantarray?@ret:$ret[0];5461};5462}5463}54645465sub _auth_providers () {5466my@rv= (5467 SVN::Client::get_simple_provider(),5468 SVN::Client::get_ssl_server_trust_file_provider(),5469 SVN::Client::get_simple_prompt_provider(5470 \&Git::SVN::Prompt::simple,2),5471 SVN::Client::get_ssl_client_cert_file_provider(),5472 SVN::Client::get_ssl_client_cert_prompt_provider(5473 \&Git::SVN::Prompt::ssl_client_cert,2),5474 SVN::Client::get_ssl_client_cert_pw_file_provider(),5475 SVN::Client::get_ssl_client_cert_pw_prompt_provider(5476 \&Git::SVN::Prompt::ssl_client_cert_pw,2),5477 SVN::Client::get_username_provider(),5478 SVN::Client::get_ssl_server_trust_prompt_provider(5479 \&Git::SVN::Prompt::ssl_server_trust),5480 SVN::Client::get_username_prompt_provider(5481 \&Git::SVN::Prompt::username,2)5482);54835484# earlier 1.6.x versions would segfault, and <= 1.5.x didn't have5485# this function5486if(::compare_svn_version('1.6.12') >0) {5487my$config= SVN::Core::config_get_config($config_dir);5488my($p,@a);5489# config_get_config returns all config files from5490# ~/.subversion, auth_get_platform_specific_client_providers5491# just wants the config "file".5492@a= ($config->{'config'},undef);5493$p= SVN::Core::auth_get_platform_specific_client_providers(@a);5494# Insert the return value from5495# auth_get_platform_specific_providers5496unshift@rv,@$p;5497}5498 \@rv;5499}55005501sub escape_uri_only {5502my($uri) =@_;5503my@tmp;5504foreach(splitm{/},$uri) {5505s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;5506push@tmp,$_;5507}5508join('/',@tmp);5509}55105511sub escape_url {5512my($url) =@_;5513if($url=~ m#^(https?)://([^/]+)(.*)$#) {5514my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));5515$url="$scheme://$domain$uri";5516}5517$url;5518}55195520sub new {5521my($class,$url) =@_;5522$url=~s!/+$!!;5523return$RAif($RA&&$RA->{url}eq$url);55245525::_req_svn();55265527 SVN::_Core::svn_config_ensure($config_dir,undef);5528my($baton,$callbacks) = SVN::Core::auth_open_helper(_auth_providers);5529my$config= SVN::Core::config_get_config($config_dir);5530$RA=undef;5531my$dont_store_passwords=1;5532my$conf_t= ${$config}{'config'};5533{5534no warnings 'once';5535# The usage of $SVN::_Core::SVN_CONFIG_* variables5536# produces warnings that variables are used only once.5537# I had not found the better way to shut them up, so5538# the warnings of type 'once' are disabled in this block.5539if(SVN::_Core::svn_config_get_bool($conf_t,5540$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5541$SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS,55421) ==0) {5543 SVN::_Core::svn_auth_set_parameter($baton,5544$SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS,5545bless(\$dont_store_passwords,"_p_void"));5546}5547if(SVN::_Core::svn_config_get_bool($conf_t,5548$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5549$SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS,55501) ==0) {5551$Git::SVN::Prompt::_no_auth_cache =1;5552}5553}# no warnings 'once'5554my$self= SVN::Ra->new(url => escape_url($url), auth =>$baton,5555 config =>$config,5556 pool => SVN::Pool->new,5557 auth_provider_callbacks =>$callbacks);5558$self->{url} =$url;5559$self->{svn_path} =$url;5560$self->{repos_root} =$self->get_repos_root;5561$self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;5562$self->{cache} = { check_path => { r =>0, data => {} },5563 get_dir => { r =>0, data => {} } };5564$RA=bless$self,$class;5565}55665567sub check_path {5568my($self,$path,$r) =@_;5569my$cache=$self->{cache}->{check_path};5570if($r==$cache->{r} &&exists$cache->{data}->{$path}) {5571return$cache->{data}->{$path};5572}5573my$pool= SVN::Pool->new;5574my$t=$self->SUPER::check_path($path,$r,$pool);5575$pool->clear;5576if($r!=$cache->{r}) {5577%{$cache->{data}} = ();5578$cache->{r} =$r;5579}5580$cache->{data}->{$path} =$t;5581}55825583sub get_dir {5584my($self,$dir,$r) =@_;5585my$cache=$self->{cache}->{get_dir};5586if($r==$cache->{r}) {5587if(my$x=$cache->{data}->{$dir}) {5588returnwantarray?@$x:$x->[0];5589}5590}5591my$pool= SVN::Pool->new;5592my($d,undef,$props) =$self->SUPER::get_dir($dir,$r,$pool);5593my%dirents=map{$_=> { kind =>$d->{$_}->kind} }keys%$d;5594$pool->clear;5595if($r!=$cache->{r}) {5596%{$cache->{data}} = ();5597$cache->{r} =$r;5598}5599$cache->{data}->{$dir} = [ \%dirents,$r,$props];5600wantarray? (\%dirents,$r,$props) : \%dirents;5601}56025603sub DESTROY {5604# do not call the real DESTROY since we store ourselves in $RA5605}56065607# get_log(paths, start, end, limit,5608# discover_changed_paths, strict_node_history, receiver)5609sub get_log {5610my($self,@args) =@_;5611my$pool= SVN::Pool->new;56125613# svn_log_changed_path_t objects passed to get_log are likely to be5614# overwritten even if only the refs are copied to an external variable,5615# so we should dup the structures in their entirety. Using an5616# externally passed pool (instead of our temporary and quickly cleared5617# pool in Git::SVN::Ra) does not help matters at all...5618my$receiver=pop@args;5619my$prefix="/".$self->{svn_path};5620$prefix=~ s#/+($)##;5621my$prefix_regex= qr#^\Q$prefix\E#;5622push(@args,sub{5623my($paths) =$_[0];5624return&$receiver(@_)unless$paths;5625$_[0] = ();5626foreachmy$p(keys%$paths) {5627my$i=$paths->{$p};5628# Make path relative to our url, not repos_root5629$p=~s/$prefix_regex//;5630my%s=map{$_=>$i->$_; }5631 qw/copyfrom_path copyfrom_rev action/;5632if($s{'copyfrom_path'}) {5633$s{'copyfrom_path'} =~s/$prefix_regex//;5634}5635$_[0]{$p} = \%s;5636}5637&$receiver(@_);5638});563956405641# the limit parameter was not supported in SVN 1.1.x, so we5642# drop it. Therefore, the receiver callback passed to it5643# is made aware of this limitation by being wrapped if5644# the limit passed to is being wrapped.5645if(::compare_svn_version('1.2.0') <=0) {5646my$limit=splice(@args,3,1);5647if($limit>0) {5648my$receiver=pop@args;5649push(@args,sub{ &$receiver(@_)if(--$limit>=0) });5650}5651}5652my$ret=$self->SUPER::get_log(@args,$pool);5653$pool->clear;5654$ret;5655}56565657sub trees_match {5658my($self,$url1,$rev1,$url2,$rev2) =@_;5659my$ctx= SVN::Client->new(auth => _auth_providers);5660my$out= IO::File->new_tmpfile;56615662# older SVN (1.1.x) doesn't take $pool as the last parameter for5663# $ctx->diff(), so we'll create a default one5664my$pool= SVN::Pool->new_default_sub;56655666$ra_invalid=1;# this will open a new SVN::Ra connection to $url15667$ctx->diff([],$url1,$rev1,$url2,$rev2,1,1,0,$out,$out);5668$out->flush;5669my$ret= (($out->stat)[7] ==0);5670close$outor croak $!;56715672$ret;5673}56745675sub get_commit_editor {5676my($self,$log,$cb,$pool) =@_;56775678my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef,0) : ();5679$self->SUPER::get_commit_editor($log,$cb,@lock,$pool);5680}56815682sub gs_do_update {5683my($self,$rev_a,$rev_b,$gs,$editor) =@_;5684my$new= ($rev_a==$rev_b);5685my$path=$gs->{path};56865687if($new&& -e $gs->{index}) {5688unlink$gs->{index}or die5689"Couldn't unlink index:$gs->{index}:$!\n";5690}5691my$pool= SVN::Pool->new;5692$editor->set_path_strip($path);5693my(@pc) =split m#/#, $path;5694my$reporter=$self->do_update($rev_b, (@pc?shift@pc:''),56951,$editor,$pool);5696my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef) : ();56975698# Since we can't rely on svn_ra_reparent being available, we'll5699# just have to do some magic with set_path to make it so5700# we only want a partial path.5701my$sp='';5702my$final=join('/',@pc);5703while(@pc) {5704$reporter->set_path($sp,$rev_b,0,@lock,$pool);5705$sp.='/'iflength$sp;5706$sp.=shift@pc;5707}5708die"BUG: '$sp' != '$final'\n"if($spne$final);57095710$reporter->set_path($sp,$rev_a,$new,@lock,$pool);57115712$reporter->finish_report($pool);5713$pool->clear;5714$editor->{git_commit_ok};5715}57165717# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and5718# svn_ra_reparent didn't work before 1.4)5719sub gs_do_switch {5720my($self,$rev_a,$rev_b,$gs,$url_b,$editor) =@_;5721my$path=$gs->{path};5722my$pool= SVN::Pool->new;57235724my$full_url=$self->{url};5725my$old_url=$full_url;5726$full_url.='/'.$pathiflength$path;5727my($ra,$reparented);57285729if($old_url=~ m#^svn(\+ssh)?://# ||5730($full_url=~ m#^https?://# &&5731 escape_url($full_url)ne$full_url)) {5732$_[0] =undef;5733$self=undef;5734$RA=undef;5735$ra= Git::SVN::Ra->new($full_url);5736$ra_invalid=1;5737}elsif($old_urlne$full_url) {5738 SVN::_Ra::svn_ra_reparent($self->{session},$full_url,$pool);5739$self->{url} =$full_url;5740$reparented=1;5741}57425743$ra||=$self;5744$url_b= escape_url($url_b);5745my$reporter=$ra->do_switch($rev_b,'',1,$url_b,$editor,$pool);5746my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef) : ();5747$reporter->set_path('',$rev_a,0,@lock,$pool);5748$reporter->finish_report($pool);57495750if($reparented) {5751 SVN::_Ra::svn_ra_reparent($self->{session},$old_url,$pool);5752$self->{url} =$old_url;5753}57545755$pool->clear;5756$editor->{git_commit_ok};5757}57585759sub longest_common_path {5760my($gsv,$globs) =@_;5761my%common;5762my$common_max=scalar@$gsv;57635764foreachmy$gs(@$gsv) {5765my@tmp=split m#/#, $gs->{path};5766my$p='';5767foreach(@tmp) {5768$p.=length($p) ?"/$_":$_;5769$common{$p} ||=0;5770$common{$p}++;5771}5772}5773$globs||= [];5774$common_max+=scalar@$globs;5775foreachmy$glob(@$globs) {5776my@tmp=split m#/#, $glob->{path}->{left};5777my$p='';5778foreach(@tmp) {5779$p.=length($p) ?"/$_":$_;5780$common{$p} ||=0;5781$common{$p}++;5782}5783}57845785my$longest_path='';5786foreach(sort{length$b<=>length$a}keys%common) {5787if($common{$_} ==$common_max) {5788$longest_path=$_;5789last;5790}5791}5792$longest_path;5793}57945795sub gs_fetch_loop_common {5796my($self,$base,$head,$gsv,$globs) =@_;5797return if($base>$head);5798my$inc=$_log_window_size;5799my($min,$max) = ($base,$head<$base+$inc?$head:$base+$inc);5800my$longest_path= longest_common_path($gsv,$globs);5801my$ra_url=$self->{url};5802my$find_trailing_edge;5803while(1) {5804my%revs;5805my$err;5806my$err_handler=$SVN::Error::handler;5807$SVN::Error::handler =sub{5808($err) =@_;5809 skip_unknown_revs($err);5810};5811sub _cb {5812my($paths,$r,$author,$date,$log) =@_;5813[$paths,5814{ author =>$author, date =>$date,log=>$log} ];5815}5816$self->get_log([$longest_path],$min,$max,0,1,1,5817sub{$revs{$_[1]} = _cb(@_) });5818if($err) {5819print"Checked through r$max\r";5820}else{5821$find_trailing_edge=1;5822}5823if($errand$find_trailing_edge) {5824print STDERR "Path '$longest_path' ",5825"was probably deleted:\n",5826$err->expanded_message,5827"\nWill attempt to follow ",5828"revisions r$min.. r$max",5829"committed before the deletion\n";5830my$hi=$max;5831while(--$hi>=$min) {5832my$ok;5833$self->get_log([$longest_path],$min,$hi,58340,1,1,sub{5835$ok=$_[1];5836$revs{$_[1]} = _cb(@_) });5837if($ok) {5838print STDERR "r$min.. r$okOK\n";5839last;5840}5841}5842$find_trailing_edge=0;5843}5844$SVN::Error::handler =$err_handler;58455846my%exists=map{$_->{path} =>$_}@$gsv;5847foreachmy$r(sort{$a<=>$b}keys%revs) {5848my($paths,$logged) = @{$revs{$r}};58495850foreachmy$gs($self->match_globs(\%exists,$paths,5851$globs,$r)) {5852if($gs->rev_map_max>=$r) {5853next;5854}5855next unless$gs->match_paths($paths,$r);5856$gs->{logged_rev_props} =$logged;5857if(my$last_commit=$gs->last_commit) {5858$gs->assert_index_clean($last_commit);5859}5860my$log_entry=$gs->do_fetch($paths,$r);5861if($log_entry) {5862$gs->do_git_commit($log_entry);5863}5864$INDEX_FILES{$gs->{index}} =1;5865}5866foreachmy$g(@$globs) {5867my$k="svn-remote.$g->{remote}.".5868"$g->{t}-maxRev";5869 Git::SVN::tmp_config($k,$r);5870}5871if($ra_invalid) {5872$_[0] =undef;5873$self=undef;5874$RA=undef;5875$self= Git::SVN::Ra->new($ra_url);5876$ra_invalid=undef;5877}5878}5879# pre-fill the .rev_db since it'll eventually get filled in5880# with '0' x40 if something new gets committed5881foreachmy$gs(@$gsv) {5882next if$gs->rev_map_max>=$max;5883next ifdefined$gs->rev_map_get($max);5884$gs->rev_map_set($max,0 x40);5885}5886foreachmy$g(@$globs) {5887my$k="svn-remote.$g->{remote}.$g->{t}-maxRev";5888 Git::SVN::tmp_config($k,$max);5889}5890last if$max>=$head;5891$min=$max+1;5892$max+=$inc;5893$max=$headif($max>$head);5894}5895 Git::SVN::gc();5896}58975898sub get_dir_globbed {5899my($self,$left,$depth,$r) =@_;59005901my@x=eval{$self->get_dir($left,$r) };5902return unlessscalar@x==3;5903my$dirents=$x[0];5904my@finalents;5905foreachmy$de(keys%$dirents) {5906next if$dirents->{$de}->{kind} !=$SVN::Node::dir;5907if($depth>1) {5908my@args= ("$left/$de",$depth-1,$r);5909foreachmy$dir($self->get_dir_globbed(@args)) {5910push@finalents,"$de/$dir";5911}5912}else{5913push@finalents,$de;5914}5915}5916@finalents;5917}59185919# return value: 0 -- don't ignore, 1 -- ignore5920sub is_ref_ignored {5921my($g,$p) =@_;5922my$refname=$g->{ref}->full_path($p);5923return1ifdefined($g->{ignore_refs_regex}) &&5924$refname=~m!$g->{ignore_refs_regex}!;5925return0unlessdefined($_ignore_refs_regex);5926return1if$refname=~m!$_ignore_refs_regex!o;5927return0;5928}59295930sub match_globs {5931my($self,$exists,$paths,$globs,$r) =@_;59325933sub get_dir_check {5934my($self,$exists,$g,$r) =@_;59355936my@dirs=$self->get_dir_globbed($g->{path}->{left},5937$g->{path}->{depth},5938$r);59395940foreachmy$de(@dirs) {5941my$p=$g->{path}->full_path($de);5942next if$exists->{$p};5943next if(length$g->{path}->{right} &&5944($self->check_path($p,$r) !=5945$SVN::Node::dir));5946next unless$p=~/$g->{path}->{regex}/;5947$exists->{$p} = Git::SVN->init($self->{url},$p,undef,5948$g->{ref}->full_path($de),1);5949}5950}5951foreachmy$g(@$globs) {5952if(my$path=$paths->{"/$g->{path}->{left}"}) {5953if($path->{action} =~/^[AR]$/) {5954 get_dir_check($self,$exists,$g,$r);5955}5956}5957foreach(keys%$paths) {5958if(/$g->{path}->{left_regex}/&&5959!/$g->{path}->{regex}/) {5960next if$paths->{$_}->{action} !~/^[AR]$/;5961 get_dir_check($self,$exists,$g,$r);5962}5963next unless/$g->{path}->{regex}/;5964my$p=$1;5965my$pathname=$g->{path}->full_path($p);5966next if is_ref_ignored($g,$p);5967next if$exists->{$pathname};5968next if($self->check_path($pathname,$r) !=5969$SVN::Node::dir);5970$exists->{$pathname} = Git::SVN->init(5971$self->{url},$pathname,undef,5972$g->{ref}->full_path($p),1);5973}5974my$c='';5975foreach(split m#/#, $g->{path}->{left}) {5976$c.="/$_";5977next unless($paths->{$c} &&5978($paths->{$c}->{action} =~/^[AR]$/));5979 get_dir_check($self,$exists,$g,$r);5980}5981}5982values%$exists;5983}59845985sub minimize_url {5986my($self) =@_;5987return$self->{url}if($self->{url}eq$self->{repos_root});5988my$url=$self->{repos_root};5989my@components=split(m!/!,$self->{svn_path});5990my$c='';5991do{5992$url.="/$c"iflength$c;5993eval{5994my$ra= (ref$self)->new($url);5995my$latest=$ra->get_latest_revnum;5996$ra->get_log("",$latest,0,1,0,1,sub{});5997};5998}while($@&& ($c=shift@components));5999$url;6000}60016002sub can_do_switch {6003my$self=shift;6004unless(defined$can_do_switch) {6005my$pool= SVN::Pool->new;6006my$rep=eval{6007$self->do_switch(1,'',0,$self->{url},6008 SVN::Delta::Editor->new,$pool);6009};6010if($@) {6011$can_do_switch=0;6012}else{6013$rep->abort_report($pool);6014$can_do_switch=1;6015}6016$pool->clear;6017}6018$can_do_switch;6019}60206021sub skip_unknown_revs {6022my($err) =@_;6023my$errno=$err->apr_err();6024# Maybe the branch we're tracking didn't6025# exist when the repo started, so it's6026# not an error if it doesn't, just continue6027#6028# Wonderfully consistent library, eh?6029# 160013 - svn:// and file://6030# 175002 - http(s)://6031# 175007 - http(s):// (this repo required authorization, too...)6032# More codes may be discovered later...6033if($errno==175007||$errno==175002||$errno==160013) {6034my$err_key=$err->expanded_message;6035# revision numbers change every time, filter them out6036$err_key=~s/\d+/\0/g;6037$err_key="$errno\0$err_key";6038unless($ignored_err{$err_key}) {6039warn"W: Ignoring error from SVN, path probably ",6040"does not exist: ($errno): ",6041$err->expanded_message,"\n";6042warn"W: Do not be alarmed at the above message ",6043"git-svn is just searching aggressively for ",6044"old history.\n",6045"This may take a while on large repositories\n";6046$ignored_err{$err_key} =1;6047}6048return;6049}6050die"Error from SVN, ($errno): ",$err->expanded_message,"\n";6051}60526053package Git::SVN::Log;6054use strict;6055use warnings;6056use POSIX qw/strftime/;6057useconstant commit_log_separator => ('-' x 72) ."\n";6058use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline6059%rusers $show_commit $incremental/;6060my$l_fmt;60616062sub cmt_showable {6063my($c) =@_;6064return1ifdefined$c->{r};60656066# big commit message got truncated by the 16k pretty buffer in rev-list6067if($c->{l} &&$c->{l}->[-1]eq"...\n"&&6068$c->{a_raw} =~/\@([a-f\d\-]+)>$/) {6069@{$c->{l}} = ();6070my@log= command(qw/cat-file commit/,$c->{c});60716072# shift off the headers6073shift@logwhile($log[0]ne'');6074shift@log;60756076# TODO: make $c->{l} not have a trailing newline in the future6077@{$c->{l}} =map{"$_\n"}grep!/^git-svn-id: /,@log;60786079(undef,$c->{r},undef) = ::extract_metadata(6080(grep(/^git-svn-id: /,@log))[-1]);6081}6082returndefined$c->{r};6083}60846085sub log_use_color {6086return$color|| Git->repository->get_colorbool('color.diff');6087}60886089sub git_svn_log_cmd {6090my($r_min,$r_max,@args) =@_;6091my$head='HEAD';6092my(@files,@log_opts);6093foreachmy$x(@args) {6094if($xeq'--'||@files) {6095push@files,$x;6096}else{6097if(::verify_ref("$x^0")) {6098$head=$x;6099}else{6100push@log_opts,$x;6101}6102}6103}61046105my($url,$rev,$uuid,$gs) = ::working_head_info($head);6106$gs||= Git::SVN->_new;6107my@cmd= (qw/log --abbrev-commit --pretty=raw --default/,6108$gs->refname);6109push@cmd,'-r'unless$non_recursive;6110push@cmd, qw/--raw --name-status/if$verbose;6111push@cmd,'--color'if log_use_color();6112push@cmd,@log_opts;6113if(defined$r_max&&$r_max==$r_min) {6114push@cmd,'--max-count=1';6115if(my$c=$gs->rev_map_get($r_max)) {6116push@cmd,$c;6117}6118}elsif(defined$r_max) {6119if($r_max<$r_min) {6120($r_min,$r_max) = ($r_max,$r_min);6121}6122my(undef,$c_max) =$gs->find_rev_before($r_max,1,$r_min);6123my(undef,$c_min) =$gs->find_rev_after($r_min,1,$r_max);6124# If there are no commits in the range, both $c_max and $c_min6125# will be undefined. If there is at least 1 commit in the6126# range, both will be defined.6127return()if!defined$c_min|| !defined$c_max;6128if($c_mineq$c_max) {6129push@cmd,'--max-count=1',$c_min;6130}else{6131push@cmd,'--boundary',"$c_min..$c_max";6132}6133}6134return(@cmd,@files);6135}61366137# adapted from pager.c6138sub config_pager {6139if(! -t *STDOUT) {6140$ENV{GIT_PAGER_IN_USE} ='false';6141$pager=undef;6142return;6143}6144chomp($pager= command_oneline(qw(var GIT_PAGER)));6145if($pagereq'cat') {6146$pager=undef;6147}6148$ENV{GIT_PAGER_IN_USE} =defined($pager);6149}61506151sub run_pager {6152return unlessdefined$pager;6153pipe my($rfd,$wfd)orreturn;6154defined(my$pid=fork)or::fatal "Can't fork:$!";6155if(!$pid) {6156open STDOUT,'>&',$wfdor6157::fatal "Can't redirect to stdout:$!";6158return;6159}6160open STDIN,'<&',$rfdor::fatal "Can't redirect stdin:$!";6161$ENV{LESS} ||='FRSX';6162exec$pageror::fatal "Can't run pager:$!($pager)";6163}61646165sub format_svn_date {6166my$t=shift||time;6167my$gmoff= Git::SVN::get_tz($t);6168return strftime("%Y-%m-%d%H:%M:%S$gmoff(%a,%d%b%Y)",localtime($t));6169}61706171sub parse_git_date {6172my($t,$tz) =@_;6173# Date::Parse isn't in the standard Perl distro :(6174if($tz=~s/^\+//) {6175$t+= tz_to_s_offset($tz);6176}elsif($tz=~s/^\-//) {6177$t-= tz_to_s_offset($tz);6178}6179return$t;6180}61816182sub set_local_timezone {6183if(defined$TZ) {6184$ENV{TZ} =$TZ;6185}else{6186delete$ENV{TZ};6187}6188}61896190sub tz_to_s_offset {6191my($tz) =@_;6192$tz=~s/(\d\d)$//;6193return($1*60) + ($tz*3600);6194}61956196sub get_author_info {6197my($dest,$author,$t,$tz) =@_;6198$author=~s/(?:^\s*|\s*$)//g;6199$dest->{a_raw} =$author;6200my$au;6201if($::_authors) {6202$au=$rusers{$author} ||undef;6203}6204if(!$au) {6205($au) = ($author=~/<([^>]+)\@[^>]+>$/);6206}6207$dest->{t} =$t;6208$dest->{tz} =$tz;6209$dest->{a} =$au;6210$dest->{t_utc} = parse_git_date($t,$tz);6211}62126213sub process_commit {6214my($c,$r_min,$r_max,$defer) =@_;6215if(defined$r_min&&defined$r_max) {6216if($r_min==$c->{r} &&$r_min==$r_max) {6217 show_commit($c);6218return0;6219}6220return1if$r_min==$r_max;6221if($r_min<$r_max) {6222# we need to reverse the print order6223return0if(defined$limit&& --$limit<0);6224push@$defer,$c;6225return1;6226}6227if($r_min!=$r_max) {6228return1if($r_min<$c->{r});6229return1if($r_max>$c->{r});6230}6231}6232return0if(defined$limit&& --$limit<0);6233 show_commit($c);6234return1;6235}62366237sub show_commit {6238my$c=shift;6239if($oneline) {6240my$x="\n";6241if(my$l=$c->{l}) {6242while($l->[0] =~/^\s*$/) {shift@$l}6243$x=$l->[0];6244}6245$l_fmt||='A'.length($c->{r});6246print'r',pack($l_fmt,$c->{r}),' | ';6247print"$c->{c} | "if$show_commit;6248print$x;6249}else{6250 show_commit_normal($c);6251}6252}62536254sub show_commit_changed_paths {6255my($c) =@_;6256return unless$c->{changed};6257print"Changed paths:\n", @{$c->{changed}};6258}62596260sub show_commit_normal {6261my($c) =@_;6262print commit_log_separator,"r$c->{r} | ";6263print"$c->{c} | "if$show_commit;6264print"$c->{a} | ", format_svn_date($c->{t_utc}),' | ';6265my$nr_line=0;62666267if(my$l=$c->{l}) {6268while($l->[$#$l]eq"\n"&&$#$l>06269&&$l->[($#$l-1)]eq"\n") {6270pop@$l;6271}6272$nr_line=scalar@$l;6273if(!$nr_line) {6274print"1 line\n\n\n";6275}else{6276if($nr_line==1) {6277$nr_line='1 line';6278}else{6279$nr_line.=' lines';6280}6281print$nr_line,"\n";6282 show_commit_changed_paths($c);6283print"\n";6284print$_foreach@$l;6285}6286}else{6287print"1 line\n";6288 show_commit_changed_paths($c);6289print"\n";62906291}6292foreachmy$x(qw/raw stat diff/) {6293if($c->{$x}) {6294print"\n";6295print$_foreach@{$c->{$x}}6296}6297}6298}62996300sub cmd_show_log {6301my(@args) =@_;6302my($r_min,$r_max);6303my$r_last= -1;# prevent dupes6304 set_local_timezone();6305if(defined$::_revision) {6306if($::_revision =~/^(\d+):(\d+)$/) {6307($r_min,$r_max) = ($1,$2);6308}elsif($::_revision =~/^\d+$/) {6309$r_min=$r_max= $::_revision;6310}else{6311::fatal "-r$::_revision is not supported, use ",6312"standard 'git log' arguments instead";6313}6314}63156316 config_pager();6317@args= git_svn_log_cmd($r_min,$r_max,@args);6318if(!@args) {6319print commit_log_separator unless$incremental||$oneline;6320return;6321}6322my$log= command_output_pipe(@args);6323 run_pager();6324my(@k,$c,$d,$stat);6325my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;6326while(<$log>) {6327if(/^${esc_color}commit (?:- )?($::sha1_short)/o) {6328my$cmt=$1;6329if($c&& cmt_showable($c) &&$c->{r} !=$r_last) {6330$r_last=$c->{r};6331 process_commit($c,$r_min,$r_max, \@k)or6332goto out;6333}6334$d=undef;6335$c= { c =>$cmt};6336}elsif(/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {6337 get_author_info($c,$1,$2,$3);6338}elsif(/^${esc_color}(?:tree|parent|committer) /o) {6339# ignore6340}elsif(/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {6341push@{$c->{raw}},$_;6342}elsif(/^${esc_color}[ACRMDT]\t/) {6343# we could add $SVN->{svn_path} here, but that requires6344# remote access at the moment (repo_path_split)...6345 s#^(${esc_color})([ACRMDT])\t#$1 $2 #o;6346push@{$c->{changed}},$_;6347}elsif(/^${esc_color}diff /o) {6348$d=1;6349push@{$c->{diff}},$_;6350}elsif($d) {6351push@{$c->{diff}},$_;6352}elsif(/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*6353$esc_color*[\+\-]*$esc_color$/x) {6354$stat=1;6355push@{$c->{stat}},$_;6356}elsif($stat&&/^ \d+ files changed, \d+ insertions/) {6357push@{$c->{stat}},$_;6358$stat=undef;6359}elsif(/^${esc_color} (git-svn-id:.+)$/o) {6360($c->{url},$c->{r},undef) = ::extract_metadata($1);6361}elsif(s/^${esc_color} //o) {6362push@{$c->{l}},$_;6363}6364}6365if($c&&defined$c->{r} &&$c->{r} !=$r_last) {6366$r_last=$c->{r};6367 process_commit($c,$r_min,$r_max, \@k);6368}6369if(@k) {6370($r_min,$r_max) = ($r_max,$r_min);6371 process_commit($_,$r_min,$r_max)foreachreverse@k;6372}6373out:6374close$log;6375print commit_log_separator unless$incremental||$oneline;6376}63776378sub cmd_blame {6379my$path=pop;63806381 config_pager();6382 run_pager();63836384my($fh,$ctx,$rev);63856386if($_git_format) {6387($fh,$ctx) = command_output_pipe('blame',@_,$path);6388while(my$line= <$fh>) {6389if($line=~/^\^?([[:xdigit:]]+)\s/) {6390# Uncommitted edits show up as a rev ID of6391# all zeros, which we can't look up with6392# cmt_metadata6393if($1!~/^0+$/) {6394(undef,$rev,undef) =6395::cmt_metadata($1);6396$rev='0'if(!$rev);6397}else{6398$rev='0';6399}6400$rev=sprintf('%-10s',$rev);6401$line=~s/^\^?[[:xdigit:]]+(\s)/$rev$1/;6402}6403print$line;6404}6405}else{6406($fh,$ctx) = command_output_pipe('blame','-p',@_,'HEAD',6407'--',$path);6408my($sha1);6409my%authors;6410my@buffer;6411my%dsha;#distinct sha keys64126413while(my$line= <$fh>) {6414push@buffer,$line;6415if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6416$dsha{$1} =1;6417}6418}64196420my$s2r= ::cmt_sha2rev_batch([keys%dsha]);64216422foreachmy$line(@buffer) {6423if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6424$rev=$s2r->{$1};6425$rev='0'if(!$rev)6426}6427elsif($line=~/^author (.*)/) {6428$authors{$rev} =$1;6429$authors{$rev} =~s/\s/_/g;6430}6431elsif($line=~/^\t(.*)$/) {6432printf("%6s%10s%s\n",$rev,$authors{$rev},$1);6433}6434}6435}6436 command_close_pipe($fh,$ctx);6437}64386439package Git::SVN::Migration;6440# these version numbers do NOT correspond to actual version numbers6441# of git nor git-svn. They are just relative.6442#6443# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD6444#6445# v1 layout: .git/$id/info/url, refs/remotes/$id6446#6447# v2 layout: .git/svn/$id/info/url, refs/remotes/$id6448#6449# v3 layout: .git/svn/$id, refs/remotes/$id6450# - info/url may remain for backwards compatibility6451# - this is what we migrate up to this layout automatically,6452# - this will be used by git svn init on single branches6453# v3.1 layout (auto migrated):6454# - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink6455# for backwards compatibility6456#6457# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id6458# - this is only created for newly multi-init-ed6459# repositories. Similar in spirit to the6460# --use-separate-remotes option in git-clone (now default)6461# - we do not automatically migrate to this (following6462# the example set by core git)6463#6464# v5 layout: .rev_db.$UUID => .rev_map.$UUID6465# - newer, more-efficient format that uses 24-bytes per record6466# with no filler space.6467# - use xxd -c24 < .rev_map.$UUID to view and debug6468# - This is a one-way migration, repositories updated to the6469# new format will not be able to use old git-svn without6470# rebuilding the .rev_db. Rebuilding the rev_db is not6471# possible if noMetadata or useSvmProps are set; but should6472# be no problem for users that use the (sensible) defaults.6473use strict;6474use warnings;6475use Carp qw/croak/;6476use File::Path qw/mkpath/;6477use File::Basename qw/dirname basename/;6478use vars qw/$_minimize/;64796480sub migrate_from_v0 {6481my$git_dir=$ENV{GIT_DIR};6482returnundefunless-d $git_dir;6483my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6484my$migrated=0;6485while(<$fh>) {6486chomp;6487my($id,$orig_ref) = ($_,$_);6488next unless$id=~ s#^refs/heads/(.+)-HEAD$#$1#;6489next unless-f "$git_dir/$id/info/url";6490my$new_ref="refs/remotes/$id";6491if(::verify_ref("$new_ref^0")) {6492print STDERR "W:$orig_refis probably an old ",6493"branch used by an ancient version of ",6494"git-svn.\n",6495"However,$new_refalso exists.\n",6496"We will not be able ",6497"to use this branch until this ",6498"ambiguity is resolved.\n";6499next;6500}6501print STDERR "Migrating from v0 layout...\n"if!$migrated;6502print STDERR "Renaming ref:$orig_ref=>$new_ref\n";6503 command_noisy('update-ref',$new_ref,$orig_ref);6504 command_noisy('update-ref','-d',$orig_ref,$orig_ref);6505$migrated++;6506}6507 command_close_pipe($fh,$ctx);6508print STDERR "Done migrating from v0 layout...\n"if$migrated;6509$migrated;6510}65116512sub migrate_from_v1 {6513my$git_dir=$ENV{GIT_DIR};6514my$migrated=0;6515return$migratedunless-d $git_dir;6516my$svn_dir="$git_dir/svn";65176518# just in case somebody used 'svn' as their $id at some point...6519return$migratedif-d $svn_dir&& ! -f "$svn_dir/info/url";65206521print STDERR "Migrating from a git-svn v1 layout...\n";6522 mkpath([$svn_dir]);6523print STDERR "Data from a previous version of git-svn exists, but\n\t",6524"$svn_dir\n\t(required for this version ",6525"($::VERSION) of git-svn) does not exist.\n";6526my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6527while(<$fh>) {6528my$x=$_;6529next unless$x=~ s#^refs/remotes/##;6530chomp$x;6531next unless-f "$git_dir/$x/info/url";6532my$u=eval{ ::file_to_s("$git_dir/$x/info/url") };6533next unless$u;6534my$dn= dirname("$git_dir/svn/$x");6535 mkpath([$dn])unless-d $dn;6536if($xeq'svn') {# they used 'svn' as GIT_SVN_ID:6537 mkpath(["$git_dir/svn/svn"]);6538print STDERR " -$git_dir/$x/info=> ",6539"$git_dir/svn/$x/info\n";6540rename"$git_dir/$x/info","$git_dir/svn/$x/info"or6541 croak "$!:$x";6542# don't worry too much about these, they probably6543# don't exist with repos this old (save for index,6544# and we can easily regenerate that)6545foreachmy$f(qw/unhandled.log index .rev_db/) {6546rename"$git_dir/$x/$f","$git_dir/svn/$x/$f";6547}6548}else{6549print STDERR " -$git_dir/$x=>$git_dir/svn/$x\n";6550rename"$git_dir/$x","$git_dir/svn/$x"or6551 croak "$!:$x";6552}6553$migrated++;6554}6555 command_close_pipe($fh,$ctx);6556print STDERR "Done migrating from a git-svn v1 layout\n";6557$migrated;6558}65596560sub read_old_urls {6561my($l_map,$pfx,$path) =@_;6562my@dir;6563foreach(<$path/*>) {6564if(-r "$_/info/url") {6565$pfx.='/'if$pfx&&$pfx!~ m!/$!;6566my$ref_id=$pfx. basename $_;6567my$url= ::file_to_s("$_/info/url");6568$l_map->{$ref_id} =$url;6569}elsif(-d $_) {6570push@dir,$_;6571}6572}6573foreach(@dir) {6574my$x=$_;6575$x=~s!^\Q$ENV{GIT_DIR}\E/svn/!!o;6576 read_old_urls($l_map,$x,$_);6577}6578}65796580sub migrate_from_v2 {6581my@cfg= command(qw/config -l/);6582return ifgrep/^svn-remote\..+\.url=/,@cfg;6583my%l_map;6584 read_old_urls(\%l_map,'',"$ENV{GIT_DIR}/svn");6585my$migrated=0;65866587foreachmy$ref_id(sort keys%l_map) {6588eval{ Git::SVN->init($l_map{$ref_id},'',undef,$ref_id) };6589if($@) {6590 Git::SVN->init($l_map{$ref_id},'',$ref_id,$ref_id);6591}6592$migrated++;6593}6594$migrated;6595}65966597sub minimize_connections {6598my$r= Git::SVN::read_all_remotes();6599my$new_urls= {};6600my$root_repos= {};6601foreachmy$repo_id(keys%$r) {6602my$url=$r->{$repo_id}->{url}ornext;6603my$fetch=$r->{$repo_id}->{fetch}ornext;6604my$ra= Git::SVN::Ra->new($url);66056606# skip existing cases where we already connect to the root6607if(($ra->{url}eq$ra->{repos_root}) ||6608($ra->{repos_root}eq$repo_id)) {6609$root_repos->{$ra->{url}} =$repo_id;6610next;6611}66126613my$root_ra= Git::SVN::Ra->new($ra->{repos_root});6614my$root_path=$ra->{url};6615$root_path=~ s#^\Q$ra->{repos_root}\E(/|$)##;6616foreachmy$path(keys%$fetch) {6617my$ref_id=$fetch->{$path};6618my$gs= Git::SVN->new($ref_id,$repo_id,$path);66196620# make sure we can read when connecting to6621# a higher level of a repository6622my($last_rev,undef) =$gs->last_rev_commit;6623if(!defined$last_rev) {6624$last_rev=eval{6625$root_ra->get_latest_revnum;6626};6627next if$@;6628}6629my$new=$root_path;6630$new.=length$path?"/$path":'';6631eval{6632$root_ra->get_log([$new],$last_rev,$last_rev,66330,0,1,sub{ });6634};6635next if$@;6636$new_urls->{$ra->{repos_root}}->{$new} =6637{ ref_id =>$ref_id,6638 old_repo_id =>$repo_id,6639 old_path =>$path};6640}6641}66426643my@emptied;6644foreachmy$url(keys%$new_urls) {6645# see if we can re-use an existing [svn-remote "repo_id"]6646# instead of creating a(n ugly) new section:6647my$repo_id=$root_repos->{$url} ||$url;66486649my$fetch=$new_urls->{$url};6650foreachmy$path(keys%$fetch) {6651my$x=$fetch->{$path};6652 Git::SVN->init($url,$path,$repo_id,$x->{ref_id});6653my$pfx="svn-remote.$x->{old_repo_id}";66546655my$old_fetch=quotemeta("$x->{old_path}:".6656"$x->{ref_id}");6657 command_noisy(qw/config --unset/,6658"$pfx.fetch",'^'.$old_fetch.'$');6659delete$r->{$x->{old_repo_id}}->6660{fetch}->{$x->{old_path}};6661if(!keys%{$r->{$x->{old_repo_id}}->{fetch}}) {6662 command_noisy(qw/config --unset/,6663"$pfx.url");6664push@emptied,$x->{old_repo_id}6665}6666}6667}6668if(@emptied) {6669my$file=$ENV{GIT_CONFIG} ||"$ENV{GIT_DIR}/config";6670print STDERR <<EOF;6671The following [svn-remote] sections in your config file ($file) are empty6672and can be safely removed:6673EOF6674print STDERR "[svn-remote\"$_\"]\n"foreach@emptied;6675}6676}66776678sub migration_check {6679 migrate_from_v0();6680 migrate_from_v1();6681 migrate_from_v2();6682 minimize_connections()if$_minimize;6683}66846685package Git::IndexInfo;6686use strict;6687use warnings;6688use Git qw/command_input_pipe command_close_pipe/;66896690sub new {6691my($class) =@_;6692my($gui,$ctx) = command_input_pipe(qw/update-index -z --index-info/);6693bless{ gui =>$gui, ctx =>$ctx, nr =>0},$class;6694}66956696sub remove {6697my($self,$path) =@_;6698if(print{$self->{gui} }'0 ',0 x 40,"\t",$path,"\0") {6699return++$self->{nr};6700}6701undef;6702}67036704sub update {6705my($self,$mode,$hash,$path) =@_;6706if(print{$self->{gui} }$mode,' ',$hash,"\t",$path,"\0") {6707return++$self->{nr};6708}6709undef;6710}67116712sub DESTROY {6713my($self) =@_;6714 command_close_pipe($self->{gui},$self->{ctx});6715}67166717package Git::SVN::GlobSpec;6718use strict;6719use warnings;67206721sub new {6722my($class,$glob,$pattern_ok) =@_;6723my$re=$glob;6724$re=~s!/+$!!g;# no need for trailing slashes6725my(@left,@right,@patterns);6726my$state="left";6727my$die_msg="Only one set of wildcard directories ".6728"(e.g. '*' or '*/*/*') is supported: '$glob'\n";6729formy$part(split(m|/|,$glob)) {6730if($part=~/\*/&&$partne"*") {6731die"Invalid pattern in '$glob':$part\n";6732}elsif($pattern_ok&&$part=~/[{}]/&&6733$part!~/^\{[^{}]+\}/) {6734die"Invalid pattern in '$glob':$part\n";6735}6736if($parteq"*") {6737die$die_msgif$stateeq"right";6738$state="pattern";6739push(@patterns,"[^/]*");6740}elsif($pattern_ok&&$part=~/^\{(.*)\}$/) {6741die$die_msgif$stateeq"right";6742$state="pattern";6743my$p=quotemeta($1);6744$p=~s/\\,/|/g;6745push(@patterns,"(?:$p)");6746}else{6747if($stateeq"left") {6748push(@left,$part);6749}else{6750push(@right,$part);6751$state="right";6752}6753}6754}6755my$depth=@patterns;6756if($depth==0) {6757die"One '*' is needed in glob: '$glob'\n";6758}6759my$left=join('/',@left);6760my$right=join('/',@right);6761$re=join('/',@patterns);6762$re=join('\/',6763grep(length,quotemeta($left),"($re)",quotemeta($right)));6764my$left_re=qr/^\/\Q$left\E(\/|$)/;6765bless{ left =>$left, right =>$right, left_regex =>$left_re,6766 regex =>qr/$re/,glob=>$glob, depth =>$depth},$class;6767}67686769sub full_path {6770my($self,$path) =@_;6771return(length$self->{left} ?"$self->{left}/":'') .6772$path. (length$self->{right} ?"/$self->{right}":'');6773}67746775__END__67766777Data structures:677867796780$remotes= {# returned by read_all_remotes()6781'svn'=> {6782# svn-remote.svn.url=https://svn.musicpd.org6783 url =>'https://svn.musicpd.org',6784# svn-remote.svn.fetch=mpd/trunk:trunk6785 fetch => {6786'mpd/trunk'=>'trunk',6787},6788# svn-remote.svn.tags=mpd/tags/*:tags/*6789 tags => {6790 path => {6791 left =>'mpd/tags',6792 right =>'',6793 regex =>qr!mpd/tags/([^/]+)$!,6794glob=>'tags/*',6795},6796ref=> {6797 left =>'tags',6798 right =>'',6799 regex =>qr!tags/([^/]+)$!,6800glob=>'tags/*',6801},6802}6803}6804};68056806$log_entry hashref as returned by libsvn_log_entry()6807{6808log=>'whitespace-formatted log entry6809',# trailing newline is preserved6810 revision =>'8',# integer6811 date =>'2004-02-24T17:01:44.108345Z',# commit date6812 author =>'committer name'6813};681468156816# this is generated by generate_diff();6817@mods= array of diff-index line hashes,each element represents one line6818 of diff-index output68196820diff-index line ($m hash)6821{6822 mode_a => first column of diff-index output,no leading ':',6823 mode_b => second column of diff-index output,6824 sha1_b => sha1sum of the final blob,6825 chg => change type [MCRADT],6826 file_a => original file name of a file (iff chg is'C'or'R')6827 file_b => new/current file name of a file (any chg)6828}6829;68306831# retval of read_url_paths{,_all}();6832$l_map= {6833# repository root url6834'https://svn.musicpd.org'=> {6835# repository path # GIT_SVN_ID6836'mpd/trunk'=>'trunk',6837'mpd/tags/0.11.5'=>'tags/0.11.5',6838},6839}68406841Notes:6842 I don't trust the each() function on unless I created%hashmyself6843 because the internal iterator may not have started at base.