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} &&exists$ENV{GIT_SSH}) { 26$ENV{SVN_SSH} =$ENV{GIT_SSH}; 27} 28 29if(exists$ENV{SVN_SSH} &&$^Oeq'msys') { 30$ENV{SVN_SSH} =~s/\\/\\\\/g; 31$ENV{SVN_SSH} =~s/(.*)/"$1"/; 32} 33 34$Git::SVN::Log::TZ =$ENV{TZ}; 35$ENV{TZ} ='UTC'; 36$| =1;# unbuffer STDOUT 37 38sub fatal (@) {print STDERR "@_\n";exit1} 39 40# All SVN commands do it. Otherwise we may die on SIGPIPE when the remote 41# repository decides to close the connection which we expect to be kept alive. 42$SIG{PIPE} ='IGNORE'; 43 44sub _req_svn { 45require SVN::Core;# use()-ing this causes segfaults for me... *shrug* 46require SVN::Ra; 47require SVN::Delta; 48if($SVN::Core::VERSION lt'1.1.0') { 49 fatal "Need SVN::Core 1.1.0 or better (got$SVN::Core::VERSION)"; 50} 51} 52my$can_compress=eval{require Compress::Zlib;1}; 53push@Git::SVN::Ra::ISA,'SVN::Ra'; 54push@SVN::Git::Editor::ISA,'SVN::Delta::Editor'; 55push@SVN::Git::Fetcher::ISA,'SVN::Delta::Editor'; 56use Carp qw/croak/; 57use Digest::MD5; 58use IO::File qw//; 59use File::Basename qw/dirname basename/; 60use File::Path qw/mkpath/; 61use File::Spec; 62use File::Find; 63use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; 64use IPC::Open3; 65use Git; 66use Memoize;# core since 5.8.0, Jul 2002 67 68BEGIN{ 69# import functions from Git into our packages, en masse 70no strict 'refs'; 71foreach(qw/command command_oneline command_noisy command_output_pipe 72 command_input_pipe command_close_pipe 73 command_bidi_pipe command_close_bidi_pipe/) { 74formy$package(qw(SVN::Git::Editor SVN::Git::Fetcher 75 Git::SVN::Migration Git::SVN::Log Git::SVN), 76 __PACKAGE__) { 77*{"${package}::$_"} = \&{"Git::$_"}; 78} 79} 80 Memoize::memoize 'Git::config'; 81 Memoize::memoize 'Git::config_bool'; 82} 83 84my($SVN); 85 86$sha1=qr/[a-f\d]{40}/; 87$sha1_short=qr/[a-f\d]{4,40}/; 88my($_stdin,$_help,$_edit, 89$_message,$_file,$_branch_dest, 90$_template,$_shared, 91$_version,$_fetch_all,$_no_rebase,$_fetch_parent, 92$_merge,$_strategy,$_dry_run,$_local, 93$_prefix,$_no_checkout,$_url,$_verbose, 94$_git_format,$_commit_url,$_tag,$_merge_info,$_interactive); 95$Git::SVN::_follow_parent =1; 96$SVN::Git::Fetcher::_placeholder_filename =".gitignore"; 97$_q||=0; 98my%remote_opts= ('username=s'=> \$Git::SVN::Prompt::_username, 99'config-dir=s'=> \$Git::SVN::Ra::config_dir, 100'no-auth-cache'=> \$Git::SVN::Prompt::_no_auth_cache, 101'ignore-paths=s'=> \$SVN::Git::Fetcher::_ignore_regex, 102'ignore-refs=s'=> \$Git::SVN::Ra::_ignore_refs_regex ); 103my%fc_opts= ('follow-parent|follow!'=> \$Git::SVN::_follow_parent, 104'authors-file|A=s'=> \$_authors, 105'authors-prog=s'=> \$_authors_prog, 106'repack:i'=> \$Git::SVN::_repack, 107'noMetadata'=> \$Git::SVN::_no_metadata, 108'useSvmProps'=> \$Git::SVN::_use_svm_props, 109'useSvnsyncProps'=> \$Git::SVN::_use_svnsync_props, 110'log-window-size=i'=> \$Git::SVN::Ra::_log_window_size, 111'no-checkout'=> \$_no_checkout, 112'quiet|q+'=> \$_q, 113'repack-flags|repack-args|repack-opts=s'=> 114 \$Git::SVN::_repack_flags, 115'use-log-author'=> \$Git::SVN::_use_log_author, 116'add-author-from'=> \$Git::SVN::_add_author_from, 117'localtime'=> \$Git::SVN::_localtime, 118%remote_opts); 119 120my($_trunk,@_tags,@_branches,$_stdlayout); 121my%icv; 122my%init_opts= ('template=s'=> \$_template,'shared:s'=> \$_shared, 123'trunk|T=s'=> \$_trunk,'tags|t=s@'=> \@_tags, 124'branches|b=s@'=> \@_branches,'prefix=s'=> \$_prefix, 125'stdlayout|s'=> \$_stdlayout, 126'minimize-url|m!'=> \$Git::SVN::_minimize_url, 127'no-metadata'=>sub{$icv{noMetadata} =1}, 128'use-svm-props'=>sub{$icv{useSvmProps} =1}, 129'use-svnsync-props'=>sub{$icv{useSvnsyncProps} =1}, 130'rewrite-root=s'=>sub{$icv{rewriteRoot} =$_[1] }, 131'rewrite-uuid=s'=>sub{$icv{rewriteUUID} =$_[1] }, 132%remote_opts); 133my%cmt_opts= ('edit|e'=> \$_edit, 134'rmdir'=> \$SVN::Git::Editor::_rmdir, 135'find-copies-harder'=> \$SVN::Git::Editor::_find_copies_harder, 136'l=i'=> \$SVN::Git::Editor::_rename_limit, 137'copy-similarity|C=i'=> \$SVN::Git::Editor::_cp_similarity 138); 139 140my%cmd= ( 141 fetch => [ \&cmd_fetch,"Download new revisions from SVN", 142{'revision|r=s'=> \$_revision, 143'fetch-all|all'=> \$_fetch_all, 144'parent|p'=> \$_fetch_parent, 145%fc_opts} ], 146 clone => [ \&cmd_clone,"Initialize and fetch revisions", 147{'revision|r=s'=> \$_revision, 148'preserve-empty-dirs'=> 149 \$SVN::Git::Fetcher::_preserve_empty_dirs, 150'placeholder-filename=s'=> 151 \$SVN::Git::Fetcher::_placeholder_filename, 152%fc_opts,%init_opts} ], 153 init => [ \&cmd_init,"Initialize a repo for tracking". 154" (requires URL argument)", 155 \%init_opts], 156'multi-init'=> [ \&cmd_multi_init, 157"Deprecated alias for ". 158"'$0init -T<trunk> -b<branches> -t<tags>'", 159 \%init_opts], 160 dcommit => [ \&cmd_dcommit, 161'Commit several diffs to merge with upstream', 162{'merge|m|M'=> \$_merge, 163'strategy|s=s'=> \$_strategy, 164'verbose|v'=> \$_verbose, 165'dry-run|n'=> \$_dry_run, 166'fetch-all|all'=> \$_fetch_all, 167'commit-url=s'=> \$_commit_url, 168'revision|r=i'=> \$_revision, 169'no-rebase'=> \$_no_rebase, 170'mergeinfo=s'=> \$_merge_info, 171'interactive|i'=> \$_interactive, 172%cmt_opts,%fc_opts} ], 173 branch => [ \&cmd_branch, 174'Create a branch in the SVN repository', 175{'message|m=s'=> \$_message, 176'destination|d=s'=> \$_branch_dest, 177'dry-run|n'=> \$_dry_run, 178'tag|t'=> \$_tag, 179'username=s'=> \$Git::SVN::Prompt::_username, 180'commit-url=s'=> \$_commit_url} ], 181 tag => [sub{$_tag=1; cmd_branch(@_) }, 182'Create a tag in the SVN repository', 183{'message|m=s'=> \$_message, 184'destination|d=s'=> \$_branch_dest, 185'dry-run|n'=> \$_dry_run, 186'username=s'=> \$Git::SVN::Prompt::_username, 187'commit-url=s'=> \$_commit_url} ], 188'set-tree'=> [ \&cmd_set_tree, 189"Set an SVN repository to a git tree-ish", 190{'stdin'=> \$_stdin,%cmt_opts,%fc_opts, } ], 191'create-ignore'=> [ \&cmd_create_ignore, 192'Create a .gitignore per svn:ignore', 193{'revision|r=i'=> \$_revision 194} ], 195'mkdirs'=> [ \&cmd_mkdirs , 196"recreate empty directories after a checkout", 197{'revision|r=i'=> \$_revision} ], 198'propget'=> [ \&cmd_propget, 199'Print the value of a property on a file or directory', 200{'revision|r=i'=> \$_revision} ], 201'proplist'=> [ \&cmd_proplist, 202'List all properties of a file or directory', 203{'revision|r=i'=> \$_revision} ], 204'show-ignore'=> [ \&cmd_show_ignore,"Show svn:ignore listings", 205{'revision|r=i'=> \$_revision 206} ], 207'show-externals'=> [ \&cmd_show_externals,"Show svn:externals listings", 208{'revision|r=i'=> \$_revision 209} ], 210'multi-fetch'=> [ \&cmd_multi_fetch, 211"Deprecated alias for$0fetch --all", 212{'revision|r=s'=> \$_revision,%fc_opts} ], 213'migrate'=> [sub{ }, 214# no-op, we automatically run this anyways, 215'Migrate configuration/metadata/layout from 216 previous versions of git-svn', 217{'minimize'=> \$Git::SVN::Migration::_minimize, 218%remote_opts} ], 219'log'=> [ \&Git::SVN::Log::cmd_show_log,'Show commit logs', 220{'limit=i'=> \$Git::SVN::Log::limit, 221'revision|r=s'=> \$_revision, 222'verbose|v'=> \$Git::SVN::Log::verbose, 223'incremental'=> \$Git::SVN::Log::incremental, 224'oneline'=> \$Git::SVN::Log::oneline, 225'show-commit'=> \$Git::SVN::Log::show_commit, 226'non-recursive'=> \$Git::SVN::Log::non_recursive, 227'authors-file|A=s'=> \$_authors, 228'color'=> \$Git::SVN::Log::color, 229'pager=s'=> \$Git::SVN::Log::pager 230} ], 231'find-rev'=> [ \&cmd_find_rev, 232"Translate between SVN revision numbers and tree-ish", 233{} ], 234'rebase'=> [ \&cmd_rebase,"Fetch and rebase your working directory", 235{'merge|m|M'=> \$_merge, 236'verbose|v'=> \$_verbose, 237'strategy|s=s'=> \$_strategy, 238'local|l'=> \$_local, 239'fetch-all|all'=> \$_fetch_all, 240'dry-run|n'=> \$_dry_run, 241%fc_opts} ], 242'commit-diff'=> [ \&cmd_commit_diff, 243'Commit a diff between two trees', 244{'message|m=s'=> \$_message, 245'file|F=s'=> \$_file, 246'revision|r=s'=> \$_revision, 247%cmt_opts} ], 248'info'=> [ \&cmd_info, 249"Show info about the latest SVN revision 250 on the current branch", 251{'url'=> \$_url, } ], 252'blame'=> [ \&Git::SVN::Log::cmd_blame, 253"Show what revision and author last modified each line of a file", 254{'git-format'=> \$_git_format} ], 255'reset'=> [ \&cmd_reset, 256"Undo fetches back to the specified SVN revision", 257{'revision|r=s'=> \$_revision, 258'parent|p'=> \$_fetch_parent} ], 259'gc'=> [ \&cmd_gc, 260"Compress unhandled.log files in .git/svn and remove ". 261"index files in .git/svn", 262{} ], 263); 264 265use Term::ReadLine; 266package FakeTerm; 267sub new { 268my($class,$reason) =@_; 269returnbless \$reason,shift; 270} 271subreadline{ 272my$self=shift; 273die"Cannot use readline on FakeTerm:$$self"; 274} 275package main; 276 277my$term=eval{ 278$ENV{"GIT_SVN_NOTTY"} 279? new Term::ReadLine 'git-svn', \*STDIN, \*STDOUT 280: new Term::ReadLine 'git-svn'; 281}; 282if($@) { 283$term= new FakeTerm "$@: going non-interactive"; 284} 285 286my$cmd; 287for(my$i=0;$i<@ARGV;$i++) { 288if(defined$cmd{$ARGV[$i]}) { 289$cmd=$ARGV[$i]; 290splice@ARGV,$i,1; 291last; 292}elsif($ARGV[$i]eq'help') { 293$cmd=$ARGV[$i+1]; 294 usage(0); 295} 296}; 297 298# make sure we're always running at the top-level working directory 299unless($cmd&&$cmd=~/(?:clone|init|multi-init)$/) { 300unless(-d $ENV{GIT_DIR}) { 301if($git_dir_user_set) { 302die"GIT_DIR=$ENV{GIT_DIR} explicitly set, ", 303"but it is not a directory\n"; 304} 305my$git_dir=delete$ENV{GIT_DIR}; 306my$cdup=undef; 307 git_cmd_try { 308$cdup= command_oneline(qw/rev-parse --show-cdup/); 309$git_dir='.'unless($cdup); 310chomp$cdupif($cdup); 311$cdup="."unless($cdup&&length$cdup); 312}"Already at toplevel, but$git_dirnot found\n"; 313chdir$cdupor die"Unable to chdir up to '$cdup'\n"; 314unless(-d $git_dir) { 315die"$git_dirstill not found after going to ", 316"'$cdup'\n"; 317} 318$ENV{GIT_DIR} =$git_dir; 319} 320$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 321} 322 323my%opts= %{$cmd{$cmd}->[2]}if(defined$cmd); 324 325read_git_config(\%opts); 326if($cmd&& ($cmdeq'log'||$cmdeq'blame')) { 327 Getopt::Long::Configure('pass_through'); 328} 329my$rv= GetOptions(%opts,'h|H'=> \$_help,'version|V'=> \$_version, 330'minimize-connections'=> \$Git::SVN::Migration::_minimize, 331'id|i=s'=> \$Git::SVN::default_ref_id, 332'svn-remote|remote|R=s'=>sub{ 333$Git::SVN::no_reuse_existing =1; 334$Git::SVN::default_repo_id =$_[1] }); 335exit1if(!$rv&&$cmd&&$cmdne'log'); 336 337usage(0)if$_help; 338version()if$_version; 339usage(1)unlessdefined$cmd; 340load_authors()if$_authors; 341if(defined$_authors_prog) { 342$_authors_prog="'". File::Spec->rel2abs($_authors_prog) ."'"; 343} 344 345unless($cmd=~/^(?:clone|init|multi-init|commit-diff)$/) { 346 Git::SVN::Migration::migration_check(); 347} 348Git::SVN::init_vars(); 349eval{ 350 Git::SVN::verify_remotes_sanity(); 351$cmd{$cmd}->[0]->(@ARGV); 352}; 353fatal $@if$@; 354post_fetch_checkout(); 355exit0; 356 357####################### primary functions ###################### 358sub usage { 359my$exit=shift||0; 360my$fd=$exit? \*STDERR : \*STDOUT; 361print$fd<<""; 362git-svn - bidirectional operations between a single Subversion tree and git 363Usage: git svn <command> [options] [arguments]\n 364 365print$fd"Available commands:\n"unless$cmd; 366 367foreach(sort keys%cmd) { 368next if$cmd&&$cmdne$_; 369next if/^multi-/;# don't show deprecated commands 370print$fd' ',pack('A17',$_),$cmd{$_}->[1],"\n"; 371foreach(sort keys%{$cmd{$_}->[2]}) { 372# mixed-case options are for .git/config only 373next if/[A-Z]/&&/^[a-z]+$/i; 374# prints out arguments as they should be passed: 375my$x= s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : ''; 376print$fd' ' x 21,join(', ',map{length$_>1? 377"--$_":"-$_"} 378split/\|/,$_),"$x\n"; 379} 380} 381print$fd<<""; 382\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an 383arbitrary identifier if you're tracking multiple SVN branches/repositories in 384one git repository and want to keep them separate. See git-svn(1) for more 385information. 386 387 exit$exit; 388} 389 390sub version { 391 ::_req_svn(); 392 print "git-svn version$VERSION(svn$SVN::Core::VERSION)\n"; 393 exit 0; 394} 395 396sub ask { 397 my ($prompt,%arg) =@_; 398 my$valid_re=$arg{valid_re}; 399 my$default=$arg{default}; 400 my$resp; 401 my$i= 0; 402 403 if ( !( defined($term->IN) 404 && defined( fileno($term->IN) ) 405 && defined($term->OUT ) 406 && defined( fileno($term->OUT) ) ) ){ 407 return defined($default) ?$default: undef; 408 } 409 410 while ($i++< 10) { 411$resp=$term->readline($prompt); 412 if (!defined$resp) { # EOF 413 print "\n"; 414 return defined$default?$default: undef; 415 } 416 if ($respeq '' and defined$default) { 417 return$default; 418 } 419 if (!defined$valid_reor$resp=~ /$valid_re/) { 420 return$resp; 421 } 422 } 423 return undef; 424} 425 426sub do_git_init_db { 427 unless (-d$ENV{GIT_DIR}) { 428 my@init_db= ('init'); 429 push@init_db, "--template=$_template" if defined$_template; 430 if (defined$_shared) { 431 if ($_shared=~ /[a-z]/) { 432 push@init_db, "--shared=$_shared"; 433 } else { 434 push@init_db, "--shared"; 435 } 436 } 437 command_noisy(@init_db); 438$_repository= Git->repository(Repository => ".git"); 439 } 440 my$set; 441 my$pfx= "svn-remote.$Git::SVN::default_repo_id"; 442 foreach my$i(keys%icv) { 443 die "'$set' and '$i' cannot both be set\n" if$set; 444 next unless defined$icv{$i}; 445 command_noisy('config', "$pfx.$i",$icv{$i}); 446$set=$i; 447 } 448 my$ignore_paths_regex= \$SVN::Git::Fetcher::_ignore_regex; 449 command_noisy('config', "$pfx.ignore-paths",$$ignore_paths_regex) 450 if defined$$ignore_paths_regex; 451 my$ignore_refs_regex= \$Git::SVN::Ra::_ignore_refs_regex; 452 command_noisy('config', "$pfx.ignore-refs",$$ignore_refs_regex) 453 if defined$$ignore_refs_regex; 454 455 if (defined$SVN::Git::Fetcher::_preserve_empty_dirs) { 456 my$fname= \$SVN::Git::Fetcher::_placeholder_filename; 457 command_noisy('config', "$pfx.preserve-empty-dirs", 'true'); 458 command_noisy('config', "$pfx.placeholder-filename",$$fname); 459 } 460} 461 462sub init_subdir { 463 my$repo_path= shift or return; 464 mkpath([$repo_path]) unless -d$repo_path; 465 chdir$repo_pathor die "Couldn't chdir to $repo_path:$!\n"; 466$ENV{GIT_DIR} = '.git'; 467$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 468} 469 470sub cmd_clone { 471 my ($url,$path) =@_; 472 if (!defined$path&& 473 (defined$_trunk||@_branches||@_tags|| 474 defined$_stdlayout) && 475$url!~ m#^[a-z\+]+://#) { 476$path=$url; 477 } 478$path= basename($url) if !defined$path|| !length$path; 479 my$authors_absolute=$_authors? File::Spec->rel2abs($_authors) : ""; 480 cmd_init($url,$path); 481 command_oneline('config', 'svn.authorsfile',$authors_absolute) 482 if$_authors; 483 Git::SVN::fetch_all($Git::SVN::default_repo_id); 484} 485 486sub cmd_init { 487 if (defined$_stdlayout) { 488$_trunk= 'trunk' if (!defined$_trunk); 489@_tags= 'tags' if (!@_tags); 490@_branches= 'branches' if (!@_branches); 491 } 492 if (defined$_trunk||@_branches||@_tags) { 493 return cmd_multi_init(@_); 494 } 495 my$url= shift or die "SVN repository location required ", 496 "as a command-line argument\n"; 497$url= canonicalize_url($url); 498 init_subdir(@_); 499 do_git_init_db(); 500 501 if ($Git::SVN::_minimize_url eq 'unset') { 502$Git::SVN::_minimize_url = 0; 503 } 504 505 Git::SVN->init($url); 506} 507 508sub cmd_fetch { 509 if (grep /^\d+=./,@_) { 510 die "'<rev>=<commit>' fetch arguments are ", 511 "no longer supported.\n"; 512 } 513 my ($remote) =@_; 514 if (@_> 1) { 515 die "Usage:$0 fetch [--all] [--parent] [svn-remote]\n"; 516 } 517$Git::SVN::no_reuse_existing = undef; 518 if ($_fetch_parent) { 519 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD'); 520 unless ($gs) { 521 die "Unable to determine upstream SVN information from ", 522 "working tree history\n"; 523 } 524 # just fetch, don't checkout. 525$_no_checkout= 'true'; 526$_fetch_all?$gs->fetch_all :$gs->fetch; 527 } elsif ($_fetch_all) { 528 cmd_multi_fetch(); 529 } else { 530$remote||=$Git::SVN::default_repo_id; 531 Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes()); 532 } 533} 534 535sub cmd_set_tree { 536 my (@commits) =@_; 537 if ($_stdin|| !@commits) { 538 print "Reading from stdin...\n"; 539@commits= (); 540 while (<STDIN>) { 541 if (/\b($sha1_short)\b/o) { 542 unshift@commits,$1; 543 } 544 } 545 } 546 my@revs; 547 foreach my$c(@commits) { 548 my@tmp= command('rev-parse',$c); 549 if (scalar@tmp== 1) { 550 push@revs,$tmp[0]; 551 } elsif (scalar@tmp> 1) { 552 push@revs, reverse(command('rev-list',@tmp)); 553 } else { 554 fatal "Failed to rev-parse $c"; 555 } 556 } 557 my$gs= Git::SVN->new; 558 my ($r_last,$cmt_last) =$gs->last_rev_commit; 559$gs->fetch; 560 if (defined$gs->{last_rev} &&$r_last!=$gs->{last_rev}) { 561 fatal "There are new revisions that were fetched ", 562 "and need to be merged (or acknowledged)", 563 "before committing.\nlast rev:$r_last\n", 564 " current:$gs->{last_rev}"; 565 } 566$gs->set_tree($_) foreach@revs; 567 print "Done committing ",scalar@revs," revisions to SVN\n"; 568 unlink$gs->{index}; 569} 570 571sub split_merge_info_range { 572 my ($range) =@_; 573 if ($range=~ /(\d+)-(\d+)/) { 574 return (int($1), int($2)); 575 } else { 576 return (int($range), int($range)); 577 } 578} 579 580sub combine_ranges { 581 my ($in) =@_; 582 583 my@fnums= (); 584 my@arr= split(/,/,$in); 585 for my$element(@arr) { 586 my ($start,$end) = split_merge_info_range($element); 587 push@fnums,$start; 588 } 589 590 my@sorted=@arr[ sort { 591$fnums[$a] <=>$fnums[$b] 592 } 0..$#arr]; 593 594 my@return= (); 595 my$last= -1; 596 my$first= -1; 597 for my$element(@sorted) { 598 my ($start,$end) = split_merge_info_range($element); 599 600 if ($last== -1) { 601$first=$start; 602$last=$end; 603 next; 604 } 605 if ($start<=$last+1) { 606 if ($end>$last) { 607$last=$end; 608 } 609 next; 610 } 611 if ($first==$last) { 612 push@return, "$first"; 613 } else { 614 push@return, "$first-$last"; 615 } 616$first=$start; 617$last=$end; 618 } 619 620 if ($first!= -1) { 621 if ($first==$last) { 622 push@return, "$first"; 623 } else { 624 push@return, "$first-$last"; 625 } 626 } 627 628 return join(',',@return); 629} 630 631sub merge_revs_into_hash { 632 my ($hash,$minfo) =@_; 633 my@lines= split(' ',$minfo); 634 635 for my$line(@lines) { 636 my ($branchpath,$revs) = split(/:/,$line); 637 638 if (exists($hash->{$branchpath})) { 639 # Merge the two revision sets 640 my$combined= "$hash->{$branchpath},$revs"; 641$hash->{$branchpath} = combine_ranges($combined); 642 } else { 643 # Just do range combining for consolidation 644$hash->{$branchpath} = combine_ranges($revs); 645 } 646 } 647} 648 649sub merge_merge_info { 650 my ($mergeinfo_one,$mergeinfo_two) =@_; 651 my%result_hash= (); 652 653 merge_revs_into_hash(\%result_hash,$mergeinfo_one); 654 merge_revs_into_hash(\%result_hash,$mergeinfo_two); 655 656 my$result= ''; 657 # Sort below is for consistency's sake 658 for my$branchname(sort keys(%result_hash)) { 659 my$revlist=$result_hash{$branchname}; 660$result.= "$branchname:$revlist\n" 661 } 662 return$result; 663} 664 665sub populate_merge_info { 666 my ($d,$gs,$uuid,$linear_refs,$rewritten_parent) =@_; 667 668 my%parentshash; 669 read_commit_parents(\%parentshash,$d); 670 my@parents= @{$parentshash{$d}}; 671 if ($#parents> 0) { 672 # Merge commit 673 my$all_parents_ok= 1; 674 my$aggregate_mergeinfo= ''; 675 my$rooturl=$gs->repos_root; 676 677 if (defined($rewritten_parent)) { 678 # Replace first parent with newly-rewritten version 679 shift@parents; 680 unshift@parents,$rewritten_parent; 681 } 682 683 foreach my$parent(@parents) { 684 my ($branchurl,$svnrev,$paruuid) = 685 cmt_metadata($parent); 686 687 unless (defined($svnrev)) { 688 # Should have been caught be preflight check 689 fatal "merge commit $dhas ancestor $parent, but that change " 690 ."doesnot have git-svn metadata!"; 691 } 692 unless ($branchurl=~ /^\Q$rooturl\E(.*)/) { 693 fatal "commit $parent git-svn metadata changed mid-run!"; 694 } 695 my$branchpath=$1; 696 697 my$ra= Git::SVN::Ra->new($branchurl); 698 my (undef, undef,$props) = 699$ra->get_dir(canonicalize_path("."),$svnrev); 700 my$par_mergeinfo=$props->{'svn:mergeinfo'}; 701 unless (defined$par_mergeinfo) { 702$par_mergeinfo= ''; 703 } 704 # Merge previous mergeinfo values 705$aggregate_mergeinfo= 706 merge_merge_info($aggregate_mergeinfo, 707$par_mergeinfo, 0); 708 709 next if$parenteq$parents[0]; # Skip first parent 710 # Add new changes being placed in tree by merge 711 my@cmd= (qw/rev-list --reverse/, 712$parent, qw/--not/); 713 foreach my$par(@parents) { 714 unless ($pareq$parent) { 715 push@cmd,$par; 716 } 717 } 718 my@revsin= (); 719 my ($revlist,$ctx) = command_output_pipe(@cmd); 720 while (<$revlist>) { 721 my$irev=$_; 722 chomp$irev; 723 my (undef,$csvnrev, undef) = 724 cmt_metadata($irev); 725 unless (defined$csvnrev) { 726 # A child is missing SVN annotations... 727 # this might be OK, or might not be. 728 warn "W:child $irevis merged into revision " 729 ."$d but doesnot have git-svn metadata." 730 ."This means git-svn cannot determine the " 731 ."svn revision numbers to place into the " 732 ."svn:mergeinfo property. You must ensure " 733 ."a branch is entirely committed to " 734 ."SVN before merging it in order for" 735 ."svn:mergeinfo population to function " 736 ."properly"; 737 } 738 push@revsin,$csvnrev; 739 } 740 command_close_pipe($revlist,$ctx); 741 742 last unless$all_parents_ok; 743 744 # We now have a list of all SVN revnos which are 745 # merged by this particular parent. Integrate them. 746 next if$#revsin== -1; 747 my$newmergeinfo= "$branchpath:" . join(',',@revsin); 748$aggregate_mergeinfo= 749 merge_merge_info($aggregate_mergeinfo, 750$newmergeinfo, 1); 751 } 752 if ($all_parents_okand$aggregate_mergeinfo) { 753 return$aggregate_mergeinfo; 754 } 755 } 756 757 return undef; 758} 759 760sub cmd_dcommit { 761 my$head= shift; 762 command_noisy(qw/update-index --refresh/); 763 git_cmd_try { command_oneline(qw/diff-index --quiet HEAD/) } 764 'Cannot dcommit with a dirty index. Commit your changes first, ' 765 . "or stash them with `git stash'.\n"; 766$head||= 'HEAD'; 767 768 my$old_head; 769 if ($headne 'HEAD') { 770$old_head= eval { 771 command_oneline([qw/symbolic-ref -q HEAD/]) 772 }; 773 if ($old_head) { 774$old_head=~ s{^refs/heads/}{}; 775 } else { 776$old_head= eval { command_oneline(qw/rev-parse HEAD/) }; 777 } 778 command(['checkout',$head], STDERR => 0); 779 } 780 781 my@refs; 782 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD', \@refs); 783 unless ($gs) { 784 die "Unable to determine upstream SVN information from ", 785 "$headhistory.\nPerhaps the repository is empty."; 786 } 787 788 if (defined$_commit_url) { 789$url=$_commit_url; 790 } else { 791$url= eval { command_oneline('config', '--get', 792 "svn-remote.$gs->{repo_id}.commiturl") }; 793 if (!$url) { 794$url=$gs->full_pushurl 795 } 796 } 797 798 my$last_rev=$_revisionif defined$_revision; 799 if ($url) { 800 print "Committing to$url...\n"; 801 } 802 my ($linear_refs,$parents) = linearize_history($gs, \@refs); 803 if ($_no_rebase&& scalar(@$linear_refs) > 1) { 804 warn "Attempting to commit more than one change while ", 805 "--no-rebase is enabled.\n", 806 "If these changes depend on each other, re-running ", 807 "without --no-rebase may be required." 808 } 809 810 if (defined$_interactive){ 811 my$ask_default= "y"; 812 foreach my$d(@$linear_refs){ 813 my ($fh,$ctx) = command_output_pipe(qw(show --summary),"$d"); 814while(<$fh>){ 815print$_; 816} 817 command_close_pipe($fh,$ctx); 818$_= ask("Commit this patch to SVN? ([y]es (default)|[n]o|[q]uit|[a]ll): ", 819 valid_re =>qr/^(?:yes|y|no|n|quit|q|all|a)/i, 820default=>$ask_default); 821die"Commit this patch reply required"unlessdefined$_; 822if(/^[nq]/i) { 823exit(0); 824}elsif(/^a/i) { 825last; 826} 827} 828} 829 830my$expect_url=$url; 831 832my$push_merge_info=eval{ 833 command_oneline(qw/config --get svn.pushmergeinfo/) 834}; 835if(not defined($push_merge_info) 836or$push_merge_infoeq"false" 837or$push_merge_infoeq"no" 838or$push_merge_infoeq"never") { 839$push_merge_info=0; 840} 841 842unless(defined($_merge_info) || !$push_merge_info) { 843# Preflight check of changes to ensure no issues with mergeinfo 844# This includes check for uncommitted-to-SVN parents 845# (other than the first parent, which we will handle), 846# information from different SVN repos, and paths 847# which are not underneath this repository root. 848my$rooturl=$gs->repos_root; 849foreachmy$d(@$linear_refs) { 850my%parentshash; 851 read_commit_parents(\%parentshash,$d); 852my@realparents= @{$parentshash{$d}}; 853if($#realparents>0) { 854# Merge commit 855shift@realparents;# Remove/ignore first parent 856foreachmy$parent(@realparents) { 857my($branchurl,$svnrev,$paruuid) = cmt_metadata($parent); 858unless(defined$paruuid) { 859# A parent is missing SVN annotations... 860# abort the whole operation. 861 fatal "$parentis merged into revision$d, " 862."but does not have git-svn metadata. " 863."Either dcommit the branch or use a " 864."local cherry-pick, FF merge, or rebase " 865."instead of an explicit merge commit."; 866} 867 868unless($paruuideq$uuid) { 869# Parent has SVN metadata from different repository 870 fatal "merge parent$parentfor change$dhas " 871."git-svn uuid$paruuid, while current change " 872."has uuid$uuid!"; 873} 874 875unless($branchurl=~/^\Q$rooturl\E(.*)/) { 876# This branch is very strange indeed. 877 fatal "merge parent$parentfor$dis on branch " 878."$branchurl, which is not under the " 879."git-svn root$rooturl!"; 880} 881} 882} 883} 884} 885 886my$rewritten_parent; 887 Git::SVN::remove_username($expect_url); 888if(defined($_merge_info)) { 889$_merge_info=~tr{ }{\n}; 890} 891while(1) { 892my$d=shift@$linear_refsorlast; 893unless(defined$last_rev) { 894(undef,$last_rev,undef) = cmt_metadata("$d~1"); 895unless(defined$last_rev) { 896 fatal "Unable to extract revision information ", 897"from commit$d~1"; 898} 899} 900if($_dry_run) { 901print"diff-tree$d~1$d\n"; 902}else{ 903my$cmt_rev; 904 905unless(defined($_merge_info) || !$push_merge_info) { 906$_merge_info= populate_merge_info($d,$gs, 907$uuid, 908$linear_refs, 909$rewritten_parent); 910} 911 912my%ed_opts= ( r =>$last_rev, 913log=> get_commit_entry($d)->{log}, 914 ra => Git::SVN::Ra->new($url), 915 config => SVN::Core::config_get_config( 916$Git::SVN::Ra::config_dir 917), 918 tree_a =>"$d~1", 919 tree_b =>$d, 920 editor_cb =>sub{ 921print"Committed r$_[0]\n"; 922$cmt_rev=$_[0]; 923}, 924 mergeinfo =>$_merge_info, 925 svn_path =>''); 926if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) { 927print"No changes\n$d~1==$d\n"; 928}elsif($parents->{$d} && @{$parents->{$d}}) { 929$gs->{inject_parents_dcommit}->{$cmt_rev} = 930$parents->{$d}; 931} 932$_fetch_all?$gs->fetch_all:$gs->fetch; 933$last_rev=$cmt_rev; 934next if$_no_rebase; 935 936# we always want to rebase against the current HEAD, 937# not any head that was passed to us 938my@diff= command('diff-tree',$d, 939$gs->refname,'--'); 940my@finish; 941if(@diff) { 942@finish= rebase_cmd(); 943print STDERR "W:$dand ",$gs->refname, 944" differ, using@finish:\n", 945join("\n",@diff),"\n"; 946}else{ 947print"No changes between current HEAD and ", 948$gs->refname, 949"\nResetting to the latest ", 950$gs->refname,"\n"; 951@finish= qw/reset --mixed/; 952} 953 command_noisy(@finish,$gs->refname); 954 955$rewritten_parent= command_oneline(qw/rev-parse HEAD/); 956 957if(@diff) { 958@refs= (); 959my($url_,$rev_,$uuid_,$gs_) = 960 working_head_info('HEAD', \@refs); 961my($linear_refs_,$parents_) = 962 linearize_history($gs_, \@refs); 963if(scalar(@$linear_refs) != 964scalar(@$linear_refs_)) { 965 fatal "# of revisions changed ", 966"\nbefore:\n", 967join("\n",@$linear_refs), 968"\n\nafter:\n", 969join("\n",@$linear_refs_),"\n", 970'If you are attempting to commit ', 971"merges, try running:\n\t", 972'git rebase --interactive', 973'--preserve-merges ', 974$gs->refname, 975"\nBefore dcommitting"; 976} 977if($url_ne$expect_url) { 978if($url_eq$gs->metadata_url) { 979print 980"Accepting rewritten URL:", 981"$url_\n"; 982}else{ 983 fatal 984"URL mismatch after rebase:", 985"$url_!=$expect_url"; 986} 987} 988if($uuid_ne$uuid) { 989 fatal "uuid mismatch after rebase: ", 990"$uuid_!=$uuid"; 991} 992# remap parents 993my(%p,@l,$i); 994for($i=0;$i<scalar@$linear_refs;$i++) { 995my$new=$linear_refs_->[$i]ornext; 996$p{$new} = 997$parents->{$linear_refs->[$i]}; 998push@l,$new; 999}1000$parents= \%p;1001$linear_refs= \@l;1002}1003}1004}10051006if($old_head) {1007my$new_head= command_oneline(qw/rev-parse HEAD/);1008my$new_is_symbolic=eval{1009 command_oneline(qw/symbolic-ref -q HEAD/);1010};1011if($new_is_symbolic) {1012print"dcommitted the branch ",$head,"\n";1013}else{1014print"dcommitted on a detached HEAD because you gave ",1015"a revision argument.\n",1016"The rewritten commit is: ",$new_head,"\n";1017}1018 command(['checkout',$old_head], STDERR =>0);1019}10201021unlink$gs->{index};1022}10231024sub cmd_branch {1025my($branch_name,$head) =@_;10261027unless(defined$branch_name&&length$branch_name) {1028die(($_tag?"tag":"branch") ." name required\n");1029}1030$head||='HEAD';10311032my(undef,$rev,undef,$gs) = working_head_info($head);1033my$src=$gs->full_pushurl;10341035my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1036my$allglobs=$remote->{$_tag?'tags':'branches'};1037my$glob;1038if($#{$allglobs} ==0) {1039$glob=$allglobs->[0];1040}else{1041unless(defined$_branch_dest) {1042die"Multiple ",1043$_tag?"tag":"branch",1044" paths defined for Subversion repository.\n",1045"You must specify where you want to create the ",1046$_tag?"tag":"branch",1047" with the --destination argument.\n";1048}1049foreachmy$g(@{$allglobs}) {1050# SVN::Git::Editor could probably be moved to Git.pm..1051my$re= SVN::Git::Editor::glob2pat($g->{path}->{left});1052if($_branch_dest=~/$re/) {1053$glob=$g;1054last;1055}1056}1057unless(defined$glob) {1058my$dest_re=qr/\b\Q$_branch_dest\E\b/;1059foreachmy$g(@{$allglobs}) {1060$g->{path}->{left} =~/$dest_re/ornext;1061if(defined$glob) {1062die"Ambiguous destination: ",1063$_branch_dest,"\nmatches both '",1064$glob->{path}->{left},"' and '",1065$g->{path}->{left},"'\n";1066}1067$glob=$g;1068}1069unless(defined$glob) {1070die"Unknown ",1071$_tag?"tag":"branch",1072" destination$_branch_dest\n";1073}1074}1075}1076my($lft,$rgt) = @{$glob->{path} }{qw/left right/};1077my$url;1078if(defined$_commit_url) {1079$url=$_commit_url;1080}else{1081$url=eval{ command_oneline('config','--get',1082"svn-remote.$gs->{repo_id}.commiturl") };1083if(!$url) {1084$url=$remote->{pushurl} ||$remote->{url};1085}1086}1087my$dst=join'/',$url,$lft,$branch_name, ($rgt|| ());10881089if($dst=~/^https:/&&$src=~/^http:/) {1090$src=~s/^http:/https:/;1091}10921093::_req_svn();10941095my$ctx= SVN::Client->new(1096 auth => Git::SVN::Ra::_auth_providers(),1097 log_msg =>sub{1098${$_[0] } =defined$_message1099?$_message1100:'Create '. ($_tag?'tag ':'branch ')1101.$branch_name;1102},1103);11041105eval{1106$ctx->ls($dst,'HEAD',0);1107}and die"branch ${branch_name} already exists\n";11081109print"Copying ${src} at r${rev} to ${dst}...\n";1110$ctx->copy($src,$rev,$dst)1111unless$_dry_run;11121113$gs->fetch_all;1114}11151116sub cmd_find_rev {1117my$revision_or_hash=shift or die"SVN or git revision required ",1118"as a command-line argument\n";1119my$result;1120if($revision_or_hash=~/^r\d+$/) {1121my$head=shift;1122$head||='HEAD';1123my@refs;1124my(undef,undef,$uuid,$gs) = working_head_info($head, \@refs);1125unless($gs) {1126die"Unable to determine upstream SVN information from ",1127"$headhistory\n";1128}1129my$desired_revision=substr($revision_or_hash,1);1130$result=$gs->rev_map_get($desired_revision,$uuid);1131}else{1132my(undef,$rev,undef) = cmt_metadata($revision_or_hash);1133$result=$rev;1134}1135print"$result\n"if$result;1136}11371138sub auto_create_empty_directories {1139my($gs) =@_;1140my$var=eval{ command_oneline('config','--get','--bool',1141"svn-remote.$gs->{repo_id}.automkdirs") };1142# By default, create empty directories by consulting the unhandled log,1143# but allow setting it to 'false' to skip it.1144return!($var&&$vareq'false');1145}11461147sub cmd_rebase {1148 command_noisy(qw/update-index --refresh/);1149my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1150unless($gs) {1151die"Unable to determine upstream SVN information from ",1152"working tree history\n";1153}1154if($_dry_run) {1155print"Remote Branch: ".$gs->refname."\n";1156print"SVN URL: ".$url."\n";1157return;1158}1159if(command(qw/diff-index HEAD --/)) {1160print STDERR "Cannot rebase with uncommited changes:\n";1161 command_noisy('status');1162exit1;1163}1164unless($_local) {1165# rebase will checkout for us, so no need to do it explicitly1166$_no_checkout='true';1167$_fetch_all?$gs->fetch_all:$gs->fetch;1168}1169 command_noisy(rebase_cmd(),$gs->refname);1170if(auto_create_empty_directories($gs)) {1171$gs->mkemptydirs;1172}1173}11741175sub cmd_show_ignore {1176my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1177$gs||= Git::SVN->new;1178my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1179$gs->prop_walk($gs->{path},$r,sub{1180my($gs,$path,$props) =@_;1181print STDOUT "\n#$path\n";1182my$s=$props->{'svn:ignore'}orreturn;1183$s=~s/[\r\n]+/\n/g;1184$s=~s/^\n+//;1185chomp$s;1186$s=~ s#^#$path#gm;1187print STDOUT "$s\n";1188});1189}11901191sub cmd_show_externals {1192my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1193$gs||= Git::SVN->new;1194my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1195$gs->prop_walk($gs->{path},$r,sub{1196my($gs,$path,$props) =@_;1197print STDOUT "\n#$path\n";1198my$s=$props->{'svn:externals'}orreturn;1199$s=~s/[\r\n]+/\n/g;1200chomp$s;1201$s=~ s#^#$path#gm;1202print STDOUT "$s\n";1203});1204}12051206sub cmd_create_ignore {1207my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1208$gs||= Git::SVN->new;1209my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1210$gs->prop_walk($gs->{path},$r,sub{1211my($gs,$path,$props) =@_;1212# $path is of the form /path/to/dir/1213$path='.'.$path;1214# SVN can have attributes on empty directories,1215# which git won't track1216 mkpath([$path])unless-d $path;1217my$ignore=$path.'.gitignore';1218my$s=$props->{'svn:ignore'}orreturn;1219open(GITIGNORE,'>',$ignore)1220or fatal("Failed to open `$ignore' for writing:$!");1221$s=~s/[\r\n]+/\n/g;1222$s=~s/^\n+//;1223chomp$s;1224# Prefix all patterns so that the ignore doesn't apply1225# to sub-directories.1226$s=~ s#^#/#gm;1227print GITIGNORE "$s\n";1228close(GITIGNORE)1229or fatal("Failed to close `$ignore':$!");1230 command_noisy('add','-f',$ignore);1231});1232}12331234sub cmd_mkdirs {1235my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1236$gs||= Git::SVN->new;1237$gs->mkemptydirs($_revision);1238}12391240sub canonicalize_path {1241my($path) =@_;1242my$dot_slash_added=0;1243if(substr($path,0,1)ne"/") {1244$path="./".$path;1245$dot_slash_added=1;1246}1247# File::Spec->canonpath doesn't collapse x/../y into y (for a1248# good reason), so let's do this manually.1249$path=~ s#/+#/#g;1250$path=~ s#/\.(?:/|$)#/#g;1251$path=~ s#/[^/]+/\.\.##g;1252$path=~ s#/$##g;1253$path=~ s#^\./## if $dot_slash_added;1254$path=~ s#^/##;1255$path=~ s#^\.$##;1256return$path;1257}12581259sub canonicalize_url {1260my($url) =@_;1261$url=~ s#^([^:]+://[^/]*/)(.*)$#$1 . canonicalize_path($2)#e;1262return$url;1263}12641265# get_svnprops(PATH)1266# ------------------1267# Helper for cmd_propget and cmd_proplist below.1268sub get_svnprops {1269my$path=shift;1270my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1271$gs||= Git::SVN->new;12721273# prefix THE PATH by the sub-directory from which the user1274# invoked us.1275$path=$cmd_dir_prefix.$path;1276 fatal("No such file or directory:$path")unless-e $path;1277my$is_dir= -d $path?1:0;1278$path=$gs->{path} .'/'.$path;12791280# canonicalize the path (otherwise libsvn will abort or fail to1281# find the file)1282$path= canonicalize_path($path);12831284my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1285my$props;1286if($is_dir) {1287(undef,undef,$props) =$gs->ra->get_dir($path,$r);1288}1289else{1290(undef,$props) =$gs->ra->get_file($path,$r,undef);1291}1292return$props;1293}12941295# cmd_propget (PROP, PATH)1296# ------------------------1297# Print the SVN property PROP for PATH.1298sub cmd_propget {1299my($prop,$path) =@_;1300$path='.'ifnot defined$path;1301 usage(1)ifnot defined$prop;1302my$props= get_svnprops($path);1303if(not defined$props->{$prop}) {1304 fatal("`$path' does not have a `$prop' SVN property.");1305}1306print$props->{$prop} ."\n";1307}13081309# cmd_proplist (PATH)1310# -------------------1311# Print the list of SVN properties for PATH.1312sub cmd_proplist {1313my$path=shift;1314$path='.'ifnot defined$path;1315my$props= get_svnprops($path);1316print"Properties on '$path':\n";1317foreach(sort keys%{$props}) {1318print"$_\n";1319}1320}13211322sub cmd_multi_init {1323my$url=shift;1324unless(defined$_trunk||@_branches||@_tags) {1325 usage(1);1326}13271328$_prefix=''unlessdefined$_prefix;1329if(defined$url) {1330$url= canonicalize_url($url);1331 init_subdir(@_);1332}1333 do_git_init_db();1334if(defined$_trunk) {1335$_trunk=~ s#^/+##;1336my$trunk_ref='refs/remotes/'.$_prefix.'trunk';1337# try both old-style and new-style lookups:1338my$gs_trunk=eval{ Git::SVN->new($trunk_ref) };1339unless($gs_trunk) {1340my($trunk_url,$trunk_path) =1341 complete_svn_url($url,$_trunk);1342$gs_trunk= Git::SVN->init($trunk_url,$trunk_path,1343undef,$trunk_ref);1344}1345}1346return unless@_branches||@_tags;1347my$ra=$url? Git::SVN::Ra->new($url) :undef;1348foreachmy$path(@_branches) {1349 complete_url_ls_init($ra,$path,'--branches/-b',$_prefix);1350}1351foreachmy$path(@_tags) {1352 complete_url_ls_init($ra,$path,'--tags/-t',$_prefix.'tags/');1353}1354}13551356sub cmd_multi_fetch {1357$Git::SVN::no_reuse_existing =undef;1358my$remotes= Git::SVN::read_all_remotes();1359foreachmy$repo_id(sort keys%$remotes) {1360if($remotes->{$repo_id}->{url}) {1361 Git::SVN::fetch_all($repo_id,$remotes);1362}1363}1364}13651366# this command is special because it requires no metadata1367sub cmd_commit_diff {1368my($ta,$tb,$url) =@_;1369my$usage="Usage:$0commit-diff -r<revision> ".1370"<tree-ish> <tree-ish> [<URL>]";1371 fatal($usage)if(!defined$ta|| !defined$tb);1372my$svn_path='';1373if(!defined$url) {1374my$gs=eval{ Git::SVN->new};1375if(!$gs) {1376 fatal("Needed URL or usable git-svn --id in ",1377"the command-line\n",$usage);1378}1379$url=$gs->{url};1380$svn_path=$gs->{path};1381}1382unless(defined$_revision) {1383 fatal("-r|--revision is a required argument\n",$usage);1384}1385if(defined$_message&&defined$_file) {1386 fatal("Both --message/-m and --file/-F specified ",1387"for the commit message.\n",1388"I have no idea what you mean");1389}1390if(defined$_file) {1391$_message= file_to_s($_file);1392}else{1393$_message||= get_commit_entry($tb)->{log};1394}1395my$ra||= Git::SVN::Ra->new($url);1396my$r=$_revision;1397if($req'HEAD') {1398$r=$ra->get_latest_revnum;1399}elsif($r!~/^\d+$/) {1400die"revision argument:$rnot understood by git-svn\n";1401}1402my%ed_opts= ( r =>$r,1403log=>$_message,1404 ra =>$ra,1405 tree_a =>$ta,1406 tree_b =>$tb,1407 editor_cb =>sub{print"Committed r$_[0]\n"},1408 svn_path =>$svn_path);1409if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {1410print"No changes\n$ta==$tb\n";1411}1412}14131414sub escape_uri_only {1415my($uri) =@_;1416my@tmp;1417foreach(splitm{/},$uri) {1418s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;1419push@tmp,$_;1420}1421join('/',@tmp);1422}14231424sub escape_url {1425my($url) =@_;1426if($url=~ m#^([^:]+)://([^/]*)(.*)$#) {1427my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));1428$url="$scheme://$domain$uri";1429}1430$url;1431}14321433sub cmd_info {1434my$path= canonicalize_path(defined($_[0]) ?$_[0] :".");1435my$fullpath= canonicalize_path($cmd_dir_prefix.$path);1436if(exists$_[1]) {1437die"Too many arguments specified\n";1438}14391440my($file_type,$diff_status) = find_file_type_and_diff_status($path);14411442if(!$file_type&& !$diff_status) {1443print STDERR "svn: '$path' is not under version control\n";1444exit1;1445}14461447my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1448unless($gs) {1449die"Unable to determine upstream SVN information from ",1450"working tree history\n";1451}14521453# canonicalize_path() will return "" to make libsvn 1.5.x happy,1454$path="."if$patheq"";14551456my$full_url=$url. ($fullpatheq""?"":"/$fullpath");14571458if($_url) {1459print escape_url($full_url),"\n";1460return;1461}14621463my$result="Path:$path\n";1464$result.="Name: ". basename($path) ."\n"if$file_typene"dir";1465$result.="URL: ". escape_url($full_url) ."\n";14661467eval{1468my$repos_root=$gs->repos_root;1469 Git::SVN::remove_username($repos_root);1470$result.="Repository Root: ". escape_url($repos_root) ."\n";1471};1472if($@) {1473$result.="Repository Root: (offline)\n";1474}1475::_req_svn();1476$result.="Repository UUID:$uuid\n"unless$diff_statuseq"A"&&1477($SVN::Core::VERSION le'1.5.4'||$file_typene"dir");1478$result.="Revision: ". ($diff_statuseq"A"?0:$rev) ."\n";14791480$result.="Node Kind: ".1481($file_typeeq"dir"?"directory":"file") ."\n";14821483my$schedule=$diff_statuseq"A"1484?"add"1485: ($diff_statuseq"D"?"delete":"normal");1486$result.="Schedule:$schedule\n";14871488if($diff_statuseq"A") {1489print$result,"\n";1490return;1491}14921493my($lc_author,$lc_rev,$lc_date_utc);1494my@args= Git::SVN::Log::git_svn_log_cmd($rev,$rev,"--",$fullpath);1495my$log= command_output_pipe(@args);1496my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;1497while(<$log>) {1498if(/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {1499$lc_author=$1;1500$lc_date_utc= Git::SVN::Log::parse_git_date($2,$3);1501}elsif(/^${esc_color} (git-svn-id:.+)$/o) {1502(undef,$lc_rev,undef) = ::extract_metadata($1);1503}1504}1505close$log;15061507 Git::SVN::Log::set_local_timezone();15081509$result.="Last Changed Author:$lc_author\n";1510$result.="Last Changed Rev:$lc_rev\n";1511$result.="Last Changed Date: ".1512 Git::SVN::Log::format_svn_date($lc_date_utc) ."\n";15131514if($file_typene"dir") {1515my$text_last_updated_date=1516($diff_statuseq"D"?$lc_date_utc: (stat$path)[9]);1517$result.=1518"Text Last Updated: ".1519 Git::SVN::Log::format_svn_date($text_last_updated_date) .1520"\n";1521my$checksum;1522if($diff_statuseq"D") {1523my($fh,$ctx) =1524 command_output_pipe(qw(cat-file blob),"HEAD:$path");1525if($file_typeeq"link") {1526my$file_name= <$fh>;1527$checksum= md5sum("link$file_name");1528}else{1529$checksum= md5sum($fh);1530}1531 command_close_pipe($fh,$ctx);1532}elsif($file_typeeq"link") {1533my$file_name=1534 command(qw(cat-file blob),"HEAD:$path");1535$checksum=1536 md5sum("link ".$file_name);1537}else{1538open FILE,"<",$pathor die$!;1539$checksum= md5sum(\*FILE);1540close FILE or die$!;1541}1542$result.="Checksum: ".$checksum."\n";1543}15441545print$result,"\n";1546}15471548sub cmd_reset {1549my$target=shift||$_revisionor die"SVN revision required\n";1550$target=$1if$target=~/^r(\d+)$/;1551$target=~/^\d+$/or die"Numeric SVN revision expected\n";1552my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1553unless($gs) {1554die"Unable to determine upstream SVN information from ".1555"history\n";1556}1557my($r,$c) =$gs->find_rev_before($target,not$_fetch_parent);1558die"Cannot find SVN revision$target\n"unlessdefined($c);1559$gs->rev_map_set($r,$c,'reset',$uuid);1560print"r$r=$c($gs->{ref_id})\n";1561}15621563sub cmd_gc {1564if(!$can_compress) {1565warn"Compress::Zlib could not be found; unhandled.log ".1566"files will not be compressed.\n";1567}1568 find({ wanted => \&gc_directory, no_chdir =>1},"$ENV{GIT_DIR}/svn");1569}15701571########################### utility functions #########################15721573sub rebase_cmd {1574my@cmd= qw/rebase/;1575push@cmd,'-v'if$_verbose;1576push@cmd, qw/--merge/if$_merge;1577push@cmd,"--strategy=$_strategy"if$_strategy;1578@cmd;1579}15801581sub post_fetch_checkout {1582return if$_no_checkout;1583my$gs=$Git::SVN::_head orreturn;1584return if verify_ref('refs/heads/master^0');15851586# look for "trunk" ref if it exists1587my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1588my$fetch=$remote->{fetch};1589if($fetch) {1590foreachmy$p(keys%$fetch) {1591 basename($fetch->{$p})eq'trunk'ornext;1592$gs= Git::SVN->new($fetch->{$p},$gs->{repo_id},$p);1593last;1594}1595}15961597my$valid_head= verify_ref('HEAD^0');1598 command_noisy(qw(update-ref refs/heads/master),$gs->refname);1599return if($valid_head|| !verify_ref('HEAD^0'));16001601return if$ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;1602my$index=$ENV{GIT_INDEX_FILE} ||"$ENV{GIT_DIR}/index";1603return if-f $index;16041605return if command_oneline(qw/rev-parse --is-inside-work-tree/)eq'false';1606return if command_oneline(qw/rev-parse --is-inside-git-dir/)eq'true';1607 command_noisy(qw/read-tree -m -u -v HEAD HEAD/);1608print STDERR "Checked out HEAD:\n",1609$gs->full_url," r",$gs->last_rev,"\n";1610if(auto_create_empty_directories($gs)) {1611$gs->mkemptydirs($gs->last_rev);1612}1613}16141615sub complete_svn_url {1616my($url,$path) =@_;1617$path=~ s#/+$##;1618if($path!~ m#^[a-z\+]+://#) {1619if(!defined$url||$url!~ m#^[a-z\+]+://#) {1620 fatal("E: '$path' is not a complete URL ",1621"and a separate URL is not specified");1622}1623return($url,$path);1624}1625return($path,'');1626}16271628sub complete_url_ls_init {1629my($ra,$repo_path,$switch,$pfx) =@_;1630unless($repo_path) {1631print STDERR "W:$switchnot specified\n";1632return;1633}1634$repo_path=~ s#/+$##;1635if($repo_path=~ m#^[a-z\+]+://#) {1636$ra= Git::SVN::Ra->new($repo_path);1637$repo_path='';1638}else{1639$repo_path=~ s#^/+##;1640unless($ra) {1641 fatal("E: '$repo_path' is not a complete URL ",1642"and a separate URL is not specified");1643}1644}1645my$url=$ra->{url};1646my$gs= Git::SVN->init($url,undef,undef,undef,1);1647my$k="svn-remote.$gs->{repo_id}.url";1648my$orig_url=eval{ command_oneline(qw/config --get/,$k) };1649if($orig_url&& ($orig_urlne$gs->{url})) {1650die"$kalready set:$orig_url\n",1651"wanted to set to:$gs->{url}\n";1652}1653 command_oneline('config',$k,$gs->{url})unless$orig_url;1654my$remote_path="$gs->{path}/$repo_path";1655$remote_path=~s{%([0-9A-F]{2})}{chr hex($1)}ieg;1656$remote_path=~ s#/+#/#g;1657$remote_path=~ s#^/##g;1658$remote_path.="/*"if$remote_path!~ /\*/;1659my($n) = ($switch=~/^--(\w+)/);1660if(length$pfx&&$pfx!~ m#/$#) {1661die"--prefix='$pfx' must have a trailing slash '/'\n";1662}1663 command_noisy('config',1664'--add',1665"svn-remote.$gs->{repo_id}.$n",1666"$remote_path:refs/remotes/$pfx*".1667('/*' x (($remote_path=~ tr/*/*/) -1)) );1668}16691670sub verify_ref {1671my($ref) =@_;1672eval{ command_oneline(['rev-parse','--verify',$ref],1673{ STDERR =>0}); };1674}16751676sub get_tree_from_treeish {1677my($treeish) =@_;1678# $treeish can be a symbolic ref, too:1679my$type= command_oneline(qw/cat-file -t/,$treeish);1680my$expected;1681while($typeeq'tag') {1682($treeish,$type) = command(qw/cat-file tag/,$treeish);1683}1684if($typeeq'commit') {1685$expected= (grep/^tree /, command(qw/cat-file commit/,1686$treeish))[0];1687($expected) = ($expected=~/^tree ($sha1)$/o);1688die"Unable to get tree from$treeish\n"unless$expected;1689}elsif($typeeq'tree') {1690$expected=$treeish;1691}else{1692die"$treeishis a$type, expected tree, tag or commit\n";1693}1694return$expected;1695}16961697sub get_commit_entry {1698my($treeish) =shift;1699my%log_entry= (log=>'', tree => get_tree_from_treeish($treeish) );1700my$commit_editmsg="$ENV{GIT_DIR}/COMMIT_EDITMSG";1701my$commit_msg="$ENV{GIT_DIR}/COMMIT_MSG";1702open my$log_fh,'>',$commit_editmsgor croak $!;17031704my$type= command_oneline(qw/cat-file -t/,$treeish);1705if($typeeq'commit'||$typeeq'tag') {1706my($msg_fh,$ctx) = command_output_pipe('cat-file',1707$type,$treeish);1708my$in_msg=0;1709my$author;1710my$saw_from=0;1711my$msgbuf="";1712while(<$msg_fh>) {1713if(!$in_msg) {1714$in_msg=1if(/^\s*$/);1715$author=$1if(/^author (.*>)/);1716}elsif(/^git-svn-id: /) {1717# skip this for now, we regenerate the1718# correct one on re-fetch anyways1719# TODO: set *:merge properties or like...1720}else{1721if(/^From:/||/^Signed-off-by:/) {1722$saw_from=1;1723}1724$msgbuf.=$_;1725}1726}1727$msgbuf=~s/\s+$//s;1728if($Git::SVN::_add_author_from &&defined($author)1729&& !$saw_from) {1730$msgbuf.="\n\nFrom:$author";1731}1732print$log_fh $msgbufor croak $!;1733 command_close_pipe($msg_fh,$ctx);1734}1735close$log_fhor croak $!;17361737if($_edit|| ($typeeq'tree')) {1738chomp(my$editor= command_oneline(qw(var GIT_EDITOR)));1739system('sh','-c',$editor.' "$@"',$editor,$commit_editmsg);1740}1741rename$commit_editmsg,$commit_msgor croak $!;1742{1743require Encode;1744# SVN requires messages to be UTF-8 when entering the repo1745local$/;1746open$log_fh,'<',$commit_msgor croak $!;1747binmode$log_fh;1748chomp($log_entry{log} = <$log_fh>);17491750my$enc= Git::config('i18n.commitencoding') ||'UTF-8';1751my$msg=$log_entry{log};17521753eval{$msg= Encode::decode($enc,$msg,1) };1754if($@) {1755die"Could not decode as$enc:\n",$msg,1756"\nPerhaps you need to set i18n.commitencoding\n";1757}17581759eval{$msg= Encode::encode('UTF-8',$msg,1) };1760die"Could not encode as UTF-8:\n$msg\n"if$@;17611762$log_entry{log} =$msg;17631764close$log_fhor croak $!;1765}1766unlink$commit_msg;1767 \%log_entry;1768}17691770sub s_to_file {1771my($str,$file,$mode) =@_;1772open my$fd,'>',$fileor croak $!;1773print$fd $str,"\n"or croak $!;1774close$fdor croak $!;1775chmod($mode&~umask,$file)if(defined$mode);1776}17771778sub file_to_s {1779my$file=shift;1780open my$fd,'<',$fileor croak "$!: file:$file\n";1781local$/;1782my$ret= <$fd>;1783close$fdor croak $!;1784$ret=~s/\s*$//s;1785return$ret;1786}17871788# '<svn username> = real-name <email address>' mapping based on git-svnimport:1789sub load_authors {1790open my$authors,'<',$_authorsor die"Can't open$_authors$!\n";1791my$log=$cmdeq'log';1792while(<$authors>) {1793chomp;1794next unless/^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;1795my($user,$name,$email) = ($1,$2,$3);1796if($log) {1797$Git::SVN::Log::rusers{"$name<$email>"} =$user;1798}else{1799$users{$user} = [$name,$email];1800}1801}1802close$authorsor croak $!;1803}18041805# convert GetOpt::Long specs for use by git-config1806sub read_git_config {1807my$opts=shift;1808my@config_only;1809foreachmy$o(keys%$opts) {1810# if we have mixedCase and a long option-only, then1811# it's a config-only variable that we don't need for1812# the command-line.1813push@config_only,$oif($o=~/[A-Z]/&&$o=~/^[a-z]+$/i);1814my$v=$opts->{$o};1815my($key) = ($o=~/^([a-zA-Z\-]+)/);1816$key=~s/-//g;1817my$arg='git config';1818$arg.=' --int'if($o=~/[:=]i$/);1819$arg.=' --bool'if($o!~/[:=][sfi]$/);1820if(ref$veq'ARRAY') {1821chomp(my@tmp=`$arg--get-all svn.$key`);1822@$v=@tmpif@tmp;1823 } else {1824 chomp(my$tmp= `$arg--get svn.$key`);1825if($tmp&& !($arg=~/ --bool/&&$tmpeq'false')) {1826$$v=$tmp;1827}1828}1829}1830delete@$opts{@config_only}if@config_only;1831}18321833sub extract_metadata {1834my$id=shift orreturn(undef,undef,undef);1835my($url,$rev,$uuid) = ($id=~ /^\s*git-svn-id:\s+(.*)\@(\d+)1836 \s([a-f\d\-]+)$/ix);1837if(!defined$rev|| !$uuid|| !$url) {1838# some of the original repositories I made had1839# identifiers like this:1840($rev,$uuid) = ($id=~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i);1841}1842return($url,$rev,$uuid);1843}18441845sub cmt_metadata {1846return extract_metadata((grep(/^git-svn-id: /,1847 command(qw/cat-file commit/,shift)))[-1]);1848}18491850sub cmt_sha2rev_batch {1851my%s2r;1852my($pid,$in,$out,$ctx) = command_bidi_pipe(qw/cat-file --batch/);1853my$list=shift;18541855foreachmy$sha(@{$list}) {1856my$first=1;1857my$size=0;1858print$out $sha,"\n";18591860while(my$line= <$in>) {1861if($first&&$line=~/^[[:xdigit:]]{40}\smissing$/) {1862last;1863}elsif($first&&1864$line=~/^[[:xdigit:]]{40}\scommit\s(\d+)$/) {1865$first=0;1866$size=$1;1867next;1868}elsif($line=~/^(git-svn-id: )/) {1869my(undef,$rev,undef) =1870 extract_metadata($line);1871$s2r{$sha} =$rev;1872}18731874$size-=length($line);1875last if($size==0);1876}1877}18781879 command_close_bidi_pipe($pid,$in,$out,$ctx);18801881return \%s2r;1882}18831884sub working_head_info {1885my($head,$refs) =@_;1886my@args= qw/rev-list --first-parent --pretty=medium/;1887my($fh,$ctx) = command_output_pipe(@args,$head);1888my$hash;1889my%max;1890while(<$fh>) {1891if(m{^commit ($::sha1)$}) {1892unshift@$refs,$hashif$hashand$refs;1893$hash=$1;1894next;1895}1896next unlesss{^\s*(git-svn-id:)}{$1};1897my($url,$rev,$uuid) = extract_metadata($_);1898if(defined$url&&defined$rev) {1899next if$max{$url}and$max{$url} <$rev;1900if(my$gs= Git::SVN->find_by_url($url)) {1901my$c=$gs->rev_map_get($rev,$uuid);1902if($c&&$ceq$hash) {1903close$fh;# break the pipe1904return($url,$rev,$uuid,$gs);1905}else{1906$max{$url} ||=$gs->rev_map_max;1907}1908}1909}1910}1911 command_close_pipe($fh,$ctx);1912(undef,undef,undef,undef);1913}19141915sub read_commit_parents {1916my($parents,$c) =@_;1917chomp(my$p= command_oneline(qw/rev-list --parents -1/,$c));1918$p=~s/^($c)\s*//or die"rev-list --parents -1$cfailed!\n";1919@{$parents->{$c}} =split(/ /,$p);1920}19211922sub linearize_history {1923my($gs,$refs) =@_;1924my%parents;1925foreachmy$c(@$refs) {1926 read_commit_parents(\%parents,$c);1927}19281929my@linear_refs;1930my%skip= ();1931my$last_svn_commit=$gs->last_commit;1932foreachmy$c(reverse@$refs) {1933next if$ceq$last_svn_commit;1934last if$skip{$c};19351936unshift@linear_refs,$c;1937$skip{$c} =1;19381939# we only want the first parent to diff against for linear1940# history, we save the rest to inject when we finalize the1941# svn commit1942my$fp_a= verify_ref("$c~1");1943my$fp_b=shift@{$parents{$c}}if$parents{$c};1944if(!$fp_a|| !$fp_b) {1945die"Commit$c\n",1946"has no parent commit, and therefore ",1947"nothing to diff against.\n",1948"You should be working from a repository ",1949"originally created by git-svn\n";1950}1951if($fp_ane$fp_b) {1952die"$c~1=$fp_a, however parsing commit$c",1953"revealed that:\n$c~1=$fp_b\nBUG!\n";1954}19551956foreachmy$p(@{$parents{$c}}) {1957$skip{$p} =1;1958}1959}1960(\@linear_refs, \%parents);1961}19621963sub find_file_type_and_diff_status {1964my($path) =@_;1965return('dir','')if$patheq'';19661967my$diff_output=1968 command_oneline(qw(diff --cached --name-status --),$path) ||"";1969my$diff_status= (split(' ',$diff_output))[0] ||"";19701971my$ls_tree= command_oneline(qw(ls-tree HEAD),$path) ||"";19721973return(undef,undef)if!$diff_status&& !$ls_tree;19741975if($diff_statuseq"A") {1976return("link",$diff_status)if-l $path;1977return("dir",$diff_status)if-d $path;1978return("file",$diff_status);1979}19801981my$mode= (split(' ',$ls_tree))[0] ||"";19821983return("link",$diff_status)if$modeeq"120000";1984return("dir",$diff_status)if$modeeq"040000";1985return("file",$diff_status);1986}19871988sub md5sum {1989my$arg=shift;1990my$ref=ref$arg;1991my$md5= Digest::MD5->new();1992if($refeq'GLOB'||$refeq'IO::File'||$refeq'File::Temp') {1993$md5->addfile($arg)or croak $!;1994}elsif($refeq'SCALAR') {1995$md5->add($$arg)or croak $!;1996}elsif(!$ref) {1997$md5->add($arg)or croak $!;1998}else{1999::fatal "Can't provide MD5 hash for unknown ref type: '",$ref,"'";2000}2001return$md5->hexdigest();2002}20032004sub gc_directory {2005if($can_compress&& -f $_&& basename($_)eq"unhandled.log") {2006my$out_filename=$_.".gz";2007open my$in_fh,"<",$_or die"Unable to open$_:$!\n";2008binmode$in_fh;2009my$gz= Compress::Zlib::gzopen($out_filename,"ab")or2010die"Unable to open$out_filename:$!\n";20112012my$res;2013while($res=sysread($in_fh,my$str,1024)) {2014$gz->gzwrite($str)or2015die"Unable to write: ".$gz->gzerror()."!\n";2016}2017unlink$_or die"unlink$File::Find::name:$!\n";2018}elsif(-f $_&& basename($_)eq"index") {2019unlink$_or die"unlink$_:$!\n";2020}2021}20222023package Git::SVN;2024use strict;2025use warnings;2026use Fcntl qw/:DEFAULT :seek/;2027useconstant rev_map_fmt =>'NH40';2028use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent2029$_repack $_repack_flags $_use_svm_props $_head2030$_use_svnsync_props $no_reuse_existing $_minimize_url2031$_use_log_author $_add_author_from $_localtime/;2032use Carp qw/croak/;2033use File::Path qw/mkpath/;2034use File::Copy qw/copy/;2035use IPC::Open3;2036use Time::Local;2037use Memoize;# core since 5.8.0, Jul 20022038use Memoize::Storable;2039use POSIX qw(:signal_h);20402041my($_gc_nr,$_gc_period);20422043# properties that we do not log:2044my%SKIP_PROP;2045BEGIN{2046%SKIP_PROP=map{$_=>1} qw/svn:wc:ra_dav:version-url2047 svn:special svn:executable2048 svn:entry:committed-rev2049 svn:entry:last-author2050 svn:entry:uuid2051 svn:entry:committed-date/;20522053# some options are read globally, but can be overridden locally2054# per [svn-remote "..."] section. Command-line options will *NOT*2055# override options set in an [svn-remote "..."] section2056no strict 'refs';2057formy$option(qw/follow_parent no_metadata use_svm_props2058 use_svnsync_props/) {2059my$key=$option;2060$key=~tr/_//d;2061my$prop="-$option";2062*$option=sub{2063my($self) =@_;2064return$self->{$prop}ifexists$self->{$prop};2065my$k="svn-remote.$self->{repo_id}.$key";2066eval{ command_oneline(qw/config --get/,$k) };2067if($@) {2068$self->{$prop} = ${"Git::SVN::_$option"};2069}else{2070my$v= command_oneline(qw/config --bool/,$k);2071$self->{$prop} =$veq'false'?0:1;2072}2073return$self->{$prop};2074}2075}2076}207720782079my(%LOCKFILES,%INDEX_FILES);2080END{2081unlink keys%LOCKFILESif%LOCKFILES;2082unlink keys%INDEX_FILESif%INDEX_FILES;2083}20842085sub resolve_local_globs {2086my($url,$fetch,$glob_spec) =@_;2087return unlessdefined$glob_spec;2088my$ref=$glob_spec->{ref};2089my$path=$glob_spec->{path};2090foreach(command(qw#for-each-ref --format=%(refname) refs/#)) {2091next unless m#^$ref->{regex}$#;2092my$p=$1;2093my$pathname= desanitize_refname($path->full_path($p));2094my$refname= desanitize_refname($ref->full_path($p));2095if(my$existing=$fetch->{$pathname}) {2096if($existingne$refname) {2097die"Refspec conflict:\n",2098"existing:$existing\n",2099" globbed:$refname\n";2100}2101my$u= (::cmt_metadata("$refname"))[0];2102$u=~s!^\Q$url\E(/|$)!!or die2103"$refname: '$url' not found in '$u'\n";2104if($pathnamene$u) {2105warn"W: Refspec glob conflict ",2106"(ref:$refname):\n",2107"expected path:$pathname\n",2108" real path:$u\n",2109"Continuing ahead with$u\n";2110next;2111}2112}else{2113$fetch->{$pathname} =$refname;2114}2115}2116}21172118sub parse_revision_argument {2119my($base,$head) =@_;2120if(!defined$::_revision || $::_revision eq'BASE:HEAD') {2121return($base,$head);2122}2123return($1,$2)if($::_revision =~/^(\d+):(\d+)$/);2124return($::_revision, $::_revision)if($::_revision =~/^\d+$/);2125return($head,$head)if($::_revision eq'HEAD');2126return($base,$1)if($::_revision =~/^BASE:(\d+)$/);2127return($1,$head)if($::_revision =~/^(\d+):HEAD$/);2128die"revision argument: $::_revision not understood by git-svn\n";2129}21302131sub fetch_all {2132my($repo_id,$remotes) =@_;2133if(ref$repo_id) {2134my$gs=$repo_id;2135$repo_id=undef;2136$repo_id=$gs->{repo_id};2137}2138$remotes||= read_all_remotes();2139my$remote=$remotes->{$repo_id}or2140die"[svn-remote\"$repo_id\"] unknown\n";2141my$fetch=$remote->{fetch};2142my$url=$remote->{url}or die"svn-remote.$repo_id.url not defined\n";2143my(@gs,@globs);2144my$ra= Git::SVN::Ra->new($url);2145my$uuid=$ra->get_uuid;2146my$head=$ra->get_latest_revnum;21472148# ignore errors, $head revision may not even exist anymore2149eval{$ra->get_log("",$head,0,1,0,1,sub{$head=$_[1] }) };2150warn"W:$@\n"if$@;21512152my$base=defined$fetch?$head:0;21532154# read the max revs for wildcard expansion (branches/*, tags/*)2155foreachmy$t(qw/branches tags/) {2156defined$remote->{$t}ornext;2157push@globs, @{$remote->{$t}};21582159my$max_rev=eval{ tmp_config(qw/--int --get/,2160"svn-remote.$repo_id.${t}-maxRev") };2161if(defined$max_rev&& ($max_rev<$base)) {2162$base=$max_rev;2163}elsif(!defined$max_rev) {2164$base=0;2165}2166}21672168if($fetch) {2169foreachmy$p(sort keys%$fetch) {2170my$gs= Git::SVN->new($fetch->{$p},$repo_id,$p);2171my$lr=$gs->rev_map_max;2172if(defined$lr) {2173$base=$lrif($lr<$base);2174}2175push@gs,$gs;2176}2177}21782179($base,$head) = parse_revision_argument($base,$head);2180$ra->gs_fetch_loop_common($base,$head, \@gs, \@globs);2181}21822183sub read_all_remotes {2184my$r= {};2185my$use_svm_props=eval{ command_oneline(qw/config --bool2186 svn.useSvmProps/) };2187$use_svm_props=$use_svm_propseq'true'if$use_svm_props;2188my$svn_refspec=qr{\s*(.*?)\s*:\s*(.+?)\s*};2189foreach(grep{s/^svn-remote\.//} command(qw/config -l/)) {2190if(m!^(.+)\.fetch=$svn_refspec$!) {2191my($remote,$local_ref,$remote_ref) = ($1,$2,$3);2192die("svn-remote.$remote: remote ref '$remote_ref' "2193."must start with 'refs/'\n")2194unless$remote_ref=~m{^refs/};2195$local_ref= uri_decode($local_ref);2196$r->{$remote}->{fetch}->{$local_ref} =$remote_ref;2197$r->{$remote}->{svm} = {}if$use_svm_props;2198}elsif(m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {2199$r->{$1}->{svm} = {};2200}elsif(m!^(.+)\.url=\s*(.*)\s*$!) {2201$r->{$1}->{url} =$2;2202}elsif(m!^(.+)\.pushurl=\s*(.*)\s*$!) {2203$r->{$1}->{pushurl} =$2;2204}elsif(m!^(.+)\.ignore-refs=\s*(.*)\s*$!) {2205$r->{$1}->{ignore_refs_regex} =$2;2206}elsif(m!^(.+)\.(branches|tags)=$svn_refspec$!) {2207my($remote,$t,$local_ref,$remote_ref) =2208($1,$2,$3,$4);2209die("svn-remote.$remote: remote ref '$remote_ref' ($t) "2210."must start with 'refs/'\n")2211unless$remote_ref=~m{^refs/};2212$local_ref= uri_decode($local_ref);2213my$rs= {2214 t =>$t,2215 remote =>$remote,2216 path => Git::SVN::GlobSpec->new($local_ref,1),2217ref=> Git::SVN::GlobSpec->new($remote_ref,0) };2218if(length($rs->{ref}->{right}) !=0) {2219die"The '*' glob character must be the last ",2220"character of '$remote_ref'\n";2221}2222push@{$r->{$remote}->{$t} },$rs;2223}2224}22252226map{2227if(defined$r->{$_}->{svm}) {2228my$svm;2229eval{2230my$section="svn-remote.$_";2231$svm= {2232 source => tmp_config('--get',2233"$section.svm-source"),2234 replace => tmp_config('--get',2235"$section.svm-replace"),2236}2237};2238$r->{$_}->{svm} =$svm;2239}2240}keys%$r;22412242foreachmy$remote(keys%$r) {2243foreach(grep{defined$_}2244map{$r->{$remote}->{$_} }qw(branches tags)) {2245foreachmy$rs(@$_) {2246$rs->{ignore_refs_regex} =2247$r->{$remote}->{ignore_refs_regex};2248}2249}2250}22512252$r;2253}22542255sub init_vars {2256$_gc_nr=$_gc_period=1000;2257if(defined$_repack||defined$_repack_flags) {2258warn"Repack options are obsolete; they have no effect.\n";2259}2260}22612262sub verify_remotes_sanity {2263return unless-d $ENV{GIT_DIR};2264my%seen;2265foreach(command(qw/config -l/)) {2266if(m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {2267if($seen{$1}) {2268die"Remote ref refs/remote/$1is tracked by",2269"\n \"$_\"\nand\n \"$seen{$1}\"\n",2270"Please resolve this ambiguity in ",2271"your git configuration file before ",2272"continuing\n";2273}2274$seen{$1} =$_;2275}2276}2277}22782279sub find_existing_remote {2280my($url,$remotes) =@_;2281returnundefif$no_reuse_existing;2282my$existing;2283foreachmy$repo_id(keys%$remotes) {2284my$u=$remotes->{$repo_id}->{url}ornext;2285next if$une$url;2286$existing=$repo_id;2287last;2288}2289$existing;2290}22912292sub init_remote_config {2293my($self,$url,$no_write) =@_;2294$url=~s!/+$!!;# strip trailing slash2295my$r= read_all_remotes();2296my$existing= find_existing_remote($url,$r);2297if($existing) {2298unless($no_write) {2299print STDERR "Using existing ",2300"[svn-remote\"$existing\"]\n";2301}2302$self->{repo_id} =$existing;2303}elsif($_minimize_url) {2304my$min_url= Git::SVN::Ra->new($url)->minimize_url;2305$existing= find_existing_remote($min_url,$r);2306if($existing) {2307unless($no_write) {2308print STDERR "Using existing ",2309"[svn-remote\"$existing\"]\n";2310}2311$self->{repo_id} =$existing;2312}2313if($min_urlne$url) {2314unless($no_write) {2315print STDERR "Using higher level of URL: ",2316"$url=>$min_url\n";2317}2318my$old_path=$self->{path};2319$self->{path} =$url;2320$self->{path} =~s!^\Q$min_url\E(/|$)!!;2321if(length$old_path) {2322$self->{path} .="/$old_path";2323}2324$url=$min_url;2325}2326}2327my$orig_url;2328if(!$existing) {2329# verify that we aren't overwriting anything:2330$orig_url=eval{2331 command_oneline('config','--get',2332"svn-remote.$self->{repo_id}.url")2333};2334if($orig_url&& ($orig_urlne$url)) {2335die"svn-remote.$self->{repo_id}.url already set: ",2336"$orig_url\nwanted to set to:$url\n";2337}2338}2339my($xrepo_id,$xpath) = find_ref($self->refname);2340if(!$no_write&&defined$xpath) {2341die"svn-remote.$xrepo_id.fetch already set to track ",2342"$xpath:",$self->refname,"\n";2343}2344unless($no_write) {2345 command_noisy('config',2346"svn-remote.$self->{repo_id}.url",$url);2347$self->{path} =~s{^/}{};2348$self->{path} =~s{%([0-9A-F]{2})}{chr hex($1)}ieg;2349 command_noisy('config','--add',2350"svn-remote.$self->{repo_id}.fetch",2351"$self->{path}:".$self->refname);2352}2353$self->{url} =$url;2354}23552356sub find_by_url {# repos_root and, path are optional2357my($class,$full_url,$repos_root,$path) =@_;23582359returnundefunlessdefined$full_url;2360 remove_username($full_url);2361 remove_username($repos_root)ifdefined$repos_root;2362my$remotes= read_all_remotes();2363if(defined$full_url&&defined$repos_root&& !defined$path) {2364$path=$full_url;2365$path=~ s#^\Q$repos_root\E(?:/|$)##;2366}2367foreachmy$repo_id(keys%$remotes) {2368my$u=$remotes->{$repo_id}->{url}ornext;2369 remove_username($u);2370next ifdefined$repos_root&&$repos_rootne$u;23712372my$fetch=$remotes->{$repo_id}->{fetch} || {};2373foreachmy$t(qw/branches tags/) {2374foreachmy$globspec(@{$remotes->{$repo_id}->{$t}}) {2375 resolve_local_globs($u,$fetch,$globspec);2376}2377}2378my$p=$path;2379my$rwr= rewrite_root({repo_id =>$repo_id});2380my$svm=$remotes->{$repo_id}->{svm}2381ifdefined$remotes->{$repo_id}->{svm};2382unless(defined$p) {2383$p=$full_url;2384my$z=$u;2385my$prefix='';2386if($rwr) {2387$z=$rwr;2388 remove_username($z);2389}elsif(defined$svm) {2390$z=$svm->{source};2391$prefix=$svm->{replace};2392$prefix=~ s#^\Q$u\E(?:/|$)##;2393$prefix=~ s#/$##;2394}2395$p=~ s#^\Q$z\E(?:/|$)#$prefix# or next;2396}2397foreachmy$f(keys%$fetch) {2398next if$fne$p;2399return Git::SVN->new($fetch->{$f},$repo_id,$f);2400}2401}2402undef;2403}24042405sub init {2406my($class,$url,$path,$repo_id,$ref_id,$no_write) =@_;2407my$self= _new($class,$repo_id,$ref_id,$path);2408if(defined$url) {2409$self->init_remote_config($url,$no_write);2410}2411$self;2412}24132414sub find_ref {2415my($ref_id) =@_;2416foreach(command(qw/config -l/)) {2417next unless m!^svn-remote\.(.+)\.fetch=2418 \s*(.*?)\s*:\s*(.+?)\s*$!x;2419my($repo_id,$path,$ref) = ($1,$2,$3);2420if($refeq$ref_id) {2421$path=''if($path=~ m#^\./?#);2422return($repo_id,$path);2423}2424}2425(undef,undef,undef);2426}24272428sub new {2429my($class,$ref_id,$repo_id,$path) =@_;2430if(defined$ref_id&& !defined$repo_id&& !defined$path) {2431($repo_id,$path) = find_ref($ref_id);2432if(!defined$repo_id) {2433die"Could not find a\"svn-remote.*.fetch\"key ",2434"in the repository configuration matching: ",2435"$ref_id\n";2436}2437}2438my$self= _new($class,$repo_id,$ref_id,$path);2439if(!defined$self->{path} || !length$self->{path}) {2440my$fetch= command_oneline('config','--get',2441"svn-remote.$repo_id.fetch",2442":$ref_id\$")or2443die"Failed to read\"svn-remote.$repo_id.fetch\"",2444"\":$ref_id\$\"in config\n";2445($self->{path},undef) =split(/\s*:\s*/,$fetch);2446}2447$self->{path} =~s{/+}{/}g;2448$self->{path} =~s{\A/}{};2449$self->{path} =~s{/\z}{};2450$self->{url} = command_oneline('config','--get',2451"svn-remote.$repo_id.url")or2452die"Failed to read\"svn-remote.$repo_id.url\"in config\n";2453$self->{pushurl} =eval{ command_oneline('config','--get',2454"svn-remote.$repo_id.pushurl") };2455$self->rebuild;2456$self;2457}24582459sub refname {2460my($refname) =$_[0]->{ref_id} ;24612462# It cannot end with a slash /, we'll throw up on this because2463# SVN can't have directories with a slash in their name, either:2464if($refname=~m{/$}) {2465die"ref: '$refname' ends with a trailing slash, this is ",2466"not permitted by git nor Subversion\n";2467}24682469# It cannot have ASCII control character space, tilde ~, caret ^,2470# colon :, question-mark ?, asterisk *, space, or open bracket [2471# anywhere.2472#2473# Additionally, % must be escaped because it is used for escaping2474# and we want our escaped refname to be reversible2475$refname=~s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;24762477# no slash-separated component can begin with a dot .2478# /.* becomes /%2E*2479$refname=~s{/\.}{/%2E}g;24802481# It cannot have two consecutive dots .. anywhere2482# .. becomes %2E%2E2483$refname=~s{\.\.}{%2E%2E}g;24842485# trailing dots and .lock are not allowed2486# .$ becomes %2E and .lock becomes %2Elock2487$refname=~s{\.(?=$|lock$)}{%2E};24882489# the sequence @{ is used to access the reflog2490# @{ becomes %40{2491$refname=~s{\@\{}{%40\{}g;24922493return$refname;2494}24952496sub desanitize_refname {2497my($refname) =@_;2498$refname=~s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;2499return$refname;2500}25012502sub svm_uuid {2503my($self) =@_;2504return$self->{svm}->{uuid}if$self->svm;2505$self->ra;2506unless($self->{svm}) {2507die"SVM UUID not cached, and reading remotely failed\n";2508}2509$self->{svm}->{uuid};2510}25112512sub svm {2513my($self) =@_;2514return$self->{svm}if$self->{svm};2515my$svm;2516# see if we have it in our config, first:2517eval{2518my$section="svn-remote.$self->{repo_id}";2519$svm= {2520 source => tmp_config('--get',"$section.svm-source"),2521 uuid => tmp_config('--get',"$section.svm-uuid"),2522 replace => tmp_config('--get',"$section.svm-replace"),2523}2524};2525if($svm&&$svm->{source} &&$svm->{uuid} &&$svm->{replace}) {2526$self->{svm} =$svm;2527}2528$self->{svm};2529}25302531sub _set_svm_vars {2532my($self,$ra) =@_;2533return$raif$self->svm;25342535my@err= ("useSvmProps set, but failed to read SVM properties\n",2536"(svm:source, svm:uuid) ",2537"from the following URLs:\n");2538sub read_svm_props {2539my($self,$ra,$path,$r) =@_;2540my$props= ($ra->get_dir($path,$r))[2];2541my$src=$props->{'svm:source'};2542my$uuid=$props->{'svm:uuid'};2543returnundefif(!$src|| !$uuid);25442545chomp($src,$uuid);25462547$uuid=~m{^[0-9a-f\-]{30,}$}i2548or die"doesn't look right - svm:uuid is '$uuid'\n";25492550# the '!' is used to mark the repos_root!/relative/path2551$src=~s{/?!/?}{/};2552$src=~s{/+$}{};# no trailing slashes please2553# username is of no interest2554$src=~s{(^[a-z\+]*://)[^/@]*@}{$1};25552556my$replace=$ra->{url};2557$replace.="/$path"iflength$path;25582559my$section="svn-remote.$self->{repo_id}";2560 tmp_config("$section.svm-source",$src);2561 tmp_config("$section.svm-replace",$replace);2562 tmp_config("$section.svm-uuid",$uuid);2563$self->{svm} = {2564 source =>$src,2565 uuid =>$uuid,2566 replace =>$replace2567};2568}25692570my$r=$ra->get_latest_revnum;2571my$path=$self->{path};2572my%tried;2573while(length$path) {2574unless($tried{"$self->{url}/$path"}) {2575return$raif$self->read_svm_props($ra,$path,$r);2576$tried{"$self->{url}/$path"} =1;2577}2578$path=~ s#/?[^/]+$##;2579}2580die"Path: '$path' should be ''\n"if$pathne'';2581return$raif$self->read_svm_props($ra,$path,$r);2582$tried{"$self->{url}/$path"} =1;25832584if($ra->{repos_root}eq$self->{url}) {2585die@err, (map{"$_\n"}keys%tried),"\n";2586}25872588# nope, make sure we're connected to the repository root:2589my$ok;2590my@tried_b;2591$path=$ra->{svn_path};2592$ra= Git::SVN::Ra->new($ra->{repos_root});2593while(length$path) {2594unless($tried{"$ra->{url}/$path"}) {2595$ok=$self->read_svm_props($ra,$path,$r);2596last if$ok;2597$tried{"$ra->{url}/$path"} =1;2598}2599$path=~ s#/?[^/]+$##;2600}2601die"Path: '$path' should be ''\n"if$pathne'';2602$ok||=$self->read_svm_props($ra,$path,$r);2603$tried{"$ra->{url}/$path"} =1;2604if(!$ok) {2605die@err, (map{"$_\n"}keys%tried),"\n";2606}2607 Git::SVN::Ra->new($self->{url});2608}26092610sub svnsync {2611my($self) =@_;2612return$self->{svnsync}if$self->{svnsync};26132614if($self->no_metadata) {2615die"Can't have both 'noMetadata' and ",2616"'useSvnsyncProps' options set!\n";2617}2618if($self->rewrite_root) {2619die"Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",2620"options set!\n";2621}2622if($self->rewrite_uuid) {2623die"Can't have both 'useSvnsyncProps' and 'rewriteUUID' ",2624"options set!\n";2625}26262627my$svnsync;2628# see if we have it in our config, first:2629eval{2630my$section="svn-remote.$self->{repo_id}";26312632my$url= tmp_config('--get',"$section.svnsync-url");2633($url) = ($url=~m{^([a-z\+]+://\S+)$})or2634die"doesn't look right - svn:sync-from-url is '$url'\n";26352636my$uuid= tmp_config('--get',"$section.svnsync-uuid");2637($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2638die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26392640$svnsync= { url =>$url, uuid =>$uuid}2641};2642if($svnsync&&$svnsync->{url} &&$svnsync->{uuid}) {2643return$self->{svnsync} =$svnsync;2644}26452646my$err="useSvnsyncProps set, but failed to read ".2647"svnsync property: svn:sync-from-";2648my$rp=$self->ra->rev_proplist(0);26492650my$url=$rp->{'svn:sync-from-url'}or die$err."url\n";2651($url) = ($url=~m{^([a-z\+]+://\S+)$})or2652die"doesn't look right - svn:sync-from-url is '$url'\n";26532654my$uuid=$rp->{'svn:sync-from-uuid'}or die$err."uuid\n";2655($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2656die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26572658my$section="svn-remote.$self->{repo_id}";2659 tmp_config('--add',"$section.svnsync-uuid",$uuid);2660 tmp_config('--add',"$section.svnsync-url",$url);2661return$self->{svnsync} = { url =>$url, uuid =>$uuid};2662}26632664# this allows us to memoize our SVN::Ra UUID locally and avoid a2665# remote lookup (useful for 'git svn log').2666sub ra_uuid {2667my($self) =@_;2668unless($self->{ra_uuid}) {2669my$key="svn-remote.$self->{repo_id}.uuid";2670my$uuid=eval{ tmp_config('--get',$key) };2671if(!$@&&$uuid&&$uuid=~/^([a-f\d\-]{30,})$/i) {2672$self->{ra_uuid} =$uuid;2673}else{2674die"ra_uuid called without URL\n"unless$self->{url};2675$self->{ra_uuid} =$self->ra->get_uuid;2676 tmp_config('--add',$key,$self->{ra_uuid});2677}2678}2679$self->{ra_uuid};2680}26812682sub _set_repos_root {2683my($self,$repos_root) =@_;2684my$k="svn-remote.$self->{repo_id}.reposRoot";2685$repos_root||=$self->ra->{repos_root};2686 tmp_config($k,$repos_root);2687$repos_root;2688}26892690sub repos_root {2691my($self) =@_;2692my$k="svn-remote.$self->{repo_id}.reposRoot";2693eval{ tmp_config('--get',$k) } ||$self->_set_repos_root;2694}26952696sub ra {2697my($self) =shift;2698my$ra= Git::SVN::Ra->new($self->{url});2699$self->_set_repos_root($ra->{repos_root});2700if($self->use_svm_props&& !$self->{svm}) {2701if($self->no_metadata) {2702die"Can't have both 'noMetadata' and ",2703"'useSvmProps' options set!\n";2704}elsif($self->use_svnsync_props) {2705die"Can't have both 'useSvnsyncProps' and ",2706"'useSvmProps' options set!\n";2707}2708$ra=$self->_set_svm_vars($ra);2709$self->{-want_revprops} =1;2710}2711$ra;2712}27132714# prop_walk(PATH, REV, SUB)2715# -------------------------2716# Recursively traverse PATH at revision REV and invoke SUB for each2717# directory that contains a SVN property. SUB will be invoked as2718# follows: &SUB(gs, path, props); where `gs' is this instance of2719# Git::SVN, `path' the path to the directory where the properties2720# `props' were found. The `path' will be relative to point of checkout,2721# that is, if url://repo/trunk is the current Git branch, and that2722# directory contains a sub-directory `d', SUB will be invoked with `/d/'2723# as `path' (note the trailing `/').2724sub prop_walk {2725my($self,$path,$rev,$sub) =@_;27262727$path=~ s#^/##;2728my($dirent,undef,$props) =$self->ra->get_dir($path,$rev);2729$path=~ s#^/*#/#g;2730my$p=$path;2731# Strip the irrelevant part of the path.2732$p=~ s#^/+\Q$self->{path}\E(/|$)#/#;2733# Ensure the path is terminated by a `/'.2734$p=~ s#/*$#/#;27352736# The properties contain all the internal SVN stuff nobody2737# (usually) cares about.2738my$interesting_props=0;2739foreach(keys%{$props}) {2740# If it doesn't start with `svn:', it must be a2741# user-defined property.2742++$interesting_propsandnext if$_!~/^svn:/;2743# FIXME: Fragile, if SVN adds new public properties,2744# this needs to be updated.2745++$interesting_propsif/^svn:(?:ignore|keywords|executable2746|eol-style|mime-type2747|externals|needs-lock)$/x;2748}2749&$sub($self,$p,$props)if$interesting_props;27502751foreach(sort keys%$dirent) {2752next if$dirent->{$_}->{kind} !=$SVN::Node::dir;2753$self->prop_walk($self->{path} .$p.$_,$rev,$sub);2754}2755}27562757sub last_rev { ($_[0]->last_rev_commit)[0] }2758sub last_commit { ($_[0]->last_rev_commit)[1] }27592760# returns the newest SVN revision number and newest commit SHA12761sub last_rev_commit {2762my($self) =@_;2763if(defined$self->{last_rev} &&defined$self->{last_commit}) {2764return($self->{last_rev},$self->{last_commit});2765}2766my$c= ::verify_ref($self->refname.'^0');2767if($c&& !$self->use_svm_props&& !$self->no_metadata) {2768my$rev= (::cmt_metadata($c))[1];2769if(defined$rev) {2770($self->{last_rev},$self->{last_commit}) = ($rev,$c);2771return($rev,$c);2772}2773}2774my$map_path=$self->map_path;2775unless(-e $map_path) {2776($self->{last_rev},$self->{last_commit}) = (undef,undef);2777return(undef,undef);2778}2779my($rev,$commit) =$self->rev_map_max(1);2780($self->{last_rev},$self->{last_commit}) = ($rev,$commit);2781return($rev,$commit);2782}27832784sub get_fetch_range {2785my($self,$min,$max) =@_;2786$max||=$self->ra->get_latest_revnum;2787$min||=$self->rev_map_max;2788(++$min,$max);2789}27902791sub tmp_config {2792my(@args) =@_;2793my$old_def_config="$ENV{GIT_DIR}/svn/config";2794my$config="$ENV{GIT_DIR}/svn/.metadata";2795if(! -f $config&& -f $old_def_config) {2796rename$old_def_config,$configor2797die"Failed rename$old_def_config=>$config:$!\n";2798}2799my$old_config=$ENV{GIT_CONFIG};2800$ENV{GIT_CONFIG} =$config;2801$@=undef;2802my@ret=eval{2803unless(-f $config) {2804 mkfile($config);2805open my$fh,'>',$configor2806die"Can't open$config:$!\n";2807print$fh"; This file is used internally by ",2808"git-svn\n"or die2809"Couldn't write to$config:$!\n";2810print$fh"; You should not have to edit it\n"or2811die"Couldn't write to$config:$!\n";2812close$fhor die"Couldn't close$config:$!\n";2813}2814 command('config',@args);2815};2816my$err=$@;2817if(defined$old_config) {2818$ENV{GIT_CONFIG} =$old_config;2819}else{2820delete$ENV{GIT_CONFIG};2821}2822die$errif$err;2823wantarray?@ret:$ret[0];2824}28252826sub tmp_index_do {2827my($self,$sub) =@_;2828my$old_index=$ENV{GIT_INDEX_FILE};2829$ENV{GIT_INDEX_FILE} =$self->{index};2830$@=undef;2831my@ret=eval{2832my($dir,$base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);2833 mkpath([$dir])unless-d $dir;2834&$sub;2835};2836my$err=$@;2837if(defined$old_index) {2838$ENV{GIT_INDEX_FILE} =$old_index;2839}else{2840delete$ENV{GIT_INDEX_FILE};2841}2842die$errif$err;2843wantarray?@ret:$ret[0];2844}28452846sub assert_index_clean {2847my($self,$treeish) =@_;28482849$self->tmp_index_do(sub{2850 command_noisy('read-tree',$treeish)unless-e $self->{index};2851my$x= command_oneline('write-tree');2852my($y) = (command(qw/cat-file commit/,$treeish) =~2853/^tree ($::sha1)/mo);2854return if$yeq$x;28552856warn"Index mismatch:$y!=$x\nrereading$treeish\n";2857unlink$self->{index}or die"unlink$self->{index}:$!\n";2858 command_noisy('read-tree',$treeish);2859$x= command_oneline('write-tree');2860if($yne$x) {2861::fatal "trees ($treeish)$y!=$x\n",2862"Something is seriously wrong...";2863}2864});2865}28662867sub get_commit_parents {2868my($self,$log_entry) =@_;2869my(%seen,@ret,@tmp);2870# legacy support for 'set-tree'; this is only used by set_tree_cb:2871if(my$ip=$self->{inject_parents}) {2872if(my$commit=delete$ip->{$log_entry->{revision}}) {2873push@tmp,$commit;2874}2875}2876if(my$cur= ::verify_ref($self->refname.'^0')) {2877push@tmp,$cur;2878}2879if(my$ipd=$self->{inject_parents_dcommit}) {2880if(my$commit=delete$ipd->{$log_entry->{revision}}) {2881push@tmp,@$commit;2882}2883}2884push@tmp,$_foreach(@{$log_entry->{parents}},@tmp);2885while(my$p=shift@tmp) {2886next if$seen{$p};2887$seen{$p} =1;2888push@ret,$p;2889}2890@ret;2891}28922893sub rewrite_root {2894my($self) =@_;2895return$self->{-rewrite_root}ifexists$self->{-rewrite_root};2896my$k="svn-remote.$self->{repo_id}.rewriteRoot";2897my$rwr=eval{ command_oneline(qw/config --get/,$k) };2898if($rwr) {2899$rwr=~ s#/+$##;2900if($rwr!~ m#^[a-z\+]+://#) {2901die"$rwris not a valid URL (key:$k)\n";2902}2903}2904$self->{-rewrite_root} =$rwr;2905}29062907sub rewrite_uuid {2908my($self) =@_;2909return$self->{-rewrite_uuid}ifexists$self->{-rewrite_uuid};2910my$k="svn-remote.$self->{repo_id}.rewriteUUID";2911my$rwid=eval{ command_oneline(qw/config --get/,$k) };2912if($rwid) {2913$rwid=~ s#/+$##;2914if($rwid!~ m#^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$#) {2915die"$rwidis not a valid UUID (key:$k)\n";2916}2917}2918$self->{-rewrite_uuid} =$rwid;2919}29202921sub metadata_url {2922my($self) =@_;2923($self->rewrite_root||$self->{url}) .2924(length$self->{path} ?'/'.$self->{path} :'');2925}29262927sub full_url {2928my($self) =@_;2929$self->{url} . (length$self->{path} ?'/'.$self->{path} :'');2930}29312932sub full_pushurl {2933my($self) =@_;2934if($self->{pushurl}) {2935return$self->{pushurl} . (length$self->{path} ?'/'.2936$self->{path} :'');2937}else{2938return$self->full_url;2939}2940}29412942sub set_commit_header_env {2943my($log_entry) =@_;2944my%env;2945foreachmy$ned(qw/NAME EMAIL DATE/) {2946foreachmy$ac(qw/AUTHOR COMMITTER/) {2947$env{"GIT_${ac}_${ned}"} =$ENV{"GIT_${ac}_${ned}"};2948}2949}29502951$ENV{GIT_AUTHOR_NAME} =$log_entry->{name};2952$ENV{GIT_AUTHOR_EMAIL} =$log_entry->{email};2953$ENV{GIT_AUTHOR_DATE} =$ENV{GIT_COMMITTER_DATE} =$log_entry->{date};29542955$ENV{GIT_COMMITTER_NAME} = (defined$log_entry->{commit_name})2956?$log_entry->{commit_name}2957:$log_entry->{name};2958$ENV{GIT_COMMITTER_EMAIL} = (defined$log_entry->{commit_email})2959?$log_entry->{commit_email}2960:$log_entry->{email};2961 \%env;2962}29632964sub restore_commit_header_env {2965my($env) =@_;2966foreachmy$ned(qw/NAME EMAIL DATE/) {2967foreachmy$ac(qw/AUTHOR COMMITTER/) {2968my$k="GIT_${ac}_${ned}";2969if(defined$env->{$k}) {2970$ENV{$k} =$env->{$k};2971}else{2972delete$ENV{$k};2973}2974}2975}2976}29772978sub gc {2979 command_noisy('gc','--auto');2980};29812982sub do_git_commit {2983my($self,$log_entry) =@_;2984my$lr=$self->last_rev;2985if(defined$lr&&$lr>=$log_entry->{revision}) {2986die"Last fetched revision of ",$self->refname,2987" was r$lr, but we are about to fetch: ",2988"r$log_entry->{revision}!\n";2989}2990if(my$c=$self->rev_map_get($log_entry->{revision})) {2991 croak "$log_entry->{revision} =$calready exists! ",2992"Why are we refetching it?\n";2993}2994my$old_env= set_commit_header_env($log_entry);2995my$tree=$log_entry->{tree};2996if(!defined$tree) {2997$tree=$self->tmp_index_do(sub{2998 command_oneline('write-tree') });2999}3000die"Tree is not a valid sha1:$tree\n"if$tree!~/^$::sha1$/o;30013002my@exec= ('git','commit-tree',$tree);3003foreach($self->get_commit_parents($log_entry)) {3004push@exec,'-p',$_;3005}3006defined(my$pid= open3(my$msg_fh,my$out_fh,'>&STDERR',@exec))3007or croak $!;3008binmode$msg_fh;30093010# we always get UTF-8 from SVN, but we may want our commits in3011# a different encoding.3012if(my$enc= Git::config('i18n.commitencoding')) {3013require Encode;3014 Encode::from_to($log_entry->{log},'UTF-8',$enc);3015}3016print$msg_fh $log_entry->{log}or croak $!;3017 restore_commit_header_env($old_env);3018unless($self->no_metadata) {3019print$msg_fh"\ngit-svn-id:$log_entry->{metadata}\n"3020or croak $!;3021}3022$msg_fh->flush==0or croak $!;3023close$msg_fhor croak $!;3024chomp(my$commit=do{local$/; <$out_fh> });3025close$out_fhor croak $!;3026waitpid$pid,0;3027 croak $?if$?;3028if($commit!~/^$::sha1$/o) {3029die"Failed to commit, invalid sha1:$commit\n";3030}30313032$self->rev_map_set($log_entry->{revision},$commit,1);30333034$self->{last_rev} =$log_entry->{revision};3035$self->{last_commit} =$commit;3036print"r$log_entry->{revision}"unless$::_q >1;3037if(defined$log_entry->{svm_revision}) {3038print" (\@$log_entry->{svm_revision})"unless$::_q >1;3039$self->rev_map_set($log_entry->{svm_revision},$commit,30400,$self->svm_uuid);3041}3042print" =$commit($self->{ref_id})\n"unless$::_q >1;3043if(--$_gc_nr==0) {3044$_gc_nr=$_gc_period;3045 gc();3046}3047return$commit;3048}30493050sub match_paths {3051my($self,$paths,$r) =@_;3052return1if$self->{path}eq'';3053if(my$path=$paths->{"/$self->{path}"}) {3054return($path->{action}eq'D') ?0:1;3055}3056$self->{path_regex} ||=qr/^\/\Q$self->{path}\E\//;3057if(grep/$self->{path_regex}/,keys%$paths) {3058return1;3059}3060my$c='';3061foreach(split m#/#, $self->{path}) {3062$c.="/$_";3063next unless($paths->{$c} &&3064($paths->{$c}->{action} =~/^[AR]$/));3065if($self->ra->check_path($self->{path},$r) ==3066$SVN::Node::dir) {3067return1;3068}3069}3070return0;3071}30723073sub find_parent_branch {3074my($self,$paths,$rev) =@_;3075returnundefunless$self->follow_parent;3076unless(defined$paths) {3077my$err_handler=$SVN::Error::handler;3078$SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;3079$self->ra->get_log([$self->{path}],$rev,$rev,0,1,1,3080sub{$paths=$_[0] });3081$SVN::Error::handler =$err_handler;3082}3083returnundefunlessdefined$paths;30843085# look for a parent from another branch:3086my@b_path_components=split m#/#, $self->{path};3087my@a_path_components;3088my$i;3089while(@b_path_components) {3090$i=$paths->{'/'.join('/',@b_path_components)};3091last if$i&&defined$i->{copyfrom_path};3092unshift(@a_path_components,pop(@b_path_components));3093}3094returnundefunlessdefined$i&&defined$i->{copyfrom_path};3095my$branch_from=$i->{copyfrom_path};3096if(@a_path_components) {3097print STDERR "branch_from:$branch_from=> ";3098$branch_from.='/'.join('/',@a_path_components);3099print STDERR $branch_from,"\n";3100}3101my$r=$i->{copyfrom_rev};3102my$repos_root=$self->ra->{repos_root};3103my$url=$self->ra->{url};3104my$new_url=$url.$branch_from;3105print STDERR "Found possible branch point: ",3106"$new_url=> ",$self->full_url,",$r\n"3107unless$::_q >1;3108$branch_from=~ s#^/##;3109my$gs=$self->other_gs($new_url,$url,3110$branch_from,$r,$self->{ref_id});3111my($r0,$parent) =$gs->find_rev_before($r,1);3112{3113my($base,$head);3114if(!defined$r0|| !defined$parent) {3115($base,$head) = parse_revision_argument(0,$r);3116}else{3117if($r0<$r) {3118$gs->ra->get_log([$gs->{path}],$r0+1,$r,1,31190,1,sub{$base=$_[1] -1});3120}3121}3122if(defined$base&&$base<=$r) {3123$gs->fetch($base,$r);3124}3125($r0,$parent) =$gs->find_rev_before($r,1);3126}3127if(defined$r0&&defined$parent) {3128print STDERR "Found branch parent: ($self->{ref_id})$parent\n"3129unless$::_q >1;3130my$ed;3131if($self->ra->can_do_switch) {3132$self->assert_index_clean($parent);3133print STDERR "Following parent with do_switch\n"3134unless$::_q >1;3135# do_switch works with svn/trunk >= r22312, but that3136# is not included with SVN 1.4.3 (the latest version3137# at the moment), so we can't rely on it3138$self->{last_rev} =$r0;3139$self->{last_commit} =$parent;3140$ed= SVN::Git::Fetcher->new($self,$gs->{path});3141$gs->ra->gs_do_switch($r0,$rev,$gs,3142$self->full_url,$ed)3143or die"SVN connection failed somewhere...\n";3144}elsif($self->ra->trees_match($new_url,$r0,3145$self->full_url,$rev)) {3146print STDERR "Trees match:\n",3147"$new_url\@$r0\n",3148" ${\$self->full_url}\@$rev\n",3149"Following parent with no changes\n"3150unless$::_q >1;3151$self->tmp_index_do(sub{3152 command_noisy('read-tree',$parent);3153});3154$self->{last_commit} =$parent;3155}else{3156print STDERR "Following parent with do_update\n"3157unless$::_q >1;3158$ed= SVN::Git::Fetcher->new($self);3159$self->ra->gs_do_update($rev,$rev,$self,$ed)3160or die"SVN connection failed somewhere...\n";3161}3162print STDERR "Successfully followed parent\n"unless$::_q >1;3163return$self->make_log_entry($rev, [$parent],$ed);3164}3165returnundef;3166}31673168sub do_fetch {3169my($self,$paths,$rev) =@_;3170my$ed;3171my($last_rev,@parents);3172if(my$lc=$self->last_commit) {3173# we can have a branch that was deleted, then re-added3174# under the same name but copied from another path, in3175# which case we'll have multiple parents (we don't3176# want to break the original ref, nor lose copypath info):3177if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3178push@{$log_entry->{parents}},$lc;3179return$log_entry;3180}3181$ed= SVN::Git::Fetcher->new($self);3182$last_rev=$self->{last_rev};3183$ed->{c} =$lc;3184@parents= ($lc);3185}else{3186$last_rev=$rev;3187if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3188return$log_entry;3189}3190$ed= SVN::Git::Fetcher->new($self);3191}3192unless($self->ra->gs_do_update($last_rev,$rev,$self,$ed)) {3193die"SVN connection failed somewhere...\n";3194}3195$self->make_log_entry($rev, \@parents,$ed);3196}31973198sub mkemptydirs {3199my($self,$r) =@_;32003201sub scan {3202my($r,$empty_dirs,$line) =@_;3203if(defined$r&&$line=~/^r(\d+)$/) {3204return0if$1>$r;3205}elsif($line=~/^ \+empty_dir: (.+)$/) {3206$empty_dirs->{$1} =1;3207}elsif($line=~/^ \-empty_dir: (.+)$/) {3208my@d=grep{m[^\Q$1\E(/|$)]} (keys%$empty_dirs);3209delete@$empty_dirs{@d};3210}32111;# continue3212};32133214my%empty_dirs= ();3215my$gz_file="$self->{dir}/unhandled.log.gz";3216if(-f $gz_file) {3217if(!$can_compress) {3218warn"Compress::Zlib could not be found; ",3219"empty directories in$gz_filewill not be read\n";3220}else{3221my$gz= Compress::Zlib::gzopen($gz_file,"rb")or3222die"Unable to open$gz_file:$!\n";3223my$line;3224while($gz->gzreadline($line) >0) {3225 scan($r, \%empty_dirs,$line)orlast;3226}3227$gz->gzclose;3228}3229}32303231if(open my$fh,'<',"$self->{dir}/unhandled.log") {3232binmode$fhor croak "binmode:$!";3233while(<$fh>) {3234 scan($r, \%empty_dirs,$_)orlast;3235}3236close$fh;3237}32383239my$strip=qr/\A\Q$self->{path}\E(?:\/|$)/;3240foreachmy$d(sort keys%empty_dirs) {3241$d= uri_decode($d);3242$d=~s/$strip//;3243next unlesslength($d);3244next if-d $d;3245if(-e $d) {3246warn"$dexists but is not a directory\n";3247}else{3248print"creating empty directory:$d\n";3249 mkpath([$d]);3250}3251}3252}32533254sub get_untracked {3255my($self,$ed) =@_;3256my@out;3257my$h=$ed->{empty};3258foreach(sort keys%$h) {3259my$act=$h->{$_} ?'+empty_dir':'-empty_dir';3260push@out,"$act: ". uri_encode($_);3261warn"W:$act:$_\n";3262}3263foreachmy$t(qw/dir_prop file_prop/) {3264$h=$ed->{$t}ornext;3265foreachmy$path(sort keys%$h) {3266my$ppath=$patheq''?'.':$path;3267foreachmy$prop(sort keys%{$h->{$path}}) {3268next if$SKIP_PROP{$prop};3269my$v=$h->{$path}->{$prop};3270my$t_ppath_prop="$t: ".3271 uri_encode($ppath) .' '.3272 uri_encode($prop);3273if(defined$v) {3274push@out," +$t_ppath_prop".3275 uri_encode($v);3276}else{3277push@out," -$t_ppath_prop";3278}3279}3280}3281}3282foreachmy$t(qw/absent_file absent_directory/) {3283$h=$ed->{$t}ornext;3284foreachmy$parent(sort keys%$h) {3285foreachmy$path(sort@{$h->{$parent}}) {3286push@out,"$t: ".3287 uri_encode("$parent/$path");3288warn"W:$t:$parent/$path",3289"Insufficient permissions?\n";3290}3291}3292}3293 \@out;3294}32953296sub get_tz {3297# some systmes don't handle or mishandle %z, so be creative.3298my$t=shift||time;3299my$gm= timelocal(gmtime($t));3300my$sign=qw( + + - )[$t<=>$gm];3301returnsprintf("%s%02d%02d",$sign, (gmtime(abs($t-$gm)))[2,1]);3302}33033304# parse_svn_date(DATE)3305# --------------------3306# Given a date (in UTC) from Subversion, return a string in the format3307# "<TZ Offset> <local date/time>" that Git will use.3308#3309# By default the parsed date will be in UTC; if $Git::SVN::_localtime3310# is true we'll convert it to the local timezone instead.3311sub parse_svn_date {3312my$date=shift||return'+0000 1970-01-01 00:00:00';3313my($Y,$m,$d,$H,$M,$S) = ($date=~ /^(\d{4})\-(\d\d)\-(\d\d)T3314(\d\d)\:(\d\d)\:(\d\d)\.\d*Z$/x)or3315 croak "Unable to parse date:$date\n";3316my$parsed_date;# Set next.33173318if($Git::SVN::_localtime) {3319# Translate the Subversion datetime to an epoch time.3320# Begin by switching ourselves to $date's timezone, UTC.3321my$old_env_TZ=$ENV{TZ};3322$ENV{TZ} ='UTC';33233324my$epoch_in_UTC=3325 POSIX::strftime('%s',$S,$M,$H,$d,$m-1,$Y-1900);33263327# Determine our local timezone (including DST) at the3328# time of $epoch_in_UTC. $Git::SVN::Log::TZ stored the3329# value of TZ, if any, at the time we were run.3330if(defined$Git::SVN::Log::TZ) {3331$ENV{TZ} =$Git::SVN::Log::TZ;3332}else{3333delete$ENV{TZ};3334}33353336my$our_TZ= get_tz();33373338# This converts $epoch_in_UTC into our local timezone.3339my($sec,$min,$hour,$mday,$mon,$year,3340$wday,$yday,$isdst) =localtime($epoch_in_UTC);33413342$parsed_date=sprintf('%s%04d-%02d-%02d%02d:%02d:%02d',3343$our_TZ,$year+1900,$mon+1,3344$mday,$hour,$min,$sec);33453346# Reset us to the timezone in effect when we entered3347# this routine.3348if(defined$old_env_TZ) {3349$ENV{TZ} =$old_env_TZ;3350}else{3351delete$ENV{TZ};3352}3353}else{3354$parsed_date="+0000$Y-$m-$d$H:$M:$S";3355}33563357return$parsed_date;3358}33593360sub other_gs {3361my($self,$new_url,$url,3362$branch_from,$r,$old_ref_id) =@_;3363my$gs= Git::SVN->find_by_url($new_url,$url,$branch_from);3364unless($gs) {3365my$ref_id=$old_ref_id;3366$ref_id=~s/\@\d+-*$//;3367$ref_id.="\@$r";3368# just grow a tail if we're not unique enough :x3369$ref_id.='-'while find_ref($ref_id);3370my($u,$p,$repo_id) = ($new_url,'',$ref_id);3371if($u=~ s#^\Q$url\E(/|$)##) {3372$p=$u;3373$u=$url;3374$repo_id=$self->{repo_id};3375}3376while(1) {3377# It is possible to tag two different subdirectories at3378# the same revision. If the url for an existing ref3379# does not match, we must either find a ref with a3380# matching url or create a new ref by growing a tail.3381$gs= Git::SVN->init($u,$p,$repo_id,$ref_id,1);3382my(undef,$max_commit) =$gs->rev_map_max(1);3383last if(!$max_commit);3384my($url) = ::cmt_metadata($max_commit);3385last if($urleq$gs->metadata_url);3386$ref_id.='-';3387}3388print STDERR "Initializing parent:$ref_id\n"unless$::_q >1;3389}3390$gs3391}33923393sub call_authors_prog {3394my($orig_author) =@_;3395$orig_author= command_oneline('rev-parse','--sq-quote',$orig_author);3396my$author=`$::_authors_prog$orig_author`;3397 if ($?!= 0) {3398 die "$::_authors_prog failed with exit code$?\n"3399 }3400 if ($author=~ /^\s*(.+?)\s*<(.*)>\s*$/) {3401 my ($name,$email) = ($1,$2);3402$email= undef if length$2== 0;3403 return [$name,$email];3404 } else {3405 die "Author:$orig_author: $::_authors_prog returned "3406 . "invalid author format:$author\n";3407 }3408}34093410sub check_author {3411 my ($author) =@_;3412 if (!defined$author|| length$author== 0) {3413$author= '(no author)';3414 }3415 if (!defined $::users{$author}) {3416 if (defined $::_authors_prog) {3417 $::users{$author} = call_authors_prog($author);3418 } elsif (defined $::_authors) {3419 die "Author:$authornot defined in $::_authors file\n";3420 }3421 }3422$author;3423}34243425sub find_extra_svk_parents {3426 my ($self,$ed,$tickets,$parents) =@_;3427 # aha! svk:merge property changed...3428 my@tickets= split "\n",$tickets;3429 my@known_parents;3430 for my$ticket(@tickets) {3431 my ($uuid,$path,$rev) = split /:/,$ticket;3432 if ($uuideq$self->ra_uuid ) {3433 my$url=$self->{url};3434 my$repos_root=$url;3435 my$branch_from=$path;3436$branch_from=~ s{^/}{};3437 my$gs=$self->other_gs($repos_root."/".$branch_from,3438$url,3439$branch_from,3440$rev,3441$self->{ref_id});3442 if ( my$commit=$gs->rev_map_get($rev,$uuid) ) {3443 # wahey! we found it, but it might be3444 # an old one (!)3445 push@known_parents, [$rev,$commit];3446 }3447 }3448 }3449 # Ordering matters; highest-numbered commit merge tickets3450 # first, as they may account for later merge ticket additions3451 # or changes.3452@known_parents= map {$_->[1]} sort {$b->[0] <=>$a->[0]}@known_parents;3453 for my$parent(@known_parents) {3454 my@cmd= ('rev-list',$parent, map { "^$_" }@$parents);3455 my ($msg_fh,$ctx) = command_output_pipe(@cmd);3456 my$new;3457 while ( <$msg_fh> ) {3458$new=1;last;3459 }3460 command_close_pipe($msg_fh,$ctx);3461 if ($new) {3462 print STDERR3463 "Found merge parent (svk:merge ticket):$parent\n";3464 push@$parents,$parent;3465 }3466 }3467}34683469sub lookup_svn_merge {3470 my$uuid= shift;3471 my$url= shift;3472 my$merge= shift;34733474 my ($source,$revs) = split ":",$merge;3475 my$path=$source;3476$path=~ s{^/}{};3477 my$gs= Git::SVN->find_by_url($url.$source,$url,$path);3478 if ( !$gs) {3479 warn "Couldn't find revmap for$url$source\n";3480 return;3481 }3482 my@ranges= split ",",$revs;3483 my ($tip,$tip_commit);3484 my@merged_commit_ranges;3485 # find the tip3486 for my$range(@ranges) {3487 my ($bottom,$top) = split "-",$range;3488$top||=$bottom;3489 my$bottom_commit=$gs->find_rev_after($bottom, 1,$top);3490 my$top_commit=$gs->find_rev_before($top, 1,$bottom);34913492 unless ($top_commitand$bottom_commit) {3493 warn "W:unknown path/rev in svn:mergeinfo "3494 ."dirprop:$source:$range\n";3495 next;3496 }34973498 if (scalar(command('rev-parse', "$bottom_commit^@"))) {3499 push@merged_commit_ranges,3500 "$bottom_commit^..$top_commit";3501 } else {3502 push@merged_commit_ranges, "$top_commit";3503 }35043505 if ( !defined$tipor$top>$tip) {3506$tip=$top;3507$tip_commit=$top_commit;3508 }3509 }3510 return ($tip_commit,@merged_commit_ranges);3511}35123513sub _rev_list {3514 my ($msg_fh,$ctx) = command_output_pipe(3515 "rev-list",@_,3516 );3517 my@rv;3518 while ( <$msg_fh> ) {3519 chomp;3520 push@rv,$_;3521 }3522 command_close_pipe($msg_fh,$ctx);3523@rv;3524}35253526sub check_cherry_pick {3527 my$base= shift;3528 my$tip= shift;3529 my$parents= shift;3530 my@ranges=@_;3531 my%commits= map {$_=> 1 }3532 _rev_list("--no-merges",$tip, "--not",$base,@$parents, "--");3533 for my$range(@ranges) {3534 delete@commits{_rev_list($range, "--")};3535 }3536 for my$commit(keys%commits) {3537 if (has_no_changes($commit)) {3538 delete$commits{$commit};3539 }3540 }3541 return (keys%commits);3542}35433544sub has_no_changes {3545 my$commit= shift;35463547 my@revs= split / /, command_oneline(3548 qw(rev-list --parents -1 -m),$commit);35493550# Commits with no parents, e.g. the start of a partial branch,3551# have changes by definition.3552return1if(@revs<2);35533554# Commits with multiple parents, e.g a merge, have no changes3555# by definition.3556return0if(@revs>2);35573558return(command_oneline("rev-parse","$commit^{tree}")eq3559 command_oneline("rev-parse","$commit~1^{tree}"));3560}35613562# The GIT_DIR environment variable is not always set until after the command3563# line arguments are processed, so we can't memoize in a BEGIN block.3564{3565my$memoized=0;35663567sub memoize_svn_mergeinfo_functions {3568return if$memoized;3569$memoized=1;35703571my$cache_path="$ENV{GIT_DIR}/svn/.caches/";3572 mkpath([$cache_path])unless-d $cache_path;35733574 tie my%lookup_svn_merge_cache=>'Memoize::Storable',3575"$cache_path/lookup_svn_merge.db",'nstore';3576 memoize 'lookup_svn_merge',3577 SCALAR_CACHE =>'FAULT',3578 LIST_CACHE => ['HASH'=> \%lookup_svn_merge_cache],3579;35803581 tie my%check_cherry_pick_cache=>'Memoize::Storable',3582"$cache_path/check_cherry_pick.db",'nstore';3583 memoize 'check_cherry_pick',3584 SCALAR_CACHE =>'FAULT',3585 LIST_CACHE => ['HASH'=> \%check_cherry_pick_cache],3586;35873588 tie my%has_no_changes_cache=>'Memoize::Storable',3589"$cache_path/has_no_changes.db",'nstore';3590 memoize 'has_no_changes',3591 SCALAR_CACHE => ['HASH'=> \%has_no_changes_cache],3592 LIST_CACHE =>'FAULT',3593;3594}35953596sub unmemoize_svn_mergeinfo_functions {3597return ifnot$memoized;3598$memoized=0;35993600 Memoize::unmemoize 'lookup_svn_merge';3601 Memoize::unmemoize 'check_cherry_pick';3602 Memoize::unmemoize 'has_no_changes';3603}36043605 Memoize::memoize 'Git::SVN::repos_root';3606}36073608END{3609# Force cache writeout explicitly instead of waiting for3610# global destruction to avoid segfault in Storable:3611# http://rt.cpan.org/Public/Bug/Display.html?id=360873612 unmemoize_svn_mergeinfo_functions();3613}36143615sub parents_exclude {3616my$parents=shift;3617my@commits=@_;3618return unless@commits;36193620my@excluded;3621my$excluded;3622do{3623my@cmd= ('rev-list',"-1",@commits,"--not",@$parents);3624$excluded= command_oneline(@cmd);3625if($excluded) {3626my@new;3627my$found;3628formy$commit(@commits) {3629if($commiteq$excluded) {3630push@excluded,$commit;3631$found++;3632last;3633}3634else{3635push@new,$commit;3636}3637}3638die"saw commit '$excluded' in rev-list output, "3639."but we didn't ask for that commit (wanted:@commits--not@$parents)"3640unless$found;3641@commits=@new;3642}3643}3644while($excludedand@commits);36453646return@excluded;3647}364836493650# note: this function should only be called if the various dirprops3651# have actually changed3652sub find_extra_svn_parents {3653my($self,$ed,$mergeinfo,$parents) =@_;3654# aha! svk:merge property changed...36553656 memoize_svn_mergeinfo_functions();36573658# We first search for merged tips which are not in our3659# history. Then, we figure out which git revisions are in3660# that tip, but not this revision. If all of those revisions3661# are now marked as merge, we can add the tip as a parent.3662my@merges=split"\n",$mergeinfo;3663my@merge_tips;3664my$url=$self->{url};3665my$uuid=$self->ra_uuid;3666my%ranges;3667formy$merge(@merges) {3668my($tip_commit,@ranges) =3669 lookup_svn_merge($uuid,$url,$merge);3670unless(!$tip_commitor3671grep{$_eq$tip_commit}@$parents) {3672push@merge_tips,$tip_commit;3673$ranges{$tip_commit} = \@ranges;3674}else{3675push@merge_tips,undef;3676}3677}36783679my%excluded=map{$_=>1}3680 parents_exclude($parents,grep{defined}@merge_tips);36813682# check merge tips for new parents3683my@new_parents;3684formy$merge_tip(@merge_tips) {3685my$spec=shift@merges;3686next unless$merge_tipand$excluded{$merge_tip};36873688my$ranges=$ranges{$merge_tip};36893690# check out 'new' tips3691my$merge_base;3692eval{3693$merge_base= command_oneline(3694"merge-base",3695@$parents,$merge_tip,3696);3697};3698if($@) {3699die"An error occurred during merge-base"3700unless$@->isa("Git::Error::Command");37013702warn"W: Cannot find common ancestor between ".3703"@$parentsand$merge_tip. Ignoring merge info.\n";3704next;3705}37063707# double check that there are no missing non-merge commits3708my(@incomplete) = check_cherry_pick(3709$merge_base,$merge_tip,3710$parents,3711@$ranges,3712);37133714if(@incomplete) {3715warn"W:svn cherry-pick ignored ($spec) - missing "3716.@incomplete." commit(s) (eg$incomplete[0])\n";3717}else{3718warn3719"Found merge parent (svn:mergeinfo prop): ",3720$merge_tip,"\n";3721push@new_parents,$merge_tip;3722}3723}37243725# cater for merges which merge commits from multiple branches3726if(@new_parents>1) {3727for(my$i=0;$i<=$#new_parents;$i++) {3728for(my$j=0;$j<=$#new_parents;$j++) {3729next if$i==$j;3730next unless$new_parents[$i];3731next unless$new_parents[$j];3732my$revs= command_oneline(3733"rev-list","-1",3734"$new_parents[$i]..$new_parents[$j]",3735);3736if( !$revs) {3737undef($new_parents[$j]);3738}3739}3740}3741}3742push@$parents,grep{defined}@new_parents;3743}37443745sub make_log_entry {3746my($self,$rev,$parents,$ed) =@_;3747my$untracked=$self->get_untracked($ed);37483749my@parents=@$parents;3750my$ps=$ed->{path_strip} ||"";3751formy$path(grep{m/$ps/} %{$ed->{dir_prop}} ) {3752my$props=$ed->{dir_prop}{$path};3753if($props->{"svk:merge"} ) {3754$self->find_extra_svk_parents3755($ed,$props->{"svk:merge"}, \@parents);3756}3757if($props->{"svn:mergeinfo"} ) {3758$self->find_extra_svn_parents3759($ed,3760$props->{"svn:mergeinfo"},3761 \@parents);3762}3763}37643765open my$un,'>>',"$self->{dir}/unhandled.log"or croak $!;3766print$un"r$rev\n"or croak $!;3767print$un $_,"\n"foreach@$untracked;3768my%log_entry= ( parents => \@parents, revision =>$rev,3769log=>'');37703771my$headrev;3772my$logged=delete$self->{logged_rev_props};3773if(!$logged||$self->{-want_revprops}) {3774my$rp=$self->ra->rev_proplist($rev);3775foreach(sort keys%$rp) {3776my$v=$rp->{$_};3777if(/^svn:(author|date|log)$/) {3778$log_entry{$1} =$v;3779}elsif($_eq'svm:headrev') {3780$headrev=$v;3781}else{3782print$un" rev_prop: ", uri_encode($_),' ',3783 uri_encode($v),"\n";3784}3785}3786}else{3787map{$log_entry{$_} =$logged->{$_} }keys%$logged;3788}3789close$unor croak $!;37903791$log_entry{date} = parse_svn_date($log_entry{date});3792$log_entry{log} .="\n";3793my$author=$log_entry{author} = check_author($log_entry{author});3794my($name,$email) =defined$::users{$author} ? @{$::users{$author}}3795: ($author,undef);37963797my($commit_name,$commit_email) = ($name,$email);3798if($_use_log_author) {3799my$name_field;3800if($log_entry{log} =~/From:\s+(.*\S)\s*\n/i) {3801$name_field=$1;3802}elsif($log_entry{log} =~/Signed-off-by:\s+(.*\S)\s*\n/i) {3803$name_field=$1;3804}3805if(!defined$name_field) {3806if(!defined$email) {3807$email=$name;3808}3809}elsif($name_field=~/(.*?)\s+<(.*)>/) {3810($name,$email) = ($1,$2);3811}elsif($name_field=~/(.*)@/) {3812($name,$email) = ($1,$name_field);3813}else{3814($name,$email) = ($name_field,$name_field);3815}3816}3817if(defined$headrev&&$self->use_svm_props) {3818if($self->rewrite_root) {3819die"Can't have both 'useSvmProps' and 'rewriteRoot' ",3820"options set!\n";3821}3822if($self->rewrite_uuid) {3823die"Can't have both 'useSvmProps' and 'rewriteUUID' ",3824"options set!\n";3825}3826my($uuid,$r) =$headrev=~m{^([a-f\d\-]{30,}):(\d+)$}i;3827# we don't want "SVM: initializing mirror for junk" ...3828returnundefif$r==0;3829my$svm=$self->svm;3830if($uuidne$svm->{uuid}) {3831die"UUID mismatch on SVM path:\n",3832"expected:$svm->{uuid}\n",3833" got:$uuid\n";3834}3835my$full_url=$self->full_url;3836$full_url=~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or3837die"Failed to replace '$svm->{replace}' with ",3838"'$svm->{source}' in$full_url\n";3839# throw away username for storing in records3840 remove_username($full_url);3841$log_entry{metadata} ="$full_url\@$r$uuid";3842$log_entry{svm_revision} =$r;3843$email||="$author\@$uuid";3844$commit_email||="$author\@$uuid";3845}elsif($self->use_svnsync_props) {3846my$full_url=$self->svnsync->{url};3847$full_url.="/$self->{path}"iflength$self->{path};3848 remove_username($full_url);3849my$uuid=$self->svnsync->{uuid};3850$log_entry{metadata} ="$full_url\@$rev$uuid";3851$email||="$author\@$uuid";3852$commit_email||="$author\@$uuid";3853}else{3854my$url=$self->metadata_url;3855 remove_username($url);3856my$uuid=$self->rewrite_uuid||$self->ra->get_uuid;3857$log_entry{metadata} ="$url\@$rev".$uuid;3858$email||="$author\@".$uuid;3859$commit_email||="$author\@".$uuid;3860}3861$log_entry{name} =$name;3862$log_entry{email} =$email;3863$log_entry{commit_name} =$commit_name;3864$log_entry{commit_email} =$commit_email;3865 \%log_entry;3866}38673868sub fetch {3869my($self,$min_rev,$max_rev,@parents) =@_;3870my($last_rev,$last_commit) =$self->last_rev_commit;3871my($base,$head) =$self->get_fetch_range($min_rev,$max_rev);3872$self->ra->gs_fetch_loop_common($base,$head, [$self]);3873}38743875sub set_tree_cb {3876my($self,$log_entry,$tree,$rev,$date,$author) =@_;3877$self->{inject_parents} = {$rev=>$tree};3878$self->fetch(undef,undef);3879}38803881sub set_tree {3882my($self,$tree) = (shift,shift);3883my$log_entry= ::get_commit_entry($tree);3884unless($self->{last_rev}) {3885::fatal("Must have an existing revision to commit");3886}3887my%ed_opts= ( r =>$self->{last_rev},3888log=>$log_entry->{log},3889 ra =>$self->ra,3890 tree_a =>$self->{last_commit},3891 tree_b =>$tree,3892 editor_cb =>sub{3893$self->set_tree_cb($log_entry,$tree,@_) },3894 svn_path =>$self->{path} );3895if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {3896print"No changes\nr$self->{last_rev} =$tree\n";3897}3898}38993900sub rebuild_from_rev_db {3901my($self,$path) =@_;3902my$r= -1;3903open my$fh,'<',$pathor croak "open:$!";3904binmode$fhor croak "binmode:$!";3905while(<$fh>) {3906length($_) ==41or croak "inconsistent size in ($_) != 41";3907chomp($_);3908++$r;3909next if$_eq('0' x 40);3910$self->rev_map_set($r,$_);3911print"r$r=$_\n";3912}3913close$fhor croak "close:$!";3914unlink$pathor croak "unlink:$!";3915}39163917sub rebuild {3918my($self) =@_;3919my$map_path=$self->map_path;3920my$partial= (-e $map_path&& ! -z $map_path);3921return unless::verify_ref($self->refname.'^0');3922if(!$partial&& ($self->use_svm_props||$self->no_metadata)) {3923my$rev_db=$self->rev_db_path;3924$self->rebuild_from_rev_db($rev_db);3925if($self->use_svm_props) {3926my$svm_rev_db=$self->rev_db_path($self->svm_uuid);3927$self->rebuild_from_rev_db($svm_rev_db);3928}3929$self->unlink_rev_db_symlink;3930return;3931}3932print"Rebuilding$map_path...\n"if(!$partial);3933my($base_rev,$head) = ($partial?$self->rev_map_max_norebuild(1) :3934(undef,undef));3935my($log,$ctx) =3936 command_output_pipe(qw/rev-list --pretty=raw --reverse/,3937($head?"$head..":"") .$self->refname,3938'--');3939my$metadata_url=$self->metadata_url;3940 remove_username($metadata_url);3941my$svn_uuid=$self->rewrite_uuid||$self->ra_uuid;3942my$c;3943while(<$log>) {3944if(m{^commit ($::sha1)$}) {3945$c=$1;3946next;3947}3948next unlesss{^\s*(git-svn-id:)}{$1};3949my($url,$rev,$uuid) = ::extract_metadata($_);3950 remove_username($url);39513952# ignore merges (from set-tree)3953next if(!defined$rev|| !$uuid);39543955# if we merged or otherwise started elsewhere, this is3956# how we break out of it3957if(($uuidne$svn_uuid) ||3958($metadata_url&&$url&& ($urlne$metadata_url))) {3959next;3960}3961if($partial&&$head) {3962print"Partial-rebuilding$map_path...\n";3963print"Currently at$base_rev=$head\n";3964$head=undef;3965}39663967$self->rev_map_set($rev,$c);3968print"r$rev=$c\n";3969}3970 command_close_pipe($log,$ctx);3971print"Done rebuilding$map_path\n"if(!$partial|| !$head);3972my$rev_db_path=$self->rev_db_path;3973if(-f $self->rev_db_path) {3974unlink$self->rev_db_pathor croak "unlink:$!";3975}3976$self->unlink_rev_db_symlink;3977}39783979# rev_map:3980# Tie::File seems to be prone to offset errors if revisions get sparse,3981# it's not that fast, either. Tie::File is also not in Perl 5.6. So3982# one of my favorite modules is out :< Next up would be one of the DBM3983# modules, but I'm not sure which is most portable...3984#3985# This is the replacement for the rev_db format, which was too big3986# and inefficient for large repositories with a lot of sparse history3987# (mainly tags)3988#3989# The format is this:3990# - 24 bytes for every record,3991# * 4 bytes for the integer representing an SVN revision number3992# * 20 bytes representing the sha1 of a git commit3993# - No empty padding records like the old format3994# (except the last record, which can be overwritten)3995# - new records are written append-only since SVN revision numbers3996# increase monotonically3997# - lookups on SVN revision number are done via a binary search3998# - Piping the file to xxd -c24 is a good way of dumping it for3999# viewing or editing (piped back through xxd -r), should the need4000# ever arise.4001# - The last record can be padding revision with an all-zero sha14002# This is used to optimize fetch performance when using multiple4003# "fetch" directives in .git/config4004#4005# These files are disposable unless noMetadata or useSvmProps is set40064007sub _rev_map_set {4008my($fh,$rev,$commit) =@_;40094010binmode$fhor croak "binmode:$!";4011my$size= (stat($fh))[7];4012($size%24) ==0or croak "inconsistent size:$size";40134014my$wr_offset=0;4015if($size>0) {4016sysseek($fh, -24, SEEK_END)or croak "seek:$!";4017my$read=sysread($fh,my$buf,24)or croak "read:$!";4018$read==24or croak "read only$readbytes (!= 24)";4019my($last_rev,$last_commit) =unpack(rev_map_fmt,$buf);4020if($last_commiteq('0' x40)) {4021if($size>=48) {4022sysseek($fh, -48, SEEK_END)or croak "seek:$!";4023$read=sysread($fh,$buf,24)or4024 croak "read:$!";4025$read==24or4026 croak "read only$readbytes (!= 24)";4027($last_rev,$last_commit) =4028unpack(rev_map_fmt,$buf);4029if($last_commiteq('0' x40)) {4030 croak "inconsistent .rev_map\n";4031}4032}4033if($last_rev>=$rev) {4034 croak "last_rev is higher!:$last_rev>=$rev";4035}4036$wr_offset= -24;4037}4038}4039sysseek($fh,$wr_offset, SEEK_END)or croak "seek:$!";4040syswrite($fh,pack(rev_map_fmt,$rev,$commit),24) ==24or4041 croak "write:$!";4042}40434044sub _rev_map_reset {4045my($fh,$rev,$commit) =@_;4046my$c= _rev_map_get($fh,$rev);4047$ceq$commitor die"_rev_map_reset(@_) commit$cdoes not match!\n";4048my$offset=sysseek($fh,0, SEEK_CUR)or croak "seek:$!";4049truncate$fh,$offsetor croak "truncate:$!";4050}40514052sub mkfile {4053my($path) =@_;4054unless(-e $path) {4055my($dir,$base) = ($path=~ m#^(.*?)/?([^/]+)$#);4056 mkpath([$dir])unless-d $dir;4057open my$fh,'>>',$pathor die"Couldn't create$path:$!\n";4058close$fhor die"Couldn't close (create)$path:$!\n";4059}4060}40614062sub rev_map_set {4063my($self,$rev,$commit,$update_ref,$uuid) =@_;4064defined$commitor die"missing arg3\n";4065length$commit==40or die"arg3 must be a full SHA1 hexsum\n";4066my$db=$self->map_path($uuid);4067my$db_lock="$db.lock";4068my$sigmask;4069$update_ref||=0;4070if($update_ref) {4071$sigmask= POSIX::SigSet->new();4072my$signew= POSIX::SigSet->new(SIGINT, SIGHUP, SIGTERM,4073 SIGALRM, SIGUSR1, SIGUSR2);4074 sigprocmask(SIG_BLOCK,$signew,$sigmask)or4075 croak "Can't block signals:$!";4076}4077 mkfile($db);40784079$LOCKFILES{$db_lock} =1;4080my$sync;4081# both of these options make our .rev_db file very, very important4082# and we can't afford to lose it because rebuild() won't work4083if($self->use_svm_props||$self->no_metadata) {4084$sync=1;4085 copy($db,$db_lock)or die"rev_map_set(@_): ",4086"Failed to copy: ",4087"$db=>$db_lock($!)\n";4088}else{4089rename$db,$db_lockor die"rev_map_set(@_): ",4090"Failed to rename: ",4091"$db=>$db_lock($!)\n";4092}40934094sysopen(my$fh,$db_lock, O_RDWR | O_CREAT)4095or croak "Couldn't open$db_lock:$!\n";4096$update_refeq'reset'? _rev_map_reset($fh,$rev,$commit) :4097 _rev_map_set($fh,$rev,$commit);4098if($sync) {4099$fh->flushor die"Couldn't flush$db_lock:$!\n";4100$fh->syncor die"Couldn't sync$db_lock:$!\n";4101}4102close$fhor croak $!;4103if($update_ref) {4104$_head=$self;4105my$note="";4106$note=" ($update_ref)"if($update_ref!~/^\d*$/);4107 command_noisy('update-ref','-m',"r$rev$note",4108$self->refname,$commit);4109}4110rename$db_lock,$dbor die"rev_map_set(@_): ","Failed to rename: ",4111"$db_lock=>$db($!)\n";4112delete$LOCKFILES{$db_lock};4113if($update_ref) {4114 sigprocmask(SIG_SETMASK,$sigmask)or4115 croak "Can't restore signal mask:$!";4116}4117}41184119# If want_commit, this will return an array of (rev, commit) where4120# commit _must_ be a valid commit in the archive.4121# Otherwise, it'll return the max revision (whether or not the4122# commit is valid or just a 0x40 placeholder).4123sub rev_map_max {4124my($self,$want_commit) =@_;4125$self->rebuild;4126my($r,$c) =$self->rev_map_max_norebuild($want_commit);4127$want_commit? ($r,$c) :$r;4128}41294130sub rev_map_max_norebuild {4131my($self,$want_commit) =@_;4132my$map_path=$self->map_path;4133stat$map_pathorreturn$want_commit? (0,undef) :0;4134sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4135binmode$fhor croak "binmode:$!";4136my$size= (stat($fh))[7];4137($size%24) ==0or croak "inconsistent size:$size";41384139if($size==0) {4140close$fhor croak "close:$!";4141return$want_commit? (0,undef) :0;4142}41434144sysseek($fh, -24, SEEK_END)or croak "seek:$!";4145sysread($fh,my$buf,24) ==24or croak "read:$!";4146my($r,$c) =unpack(rev_map_fmt,$buf);4147if($want_commit&&$ceq('0' x40)) {4148if($size<48) {4149return$want_commit? (0,undef) :0;4150}4151sysseek($fh, -48, SEEK_END)or croak "seek:$!";4152sysread($fh,$buf,24) ==24or croak "read:$!";4153($r,$c) =unpack(rev_map_fmt,$buf);4154if($ceq('0'x40)) {4155 croak "Penultimate record is all-zeroes in$map_path";4156}4157}4158close$fhor croak "close:$!";4159$want_commit? ($r,$c) :$r;4160}41614162sub rev_map_get {4163my($self,$rev,$uuid) =@_;4164my$map_path=$self->map_path($uuid);4165returnundefunless-e $map_path;41664167sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4168my$c= _rev_map_get($fh,$rev);4169close($fh)or croak "close:$!";4170$c4171}41724173sub _rev_map_get {4174my($fh,$rev) =@_;41754176binmode$fhor croak "binmode:$!";4177my$size= (stat($fh))[7];4178($size%24) ==0or croak "inconsistent size:$size";41794180if($size==0) {4181returnundef;4182}41834184my($l,$u) = (0,$size-24);4185my($r,$c,$buf);41864187while($l<=$u) {4188my$i=int(($l/24+$u/24) /2) *24;4189sysseek($fh,$i, SEEK_SET)or croak "seek:$!";4190sysread($fh,my$buf,24) ==24or croak "read:$!";4191my($r,$c) =unpack(rev_map_fmt,$buf);41924193if($r<$rev) {4194$l=$i+24;4195}elsif($r>$rev) {4196$u=$i-24;4197}else{# $r == $rev4198return$ceq('0' x 40) ?undef:$c;4199}4200}4201undef;4202}42034204# Finds the first svn revision that exists on (if $eq_ok is true) or4205# before $rev for the current branch. It will not search any lower4206# than $min_rev. Returns the git commit hash and svn revision number4207# if found, else (undef, undef).4208sub find_rev_before {4209my($self,$rev,$eq_ok,$min_rev) =@_;4210--$revunless$eq_ok;4211$min_rev||=1;4212my$max_rev=$self->rev_map_max;4213$rev=$max_revif($rev>$max_rev);4214while($rev>=$min_rev) {4215if(my$c=$self->rev_map_get($rev)) {4216return($rev,$c);4217}4218--$rev;4219}4220return(undef,undef);4221}42224223# Finds the first svn revision that exists on (if $eq_ok is true) or4224# after $rev for the current branch. It will not search any higher4225# than $max_rev. Returns the git commit hash and svn revision number4226# if found, else (undef, undef).4227sub find_rev_after {4228my($self,$rev,$eq_ok,$max_rev) =@_;4229++$revunless$eq_ok;4230$max_rev||=$self->rev_map_max;4231while($rev<=$max_rev) {4232if(my$c=$self->rev_map_get($rev)) {4233return($rev,$c);4234}4235++$rev;4236}4237return(undef,undef);4238}42394240sub _new {4241my($class,$repo_id,$ref_id,$path) =@_;4242unless(defined$repo_id&&length$repo_id) {4243$repo_id=$Git::SVN::default_repo_id;4244}4245unless(defined$ref_id&&length$ref_id) {4246$_prefix=''unlessdefined($_prefix);4247$_[2] =$ref_id=4248"refs/remotes/$_prefix$Git::SVN::default_ref_id";4249}4250$_[1] =$repo_id;4251my$dir="$ENV{GIT_DIR}/svn/$ref_id";42524253# Older repos imported by us used $GIT_DIR/svn/foo instead of4254# $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo4255if($ref_id=~m{^refs/remotes/(.*)}) {4256my$old_dir="$ENV{GIT_DIR}/svn/$1";4257if(-d $old_dir&& ! -d $dir) {4258$dir=$old_dir;4259}4260}42614262$_[3] =$path=''unless(defined$path);4263 mkpath([$dir]);4264bless{4265 ref_id =>$ref_id, dir =>$dir,index=>"$dir/index",4266 path =>$path, config =>"$ENV{GIT_DIR}/svn/config",4267 map_root =>"$dir/.rev_map", repo_id =>$repo_id},$class;4268}42694270# for read-only access of old .rev_db formats4271sub unlink_rev_db_symlink {4272my($self) =@_;4273my$link=$self->rev_db_path;4274$link=~s/\.[\w-]+$//or croak "missing UUID at the end of$link";4275if(-l $link) {4276unlink$linkor croak "unlink:$linkfailed!";4277}4278}42794280sub rev_db_path {4281my($self,$uuid) =@_;4282my$db_path=$self->map_path($uuid);4283$db_path=~s{/\.rev_map\.}{/\.rev_db\.}4284or croak "map_path:$db_pathdoes not contain '/.rev_map.' !";4285$db_path;4286}42874288# the new replacement for .rev_db4289sub map_path {4290my($self,$uuid) =@_;4291$uuid||=$self->ra_uuid;4292"$self->{map_root}.$uuid";4293}42944295sub uri_encode {4296my($f) =@_;4297$f=~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;4298$f4299}43004301sub uri_decode {4302my($f) =@_;4303$f=~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;4304$f4305}43064307sub remove_username {4308$_[0] =~s{^([^:]*://)[^@]+@}{$1};4309}43104311package Git::SVN::Prompt;4312use strict;4313use warnings;4314require SVN::Core;4315use vars qw/$_no_auth_cache $_username/;43164317sub simple {4318my($cred,$realm,$default_username,$may_save,$pool) =@_;4319$may_save=undefif$_no_auth_cache;4320$default_username=$_usernameifdefined$_username;4321if(defined$default_username&&length$default_username) {4322if(defined$realm&&length$realm) {4323print STDERR "Authentication realm:$realm\n";4324 STDERR->flush;4325}4326$cred->username($default_username);4327}else{4328 username($cred,$realm,$may_save,$pool);4329}4330$cred->password(_read_password("Password for '".4331$cred->username."': ",$realm));4332$cred->may_save($may_save);4333$SVN::_Core::SVN_NO_ERROR;4334}43354336sub ssl_server_trust {4337my($cred,$realm,$failures,$cert_info,$may_save,$pool) =@_;4338$may_save=undefif$_no_auth_cache;4339print STDERR "Error validating server certificate for '$realm':\n";4340{4341no warnings 'once';4342# All variables SVN::Auth::SSL::* are used only once,4343# so we're shutting up Perl warnings about this.4344if($failures&$SVN::Auth::SSL::UNKNOWNCA) {4345print STDERR " - The certificate is not issued ",4346"by a trusted authority. Use the\n",4347" fingerprint to validate ",4348"the certificate manually!\n";4349}4350if($failures&$SVN::Auth::SSL::CNMISMATCH) {4351print STDERR " - The certificate hostname ",4352"does not match.\n";4353}4354if($failures&$SVN::Auth::SSL::NOTYETVALID) {4355print STDERR " - The certificate is not yet valid.\n";4356}4357if($failures&$SVN::Auth::SSL::EXPIRED) {4358print STDERR " - The certificate has expired.\n";4359}4360if($failures&$SVN::Auth::SSL::OTHER) {4361print STDERR " - The certificate has ",4362"an unknown error.\n";4363}4364}# no warnings 'once'4365printf STDERR4366"Certificate information:\n".4367" - Hostname:%s\n".4368" - Valid: from%suntil%s\n".4369" - Issuer:%s\n".4370" - Fingerprint:%s\n",4371map$cert_info->$_,qw(hostname valid_from valid_until4372 issuer_dname fingerprint);4373my$choice;4374prompt:4375print STDERR $may_save?4376"(R)eject, accept (t)emporarily or accept (p)ermanently? ":4377"(R)eject or accept (t)emporarily? ";4378 STDERR->flush;4379$choice=lc(substr(<STDIN> ||'R',0,1));4380if($choice=~/^t$/i) {4381$cred->may_save(undef);4382}elsif($choice=~/^r$/i) {4383return-1;4384}elsif($may_save&&$choice=~/^p$/i) {4385$cred->may_save($may_save);4386}else{4387goto prompt;4388}4389$cred->accepted_failures($failures);4390$SVN::_Core::SVN_NO_ERROR;4391}43924393sub ssl_client_cert {4394my($cred,$realm,$may_save,$pool) =@_;4395$may_save=undefif$_no_auth_cache;4396print STDERR "Client certificate filename: ";4397 STDERR->flush;4398chomp(my$filename= <STDIN>);4399$cred->cert_file($filename);4400$cred->may_save($may_save);4401$SVN::_Core::SVN_NO_ERROR;4402}44034404sub ssl_client_cert_pw {4405my($cred,$realm,$may_save,$pool) =@_;4406$may_save=undefif$_no_auth_cache;4407$cred->password(_read_password("Password: ",$realm));4408$cred->may_save($may_save);4409$SVN::_Core::SVN_NO_ERROR;4410}44114412sub username {4413my($cred,$realm,$may_save,$pool) =@_;4414$may_save=undefif$_no_auth_cache;4415if(defined$realm&&length$realm) {4416print STDERR "Authentication realm:$realm\n";4417}4418my$username;4419if(defined$_username) {4420$username=$_username;4421}else{4422print STDERR "Username: ";4423 STDERR->flush;4424chomp($username= <STDIN>);4425}4426$cred->username($username);4427$cred->may_save($may_save);4428$SVN::_Core::SVN_NO_ERROR;4429}44304431sub _read_password {4432my($prompt,$realm) =@_;4433my$password='';4434if(exists$ENV{GIT_ASKPASS}) {4435open(PH,"-|",$ENV{GIT_ASKPASS},$prompt);4436$password= <PH>;4437$password=~s/[\012\015]//;# \n\r4438close(PH);4439}else{4440print STDERR $prompt;4441 STDERR->flush;4442require Term::ReadKey;4443 Term::ReadKey::ReadMode('noecho');4444while(defined(my$key= Term::ReadKey::ReadKey(0))) {4445last if$key=~/[\012\015]/;# \n\r4446$password.=$key;4447}4448 Term::ReadKey::ReadMode('restore');4449print STDERR "\n";4450 STDERR->flush;4451}4452$password;4453}44544455package SVN::Git::Fetcher;4456use vars qw/@ISA $_ignore_regex $_preserve_empty_dirs $_placeholder_filename4457@deleted_gpath %added_placeholder $repo_id/;4458use strict;4459use warnings;4460use Carp qw/croak/;4461use File::Basename qw/dirname/;4462use IO::File qw//;44634464# file baton members: path, mode_a, mode_b, pool, fh, blob, base4465sub new {4466my($class,$git_svn,$switch_path) =@_;4467my$self= SVN::Delta::Editor->new;4468bless$self,$class;4469if(exists$git_svn->{last_commit}) {4470$self->{c} =$git_svn->{last_commit};4471$self->{empty_symlinks} =4472 _mark_empty_symlinks($git_svn,$switch_path);4473}44744475# some options are read globally, but can be overridden locally4476# per [svn-remote "..."] section. Command-line options will *NOT*4477# override options set in an [svn-remote "..."] section4478$repo_id=$git_svn->{repo_id};4479my$k="svn-remote.$repo_id.ignore-paths";4480my$v=eval{ command_oneline('config','--get',$k) };4481$self->{ignore_regex} =$v;44824483$k="svn-remote.$repo_id.preserve-empty-dirs";4484$v=eval{ command_oneline('config','--get','--bool',$k) };4485if($v&&$veq'true') {4486$_preserve_empty_dirs=1;4487$k="svn-remote.$repo_id.placeholder-filename";4488$v=eval{ command_oneline('config','--get',$k) };4489$_placeholder_filename=$v;4490}44914492# Load the list of placeholder files added during previous invocations.4493$k="svn-remote.$repo_id.added-placeholder";4494$v=eval{ command_oneline('config','--get-all',$k) };4495if($_preserve_empty_dirs&&$v) {4496# command() prints errors to stderr, so we only call it if4497# command_oneline() succeeded.4498my@v= command('config','--get-all',$k);4499$added_placeholder{ dirname($_) } =$_foreach@v;4500}45014502$self->{empty} = {};4503$self->{dir_prop} = {};4504$self->{file_prop} = {};4505$self->{absent_dir} = {};4506$self->{absent_file} = {};4507$self->{gii} =$git_svn->tmp_index_do(sub{ Git::IndexInfo->new});4508$self->{pathnameencoding} = Git::config('svn.pathnameencoding');4509$self;4510}45114512# this uses the Ra object, so it must be called before do_{switch,update},4513# not inside them (when the Git::SVN::Fetcher object is passed) to4514# do_{switch,update}4515sub _mark_empty_symlinks {4516my($git_svn,$switch_path) =@_;4517my$bool= Git::config_bool('svn.brokenSymlinkWorkaround');4518return{}if(!defined($bool)) || (defined($bool) && !$bool);45194520my%ret;4521my($rev,$cmt) =$git_svn->last_rev_commit;4522return{}unless($rev&&$cmt);45234524# allow the warning to be printed for each revision we fetch to4525# ensure the user sees it. The user can also disable the workaround4526# on the repository even while git svn is running and the next4527# revision fetched will skip this expensive function.4528my$printed_warning;4529chomp(my$empty_blob=`git hash-object -t blob --stdin < /dev/null`);4530my($ls,$ctx) = command_output_pipe(qw/ls-tree -r -z/,$cmt);4531local$/="\0";4532my$pfx=defined($switch_path) ?$switch_path:$git_svn->{path};4533$pfx.='/'iflength($pfx);4534while(<$ls>) {4535chomp;4536s/\A100644 blob $empty_blob\t//oornext;4537unless($printed_warning) {4538print STDERR "Scanning for empty symlinks, ",4539"this may take a while if you have ",4540"many empty files\n",4541"You may disable this with `",4542"git config svn.brokenSymlinkWorkaround ",4543"false'.\n",4544"This may be done in a different ",4545"terminal without restarting ",4546"git svn\n";4547$printed_warning=1;4548}4549my$path=$_;4550my(undef,$props) =4551$git_svn->ra->get_file($pfx.$path,$rev,undef);4552if($props->{'svn:special'}) {4553$ret{$path} =1;4554}4555}4556 command_close_pipe($ls,$ctx);4557 \%ret;4558}45594560# returns true if a given path is inside a ".git" directory4561sub in_dot_git {4562$_[0] =~m{(?:^|/)\.git(?:/|$)};4563}45644565# return value: 0 -- don't ignore, 1 -- ignore4566sub is_path_ignored {4567my($self,$path) =@_;4568return1if in_dot_git($path);4569return1ifdefined($self->{ignore_regex}) &&4570$path=~m!$self->{ignore_regex}!;4571return0unlessdefined($_ignore_regex);4572return1if$path=~m!$_ignore_regex!o;4573return0;4574}45754576sub set_path_strip {4577my($self,$path) =@_;4578$self->{path_strip} =qr/^\Q$path\E(\/|$)/iflength$path;4579}45804581sub open_root {4582{ path =>''};4583}45844585sub open_directory {4586my($self,$path,$pb,$rev) =@_;4587{ path =>$path};4588}45894590sub git_path {4591my($self,$path) =@_;4592if(my$enc=$self->{pathnameencoding}) {4593require Encode;4594 Encode::from_to($path,'UTF-8',$enc);4595}4596if($self->{path_strip}) {4597$path=~s!$self->{path_strip}!!or4598die"Failed to strip path '$path' ($self->{path_strip})\n";4599}4600$path;4601}46024603sub delete_entry {4604my($self,$path,$rev,$pb) =@_;4605returnundefif$self->is_path_ignored($path);46064607my$gpath=$self->git_path($path);4608returnundefif($gpatheq'');46094610# remove entire directories.4611my($tree) = (command('ls-tree','-z',$self->{c},"./$gpath")4612=~/\A040000 tree ([a-f\d]{40})\t\Q$gpath\E\0/);4613if($tree) {4614my($ls,$ctx) = command_output_pipe(qw/ls-tree4615-r --name-only -z/,4616$tree);4617local$/="\0";4618while(<$ls>) {4619chomp;4620my$rmpath="$gpath/$_";4621$self->{gii}->remove($rmpath);4622print"\tD\t$rmpath\n"unless$::_q;4623}4624print"\tD\t$gpath/\n"unless$::_q;4625 command_close_pipe($ls,$ctx);4626}else{4627$self->{gii}->remove($gpath);4628print"\tD\t$gpath\n"unless$::_q;4629}4630# Don't add to @deleted_gpath if we're deleting a placeholder file.4631push@deleted_gpath,$gpathunless$added_placeholder{dirname($path)};4632$self->{empty}->{$path} =0;4633undef;4634}46354636sub open_file {4637my($self,$path,$pb,$rev) =@_;4638my($mode,$blob);46394640goto out if$self->is_path_ignored($path);46414642my$gpath=$self->git_path($path);4643($mode,$blob) = (command('ls-tree','-z',$self->{c},"./$gpath")4644=~/\A(\d{6}) blob ([a-f\d]{40})\t\Q$gpath\E\0/);4645unless(defined$mode&&defined$blob) {4646die"$pathwas not found in commit$self->{c} (r$rev)\n";4647}4648if($modeeq'100644'&&$self->{empty_symlinks}->{$path}) {4649$mode='120000';4650}4651out:4652{ path =>$path, mode_a =>$mode, mode_b =>$mode, blob =>$blob,4653 pool => SVN::Pool->new, action =>'M'};4654}46554656sub add_file {4657my($self,$path,$pb,$cp_path,$cp_rev) =@_;4658my$mode;46594660if(!$self->is_path_ignored($path)) {4661my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4662delete$self->{empty}->{$dir};4663$mode='100644';46644665if($added_placeholder{$dir}) {4666# Remove our placeholder file, if we created one.4667 delete_entry($self,$added_placeholder{$dir})4668unless$patheq$added_placeholder{$dir};4669delete$added_placeholder{$dir}4670}4671}46724673{ path =>$path, mode_a =>$mode, mode_b =>$mode,4674 pool => SVN::Pool->new, action =>'A'};4675}46764677sub add_directory {4678my($self,$path,$cp_path,$cp_rev) =@_;4679goto out if$self->is_path_ignored($path);4680my$gpath=$self->git_path($path);4681if($gpatheq'') {4682my($ls,$ctx) = command_output_pipe(qw/ls-tree4683-r --name-only -z/,4684$self->{c});4685local$/="\0";4686while(<$ls>) {4687chomp;4688$self->{gii}->remove($_);4689print"\tD\t$_\n"unless$::_q;4690push@deleted_gpath,$gpath;4691}4692 command_close_pipe($ls,$ctx);4693$self->{empty}->{$path} =0;4694}4695my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4696delete$self->{empty}->{$dir};4697$self->{empty}->{$path} =1;46984699if($added_placeholder{$dir}) {4700# Remove our placeholder file, if we created one.4701 delete_entry($self,$added_placeholder{$dir});4702delete$added_placeholder{$dir}4703}47044705out:4706{ path =>$path};4707}47084709sub change_dir_prop {4710my($self,$db,$prop,$value) =@_;4711returnundefif$self->is_path_ignored($db->{path});4712$self->{dir_prop}->{$db->{path}} ||= {};4713$self->{dir_prop}->{$db->{path}}->{$prop} =$value;4714undef;4715}47164717sub absent_directory {4718my($self,$path,$pb) =@_;4719returnundefif$self->is_path_ignored($path);4720$self->{absent_dir}->{$pb->{path}} ||= [];4721push@{$self->{absent_dir}->{$pb->{path}}},$path;4722undef;4723}47244725sub absent_file {4726my($self,$path,$pb) =@_;4727returnundefif$self->is_path_ignored($path);4728$self->{absent_file}->{$pb->{path}} ||= [];4729push@{$self->{absent_file}->{$pb->{path}}},$path;4730undef;4731}47324733sub change_file_prop {4734my($self,$fb,$prop,$value) =@_;4735returnundefif$self->is_path_ignored($fb->{path});4736if($propeq'svn:executable') {4737if($fb->{mode_b} !=120000) {4738$fb->{mode_b} =defined$value?100755:100644;4739}4740}elsif($propeq'svn:special') {4741$fb->{mode_b} =defined$value?120000:100644;4742}else{4743$self->{file_prop}->{$fb->{path}} ||= {};4744$self->{file_prop}->{$fb->{path}}->{$prop} =$value;4745}4746undef;4747}47484749sub apply_textdelta {4750my($self,$fb,$exp) =@_;4751returnundefif$self->is_path_ignored($fb->{path});4752my$fh= $::_repository->temp_acquire('svn_delta');4753# $fh gets auto-closed() by SVN::TxDelta::apply(),4754# (but $base does not,) so dup() it for reading in close_file4755open my$dup,'<&',$fhor croak $!;4756my$base= $::_repository->temp_acquire('git_blob');47574758if($fb->{blob}) {4759my($base_is_link,$size);47604761if($fb->{mode_a}eq'120000'&&4762!$self->{empty_symlinks}->{$fb->{path}}) {4763print$base'link 'or die"print$!\n";4764$base_is_link=1;4765}4766 retry:4767$size= $::_repository->cat_blob($fb->{blob},$base);4768die"Failed to read object$fb->{blob}"if($size<0);47694770if(defined$exp) {4771seek$base,0,0or croak $!;4772my$got= ::md5sum($base);4773if($gotne$exp) {4774my$err="Checksum mismatch: ".4775"$fb->{path}$fb->{blob}\n".4776"expected:$exp\n".4777" got:$got\n";4778if($base_is_link) {4779warn$err,4780"Retrying... (possibly ",4781"a bad symlink from SVN)\n";4782$::_repository->temp_reset($base);4783$base_is_link=0;4784goto retry;4785}4786die$err;4787}4788}4789}4790seek$base,0,0or croak $!;4791$fb->{fh} =$fh;4792$fb->{base} =$base;4793[ SVN::TxDelta::apply($base,$dup,undef,$fb->{path},$fb->{pool}) ];4794}47954796sub close_file {4797my($self,$fb,$exp) =@_;4798returnundefif$self->is_path_ignored($fb->{path});47994800my$hash;4801my$path=$self->git_path($fb->{path});4802if(my$fh=$fb->{fh}) {4803if(defined$exp) {4804seek($fh,0,0)or croak $!;4805my$got= ::md5sum($fh);4806if($gotne$exp) {4807die"Checksum mismatch:$path\n",4808"expected:$exp\ngot:$got\n";4809}4810}4811if($fb->{mode_b} ==120000) {4812sysseek($fh,0,0)or croak $!;4813my$rd=sysread($fh,my$buf,5);48144815if(!defined$rd) {4816 croak "sysread:$!\n";4817}elsif($rd==0) {4818warn"$pathhas mode 120000",4819" but it points to nothing\n",4820"converting to an empty file with mode",4821" 100644\n";4822$fb->{mode_b} ='100644';4823}elsif($bufne'link ') {4824warn"$pathhas mode 120000",4825" but is not a link\n";4826}else{4827my$tmp_fh= $::_repository->temp_acquire(4828'svn_hash');4829my$res;4830while($res=sysread($fh,my$str,1024)) {4831my$out=syswrite($tmp_fh,$str,$res);4832defined($out) &&$out==$res4833or croak("write ",4834 Git::temp_path($tmp_fh),4835":$!\n");4836}4837defined$resor croak $!;48384839($fh,$tmp_fh) = ($tmp_fh,$fh);4840 Git::temp_release($tmp_fh,1);4841}4842}48434844$hash= $::_repository->hash_and_insert_object(4845 Git::temp_path($fh));4846$hash=~/^[a-f\d]{40}$/or die"not a sha1:$hash\n";48474848 Git::temp_release($fb->{base},1);4849 Git::temp_release($fh,1);4850}else{4851$hash=$fb->{blob}or die"no blob information\n";4852}4853$fb->{pool}->clear;4854$self->{gii}->update($fb->{mode_b},$hash,$path)or croak $!;4855print"\t$fb->{action}\t$path\n"if$fb->{action} && ! $::_q;4856undef;4857}48584859sub abort_edit {4860my$self=shift;4861$self->{nr} =$self->{gii}->{nr};4862delete$self->{gii};4863$self->SUPER::abort_edit(@_);4864}48654866sub close_edit {4867my$self=shift;48684869if($_preserve_empty_dirs) {4870my@empty_dirs;48714872# Any entry flagged as empty that also has an associated4873# dir_prop represents a newly created empty directory.4874foreachmy$i(keys%{$self->{empty}}) {4875push@empty_dirs,$iifexists$self->{dir_prop}->{$i};4876}48774878# Search for directories that have become empty due subsequent4879# file deletes.4880push@empty_dirs,$self->find_empty_directories();48814882# Finally, add a placeholder file to each empty directory.4883$self->add_placeholder_file($_)foreach(@empty_dirs);48844885$self->stash_placeholder_list();4886}48874888$self->{git_commit_ok} =1;4889$self->{nr} =$self->{gii}->{nr};4890delete$self->{gii};4891$self->SUPER::close_edit(@_);4892}48934894sub find_empty_directories {4895my($self) =@_;4896my@empty_dirs;4897my%dirs=map{ dirname($_) =>1}@deleted_gpath;48984899foreachmy$dir(sort keys%dirs) {4900next if$direq".";49014902# If there have been any additions to this directory, there is4903# no reason to check if it is empty.4904my$skip_added=0;4905foreachmy$t(qw/dir_prop file_prop/) {4906foreachmy$path(keys%{$self->{$t} }) {4907if(exists$self->{$t}->{dirname($path)}) {4908$skip_added=1;4909last;4910}4911}4912last if$skip_added;4913}4914next if$skip_added;49154916# Use `git ls-tree` to get the filenames of this directory4917# that existed prior to this particular commit.4918my$ls= command('ls-tree','-z','--name-only',4919$self->{c},"$dir/");4920my%files=map{$_=>1}split(/\0/,$ls);49214922# Remove the filenames that were deleted during this commit.4923delete$files{$_}foreach(@deleted_gpath);49244925# Report the directory if there are no filenames left.4926push@empty_dirs,$dirunless(scalar%files);4927}4928@empty_dirs;4929}49304931sub add_placeholder_file {4932my($self,$dir) =@_;4933my$path="$dir/$_placeholder_filename";4934my$gpath=$self->git_path($path);49354936my$fh= $::_repository->temp_acquire($gpath);4937my$hash= $::_repository->hash_and_insert_object(Git::temp_path($fh));4938 Git::temp_release($fh,1);4939$self->{gii}->update('100644',$hash,$gpath)or croak $!;49404941# The directory should no longer be considered empty.4942delete$self->{empty}->{$dir}ifexists$self->{empty}->{$dir};49434944# Keep track of any placeholder files we create.4945$added_placeholder{$dir} =$path;4946}49474948sub stash_placeholder_list {4949my($self) =@_;4950my$k="svn-remote.$repo_id.added-placeholder";4951my$v=eval{ command_oneline('config','--get-all',$k) };4952 command_noisy('config','--unset-all',$k)if$v;4953foreach(values%added_placeholder) {4954 command_noisy('config','--add',$k,$_);4955}4956}49574958package SVN::Git::Editor;4959use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;4960use strict;4961use warnings;4962use Carp qw/croak/;4963use IO::File;49644965sub new {4966my($class,$opts) =@_;4967foreach(qw/svn_path r ra tree_a tree_b log editor_cb/) {4968die"$_required!\n"unless(defined$opts->{$_});4969}49704971my$pool= SVN::Pool->new;4972my$mods= generate_diff($opts->{tree_a},$opts->{tree_b});4973my$types= check_diff_paths($opts->{ra},$opts->{svn_path},4974$opts->{r},$mods);49754976# $opts->{ra} functions should not be used after this:4977my@ce=$opts->{ra}->get_commit_editor($opts->{log},4978$opts->{editor_cb},$pool);4979my$self= SVN::Delta::Editor->new(@ce,$pool);4980bless$self,$class;4981foreach(qw/svn_path r tree_a tree_b/) {4982$self->{$_} =$opts->{$_};4983}4984$self->{url} =$opts->{ra}->{url};4985$self->{mods} =$mods;4986$self->{types} =$types;4987$self->{pool} =$pool;4988$self->{bat} = {''=>$self->open_root($self->{r},$self->{pool}) };4989$self->{rm} = { };4990$self->{path_prefix} =length$self->{svn_path} ?4991"$self->{svn_path}/":'';4992$self->{config} =$opts->{config};4993$self->{mergeinfo} =$opts->{mergeinfo};4994return$self;4995}49964997sub generate_diff {4998my($tree_a,$tree_b) =@_;4999my@diff_tree=qw(diff-tree -z -r);5000if($_cp_similarity) {5001push@diff_tree,"-C$_cp_similarity";5002}else{5003push@diff_tree,'-C';5004}5005push@diff_tree,'--find-copies-harder'if$_find_copies_harder;5006push@diff_tree,"-l$_rename_limit"ifdefined$_rename_limit;5007push@diff_tree,$tree_a,$tree_b;5008my($diff_fh,$ctx) = command_output_pipe(@diff_tree);5009local$/="\0";5010my$state='meta';5011my@mods;5012while(<$diff_fh>) {5013chomp$_;# this gets rid of the trailing "\0"5014if($stateeq'meta'&& /^:(\d{6})\s(\d{6})\s5015($::sha1)\s($::sha1)\s5016([MTCRAD])\d*$/xo) {5017push@mods, { mode_a =>$1, mode_b =>$2,5018 sha1_a =>$3, sha1_b =>$4,5019 chg =>$5};5020if($5=~/^(?:C|R)$/) {5021$state='file_a';5022}else{5023$state='file_b';5024}5025}elsif($stateeq'file_a') {5026my$x=$mods[$#mods]or croak "Empty array\n";5027if($x->{chg} !~/^(?:C|R)$/) {5028 croak "Error parsing$_,$x->{chg}\n";5029}5030$x->{file_a} =$_;5031$state='file_b';5032}elsif($stateeq'file_b') {5033my$x=$mods[$#mods]or croak "Empty array\n";5034if(exists$x->{file_a} &&$x->{chg} !~/^(?:C|R)$/) {5035 croak "Error parsing$_,$x->{chg}\n";5036}5037if(!exists$x->{file_a} &&$x->{chg} =~/^(?:C|R)$/) {5038 croak "Error parsing$_,$x->{chg}\n";5039}5040$x->{file_b} =$_;5041$state='meta';5042}else{5043 croak "Error parsing$_\n";5044}5045}5046 command_close_pipe($diff_fh,$ctx);5047 \@mods;5048}50495050sub check_diff_paths {5051my($ra,$pfx,$rev,$mods) =@_;5052my%types;5053$pfx.='/'iflength$pfx;50545055sub type_diff_paths {5056my($ra,$types,$path,$rev) =@_;5057my@p=split m#/+#, $path;5058my$c=shift@p;5059unless(defined$types->{$c}) {5060$types->{$c} =$ra->check_path($c,$rev);5061}5062while(@p) {5063$c.='/'.shift@p;5064next ifdefined$types->{$c};5065$types->{$c} =$ra->check_path($c,$rev);5066}5067}50685069foreachmy$m(@$mods) {5070foreachmy$f(qw/file_a file_b/) {5071next unlessdefined$m->{$f};5072my($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);5073if(length$pfx.$dir&& !defined$types{$dir}) {5074 type_diff_paths($ra, \%types,$pfx.$dir,$rev);5075}5076}5077}5078 \%types;5079}50805081sub split_path {5082return($_[0] =~ m#^(.*?)/?([^/]+)$#);5083}50845085sub repo_path {5086my($self,$path) =@_;5087if(my$enc=$self->{pathnameencoding}) {5088require Encode;5089 Encode::from_to($path,$enc,'UTF-8');5090}5091$self->{path_prefix}.(defined$path?$path:'');5092}50935094sub url_path {5095my($self,$path) =@_;5096if($self->{url} =~ m#^https?://#) {5097$path=~s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;5098}5099$self->{url} .'/'.$self->repo_path($path);5100}51015102sub rmdirs {5103my($self) =@_;5104my$rm=$self->{rm};5105delete$rm->{''};# we never delete the url we're tracking5106return unless%$rm;51075108foreach(keys%$rm) {5109my@d=split m#/#, $_;5110my$c=shift@d;5111$rm->{$c} =1;5112while(@d) {5113$c.='/'.shift@d;5114$rm->{$c} =1;5115}5116}5117delete$rm->{$self->{svn_path}};5118delete$rm->{''};# we never delete the url we're tracking5119return unless%$rm;51205121my($fh,$ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,5122$self->{tree_b});5123local$/="\0";5124while(<$fh>) {5125chomp;5126my@dn=split m#/#, $_;5127while(pop@dn) {5128delete$rm->{join'/',@dn};5129}5130unless(%$rm) {5131close$fh;5132return;5133}5134}5135 command_close_pipe($fh,$ctx);51365137my($r,$p,$bat) = ($self->{r},$self->{pool},$self->{bat});5138foreachmy$d(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {5139$self->close_directory($bat->{$d},$p);5140my($dn) = ($d=~ m#^(.*?)/?(?:[^/]+)$#);5141print"\tD+\t$d/\n"unless$::_q;5142$self->SUPER::delete_entry($d,$r,$bat->{$dn},$p);5143delete$bat->{$d};5144}5145}51465147sub open_or_add_dir {5148my($self,$full_path,$baton,$deletions) =@_;5149my$t=$self->{types}->{$full_path};5150if(!defined$t) {5151die"$full_pathnot known in r$self->{r} or we have a bug!\n";5152}5153{5154no warnings 'once';5155# SVN::Node::none and SVN::Node::file are used only once,5156# so we're shutting up Perl's warnings about them.5157if($t==$SVN::Node::none ||defined($deletions->{$full_path})) {5158return$self->add_directory($full_path,$baton,5159undef, -1,$self->{pool});5160}elsif($t==$SVN::Node::dir) {5161return$self->open_directory($full_path,$baton,5162$self->{r},$self->{pool});5163}# no warnings 'once'5164print STDERR "$full_pathalready exists in repository at ",5165"r$self->{r} and it is not a directory (",5166($t==$SVN::Node::file ?'file':'unknown'),"/$t)\n";5167}# no warnings 'once'5168exit1;5169}51705171sub ensure_path {5172my($self,$path,$deletions) =@_;5173my$bat=$self->{bat};5174my$repo_path=$self->repo_path($path);5175return$bat->{''}unless(length$repo_path);51765177my@p=split m#/+#, $repo_path;5178my$c=shift@p;5179$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{''},$deletions);5180while(@p) {5181my$c0=$c;5182$c.='/'.shift@p;5183$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{$c0},$deletions);5184}5185return$bat->{$c};5186}51875188# Subroutine to convert a globbing pattern to a regular expression.5189# From perl cookbook.5190sub glob2pat {5191my$globstr=shift;5192my%patmap= ('*'=>'.*','?'=>'.','['=>'[',']'=>']');5193$globstr=~s{(.)} { $patmap{$1}||"\Q$1"}ge;5194return'^'.$globstr.'$';5195}51965197sub check_autoprop {5198my($self,$pattern,$properties,$file,$fbat) =@_;5199# Convert the globbing pattern to a regular expression.5200my$regex= glob2pat($pattern);5201# Check if the pattern matches the file name.5202if($file=~m/($regex)/) {5203# Parse the list of properties to set.5204my@props=split(/;/,$properties);5205foreachmy$prop(@props) {5206# Parse 'name=value' syntax and set the property.5207if($prop=~/([^=]+)=(.*)/) {5208my($n,$v) = ($1,$2);5209for($n,$v) {5210s/^\s+//;s/\s+$//;5211}5212$self->change_file_prop($fbat,$n,$v);5213}5214}5215}5216}52175218sub apply_autoprops {5219my($self,$file,$fbat) =@_;5220my$conf_t= ${$self->{config}}{'config'};5221no warnings 'once';5222# Check [miscellany]/enable-auto-props in svn configuration.5223if(SVN::_Core::svn_config_get_bool(5224$conf_t,5225$SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY,5226$SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS,52270)) {5228# Auto-props are enabled. Enumerate them to look for matches.5229my$callback=sub{5230$self->check_autoprop($_[0],$_[1],$file,$fbat);5231};5232 SVN::_Core::svn_config_enumerate(5233$conf_t,5234$SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS,5235$callback);5236}5237}52385239sub A {5240my($self,$m,$deletions) =@_;5241my($dir,$file) = split_path($m->{file_b});5242my$pbat=$self->ensure_path($dir,$deletions);5243my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5244undef, -1);5245print"\tA\t$m->{file_b}\n"unless$::_q;5246$self->apply_autoprops($file,$fbat);5247$self->chg_file($fbat,$m);5248$self->close_file($fbat,undef,$self->{pool});5249}52505251sub C {5252my($self,$m,$deletions) =@_;5253my($dir,$file) = split_path($m->{file_b});5254my$pbat=$self->ensure_path($dir,$deletions);5255my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5256$self->url_path($m->{file_a}),$self->{r});5257print"\tC\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5258$self->chg_file($fbat,$m);5259$self->close_file($fbat,undef,$self->{pool});5260}52615262sub delete_entry {5263my($self,$path,$pbat) =@_;5264my$rpath=$self->repo_path($path);5265my($dir,$file) = split_path($rpath);5266$self->{rm}->{$dir} =1;5267$self->SUPER::delete_entry($rpath,$self->{r},$pbat,$self->{pool});5268}52695270sub R {5271my($self,$m,$deletions) =@_;5272my($dir,$file) = split_path($m->{file_b});5273my$pbat=$self->ensure_path($dir,$deletions);5274my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5275$self->url_path($m->{file_a}),$self->{r});5276print"\tR\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5277$self->apply_autoprops($file,$fbat);5278$self->chg_file($fbat,$m);5279$self->close_file($fbat,undef,$self->{pool});52805281($dir,$file) = split_path($m->{file_a});5282$pbat=$self->ensure_path($dir,$deletions);5283$self->delete_entry($m->{file_a},$pbat);5284}52855286sub M {5287my($self,$m,$deletions) =@_;5288my($dir,$file) = split_path($m->{file_b});5289my$pbat=$self->ensure_path($dir,$deletions);5290my$fbat=$self->open_file($self->repo_path($m->{file_b}),5291$pbat,$self->{r},$self->{pool});5292print"\t$m->{chg}\t$m->{file_b}\n"unless$::_q;5293$self->chg_file($fbat,$m);5294$self->close_file($fbat,undef,$self->{pool});5295}52965297sub T { shift->M(@_) }52985299sub change_file_prop {5300my($self,$fbat,$pname,$pval) =@_;5301$self->SUPER::change_file_prop($fbat,$pname,$pval,$self->{pool});5302}53035304sub change_dir_prop {5305my($self,$pbat,$pname,$pval) =@_;5306$self->SUPER::change_dir_prop($pbat,$pname,$pval,$self->{pool});5307}53085309sub _chg_file_get_blob ($$$$) {5310my($self,$fbat,$m,$which) =@_;5311my$fh= $::_repository->temp_acquire("git_blob_$which");5312if($m->{"mode_$which"} =~/^120/) {5313print$fh'link 'or croak $!;5314$self->change_file_prop($fbat,'svn:special','*');5315}elsif($m->{mode_a} =~/^120/&&$m->{"mode_$which"} !~/^120/) {5316$self->change_file_prop($fbat,'svn:special',undef);5317}5318my$blob=$m->{"sha1_$which"};5319return($fh,)if($blob=~/^0{40}$/);5320my$size= $::_repository->cat_blob($blob,$fh);5321 croak "Failed to read object$blob"if($size<0);5322$fh->flush==0or croak $!;5323seek$fh,0,0or croak $!;53245325my$exp= ::md5sum($fh);5326seek$fh,0,0or croak $!;5327return($fh,$exp);5328}53295330sub chg_file {5331my($self,$fbat,$m) =@_;5332if($m->{mode_b} =~/755$/&&$m->{mode_a} !~/755$/) {5333$self->change_file_prop($fbat,'svn:executable','*');5334}elsif($m->{mode_b} !~/755$/&&$m->{mode_a} =~/755$/) {5335$self->change_file_prop($fbat,'svn:executable',undef);5336}5337my($fh_a,$exp_a) = _chg_file_get_blob $self,$fbat,$m,'a';5338my($fh_b,$exp_b) = _chg_file_get_blob $self,$fbat,$m,'b';5339my$pool= SVN::Pool->new;5340my$atd=$self->apply_textdelta($fbat,$exp_a,$pool);5341if(-s $fh_a) {5342my$txstream= SVN::TxDelta::new ($fh_a,$fh_b,$pool);5343my$res= SVN::TxDelta::send_txstream($txstream,@$atd,$pool);5344if(defined$res) {5345die"Unexpected result from send_txstream:$res\n",5346"(SVN::Core::VERSION:$SVN::Core::VERSION)\n";5347}5348}else{5349my$got= SVN::TxDelta::send_stream($fh_b,@$atd,$pool);5350die"Checksum mismatch\nexpected:$exp_b\ngot:$got\n"5351if($gotne$exp_b);5352}5353 Git::temp_release($fh_b,1);5354 Git::temp_release($fh_a,1);5355$pool->clear;5356}53575358sub D {5359my($self,$m,$deletions) =@_;5360my($dir,$file) = split_path($m->{file_b});5361my$pbat=$self->ensure_path($dir,$deletions);5362print"\tD\t$m->{file_b}\n"unless$::_q;5363$self->delete_entry($m->{file_b},$pbat);5364}53655366sub close_edit {5367my($self) =@_;5368my($p,$bat) = ($self->{pool},$self->{bat});5369foreach(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {5370next if$_eq'';5371$self->close_directory($bat->{$_},$p);5372}5373$self->close_directory($bat->{''},$p);5374$self->SUPER::close_edit($p);5375$p->clear;5376}53775378sub abort_edit {5379my($self) =@_;5380$self->SUPER::abort_edit($self->{pool});5381}53825383sub DESTROY {5384my$self=shift;5385$self->SUPER::DESTROY(@_);5386$self->{pool}->clear;5387}53885389# this drives the editor5390sub apply_diff {5391my($self) =@_;5392my$mods=$self->{mods};5393my%o= ( D =>0, C =>1, R =>2, A =>3, M =>4, T =>5);5394my%deletions;53955396foreachmy$m(@$mods) {5397if($m->{chg}eq"D") {5398$deletions{$m->{file_b}} =1;5399}5400}54015402foreachmy$m(sort{$o{$a->{chg}} <=>$o{$b->{chg}} }@$mods) {5403my$f=$m->{chg};5404if(defined$o{$f}) {5405$self->$f($m, \%deletions);5406}else{5407 fatal("Invalid change type:$f");5408}5409}54105411if(defined($self->{mergeinfo})) {5412$self->change_dir_prop($self->{bat}{''},"svn:mergeinfo",5413$self->{mergeinfo});5414}5415$self->rmdirsif$_rmdir;5416if(@$mods==0&& !defined($self->{mergeinfo})) {5417$self->abort_edit;5418}else{5419$self->close_edit;5420}5421returnscalar@$mods;5422}54235424package Git::SVN::Ra;5425use vars qw/@ISA $config_dir $_ignore_refs_regex $_log_window_size/;5426use strict;5427use warnings;5428my($ra_invalid,$can_do_switch,%ignored_err,$RA);54295430BEGIN{5431# enforce temporary pool usage for some simple functions5432no strict 'refs';5433formy$f(qw/rev_proplist get_latest_revnum get_uuid get_repos_root5434 get_file/) {5435my$SUPER="SUPER::$f";5436*$f=sub{5437my$self=shift;5438my$pool= SVN::Pool->new;5439my@ret=$self->$SUPER(@_,$pool);5440$pool->clear;5441wantarray?@ret:$ret[0];5442};5443}5444}54455446sub _auth_providers () {5447my@rv= (5448 SVN::Client::get_simple_provider(),5449 SVN::Client::get_ssl_server_trust_file_provider(),5450 SVN::Client::get_simple_prompt_provider(5451 \&Git::SVN::Prompt::simple,2),5452 SVN::Client::get_ssl_client_cert_file_provider(),5453 SVN::Client::get_ssl_client_cert_prompt_provider(5454 \&Git::SVN::Prompt::ssl_client_cert,2),5455 SVN::Client::get_ssl_client_cert_pw_file_provider(),5456 SVN::Client::get_ssl_client_cert_pw_prompt_provider(5457 \&Git::SVN::Prompt::ssl_client_cert_pw,2),5458 SVN::Client::get_username_provider(),5459 SVN::Client::get_ssl_server_trust_prompt_provider(5460 \&Git::SVN::Prompt::ssl_server_trust),5461 SVN::Client::get_username_prompt_provider(5462 \&Git::SVN::Prompt::username,2)5463);54645465# earlier 1.6.x versions would segfault, and <= 1.5.x didn't have5466# this function5467if($SVN::Core::VERSION gt'1.6.12') {5468my$config= SVN::Core::config_get_config($config_dir);5469my($p,@a);5470# config_get_config returns all config files from5471# ~/.subversion, auth_get_platform_specific_client_providers5472# just wants the config "file".5473@a= ($config->{'config'},undef);5474$p= SVN::Core::auth_get_platform_specific_client_providers(@a);5475# Insert the return value from5476# auth_get_platform_specific_providers5477unshift@rv,@$p;5478}5479 \@rv;5480}54815482sub escape_uri_only {5483my($uri) =@_;5484my@tmp;5485foreach(splitm{/},$uri) {5486s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;5487push@tmp,$_;5488}5489join('/',@tmp);5490}54915492sub escape_url {5493my($url) =@_;5494if($url=~ m#^(https?)://([^/]+)(.*)$#) {5495my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));5496$url="$scheme://$domain$uri";5497}5498$url;5499}55005501sub new {5502my($class,$url) =@_;5503$url=~s!/+$!!;5504return$RAif($RA&&$RA->{url}eq$url);55055506::_req_svn();55075508 SVN::_Core::svn_config_ensure($config_dir,undef);5509my($baton,$callbacks) = SVN::Core::auth_open_helper(_auth_providers);5510my$config= SVN::Core::config_get_config($config_dir);5511$RA=undef;5512my$dont_store_passwords=1;5513my$conf_t= ${$config}{'config'};5514{5515no warnings 'once';5516# The usage of $SVN::_Core::SVN_CONFIG_* variables5517# produces warnings that variables are used only once.5518# I had not found the better way to shut them up, so5519# the warnings of type 'once' are disabled in this block.5520if(SVN::_Core::svn_config_get_bool($conf_t,5521$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5522$SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS,55231) ==0) {5524 SVN::_Core::svn_auth_set_parameter($baton,5525$SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS,5526bless(\$dont_store_passwords,"_p_void"));5527}5528if(SVN::_Core::svn_config_get_bool($conf_t,5529$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5530$SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS,55311) ==0) {5532$Git::SVN::Prompt::_no_auth_cache =1;5533}5534}# no warnings 'once'5535my$self= SVN::Ra->new(url => escape_url($url), auth =>$baton,5536 config =>$config,5537 pool => SVN::Pool->new,5538 auth_provider_callbacks =>$callbacks);5539$self->{url} =$url;5540$self->{svn_path} =$url;5541$self->{repos_root} =$self->get_repos_root;5542$self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;5543$self->{cache} = { check_path => { r =>0, data => {} },5544 get_dir => { r =>0, data => {} } };5545$RA=bless$self,$class;5546}55475548sub check_path {5549my($self,$path,$r) =@_;5550my$cache=$self->{cache}->{check_path};5551if($r==$cache->{r} &&exists$cache->{data}->{$path}) {5552return$cache->{data}->{$path};5553}5554my$pool= SVN::Pool->new;5555my$t=$self->SUPER::check_path($path,$r,$pool);5556$pool->clear;5557if($r!=$cache->{r}) {5558%{$cache->{data}} = ();5559$cache->{r} =$r;5560}5561$cache->{data}->{$path} =$t;5562}55635564sub get_dir {5565my($self,$dir,$r) =@_;5566my$cache=$self->{cache}->{get_dir};5567if($r==$cache->{r}) {5568if(my$x=$cache->{data}->{$dir}) {5569returnwantarray?@$x:$x->[0];5570}5571}5572my$pool= SVN::Pool->new;5573my($d,undef,$props) =$self->SUPER::get_dir($dir,$r,$pool);5574my%dirents=map{$_=> { kind =>$d->{$_}->kind} }keys%$d;5575$pool->clear;5576if($r!=$cache->{r}) {5577%{$cache->{data}} = ();5578$cache->{r} =$r;5579}5580$cache->{data}->{$dir} = [ \%dirents,$r,$props];5581wantarray? (\%dirents,$r,$props) : \%dirents;5582}55835584sub DESTROY {5585# do not call the real DESTROY since we store ourselves in $RA5586}55875588# get_log(paths, start, end, limit,5589# discover_changed_paths, strict_node_history, receiver)5590sub get_log {5591my($self,@args) =@_;5592my$pool= SVN::Pool->new;55935594# svn_log_changed_path_t objects passed to get_log are likely to be5595# overwritten even if only the refs are copied to an external variable,5596# so we should dup the structures in their entirety. Using an5597# externally passed pool (instead of our temporary and quickly cleared5598# pool in Git::SVN::Ra) does not help matters at all...5599my$receiver=pop@args;5600my$prefix="/".$self->{svn_path};5601$prefix=~ s#/+($)##;5602my$prefix_regex= qr#^\Q$prefix\E#;5603push(@args,sub{5604my($paths) =$_[0];5605return&$receiver(@_)unless$paths;5606$_[0] = ();5607foreachmy$p(keys%$paths) {5608my$i=$paths->{$p};5609# Make path relative to our url, not repos_root5610$p=~s/$prefix_regex//;5611my%s=map{$_=>$i->$_; }5612 qw/copyfrom_path copyfrom_rev action/;5613if($s{'copyfrom_path'}) {5614$s{'copyfrom_path'} =~s/$prefix_regex//;5615}5616$_[0]{$p} = \%s;5617}5618&$receiver(@_);5619});562056215622# the limit parameter was not supported in SVN 1.1.x, so we5623# drop it. Therefore, the receiver callback passed to it5624# is made aware of this limitation by being wrapped if5625# the limit passed to is being wrapped.5626if($SVN::Core::VERSION le'1.2.0') {5627my$limit=splice(@args,3,1);5628if($limit>0) {5629my$receiver=pop@args;5630push(@args,sub{ &$receiver(@_)if(--$limit>=0) });5631}5632}5633my$ret=$self->SUPER::get_log(@args,$pool);5634$pool->clear;5635$ret;5636}56375638sub trees_match {5639my($self,$url1,$rev1,$url2,$rev2) =@_;5640my$ctx= SVN::Client->new(auth => _auth_providers);5641my$out= IO::File->new_tmpfile;56425643# older SVN (1.1.x) doesn't take $pool as the last parameter for5644# $ctx->diff(), so we'll create a default one5645my$pool= SVN::Pool->new_default_sub;56465647$ra_invalid=1;# this will open a new SVN::Ra connection to $url15648$ctx->diff([],$url1,$rev1,$url2,$rev2,1,1,0,$out,$out);5649$out->flush;5650my$ret= (($out->stat)[7] ==0);5651close$outor croak $!;56525653$ret;5654}56555656sub get_commit_editor {5657my($self,$log,$cb,$pool) =@_;5658my@lock=$SVN::Core::VERSION ge'1.2.0'? (undef,0) : ();5659$self->SUPER::get_commit_editor($log,$cb,@lock,$pool);5660}56615662sub gs_do_update {5663my($self,$rev_a,$rev_b,$gs,$editor) =@_;5664my$new= ($rev_a==$rev_b);5665my$path=$gs->{path};56665667if($new&& -e $gs->{index}) {5668unlink$gs->{index}or die5669"Couldn't unlink index:$gs->{index}:$!\n";5670}5671my$pool= SVN::Pool->new;5672$editor->set_path_strip($path);5673my(@pc) =split m#/#, $path;5674my$reporter=$self->do_update($rev_b, (@pc?shift@pc:''),56751,$editor,$pool);5676my@lock=$SVN::Core::VERSION ge'1.2.0'? (undef) : ();56775678# Since we can't rely on svn_ra_reparent being available, we'll5679# just have to do some magic with set_path to make it so5680# we only want a partial path.5681my$sp='';5682my$final=join('/',@pc);5683while(@pc) {5684$reporter->set_path($sp,$rev_b,0,@lock,$pool);5685$sp.='/'iflength$sp;5686$sp.=shift@pc;5687}5688die"BUG: '$sp' != '$final'\n"if($spne$final);56895690$reporter->set_path($sp,$rev_a,$new,@lock,$pool);56915692$reporter->finish_report($pool);5693$pool->clear;5694$editor->{git_commit_ok};5695}56965697# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and5698# svn_ra_reparent didn't work before 1.4)5699sub gs_do_switch {5700my($self,$rev_a,$rev_b,$gs,$url_b,$editor) =@_;5701my$path=$gs->{path};5702my$pool= SVN::Pool->new;57035704my$full_url=$self->{url};5705my$old_url=$full_url;5706$full_url.='/'.$pathiflength$path;5707my($ra,$reparented);57085709if($old_url=~ m#^svn(\+ssh)?://# ||5710($full_url=~ m#^https?://# &&5711 escape_url($full_url)ne$full_url)) {5712$_[0] =undef;5713$self=undef;5714$RA=undef;5715$ra= Git::SVN::Ra->new($full_url);5716$ra_invalid=1;5717}elsif($old_urlne$full_url) {5718 SVN::_Ra::svn_ra_reparent($self->{session},$full_url,$pool);5719$self->{url} =$full_url;5720$reparented=1;5721}57225723$ra||=$self;5724$url_b= escape_url($url_b);5725my$reporter=$ra->do_switch($rev_b,'',1,$url_b,$editor,$pool);5726my@lock=$SVN::Core::VERSION ge'1.2.0'? (undef) : ();5727$reporter->set_path('',$rev_a,0,@lock,$pool);5728$reporter->finish_report($pool);57295730if($reparented) {5731 SVN::_Ra::svn_ra_reparent($self->{session},$old_url,$pool);5732$self->{url} =$old_url;5733}57345735$pool->clear;5736$editor->{git_commit_ok};5737}57385739sub longest_common_path {5740my($gsv,$globs) =@_;5741my%common;5742my$common_max=scalar@$gsv;57435744foreachmy$gs(@$gsv) {5745my@tmp=split m#/#, $gs->{path};5746my$p='';5747foreach(@tmp) {5748$p.=length($p) ?"/$_":$_;5749$common{$p} ||=0;5750$common{$p}++;5751}5752}5753$globs||= [];5754$common_max+=scalar@$globs;5755foreachmy$glob(@$globs) {5756my@tmp=split m#/#, $glob->{path}->{left};5757my$p='';5758foreach(@tmp) {5759$p.=length($p) ?"/$_":$_;5760$common{$p} ||=0;5761$common{$p}++;5762}5763}57645765my$longest_path='';5766foreach(sort{length$b<=>length$a}keys%common) {5767if($common{$_} ==$common_max) {5768$longest_path=$_;5769last;5770}5771}5772$longest_path;5773}57745775sub gs_fetch_loop_common {5776my($self,$base,$head,$gsv,$globs) =@_;5777return if($base>$head);5778my$inc=$_log_window_size;5779my($min,$max) = ($base,$head<$base+$inc?$head:$base+$inc);5780my$longest_path= longest_common_path($gsv,$globs);5781my$ra_url=$self->{url};5782my$find_trailing_edge;5783while(1) {5784my%revs;5785my$err;5786my$err_handler=$SVN::Error::handler;5787$SVN::Error::handler =sub{5788($err) =@_;5789 skip_unknown_revs($err);5790};5791sub _cb {5792my($paths,$r,$author,$date,$log) =@_;5793[$paths,5794{ author =>$author, date =>$date,log=>$log} ];5795}5796$self->get_log([$longest_path],$min,$max,0,1,1,5797sub{$revs{$_[1]} = _cb(@_) });5798if($err) {5799print"Checked through r$max\r";5800}else{5801$find_trailing_edge=1;5802}5803if($errand$find_trailing_edge) {5804print STDERR "Path '$longest_path' ",5805"was probably deleted:\n",5806$err->expanded_message,5807"\nWill attempt to follow ",5808"revisions r$min.. r$max",5809"committed before the deletion\n";5810my$hi=$max;5811while(--$hi>=$min) {5812my$ok;5813$self->get_log([$longest_path],$min,$hi,58140,1,1,sub{5815$ok=$_[1];5816$revs{$_[1]} = _cb(@_) });5817if($ok) {5818print STDERR "r$min.. r$okOK\n";5819last;5820}5821}5822$find_trailing_edge=0;5823}5824$SVN::Error::handler =$err_handler;58255826my%exists=map{$_->{path} =>$_}@$gsv;5827foreachmy$r(sort{$a<=>$b}keys%revs) {5828my($paths,$logged) = @{$revs{$r}};58295830foreachmy$gs($self->match_globs(\%exists,$paths,5831$globs,$r)) {5832if($gs->rev_map_max>=$r) {5833next;5834}5835next unless$gs->match_paths($paths,$r);5836$gs->{logged_rev_props} =$logged;5837if(my$last_commit=$gs->last_commit) {5838$gs->assert_index_clean($last_commit);5839}5840my$log_entry=$gs->do_fetch($paths,$r);5841if($log_entry) {5842$gs->do_git_commit($log_entry);5843}5844$INDEX_FILES{$gs->{index}} =1;5845}5846foreachmy$g(@$globs) {5847my$k="svn-remote.$g->{remote}.".5848"$g->{t}-maxRev";5849 Git::SVN::tmp_config($k,$r);5850}5851if($ra_invalid) {5852$_[0] =undef;5853$self=undef;5854$RA=undef;5855$self= Git::SVN::Ra->new($ra_url);5856$ra_invalid=undef;5857}5858}5859# pre-fill the .rev_db since it'll eventually get filled in5860# with '0' x40 if something new gets committed5861foreachmy$gs(@$gsv) {5862next if$gs->rev_map_max>=$max;5863next ifdefined$gs->rev_map_get($max);5864$gs->rev_map_set($max,0 x40);5865}5866foreachmy$g(@$globs) {5867my$k="svn-remote.$g->{remote}.$g->{t}-maxRev";5868 Git::SVN::tmp_config($k,$max);5869}5870last if$max>=$head;5871$min=$max+1;5872$max+=$inc;5873$max=$headif($max>$head);5874}5875 Git::SVN::gc();5876}58775878sub get_dir_globbed {5879my($self,$left,$depth,$r) =@_;58805881my@x=eval{$self->get_dir($left,$r) };5882return unlessscalar@x==3;5883my$dirents=$x[0];5884my@finalents;5885foreachmy$de(keys%$dirents) {5886next if$dirents->{$de}->{kind} !=$SVN::Node::dir;5887if($depth>1) {5888my@args= ("$left/$de",$depth-1,$r);5889foreachmy$dir($self->get_dir_globbed(@args)) {5890push@finalents,"$de/$dir";5891}5892}else{5893push@finalents,$de;5894}5895}5896@finalents;5897}58985899# return value: 0 -- don't ignore, 1 -- ignore5900sub is_ref_ignored {5901my($g,$p) =@_;5902my$refname=$g->{ref}->full_path($p);5903return1ifdefined($g->{ignore_refs_regex}) &&5904$refname=~m!$g->{ignore_refs_regex}!;5905return0unlessdefined($_ignore_refs_regex);5906return1if$refname=~m!$_ignore_refs_regex!o;5907return0;5908}59095910sub match_globs {5911my($self,$exists,$paths,$globs,$r) =@_;59125913sub get_dir_check {5914my($self,$exists,$g,$r) =@_;59155916my@dirs=$self->get_dir_globbed($g->{path}->{left},5917$g->{path}->{depth},5918$r);59195920foreachmy$de(@dirs) {5921my$p=$g->{path}->full_path($de);5922next if$exists->{$p};5923next if(length$g->{path}->{right} &&5924($self->check_path($p,$r) !=5925$SVN::Node::dir));5926next unless$p=~/$g->{path}->{regex}/;5927$exists->{$p} = Git::SVN->init($self->{url},$p,undef,5928$g->{ref}->full_path($de),1);5929}5930}5931foreachmy$g(@$globs) {5932if(my$path=$paths->{"/$g->{path}->{left}"}) {5933if($path->{action} =~/^[AR]$/) {5934 get_dir_check($self,$exists,$g,$r);5935}5936}5937foreach(keys%$paths) {5938if(/$g->{path}->{left_regex}/&&5939!/$g->{path}->{regex}/) {5940next if$paths->{$_}->{action} !~/^[AR]$/;5941 get_dir_check($self,$exists,$g,$r);5942}5943next unless/$g->{path}->{regex}/;5944my$p=$1;5945my$pathname=$g->{path}->full_path($p);5946next if is_ref_ignored($g,$p);5947next if$exists->{$pathname};5948next if($self->check_path($pathname,$r) !=5949$SVN::Node::dir);5950$exists->{$pathname} = Git::SVN->init(5951$self->{url},$pathname,undef,5952$g->{ref}->full_path($p),1);5953}5954my$c='';5955foreach(split m#/#, $g->{path}->{left}) {5956$c.="/$_";5957next unless($paths->{$c} &&5958($paths->{$c}->{action} =~/^[AR]$/));5959 get_dir_check($self,$exists,$g,$r);5960}5961}5962values%$exists;5963}59645965sub minimize_url {5966my($self) =@_;5967return$self->{url}if($self->{url}eq$self->{repos_root});5968my$url=$self->{repos_root};5969my@components=split(m!/!,$self->{svn_path});5970my$c='';5971do{5972$url.="/$c"iflength$c;5973eval{5974my$ra= (ref$self)->new($url);5975my$latest=$ra->get_latest_revnum;5976$ra->get_log("",$latest,0,1,0,1,sub{});5977};5978}while($@&& ($c=shift@components));5979$url;5980}59815982sub can_do_switch {5983my$self=shift;5984unless(defined$can_do_switch) {5985my$pool= SVN::Pool->new;5986my$rep=eval{5987$self->do_switch(1,'',0,$self->{url},5988 SVN::Delta::Editor->new,$pool);5989};5990if($@) {5991$can_do_switch=0;5992}else{5993$rep->abort_report($pool);5994$can_do_switch=1;5995}5996$pool->clear;5997}5998$can_do_switch;5999}60006001sub skip_unknown_revs {6002my($err) =@_;6003my$errno=$err->apr_err();6004# Maybe the branch we're tracking didn't6005# exist when the repo started, so it's6006# not an error if it doesn't, just continue6007#6008# Wonderfully consistent library, eh?6009# 160013 - svn:// and file://6010# 175002 - http(s)://6011# 175007 - http(s):// (this repo required authorization, too...)6012# More codes may be discovered later...6013if($errno==175007||$errno==175002||$errno==160013) {6014my$err_key=$err->expanded_message;6015# revision numbers change every time, filter them out6016$err_key=~s/\d+/\0/g;6017$err_key="$errno\0$err_key";6018unless($ignored_err{$err_key}) {6019warn"W: Ignoring error from SVN, path probably ",6020"does not exist: ($errno): ",6021$err->expanded_message,"\n";6022warn"W: Do not be alarmed at the above message ",6023"git-svn is just searching aggressively for ",6024"old history.\n",6025"This may take a while on large repositories\n";6026$ignored_err{$err_key} =1;6027}6028return;6029}6030die"Error from SVN, ($errno): ",$err->expanded_message,"\n";6031}60326033package Git::SVN::Log;6034use strict;6035use warnings;6036use POSIX qw/strftime/;6037useconstant commit_log_separator => ('-' x 72) ."\n";6038use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline6039%rusers $show_commit $incremental/;6040my$l_fmt;60416042sub cmt_showable {6043my($c) =@_;6044return1ifdefined$c->{r};60456046# big commit message got truncated by the 16k pretty buffer in rev-list6047if($c->{l} &&$c->{l}->[-1]eq"...\n"&&6048$c->{a_raw} =~/\@([a-f\d\-]+)>$/) {6049@{$c->{l}} = ();6050my@log= command(qw/cat-file commit/,$c->{c});60516052# shift off the headers6053shift@logwhile($log[0]ne'');6054shift@log;60556056# TODO: make $c->{l} not have a trailing newline in the future6057@{$c->{l}} =map{"$_\n"}grep!/^git-svn-id: /,@log;60586059(undef,$c->{r},undef) = ::extract_metadata(6060(grep(/^git-svn-id: /,@log))[-1]);6061}6062returndefined$c->{r};6063}60646065sub log_use_color {6066return$color|| Git->repository->get_colorbool('color.diff');6067}60686069sub git_svn_log_cmd {6070my($r_min,$r_max,@args) =@_;6071my$head='HEAD';6072my(@files,@log_opts);6073foreachmy$x(@args) {6074if($xeq'--'||@files) {6075push@files,$x;6076}else{6077if(::verify_ref("$x^0")) {6078$head=$x;6079}else{6080push@log_opts,$x;6081}6082}6083}60846085my($url,$rev,$uuid,$gs) = ::working_head_info($head);6086$gs||= Git::SVN->_new;6087my@cmd= (qw/log --abbrev-commit --pretty=raw --default/,6088$gs->refname);6089push@cmd,'-r'unless$non_recursive;6090push@cmd, qw/--raw --name-status/if$verbose;6091push@cmd,'--color'if log_use_color();6092push@cmd,@log_opts;6093if(defined$r_max&&$r_max==$r_min) {6094push@cmd,'--max-count=1';6095if(my$c=$gs->rev_map_get($r_max)) {6096push@cmd,$c;6097}6098}elsif(defined$r_max) {6099if($r_max<$r_min) {6100($r_min,$r_max) = ($r_max,$r_min);6101}6102my(undef,$c_max) =$gs->find_rev_before($r_max,1,$r_min);6103my(undef,$c_min) =$gs->find_rev_after($r_min,1,$r_max);6104# If there are no commits in the range, both $c_max and $c_min6105# will be undefined. If there is at least 1 commit in the6106# range, both will be defined.6107return()if!defined$c_min|| !defined$c_max;6108if($c_mineq$c_max) {6109push@cmd,'--max-count=1',$c_min;6110}else{6111push@cmd,'--boundary',"$c_min..$c_max";6112}6113}6114return(@cmd,@files);6115}61166117# adapted from pager.c6118sub config_pager {6119if(! -t *STDOUT) {6120$ENV{GIT_PAGER_IN_USE} ='false';6121$pager=undef;6122return;6123}6124chomp($pager= command_oneline(qw(var GIT_PAGER)));6125if($pagereq'cat') {6126$pager=undef;6127}6128$ENV{GIT_PAGER_IN_USE} =defined($pager);6129}61306131sub run_pager {6132return unlessdefined$pager;6133pipe my($rfd,$wfd)orreturn;6134defined(my$pid=fork)or::fatal "Can't fork:$!";6135if(!$pid) {6136open STDOUT,'>&',$wfdor6137::fatal "Can't redirect to stdout:$!";6138return;6139}6140open STDIN,'<&',$rfdor::fatal "Can't redirect stdin:$!";6141$ENV{LESS} ||='FRSX';6142exec$pageror::fatal "Can't run pager:$!($pager)";6143}61446145sub format_svn_date {6146my$t=shift||time;6147my$gmoff= Git::SVN::get_tz($t);6148return strftime("%Y-%m-%d%H:%M:%S$gmoff(%a,%d%b%Y)",localtime($t));6149}61506151sub parse_git_date {6152my($t,$tz) =@_;6153# Date::Parse isn't in the standard Perl distro :(6154if($tz=~s/^\+//) {6155$t+= tz_to_s_offset($tz);6156}elsif($tz=~s/^\-//) {6157$t-= tz_to_s_offset($tz);6158}6159return$t;6160}61616162sub set_local_timezone {6163if(defined$TZ) {6164$ENV{TZ} =$TZ;6165}else{6166delete$ENV{TZ};6167}6168}61696170sub tz_to_s_offset {6171my($tz) =@_;6172$tz=~s/(\d\d)$//;6173return($1*60) + ($tz*3600);6174}61756176sub get_author_info {6177my($dest,$author,$t,$tz) =@_;6178$author=~s/(?:^\s*|\s*$)//g;6179$dest->{a_raw} =$author;6180my$au;6181if($::_authors) {6182$au=$rusers{$author} ||undef;6183}6184if(!$au) {6185($au) = ($author=~/<([^>]+)\@[^>]+>$/);6186}6187$dest->{t} =$t;6188$dest->{tz} =$tz;6189$dest->{a} =$au;6190$dest->{t_utc} = parse_git_date($t,$tz);6191}61926193sub process_commit {6194my($c,$r_min,$r_max,$defer) =@_;6195if(defined$r_min&&defined$r_max) {6196if($r_min==$c->{r} &&$r_min==$r_max) {6197 show_commit($c);6198return0;6199}6200return1if$r_min==$r_max;6201if($r_min<$r_max) {6202# we need to reverse the print order6203return0if(defined$limit&& --$limit<0);6204push@$defer,$c;6205return1;6206}6207if($r_min!=$r_max) {6208return1if($r_min<$c->{r});6209return1if($r_max>$c->{r});6210}6211}6212return0if(defined$limit&& --$limit<0);6213 show_commit($c);6214return1;6215}62166217sub show_commit {6218my$c=shift;6219if($oneline) {6220my$x="\n";6221if(my$l=$c->{l}) {6222while($l->[0] =~/^\s*$/) {shift@$l}6223$x=$l->[0];6224}6225$l_fmt||='A'.length($c->{r});6226print'r',pack($l_fmt,$c->{r}),' | ';6227print"$c->{c} | "if$show_commit;6228print$x;6229}else{6230 show_commit_normal($c);6231}6232}62336234sub show_commit_changed_paths {6235my($c) =@_;6236return unless$c->{changed};6237print"Changed paths:\n", @{$c->{changed}};6238}62396240sub show_commit_normal {6241my($c) =@_;6242print commit_log_separator,"r$c->{r} | ";6243print"$c->{c} | "if$show_commit;6244print"$c->{a} | ", format_svn_date($c->{t_utc}),' | ';6245my$nr_line=0;62466247if(my$l=$c->{l}) {6248while($l->[$#$l]eq"\n"&&$#$l>06249&&$l->[($#$l-1)]eq"\n") {6250pop@$l;6251}6252$nr_line=scalar@$l;6253if(!$nr_line) {6254print"1 line\n\n\n";6255}else{6256if($nr_line==1) {6257$nr_line='1 line';6258}else{6259$nr_line.=' lines';6260}6261print$nr_line,"\n";6262 show_commit_changed_paths($c);6263print"\n";6264print$_foreach@$l;6265}6266}else{6267print"1 line\n";6268 show_commit_changed_paths($c);6269print"\n";62706271}6272foreachmy$x(qw/raw stat diff/) {6273if($c->{$x}) {6274print"\n";6275print$_foreach@{$c->{$x}}6276}6277}6278}62796280sub cmd_show_log {6281my(@args) =@_;6282my($r_min,$r_max);6283my$r_last= -1;# prevent dupes6284 set_local_timezone();6285if(defined$::_revision) {6286if($::_revision =~/^(\d+):(\d+)$/) {6287($r_min,$r_max) = ($1,$2);6288}elsif($::_revision =~/^\d+$/) {6289$r_min=$r_max= $::_revision;6290}else{6291::fatal "-r$::_revision is not supported, use ",6292"standard 'git log' arguments instead";6293}6294}62956296 config_pager();6297@args= git_svn_log_cmd($r_min,$r_max,@args);6298if(!@args) {6299print commit_log_separator unless$incremental||$oneline;6300return;6301}6302my$log= command_output_pipe(@args);6303 run_pager();6304my(@k,$c,$d,$stat);6305my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;6306while(<$log>) {6307if(/^${esc_color}commit (?:- )?($::sha1_short)/o) {6308my$cmt=$1;6309if($c&& cmt_showable($c) &&$c->{r} !=$r_last) {6310$r_last=$c->{r};6311 process_commit($c,$r_min,$r_max, \@k)or6312goto out;6313}6314$d=undef;6315$c= { c =>$cmt};6316}elsif(/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {6317 get_author_info($c,$1,$2,$3);6318}elsif(/^${esc_color}(?:tree|parent|committer) /o) {6319# ignore6320}elsif(/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {6321push@{$c->{raw}},$_;6322}elsif(/^${esc_color}[ACRMDT]\t/) {6323# we could add $SVN->{svn_path} here, but that requires6324# remote access at the moment (repo_path_split)...6325 s#^(${esc_color})([ACRMDT])\t#$1 $2 #o;6326push@{$c->{changed}},$_;6327}elsif(/^${esc_color}diff /o) {6328$d=1;6329push@{$c->{diff}},$_;6330}elsif($d) {6331push@{$c->{diff}},$_;6332}elsif(/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*6333$esc_color*[\+\-]*$esc_color$/x) {6334$stat=1;6335push@{$c->{stat}},$_;6336}elsif($stat&&/^ \d+ files changed, \d+ insertions/) {6337push@{$c->{stat}},$_;6338$stat=undef;6339}elsif(/^${esc_color} (git-svn-id:.+)$/o) {6340($c->{url},$c->{r},undef) = ::extract_metadata($1);6341}elsif(s/^${esc_color} //o) {6342push@{$c->{l}},$_;6343}6344}6345if($c&&defined$c->{r} &&$c->{r} !=$r_last) {6346$r_last=$c->{r};6347 process_commit($c,$r_min,$r_max, \@k);6348}6349if(@k) {6350($r_min,$r_max) = ($r_max,$r_min);6351 process_commit($_,$r_min,$r_max)foreachreverse@k;6352}6353out:6354close$log;6355print commit_log_separator unless$incremental||$oneline;6356}63576358sub cmd_blame {6359my$path=pop;63606361 config_pager();6362 run_pager();63636364my($fh,$ctx,$rev);63656366if($_git_format) {6367($fh,$ctx) = command_output_pipe('blame',@_,$path);6368while(my$line= <$fh>) {6369if($line=~/^\^?([[:xdigit:]]+)\s/) {6370# Uncommitted edits show up as a rev ID of6371# all zeros, which we can't look up with6372# cmt_metadata6373if($1!~/^0+$/) {6374(undef,$rev,undef) =6375::cmt_metadata($1);6376$rev='0'if(!$rev);6377}else{6378$rev='0';6379}6380$rev=sprintf('%-10s',$rev);6381$line=~s/^\^?[[:xdigit:]]+(\s)/$rev$1/;6382}6383print$line;6384}6385}else{6386($fh,$ctx) = command_output_pipe('blame','-p',@_,'HEAD',6387'--',$path);6388my($sha1);6389my%authors;6390my@buffer;6391my%dsha;#distinct sha keys63926393while(my$line= <$fh>) {6394push@buffer,$line;6395if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6396$dsha{$1} =1;6397}6398}63996400my$s2r= ::cmt_sha2rev_batch([keys%dsha]);64016402foreachmy$line(@buffer) {6403if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6404$rev=$s2r->{$1};6405$rev='0'if(!$rev)6406}6407elsif($line=~/^author (.*)/) {6408$authors{$rev} =$1;6409$authors{$rev} =~s/\s/_/g;6410}6411elsif($line=~/^\t(.*)$/) {6412printf("%6s%10s%s\n",$rev,$authors{$rev},$1);6413}6414}6415}6416 command_close_pipe($fh,$ctx);6417}64186419package Git::SVN::Migration;6420# these version numbers do NOT correspond to actual version numbers6421# of git nor git-svn. They are just relative.6422#6423# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD6424#6425# v1 layout: .git/$id/info/url, refs/remotes/$id6426#6427# v2 layout: .git/svn/$id/info/url, refs/remotes/$id6428#6429# v3 layout: .git/svn/$id, refs/remotes/$id6430# - info/url may remain for backwards compatibility6431# - this is what we migrate up to this layout automatically,6432# - this will be used by git svn init on single branches6433# v3.1 layout (auto migrated):6434# - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink6435# for backwards compatibility6436#6437# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id6438# - this is only created for newly multi-init-ed6439# repositories. Similar in spirit to the6440# --use-separate-remotes option in git-clone (now default)6441# - we do not automatically migrate to this (following6442# the example set by core git)6443#6444# v5 layout: .rev_db.$UUID => .rev_map.$UUID6445# - newer, more-efficient format that uses 24-bytes per record6446# with no filler space.6447# - use xxd -c24 < .rev_map.$UUID to view and debug6448# - This is a one-way migration, repositories updated to the6449# new format will not be able to use old git-svn without6450# rebuilding the .rev_db. Rebuilding the rev_db is not6451# possible if noMetadata or useSvmProps are set; but should6452# be no problem for users that use the (sensible) defaults.6453use strict;6454use warnings;6455use Carp qw/croak/;6456use File::Path qw/mkpath/;6457use File::Basename qw/dirname basename/;6458use vars qw/$_minimize/;64596460sub migrate_from_v0 {6461my$git_dir=$ENV{GIT_DIR};6462returnundefunless-d $git_dir;6463my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6464my$migrated=0;6465while(<$fh>) {6466chomp;6467my($id,$orig_ref) = ($_,$_);6468next unless$id=~ s#^refs/heads/(.+)-HEAD$#$1#;6469next unless-f "$git_dir/$id/info/url";6470my$new_ref="refs/remotes/$id";6471if(::verify_ref("$new_ref^0")) {6472print STDERR "W:$orig_refis probably an old ",6473"branch used by an ancient version of ",6474"git-svn.\n",6475"However,$new_refalso exists.\n",6476"We will not be able ",6477"to use this branch until this ",6478"ambiguity is resolved.\n";6479next;6480}6481print STDERR "Migrating from v0 layout...\n"if!$migrated;6482print STDERR "Renaming ref:$orig_ref=>$new_ref\n";6483 command_noisy('update-ref',$new_ref,$orig_ref);6484 command_noisy('update-ref','-d',$orig_ref,$orig_ref);6485$migrated++;6486}6487 command_close_pipe($fh,$ctx);6488print STDERR "Done migrating from v0 layout...\n"if$migrated;6489$migrated;6490}64916492sub migrate_from_v1 {6493my$git_dir=$ENV{GIT_DIR};6494my$migrated=0;6495return$migratedunless-d $git_dir;6496my$svn_dir="$git_dir/svn";64976498# just in case somebody used 'svn' as their $id at some point...6499return$migratedif-d $svn_dir&& ! -f "$svn_dir/info/url";65006501print STDERR "Migrating from a git-svn v1 layout...\n";6502 mkpath([$svn_dir]);6503print STDERR "Data from a previous version of git-svn exists, but\n\t",6504"$svn_dir\n\t(required for this version ",6505"($::VERSION) of git-svn) does not exist.\n";6506my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6507while(<$fh>) {6508my$x=$_;6509next unless$x=~ s#^refs/remotes/##;6510chomp$x;6511next unless-f "$git_dir/$x/info/url";6512my$u=eval{ ::file_to_s("$git_dir/$x/info/url") };6513next unless$u;6514my$dn= dirname("$git_dir/svn/$x");6515 mkpath([$dn])unless-d $dn;6516if($xeq'svn') {# they used 'svn' as GIT_SVN_ID:6517 mkpath(["$git_dir/svn/svn"]);6518print STDERR " -$git_dir/$x/info=> ",6519"$git_dir/svn/$x/info\n";6520rename"$git_dir/$x/info","$git_dir/svn/$x/info"or6521 croak "$!:$x";6522# don't worry too much about these, they probably6523# don't exist with repos this old (save for index,6524# and we can easily regenerate that)6525foreachmy$f(qw/unhandled.log index .rev_db/) {6526rename"$git_dir/$x/$f","$git_dir/svn/$x/$f";6527}6528}else{6529print STDERR " -$git_dir/$x=>$git_dir/svn/$x\n";6530rename"$git_dir/$x","$git_dir/svn/$x"or6531 croak "$!:$x";6532}6533$migrated++;6534}6535 command_close_pipe($fh,$ctx);6536print STDERR "Done migrating from a git-svn v1 layout\n";6537$migrated;6538}65396540sub read_old_urls {6541my($l_map,$pfx,$path) =@_;6542my@dir;6543foreach(<$path/*>) {6544if(-r "$_/info/url") {6545$pfx.='/'if$pfx&&$pfx!~ m!/$!;6546my$ref_id=$pfx. basename $_;6547my$url= ::file_to_s("$_/info/url");6548$l_map->{$ref_id} =$url;6549}elsif(-d $_) {6550push@dir,$_;6551}6552}6553foreach(@dir) {6554my$x=$_;6555$x=~s!^\Q$ENV{GIT_DIR}\E/svn/!!o;6556 read_old_urls($l_map,$x,$_);6557}6558}65596560sub migrate_from_v2 {6561my@cfg= command(qw/config -l/);6562return ifgrep/^svn-remote\..+\.url=/,@cfg;6563my%l_map;6564 read_old_urls(\%l_map,'',"$ENV{GIT_DIR}/svn");6565my$migrated=0;65666567foreachmy$ref_id(sort keys%l_map) {6568eval{ Git::SVN->init($l_map{$ref_id},'',undef,$ref_id) };6569if($@) {6570 Git::SVN->init($l_map{$ref_id},'',$ref_id,$ref_id);6571}6572$migrated++;6573}6574$migrated;6575}65766577sub minimize_connections {6578my$r= Git::SVN::read_all_remotes();6579my$new_urls= {};6580my$root_repos= {};6581foreachmy$repo_id(keys%$r) {6582my$url=$r->{$repo_id}->{url}ornext;6583my$fetch=$r->{$repo_id}->{fetch}ornext;6584my$ra= Git::SVN::Ra->new($url);65856586# skip existing cases where we already connect to the root6587if(($ra->{url}eq$ra->{repos_root}) ||6588($ra->{repos_root}eq$repo_id)) {6589$root_repos->{$ra->{url}} =$repo_id;6590next;6591}65926593my$root_ra= Git::SVN::Ra->new($ra->{repos_root});6594my$root_path=$ra->{url};6595$root_path=~ s#^\Q$ra->{repos_root}\E(/|$)##;6596foreachmy$path(keys%$fetch) {6597my$ref_id=$fetch->{$path};6598my$gs= Git::SVN->new($ref_id,$repo_id,$path);65996600# make sure we can read when connecting to6601# a higher level of a repository6602my($last_rev,undef) =$gs->last_rev_commit;6603if(!defined$last_rev) {6604$last_rev=eval{6605$root_ra->get_latest_revnum;6606};6607next if$@;6608}6609my$new=$root_path;6610$new.=length$path?"/$path":'';6611eval{6612$root_ra->get_log([$new],$last_rev,$last_rev,66130,0,1,sub{ });6614};6615next if$@;6616$new_urls->{$ra->{repos_root}}->{$new} =6617{ ref_id =>$ref_id,6618 old_repo_id =>$repo_id,6619 old_path =>$path};6620}6621}66226623my@emptied;6624foreachmy$url(keys%$new_urls) {6625# see if we can re-use an existing [svn-remote "repo_id"]6626# instead of creating a(n ugly) new section:6627my$repo_id=$root_repos->{$url} ||$url;66286629my$fetch=$new_urls->{$url};6630foreachmy$path(keys%$fetch) {6631my$x=$fetch->{$path};6632 Git::SVN->init($url,$path,$repo_id,$x->{ref_id});6633my$pfx="svn-remote.$x->{old_repo_id}";66346635my$old_fetch=quotemeta("$x->{old_path}:".6636"$x->{ref_id}");6637 command_noisy(qw/config --unset/,6638"$pfx.fetch",'^'.$old_fetch.'$');6639delete$r->{$x->{old_repo_id}}->6640{fetch}->{$x->{old_path}};6641if(!keys%{$r->{$x->{old_repo_id}}->{fetch}}) {6642 command_noisy(qw/config --unset/,6643"$pfx.url");6644push@emptied,$x->{old_repo_id}6645}6646}6647}6648if(@emptied) {6649my$file=$ENV{GIT_CONFIG} ||"$ENV{GIT_DIR}/config";6650print STDERR <<EOF;6651The following [svn-remote] sections in your config file ($file) are empty6652and can be safely removed:6653EOF6654print STDERR "[svn-remote\"$_\"]\n"foreach@emptied;6655}6656}66576658sub migration_check {6659 migrate_from_v0();6660 migrate_from_v1();6661 migrate_from_v2();6662 minimize_connections()if$_minimize;6663}66646665package Git::IndexInfo;6666use strict;6667use warnings;6668use Git qw/command_input_pipe command_close_pipe/;66696670sub new {6671my($class) =@_;6672my($gui,$ctx) = command_input_pipe(qw/update-index -z --index-info/);6673bless{ gui =>$gui, ctx =>$ctx, nr =>0},$class;6674}66756676sub remove {6677my($self,$path) =@_;6678if(print{$self->{gui} }'0 ',0 x 40,"\t",$path,"\0") {6679return++$self->{nr};6680}6681undef;6682}66836684sub update {6685my($self,$mode,$hash,$path) =@_;6686if(print{$self->{gui} }$mode,' ',$hash,"\t",$path,"\0") {6687return++$self->{nr};6688}6689undef;6690}66916692sub DESTROY {6693my($self) =@_;6694 command_close_pipe($self->{gui},$self->{ctx});6695}66966697package Git::SVN::GlobSpec;6698use strict;6699use warnings;67006701sub new {6702my($class,$glob,$pattern_ok) =@_;6703my$re=$glob;6704$re=~s!/+$!!g;# no need for trailing slashes6705my(@left,@right,@patterns);6706my$state="left";6707my$die_msg="Only one set of wildcard directories ".6708"(e.g. '*' or '*/*/*') is supported: '$glob'\n";6709formy$part(split(m|/|,$glob)) {6710if($part=~/\*/&&$partne"*") {6711die"Invalid pattern in '$glob':$part\n";6712}elsif($pattern_ok&&$part=~/[{}]/&&6713$part!~/^\{[^{}]+\}/) {6714die"Invalid pattern in '$glob':$part\n";6715}6716if($parteq"*") {6717die$die_msgif$stateeq"right";6718$state="pattern";6719push(@patterns,"[^/]*");6720}elsif($pattern_ok&&$part=~/^\{(.*)\}$/) {6721die$die_msgif$stateeq"right";6722$state="pattern";6723my$p=quotemeta($1);6724$p=~s/\\,/|/g;6725push(@patterns,"(?:$p)");6726}else{6727if($stateeq"left") {6728push(@left,$part);6729}else{6730push(@right,$part);6731$state="right";6732}6733}6734}6735my$depth=@patterns;6736if($depth==0) {6737die"One '*' is needed in glob: '$glob'\n";6738}6739my$left=join('/',@left);6740my$right=join('/',@right);6741$re=join('/',@patterns);6742$re=join('\/',6743grep(length,quotemeta($left),"($re)",quotemeta($right)));6744my$left_re=qr/^\/\Q$left\E(\/|$)/;6745bless{ left =>$left, right =>$right, left_regex =>$left_re,6746 regex =>qr/$re/,glob=>$glob, depth =>$depth},$class;6747}67486749sub full_path {6750my($self,$path) =@_;6751return(length$self->{left} ?"$self->{left}/":'') .6752$path. (length$self->{right} ?"/$self->{right}":'');6753}67546755__END__67566757Data structures:675867596760$remotes= {# returned by read_all_remotes()6761'svn'=> {6762# svn-remote.svn.url=https://svn.musicpd.org6763 url =>'https://svn.musicpd.org',6764# svn-remote.svn.fetch=mpd/trunk:trunk6765 fetch => {6766'mpd/trunk'=>'trunk',6767},6768# svn-remote.svn.tags=mpd/tags/*:tags/*6769 tags => {6770 path => {6771 left =>'mpd/tags',6772 right =>'',6773 regex =>qr!mpd/tags/([^/]+)$!,6774glob=>'tags/*',6775},6776ref=> {6777 left =>'tags',6778 right =>'',6779 regex =>qr!tags/([^/]+)$!,6780glob=>'tags/*',6781},6782}6783}6784};67856786$log_entry hashref as returned by libsvn_log_entry()6787{6788log=>'whitespace-formatted log entry6789',# trailing newline is preserved6790 revision =>'8',# integer6791 date =>'2004-02-24T17:01:44.108345Z',# commit date6792 author =>'committer name'6793};679467956796# this is generated by generate_diff();6797@mods= array of diff-index line hashes,each element represents one line6798 of diff-index output67996800diff-index line ($m hash)6801{6802 mode_a => first column of diff-index output,no leading ':',6803 mode_b => second column of diff-index output,6804 sha1_b => sha1sum of the final blob,6805 chg => change type [MCRADT],6806 file_a => original file name of a file (iff chg is'C'or'R')6807 file_b => new/current file name of a file (any chg)6808}6809;68106811# retval of read_url_paths{,_all}();6812$l_map= {6813# repository root url6814'https://svn.musicpd.org'=> {6815# repository path # GIT_SVN_ID6816'mpd/trunk'=>'trunk',6817'mpd/tags/0.11.5'=>'tags/0.11.5',6818},6819}68206821Notes:6822 I don't trust the each() function on unless I created%hashmyself6823 because the internal iterator may not have started at base.