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'; 71use Carp qw/croak/; 72use Digest::MD5; 73use IO::File qw//; 74use File::Basename qw/dirname basename/; 75use File::Path qw/mkpath/; 76use File::Spec; 77use File::Find; 78use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; 79use IPC::Open3; 80use Git; 81use Git::SVN::Editor qw//; 82use Git::SVN::Fetcher qw//; 83use Git::SVN::Prompt qw//; 84use Memoize;# core since 5.8.0, Jul 2002 85 86BEGIN{ 87# import functions from Git into our packages, en masse 88no strict 'refs'; 89foreach(qw/command command_oneline command_noisy command_output_pipe 90 command_input_pipe command_close_pipe 91 command_bidi_pipe command_close_bidi_pipe/) { 92formy$package(qw(Git::SVN::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$Git::SVN::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'=> \$Git::SVN::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'=> \$Git::SVN::Editor::_rmdir, 152'find-copies-harder'=> \$Git::SVN::Editor::_find_copies_harder, 153'l=i'=> \$Git::SVN::Editor::_rename_limit, 154'copy-similarity|C=i'=> \$Git::SVN::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 \$Git::SVN::Fetcher::_preserve_empty_dirs, 167'placeholder-filename=s'=> 168 \$Git::SVN::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= \$Git::SVN::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$Git::SVN::Fetcher::_preserve_empty_dirs) { 474 my$fname= \$Git::SVN::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(!Git::SVN::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}) {1068my$re= Git::SVN::Editor::glob2pat($g->{path}->{left});1069if($_branch_dest=~/$re/) {1070$glob=$g;1071last;1072}1073}1074unless(defined$glob) {1075my$dest_re=qr/\b\Q$_branch_dest\E\b/;1076foreachmy$g(@{$allglobs}) {1077$g->{path}->{left} =~/$dest_re/ornext;1078if(defined$glob) {1079die"Ambiguous destination: ",1080$_branch_dest,"\nmatches both '",1081$glob->{path}->{left},"' and '",1082$g->{path}->{left},"'\n";1083}1084$glob=$g;1085}1086unless(defined$glob) {1087die"Unknown ",1088$_tag?"tag":"branch",1089" destination$_branch_dest\n";1090}1091}1092}1093my($lft,$rgt) = @{$glob->{path} }{qw/left right/};1094my$url;1095if(defined$_commit_url) {1096$url=$_commit_url;1097}else{1098$url=eval{ command_oneline('config','--get',1099"svn-remote.$gs->{repo_id}.commiturl") };1100if(!$url) {1101$url=$remote->{pushurl} ||$remote->{url};1102}1103}1104my$dst=join'/',$url,$lft,$branch_name, ($rgt|| ());11051106if($dst=~/^https:/&&$src=~/^http:/) {1107$src=~s/^http:/https:/;1108}11091110::_req_svn();11111112my$ctx= SVN::Client->new(1113 auth => Git::SVN::Ra::_auth_providers(),1114 log_msg =>sub{1115${$_[0] } =defined$_message1116?$_message1117:'Create '. ($_tag?'tag ':'branch ')1118.$branch_name;1119},1120);11211122eval{1123$ctx->ls($dst,'HEAD',0);1124}and die"branch ${branch_name} already exists\n";11251126print"Copying ${src} at r${rev} to ${dst}...\n";1127$ctx->copy($src,$rev,$dst)1128unless$_dry_run;11291130$gs->fetch_all;1131}11321133sub cmd_find_rev {1134my$revision_or_hash=shift or die"SVN or git revision required ",1135"as a command-line argument\n";1136my$result;1137if($revision_or_hash=~/^r\d+$/) {1138my$head=shift;1139$head||='HEAD';1140my@refs;1141my(undef,undef,$uuid,$gs) = working_head_info($head, \@refs);1142unless($gs) {1143die"Unable to determine upstream SVN information from ",1144"$headhistory\n";1145}1146my$desired_revision=substr($revision_or_hash,1);1147$result=$gs->rev_map_get($desired_revision,$uuid);1148}else{1149my(undef,$rev,undef) = cmt_metadata($revision_or_hash);1150$result=$rev;1151}1152print"$result\n"if$result;1153}11541155sub auto_create_empty_directories {1156my($gs) =@_;1157my$var=eval{ command_oneline('config','--get','--bool',1158"svn-remote.$gs->{repo_id}.automkdirs") };1159# By default, create empty directories by consulting the unhandled log,1160# but allow setting it to 'false' to skip it.1161return!($var&&$vareq'false');1162}11631164sub cmd_rebase {1165 command_noisy(qw/update-index --refresh/);1166my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1167unless($gs) {1168die"Unable to determine upstream SVN information from ",1169"working tree history\n";1170}1171if($_dry_run) {1172print"Remote Branch: ".$gs->refname."\n";1173print"SVN URL: ".$url."\n";1174return;1175}1176if(command(qw/diff-index HEAD --/)) {1177print STDERR "Cannot rebase with uncommited changes:\n";1178 command_noisy('status');1179exit1;1180}1181unless($_local) {1182# rebase will checkout for us, so no need to do it explicitly1183$_no_checkout='true';1184$_fetch_all?$gs->fetch_all:$gs->fetch;1185}1186 command_noisy(rebase_cmd(),$gs->refname);1187if(auto_create_empty_directories($gs)) {1188$gs->mkemptydirs;1189}1190}11911192sub cmd_show_ignore {1193my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1194$gs||= Git::SVN->new;1195my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1196$gs->prop_walk($gs->{path},$r,sub{1197my($gs,$path,$props) =@_;1198print STDOUT "\n#$path\n";1199my$s=$props->{'svn:ignore'}orreturn;1200$s=~s/[\r\n]+/\n/g;1201$s=~s/^\n+//;1202chomp$s;1203$s=~ s#^#$path#gm;1204print STDOUT "$s\n";1205});1206}12071208sub cmd_show_externals {1209my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1210$gs||= Git::SVN->new;1211my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1212$gs->prop_walk($gs->{path},$r,sub{1213my($gs,$path,$props) =@_;1214print STDOUT "\n#$path\n";1215my$s=$props->{'svn:externals'}orreturn;1216$s=~s/[\r\n]+/\n/g;1217chomp$s;1218$s=~ s#^#$path#gm;1219print STDOUT "$s\n";1220});1221}12221223sub cmd_create_ignore {1224my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1225$gs||= Git::SVN->new;1226my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1227$gs->prop_walk($gs->{path},$r,sub{1228my($gs,$path,$props) =@_;1229# $path is of the form /path/to/dir/1230$path='.'.$path;1231# SVN can have attributes on empty directories,1232# which git won't track1233 mkpath([$path])unless-d $path;1234my$ignore=$path.'.gitignore';1235my$s=$props->{'svn:ignore'}orreturn;1236open(GITIGNORE,'>',$ignore)1237or fatal("Failed to open `$ignore' for writing:$!");1238$s=~s/[\r\n]+/\n/g;1239$s=~s/^\n+//;1240chomp$s;1241# Prefix all patterns so that the ignore doesn't apply1242# to sub-directories.1243$s=~ s#^#/#gm;1244print GITIGNORE "$s\n";1245close(GITIGNORE)1246or fatal("Failed to close `$ignore':$!");1247 command_noisy('add','-f',$ignore);1248});1249}12501251sub cmd_mkdirs {1252my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1253$gs||= Git::SVN->new;1254$gs->mkemptydirs($_revision);1255}12561257sub canonicalize_path {1258my($path) =@_;1259my$dot_slash_added=0;1260if(substr($path,0,1)ne"/") {1261$path="./".$path;1262$dot_slash_added=1;1263}1264# File::Spec->canonpath doesn't collapse x/../y into y (for a1265# good reason), so let's do this manually.1266$path=~ s#/+#/#g;1267$path=~ s#/\.(?:/|$)#/#g;1268$path=~ s#/[^/]+/\.\.##g;1269$path=~ s#/$##g;1270$path=~ s#^\./## if $dot_slash_added;1271$path=~ s#^/##;1272$path=~ s#^\.$##;1273return$path;1274}12751276sub canonicalize_url {1277my($url) =@_;1278$url=~ s#^([^:]+://[^/]*/)(.*)$#$1 . canonicalize_path($2)#e;1279return$url;1280}12811282# get_svnprops(PATH)1283# ------------------1284# Helper for cmd_propget and cmd_proplist below.1285sub get_svnprops {1286my$path=shift;1287my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1288$gs||= Git::SVN->new;12891290# prefix THE PATH by the sub-directory from which the user1291# invoked us.1292$path=$cmd_dir_prefix.$path;1293 fatal("No such file or directory:$path")unless-e $path;1294my$is_dir= -d $path?1:0;1295$path=$gs->{path} .'/'.$path;12961297# canonicalize the path (otherwise libsvn will abort or fail to1298# find the file)1299$path= canonicalize_path($path);13001301my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1302my$props;1303if($is_dir) {1304(undef,undef,$props) =$gs->ra->get_dir($path,$r);1305}1306else{1307(undef,$props) =$gs->ra->get_file($path,$r,undef);1308}1309return$props;1310}13111312# cmd_propget (PROP, PATH)1313# ------------------------1314# Print the SVN property PROP for PATH.1315sub cmd_propget {1316my($prop,$path) =@_;1317$path='.'ifnot defined$path;1318 usage(1)ifnot defined$prop;1319my$props= get_svnprops($path);1320if(not defined$props->{$prop}) {1321 fatal("`$path' does not have a `$prop' SVN property.");1322}1323print$props->{$prop} ."\n";1324}13251326# cmd_proplist (PATH)1327# -------------------1328# Print the list of SVN properties for PATH.1329sub cmd_proplist {1330my$path=shift;1331$path='.'ifnot defined$path;1332my$props= get_svnprops($path);1333print"Properties on '$path':\n";1334foreach(sort keys%{$props}) {1335print"$_\n";1336}1337}13381339sub cmd_multi_init {1340my$url=shift;1341unless(defined$_trunk||@_branches||@_tags) {1342 usage(1);1343}13441345$_prefix=''unlessdefined$_prefix;1346if(defined$url) {1347$url= canonicalize_url($url);1348 init_subdir(@_);1349}1350 do_git_init_db();1351if(defined$_trunk) {1352$_trunk=~ s#^/+##;1353my$trunk_ref='refs/remotes/'.$_prefix.'trunk';1354# try both old-style and new-style lookups:1355my$gs_trunk=eval{ Git::SVN->new($trunk_ref) };1356unless($gs_trunk) {1357my($trunk_url,$trunk_path) =1358 complete_svn_url($url,$_trunk);1359$gs_trunk= Git::SVN->init($trunk_url,$trunk_path,1360undef,$trunk_ref);1361}1362}1363return unless@_branches||@_tags;1364my$ra=$url? Git::SVN::Ra->new($url) :undef;1365foreachmy$path(@_branches) {1366 complete_url_ls_init($ra,$path,'--branches/-b',$_prefix);1367}1368foreachmy$path(@_tags) {1369 complete_url_ls_init($ra,$path,'--tags/-t',$_prefix.'tags/');1370}1371}13721373sub cmd_multi_fetch {1374$Git::SVN::no_reuse_existing =undef;1375my$remotes= Git::SVN::read_all_remotes();1376foreachmy$repo_id(sort keys%$remotes) {1377if($remotes->{$repo_id}->{url}) {1378 Git::SVN::fetch_all($repo_id,$remotes);1379}1380}1381}13821383# this command is special because it requires no metadata1384sub cmd_commit_diff {1385my($ta,$tb,$url) =@_;1386my$usage="Usage:$0commit-diff -r<revision> ".1387"<tree-ish> <tree-ish> [<URL>]";1388 fatal($usage)if(!defined$ta|| !defined$tb);1389my$svn_path='';1390if(!defined$url) {1391my$gs=eval{ Git::SVN->new};1392if(!$gs) {1393 fatal("Needed URL or usable git-svn --id in ",1394"the command-line\n",$usage);1395}1396$url=$gs->{url};1397$svn_path=$gs->{path};1398}1399unless(defined$_revision) {1400 fatal("-r|--revision is a required argument\n",$usage);1401}1402if(defined$_message&&defined$_file) {1403 fatal("Both --message/-m and --file/-F specified ",1404"for the commit message.\n",1405"I have no idea what you mean");1406}1407if(defined$_file) {1408$_message= file_to_s($_file);1409}else{1410$_message||= get_commit_entry($tb)->{log};1411}1412my$ra||= Git::SVN::Ra->new($url);1413my$r=$_revision;1414if($req'HEAD') {1415$r=$ra->get_latest_revnum;1416}elsif($r!~/^\d+$/) {1417die"revision argument:$rnot understood by git-svn\n";1418}1419my%ed_opts= ( r =>$r,1420log=>$_message,1421 ra =>$ra,1422 tree_a =>$ta,1423 tree_b =>$tb,1424 editor_cb =>sub{print"Committed r$_[0]\n"},1425 svn_path =>$svn_path);1426if(!Git::SVN::Editor->new(\%ed_opts)->apply_diff) {1427print"No changes\n$ta==$tb\n";1428}1429}14301431sub escape_uri_only {1432my($uri) =@_;1433my@tmp;1434foreach(splitm{/},$uri) {1435s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;1436push@tmp,$_;1437}1438join('/',@tmp);1439}14401441sub escape_url {1442my($url) =@_;1443if($url=~ m#^([^:]+)://([^/]*)(.*)$#) {1444my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));1445$url="$scheme://$domain$uri";1446}1447$url;1448}14491450sub cmd_info {1451my$path= canonicalize_path(defined($_[0]) ?$_[0] :".");1452my$fullpath= canonicalize_path($cmd_dir_prefix.$path);1453if(exists$_[1]) {1454die"Too many arguments specified\n";1455}14561457my($file_type,$diff_status) = find_file_type_and_diff_status($path);14581459if(!$file_type&& !$diff_status) {1460print STDERR "svn: '$path' is not under version control\n";1461exit1;1462}14631464my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1465unless($gs) {1466die"Unable to determine upstream SVN information from ",1467"working tree history\n";1468}14691470# canonicalize_path() will return "" to make libsvn 1.5.x happy,1471$path="."if$patheq"";14721473my$full_url=$url. ($fullpatheq""?"":"/$fullpath");14741475if($_url) {1476print escape_url($full_url),"\n";1477return;1478}14791480my$result="Path:$path\n";1481$result.="Name: ". basename($path) ."\n"if$file_typene"dir";1482$result.="URL: ". escape_url($full_url) ."\n";14831484eval{1485my$repos_root=$gs->repos_root;1486 Git::SVN::remove_username($repos_root);1487$result.="Repository Root: ". escape_url($repos_root) ."\n";1488};1489if($@) {1490$result.="Repository Root: (offline)\n";1491}1492::_req_svn();1493$result.="Repository UUID:$uuid\n"unless$diff_statuseq"A"&&1494(::compare_svn_version('1.5.4') <=0||$file_typene"dir");1495$result.="Revision: ". ($diff_statuseq"A"?0:$rev) ."\n";14961497$result.="Node Kind: ".1498($file_typeeq"dir"?"directory":"file") ."\n";14991500my$schedule=$diff_statuseq"A"1501?"add"1502: ($diff_statuseq"D"?"delete":"normal");1503$result.="Schedule:$schedule\n";15041505if($diff_statuseq"A") {1506print$result,"\n";1507return;1508}15091510my($lc_author,$lc_rev,$lc_date_utc);1511my@args= Git::SVN::Log::git_svn_log_cmd($rev,$rev,"--",$fullpath);1512my$log= command_output_pipe(@args);1513my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;1514while(<$log>) {1515if(/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {1516$lc_author=$1;1517$lc_date_utc= Git::SVN::Log::parse_git_date($2,$3);1518}elsif(/^${esc_color} (git-svn-id:.+)$/o) {1519(undef,$lc_rev,undef) = ::extract_metadata($1);1520}1521}1522close$log;15231524 Git::SVN::Log::set_local_timezone();15251526$result.="Last Changed Author:$lc_author\n";1527$result.="Last Changed Rev:$lc_rev\n";1528$result.="Last Changed Date: ".1529 Git::SVN::Log::format_svn_date($lc_date_utc) ."\n";15301531if($file_typene"dir") {1532my$text_last_updated_date=1533($diff_statuseq"D"?$lc_date_utc: (stat$path)[9]);1534$result.=1535"Text Last Updated: ".1536 Git::SVN::Log::format_svn_date($text_last_updated_date) .1537"\n";1538my$checksum;1539if($diff_statuseq"D") {1540my($fh,$ctx) =1541 command_output_pipe(qw(cat-file blob),"HEAD:$path");1542if($file_typeeq"link") {1543my$file_name= <$fh>;1544$checksum= md5sum("link$file_name");1545}else{1546$checksum= md5sum($fh);1547}1548 command_close_pipe($fh,$ctx);1549}elsif($file_typeeq"link") {1550my$file_name=1551 command(qw(cat-file blob),"HEAD:$path");1552$checksum=1553 md5sum("link ".$file_name);1554}else{1555open FILE,"<",$pathor die$!;1556$checksum= md5sum(\*FILE);1557close FILE or die$!;1558}1559$result.="Checksum: ".$checksum."\n";1560}15611562print$result,"\n";1563}15641565sub cmd_reset {1566my$target=shift||$_revisionor die"SVN revision required\n";1567$target=$1if$target=~/^r(\d+)$/;1568$target=~/^\d+$/or die"Numeric SVN revision expected\n";1569my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1570unless($gs) {1571die"Unable to determine upstream SVN information from ".1572"history\n";1573}1574my($r,$c) =$gs->find_rev_before($target,not$_fetch_parent);1575die"Cannot find SVN revision$target\n"unlessdefined($c);1576$gs->rev_map_set($r,$c,'reset',$uuid);1577print"r$r=$c($gs->{ref_id})\n";1578}15791580sub cmd_gc {1581if(!$can_compress) {1582warn"Compress::Zlib could not be found; unhandled.log ".1583"files will not be compressed.\n";1584}1585 find({ wanted => \&gc_directory, no_chdir =>1},"$ENV{GIT_DIR}/svn");1586}15871588########################### utility functions #########################15891590sub rebase_cmd {1591my@cmd= qw/rebase/;1592push@cmd,'-v'if$_verbose;1593push@cmd, qw/--merge/if$_merge;1594push@cmd,"--strategy=$_strategy"if$_strategy;1595push@cmd,"--preserve-merges"if$_preserve_merges;1596@cmd;1597}15981599sub post_fetch_checkout {1600return if$_no_checkout;1601my$gs=$Git::SVN::_head orreturn;1602return if verify_ref('refs/heads/master^0');16031604# look for "trunk" ref if it exists1605my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1606my$fetch=$remote->{fetch};1607if($fetch) {1608foreachmy$p(keys%$fetch) {1609 basename($fetch->{$p})eq'trunk'ornext;1610$gs= Git::SVN->new($fetch->{$p},$gs->{repo_id},$p);1611last;1612}1613}16141615my$valid_head= verify_ref('HEAD^0');1616 command_noisy(qw(update-ref refs/heads/master),$gs->refname);1617return if($valid_head|| !verify_ref('HEAD^0'));16181619return if$ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;1620my$index=$ENV{GIT_INDEX_FILE} ||"$ENV{GIT_DIR}/index";1621return if-f $index;16221623return if command_oneline(qw/rev-parse --is-inside-work-tree/)eq'false';1624return if command_oneline(qw/rev-parse --is-inside-git-dir/)eq'true';1625 command_noisy(qw/read-tree -m -u -v HEAD HEAD/);1626print STDERR "Checked out HEAD:\n",1627$gs->full_url," r",$gs->last_rev,"\n";1628if(auto_create_empty_directories($gs)) {1629$gs->mkemptydirs($gs->last_rev);1630}1631}16321633sub complete_svn_url {1634my($url,$path) =@_;1635$path=~ s#/+$##;1636if($path!~ m#^[a-z\+]+://#) {1637if(!defined$url||$url!~ m#^[a-z\+]+://#) {1638 fatal("E: '$path' is not a complete URL ",1639"and a separate URL is not specified");1640}1641return($url,$path);1642}1643return($path,'');1644}16451646sub complete_url_ls_init {1647my($ra,$repo_path,$switch,$pfx) =@_;1648unless($repo_path) {1649print STDERR "W:$switchnot specified\n";1650return;1651}1652$repo_path=~ s#/+$##;1653if($repo_path=~ m#^[a-z\+]+://#) {1654$ra= Git::SVN::Ra->new($repo_path);1655$repo_path='';1656}else{1657$repo_path=~ s#^/+##;1658unless($ra) {1659 fatal("E: '$repo_path' is not a complete URL ",1660"and a separate URL is not specified");1661}1662}1663my$url=$ra->{url};1664my$gs= Git::SVN->init($url,undef,undef,undef,1);1665my$k="svn-remote.$gs->{repo_id}.url";1666my$orig_url=eval{ command_oneline(qw/config --get/,$k) };1667if($orig_url&& ($orig_urlne$gs->{url})) {1668die"$kalready set:$orig_url\n",1669"wanted to set to:$gs->{url}\n";1670}1671 command_oneline('config',$k,$gs->{url})unless$orig_url;1672my$remote_path="$gs->{path}/$repo_path";1673$remote_path=~s{%([0-9A-F]{2})}{chr hex($1)}ieg;1674$remote_path=~ s#/+#/#g;1675$remote_path=~ s#^/##g;1676$remote_path.="/*"if$remote_path!~ /\*/;1677my($n) = ($switch=~/^--(\w+)/);1678if(length$pfx&&$pfx!~ m#/$#) {1679die"--prefix='$pfx' must have a trailing slash '/'\n";1680}1681 command_noisy('config',1682'--add',1683"svn-remote.$gs->{repo_id}.$n",1684"$remote_path:refs/remotes/$pfx*".1685('/*' x (($remote_path=~ tr/*/*/) -1)) );1686}16871688sub verify_ref {1689my($ref) =@_;1690eval{ command_oneline(['rev-parse','--verify',$ref],1691{ STDERR =>0}); };1692}16931694sub get_tree_from_treeish {1695my($treeish) =@_;1696# $treeish can be a symbolic ref, too:1697my$type= command_oneline(qw/cat-file -t/,$treeish);1698my$expected;1699while($typeeq'tag') {1700($treeish,$type) = command(qw/cat-file tag/,$treeish);1701}1702if($typeeq'commit') {1703$expected= (grep/^tree /, command(qw/cat-file commit/,1704$treeish))[0];1705($expected) = ($expected=~/^tree ($sha1)$/o);1706die"Unable to get tree from$treeish\n"unless$expected;1707}elsif($typeeq'tree') {1708$expected=$treeish;1709}else{1710die"$treeishis a$type, expected tree, tag or commit\n";1711}1712return$expected;1713}17141715sub get_commit_entry {1716my($treeish) =shift;1717my%log_entry= (log=>'', tree => get_tree_from_treeish($treeish) );1718my$commit_editmsg="$ENV{GIT_DIR}/COMMIT_EDITMSG";1719my$commit_msg="$ENV{GIT_DIR}/COMMIT_MSG";1720open my$log_fh,'>',$commit_editmsgor croak $!;17211722my$type= command_oneline(qw/cat-file -t/,$treeish);1723if($typeeq'commit'||$typeeq'tag') {1724my($msg_fh,$ctx) = command_output_pipe('cat-file',1725$type,$treeish);1726my$in_msg=0;1727my$author;1728my$saw_from=0;1729my$msgbuf="";1730while(<$msg_fh>) {1731if(!$in_msg) {1732$in_msg=1if(/^\s*$/);1733$author=$1if(/^author (.*>)/);1734}elsif(/^git-svn-id: /) {1735# skip this for now, we regenerate the1736# correct one on re-fetch anyways1737# TODO: set *:merge properties or like...1738}else{1739if(/^From:/||/^Signed-off-by:/) {1740$saw_from=1;1741}1742$msgbuf.=$_;1743}1744}1745$msgbuf=~s/\s+$//s;1746if($Git::SVN::_add_author_from &&defined($author)1747&& !$saw_from) {1748$msgbuf.="\n\nFrom:$author";1749}1750print$log_fh $msgbufor croak $!;1751 command_close_pipe($msg_fh,$ctx);1752}1753close$log_fhor croak $!;17541755if($_edit|| ($typeeq'tree')) {1756chomp(my$editor= command_oneline(qw(var GIT_EDITOR)));1757system('sh','-c',$editor.' "$@"',$editor,$commit_editmsg);1758}1759rename$commit_editmsg,$commit_msgor croak $!;1760{1761require Encode;1762# SVN requires messages to be UTF-8 when entering the repo1763local$/;1764open$log_fh,'<',$commit_msgor croak $!;1765binmode$log_fh;1766chomp($log_entry{log} = <$log_fh>);17671768my$enc= Git::config('i18n.commitencoding') ||'UTF-8';1769my$msg=$log_entry{log};17701771eval{$msg= Encode::decode($enc,$msg,1) };1772if($@) {1773die"Could not decode as$enc:\n",$msg,1774"\nPerhaps you need to set i18n.commitencoding\n";1775}17761777eval{$msg= Encode::encode('UTF-8',$msg,1) };1778die"Could not encode as UTF-8:\n$msg\n"if$@;17791780$log_entry{log} =$msg;17811782close$log_fhor croak $!;1783}1784unlink$commit_msg;1785 \%log_entry;1786}17871788sub s_to_file {1789my($str,$file,$mode) =@_;1790open my$fd,'>',$fileor croak $!;1791print$fd $str,"\n"or croak $!;1792close$fdor croak $!;1793chmod($mode&~umask,$file)if(defined$mode);1794}17951796sub file_to_s {1797my$file=shift;1798open my$fd,'<',$fileor croak "$!: file:$file\n";1799local$/;1800my$ret= <$fd>;1801close$fdor croak $!;1802$ret=~s/\s*$//s;1803return$ret;1804}18051806# '<svn username> = real-name <email address>' mapping based on git-svnimport:1807sub load_authors {1808open my$authors,'<',$_authorsor die"Can't open$_authors$!\n";1809my$log=$cmdeq'log';1810while(<$authors>) {1811chomp;1812next unless/^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;1813my($user,$name,$email) = ($1,$2,$3);1814if($log) {1815$Git::SVN::Log::rusers{"$name<$email>"} =$user;1816}else{1817$users{$user} = [$name,$email];1818}1819}1820close$authorsor croak $!;1821}18221823# convert GetOpt::Long specs for use by git-config1824sub read_git_config {1825my$opts=shift;1826my@config_only;1827foreachmy$o(keys%$opts) {1828# if we have mixedCase and a long option-only, then1829# it's a config-only variable that we don't need for1830# the command-line.1831push@config_only,$oif($o=~/[A-Z]/&&$o=~/^[a-z]+$/i);1832my$v=$opts->{$o};1833my($key) = ($o=~/^([a-zA-Z\-]+)/);1834$key=~s/-//g;1835my$arg='git config';1836$arg.=' --int'if($o=~/[:=]i$/);1837$arg.=' --bool'if($o!~/[:=][sfi]$/);1838if(ref$veq'ARRAY') {1839chomp(my@tmp=`$arg--get-all svn.$key`);1840@$v=@tmpif@tmp;1841 } else {1842 chomp(my$tmp= `$arg--get svn.$key`);1843if($tmp&& !($arg=~/ --bool/&&$tmpeq'false')) {1844$$v=$tmp;1845}1846}1847}1848delete@$opts{@config_only}if@config_only;1849}18501851sub extract_metadata {1852my$id=shift orreturn(undef,undef,undef);1853my($url,$rev,$uuid) = ($id=~ /^\s*git-svn-id:\s+(.*)\@(\d+)1854 \s([a-f\d\-]+)$/ix);1855if(!defined$rev|| !$uuid|| !$url) {1856# some of the original repositories I made had1857# identifiers like this:1858($rev,$uuid) = ($id=~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i);1859}1860return($url,$rev,$uuid);1861}18621863sub cmt_metadata {1864return extract_metadata((grep(/^git-svn-id: /,1865 command(qw/cat-file commit/,shift)))[-1]);1866}18671868sub cmt_sha2rev_batch {1869my%s2r;1870my($pid,$in,$out,$ctx) = command_bidi_pipe(qw/cat-file --batch/);1871my$list=shift;18721873foreachmy$sha(@{$list}) {1874my$first=1;1875my$size=0;1876print$out $sha,"\n";18771878while(my$line= <$in>) {1879if($first&&$line=~/^[[:xdigit:]]{40}\smissing$/) {1880last;1881}elsif($first&&1882$line=~/^[[:xdigit:]]{40}\scommit\s(\d+)$/) {1883$first=0;1884$size=$1;1885next;1886}elsif($line=~/^(git-svn-id: )/) {1887my(undef,$rev,undef) =1888 extract_metadata($line);1889$s2r{$sha} =$rev;1890}18911892$size-=length($line);1893last if($size==0);1894}1895}18961897 command_close_bidi_pipe($pid,$in,$out,$ctx);18981899return \%s2r;1900}19011902sub working_head_info {1903my($head,$refs) =@_;1904my@args= qw/rev-list --first-parent --pretty=medium/;1905my($fh,$ctx) = command_output_pipe(@args,$head);1906my$hash;1907my%max;1908while(<$fh>) {1909if(m{^commit ($::sha1)$}) {1910unshift@$refs,$hashif$hashand$refs;1911$hash=$1;1912next;1913}1914next unlesss{^\s*(git-svn-id:)}{$1};1915my($url,$rev,$uuid) = extract_metadata($_);1916if(defined$url&&defined$rev) {1917next if$max{$url}and$max{$url} <$rev;1918if(my$gs= Git::SVN->find_by_url($url)) {1919my$c=$gs->rev_map_get($rev,$uuid);1920if($c&&$ceq$hash) {1921close$fh;# break the pipe1922return($url,$rev,$uuid,$gs);1923}else{1924$max{$url} ||=$gs->rev_map_max;1925}1926}1927}1928}1929 command_close_pipe($fh,$ctx);1930(undef,undef,undef,undef);1931}19321933sub read_commit_parents {1934my($parents,$c) =@_;1935chomp(my$p= command_oneline(qw/rev-list --parents -1/,$c));1936$p=~s/^($c)\s*//or die"rev-list --parents -1$cfailed!\n";1937@{$parents->{$c}} =split(/ /,$p);1938}19391940sub linearize_history {1941my($gs,$refs) =@_;1942my%parents;1943foreachmy$c(@$refs) {1944 read_commit_parents(\%parents,$c);1945}19461947my@linear_refs;1948my%skip= ();1949my$last_svn_commit=$gs->last_commit;1950foreachmy$c(reverse@$refs) {1951next if$ceq$last_svn_commit;1952last if$skip{$c};19531954unshift@linear_refs,$c;1955$skip{$c} =1;19561957# we only want the first parent to diff against for linear1958# history, we save the rest to inject when we finalize the1959# svn commit1960my$fp_a= verify_ref("$c~1");1961my$fp_b=shift@{$parents{$c}}if$parents{$c};1962if(!$fp_a|| !$fp_b) {1963die"Commit$c\n",1964"has no parent commit, and therefore ",1965"nothing to diff against.\n",1966"You should be working from a repository ",1967"originally created by git-svn\n";1968}1969if($fp_ane$fp_b) {1970die"$c~1=$fp_a, however parsing commit$c",1971"revealed that:\n$c~1=$fp_b\nBUG!\n";1972}19731974foreachmy$p(@{$parents{$c}}) {1975$skip{$p} =1;1976}1977}1978(\@linear_refs, \%parents);1979}19801981sub find_file_type_and_diff_status {1982my($path) =@_;1983return('dir','')if$patheq'';19841985my$diff_output=1986 command_oneline(qw(diff --cached --name-status --),$path) ||"";1987my$diff_status= (split(' ',$diff_output))[0] ||"";19881989my$ls_tree= command_oneline(qw(ls-tree HEAD),$path) ||"";19901991return(undef,undef)if!$diff_status&& !$ls_tree;19921993if($diff_statuseq"A") {1994return("link",$diff_status)if-l $path;1995return("dir",$diff_status)if-d $path;1996return("file",$diff_status);1997}19981999my$mode= (split(' ',$ls_tree))[0] ||"";20002001return("link",$diff_status)if$modeeq"120000";2002return("dir",$diff_status)if$modeeq"040000";2003return("file",$diff_status);2004}20052006sub md5sum {2007my$arg=shift;2008my$ref=ref$arg;2009my$md5= Digest::MD5->new();2010if($refeq'GLOB'||$refeq'IO::File'||$refeq'File::Temp') {2011$md5->addfile($arg)or croak $!;2012}elsif($refeq'SCALAR') {2013$md5->add($$arg)or croak $!;2014}elsif(!$ref) {2015$md5->add($arg)or croak $!;2016}else{2017::fatal "Can't provide MD5 hash for unknown ref type: '",$ref,"'";2018}2019return$md5->hexdigest();2020}20212022sub gc_directory {2023if($can_compress&& -f $_&& basename($_)eq"unhandled.log") {2024my$out_filename=$_.".gz";2025open my$in_fh,"<",$_or die"Unable to open$_:$!\n";2026binmode$in_fh;2027my$gz= Compress::Zlib::gzopen($out_filename,"ab")or2028die"Unable to open$out_filename:$!\n";20292030my$res;2031while($res=sysread($in_fh,my$str,1024)) {2032$gz->gzwrite($str)or2033die"Unable to write: ".$gz->gzerror()."!\n";2034}2035unlink$_or die"unlink$File::Find::name:$!\n";2036}elsif(-f $_&& basename($_)eq"index") {2037unlink$_or die"unlink$_:$!\n";2038}2039}20402041package Git::SVN;2042use strict;2043use warnings;2044use Fcntl qw/:DEFAULT :seek/;2045useconstant rev_map_fmt =>'NH40';2046use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent2047$_repack $_repack_flags $_use_svm_props $_head2048$_use_svnsync_props $no_reuse_existing $_minimize_url2049$_use_log_author $_add_author_from $_localtime/;2050use Carp qw/croak/;2051use File::Path qw/mkpath/;2052use File::Copy qw/copy/;2053use IPC::Open3;2054use Time::Local;2055use Memoize;# core since 5.8.0, Jul 20022056use Memoize::Storable;2057use POSIX qw(:signal_h);20582059my($_gc_nr,$_gc_period);20602061# properties that we do not log:2062my%SKIP_PROP;2063BEGIN{2064%SKIP_PROP=map{$_=>1} qw/svn:wc:ra_dav:version-url2065 svn:special svn:executable2066 svn:entry:committed-rev2067 svn:entry:last-author2068 svn:entry:uuid2069 svn:entry:committed-date/;20702071# some options are read globally, but can be overridden locally2072# per [svn-remote "..."] section. Command-line options will *NOT*2073# override options set in an [svn-remote "..."] section2074no strict 'refs';2075formy$option(qw/follow_parent no_metadata use_svm_props2076 use_svnsync_props/) {2077my$key=$option;2078$key=~tr/_//d;2079my$prop="-$option";2080*$option=sub{2081my($self) =@_;2082return$self->{$prop}ifexists$self->{$prop};2083my$k="svn-remote.$self->{repo_id}.$key";2084eval{ command_oneline(qw/config --get/,$k) };2085if($@) {2086$self->{$prop} = ${"Git::SVN::_$option"};2087}else{2088my$v= command_oneline(qw/config --bool/,$k);2089$self->{$prop} =$veq'false'?0:1;2090}2091return$self->{$prop};2092}2093}2094}209520962097my(%LOCKFILES,%INDEX_FILES);2098END{2099unlink keys%LOCKFILESif%LOCKFILES;2100unlink keys%INDEX_FILESif%INDEX_FILES;2101}21022103sub resolve_local_globs {2104my($url,$fetch,$glob_spec) =@_;2105return unlessdefined$glob_spec;2106my$ref=$glob_spec->{ref};2107my$path=$glob_spec->{path};2108foreach(command(qw#for-each-ref --format=%(refname) refs/#)) {2109next unless m#^$ref->{regex}$#;2110my$p=$1;2111my$pathname= desanitize_refname($path->full_path($p));2112my$refname= desanitize_refname($ref->full_path($p));2113if(my$existing=$fetch->{$pathname}) {2114if($existingne$refname) {2115die"Refspec conflict:\n",2116"existing:$existing\n",2117" globbed:$refname\n";2118}2119my$u= (::cmt_metadata("$refname"))[0];2120$u=~s!^\Q$url\E(/|$)!!or die2121"$refname: '$url' not found in '$u'\n";2122if($pathnamene$u) {2123warn"W: Refspec glob conflict ",2124"(ref:$refname):\n",2125"expected path:$pathname\n",2126" real path:$u\n",2127"Continuing ahead with$u\n";2128next;2129}2130}else{2131$fetch->{$pathname} =$refname;2132}2133}2134}21352136sub parse_revision_argument {2137my($base,$head) =@_;2138if(!defined$::_revision || $::_revision eq'BASE:HEAD') {2139return($base,$head);2140}2141return($1,$2)if($::_revision =~/^(\d+):(\d+)$/);2142return($::_revision, $::_revision)if($::_revision =~/^\d+$/);2143return($head,$head)if($::_revision eq'HEAD');2144return($base,$1)if($::_revision =~/^BASE:(\d+)$/);2145return($1,$head)if($::_revision =~/^(\d+):HEAD$/);2146die"revision argument: $::_revision not understood by git-svn\n";2147}21482149sub fetch_all {2150my($repo_id,$remotes) =@_;2151if(ref$repo_id) {2152my$gs=$repo_id;2153$repo_id=undef;2154$repo_id=$gs->{repo_id};2155}2156$remotes||= read_all_remotes();2157my$remote=$remotes->{$repo_id}or2158die"[svn-remote\"$repo_id\"] unknown\n";2159my$fetch=$remote->{fetch};2160my$url=$remote->{url}or die"svn-remote.$repo_id.url not defined\n";2161my(@gs,@globs);2162my$ra= Git::SVN::Ra->new($url);2163my$uuid=$ra->get_uuid;2164my$head=$ra->get_latest_revnum;21652166# ignore errors, $head revision may not even exist anymore2167eval{$ra->get_log("",$head,0,1,0,1,sub{$head=$_[1] }) };2168warn"W:$@\n"if$@;21692170my$base=defined$fetch?$head:0;21712172# read the max revs for wildcard expansion (branches/*, tags/*)2173foreachmy$t(qw/branches tags/) {2174defined$remote->{$t}ornext;2175push@globs, @{$remote->{$t}};21762177my$max_rev=eval{ tmp_config(qw/--int --get/,2178"svn-remote.$repo_id.${t}-maxRev") };2179if(defined$max_rev&& ($max_rev<$base)) {2180$base=$max_rev;2181}elsif(!defined$max_rev) {2182$base=0;2183}2184}21852186if($fetch) {2187foreachmy$p(sort keys%$fetch) {2188my$gs= Git::SVN->new($fetch->{$p},$repo_id,$p);2189my$lr=$gs->rev_map_max;2190if(defined$lr) {2191$base=$lrif($lr<$base);2192}2193push@gs,$gs;2194}2195}21962197($base,$head) = parse_revision_argument($base,$head);2198$ra->gs_fetch_loop_common($base,$head, \@gs, \@globs);2199}22002201sub read_all_remotes {2202my$r= {};2203my$use_svm_props=eval{ command_oneline(qw/config --bool2204 svn.useSvmProps/) };2205$use_svm_props=$use_svm_propseq'true'if$use_svm_props;2206my$svn_refspec=qr{\s*(.*?)\s*:\s*(.+?)\s*};2207foreach(grep{s/^svn-remote\.//} command(qw/config -l/)) {2208if(m!^(.+)\.fetch=$svn_refspec$!) {2209my($remote,$local_ref,$remote_ref) = ($1,$2,$3);2210die("svn-remote.$remote: remote ref '$remote_ref' "2211."must start with 'refs/'\n")2212unless$remote_ref=~m{^refs/};2213$local_ref= uri_decode($local_ref);2214$r->{$remote}->{fetch}->{$local_ref} =$remote_ref;2215$r->{$remote}->{svm} = {}if$use_svm_props;2216}elsif(m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {2217$r->{$1}->{svm} = {};2218}elsif(m!^(.+)\.url=\s*(.*)\s*$!) {2219$r->{$1}->{url} =$2;2220}elsif(m!^(.+)\.pushurl=\s*(.*)\s*$!) {2221$r->{$1}->{pushurl} =$2;2222}elsif(m!^(.+)\.ignore-refs=\s*(.*)\s*$!) {2223$r->{$1}->{ignore_refs_regex} =$2;2224}elsif(m!^(.+)\.(branches|tags)=$svn_refspec$!) {2225my($remote,$t,$local_ref,$remote_ref) =2226($1,$2,$3,$4);2227die("svn-remote.$remote: remote ref '$remote_ref' ($t) "2228."must start with 'refs/'\n")2229unless$remote_ref=~m{^refs/};2230$local_ref= uri_decode($local_ref);2231my$rs= {2232 t =>$t,2233 remote =>$remote,2234 path => Git::SVN::GlobSpec->new($local_ref,1),2235ref=> Git::SVN::GlobSpec->new($remote_ref,0) };2236if(length($rs->{ref}->{right}) !=0) {2237die"The '*' glob character must be the last ",2238"character of '$remote_ref'\n";2239}2240push@{$r->{$remote}->{$t} },$rs;2241}2242}22432244map{2245if(defined$r->{$_}->{svm}) {2246my$svm;2247eval{2248my$section="svn-remote.$_";2249$svm= {2250 source => tmp_config('--get',2251"$section.svm-source"),2252 replace => tmp_config('--get',2253"$section.svm-replace"),2254}2255};2256$r->{$_}->{svm} =$svm;2257}2258}keys%$r;22592260foreachmy$remote(keys%$r) {2261foreach(grep{defined$_}2262map{$r->{$remote}->{$_} }qw(branches tags)) {2263foreachmy$rs(@$_) {2264$rs->{ignore_refs_regex} =2265$r->{$remote}->{ignore_refs_regex};2266}2267}2268}22692270$r;2271}22722273sub init_vars {2274$_gc_nr=$_gc_period=1000;2275if(defined$_repack||defined$_repack_flags) {2276warn"Repack options are obsolete; they have no effect.\n";2277}2278}22792280sub verify_remotes_sanity {2281return unless-d $ENV{GIT_DIR};2282my%seen;2283foreach(command(qw/config -l/)) {2284if(m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {2285if($seen{$1}) {2286die"Remote ref refs/remote/$1is tracked by",2287"\n \"$_\"\nand\n \"$seen{$1}\"\n",2288"Please resolve this ambiguity in ",2289"your git configuration file before ",2290"continuing\n";2291}2292$seen{$1} =$_;2293}2294}2295}22962297sub find_existing_remote {2298my($url,$remotes) =@_;2299returnundefif$no_reuse_existing;2300my$existing;2301foreachmy$repo_id(keys%$remotes) {2302my$u=$remotes->{$repo_id}->{url}ornext;2303next if$une$url;2304$existing=$repo_id;2305last;2306}2307$existing;2308}23092310sub init_remote_config {2311my($self,$url,$no_write) =@_;2312$url=~s!/+$!!;# strip trailing slash2313my$r= read_all_remotes();2314my$existing= find_existing_remote($url,$r);2315if($existing) {2316unless($no_write) {2317print STDERR "Using existing ",2318"[svn-remote\"$existing\"]\n";2319}2320$self->{repo_id} =$existing;2321}elsif($_minimize_url) {2322my$min_url= Git::SVN::Ra->new($url)->minimize_url;2323$existing= find_existing_remote($min_url,$r);2324if($existing) {2325unless($no_write) {2326print STDERR "Using existing ",2327"[svn-remote\"$existing\"]\n";2328}2329$self->{repo_id} =$existing;2330}2331if($min_urlne$url) {2332unless($no_write) {2333print STDERR "Using higher level of URL: ",2334"$url=>$min_url\n";2335}2336my$old_path=$self->{path};2337$self->{path} =$url;2338$self->{path} =~s!^\Q$min_url\E(/|$)!!;2339if(length$old_path) {2340$self->{path} .="/$old_path";2341}2342$url=$min_url;2343}2344}2345my$orig_url;2346if(!$existing) {2347# verify that we aren't overwriting anything:2348$orig_url=eval{2349 command_oneline('config','--get',2350"svn-remote.$self->{repo_id}.url")2351};2352if($orig_url&& ($orig_urlne$url)) {2353die"svn-remote.$self->{repo_id}.url already set: ",2354"$orig_url\nwanted to set to:$url\n";2355}2356}2357my($xrepo_id,$xpath) = find_ref($self->refname);2358if(!$no_write&&defined$xpath) {2359die"svn-remote.$xrepo_id.fetch already set to track ",2360"$xpath:",$self->refname,"\n";2361}2362unless($no_write) {2363 command_noisy('config',2364"svn-remote.$self->{repo_id}.url",$url);2365$self->{path} =~s{^/}{};2366$self->{path} =~s{%([0-9A-F]{2})}{chr hex($1)}ieg;2367 command_noisy('config','--add',2368"svn-remote.$self->{repo_id}.fetch",2369"$self->{path}:".$self->refname);2370}2371$self->{url} =$url;2372}23732374sub find_by_url {# repos_root and, path are optional2375my($class,$full_url,$repos_root,$path) =@_;23762377returnundefunlessdefined$full_url;2378 remove_username($full_url);2379 remove_username($repos_root)ifdefined$repos_root;2380my$remotes= read_all_remotes();2381if(defined$full_url&&defined$repos_root&& !defined$path) {2382$path=$full_url;2383$path=~ s#^\Q$repos_root\E(?:/|$)##;2384}2385foreachmy$repo_id(keys%$remotes) {2386my$u=$remotes->{$repo_id}->{url}ornext;2387 remove_username($u);2388next ifdefined$repos_root&&$repos_rootne$u;23892390my$fetch=$remotes->{$repo_id}->{fetch} || {};2391foreachmy$t(qw/branches tags/) {2392foreachmy$globspec(@{$remotes->{$repo_id}->{$t}}) {2393 resolve_local_globs($u,$fetch,$globspec);2394}2395}2396my$p=$path;2397my$rwr= rewrite_root({repo_id =>$repo_id});2398my$svm=$remotes->{$repo_id}->{svm}2399ifdefined$remotes->{$repo_id}->{svm};2400unless(defined$p) {2401$p=$full_url;2402my$z=$u;2403my$prefix='';2404if($rwr) {2405$z=$rwr;2406 remove_username($z);2407}elsif(defined$svm) {2408$z=$svm->{source};2409$prefix=$svm->{replace};2410$prefix=~ s#^\Q$u\E(?:/|$)##;2411$prefix=~ s#/$##;2412}2413$p=~ s#^\Q$z\E(?:/|$)#$prefix# or next;2414}2415foreachmy$f(keys%$fetch) {2416next if$fne$p;2417return Git::SVN->new($fetch->{$f},$repo_id,$f);2418}2419}2420undef;2421}24222423sub init {2424my($class,$url,$path,$repo_id,$ref_id,$no_write) =@_;2425my$self= _new($class,$repo_id,$ref_id,$path);2426if(defined$url) {2427$self->init_remote_config($url,$no_write);2428}2429$self;2430}24312432sub find_ref {2433my($ref_id) =@_;2434foreach(command(qw/config -l/)) {2435next unless m!^svn-remote\.(.+)\.fetch=2436 \s*(.*?)\s*:\s*(.+?)\s*$!x;2437my($repo_id,$path,$ref) = ($1,$2,$3);2438if($refeq$ref_id) {2439$path=''if($path=~ m#^\./?#);2440return($repo_id,$path);2441}2442}2443(undef,undef,undef);2444}24452446sub new {2447my($class,$ref_id,$repo_id,$path) =@_;2448if(defined$ref_id&& !defined$repo_id&& !defined$path) {2449($repo_id,$path) = find_ref($ref_id);2450if(!defined$repo_id) {2451die"Could not find a\"svn-remote.*.fetch\"key ",2452"in the repository configuration matching: ",2453"$ref_id\n";2454}2455}2456my$self= _new($class,$repo_id,$ref_id,$path);2457if(!defined$self->{path} || !length$self->{path}) {2458my$fetch= command_oneline('config','--get',2459"svn-remote.$repo_id.fetch",2460":$ref_id\$")or2461die"Failed to read\"svn-remote.$repo_id.fetch\"",2462"\":$ref_id\$\"in config\n";2463($self->{path},undef) =split(/\s*:\s*/,$fetch);2464}2465$self->{path} =~s{/+}{/}g;2466$self->{path} =~s{\A/}{};2467$self->{path} =~s{/\z}{};2468$self->{url} = command_oneline('config','--get',2469"svn-remote.$repo_id.url")or2470die"Failed to read\"svn-remote.$repo_id.url\"in config\n";2471$self->{pushurl} =eval{ command_oneline('config','--get',2472"svn-remote.$repo_id.pushurl") };2473$self->rebuild;2474$self;2475}24762477sub refname {2478my($refname) =$_[0]->{ref_id} ;24792480# It cannot end with a slash /, we'll throw up on this because2481# SVN can't have directories with a slash in their name, either:2482if($refname=~m{/$}) {2483die"ref: '$refname' ends with a trailing slash, this is ",2484"not permitted by git nor Subversion\n";2485}24862487# It cannot have ASCII control character space, tilde ~, caret ^,2488# colon :, question-mark ?, asterisk *, space, or open bracket [2489# anywhere.2490#2491# Additionally, % must be escaped because it is used for escaping2492# and we want our escaped refname to be reversible2493$refname=~s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;24942495# no slash-separated component can begin with a dot .2496# /.* becomes /%2E*2497$refname=~s{/\.}{/%2E}g;24982499# It cannot have two consecutive dots .. anywhere2500# .. becomes %2E%2E2501$refname=~s{\.\.}{%2E%2E}g;25022503# trailing dots and .lock are not allowed2504# .$ becomes %2E and .lock becomes %2Elock2505$refname=~s{\.(?=$|lock$)}{%2E};25062507# the sequence @{ is used to access the reflog2508# @{ becomes %40{2509$refname=~s{\@\{}{%40\{}g;25102511return$refname;2512}25132514sub desanitize_refname {2515my($refname) =@_;2516$refname=~s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;2517return$refname;2518}25192520sub svm_uuid {2521my($self) =@_;2522return$self->{svm}->{uuid}if$self->svm;2523$self->ra;2524unless($self->{svm}) {2525die"SVM UUID not cached, and reading remotely failed\n";2526}2527$self->{svm}->{uuid};2528}25292530sub svm {2531my($self) =@_;2532return$self->{svm}if$self->{svm};2533my$svm;2534# see if we have it in our config, first:2535eval{2536my$section="svn-remote.$self->{repo_id}";2537$svm= {2538 source => tmp_config('--get',"$section.svm-source"),2539 uuid => tmp_config('--get',"$section.svm-uuid"),2540 replace => tmp_config('--get',"$section.svm-replace"),2541}2542};2543if($svm&&$svm->{source} &&$svm->{uuid} &&$svm->{replace}) {2544$self->{svm} =$svm;2545}2546$self->{svm};2547}25482549sub _set_svm_vars {2550my($self,$ra) =@_;2551return$raif$self->svm;25522553my@err= ("useSvmProps set, but failed to read SVM properties\n",2554"(svm:source, svm:uuid) ",2555"from the following URLs:\n");2556sub read_svm_props {2557my($self,$ra,$path,$r) =@_;2558my$props= ($ra->get_dir($path,$r))[2];2559my$src=$props->{'svm:source'};2560my$uuid=$props->{'svm:uuid'};2561returnundefif(!$src|| !$uuid);25622563chomp($src,$uuid);25642565$uuid=~m{^[0-9a-f\-]{30,}$}i2566or die"doesn't look right - svm:uuid is '$uuid'\n";25672568# the '!' is used to mark the repos_root!/relative/path2569$src=~s{/?!/?}{/};2570$src=~s{/+$}{};# no trailing slashes please2571# username is of no interest2572$src=~s{(^[a-z\+]*://)[^/@]*@}{$1};25732574my$replace=$ra->{url};2575$replace.="/$path"iflength$path;25762577my$section="svn-remote.$self->{repo_id}";2578 tmp_config("$section.svm-source",$src);2579 tmp_config("$section.svm-replace",$replace);2580 tmp_config("$section.svm-uuid",$uuid);2581$self->{svm} = {2582 source =>$src,2583 uuid =>$uuid,2584 replace =>$replace2585};2586}25872588my$r=$ra->get_latest_revnum;2589my$path=$self->{path};2590my%tried;2591while(length$path) {2592unless($tried{"$self->{url}/$path"}) {2593return$raif$self->read_svm_props($ra,$path,$r);2594$tried{"$self->{url}/$path"} =1;2595}2596$path=~ s#/?[^/]+$##;2597}2598die"Path: '$path' should be ''\n"if$pathne'';2599return$raif$self->read_svm_props($ra,$path,$r);2600$tried{"$self->{url}/$path"} =1;26012602if($ra->{repos_root}eq$self->{url}) {2603die@err, (map{"$_\n"}keys%tried),"\n";2604}26052606# nope, make sure we're connected to the repository root:2607my$ok;2608my@tried_b;2609$path=$ra->{svn_path};2610$ra= Git::SVN::Ra->new($ra->{repos_root});2611while(length$path) {2612unless($tried{"$ra->{url}/$path"}) {2613$ok=$self->read_svm_props($ra,$path,$r);2614last if$ok;2615$tried{"$ra->{url}/$path"} =1;2616}2617$path=~ s#/?[^/]+$##;2618}2619die"Path: '$path' should be ''\n"if$pathne'';2620$ok||=$self->read_svm_props($ra,$path,$r);2621$tried{"$ra->{url}/$path"} =1;2622if(!$ok) {2623die@err, (map{"$_\n"}keys%tried),"\n";2624}2625 Git::SVN::Ra->new($self->{url});2626}26272628sub svnsync {2629my($self) =@_;2630return$self->{svnsync}if$self->{svnsync};26312632if($self->no_metadata) {2633die"Can't have both 'noMetadata' and ",2634"'useSvnsyncProps' options set!\n";2635}2636if($self->rewrite_root) {2637die"Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",2638"options set!\n";2639}2640if($self->rewrite_uuid) {2641die"Can't have both 'useSvnsyncProps' and 'rewriteUUID' ",2642"options set!\n";2643}26442645my$svnsync;2646# see if we have it in our config, first:2647eval{2648my$section="svn-remote.$self->{repo_id}";26492650my$url= tmp_config('--get',"$section.svnsync-url");2651($url) = ($url=~m{^([a-z\+]+://\S+)$})or2652die"doesn't look right - svn:sync-from-url is '$url'\n";26532654my$uuid= tmp_config('--get',"$section.svnsync-uuid");2655($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2656die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26572658$svnsync= { url =>$url, uuid =>$uuid}2659};2660if($svnsync&&$svnsync->{url} &&$svnsync->{uuid}) {2661return$self->{svnsync} =$svnsync;2662}26632664my$err="useSvnsyncProps set, but failed to read ".2665"svnsync property: svn:sync-from-";2666my$rp=$self->ra->rev_proplist(0);26672668my$url=$rp->{'svn:sync-from-url'}or die$err."url\n";2669($url) = ($url=~m{^([a-z\+]+://\S+)$})or2670die"doesn't look right - svn:sync-from-url is '$url'\n";26712672my$uuid=$rp->{'svn:sync-from-uuid'}or die$err."uuid\n";2673($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2674die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26752676my$section="svn-remote.$self->{repo_id}";2677 tmp_config('--add',"$section.svnsync-uuid",$uuid);2678 tmp_config('--add',"$section.svnsync-url",$url);2679return$self->{svnsync} = { url =>$url, uuid =>$uuid};2680}26812682# this allows us to memoize our SVN::Ra UUID locally and avoid a2683# remote lookup (useful for 'git svn log').2684sub ra_uuid {2685my($self) =@_;2686unless($self->{ra_uuid}) {2687my$key="svn-remote.$self->{repo_id}.uuid";2688my$uuid=eval{ tmp_config('--get',$key) };2689if(!$@&&$uuid&&$uuid=~/^([a-f\d\-]{30,})$/i) {2690$self->{ra_uuid} =$uuid;2691}else{2692die"ra_uuid called without URL\n"unless$self->{url};2693$self->{ra_uuid} =$self->ra->get_uuid;2694 tmp_config('--add',$key,$self->{ra_uuid});2695}2696}2697$self->{ra_uuid};2698}26992700sub _set_repos_root {2701my($self,$repos_root) =@_;2702my$k="svn-remote.$self->{repo_id}.reposRoot";2703$repos_root||=$self->ra->{repos_root};2704 tmp_config($k,$repos_root);2705$repos_root;2706}27072708sub repos_root {2709my($self) =@_;2710my$k="svn-remote.$self->{repo_id}.reposRoot";2711eval{ tmp_config('--get',$k) } ||$self->_set_repos_root;2712}27132714sub ra {2715my($self) =shift;2716my$ra= Git::SVN::Ra->new($self->{url});2717$self->_set_repos_root($ra->{repos_root});2718if($self->use_svm_props&& !$self->{svm}) {2719if($self->no_metadata) {2720die"Can't have both 'noMetadata' and ",2721"'useSvmProps' options set!\n";2722}elsif($self->use_svnsync_props) {2723die"Can't have both 'useSvnsyncProps' and ",2724"'useSvmProps' options set!\n";2725}2726$ra=$self->_set_svm_vars($ra);2727$self->{-want_revprops} =1;2728}2729$ra;2730}27312732# prop_walk(PATH, REV, SUB)2733# -------------------------2734# Recursively traverse PATH at revision REV and invoke SUB for each2735# directory that contains a SVN property. SUB will be invoked as2736# follows: &SUB(gs, path, props); where `gs' is this instance of2737# Git::SVN, `path' the path to the directory where the properties2738# `props' were found. The `path' will be relative to point of checkout,2739# that is, if url://repo/trunk is the current Git branch, and that2740# directory contains a sub-directory `d', SUB will be invoked with `/d/'2741# as `path' (note the trailing `/').2742sub prop_walk {2743my($self,$path,$rev,$sub) =@_;27442745$path=~ s#^/##;2746my($dirent,undef,$props) =$self->ra->get_dir($path,$rev);2747$path=~ s#^/*#/#g;2748my$p=$path;2749# Strip the irrelevant part of the path.2750$p=~ s#^/+\Q$self->{path}\E(/|$)#/#;2751# Ensure the path is terminated by a `/'.2752$p=~ s#/*$#/#;27532754# The properties contain all the internal SVN stuff nobody2755# (usually) cares about.2756my$interesting_props=0;2757foreach(keys%{$props}) {2758# If it doesn't start with `svn:', it must be a2759# user-defined property.2760++$interesting_propsandnext if$_!~/^svn:/;2761# FIXME: Fragile, if SVN adds new public properties,2762# this needs to be updated.2763++$interesting_propsif/^svn:(?:ignore|keywords|executable2764|eol-style|mime-type2765|externals|needs-lock)$/x;2766}2767&$sub($self,$p,$props)if$interesting_props;27682769foreach(sort keys%$dirent) {2770next if$dirent->{$_}->{kind} !=$SVN::Node::dir;2771$self->prop_walk($self->{path} .$p.$_,$rev,$sub);2772}2773}27742775sub last_rev { ($_[0]->last_rev_commit)[0] }2776sub last_commit { ($_[0]->last_rev_commit)[1] }27772778# returns the newest SVN revision number and newest commit SHA12779sub last_rev_commit {2780my($self) =@_;2781if(defined$self->{last_rev} &&defined$self->{last_commit}) {2782return($self->{last_rev},$self->{last_commit});2783}2784my$c= ::verify_ref($self->refname.'^0');2785if($c&& !$self->use_svm_props&& !$self->no_metadata) {2786my$rev= (::cmt_metadata($c))[1];2787if(defined$rev) {2788($self->{last_rev},$self->{last_commit}) = ($rev,$c);2789return($rev,$c);2790}2791}2792my$map_path=$self->map_path;2793unless(-e $map_path) {2794($self->{last_rev},$self->{last_commit}) = (undef,undef);2795return(undef,undef);2796}2797my($rev,$commit) =$self->rev_map_max(1);2798($self->{last_rev},$self->{last_commit}) = ($rev,$commit);2799return($rev,$commit);2800}28012802sub get_fetch_range {2803my($self,$min,$max) =@_;2804$max||=$self->ra->get_latest_revnum;2805$min||=$self->rev_map_max;2806(++$min,$max);2807}28082809sub tmp_config {2810my(@args) =@_;2811my$old_def_config="$ENV{GIT_DIR}/svn/config";2812my$config="$ENV{GIT_DIR}/svn/.metadata";2813if(! -f $config&& -f $old_def_config) {2814rename$old_def_config,$configor2815die"Failed rename$old_def_config=>$config:$!\n";2816}2817my$old_config=$ENV{GIT_CONFIG};2818$ENV{GIT_CONFIG} =$config;2819$@=undef;2820my@ret=eval{2821unless(-f $config) {2822 mkfile($config);2823open my$fh,'>',$configor2824die"Can't open$config:$!\n";2825print$fh"; This file is used internally by ",2826"git-svn\n"or die2827"Couldn't write to$config:$!\n";2828print$fh"; You should not have to edit it\n"or2829die"Couldn't write to$config:$!\n";2830close$fhor die"Couldn't close$config:$!\n";2831}2832 command('config',@args);2833};2834my$err=$@;2835if(defined$old_config) {2836$ENV{GIT_CONFIG} =$old_config;2837}else{2838delete$ENV{GIT_CONFIG};2839}2840die$errif$err;2841wantarray?@ret:$ret[0];2842}28432844sub tmp_index_do {2845my($self,$sub) =@_;2846my$old_index=$ENV{GIT_INDEX_FILE};2847$ENV{GIT_INDEX_FILE} =$self->{index};2848$@=undef;2849my@ret=eval{2850my($dir,$base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);2851 mkpath([$dir])unless-d $dir;2852&$sub;2853};2854my$err=$@;2855if(defined$old_index) {2856$ENV{GIT_INDEX_FILE} =$old_index;2857}else{2858delete$ENV{GIT_INDEX_FILE};2859}2860die$errif$err;2861wantarray?@ret:$ret[0];2862}28632864sub assert_index_clean {2865my($self,$treeish) =@_;28662867$self->tmp_index_do(sub{2868 command_noisy('read-tree',$treeish)unless-e $self->{index};2869my$x= command_oneline('write-tree');2870my($y) = (command(qw/cat-file commit/,$treeish) =~2871/^tree ($::sha1)/mo);2872return if$yeq$x;28732874warn"Index mismatch:$y!=$x\nrereading$treeish\n";2875unlink$self->{index}or die"unlink$self->{index}:$!\n";2876 command_noisy('read-tree',$treeish);2877$x= command_oneline('write-tree');2878if($yne$x) {2879::fatal "trees ($treeish)$y!=$x\n",2880"Something is seriously wrong...";2881}2882});2883}28842885sub get_commit_parents {2886my($self,$log_entry) =@_;2887my(%seen,@ret,@tmp);2888# legacy support for 'set-tree'; this is only used by set_tree_cb:2889if(my$ip=$self->{inject_parents}) {2890if(my$commit=delete$ip->{$log_entry->{revision}}) {2891push@tmp,$commit;2892}2893}2894if(my$cur= ::verify_ref($self->refname.'^0')) {2895push@tmp,$cur;2896}2897if(my$ipd=$self->{inject_parents_dcommit}) {2898if(my$commit=delete$ipd->{$log_entry->{revision}}) {2899push@tmp,@$commit;2900}2901}2902push@tmp,$_foreach(@{$log_entry->{parents}},@tmp);2903while(my$p=shift@tmp) {2904next if$seen{$p};2905$seen{$p} =1;2906push@ret,$p;2907}2908@ret;2909}29102911sub rewrite_root {2912my($self) =@_;2913return$self->{-rewrite_root}ifexists$self->{-rewrite_root};2914my$k="svn-remote.$self->{repo_id}.rewriteRoot";2915my$rwr=eval{ command_oneline(qw/config --get/,$k) };2916if($rwr) {2917$rwr=~ s#/+$##;2918if($rwr!~ m#^[a-z\+]+://#) {2919die"$rwris not a valid URL (key:$k)\n";2920}2921}2922$self->{-rewrite_root} =$rwr;2923}29242925sub rewrite_uuid {2926my($self) =@_;2927return$self->{-rewrite_uuid}ifexists$self->{-rewrite_uuid};2928my$k="svn-remote.$self->{repo_id}.rewriteUUID";2929my$rwid=eval{ command_oneline(qw/config --get/,$k) };2930if($rwid) {2931$rwid=~ s#/+$##;2932if($rwid!~ m#^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$#) {2933die"$rwidis not a valid UUID (key:$k)\n";2934}2935}2936$self->{-rewrite_uuid} =$rwid;2937}29382939sub metadata_url {2940my($self) =@_;2941($self->rewrite_root||$self->{url}) .2942(length$self->{path} ?'/'.$self->{path} :'');2943}29442945sub full_url {2946my($self) =@_;2947$self->{url} . (length$self->{path} ?'/'.$self->{path} :'');2948}29492950sub full_pushurl {2951my($self) =@_;2952if($self->{pushurl}) {2953return$self->{pushurl} . (length$self->{path} ?'/'.2954$self->{path} :'');2955}else{2956return$self->full_url;2957}2958}29592960sub set_commit_header_env {2961my($log_entry) =@_;2962my%env;2963foreachmy$ned(qw/NAME EMAIL DATE/) {2964foreachmy$ac(qw/AUTHOR COMMITTER/) {2965$env{"GIT_${ac}_${ned}"} =$ENV{"GIT_${ac}_${ned}"};2966}2967}29682969$ENV{GIT_AUTHOR_NAME} =$log_entry->{name};2970$ENV{GIT_AUTHOR_EMAIL} =$log_entry->{email};2971$ENV{GIT_AUTHOR_DATE} =$ENV{GIT_COMMITTER_DATE} =$log_entry->{date};29722973$ENV{GIT_COMMITTER_NAME} = (defined$log_entry->{commit_name})2974?$log_entry->{commit_name}2975:$log_entry->{name};2976$ENV{GIT_COMMITTER_EMAIL} = (defined$log_entry->{commit_email})2977?$log_entry->{commit_email}2978:$log_entry->{email};2979 \%env;2980}29812982sub restore_commit_header_env {2983my($env) =@_;2984foreachmy$ned(qw/NAME EMAIL DATE/) {2985foreachmy$ac(qw/AUTHOR COMMITTER/) {2986my$k="GIT_${ac}_${ned}";2987if(defined$env->{$k}) {2988$ENV{$k} =$env->{$k};2989}else{2990delete$ENV{$k};2991}2992}2993}2994}29952996sub gc {2997 command_noisy('gc','--auto');2998};29993000sub do_git_commit {3001my($self,$log_entry) =@_;3002my$lr=$self->last_rev;3003if(defined$lr&&$lr>=$log_entry->{revision}) {3004die"Last fetched revision of ",$self->refname,3005" was r$lr, but we are about to fetch: ",3006"r$log_entry->{revision}!\n";3007}3008if(my$c=$self->rev_map_get($log_entry->{revision})) {3009 croak "$log_entry->{revision} =$calready exists! ",3010"Why are we refetching it?\n";3011}3012my$old_env= set_commit_header_env($log_entry);3013my$tree=$log_entry->{tree};3014if(!defined$tree) {3015$tree=$self->tmp_index_do(sub{3016 command_oneline('write-tree') });3017}3018die"Tree is not a valid sha1:$tree\n"if$tree!~/^$::sha1$/o;30193020my@exec= ('git','commit-tree',$tree);3021foreach($self->get_commit_parents($log_entry)) {3022push@exec,'-p',$_;3023}3024defined(my$pid= open3(my$msg_fh,my$out_fh,'>&STDERR',@exec))3025or croak $!;3026binmode$msg_fh;30273028# we always get UTF-8 from SVN, but we may want our commits in3029# a different encoding.3030if(my$enc= Git::config('i18n.commitencoding')) {3031require Encode;3032 Encode::from_to($log_entry->{log},'UTF-8',$enc);3033}3034print$msg_fh $log_entry->{log}or croak $!;3035 restore_commit_header_env($old_env);3036unless($self->no_metadata) {3037print$msg_fh"\ngit-svn-id:$log_entry->{metadata}\n"3038or croak $!;3039}3040$msg_fh->flush==0or croak $!;3041close$msg_fhor croak $!;3042chomp(my$commit=do{local$/; <$out_fh> });3043close$out_fhor croak $!;3044waitpid$pid,0;3045 croak $?if$?;3046if($commit!~/^$::sha1$/o) {3047die"Failed to commit, invalid sha1:$commit\n";3048}30493050$self->rev_map_set($log_entry->{revision},$commit,1);30513052$self->{last_rev} =$log_entry->{revision};3053$self->{last_commit} =$commit;3054print"r$log_entry->{revision}"unless$::_q >1;3055if(defined$log_entry->{svm_revision}) {3056print" (\@$log_entry->{svm_revision})"unless$::_q >1;3057$self->rev_map_set($log_entry->{svm_revision},$commit,30580,$self->svm_uuid);3059}3060print" =$commit($self->{ref_id})\n"unless$::_q >1;3061if(--$_gc_nr==0) {3062$_gc_nr=$_gc_period;3063 gc();3064}3065return$commit;3066}30673068sub match_paths {3069my($self,$paths,$r) =@_;3070return1if$self->{path}eq'';3071if(my$path=$paths->{"/$self->{path}"}) {3072return($path->{action}eq'D') ?0:1;3073}3074$self->{path_regex} ||=qr/^\/\Q$self->{path}\E\//;3075if(grep/$self->{path_regex}/,keys%$paths) {3076return1;3077}3078my$c='';3079foreach(split m#/#, $self->{path}) {3080$c.="/$_";3081next unless($paths->{$c} &&3082($paths->{$c}->{action} =~/^[AR]$/));3083if($self->ra->check_path($self->{path},$r) ==3084$SVN::Node::dir) {3085return1;3086}3087}3088return0;3089}30903091sub find_parent_branch {3092my($self,$paths,$rev) =@_;3093returnundefunless$self->follow_parent;3094unless(defined$paths) {3095my$err_handler=$SVN::Error::handler;3096$SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;3097$self->ra->get_log([$self->{path}],$rev,$rev,0,1,1,3098sub{$paths=$_[0] });3099$SVN::Error::handler =$err_handler;3100}3101returnundefunlessdefined$paths;31023103# look for a parent from another branch:3104my@b_path_components=split m#/#, $self->{path};3105my@a_path_components;3106my$i;3107while(@b_path_components) {3108$i=$paths->{'/'.join('/',@b_path_components)};3109last if$i&&defined$i->{copyfrom_path};3110unshift(@a_path_components,pop(@b_path_components));3111}3112returnundefunlessdefined$i&&defined$i->{copyfrom_path};3113my$branch_from=$i->{copyfrom_path};3114if(@a_path_components) {3115print STDERR "branch_from:$branch_from=> ";3116$branch_from.='/'.join('/',@a_path_components);3117print STDERR $branch_from,"\n";3118}3119my$r=$i->{copyfrom_rev};3120my$repos_root=$self->ra->{repos_root};3121my$url=$self->ra->{url};3122my$new_url=$url.$branch_from;3123print STDERR "Found possible branch point: ",3124"$new_url=> ",$self->full_url,",$r\n"3125unless$::_q >1;3126$branch_from=~ s#^/##;3127my$gs=$self->other_gs($new_url,$url,3128$branch_from,$r,$self->{ref_id});3129my($r0,$parent) =$gs->find_rev_before($r,1);3130{3131my($base,$head);3132if(!defined$r0|| !defined$parent) {3133($base,$head) = parse_revision_argument(0,$r);3134}else{3135if($r0<$r) {3136$gs->ra->get_log([$gs->{path}],$r0+1,$r,1,31370,1,sub{$base=$_[1] -1});3138}3139}3140if(defined$base&&$base<=$r) {3141$gs->fetch($base,$r);3142}3143($r0,$parent) =$gs->find_rev_before($r,1);3144}3145if(defined$r0&&defined$parent) {3146print STDERR "Found branch parent: ($self->{ref_id})$parent\n"3147unless$::_q >1;3148my$ed;3149if($self->ra->can_do_switch) {3150$self->assert_index_clean($parent);3151print STDERR "Following parent with do_switch\n"3152unless$::_q >1;3153# do_switch works with svn/trunk >= r22312, but that3154# is not included with SVN 1.4.3 (the latest version3155# at the moment), so we can't rely on it3156$self->{last_rev} =$r0;3157$self->{last_commit} =$parent;3158$ed= Git::SVN::Fetcher->new($self,$gs->{path});3159$gs->ra->gs_do_switch($r0,$rev,$gs,3160$self->full_url,$ed)3161or die"SVN connection failed somewhere...\n";3162}elsif($self->ra->trees_match($new_url,$r0,3163$self->full_url,$rev)) {3164print STDERR "Trees match:\n",3165"$new_url\@$r0\n",3166" ${\$self->full_url}\@$rev\n",3167"Following parent with no changes\n"3168unless$::_q >1;3169$self->tmp_index_do(sub{3170 command_noisy('read-tree',$parent);3171});3172$self->{last_commit} =$parent;3173}else{3174print STDERR "Following parent with do_update\n"3175unless$::_q >1;3176$ed= Git::SVN::Fetcher->new($self);3177$self->ra->gs_do_update($rev,$rev,$self,$ed)3178or die"SVN connection failed somewhere...\n";3179}3180print STDERR "Successfully followed parent\n"unless$::_q >1;3181return$self->make_log_entry($rev, [$parent],$ed);3182}3183returnundef;3184}31853186sub do_fetch {3187my($self,$paths,$rev) =@_;3188my$ed;3189my($last_rev,@parents);3190if(my$lc=$self->last_commit) {3191# we can have a branch that was deleted, then re-added3192# under the same name but copied from another path, in3193# which case we'll have multiple parents (we don't3194# want to break the original ref, nor lose copypath info):3195if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3196push@{$log_entry->{parents}},$lc;3197return$log_entry;3198}3199$ed= Git::SVN::Fetcher->new($self);3200$last_rev=$self->{last_rev};3201$ed->{c} =$lc;3202@parents= ($lc);3203}else{3204$last_rev=$rev;3205if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3206return$log_entry;3207}3208$ed= Git::SVN::Fetcher->new($self);3209}3210unless($self->ra->gs_do_update($last_rev,$rev,$self,$ed)) {3211die"SVN connection failed somewhere...\n";3212}3213$self->make_log_entry($rev, \@parents,$ed);3214}32153216sub mkemptydirs {3217my($self,$r) =@_;32183219sub scan {3220my($r,$empty_dirs,$line) =@_;3221if(defined$r&&$line=~/^r(\d+)$/) {3222return0if$1>$r;3223}elsif($line=~/^ \+empty_dir: (.+)$/) {3224$empty_dirs->{$1} =1;3225}elsif($line=~/^ \-empty_dir: (.+)$/) {3226my@d=grep{m[^\Q$1\E(/|$)]} (keys%$empty_dirs);3227delete@$empty_dirs{@d};3228}32291;# continue3230};32313232my%empty_dirs= ();3233my$gz_file="$self->{dir}/unhandled.log.gz";3234if(-f $gz_file) {3235if(!$can_compress) {3236warn"Compress::Zlib could not be found; ",3237"empty directories in$gz_filewill not be read\n";3238}else{3239my$gz= Compress::Zlib::gzopen($gz_file,"rb")or3240die"Unable to open$gz_file:$!\n";3241my$line;3242while($gz->gzreadline($line) >0) {3243 scan($r, \%empty_dirs,$line)orlast;3244}3245$gz->gzclose;3246}3247}32483249if(open my$fh,'<',"$self->{dir}/unhandled.log") {3250binmode$fhor croak "binmode:$!";3251while(<$fh>) {3252 scan($r, \%empty_dirs,$_)orlast;3253}3254close$fh;3255}32563257my$strip=qr/\A\Q$self->{path}\E(?:\/|$)/;3258foreachmy$d(sort keys%empty_dirs) {3259$d= uri_decode($d);3260$d=~s/$strip//;3261next unlesslength($d);3262next if-d $d;3263if(-e $d) {3264warn"$dexists but is not a directory\n";3265}else{3266print"creating empty directory:$d\n";3267 mkpath([$d]);3268}3269}3270}32713272sub get_untracked {3273my($self,$ed) =@_;3274my@out;3275my$h=$ed->{empty};3276foreach(sort keys%$h) {3277my$act=$h->{$_} ?'+empty_dir':'-empty_dir';3278push@out,"$act: ". uri_encode($_);3279warn"W:$act:$_\n";3280}3281foreachmy$t(qw/dir_prop file_prop/) {3282$h=$ed->{$t}ornext;3283foreachmy$path(sort keys%$h) {3284my$ppath=$patheq''?'.':$path;3285foreachmy$prop(sort keys%{$h->{$path}}) {3286next if$SKIP_PROP{$prop};3287my$v=$h->{$path}->{$prop};3288my$t_ppath_prop="$t: ".3289 uri_encode($ppath) .' '.3290 uri_encode($prop);3291if(defined$v) {3292push@out," +$t_ppath_prop".3293 uri_encode($v);3294}else{3295push@out," -$t_ppath_prop";3296}3297}3298}3299}3300foreachmy$t(qw/absent_file absent_directory/) {3301$h=$ed->{$t}ornext;3302foreachmy$parent(sort keys%$h) {3303foreachmy$path(sort@{$h->{$parent}}) {3304push@out,"$t: ".3305 uri_encode("$parent/$path");3306warn"W:$t:$parent/$path",3307"Insufficient permissions?\n";3308}3309}3310}3311 \@out;3312}33133314sub get_tz {3315# some systmes don't handle or mishandle %z, so be creative.3316my$t=shift||time;3317my$gm= timelocal(gmtime($t));3318my$sign=qw( + + - )[$t<=>$gm];3319returnsprintf("%s%02d%02d",$sign, (gmtime(abs($t-$gm)))[2,1]);3320}33213322# parse_svn_date(DATE)3323# --------------------3324# Given a date (in UTC) from Subversion, return a string in the format3325# "<TZ Offset> <local date/time>" that Git will use.3326#3327# By default the parsed date will be in UTC; if $Git::SVN::_localtime3328# is true we'll convert it to the local timezone instead.3329sub parse_svn_date {3330my$date=shift||return'+0000 1970-01-01 00:00:00';3331my($Y,$m,$d,$H,$M,$S) = ($date=~ /^(\d{4})\-(\d\d)\-(\d\d)T3332(\d\d)\:(\d\d)\:(\d\d)\.\d*Z$/x)or3333 croak "Unable to parse date:$date\n";3334my$parsed_date;# Set next.33353336if($Git::SVN::_localtime) {3337# Translate the Subversion datetime to an epoch time.3338# Begin by switching ourselves to $date's timezone, UTC.3339my$old_env_TZ=$ENV{TZ};3340$ENV{TZ} ='UTC';33413342my$epoch_in_UTC=3343 POSIX::strftime('%s',$S,$M,$H,$d,$m-1,$Y-1900);33443345# Determine our local timezone (including DST) at the3346# time of $epoch_in_UTC. $Git::SVN::Log::TZ stored the3347# value of TZ, if any, at the time we were run.3348if(defined$Git::SVN::Log::TZ) {3349$ENV{TZ} =$Git::SVN::Log::TZ;3350}else{3351delete$ENV{TZ};3352}33533354my$our_TZ= get_tz();33553356# This converts $epoch_in_UTC into our local timezone.3357my($sec,$min,$hour,$mday,$mon,$year,3358$wday,$yday,$isdst) =localtime($epoch_in_UTC);33593360$parsed_date=sprintf('%s%04d-%02d-%02d%02d:%02d:%02d',3361$our_TZ,$year+1900,$mon+1,3362$mday,$hour,$min,$sec);33633364# Reset us to the timezone in effect when we entered3365# this routine.3366if(defined$old_env_TZ) {3367$ENV{TZ} =$old_env_TZ;3368}else{3369delete$ENV{TZ};3370}3371}else{3372$parsed_date="+0000$Y-$m-$d$H:$M:$S";3373}33743375return$parsed_date;3376}33773378sub other_gs {3379my($self,$new_url,$url,3380$branch_from,$r,$old_ref_id) =@_;3381my$gs= Git::SVN->find_by_url($new_url,$url,$branch_from);3382unless($gs) {3383my$ref_id=$old_ref_id;3384$ref_id=~s/\@\d+-*$//;3385$ref_id.="\@$r";3386# just grow a tail if we're not unique enough :x3387$ref_id.='-'while find_ref($ref_id);3388my($u,$p,$repo_id) = ($new_url,'',$ref_id);3389if($u=~ s#^\Q$url\E(/|$)##) {3390$p=$u;3391$u=$url;3392$repo_id=$self->{repo_id};3393}3394while(1) {3395# It is possible to tag two different subdirectories at3396# the same revision. If the url for an existing ref3397# does not match, we must either find a ref with a3398# matching url or create a new ref by growing a tail.3399$gs= Git::SVN->init($u,$p,$repo_id,$ref_id,1);3400my(undef,$max_commit) =$gs->rev_map_max(1);3401last if(!$max_commit);3402my($url) = ::cmt_metadata($max_commit);3403last if($urleq$gs->metadata_url);3404$ref_id.='-';3405}3406print STDERR "Initializing parent:$ref_id\n"unless$::_q >1;3407}3408$gs3409}34103411sub call_authors_prog {3412my($orig_author) =@_;3413$orig_author= command_oneline('rev-parse','--sq-quote',$orig_author);3414my$author=`$::_authors_prog$orig_author`;3415 if ($?!= 0) {3416 die "$::_authors_prog failed with exit code$?\n"3417 }3418 if ($author=~ /^\s*(.+?)\s*<(.*)>\s*$/) {3419 my ($name,$email) = ($1,$2);3420$email= undef if length$2== 0;3421 return [$name,$email];3422 } else {3423 die "Author:$orig_author: $::_authors_prog returned "3424 . "invalid author format:$author\n";3425 }3426}34273428sub check_author {3429 my ($author) =@_;3430 if (!defined$author|| length$author== 0) {3431$author= '(no author)';3432 }3433 if (!defined $::users{$author}) {3434 if (defined $::_authors_prog) {3435 $::users{$author} = call_authors_prog($author);3436 } elsif (defined $::_authors) {3437 die "Author:$authornot defined in $::_authors file\n";3438 }3439 }3440$author;3441}34423443sub find_extra_svk_parents {3444 my ($self,$ed,$tickets,$parents) =@_;3445 # aha! svk:merge property changed...3446 my@tickets= split "\n",$tickets;3447 my@known_parents;3448 for my$ticket(@tickets) {3449 my ($uuid,$path,$rev) = split /:/,$ticket;3450 if ($uuideq$self->ra_uuid ) {3451 my$url=$self->{url};3452 my$repos_root=$url;3453 my$branch_from=$path;3454$branch_from=~ s{^/}{};3455 my$gs=$self->other_gs($repos_root."/".$branch_from,3456$url,3457$branch_from,3458$rev,3459$self->{ref_id});3460 if ( my$commit=$gs->rev_map_get($rev,$uuid) ) {3461 # wahey! we found it, but it might be3462 # an old one (!)3463 push@known_parents, [$rev,$commit];3464 }3465 }3466 }3467 # Ordering matters; highest-numbered commit merge tickets3468 # first, as they may account for later merge ticket additions3469 # or changes.3470@known_parents= map {$_->[1]} sort {$b->[0] <=>$a->[0]}@known_parents;3471 for my$parent(@known_parents) {3472 my@cmd= ('rev-list',$parent, map { "^$_" }@$parents);3473 my ($msg_fh,$ctx) = command_output_pipe(@cmd);3474 my$new;3475 while ( <$msg_fh> ) {3476$new=1;last;3477 }3478 command_close_pipe($msg_fh,$ctx);3479 if ($new) {3480 print STDERR3481 "Found merge parent (svk:merge ticket):$parent\n";3482 push@$parents,$parent;3483 }3484 }3485}34863487sub lookup_svn_merge {3488 my$uuid= shift;3489 my$url= shift;3490 my$merge= shift;34913492 my ($source,$revs) = split ":",$merge;3493 my$path=$source;3494$path=~ s{^/}{};3495 my$gs= Git::SVN->find_by_url($url.$source,$url,$path);3496 if ( !$gs) {3497 warn "Couldn't find revmap for$url$source\n";3498 return;3499 }3500 my@ranges= split ",",$revs;3501 my ($tip,$tip_commit);3502 my@merged_commit_ranges;3503 # find the tip3504 for my$range(@ranges) {3505 my ($bottom,$top) = split "-",$range;3506$top||=$bottom;3507 my$bottom_commit=$gs->find_rev_after($bottom, 1,$top);3508 my$top_commit=$gs->find_rev_before($top, 1,$bottom);35093510 unless ($top_commitand$bottom_commit) {3511 warn "W:unknown path/rev in svn:mergeinfo "3512 ."dirprop:$source:$range\n";3513 next;3514 }35153516 if (scalar(command('rev-parse', "$bottom_commit^@"))) {3517 push@merged_commit_ranges,3518 "$bottom_commit^..$top_commit";3519 } else {3520 push@merged_commit_ranges, "$top_commit";3521 }35223523 if ( !defined$tipor$top>$tip) {3524$tip=$top;3525$tip_commit=$top_commit;3526 }3527 }3528 return ($tip_commit,@merged_commit_ranges);3529}35303531sub _rev_list {3532 my ($msg_fh,$ctx) = command_output_pipe(3533 "rev-list",@_,3534 );3535 my@rv;3536 while ( <$msg_fh> ) {3537 chomp;3538 push@rv,$_;3539 }3540 command_close_pipe($msg_fh,$ctx);3541@rv;3542}35433544sub check_cherry_pick {3545 my$base= shift;3546 my$tip= shift;3547 my$parents= shift;3548 my@ranges=@_;3549 my%commits= map {$_=> 1 }3550 _rev_list("--no-merges",$tip, "--not",$base,@$parents, "--");3551 for my$range(@ranges) {3552 delete@commits{_rev_list($range, "--")};3553 }3554 for my$commit(keys%commits) {3555 if (has_no_changes($commit)) {3556 delete$commits{$commit};3557 }3558 }3559 return (keys%commits);3560}35613562sub has_no_changes {3563 my$commit= shift;35643565 my@revs= split / /, command_oneline(3566 qw(rev-list --parents -1 -m),$commit);35673568# Commits with no parents, e.g. the start of a partial branch,3569# have changes by definition.3570return1if(@revs<2);35713572# Commits with multiple parents, e.g a merge, have no changes3573# by definition.3574return0if(@revs>2);35753576return(command_oneline("rev-parse","$commit^{tree}")eq3577 command_oneline("rev-parse","$commit~1^{tree}"));3578}35793580# The GIT_DIR environment variable is not always set until after the command3581# line arguments are processed, so we can't memoize in a BEGIN block.3582{3583my$memoized=0;35843585sub memoize_svn_mergeinfo_functions {3586return if$memoized;3587$memoized=1;35883589my$cache_path="$ENV{GIT_DIR}/svn/.caches/";3590 mkpath([$cache_path])unless-d $cache_path;35913592 tie my%lookup_svn_merge_cache=>'Memoize::Storable',3593"$cache_path/lookup_svn_merge.db",'nstore';3594 memoize 'lookup_svn_merge',3595 SCALAR_CACHE =>'FAULT',3596 LIST_CACHE => ['HASH'=> \%lookup_svn_merge_cache],3597;35983599 tie my%check_cherry_pick_cache=>'Memoize::Storable',3600"$cache_path/check_cherry_pick.db",'nstore';3601 memoize 'check_cherry_pick',3602 SCALAR_CACHE =>'FAULT',3603 LIST_CACHE => ['HASH'=> \%check_cherry_pick_cache],3604;36053606 tie my%has_no_changes_cache=>'Memoize::Storable',3607"$cache_path/has_no_changes.db",'nstore';3608 memoize 'has_no_changes',3609 SCALAR_CACHE => ['HASH'=> \%has_no_changes_cache],3610 LIST_CACHE =>'FAULT',3611;3612}36133614sub unmemoize_svn_mergeinfo_functions {3615return ifnot$memoized;3616$memoized=0;36173618 Memoize::unmemoize 'lookup_svn_merge';3619 Memoize::unmemoize 'check_cherry_pick';3620 Memoize::unmemoize 'has_no_changes';3621}36223623 Memoize::memoize 'Git::SVN::repos_root';3624}36253626END{3627# Force cache writeout explicitly instead of waiting for3628# global destruction to avoid segfault in Storable:3629# http://rt.cpan.org/Public/Bug/Display.html?id=360873630 unmemoize_svn_mergeinfo_functions();3631}36323633sub parents_exclude {3634my$parents=shift;3635my@commits=@_;3636return unless@commits;36373638my@excluded;3639my$excluded;3640do{3641my@cmd= ('rev-list',"-1",@commits,"--not",@$parents);3642$excluded= command_oneline(@cmd);3643if($excluded) {3644my@new;3645my$found;3646formy$commit(@commits) {3647if($commiteq$excluded) {3648push@excluded,$commit;3649$found++;3650last;3651}3652else{3653push@new,$commit;3654}3655}3656die"saw commit '$excluded' in rev-list output, "3657."but we didn't ask for that commit (wanted:@commits--not@$parents)"3658unless$found;3659@commits=@new;3660}3661}3662while($excludedand@commits);36633664return@excluded;3665}366636673668# note: this function should only be called if the various dirprops3669# have actually changed3670sub find_extra_svn_parents {3671my($self,$ed,$mergeinfo,$parents) =@_;3672# aha! svk:merge property changed...36733674 memoize_svn_mergeinfo_functions();36753676# We first search for merged tips which are not in our3677# history. Then, we figure out which git revisions are in3678# that tip, but not this revision. If all of those revisions3679# are now marked as merge, we can add the tip as a parent.3680my@merges=split"\n",$mergeinfo;3681my@merge_tips;3682my$url=$self->{url};3683my$uuid=$self->ra_uuid;3684my%ranges;3685formy$merge(@merges) {3686my($tip_commit,@ranges) =3687 lookup_svn_merge($uuid,$url,$merge);3688unless(!$tip_commitor3689grep{$_eq$tip_commit}@$parents) {3690push@merge_tips,$tip_commit;3691$ranges{$tip_commit} = \@ranges;3692}else{3693push@merge_tips,undef;3694}3695}36963697my%excluded=map{$_=>1}3698 parents_exclude($parents,grep{defined}@merge_tips);36993700# check merge tips for new parents3701my@new_parents;3702formy$merge_tip(@merge_tips) {3703my$spec=shift@merges;3704next unless$merge_tipand$excluded{$merge_tip};37053706my$ranges=$ranges{$merge_tip};37073708# check out 'new' tips3709my$merge_base;3710eval{3711$merge_base= command_oneline(3712"merge-base",3713@$parents,$merge_tip,3714);3715};3716if($@) {3717die"An error occurred during merge-base"3718unless$@->isa("Git::Error::Command");37193720warn"W: Cannot find common ancestor between ".3721"@$parentsand$merge_tip. Ignoring merge info.\n";3722next;3723}37243725# double check that there are no missing non-merge commits3726my(@incomplete) = check_cherry_pick(3727$merge_base,$merge_tip,3728$parents,3729@$ranges,3730);37313732if(@incomplete) {3733warn"W:svn cherry-pick ignored ($spec) - missing "3734.@incomplete." commit(s) (eg$incomplete[0])\n";3735}else{3736warn3737"Found merge parent (svn:mergeinfo prop): ",3738$merge_tip,"\n";3739push@new_parents,$merge_tip;3740}3741}37423743# cater for merges which merge commits from multiple branches3744if(@new_parents>1) {3745for(my$i=0;$i<=$#new_parents;$i++) {3746for(my$j=0;$j<=$#new_parents;$j++) {3747next if$i==$j;3748next unless$new_parents[$i];3749next unless$new_parents[$j];3750my$revs= command_oneline(3751"rev-list","-1",3752"$new_parents[$i]..$new_parents[$j]",3753);3754if( !$revs) {3755undef($new_parents[$j]);3756}3757}3758}3759}3760push@$parents,grep{defined}@new_parents;3761}37623763sub make_log_entry {3764my($self,$rev,$parents,$ed) =@_;3765my$untracked=$self->get_untracked($ed);37663767my@parents=@$parents;3768my$ps=$ed->{path_strip} ||"";3769formy$path(grep{m/$ps/} %{$ed->{dir_prop}} ) {3770my$props=$ed->{dir_prop}{$path};3771if($props->{"svk:merge"} ) {3772$self->find_extra_svk_parents3773($ed,$props->{"svk:merge"}, \@parents);3774}3775if($props->{"svn:mergeinfo"} ) {3776$self->find_extra_svn_parents3777($ed,3778$props->{"svn:mergeinfo"},3779 \@parents);3780}3781}37823783open my$un,'>>',"$self->{dir}/unhandled.log"or croak $!;3784print$un"r$rev\n"or croak $!;3785print$un $_,"\n"foreach@$untracked;3786my%log_entry= ( parents => \@parents, revision =>$rev,3787log=>'');37883789my$headrev;3790my$logged=delete$self->{logged_rev_props};3791if(!$logged||$self->{-want_revprops}) {3792my$rp=$self->ra->rev_proplist($rev);3793foreach(sort keys%$rp) {3794my$v=$rp->{$_};3795if(/^svn:(author|date|log)$/) {3796$log_entry{$1} =$v;3797}elsif($_eq'svm:headrev') {3798$headrev=$v;3799}else{3800print$un" rev_prop: ", uri_encode($_),' ',3801 uri_encode($v),"\n";3802}3803}3804}else{3805map{$log_entry{$_} =$logged->{$_} }keys%$logged;3806}3807close$unor croak $!;38083809$log_entry{date} = parse_svn_date($log_entry{date});3810$log_entry{log} .="\n";3811my$author=$log_entry{author} = check_author($log_entry{author});3812my($name,$email) =defined$::users{$author} ? @{$::users{$author}}3813: ($author,undef);38143815my($commit_name,$commit_email) = ($name,$email);3816if($_use_log_author) {3817my$name_field;3818if($log_entry{log} =~/From:\s+(.*\S)\s*\n/i) {3819$name_field=$1;3820}elsif($log_entry{log} =~/Signed-off-by:\s+(.*\S)\s*\n/i) {3821$name_field=$1;3822}3823if(!defined$name_field) {3824if(!defined$email) {3825$email=$name;3826}3827}elsif($name_field=~/(.*?)\s+<(.*)>/) {3828($name,$email) = ($1,$2);3829}elsif($name_field=~/(.*)@/) {3830($name,$email) = ($1,$name_field);3831}else{3832($name,$email) = ($name_field,$name_field);3833}3834}3835if(defined$headrev&&$self->use_svm_props) {3836if($self->rewrite_root) {3837die"Can't have both 'useSvmProps' and 'rewriteRoot' ",3838"options set!\n";3839}3840if($self->rewrite_uuid) {3841die"Can't have both 'useSvmProps' and 'rewriteUUID' ",3842"options set!\n";3843}3844my($uuid,$r) =$headrev=~m{^([a-f\d\-]{30,}):(\d+)$}i;3845# we don't want "SVM: initializing mirror for junk" ...3846returnundefif$r==0;3847my$svm=$self->svm;3848if($uuidne$svm->{uuid}) {3849die"UUID mismatch on SVM path:\n",3850"expected:$svm->{uuid}\n",3851" got:$uuid\n";3852}3853my$full_url=$self->full_url;3854$full_url=~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or3855die"Failed to replace '$svm->{replace}' with ",3856"'$svm->{source}' in$full_url\n";3857# throw away username for storing in records3858 remove_username($full_url);3859$log_entry{metadata} ="$full_url\@$r$uuid";3860$log_entry{svm_revision} =$r;3861$email||="$author\@$uuid";3862$commit_email||="$author\@$uuid";3863}elsif($self->use_svnsync_props) {3864my$full_url=$self->svnsync->{url};3865$full_url.="/$self->{path}"iflength$self->{path};3866 remove_username($full_url);3867my$uuid=$self->svnsync->{uuid};3868$log_entry{metadata} ="$full_url\@$rev$uuid";3869$email||="$author\@$uuid";3870$commit_email||="$author\@$uuid";3871}else{3872my$url=$self->metadata_url;3873 remove_username($url);3874my$uuid=$self->rewrite_uuid||$self->ra->get_uuid;3875$log_entry{metadata} ="$url\@$rev".$uuid;3876$email||="$author\@".$uuid;3877$commit_email||="$author\@".$uuid;3878}3879$log_entry{name} =$name;3880$log_entry{email} =$email;3881$log_entry{commit_name} =$commit_name;3882$log_entry{commit_email} =$commit_email;3883 \%log_entry;3884}38853886sub fetch {3887my($self,$min_rev,$max_rev,@parents) =@_;3888my($last_rev,$last_commit) =$self->last_rev_commit;3889my($base,$head) =$self->get_fetch_range($min_rev,$max_rev);3890$self->ra->gs_fetch_loop_common($base,$head, [$self]);3891}38923893sub set_tree_cb {3894my($self,$log_entry,$tree,$rev,$date,$author) =@_;3895$self->{inject_parents} = {$rev=>$tree};3896$self->fetch(undef,undef);3897}38983899sub set_tree {3900my($self,$tree) = (shift,shift);3901my$log_entry= ::get_commit_entry($tree);3902unless($self->{last_rev}) {3903::fatal("Must have an existing revision to commit");3904}3905my%ed_opts= ( r =>$self->{last_rev},3906log=>$log_entry->{log},3907 ra =>$self->ra,3908 tree_a =>$self->{last_commit},3909 tree_b =>$tree,3910 editor_cb =>sub{3911$self->set_tree_cb($log_entry,$tree,@_) },3912 svn_path =>$self->{path} );3913if(!Git::SVN::Editor->new(\%ed_opts)->apply_diff) {3914print"No changes\nr$self->{last_rev} =$tree\n";3915}3916}39173918sub rebuild_from_rev_db {3919my($self,$path) =@_;3920my$r= -1;3921open my$fh,'<',$pathor croak "open:$!";3922binmode$fhor croak "binmode:$!";3923while(<$fh>) {3924length($_) ==41or croak "inconsistent size in ($_) != 41";3925chomp($_);3926++$r;3927next if$_eq('0' x 40);3928$self->rev_map_set($r,$_);3929print"r$r=$_\n";3930}3931close$fhor croak "close:$!";3932unlink$pathor croak "unlink:$!";3933}39343935sub rebuild {3936my($self) =@_;3937my$map_path=$self->map_path;3938my$partial= (-e $map_path&& ! -z $map_path);3939return unless::verify_ref($self->refname.'^0');3940if(!$partial&& ($self->use_svm_props||$self->no_metadata)) {3941my$rev_db=$self->rev_db_path;3942$self->rebuild_from_rev_db($rev_db);3943if($self->use_svm_props) {3944my$svm_rev_db=$self->rev_db_path($self->svm_uuid);3945$self->rebuild_from_rev_db($svm_rev_db);3946}3947$self->unlink_rev_db_symlink;3948return;3949}3950print"Rebuilding$map_path...\n"if(!$partial);3951my($base_rev,$head) = ($partial?$self->rev_map_max_norebuild(1) :3952(undef,undef));3953my($log,$ctx) =3954 command_output_pipe(qw/rev-list --pretty=raw --reverse/,3955($head?"$head..":"") .$self->refname,3956'--');3957my$metadata_url=$self->metadata_url;3958 remove_username($metadata_url);3959my$svn_uuid=$self->rewrite_uuid||$self->ra_uuid;3960my$c;3961while(<$log>) {3962if(m{^commit ($::sha1)$}) {3963$c=$1;3964next;3965}3966next unlesss{^\s*(git-svn-id:)}{$1};3967my($url,$rev,$uuid) = ::extract_metadata($_);3968 remove_username($url);39693970# ignore merges (from set-tree)3971next if(!defined$rev|| !$uuid);39723973# if we merged or otherwise started elsewhere, this is3974# how we break out of it3975if(($uuidne$svn_uuid) ||3976($metadata_url&&$url&& ($urlne$metadata_url))) {3977next;3978}3979if($partial&&$head) {3980print"Partial-rebuilding$map_path...\n";3981print"Currently at$base_rev=$head\n";3982$head=undef;3983}39843985$self->rev_map_set($rev,$c);3986print"r$rev=$c\n";3987}3988 command_close_pipe($log,$ctx);3989print"Done rebuilding$map_path\n"if(!$partial|| !$head);3990my$rev_db_path=$self->rev_db_path;3991if(-f $self->rev_db_path) {3992unlink$self->rev_db_pathor croak "unlink:$!";3993}3994$self->unlink_rev_db_symlink;3995}39963997# rev_map:3998# Tie::File seems to be prone to offset errors if revisions get sparse,3999# it's not that fast, either. Tie::File is also not in Perl 5.6. So4000# one of my favorite modules is out :< Next up would be one of the DBM4001# modules, but I'm not sure which is most portable...4002#4003# This is the replacement for the rev_db format, which was too big4004# and inefficient for large repositories with a lot of sparse history4005# (mainly tags)4006#4007# The format is this:4008# - 24 bytes for every record,4009# * 4 bytes for the integer representing an SVN revision number4010# * 20 bytes representing the sha1 of a git commit4011# - No empty padding records like the old format4012# (except the last record, which can be overwritten)4013# - new records are written append-only since SVN revision numbers4014# increase monotonically4015# - lookups on SVN revision number are done via a binary search4016# - Piping the file to xxd -c24 is a good way of dumping it for4017# viewing or editing (piped back through xxd -r), should the need4018# ever arise.4019# - The last record can be padding revision with an all-zero sha14020# This is used to optimize fetch performance when using multiple4021# "fetch" directives in .git/config4022#4023# These files are disposable unless noMetadata or useSvmProps is set40244025sub _rev_map_set {4026my($fh,$rev,$commit) =@_;40274028binmode$fhor croak "binmode:$!";4029my$size= (stat($fh))[7];4030($size%24) ==0or croak "inconsistent size:$size";40314032my$wr_offset=0;4033if($size>0) {4034sysseek($fh, -24, SEEK_END)or croak "seek:$!";4035my$read=sysread($fh,my$buf,24)or croak "read:$!";4036$read==24or croak "read only$readbytes (!= 24)";4037my($last_rev,$last_commit) =unpack(rev_map_fmt,$buf);4038if($last_commiteq('0' x40)) {4039if($size>=48) {4040sysseek($fh, -48, SEEK_END)or croak "seek:$!";4041$read=sysread($fh,$buf,24)or4042 croak "read:$!";4043$read==24or4044 croak "read only$readbytes (!= 24)";4045($last_rev,$last_commit) =4046unpack(rev_map_fmt,$buf);4047if($last_commiteq('0' x40)) {4048 croak "inconsistent .rev_map\n";4049}4050}4051if($last_rev>=$rev) {4052 croak "last_rev is higher!:$last_rev>=$rev";4053}4054$wr_offset= -24;4055}4056}4057sysseek($fh,$wr_offset, SEEK_END)or croak "seek:$!";4058syswrite($fh,pack(rev_map_fmt,$rev,$commit),24) ==24or4059 croak "write:$!";4060}40614062sub _rev_map_reset {4063my($fh,$rev,$commit) =@_;4064my$c= _rev_map_get($fh,$rev);4065$ceq$commitor die"_rev_map_reset(@_) commit$cdoes not match!\n";4066my$offset=sysseek($fh,0, SEEK_CUR)or croak "seek:$!";4067truncate$fh,$offsetor croak "truncate:$!";4068}40694070sub mkfile {4071my($path) =@_;4072unless(-e $path) {4073my($dir,$base) = ($path=~ m#^(.*?)/?([^/]+)$#);4074 mkpath([$dir])unless-d $dir;4075open my$fh,'>>',$pathor die"Couldn't create$path:$!\n";4076close$fhor die"Couldn't close (create)$path:$!\n";4077}4078}40794080sub rev_map_set {4081my($self,$rev,$commit,$update_ref,$uuid) =@_;4082defined$commitor die"missing arg3\n";4083length$commit==40or die"arg3 must be a full SHA1 hexsum\n";4084my$db=$self->map_path($uuid);4085my$db_lock="$db.lock";4086my$sigmask;4087$update_ref||=0;4088if($update_ref) {4089$sigmask= POSIX::SigSet->new();4090my$signew= POSIX::SigSet->new(SIGINT, SIGHUP, SIGTERM,4091 SIGALRM, SIGUSR1, SIGUSR2);4092 sigprocmask(SIG_BLOCK,$signew,$sigmask)or4093 croak "Can't block signals:$!";4094}4095 mkfile($db);40964097$LOCKFILES{$db_lock} =1;4098my$sync;4099# both of these options make our .rev_db file very, very important4100# and we can't afford to lose it because rebuild() won't work4101if($self->use_svm_props||$self->no_metadata) {4102$sync=1;4103 copy($db,$db_lock)or die"rev_map_set(@_): ",4104"Failed to copy: ",4105"$db=>$db_lock($!)\n";4106}else{4107rename$db,$db_lockor die"rev_map_set(@_): ",4108"Failed to rename: ",4109"$db=>$db_lock($!)\n";4110}41114112sysopen(my$fh,$db_lock, O_RDWR | O_CREAT)4113or croak "Couldn't open$db_lock:$!\n";4114$update_refeq'reset'? _rev_map_reset($fh,$rev,$commit) :4115 _rev_map_set($fh,$rev,$commit);4116if($sync) {4117$fh->flushor die"Couldn't flush$db_lock:$!\n";4118$fh->syncor die"Couldn't sync$db_lock:$!\n";4119}4120close$fhor croak $!;4121if($update_ref) {4122$_head=$self;4123my$note="";4124$note=" ($update_ref)"if($update_ref!~/^\d*$/);4125 command_noisy('update-ref','-m',"r$rev$note",4126$self->refname,$commit);4127}4128rename$db_lock,$dbor die"rev_map_set(@_): ","Failed to rename: ",4129"$db_lock=>$db($!)\n";4130delete$LOCKFILES{$db_lock};4131if($update_ref) {4132 sigprocmask(SIG_SETMASK,$sigmask)or4133 croak "Can't restore signal mask:$!";4134}4135}41364137# If want_commit, this will return an array of (rev, commit) where4138# commit _must_ be a valid commit in the archive.4139# Otherwise, it'll return the max revision (whether or not the4140# commit is valid or just a 0x40 placeholder).4141sub rev_map_max {4142my($self,$want_commit) =@_;4143$self->rebuild;4144my($r,$c) =$self->rev_map_max_norebuild($want_commit);4145$want_commit? ($r,$c) :$r;4146}41474148sub rev_map_max_norebuild {4149my($self,$want_commit) =@_;4150my$map_path=$self->map_path;4151stat$map_pathorreturn$want_commit? (0,undef) :0;4152sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4153binmode$fhor croak "binmode:$!";4154my$size= (stat($fh))[7];4155($size%24) ==0or croak "inconsistent size:$size";41564157if($size==0) {4158close$fhor croak "close:$!";4159return$want_commit? (0,undef) :0;4160}41614162sysseek($fh, -24, SEEK_END)or croak "seek:$!";4163sysread($fh,my$buf,24) ==24or croak "read:$!";4164my($r,$c) =unpack(rev_map_fmt,$buf);4165if($want_commit&&$ceq('0' x40)) {4166if($size<48) {4167return$want_commit? (0,undef) :0;4168}4169sysseek($fh, -48, SEEK_END)or croak "seek:$!";4170sysread($fh,$buf,24) ==24or croak "read:$!";4171($r,$c) =unpack(rev_map_fmt,$buf);4172if($ceq('0'x40)) {4173 croak "Penultimate record is all-zeroes in$map_path";4174}4175}4176close$fhor croak "close:$!";4177$want_commit? ($r,$c) :$r;4178}41794180sub rev_map_get {4181my($self,$rev,$uuid) =@_;4182my$map_path=$self->map_path($uuid);4183returnundefunless-e $map_path;41844185sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4186my$c= _rev_map_get($fh,$rev);4187close($fh)or croak "close:$!";4188$c4189}41904191sub _rev_map_get {4192my($fh,$rev) =@_;41934194binmode$fhor croak "binmode:$!";4195my$size= (stat($fh))[7];4196($size%24) ==0or croak "inconsistent size:$size";41974198if($size==0) {4199returnundef;4200}42014202my($l,$u) = (0,$size-24);4203my($r,$c,$buf);42044205while($l<=$u) {4206my$i=int(($l/24+$u/24) /2) *24;4207sysseek($fh,$i, SEEK_SET)or croak "seek:$!";4208sysread($fh,my$buf,24) ==24or croak "read:$!";4209my($r,$c) =unpack(rev_map_fmt,$buf);42104211if($r<$rev) {4212$l=$i+24;4213}elsif($r>$rev) {4214$u=$i-24;4215}else{# $r == $rev4216return$ceq('0' x 40) ?undef:$c;4217}4218}4219undef;4220}42214222# Finds the first svn revision that exists on (if $eq_ok is true) or4223# before $rev for the current branch. It will not search any lower4224# than $min_rev. Returns the git commit hash and svn revision number4225# if found, else (undef, undef).4226sub find_rev_before {4227my($self,$rev,$eq_ok,$min_rev) =@_;4228--$revunless$eq_ok;4229$min_rev||=1;4230my$max_rev=$self->rev_map_max;4231$rev=$max_revif($rev>$max_rev);4232while($rev>=$min_rev) {4233if(my$c=$self->rev_map_get($rev)) {4234return($rev,$c);4235}4236--$rev;4237}4238return(undef,undef);4239}42404241# Finds the first svn revision that exists on (if $eq_ok is true) or4242# after $rev for the current branch. It will not search any higher4243# than $max_rev. Returns the git commit hash and svn revision number4244# if found, else (undef, undef).4245sub find_rev_after {4246my($self,$rev,$eq_ok,$max_rev) =@_;4247++$revunless$eq_ok;4248$max_rev||=$self->rev_map_max;4249while($rev<=$max_rev) {4250if(my$c=$self->rev_map_get($rev)) {4251return($rev,$c);4252}4253++$rev;4254}4255return(undef,undef);4256}42574258sub _new {4259my($class,$repo_id,$ref_id,$path) =@_;4260unless(defined$repo_id&&length$repo_id) {4261$repo_id=$Git::SVN::default_repo_id;4262}4263unless(defined$ref_id&&length$ref_id) {4264$_prefix=''unlessdefined($_prefix);4265$_[2] =$ref_id=4266"refs/remotes/$_prefix$Git::SVN::default_ref_id";4267}4268$_[1] =$repo_id;4269my$dir="$ENV{GIT_DIR}/svn/$ref_id";42704271# Older repos imported by us used $GIT_DIR/svn/foo instead of4272# $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo4273if($ref_id=~m{^refs/remotes/(.*)}) {4274my$old_dir="$ENV{GIT_DIR}/svn/$1";4275if(-d $old_dir&& ! -d $dir) {4276$dir=$old_dir;4277}4278}42794280$_[3] =$path=''unless(defined$path);4281 mkpath([$dir]);4282bless{4283 ref_id =>$ref_id, dir =>$dir,index=>"$dir/index",4284 path =>$path, config =>"$ENV{GIT_DIR}/svn/config",4285 map_root =>"$dir/.rev_map", repo_id =>$repo_id},$class;4286}42874288# for read-only access of old .rev_db formats4289sub unlink_rev_db_symlink {4290my($self) =@_;4291my$link=$self->rev_db_path;4292$link=~s/\.[\w-]+$//or croak "missing UUID at the end of$link";4293if(-l $link) {4294unlink$linkor croak "unlink:$linkfailed!";4295}4296}42974298sub rev_db_path {4299my($self,$uuid) =@_;4300my$db_path=$self->map_path($uuid);4301$db_path=~s{/\.rev_map\.}{/\.rev_db\.}4302or croak "map_path:$db_pathdoes not contain '/.rev_map.' !";4303$db_path;4304}43054306# the new replacement for .rev_db4307sub map_path {4308my($self,$uuid) =@_;4309$uuid||=$self->ra_uuid;4310"$self->{map_root}.$uuid";4311}43124313sub uri_encode {4314my($f) =@_;4315$f=~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;4316$f4317}43184319sub uri_decode {4320my($f) =@_;4321$f=~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;4322$f4323}43244325sub remove_username {4326$_[0] =~s{^([^:]*://)[^@]+@}{$1};4327}43284329package Git::SVN::Ra;4330use vars qw/@ISA $config_dir $_ignore_refs_regex $_log_window_size/;4331use strict;4332use warnings;4333my($ra_invalid,$can_do_switch,%ignored_err,$RA);43344335BEGIN{4336# enforce temporary pool usage for some simple functions4337no strict 'refs';4338formy$f(qw/rev_proplist get_latest_revnum get_uuid get_repos_root4339 get_file/) {4340my$SUPER="SUPER::$f";4341*$f=sub{4342my$self=shift;4343my$pool= SVN::Pool->new;4344my@ret=$self->$SUPER(@_,$pool);4345$pool->clear;4346wantarray?@ret:$ret[0];4347};4348}4349}43504351sub _auth_providers () {4352my@rv= (4353 SVN::Client::get_simple_provider(),4354 SVN::Client::get_ssl_server_trust_file_provider(),4355 SVN::Client::get_simple_prompt_provider(4356 \&Git::SVN::Prompt::simple,2),4357 SVN::Client::get_ssl_client_cert_file_provider(),4358 SVN::Client::get_ssl_client_cert_prompt_provider(4359 \&Git::SVN::Prompt::ssl_client_cert,2),4360 SVN::Client::get_ssl_client_cert_pw_file_provider(),4361 SVN::Client::get_ssl_client_cert_pw_prompt_provider(4362 \&Git::SVN::Prompt::ssl_client_cert_pw,2),4363 SVN::Client::get_username_provider(),4364 SVN::Client::get_ssl_server_trust_prompt_provider(4365 \&Git::SVN::Prompt::ssl_server_trust),4366 SVN::Client::get_username_prompt_provider(4367 \&Git::SVN::Prompt::username,2)4368);43694370# earlier 1.6.x versions would segfault, and <= 1.5.x didn't have4371# this function4372if(::compare_svn_version('1.6.15') >=0) {4373my$config= SVN::Core::config_get_config($config_dir);4374my($p,@a);4375# config_get_config returns all config files from4376# ~/.subversion, auth_get_platform_specific_client_providers4377# just wants the config "file".4378@a= ($config->{'config'},undef);4379$p= SVN::Core::auth_get_platform_specific_client_providers(@a);4380# Insert the return value from4381# auth_get_platform_specific_providers4382unshift@rv,@$p;4383}4384 \@rv;4385}43864387sub escape_uri_only {4388my($uri) =@_;4389my@tmp;4390foreach(splitm{/},$uri) {4391s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;4392push@tmp,$_;4393}4394join('/',@tmp);4395}43964397sub escape_url {4398my($url) =@_;4399if($url=~ m#^(https?)://([^/]+)(.*)$#) {4400my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));4401$url="$scheme://$domain$uri";4402}4403$url;4404}44054406sub new {4407my($class,$url) =@_;4408$url=~s!/+$!!;4409return$RAif($RA&&$RA->{url}eq$url);44104411::_req_svn();44124413 SVN::_Core::svn_config_ensure($config_dir,undef);4414my($baton,$callbacks) = SVN::Core::auth_open_helper(_auth_providers);4415my$config= SVN::Core::config_get_config($config_dir);4416$RA=undef;4417my$dont_store_passwords=1;4418my$conf_t= ${$config}{'config'};4419{4420no warnings 'once';4421# The usage of $SVN::_Core::SVN_CONFIG_* variables4422# produces warnings that variables are used only once.4423# I had not found the better way to shut them up, so4424# the warnings of type 'once' are disabled in this block.4425if(SVN::_Core::svn_config_get_bool($conf_t,4426$SVN::_Core::SVN_CONFIG_SECTION_AUTH,4427$SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS,44281) ==0) {4429 SVN::_Core::svn_auth_set_parameter($baton,4430$SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS,4431bless(\$dont_store_passwords,"_p_void"));4432}4433if(SVN::_Core::svn_config_get_bool($conf_t,4434$SVN::_Core::SVN_CONFIG_SECTION_AUTH,4435$SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS,44361) ==0) {4437$Git::SVN::Prompt::_no_auth_cache =1;4438}4439}# no warnings 'once'4440my$self= SVN::Ra->new(url => escape_url($url), auth =>$baton,4441 config =>$config,4442 pool => SVN::Pool->new,4443 auth_provider_callbacks =>$callbacks);4444$self->{url} =$url;4445$self->{svn_path} =$url;4446$self->{repos_root} =$self->get_repos_root;4447$self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;4448$self->{cache} = { check_path => { r =>0, data => {} },4449 get_dir => { r =>0, data => {} } };4450$RA=bless$self,$class;4451}44524453sub check_path {4454my($self,$path,$r) =@_;4455my$cache=$self->{cache}->{check_path};4456if($r==$cache->{r} &&exists$cache->{data}->{$path}) {4457return$cache->{data}->{$path};4458}4459my$pool= SVN::Pool->new;4460my$t=$self->SUPER::check_path($path,$r,$pool);4461$pool->clear;4462if($r!=$cache->{r}) {4463%{$cache->{data}} = ();4464$cache->{r} =$r;4465}4466$cache->{data}->{$path} =$t;4467}44684469sub get_dir {4470my($self,$dir,$r) =@_;4471my$cache=$self->{cache}->{get_dir};4472if($r==$cache->{r}) {4473if(my$x=$cache->{data}->{$dir}) {4474returnwantarray?@$x:$x->[0];4475}4476}4477my$pool= SVN::Pool->new;4478my($d,undef,$props) =$self->SUPER::get_dir($dir,$r,$pool);4479my%dirents=map{$_=> { kind =>$d->{$_}->kind} }keys%$d;4480$pool->clear;4481if($r!=$cache->{r}) {4482%{$cache->{data}} = ();4483$cache->{r} =$r;4484}4485$cache->{data}->{$dir} = [ \%dirents,$r,$props];4486wantarray? (\%dirents,$r,$props) : \%dirents;4487}44884489sub DESTROY {4490# do not call the real DESTROY since we store ourselves in $RA4491}44924493# get_log(paths, start, end, limit,4494# discover_changed_paths, strict_node_history, receiver)4495sub get_log {4496my($self,@args) =@_;4497my$pool= SVN::Pool->new;44984499# svn_log_changed_path_t objects passed to get_log are likely to be4500# overwritten even if only the refs are copied to an external variable,4501# so we should dup the structures in their entirety. Using an4502# externally passed pool (instead of our temporary and quickly cleared4503# pool in Git::SVN::Ra) does not help matters at all...4504my$receiver=pop@args;4505my$prefix="/".$self->{svn_path};4506$prefix=~ s#/+($)##;4507my$prefix_regex= qr#^\Q$prefix\E#;4508push(@args,sub{4509my($paths) =$_[0];4510return&$receiver(@_)unless$paths;4511$_[0] = ();4512foreachmy$p(keys%$paths) {4513my$i=$paths->{$p};4514# Make path relative to our url, not repos_root4515$p=~s/$prefix_regex//;4516my%s=map{$_=>$i->$_; }4517 qw/copyfrom_path copyfrom_rev action/;4518if($s{'copyfrom_path'}) {4519$s{'copyfrom_path'} =~s/$prefix_regex//;4520}4521$_[0]{$p} = \%s;4522}4523&$receiver(@_);4524});452545264527# the limit parameter was not supported in SVN 1.1.x, so we4528# drop it. Therefore, the receiver callback passed to it4529# is made aware of this limitation by being wrapped if4530# the limit passed to is being wrapped.4531if(::compare_svn_version('1.2.0') <=0) {4532my$limit=splice(@args,3,1);4533if($limit>0) {4534my$receiver=pop@args;4535push(@args,sub{ &$receiver(@_)if(--$limit>=0) });4536}4537}4538my$ret=$self->SUPER::get_log(@args,$pool);4539$pool->clear;4540$ret;4541}45424543sub trees_match {4544my($self,$url1,$rev1,$url2,$rev2) =@_;4545my$ctx= SVN::Client->new(auth => _auth_providers);4546my$out= IO::File->new_tmpfile;45474548# older SVN (1.1.x) doesn't take $pool as the last parameter for4549# $ctx->diff(), so we'll create a default one4550my$pool= SVN::Pool->new_default_sub;45514552$ra_invalid=1;# this will open a new SVN::Ra connection to $url14553$ctx->diff([],$url1,$rev1,$url2,$rev2,1,1,0,$out,$out);4554$out->flush;4555my$ret= (($out->stat)[7] ==0);4556close$outor croak $!;45574558$ret;4559}45604561sub get_commit_editor {4562my($self,$log,$cb,$pool) =@_;45634564my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef,0) : ();4565$self->SUPER::get_commit_editor($log,$cb,@lock,$pool);4566}45674568sub gs_do_update {4569my($self,$rev_a,$rev_b,$gs,$editor) =@_;4570my$new= ($rev_a==$rev_b);4571my$path=$gs->{path};45724573if($new&& -e $gs->{index}) {4574unlink$gs->{index}or die4575"Couldn't unlink index:$gs->{index}:$!\n";4576}4577my$pool= SVN::Pool->new;4578$editor->set_path_strip($path);4579my(@pc) =split m#/#, $path;4580my$reporter=$self->do_update($rev_b, (@pc?shift@pc:''),45811,$editor,$pool);4582my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef) : ();45834584# Since we can't rely on svn_ra_reparent being available, we'll4585# just have to do some magic with set_path to make it so4586# we only want a partial path.4587my$sp='';4588my$final=join('/',@pc);4589while(@pc) {4590$reporter->set_path($sp,$rev_b,0,@lock,$pool);4591$sp.='/'iflength$sp;4592$sp.=shift@pc;4593}4594die"BUG: '$sp' != '$final'\n"if($spne$final);45954596$reporter->set_path($sp,$rev_a,$new,@lock,$pool);45974598$reporter->finish_report($pool);4599$pool->clear;4600$editor->{git_commit_ok};4601}46024603# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and4604# svn_ra_reparent didn't work before 1.4)4605sub gs_do_switch {4606my($self,$rev_a,$rev_b,$gs,$url_b,$editor) =@_;4607my$path=$gs->{path};4608my$pool= SVN::Pool->new;46094610my$full_url=$self->{url};4611my$old_url=$full_url;4612$full_url.='/'.$pathiflength$path;4613my($ra,$reparented);46144615if($old_url=~ m#^svn(\+ssh)?://# ||4616($full_url=~ m#^https?://# &&4617 escape_url($full_url)ne$full_url)) {4618$_[0] =undef;4619$self=undef;4620$RA=undef;4621$ra= Git::SVN::Ra->new($full_url);4622$ra_invalid=1;4623}elsif($old_urlne$full_url) {4624 SVN::_Ra::svn_ra_reparent($self->{session},$full_url,$pool);4625$self->{url} =$full_url;4626$reparented=1;4627}46284629$ra||=$self;4630$url_b= escape_url($url_b);4631my$reporter=$ra->do_switch($rev_b,'',1,$url_b,$editor,$pool);4632my@lock= (::compare_svn_version('1.2.0') >=0) ? (undef) : ();4633$reporter->set_path('',$rev_a,0,@lock,$pool);4634$reporter->finish_report($pool);46354636if($reparented) {4637 SVN::_Ra::svn_ra_reparent($self->{session},$old_url,$pool);4638$self->{url} =$old_url;4639}46404641$pool->clear;4642$editor->{git_commit_ok};4643}46444645sub longest_common_path {4646my($gsv,$globs) =@_;4647my%common;4648my$common_max=scalar@$gsv;46494650foreachmy$gs(@$gsv) {4651my@tmp=split m#/#, $gs->{path};4652my$p='';4653foreach(@tmp) {4654$p.=length($p) ?"/$_":$_;4655$common{$p} ||=0;4656$common{$p}++;4657}4658}4659$globs||= [];4660$common_max+=scalar@$globs;4661foreachmy$glob(@$globs) {4662my@tmp=split m#/#, $glob->{path}->{left};4663my$p='';4664foreach(@tmp) {4665$p.=length($p) ?"/$_":$_;4666$common{$p} ||=0;4667$common{$p}++;4668}4669}46704671my$longest_path='';4672foreach(sort{length$b<=>length$a}keys%common) {4673if($common{$_} ==$common_max) {4674$longest_path=$_;4675last;4676}4677}4678$longest_path;4679}46804681sub gs_fetch_loop_common {4682my($self,$base,$head,$gsv,$globs) =@_;4683return if($base>$head);4684my$inc=$_log_window_size;4685my($min,$max) = ($base,$head<$base+$inc?$head:$base+$inc);4686my$longest_path= longest_common_path($gsv,$globs);4687my$ra_url=$self->{url};4688my$find_trailing_edge;4689while(1) {4690my%revs;4691my$err;4692my$err_handler=$SVN::Error::handler;4693$SVN::Error::handler =sub{4694($err) =@_;4695 skip_unknown_revs($err);4696};4697sub _cb {4698my($paths,$r,$author,$date,$log) =@_;4699[$paths,4700{ author =>$author, date =>$date,log=>$log} ];4701}4702$self->get_log([$longest_path],$min,$max,0,1,1,4703sub{$revs{$_[1]} = _cb(@_) });4704if($err) {4705print"Checked through r$max\r";4706}else{4707$find_trailing_edge=1;4708}4709if($errand$find_trailing_edge) {4710print STDERR "Path '$longest_path' ",4711"was probably deleted:\n",4712$err->expanded_message,4713"\nWill attempt to follow ",4714"revisions r$min.. r$max",4715"committed before the deletion\n";4716my$hi=$max;4717while(--$hi>=$min) {4718my$ok;4719$self->get_log([$longest_path],$min,$hi,47200,1,1,sub{4721$ok=$_[1];4722$revs{$_[1]} = _cb(@_) });4723if($ok) {4724print STDERR "r$min.. r$okOK\n";4725last;4726}4727}4728$find_trailing_edge=0;4729}4730$SVN::Error::handler =$err_handler;47314732my%exists=map{$_->{path} =>$_}@$gsv;4733foreachmy$r(sort{$a<=>$b}keys%revs) {4734my($paths,$logged) = @{$revs{$r}};47354736foreachmy$gs($self->match_globs(\%exists,$paths,4737$globs,$r)) {4738if($gs->rev_map_max>=$r) {4739next;4740}4741next unless$gs->match_paths($paths,$r);4742$gs->{logged_rev_props} =$logged;4743if(my$last_commit=$gs->last_commit) {4744$gs->assert_index_clean($last_commit);4745}4746my$log_entry=$gs->do_fetch($paths,$r);4747if($log_entry) {4748$gs->do_git_commit($log_entry);4749}4750$INDEX_FILES{$gs->{index}} =1;4751}4752foreachmy$g(@$globs) {4753my$k="svn-remote.$g->{remote}.".4754"$g->{t}-maxRev";4755 Git::SVN::tmp_config($k,$r);4756}4757if($ra_invalid) {4758$_[0] =undef;4759$self=undef;4760$RA=undef;4761$self= Git::SVN::Ra->new($ra_url);4762$ra_invalid=undef;4763}4764}4765# pre-fill the .rev_db since it'll eventually get filled in4766# with '0' x40 if something new gets committed4767foreachmy$gs(@$gsv) {4768next if$gs->rev_map_max>=$max;4769next ifdefined$gs->rev_map_get($max);4770$gs->rev_map_set($max,0 x40);4771}4772foreachmy$g(@$globs) {4773my$k="svn-remote.$g->{remote}.$g->{t}-maxRev";4774 Git::SVN::tmp_config($k,$max);4775}4776last if$max>=$head;4777$min=$max+1;4778$max+=$inc;4779$max=$headif($max>$head);4780}4781 Git::SVN::gc();4782}47834784sub get_dir_globbed {4785my($self,$left,$depth,$r) =@_;47864787my@x=eval{$self->get_dir($left,$r) };4788return unlessscalar@x==3;4789my$dirents=$x[0];4790my@finalents;4791foreachmy$de(keys%$dirents) {4792next if$dirents->{$de}->{kind} !=$SVN::Node::dir;4793if($depth>1) {4794my@args= ("$left/$de",$depth-1,$r);4795foreachmy$dir($self->get_dir_globbed(@args)) {4796push@finalents,"$de/$dir";4797}4798}else{4799push@finalents,$de;4800}4801}4802@finalents;4803}48044805# return value: 0 -- don't ignore, 1 -- ignore4806sub is_ref_ignored {4807my($g,$p) =@_;4808my$refname=$g->{ref}->full_path($p);4809return1ifdefined($g->{ignore_refs_regex}) &&4810$refname=~m!$g->{ignore_refs_regex}!;4811return0unlessdefined($_ignore_refs_regex);4812return1if$refname=~m!$_ignore_refs_regex!o;4813return0;4814}48154816sub match_globs {4817my($self,$exists,$paths,$globs,$r) =@_;48184819sub get_dir_check {4820my($self,$exists,$g,$r) =@_;48214822my@dirs=$self->get_dir_globbed($g->{path}->{left},4823$g->{path}->{depth},4824$r);48254826foreachmy$de(@dirs) {4827my$p=$g->{path}->full_path($de);4828next if$exists->{$p};4829next if(length$g->{path}->{right} &&4830($self->check_path($p,$r) !=4831$SVN::Node::dir));4832next unless$p=~/$g->{path}->{regex}/;4833$exists->{$p} = Git::SVN->init($self->{url},$p,undef,4834$g->{ref}->full_path($de),1);4835}4836}4837foreachmy$g(@$globs) {4838if(my$path=$paths->{"/$g->{path}->{left}"}) {4839if($path->{action} =~/^[AR]$/) {4840 get_dir_check($self,$exists,$g,$r);4841}4842}4843foreach(keys%$paths) {4844if(/$g->{path}->{left_regex}/&&4845!/$g->{path}->{regex}/) {4846next if$paths->{$_}->{action} !~/^[AR]$/;4847 get_dir_check($self,$exists,$g,$r);4848}4849next unless/$g->{path}->{regex}/;4850my$p=$1;4851my$pathname=$g->{path}->full_path($p);4852next if is_ref_ignored($g,$p);4853next if$exists->{$pathname};4854next if($self->check_path($pathname,$r) !=4855$SVN::Node::dir);4856$exists->{$pathname} = Git::SVN->init(4857$self->{url},$pathname,undef,4858$g->{ref}->full_path($p),1);4859}4860my$c='';4861foreach(split m#/#, $g->{path}->{left}) {4862$c.="/$_";4863next unless($paths->{$c} &&4864($paths->{$c}->{action} =~/^[AR]$/));4865 get_dir_check($self,$exists,$g,$r);4866}4867}4868values%$exists;4869}48704871sub minimize_url {4872my($self) =@_;4873return$self->{url}if($self->{url}eq$self->{repos_root});4874my$url=$self->{repos_root};4875my@components=split(m!/!,$self->{svn_path});4876my$c='';4877do{4878$url.="/$c"iflength$c;4879eval{4880my$ra= (ref$self)->new($url);4881my$latest=$ra->get_latest_revnum;4882$ra->get_log("",$latest,0,1,0,1,sub{});4883};4884}while($@&& ($c=shift@components));4885$url;4886}48874888sub can_do_switch {4889my$self=shift;4890unless(defined$can_do_switch) {4891my$pool= SVN::Pool->new;4892my$rep=eval{4893$self->do_switch(1,'',0,$self->{url},4894 SVN::Delta::Editor->new,$pool);4895};4896if($@) {4897$can_do_switch=0;4898}else{4899$rep->abort_report($pool);4900$can_do_switch=1;4901}4902$pool->clear;4903}4904$can_do_switch;4905}49064907sub skip_unknown_revs {4908my($err) =@_;4909my$errno=$err->apr_err();4910# Maybe the branch we're tracking didn't4911# exist when the repo started, so it's4912# not an error if it doesn't, just continue4913#4914# Wonderfully consistent library, eh?4915# 160013 - svn:// and file://4916# 175002 - http(s)://4917# 175007 - http(s):// (this repo required authorization, too...)4918# More codes may be discovered later...4919if($errno==175007||$errno==175002||$errno==160013) {4920my$err_key=$err->expanded_message;4921# revision numbers change every time, filter them out4922$err_key=~s/\d+/\0/g;4923$err_key="$errno\0$err_key";4924unless($ignored_err{$err_key}) {4925warn"W: Ignoring error from SVN, path probably ",4926"does not exist: ($errno): ",4927$err->expanded_message,"\n";4928warn"W: Do not be alarmed at the above message ",4929"git-svn is just searching aggressively for ",4930"old history.\n",4931"This may take a while on large repositories\n";4932$ignored_err{$err_key} =1;4933}4934return;4935}4936die"Error from SVN, ($errno): ",$err->expanded_message,"\n";4937}49384939package Git::SVN::Log;4940use strict;4941use warnings;4942use POSIX qw/strftime/;4943useconstant commit_log_separator => ('-' x 72) ."\n";4944use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline4945%rusers $show_commit $incremental/;4946my$l_fmt;49474948sub cmt_showable {4949my($c) =@_;4950return1ifdefined$c->{r};49514952# big commit message got truncated by the 16k pretty buffer in rev-list4953if($c->{l} &&$c->{l}->[-1]eq"...\n"&&4954$c->{a_raw} =~/\@([a-f\d\-]+)>$/) {4955@{$c->{l}} = ();4956my@log= command(qw/cat-file commit/,$c->{c});49574958# shift off the headers4959shift@logwhile($log[0]ne'');4960shift@log;49614962# TODO: make $c->{l} not have a trailing newline in the future4963@{$c->{l}} =map{"$_\n"}grep!/^git-svn-id: /,@log;49644965(undef,$c->{r},undef) = ::extract_metadata(4966(grep(/^git-svn-id: /,@log))[-1]);4967}4968returndefined$c->{r};4969}49704971sub log_use_color {4972return$color|| Git->repository->get_colorbool('color.diff');4973}49744975sub git_svn_log_cmd {4976my($r_min,$r_max,@args) =@_;4977my$head='HEAD';4978my(@files,@log_opts);4979foreachmy$x(@args) {4980if($xeq'--'||@files) {4981push@files,$x;4982}else{4983if(::verify_ref("$x^0")) {4984$head=$x;4985}else{4986push@log_opts,$x;4987}4988}4989}49904991my($url,$rev,$uuid,$gs) = ::working_head_info($head);4992$gs||= Git::SVN->_new;4993my@cmd= (qw/log --abbrev-commit --pretty=raw --default/,4994$gs->refname);4995push@cmd,'-r'unless$non_recursive;4996push@cmd, qw/--raw --name-status/if$verbose;4997push@cmd,'--color'if log_use_color();4998push@cmd,@log_opts;4999if(defined$r_max&&$r_max==$r_min) {5000push@cmd,'--max-count=1';5001if(my$c=$gs->rev_map_get($r_max)) {5002push@cmd,$c;5003}5004}elsif(defined$r_max) {5005if($r_max<$r_min) {5006($r_min,$r_max) = ($r_max,$r_min);5007}5008my(undef,$c_max) =$gs->find_rev_before($r_max,1,$r_min);5009my(undef,$c_min) =$gs->find_rev_after($r_min,1,$r_max);5010# If there are no commits in the range, both $c_max and $c_min5011# will be undefined. If there is at least 1 commit in the5012# range, both will be defined.5013return()if!defined$c_min|| !defined$c_max;5014if($c_mineq$c_max) {5015push@cmd,'--max-count=1',$c_min;5016}else{5017push@cmd,'--boundary',"$c_min..$c_max";5018}5019}5020return(@cmd,@files);5021}50225023# adapted from pager.c5024sub config_pager {5025if(! -t *STDOUT) {5026$ENV{GIT_PAGER_IN_USE} ='false';5027$pager=undef;5028return;5029}5030chomp($pager= command_oneline(qw(var GIT_PAGER)));5031if($pagereq'cat') {5032$pager=undef;5033}5034$ENV{GIT_PAGER_IN_USE} =defined($pager);5035}50365037sub run_pager {5038return unlessdefined$pager;5039pipe my($rfd,$wfd)orreturn;5040defined(my$pid=fork)or::fatal "Can't fork:$!";5041if(!$pid) {5042open STDOUT,'>&',$wfdor5043::fatal "Can't redirect to stdout:$!";5044return;5045}5046open STDIN,'<&',$rfdor::fatal "Can't redirect stdin:$!";5047$ENV{LESS} ||='FRSX';5048exec$pageror::fatal "Can't run pager:$!($pager)";5049}50505051sub format_svn_date {5052my$t=shift||time;5053my$gmoff= Git::SVN::get_tz($t);5054return strftime("%Y-%m-%d%H:%M:%S$gmoff(%a,%d%b%Y)",localtime($t));5055}50565057sub parse_git_date {5058my($t,$tz) =@_;5059# Date::Parse isn't in the standard Perl distro :(5060if($tz=~s/^\+//) {5061$t+= tz_to_s_offset($tz);5062}elsif($tz=~s/^\-//) {5063$t-= tz_to_s_offset($tz);5064}5065return$t;5066}50675068sub set_local_timezone {5069if(defined$TZ) {5070$ENV{TZ} =$TZ;5071}else{5072delete$ENV{TZ};5073}5074}50755076sub tz_to_s_offset {5077my($tz) =@_;5078$tz=~s/(\d\d)$//;5079return($1*60) + ($tz*3600);5080}50815082sub get_author_info {5083my($dest,$author,$t,$tz) =@_;5084$author=~s/(?:^\s*|\s*$)//g;5085$dest->{a_raw} =$author;5086my$au;5087if($::_authors) {5088$au=$rusers{$author} ||undef;5089}5090if(!$au) {5091($au) = ($author=~/<([^>]+)\@[^>]+>$/);5092}5093$dest->{t} =$t;5094$dest->{tz} =$tz;5095$dest->{a} =$au;5096$dest->{t_utc} = parse_git_date($t,$tz);5097}50985099sub process_commit {5100my($c,$r_min,$r_max,$defer) =@_;5101if(defined$r_min&&defined$r_max) {5102if($r_min==$c->{r} &&$r_min==$r_max) {5103 show_commit($c);5104return0;5105}5106return1if$r_min==$r_max;5107if($r_min<$r_max) {5108# we need to reverse the print order5109return0if(defined$limit&& --$limit<0);5110push@$defer,$c;5111return1;5112}5113if($r_min!=$r_max) {5114return1if($r_min<$c->{r});5115return1if($r_max>$c->{r});5116}5117}5118return0if(defined$limit&& --$limit<0);5119 show_commit($c);5120return1;5121}51225123sub show_commit {5124my$c=shift;5125if($oneline) {5126my$x="\n";5127if(my$l=$c->{l}) {5128while($l->[0] =~/^\s*$/) {shift@$l}5129$x=$l->[0];5130}5131$l_fmt||='A'.length($c->{r});5132print'r',pack($l_fmt,$c->{r}),' | ';5133print"$c->{c} | "if$show_commit;5134print$x;5135}else{5136 show_commit_normal($c);5137}5138}51395140sub show_commit_changed_paths {5141my($c) =@_;5142return unless$c->{changed};5143print"Changed paths:\n", @{$c->{changed}};5144}51455146sub show_commit_normal {5147my($c) =@_;5148print commit_log_separator,"r$c->{r} | ";5149print"$c->{c} | "if$show_commit;5150print"$c->{a} | ", format_svn_date($c->{t_utc}),' | ';5151my$nr_line=0;51525153if(my$l=$c->{l}) {5154while($l->[$#$l]eq"\n"&&$#$l>05155&&$l->[($#$l-1)]eq"\n") {5156pop@$l;5157}5158$nr_line=scalar@$l;5159if(!$nr_line) {5160print"1 line\n\n\n";5161}else{5162if($nr_line==1) {5163$nr_line='1 line';5164}else{5165$nr_line.=' lines';5166}5167print$nr_line,"\n";5168 show_commit_changed_paths($c);5169print"\n";5170print$_foreach@$l;5171}5172}else{5173print"1 line\n";5174 show_commit_changed_paths($c);5175print"\n";51765177}5178foreachmy$x(qw/raw stat diff/) {5179if($c->{$x}) {5180print"\n";5181print$_foreach@{$c->{$x}}5182}5183}5184}51855186sub cmd_show_log {5187my(@args) =@_;5188my($r_min,$r_max);5189my$r_last= -1;# prevent dupes5190 set_local_timezone();5191if(defined$::_revision) {5192if($::_revision =~/^(\d+):(\d+)$/) {5193($r_min,$r_max) = ($1,$2);5194}elsif($::_revision =~/^\d+$/) {5195$r_min=$r_max= $::_revision;5196}else{5197::fatal "-r$::_revision is not supported, use ",5198"standard 'git log' arguments instead";5199}5200}52015202 config_pager();5203@args= git_svn_log_cmd($r_min,$r_max,@args);5204if(!@args) {5205print commit_log_separator unless$incremental||$oneline;5206return;5207}5208my$log= command_output_pipe(@args);5209 run_pager();5210my(@k,$c,$d,$stat);5211my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;5212while(<$log>) {5213if(/^${esc_color}commit (?:- )?($::sha1_short)/o) {5214my$cmt=$1;5215if($c&& cmt_showable($c) &&$c->{r} !=$r_last) {5216$r_last=$c->{r};5217 process_commit($c,$r_min,$r_max, \@k)or5218goto out;5219}5220$d=undef;5221$c= { c =>$cmt};5222}elsif(/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {5223 get_author_info($c,$1,$2,$3);5224}elsif(/^${esc_color}(?:tree|parent|committer) /o) {5225# ignore5226}elsif(/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {5227push@{$c->{raw}},$_;5228}elsif(/^${esc_color}[ACRMDT]\t/) {5229# we could add $SVN->{svn_path} here, but that requires5230# remote access at the moment (repo_path_split)...5231 s#^(${esc_color})([ACRMDT])\t#$1 $2 #o;5232push@{$c->{changed}},$_;5233}elsif(/^${esc_color}diff /o) {5234$d=1;5235push@{$c->{diff}},$_;5236}elsif($d) {5237push@{$c->{diff}},$_;5238}elsif(/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*5239$esc_color*[\+\-]*$esc_color$/x) {5240$stat=1;5241push@{$c->{stat}},$_;5242}elsif($stat&&/^ \d+ files changed, \d+ insertions/) {5243push@{$c->{stat}},$_;5244$stat=undef;5245}elsif(/^${esc_color} (git-svn-id:.+)$/o) {5246($c->{url},$c->{r},undef) = ::extract_metadata($1);5247}elsif(s/^${esc_color} //o) {5248push@{$c->{l}},$_;5249}5250}5251if($c&&defined$c->{r} &&$c->{r} !=$r_last) {5252$r_last=$c->{r};5253 process_commit($c,$r_min,$r_max, \@k);5254}5255if(@k) {5256($r_min,$r_max) = ($r_max,$r_min);5257 process_commit($_,$r_min,$r_max)foreachreverse@k;5258}5259out:5260close$log;5261print commit_log_separator unless$incremental||$oneline;5262}52635264sub cmd_blame {5265my$path=pop;52665267 config_pager();5268 run_pager();52695270my($fh,$ctx,$rev);52715272if($_git_format) {5273($fh,$ctx) = command_output_pipe('blame',@_,$path);5274while(my$line= <$fh>) {5275if($line=~/^\^?([[:xdigit:]]+)\s/) {5276# Uncommitted edits show up as a rev ID of5277# all zeros, which we can't look up with5278# cmt_metadata5279if($1!~/^0+$/) {5280(undef,$rev,undef) =5281::cmt_metadata($1);5282$rev='0'if(!$rev);5283}else{5284$rev='0';5285}5286$rev=sprintf('%-10s',$rev);5287$line=~s/^\^?[[:xdigit:]]+(\s)/$rev$1/;5288}5289print$line;5290}5291}else{5292($fh,$ctx) = command_output_pipe('blame','-p',@_,'HEAD',5293'--',$path);5294my($sha1);5295my%authors;5296my@buffer;5297my%dsha;#distinct sha keys52985299while(my$line= <$fh>) {5300push@buffer,$line;5301if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {5302$dsha{$1} =1;5303}5304}53055306my$s2r= ::cmt_sha2rev_batch([keys%dsha]);53075308foreachmy$line(@buffer) {5309if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {5310$rev=$s2r->{$1};5311$rev='0'if(!$rev)5312}5313elsif($line=~/^author (.*)/) {5314$authors{$rev} =$1;5315$authors{$rev} =~s/\s/_/g;5316}5317elsif($line=~/^\t(.*)$/) {5318printf("%6s%10s%s\n",$rev,$authors{$rev},$1);5319}5320}5321}5322 command_close_pipe($fh,$ctx);5323}53245325package Git::SVN::Migration;5326# these version numbers do NOT correspond to actual version numbers5327# of git nor git-svn. They are just relative.5328#5329# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD5330#5331# v1 layout: .git/$id/info/url, refs/remotes/$id5332#5333# v2 layout: .git/svn/$id/info/url, refs/remotes/$id5334#5335# v3 layout: .git/svn/$id, refs/remotes/$id5336# - info/url may remain for backwards compatibility5337# - this is what we migrate up to this layout automatically,5338# - this will be used by git svn init on single branches5339# v3.1 layout (auto migrated):5340# - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink5341# for backwards compatibility5342#5343# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id5344# - this is only created for newly multi-init-ed5345# repositories. Similar in spirit to the5346# --use-separate-remotes option in git-clone (now default)5347# - we do not automatically migrate to this (following5348# the example set by core git)5349#5350# v5 layout: .rev_db.$UUID => .rev_map.$UUID5351# - newer, more-efficient format that uses 24-bytes per record5352# with no filler space.5353# - use xxd -c24 < .rev_map.$UUID to view and debug5354# - This is a one-way migration, repositories updated to the5355# new format will not be able to use old git-svn without5356# rebuilding the .rev_db. Rebuilding the rev_db is not5357# possible if noMetadata or useSvmProps are set; but should5358# be no problem for users that use the (sensible) defaults.5359use strict;5360use warnings;5361use Carp qw/croak/;5362use File::Path qw/mkpath/;5363use File::Basename qw/dirname basename/;5364use vars qw/$_minimize/;53655366sub migrate_from_v0 {5367my$git_dir=$ENV{GIT_DIR};5368returnundefunless-d $git_dir;5369my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);5370my$migrated=0;5371while(<$fh>) {5372chomp;5373my($id,$orig_ref) = ($_,$_);5374next unless$id=~ s#^refs/heads/(.+)-HEAD$#$1#;5375next unless-f "$git_dir/$id/info/url";5376my$new_ref="refs/remotes/$id";5377if(::verify_ref("$new_ref^0")) {5378print STDERR "W:$orig_refis probably an old ",5379"branch used by an ancient version of ",5380"git-svn.\n",5381"However,$new_refalso exists.\n",5382"We will not be able ",5383"to use this branch until this ",5384"ambiguity is resolved.\n";5385next;5386}5387print STDERR "Migrating from v0 layout...\n"if!$migrated;5388print STDERR "Renaming ref:$orig_ref=>$new_ref\n";5389 command_noisy('update-ref',$new_ref,$orig_ref);5390 command_noisy('update-ref','-d',$orig_ref,$orig_ref);5391$migrated++;5392}5393 command_close_pipe($fh,$ctx);5394print STDERR "Done migrating from v0 layout...\n"if$migrated;5395$migrated;5396}53975398sub migrate_from_v1 {5399my$git_dir=$ENV{GIT_DIR};5400my$migrated=0;5401return$migratedunless-d $git_dir;5402my$svn_dir="$git_dir/svn";54035404# just in case somebody used 'svn' as their $id at some point...5405return$migratedif-d $svn_dir&& ! -f "$svn_dir/info/url";54065407print STDERR "Migrating from a git-svn v1 layout...\n";5408 mkpath([$svn_dir]);5409print STDERR "Data from a previous version of git-svn exists, but\n\t",5410"$svn_dir\n\t(required for this version ",5411"($::VERSION) of git-svn) does not exist.\n";5412my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);5413while(<$fh>) {5414my$x=$_;5415next unless$x=~ s#^refs/remotes/##;5416chomp$x;5417next unless-f "$git_dir/$x/info/url";5418my$u=eval{ ::file_to_s("$git_dir/$x/info/url") };5419next unless$u;5420my$dn= dirname("$git_dir/svn/$x");5421 mkpath([$dn])unless-d $dn;5422if($xeq'svn') {# they used 'svn' as GIT_SVN_ID:5423 mkpath(["$git_dir/svn/svn"]);5424print STDERR " -$git_dir/$x/info=> ",5425"$git_dir/svn/$x/info\n";5426rename"$git_dir/$x/info","$git_dir/svn/$x/info"or5427 croak "$!:$x";5428# don't worry too much about these, they probably5429# don't exist with repos this old (save for index,5430# and we can easily regenerate that)5431foreachmy$f(qw/unhandled.log index .rev_db/) {5432rename"$git_dir/$x/$f","$git_dir/svn/$x/$f";5433}5434}else{5435print STDERR " -$git_dir/$x=>$git_dir/svn/$x\n";5436rename"$git_dir/$x","$git_dir/svn/$x"or5437 croak "$!:$x";5438}5439$migrated++;5440}5441 command_close_pipe($fh,$ctx);5442print STDERR "Done migrating from a git-svn v1 layout\n";5443$migrated;5444}54455446sub read_old_urls {5447my($l_map,$pfx,$path) =@_;5448my@dir;5449foreach(<$path/*>) {5450if(-r "$_/info/url") {5451$pfx.='/'if$pfx&&$pfx!~ m!/$!;5452my$ref_id=$pfx. basename $_;5453my$url= ::file_to_s("$_/info/url");5454$l_map->{$ref_id} =$url;5455}elsif(-d $_) {5456push@dir,$_;5457}5458}5459foreach(@dir) {5460my$x=$_;5461$x=~s!^\Q$ENV{GIT_DIR}\E/svn/!!o;5462 read_old_urls($l_map,$x,$_);5463}5464}54655466sub migrate_from_v2 {5467my@cfg= command(qw/config -l/);5468return ifgrep/^svn-remote\..+\.url=/,@cfg;5469my%l_map;5470 read_old_urls(\%l_map,'',"$ENV{GIT_DIR}/svn");5471my$migrated=0;54725473foreachmy$ref_id(sort keys%l_map) {5474eval{ Git::SVN->init($l_map{$ref_id},'',undef,$ref_id) };5475if($@) {5476 Git::SVN->init($l_map{$ref_id},'',$ref_id,$ref_id);5477}5478$migrated++;5479}5480$migrated;5481}54825483sub minimize_connections {5484my$r= Git::SVN::read_all_remotes();5485my$new_urls= {};5486my$root_repos= {};5487foreachmy$repo_id(keys%$r) {5488my$url=$r->{$repo_id}->{url}ornext;5489my$fetch=$r->{$repo_id}->{fetch}ornext;5490my$ra= Git::SVN::Ra->new($url);54915492# skip existing cases where we already connect to the root5493if(($ra->{url}eq$ra->{repos_root}) ||5494($ra->{repos_root}eq$repo_id)) {5495$root_repos->{$ra->{url}} =$repo_id;5496next;5497}54985499my$root_ra= Git::SVN::Ra->new($ra->{repos_root});5500my$root_path=$ra->{url};5501$root_path=~ s#^\Q$ra->{repos_root}\E(/|$)##;5502foreachmy$path(keys%$fetch) {5503my$ref_id=$fetch->{$path};5504my$gs= Git::SVN->new($ref_id,$repo_id,$path);55055506# make sure we can read when connecting to5507# a higher level of a repository5508my($last_rev,undef) =$gs->last_rev_commit;5509if(!defined$last_rev) {5510$last_rev=eval{5511$root_ra->get_latest_revnum;5512};5513next if$@;5514}5515my$new=$root_path;5516$new.=length$path?"/$path":'';5517eval{5518$root_ra->get_log([$new],$last_rev,$last_rev,55190,0,1,sub{ });5520};5521next if$@;5522$new_urls->{$ra->{repos_root}}->{$new} =5523{ ref_id =>$ref_id,5524 old_repo_id =>$repo_id,5525 old_path =>$path};5526}5527}55285529my@emptied;5530foreachmy$url(keys%$new_urls) {5531# see if we can re-use an existing [svn-remote "repo_id"]5532# instead of creating a(n ugly) new section:5533my$repo_id=$root_repos->{$url} ||$url;55345535my$fetch=$new_urls->{$url};5536foreachmy$path(keys%$fetch) {5537my$x=$fetch->{$path};5538 Git::SVN->init($url,$path,$repo_id,$x->{ref_id});5539my$pfx="svn-remote.$x->{old_repo_id}";55405541my$old_fetch=quotemeta("$x->{old_path}:".5542"$x->{ref_id}");5543 command_noisy(qw/config --unset/,5544"$pfx.fetch",'^'.$old_fetch.'$');5545delete$r->{$x->{old_repo_id}}->5546{fetch}->{$x->{old_path}};5547if(!keys%{$r->{$x->{old_repo_id}}->{fetch}}) {5548 command_noisy(qw/config --unset/,5549"$pfx.url");5550push@emptied,$x->{old_repo_id}5551}5552}5553}5554if(@emptied) {5555my$file=$ENV{GIT_CONFIG} ||"$ENV{GIT_DIR}/config";5556print STDERR <<EOF;5557The following [svn-remote] sections in your config file ($file) are empty5558and can be safely removed:5559EOF5560print STDERR "[svn-remote\"$_\"]\n"foreach@emptied;5561}5562}55635564sub migration_check {5565 migrate_from_v0();5566 migrate_from_v1();5567 migrate_from_v2();5568 minimize_connections()if$_minimize;5569}55705571package Git::IndexInfo;5572use strict;5573use warnings;5574use Git qw/command_input_pipe command_close_pipe/;55755576sub new {5577my($class) =@_;5578my($gui,$ctx) = command_input_pipe(qw/update-index -z --index-info/);5579bless{ gui =>$gui, ctx =>$ctx, nr =>0},$class;5580}55815582sub remove {5583my($self,$path) =@_;5584if(print{$self->{gui} }'0 ',0 x 40,"\t",$path,"\0") {5585return++$self->{nr};5586}5587undef;5588}55895590sub update {5591my($self,$mode,$hash,$path) =@_;5592if(print{$self->{gui} }$mode,' ',$hash,"\t",$path,"\0") {5593return++$self->{nr};5594}5595undef;5596}55975598sub DESTROY {5599my($self) =@_;5600 command_close_pipe($self->{gui},$self->{ctx});5601}56025603package Git::SVN::GlobSpec;5604use strict;5605use warnings;56065607sub new {5608my($class,$glob,$pattern_ok) =@_;5609my$re=$glob;5610$re=~s!/+$!!g;# no need for trailing slashes5611my(@left,@right,@patterns);5612my$state="left";5613my$die_msg="Only one set of wildcard directories ".5614"(e.g. '*' or '*/*/*') is supported: '$glob'\n";5615formy$part(split(m|/|,$glob)) {5616if($part=~/\*/&&$partne"*") {5617die"Invalid pattern in '$glob':$part\n";5618}elsif($pattern_ok&&$part=~/[{}]/&&5619$part!~/^\{[^{}]+\}/) {5620die"Invalid pattern in '$glob':$part\n";5621}5622if($parteq"*") {5623die$die_msgif$stateeq"right";5624$state="pattern";5625push(@patterns,"[^/]*");5626}elsif($pattern_ok&&$part=~/^\{(.*)\}$/) {5627die$die_msgif$stateeq"right";5628$state="pattern";5629my$p=quotemeta($1);5630$p=~s/\\,/|/g;5631push(@patterns,"(?:$p)");5632}else{5633if($stateeq"left") {5634push(@left,$part);5635}else{5636push(@right,$part);5637$state="right";5638}5639}5640}5641my$depth=@patterns;5642if($depth==0) {5643die"One '*' is needed in glob: '$glob'\n";5644}5645my$left=join('/',@left);5646my$right=join('/',@right);5647$re=join('/',@patterns);5648$re=join('\/',5649grep(length,quotemeta($left),"($re)",quotemeta($right)));5650my$left_re=qr/^\/\Q$left\E(\/|$)/;5651bless{ left =>$left, right =>$right, left_regex =>$left_re,5652 regex =>qr/$re/,glob=>$glob, depth =>$depth},$class;5653}56545655sub full_path {5656my($self,$path) =@_;5657return(length$self->{left} ?"$self->{left}/":'') .5658$path. (length$self->{right} ?"/$self->{right}":'');5659}56605661__END__56625663Data structures:566456655666$remotes= {# returned by read_all_remotes()5667'svn'=> {5668# svn-remote.svn.url=https://svn.musicpd.org5669 url =>'https://svn.musicpd.org',5670# svn-remote.svn.fetch=mpd/trunk:trunk5671 fetch => {5672'mpd/trunk'=>'trunk',5673},5674# svn-remote.svn.tags=mpd/tags/*:tags/*5675 tags => {5676 path => {5677 left =>'mpd/tags',5678 right =>'',5679 regex =>qr!mpd/tags/([^/]+)$!,5680glob=>'tags/*',5681},5682ref=> {5683 left =>'tags',5684 right =>'',5685 regex =>qr!tags/([^/]+)$!,5686glob=>'tags/*',5687},5688}5689}5690};56915692$log_entry hashref as returned by libsvn_log_entry()5693{5694log=>'whitespace-formatted log entry5695',# trailing newline is preserved5696 revision =>'8',# integer5697 date =>'2004-02-24T17:01:44.108345Z',# commit date5698 author =>'committer name'5699};570057015702# this is generated by generate_diff();5703@mods= array of diff-index line hashes,each element represents one line5704 of diff-index output57055706diff-index line ($m hash)5707{5708 mode_a => first column of diff-index output,no leading ':',5709 mode_b => second column of diff-index output,5710 sha1_b => sha1sum of the final blob,5711 chg => change type [MCRADT],5712 file_a => original file name of a file (iff chg is'C'or'R')5713 file_b => new/current file name of a file (any chg)5714}5715;57165717# retval of read_url_paths{,_all}();5718$l_map= {5719# repository root url5720'https://svn.musicpd.org'=> {5721# repository path # GIT_SVN_ID5722'mpd/trunk'=>'trunk',5723'mpd/tags/0.11.5'=>'tags/0.11.5',5724},5725}57265727Notes:5728 I don't trust the each() function on unless I created%hashmyself5729 because the internal iterator may not have started at base.