1#!/usr/bin/env 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}) { 26if(exists$ENV{GIT_SSH}) { 27$ENV{SVN_SSH} =$ENV{GIT_SSH}; 28if($^Oeq'msys') { 29$ENV{SVN_SSH} =~s/\\/\\\\/g; 30$ENV{SVN_SSH} =~s/(.*)/"$1"/; 31} 32} 33} 34 35$Git::SVN::Log::TZ =$ENV{TZ}; 36$ENV{TZ} ='UTC'; 37$| =1;# unbuffer STDOUT 38 39sub fatal (@) {print STDERR "@_\n";exit1} 40sub _req_svn { 41require SVN::Core;# use()-ing this causes segfaults for me... *shrug* 42require SVN::Ra; 43require SVN::Delta; 44if($SVN::Core::VERSION lt'1.1.0') { 45 fatal "Need SVN::Core 1.1.0 or better (got$SVN::Core::VERSION)"; 46} 47} 48my$can_compress=eval{require Compress::Zlib;1}; 49push@Git::SVN::Ra::ISA,'SVN::Ra'; 50push@SVN::Git::Editor::ISA,'SVN::Delta::Editor'; 51push@SVN::Git::Fetcher::ISA,'SVN::Delta::Editor'; 52use Carp qw/croak/; 53use Digest::MD5; 54use IO::File qw//; 55use File::Basename qw/dirname basename/; 56use File::Path qw/mkpath/; 57use File::Spec; 58use File::Find; 59use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; 60use IPC::Open3; 61use Git; 62use Memoize;# core since 5.8.0, Jul 2002 63 64BEGIN{ 65# import functions from Git into our packages, en masse 66no strict 'refs'; 67foreach(qw/command command_oneline command_noisy command_output_pipe 68 command_input_pipe command_close_pipe 69 command_bidi_pipe command_close_bidi_pipe/) { 70formy$package(qw(SVN::Git::Editor SVN::Git::Fetcher 71 Git::SVN::Migration Git::SVN::Log Git::SVN), 72 __PACKAGE__) { 73*{"${package}::$_"} = \&{"Git::$_"}; 74} 75} 76 Memoize::memoize 'Git::config'; 77 Memoize::memoize 'Git::config_bool'; 78} 79 80my($SVN); 81 82$sha1=qr/[a-f\d]{40}/; 83$sha1_short=qr/[a-f\d]{4,40}/; 84my($_stdin,$_help,$_edit, 85$_message,$_file,$_branch_dest, 86$_template,$_shared, 87$_version,$_fetch_all,$_no_rebase,$_fetch_parent, 88$_merge,$_strategy,$_dry_run,$_local, 89$_prefix,$_no_checkout,$_url,$_verbose, 90$_git_format,$_commit_url,$_tag,$_merge_info,$_interactive); 91$Git::SVN::_follow_parent =1; 92$SVN::Git::Fetcher::_placeholder_filename =".gitignore"; 93$_q||=0; 94my%remote_opts= ('username=s'=> \$Git::SVN::Prompt::_username, 95'config-dir=s'=> \$Git::SVN::Ra::config_dir, 96'no-auth-cache'=> \$Git::SVN::Prompt::_no_auth_cache, 97'ignore-paths=s'=> \$SVN::Git::Fetcher::_ignore_regex, 98'ignore-refs=s'=> \$Git::SVN::Ra::_ignore_refs_regex ); 99my%fc_opts= ('follow-parent|follow!'=> \$Git::SVN::_follow_parent, 100'authors-file|A=s'=> \$_authors, 101'authors-prog=s'=> \$_authors_prog, 102'repack:i'=> \$Git::SVN::_repack, 103'noMetadata'=> \$Git::SVN::_no_metadata, 104'useSvmProps'=> \$Git::SVN::_use_svm_props, 105'useSvnsyncProps'=> \$Git::SVN::_use_svnsync_props, 106'log-window-size=i'=> \$Git::SVN::Ra::_log_window_size, 107'no-checkout'=> \$_no_checkout, 108'quiet|q+'=> \$_q, 109'repack-flags|repack-args|repack-opts=s'=> 110 \$Git::SVN::_repack_flags, 111'use-log-author'=> \$Git::SVN::_use_log_author, 112'add-author-from'=> \$Git::SVN::_add_author_from, 113'localtime'=> \$Git::SVN::_localtime, 114%remote_opts); 115 116my($_trunk,@_tags,@_branches,$_stdlayout); 117my%icv; 118my%init_opts= ('template=s'=> \$_template,'shared:s'=> \$_shared, 119'trunk|T=s'=> \$_trunk,'tags|t=s@'=> \@_tags, 120'branches|b=s@'=> \@_branches,'prefix=s'=> \$_prefix, 121'stdlayout|s'=> \$_stdlayout, 122'minimize-url|m!'=> \$Git::SVN::_minimize_url, 123'no-metadata'=>sub{$icv{noMetadata} =1}, 124'use-svm-props'=>sub{$icv{useSvmProps} =1}, 125'use-svnsync-props'=>sub{$icv{useSvnsyncProps} =1}, 126'rewrite-root=s'=>sub{$icv{rewriteRoot} =$_[1] }, 127'rewrite-uuid=s'=>sub{$icv{rewriteUUID} =$_[1] }, 128%remote_opts); 129my%cmt_opts= ('edit|e'=> \$_edit, 130'rmdir'=> \$SVN::Git::Editor::_rmdir, 131'find-copies-harder'=> \$SVN::Git::Editor::_find_copies_harder, 132'l=i'=> \$SVN::Git::Editor::_rename_limit, 133'copy-similarity|C=i'=> \$SVN::Git::Editor::_cp_similarity 134); 135 136my%cmd= ( 137 fetch => [ \&cmd_fetch,"Download new revisions from SVN", 138{'revision|r=s'=> \$_revision, 139'fetch-all|all'=> \$_fetch_all, 140'parent|p'=> \$_fetch_parent, 141%fc_opts} ], 142 clone => [ \&cmd_clone,"Initialize and fetch revisions", 143{'revision|r=s'=> \$_revision, 144'preserve-empty-dirs'=> 145 \$SVN::Git::Fetcher::_preserve_empty_dirs, 146'placeholder-filename=s'=> 147 \$SVN::Git::Fetcher::_placeholder_filename, 148%fc_opts,%init_opts} ], 149 init => [ \&cmd_init,"Initialize a repo for tracking". 150" (requires URL argument)", 151 \%init_opts], 152'multi-init'=> [ \&cmd_multi_init, 153"Deprecated alias for ". 154"'$0init -T<trunk> -b<branches> -t<tags>'", 155 \%init_opts], 156 dcommit => [ \&cmd_dcommit, 157'Commit several diffs to merge with upstream', 158{'merge|m|M'=> \$_merge, 159'strategy|s=s'=> \$_strategy, 160'verbose|v'=> \$_verbose, 161'dry-run|n'=> \$_dry_run, 162'fetch-all|all'=> \$_fetch_all, 163'commit-url=s'=> \$_commit_url, 164'revision|r=i'=> \$_revision, 165'no-rebase'=> \$_no_rebase, 166'mergeinfo=s'=> \$_merge_info, 167'interactive|i'=> \$_interactive, 168%cmt_opts,%fc_opts} ], 169 branch => [ \&cmd_branch, 170'Create a branch in the SVN repository', 171{'message|m=s'=> \$_message, 172'destination|d=s'=> \$_branch_dest, 173'dry-run|n'=> \$_dry_run, 174'tag|t'=> \$_tag, 175'username=s'=> \$Git::SVN::Prompt::_username, 176'commit-url=s'=> \$_commit_url} ], 177 tag => [sub{$_tag=1; cmd_branch(@_) }, 178'Create a tag in the SVN repository', 179{'message|m=s'=> \$_message, 180'destination|d=s'=> \$_branch_dest, 181'dry-run|n'=> \$_dry_run, 182'username=s'=> \$Git::SVN::Prompt::_username, 183'commit-url=s'=> \$_commit_url} ], 184'set-tree'=> [ \&cmd_set_tree, 185"Set an SVN repository to a git tree-ish", 186{'stdin'=> \$_stdin,%cmt_opts,%fc_opts, } ], 187'create-ignore'=> [ \&cmd_create_ignore, 188'Create a .gitignore per svn:ignore', 189{'revision|r=i'=> \$_revision 190} ], 191'mkdirs'=> [ \&cmd_mkdirs , 192"recreate empty directories after a checkout", 193{'revision|r=i'=> \$_revision} ], 194'propget'=> [ \&cmd_propget, 195'Print the value of a property on a file or directory', 196{'revision|r=i'=> \$_revision} ], 197'proplist'=> [ \&cmd_proplist, 198'List all properties of a file or directory', 199{'revision|r=i'=> \$_revision} ], 200'show-ignore'=> [ \&cmd_show_ignore,"Show svn:ignore listings", 201{'revision|r=i'=> \$_revision 202} ], 203'show-externals'=> [ \&cmd_show_externals,"Show svn:externals listings", 204{'revision|r=i'=> \$_revision 205} ], 206'multi-fetch'=> [ \&cmd_multi_fetch, 207"Deprecated alias for$0fetch --all", 208{'revision|r=s'=> \$_revision,%fc_opts} ], 209'migrate'=> [sub{ }, 210# no-op, we automatically run this anyways, 211'Migrate configuration/metadata/layout from 212 previous versions of git-svn', 213{'minimize'=> \$Git::SVN::Migration::_minimize, 214%remote_opts} ], 215'log'=> [ \&Git::SVN::Log::cmd_show_log,'Show commit logs', 216{'limit=i'=> \$Git::SVN::Log::limit, 217'revision|r=s'=> \$_revision, 218'verbose|v'=> \$Git::SVN::Log::verbose, 219'incremental'=> \$Git::SVN::Log::incremental, 220'oneline'=> \$Git::SVN::Log::oneline, 221'show-commit'=> \$Git::SVN::Log::show_commit, 222'non-recursive'=> \$Git::SVN::Log::non_recursive, 223'authors-file|A=s'=> \$_authors, 224'color'=> \$Git::SVN::Log::color, 225'pager=s'=> \$Git::SVN::Log::pager 226} ], 227'find-rev'=> [ \&cmd_find_rev, 228"Translate between SVN revision numbers and tree-ish", 229{} ], 230'rebase'=> [ \&cmd_rebase,"Fetch and rebase your working directory", 231{'merge|m|M'=> \$_merge, 232'verbose|v'=> \$_verbose, 233'strategy|s=s'=> \$_strategy, 234'local|l'=> \$_local, 235'fetch-all|all'=> \$_fetch_all, 236'dry-run|n'=> \$_dry_run, 237%fc_opts} ], 238'commit-diff'=> [ \&cmd_commit_diff, 239'Commit a diff between two trees', 240{'message|m=s'=> \$_message, 241'file|F=s'=> \$_file, 242'revision|r=s'=> \$_revision, 243%cmt_opts} ], 244'info'=> [ \&cmd_info, 245"Show info about the latest SVN revision 246 on the current branch", 247{'url'=> \$_url, } ], 248'blame'=> [ \&Git::SVN::Log::cmd_blame, 249"Show what revision and author last modified each line of a file", 250{'git-format'=> \$_git_format} ], 251'reset'=> [ \&cmd_reset, 252"Undo fetches back to the specified SVN revision", 253{'revision|r=s'=> \$_revision, 254'parent|p'=> \$_fetch_parent} ], 255'gc'=> [ \&cmd_gc, 256"Compress unhandled.log files in .git/svn and remove ". 257"index files in .git/svn", 258{} ], 259); 260 261use Term::ReadLine; 262package FakeTerm; 263sub new { 264my($class,$reason) =@_; 265returnbless \$reason,shift; 266} 267subreadline{ 268my$self=shift; 269die"Cannot use readline on FakeTerm:$$self"; 270} 271package main; 272 273my$term=eval{ 274$ENV{"GIT_SVN_NOTTY"} 275? new Term::ReadLine 'git-svn', \*STDIN, \*STDOUT 276: new Term::ReadLine 'git-svn'; 277}; 278if($@) { 279$term= new FakeTerm "$@: going non-interactive"; 280} 281 282my$cmd; 283for(my$i=0;$i<@ARGV;$i++) { 284if(defined$cmd{$ARGV[$i]}) { 285$cmd=$ARGV[$i]; 286splice@ARGV,$i,1; 287last; 288}elsif($ARGV[$i]eq'help') { 289$cmd=$ARGV[$i+1]; 290 usage(0); 291} 292}; 293 294# make sure we're always running at the top-level working directory 295unless($cmd&&$cmd=~/(?:clone|init|multi-init)$/) { 296unless(-d $ENV{GIT_DIR}) { 297if($git_dir_user_set) { 298die"GIT_DIR=$ENV{GIT_DIR} explicitly set, ", 299"but it is not a directory\n"; 300} 301my$git_dir=delete$ENV{GIT_DIR}; 302my$cdup=undef; 303 git_cmd_try { 304$cdup= command_oneline(qw/rev-parse --show-cdup/); 305$git_dir='.'unless($cdup); 306chomp$cdupif($cdup); 307$cdup="."unless($cdup&&length$cdup); 308}"Already at toplevel, but$git_dirnot found\n"; 309chdir$cdupor die"Unable to chdir up to '$cdup'\n"; 310unless(-d $git_dir) { 311die"$git_dirstill not found after going to ", 312"'$cdup'\n"; 313} 314$ENV{GIT_DIR} =$git_dir; 315} 316$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 317} 318 319my%opts= %{$cmd{$cmd}->[2]}if(defined$cmd); 320 321read_git_config(\%opts); 322if($cmd&& ($cmdeq'log'||$cmdeq'blame')) { 323 Getopt::Long::Configure('pass_through'); 324} 325my$rv= GetOptions(%opts,'h|H'=> \$_help,'version|V'=> \$_version, 326'minimize-connections'=> \$Git::SVN::Migration::_minimize, 327'id|i=s'=> \$Git::SVN::default_ref_id, 328'svn-remote|remote|R=s'=>sub{ 329$Git::SVN::no_reuse_existing =1; 330$Git::SVN::default_repo_id =$_[1] }); 331exit1if(!$rv&&$cmd&&$cmdne'log'); 332 333usage(0)if$_help; 334version()if$_version; 335usage(1)unlessdefined$cmd; 336load_authors()if$_authors; 337if(defined$_authors_prog) { 338$_authors_prog="'". File::Spec->rel2abs($_authors_prog) ."'"; 339} 340 341unless($cmd=~/^(?:clone|init|multi-init|commit-diff)$/) { 342 Git::SVN::Migration::migration_check(); 343} 344Git::SVN::init_vars(); 345eval{ 346 Git::SVN::verify_remotes_sanity(); 347$cmd{$cmd}->[0]->(@ARGV); 348}; 349fatal $@if$@; 350post_fetch_checkout(); 351exit0; 352 353####################### primary functions ###################### 354sub usage { 355my$exit=shift||0; 356my$fd=$exit? \*STDERR : \*STDOUT; 357print$fd<<""; 358git-svn - bidirectional operations between a single Subversion tree and git 359Usage: git svn <command> [options] [arguments]\n 360 361print$fd"Available commands:\n"unless$cmd; 362 363foreach(sort keys%cmd) { 364next if$cmd&&$cmdne$_; 365next if/^multi-/;# don't show deprecated commands 366print$fd' ',pack('A17',$_),$cmd{$_}->[1],"\n"; 367foreach(sort keys%{$cmd{$_}->[2]}) { 368# mixed-case options are for .git/config only 369next if/[A-Z]/&&/^[a-z]+$/i; 370# prints out arguments as they should be passed: 371my$x= s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : ''; 372print$fd' ' x 21,join(', ',map{length$_>1? 373"--$_":"-$_"} 374split/\|/,$_),"$x\n"; 375} 376} 377print$fd<<""; 378\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an 379arbitrary identifier if you're tracking multiple SVN branches/repositories in 380one git repository and want to keep them separate. See git-svn(1) for more 381information. 382 383 exit$exit; 384} 385 386sub version { 387 ::_req_svn(); 388 print "git-svn version$VERSION(svn$SVN::Core::VERSION)\n"; 389 exit 0; 390} 391 392sub ask { 393 my ($prompt,%arg) =@_; 394 my$valid_re=$arg{valid_re}; 395 my$default=$arg{default}; 396 my$resp; 397 my$i= 0; 398 399 if ( !( defined($term->IN) 400 && defined( fileno($term->IN) ) 401 && defined($term->OUT ) 402 && defined( fileno($term->OUT) ) ) ){ 403 return defined($default) ?$default: undef; 404 } 405 406 while ($i++< 10) { 407$resp=$term->readline($prompt); 408 if (!defined$resp) { # EOF 409 print "\n"; 410 return defined$default?$default: undef; 411 } 412 if ($respeq '' and defined$default) { 413 return$default; 414 } 415 if (!defined$valid_reor$resp=~ /$valid_re/) { 416 return$resp; 417 } 418 } 419 return undef; 420} 421 422sub do_git_init_db { 423 unless (-d$ENV{GIT_DIR}) { 424 my@init_db= ('init'); 425 push@init_db, "--template=$_template" if defined$_template; 426 if (defined$_shared) { 427 if ($_shared=~ /[a-z]/) { 428 push@init_db, "--shared=$_shared"; 429 } else { 430 push@init_db, "--shared"; 431 } 432 } 433 command_noisy(@init_db); 434$_repository= Git->repository(Repository => ".git"); 435 } 436 my$set; 437 my$pfx= "svn-remote.$Git::SVN::default_repo_id"; 438 foreach my$i(keys%icv) { 439 die "'$set' and '$i' cannot both be set\n" if$set; 440 next unless defined$icv{$i}; 441 command_noisy('config', "$pfx.$i",$icv{$i}); 442$set=$i; 443 } 444 my$ignore_paths_regex= \$SVN::Git::Fetcher::_ignore_regex; 445 command_noisy('config', "$pfx.ignore-paths",$$ignore_paths_regex) 446 if defined$$ignore_paths_regex; 447 my$ignore_refs_regex= \$Git::SVN::Ra::_ignore_refs_regex; 448 command_noisy('config', "$pfx.ignore-refs",$$ignore_refs_regex) 449 if defined$$ignore_refs_regex; 450 451 if (defined$SVN::Git::Fetcher::_preserve_empty_dirs) { 452 my$fname= \$SVN::Git::Fetcher::_placeholder_filename; 453 command_noisy('config', "$pfx.preserve-empty-dirs", 'true'); 454 command_noisy('config', "$pfx.placeholder-filename",$$fname); 455 } 456} 457 458sub init_subdir { 459 my$repo_path= shift or return; 460 mkpath([$repo_path]) unless -d$repo_path; 461 chdir$repo_pathor die "Couldn't chdir to $repo_path:$!\n"; 462$ENV{GIT_DIR} = '.git'; 463$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 464} 465 466sub cmd_clone { 467 my ($url,$path) =@_; 468 if (!defined$path&& 469 (defined$_trunk||@_branches||@_tags|| 470 defined$_stdlayout) && 471$url!~ m#^[a-z\+]+://#) { 472$path=$url; 473 } 474$path= basename($url) if !defined$path|| !length$path; 475 my$authors_absolute=$_authors? File::Spec->rel2abs($_authors) : ""; 476 cmd_init($url,$path); 477 command_oneline('config', 'svn.authorsfile',$authors_absolute) 478 if$_authors; 479 Git::SVN::fetch_all($Git::SVN::default_repo_id); 480} 481 482sub cmd_init { 483 if (defined$_stdlayout) { 484$_trunk= 'trunk' if (!defined$_trunk); 485@_tags= 'tags' if (!@_tags); 486@_branches= 'branches' if (!@_branches); 487 } 488 if (defined$_trunk||@_branches||@_tags) { 489 return cmd_multi_init(@_); 490 } 491 my$url= shift or die "SVN repository location required ", 492 "as a command-line argument\n"; 493$url= canonicalize_url($url); 494 init_subdir(@_); 495 do_git_init_db(); 496 497 if ($Git::SVN::_minimize_url eq 'unset') { 498$Git::SVN::_minimize_url = 0; 499 } 500 501 Git::SVN->init($url); 502} 503 504sub cmd_fetch { 505 if (grep /^\d+=./,@_) { 506 die "'<rev>=<commit>' fetch arguments are ", 507 "no longer supported.\n"; 508 } 509 my ($remote) =@_; 510 if (@_> 1) { 511 die "Usage:$0 fetch [--all] [--parent] [svn-remote]\n"; 512 } 513$Git::SVN::no_reuse_existing = undef; 514 if ($_fetch_parent) { 515 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD'); 516 unless ($gs) { 517 die "Unable to determine upstream SVN information from ", 518 "working tree history\n"; 519 } 520 # just fetch, don't checkout. 521$_no_checkout= 'true'; 522$_fetch_all?$gs->fetch_all :$gs->fetch; 523 } elsif ($_fetch_all) { 524 cmd_multi_fetch(); 525 } else { 526$remote||=$Git::SVN::default_repo_id; 527 Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes()); 528 } 529} 530 531sub cmd_set_tree { 532 my (@commits) =@_; 533 if ($_stdin|| !@commits) { 534 print "Reading from stdin...\n"; 535@commits= (); 536 while (<STDIN>) { 537 if (/\b($sha1_short)\b/o) { 538 unshift@commits,$1; 539 } 540 } 541 } 542 my@revs; 543 foreach my$c(@commits) { 544 my@tmp= command('rev-parse',$c); 545 if (scalar@tmp== 1) { 546 push@revs,$tmp[0]; 547 } elsif (scalar@tmp> 1) { 548 push@revs, reverse(command('rev-list',@tmp)); 549 } else { 550 fatal "Failed to rev-parse $c"; 551 } 552 } 553 my$gs= Git::SVN->new; 554 my ($r_last,$cmt_last) =$gs->last_rev_commit; 555$gs->fetch; 556 if (defined$gs->{last_rev} &&$r_last!=$gs->{last_rev}) { 557 fatal "There are new revisions that were fetched ", 558 "and need to be merged (or acknowledged)", 559 "before committing.\nlast rev:$r_last\n", 560 " current:$gs->{last_rev}"; 561 } 562$gs->set_tree($_) foreach@revs; 563 print "Done committing ",scalar@revs," revisions to SVN\n"; 564 unlink$gs->{index}; 565} 566 567sub split_merge_info_range { 568 my ($range) =@_; 569 if ($range=~ /(\d+)-(\d+)/) { 570 return (int($1), int($2)); 571 } else { 572 return (int($range), int($range)); 573 } 574} 575 576sub combine_ranges { 577 my ($in) =@_; 578 579 my@fnums= (); 580 my@arr= split(/,/,$in); 581 for my$element(@arr) { 582 my ($start,$end) = split_merge_info_range($element); 583 push@fnums,$start; 584 } 585 586 my@sorted=@arr[ sort { 587$fnums[$a] <=>$fnums[$b] 588 } 0..$#arr]; 589 590 my@return= (); 591 my$last= -1; 592 my$first= -1; 593 for my$element(@sorted) { 594 my ($start,$end) = split_merge_info_range($element); 595 596 if ($last== -1) { 597$first=$start; 598$last=$end; 599 next; 600 } 601 if ($start<=$last+1) { 602 if ($end>$last) { 603$last=$end; 604 } 605 next; 606 } 607 if ($first==$last) { 608 push@return, "$first"; 609 } else { 610 push@return, "$first-$last"; 611 } 612$first=$start; 613$last=$end; 614 } 615 616 if ($first!= -1) { 617 if ($first==$last) { 618 push@return, "$first"; 619 } else { 620 push@return, "$first-$last"; 621 } 622 } 623 624 return join(',',@return); 625} 626 627sub merge_revs_into_hash { 628 my ($hash,$minfo) =@_; 629 my@lines= split(' ',$minfo); 630 631 for my$line(@lines) { 632 my ($branchpath,$revs) = split(/:/,$line); 633 634 if (exists($hash->{$branchpath})) { 635 # Merge the two revision sets 636 my$combined= "$hash->{$branchpath},$revs"; 637$hash->{$branchpath} = combine_ranges($combined); 638 } else { 639 # Just do range combining for consolidation 640$hash->{$branchpath} = combine_ranges($revs); 641 } 642 } 643} 644 645sub merge_merge_info { 646 my ($mergeinfo_one,$mergeinfo_two) =@_; 647 my%result_hash= (); 648 649 merge_revs_into_hash(\%result_hash,$mergeinfo_one); 650 merge_revs_into_hash(\%result_hash,$mergeinfo_two); 651 652 my$result= ''; 653 # Sort below is for consistency's sake 654 for my$branchname(sort keys(%result_hash)) { 655 my$revlist=$result_hash{$branchname}; 656$result.= "$branchname:$revlist\n" 657 } 658 return$result; 659} 660 661sub populate_merge_info { 662 my ($d,$gs,$uuid,$linear_refs,$rewritten_parent) =@_; 663 664 my%parentshash; 665 read_commit_parents(\%parentshash,$d); 666 my@parents= @{$parentshash{$d}}; 667 if ($#parents> 0) { 668 # Merge commit 669 my$all_parents_ok= 1; 670 my$aggregate_mergeinfo= ''; 671 my$rooturl=$gs->repos_root; 672 673 if (defined($rewritten_parent)) { 674 # Replace first parent with newly-rewritten version 675 shift@parents; 676 unshift@parents,$rewritten_parent; 677 } 678 679 foreach my$parent(@parents) { 680 my ($branchurl,$svnrev,$paruuid) = 681 cmt_metadata($parent); 682 683 unless (defined($svnrev)) { 684 # Should have been caught be preflight check 685 fatal "merge commit $dhas ancestor $parent, but that change " 686 ."doesnot have git-svn metadata!"; 687 } 688 unless ($branchurl=~ /^$rooturl(.*)/) { 689 fatal "commit $parent git-svn metadata changed mid-run!"; 690 } 691 my$branchpath=$1; 692 693 my$ra= Git::SVN::Ra->new($branchurl); 694 my (undef, undef,$props) = 695$ra->get_dir(canonicalize_path("."),$svnrev); 696 my$par_mergeinfo=$props->{'svn:mergeinfo'}; 697 unless (defined$par_mergeinfo) { 698$par_mergeinfo= ''; 699 } 700 # Merge previous mergeinfo values 701$aggregate_mergeinfo= 702 merge_merge_info($aggregate_mergeinfo, 703$par_mergeinfo, 0); 704 705 next if$parenteq$parents[0]; # Skip first parent 706 # Add new changes being placed in tree by merge 707 my@cmd= (qw/rev-list --reverse/, 708$parent, qw/--not/); 709 foreach my$par(@parents) { 710 unless ($pareq$parent) { 711 push@cmd,$par; 712 } 713 } 714 my@revsin= (); 715 my ($revlist,$ctx) = command_output_pipe(@cmd); 716 while (<$revlist>) { 717 my$irev=$_; 718 chomp$irev; 719 my (undef,$csvnrev, undef) = 720 cmt_metadata($irev); 721 unless (defined$csvnrev) { 722 # A child is missing SVN annotations... 723 # this might be OK, or might not be. 724 warn "W:child $irevis merged into revision " 725 ."$d but doesnot have git-svn metadata." 726 ."This means git-svn cannot determine the " 727 ."svn revision numbers to place into the " 728 ."svn:mergeinfo property. You must ensure " 729 ."a branch is entirely committed to " 730 ."SVN before merging it in order for" 731 ."svn:mergeinfo population to function " 732 ."properly"; 733 } 734 push@revsin,$csvnrev; 735 } 736 command_close_pipe($revlist,$ctx); 737 738 last unless$all_parents_ok; 739 740 # We now have a list of all SVN revnos which are 741 # merged by this particular parent. Integrate them. 742 next if$#revsin== -1; 743 my$newmergeinfo= "$branchpath:" . join(',',@revsin); 744$aggregate_mergeinfo= 745 merge_merge_info($aggregate_mergeinfo, 746$newmergeinfo, 1); 747 } 748 if ($all_parents_okand$aggregate_mergeinfo) { 749 return$aggregate_mergeinfo; 750 } 751 } 752 753 return undef; 754} 755 756sub cmd_dcommit { 757 my$head= shift; 758 command_noisy(qw/update-index --refresh/); 759 git_cmd_try { command_oneline(qw/diff-index --quiet HEAD/) } 760 'Cannot dcommit with a dirty index. Commit your changes first, ' 761 . "or stash them with `git stash'.\n"; 762$head||= 'HEAD'; 763 764 my$old_head; 765 if ($headne 'HEAD') { 766$old_head= eval { 767 command_oneline([qw/symbolic-ref -q HEAD/]) 768 }; 769 if ($old_head) { 770$old_head=~ s{^refs/heads/}{}; 771 } else { 772$old_head= eval { command_oneline(qw/rev-parse HEAD/) }; 773 } 774 command(['checkout',$head], STDERR => 0); 775 } 776 777 my@refs; 778 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD', \@refs); 779 unless ($gs) { 780 die "Unable to determine upstream SVN information from ", 781 "$headhistory.\nPerhaps the repository is empty."; 782 } 783 784 if (defined$_commit_url) { 785$url=$_commit_url; 786 } else { 787$url= eval { command_oneline('config', '--get', 788 "svn-remote.$gs->{repo_id}.commiturl") }; 789 if (!$url) { 790$url=$gs->full_pushurl 791 } 792 } 793 794 my$last_rev=$_revisionif defined$_revision; 795 if ($url) { 796 print "Committing to$url...\n"; 797 } 798 my ($linear_refs,$parents) = linearize_history($gs, \@refs); 799 if ($_no_rebase&& scalar(@$linear_refs) > 1) { 800 warn "Attempting to commit more than one change while ", 801 "--no-rebase is enabled.\n", 802 "If these changes depend on each other, re-running ", 803 "without --no-rebase may be required." 804 } 805 806 if (defined$_interactive){ 807 my$ask_default= "y"; 808 foreach my$d(@$linear_refs){ 809 my ($fh,$ctx) = command_output_pipe(qw(show --summary),"$d"); 810while(<$fh>){ 811print$_; 812} 813 command_close_pipe($fh,$ctx); 814$_= ask("Commit this patch to SVN? ([y]es (default)|[n]o|[q]uit|[a]ll): ", 815 valid_re =>qr/^(?:yes|y|no|n|quit|q|all|a)/i, 816default=>$ask_default); 817die"Commit this patch reply required"unlessdefined$_; 818if(/^[nq]/i) { 819exit(0); 820}elsif(/^a/i) { 821last; 822} 823} 824} 825 826my$expect_url=$url; 827 828my$push_merge_info=eval{ 829 command_oneline(qw/config --get svn.pushmergeinfo/) 830}; 831if(not defined($push_merge_info) 832or$push_merge_infoeq"false" 833or$push_merge_infoeq"no" 834or$push_merge_infoeq"never") { 835$push_merge_info=0; 836} 837 838unless(defined($_merge_info) || !$push_merge_info) { 839# Preflight check of changes to ensure no issues with mergeinfo 840# This includes check for uncommitted-to-SVN parents 841# (other than the first parent, which we will handle), 842# information from different SVN repos, and paths 843# which are not underneath this repository root. 844my$rooturl=$gs->repos_root; 845foreachmy$d(@$linear_refs) { 846my%parentshash; 847 read_commit_parents(\%parentshash,$d); 848my@realparents= @{$parentshash{$d}}; 849if($#realparents>0) { 850# Merge commit 851shift@realparents;# Remove/ignore first parent 852foreachmy$parent(@realparents) { 853my($branchurl,$svnrev,$paruuid) = cmt_metadata($parent); 854unless(defined$paruuid) { 855# A parent is missing SVN annotations... 856# abort the whole operation. 857 fatal "$parentis merged into revision$d, " 858."but does not have git-svn metadata. " 859."Either dcommit the branch or use a " 860."local cherry-pick, FF merge, or rebase " 861."instead of an explicit merge commit."; 862} 863 864unless($paruuideq$uuid) { 865# Parent has SVN metadata from different repository 866 fatal "merge parent$parentfor change$dhas " 867."git-svn uuid$paruuid, while current change " 868."has uuid$uuid!"; 869} 870 871unless($branchurl=~/^$rooturl(.*)/) { 872# This branch is very strange indeed. 873 fatal "merge parent$parentfor$dis on branch " 874."$branchurl, which is not under the " 875."git-svn root$rooturl!"; 876} 877} 878} 879} 880} 881 882my$rewritten_parent; 883 Git::SVN::remove_username($expect_url); 884if(defined($_merge_info)) { 885$_merge_info=~tr{ }{\n}; 886} 887while(1) { 888my$d=shift@$linear_refsorlast; 889unless(defined$last_rev) { 890(undef,$last_rev,undef) = cmt_metadata("$d~1"); 891unless(defined$last_rev) { 892 fatal "Unable to extract revision information ", 893"from commit$d~1"; 894} 895} 896if($_dry_run) { 897print"diff-tree$d~1$d\n"; 898}else{ 899my$cmt_rev; 900 901unless(defined($_merge_info) || !$push_merge_info) { 902$_merge_info= populate_merge_info($d,$gs, 903$uuid, 904$linear_refs, 905$rewritten_parent); 906} 907 908my%ed_opts= ( r =>$last_rev, 909log=> get_commit_entry($d)->{log}, 910 ra => Git::SVN::Ra->new($url), 911 config => SVN::Core::config_get_config( 912$Git::SVN::Ra::config_dir 913), 914 tree_a =>"$d~1", 915 tree_b =>$d, 916 editor_cb =>sub{ 917print"Committed r$_[0]\n"; 918$cmt_rev=$_[0]; 919}, 920 mergeinfo =>$_merge_info, 921 svn_path =>''); 922if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) { 923print"No changes\n$d~1==$d\n"; 924}elsif($parents->{$d} && @{$parents->{$d}}) { 925$gs->{inject_parents_dcommit}->{$cmt_rev} = 926$parents->{$d}; 927} 928$_fetch_all?$gs->fetch_all:$gs->fetch; 929$last_rev=$cmt_rev; 930next if$_no_rebase; 931 932# we always want to rebase against the current HEAD, 933# not any head that was passed to us 934my@diff= command('diff-tree',$d, 935$gs->refname,'--'); 936my@finish; 937if(@diff) { 938@finish= rebase_cmd(); 939print STDERR "W:$dand ",$gs->refname, 940" differ, using@finish:\n", 941join("\n",@diff),"\n"; 942}else{ 943print"No changes between current HEAD and ", 944$gs->refname, 945"\nResetting to the latest ", 946$gs->refname,"\n"; 947@finish= qw/reset --mixed/; 948} 949 command_noisy(@finish,$gs->refname); 950 951$rewritten_parent= command_oneline(qw/rev-parse HEAD/); 952 953if(@diff) { 954@refs= (); 955my($url_,$rev_,$uuid_,$gs_) = 956 working_head_info('HEAD', \@refs); 957my($linear_refs_,$parents_) = 958 linearize_history($gs_, \@refs); 959if(scalar(@$linear_refs) != 960scalar(@$linear_refs_)) { 961 fatal "# of revisions changed ", 962"\nbefore:\n", 963join("\n",@$linear_refs), 964"\n\nafter:\n", 965join("\n",@$linear_refs_),"\n", 966'If you are attempting to commit ', 967"merges, try running:\n\t", 968'git rebase --interactive', 969'--preserve-merges ', 970$gs->refname, 971"\nBefore dcommitting"; 972} 973if($url_ne$expect_url) { 974if($url_eq$gs->metadata_url) { 975print 976"Accepting rewritten URL:", 977"$url_\n"; 978}else{ 979 fatal 980"URL mismatch after rebase:", 981"$url_!=$expect_url"; 982} 983} 984if($uuid_ne$uuid) { 985 fatal "uuid mismatch after rebase: ", 986"$uuid_!=$uuid"; 987} 988# remap parents 989my(%p,@l,$i); 990for($i=0;$i<scalar@$linear_refs;$i++) { 991my$new=$linear_refs_->[$i]ornext; 992$p{$new} = 993$parents->{$linear_refs->[$i]}; 994push@l,$new; 995} 996$parents= \%p; 997$linear_refs= \@l; 998} 999}1000}10011002if($old_head) {1003my$new_head= command_oneline(qw/rev-parse HEAD/);1004my$new_is_symbolic=eval{1005 command_oneline(qw/symbolic-ref -q HEAD/);1006};1007if($new_is_symbolic) {1008print"dcommitted the branch ",$head,"\n";1009}else{1010print"dcommitted on a detached HEAD because you gave ",1011"a revision argument.\n",1012"The rewritten commit is: ",$new_head,"\n";1013}1014 command(['checkout',$old_head], STDERR =>0);1015}10161017unlink$gs->{index};1018}10191020sub cmd_branch {1021my($branch_name,$head) =@_;10221023unless(defined$branch_name&&length$branch_name) {1024die(($_tag?"tag":"branch") ." name required\n");1025}1026$head||='HEAD';10271028my(undef,$rev,undef,$gs) = working_head_info($head);1029my$src=$gs->full_pushurl;10301031my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1032my$allglobs=$remote->{$_tag?'tags':'branches'};1033my$glob;1034if($#{$allglobs} ==0) {1035$glob=$allglobs->[0];1036}else{1037unless(defined$_branch_dest) {1038die"Multiple ",1039$_tag?"tag":"branch",1040" paths defined for Subversion repository.\n",1041"You must specify where you want to create the ",1042$_tag?"tag":"branch",1043" with the --destination argument.\n";1044}1045foreachmy$g(@{$allglobs}) {1046# SVN::Git::Editor could probably be moved to Git.pm..1047my$re= SVN::Git::Editor::glob2pat($g->{path}->{left});1048if($_branch_dest=~/$re/) {1049$glob=$g;1050last;1051}1052}1053unless(defined$glob) {1054my$dest_re=qr/\b\Q$_branch_dest\E\b/;1055foreachmy$g(@{$allglobs}) {1056$g->{path}->{left} =~/$dest_re/ornext;1057if(defined$glob) {1058die"Ambiguous destination: ",1059$_branch_dest,"\nmatches both '",1060$glob->{path}->{left},"' and '",1061$g->{path}->{left},"'\n";1062}1063$glob=$g;1064}1065unless(defined$glob) {1066die"Unknown ",1067$_tag?"tag":"branch",1068" destination$_branch_dest\n";1069}1070}1071}1072my($lft,$rgt) = @{$glob->{path} }{qw/left right/};1073my$url;1074if(defined$_commit_url) {1075$url=$_commit_url;1076}else{1077$url=eval{ command_oneline('config','--get',1078"svn-remote.$gs->{repo_id}.commiturl") };1079if(!$url) {1080$url=$remote->{pushurl} ||$remote->{url};1081}1082}1083my$dst=join'/',$url,$lft,$branch_name, ($rgt|| ());10841085if($dst=~/^https:/&&$src=~/^http:/) {1086$src=~s/^http:/https:/;1087}10881089::_req_svn();10901091my$ctx= SVN::Client->new(1092 auth => Git::SVN::Ra::_auth_providers(),1093 log_msg =>sub{1094${$_[0] } =defined$_message1095?$_message1096:'Create '. ($_tag?'tag ':'branch ')1097.$branch_name;1098},1099);11001101eval{1102$ctx->ls($dst,'HEAD',0);1103}and die"branch ${branch_name} already exists\n";11041105print"Copying ${src} at r${rev} to ${dst}...\n";1106$ctx->copy($src,$rev,$dst)1107unless$_dry_run;11081109$gs->fetch_all;1110}11111112sub cmd_find_rev {1113my$revision_or_hash=shift or die"SVN or git revision required ",1114"as a command-line argument\n";1115my$result;1116if($revision_or_hash=~/^r\d+$/) {1117my$head=shift;1118$head||='HEAD';1119my@refs;1120my(undef,undef,$uuid,$gs) = working_head_info($head, \@refs);1121unless($gs) {1122die"Unable to determine upstream SVN information from ",1123"$headhistory\n";1124}1125my$desired_revision=substr($revision_or_hash,1);1126$result=$gs->rev_map_get($desired_revision,$uuid);1127}else{1128my(undef,$rev,undef) = cmt_metadata($revision_or_hash);1129$result=$rev;1130}1131print"$result\n"if$result;1132}11331134sub auto_create_empty_directories {1135my($gs) =@_;1136my$var=eval{ command_oneline('config','--get','--bool',1137"svn-remote.$gs->{repo_id}.automkdirs") };1138# By default, create empty directories by consulting the unhandled log,1139# but allow setting it to 'false' to skip it.1140return!($var&&$vareq'false');1141}11421143sub cmd_rebase {1144 command_noisy(qw/update-index --refresh/);1145my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1146unless($gs) {1147die"Unable to determine upstream SVN information from ",1148"working tree history\n";1149}1150if($_dry_run) {1151print"Remote Branch: ".$gs->refname."\n";1152print"SVN URL: ".$url."\n";1153return;1154}1155if(command(qw/diff-index HEAD --/)) {1156print STDERR "Cannot rebase with uncommited changes:\n";1157 command_noisy('status');1158exit1;1159}1160unless($_local) {1161# rebase will checkout for us, so no need to do it explicitly1162$_no_checkout='true';1163$_fetch_all?$gs->fetch_all:$gs->fetch;1164}1165 command_noisy(rebase_cmd(),$gs->refname);1166if(auto_create_empty_directories($gs)) {1167$gs->mkemptydirs;1168}1169}11701171sub cmd_show_ignore {1172my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1173$gs||= Git::SVN->new;1174my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1175$gs->prop_walk($gs->{path},$r,sub{1176my($gs,$path,$props) =@_;1177print STDOUT "\n#$path\n";1178my$s=$props->{'svn:ignore'}orreturn;1179$s=~s/[\r\n]+/\n/g;1180$s=~s/^\n+//;1181chomp$s;1182$s=~ s#^#$path#gm;1183print STDOUT "$s\n";1184});1185}11861187sub cmd_show_externals {1188my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1189$gs||= Git::SVN->new;1190my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1191$gs->prop_walk($gs->{path},$r,sub{1192my($gs,$path,$props) =@_;1193print STDOUT "\n#$path\n";1194my$s=$props->{'svn:externals'}orreturn;1195$s=~s/[\r\n]+/\n/g;1196chomp$s;1197$s=~ s#^#$path#gm;1198print STDOUT "$s\n";1199});1200}12011202sub cmd_create_ignore {1203my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1204$gs||= Git::SVN->new;1205my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1206$gs->prop_walk($gs->{path},$r,sub{1207my($gs,$path,$props) =@_;1208# $path is of the form /path/to/dir/1209$path='.'.$path;1210# SVN can have attributes on empty directories,1211# which git won't track1212 mkpath([$path])unless-d $path;1213my$ignore=$path.'.gitignore';1214my$s=$props->{'svn:ignore'}orreturn;1215open(GITIGNORE,'>',$ignore)1216or fatal("Failed to open `$ignore' for writing:$!");1217$s=~s/[\r\n]+/\n/g;1218$s=~s/^\n+//;1219chomp$s;1220# Prefix all patterns so that the ignore doesn't apply1221# to sub-directories.1222$s=~ s#^#/#gm;1223print GITIGNORE "$s\n";1224close(GITIGNORE)1225or fatal("Failed to close `$ignore':$!");1226 command_noisy('add','-f',$ignore);1227});1228}12291230sub cmd_mkdirs {1231my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1232$gs||= Git::SVN->new;1233$gs->mkemptydirs($_revision);1234}12351236sub canonicalize_path {1237my($path) =@_;1238my$dot_slash_added=0;1239if(substr($path,0,1)ne"/") {1240$path="./".$path;1241$dot_slash_added=1;1242}1243# File::Spec->canonpath doesn't collapse x/../y into y (for a1244# good reason), so let's do this manually.1245$path=~ s#/+#/#g;1246$path=~ s#/\.(?:/|$)#/#g;1247$path=~ s#/[^/]+/\.\.##g;1248$path=~ s#/$##g;1249$path=~ s#^\./## if $dot_slash_added;1250$path=~ s#^/##;1251$path=~ s#^\.$##;1252return$path;1253}12541255sub canonicalize_url {1256my($url) =@_;1257$url=~ s#^([^:]+://[^/]*/)(.*)$#$1 . canonicalize_path($2)#e;1258return$url;1259}12601261# get_svnprops(PATH)1262# ------------------1263# Helper for cmd_propget and cmd_proplist below.1264sub get_svnprops {1265my$path=shift;1266my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1267$gs||= Git::SVN->new;12681269# prefix THE PATH by the sub-directory from which the user1270# invoked us.1271$path=$cmd_dir_prefix.$path;1272 fatal("No such file or directory:$path")unless-e $path;1273my$is_dir= -d $path?1:0;1274$path=$gs->{path} .'/'.$path;12751276# canonicalize the path (otherwise libsvn will abort or fail to1277# find the file)1278$path= canonicalize_path($path);12791280my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1281my$props;1282if($is_dir) {1283(undef,undef,$props) =$gs->ra->get_dir($path,$r);1284}1285else{1286(undef,$props) =$gs->ra->get_file($path,$r,undef);1287}1288return$props;1289}12901291# cmd_propget (PROP, PATH)1292# ------------------------1293# Print the SVN property PROP for PATH.1294sub cmd_propget {1295my($prop,$path) =@_;1296$path='.'ifnot defined$path;1297 usage(1)ifnot defined$prop;1298my$props= get_svnprops($path);1299if(not defined$props->{$prop}) {1300 fatal("`$path' does not have a `$prop' SVN property.");1301}1302print$props->{$prop} ."\n";1303}13041305# cmd_proplist (PATH)1306# -------------------1307# Print the list of SVN properties for PATH.1308sub cmd_proplist {1309my$path=shift;1310$path='.'ifnot defined$path;1311my$props= get_svnprops($path);1312print"Properties on '$path':\n";1313foreach(sort keys%{$props}) {1314print"$_\n";1315}1316}13171318sub cmd_multi_init {1319my$url=shift;1320unless(defined$_trunk||@_branches||@_tags) {1321 usage(1);1322}13231324$_prefix=''unlessdefined$_prefix;1325if(defined$url) {1326$url= canonicalize_url($url);1327 init_subdir(@_);1328}1329 do_git_init_db();1330if(defined$_trunk) {1331$_trunk=~ s#^/+##;1332my$trunk_ref='refs/remotes/'.$_prefix.'trunk';1333# try both old-style and new-style lookups:1334my$gs_trunk=eval{ Git::SVN->new($trunk_ref) };1335unless($gs_trunk) {1336my($trunk_url,$trunk_path) =1337 complete_svn_url($url,$_trunk);1338$gs_trunk= Git::SVN->init($trunk_url,$trunk_path,1339undef,$trunk_ref);1340}1341}1342return unless@_branches||@_tags;1343my$ra=$url? Git::SVN::Ra->new($url) :undef;1344foreachmy$path(@_branches) {1345 complete_url_ls_init($ra,$path,'--branches/-b',$_prefix);1346}1347foreachmy$path(@_tags) {1348 complete_url_ls_init($ra,$path,'--tags/-t',$_prefix.'tags/');1349}1350}13511352sub cmd_multi_fetch {1353$Git::SVN::no_reuse_existing =undef;1354my$remotes= Git::SVN::read_all_remotes();1355foreachmy$repo_id(sort keys%$remotes) {1356if($remotes->{$repo_id}->{url}) {1357 Git::SVN::fetch_all($repo_id,$remotes);1358}1359}1360}13611362# this command is special because it requires no metadata1363sub cmd_commit_diff {1364my($ta,$tb,$url) =@_;1365my$usage="Usage:$0commit-diff -r<revision> ".1366"<tree-ish> <tree-ish> [<URL>]";1367 fatal($usage)if(!defined$ta|| !defined$tb);1368my$svn_path='';1369if(!defined$url) {1370my$gs=eval{ Git::SVN->new};1371if(!$gs) {1372 fatal("Needed URL or usable git-svn --id in ",1373"the command-line\n",$usage);1374}1375$url=$gs->{url};1376$svn_path=$gs->{path};1377}1378unless(defined$_revision) {1379 fatal("-r|--revision is a required argument\n",$usage);1380}1381if(defined$_message&&defined$_file) {1382 fatal("Both --message/-m and --file/-F specified ",1383"for the commit message.\n",1384"I have no idea what you mean");1385}1386if(defined$_file) {1387$_message= file_to_s($_file);1388}else{1389$_message||= get_commit_entry($tb)->{log};1390}1391my$ra||= Git::SVN::Ra->new($url);1392my$r=$_revision;1393if($req'HEAD') {1394$r=$ra->get_latest_revnum;1395}elsif($r!~/^\d+$/) {1396die"revision argument:$rnot understood by git-svn\n";1397}1398my%ed_opts= ( r =>$r,1399log=>$_message,1400 ra =>$ra,1401 tree_a =>$ta,1402 tree_b =>$tb,1403 editor_cb =>sub{print"Committed r$_[0]\n"},1404 svn_path =>$svn_path);1405if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {1406print"No changes\n$ta==$tb\n";1407}1408}14091410sub escape_uri_only {1411my($uri) =@_;1412my@tmp;1413foreach(splitm{/},$uri) {1414s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;1415push@tmp,$_;1416}1417join('/',@tmp);1418}14191420sub escape_url {1421my($url) =@_;1422if($url=~ m#^([^:]+)://([^/]*)(.*)$#) {1423my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));1424$url="$scheme://$domain$uri";1425}1426$url;1427}14281429sub cmd_info {1430my$path= canonicalize_path(defined($_[0]) ?$_[0] :".");1431my$fullpath= canonicalize_path($cmd_dir_prefix.$path);1432if(exists$_[1]) {1433die"Too many arguments specified\n";1434}14351436my($file_type,$diff_status) = find_file_type_and_diff_status($path);14371438if(!$file_type&& !$diff_status) {1439print STDERR "svn: '$path' is not under version control\n";1440exit1;1441}14421443my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1444unless($gs) {1445die"Unable to determine upstream SVN information from ",1446"working tree history\n";1447}14481449# canonicalize_path() will return "" to make libsvn 1.5.x happy,1450$path="."if$patheq"";14511452my$full_url=$url. ($fullpatheq""?"":"/$fullpath");14531454if($_url) {1455print escape_url($full_url),"\n";1456return;1457}14581459my$result="Path:$path\n";1460$result.="Name: ". basename($path) ."\n"if$file_typene"dir";1461$result.="URL: ". escape_url($full_url) ."\n";14621463eval{1464my$repos_root=$gs->repos_root;1465 Git::SVN::remove_username($repos_root);1466$result.="Repository Root: ". escape_url($repos_root) ."\n";1467};1468if($@) {1469$result.="Repository Root: (offline)\n";1470}1471::_req_svn();1472$result.="Repository UUID:$uuid\n"unless$diff_statuseq"A"&&1473($SVN::Core::VERSION le'1.5.4'||$file_typene"dir");1474$result.="Revision: ". ($diff_statuseq"A"?0:$rev) ."\n";14751476$result.="Node Kind: ".1477($file_typeeq"dir"?"directory":"file") ."\n";14781479my$schedule=$diff_statuseq"A"1480?"add"1481: ($diff_statuseq"D"?"delete":"normal");1482$result.="Schedule:$schedule\n";14831484if($diff_statuseq"A") {1485print$result,"\n";1486return;1487}14881489my($lc_author,$lc_rev,$lc_date_utc);1490my@args= Git::SVN::Log::git_svn_log_cmd($rev,$rev,"--",$fullpath);1491my$log= command_output_pipe(@args);1492my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;1493while(<$log>) {1494if(/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {1495$lc_author=$1;1496$lc_date_utc= Git::SVN::Log::parse_git_date($2,$3);1497}elsif(/^${esc_color} (git-svn-id:.+)$/o) {1498(undef,$lc_rev,undef) = ::extract_metadata($1);1499}1500}1501close$log;15021503 Git::SVN::Log::set_local_timezone();15041505$result.="Last Changed Author:$lc_author\n";1506$result.="Last Changed Rev:$lc_rev\n";1507$result.="Last Changed Date: ".1508 Git::SVN::Log::format_svn_date($lc_date_utc) ."\n";15091510if($file_typene"dir") {1511my$text_last_updated_date=1512($diff_statuseq"D"?$lc_date_utc: (stat$path)[9]);1513$result.=1514"Text Last Updated: ".1515 Git::SVN::Log::format_svn_date($text_last_updated_date) .1516"\n";1517my$checksum;1518if($diff_statuseq"D") {1519my($fh,$ctx) =1520 command_output_pipe(qw(cat-file blob),"HEAD:$path");1521if($file_typeeq"link") {1522my$file_name= <$fh>;1523$checksum= md5sum("link$file_name");1524}else{1525$checksum= md5sum($fh);1526}1527 command_close_pipe($fh,$ctx);1528}elsif($file_typeeq"link") {1529my$file_name=1530 command(qw(cat-file blob),"HEAD:$path");1531$checksum=1532 md5sum("link ".$file_name);1533}else{1534open FILE,"<",$pathor die$!;1535$checksum= md5sum(\*FILE);1536close FILE or die$!;1537}1538$result.="Checksum: ".$checksum."\n";1539}15401541print$result,"\n";1542}15431544sub cmd_reset {1545my$target=shift||$_revisionor die"SVN revision required\n";1546$target=$1if$target=~/^r(\d+)$/;1547$target=~/^\d+$/or die"Numeric SVN revision expected\n";1548my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1549unless($gs) {1550die"Unable to determine upstream SVN information from ".1551"history\n";1552}1553my($r,$c) =$gs->find_rev_before($target,not$_fetch_parent);1554die"Cannot find SVN revision$target\n"unlessdefined($c);1555$gs->rev_map_set($r,$c,'reset',$uuid);1556print"r$r=$c($gs->{ref_id})\n";1557}15581559sub cmd_gc {1560if(!$can_compress) {1561warn"Compress::Zlib could not be found; unhandled.log ".1562"files will not be compressed.\n";1563}1564 find({ wanted => \&gc_directory, no_chdir =>1},"$ENV{GIT_DIR}/svn");1565}15661567########################### utility functions #########################15681569sub rebase_cmd {1570my@cmd= qw/rebase/;1571push@cmd,'-v'if$_verbose;1572push@cmd, qw/--merge/if$_merge;1573push@cmd,"--strategy=$_strategy"if$_strategy;1574@cmd;1575}15761577sub post_fetch_checkout {1578return if$_no_checkout;1579my$gs=$Git::SVN::_head orreturn;1580return if verify_ref('refs/heads/master^0');15811582# look for "trunk" ref if it exists1583my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1584my$fetch=$remote->{fetch};1585if($fetch) {1586foreachmy$p(keys%$fetch) {1587 basename($fetch->{$p})eq'trunk'ornext;1588$gs= Git::SVN->new($fetch->{$p},$gs->{repo_id},$p);1589last;1590}1591}15921593my$valid_head= verify_ref('HEAD^0');1594 command_noisy(qw(update-ref refs/heads/master),$gs->refname);1595return if($valid_head|| !verify_ref('HEAD^0'));15961597return if$ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;1598my$index=$ENV{GIT_INDEX_FILE} ||"$ENV{GIT_DIR}/index";1599return if-f $index;16001601return if command_oneline(qw/rev-parse --is-inside-work-tree/)eq'false';1602return if command_oneline(qw/rev-parse --is-inside-git-dir/)eq'true';1603 command_noisy(qw/read-tree -m -u -v HEAD HEAD/);1604print STDERR "Checked out HEAD:\n",1605$gs->full_url," r",$gs->last_rev,"\n";1606if(auto_create_empty_directories($gs)) {1607$gs->mkemptydirs($gs->last_rev);1608}1609}16101611sub complete_svn_url {1612my($url,$path) =@_;1613$path=~ s#/+$##;1614if($path!~ m#^[a-z\+]+://#) {1615if(!defined$url||$url!~ m#^[a-z\+]+://#) {1616 fatal("E: '$path' is not a complete URL ",1617"and a separate URL is not specified");1618}1619return($url,$path);1620}1621return($path,'');1622}16231624sub complete_url_ls_init {1625my($ra,$repo_path,$switch,$pfx) =@_;1626unless($repo_path) {1627print STDERR "W:$switchnot specified\n";1628return;1629}1630$repo_path=~ s#/+$##;1631if($repo_path=~ m#^[a-z\+]+://#) {1632$ra= Git::SVN::Ra->new($repo_path);1633$repo_path='';1634}else{1635$repo_path=~ s#^/+##;1636unless($ra) {1637 fatal("E: '$repo_path' is not a complete URL ",1638"and a separate URL is not specified");1639}1640}1641my$url=$ra->{url};1642my$gs= Git::SVN->init($url,undef,undef,undef,1);1643my$k="svn-remote.$gs->{repo_id}.url";1644my$orig_url=eval{ command_oneline(qw/config --get/,$k) };1645if($orig_url&& ($orig_urlne$gs->{url})) {1646die"$kalready set:$orig_url\n",1647"wanted to set to:$gs->{url}\n";1648}1649 command_oneline('config',$k,$gs->{url})unless$orig_url;1650my$remote_path="$gs->{path}/$repo_path";1651$remote_path=~s{%([0-9A-F]{2})}{chr hex($1)}ieg;1652$remote_path=~ s#/+#/#g;1653$remote_path=~ s#^/##g;1654$remote_path.="/*"if$remote_path!~ /\*/;1655my($n) = ($switch=~/^--(\w+)/);1656if(length$pfx&&$pfx!~ m#/$#) {1657die"--prefix='$pfx' must have a trailing slash '/'\n";1658}1659 command_noisy('config',1660'--add',1661"svn-remote.$gs->{repo_id}.$n",1662"$remote_path:refs/remotes/$pfx*".1663('/*' x (($remote_path=~ tr/*/*/) -1)) );1664}16651666sub verify_ref {1667my($ref) =@_;1668eval{ command_oneline(['rev-parse','--verify',$ref],1669{ STDERR =>0}); };1670}16711672sub get_tree_from_treeish {1673my($treeish) =@_;1674# $treeish can be a symbolic ref, too:1675my$type= command_oneline(qw/cat-file -t/,$treeish);1676my$expected;1677while($typeeq'tag') {1678($treeish,$type) = command(qw/cat-file tag/,$treeish);1679}1680if($typeeq'commit') {1681$expected= (grep/^tree /, command(qw/cat-file commit/,1682$treeish))[0];1683($expected) = ($expected=~/^tree ($sha1)$/o);1684die"Unable to get tree from$treeish\n"unless$expected;1685}elsif($typeeq'tree') {1686$expected=$treeish;1687}else{1688die"$treeishis a$type, expected tree, tag or commit\n";1689}1690return$expected;1691}16921693sub get_commit_entry {1694my($treeish) =shift;1695my%log_entry= (log=>'', tree => get_tree_from_treeish($treeish) );1696my$commit_editmsg="$ENV{GIT_DIR}/COMMIT_EDITMSG";1697my$commit_msg="$ENV{GIT_DIR}/COMMIT_MSG";1698open my$log_fh,'>',$commit_editmsgor croak $!;16991700my$type= command_oneline(qw/cat-file -t/,$treeish);1701if($typeeq'commit'||$typeeq'tag') {1702my($msg_fh,$ctx) = command_output_pipe('cat-file',1703$type,$treeish);1704my$in_msg=0;1705my$author;1706my$saw_from=0;1707my$msgbuf="";1708while(<$msg_fh>) {1709if(!$in_msg) {1710$in_msg=1if(/^\s*$/);1711$author=$1if(/^author (.*>)/);1712}elsif(/^git-svn-id: /) {1713# skip this for now, we regenerate the1714# correct one on re-fetch anyways1715# TODO: set *:merge properties or like...1716}else{1717if(/^From:/||/^Signed-off-by:/) {1718$saw_from=1;1719}1720$msgbuf.=$_;1721}1722}1723$msgbuf=~s/\s+$//s;1724if($Git::SVN::_add_author_from &&defined($author)1725&& !$saw_from) {1726$msgbuf.="\n\nFrom:$author";1727}1728print$log_fh $msgbufor croak $!;1729 command_close_pipe($msg_fh,$ctx);1730}1731close$log_fhor croak $!;17321733if($_edit|| ($typeeq'tree')) {1734chomp(my$editor= command_oneline(qw(var GIT_EDITOR)));1735system('sh','-c',$editor.' "$@"',$editor,$commit_editmsg);1736}1737rename$commit_editmsg,$commit_msgor croak $!;1738{1739require Encode;1740# SVN requires messages to be UTF-8 when entering the repo1741local$/;1742open$log_fh,'<',$commit_msgor croak $!;1743binmode$log_fh;1744chomp($log_entry{log} = <$log_fh>);17451746my$enc= Git::config('i18n.commitencoding') ||'UTF-8';1747my$msg=$log_entry{log};17481749eval{$msg= Encode::decode($enc,$msg,1) };1750if($@) {1751die"Could not decode as$enc:\n",$msg,1752"\nPerhaps you need to set i18n.commitencoding\n";1753}17541755eval{$msg= Encode::encode('UTF-8',$msg,1) };1756die"Could not encode as UTF-8:\n$msg\n"if$@;17571758$log_entry{log} =$msg;17591760close$log_fhor croak $!;1761}1762unlink$commit_msg;1763 \%log_entry;1764}17651766sub s_to_file {1767my($str,$file,$mode) =@_;1768open my$fd,'>',$fileor croak $!;1769print$fd $str,"\n"or croak $!;1770close$fdor croak $!;1771chmod($mode&~umask,$file)if(defined$mode);1772}17731774sub file_to_s {1775my$file=shift;1776open my$fd,'<',$fileor croak "$!: file:$file\n";1777local$/;1778my$ret= <$fd>;1779close$fdor croak $!;1780$ret=~s/\s*$//s;1781return$ret;1782}17831784# '<svn username> = real-name <email address>' mapping based on git-svnimport:1785sub load_authors {1786open my$authors,'<',$_authorsor die"Can't open$_authors$!\n";1787my$log=$cmdeq'log';1788while(<$authors>) {1789chomp;1790next unless/^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;1791my($user,$name,$email) = ($1,$2,$3);1792if($log) {1793$Git::SVN::Log::rusers{"$name<$email>"} =$user;1794}else{1795$users{$user} = [$name,$email];1796}1797}1798close$authorsor croak $!;1799}18001801# convert GetOpt::Long specs for use by git-config1802sub read_git_config {1803my$opts=shift;1804my@config_only;1805foreachmy$o(keys%$opts) {1806# if we have mixedCase and a long option-only, then1807# it's a config-only variable that we don't need for1808# the command-line.1809push@config_only,$oif($o=~/[A-Z]/&&$o=~/^[a-z]+$/i);1810my$v=$opts->{$o};1811my($key) = ($o=~/^([a-zA-Z\-]+)/);1812$key=~s/-//g;1813my$arg='git config';1814$arg.=' --int'if($o=~/[:=]i$/);1815$arg.=' --bool'if($o!~/[:=][sfi]$/);1816if(ref$veq'ARRAY') {1817chomp(my@tmp=`$arg--get-all svn.$key`);1818@$v=@tmpif@tmp;1819 } else {1820 chomp(my$tmp= `$arg--get svn.$key`);1821if($tmp&& !($arg=~/ --bool/&&$tmpeq'false')) {1822$$v=$tmp;1823}1824}1825}1826delete@$opts{@config_only}if@config_only;1827}18281829sub extract_metadata {1830my$id=shift orreturn(undef,undef,undef);1831my($url,$rev,$uuid) = ($id=~ /^\s*git-svn-id:\s+(.*)\@(\d+)1832 \s([a-f\d\-]+)$/ix);1833if(!defined$rev|| !$uuid|| !$url) {1834# some of the original repositories I made had1835# identifiers like this:1836($rev,$uuid) = ($id=~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i);1837}1838return($url,$rev,$uuid);1839}18401841sub cmt_metadata {1842return extract_metadata((grep(/^git-svn-id: /,1843 command(qw/cat-file commit/,shift)))[-1]);1844}18451846sub cmt_sha2rev_batch {1847my%s2r;1848my($pid,$in,$out,$ctx) = command_bidi_pipe(qw/cat-file --batch/);1849my$list=shift;18501851foreachmy$sha(@{$list}) {1852my$first=1;1853my$size=0;1854print$out $sha,"\n";18551856while(my$line= <$in>) {1857if($first&&$line=~/^[[:xdigit:]]{40}\smissing$/) {1858last;1859}elsif($first&&1860$line=~/^[[:xdigit:]]{40}\scommit\s(\d+)$/) {1861$first=0;1862$size=$1;1863next;1864}elsif($line=~/^(git-svn-id: )/) {1865my(undef,$rev,undef) =1866 extract_metadata($line);1867$s2r{$sha} =$rev;1868}18691870$size-=length($line);1871last if($size==0);1872}1873}18741875 command_close_bidi_pipe($pid,$in,$out,$ctx);18761877return \%s2r;1878}18791880sub working_head_info {1881my($head,$refs) =@_;1882my@args= qw/log--no-color --no-decorate --first-parent1883--pretty=medium/;1884my($fh,$ctx) = command_output_pipe(@args,$head);1885my$hash;1886my%max;1887while(<$fh>) {1888if(m{^commit ($::sha1)$}) {1889unshift@$refs,$hashif$hashand$refs;1890$hash=$1;1891next;1892}1893next unlesss{^\s*(git-svn-id:)}{$1};1894my($url,$rev,$uuid) = extract_metadata($_);1895if(defined$url&&defined$rev) {1896next if$max{$url}and$max{$url} <$rev;1897if(my$gs= Git::SVN->find_by_url($url)) {1898my$c=$gs->rev_map_get($rev,$uuid);1899if($c&&$ceq$hash) {1900close$fh;# break the pipe1901return($url,$rev,$uuid,$gs);1902}else{1903$max{$url} ||=$gs->rev_map_max;1904}1905}1906}1907}1908 command_close_pipe($fh,$ctx);1909(undef,undef,undef,undef);1910}19111912sub read_commit_parents {1913my($parents,$c) =@_;1914chomp(my$p= command_oneline(qw/rev-list --parents -1/,$c));1915$p=~s/^($c)\s*//or die"rev-list --parents -1$cfailed!\n";1916@{$parents->{$c}} =split(/ /,$p);1917}19181919sub linearize_history {1920my($gs,$refs) =@_;1921my%parents;1922foreachmy$c(@$refs) {1923 read_commit_parents(\%parents,$c);1924}19251926my@linear_refs;1927my%skip= ();1928my$last_svn_commit=$gs->last_commit;1929foreachmy$c(reverse@$refs) {1930next if$ceq$last_svn_commit;1931last if$skip{$c};19321933unshift@linear_refs,$c;1934$skip{$c} =1;19351936# we only want the first parent to diff against for linear1937# history, we save the rest to inject when we finalize the1938# svn commit1939my$fp_a= verify_ref("$c~1");1940my$fp_b=shift@{$parents{$c}}if$parents{$c};1941if(!$fp_a|| !$fp_b) {1942die"Commit$c\n",1943"has no parent commit, and therefore ",1944"nothing to diff against.\n",1945"You should be working from a repository ",1946"originally created by git-svn\n";1947}1948if($fp_ane$fp_b) {1949die"$c~1=$fp_a, however parsing commit$c",1950"revealed that:\n$c~1=$fp_b\nBUG!\n";1951}19521953foreachmy$p(@{$parents{$c}}) {1954$skip{$p} =1;1955}1956}1957(\@linear_refs, \%parents);1958}19591960sub find_file_type_and_diff_status {1961my($path) =@_;1962return('dir','')if$patheq'';19631964my$diff_output=1965 command_oneline(qw(diff --cached --name-status --),$path) ||"";1966my$diff_status= (split(' ',$diff_output))[0] ||"";19671968my$ls_tree= command_oneline(qw(ls-tree HEAD),$path) ||"";19691970return(undef,undef)if!$diff_status&& !$ls_tree;19711972if($diff_statuseq"A") {1973return("link",$diff_status)if-l $path;1974return("dir",$diff_status)if-d $path;1975return("file",$diff_status);1976}19771978my$mode= (split(' ',$ls_tree))[0] ||"";19791980return("link",$diff_status)if$modeeq"120000";1981return("dir",$diff_status)if$modeeq"040000";1982return("file",$diff_status);1983}19841985sub md5sum {1986my$arg=shift;1987my$ref=ref$arg;1988my$md5= Digest::MD5->new();1989if($refeq'GLOB'||$refeq'IO::File'||$refeq'File::Temp') {1990$md5->addfile($arg)or croak $!;1991}elsif($refeq'SCALAR') {1992$md5->add($$arg)or croak $!;1993}elsif(!$ref) {1994$md5->add($arg)or croak $!;1995}else{1996::fatal "Can't provide MD5 hash for unknown ref type: '",$ref,"'";1997}1998return$md5->hexdigest();1999}20002001sub gc_directory {2002if($can_compress&& -f $_&& basename($_)eq"unhandled.log") {2003my$out_filename=$_.".gz";2004open my$in_fh,"<",$_or die"Unable to open$_:$!\n";2005binmode$in_fh;2006my$gz= Compress::Zlib::gzopen($out_filename,"ab")or2007die"Unable to open$out_filename:$!\n";20082009my$res;2010while($res=sysread($in_fh,my$str,1024)) {2011$gz->gzwrite($str)or2012die"Unable to write: ".$gz->gzerror()."!\n";2013}2014unlink$_or die"unlink$File::Find::name:$!\n";2015}elsif(-f $_&& basename($_)eq"index") {2016unlink$_or die"unlink$_:$!\n";2017}2018}20192020package Git::SVN;2021use strict;2022use warnings;2023use Fcntl qw/:DEFAULT :seek/;2024useconstant rev_map_fmt =>'NH40';2025use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent2026$_repack $_repack_flags $_use_svm_props $_head2027$_use_svnsync_props $no_reuse_existing $_minimize_url2028$_use_log_author $_add_author_from $_localtime/;2029use Carp qw/croak/;2030use File::Path qw/mkpath/;2031use File::Copy qw/copy/;2032use IPC::Open3;2033use Memoize;# core since 5.8.0, Jul 20022034use Memoize::Storable;20352036my($_gc_nr,$_gc_period);20372038# properties that we do not log:2039my%SKIP_PROP;2040BEGIN{2041%SKIP_PROP=map{$_=>1} qw/svn:wc:ra_dav:version-url2042 svn:special svn:executable2043 svn:entry:committed-rev2044 svn:entry:last-author2045 svn:entry:uuid2046 svn:entry:committed-date/;20472048# some options are read globally, but can be overridden locally2049# per [svn-remote "..."] section. Command-line options will *NOT*2050# override options set in an [svn-remote "..."] section2051no strict 'refs';2052formy$option(qw/follow_parent no_metadata use_svm_props2053 use_svnsync_props/) {2054my$key=$option;2055$key=~tr/_//d;2056my$prop="-$option";2057*$option=sub{2058my($self) =@_;2059return$self->{$prop}ifexists$self->{$prop};2060my$k="svn-remote.$self->{repo_id}.$key";2061eval{ command_oneline(qw/config --get/,$k) };2062if($@) {2063$self->{$prop} = ${"Git::SVN::_$option"};2064}else{2065my$v= command_oneline(qw/config --bool/,$k);2066$self->{$prop} =$veq'false'?0:1;2067}2068return$self->{$prop};2069}2070}2071}207220732074my(%LOCKFILES,%INDEX_FILES);2075END{2076unlink keys%LOCKFILESif%LOCKFILES;2077unlink keys%INDEX_FILESif%INDEX_FILES;2078}20792080sub resolve_local_globs {2081my($url,$fetch,$glob_spec) =@_;2082return unlessdefined$glob_spec;2083my$ref=$glob_spec->{ref};2084my$path=$glob_spec->{path};2085foreach(command(qw#for-each-ref --format=%(refname) refs/#)) {2086next unless m#^$ref->{regex}$#;2087my$p=$1;2088my$pathname= desanitize_refname($path->full_path($p));2089my$refname= desanitize_refname($ref->full_path($p));2090if(my$existing=$fetch->{$pathname}) {2091if($existingne$refname) {2092die"Refspec conflict:\n",2093"existing:$existing\n",2094" globbed:$refname\n";2095}2096my$u= (::cmt_metadata("$refname"))[0];2097$u=~s!^\Q$url\E(/|$)!!or die2098"$refname: '$url' not found in '$u'\n";2099if($pathnamene$u) {2100warn"W: Refspec glob conflict ",2101"(ref:$refname):\n",2102"expected path:$pathname\n",2103" real path:$u\n",2104"Continuing ahead with$u\n";2105next;2106}2107}else{2108$fetch->{$pathname} =$refname;2109}2110}2111}21122113sub parse_revision_argument {2114my($base,$head) =@_;2115if(!defined$::_revision || $::_revision eq'BASE:HEAD') {2116return($base,$head);2117}2118return($1,$2)if($::_revision =~/^(\d+):(\d+)$/);2119return($::_revision, $::_revision)if($::_revision =~/^\d+$/);2120return($head,$head)if($::_revision eq'HEAD');2121return($base,$1)if($::_revision =~/^BASE:(\d+)$/);2122return($1,$head)if($::_revision =~/^(\d+):HEAD$/);2123die"revision argument: $::_revision not understood by git-svn\n";2124}21252126sub fetch_all {2127my($repo_id,$remotes) =@_;2128if(ref$repo_id) {2129my$gs=$repo_id;2130$repo_id=undef;2131$repo_id=$gs->{repo_id};2132}2133$remotes||= read_all_remotes();2134my$remote=$remotes->{$repo_id}or2135die"[svn-remote\"$repo_id\"] unknown\n";2136my$fetch=$remote->{fetch};2137my$url=$remote->{url}or die"svn-remote.$repo_id.url not defined\n";2138my(@gs,@globs);2139my$ra= Git::SVN::Ra->new($url);2140my$uuid=$ra->get_uuid;2141my$head=$ra->get_latest_revnum;21422143# ignore errors, $head revision may not even exist anymore2144eval{$ra->get_log("",$head,0,1,0,1,sub{$head=$_[1] }) };2145warn"W:$@\n"if$@;21462147my$base=defined$fetch?$head:0;21482149# read the max revs for wildcard expansion (branches/*, tags/*)2150foreachmy$t(qw/branches tags/) {2151defined$remote->{$t}ornext;2152push@globs, @{$remote->{$t}};21532154my$max_rev=eval{ tmp_config(qw/--int --get/,2155"svn-remote.$repo_id.${t}-maxRev") };2156if(defined$max_rev&& ($max_rev<$base)) {2157$base=$max_rev;2158}elsif(!defined$max_rev) {2159$base=0;2160}2161}21622163if($fetch) {2164foreachmy$p(sort keys%$fetch) {2165my$gs= Git::SVN->new($fetch->{$p},$repo_id,$p);2166my$lr=$gs->rev_map_max;2167if(defined$lr) {2168$base=$lrif($lr<$base);2169}2170push@gs,$gs;2171}2172}21732174($base,$head) = parse_revision_argument($base,$head);2175$ra->gs_fetch_loop_common($base,$head, \@gs, \@globs);2176}21772178sub read_all_remotes {2179my$r= {};2180my$use_svm_props=eval{ command_oneline(qw/config --bool2181 svn.useSvmProps/) };2182$use_svm_props=$use_svm_propseq'true'if$use_svm_props;2183my$svn_refspec=qr{\s*(.*?)\s*:\s*(.+?)\s*};2184foreach(grep{s/^svn-remote\.//} command(qw/config -l/)) {2185if(m!^(.+)\.fetch=$svn_refspec$!) {2186my($remote,$local_ref,$remote_ref) = ($1,$2,$3);2187die("svn-remote.$remote: remote ref '$remote_ref' "2188."must start with 'refs/'\n")2189unless$remote_ref=~m{^refs/};2190$local_ref= uri_decode($local_ref);2191$r->{$remote}->{fetch}->{$local_ref} =$remote_ref;2192$r->{$remote}->{svm} = {}if$use_svm_props;2193}elsif(m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {2194$r->{$1}->{svm} = {};2195}elsif(m!^(.+)\.url=\s*(.*)\s*$!) {2196$r->{$1}->{url} =$2;2197}elsif(m!^(.+)\.pushurl=\s*(.*)\s*$!) {2198$r->{$1}->{pushurl} =$2;2199}elsif(m!^(.+)\.ignore-refs=\s*(.*)\s*$!) {2200$r->{$1}->{ignore_refs_regex} =$2;2201}elsif(m!^(.+)\.(branches|tags)=$svn_refspec$!) {2202my($remote,$t,$local_ref,$remote_ref) =2203($1,$2,$3,$4);2204die("svn-remote.$remote: remote ref '$remote_ref' ($t) "2205."must start with 'refs/'\n")2206unless$remote_ref=~m{^refs/};2207$local_ref= uri_decode($local_ref);2208my$rs= {2209 t =>$t,2210 remote =>$remote,2211 path => Git::SVN::GlobSpec->new($local_ref,1),2212ref=> Git::SVN::GlobSpec->new($remote_ref,0) };2213if(length($rs->{ref}->{right}) !=0) {2214die"The '*' glob character must be the last ",2215"character of '$remote_ref'\n";2216}2217push@{$r->{$remote}->{$t} },$rs;2218}2219}22202221map{2222if(defined$r->{$_}->{svm}) {2223my$svm;2224eval{2225my$section="svn-remote.$_";2226$svm= {2227 source => tmp_config('--get',2228"$section.svm-source"),2229 replace => tmp_config('--get',2230"$section.svm-replace"),2231}2232};2233$r->{$_}->{svm} =$svm;2234}2235}keys%$r;22362237foreachmy$remote(keys%$r) {2238foreach(grep{defined$_}2239map{$r->{$remote}->{$_} }qw(branches tags)) {2240foreachmy$rs(@$_) {2241$rs->{ignore_refs_regex} =2242$r->{$remote}->{ignore_refs_regex};2243}2244}2245}22462247$r;2248}22492250sub init_vars {2251$_gc_nr=$_gc_period=1000;2252if(defined$_repack||defined$_repack_flags) {2253warn"Repack options are obsolete; they have no effect.\n";2254}2255}22562257sub verify_remotes_sanity {2258return unless-d $ENV{GIT_DIR};2259my%seen;2260foreach(command(qw/config -l/)) {2261if(m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {2262if($seen{$1}) {2263die"Remote ref refs/remote/$1is tracked by",2264"\n \"$_\"\nand\n \"$seen{$1}\"\n",2265"Please resolve this ambiguity in ",2266"your git configuration file before ",2267"continuing\n";2268}2269$seen{$1} =$_;2270}2271}2272}22732274sub find_existing_remote {2275my($url,$remotes) =@_;2276returnundefif$no_reuse_existing;2277my$existing;2278foreachmy$repo_id(keys%$remotes) {2279my$u=$remotes->{$repo_id}->{url}ornext;2280next if$une$url;2281$existing=$repo_id;2282last;2283}2284$existing;2285}22862287sub init_remote_config {2288my($self,$url,$no_write) =@_;2289$url=~s!/+$!!;# strip trailing slash2290my$r= read_all_remotes();2291my$existing= find_existing_remote($url,$r);2292if($existing) {2293unless($no_write) {2294print STDERR "Using existing ",2295"[svn-remote\"$existing\"]\n";2296}2297$self->{repo_id} =$existing;2298}elsif($_minimize_url) {2299my$min_url= Git::SVN::Ra->new($url)->minimize_url;2300$existing= find_existing_remote($min_url,$r);2301if($existing) {2302unless($no_write) {2303print STDERR "Using existing ",2304"[svn-remote\"$existing\"]\n";2305}2306$self->{repo_id} =$existing;2307}2308if($min_urlne$url) {2309unless($no_write) {2310print STDERR "Using higher level of URL: ",2311"$url=>$min_url\n";2312}2313my$old_path=$self->{path};2314$self->{path} =$url;2315$self->{path} =~s!^\Q$min_url\E(/|$)!!;2316if(length$old_path) {2317$self->{path} .="/$old_path";2318}2319$url=$min_url;2320}2321}2322my$orig_url;2323if(!$existing) {2324# verify that we aren't overwriting anything:2325$orig_url=eval{2326 command_oneline('config','--get',2327"svn-remote.$self->{repo_id}.url")2328};2329if($orig_url&& ($orig_urlne$url)) {2330die"svn-remote.$self->{repo_id}.url already set: ",2331"$orig_url\nwanted to set to:$url\n";2332}2333}2334my($xrepo_id,$xpath) = find_ref($self->refname);2335if(!$no_write&&defined$xpath) {2336die"svn-remote.$xrepo_id.fetch already set to track ",2337"$xpath:",$self->refname,"\n";2338}2339unless($no_write) {2340 command_noisy('config',2341"svn-remote.$self->{repo_id}.url",$url);2342$self->{path} =~s{^/}{};2343$self->{path} =~s{%([0-9A-F]{2})}{chr hex($1)}ieg;2344 command_noisy('config','--add',2345"svn-remote.$self->{repo_id}.fetch",2346"$self->{path}:".$self->refname);2347}2348$self->{url} =$url;2349}23502351sub find_by_url {# repos_root and, path are optional2352my($class,$full_url,$repos_root,$path) =@_;23532354returnundefunlessdefined$full_url;2355 remove_username($full_url);2356 remove_username($repos_root)ifdefined$repos_root;2357my$remotes= read_all_remotes();2358if(defined$full_url&&defined$repos_root&& !defined$path) {2359$path=$full_url;2360$path=~ s#^\Q$repos_root\E(?:/|$)##;2361}2362foreachmy$repo_id(keys%$remotes) {2363my$u=$remotes->{$repo_id}->{url}ornext;2364 remove_username($u);2365next ifdefined$repos_root&&$repos_rootne$u;23662367my$fetch=$remotes->{$repo_id}->{fetch} || {};2368foreachmy$t(qw/branches tags/) {2369foreachmy$globspec(@{$remotes->{$repo_id}->{$t}}) {2370 resolve_local_globs($u,$fetch,$globspec);2371}2372}2373my$p=$path;2374my$rwr= rewrite_root({repo_id =>$repo_id});2375my$svm=$remotes->{$repo_id}->{svm}2376ifdefined$remotes->{$repo_id}->{svm};2377unless(defined$p) {2378$p=$full_url;2379my$z=$u;2380my$prefix='';2381if($rwr) {2382$z=$rwr;2383 remove_username($z);2384}elsif(defined$svm) {2385$z=$svm->{source};2386$prefix=$svm->{replace};2387$prefix=~ s#^\Q$u\E(?:/|$)##;2388$prefix=~ s#/$##;2389}2390$p=~ s#^\Q$z\E(?:/|$)#$prefix# or next;2391}2392foreachmy$f(keys%$fetch) {2393next if$fne$p;2394return Git::SVN->new($fetch->{$f},$repo_id,$f);2395}2396}2397undef;2398}23992400sub init {2401my($class,$url,$path,$repo_id,$ref_id,$no_write) =@_;2402my$self= _new($class,$repo_id,$ref_id,$path);2403if(defined$url) {2404$self->init_remote_config($url,$no_write);2405}2406$self;2407}24082409sub find_ref {2410my($ref_id) =@_;2411foreach(command(qw/config -l/)) {2412next unless m!^svn-remote\.(.+)\.fetch=2413 \s*(.*?)\s*:\s*(.+?)\s*$!x;2414my($repo_id,$path,$ref) = ($1,$2,$3);2415if($refeq$ref_id) {2416$path=''if($path=~ m#^\./?#);2417return($repo_id,$path);2418}2419}2420(undef,undef,undef);2421}24222423sub new {2424my($class,$ref_id,$repo_id,$path) =@_;2425if(defined$ref_id&& !defined$repo_id&& !defined$path) {2426($repo_id,$path) = find_ref($ref_id);2427if(!defined$repo_id) {2428die"Could not find a\"svn-remote.*.fetch\"key ",2429"in the repository configuration matching: ",2430"$ref_id\n";2431}2432}2433my$self= _new($class,$repo_id,$ref_id,$path);2434if(!defined$self->{path} || !length$self->{path}) {2435my$fetch= command_oneline('config','--get',2436"svn-remote.$repo_id.fetch",2437":$ref_id\$")or2438die"Failed to read\"svn-remote.$repo_id.fetch\"",2439"\":$ref_id\$\"in config\n";2440($self->{path},undef) =split(/\s*:\s*/,$fetch);2441}2442$self->{path} =~s{/+}{/}g;2443$self->{path} =~s{\A/}{};2444$self->{path} =~s{/\z}{};2445$self->{url} = command_oneline('config','--get',2446"svn-remote.$repo_id.url")or2447die"Failed to read\"svn-remote.$repo_id.url\"in config\n";2448$self->{pushurl} =eval{ command_oneline('config','--get',2449"svn-remote.$repo_id.pushurl") };2450$self->rebuild;2451$self;2452}24532454sub refname {2455my($refname) =$_[0]->{ref_id} ;24562457# It cannot end with a slash /, we'll throw up on this because2458# SVN can't have directories with a slash in their name, either:2459if($refname=~m{/$}) {2460die"ref: '$refname' ends with a trailing slash, this is ",2461"not permitted by git nor Subversion\n";2462}24632464# It cannot have ASCII control character space, tilde ~, caret ^,2465# colon :, question-mark ?, asterisk *, space, or open bracket [2466# anywhere.2467#2468# Additionally, % must be escaped because it is used for escaping2469# and we want our escaped refname to be reversible2470$refname=~s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;24712472# no slash-separated component can begin with a dot .2473# /.* becomes /%2E*2474$refname=~s{/\.}{/%2E}g;24752476# It cannot have two consecutive dots .. anywhere2477# .. becomes %2E%2E2478$refname=~s{\.\.}{%2E%2E}g;24792480# trailing dots and .lock are not allowed2481# .$ becomes %2E and .lock becomes %2Elock2482$refname=~s{\.(?=$|lock$)}{%2E};24832484# the sequence @{ is used to access the reflog2485# @{ becomes %40{2486$refname=~s{\@\{}{%40\{}g;24872488return$refname;2489}24902491sub desanitize_refname {2492my($refname) =@_;2493$refname=~s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;2494return$refname;2495}24962497sub svm_uuid {2498my($self) =@_;2499return$self->{svm}->{uuid}if$self->svm;2500$self->ra;2501unless($self->{svm}) {2502die"SVM UUID not cached, and reading remotely failed\n";2503}2504$self->{svm}->{uuid};2505}25062507sub svm {2508my($self) =@_;2509return$self->{svm}if$self->{svm};2510my$svm;2511# see if we have it in our config, first:2512eval{2513my$section="svn-remote.$self->{repo_id}";2514$svm= {2515 source => tmp_config('--get',"$section.svm-source"),2516 uuid => tmp_config('--get',"$section.svm-uuid"),2517 replace => tmp_config('--get',"$section.svm-replace"),2518}2519};2520if($svm&&$svm->{source} &&$svm->{uuid} &&$svm->{replace}) {2521$self->{svm} =$svm;2522}2523$self->{svm};2524}25252526sub _set_svm_vars {2527my($self,$ra) =@_;2528return$raif$self->svm;25292530my@err= ("useSvmProps set, but failed to read SVM properties\n",2531"(svm:source, svm:uuid) ",2532"from the following URLs:\n");2533sub read_svm_props {2534my($self,$ra,$path,$r) =@_;2535my$props= ($ra->get_dir($path,$r))[2];2536my$src=$props->{'svm:source'};2537my$uuid=$props->{'svm:uuid'};2538returnundefif(!$src|| !$uuid);25392540chomp($src,$uuid);25412542$uuid=~m{^[0-9a-f\-]{30,}$}i2543or die"doesn't look right - svm:uuid is '$uuid'\n";25442545# the '!' is used to mark the repos_root!/relative/path2546$src=~s{/?!/?}{/};2547$src=~s{/+$}{};# no trailing slashes please2548# username is of no interest2549$src=~s{(^[a-z\+]*://)[^/@]*@}{$1};25502551my$replace=$ra->{url};2552$replace.="/$path"iflength$path;25532554my$section="svn-remote.$self->{repo_id}";2555 tmp_config("$section.svm-source",$src);2556 tmp_config("$section.svm-replace",$replace);2557 tmp_config("$section.svm-uuid",$uuid);2558$self->{svm} = {2559 source =>$src,2560 uuid =>$uuid,2561 replace =>$replace2562};2563}25642565my$r=$ra->get_latest_revnum;2566my$path=$self->{path};2567my%tried;2568while(length$path) {2569unless($tried{"$self->{url}/$path"}) {2570return$raif$self->read_svm_props($ra,$path,$r);2571$tried{"$self->{url}/$path"} =1;2572}2573$path=~ s#/?[^/]+$##;2574}2575die"Path: '$path' should be ''\n"if$pathne'';2576return$raif$self->read_svm_props($ra,$path,$r);2577$tried{"$self->{url}/$path"} =1;25782579if($ra->{repos_root}eq$self->{url}) {2580die@err, (map{"$_\n"}keys%tried),"\n";2581}25822583# nope, make sure we're connected to the repository root:2584my$ok;2585my@tried_b;2586$path=$ra->{svn_path};2587$ra= Git::SVN::Ra->new($ra->{repos_root});2588while(length$path) {2589unless($tried{"$ra->{url}/$path"}) {2590$ok=$self->read_svm_props($ra,$path,$r);2591last if$ok;2592$tried{"$ra->{url}/$path"} =1;2593}2594$path=~ s#/?[^/]+$##;2595}2596die"Path: '$path' should be ''\n"if$pathne'';2597$ok||=$self->read_svm_props($ra,$path,$r);2598$tried{"$ra->{url}/$path"} =1;2599if(!$ok) {2600die@err, (map{"$_\n"}keys%tried),"\n";2601}2602 Git::SVN::Ra->new($self->{url});2603}26042605sub svnsync {2606my($self) =@_;2607return$self->{svnsync}if$self->{svnsync};26082609if($self->no_metadata) {2610die"Can't have both 'noMetadata' and ",2611"'useSvnsyncProps' options set!\n";2612}2613if($self->rewrite_root) {2614die"Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",2615"options set!\n";2616}2617if($self->rewrite_uuid) {2618die"Can't have both 'useSvnsyncProps' and 'rewriteUUID' ",2619"options set!\n";2620}26212622my$svnsync;2623# see if we have it in our config, first:2624eval{2625my$section="svn-remote.$self->{repo_id}";26262627my$url= tmp_config('--get',"$section.svnsync-url");2628($url) = ($url=~m{^([a-z\+]+://\S+)$})or2629die"doesn't look right - svn:sync-from-url is '$url'\n";26302631my$uuid= tmp_config('--get',"$section.svnsync-uuid");2632($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2633die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26342635$svnsync= { url =>$url, uuid =>$uuid}2636};2637if($svnsync&&$svnsync->{url} &&$svnsync->{uuid}) {2638return$self->{svnsync} =$svnsync;2639}26402641my$err="useSvnsyncProps set, but failed to read ".2642"svnsync property: svn:sync-from-";2643my$rp=$self->ra->rev_proplist(0);26442645my$url=$rp->{'svn:sync-from-url'}or die$err."url\n";2646($url) = ($url=~m{^([a-z\+]+://\S+)$})or2647die"doesn't look right - svn:sync-from-url is '$url'\n";26482649my$uuid=$rp->{'svn:sync-from-uuid'}or die$err."uuid\n";2650($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2651die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26522653my$section="svn-remote.$self->{repo_id}";2654 tmp_config('--add',"$section.svnsync-uuid",$uuid);2655 tmp_config('--add',"$section.svnsync-url",$url);2656return$self->{svnsync} = { url =>$url, uuid =>$uuid};2657}26582659# this allows us to memoize our SVN::Ra UUID locally and avoid a2660# remote lookup (useful for 'git svn log').2661sub ra_uuid {2662my($self) =@_;2663unless($self->{ra_uuid}) {2664my$key="svn-remote.$self->{repo_id}.uuid";2665my$uuid=eval{ tmp_config('--get',$key) };2666if(!$@&&$uuid&&$uuid=~/^([a-f\d\-]{30,})$/i) {2667$self->{ra_uuid} =$uuid;2668}else{2669die"ra_uuid called without URL\n"unless$self->{url};2670$self->{ra_uuid} =$self->ra->get_uuid;2671 tmp_config('--add',$key,$self->{ra_uuid});2672}2673}2674$self->{ra_uuid};2675}26762677sub _set_repos_root {2678my($self,$repos_root) =@_;2679my$k="svn-remote.$self->{repo_id}.reposRoot";2680$repos_root||=$self->ra->{repos_root};2681 tmp_config($k,$repos_root);2682$repos_root;2683}26842685sub repos_root {2686my($self) =@_;2687my$k="svn-remote.$self->{repo_id}.reposRoot";2688eval{ tmp_config('--get',$k) } ||$self->_set_repos_root;2689}26902691sub ra {2692my($self) =shift;2693my$ra= Git::SVN::Ra->new($self->{url});2694$self->_set_repos_root($ra->{repos_root});2695if($self->use_svm_props&& !$self->{svm}) {2696if($self->no_metadata) {2697die"Can't have both 'noMetadata' and ",2698"'useSvmProps' options set!\n";2699}elsif($self->use_svnsync_props) {2700die"Can't have both 'useSvnsyncProps' and ",2701"'useSvmProps' options set!\n";2702}2703$ra=$self->_set_svm_vars($ra);2704$self->{-want_revprops} =1;2705}2706$ra;2707}27082709# prop_walk(PATH, REV, SUB)2710# -------------------------2711# Recursively traverse PATH at revision REV and invoke SUB for each2712# directory that contains a SVN property. SUB will be invoked as2713# follows: &SUB(gs, path, props); where `gs' is this instance of2714# Git::SVN, `path' the path to the directory where the properties2715# `props' were found. The `path' will be relative to point of checkout,2716# that is, if url://repo/trunk is the current Git branch, and that2717# directory contains a sub-directory `d', SUB will be invoked with `/d/'2718# as `path' (note the trailing `/').2719sub prop_walk {2720my($self,$path,$rev,$sub) =@_;27212722$path=~ s#^/##;2723my($dirent,undef,$props) =$self->ra->get_dir($path,$rev);2724$path=~ s#^/*#/#g;2725my$p=$path;2726# Strip the irrelevant part of the path.2727$p=~ s#^/+\Q$self->{path}\E(/|$)#/#;2728# Ensure the path is terminated by a `/'.2729$p=~ s#/*$#/#;27302731# The properties contain all the internal SVN stuff nobody2732# (usually) cares about.2733my$interesting_props=0;2734foreach(keys%{$props}) {2735# If it doesn't start with `svn:', it must be a2736# user-defined property.2737++$interesting_propsandnext if$_!~/^svn:/;2738# FIXME: Fragile, if SVN adds new public properties,2739# this needs to be updated.2740++$interesting_propsif/^svn:(?:ignore|keywords|executable2741|eol-style|mime-type2742|externals|needs-lock)$/x;2743}2744&$sub($self,$p,$props)if$interesting_props;27452746foreach(sort keys%$dirent) {2747next if$dirent->{$_}->{kind} !=$SVN::Node::dir;2748$self->prop_walk($self->{path} .$p.$_,$rev,$sub);2749}2750}27512752sub last_rev { ($_[0]->last_rev_commit)[0] }2753sub last_commit { ($_[0]->last_rev_commit)[1] }27542755# returns the newest SVN revision number and newest commit SHA12756sub last_rev_commit {2757my($self) =@_;2758if(defined$self->{last_rev} &&defined$self->{last_commit}) {2759return($self->{last_rev},$self->{last_commit});2760}2761my$c= ::verify_ref($self->refname.'^0');2762if($c&& !$self->use_svm_props&& !$self->no_metadata) {2763my$rev= (::cmt_metadata($c))[1];2764if(defined$rev) {2765($self->{last_rev},$self->{last_commit}) = ($rev,$c);2766return($rev,$c);2767}2768}2769my$map_path=$self->map_path;2770unless(-e $map_path) {2771($self->{last_rev},$self->{last_commit}) = (undef,undef);2772return(undef,undef);2773}2774my($rev,$commit) =$self->rev_map_max(1);2775($self->{last_rev},$self->{last_commit}) = ($rev,$commit);2776return($rev,$commit);2777}27782779sub get_fetch_range {2780my($self,$min,$max) =@_;2781$max||=$self->ra->get_latest_revnum;2782$min||=$self->rev_map_max;2783(++$min,$max);2784}27852786sub tmp_config {2787my(@args) =@_;2788my$old_def_config="$ENV{GIT_DIR}/svn/config";2789my$config="$ENV{GIT_DIR}/svn/.metadata";2790if(! -f $config&& -f $old_def_config) {2791rename$old_def_config,$configor2792die"Failed rename$old_def_config=>$config:$!\n";2793}2794my$old_config=$ENV{GIT_CONFIG};2795$ENV{GIT_CONFIG} =$config;2796$@=undef;2797my@ret=eval{2798unless(-f $config) {2799 mkfile($config);2800open my$fh,'>',$configor2801die"Can't open$config:$!\n";2802print$fh"; This file is used internally by ",2803"git-svn\n"or die2804"Couldn't write to$config:$!\n";2805print$fh"; You should not have to edit it\n"or2806die"Couldn't write to$config:$!\n";2807close$fhor die"Couldn't close$config:$!\n";2808}2809 command('config',@args);2810};2811my$err=$@;2812if(defined$old_config) {2813$ENV{GIT_CONFIG} =$old_config;2814}else{2815delete$ENV{GIT_CONFIG};2816}2817die$errif$err;2818wantarray?@ret:$ret[0];2819}28202821sub tmp_index_do {2822my($self,$sub) =@_;2823my$old_index=$ENV{GIT_INDEX_FILE};2824$ENV{GIT_INDEX_FILE} =$self->{index};2825$@=undef;2826my@ret=eval{2827my($dir,$base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);2828 mkpath([$dir])unless-d $dir;2829&$sub;2830};2831my$err=$@;2832if(defined$old_index) {2833$ENV{GIT_INDEX_FILE} =$old_index;2834}else{2835delete$ENV{GIT_INDEX_FILE};2836}2837die$errif$err;2838wantarray?@ret:$ret[0];2839}28402841sub assert_index_clean {2842my($self,$treeish) =@_;28432844$self->tmp_index_do(sub{2845 command_noisy('read-tree',$treeish)unless-e $self->{index};2846my$x= command_oneline('write-tree');2847my($y) = (command(qw/cat-file commit/,$treeish) =~2848/^tree ($::sha1)/mo);2849return if$yeq$x;28502851warn"Index mismatch:$y!=$x\nrereading$treeish\n";2852unlink$self->{index}or die"unlink$self->{index}:$!\n";2853 command_noisy('read-tree',$treeish);2854$x= command_oneline('write-tree');2855if($yne$x) {2856::fatal "trees ($treeish)$y!=$x\n",2857"Something is seriously wrong...";2858}2859});2860}28612862sub get_commit_parents {2863my($self,$log_entry) =@_;2864my(%seen,@ret,@tmp);2865# legacy support for 'set-tree'; this is only used by set_tree_cb:2866if(my$ip=$self->{inject_parents}) {2867if(my$commit=delete$ip->{$log_entry->{revision}}) {2868push@tmp,$commit;2869}2870}2871if(my$cur= ::verify_ref($self->refname.'^0')) {2872push@tmp,$cur;2873}2874if(my$ipd=$self->{inject_parents_dcommit}) {2875if(my$commit=delete$ipd->{$log_entry->{revision}}) {2876push@tmp,@$commit;2877}2878}2879push@tmp,$_foreach(@{$log_entry->{parents}},@tmp);2880while(my$p=shift@tmp) {2881next if$seen{$p};2882$seen{$p} =1;2883push@ret,$p;2884}2885@ret;2886}28872888sub rewrite_root {2889my($self) =@_;2890return$self->{-rewrite_root}ifexists$self->{-rewrite_root};2891my$k="svn-remote.$self->{repo_id}.rewriteRoot";2892my$rwr=eval{ command_oneline(qw/config --get/,$k) };2893if($rwr) {2894$rwr=~ s#/+$##;2895if($rwr!~ m#^[a-z\+]+://#) {2896die"$rwris not a valid URL (key:$k)\n";2897}2898}2899$self->{-rewrite_root} =$rwr;2900}29012902sub rewrite_uuid {2903my($self) =@_;2904return$self->{-rewrite_uuid}ifexists$self->{-rewrite_uuid};2905my$k="svn-remote.$self->{repo_id}.rewriteUUID";2906my$rwid=eval{ command_oneline(qw/config --get/,$k) };2907if($rwid) {2908$rwid=~ s#/+$##;2909if($rwid!~ m#^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$#) {2910die"$rwidis not a valid UUID (key:$k)\n";2911}2912}2913$self->{-rewrite_uuid} =$rwid;2914}29152916sub metadata_url {2917my($self) =@_;2918($self->rewrite_root||$self->{url}) .2919(length$self->{path} ?'/'.$self->{path} :'');2920}29212922sub full_url {2923my($self) =@_;2924$self->{url} . (length$self->{path} ?'/'.$self->{path} :'');2925}29262927sub full_pushurl {2928my($self) =@_;2929if($self->{pushurl}) {2930return$self->{pushurl} . (length$self->{path} ?'/'.2931$self->{path} :'');2932}else{2933return$self->full_url;2934}2935}29362937sub set_commit_header_env {2938my($log_entry) =@_;2939my%env;2940foreachmy$ned(qw/NAME EMAIL DATE/) {2941foreachmy$ac(qw/AUTHOR COMMITTER/) {2942$env{"GIT_${ac}_${ned}"} =$ENV{"GIT_${ac}_${ned}"};2943}2944}29452946$ENV{GIT_AUTHOR_NAME} =$log_entry->{name};2947$ENV{GIT_AUTHOR_EMAIL} =$log_entry->{email};2948$ENV{GIT_AUTHOR_DATE} =$ENV{GIT_COMMITTER_DATE} =$log_entry->{date};29492950$ENV{GIT_COMMITTER_NAME} = (defined$log_entry->{commit_name})2951?$log_entry->{commit_name}2952:$log_entry->{name};2953$ENV{GIT_COMMITTER_EMAIL} = (defined$log_entry->{commit_email})2954?$log_entry->{commit_email}2955:$log_entry->{email};2956 \%env;2957}29582959sub restore_commit_header_env {2960my($env) =@_;2961foreachmy$ned(qw/NAME EMAIL DATE/) {2962foreachmy$ac(qw/AUTHOR COMMITTER/) {2963my$k="GIT_${ac}_${ned}";2964if(defined$env->{$k}) {2965$ENV{$k} =$env->{$k};2966}else{2967delete$ENV{$k};2968}2969}2970}2971}29722973sub gc {2974 command_noisy('gc','--auto');2975};29762977sub do_git_commit {2978my($self,$log_entry) =@_;2979my$lr=$self->last_rev;2980if(defined$lr&&$lr>=$log_entry->{revision}) {2981die"Last fetched revision of ",$self->refname,2982" was r$lr, but we are about to fetch: ",2983"r$log_entry->{revision}!\n";2984}2985if(my$c=$self->rev_map_get($log_entry->{revision})) {2986 croak "$log_entry->{revision} =$calready exists! ",2987"Why are we refetching it?\n";2988}2989my$old_env= set_commit_header_env($log_entry);2990my$tree=$log_entry->{tree};2991if(!defined$tree) {2992$tree=$self->tmp_index_do(sub{2993 command_oneline('write-tree') });2994}2995die"Tree is not a valid sha1:$tree\n"if$tree!~/^$::sha1$/o;29962997my@exec= ('git','commit-tree',$tree);2998foreach($self->get_commit_parents($log_entry)) {2999push@exec,'-p',$_;3000}3001defined(my$pid= open3(my$msg_fh,my$out_fh,'>&STDERR',@exec))3002or croak $!;3003binmode$msg_fh;30043005# we always get UTF-8 from SVN, but we may want our commits in3006# a different encoding.3007if(my$enc= Git::config('i18n.commitencoding')) {3008require Encode;3009 Encode::from_to($log_entry->{log},'UTF-8',$enc);3010}3011print$msg_fh $log_entry->{log}or croak $!;3012 restore_commit_header_env($old_env);3013unless($self->no_metadata) {3014print$msg_fh"\ngit-svn-id:$log_entry->{metadata}\n"3015or croak $!;3016}3017$msg_fh->flush==0or croak $!;3018close$msg_fhor croak $!;3019chomp(my$commit=do{local$/; <$out_fh> });3020close$out_fhor croak $!;3021waitpid$pid,0;3022 croak $?if$?;3023if($commit!~/^$::sha1$/o) {3024die"Failed to commit, invalid sha1:$commit\n";3025}30263027$self->rev_map_set($log_entry->{revision},$commit,1);30283029$self->{last_rev} =$log_entry->{revision};3030$self->{last_commit} =$commit;3031print"r$log_entry->{revision}"unless$::_q >1;3032if(defined$log_entry->{svm_revision}) {3033print" (\@$log_entry->{svm_revision})"unless$::_q >1;3034$self->rev_map_set($log_entry->{svm_revision},$commit,30350,$self->svm_uuid);3036}3037print" =$commit($self->{ref_id})\n"unless$::_q >1;3038if(--$_gc_nr==0) {3039$_gc_nr=$_gc_period;3040 gc();3041}3042return$commit;3043}30443045sub match_paths {3046my($self,$paths,$r) =@_;3047return1if$self->{path}eq'';3048if(my$path=$paths->{"/$self->{path}"}) {3049return($path->{action}eq'D') ?0:1;3050}3051$self->{path_regex} ||=qr/^\/\Q$self->{path}\E\//;3052if(grep/$self->{path_regex}/,keys%$paths) {3053return1;3054}3055my$c='';3056foreach(split m#/#, $self->{path}) {3057$c.="/$_";3058next unless($paths->{$c} &&3059($paths->{$c}->{action} =~/^[AR]$/));3060if($self->ra->check_path($self->{path},$r) ==3061$SVN::Node::dir) {3062return1;3063}3064}3065return0;3066}30673068sub find_parent_branch {3069my($self,$paths,$rev) =@_;3070returnundefunless$self->follow_parent;3071unless(defined$paths) {3072my$err_handler=$SVN::Error::handler;3073$SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;3074$self->ra->get_log([$self->{path}],$rev,$rev,0,1,1,3075sub{$paths=$_[0] });3076$SVN::Error::handler =$err_handler;3077}3078returnundefunlessdefined$paths;30793080# look for a parent from another branch:3081my@b_path_components=split m#/#, $self->{path};3082my@a_path_components;3083my$i;3084while(@b_path_components) {3085$i=$paths->{'/'.join('/',@b_path_components)};3086last if$i&&defined$i->{copyfrom_path};3087unshift(@a_path_components,pop(@b_path_components));3088}3089returnundefunlessdefined$i&&defined$i->{copyfrom_path};3090my$branch_from=$i->{copyfrom_path};3091if(@a_path_components) {3092print STDERR "branch_from:$branch_from=> ";3093$branch_from.='/'.join('/',@a_path_components);3094print STDERR $branch_from,"\n";3095}3096my$r=$i->{copyfrom_rev};3097my$repos_root=$self->ra->{repos_root};3098my$url=$self->ra->{url};3099my$new_url=$url.$branch_from;3100print STDERR "Found possible branch point: ",3101"$new_url=> ",$self->full_url,",$r\n"3102unless$::_q >1;3103$branch_from=~ s#^/##;3104my$gs=$self->other_gs($new_url,$url,3105$branch_from,$r,$self->{ref_id});3106my($r0,$parent) =$gs->find_rev_before($r,1);3107{3108my($base,$head);3109if(!defined$r0|| !defined$parent) {3110($base,$head) = parse_revision_argument(0,$r);3111}else{3112if($r0<$r) {3113$gs->ra->get_log([$gs->{path}],$r0+1,$r,1,31140,1,sub{$base=$_[1] -1});3115}3116}3117if(defined$base&&$base<=$r) {3118$gs->fetch($base,$r);3119}3120($r0,$parent) =$gs->find_rev_before($r,1);3121}3122if(defined$r0&&defined$parent) {3123print STDERR "Found branch parent: ($self->{ref_id})$parent\n"3124unless$::_q >1;3125my$ed;3126if($self->ra->can_do_switch) {3127$self->assert_index_clean($parent);3128print STDERR "Following parent with do_switch\n"3129unless$::_q >1;3130# do_switch works with svn/trunk >= r22312, but that3131# is not included with SVN 1.4.3 (the latest version3132# at the moment), so we can't rely on it3133$self->{last_rev} =$r0;3134$self->{last_commit} =$parent;3135$ed= SVN::Git::Fetcher->new($self,$gs->{path});3136$gs->ra->gs_do_switch($r0,$rev,$gs,3137$self->full_url,$ed)3138or die"SVN connection failed somewhere...\n";3139}elsif($self->ra->trees_match($new_url,$r0,3140$self->full_url,$rev)) {3141print STDERR "Trees match:\n",3142"$new_url\@$r0\n",3143" ${\$self->full_url}\@$rev\n",3144"Following parent with no changes\n"3145unless$::_q >1;3146$self->tmp_index_do(sub{3147 command_noisy('read-tree',$parent);3148});3149$self->{last_commit} =$parent;3150}else{3151print STDERR "Following parent with do_update\n"3152unless$::_q >1;3153$ed= SVN::Git::Fetcher->new($self);3154$self->ra->gs_do_update($rev,$rev,$self,$ed)3155or die"SVN connection failed somewhere...\n";3156}3157print STDERR "Successfully followed parent\n"unless$::_q >1;3158return$self->make_log_entry($rev, [$parent],$ed);3159}3160returnundef;3161}31623163sub do_fetch {3164my($self,$paths,$rev) =@_;3165my$ed;3166my($last_rev,@parents);3167if(my$lc=$self->last_commit) {3168# we can have a branch that was deleted, then re-added3169# under the same name but copied from another path, in3170# which case we'll have multiple parents (we don't3171# want to break the original ref, nor lose copypath info):3172if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3173push@{$log_entry->{parents}},$lc;3174return$log_entry;3175}3176$ed= SVN::Git::Fetcher->new($self);3177$last_rev=$self->{last_rev};3178$ed->{c} =$lc;3179@parents= ($lc);3180}else{3181$last_rev=$rev;3182if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3183return$log_entry;3184}3185$ed= SVN::Git::Fetcher->new($self);3186}3187unless($self->ra->gs_do_update($last_rev,$rev,$self,$ed)) {3188die"SVN connection failed somewhere...\n";3189}3190$self->make_log_entry($rev, \@parents,$ed);3191}31923193sub mkemptydirs {3194my($self,$r) =@_;31953196sub scan {3197my($r,$empty_dirs,$line) =@_;3198if(defined$r&&$line=~/^r(\d+)$/) {3199return0if$1>$r;3200}elsif($line=~/^ \+empty_dir: (.+)$/) {3201$empty_dirs->{$1} =1;3202}elsif($line=~/^ \-empty_dir: (.+)$/) {3203my@d=grep{m[^\Q$1\E(/|$)]} (keys%$empty_dirs);3204delete@$empty_dirs{@d};3205}32061;# continue3207};32083209my%empty_dirs= ();3210my$gz_file="$self->{dir}/unhandled.log.gz";3211if(-f $gz_file) {3212if(!$can_compress) {3213warn"Compress::Zlib could not be found; ",3214"empty directories in$gz_filewill not be read\n";3215}else{3216my$gz= Compress::Zlib::gzopen($gz_file,"rb")or3217die"Unable to open$gz_file:$!\n";3218my$line;3219while($gz->gzreadline($line) >0) {3220 scan($r, \%empty_dirs,$line)orlast;3221}3222$gz->gzclose;3223}3224}32253226if(open my$fh,'<',"$self->{dir}/unhandled.log") {3227binmode$fhor croak "binmode:$!";3228while(<$fh>) {3229 scan($r, \%empty_dirs,$_)orlast;3230}3231close$fh;3232}32333234my$strip=qr/\A\Q$self->{path}\E(?:\/|$)/;3235foreachmy$d(sort keys%empty_dirs) {3236$d= uri_decode($d);3237$d=~s/$strip//;3238next unlesslength($d);3239next if-d $d;3240if(-e $d) {3241warn"$dexists but is not a directory\n";3242}else{3243print"creating empty directory:$d\n";3244 mkpath([$d]);3245}3246}3247}32483249sub get_untracked {3250my($self,$ed) =@_;3251my@out;3252my$h=$ed->{empty};3253foreach(sort keys%$h) {3254my$act=$h->{$_} ?'+empty_dir':'-empty_dir';3255push@out,"$act: ". uri_encode($_);3256warn"W:$act:$_\n";3257}3258foreachmy$t(qw/dir_prop file_prop/) {3259$h=$ed->{$t}ornext;3260foreachmy$path(sort keys%$h) {3261my$ppath=$patheq''?'.':$path;3262foreachmy$prop(sort keys%{$h->{$path}}) {3263next if$SKIP_PROP{$prop};3264my$v=$h->{$path}->{$prop};3265my$t_ppath_prop="$t: ".3266 uri_encode($ppath) .' '.3267 uri_encode($prop);3268if(defined$v) {3269push@out," +$t_ppath_prop".3270 uri_encode($v);3271}else{3272push@out," -$t_ppath_prop";3273}3274}3275}3276}3277foreachmy$t(qw/absent_file absent_directory/) {3278$h=$ed->{$t}ornext;3279foreachmy$parent(sort keys%$h) {3280foreachmy$path(sort@{$h->{$parent}}) {3281push@out,"$t: ".3282 uri_encode("$parent/$path");3283warn"W:$t:$parent/$path",3284"Insufficient permissions?\n";3285}3286}3287}3288 \@out;3289}32903291# parse_svn_date(DATE)3292# --------------------3293# Given a date (in UTC) from Subversion, return a string in the format3294# "<TZ Offset> <local date/time>" that Git will use.3295#3296# By default the parsed date will be in UTC; if $Git::SVN::_localtime3297# is true we'll convert it to the local timezone instead.3298sub parse_svn_date {3299my$date=shift||return'+0000 1970-01-01 00:00:00';3300my($Y,$m,$d,$H,$M,$S) = ($date=~ /^(\d{4})\-(\d\d)\-(\d\d)T3301(\d\d)\:(\d\d)\:(\d\d)\.\d*Z$/x)or3302 croak "Unable to parse date:$date\n";3303my$parsed_date;# Set next.33043305if($Git::SVN::_localtime) {3306# Translate the Subversion datetime to an epoch time.3307# Begin by switching ourselves to $date's timezone, UTC.3308my$old_env_TZ=$ENV{TZ};3309$ENV{TZ} ='UTC';33103311my$epoch_in_UTC=3312 POSIX::strftime('%s',$S,$M,$H,$d,$m-1,$Y-1900);33133314# Determine our local timezone (including DST) at the3315# time of $epoch_in_UTC. $Git::SVN::Log::TZ stored the3316# value of TZ, if any, at the time we were run.3317if(defined$Git::SVN::Log::TZ) {3318$ENV{TZ} =$Git::SVN::Log::TZ;3319}else{3320delete$ENV{TZ};3321}33223323my$our_TZ=3324 POSIX::strftime('%Z',$S,$M,$H,$d,$m-1,$Y-1900);33253326# This converts $epoch_in_UTC into our local timezone.3327my($sec,$min,$hour,$mday,$mon,$year,3328$wday,$yday,$isdst) =localtime($epoch_in_UTC);33293330$parsed_date=sprintf('%s%04d-%02d-%02d%02d:%02d:%02d',3331$our_TZ,$year+1900,$mon+1,3332$mday,$hour,$min,$sec);33333334# Reset us to the timezone in effect when we entered3335# this routine.3336if(defined$old_env_TZ) {3337$ENV{TZ} =$old_env_TZ;3338}else{3339delete$ENV{TZ};3340}3341}else{3342$parsed_date="+0000$Y-$m-$d$H:$M:$S";3343}33443345return$parsed_date;3346}33473348sub other_gs {3349my($self,$new_url,$url,3350$branch_from,$r,$old_ref_id) =@_;3351my$gs= Git::SVN->find_by_url($new_url,$url,$branch_from);3352unless($gs) {3353my$ref_id=$old_ref_id;3354$ref_id=~s/\@\d+-*$//;3355$ref_id.="\@$r";3356# just grow a tail if we're not unique enough :x3357$ref_id.='-'while find_ref($ref_id);3358my($u,$p,$repo_id) = ($new_url,'',$ref_id);3359if($u=~ s#^\Q$url\E(/|$)##) {3360$p=$u;3361$u=$url;3362$repo_id=$self->{repo_id};3363}3364while(1) {3365# It is possible to tag two different subdirectories at3366# the same revision. If the url for an existing ref3367# does not match, we must either find a ref with a3368# matching url or create a new ref by growing a tail.3369$gs= Git::SVN->init($u,$p,$repo_id,$ref_id,1);3370my(undef,$max_commit) =$gs->rev_map_max(1);3371last if(!$max_commit);3372my($url) = ::cmt_metadata($max_commit);3373last if($urleq$gs->metadata_url);3374$ref_id.='-';3375}3376print STDERR "Initializing parent:$ref_id\n"unless$::_q >1;3377}3378$gs3379}33803381sub call_authors_prog {3382my($orig_author) =@_;3383$orig_author= command_oneline('rev-parse','--sq-quote',$orig_author);3384my$author=`$::_authors_prog$orig_author`;3385 if ($?!= 0) {3386 die "$::_authors_prog failed with exit code$?\n"3387 }3388 if ($author=~ /^\s*(.+?)\s*<(.*)>\s*$/) {3389 my ($name,$email) = ($1,$2);3390$email= undef if length$2== 0;3391 return [$name,$email];3392 } else {3393 die "Author:$orig_author: $::_authors_prog returned "3394 . "invalid author format:$author\n";3395 }3396}33973398sub check_author {3399 my ($author) =@_;3400 if (!defined$author|| length$author== 0) {3401$author= '(no author)';3402 }3403 if (!defined $::users{$author}) {3404 if (defined $::_authors_prog) {3405 $::users{$author} = call_authors_prog($author);3406 } elsif (defined $::_authors) {3407 die "Author:$authornot defined in $::_authors file\n";3408 }3409 }3410$author;3411}34123413sub find_extra_svk_parents {3414 my ($self,$ed,$tickets,$parents) =@_;3415 # aha! svk:merge property changed...3416 my@tickets= split "\n",$tickets;3417 my@known_parents;3418 for my$ticket(@tickets) {3419 my ($uuid,$path,$rev) = split /:/,$ticket;3420 if ($uuideq$self->ra_uuid ) {3421 my$url=$self->{url};3422 my$repos_root=$url;3423 my$branch_from=$path;3424$branch_from=~ s{^/}{};3425 my$gs=$self->other_gs($repos_root."/".$branch_from,3426$url,3427$branch_from,3428$rev,3429$self->{ref_id});3430 if ( my$commit=$gs->rev_map_get($rev,$uuid) ) {3431 # wahey! we found it, but it might be3432 # an old one (!)3433 push@known_parents, [$rev,$commit];3434 }3435 }3436 }3437 # Ordering matters; highest-numbered commit merge tickets3438 # first, as they may account for later merge ticket additions3439 # or changes.3440@known_parents= map {$_->[1]} sort {$b->[0] <=>$a->[0]}@known_parents;3441 for my$parent(@known_parents) {3442 my@cmd= ('rev-list',$parent, map { "^$_" }@$parents);3443 my ($msg_fh,$ctx) = command_output_pipe(@cmd);3444 my$new;3445 while ( <$msg_fh> ) {3446$new=1;last;3447 }3448 command_close_pipe($msg_fh,$ctx);3449 if ($new) {3450 print STDERR3451 "Found merge parent (svk:merge ticket):$parent\n";3452 push@$parents,$parent;3453 }3454 }3455}34563457sub lookup_svn_merge {3458 my$uuid= shift;3459 my$url= shift;3460 my$merge= shift;34613462 my ($source,$revs) = split ":",$merge;3463 my$path=$source;3464$path=~ s{^/}{};3465 my$gs= Git::SVN->find_by_url($url.$source,$url,$path);3466 if ( !$gs) {3467 warn "Couldn't find revmap for$url$source\n";3468 return;3469 }3470 my@ranges= split ",",$revs;3471 my ($tip,$tip_commit);3472 my@merged_commit_ranges;3473 # find the tip3474 for my$range(@ranges) {3475 my ($bottom,$top) = split "-",$range;3476$top||=$bottom;3477 my$bottom_commit=$gs->find_rev_after($bottom, 1,$top);3478 my$top_commit=$gs->find_rev_before($top, 1,$bottom);34793480 unless ($top_commitand$bottom_commit) {3481 warn "W:unknown path/rev in svn:mergeinfo "3482 ."dirprop:$source:$range\n";3483 next;3484 }34853486 if (scalar(command('rev-parse', "$bottom_commit^@"))) {3487 push@merged_commit_ranges,3488 "$bottom_commit^..$top_commit";3489 } else {3490 push@merged_commit_ranges, "$top_commit";3491 }34923493 if ( !defined$tipor$top>$tip) {3494$tip=$top;3495$tip_commit=$top_commit;3496 }3497 }3498 return ($tip_commit,@merged_commit_ranges);3499}35003501sub _rev_list {3502 my ($msg_fh,$ctx) = command_output_pipe(3503 "rev-list",@_,3504 );3505 my@rv;3506 while ( <$msg_fh> ) {3507 chomp;3508 push@rv,$_;3509 }3510 command_close_pipe($msg_fh,$ctx);3511@rv;3512}35133514sub check_cherry_pick {3515 my$base= shift;3516 my$tip= shift;3517 my$parents= shift;3518 my@ranges=@_;3519 my%commits= map {$_=> 1 }3520 _rev_list("--no-merges",$tip, "--not",$base,@$parents, "--");3521 for my$range(@ranges) {3522 delete@commits{_rev_list($range, "--")};3523 }3524 for my$commit(keys%commits) {3525 if (has_no_changes($commit)) {3526 delete$commits{$commit};3527 }3528 }3529 return (keys%commits);3530}35313532sub has_no_changes {3533 my$commit= shift;35343535 my@revs= split / /, command_oneline(3536 qw(rev-list --parents -1 -m),$commit);35373538# Commits with no parents, e.g. the start of a partial branch,3539# have changes by definition.3540return1if(@revs<2);35413542# Commits with multiple parents, e.g a merge, have no changes3543# by definition.3544return0if(@revs>2);35453546return(command_oneline("rev-parse","$commit^{tree}")eq3547 command_oneline("rev-parse","$commit~1^{tree}"));3548}35493550# The GIT_DIR environment variable is not always set until after the command3551# line arguments are processed, so we can't memoize in a BEGIN block.3552{3553my$memoized=0;35543555sub memoize_svn_mergeinfo_functions {3556return if$memoized;3557$memoized=1;35583559my$cache_path="$ENV{GIT_DIR}/svn/.caches/";3560 mkpath([$cache_path])unless-d $cache_path;35613562 tie my%lookup_svn_merge_cache=>'Memoize::Storable',3563"$cache_path/lookup_svn_merge.db",'nstore';3564 memoize 'lookup_svn_merge',3565 SCALAR_CACHE =>'FAULT',3566 LIST_CACHE => ['HASH'=> \%lookup_svn_merge_cache],3567;35683569 tie my%check_cherry_pick_cache=>'Memoize::Storable',3570"$cache_path/check_cherry_pick.db",'nstore';3571 memoize 'check_cherry_pick',3572 SCALAR_CACHE =>'FAULT',3573 LIST_CACHE => ['HASH'=> \%check_cherry_pick_cache],3574;35753576 tie my%has_no_changes_cache=>'Memoize::Storable',3577"$cache_path/has_no_changes.db",'nstore';3578 memoize 'has_no_changes',3579 SCALAR_CACHE => ['HASH'=> \%has_no_changes_cache],3580 LIST_CACHE =>'FAULT',3581;3582}35833584sub unmemoize_svn_mergeinfo_functions {3585return ifnot$memoized;3586$memoized=0;35873588 Memoize::unmemoize 'lookup_svn_merge';3589 Memoize::unmemoize 'check_cherry_pick';3590 Memoize::unmemoize 'has_no_changes';3591}35923593 Memoize::memoize 'Git::SVN::repos_root';3594}35953596END{3597# Force cache writeout explicitly instead of waiting for3598# global destruction to avoid segfault in Storable:3599# http://rt.cpan.org/Public/Bug/Display.html?id=360873600 unmemoize_svn_mergeinfo_functions();3601}36023603sub parents_exclude {3604my$parents=shift;3605my@commits=@_;3606return unless@commits;36073608my@excluded;3609my$excluded;3610do{3611my@cmd= ('rev-list',"-1",@commits,"--not",@$parents);3612$excluded= command_oneline(@cmd);3613if($excluded) {3614my@new;3615my$found;3616formy$commit(@commits) {3617if($commiteq$excluded) {3618push@excluded,$commit;3619$found++;3620last;3621}3622else{3623push@new,$commit;3624}3625}3626die"saw commit '$excluded' in rev-list output, "3627."but we didn't ask for that commit (wanted:@commits--not@$parents)"3628unless$found;3629@commits=@new;3630}3631}3632while($excludedand@commits);36333634return@excluded;3635}363636373638# note: this function should only be called if the various dirprops3639# have actually changed3640sub find_extra_svn_parents {3641my($self,$ed,$mergeinfo,$parents) =@_;3642# aha! svk:merge property changed...36433644 memoize_svn_mergeinfo_functions();36453646# We first search for merged tips which are not in our3647# history. Then, we figure out which git revisions are in3648# that tip, but not this revision. If all of those revisions3649# are now marked as merge, we can add the tip as a parent.3650my@merges=split"\n",$mergeinfo;3651my@merge_tips;3652my$url=$self->{url};3653my$uuid=$self->ra_uuid;3654my%ranges;3655formy$merge(@merges) {3656my($tip_commit,@ranges) =3657 lookup_svn_merge($uuid,$url,$merge);3658unless(!$tip_commitor3659grep{$_eq$tip_commit}@$parents) {3660push@merge_tips,$tip_commit;3661$ranges{$tip_commit} = \@ranges;3662}else{3663push@merge_tips,undef;3664}3665}36663667my%excluded=map{$_=>1}3668 parents_exclude($parents,grep{defined}@merge_tips);36693670# check merge tips for new parents3671my@new_parents;3672formy$merge_tip(@merge_tips) {3673my$spec=shift@merges;3674next unless$merge_tipand$excluded{$merge_tip};36753676my$ranges=$ranges{$merge_tip};36773678# check out 'new' tips3679my$merge_base;3680eval{3681$merge_base= command_oneline(3682"merge-base",3683@$parents,$merge_tip,3684);3685};3686if($@) {3687die"An error occurred during merge-base"3688unless$@->isa("Git::Error::Command");36893690warn"W: Cannot find common ancestor between ".3691"@$parentsand$merge_tip. Ignoring merge info.\n";3692next;3693}36943695# double check that there are no missing non-merge commits3696my(@incomplete) = check_cherry_pick(3697$merge_base,$merge_tip,3698$parents,3699@$ranges,3700);37013702if(@incomplete) {3703warn"W:svn cherry-pick ignored ($spec) - missing "3704.@incomplete." commit(s) (eg$incomplete[0])\n";3705}else{3706warn3707"Found merge parent (svn:mergeinfo prop): ",3708$merge_tip,"\n";3709push@new_parents,$merge_tip;3710}3711}37123713# cater for merges which merge commits from multiple branches3714if(@new_parents>1) {3715for(my$i=0;$i<=$#new_parents;$i++) {3716for(my$j=0;$j<=$#new_parents;$j++) {3717next if$i==$j;3718next unless$new_parents[$i];3719next unless$new_parents[$j];3720my$revs= command_oneline(3721"rev-list","-1",3722"$new_parents[$i]..$new_parents[$j]",3723);3724if( !$revs) {3725undef($new_parents[$j]);3726}3727}3728}3729}3730push@$parents,grep{defined}@new_parents;3731}37323733sub make_log_entry {3734my($self,$rev,$parents,$ed) =@_;3735my$untracked=$self->get_untracked($ed);37363737my@parents=@$parents;3738my$ps=$ed->{path_strip} ||"";3739formy$path(grep{m/$ps/} %{$ed->{dir_prop}} ) {3740my$props=$ed->{dir_prop}{$path};3741if($props->{"svk:merge"} ) {3742$self->find_extra_svk_parents3743($ed,$props->{"svk:merge"}, \@parents);3744}3745if($props->{"svn:mergeinfo"} ) {3746$self->find_extra_svn_parents3747($ed,3748$props->{"svn:mergeinfo"},3749 \@parents);3750}3751}37523753open my$un,'>>',"$self->{dir}/unhandled.log"or croak $!;3754print$un"r$rev\n"or croak $!;3755print$un $_,"\n"foreach@$untracked;3756my%log_entry= ( parents => \@parents, revision =>$rev,3757log=>'');37583759my$headrev;3760my$logged=delete$self->{logged_rev_props};3761if(!$logged||$self->{-want_revprops}) {3762my$rp=$self->ra->rev_proplist($rev);3763foreach(sort keys%$rp) {3764my$v=$rp->{$_};3765if(/^svn:(author|date|log)$/) {3766$log_entry{$1} =$v;3767}elsif($_eq'svm:headrev') {3768$headrev=$v;3769}else{3770print$un" rev_prop: ", uri_encode($_),' ',3771 uri_encode($v),"\n";3772}3773}3774}else{3775map{$log_entry{$_} =$logged->{$_} }keys%$logged;3776}3777close$unor croak $!;37783779$log_entry{date} = parse_svn_date($log_entry{date});3780$log_entry{log} .="\n";3781my$author=$log_entry{author} = check_author($log_entry{author});3782my($name,$email) =defined$::users{$author} ? @{$::users{$author}}3783: ($author,undef);37843785my($commit_name,$commit_email) = ($name,$email);3786if($_use_log_author) {3787my$name_field;3788if($log_entry{log} =~/From:\s+(.*\S)\s*\n/i) {3789$name_field=$1;3790}elsif($log_entry{log} =~/Signed-off-by:\s+(.*\S)\s*\n/i) {3791$name_field=$1;3792}3793if(!defined$name_field) {3794if(!defined$email) {3795$email=$name;3796}3797}elsif($name_field=~/(.*?)\s+<(.*)>/) {3798($name,$email) = ($1,$2);3799}elsif($name_field=~/(.*)@/) {3800($name,$email) = ($1,$name_field);3801}else{3802($name,$email) = ($name_field,$name_field);3803}3804}3805if(defined$headrev&&$self->use_svm_props) {3806if($self->rewrite_root) {3807die"Can't have both 'useSvmProps' and 'rewriteRoot' ",3808"options set!\n";3809}3810if($self->rewrite_uuid) {3811die"Can't have both 'useSvmProps' and 'rewriteUUID' ",3812"options set!\n";3813}3814my($uuid,$r) =$headrev=~m{^([a-f\d\-]{30,}):(\d+)$}i;3815# we don't want "SVM: initializing mirror for junk" ...3816returnundefif$r==0;3817my$svm=$self->svm;3818if($uuidne$svm->{uuid}) {3819die"UUID mismatch on SVM path:\n",3820"expected:$svm->{uuid}\n",3821" got:$uuid\n";3822}3823my$full_url=$self->full_url;3824$full_url=~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or3825die"Failed to replace '$svm->{replace}' with ",3826"'$svm->{source}' in$full_url\n";3827# throw away username for storing in records3828 remove_username($full_url);3829$log_entry{metadata} ="$full_url\@$r$uuid";3830$log_entry{svm_revision} =$r;3831$email||="$author\@$uuid";3832$commit_email||="$author\@$uuid";3833}elsif($self->use_svnsync_props) {3834my$full_url=$self->svnsync->{url};3835$full_url.="/$self->{path}"iflength$self->{path};3836 remove_username($full_url);3837my$uuid=$self->svnsync->{uuid};3838$log_entry{metadata} ="$full_url\@$rev$uuid";3839$email||="$author\@$uuid";3840$commit_email||="$author\@$uuid";3841}else{3842my$url=$self->metadata_url;3843 remove_username($url);3844my$uuid=$self->rewrite_uuid||$self->ra->get_uuid;3845$log_entry{metadata} ="$url\@$rev".$uuid;3846$email||="$author\@".$uuid;3847$commit_email||="$author\@".$uuid;3848}3849$log_entry{name} =$name;3850$log_entry{email} =$email;3851$log_entry{commit_name} =$commit_name;3852$log_entry{commit_email} =$commit_email;3853 \%log_entry;3854}38553856sub fetch {3857my($self,$min_rev,$max_rev,@parents) =@_;3858my($last_rev,$last_commit) =$self->last_rev_commit;3859my($base,$head) =$self->get_fetch_range($min_rev,$max_rev);3860$self->ra->gs_fetch_loop_common($base,$head, [$self]);3861}38623863sub set_tree_cb {3864my($self,$log_entry,$tree,$rev,$date,$author) =@_;3865$self->{inject_parents} = {$rev=>$tree};3866$self->fetch(undef,undef);3867}38683869sub set_tree {3870my($self,$tree) = (shift,shift);3871my$log_entry= ::get_commit_entry($tree);3872unless($self->{last_rev}) {3873::fatal("Must have an existing revision to commit");3874}3875my%ed_opts= ( r =>$self->{last_rev},3876log=>$log_entry->{log},3877 ra =>$self->ra,3878 tree_a =>$self->{last_commit},3879 tree_b =>$tree,3880 editor_cb =>sub{3881$self->set_tree_cb($log_entry,$tree,@_) },3882 svn_path =>$self->{path} );3883if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {3884print"No changes\nr$self->{last_rev} =$tree\n";3885}3886}38873888sub rebuild_from_rev_db {3889my($self,$path) =@_;3890my$r= -1;3891open my$fh,'<',$pathor croak "open:$!";3892binmode$fhor croak "binmode:$!";3893while(<$fh>) {3894length($_) ==41or croak "inconsistent size in ($_) != 41";3895chomp($_);3896++$r;3897next if$_eq('0' x 40);3898$self->rev_map_set($r,$_);3899print"r$r=$_\n";3900}3901close$fhor croak "close:$!";3902unlink$pathor croak "unlink:$!";3903}39043905sub rebuild {3906my($self) =@_;3907my$map_path=$self->map_path;3908my$partial= (-e $map_path&& ! -z $map_path);3909return unless::verify_ref($self->refname.'^0');3910if(!$partial&& ($self->use_svm_props||$self->no_metadata)) {3911my$rev_db=$self->rev_db_path;3912$self->rebuild_from_rev_db($rev_db);3913if($self->use_svm_props) {3914my$svm_rev_db=$self->rev_db_path($self->svm_uuid);3915$self->rebuild_from_rev_db($svm_rev_db);3916}3917$self->unlink_rev_db_symlink;3918return;3919}3920print"Rebuilding$map_path...\n"if(!$partial);3921my($base_rev,$head) = ($partial?$self->rev_map_max_norebuild(1) :3922(undef,undef));3923my($log,$ctx) =3924 command_output_pipe(qw/rev-list --pretty=raw --no-color --reverse/,3925($head?"$head..":"") .$self->refname,3926'--');3927my$metadata_url=$self->metadata_url;3928 remove_username($metadata_url);3929my$svn_uuid=$self->rewrite_uuid||$self->ra_uuid;3930my$c;3931while(<$log>) {3932if(m{^commit ($::sha1)$}) {3933$c=$1;3934next;3935}3936next unlesss{^\s*(git-svn-id:)}{$1};3937my($url,$rev,$uuid) = ::extract_metadata($_);3938 remove_username($url);39393940# ignore merges (from set-tree)3941next if(!defined$rev|| !$uuid);39423943# if we merged or otherwise started elsewhere, this is3944# how we break out of it3945if(($uuidne$svn_uuid) ||3946($metadata_url&&$url&& ($urlne$metadata_url))) {3947next;3948}3949if($partial&&$head) {3950print"Partial-rebuilding$map_path...\n";3951print"Currently at$base_rev=$head\n";3952$head=undef;3953}39543955$self->rev_map_set($rev,$c);3956print"r$rev=$c\n";3957}3958 command_close_pipe($log,$ctx);3959print"Done rebuilding$map_path\n"if(!$partial|| !$head);3960my$rev_db_path=$self->rev_db_path;3961if(-f $self->rev_db_path) {3962unlink$self->rev_db_pathor croak "unlink:$!";3963}3964$self->unlink_rev_db_symlink;3965}39663967# rev_map:3968# Tie::File seems to be prone to offset errors if revisions get sparse,3969# it's not that fast, either. Tie::File is also not in Perl 5.6. So3970# one of my favorite modules is out :< Next up would be one of the DBM3971# modules, but I'm not sure which is most portable...3972#3973# This is the replacement for the rev_db format, which was too big3974# and inefficient for large repositories with a lot of sparse history3975# (mainly tags)3976#3977# The format is this:3978# - 24 bytes for every record,3979# * 4 bytes for the integer representing an SVN revision number3980# * 20 bytes representing the sha1 of a git commit3981# - No empty padding records like the old format3982# (except the last record, which can be overwritten)3983# - new records are written append-only since SVN revision numbers3984# increase monotonically3985# - lookups on SVN revision number are done via a binary search3986# - Piping the file to xxd -c24 is a good way of dumping it for3987# viewing or editing (piped back through xxd -r), should the need3988# ever arise.3989# - The last record can be padding revision with an all-zero sha13990# This is used to optimize fetch performance when using multiple3991# "fetch" directives in .git/config3992#3993# These files are disposable unless noMetadata or useSvmProps is set39943995sub _rev_map_set {3996my($fh,$rev,$commit) =@_;39973998binmode$fhor croak "binmode:$!";3999my$size= (stat($fh))[7];4000($size%24) ==0or croak "inconsistent size:$size";40014002my$wr_offset=0;4003if($size>0) {4004sysseek($fh, -24, SEEK_END)or croak "seek:$!";4005my$read=sysread($fh,my$buf,24)or croak "read:$!";4006$read==24or croak "read only$readbytes (!= 24)";4007my($last_rev,$last_commit) =unpack(rev_map_fmt,$buf);4008if($last_commiteq('0' x40)) {4009if($size>=48) {4010sysseek($fh, -48, SEEK_END)or croak "seek:$!";4011$read=sysread($fh,$buf,24)or4012 croak "read:$!";4013$read==24or4014 croak "read only$readbytes (!= 24)";4015($last_rev,$last_commit) =4016unpack(rev_map_fmt,$buf);4017if($last_commiteq('0' x40)) {4018 croak "inconsistent .rev_map\n";4019}4020}4021if($last_rev>=$rev) {4022 croak "last_rev is higher!:$last_rev>=$rev";4023}4024$wr_offset= -24;4025}4026}4027sysseek($fh,$wr_offset, SEEK_END)or croak "seek:$!";4028syswrite($fh,pack(rev_map_fmt,$rev,$commit),24) ==24or4029 croak "write:$!";4030}40314032sub _rev_map_reset {4033my($fh,$rev,$commit) =@_;4034my$c= _rev_map_get($fh,$rev);4035$ceq$commitor die"_rev_map_reset(@_) commit$cdoes not match!\n";4036my$offset=sysseek($fh,0, SEEK_CUR)or croak "seek:$!";4037truncate$fh,$offsetor croak "truncate:$!";4038}40394040sub mkfile {4041my($path) =@_;4042unless(-e $path) {4043my($dir,$base) = ($path=~ m#^(.*?)/?([^/]+)$#);4044 mkpath([$dir])unless-d $dir;4045open my$fh,'>>',$pathor die"Couldn't create$path:$!\n";4046close$fhor die"Couldn't close (create)$path:$!\n";4047}4048}40494050sub rev_map_set {4051my($self,$rev,$commit,$update_ref,$uuid) =@_;4052defined$commitor die"missing arg3\n";4053length$commit==40or die"arg3 must be a full SHA1 hexsum\n";4054my$db=$self->map_path($uuid);4055my$db_lock="$db.lock";4056my$sig;4057$update_ref||=0;4058if($update_ref) {4059$SIG{INT} =$SIG{HUP} =$SIG{TERM} =$SIG{ALRM} =$SIG{PIPE} =4060$SIG{USR1} =$SIG{USR2} =sub{$sig=$_[0] };4061}4062 mkfile($db);40634064$LOCKFILES{$db_lock} =1;4065my$sync;4066# both of these options make our .rev_db file very, very important4067# and we can't afford to lose it because rebuild() won't work4068if($self->use_svm_props||$self->no_metadata) {4069$sync=1;4070 copy($db,$db_lock)or die"rev_map_set(@_): ",4071"Failed to copy: ",4072"$db=>$db_lock($!)\n";4073}else{4074rename$db,$db_lockor die"rev_map_set(@_): ",4075"Failed to rename: ",4076"$db=>$db_lock($!)\n";4077}40784079sysopen(my$fh,$db_lock, O_RDWR | O_CREAT)4080or croak "Couldn't open$db_lock:$!\n";4081$update_refeq'reset'? _rev_map_reset($fh,$rev,$commit) :4082 _rev_map_set($fh,$rev,$commit);4083if($sync) {4084$fh->flushor die"Couldn't flush$db_lock:$!\n";4085$fh->syncor die"Couldn't sync$db_lock:$!\n";4086}4087close$fhor croak $!;4088if($update_ref) {4089$_head=$self;4090my$note="";4091$note=" ($update_ref)"if($update_ref!~/^\d*$/);4092 command_noisy('update-ref','-m',"r$rev$note",4093$self->refname,$commit);4094}4095rename$db_lock,$dbor die"rev_map_set(@_): ","Failed to rename: ",4096"$db_lock=>$db($!)\n";4097delete$LOCKFILES{$db_lock};4098if($update_ref) {4099$SIG{INT} =$SIG{HUP} =$SIG{TERM} =$SIG{ALRM} =$SIG{PIPE} =4100$SIG{USR1} =$SIG{USR2} ='DEFAULT';4101kill$sig,$$ifdefined$sig;4102}4103}41044105# If want_commit, this will return an array of (rev, commit) where4106# commit _must_ be a valid commit in the archive.4107# Otherwise, it'll return the max revision (whether or not the4108# commit is valid or just a 0x40 placeholder).4109sub rev_map_max {4110my($self,$want_commit) =@_;4111$self->rebuild;4112my($r,$c) =$self->rev_map_max_norebuild($want_commit);4113$want_commit? ($r,$c) :$r;4114}41154116sub rev_map_max_norebuild {4117my($self,$want_commit) =@_;4118my$map_path=$self->map_path;4119stat$map_pathorreturn$want_commit? (0,undef) :0;4120sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4121binmode$fhor croak "binmode:$!";4122my$size= (stat($fh))[7];4123($size%24) ==0or croak "inconsistent size:$size";41244125if($size==0) {4126close$fhor croak "close:$!";4127return$want_commit? (0,undef) :0;4128}41294130sysseek($fh, -24, SEEK_END)or croak "seek:$!";4131sysread($fh,my$buf,24) ==24or croak "read:$!";4132my($r,$c) =unpack(rev_map_fmt,$buf);4133if($want_commit&&$ceq('0' x40)) {4134if($size<48) {4135return$want_commit? (0,undef) :0;4136}4137sysseek($fh, -48, SEEK_END)or croak "seek:$!";4138sysread($fh,$buf,24) ==24or croak "read:$!";4139($r,$c) =unpack(rev_map_fmt,$buf);4140if($ceq('0'x40)) {4141 croak "Penultimate record is all-zeroes in$map_path";4142}4143}4144close$fhor croak "close:$!";4145$want_commit? ($r,$c) :$r;4146}41474148sub rev_map_get {4149my($self,$rev,$uuid) =@_;4150my$map_path=$self->map_path($uuid);4151returnundefunless-e $map_path;41524153sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4154my$c= _rev_map_get($fh,$rev);4155close($fh)or croak "close:$!";4156$c4157}41584159sub _rev_map_get {4160my($fh,$rev) =@_;41614162binmode$fhor croak "binmode:$!";4163my$size= (stat($fh))[7];4164($size%24) ==0or croak "inconsistent size:$size";41654166if($size==0) {4167returnundef;4168}41694170my($l,$u) = (0,$size-24);4171my($r,$c,$buf);41724173while($l<=$u) {4174my$i=int(($l/24+$u/24) /2) *24;4175sysseek($fh,$i, SEEK_SET)or croak "seek:$!";4176sysread($fh,my$buf,24) ==24or croak "read:$!";4177my($r,$c) =unpack(rev_map_fmt,$buf);41784179if($r<$rev) {4180$l=$i+24;4181}elsif($r>$rev) {4182$u=$i-24;4183}else{# $r == $rev4184return$ceq('0' x 40) ?undef:$c;4185}4186}4187undef;4188}41894190# Finds the first svn revision that exists on (if $eq_ok is true) or4191# before $rev for the current branch. It will not search any lower4192# than $min_rev. Returns the git commit hash and svn revision number4193# if found, else (undef, undef).4194sub find_rev_before {4195my($self,$rev,$eq_ok,$min_rev) =@_;4196--$revunless$eq_ok;4197$min_rev||=1;4198my$max_rev=$self->rev_map_max;4199$rev=$max_revif($rev>$max_rev);4200while($rev>=$min_rev) {4201if(my$c=$self->rev_map_get($rev)) {4202return($rev,$c);4203}4204--$rev;4205}4206return(undef,undef);4207}42084209# Finds the first svn revision that exists on (if $eq_ok is true) or4210# after $rev for the current branch. It will not search any higher4211# than $max_rev. Returns the git commit hash and svn revision number4212# if found, else (undef, undef).4213sub find_rev_after {4214my($self,$rev,$eq_ok,$max_rev) =@_;4215++$revunless$eq_ok;4216$max_rev||=$self->rev_map_max;4217while($rev<=$max_rev) {4218if(my$c=$self->rev_map_get($rev)) {4219return($rev,$c);4220}4221++$rev;4222}4223return(undef,undef);4224}42254226sub _new {4227my($class,$repo_id,$ref_id,$path) =@_;4228unless(defined$repo_id&&length$repo_id) {4229$repo_id=$Git::SVN::default_repo_id;4230}4231unless(defined$ref_id&&length$ref_id) {4232$_prefix=''unlessdefined($_prefix);4233$_[2] =$ref_id=4234"refs/remotes/$_prefix$Git::SVN::default_ref_id";4235}4236$_[1] =$repo_id;4237my$dir="$ENV{GIT_DIR}/svn/$ref_id";42384239# Older repos imported by us used $GIT_DIR/svn/foo instead of4240# $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo4241if($ref_id=~m{^refs/remotes/(.*)}) {4242my$old_dir="$ENV{GIT_DIR}/svn/$1";4243if(-d $old_dir&& ! -d $dir) {4244$dir=$old_dir;4245}4246}42474248$_[3] =$path=''unless(defined$path);4249 mkpath([$dir]);4250bless{4251 ref_id =>$ref_id, dir =>$dir,index=>"$dir/index",4252 path =>$path, config =>"$ENV{GIT_DIR}/svn/config",4253 map_root =>"$dir/.rev_map", repo_id =>$repo_id},$class;4254}42554256# for read-only access of old .rev_db formats4257sub unlink_rev_db_symlink {4258my($self) =@_;4259my$link=$self->rev_db_path;4260$link=~s/\.[\w-]+$//or croak "missing UUID at the end of$link";4261if(-l $link) {4262unlink$linkor croak "unlink:$linkfailed!";4263}4264}42654266sub rev_db_path {4267my($self,$uuid) =@_;4268my$db_path=$self->map_path($uuid);4269$db_path=~s{/\.rev_map\.}{/\.rev_db\.}4270or croak "map_path:$db_pathdoes not contain '/.rev_map.' !";4271$db_path;4272}42734274# the new replacement for .rev_db4275sub map_path {4276my($self,$uuid) =@_;4277$uuid||=$self->ra_uuid;4278"$self->{map_root}.$uuid";4279}42804281sub uri_encode {4282my($f) =@_;4283$f=~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;4284$f4285}42864287sub uri_decode {4288my($f) =@_;4289$f=~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;4290$f4291}42924293sub remove_username {4294$_[0] =~s{^([^:]*://)[^@]+@}{$1};4295}42964297package Git::SVN::Prompt;4298use strict;4299use warnings;4300require SVN::Core;4301use vars qw/$_no_auth_cache $_username/;43024303sub simple {4304my($cred,$realm,$default_username,$may_save,$pool) =@_;4305$may_save=undefif$_no_auth_cache;4306$default_username=$_usernameifdefined$_username;4307if(defined$default_username&&length$default_username) {4308if(defined$realm&&length$realm) {4309print STDERR "Authentication realm:$realm\n";4310 STDERR->flush;4311}4312$cred->username($default_username);4313}else{4314 username($cred,$realm,$may_save,$pool);4315}4316$cred->password(_read_password("Password for '".4317$cred->username."': ",$realm));4318$cred->may_save($may_save);4319$SVN::_Core::SVN_NO_ERROR;4320}43214322sub ssl_server_trust {4323my($cred,$realm,$failures,$cert_info,$may_save,$pool) =@_;4324$may_save=undefif$_no_auth_cache;4325print STDERR "Error validating server certificate for '$realm':\n";4326{4327no warnings 'once';4328# All variables SVN::Auth::SSL::* are used only once,4329# so we're shutting up Perl warnings about this.4330if($failures&$SVN::Auth::SSL::UNKNOWNCA) {4331print STDERR " - The certificate is not issued ",4332"by a trusted authority. Use the\n",4333" fingerprint to validate ",4334"the certificate manually!\n";4335}4336if($failures&$SVN::Auth::SSL::CNMISMATCH) {4337print STDERR " - The certificate hostname ",4338"does not match.\n";4339}4340if($failures&$SVN::Auth::SSL::NOTYETVALID) {4341print STDERR " - The certificate is not yet valid.\n";4342}4343if($failures&$SVN::Auth::SSL::EXPIRED) {4344print STDERR " - The certificate has expired.\n";4345}4346if($failures&$SVN::Auth::SSL::OTHER) {4347print STDERR " - The certificate has ",4348"an unknown error.\n";4349}4350}# no warnings 'once'4351printf STDERR4352"Certificate information:\n".4353" - Hostname:%s\n".4354" - Valid: from%suntil%s\n".4355" - Issuer:%s\n".4356" - Fingerprint:%s\n",4357map$cert_info->$_,qw(hostname valid_from valid_until4358 issuer_dname fingerprint);4359my$choice;4360prompt:4361print STDERR $may_save?4362"(R)eject, accept (t)emporarily or accept (p)ermanently? ":4363"(R)eject or accept (t)emporarily? ";4364 STDERR->flush;4365$choice=lc(substr(<STDIN> ||'R',0,1));4366if($choice=~/^t$/i) {4367$cred->may_save(undef);4368}elsif($choice=~/^r$/i) {4369return-1;4370}elsif($may_save&&$choice=~/^p$/i) {4371$cred->may_save($may_save);4372}else{4373goto prompt;4374}4375$cred->accepted_failures($failures);4376$SVN::_Core::SVN_NO_ERROR;4377}43784379sub ssl_client_cert {4380my($cred,$realm,$may_save,$pool) =@_;4381$may_save=undefif$_no_auth_cache;4382print STDERR "Client certificate filename: ";4383 STDERR->flush;4384chomp(my$filename= <STDIN>);4385$cred->cert_file($filename);4386$cred->may_save($may_save);4387$SVN::_Core::SVN_NO_ERROR;4388}43894390sub ssl_client_cert_pw {4391my($cred,$realm,$may_save,$pool) =@_;4392$may_save=undefif$_no_auth_cache;4393$cred->password(_read_password("Password: ",$realm));4394$cred->may_save($may_save);4395$SVN::_Core::SVN_NO_ERROR;4396}43974398sub username {4399my($cred,$realm,$may_save,$pool) =@_;4400$may_save=undefif$_no_auth_cache;4401if(defined$realm&&length$realm) {4402print STDERR "Authentication realm:$realm\n";4403}4404my$username;4405if(defined$_username) {4406$username=$_username;4407}else{4408print STDERR "Username: ";4409 STDERR->flush;4410chomp($username= <STDIN>);4411}4412$cred->username($username);4413$cred->may_save($may_save);4414$SVN::_Core::SVN_NO_ERROR;4415}44164417sub _read_password {4418my($prompt,$realm) =@_;4419my$password='';4420if(exists$ENV{GIT_ASKPASS}) {4421open(PH,"-|",$ENV{GIT_ASKPASS},$prompt);4422$password= <PH>;4423$password=~s/[\012\015]//;# \n\r4424close(PH);4425}else{4426print STDERR $prompt;4427 STDERR->flush;4428require Term::ReadKey;4429 Term::ReadKey::ReadMode('noecho');4430while(defined(my$key= Term::ReadKey::ReadKey(0))) {4431last if$key=~/[\012\015]/;# \n\r4432$password.=$key;4433}4434 Term::ReadKey::ReadMode('restore');4435print STDERR "\n";4436 STDERR->flush;4437}4438$password;4439}44404441package SVN::Git::Fetcher;4442use vars qw/@ISA $_ignore_regex $_preserve_empty_dirs $_placeholder_filename4443@deleted_gpath %added_placeholder $repo_id/;4444use strict;4445use warnings;4446use Carp qw/croak/;4447use File::Basename qw/dirname/;4448use IO::File qw//;44494450# file baton members: path, mode_a, mode_b, pool, fh, blob, base4451sub new {4452my($class,$git_svn,$switch_path) =@_;4453my$self= SVN::Delta::Editor->new;4454bless$self,$class;4455if(exists$git_svn->{last_commit}) {4456$self->{c} =$git_svn->{last_commit};4457$self->{empty_symlinks} =4458 _mark_empty_symlinks($git_svn,$switch_path);4459}44604461# some options are read globally, but can be overridden locally4462# per [svn-remote "..."] section. Command-line options will *NOT*4463# override options set in an [svn-remote "..."] section4464$repo_id=$git_svn->{repo_id};4465my$k="svn-remote.$repo_id.ignore-paths";4466my$v=eval{ command_oneline('config','--get',$k) };4467$self->{ignore_regex} =$v;44684469$k="svn-remote.$repo_id.preserve-empty-dirs";4470$v=eval{ command_oneline('config','--get','--bool',$k) };4471if($v&&$veq'true') {4472$_preserve_empty_dirs=1;4473$k="svn-remote.$repo_id.placeholder-filename";4474$v=eval{ command_oneline('config','--get',$k) };4475$_placeholder_filename=$v;4476}44774478# Load the list of placeholder files added during previous invocations.4479$k="svn-remote.$repo_id.added-placeholder";4480$v=eval{ command_oneline('config','--get-all',$k) };4481if($_preserve_empty_dirs&&$v) {4482# command() prints errors to stderr, so we only call it if4483# command_oneline() succeeded.4484my@v= command('config','--get-all',$k);4485$added_placeholder{ dirname($_) } =$_foreach@v;4486}44874488$self->{empty} = {};4489$self->{dir_prop} = {};4490$self->{file_prop} = {};4491$self->{absent_dir} = {};4492$self->{absent_file} = {};4493$self->{gii} =$git_svn->tmp_index_do(sub{ Git::IndexInfo->new});4494$self->{pathnameencoding} = Git::config('svn.pathnameencoding');4495$self;4496}44974498# this uses the Ra object, so it must be called before do_{switch,update},4499# not inside them (when the Git::SVN::Fetcher object is passed) to4500# do_{switch,update}4501sub _mark_empty_symlinks {4502my($git_svn,$switch_path) =@_;4503my$bool= Git::config_bool('svn.brokenSymlinkWorkaround');4504return{}if(!defined($bool)) || (defined($bool) && !$bool);45054506my%ret;4507my($rev,$cmt) =$git_svn->last_rev_commit;4508return{}unless($rev&&$cmt);45094510# allow the warning to be printed for each revision we fetch to4511# ensure the user sees it. The user can also disable the workaround4512# on the repository even while git svn is running and the next4513# revision fetched will skip this expensive function.4514my$printed_warning;4515chomp(my$empty_blob=`git hash-object -t blob --stdin < /dev/null`);4516my($ls,$ctx) = command_output_pipe(qw/ls-tree -r -z/,$cmt);4517local$/="\0";4518my$pfx=defined($switch_path) ?$switch_path:$git_svn->{path};4519$pfx.='/'iflength($pfx);4520while(<$ls>) {4521chomp;4522s/\A100644 blob $empty_blob\t//oornext;4523unless($printed_warning) {4524print STDERR "Scanning for empty symlinks, ",4525"this may take a while if you have ",4526"many empty files\n",4527"You may disable this with `",4528"git config svn.brokenSymlinkWorkaround ",4529"false'.\n",4530"This may be done in a different ",4531"terminal without restarting ",4532"git svn\n";4533$printed_warning=1;4534}4535my$path=$_;4536my(undef,$props) =4537$git_svn->ra->get_file($pfx.$path,$rev,undef);4538if($props->{'svn:special'}) {4539$ret{$path} =1;4540}4541}4542 command_close_pipe($ls,$ctx);4543 \%ret;4544}45454546# returns true if a given path is inside a ".git" directory4547sub in_dot_git {4548$_[0] =~m{(?:^|/)\.git(?:/|$)};4549}45504551# return value: 0 -- don't ignore, 1 -- ignore4552sub is_path_ignored {4553my($self,$path) =@_;4554return1if in_dot_git($path);4555return1ifdefined($self->{ignore_regex}) &&4556$path=~m!$self->{ignore_regex}!;4557return0unlessdefined($_ignore_regex);4558return1if$path=~m!$_ignore_regex!o;4559return0;4560}45614562sub set_path_strip {4563my($self,$path) =@_;4564$self->{path_strip} =qr/^\Q$path\E(\/|$)/iflength$path;4565}45664567sub open_root {4568{ path =>''};4569}45704571sub open_directory {4572my($self,$path,$pb,$rev) =@_;4573{ path =>$path};4574}45754576sub git_path {4577my($self,$path) =@_;4578if(my$enc=$self->{pathnameencoding}) {4579require Encode;4580 Encode::from_to($path,'UTF-8',$enc);4581}4582if($self->{path_strip}) {4583$path=~s!$self->{path_strip}!!or4584die"Failed to strip path '$path' ($self->{path_strip})\n";4585}4586$path;4587}45884589sub delete_entry {4590my($self,$path,$rev,$pb) =@_;4591returnundefif$self->is_path_ignored($path);45924593my$gpath=$self->git_path($path);4594returnundefif($gpatheq'');45954596# remove entire directories.4597my($tree) = (command('ls-tree','-z',$self->{c},"./$gpath")4598=~/\A040000 tree ([a-f\d]{40})\t\Q$gpath\E\0/);4599if($tree) {4600my($ls,$ctx) = command_output_pipe(qw/ls-tree4601-r --name-only -z/,4602$tree);4603local$/="\0";4604while(<$ls>) {4605chomp;4606my$rmpath="$gpath/$_";4607$self->{gii}->remove($rmpath);4608print"\tD\t$rmpath\n"unless$::_q;4609}4610print"\tD\t$gpath/\n"unless$::_q;4611 command_close_pipe($ls,$ctx);4612}else{4613$self->{gii}->remove($gpath);4614print"\tD\t$gpath\n"unless$::_q;4615}4616# Don't add to @deleted_gpath if we're deleting a placeholder file.4617push@deleted_gpath,$gpathunless$added_placeholder{dirname($path)};4618$self->{empty}->{$path} =0;4619undef;4620}46214622sub open_file {4623my($self,$path,$pb,$rev) =@_;4624my($mode,$blob);46254626goto out if$self->is_path_ignored($path);46274628my$gpath=$self->git_path($path);4629($mode,$blob) = (command('ls-tree','-z',$self->{c},"./$gpath")4630=~/\A(\d{6}) blob ([a-f\d]{40})\t\Q$gpath\E\0/);4631unless(defined$mode&&defined$blob) {4632die"$pathwas not found in commit$self->{c} (r$rev)\n";4633}4634if($modeeq'100644'&&$self->{empty_symlinks}->{$path}) {4635$mode='120000';4636}4637out:4638{ path =>$path, mode_a =>$mode, mode_b =>$mode, blob =>$blob,4639 pool => SVN::Pool->new, action =>'M'};4640}46414642sub add_file {4643my($self,$path,$pb,$cp_path,$cp_rev) =@_;4644my$mode;46454646if(!$self->is_path_ignored($path)) {4647my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4648delete$self->{empty}->{$dir};4649$mode='100644';46504651if($added_placeholder{$dir}) {4652# Remove our placeholder file, if we created one.4653 delete_entry($self,$added_placeholder{$dir})4654unless$patheq$added_placeholder{$dir};4655delete$added_placeholder{$dir}4656}4657}46584659{ path =>$path, mode_a =>$mode, mode_b =>$mode,4660 pool => SVN::Pool->new, action =>'A'};4661}46624663sub add_directory {4664my($self,$path,$cp_path,$cp_rev) =@_;4665goto out if$self->is_path_ignored($path);4666my$gpath=$self->git_path($path);4667if($gpatheq'') {4668my($ls,$ctx) = command_output_pipe(qw/ls-tree4669-r --name-only -z/,4670$self->{c});4671local$/="\0";4672while(<$ls>) {4673chomp;4674$self->{gii}->remove($_);4675print"\tD\t$_\n"unless$::_q;4676push@deleted_gpath,$gpath;4677}4678 command_close_pipe($ls,$ctx);4679$self->{empty}->{$path} =0;4680}4681my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4682delete$self->{empty}->{$dir};4683$self->{empty}->{$path} =1;46844685if($added_placeholder{$dir}) {4686# Remove our placeholder file, if we created one.4687 delete_entry($self,$added_placeholder{$dir});4688delete$added_placeholder{$dir}4689}46904691out:4692{ path =>$path};4693}46944695sub change_dir_prop {4696my($self,$db,$prop,$value) =@_;4697returnundefif$self->is_path_ignored($db->{path});4698$self->{dir_prop}->{$db->{path}} ||= {};4699$self->{dir_prop}->{$db->{path}}->{$prop} =$value;4700undef;4701}47024703sub absent_directory {4704my($self,$path,$pb) =@_;4705returnundefif$self->is_path_ignored($path);4706$self->{absent_dir}->{$pb->{path}} ||= [];4707push@{$self->{absent_dir}->{$pb->{path}}},$path;4708undef;4709}47104711sub absent_file {4712my($self,$path,$pb) =@_;4713returnundefif$self->is_path_ignored($path);4714$self->{absent_file}->{$pb->{path}} ||= [];4715push@{$self->{absent_file}->{$pb->{path}}},$path;4716undef;4717}47184719sub change_file_prop {4720my($self,$fb,$prop,$value) =@_;4721returnundefif$self->is_path_ignored($fb->{path});4722if($propeq'svn:executable') {4723if($fb->{mode_b} !=120000) {4724$fb->{mode_b} =defined$value?100755:100644;4725}4726}elsif($propeq'svn:special') {4727$fb->{mode_b} =defined$value?120000:100644;4728}else{4729$self->{file_prop}->{$fb->{path}} ||= {};4730$self->{file_prop}->{$fb->{path}}->{$prop} =$value;4731}4732undef;4733}47344735sub apply_textdelta {4736my($self,$fb,$exp) =@_;4737returnundefif$self->is_path_ignored($fb->{path});4738my$fh= $::_repository->temp_acquire('svn_delta');4739# $fh gets auto-closed() by SVN::TxDelta::apply(),4740# (but $base does not,) so dup() it for reading in close_file4741open my$dup,'<&',$fhor croak $!;4742my$base= $::_repository->temp_acquire('git_blob');47434744if($fb->{blob}) {4745my($base_is_link,$size);47464747if($fb->{mode_a}eq'120000'&&4748!$self->{empty_symlinks}->{$fb->{path}}) {4749print$base'link 'or die"print$!\n";4750$base_is_link=1;4751}4752 retry:4753$size= $::_repository->cat_blob($fb->{blob},$base);4754die"Failed to read object$fb->{blob}"if($size<0);47554756if(defined$exp) {4757seek$base,0,0or croak $!;4758my$got= ::md5sum($base);4759if($gotne$exp) {4760my$err="Checksum mismatch: ".4761"$fb->{path}$fb->{blob}\n".4762"expected:$exp\n".4763" got:$got\n";4764if($base_is_link) {4765warn$err,4766"Retrying... (possibly ",4767"a bad symlink from SVN)\n";4768$::_repository->temp_reset($base);4769$base_is_link=0;4770goto retry;4771}4772die$err;4773}4774}4775}4776seek$base,0,0or croak $!;4777$fb->{fh} =$fh;4778$fb->{base} =$base;4779[ SVN::TxDelta::apply($base,$dup,undef,$fb->{path},$fb->{pool}) ];4780}47814782sub close_file {4783my($self,$fb,$exp) =@_;4784returnundefif$self->is_path_ignored($fb->{path});47854786my$hash;4787my$path=$self->git_path($fb->{path});4788if(my$fh=$fb->{fh}) {4789if(defined$exp) {4790seek($fh,0,0)or croak $!;4791my$got= ::md5sum($fh);4792if($gotne$exp) {4793die"Checksum mismatch:$path\n",4794"expected:$exp\ngot:$got\n";4795}4796}4797if($fb->{mode_b} ==120000) {4798sysseek($fh,0,0)or croak $!;4799my$rd=sysread($fh,my$buf,5);48004801if(!defined$rd) {4802 croak "sysread:$!\n";4803}elsif($rd==0) {4804warn"$pathhas mode 120000",4805" but it points to nothing\n",4806"converting to an empty file with mode",4807" 100644\n";4808$fb->{mode_b} ='100644';4809}elsif($bufne'link ') {4810warn"$pathhas mode 120000",4811" but is not a link\n";4812}else{4813my$tmp_fh= $::_repository->temp_acquire(4814'svn_hash');4815my$res;4816while($res=sysread($fh,my$str,1024)) {4817my$out=syswrite($tmp_fh,$str,$res);4818defined($out) &&$out==$res4819or croak("write ",4820 Git::temp_path($tmp_fh),4821":$!\n");4822}4823defined$resor croak $!;48244825($fh,$tmp_fh) = ($tmp_fh,$fh);4826 Git::temp_release($tmp_fh,1);4827}4828}48294830$hash= $::_repository->hash_and_insert_object(4831 Git::temp_path($fh));4832$hash=~/^[a-f\d]{40}$/or die"not a sha1:$hash\n";48334834 Git::temp_release($fb->{base},1);4835 Git::temp_release($fh,1);4836}else{4837$hash=$fb->{blob}or die"no blob information\n";4838}4839$fb->{pool}->clear;4840$self->{gii}->update($fb->{mode_b},$hash,$path)or croak $!;4841print"\t$fb->{action}\t$path\n"if$fb->{action} && ! $::_q;4842undef;4843}48444845sub abort_edit {4846my$self=shift;4847$self->{nr} =$self->{gii}->{nr};4848delete$self->{gii};4849$self->SUPER::abort_edit(@_);4850}48514852sub close_edit {4853my$self=shift;48544855if($_preserve_empty_dirs) {4856my@empty_dirs;48574858# Any entry flagged as empty that also has an associated4859# dir_prop represents a newly created empty directory.4860foreachmy$i(keys%{$self->{empty}}) {4861push@empty_dirs,$iifexists$self->{dir_prop}->{$i};4862}48634864# Search for directories that have become empty due subsequent4865# file deletes.4866push@empty_dirs,$self->find_empty_directories();48674868# Finally, add a placeholder file to each empty directory.4869$self->add_placeholder_file($_)foreach(@empty_dirs);48704871$self->stash_placeholder_list();4872}48734874$self->{git_commit_ok} =1;4875$self->{nr} =$self->{gii}->{nr};4876delete$self->{gii};4877$self->SUPER::close_edit(@_);4878}48794880sub find_empty_directories {4881my($self) =@_;4882my@empty_dirs;4883my%dirs=map{ dirname($_) =>1}@deleted_gpath;48844885foreachmy$dir(sort keys%dirs) {4886next if$direq".";48874888# If there have been any additions to this directory, there is4889# no reason to check if it is empty.4890my$skip_added=0;4891foreachmy$t(qw/dir_prop file_prop/) {4892foreachmy$path(keys%{$self->{$t} }) {4893if(exists$self->{$t}->{dirname($path)}) {4894$skip_added=1;4895last;4896}4897}4898last if$skip_added;4899}4900next if$skip_added;49014902# Use `git ls-tree` to get the filenames of this directory4903# that existed prior to this particular commit.4904my$ls= command('ls-tree','-z','--name-only',4905$self->{c},"$dir/");4906my%files=map{$_=>1}split(/\0/,$ls);49074908# Remove the filenames that were deleted during this commit.4909delete$files{$_}foreach(@deleted_gpath);49104911# Report the directory if there are no filenames left.4912push@empty_dirs,$dirunless(scalar%files);4913}4914@empty_dirs;4915}49164917sub add_placeholder_file {4918my($self,$dir) =@_;4919my$path="$dir/$_placeholder_filename";4920my$gpath=$self->git_path($path);49214922my$fh= $::_repository->temp_acquire($gpath);4923my$hash= $::_repository->hash_and_insert_object(Git::temp_path($fh));4924 Git::temp_release($fh,1);4925$self->{gii}->update('100644',$hash,$gpath)or croak $!;49264927# The directory should no longer be considered empty.4928delete$self->{empty}->{$dir}ifexists$self->{empty}->{$dir};49294930# Keep track of any placeholder files we create.4931$added_placeholder{$dir} =$path;4932}49334934sub stash_placeholder_list {4935my($self) =@_;4936my$k="svn-remote.$repo_id.added-placeholder";4937my$v=eval{ command_oneline('config','--get-all',$k) };4938 command_noisy('config','--unset-all',$k)if$v;4939foreach(values%added_placeholder) {4940 command_noisy('config','--add',$k,$_);4941}4942}49434944package SVN::Git::Editor;4945use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;4946use strict;4947use warnings;4948use Carp qw/croak/;4949use IO::File;49504951sub new {4952my($class,$opts) =@_;4953foreach(qw/svn_path r ra tree_a tree_b log editor_cb/) {4954die"$_required!\n"unless(defined$opts->{$_});4955}49564957my$pool= SVN::Pool->new;4958my$mods= generate_diff($opts->{tree_a},$opts->{tree_b});4959my$types= check_diff_paths($opts->{ra},$opts->{svn_path},4960$opts->{r},$mods);49614962# $opts->{ra} functions should not be used after this:4963my@ce=$opts->{ra}->get_commit_editor($opts->{log},4964$opts->{editor_cb},$pool);4965my$self= SVN::Delta::Editor->new(@ce,$pool);4966bless$self,$class;4967foreach(qw/svn_path r tree_a tree_b/) {4968$self->{$_} =$opts->{$_};4969}4970$self->{url} =$opts->{ra}->{url};4971$self->{mods} =$mods;4972$self->{types} =$types;4973$self->{pool} =$pool;4974$self->{bat} = {''=>$self->open_root($self->{r},$self->{pool}) };4975$self->{rm} = { };4976$self->{path_prefix} =length$self->{svn_path} ?4977"$self->{svn_path}/":'';4978$self->{config} =$opts->{config};4979$self->{mergeinfo} =$opts->{mergeinfo};4980return$self;4981}49824983sub generate_diff {4984my($tree_a,$tree_b) =@_;4985my@diff_tree=qw(diff-tree -z -r);4986if($_cp_similarity) {4987push@diff_tree,"-C$_cp_similarity";4988}else{4989push@diff_tree,'-C';4990}4991push@diff_tree,'--find-copies-harder'if$_find_copies_harder;4992push@diff_tree,"-l$_rename_limit"ifdefined$_rename_limit;4993push@diff_tree,$tree_a,$tree_b;4994my($diff_fh,$ctx) = command_output_pipe(@diff_tree);4995local$/="\0";4996my$state='meta';4997my@mods;4998while(<$diff_fh>) {4999chomp$_;# this gets rid of the trailing "\0"5000if($stateeq'meta'&& /^:(\d{6})\s(\d{6})\s5001($::sha1)\s($::sha1)\s5002([MTCRAD])\d*$/xo) {5003push@mods, { mode_a =>$1, mode_b =>$2,5004 sha1_a =>$3, sha1_b =>$4,5005 chg =>$5};5006if($5=~/^(?:C|R)$/) {5007$state='file_a';5008}else{5009$state='file_b';5010}5011}elsif($stateeq'file_a') {5012my$x=$mods[$#mods]or croak "Empty array\n";5013if($x->{chg} !~/^(?:C|R)$/) {5014 croak "Error parsing$_,$x->{chg}\n";5015}5016$x->{file_a} =$_;5017$state='file_b';5018}elsif($stateeq'file_b') {5019my$x=$mods[$#mods]or croak "Empty array\n";5020if(exists$x->{file_a} &&$x->{chg} !~/^(?:C|R)$/) {5021 croak "Error parsing$_,$x->{chg}\n";5022}5023if(!exists$x->{file_a} &&$x->{chg} =~/^(?:C|R)$/) {5024 croak "Error parsing$_,$x->{chg}\n";5025}5026$x->{file_b} =$_;5027$state='meta';5028}else{5029 croak "Error parsing$_\n";5030}5031}5032 command_close_pipe($diff_fh,$ctx);5033 \@mods;5034}50355036sub check_diff_paths {5037my($ra,$pfx,$rev,$mods) =@_;5038my%types;5039$pfx.='/'iflength$pfx;50405041sub type_diff_paths {5042my($ra,$types,$path,$rev) =@_;5043my@p=split m#/+#, $path;5044my$c=shift@p;5045unless(defined$types->{$c}) {5046$types->{$c} =$ra->check_path($c,$rev);5047}5048while(@p) {5049$c.='/'.shift@p;5050next ifdefined$types->{$c};5051$types->{$c} =$ra->check_path($c,$rev);5052}5053}50545055foreachmy$m(@$mods) {5056foreachmy$f(qw/file_a file_b/) {5057next unlessdefined$m->{$f};5058my($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);5059if(length$pfx.$dir&& !defined$types{$dir}) {5060 type_diff_paths($ra, \%types,$pfx.$dir,$rev);5061}5062}5063}5064 \%types;5065}50665067sub split_path {5068return($_[0] =~ m#^(.*?)/?([^/]+)$#);5069}50705071sub repo_path {5072my($self,$path) =@_;5073if(my$enc=$self->{pathnameencoding}) {5074require Encode;5075 Encode::from_to($path,$enc,'UTF-8');5076}5077$self->{path_prefix}.(defined$path?$path:'');5078}50795080sub url_path {5081my($self,$path) =@_;5082if($self->{url} =~ m#^https?://#) {5083$path=~s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;5084}5085$self->{url} .'/'.$self->repo_path($path);5086}50875088sub rmdirs {5089my($self) =@_;5090my$rm=$self->{rm};5091delete$rm->{''};# we never delete the url we're tracking5092return unless%$rm;50935094foreach(keys%$rm) {5095my@d=split m#/#, $_;5096my$c=shift@d;5097$rm->{$c} =1;5098while(@d) {5099$c.='/'.shift@d;5100$rm->{$c} =1;5101}5102}5103delete$rm->{$self->{svn_path}};5104delete$rm->{''};# we never delete the url we're tracking5105return unless%$rm;51065107my($fh,$ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,5108$self->{tree_b});5109local$/="\0";5110while(<$fh>) {5111chomp;5112my@dn=split m#/#, $_;5113while(pop@dn) {5114delete$rm->{join'/',@dn};5115}5116unless(%$rm) {5117close$fh;5118return;5119}5120}5121 command_close_pipe($fh,$ctx);51225123my($r,$p,$bat) = ($self->{r},$self->{pool},$self->{bat});5124foreachmy$d(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {5125$self->close_directory($bat->{$d},$p);5126my($dn) = ($d=~ m#^(.*?)/?(?:[^/]+)$#);5127print"\tD+\t$d/\n"unless$::_q;5128$self->SUPER::delete_entry($d,$r,$bat->{$dn},$p);5129delete$bat->{$d};5130}5131}51325133sub open_or_add_dir {5134my($self,$full_path,$baton) =@_;5135my$t=$self->{types}->{$full_path};5136if(!defined$t) {5137die"$full_pathnot known in r$self->{r} or we have a bug!\n";5138}5139{5140no warnings 'once';5141# SVN::Node::none and SVN::Node::file are used only once,5142# so we're shutting up Perl's warnings about them.5143if($t==$SVN::Node::none) {5144return$self->add_directory($full_path,$baton,5145undef, -1,$self->{pool});5146}elsif($t==$SVN::Node::dir) {5147return$self->open_directory($full_path,$baton,5148$self->{r},$self->{pool});5149}# no warnings 'once'5150print STDERR "$full_pathalready exists in repository at ",5151"r$self->{r} and it is not a directory (",5152($t==$SVN::Node::file ?'file':'unknown'),"/$t)\n";5153}# no warnings 'once'5154exit1;5155}51565157sub ensure_path {5158my($self,$path) =@_;5159my$bat=$self->{bat};5160my$repo_path=$self->repo_path($path);5161return$bat->{''}unless(length$repo_path);5162my@p=split m#/+#, $repo_path;5163my$c=shift@p;5164$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{''});5165while(@p) {5166my$c0=$c;5167$c.='/'.shift@p;5168$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{$c0});5169}5170return$bat->{$c};5171}51725173# Subroutine to convert a globbing pattern to a regular expression.5174# From perl cookbook.5175sub glob2pat {5176my$globstr=shift;5177my%patmap= ('*'=>'.*','?'=>'.','['=>'[',']'=>']');5178$globstr=~s{(.)} { $patmap{$1}||"\Q$1"}ge;5179return'^'.$globstr.'$';5180}51815182sub check_autoprop {5183my($self,$pattern,$properties,$file,$fbat) =@_;5184# Convert the globbing pattern to a regular expression.5185my$regex= glob2pat($pattern);5186# Check if the pattern matches the file name.5187if($file=~m/($regex)/) {5188# Parse the list of properties to set.5189my@props=split(/;/,$properties);5190foreachmy$prop(@props) {5191# Parse 'name=value' syntax and set the property.5192if($prop=~/([^=]+)=(.*)/) {5193my($n,$v) = ($1,$2);5194for($n,$v) {5195s/^\s+//;s/\s+$//;5196}5197$self->change_file_prop($fbat,$n,$v);5198}5199}5200}5201}52025203sub apply_autoprops {5204my($self,$file,$fbat) =@_;5205my$conf_t= ${$self->{config}}{'config'};5206no warnings 'once';5207# Check [miscellany]/enable-auto-props in svn configuration.5208if(SVN::_Core::svn_config_get_bool(5209$conf_t,5210$SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY,5211$SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS,52120)) {5213# Auto-props are enabled. Enumerate them to look for matches.5214my$callback=sub{5215$self->check_autoprop($_[0],$_[1],$file,$fbat);5216};5217 SVN::_Core::svn_config_enumerate(5218$conf_t,5219$SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS,5220$callback);5221}5222}52235224sub A {5225my($self,$m) =@_;5226my($dir,$file) = split_path($m->{file_b});5227my$pbat=$self->ensure_path($dir);5228my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5229undef, -1);5230print"\tA\t$m->{file_b}\n"unless$::_q;5231$self->apply_autoprops($file,$fbat);5232$self->chg_file($fbat,$m);5233$self->close_file($fbat,undef,$self->{pool});5234}52355236sub C {5237my($self,$m) =@_;5238my($dir,$file) = split_path($m->{file_b});5239my$pbat=$self->ensure_path($dir);5240my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5241$self->url_path($m->{file_a}),$self->{r});5242print"\tC\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5243$self->chg_file($fbat,$m);5244$self->close_file($fbat,undef,$self->{pool});5245}52465247sub delete_entry {5248my($self,$path,$pbat) =@_;5249my$rpath=$self->repo_path($path);5250my($dir,$file) = split_path($rpath);5251$self->{rm}->{$dir} =1;5252$self->SUPER::delete_entry($rpath,$self->{r},$pbat,$self->{pool});5253}52545255sub R {5256my($self,$m) =@_;5257my($dir,$file) = split_path($m->{file_b});5258my$pbat=$self->ensure_path($dir);5259my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5260$self->url_path($m->{file_a}),$self->{r});5261print"\tR\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5262$self->apply_autoprops($file,$fbat);5263$self->chg_file($fbat,$m);5264$self->close_file($fbat,undef,$self->{pool});52655266($dir,$file) = split_path($m->{file_a});5267$pbat=$self->ensure_path($dir);5268$self->delete_entry($m->{file_a},$pbat);5269}52705271sub M {5272my($self,$m) =@_;5273my($dir,$file) = split_path($m->{file_b});5274my$pbat=$self->ensure_path($dir);5275my$fbat=$self->open_file($self->repo_path($m->{file_b}),5276$pbat,$self->{r},$self->{pool});5277print"\t$m->{chg}\t$m->{file_b}\n"unless$::_q;5278$self->chg_file($fbat,$m);5279$self->close_file($fbat,undef,$self->{pool});5280}52815282sub T { shift->M(@_) }52835284sub change_file_prop {5285my($self,$fbat,$pname,$pval) =@_;5286$self->SUPER::change_file_prop($fbat,$pname,$pval,$self->{pool});5287}52885289sub change_dir_prop {5290my($self,$pbat,$pname,$pval) =@_;5291$self->SUPER::change_dir_prop($pbat,$pname,$pval,$self->{pool});5292}52935294sub _chg_file_get_blob ($$$$) {5295my($self,$fbat,$m,$which) =@_;5296my$fh= $::_repository->temp_acquire("git_blob_$which");5297if($m->{"mode_$which"} =~/^120/) {5298print$fh'link 'or croak $!;5299$self->change_file_prop($fbat,'svn:special','*');5300}elsif($m->{mode_a} =~/^120/&&$m->{"mode_$which"} !~/^120/) {5301$self->change_file_prop($fbat,'svn:special',undef);5302}5303my$blob=$m->{"sha1_$which"};5304return($fh,)if($blob=~/^0{40}$/);5305my$size= $::_repository->cat_blob($blob,$fh);5306 croak "Failed to read object$blob"if($size<0);5307$fh->flush==0or croak $!;5308seek$fh,0,0or croak $!;53095310my$exp= ::md5sum($fh);5311seek$fh,0,0or croak $!;5312return($fh,$exp);5313}53145315sub chg_file {5316my($self,$fbat,$m) =@_;5317if($m->{mode_b} =~/755$/&&$m->{mode_a} !~/755$/) {5318$self->change_file_prop($fbat,'svn:executable','*');5319}elsif($m->{mode_b} !~/755$/&&$m->{mode_a} =~/755$/) {5320$self->change_file_prop($fbat,'svn:executable',undef);5321}5322my($fh_a,$exp_a) = _chg_file_get_blob $self,$fbat,$m,'a';5323my($fh_b,$exp_b) = _chg_file_get_blob $self,$fbat,$m,'b';5324my$pool= SVN::Pool->new;5325my$atd=$self->apply_textdelta($fbat,$exp_a,$pool);5326if(-s $fh_a) {5327my$txstream= SVN::TxDelta::new ($fh_a,$fh_b,$pool);5328my$res= SVN::TxDelta::send_txstream($txstream,@$atd,$pool);5329if(defined$res) {5330die"Unexpected result from send_txstream:$res\n",5331"(SVN::Core::VERSION:$SVN::Core::VERSION)\n";5332}5333}else{5334my$got= SVN::TxDelta::send_stream($fh_b,@$atd,$pool);5335die"Checksum mismatch\nexpected:$exp_b\ngot:$got\n"5336if($gotne$exp_b);5337}5338 Git::temp_release($fh_b,1);5339 Git::temp_release($fh_a,1);5340$pool->clear;5341}53425343sub D {5344my($self,$m) =@_;5345my($dir,$file) = split_path($m->{file_b});5346my$pbat=$self->ensure_path($dir);5347print"\tD\t$m->{file_b}\n"unless$::_q;5348$self->delete_entry($m->{file_b},$pbat);5349}53505351sub close_edit {5352my($self) =@_;5353my($p,$bat) = ($self->{pool},$self->{bat});5354foreach(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {5355next if$_eq'';5356$self->close_directory($bat->{$_},$p);5357}5358$self->close_directory($bat->{''},$p);5359$self->SUPER::close_edit($p);5360$p->clear;5361}53625363sub abort_edit {5364my($self) =@_;5365$self->SUPER::abort_edit($self->{pool});5366}53675368sub DESTROY {5369my$self=shift;5370$self->SUPER::DESTROY(@_);5371$self->{pool}->clear;5372}53735374# this drives the editor5375sub apply_diff {5376my($self) =@_;5377my$mods=$self->{mods};5378my%o= ( D =>1, R =>0, C => -1, A =>3, M =>3, T =>3);5379foreachmy$m(sort{$o{$a->{chg}} <=>$o{$b->{chg}} }@$mods) {5380my$f=$m->{chg};5381if(defined$o{$f}) {5382$self->$f($m);5383}else{5384 fatal("Invalid change type:$f");5385}5386}53875388if(defined($self->{mergeinfo})) {5389$self->change_dir_prop($self->{bat}{''},"svn:mergeinfo",5390$self->{mergeinfo});5391}5392$self->rmdirsif$_rmdir;5393if(@$mods==0) {5394$self->abort_edit;5395}else{5396$self->close_edit;5397}5398returnscalar@$mods;5399}54005401package Git::SVN::Ra;5402use vars qw/@ISA $config_dir $_ignore_refs_regex $_log_window_size/;5403use strict;5404use warnings;5405my($ra_invalid,$can_do_switch,%ignored_err,$RA);54065407BEGIN{5408# enforce temporary pool usage for some simple functions5409no strict 'refs';5410formy$f(qw/rev_proplist get_latest_revnum get_uuid get_repos_root5411 get_file/) {5412my$SUPER="SUPER::$f";5413*$f=sub{5414my$self=shift;5415my$pool= SVN::Pool->new;5416my@ret=$self->$SUPER(@_,$pool);5417$pool->clear;5418wantarray?@ret:$ret[0];5419};5420}5421}54225423sub _auth_providers () {5424[5425 SVN::Client::get_simple_provider(),5426 SVN::Client::get_ssl_server_trust_file_provider(),5427 SVN::Client::get_simple_prompt_provider(5428 \&Git::SVN::Prompt::simple,2),5429 SVN::Client::get_ssl_client_cert_file_provider(),5430 SVN::Client::get_ssl_client_cert_prompt_provider(5431 \&Git::SVN::Prompt::ssl_client_cert,2),5432 SVN::Client::get_ssl_client_cert_pw_file_provider(),5433 SVN::Client::get_ssl_client_cert_pw_prompt_provider(5434 \&Git::SVN::Prompt::ssl_client_cert_pw,2),5435 SVN::Client::get_username_provider(),5436 SVN::Client::get_ssl_server_trust_prompt_provider(5437 \&Git::SVN::Prompt::ssl_server_trust),5438 SVN::Client::get_username_prompt_provider(5439 \&Git::SVN::Prompt::username,2)5440]5441}54425443sub escape_uri_only {5444my($uri) =@_;5445my@tmp;5446foreach(splitm{/},$uri) {5447s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;5448push@tmp,$_;5449}5450join('/',@tmp);5451}54525453sub escape_url {5454my($url) =@_;5455if($url=~ m#^(https?)://([^/]+)(.*)$#) {5456my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));5457$url="$scheme://$domain$uri";5458}5459$url;5460}54615462sub new {5463my($class,$url) =@_;5464$url=~s!/+$!!;5465return$RAif($RA&&$RA->{url}eq$url);54665467::_req_svn();54685469 SVN::_Core::svn_config_ensure($config_dir,undef);5470my($baton,$callbacks) = SVN::Core::auth_open_helper(_auth_providers);5471my$config= SVN::Core::config_get_config($config_dir);5472$RA=undef;5473my$dont_store_passwords=1;5474my$conf_t= ${$config}{'config'};5475{5476no warnings 'once';5477# The usage of $SVN::_Core::SVN_CONFIG_* variables5478# produces warnings that variables are used only once.5479# I had not found the better way to shut them up, so5480# the warnings of type 'once' are disabled in this block.5481if(SVN::_Core::svn_config_get_bool($conf_t,5482$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5483$SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS,54841) ==0) {5485 SVN::_Core::svn_auth_set_parameter($baton,5486$SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS,5487bless(\$dont_store_passwords,"_p_void"));5488}5489if(SVN::_Core::svn_config_get_bool($conf_t,5490$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5491$SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS,54921) ==0) {5493$Git::SVN::Prompt::_no_auth_cache =1;5494}5495}# no warnings 'once'5496my$self= SVN::Ra->new(url => escape_url($url), auth =>$baton,5497 config =>$config,5498 pool => SVN::Pool->new,5499 auth_provider_callbacks =>$callbacks);5500$self->{url} =$url;5501$self->{svn_path} =$url;5502$self->{repos_root} =$self->get_repos_root;5503$self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;5504$self->{cache} = { check_path => { r =>0, data => {} },5505 get_dir => { r =>0, data => {} } };5506$RA=bless$self,$class;5507}55085509sub check_path {5510my($self,$path,$r) =@_;5511my$cache=$self->{cache}->{check_path};5512if($r==$cache->{r} &&exists$cache->{data}->{$path}) {5513return$cache->{data}->{$path};5514}5515my$pool= SVN::Pool->new;5516my$t=$self->SUPER::check_path($path,$r,$pool);5517$pool->clear;5518if($r!=$cache->{r}) {5519%{$cache->{data}} = ();5520$cache->{r} =$r;5521}5522$cache->{data}->{$path} =$t;5523}55245525sub get_dir {5526my($self,$dir,$r) =@_;5527my$cache=$self->{cache}->{get_dir};5528if($r==$cache->{r}) {5529if(my$x=$cache->{data}->{$dir}) {5530returnwantarray?@$x:$x->[0];5531}5532}5533my$pool= SVN::Pool->new;5534my($d,undef,$props) =$self->SUPER::get_dir($dir,$r,$pool);5535my%dirents=map{$_=> { kind =>$d->{$_}->kind} }keys%$d;5536$pool->clear;5537if($r!=$cache->{r}) {5538%{$cache->{data}} = ();5539$cache->{r} =$r;5540}5541$cache->{data}->{$dir} = [ \%dirents,$r,$props];5542wantarray? (\%dirents,$r,$props) : \%dirents;5543}55445545sub DESTROY {5546# do not call the real DESTROY since we store ourselves in $RA5547}55485549# get_log(paths, start, end, limit,5550# discover_changed_paths, strict_node_history, receiver)5551sub get_log {5552my($self,@args) =@_;5553my$pool= SVN::Pool->new;55545555# svn_log_changed_path_t objects passed to get_log are likely to be5556# overwritten even if only the refs are copied to an external variable,5557# so we should dup the structures in their entirety. Using an5558# externally passed pool (instead of our temporary and quickly cleared5559# pool in Git::SVN::Ra) does not help matters at all...5560my$receiver=pop@args;5561my$prefix="/".$self->{svn_path};5562$prefix=~ s#/+($)##;5563my$prefix_regex= qr#^\Q$prefix\E#;5564push(@args,sub{5565my($paths) =$_[0];5566return&$receiver(@_)unless$paths;5567$_[0] = ();5568foreachmy$p(keys%$paths) {5569my$i=$paths->{$p};5570# Make path relative to our url, not repos_root5571$p=~s/$prefix_regex//;5572my%s=map{$_=>$i->$_; }5573 qw/copyfrom_path copyfrom_rev action/;5574if($s{'copyfrom_path'}) {5575$s{'copyfrom_path'} =~s/$prefix_regex//;5576}5577$_[0]{$p} = \%s;5578}5579&$receiver(@_);5580});558155825583# the limit parameter was not supported in SVN 1.1.x, so we5584# drop it. Therefore, the receiver callback passed to it5585# is made aware of this limitation by being wrapped if5586# the limit passed to is being wrapped.5587if($SVN::Core::VERSION le'1.2.0') {5588my$limit=splice(@args,3,1);5589if($limit>0) {5590my$receiver=pop@args;5591push(@args,sub{ &$receiver(@_)if(--$limit>=0) });5592}5593}5594my$ret=$self->SUPER::get_log(@args,$pool);5595$pool->clear;5596$ret;5597}55985599sub trees_match {5600my($self,$url1,$rev1,$url2,$rev2) =@_;5601my$ctx= SVN::Client->new(auth => _auth_providers);5602my$out= IO::File->new_tmpfile;56035604# older SVN (1.1.x) doesn't take $pool as the last parameter for5605# $ctx->diff(), so we'll create a default one5606my$pool= SVN::Pool->new_default_sub;56075608$ra_invalid=1;# this will open a new SVN::Ra connection to $url15609$ctx->diff([],$url1,$rev1,$url2,$rev2,1,1,0,$out,$out);5610$out->flush;5611my$ret= (($out->stat)[7] ==0);5612close$outor croak $!;56135614$ret;5615}56165617sub get_commit_editor {5618my($self,$log,$cb,$pool) =@_;5619my@lock=$SVN::Core::VERSION ge'1.2.0'? (undef,0) : ();5620$self->SUPER::get_commit_editor($log,$cb,@lock,$pool);5621}56225623sub gs_do_update {5624my($self,$rev_a,$rev_b,$gs,$editor) =@_;5625my$new= ($rev_a==$rev_b);5626my$path=$gs->{path};56275628if($new&& -e $gs->{index}) {5629unlink$gs->{index}or die5630"Couldn't unlink index:$gs->{index}:$!\n";5631}5632my$pool= SVN::Pool->new;5633$editor->set_path_strip($path);5634my(@pc) =split m#/#, $path;5635my$reporter=$self->do_update($rev_b, (@pc?shift@pc:''),56361,$editor,$pool);5637my@lock=$SVN::Core::VERSION ge'1.2.0'? (undef) : ();56385639# Since we can't rely on svn_ra_reparent being available, we'll5640# just have to do some magic with set_path to make it so5641# we only want a partial path.5642my$sp='';5643my$final=join('/',@pc);5644while(@pc) {5645$reporter->set_path($sp,$rev_b,0,@lock,$pool);5646$sp.='/'iflength$sp;5647$sp.=shift@pc;5648}5649die"BUG: '$sp' != '$final'\n"if($spne$final);56505651$reporter->set_path($sp,$rev_a,$new,@lock,$pool);56525653$reporter->finish_report($pool);5654$pool->clear;5655$editor->{git_commit_ok};5656}56575658# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and5659# svn_ra_reparent didn't work before 1.4)5660sub gs_do_switch {5661my($self,$rev_a,$rev_b,$gs,$url_b,$editor) =@_;5662my$path=$gs->{path};5663my$pool= SVN::Pool->new;56645665my$full_url=$self->{url};5666my$old_url=$full_url;5667$full_url.='/'.$pathiflength$path;5668my($ra,$reparented);56695670if($old_url=~ m#^svn(\+ssh)?://# ||5671($full_url=~ m#^https?://# &&5672 escape_url($full_url)ne$full_url)) {5673$_[0] =undef;5674$self=undef;5675$RA=undef;5676$ra= Git::SVN::Ra->new($full_url);5677$ra_invalid=1;5678}elsif($old_urlne$full_url) {5679 SVN::_Ra::svn_ra_reparent($self->{session},$full_url,$pool);5680$self->{url} =$full_url;5681$reparented=1;5682}56835684$ra||=$self;5685$url_b= escape_url($url_b);5686my$reporter=$ra->do_switch($rev_b,'',1,$url_b,$editor,$pool);5687my@lock=$SVN::Core::VERSION ge'1.2.0'? (undef) : ();5688$reporter->set_path('',$rev_a,0,@lock,$pool);5689$reporter->finish_report($pool);56905691if($reparented) {5692 SVN::_Ra::svn_ra_reparent($self->{session},$old_url,$pool);5693$self->{url} =$old_url;5694}56955696$pool->clear;5697$editor->{git_commit_ok};5698}56995700sub longest_common_path {5701my($gsv,$globs) =@_;5702my%common;5703my$common_max=scalar@$gsv;57045705foreachmy$gs(@$gsv) {5706my@tmp=split m#/#, $gs->{path};5707my$p='';5708foreach(@tmp) {5709$p.=length($p) ?"/$_":$_;5710$common{$p} ||=0;5711$common{$p}++;5712}5713}5714$globs||= [];5715$common_max+=scalar@$globs;5716foreachmy$glob(@$globs) {5717my@tmp=split m#/#, $glob->{path}->{left};5718my$p='';5719foreach(@tmp) {5720$p.=length($p) ?"/$_":$_;5721$common{$p} ||=0;5722$common{$p}++;5723}5724}57255726my$longest_path='';5727foreach(sort{length$b<=>length$a}keys%common) {5728if($common{$_} ==$common_max) {5729$longest_path=$_;5730last;5731}5732}5733$longest_path;5734}57355736sub gs_fetch_loop_common {5737my($self,$base,$head,$gsv,$globs) =@_;5738return if($base>$head);5739my$inc=$_log_window_size;5740my($min,$max) = ($base,$head<$base+$inc?$head:$base+$inc);5741my$longest_path= longest_common_path($gsv,$globs);5742my$ra_url=$self->{url};5743my$find_trailing_edge;5744while(1) {5745my%revs;5746my$err;5747my$err_handler=$SVN::Error::handler;5748$SVN::Error::handler =sub{5749($err) =@_;5750 skip_unknown_revs($err);5751};5752sub _cb {5753my($paths,$r,$author,$date,$log) =@_;5754[$paths,5755{ author =>$author, date =>$date,log=>$log} ];5756}5757$self->get_log([$longest_path],$min,$max,0,1,1,5758sub{$revs{$_[1]} = _cb(@_) });5759if($err) {5760print"Checked through r$max\r";5761}else{5762$find_trailing_edge=1;5763}5764if($errand$find_trailing_edge) {5765print STDERR "Path '$longest_path' ",5766"was probably deleted:\n",5767$err->expanded_message,5768"\nWill attempt to follow ",5769"revisions r$min.. r$max",5770"committed before the deletion\n";5771my$hi=$max;5772while(--$hi>=$min) {5773my$ok;5774$self->get_log([$longest_path],$min,$hi,57750,1,1,sub{5776$ok=$_[1];5777$revs{$_[1]} = _cb(@_) });5778if($ok) {5779print STDERR "r$min.. r$okOK\n";5780last;5781}5782}5783$find_trailing_edge=0;5784}5785$SVN::Error::handler =$err_handler;57865787my%exists=map{$_->{path} =>$_}@$gsv;5788foreachmy$r(sort{$a<=>$b}keys%revs) {5789my($paths,$logged) = @{$revs{$r}};57905791foreachmy$gs($self->match_globs(\%exists,$paths,5792$globs,$r)) {5793if($gs->rev_map_max>=$r) {5794next;5795}5796next unless$gs->match_paths($paths,$r);5797$gs->{logged_rev_props} =$logged;5798if(my$last_commit=$gs->last_commit) {5799$gs->assert_index_clean($last_commit);5800}5801my$log_entry=$gs->do_fetch($paths,$r);5802if($log_entry) {5803$gs->do_git_commit($log_entry);5804}5805$INDEX_FILES{$gs->{index}} =1;5806}5807foreachmy$g(@$globs) {5808my$k="svn-remote.$g->{remote}.".5809"$g->{t}-maxRev";5810 Git::SVN::tmp_config($k,$r);5811}5812if($ra_invalid) {5813$_[0] =undef;5814$self=undef;5815$RA=undef;5816$self= Git::SVN::Ra->new($ra_url);5817$ra_invalid=undef;5818}5819}5820# pre-fill the .rev_db since it'll eventually get filled in5821# with '0' x40 if something new gets committed5822foreachmy$gs(@$gsv) {5823next if$gs->rev_map_max>=$max;5824next ifdefined$gs->rev_map_get($max);5825$gs->rev_map_set($max,0 x40);5826}5827foreachmy$g(@$globs) {5828my$k="svn-remote.$g->{remote}.$g->{t}-maxRev";5829 Git::SVN::tmp_config($k,$max);5830}5831last if$max>=$head;5832$min=$max+1;5833$max+=$inc;5834$max=$headif($max>$head);5835}5836 Git::SVN::gc();5837}58385839sub get_dir_globbed {5840my($self,$left,$depth,$r) =@_;58415842my@x=eval{$self->get_dir($left,$r) };5843return unlessscalar@x==3;5844my$dirents=$x[0];5845my@finalents;5846foreachmy$de(keys%$dirents) {5847next if$dirents->{$de}->{kind} !=$SVN::Node::dir;5848if($depth>1) {5849my@args= ("$left/$de",$depth-1,$r);5850foreachmy$dir($self->get_dir_globbed(@args)) {5851push@finalents,"$de/$dir";5852}5853}else{5854push@finalents,$de;5855}5856}5857@finalents;5858}58595860# return value: 0 -- don't ignore, 1 -- ignore5861sub is_ref_ignored {5862my($g,$p) =@_;5863my$refname=$g->{ref}->full_path($p);5864return1ifdefined($g->{ignore_refs_regex}) &&5865$refname=~m!$g->{ignore_refs_regex}!;5866return0unlessdefined($_ignore_refs_regex);5867return1if$refname=~m!$_ignore_refs_regex!o;5868return0;5869}58705871sub match_globs {5872my($self,$exists,$paths,$globs,$r) =@_;58735874sub get_dir_check {5875my($self,$exists,$g,$r) =@_;58765877my@dirs=$self->get_dir_globbed($g->{path}->{left},5878$g->{path}->{depth},5879$r);58805881foreachmy$de(@dirs) {5882my$p=$g->{path}->full_path($de);5883next if$exists->{$p};5884next if(length$g->{path}->{right} &&5885($self->check_path($p,$r) !=5886$SVN::Node::dir));5887next unless$p=~/$g->{path}->{regex}/;5888$exists->{$p} = Git::SVN->init($self->{url},$p,undef,5889$g->{ref}->full_path($de),1);5890}5891}5892foreachmy$g(@$globs) {5893if(my$path=$paths->{"/$g->{path}->{left}"}) {5894if($path->{action} =~/^[AR]$/) {5895 get_dir_check($self,$exists,$g,$r);5896}5897}5898foreach(keys%$paths) {5899if(/$g->{path}->{left_regex}/&&5900!/$g->{path}->{regex}/) {5901next if$paths->{$_}->{action} !~/^[AR]$/;5902 get_dir_check($self,$exists,$g,$r);5903}5904next unless/$g->{path}->{regex}/;5905my$p=$1;5906my$pathname=$g->{path}->full_path($p);5907next if is_ref_ignored($g,$p);5908next if$exists->{$pathname};5909next if($self->check_path($pathname,$r) !=5910$SVN::Node::dir);5911$exists->{$pathname} = Git::SVN->init(5912$self->{url},$pathname,undef,5913$g->{ref}->full_path($p),1);5914}5915my$c='';5916foreach(split m#/#, $g->{path}->{left}) {5917$c.="/$_";5918next unless($paths->{$c} &&5919($paths->{$c}->{action} =~/^[AR]$/));5920 get_dir_check($self,$exists,$g,$r);5921}5922}5923values%$exists;5924}59255926sub minimize_url {5927my($self) =@_;5928return$self->{url}if($self->{url}eq$self->{repos_root});5929my$url=$self->{repos_root};5930my@components=split(m!/!,$self->{svn_path});5931my$c='';5932do{5933$url.="/$c"iflength$c;5934eval{5935my$ra= (ref$self)->new($url);5936my$latest=$ra->get_latest_revnum;5937$ra->get_log("",$latest,0,1,0,1,sub{});5938};5939}while($@&& ($c=shift@components));5940$url;5941}59425943sub can_do_switch {5944my$self=shift;5945unless(defined$can_do_switch) {5946my$pool= SVN::Pool->new;5947my$rep=eval{5948$self->do_switch(1,'',0,$self->{url},5949 SVN::Delta::Editor->new,$pool);5950};5951if($@) {5952$can_do_switch=0;5953}else{5954$rep->abort_report($pool);5955$can_do_switch=1;5956}5957$pool->clear;5958}5959$can_do_switch;5960}59615962sub skip_unknown_revs {5963my($err) =@_;5964my$errno=$err->apr_err();5965# Maybe the branch we're tracking didn't5966# exist when the repo started, so it's5967# not an error if it doesn't, just continue5968#5969# Wonderfully consistent library, eh?5970# 160013 - svn:// and file://5971# 175002 - http(s)://5972# 175007 - http(s):// (this repo required authorization, too...)5973# More codes may be discovered later...5974if($errno==175007||$errno==175002||$errno==160013) {5975my$err_key=$err->expanded_message;5976# revision numbers change every time, filter them out5977$err_key=~s/\d+/\0/g;5978$err_key="$errno\0$err_key";5979unless($ignored_err{$err_key}) {5980warn"W: Ignoring error from SVN, path probably ",5981"does not exist: ($errno): ",5982$err->expanded_message,"\n";5983warn"W: Do not be alarmed at the above message ",5984"git-svn is just searching aggressively for ",5985"old history.\n",5986"This may take a while on large repositories\n";5987$ignored_err{$err_key} =1;5988}5989return;5990}5991die"Error from SVN, ($errno): ",$err->expanded_message,"\n";5992}59935994package Git::SVN::Log;5995use strict;5996use warnings;5997use POSIX qw/strftime/;5998use Time::Local;5999useconstant commit_log_separator => ('-' x 72) ."\n";6000use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline6001%rusers $show_commit $incremental/;6002my$l_fmt;60036004sub cmt_showable {6005my($c) =@_;6006return1ifdefined$c->{r};60076008# big commit message got truncated by the 16k pretty buffer in rev-list6009if($c->{l} &&$c->{l}->[-1]eq"...\n"&&6010$c->{a_raw} =~/\@([a-f\d\-]+)>$/) {6011@{$c->{l}} = ();6012my@log= command(qw/cat-file commit/,$c->{c});60136014# shift off the headers6015shift@logwhile($log[0]ne'');6016shift@log;60176018# TODO: make $c->{l} not have a trailing newline in the future6019@{$c->{l}} =map{"$_\n"}grep!/^git-svn-id: /,@log;60206021(undef,$c->{r},undef) = ::extract_metadata(6022(grep(/^git-svn-id: /,@log))[-1]);6023}6024returndefined$c->{r};6025}60266027sub log_use_color {6028return$color|| Git->repository->get_colorbool('color.diff');6029}60306031sub git_svn_log_cmd {6032my($r_min,$r_max,@args) =@_;6033my$head='HEAD';6034my(@files,@log_opts);6035foreachmy$x(@args) {6036if($xeq'--'||@files) {6037push@files,$x;6038}else{6039if(::verify_ref("$x^0")) {6040$head=$x;6041}else{6042push@log_opts,$x;6043}6044}6045}60466047my($url,$rev,$uuid,$gs) = ::working_head_info($head);6048$gs||= Git::SVN->_new;6049my@cmd= (qw/log --abbrev-commit --pretty=raw --default/,6050$gs->refname);6051push@cmd,'-r'unless$non_recursive;6052push@cmd, qw/--raw --name-status/if$verbose;6053push@cmd,'--color'if log_use_color();6054push@cmd,@log_opts;6055if(defined$r_max&&$r_max==$r_min) {6056push@cmd,'--max-count=1';6057if(my$c=$gs->rev_map_get($r_max)) {6058push@cmd,$c;6059}6060}elsif(defined$r_max) {6061if($r_max<$r_min) {6062($r_min,$r_max) = ($r_max,$r_min);6063}6064my(undef,$c_max) =$gs->find_rev_before($r_max,1,$r_min);6065my(undef,$c_min) =$gs->find_rev_after($r_min,1,$r_max);6066# If there are no commits in the range, both $c_max and $c_min6067# will be undefined. If there is at least 1 commit in the6068# range, both will be defined.6069return()if!defined$c_min|| !defined$c_max;6070if($c_mineq$c_max) {6071push@cmd,'--max-count=1',$c_min;6072}else{6073push@cmd,'--boundary',"$c_min..$c_max";6074}6075}6076return(@cmd,@files);6077}60786079# adapted from pager.c6080sub config_pager {6081if(! -t *STDOUT) {6082$ENV{GIT_PAGER_IN_USE} ='false';6083$pager=undef;6084return;6085}6086chomp($pager= command_oneline(qw(var GIT_PAGER)));6087if($pagereq'cat') {6088$pager=undef;6089}6090$ENV{GIT_PAGER_IN_USE} =defined($pager);6091}60926093sub run_pager {6094return unlessdefined$pager;6095pipe my($rfd,$wfd)orreturn;6096defined(my$pid=fork)or::fatal "Can't fork:$!";6097if(!$pid) {6098open STDOUT,'>&',$wfdor6099::fatal "Can't redirect to stdout:$!";6100return;6101}6102open STDIN,'<&',$rfdor::fatal "Can't redirect stdin:$!";6103$ENV{LESS} ||='FRSX';6104exec$pageror::fatal "Can't run pager:$!($pager)";6105}61066107sub format_svn_date {6108# some systmes don't handle or mishandle %z, so be creative.6109my$t=shift||time;6110my$gm= timelocal(gmtime($t));6111my$sign=qw( + + - )[$t<=>$gm];6112my$gmoff=sprintf("%s%02d%02d",$sign, (gmtime(abs($t-$gm)))[2,1]);6113return strftime("%Y-%m-%d%H:%M:%S$gmoff(%a,%d%b%Y)",localtime($t));6114}61156116sub parse_git_date {6117my($t,$tz) =@_;6118# Date::Parse isn't in the standard Perl distro :(6119if($tz=~s/^\+//) {6120$t+= tz_to_s_offset($tz);6121}elsif($tz=~s/^\-//) {6122$t-= tz_to_s_offset($tz);6123}6124return$t;6125}61266127sub set_local_timezone {6128if(defined$TZ) {6129$ENV{TZ} =$TZ;6130}else{6131delete$ENV{TZ};6132}6133}61346135sub tz_to_s_offset {6136my($tz) =@_;6137$tz=~s/(\d\d)$//;6138return($1*60) + ($tz*3600);6139}61406141sub get_author_info {6142my($dest,$author,$t,$tz) =@_;6143$author=~s/(?:^\s*|\s*$)//g;6144$dest->{a_raw} =$author;6145my$au;6146if($::_authors) {6147$au=$rusers{$author} ||undef;6148}6149if(!$au) {6150($au) = ($author=~/<([^>]+)\@[^>]+>$/);6151}6152$dest->{t} =$t;6153$dest->{tz} =$tz;6154$dest->{a} =$au;6155$dest->{t_utc} = parse_git_date($t,$tz);6156}61576158sub process_commit {6159my($c,$r_min,$r_max,$defer) =@_;6160if(defined$r_min&&defined$r_max) {6161if($r_min==$c->{r} &&$r_min==$r_max) {6162 show_commit($c);6163return0;6164}6165return1if$r_min==$r_max;6166if($r_min<$r_max) {6167# we need to reverse the print order6168return0if(defined$limit&& --$limit<0);6169push@$defer,$c;6170return1;6171}6172if($r_min!=$r_max) {6173return1if($r_min<$c->{r});6174return1if($r_max>$c->{r});6175}6176}6177return0if(defined$limit&& --$limit<0);6178 show_commit($c);6179return1;6180}61816182sub show_commit {6183my$c=shift;6184if($oneline) {6185my$x="\n";6186if(my$l=$c->{l}) {6187while($l->[0] =~/^\s*$/) {shift@$l}6188$x=$l->[0];6189}6190$l_fmt||='A'.length($c->{r});6191print'r',pack($l_fmt,$c->{r}),' | ';6192print"$c->{c} | "if$show_commit;6193print$x;6194}else{6195 show_commit_normal($c);6196}6197}61986199sub show_commit_changed_paths {6200my($c) =@_;6201return unless$c->{changed};6202print"Changed paths:\n", @{$c->{changed}};6203}62046205sub show_commit_normal {6206my($c) =@_;6207print commit_log_separator,"r$c->{r} | ";6208print"$c->{c} | "if$show_commit;6209print"$c->{a} | ", format_svn_date($c->{t_utc}),' | ';6210my$nr_line=0;62116212if(my$l=$c->{l}) {6213while($l->[$#$l]eq"\n"&&$#$l>06214&&$l->[($#$l-1)]eq"\n") {6215pop@$l;6216}6217$nr_line=scalar@$l;6218if(!$nr_line) {6219print"1 line\n\n\n";6220}else{6221if($nr_line==1) {6222$nr_line='1 line';6223}else{6224$nr_line.=' lines';6225}6226print$nr_line,"\n";6227 show_commit_changed_paths($c);6228print"\n";6229print$_foreach@$l;6230}6231}else{6232print"1 line\n";6233 show_commit_changed_paths($c);6234print"\n";62356236}6237foreachmy$x(qw/raw stat diff/) {6238if($c->{$x}) {6239print"\n";6240print$_foreach@{$c->{$x}}6241}6242}6243}62446245sub cmd_show_log {6246my(@args) =@_;6247my($r_min,$r_max);6248my$r_last= -1;# prevent dupes6249 set_local_timezone();6250if(defined$::_revision) {6251if($::_revision =~/^(\d+):(\d+)$/) {6252($r_min,$r_max) = ($1,$2);6253}elsif($::_revision =~/^\d+$/) {6254$r_min=$r_max= $::_revision;6255}else{6256::fatal "-r$::_revision is not supported, use ",6257"standard 'git log' arguments instead";6258}6259}62606261 config_pager();6262@args= git_svn_log_cmd($r_min,$r_max,@args);6263if(!@args) {6264print commit_log_separator unless$incremental||$oneline;6265return;6266}6267my$log= command_output_pipe(@args);6268 run_pager();6269my(@k,$c,$d,$stat);6270my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;6271while(<$log>) {6272if(/^${esc_color}commit (?:- )?($::sha1_short)/o) {6273my$cmt=$1;6274if($c&& cmt_showable($c) &&$c->{r} !=$r_last) {6275$r_last=$c->{r};6276 process_commit($c,$r_min,$r_max, \@k)or6277goto out;6278}6279$d=undef;6280$c= { c =>$cmt};6281}elsif(/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {6282 get_author_info($c,$1,$2,$3);6283}elsif(/^${esc_color}(?:tree|parent|committer) /o) {6284# ignore6285}elsif(/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {6286push@{$c->{raw}},$_;6287}elsif(/^${esc_color}[ACRMDT]\t/) {6288# we could add $SVN->{svn_path} here, but that requires6289# remote access at the moment (repo_path_split)...6290 s#^(${esc_color})([ACRMDT])\t#$1 $2 #o;6291push@{$c->{changed}},$_;6292}elsif(/^${esc_color}diff /o) {6293$d=1;6294push@{$c->{diff}},$_;6295}elsif($d) {6296push@{$c->{diff}},$_;6297}elsif(/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*6298$esc_color*[\+\-]*$esc_color$/x) {6299$stat=1;6300push@{$c->{stat}},$_;6301}elsif($stat&&/^ \d+ files changed, \d+ insertions/) {6302push@{$c->{stat}},$_;6303$stat=undef;6304}elsif(/^${esc_color} (git-svn-id:.+)$/o) {6305($c->{url},$c->{r},undef) = ::extract_metadata($1);6306}elsif(s/^${esc_color} //o) {6307push@{$c->{l}},$_;6308}6309}6310if($c&&defined$c->{r} &&$c->{r} !=$r_last) {6311$r_last=$c->{r};6312 process_commit($c,$r_min,$r_max, \@k);6313}6314if(@k) {6315($r_min,$r_max) = ($r_max,$r_min);6316 process_commit($_,$r_min,$r_max)foreachreverse@k;6317}6318out:6319close$log;6320print commit_log_separator unless$incremental||$oneline;6321}63226323sub cmd_blame {6324my$path=pop;63256326 config_pager();6327 run_pager();63286329my($fh,$ctx,$rev);63306331if($_git_format) {6332($fh,$ctx) = command_output_pipe('blame',@_,$path);6333while(my$line= <$fh>) {6334if($line=~/^\^?([[:xdigit:]]+)\s/) {6335# Uncommitted edits show up as a rev ID of6336# all zeros, which we can't look up with6337# cmt_metadata6338if($1!~/^0+$/) {6339(undef,$rev,undef) =6340::cmt_metadata($1);6341$rev='0'if(!$rev);6342}else{6343$rev='0';6344}6345$rev=sprintf('%-10s',$rev);6346$line=~s/^\^?[[:xdigit:]]+(\s)/$rev$1/;6347}6348print$line;6349}6350}else{6351($fh,$ctx) = command_output_pipe('blame','-p',@_,'HEAD',6352'--',$path);6353my($sha1);6354my%authors;6355my@buffer;6356my%dsha;#distinct sha keys63576358while(my$line= <$fh>) {6359push@buffer,$line;6360if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6361$dsha{$1} =1;6362}6363}63646365my$s2r= ::cmt_sha2rev_batch([keys%dsha]);63666367foreachmy$line(@buffer) {6368if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6369$rev=$s2r->{$1};6370$rev='0'if(!$rev)6371}6372elsif($line=~/^author (.*)/) {6373$authors{$rev} =$1;6374$authors{$rev} =~s/\s/_/g;6375}6376elsif($line=~/^\t(.*)$/) {6377printf("%6s%10s%s\n",$rev,$authors{$rev},$1);6378}6379}6380}6381 command_close_pipe($fh,$ctx);6382}63836384package Git::SVN::Migration;6385# these version numbers do NOT correspond to actual version numbers6386# of git nor git-svn. They are just relative.6387#6388# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD6389#6390# v1 layout: .git/$id/info/url, refs/remotes/$id6391#6392# v2 layout: .git/svn/$id/info/url, refs/remotes/$id6393#6394# v3 layout: .git/svn/$id, refs/remotes/$id6395# - info/url may remain for backwards compatibility6396# - this is what we migrate up to this layout automatically,6397# - this will be used by git svn init on single branches6398# v3.1 layout (auto migrated):6399# - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink6400# for backwards compatibility6401#6402# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id6403# - this is only created for newly multi-init-ed6404# repositories. Similar in spirit to the6405# --use-separate-remotes option in git-clone (now default)6406# - we do not automatically migrate to this (following6407# the example set by core git)6408#6409# v5 layout: .rev_db.$UUID => .rev_map.$UUID6410# - newer, more-efficient format that uses 24-bytes per record6411# with no filler space.6412# - use xxd -c24 < .rev_map.$UUID to view and debug6413# - This is a one-way migration, repositories updated to the6414# new format will not be able to use old git-svn without6415# rebuilding the .rev_db. Rebuilding the rev_db is not6416# possible if noMetadata or useSvmProps are set; but should6417# be no problem for users that use the (sensible) defaults.6418use strict;6419use warnings;6420use Carp qw/croak/;6421use File::Path qw/mkpath/;6422use File::Basename qw/dirname basename/;6423use vars qw/$_minimize/;64246425sub migrate_from_v0 {6426my$git_dir=$ENV{GIT_DIR};6427returnundefunless-d $git_dir;6428my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6429my$migrated=0;6430while(<$fh>) {6431chomp;6432my($id,$orig_ref) = ($_,$_);6433next unless$id=~ s#^refs/heads/(.+)-HEAD$#$1#;6434next unless-f "$git_dir/$id/info/url";6435my$new_ref="refs/remotes/$id";6436if(::verify_ref("$new_ref^0")) {6437print STDERR "W:$orig_refis probably an old ",6438"branch used by an ancient version of ",6439"git-svn.\n",6440"However,$new_refalso exists.\n",6441"We will not be able ",6442"to use this branch until this ",6443"ambiguity is resolved.\n";6444next;6445}6446print STDERR "Migrating from v0 layout...\n"if!$migrated;6447print STDERR "Renaming ref:$orig_ref=>$new_ref\n";6448 command_noisy('update-ref',$new_ref,$orig_ref);6449 command_noisy('update-ref','-d',$orig_ref,$orig_ref);6450$migrated++;6451}6452 command_close_pipe($fh,$ctx);6453print STDERR "Done migrating from v0 layout...\n"if$migrated;6454$migrated;6455}64566457sub migrate_from_v1 {6458my$git_dir=$ENV{GIT_DIR};6459my$migrated=0;6460return$migratedunless-d $git_dir;6461my$svn_dir="$git_dir/svn";64626463# just in case somebody used 'svn' as their $id at some point...6464return$migratedif-d $svn_dir&& ! -f "$svn_dir/info/url";64656466print STDERR "Migrating from a git-svn v1 layout...\n";6467 mkpath([$svn_dir]);6468print STDERR "Data from a previous version of git-svn exists, but\n\t",6469"$svn_dir\n\t(required for this version ",6470"($::VERSION) of git-svn) does not exist.\n";6471my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6472while(<$fh>) {6473my$x=$_;6474next unless$x=~ s#^refs/remotes/##;6475chomp$x;6476next unless-f "$git_dir/$x/info/url";6477my$u=eval{ ::file_to_s("$git_dir/$x/info/url") };6478next unless$u;6479my$dn= dirname("$git_dir/svn/$x");6480 mkpath([$dn])unless-d $dn;6481if($xeq'svn') {# they used 'svn' as GIT_SVN_ID:6482 mkpath(["$git_dir/svn/svn"]);6483print STDERR " -$git_dir/$x/info=> ",6484"$git_dir/svn/$x/info\n";6485rename"$git_dir/$x/info","$git_dir/svn/$x/info"or6486 croak "$!:$x";6487# don't worry too much about these, they probably6488# don't exist with repos this old (save for index,6489# and we can easily regenerate that)6490foreachmy$f(qw/unhandled.log index .rev_db/) {6491rename"$git_dir/$x/$f","$git_dir/svn/$x/$f";6492}6493}else{6494print STDERR " -$git_dir/$x=>$git_dir/svn/$x\n";6495rename"$git_dir/$x","$git_dir/svn/$x"or6496 croak "$!:$x";6497}6498$migrated++;6499}6500 command_close_pipe($fh,$ctx);6501print STDERR "Done migrating from a git-svn v1 layout\n";6502$migrated;6503}65046505sub read_old_urls {6506my($l_map,$pfx,$path) =@_;6507my@dir;6508foreach(<$path/*>) {6509if(-r "$_/info/url") {6510$pfx.='/'if$pfx&&$pfx!~ m!/$!;6511my$ref_id=$pfx. basename $_;6512my$url= ::file_to_s("$_/info/url");6513$l_map->{$ref_id} =$url;6514}elsif(-d $_) {6515push@dir,$_;6516}6517}6518foreach(@dir) {6519my$x=$_;6520$x=~s!^\Q$ENV{GIT_DIR}\E/svn/!!o;6521 read_old_urls($l_map,$x,$_);6522}6523}65246525sub migrate_from_v2 {6526my@cfg= command(qw/config -l/);6527return ifgrep/^svn-remote\..+\.url=/,@cfg;6528my%l_map;6529 read_old_urls(\%l_map,'',"$ENV{GIT_DIR}/svn");6530my$migrated=0;65316532foreachmy$ref_id(sort keys%l_map) {6533eval{ Git::SVN->init($l_map{$ref_id},'',undef,$ref_id) };6534if($@) {6535 Git::SVN->init($l_map{$ref_id},'',$ref_id,$ref_id);6536}6537$migrated++;6538}6539$migrated;6540}65416542sub minimize_connections {6543my$r= Git::SVN::read_all_remotes();6544my$new_urls= {};6545my$root_repos= {};6546foreachmy$repo_id(keys%$r) {6547my$url=$r->{$repo_id}->{url}ornext;6548my$fetch=$r->{$repo_id}->{fetch}ornext;6549my$ra= Git::SVN::Ra->new($url);65506551# skip existing cases where we already connect to the root6552if(($ra->{url}eq$ra->{repos_root}) ||6553($ra->{repos_root}eq$repo_id)) {6554$root_repos->{$ra->{url}} =$repo_id;6555next;6556}65576558my$root_ra= Git::SVN::Ra->new($ra->{repos_root});6559my$root_path=$ra->{url};6560$root_path=~ s#^\Q$ra->{repos_root}\E(/|$)##;6561foreachmy$path(keys%$fetch) {6562my$ref_id=$fetch->{$path};6563my$gs= Git::SVN->new($ref_id,$repo_id,$path);65646565# make sure we can read when connecting to6566# a higher level of a repository6567my($last_rev,undef) =$gs->last_rev_commit;6568if(!defined$last_rev) {6569$last_rev=eval{6570$root_ra->get_latest_revnum;6571};6572next if$@;6573}6574my$new=$root_path;6575$new.=length$path?"/$path":'';6576eval{6577$root_ra->get_log([$new],$last_rev,$last_rev,65780,0,1,sub{ });6579};6580next if$@;6581$new_urls->{$ra->{repos_root}}->{$new} =6582{ ref_id =>$ref_id,6583 old_repo_id =>$repo_id,6584 old_path =>$path};6585}6586}65876588my@emptied;6589foreachmy$url(keys%$new_urls) {6590# see if we can re-use an existing [svn-remote "repo_id"]6591# instead of creating a(n ugly) new section:6592my$repo_id=$root_repos->{$url} ||$url;65936594my$fetch=$new_urls->{$url};6595foreachmy$path(keys%$fetch) {6596my$x=$fetch->{$path};6597 Git::SVN->init($url,$path,$repo_id,$x->{ref_id});6598my$pfx="svn-remote.$x->{old_repo_id}";65996600my$old_fetch=quotemeta("$x->{old_path}:".6601"$x->{ref_id}");6602 command_noisy(qw/config --unset/,6603"$pfx.fetch",'^'.$old_fetch.'$');6604delete$r->{$x->{old_repo_id}}->6605{fetch}->{$x->{old_path}};6606if(!keys%{$r->{$x->{old_repo_id}}->{fetch}}) {6607 command_noisy(qw/config --unset/,6608"$pfx.url");6609push@emptied,$x->{old_repo_id}6610}6611}6612}6613if(@emptied) {6614my$file=$ENV{GIT_CONFIG} ||"$ENV{GIT_DIR}/config";6615print STDERR <<EOF;6616The following [svn-remote] sections in your config file ($file) are empty6617and can be safely removed:6618EOF6619print STDERR "[svn-remote\"$_\"]\n"foreach@emptied;6620}6621}66226623sub migration_check {6624 migrate_from_v0();6625 migrate_from_v1();6626 migrate_from_v2();6627 minimize_connections()if$_minimize;6628}66296630package Git::IndexInfo;6631use strict;6632use warnings;6633use Git qw/command_input_pipe command_close_pipe/;66346635sub new {6636my($class) =@_;6637my($gui,$ctx) = command_input_pipe(qw/update-index -z --index-info/);6638bless{ gui =>$gui, ctx =>$ctx, nr =>0},$class;6639}66406641sub remove {6642my($self,$path) =@_;6643if(print{$self->{gui} }'0 ',0 x 40,"\t",$path,"\0") {6644return++$self->{nr};6645}6646undef;6647}66486649sub update {6650my($self,$mode,$hash,$path) =@_;6651if(print{$self->{gui} }$mode,' ',$hash,"\t",$path,"\0") {6652return++$self->{nr};6653}6654undef;6655}66566657sub DESTROY {6658my($self) =@_;6659 command_close_pipe($self->{gui},$self->{ctx});6660}66616662package Git::SVN::GlobSpec;6663use strict;6664use warnings;66656666sub new {6667my($class,$glob,$pattern_ok) =@_;6668my$re=$glob;6669$re=~s!/+$!!g;# no need for trailing slashes6670my(@left,@right,@patterns);6671my$state="left";6672my$die_msg="Only one set of wildcard directories ".6673"(e.g. '*' or '*/*/*') is supported: '$glob'\n";6674formy$part(split(m|/|,$glob)) {6675if($part=~/\*/&&$partne"*") {6676die"Invalid pattern in '$glob':$part\n";6677}elsif($pattern_ok&&$part=~/[{}]/&&6678$part!~/^\{[^{}]+\}/) {6679die"Invalid pattern in '$glob':$part\n";6680}6681if($parteq"*") {6682die$die_msgif$stateeq"right";6683$state="pattern";6684push(@patterns,"[^/]*");6685}elsif($pattern_ok&&$part=~/^\{(.*)\}$/) {6686die$die_msgif$stateeq"right";6687$state="pattern";6688my$p=quotemeta($1);6689$p=~s/\\,/|/g;6690push(@patterns,"(?:$p)");6691}else{6692if($stateeq"left") {6693push(@left,$part);6694}else{6695push(@right,$part);6696$state="right";6697}6698}6699}6700my$depth=@patterns;6701if($depth==0) {6702die"One '*' is needed in glob: '$glob'\n";6703}6704my$left=join('/',@left);6705my$right=join('/',@right);6706$re=join('/',@patterns);6707$re=join('\/',6708grep(length,quotemeta($left),"($re)",quotemeta($right)));6709my$left_re=qr/^\/\Q$left\E(\/|$)/;6710bless{ left =>$left, right =>$right, left_regex =>$left_re,6711 regex =>qr/$re/,glob=>$glob, depth =>$depth},$class;6712}67136714sub full_path {6715my($self,$path) =@_;6716return(length$self->{left} ?"$self->{left}/":'') .6717$path. (length$self->{right} ?"/$self->{right}":'');6718}67196720__END__67216722Data structures:672367246725$remotes= {# returned by read_all_remotes()6726'svn'=> {6727# svn-remote.svn.url=https://svn.musicpd.org6728 url =>'https://svn.musicpd.org',6729# svn-remote.svn.fetch=mpd/trunk:trunk6730 fetch => {6731'mpd/trunk'=>'trunk',6732},6733# svn-remote.svn.tags=mpd/tags/*:tags/*6734 tags => {6735 path => {6736 left =>'mpd/tags',6737 right =>'',6738 regex =>qr!mpd/tags/([^/]+)$!,6739glob=>'tags/*',6740},6741ref=> {6742 left =>'tags',6743 right =>'',6744 regex =>qr!tags/([^/]+)$!,6745glob=>'tags/*',6746},6747}6748}6749};67506751$log_entry hashref as returned by libsvn_log_entry()6752{6753log=>'whitespace-formatted log entry6754',# trailing newline is preserved6755 revision =>'8',# integer6756 date =>'2004-02-24T17:01:44.108345Z',# commit date6757 author =>'committer name'6758};675967606761# this is generated by generate_diff();6762@mods= array of diff-index line hashes,each element represents one line6763 of diff-index output67646765diff-index line ($m hash)6766{6767 mode_a => first column of diff-index output,no leading ':',6768 mode_b => second column of diff-index output,6769 sha1_b => sha1sum of the final blob,6770 chg => change type [MCRADT],6771 file_a => original file name of a file (iff chg is'C'or'R')6772 file_b => new/current file name of a file (any chg)6773}6774;67756776# retval of read_url_paths{,_all}();6777$l_map= {6778# repository root url6779'https://svn.musicpd.org'=> {6780# repository path # GIT_SVN_ID6781'mpd/trunk'=>'trunk',6782'mpd/tags/0.11.5'=>'tags/0.11.5',6783},6784}67856786Notes:6787 I don't trust the each() function on unless I created%hashmyself6788 because the internal iterator may not have started at base.