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} 39sub _req_svn { 40require SVN::Core;# use()-ing this causes segfaults for me... *shrug* 41require SVN::Ra; 42require SVN::Delta; 43if($SVN::Core::VERSION lt'1.1.0') { 44 fatal "Need SVN::Core 1.1.0 or better (got$SVN::Core::VERSION)"; 45} 46} 47my$can_compress=eval{require Compress::Zlib;1}; 48push@Git::SVN::Ra::ISA,'SVN::Ra'; 49push@SVN::Git::Editor::ISA,'SVN::Delta::Editor'; 50push@SVN::Git::Fetcher::ISA,'SVN::Delta::Editor'; 51use Carp qw/croak/; 52use Digest::MD5; 53use IO::File qw//; 54use File::Basename qw/dirname basename/; 55use File::Path qw/mkpath/; 56use File::Spec; 57use File::Find; 58use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; 59use IPC::Open3; 60use Git; 61use Memoize;# core since 5.8.0, Jul 2002 62 63BEGIN{ 64# import functions from Git into our packages, en masse 65no strict 'refs'; 66foreach(qw/command command_oneline command_noisy command_output_pipe 67 command_input_pipe command_close_pipe 68 command_bidi_pipe command_close_bidi_pipe/) { 69formy$package(qw(SVN::Git::Editor SVN::Git::Fetcher 70 Git::SVN::Migration Git::SVN::Log Git::SVN), 71 __PACKAGE__) { 72*{"${package}::$_"} = \&{"Git::$_"}; 73} 74} 75 Memoize::memoize 'Git::config'; 76 Memoize::memoize 'Git::config_bool'; 77} 78 79my($SVN); 80 81$sha1=qr/[a-f\d]{40}/; 82$sha1_short=qr/[a-f\d]{4,40}/; 83my($_stdin,$_help,$_edit, 84$_message,$_file,$_branch_dest, 85$_template,$_shared, 86$_version,$_fetch_all,$_no_rebase,$_fetch_parent, 87$_merge,$_strategy,$_dry_run,$_local, 88$_prefix,$_no_checkout,$_url,$_verbose, 89$_git_format,$_commit_url,$_tag,$_merge_info,$_interactive); 90$Git::SVN::_follow_parent =1; 91$SVN::Git::Fetcher::_placeholder_filename =".gitignore"; 92$_q||=0; 93my%remote_opts= ('username=s'=> \$Git::SVN::Prompt::_username, 94'config-dir=s'=> \$Git::SVN::Ra::config_dir, 95'no-auth-cache'=> \$Git::SVN::Prompt::_no_auth_cache, 96'ignore-paths=s'=> \$SVN::Git::Fetcher::_ignore_regex, 97'ignore-refs=s'=> \$Git::SVN::Ra::_ignore_refs_regex ); 98my%fc_opts= ('follow-parent|follow!'=> \$Git::SVN::_follow_parent, 99'authors-file|A=s'=> \$_authors, 100'authors-prog=s'=> \$_authors_prog, 101'repack:i'=> \$Git::SVN::_repack, 102'noMetadata'=> \$Git::SVN::_no_metadata, 103'useSvmProps'=> \$Git::SVN::_use_svm_props, 104'useSvnsyncProps'=> \$Git::SVN::_use_svnsync_props, 105'log-window-size=i'=> \$Git::SVN::Ra::_log_window_size, 106'no-checkout'=> \$_no_checkout, 107'quiet|q+'=> \$_q, 108'repack-flags|repack-args|repack-opts=s'=> 109 \$Git::SVN::_repack_flags, 110'use-log-author'=> \$Git::SVN::_use_log_author, 111'add-author-from'=> \$Git::SVN::_add_author_from, 112'localtime'=> \$Git::SVN::_localtime, 113%remote_opts); 114 115my($_trunk,@_tags,@_branches,$_stdlayout); 116my%icv; 117my%init_opts= ('template=s'=> \$_template,'shared:s'=> \$_shared, 118'trunk|T=s'=> \$_trunk,'tags|t=s@'=> \@_tags, 119'branches|b=s@'=> \@_branches,'prefix=s'=> \$_prefix, 120'stdlayout|s'=> \$_stdlayout, 121'minimize-url|m!'=> \$Git::SVN::_minimize_url, 122'no-metadata'=>sub{$icv{noMetadata} =1}, 123'use-svm-props'=>sub{$icv{useSvmProps} =1}, 124'use-svnsync-props'=>sub{$icv{useSvnsyncProps} =1}, 125'rewrite-root=s'=>sub{$icv{rewriteRoot} =$_[1] }, 126'rewrite-uuid=s'=>sub{$icv{rewriteUUID} =$_[1] }, 127%remote_opts); 128my%cmt_opts= ('edit|e'=> \$_edit, 129'rmdir'=> \$SVN::Git::Editor::_rmdir, 130'find-copies-harder'=> \$SVN::Git::Editor::_find_copies_harder, 131'l=i'=> \$SVN::Git::Editor::_rename_limit, 132'copy-similarity|C=i'=> \$SVN::Git::Editor::_cp_similarity 133); 134 135my%cmd= ( 136 fetch => [ \&cmd_fetch,"Download new revisions from SVN", 137{'revision|r=s'=> \$_revision, 138'fetch-all|all'=> \$_fetch_all, 139'parent|p'=> \$_fetch_parent, 140%fc_opts} ], 141 clone => [ \&cmd_clone,"Initialize and fetch revisions", 142{'revision|r=s'=> \$_revision, 143'preserve-empty-dirs'=> 144 \$SVN::Git::Fetcher::_preserve_empty_dirs, 145'placeholder-filename=s'=> 146 \$SVN::Git::Fetcher::_placeholder_filename, 147%fc_opts,%init_opts} ], 148 init => [ \&cmd_init,"Initialize a repo for tracking". 149" (requires URL argument)", 150 \%init_opts], 151'multi-init'=> [ \&cmd_multi_init, 152"Deprecated alias for ". 153"'$0init -T<trunk> -b<branches> -t<tags>'", 154 \%init_opts], 155 dcommit => [ \&cmd_dcommit, 156'Commit several diffs to merge with upstream', 157{'merge|m|M'=> \$_merge, 158'strategy|s=s'=> \$_strategy, 159'verbose|v'=> \$_verbose, 160'dry-run|n'=> \$_dry_run, 161'fetch-all|all'=> \$_fetch_all, 162'commit-url=s'=> \$_commit_url, 163'revision|r=i'=> \$_revision, 164'no-rebase'=> \$_no_rebase, 165'mergeinfo=s'=> \$_merge_info, 166'interactive|i'=> \$_interactive, 167%cmt_opts,%fc_opts} ], 168 branch => [ \&cmd_branch, 169'Create a branch in the SVN repository', 170{'message|m=s'=> \$_message, 171'destination|d=s'=> \$_branch_dest, 172'dry-run|n'=> \$_dry_run, 173'tag|t'=> \$_tag, 174'username=s'=> \$Git::SVN::Prompt::_username, 175'commit-url=s'=> \$_commit_url} ], 176 tag => [sub{$_tag=1; cmd_branch(@_) }, 177'Create a tag in the SVN repository', 178{'message|m=s'=> \$_message, 179'destination|d=s'=> \$_branch_dest, 180'dry-run|n'=> \$_dry_run, 181'username=s'=> \$Git::SVN::Prompt::_username, 182'commit-url=s'=> \$_commit_url} ], 183'set-tree'=> [ \&cmd_set_tree, 184"Set an SVN repository to a git tree-ish", 185{'stdin'=> \$_stdin,%cmt_opts,%fc_opts, } ], 186'create-ignore'=> [ \&cmd_create_ignore, 187'Create a .gitignore per svn:ignore', 188{'revision|r=i'=> \$_revision 189} ], 190'mkdirs'=> [ \&cmd_mkdirs , 191"recreate empty directories after a checkout", 192{'revision|r=i'=> \$_revision} ], 193'propget'=> [ \&cmd_propget, 194'Print the value of a property on a file or directory', 195{'revision|r=i'=> \$_revision} ], 196'proplist'=> [ \&cmd_proplist, 197'List all properties of a file or directory', 198{'revision|r=i'=> \$_revision} ], 199'show-ignore'=> [ \&cmd_show_ignore,"Show svn:ignore listings", 200{'revision|r=i'=> \$_revision 201} ], 202'show-externals'=> [ \&cmd_show_externals,"Show svn:externals listings", 203{'revision|r=i'=> \$_revision 204} ], 205'multi-fetch'=> [ \&cmd_multi_fetch, 206"Deprecated alias for$0fetch --all", 207{'revision|r=s'=> \$_revision,%fc_opts} ], 208'migrate'=> [sub{ }, 209# no-op, we automatically run this anyways, 210'Migrate configuration/metadata/layout from 211 previous versions of git-svn', 212{'minimize'=> \$Git::SVN::Migration::_minimize, 213%remote_opts} ], 214'log'=> [ \&Git::SVN::Log::cmd_show_log,'Show commit logs', 215{'limit=i'=> \$Git::SVN::Log::limit, 216'revision|r=s'=> \$_revision, 217'verbose|v'=> \$Git::SVN::Log::verbose, 218'incremental'=> \$Git::SVN::Log::incremental, 219'oneline'=> \$Git::SVN::Log::oneline, 220'show-commit'=> \$Git::SVN::Log::show_commit, 221'non-recursive'=> \$Git::SVN::Log::non_recursive, 222'authors-file|A=s'=> \$_authors, 223'color'=> \$Git::SVN::Log::color, 224'pager=s'=> \$Git::SVN::Log::pager 225} ], 226'find-rev'=> [ \&cmd_find_rev, 227"Translate between SVN revision numbers and tree-ish", 228{} ], 229'rebase'=> [ \&cmd_rebase,"Fetch and rebase your working directory", 230{'merge|m|M'=> \$_merge, 231'verbose|v'=> \$_verbose, 232'strategy|s=s'=> \$_strategy, 233'local|l'=> \$_local, 234'fetch-all|all'=> \$_fetch_all, 235'dry-run|n'=> \$_dry_run, 236%fc_opts} ], 237'commit-diff'=> [ \&cmd_commit_diff, 238'Commit a diff between two trees', 239{'message|m=s'=> \$_message, 240'file|F=s'=> \$_file, 241'revision|r=s'=> \$_revision, 242%cmt_opts} ], 243'info'=> [ \&cmd_info, 244"Show info about the latest SVN revision 245 on the current branch", 246{'url'=> \$_url, } ], 247'blame'=> [ \&Git::SVN::Log::cmd_blame, 248"Show what revision and author last modified each line of a file", 249{'git-format'=> \$_git_format} ], 250'reset'=> [ \&cmd_reset, 251"Undo fetches back to the specified SVN revision", 252{'revision|r=s'=> \$_revision, 253'parent|p'=> \$_fetch_parent} ], 254'gc'=> [ \&cmd_gc, 255"Compress unhandled.log files in .git/svn and remove ". 256"index files in .git/svn", 257{} ], 258); 259 260use Term::ReadLine; 261package FakeTerm; 262sub new { 263my($class,$reason) =@_; 264returnbless \$reason,shift; 265} 266subreadline{ 267my$self=shift; 268die"Cannot use readline on FakeTerm:$$self"; 269} 270package main; 271 272my$term=eval{ 273$ENV{"GIT_SVN_NOTTY"} 274? new Term::ReadLine 'git-svn', \*STDIN, \*STDOUT 275: new Term::ReadLine 'git-svn'; 276}; 277if($@) { 278$term= new FakeTerm "$@: going non-interactive"; 279} 280 281my$cmd; 282for(my$i=0;$i<@ARGV;$i++) { 283if(defined$cmd{$ARGV[$i]}) { 284$cmd=$ARGV[$i]; 285splice@ARGV,$i,1; 286last; 287}elsif($ARGV[$i]eq'help') { 288$cmd=$ARGV[$i+1]; 289 usage(0); 290} 291}; 292 293# make sure we're always running at the top-level working directory 294unless($cmd&&$cmd=~/(?:clone|init|multi-init)$/) { 295unless(-d $ENV{GIT_DIR}) { 296if($git_dir_user_set) { 297die"GIT_DIR=$ENV{GIT_DIR} explicitly set, ", 298"but it is not a directory\n"; 299} 300my$git_dir=delete$ENV{GIT_DIR}; 301my$cdup=undef; 302 git_cmd_try { 303$cdup= command_oneline(qw/rev-parse --show-cdup/); 304$git_dir='.'unless($cdup); 305chomp$cdupif($cdup); 306$cdup="."unless($cdup&&length$cdup); 307}"Already at toplevel, but$git_dirnot found\n"; 308chdir$cdupor die"Unable to chdir up to '$cdup'\n"; 309unless(-d $git_dir) { 310die"$git_dirstill not found after going to ", 311"'$cdup'\n"; 312} 313$ENV{GIT_DIR} =$git_dir; 314} 315$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 316} 317 318my%opts= %{$cmd{$cmd}->[2]}if(defined$cmd); 319 320read_git_config(\%opts); 321if($cmd&& ($cmdeq'log'||$cmdeq'blame')) { 322 Getopt::Long::Configure('pass_through'); 323} 324my$rv= GetOptions(%opts,'h|H'=> \$_help,'version|V'=> \$_version, 325'minimize-connections'=> \$Git::SVN::Migration::_minimize, 326'id|i=s'=> \$Git::SVN::default_ref_id, 327'svn-remote|remote|R=s'=>sub{ 328$Git::SVN::no_reuse_existing =1; 329$Git::SVN::default_repo_id =$_[1] }); 330exit1if(!$rv&&$cmd&&$cmdne'log'); 331 332usage(0)if$_help; 333version()if$_version; 334usage(1)unlessdefined$cmd; 335load_authors()if$_authors; 336if(defined$_authors_prog) { 337$_authors_prog="'". File::Spec->rel2abs($_authors_prog) ."'"; 338} 339 340unless($cmd=~/^(?:clone|init|multi-init|commit-diff)$/) { 341 Git::SVN::Migration::migration_check(); 342} 343Git::SVN::init_vars(); 344eval{ 345 Git::SVN::verify_remotes_sanity(); 346$cmd{$cmd}->[0]->(@ARGV); 347}; 348fatal $@if$@; 349post_fetch_checkout(); 350exit0; 351 352####################### primary functions ###################### 353sub usage { 354my$exit=shift||0; 355my$fd=$exit? \*STDERR : \*STDOUT; 356print$fd<<""; 357git-svn - bidirectional operations between a single Subversion tree and git 358Usage: git svn <command> [options] [arguments]\n 359 360print$fd"Available commands:\n"unless$cmd; 361 362foreach(sort keys%cmd) { 363next if$cmd&&$cmdne$_; 364next if/^multi-/;# don't show deprecated commands 365print$fd' ',pack('A17',$_),$cmd{$_}->[1],"\n"; 366foreach(sort keys%{$cmd{$_}->[2]}) { 367# mixed-case options are for .git/config only 368next if/[A-Z]/&&/^[a-z]+$/i; 369# prints out arguments as they should be passed: 370my$x= s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : ''; 371print$fd' ' x 21,join(', ',map{length$_>1? 372"--$_":"-$_"} 373split/\|/,$_),"$x\n"; 374} 375} 376print$fd<<""; 377\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an 378arbitrary identifier if you're tracking multiple SVN branches/repositories in 379one git repository and want to keep them separate. See git-svn(1) for more 380information. 381 382 exit$exit; 383} 384 385sub version { 386 ::_req_svn(); 387 print "git-svn version$VERSION(svn$SVN::Core::VERSION)\n"; 388 exit 0; 389} 390 391sub ask { 392 my ($prompt,%arg) =@_; 393 my$valid_re=$arg{valid_re}; 394 my$default=$arg{default}; 395 my$resp; 396 my$i= 0; 397 398 if ( !( defined($term->IN) 399 && defined( fileno($term->IN) ) 400 && defined($term->OUT ) 401 && defined( fileno($term->OUT) ) ) ){ 402 return defined($default) ?$default: undef; 403 } 404 405 while ($i++< 10) { 406$resp=$term->readline($prompt); 407 if (!defined$resp) { # EOF 408 print "\n"; 409 return defined$default?$default: undef; 410 } 411 if ($respeq '' and defined$default) { 412 return$default; 413 } 414 if (!defined$valid_reor$resp=~ /$valid_re/) { 415 return$resp; 416 } 417 } 418 return undef; 419} 420 421sub do_git_init_db { 422 unless (-d$ENV{GIT_DIR}) { 423 my@init_db= ('init'); 424 push@init_db, "--template=$_template" if defined$_template; 425 if (defined$_shared) { 426 if ($_shared=~ /[a-z]/) { 427 push@init_db, "--shared=$_shared"; 428 } else { 429 push@init_db, "--shared"; 430 } 431 } 432 command_noisy(@init_db); 433$_repository= Git->repository(Repository => ".git"); 434 } 435 my$set; 436 my$pfx= "svn-remote.$Git::SVN::default_repo_id"; 437 foreach my$i(keys%icv) { 438 die "'$set' and '$i' cannot both be set\n" if$set; 439 next unless defined$icv{$i}; 440 command_noisy('config', "$pfx.$i",$icv{$i}); 441$set=$i; 442 } 443 my$ignore_paths_regex= \$SVN::Git::Fetcher::_ignore_regex; 444 command_noisy('config', "$pfx.ignore-paths",$$ignore_paths_regex) 445 if defined$$ignore_paths_regex; 446 my$ignore_refs_regex= \$Git::SVN::Ra::_ignore_refs_regex; 447 command_noisy('config', "$pfx.ignore-refs",$$ignore_refs_regex) 448 if defined$$ignore_refs_regex; 449 450 if (defined$SVN::Git::Fetcher::_preserve_empty_dirs) { 451 my$fname= \$SVN::Git::Fetcher::_placeholder_filename; 452 command_noisy('config', "$pfx.preserve-empty-dirs", 'true'); 453 command_noisy('config', "$pfx.placeholder-filename",$$fname); 454 } 455} 456 457sub init_subdir { 458 my$repo_path= shift or return; 459 mkpath([$repo_path]) unless -d$repo_path; 460 chdir$repo_pathor die "Couldn't chdir to $repo_path:$!\n"; 461$ENV{GIT_DIR} = '.git'; 462$_repository= Git->repository(Repository =>$ENV{GIT_DIR}); 463} 464 465sub cmd_clone { 466 my ($url,$path) =@_; 467 if (!defined$path&& 468 (defined$_trunk||@_branches||@_tags|| 469 defined$_stdlayout) && 470$url!~ m#^[a-z\+]+://#) { 471$path=$url; 472 } 473$path= basename($url) if !defined$path|| !length$path; 474 my$authors_absolute=$_authors? File::Spec->rel2abs($_authors) : ""; 475 cmd_init($url,$path); 476 command_oneline('config', 'svn.authorsfile',$authors_absolute) 477 if$_authors; 478 Git::SVN::fetch_all($Git::SVN::default_repo_id); 479} 480 481sub cmd_init { 482 if (defined$_stdlayout) { 483$_trunk= 'trunk' if (!defined$_trunk); 484@_tags= 'tags' if (!@_tags); 485@_branches= 'branches' if (!@_branches); 486 } 487 if (defined$_trunk||@_branches||@_tags) { 488 return cmd_multi_init(@_); 489 } 490 my$url= shift or die "SVN repository location required ", 491 "as a command-line argument\n"; 492$url= canonicalize_url($url); 493 init_subdir(@_); 494 do_git_init_db(); 495 496 if ($Git::SVN::_minimize_url eq 'unset') { 497$Git::SVN::_minimize_url = 0; 498 } 499 500 Git::SVN->init($url); 501} 502 503sub cmd_fetch { 504 if (grep /^\d+=./,@_) { 505 die "'<rev>=<commit>' fetch arguments are ", 506 "no longer supported.\n"; 507 } 508 my ($remote) =@_; 509 if (@_> 1) { 510 die "Usage:$0 fetch [--all] [--parent] [svn-remote]\n"; 511 } 512$Git::SVN::no_reuse_existing = undef; 513 if ($_fetch_parent) { 514 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD'); 515 unless ($gs) { 516 die "Unable to determine upstream SVN information from ", 517 "working tree history\n"; 518 } 519 # just fetch, don't checkout. 520$_no_checkout= 'true'; 521$_fetch_all?$gs->fetch_all :$gs->fetch; 522 } elsif ($_fetch_all) { 523 cmd_multi_fetch(); 524 } else { 525$remote||=$Git::SVN::default_repo_id; 526 Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes()); 527 } 528} 529 530sub cmd_set_tree { 531 my (@commits) =@_; 532 if ($_stdin|| !@commits) { 533 print "Reading from stdin...\n"; 534@commits= (); 535 while (<STDIN>) { 536 if (/\b($sha1_short)\b/o) { 537 unshift@commits,$1; 538 } 539 } 540 } 541 my@revs; 542 foreach my$c(@commits) { 543 my@tmp= command('rev-parse',$c); 544 if (scalar@tmp== 1) { 545 push@revs,$tmp[0]; 546 } elsif (scalar@tmp> 1) { 547 push@revs, reverse(command('rev-list',@tmp)); 548 } else { 549 fatal "Failed to rev-parse $c"; 550 } 551 } 552 my$gs= Git::SVN->new; 553 my ($r_last,$cmt_last) =$gs->last_rev_commit; 554$gs->fetch; 555 if (defined$gs->{last_rev} &&$r_last!=$gs->{last_rev}) { 556 fatal "There are new revisions that were fetched ", 557 "and need to be merged (or acknowledged)", 558 "before committing.\nlast rev:$r_last\n", 559 " current:$gs->{last_rev}"; 560 } 561$gs->set_tree($_) foreach@revs; 562 print "Done committing ",scalar@revs," revisions to SVN\n"; 563 unlink$gs->{index}; 564} 565 566sub split_merge_info_range { 567 my ($range) =@_; 568 if ($range=~ /(\d+)-(\d+)/) { 569 return (int($1), int($2)); 570 } else { 571 return (int($range), int($range)); 572 } 573} 574 575sub combine_ranges { 576 my ($in) =@_; 577 578 my@fnums= (); 579 my@arr= split(/,/,$in); 580 for my$element(@arr) { 581 my ($start,$end) = split_merge_info_range($element); 582 push@fnums,$start; 583 } 584 585 my@sorted=@arr[ sort { 586$fnums[$a] <=>$fnums[$b] 587 } 0..$#arr]; 588 589 my@return= (); 590 my$last= -1; 591 my$first= -1; 592 for my$element(@sorted) { 593 my ($start,$end) = split_merge_info_range($element); 594 595 if ($last== -1) { 596$first=$start; 597$last=$end; 598 next; 599 } 600 if ($start<=$last+1) { 601 if ($end>$last) { 602$last=$end; 603 } 604 next; 605 } 606 if ($first==$last) { 607 push@return, "$first"; 608 } else { 609 push@return, "$first-$last"; 610 } 611$first=$start; 612$last=$end; 613 } 614 615 if ($first!= -1) { 616 if ($first==$last) { 617 push@return, "$first"; 618 } else { 619 push@return, "$first-$last"; 620 } 621 } 622 623 return join(',',@return); 624} 625 626sub merge_revs_into_hash { 627 my ($hash,$minfo) =@_; 628 my@lines= split(' ',$minfo); 629 630 for my$line(@lines) { 631 my ($branchpath,$revs) = split(/:/,$line); 632 633 if (exists($hash->{$branchpath})) { 634 # Merge the two revision sets 635 my$combined= "$hash->{$branchpath},$revs"; 636$hash->{$branchpath} = combine_ranges($combined); 637 } else { 638 # Just do range combining for consolidation 639$hash->{$branchpath} = combine_ranges($revs); 640 } 641 } 642} 643 644sub merge_merge_info { 645 my ($mergeinfo_one,$mergeinfo_two) =@_; 646 my%result_hash= (); 647 648 merge_revs_into_hash(\%result_hash,$mergeinfo_one); 649 merge_revs_into_hash(\%result_hash,$mergeinfo_two); 650 651 my$result= ''; 652 # Sort below is for consistency's sake 653 for my$branchname(sort keys(%result_hash)) { 654 my$revlist=$result_hash{$branchname}; 655$result.= "$branchname:$revlist\n" 656 } 657 return$result; 658} 659 660sub populate_merge_info { 661 my ($d,$gs,$uuid,$linear_refs,$rewritten_parent) =@_; 662 663 my%parentshash; 664 read_commit_parents(\%parentshash,$d); 665 my@parents= @{$parentshash{$d}}; 666 if ($#parents> 0) { 667 # Merge commit 668 my$all_parents_ok= 1; 669 my$aggregate_mergeinfo= ''; 670 my$rooturl=$gs->repos_root; 671 672 if (defined($rewritten_parent)) { 673 # Replace first parent with newly-rewritten version 674 shift@parents; 675 unshift@parents,$rewritten_parent; 676 } 677 678 foreach my$parent(@parents) { 679 my ($branchurl,$svnrev,$paruuid) = 680 cmt_metadata($parent); 681 682 unless (defined($svnrev)) { 683 # Should have been caught be preflight check 684 fatal "merge commit $dhas ancestor $parent, but that change " 685 ."doesnot have git-svn metadata!"; 686 } 687 unless ($branchurl=~ /^\Q$rooturl\E(.*)/) { 688 fatal "commit $parent git-svn metadata changed mid-run!"; 689 } 690 my$branchpath=$1; 691 692 my$ra= Git::SVN::Ra->new($branchurl); 693 my (undef, undef,$props) = 694$ra->get_dir(canonicalize_path("."),$svnrev); 695 my$par_mergeinfo=$props->{'svn:mergeinfo'}; 696 unless (defined$par_mergeinfo) { 697$par_mergeinfo= ''; 698 } 699 # Merge previous mergeinfo values 700$aggregate_mergeinfo= 701 merge_merge_info($aggregate_mergeinfo, 702$par_mergeinfo, 0); 703 704 next if$parenteq$parents[0]; # Skip first parent 705 # Add new changes being placed in tree by merge 706 my@cmd= (qw/rev-list --reverse/, 707$parent, qw/--not/); 708 foreach my$par(@parents) { 709 unless ($pareq$parent) { 710 push@cmd,$par; 711 } 712 } 713 my@revsin= (); 714 my ($revlist,$ctx) = command_output_pipe(@cmd); 715 while (<$revlist>) { 716 my$irev=$_; 717 chomp$irev; 718 my (undef,$csvnrev, undef) = 719 cmt_metadata($irev); 720 unless (defined$csvnrev) { 721 # A child is missing SVN annotations... 722 # this might be OK, or might not be. 723 warn "W:child $irevis merged into revision " 724 ."$d but doesnot have git-svn metadata." 725 ."This means git-svn cannot determine the " 726 ."svn revision numbers to place into the " 727 ."svn:mergeinfo property. You must ensure " 728 ."a branch is entirely committed to " 729 ."SVN before merging it in order for" 730 ."svn:mergeinfo population to function " 731 ."properly"; 732 } 733 push@revsin,$csvnrev; 734 } 735 command_close_pipe($revlist,$ctx); 736 737 last unless$all_parents_ok; 738 739 # We now have a list of all SVN revnos which are 740 # merged by this particular parent. Integrate them. 741 next if$#revsin== -1; 742 my$newmergeinfo= "$branchpath:" . join(',',@revsin); 743$aggregate_mergeinfo= 744 merge_merge_info($aggregate_mergeinfo, 745$newmergeinfo, 1); 746 } 747 if ($all_parents_okand$aggregate_mergeinfo) { 748 return$aggregate_mergeinfo; 749 } 750 } 751 752 return undef; 753} 754 755sub cmd_dcommit { 756 my$head= shift; 757 command_noisy(qw/update-index --refresh/); 758 git_cmd_try { command_oneline(qw/diff-index --quiet HEAD/) } 759 'Cannot dcommit with a dirty index. Commit your changes first, ' 760 . "or stash them with `git stash'.\n"; 761$head||= 'HEAD'; 762 763 my$old_head; 764 if ($headne 'HEAD') { 765$old_head= eval { 766 command_oneline([qw/symbolic-ref -q HEAD/]) 767 }; 768 if ($old_head) { 769$old_head=~ s{^refs/heads/}{}; 770 } else { 771$old_head= eval { command_oneline(qw/rev-parse HEAD/) }; 772 } 773 command(['checkout',$head], STDERR => 0); 774 } 775 776 my@refs; 777 my ($url,$rev,$uuid,$gs) = working_head_info('HEAD', \@refs); 778 unless ($gs) { 779 die "Unable to determine upstream SVN information from ", 780 "$headhistory.\nPerhaps the repository is empty."; 781 } 782 783 if (defined$_commit_url) { 784$url=$_commit_url; 785 } else { 786$url= eval { command_oneline('config', '--get', 787 "svn-remote.$gs->{repo_id}.commiturl") }; 788 if (!$url) { 789$url=$gs->full_pushurl 790 } 791 } 792 793 my$last_rev=$_revisionif defined$_revision; 794 if ($url) { 795 print "Committing to$url...\n"; 796 } 797 my ($linear_refs,$parents) = linearize_history($gs, \@refs); 798 if ($_no_rebase&& scalar(@$linear_refs) > 1) { 799 warn "Attempting to commit more than one change while ", 800 "--no-rebase is enabled.\n", 801 "If these changes depend on each other, re-running ", 802 "without --no-rebase may be required." 803 } 804 805 if (defined$_interactive){ 806 my$ask_default= "y"; 807 foreach my$d(@$linear_refs){ 808 my ($fh,$ctx) = command_output_pipe(qw(show --summary),"$d"); 809while(<$fh>){ 810print$_; 811} 812 command_close_pipe($fh,$ctx); 813$_= ask("Commit this patch to SVN? ([y]es (default)|[n]o|[q]uit|[a]ll): ", 814 valid_re =>qr/^(?:yes|y|no|n|quit|q|all|a)/i, 815default=>$ask_default); 816die"Commit this patch reply required"unlessdefined$_; 817if(/^[nq]/i) { 818exit(0); 819}elsif(/^a/i) { 820last; 821} 822} 823} 824 825my$expect_url=$url; 826 827my$push_merge_info=eval{ 828 command_oneline(qw/config --get svn.pushmergeinfo/) 829}; 830if(not defined($push_merge_info) 831or$push_merge_infoeq"false" 832or$push_merge_infoeq"no" 833or$push_merge_infoeq"never") { 834$push_merge_info=0; 835} 836 837unless(defined($_merge_info) || !$push_merge_info) { 838# Preflight check of changes to ensure no issues with mergeinfo 839# This includes check for uncommitted-to-SVN parents 840# (other than the first parent, which we will handle), 841# information from different SVN repos, and paths 842# which are not underneath this repository root. 843my$rooturl=$gs->repos_root; 844foreachmy$d(@$linear_refs) { 845my%parentshash; 846 read_commit_parents(\%parentshash,$d); 847my@realparents= @{$parentshash{$d}}; 848if($#realparents>0) { 849# Merge commit 850shift@realparents;# Remove/ignore first parent 851foreachmy$parent(@realparents) { 852my($branchurl,$svnrev,$paruuid) = cmt_metadata($parent); 853unless(defined$paruuid) { 854# A parent is missing SVN annotations... 855# abort the whole operation. 856 fatal "$parentis merged into revision$d, " 857."but does not have git-svn metadata. " 858."Either dcommit the branch or use a " 859."local cherry-pick, FF merge, or rebase " 860."instead of an explicit merge commit."; 861} 862 863unless($paruuideq$uuid) { 864# Parent has SVN metadata from different repository 865 fatal "merge parent$parentfor change$dhas " 866."git-svn uuid$paruuid, while current change " 867."has uuid$uuid!"; 868} 869 870unless($branchurl=~/^\Q$rooturl\E(.*)/) { 871# This branch is very strange indeed. 872 fatal "merge parent$parentfor$dis on branch " 873."$branchurl, which is not under the " 874."git-svn root$rooturl!"; 875} 876} 877} 878} 879} 880 881my$rewritten_parent; 882 Git::SVN::remove_username($expect_url); 883if(defined($_merge_info)) { 884$_merge_info=~tr{ }{\n}; 885} 886while(1) { 887my$d=shift@$linear_refsorlast; 888unless(defined$last_rev) { 889(undef,$last_rev,undef) = cmt_metadata("$d~1"); 890unless(defined$last_rev) { 891 fatal "Unable to extract revision information ", 892"from commit$d~1"; 893} 894} 895if($_dry_run) { 896print"diff-tree$d~1$d\n"; 897}else{ 898my$cmt_rev; 899 900unless(defined($_merge_info) || !$push_merge_info) { 901$_merge_info= populate_merge_info($d,$gs, 902$uuid, 903$linear_refs, 904$rewritten_parent); 905} 906 907my%ed_opts= ( r =>$last_rev, 908log=> get_commit_entry($d)->{log}, 909 ra => Git::SVN::Ra->new($url), 910 config => SVN::Core::config_get_config( 911$Git::SVN::Ra::config_dir 912), 913 tree_a =>"$d~1", 914 tree_b =>$d, 915 editor_cb =>sub{ 916print"Committed r$_[0]\n"; 917$cmt_rev=$_[0]; 918}, 919 mergeinfo =>$_merge_info, 920 svn_path =>''); 921if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) { 922print"No changes\n$d~1==$d\n"; 923}elsif($parents->{$d} && @{$parents->{$d}}) { 924$gs->{inject_parents_dcommit}->{$cmt_rev} = 925$parents->{$d}; 926} 927$_fetch_all?$gs->fetch_all:$gs->fetch; 928$last_rev=$cmt_rev; 929next if$_no_rebase; 930 931# we always want to rebase against the current HEAD, 932# not any head that was passed to us 933my@diff= command('diff-tree',$d, 934$gs->refname,'--'); 935my@finish; 936if(@diff) { 937@finish= rebase_cmd(); 938print STDERR "W:$dand ",$gs->refname, 939" differ, using@finish:\n", 940join("\n",@diff),"\n"; 941}else{ 942print"No changes between current HEAD and ", 943$gs->refname, 944"\nResetting to the latest ", 945$gs->refname,"\n"; 946@finish= qw/reset --mixed/; 947} 948 command_noisy(@finish,$gs->refname); 949 950$rewritten_parent= command_oneline(qw/rev-parse HEAD/); 951 952if(@diff) { 953@refs= (); 954my($url_,$rev_,$uuid_,$gs_) = 955 working_head_info('HEAD', \@refs); 956my($linear_refs_,$parents_) = 957 linearize_history($gs_, \@refs); 958if(scalar(@$linear_refs) != 959scalar(@$linear_refs_)) { 960 fatal "# of revisions changed ", 961"\nbefore:\n", 962join("\n",@$linear_refs), 963"\n\nafter:\n", 964join("\n",@$linear_refs_),"\n", 965'If you are attempting to commit ', 966"merges, try running:\n\t", 967'git rebase --interactive', 968'--preserve-merges ', 969$gs->refname, 970"\nBefore dcommitting"; 971} 972if($url_ne$expect_url) { 973if($url_eq$gs->metadata_url) { 974print 975"Accepting rewritten URL:", 976"$url_\n"; 977}else{ 978 fatal 979"URL mismatch after rebase:", 980"$url_!=$expect_url"; 981} 982} 983if($uuid_ne$uuid) { 984 fatal "uuid mismatch after rebase: ", 985"$uuid_!=$uuid"; 986} 987# remap parents 988my(%p,@l,$i); 989for($i=0;$i<scalar@$linear_refs;$i++) { 990my$new=$linear_refs_->[$i]ornext; 991$p{$new} = 992$parents->{$linear_refs->[$i]}; 993push@l,$new; 994} 995$parents= \%p; 996$linear_refs= \@l; 997} 998} 999}10001001if($old_head) {1002my$new_head= command_oneline(qw/rev-parse HEAD/);1003my$new_is_symbolic=eval{1004 command_oneline(qw/symbolic-ref -q HEAD/);1005};1006if($new_is_symbolic) {1007print"dcommitted the branch ",$head,"\n";1008}else{1009print"dcommitted on a detached HEAD because you gave ",1010"a revision argument.\n",1011"The rewritten commit is: ",$new_head,"\n";1012}1013 command(['checkout',$old_head], STDERR =>0);1014}10151016unlink$gs->{index};1017}10181019sub cmd_branch {1020my($branch_name,$head) =@_;10211022unless(defined$branch_name&&length$branch_name) {1023die(($_tag?"tag":"branch") ." name required\n");1024}1025$head||='HEAD';10261027my(undef,$rev,undef,$gs) = working_head_info($head);1028my$src=$gs->full_pushurl;10291030my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1031my$allglobs=$remote->{$_tag?'tags':'branches'};1032my$glob;1033if($#{$allglobs} ==0) {1034$glob=$allglobs->[0];1035}else{1036unless(defined$_branch_dest) {1037die"Multiple ",1038$_tag?"tag":"branch",1039" paths defined for Subversion repository.\n",1040"You must specify where you want to create the ",1041$_tag?"tag":"branch",1042" with the --destination argument.\n";1043}1044foreachmy$g(@{$allglobs}) {1045# SVN::Git::Editor could probably be moved to Git.pm..1046my$re= SVN::Git::Editor::glob2pat($g->{path}->{left});1047if($_branch_dest=~/$re/) {1048$glob=$g;1049last;1050}1051}1052unless(defined$glob) {1053my$dest_re=qr/\b\Q$_branch_dest\E\b/;1054foreachmy$g(@{$allglobs}) {1055$g->{path}->{left} =~/$dest_re/ornext;1056if(defined$glob) {1057die"Ambiguous destination: ",1058$_branch_dest,"\nmatches both '",1059$glob->{path}->{left},"' and '",1060$g->{path}->{left},"'\n";1061}1062$glob=$g;1063}1064unless(defined$glob) {1065die"Unknown ",1066$_tag?"tag":"branch",1067" destination$_branch_dest\n";1068}1069}1070}1071my($lft,$rgt) = @{$glob->{path} }{qw/left right/};1072my$url;1073if(defined$_commit_url) {1074$url=$_commit_url;1075}else{1076$url=eval{ command_oneline('config','--get',1077"svn-remote.$gs->{repo_id}.commiturl") };1078if(!$url) {1079$url=$remote->{pushurl} ||$remote->{url};1080}1081}1082my$dst=join'/',$url,$lft,$branch_name, ($rgt|| ());10831084if($dst=~/^https:/&&$src=~/^http:/) {1085$src=~s/^http:/https:/;1086}10871088::_req_svn();10891090my$ctx= SVN::Client->new(1091 auth => Git::SVN::Ra::_auth_providers(),1092 log_msg =>sub{1093${$_[0] } =defined$_message1094?$_message1095:'Create '. ($_tag?'tag ':'branch ')1096.$branch_name;1097},1098);10991100eval{1101$ctx->ls($dst,'HEAD',0);1102}and die"branch ${branch_name} already exists\n";11031104print"Copying ${src} at r${rev} to ${dst}...\n";1105$ctx->copy($src,$rev,$dst)1106unless$_dry_run;11071108$gs->fetch_all;1109}11101111sub cmd_find_rev {1112my$revision_or_hash=shift or die"SVN or git revision required ",1113"as a command-line argument\n";1114my$result;1115if($revision_or_hash=~/^r\d+$/) {1116my$head=shift;1117$head||='HEAD';1118my@refs;1119my(undef,undef,$uuid,$gs) = working_head_info($head, \@refs);1120unless($gs) {1121die"Unable to determine upstream SVN information from ",1122"$headhistory\n";1123}1124my$desired_revision=substr($revision_or_hash,1);1125$result=$gs->rev_map_get($desired_revision,$uuid);1126}else{1127my(undef,$rev,undef) = cmt_metadata($revision_or_hash);1128$result=$rev;1129}1130print"$result\n"if$result;1131}11321133sub auto_create_empty_directories {1134my($gs) =@_;1135my$var=eval{ command_oneline('config','--get','--bool',1136"svn-remote.$gs->{repo_id}.automkdirs") };1137# By default, create empty directories by consulting the unhandled log,1138# but allow setting it to 'false' to skip it.1139return!($var&&$vareq'false');1140}11411142sub cmd_rebase {1143 command_noisy(qw/update-index --refresh/);1144my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1145unless($gs) {1146die"Unable to determine upstream SVN information from ",1147"working tree history\n";1148}1149if($_dry_run) {1150print"Remote Branch: ".$gs->refname."\n";1151print"SVN URL: ".$url."\n";1152return;1153}1154if(command(qw/diff-index HEAD --/)) {1155print STDERR "Cannot rebase with uncommited changes:\n";1156 command_noisy('status');1157exit1;1158}1159unless($_local) {1160# rebase will checkout for us, so no need to do it explicitly1161$_no_checkout='true';1162$_fetch_all?$gs->fetch_all:$gs->fetch;1163}1164 command_noisy(rebase_cmd(),$gs->refname);1165if(auto_create_empty_directories($gs)) {1166$gs->mkemptydirs;1167}1168}11691170sub cmd_show_ignore {1171my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1172$gs||= Git::SVN->new;1173my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1174$gs->prop_walk($gs->{path},$r,sub{1175my($gs,$path,$props) =@_;1176print STDOUT "\n#$path\n";1177my$s=$props->{'svn:ignore'}orreturn;1178$s=~s/[\r\n]+/\n/g;1179$s=~s/^\n+//;1180chomp$s;1181$s=~ s#^#$path#gm;1182print STDOUT "$s\n";1183});1184}11851186sub cmd_show_externals {1187my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1188$gs||= Git::SVN->new;1189my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1190$gs->prop_walk($gs->{path},$r,sub{1191my($gs,$path,$props) =@_;1192print STDOUT "\n#$path\n";1193my$s=$props->{'svn:externals'}orreturn;1194$s=~s/[\r\n]+/\n/g;1195chomp$s;1196$s=~ s#^#$path#gm;1197print STDOUT "$s\n";1198});1199}12001201sub cmd_create_ignore {1202my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1203$gs||= Git::SVN->new;1204my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1205$gs->prop_walk($gs->{path},$r,sub{1206my($gs,$path,$props) =@_;1207# $path is of the form /path/to/dir/1208$path='.'.$path;1209# SVN can have attributes on empty directories,1210# which git won't track1211 mkpath([$path])unless-d $path;1212my$ignore=$path.'.gitignore';1213my$s=$props->{'svn:ignore'}orreturn;1214open(GITIGNORE,'>',$ignore)1215or fatal("Failed to open `$ignore' for writing:$!");1216$s=~s/[\r\n]+/\n/g;1217$s=~s/^\n+//;1218chomp$s;1219# Prefix all patterns so that the ignore doesn't apply1220# to sub-directories.1221$s=~ s#^#/#gm;1222print GITIGNORE "$s\n";1223close(GITIGNORE)1224or fatal("Failed to close `$ignore':$!");1225 command_noisy('add','-f',$ignore);1226});1227}12281229sub cmd_mkdirs {1230my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1231$gs||= Git::SVN->new;1232$gs->mkemptydirs($_revision);1233}12341235sub canonicalize_path {1236my($path) =@_;1237my$dot_slash_added=0;1238if(substr($path,0,1)ne"/") {1239$path="./".$path;1240$dot_slash_added=1;1241}1242# File::Spec->canonpath doesn't collapse x/../y into y (for a1243# good reason), so let's do this manually.1244$path=~ s#/+#/#g;1245$path=~ s#/\.(?:/|$)#/#g;1246$path=~ s#/[^/]+/\.\.##g;1247$path=~ s#/$##g;1248$path=~ s#^\./## if $dot_slash_added;1249$path=~ s#^/##;1250$path=~ s#^\.$##;1251return$path;1252}12531254sub canonicalize_url {1255my($url) =@_;1256$url=~ s#^([^:]+://[^/]*/)(.*)$#$1 . canonicalize_path($2)#e;1257return$url;1258}12591260# get_svnprops(PATH)1261# ------------------1262# Helper for cmd_propget and cmd_proplist below.1263sub get_svnprops {1264my$path=shift;1265my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1266$gs||= Git::SVN->new;12671268# prefix THE PATH by the sub-directory from which the user1269# invoked us.1270$path=$cmd_dir_prefix.$path;1271 fatal("No such file or directory:$path")unless-e $path;1272my$is_dir= -d $path?1:0;1273$path=$gs->{path} .'/'.$path;12741275# canonicalize the path (otherwise libsvn will abort or fail to1276# find the file)1277$path= canonicalize_path($path);12781279my$r= (defined$_revision?$_revision:$gs->ra->get_latest_revnum);1280my$props;1281if($is_dir) {1282(undef,undef,$props) =$gs->ra->get_dir($path,$r);1283}1284else{1285(undef,$props) =$gs->ra->get_file($path,$r,undef);1286}1287return$props;1288}12891290# cmd_propget (PROP, PATH)1291# ------------------------1292# Print the SVN property PROP for PATH.1293sub cmd_propget {1294my($prop,$path) =@_;1295$path='.'ifnot defined$path;1296 usage(1)ifnot defined$prop;1297my$props= get_svnprops($path);1298if(not defined$props->{$prop}) {1299 fatal("`$path' does not have a `$prop' SVN property.");1300}1301print$props->{$prop} ."\n";1302}13031304# cmd_proplist (PATH)1305# -------------------1306# Print the list of SVN properties for PATH.1307sub cmd_proplist {1308my$path=shift;1309$path='.'ifnot defined$path;1310my$props= get_svnprops($path);1311print"Properties on '$path':\n";1312foreach(sort keys%{$props}) {1313print"$_\n";1314}1315}13161317sub cmd_multi_init {1318my$url=shift;1319unless(defined$_trunk||@_branches||@_tags) {1320 usage(1);1321}13221323$_prefix=''unlessdefined$_prefix;1324if(defined$url) {1325$url= canonicalize_url($url);1326 init_subdir(@_);1327}1328 do_git_init_db();1329if(defined$_trunk) {1330$_trunk=~ s#^/+##;1331my$trunk_ref='refs/remotes/'.$_prefix.'trunk';1332# try both old-style and new-style lookups:1333my$gs_trunk=eval{ Git::SVN->new($trunk_ref) };1334unless($gs_trunk) {1335my($trunk_url,$trunk_path) =1336 complete_svn_url($url,$_trunk);1337$gs_trunk= Git::SVN->init($trunk_url,$trunk_path,1338undef,$trunk_ref);1339}1340}1341return unless@_branches||@_tags;1342my$ra=$url? Git::SVN::Ra->new($url) :undef;1343foreachmy$path(@_branches) {1344 complete_url_ls_init($ra,$path,'--branches/-b',$_prefix);1345}1346foreachmy$path(@_tags) {1347 complete_url_ls_init($ra,$path,'--tags/-t',$_prefix.'tags/');1348}1349}13501351sub cmd_multi_fetch {1352$Git::SVN::no_reuse_existing =undef;1353my$remotes= Git::SVN::read_all_remotes();1354foreachmy$repo_id(sort keys%$remotes) {1355if($remotes->{$repo_id}->{url}) {1356 Git::SVN::fetch_all($repo_id,$remotes);1357}1358}1359}13601361# this command is special because it requires no metadata1362sub cmd_commit_diff {1363my($ta,$tb,$url) =@_;1364my$usage="Usage:$0commit-diff -r<revision> ".1365"<tree-ish> <tree-ish> [<URL>]";1366 fatal($usage)if(!defined$ta|| !defined$tb);1367my$svn_path='';1368if(!defined$url) {1369my$gs=eval{ Git::SVN->new};1370if(!$gs) {1371 fatal("Needed URL or usable git-svn --id in ",1372"the command-line\n",$usage);1373}1374$url=$gs->{url};1375$svn_path=$gs->{path};1376}1377unless(defined$_revision) {1378 fatal("-r|--revision is a required argument\n",$usage);1379}1380if(defined$_message&&defined$_file) {1381 fatal("Both --message/-m and --file/-F specified ",1382"for the commit message.\n",1383"I have no idea what you mean");1384}1385if(defined$_file) {1386$_message= file_to_s($_file);1387}else{1388$_message||= get_commit_entry($tb)->{log};1389}1390my$ra||= Git::SVN::Ra->new($url);1391my$r=$_revision;1392if($req'HEAD') {1393$r=$ra->get_latest_revnum;1394}elsif($r!~/^\d+$/) {1395die"revision argument:$rnot understood by git-svn\n";1396}1397my%ed_opts= ( r =>$r,1398log=>$_message,1399 ra =>$ra,1400 tree_a =>$ta,1401 tree_b =>$tb,1402 editor_cb =>sub{print"Committed r$_[0]\n"},1403 svn_path =>$svn_path);1404if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {1405print"No changes\n$ta==$tb\n";1406}1407}14081409sub escape_uri_only {1410my($uri) =@_;1411my@tmp;1412foreach(splitm{/},$uri) {1413s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;1414push@tmp,$_;1415}1416join('/',@tmp);1417}14181419sub escape_url {1420my($url) =@_;1421if($url=~ m#^([^:]+)://([^/]*)(.*)$#) {1422my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));1423$url="$scheme://$domain$uri";1424}1425$url;1426}14271428sub cmd_info {1429my$path= canonicalize_path(defined($_[0]) ?$_[0] :".");1430my$fullpath= canonicalize_path($cmd_dir_prefix.$path);1431if(exists$_[1]) {1432die"Too many arguments specified\n";1433}14341435my($file_type,$diff_status) = find_file_type_and_diff_status($path);14361437if(!$file_type&& !$diff_status) {1438print STDERR "svn: '$path' is not under version control\n";1439exit1;1440}14411442my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1443unless($gs) {1444die"Unable to determine upstream SVN information from ",1445"working tree history\n";1446}14471448# canonicalize_path() will return "" to make libsvn 1.5.x happy,1449$path="."if$patheq"";14501451my$full_url=$url. ($fullpatheq""?"":"/$fullpath");14521453if($_url) {1454print escape_url($full_url),"\n";1455return;1456}14571458my$result="Path:$path\n";1459$result.="Name: ". basename($path) ."\n"if$file_typene"dir";1460$result.="URL: ". escape_url($full_url) ."\n";14611462eval{1463my$repos_root=$gs->repos_root;1464 Git::SVN::remove_username($repos_root);1465$result.="Repository Root: ". escape_url($repos_root) ."\n";1466};1467if($@) {1468$result.="Repository Root: (offline)\n";1469}1470::_req_svn();1471$result.="Repository UUID:$uuid\n"unless$diff_statuseq"A"&&1472($SVN::Core::VERSION le'1.5.4'||$file_typene"dir");1473$result.="Revision: ". ($diff_statuseq"A"?0:$rev) ."\n";14741475$result.="Node Kind: ".1476($file_typeeq"dir"?"directory":"file") ."\n";14771478my$schedule=$diff_statuseq"A"1479?"add"1480: ($diff_statuseq"D"?"delete":"normal");1481$result.="Schedule:$schedule\n";14821483if($diff_statuseq"A") {1484print$result,"\n";1485return;1486}14871488my($lc_author,$lc_rev,$lc_date_utc);1489my@args= Git::SVN::Log::git_svn_log_cmd($rev,$rev,"--",$fullpath);1490my$log= command_output_pipe(@args);1491my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;1492while(<$log>) {1493if(/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {1494$lc_author=$1;1495$lc_date_utc= Git::SVN::Log::parse_git_date($2,$3);1496}elsif(/^${esc_color} (git-svn-id:.+)$/o) {1497(undef,$lc_rev,undef) = ::extract_metadata($1);1498}1499}1500close$log;15011502 Git::SVN::Log::set_local_timezone();15031504$result.="Last Changed Author:$lc_author\n";1505$result.="Last Changed Rev:$lc_rev\n";1506$result.="Last Changed Date: ".1507 Git::SVN::Log::format_svn_date($lc_date_utc) ."\n";15081509if($file_typene"dir") {1510my$text_last_updated_date=1511($diff_statuseq"D"?$lc_date_utc: (stat$path)[9]);1512$result.=1513"Text Last Updated: ".1514 Git::SVN::Log::format_svn_date($text_last_updated_date) .1515"\n";1516my$checksum;1517if($diff_statuseq"D") {1518my($fh,$ctx) =1519 command_output_pipe(qw(cat-file blob),"HEAD:$path");1520if($file_typeeq"link") {1521my$file_name= <$fh>;1522$checksum= md5sum("link$file_name");1523}else{1524$checksum= md5sum($fh);1525}1526 command_close_pipe($fh,$ctx);1527}elsif($file_typeeq"link") {1528my$file_name=1529 command(qw(cat-file blob),"HEAD:$path");1530$checksum=1531 md5sum("link ".$file_name);1532}else{1533open FILE,"<",$pathor die$!;1534$checksum= md5sum(\*FILE);1535close FILE or die$!;1536}1537$result.="Checksum: ".$checksum."\n";1538}15391540print$result,"\n";1541}15421543sub cmd_reset {1544my$target=shift||$_revisionor die"SVN revision required\n";1545$target=$1if$target=~/^r(\d+)$/;1546$target=~/^\d+$/or die"Numeric SVN revision expected\n";1547my($url,$rev,$uuid,$gs) = working_head_info('HEAD');1548unless($gs) {1549die"Unable to determine upstream SVN information from ".1550"history\n";1551}1552my($r,$c) =$gs->find_rev_before($target,not$_fetch_parent);1553die"Cannot find SVN revision$target\n"unlessdefined($c);1554$gs->rev_map_set($r,$c,'reset',$uuid);1555print"r$r=$c($gs->{ref_id})\n";1556}15571558sub cmd_gc {1559if(!$can_compress) {1560warn"Compress::Zlib could not be found; unhandled.log ".1561"files will not be compressed.\n";1562}1563 find({ wanted => \&gc_directory, no_chdir =>1},"$ENV{GIT_DIR}/svn");1564}15651566########################### utility functions #########################15671568sub rebase_cmd {1569my@cmd= qw/rebase/;1570push@cmd,'-v'if$_verbose;1571push@cmd, qw/--merge/if$_merge;1572push@cmd,"--strategy=$_strategy"if$_strategy;1573@cmd;1574}15751576sub post_fetch_checkout {1577return if$_no_checkout;1578my$gs=$Git::SVN::_head orreturn;1579return if verify_ref('refs/heads/master^0');15801581# look for "trunk" ref if it exists1582my$remote= Git::SVN::read_all_remotes()->{$gs->{repo_id}};1583my$fetch=$remote->{fetch};1584if($fetch) {1585foreachmy$p(keys%$fetch) {1586 basename($fetch->{$p})eq'trunk'ornext;1587$gs= Git::SVN->new($fetch->{$p},$gs->{repo_id},$p);1588last;1589}1590}15911592my$valid_head= verify_ref('HEAD^0');1593 command_noisy(qw(update-ref refs/heads/master),$gs->refname);1594return if($valid_head|| !verify_ref('HEAD^0'));15951596return if$ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;1597my$index=$ENV{GIT_INDEX_FILE} ||"$ENV{GIT_DIR}/index";1598return if-f $index;15991600return if command_oneline(qw/rev-parse --is-inside-work-tree/)eq'false';1601return if command_oneline(qw/rev-parse --is-inside-git-dir/)eq'true';1602 command_noisy(qw/read-tree -m -u -v HEAD HEAD/);1603print STDERR "Checked out HEAD:\n",1604$gs->full_url," r",$gs->last_rev,"\n";1605if(auto_create_empty_directories($gs)) {1606$gs->mkemptydirs($gs->last_rev);1607}1608}16091610sub complete_svn_url {1611my($url,$path) =@_;1612$path=~ s#/+$##;1613if($path!~ m#^[a-z\+]+://#) {1614if(!defined$url||$url!~ m#^[a-z\+]+://#) {1615 fatal("E: '$path' is not a complete URL ",1616"and a separate URL is not specified");1617}1618return($url,$path);1619}1620return($path,'');1621}16221623sub complete_url_ls_init {1624my($ra,$repo_path,$switch,$pfx) =@_;1625unless($repo_path) {1626print STDERR "W:$switchnot specified\n";1627return;1628}1629$repo_path=~ s#/+$##;1630if($repo_path=~ m#^[a-z\+]+://#) {1631$ra= Git::SVN::Ra->new($repo_path);1632$repo_path='';1633}else{1634$repo_path=~ s#^/+##;1635unless($ra) {1636 fatal("E: '$repo_path' is not a complete URL ",1637"and a separate URL is not specified");1638}1639}1640my$url=$ra->{url};1641my$gs= Git::SVN->init($url,undef,undef,undef,1);1642my$k="svn-remote.$gs->{repo_id}.url";1643my$orig_url=eval{ command_oneline(qw/config --get/,$k) };1644if($orig_url&& ($orig_urlne$gs->{url})) {1645die"$kalready set:$orig_url\n",1646"wanted to set to:$gs->{url}\n";1647}1648 command_oneline('config',$k,$gs->{url})unless$orig_url;1649my$remote_path="$gs->{path}/$repo_path";1650$remote_path=~s{%([0-9A-F]{2})}{chr hex($1)}ieg;1651$remote_path=~ s#/+#/#g;1652$remote_path=~ s#^/##g;1653$remote_path.="/*"if$remote_path!~ /\*/;1654my($n) = ($switch=~/^--(\w+)/);1655if(length$pfx&&$pfx!~ m#/$#) {1656die"--prefix='$pfx' must have a trailing slash '/'\n";1657}1658 command_noisy('config',1659'--add',1660"svn-remote.$gs->{repo_id}.$n",1661"$remote_path:refs/remotes/$pfx*".1662('/*' x (($remote_path=~ tr/*/*/) -1)) );1663}16641665sub verify_ref {1666my($ref) =@_;1667eval{ command_oneline(['rev-parse','--verify',$ref],1668{ STDERR =>0}); };1669}16701671sub get_tree_from_treeish {1672my($treeish) =@_;1673# $treeish can be a symbolic ref, too:1674my$type= command_oneline(qw/cat-file -t/,$treeish);1675my$expected;1676while($typeeq'tag') {1677($treeish,$type) = command(qw/cat-file tag/,$treeish);1678}1679if($typeeq'commit') {1680$expected= (grep/^tree /, command(qw/cat-file commit/,1681$treeish))[0];1682($expected) = ($expected=~/^tree ($sha1)$/o);1683die"Unable to get tree from$treeish\n"unless$expected;1684}elsif($typeeq'tree') {1685$expected=$treeish;1686}else{1687die"$treeishis a$type, expected tree, tag or commit\n";1688}1689return$expected;1690}16911692sub get_commit_entry {1693my($treeish) =shift;1694my%log_entry= (log=>'', tree => get_tree_from_treeish($treeish) );1695my$commit_editmsg="$ENV{GIT_DIR}/COMMIT_EDITMSG";1696my$commit_msg="$ENV{GIT_DIR}/COMMIT_MSG";1697open my$log_fh,'>',$commit_editmsgor croak $!;16981699my$type= command_oneline(qw/cat-file -t/,$treeish);1700if($typeeq'commit'||$typeeq'tag') {1701my($msg_fh,$ctx) = command_output_pipe('cat-file',1702$type,$treeish);1703my$in_msg=0;1704my$author;1705my$saw_from=0;1706my$msgbuf="";1707while(<$msg_fh>) {1708if(!$in_msg) {1709$in_msg=1if(/^\s*$/);1710$author=$1if(/^author (.*>)/);1711}elsif(/^git-svn-id: /) {1712# skip this for now, we regenerate the1713# correct one on re-fetch anyways1714# TODO: set *:merge properties or like...1715}else{1716if(/^From:/||/^Signed-off-by:/) {1717$saw_from=1;1718}1719$msgbuf.=$_;1720}1721}1722$msgbuf=~s/\s+$//s;1723if($Git::SVN::_add_author_from &&defined($author)1724&& !$saw_from) {1725$msgbuf.="\n\nFrom:$author";1726}1727print$log_fh $msgbufor croak $!;1728 command_close_pipe($msg_fh,$ctx);1729}1730close$log_fhor croak $!;17311732if($_edit|| ($typeeq'tree')) {1733chomp(my$editor= command_oneline(qw(var GIT_EDITOR)));1734system('sh','-c',$editor.' "$@"',$editor,$commit_editmsg);1735}1736rename$commit_editmsg,$commit_msgor croak $!;1737{1738require Encode;1739# SVN requires messages to be UTF-8 when entering the repo1740local$/;1741open$log_fh,'<',$commit_msgor croak $!;1742binmode$log_fh;1743chomp($log_entry{log} = <$log_fh>);17441745my$enc= Git::config('i18n.commitencoding') ||'UTF-8';1746my$msg=$log_entry{log};17471748eval{$msg= Encode::decode($enc,$msg,1) };1749if($@) {1750die"Could not decode as$enc:\n",$msg,1751"\nPerhaps you need to set i18n.commitencoding\n";1752}17531754eval{$msg= Encode::encode('UTF-8',$msg,1) };1755die"Could not encode as UTF-8:\n$msg\n"if$@;17561757$log_entry{log} =$msg;17581759close$log_fhor croak $!;1760}1761unlink$commit_msg;1762 \%log_entry;1763}17641765sub s_to_file {1766my($str,$file,$mode) =@_;1767open my$fd,'>',$fileor croak $!;1768print$fd $str,"\n"or croak $!;1769close$fdor croak $!;1770chmod($mode&~umask,$file)if(defined$mode);1771}17721773sub file_to_s {1774my$file=shift;1775open my$fd,'<',$fileor croak "$!: file:$file\n";1776local$/;1777my$ret= <$fd>;1778close$fdor croak $!;1779$ret=~s/\s*$//s;1780return$ret;1781}17821783# '<svn username> = real-name <email address>' mapping based on git-svnimport:1784sub load_authors {1785open my$authors,'<',$_authorsor die"Can't open$_authors$!\n";1786my$log=$cmdeq'log';1787while(<$authors>) {1788chomp;1789next unless/^(.+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/;1790my($user,$name,$email) = ($1,$2,$3);1791if($log) {1792$Git::SVN::Log::rusers{"$name<$email>"} =$user;1793}else{1794$users{$user} = [$name,$email];1795}1796}1797close$authorsor croak $!;1798}17991800# convert GetOpt::Long specs for use by git-config1801sub read_git_config {1802my$opts=shift;1803my@config_only;1804foreachmy$o(keys%$opts) {1805# if we have mixedCase and a long option-only, then1806# it's a config-only variable that we don't need for1807# the command-line.1808push@config_only,$oif($o=~/[A-Z]/&&$o=~/^[a-z]+$/i);1809my$v=$opts->{$o};1810my($key) = ($o=~/^([a-zA-Z\-]+)/);1811$key=~s/-//g;1812my$arg='git config';1813$arg.=' --int'if($o=~/[:=]i$/);1814$arg.=' --bool'if($o!~/[:=][sfi]$/);1815if(ref$veq'ARRAY') {1816chomp(my@tmp=`$arg--get-all svn.$key`);1817@$v=@tmpif@tmp;1818 } else {1819 chomp(my$tmp= `$arg--get svn.$key`);1820if($tmp&& !($arg=~/ --bool/&&$tmpeq'false')) {1821$$v=$tmp;1822}1823}1824}1825delete@$opts{@config_only}if@config_only;1826}18271828sub extract_metadata {1829my$id=shift orreturn(undef,undef,undef);1830my($url,$rev,$uuid) = ($id=~ /^\s*git-svn-id:\s+(.*)\@(\d+)1831 \s([a-f\d\-]+)$/ix);1832if(!defined$rev|| !$uuid|| !$url) {1833# some of the original repositories I made had1834# identifiers like this:1835($rev,$uuid) = ($id=~/^\s*git-svn-id:\s(\d+)\@([a-f\d\-]+)/i);1836}1837return($url,$rev,$uuid);1838}18391840sub cmt_metadata {1841return extract_metadata((grep(/^git-svn-id: /,1842 command(qw/cat-file commit/,shift)))[-1]);1843}18441845sub cmt_sha2rev_batch {1846my%s2r;1847my($pid,$in,$out,$ctx) = command_bidi_pipe(qw/cat-file --batch/);1848my$list=shift;18491850foreachmy$sha(@{$list}) {1851my$first=1;1852my$size=0;1853print$out $sha,"\n";18541855while(my$line= <$in>) {1856if($first&&$line=~/^[[:xdigit:]]{40}\smissing$/) {1857last;1858}elsif($first&&1859$line=~/^[[:xdigit:]]{40}\scommit\s(\d+)$/) {1860$first=0;1861$size=$1;1862next;1863}elsif($line=~/^(git-svn-id: )/) {1864my(undef,$rev,undef) =1865 extract_metadata($line);1866$s2r{$sha} =$rev;1867}18681869$size-=length($line);1870last if($size==0);1871}1872}18731874 command_close_bidi_pipe($pid,$in,$out,$ctx);18751876return \%s2r;1877}18781879sub working_head_info {1880my($head,$refs) =@_;1881my@args= qw/rev-list --first-parent --pretty=medium/;1882my($fh,$ctx) = command_output_pipe(@args,$head);1883my$hash;1884my%max;1885while(<$fh>) {1886if(m{^commit ($::sha1)$}) {1887unshift@$refs,$hashif$hashand$refs;1888$hash=$1;1889next;1890}1891next unlesss{^\s*(git-svn-id:)}{$1};1892my($url,$rev,$uuid) = extract_metadata($_);1893if(defined$url&&defined$rev) {1894next if$max{$url}and$max{$url} <$rev;1895if(my$gs= Git::SVN->find_by_url($url)) {1896my$c=$gs->rev_map_get($rev,$uuid);1897if($c&&$ceq$hash) {1898close$fh;# break the pipe1899return($url,$rev,$uuid,$gs);1900}else{1901$max{$url} ||=$gs->rev_map_max;1902}1903}1904}1905}1906 command_close_pipe($fh,$ctx);1907(undef,undef,undef,undef);1908}19091910sub read_commit_parents {1911my($parents,$c) =@_;1912chomp(my$p= command_oneline(qw/rev-list --parents -1/,$c));1913$p=~s/^($c)\s*//or die"rev-list --parents -1$cfailed!\n";1914@{$parents->{$c}} =split(/ /,$p);1915}19161917sub linearize_history {1918my($gs,$refs) =@_;1919my%parents;1920foreachmy$c(@$refs) {1921 read_commit_parents(\%parents,$c);1922}19231924my@linear_refs;1925my%skip= ();1926my$last_svn_commit=$gs->last_commit;1927foreachmy$c(reverse@$refs) {1928next if$ceq$last_svn_commit;1929last if$skip{$c};19301931unshift@linear_refs,$c;1932$skip{$c} =1;19331934# we only want the first parent to diff against for linear1935# history, we save the rest to inject when we finalize the1936# svn commit1937my$fp_a= verify_ref("$c~1");1938my$fp_b=shift@{$parents{$c}}if$parents{$c};1939if(!$fp_a|| !$fp_b) {1940die"Commit$c\n",1941"has no parent commit, and therefore ",1942"nothing to diff against.\n",1943"You should be working from a repository ",1944"originally created by git-svn\n";1945}1946if($fp_ane$fp_b) {1947die"$c~1=$fp_a, however parsing commit$c",1948"revealed that:\n$c~1=$fp_b\nBUG!\n";1949}19501951foreachmy$p(@{$parents{$c}}) {1952$skip{$p} =1;1953}1954}1955(\@linear_refs, \%parents);1956}19571958sub find_file_type_and_diff_status {1959my($path) =@_;1960return('dir','')if$patheq'';19611962my$diff_output=1963 command_oneline(qw(diff --cached --name-status --),$path) ||"";1964my$diff_status= (split(' ',$diff_output))[0] ||"";19651966my$ls_tree= command_oneline(qw(ls-tree HEAD),$path) ||"";19671968return(undef,undef)if!$diff_status&& !$ls_tree;19691970if($diff_statuseq"A") {1971return("link",$diff_status)if-l $path;1972return("dir",$diff_status)if-d $path;1973return("file",$diff_status);1974}19751976my$mode= (split(' ',$ls_tree))[0] ||"";19771978return("link",$diff_status)if$modeeq"120000";1979return("dir",$diff_status)if$modeeq"040000";1980return("file",$diff_status);1981}19821983sub md5sum {1984my$arg=shift;1985my$ref=ref$arg;1986my$md5= Digest::MD5->new();1987if($refeq'GLOB'||$refeq'IO::File'||$refeq'File::Temp') {1988$md5->addfile($arg)or croak $!;1989}elsif($refeq'SCALAR') {1990$md5->add($$arg)or croak $!;1991}elsif(!$ref) {1992$md5->add($arg)or croak $!;1993}else{1994::fatal "Can't provide MD5 hash for unknown ref type: '",$ref,"'";1995}1996return$md5->hexdigest();1997}19981999sub gc_directory {2000if($can_compress&& -f $_&& basename($_)eq"unhandled.log") {2001my$out_filename=$_.".gz";2002open my$in_fh,"<",$_or die"Unable to open$_:$!\n";2003binmode$in_fh;2004my$gz= Compress::Zlib::gzopen($out_filename,"ab")or2005die"Unable to open$out_filename:$!\n";20062007my$res;2008while($res=sysread($in_fh,my$str,1024)) {2009$gz->gzwrite($str)or2010die"Unable to write: ".$gz->gzerror()."!\n";2011}2012unlink$_or die"unlink$File::Find::name:$!\n";2013}elsif(-f $_&& basename($_)eq"index") {2014unlink$_or die"unlink$_:$!\n";2015}2016}20172018package Git::SVN;2019use strict;2020use warnings;2021use Fcntl qw/:DEFAULT :seek/;2022useconstant rev_map_fmt =>'NH40';2023use vars qw/$default_repo_id $default_ref_id $_no_metadata $_follow_parent2024$_repack $_repack_flags $_use_svm_props $_head2025$_use_svnsync_props $no_reuse_existing $_minimize_url2026$_use_log_author $_add_author_from $_localtime/;2027use Carp qw/croak/;2028use File::Path qw/mkpath/;2029use File::Copy qw/copy/;2030use IPC::Open3;2031use Time::Local;2032use Memoize;# core since 5.8.0, Jul 20022033use Memoize::Storable;20342035my($_gc_nr,$_gc_period);20362037# properties that we do not log:2038my%SKIP_PROP;2039BEGIN{2040%SKIP_PROP=map{$_=>1} qw/svn:wc:ra_dav:version-url2041 svn:special svn:executable2042 svn:entry:committed-rev2043 svn:entry:last-author2044 svn:entry:uuid2045 svn:entry:committed-date/;20462047# some options are read globally, but can be overridden locally2048# per [svn-remote "..."] section. Command-line options will *NOT*2049# override options set in an [svn-remote "..."] section2050no strict 'refs';2051formy$option(qw/follow_parent no_metadata use_svm_props2052 use_svnsync_props/) {2053my$key=$option;2054$key=~tr/_//d;2055my$prop="-$option";2056*$option=sub{2057my($self) =@_;2058return$self->{$prop}ifexists$self->{$prop};2059my$k="svn-remote.$self->{repo_id}.$key";2060eval{ command_oneline(qw/config --get/,$k) };2061if($@) {2062$self->{$prop} = ${"Git::SVN::_$option"};2063}else{2064my$v= command_oneline(qw/config --bool/,$k);2065$self->{$prop} =$veq'false'?0:1;2066}2067return$self->{$prop};2068}2069}2070}207120722073my(%LOCKFILES,%INDEX_FILES);2074END{2075unlink keys%LOCKFILESif%LOCKFILES;2076unlink keys%INDEX_FILESif%INDEX_FILES;2077}20782079sub resolve_local_globs {2080my($url,$fetch,$glob_spec) =@_;2081return unlessdefined$glob_spec;2082my$ref=$glob_spec->{ref};2083my$path=$glob_spec->{path};2084foreach(command(qw#for-each-ref --format=%(refname) refs/#)) {2085next unless m#^$ref->{regex}$#;2086my$p=$1;2087my$pathname= desanitize_refname($path->full_path($p));2088my$refname= desanitize_refname($ref->full_path($p));2089if(my$existing=$fetch->{$pathname}) {2090if($existingne$refname) {2091die"Refspec conflict:\n",2092"existing:$existing\n",2093" globbed:$refname\n";2094}2095my$u= (::cmt_metadata("$refname"))[0];2096$u=~s!^\Q$url\E(/|$)!!or die2097"$refname: '$url' not found in '$u'\n";2098if($pathnamene$u) {2099warn"W: Refspec glob conflict ",2100"(ref:$refname):\n",2101"expected path:$pathname\n",2102" real path:$u\n",2103"Continuing ahead with$u\n";2104next;2105}2106}else{2107$fetch->{$pathname} =$refname;2108}2109}2110}21112112sub parse_revision_argument {2113my($base,$head) =@_;2114if(!defined$::_revision || $::_revision eq'BASE:HEAD') {2115return($base,$head);2116}2117return($1,$2)if($::_revision =~/^(\d+):(\d+)$/);2118return($::_revision, $::_revision)if($::_revision =~/^\d+$/);2119return($head,$head)if($::_revision eq'HEAD');2120return($base,$1)if($::_revision =~/^BASE:(\d+)$/);2121return($1,$head)if($::_revision =~/^(\d+):HEAD$/);2122die"revision argument: $::_revision not understood by git-svn\n";2123}21242125sub fetch_all {2126my($repo_id,$remotes) =@_;2127if(ref$repo_id) {2128my$gs=$repo_id;2129$repo_id=undef;2130$repo_id=$gs->{repo_id};2131}2132$remotes||= read_all_remotes();2133my$remote=$remotes->{$repo_id}or2134die"[svn-remote\"$repo_id\"] unknown\n";2135my$fetch=$remote->{fetch};2136my$url=$remote->{url}or die"svn-remote.$repo_id.url not defined\n";2137my(@gs,@globs);2138my$ra= Git::SVN::Ra->new($url);2139my$uuid=$ra->get_uuid;2140my$head=$ra->get_latest_revnum;21412142# ignore errors, $head revision may not even exist anymore2143eval{$ra->get_log("",$head,0,1,0,1,sub{$head=$_[1] }) };2144warn"W:$@\n"if$@;21452146my$base=defined$fetch?$head:0;21472148# read the max revs for wildcard expansion (branches/*, tags/*)2149foreachmy$t(qw/branches tags/) {2150defined$remote->{$t}ornext;2151push@globs, @{$remote->{$t}};21522153my$max_rev=eval{ tmp_config(qw/--int --get/,2154"svn-remote.$repo_id.${t}-maxRev") };2155if(defined$max_rev&& ($max_rev<$base)) {2156$base=$max_rev;2157}elsif(!defined$max_rev) {2158$base=0;2159}2160}21612162if($fetch) {2163foreachmy$p(sort keys%$fetch) {2164my$gs= Git::SVN->new($fetch->{$p},$repo_id,$p);2165my$lr=$gs->rev_map_max;2166if(defined$lr) {2167$base=$lrif($lr<$base);2168}2169push@gs,$gs;2170}2171}21722173($base,$head) = parse_revision_argument($base,$head);2174$ra->gs_fetch_loop_common($base,$head, \@gs, \@globs);2175}21762177sub read_all_remotes {2178my$r= {};2179my$use_svm_props=eval{ command_oneline(qw/config --bool2180 svn.useSvmProps/) };2181$use_svm_props=$use_svm_propseq'true'if$use_svm_props;2182my$svn_refspec=qr{\s*(.*?)\s*:\s*(.+?)\s*};2183foreach(grep{s/^svn-remote\.//} command(qw/config -l/)) {2184if(m!^(.+)\.fetch=$svn_refspec$!) {2185my($remote,$local_ref,$remote_ref) = ($1,$2,$3);2186die("svn-remote.$remote: remote ref '$remote_ref' "2187."must start with 'refs/'\n")2188unless$remote_ref=~m{^refs/};2189$local_ref= uri_decode($local_ref);2190$r->{$remote}->{fetch}->{$local_ref} =$remote_ref;2191$r->{$remote}->{svm} = {}if$use_svm_props;2192}elsif(m!^(.+)\.usesvmprops=\s*(.*)\s*$!) {2193$r->{$1}->{svm} = {};2194}elsif(m!^(.+)\.url=\s*(.*)\s*$!) {2195$r->{$1}->{url} =$2;2196}elsif(m!^(.+)\.pushurl=\s*(.*)\s*$!) {2197$r->{$1}->{pushurl} =$2;2198}elsif(m!^(.+)\.ignore-refs=\s*(.*)\s*$!) {2199$r->{$1}->{ignore_refs_regex} =$2;2200}elsif(m!^(.+)\.(branches|tags)=$svn_refspec$!) {2201my($remote,$t,$local_ref,$remote_ref) =2202($1,$2,$3,$4);2203die("svn-remote.$remote: remote ref '$remote_ref' ($t) "2204."must start with 'refs/'\n")2205unless$remote_ref=~m{^refs/};2206$local_ref= uri_decode($local_ref);2207my$rs= {2208 t =>$t,2209 remote =>$remote,2210 path => Git::SVN::GlobSpec->new($local_ref,1),2211ref=> Git::SVN::GlobSpec->new($remote_ref,0) };2212if(length($rs->{ref}->{right}) !=0) {2213die"The '*' glob character must be the last ",2214"character of '$remote_ref'\n";2215}2216push@{$r->{$remote}->{$t} },$rs;2217}2218}22192220map{2221if(defined$r->{$_}->{svm}) {2222my$svm;2223eval{2224my$section="svn-remote.$_";2225$svm= {2226 source => tmp_config('--get',2227"$section.svm-source"),2228 replace => tmp_config('--get',2229"$section.svm-replace"),2230}2231};2232$r->{$_}->{svm} =$svm;2233}2234}keys%$r;22352236foreachmy$remote(keys%$r) {2237foreach(grep{defined$_}2238map{$r->{$remote}->{$_} }qw(branches tags)) {2239foreachmy$rs(@$_) {2240$rs->{ignore_refs_regex} =2241$r->{$remote}->{ignore_refs_regex};2242}2243}2244}22452246$r;2247}22482249sub init_vars {2250$_gc_nr=$_gc_period=1000;2251if(defined$_repack||defined$_repack_flags) {2252warn"Repack options are obsolete; they have no effect.\n";2253}2254}22552256sub verify_remotes_sanity {2257return unless-d $ENV{GIT_DIR};2258my%seen;2259foreach(command(qw/config -l/)) {2260if(m!^svn-remote\.(?:.+)\.fetch=.*:refs/remotes/(\S+)\s*$!) {2261if($seen{$1}) {2262die"Remote ref refs/remote/$1is tracked by",2263"\n \"$_\"\nand\n \"$seen{$1}\"\n",2264"Please resolve this ambiguity in ",2265"your git configuration file before ",2266"continuing\n";2267}2268$seen{$1} =$_;2269}2270}2271}22722273sub find_existing_remote {2274my($url,$remotes) =@_;2275returnundefif$no_reuse_existing;2276my$existing;2277foreachmy$repo_id(keys%$remotes) {2278my$u=$remotes->{$repo_id}->{url}ornext;2279next if$une$url;2280$existing=$repo_id;2281last;2282}2283$existing;2284}22852286sub init_remote_config {2287my($self,$url,$no_write) =@_;2288$url=~s!/+$!!;# strip trailing slash2289my$r= read_all_remotes();2290my$existing= find_existing_remote($url,$r);2291if($existing) {2292unless($no_write) {2293print STDERR "Using existing ",2294"[svn-remote\"$existing\"]\n";2295}2296$self->{repo_id} =$existing;2297}elsif($_minimize_url) {2298my$min_url= Git::SVN::Ra->new($url)->minimize_url;2299$existing= find_existing_remote($min_url,$r);2300if($existing) {2301unless($no_write) {2302print STDERR "Using existing ",2303"[svn-remote\"$existing\"]\n";2304}2305$self->{repo_id} =$existing;2306}2307if($min_urlne$url) {2308unless($no_write) {2309print STDERR "Using higher level of URL: ",2310"$url=>$min_url\n";2311}2312my$old_path=$self->{path};2313$self->{path} =$url;2314$self->{path} =~s!^\Q$min_url\E(/|$)!!;2315if(length$old_path) {2316$self->{path} .="/$old_path";2317}2318$url=$min_url;2319}2320}2321my$orig_url;2322if(!$existing) {2323# verify that we aren't overwriting anything:2324$orig_url=eval{2325 command_oneline('config','--get',2326"svn-remote.$self->{repo_id}.url")2327};2328if($orig_url&& ($orig_urlne$url)) {2329die"svn-remote.$self->{repo_id}.url already set: ",2330"$orig_url\nwanted to set to:$url\n";2331}2332}2333my($xrepo_id,$xpath) = find_ref($self->refname);2334if(!$no_write&&defined$xpath) {2335die"svn-remote.$xrepo_id.fetch already set to track ",2336"$xpath:",$self->refname,"\n";2337}2338unless($no_write) {2339 command_noisy('config',2340"svn-remote.$self->{repo_id}.url",$url);2341$self->{path} =~s{^/}{};2342$self->{path} =~s{%([0-9A-F]{2})}{chr hex($1)}ieg;2343 command_noisy('config','--add',2344"svn-remote.$self->{repo_id}.fetch",2345"$self->{path}:".$self->refname);2346}2347$self->{url} =$url;2348}23492350sub find_by_url {# repos_root and, path are optional2351my($class,$full_url,$repos_root,$path) =@_;23522353returnundefunlessdefined$full_url;2354 remove_username($full_url);2355 remove_username($repos_root)ifdefined$repos_root;2356my$remotes= read_all_remotes();2357if(defined$full_url&&defined$repos_root&& !defined$path) {2358$path=$full_url;2359$path=~ s#^\Q$repos_root\E(?:/|$)##;2360}2361foreachmy$repo_id(keys%$remotes) {2362my$u=$remotes->{$repo_id}->{url}ornext;2363 remove_username($u);2364next ifdefined$repos_root&&$repos_rootne$u;23652366my$fetch=$remotes->{$repo_id}->{fetch} || {};2367foreachmy$t(qw/branches tags/) {2368foreachmy$globspec(@{$remotes->{$repo_id}->{$t}}) {2369 resolve_local_globs($u,$fetch,$globspec);2370}2371}2372my$p=$path;2373my$rwr= rewrite_root({repo_id =>$repo_id});2374my$svm=$remotes->{$repo_id}->{svm}2375ifdefined$remotes->{$repo_id}->{svm};2376unless(defined$p) {2377$p=$full_url;2378my$z=$u;2379my$prefix='';2380if($rwr) {2381$z=$rwr;2382 remove_username($z);2383}elsif(defined$svm) {2384$z=$svm->{source};2385$prefix=$svm->{replace};2386$prefix=~ s#^\Q$u\E(?:/|$)##;2387$prefix=~ s#/$##;2388}2389$p=~ s#^\Q$z\E(?:/|$)#$prefix# or next;2390}2391foreachmy$f(keys%$fetch) {2392next if$fne$p;2393return Git::SVN->new($fetch->{$f},$repo_id,$f);2394}2395}2396undef;2397}23982399sub init {2400my($class,$url,$path,$repo_id,$ref_id,$no_write) =@_;2401my$self= _new($class,$repo_id,$ref_id,$path);2402if(defined$url) {2403$self->init_remote_config($url,$no_write);2404}2405$self;2406}24072408sub find_ref {2409my($ref_id) =@_;2410foreach(command(qw/config -l/)) {2411next unless m!^svn-remote\.(.+)\.fetch=2412 \s*(.*?)\s*:\s*(.+?)\s*$!x;2413my($repo_id,$path,$ref) = ($1,$2,$3);2414if($refeq$ref_id) {2415$path=''if($path=~ m#^\./?#);2416return($repo_id,$path);2417}2418}2419(undef,undef,undef);2420}24212422sub new {2423my($class,$ref_id,$repo_id,$path) =@_;2424if(defined$ref_id&& !defined$repo_id&& !defined$path) {2425($repo_id,$path) = find_ref($ref_id);2426if(!defined$repo_id) {2427die"Could not find a\"svn-remote.*.fetch\"key ",2428"in the repository configuration matching: ",2429"$ref_id\n";2430}2431}2432my$self= _new($class,$repo_id,$ref_id,$path);2433if(!defined$self->{path} || !length$self->{path}) {2434my$fetch= command_oneline('config','--get',2435"svn-remote.$repo_id.fetch",2436":$ref_id\$")or2437die"Failed to read\"svn-remote.$repo_id.fetch\"",2438"\":$ref_id\$\"in config\n";2439($self->{path},undef) =split(/\s*:\s*/,$fetch);2440}2441$self->{path} =~s{/+}{/}g;2442$self->{path} =~s{\A/}{};2443$self->{path} =~s{/\z}{};2444$self->{url} = command_oneline('config','--get',2445"svn-remote.$repo_id.url")or2446die"Failed to read\"svn-remote.$repo_id.url\"in config\n";2447$self->{pushurl} =eval{ command_oneline('config','--get',2448"svn-remote.$repo_id.pushurl") };2449$self->rebuild;2450$self;2451}24522453sub refname {2454my($refname) =$_[0]->{ref_id} ;24552456# It cannot end with a slash /, we'll throw up on this because2457# SVN can't have directories with a slash in their name, either:2458if($refname=~m{/$}) {2459die"ref: '$refname' ends with a trailing slash, this is ",2460"not permitted by git nor Subversion\n";2461}24622463# It cannot have ASCII control character space, tilde ~, caret ^,2464# colon :, question-mark ?, asterisk *, space, or open bracket [2465# anywhere.2466#2467# Additionally, % must be escaped because it is used for escaping2468# and we want our escaped refname to be reversible2469$refname=~s{([ \%~\^:\?\*\[\t])}{uc sprintf('%%%02x',ord($1))}eg;24702471# no slash-separated component can begin with a dot .2472# /.* becomes /%2E*2473$refname=~s{/\.}{/%2E}g;24742475# It cannot have two consecutive dots .. anywhere2476# .. becomes %2E%2E2477$refname=~s{\.\.}{%2E%2E}g;24782479# trailing dots and .lock are not allowed2480# .$ becomes %2E and .lock becomes %2Elock2481$refname=~s{\.(?=$|lock$)}{%2E};24822483# the sequence @{ is used to access the reflog2484# @{ becomes %40{2485$refname=~s{\@\{}{%40\{}g;24862487return$refname;2488}24892490sub desanitize_refname {2491my($refname) =@_;2492$refname=~s{%(?:([0-9A-F]{2}))}{chr hex($1)}eg;2493return$refname;2494}24952496sub svm_uuid {2497my($self) =@_;2498return$self->{svm}->{uuid}if$self->svm;2499$self->ra;2500unless($self->{svm}) {2501die"SVM UUID not cached, and reading remotely failed\n";2502}2503$self->{svm}->{uuid};2504}25052506sub svm {2507my($self) =@_;2508return$self->{svm}if$self->{svm};2509my$svm;2510# see if we have it in our config, first:2511eval{2512my$section="svn-remote.$self->{repo_id}";2513$svm= {2514 source => tmp_config('--get',"$section.svm-source"),2515 uuid => tmp_config('--get',"$section.svm-uuid"),2516 replace => tmp_config('--get',"$section.svm-replace"),2517}2518};2519if($svm&&$svm->{source} &&$svm->{uuid} &&$svm->{replace}) {2520$self->{svm} =$svm;2521}2522$self->{svm};2523}25242525sub _set_svm_vars {2526my($self,$ra) =@_;2527return$raif$self->svm;25282529my@err= ("useSvmProps set, but failed to read SVM properties\n",2530"(svm:source, svm:uuid) ",2531"from the following URLs:\n");2532sub read_svm_props {2533my($self,$ra,$path,$r) =@_;2534my$props= ($ra->get_dir($path,$r))[2];2535my$src=$props->{'svm:source'};2536my$uuid=$props->{'svm:uuid'};2537returnundefif(!$src|| !$uuid);25382539chomp($src,$uuid);25402541$uuid=~m{^[0-9a-f\-]{30,}$}i2542or die"doesn't look right - svm:uuid is '$uuid'\n";25432544# the '!' is used to mark the repos_root!/relative/path2545$src=~s{/?!/?}{/};2546$src=~s{/+$}{};# no trailing slashes please2547# username is of no interest2548$src=~s{(^[a-z\+]*://)[^/@]*@}{$1};25492550my$replace=$ra->{url};2551$replace.="/$path"iflength$path;25522553my$section="svn-remote.$self->{repo_id}";2554 tmp_config("$section.svm-source",$src);2555 tmp_config("$section.svm-replace",$replace);2556 tmp_config("$section.svm-uuid",$uuid);2557$self->{svm} = {2558 source =>$src,2559 uuid =>$uuid,2560 replace =>$replace2561};2562}25632564my$r=$ra->get_latest_revnum;2565my$path=$self->{path};2566my%tried;2567while(length$path) {2568unless($tried{"$self->{url}/$path"}) {2569return$raif$self->read_svm_props($ra,$path,$r);2570$tried{"$self->{url}/$path"} =1;2571}2572$path=~ s#/?[^/]+$##;2573}2574die"Path: '$path' should be ''\n"if$pathne'';2575return$raif$self->read_svm_props($ra,$path,$r);2576$tried{"$self->{url}/$path"} =1;25772578if($ra->{repos_root}eq$self->{url}) {2579die@err, (map{"$_\n"}keys%tried),"\n";2580}25812582# nope, make sure we're connected to the repository root:2583my$ok;2584my@tried_b;2585$path=$ra->{svn_path};2586$ra= Git::SVN::Ra->new($ra->{repos_root});2587while(length$path) {2588unless($tried{"$ra->{url}/$path"}) {2589$ok=$self->read_svm_props($ra,$path,$r);2590last if$ok;2591$tried{"$ra->{url}/$path"} =1;2592}2593$path=~ s#/?[^/]+$##;2594}2595die"Path: '$path' should be ''\n"if$pathne'';2596$ok||=$self->read_svm_props($ra,$path,$r);2597$tried{"$ra->{url}/$path"} =1;2598if(!$ok) {2599die@err, (map{"$_\n"}keys%tried),"\n";2600}2601 Git::SVN::Ra->new($self->{url});2602}26032604sub svnsync {2605my($self) =@_;2606return$self->{svnsync}if$self->{svnsync};26072608if($self->no_metadata) {2609die"Can't have both 'noMetadata' and ",2610"'useSvnsyncProps' options set!\n";2611}2612if($self->rewrite_root) {2613die"Can't have both 'useSvnsyncProps' and 'rewriteRoot' ",2614"options set!\n";2615}2616if($self->rewrite_uuid) {2617die"Can't have both 'useSvnsyncProps' and 'rewriteUUID' ",2618"options set!\n";2619}26202621my$svnsync;2622# see if we have it in our config, first:2623eval{2624my$section="svn-remote.$self->{repo_id}";26252626my$url= tmp_config('--get',"$section.svnsync-url");2627($url) = ($url=~m{^([a-z\+]+://\S+)$})or2628die"doesn't look right - svn:sync-from-url is '$url'\n";26292630my$uuid= tmp_config('--get',"$section.svnsync-uuid");2631($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2632die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26332634$svnsync= { url =>$url, uuid =>$uuid}2635};2636if($svnsync&&$svnsync->{url} &&$svnsync->{uuid}) {2637return$self->{svnsync} =$svnsync;2638}26392640my$err="useSvnsyncProps set, but failed to read ".2641"svnsync property: svn:sync-from-";2642my$rp=$self->ra->rev_proplist(0);26432644my$url=$rp->{'svn:sync-from-url'}or die$err."url\n";2645($url) = ($url=~m{^([a-z\+]+://\S+)$})or2646die"doesn't look right - svn:sync-from-url is '$url'\n";26472648my$uuid=$rp->{'svn:sync-from-uuid'}or die$err."uuid\n";2649($uuid) = ($uuid=~m{^([0-9a-f\-]{30,})$}i)or2650die"doesn't look right - svn:sync-from-uuid is '$uuid'\n";26512652my$section="svn-remote.$self->{repo_id}";2653 tmp_config('--add',"$section.svnsync-uuid",$uuid);2654 tmp_config('--add',"$section.svnsync-url",$url);2655return$self->{svnsync} = { url =>$url, uuid =>$uuid};2656}26572658# this allows us to memoize our SVN::Ra UUID locally and avoid a2659# remote lookup (useful for 'git svn log').2660sub ra_uuid {2661my($self) =@_;2662unless($self->{ra_uuid}) {2663my$key="svn-remote.$self->{repo_id}.uuid";2664my$uuid=eval{ tmp_config('--get',$key) };2665if(!$@&&$uuid&&$uuid=~/^([a-f\d\-]{30,})$/i) {2666$self->{ra_uuid} =$uuid;2667}else{2668die"ra_uuid called without URL\n"unless$self->{url};2669$self->{ra_uuid} =$self->ra->get_uuid;2670 tmp_config('--add',$key,$self->{ra_uuid});2671}2672}2673$self->{ra_uuid};2674}26752676sub _set_repos_root {2677my($self,$repos_root) =@_;2678my$k="svn-remote.$self->{repo_id}.reposRoot";2679$repos_root||=$self->ra->{repos_root};2680 tmp_config($k,$repos_root);2681$repos_root;2682}26832684sub repos_root {2685my($self) =@_;2686my$k="svn-remote.$self->{repo_id}.reposRoot";2687eval{ tmp_config('--get',$k) } ||$self->_set_repos_root;2688}26892690sub ra {2691my($self) =shift;2692my$ra= Git::SVN::Ra->new($self->{url});2693$self->_set_repos_root($ra->{repos_root});2694if($self->use_svm_props&& !$self->{svm}) {2695if($self->no_metadata) {2696die"Can't have both 'noMetadata' and ",2697"'useSvmProps' options set!\n";2698}elsif($self->use_svnsync_props) {2699die"Can't have both 'useSvnsyncProps' and ",2700"'useSvmProps' options set!\n";2701}2702$ra=$self->_set_svm_vars($ra);2703$self->{-want_revprops} =1;2704}2705$ra;2706}27072708# prop_walk(PATH, REV, SUB)2709# -------------------------2710# Recursively traverse PATH at revision REV and invoke SUB for each2711# directory that contains a SVN property. SUB will be invoked as2712# follows: &SUB(gs, path, props); where `gs' is this instance of2713# Git::SVN, `path' the path to the directory where the properties2714# `props' were found. The `path' will be relative to point of checkout,2715# that is, if url://repo/trunk is the current Git branch, and that2716# directory contains a sub-directory `d', SUB will be invoked with `/d/'2717# as `path' (note the trailing `/').2718sub prop_walk {2719my($self,$path,$rev,$sub) =@_;27202721$path=~ s#^/##;2722my($dirent,undef,$props) =$self->ra->get_dir($path,$rev);2723$path=~ s#^/*#/#g;2724my$p=$path;2725# Strip the irrelevant part of the path.2726$p=~ s#^/+\Q$self->{path}\E(/|$)#/#;2727# Ensure the path is terminated by a `/'.2728$p=~ s#/*$#/#;27292730# The properties contain all the internal SVN stuff nobody2731# (usually) cares about.2732my$interesting_props=0;2733foreach(keys%{$props}) {2734# If it doesn't start with `svn:', it must be a2735# user-defined property.2736++$interesting_propsandnext if$_!~/^svn:/;2737# FIXME: Fragile, if SVN adds new public properties,2738# this needs to be updated.2739++$interesting_propsif/^svn:(?:ignore|keywords|executable2740|eol-style|mime-type2741|externals|needs-lock)$/x;2742}2743&$sub($self,$p,$props)if$interesting_props;27442745foreach(sort keys%$dirent) {2746next if$dirent->{$_}->{kind} !=$SVN::Node::dir;2747$self->prop_walk($self->{path} .$p.$_,$rev,$sub);2748}2749}27502751sub last_rev { ($_[0]->last_rev_commit)[0] }2752sub last_commit { ($_[0]->last_rev_commit)[1] }27532754# returns the newest SVN revision number and newest commit SHA12755sub last_rev_commit {2756my($self) =@_;2757if(defined$self->{last_rev} &&defined$self->{last_commit}) {2758return($self->{last_rev},$self->{last_commit});2759}2760my$c= ::verify_ref($self->refname.'^0');2761if($c&& !$self->use_svm_props&& !$self->no_metadata) {2762my$rev= (::cmt_metadata($c))[1];2763if(defined$rev) {2764($self->{last_rev},$self->{last_commit}) = ($rev,$c);2765return($rev,$c);2766}2767}2768my$map_path=$self->map_path;2769unless(-e $map_path) {2770($self->{last_rev},$self->{last_commit}) = (undef,undef);2771return(undef,undef);2772}2773my($rev,$commit) =$self->rev_map_max(1);2774($self->{last_rev},$self->{last_commit}) = ($rev,$commit);2775return($rev,$commit);2776}27772778sub get_fetch_range {2779my($self,$min,$max) =@_;2780$max||=$self->ra->get_latest_revnum;2781$min||=$self->rev_map_max;2782(++$min,$max);2783}27842785sub tmp_config {2786my(@args) =@_;2787my$old_def_config="$ENV{GIT_DIR}/svn/config";2788my$config="$ENV{GIT_DIR}/svn/.metadata";2789if(! -f $config&& -f $old_def_config) {2790rename$old_def_config,$configor2791die"Failed rename$old_def_config=>$config:$!\n";2792}2793my$old_config=$ENV{GIT_CONFIG};2794$ENV{GIT_CONFIG} =$config;2795$@=undef;2796my@ret=eval{2797unless(-f $config) {2798 mkfile($config);2799open my$fh,'>',$configor2800die"Can't open$config:$!\n";2801print$fh"; This file is used internally by ",2802"git-svn\n"or die2803"Couldn't write to$config:$!\n";2804print$fh"; You should not have to edit it\n"or2805die"Couldn't write to$config:$!\n";2806close$fhor die"Couldn't close$config:$!\n";2807}2808 command('config',@args);2809};2810my$err=$@;2811if(defined$old_config) {2812$ENV{GIT_CONFIG} =$old_config;2813}else{2814delete$ENV{GIT_CONFIG};2815}2816die$errif$err;2817wantarray?@ret:$ret[0];2818}28192820sub tmp_index_do {2821my($self,$sub) =@_;2822my$old_index=$ENV{GIT_INDEX_FILE};2823$ENV{GIT_INDEX_FILE} =$self->{index};2824$@=undef;2825my@ret=eval{2826my($dir,$base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);2827 mkpath([$dir])unless-d $dir;2828&$sub;2829};2830my$err=$@;2831if(defined$old_index) {2832$ENV{GIT_INDEX_FILE} =$old_index;2833}else{2834delete$ENV{GIT_INDEX_FILE};2835}2836die$errif$err;2837wantarray?@ret:$ret[0];2838}28392840sub assert_index_clean {2841my($self,$treeish) =@_;28422843$self->tmp_index_do(sub{2844 command_noisy('read-tree',$treeish)unless-e $self->{index};2845my$x= command_oneline('write-tree');2846my($y) = (command(qw/cat-file commit/,$treeish) =~2847/^tree ($::sha1)/mo);2848return if$yeq$x;28492850warn"Index mismatch:$y!=$x\nrereading$treeish\n";2851unlink$self->{index}or die"unlink$self->{index}:$!\n";2852 command_noisy('read-tree',$treeish);2853$x= command_oneline('write-tree');2854if($yne$x) {2855::fatal "trees ($treeish)$y!=$x\n",2856"Something is seriously wrong...";2857}2858});2859}28602861sub get_commit_parents {2862my($self,$log_entry) =@_;2863my(%seen,@ret,@tmp);2864# legacy support for 'set-tree'; this is only used by set_tree_cb:2865if(my$ip=$self->{inject_parents}) {2866if(my$commit=delete$ip->{$log_entry->{revision}}) {2867push@tmp,$commit;2868}2869}2870if(my$cur= ::verify_ref($self->refname.'^0')) {2871push@tmp,$cur;2872}2873if(my$ipd=$self->{inject_parents_dcommit}) {2874if(my$commit=delete$ipd->{$log_entry->{revision}}) {2875push@tmp,@$commit;2876}2877}2878push@tmp,$_foreach(@{$log_entry->{parents}},@tmp);2879while(my$p=shift@tmp) {2880next if$seen{$p};2881$seen{$p} =1;2882push@ret,$p;2883}2884@ret;2885}28862887sub rewrite_root {2888my($self) =@_;2889return$self->{-rewrite_root}ifexists$self->{-rewrite_root};2890my$k="svn-remote.$self->{repo_id}.rewriteRoot";2891my$rwr=eval{ command_oneline(qw/config --get/,$k) };2892if($rwr) {2893$rwr=~ s#/+$##;2894if($rwr!~ m#^[a-z\+]+://#) {2895die"$rwris not a valid URL (key:$k)\n";2896}2897}2898$self->{-rewrite_root} =$rwr;2899}29002901sub rewrite_uuid {2902my($self) =@_;2903return$self->{-rewrite_uuid}ifexists$self->{-rewrite_uuid};2904my$k="svn-remote.$self->{repo_id}.rewriteUUID";2905my$rwid=eval{ command_oneline(qw/config --get/,$k) };2906if($rwid) {2907$rwid=~ s#/+$##;2908if($rwid!~ m#^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$#) {2909die"$rwidis not a valid UUID (key:$k)\n";2910}2911}2912$self->{-rewrite_uuid} =$rwid;2913}29142915sub metadata_url {2916my($self) =@_;2917($self->rewrite_root||$self->{url}) .2918(length$self->{path} ?'/'.$self->{path} :'');2919}29202921sub full_url {2922my($self) =@_;2923$self->{url} . (length$self->{path} ?'/'.$self->{path} :'');2924}29252926sub full_pushurl {2927my($self) =@_;2928if($self->{pushurl}) {2929return$self->{pushurl} . (length$self->{path} ?'/'.2930$self->{path} :'');2931}else{2932return$self->full_url;2933}2934}29352936sub set_commit_header_env {2937my($log_entry) =@_;2938my%env;2939foreachmy$ned(qw/NAME EMAIL DATE/) {2940foreachmy$ac(qw/AUTHOR COMMITTER/) {2941$env{"GIT_${ac}_${ned}"} =$ENV{"GIT_${ac}_${ned}"};2942}2943}29442945$ENV{GIT_AUTHOR_NAME} =$log_entry->{name};2946$ENV{GIT_AUTHOR_EMAIL} =$log_entry->{email};2947$ENV{GIT_AUTHOR_DATE} =$ENV{GIT_COMMITTER_DATE} =$log_entry->{date};29482949$ENV{GIT_COMMITTER_NAME} = (defined$log_entry->{commit_name})2950?$log_entry->{commit_name}2951:$log_entry->{name};2952$ENV{GIT_COMMITTER_EMAIL} = (defined$log_entry->{commit_email})2953?$log_entry->{commit_email}2954:$log_entry->{email};2955 \%env;2956}29572958sub restore_commit_header_env {2959my($env) =@_;2960foreachmy$ned(qw/NAME EMAIL DATE/) {2961foreachmy$ac(qw/AUTHOR COMMITTER/) {2962my$k="GIT_${ac}_${ned}";2963if(defined$env->{$k}) {2964$ENV{$k} =$env->{$k};2965}else{2966delete$ENV{$k};2967}2968}2969}2970}29712972sub gc {2973 command_noisy('gc','--auto');2974};29752976sub do_git_commit {2977my($self,$log_entry) =@_;2978my$lr=$self->last_rev;2979if(defined$lr&&$lr>=$log_entry->{revision}) {2980die"Last fetched revision of ",$self->refname,2981" was r$lr, but we are about to fetch: ",2982"r$log_entry->{revision}!\n";2983}2984if(my$c=$self->rev_map_get($log_entry->{revision})) {2985 croak "$log_entry->{revision} =$calready exists! ",2986"Why are we refetching it?\n";2987}2988my$old_env= set_commit_header_env($log_entry);2989my$tree=$log_entry->{tree};2990if(!defined$tree) {2991$tree=$self->tmp_index_do(sub{2992 command_oneline('write-tree') });2993}2994die"Tree is not a valid sha1:$tree\n"if$tree!~/^$::sha1$/o;29952996my@exec= ('git','commit-tree',$tree);2997foreach($self->get_commit_parents($log_entry)) {2998push@exec,'-p',$_;2999}3000defined(my$pid= open3(my$msg_fh,my$out_fh,'>&STDERR',@exec))3001or croak $!;3002binmode$msg_fh;30033004# we always get UTF-8 from SVN, but we may want our commits in3005# a different encoding.3006if(my$enc= Git::config('i18n.commitencoding')) {3007require Encode;3008 Encode::from_to($log_entry->{log},'UTF-8',$enc);3009}3010print$msg_fh $log_entry->{log}or croak $!;3011 restore_commit_header_env($old_env);3012unless($self->no_metadata) {3013print$msg_fh"\ngit-svn-id:$log_entry->{metadata}\n"3014or croak $!;3015}3016$msg_fh->flush==0or croak $!;3017close$msg_fhor croak $!;3018chomp(my$commit=do{local$/; <$out_fh> });3019close$out_fhor croak $!;3020waitpid$pid,0;3021 croak $?if$?;3022if($commit!~/^$::sha1$/o) {3023die"Failed to commit, invalid sha1:$commit\n";3024}30253026$self->rev_map_set($log_entry->{revision},$commit,1);30273028$self->{last_rev} =$log_entry->{revision};3029$self->{last_commit} =$commit;3030print"r$log_entry->{revision}"unless$::_q >1;3031if(defined$log_entry->{svm_revision}) {3032print" (\@$log_entry->{svm_revision})"unless$::_q >1;3033$self->rev_map_set($log_entry->{svm_revision},$commit,30340,$self->svm_uuid);3035}3036print" =$commit($self->{ref_id})\n"unless$::_q >1;3037if(--$_gc_nr==0) {3038$_gc_nr=$_gc_period;3039 gc();3040}3041return$commit;3042}30433044sub match_paths {3045my($self,$paths,$r) =@_;3046return1if$self->{path}eq'';3047if(my$path=$paths->{"/$self->{path}"}) {3048return($path->{action}eq'D') ?0:1;3049}3050$self->{path_regex} ||=qr/^\/\Q$self->{path}\E\//;3051if(grep/$self->{path_regex}/,keys%$paths) {3052return1;3053}3054my$c='';3055foreach(split m#/#, $self->{path}) {3056$c.="/$_";3057next unless($paths->{$c} &&3058($paths->{$c}->{action} =~/^[AR]$/));3059if($self->ra->check_path($self->{path},$r) ==3060$SVN::Node::dir) {3061return1;3062}3063}3064return0;3065}30663067sub find_parent_branch {3068my($self,$paths,$rev) =@_;3069returnundefunless$self->follow_parent;3070unless(defined$paths) {3071my$err_handler=$SVN::Error::handler;3072$SVN::Error::handler = \&Git::SVN::Ra::skip_unknown_revs;3073$self->ra->get_log([$self->{path}],$rev,$rev,0,1,1,3074sub{$paths=$_[0] });3075$SVN::Error::handler =$err_handler;3076}3077returnundefunlessdefined$paths;30783079# look for a parent from another branch:3080my@b_path_components=split m#/#, $self->{path};3081my@a_path_components;3082my$i;3083while(@b_path_components) {3084$i=$paths->{'/'.join('/',@b_path_components)};3085last if$i&&defined$i->{copyfrom_path};3086unshift(@a_path_components,pop(@b_path_components));3087}3088returnundefunlessdefined$i&&defined$i->{copyfrom_path};3089my$branch_from=$i->{copyfrom_path};3090if(@a_path_components) {3091print STDERR "branch_from:$branch_from=> ";3092$branch_from.='/'.join('/',@a_path_components);3093print STDERR $branch_from,"\n";3094}3095my$r=$i->{copyfrom_rev};3096my$repos_root=$self->ra->{repos_root};3097my$url=$self->ra->{url};3098my$new_url=$url.$branch_from;3099print STDERR "Found possible branch point: ",3100"$new_url=> ",$self->full_url,",$r\n"3101unless$::_q >1;3102$branch_from=~ s#^/##;3103my$gs=$self->other_gs($new_url,$url,3104$branch_from,$r,$self->{ref_id});3105my($r0,$parent) =$gs->find_rev_before($r,1);3106{3107my($base,$head);3108if(!defined$r0|| !defined$parent) {3109($base,$head) = parse_revision_argument(0,$r);3110}else{3111if($r0<$r) {3112$gs->ra->get_log([$gs->{path}],$r0+1,$r,1,31130,1,sub{$base=$_[1] -1});3114}3115}3116if(defined$base&&$base<=$r) {3117$gs->fetch($base,$r);3118}3119($r0,$parent) =$gs->find_rev_before($r,1);3120}3121if(defined$r0&&defined$parent) {3122print STDERR "Found branch parent: ($self->{ref_id})$parent\n"3123unless$::_q >1;3124my$ed;3125if($self->ra->can_do_switch) {3126$self->assert_index_clean($parent);3127print STDERR "Following parent with do_switch\n"3128unless$::_q >1;3129# do_switch works with svn/trunk >= r22312, but that3130# is not included with SVN 1.4.3 (the latest version3131# at the moment), so we can't rely on it3132$self->{last_rev} =$r0;3133$self->{last_commit} =$parent;3134$ed= SVN::Git::Fetcher->new($self,$gs->{path});3135$gs->ra->gs_do_switch($r0,$rev,$gs,3136$self->full_url,$ed)3137or die"SVN connection failed somewhere...\n";3138}elsif($self->ra->trees_match($new_url,$r0,3139$self->full_url,$rev)) {3140print STDERR "Trees match:\n",3141"$new_url\@$r0\n",3142" ${\$self->full_url}\@$rev\n",3143"Following parent with no changes\n"3144unless$::_q >1;3145$self->tmp_index_do(sub{3146 command_noisy('read-tree',$parent);3147});3148$self->{last_commit} =$parent;3149}else{3150print STDERR "Following parent with do_update\n"3151unless$::_q >1;3152$ed= SVN::Git::Fetcher->new($self);3153$self->ra->gs_do_update($rev,$rev,$self,$ed)3154or die"SVN connection failed somewhere...\n";3155}3156print STDERR "Successfully followed parent\n"unless$::_q >1;3157return$self->make_log_entry($rev, [$parent],$ed);3158}3159returnundef;3160}31613162sub do_fetch {3163my($self,$paths,$rev) =@_;3164my$ed;3165my($last_rev,@parents);3166if(my$lc=$self->last_commit) {3167# we can have a branch that was deleted, then re-added3168# under the same name but copied from another path, in3169# which case we'll have multiple parents (we don't3170# want to break the original ref, nor lose copypath info):3171if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3172push@{$log_entry->{parents}},$lc;3173return$log_entry;3174}3175$ed= SVN::Git::Fetcher->new($self);3176$last_rev=$self->{last_rev};3177$ed->{c} =$lc;3178@parents= ($lc);3179}else{3180$last_rev=$rev;3181if(my$log_entry=$self->find_parent_branch($paths,$rev)) {3182return$log_entry;3183}3184$ed= SVN::Git::Fetcher->new($self);3185}3186unless($self->ra->gs_do_update($last_rev,$rev,$self,$ed)) {3187die"SVN connection failed somewhere...\n";3188}3189$self->make_log_entry($rev, \@parents,$ed);3190}31913192sub mkemptydirs {3193my($self,$r) =@_;31943195sub scan {3196my($r,$empty_dirs,$line) =@_;3197if(defined$r&&$line=~/^r(\d+)$/) {3198return0if$1>$r;3199}elsif($line=~/^ \+empty_dir: (.+)$/) {3200$empty_dirs->{$1} =1;3201}elsif($line=~/^ \-empty_dir: (.+)$/) {3202my@d=grep{m[^\Q$1\E(/|$)]} (keys%$empty_dirs);3203delete@$empty_dirs{@d};3204}32051;# continue3206};32073208my%empty_dirs= ();3209my$gz_file="$self->{dir}/unhandled.log.gz";3210if(-f $gz_file) {3211if(!$can_compress) {3212warn"Compress::Zlib could not be found; ",3213"empty directories in$gz_filewill not be read\n";3214}else{3215my$gz= Compress::Zlib::gzopen($gz_file,"rb")or3216die"Unable to open$gz_file:$!\n";3217my$line;3218while($gz->gzreadline($line) >0) {3219 scan($r, \%empty_dirs,$line)orlast;3220}3221$gz->gzclose;3222}3223}32243225if(open my$fh,'<',"$self->{dir}/unhandled.log") {3226binmode$fhor croak "binmode:$!";3227while(<$fh>) {3228 scan($r, \%empty_dirs,$_)orlast;3229}3230close$fh;3231}32323233my$strip=qr/\A\Q$self->{path}\E(?:\/|$)/;3234foreachmy$d(sort keys%empty_dirs) {3235$d= uri_decode($d);3236$d=~s/$strip//;3237next unlesslength($d);3238next if-d $d;3239if(-e $d) {3240warn"$dexists but is not a directory\n";3241}else{3242print"creating empty directory:$d\n";3243 mkpath([$d]);3244}3245}3246}32473248sub get_untracked {3249my($self,$ed) =@_;3250my@out;3251my$h=$ed->{empty};3252foreach(sort keys%$h) {3253my$act=$h->{$_} ?'+empty_dir':'-empty_dir';3254push@out,"$act: ". uri_encode($_);3255warn"W:$act:$_\n";3256}3257foreachmy$t(qw/dir_prop file_prop/) {3258$h=$ed->{$t}ornext;3259foreachmy$path(sort keys%$h) {3260my$ppath=$patheq''?'.':$path;3261foreachmy$prop(sort keys%{$h->{$path}}) {3262next if$SKIP_PROP{$prop};3263my$v=$h->{$path}->{$prop};3264my$t_ppath_prop="$t: ".3265 uri_encode($ppath) .' '.3266 uri_encode($prop);3267if(defined$v) {3268push@out," +$t_ppath_prop".3269 uri_encode($v);3270}else{3271push@out," -$t_ppath_prop";3272}3273}3274}3275}3276foreachmy$t(qw/absent_file absent_directory/) {3277$h=$ed->{$t}ornext;3278foreachmy$parent(sort keys%$h) {3279foreachmy$path(sort@{$h->{$parent}}) {3280push@out,"$t: ".3281 uri_encode("$parent/$path");3282warn"W:$t:$parent/$path",3283"Insufficient permissions?\n";3284}3285}3286}3287 \@out;3288}32893290sub get_tz {3291# some systmes don't handle or mishandle %z, so be creative.3292my$t=shift||time;3293my$gm= timelocal(gmtime($t));3294my$sign=qw( + + - )[$t<=>$gm];3295returnsprintf("%s%02d%02d",$sign, (gmtime(abs($t-$gm)))[2,1]);3296}32973298# parse_svn_date(DATE)3299# --------------------3300# Given a date (in UTC) from Subversion, return a string in the format3301# "<TZ Offset> <local date/time>" that Git will use.3302#3303# By default the parsed date will be in UTC; if $Git::SVN::_localtime3304# is true we'll convert it to the local timezone instead.3305sub parse_svn_date {3306my$date=shift||return'+0000 1970-01-01 00:00:00';3307my($Y,$m,$d,$H,$M,$S) = ($date=~ /^(\d{4})\-(\d\d)\-(\d\d)T3308(\d\d)\:(\d\d)\:(\d\d)\.\d*Z$/x)or3309 croak "Unable to parse date:$date\n";3310my$parsed_date;# Set next.33113312if($Git::SVN::_localtime) {3313# Translate the Subversion datetime to an epoch time.3314# Begin by switching ourselves to $date's timezone, UTC.3315my$old_env_TZ=$ENV{TZ};3316$ENV{TZ} ='UTC';33173318my$epoch_in_UTC=3319 POSIX::strftime('%s',$S,$M,$H,$d,$m-1,$Y-1900);33203321# Determine our local timezone (including DST) at the3322# time of $epoch_in_UTC. $Git::SVN::Log::TZ stored the3323# value of TZ, if any, at the time we were run.3324if(defined$Git::SVN::Log::TZ) {3325$ENV{TZ} =$Git::SVN::Log::TZ;3326}else{3327delete$ENV{TZ};3328}33293330my$our_TZ= get_tz();33313332# This converts $epoch_in_UTC into our local timezone.3333my($sec,$min,$hour,$mday,$mon,$year,3334$wday,$yday,$isdst) =localtime($epoch_in_UTC);33353336$parsed_date=sprintf('%s%04d-%02d-%02d%02d:%02d:%02d',3337$our_TZ,$year+1900,$mon+1,3338$mday,$hour,$min,$sec);33393340# Reset us to the timezone in effect when we entered3341# this routine.3342if(defined$old_env_TZ) {3343$ENV{TZ} =$old_env_TZ;3344}else{3345delete$ENV{TZ};3346}3347}else{3348$parsed_date="+0000$Y-$m-$d$H:$M:$S";3349}33503351return$parsed_date;3352}33533354sub other_gs {3355my($self,$new_url,$url,3356$branch_from,$r,$old_ref_id) =@_;3357my$gs= Git::SVN->find_by_url($new_url,$url,$branch_from);3358unless($gs) {3359my$ref_id=$old_ref_id;3360$ref_id=~s/\@\d+-*$//;3361$ref_id.="\@$r";3362# just grow a tail if we're not unique enough :x3363$ref_id.='-'while find_ref($ref_id);3364my($u,$p,$repo_id) = ($new_url,'',$ref_id);3365if($u=~ s#^\Q$url\E(/|$)##) {3366$p=$u;3367$u=$url;3368$repo_id=$self->{repo_id};3369}3370while(1) {3371# It is possible to tag two different subdirectories at3372# the same revision. If the url for an existing ref3373# does not match, we must either find a ref with a3374# matching url or create a new ref by growing a tail.3375$gs= Git::SVN->init($u,$p,$repo_id,$ref_id,1);3376my(undef,$max_commit) =$gs->rev_map_max(1);3377last if(!$max_commit);3378my($url) = ::cmt_metadata($max_commit);3379last if($urleq$gs->metadata_url);3380$ref_id.='-';3381}3382print STDERR "Initializing parent:$ref_id\n"unless$::_q >1;3383}3384$gs3385}33863387sub call_authors_prog {3388my($orig_author) =@_;3389$orig_author= command_oneline('rev-parse','--sq-quote',$orig_author);3390my$author=`$::_authors_prog$orig_author`;3391 if ($?!= 0) {3392 die "$::_authors_prog failed with exit code$?\n"3393 }3394 if ($author=~ /^\s*(.+?)\s*<(.*)>\s*$/) {3395 my ($name,$email) = ($1,$2);3396$email= undef if length$2== 0;3397 return [$name,$email];3398 } else {3399 die "Author:$orig_author: $::_authors_prog returned "3400 . "invalid author format:$author\n";3401 }3402}34033404sub check_author {3405 my ($author) =@_;3406 if (!defined$author|| length$author== 0) {3407$author= '(no author)';3408 }3409 if (!defined $::users{$author}) {3410 if (defined $::_authors_prog) {3411 $::users{$author} = call_authors_prog($author);3412 } elsif (defined $::_authors) {3413 die "Author:$authornot defined in $::_authors file\n";3414 }3415 }3416$author;3417}34183419sub find_extra_svk_parents {3420 my ($self,$ed,$tickets,$parents) =@_;3421 # aha! svk:merge property changed...3422 my@tickets= split "\n",$tickets;3423 my@known_parents;3424 for my$ticket(@tickets) {3425 my ($uuid,$path,$rev) = split /:/,$ticket;3426 if ($uuideq$self->ra_uuid ) {3427 my$url=$self->{url};3428 my$repos_root=$url;3429 my$branch_from=$path;3430$branch_from=~ s{^/}{};3431 my$gs=$self->other_gs($repos_root."/".$branch_from,3432$url,3433$branch_from,3434$rev,3435$self->{ref_id});3436 if ( my$commit=$gs->rev_map_get($rev,$uuid) ) {3437 # wahey! we found it, but it might be3438 # an old one (!)3439 push@known_parents, [$rev,$commit];3440 }3441 }3442 }3443 # Ordering matters; highest-numbered commit merge tickets3444 # first, as they may account for later merge ticket additions3445 # or changes.3446@known_parents= map {$_->[1]} sort {$b->[0] <=>$a->[0]}@known_parents;3447 for my$parent(@known_parents) {3448 my@cmd= ('rev-list',$parent, map { "^$_" }@$parents);3449 my ($msg_fh,$ctx) = command_output_pipe(@cmd);3450 my$new;3451 while ( <$msg_fh> ) {3452$new=1;last;3453 }3454 command_close_pipe($msg_fh,$ctx);3455 if ($new) {3456 print STDERR3457 "Found merge parent (svk:merge ticket):$parent\n";3458 push@$parents,$parent;3459 }3460 }3461}34623463sub lookup_svn_merge {3464 my$uuid= shift;3465 my$url= shift;3466 my$merge= shift;34673468 my ($source,$revs) = split ":",$merge;3469 my$path=$source;3470$path=~ s{^/}{};3471 my$gs= Git::SVN->find_by_url($url.$source,$url,$path);3472 if ( !$gs) {3473 warn "Couldn't find revmap for$url$source\n";3474 return;3475 }3476 my@ranges= split ",",$revs;3477 my ($tip,$tip_commit);3478 my@merged_commit_ranges;3479 # find the tip3480 for my$range(@ranges) {3481 my ($bottom,$top) = split "-",$range;3482$top||=$bottom;3483 my$bottom_commit=$gs->find_rev_after($bottom, 1,$top);3484 my$top_commit=$gs->find_rev_before($top, 1,$bottom);34853486 unless ($top_commitand$bottom_commit) {3487 warn "W:unknown path/rev in svn:mergeinfo "3488 ."dirprop:$source:$range\n";3489 next;3490 }34913492 if (scalar(command('rev-parse', "$bottom_commit^@"))) {3493 push@merged_commit_ranges,3494 "$bottom_commit^..$top_commit";3495 } else {3496 push@merged_commit_ranges, "$top_commit";3497 }34983499 if ( !defined$tipor$top>$tip) {3500$tip=$top;3501$tip_commit=$top_commit;3502 }3503 }3504 return ($tip_commit,@merged_commit_ranges);3505}35063507sub _rev_list {3508 my ($msg_fh,$ctx) = command_output_pipe(3509 "rev-list",@_,3510 );3511 my@rv;3512 while ( <$msg_fh> ) {3513 chomp;3514 push@rv,$_;3515 }3516 command_close_pipe($msg_fh,$ctx);3517@rv;3518}35193520sub check_cherry_pick {3521 my$base= shift;3522 my$tip= shift;3523 my$parents= shift;3524 my@ranges=@_;3525 my%commits= map {$_=> 1 }3526 _rev_list("--no-merges",$tip, "--not",$base,@$parents, "--");3527 for my$range(@ranges) {3528 delete@commits{_rev_list($range, "--")};3529 }3530 for my$commit(keys%commits) {3531 if (has_no_changes($commit)) {3532 delete$commits{$commit};3533 }3534 }3535 return (keys%commits);3536}35373538sub has_no_changes {3539 my$commit= shift;35403541 my@revs= split / /, command_oneline(3542 qw(rev-list --parents -1 -m),$commit);35433544# Commits with no parents, e.g. the start of a partial branch,3545# have changes by definition.3546return1if(@revs<2);35473548# Commits with multiple parents, e.g a merge, have no changes3549# by definition.3550return0if(@revs>2);35513552return(command_oneline("rev-parse","$commit^{tree}")eq3553 command_oneline("rev-parse","$commit~1^{tree}"));3554}35553556# The GIT_DIR environment variable is not always set until after the command3557# line arguments are processed, so we can't memoize in a BEGIN block.3558{3559my$memoized=0;35603561sub memoize_svn_mergeinfo_functions {3562return if$memoized;3563$memoized=1;35643565my$cache_path="$ENV{GIT_DIR}/svn/.caches/";3566 mkpath([$cache_path])unless-d $cache_path;35673568 tie my%lookup_svn_merge_cache=>'Memoize::Storable',3569"$cache_path/lookup_svn_merge.db",'nstore';3570 memoize 'lookup_svn_merge',3571 SCALAR_CACHE =>'FAULT',3572 LIST_CACHE => ['HASH'=> \%lookup_svn_merge_cache],3573;35743575 tie my%check_cherry_pick_cache=>'Memoize::Storable',3576"$cache_path/check_cherry_pick.db",'nstore';3577 memoize 'check_cherry_pick',3578 SCALAR_CACHE =>'FAULT',3579 LIST_CACHE => ['HASH'=> \%check_cherry_pick_cache],3580;35813582 tie my%has_no_changes_cache=>'Memoize::Storable',3583"$cache_path/has_no_changes.db",'nstore';3584 memoize 'has_no_changes',3585 SCALAR_CACHE => ['HASH'=> \%has_no_changes_cache],3586 LIST_CACHE =>'FAULT',3587;3588}35893590sub unmemoize_svn_mergeinfo_functions {3591return ifnot$memoized;3592$memoized=0;35933594 Memoize::unmemoize 'lookup_svn_merge';3595 Memoize::unmemoize 'check_cherry_pick';3596 Memoize::unmemoize 'has_no_changes';3597}35983599 Memoize::memoize 'Git::SVN::repos_root';3600}36013602END{3603# Force cache writeout explicitly instead of waiting for3604# global destruction to avoid segfault in Storable:3605# http://rt.cpan.org/Public/Bug/Display.html?id=360873606 unmemoize_svn_mergeinfo_functions();3607}36083609sub parents_exclude {3610my$parents=shift;3611my@commits=@_;3612return unless@commits;36133614my@excluded;3615my$excluded;3616do{3617my@cmd= ('rev-list',"-1",@commits,"--not",@$parents);3618$excluded= command_oneline(@cmd);3619if($excluded) {3620my@new;3621my$found;3622formy$commit(@commits) {3623if($commiteq$excluded) {3624push@excluded,$commit;3625$found++;3626last;3627}3628else{3629push@new,$commit;3630}3631}3632die"saw commit '$excluded' in rev-list output, "3633."but we didn't ask for that commit (wanted:@commits--not@$parents)"3634unless$found;3635@commits=@new;3636}3637}3638while($excludedand@commits);36393640return@excluded;3641}364236433644# note: this function should only be called if the various dirprops3645# have actually changed3646sub find_extra_svn_parents {3647my($self,$ed,$mergeinfo,$parents) =@_;3648# aha! svk:merge property changed...36493650 memoize_svn_mergeinfo_functions();36513652# We first search for merged tips which are not in our3653# history. Then, we figure out which git revisions are in3654# that tip, but not this revision. If all of those revisions3655# are now marked as merge, we can add the tip as a parent.3656my@merges=split"\n",$mergeinfo;3657my@merge_tips;3658my$url=$self->{url};3659my$uuid=$self->ra_uuid;3660my%ranges;3661formy$merge(@merges) {3662my($tip_commit,@ranges) =3663 lookup_svn_merge($uuid,$url,$merge);3664unless(!$tip_commitor3665grep{$_eq$tip_commit}@$parents) {3666push@merge_tips,$tip_commit;3667$ranges{$tip_commit} = \@ranges;3668}else{3669push@merge_tips,undef;3670}3671}36723673my%excluded=map{$_=>1}3674 parents_exclude($parents,grep{defined}@merge_tips);36753676# check merge tips for new parents3677my@new_parents;3678formy$merge_tip(@merge_tips) {3679my$spec=shift@merges;3680next unless$merge_tipand$excluded{$merge_tip};36813682my$ranges=$ranges{$merge_tip};36833684# check out 'new' tips3685my$merge_base;3686eval{3687$merge_base= command_oneline(3688"merge-base",3689@$parents,$merge_tip,3690);3691};3692if($@) {3693die"An error occurred during merge-base"3694unless$@->isa("Git::Error::Command");36953696warn"W: Cannot find common ancestor between ".3697"@$parentsand$merge_tip. Ignoring merge info.\n";3698next;3699}37003701# double check that there are no missing non-merge commits3702my(@incomplete) = check_cherry_pick(3703$merge_base,$merge_tip,3704$parents,3705@$ranges,3706);37073708if(@incomplete) {3709warn"W:svn cherry-pick ignored ($spec) - missing "3710.@incomplete." commit(s) (eg$incomplete[0])\n";3711}else{3712warn3713"Found merge parent (svn:mergeinfo prop): ",3714$merge_tip,"\n";3715push@new_parents,$merge_tip;3716}3717}37183719# cater for merges which merge commits from multiple branches3720if(@new_parents>1) {3721for(my$i=0;$i<=$#new_parents;$i++) {3722for(my$j=0;$j<=$#new_parents;$j++) {3723next if$i==$j;3724next unless$new_parents[$i];3725next unless$new_parents[$j];3726my$revs= command_oneline(3727"rev-list","-1",3728"$new_parents[$i]..$new_parents[$j]",3729);3730if( !$revs) {3731undef($new_parents[$j]);3732}3733}3734}3735}3736push@$parents,grep{defined}@new_parents;3737}37383739sub make_log_entry {3740my($self,$rev,$parents,$ed) =@_;3741my$untracked=$self->get_untracked($ed);37423743my@parents=@$parents;3744my$ps=$ed->{path_strip} ||"";3745formy$path(grep{m/$ps/} %{$ed->{dir_prop}} ) {3746my$props=$ed->{dir_prop}{$path};3747if($props->{"svk:merge"} ) {3748$self->find_extra_svk_parents3749($ed,$props->{"svk:merge"}, \@parents);3750}3751if($props->{"svn:mergeinfo"} ) {3752$self->find_extra_svn_parents3753($ed,3754$props->{"svn:mergeinfo"},3755 \@parents);3756}3757}37583759open my$un,'>>',"$self->{dir}/unhandled.log"or croak $!;3760print$un"r$rev\n"or croak $!;3761print$un $_,"\n"foreach@$untracked;3762my%log_entry= ( parents => \@parents, revision =>$rev,3763log=>'');37643765my$headrev;3766my$logged=delete$self->{logged_rev_props};3767if(!$logged||$self->{-want_revprops}) {3768my$rp=$self->ra->rev_proplist($rev);3769foreach(sort keys%$rp) {3770my$v=$rp->{$_};3771if(/^svn:(author|date|log)$/) {3772$log_entry{$1} =$v;3773}elsif($_eq'svm:headrev') {3774$headrev=$v;3775}else{3776print$un" rev_prop: ", uri_encode($_),' ',3777 uri_encode($v),"\n";3778}3779}3780}else{3781map{$log_entry{$_} =$logged->{$_} }keys%$logged;3782}3783close$unor croak $!;37843785$log_entry{date} = parse_svn_date($log_entry{date});3786$log_entry{log} .="\n";3787my$author=$log_entry{author} = check_author($log_entry{author});3788my($name,$email) =defined$::users{$author} ? @{$::users{$author}}3789: ($author,undef);37903791my($commit_name,$commit_email) = ($name,$email);3792if($_use_log_author) {3793my$name_field;3794if($log_entry{log} =~/From:\s+(.*\S)\s*\n/i) {3795$name_field=$1;3796}elsif($log_entry{log} =~/Signed-off-by:\s+(.*\S)\s*\n/i) {3797$name_field=$1;3798}3799if(!defined$name_field) {3800if(!defined$email) {3801$email=$name;3802}3803}elsif($name_field=~/(.*?)\s+<(.*)>/) {3804($name,$email) = ($1,$2);3805}elsif($name_field=~/(.*)@/) {3806($name,$email) = ($1,$name_field);3807}else{3808($name,$email) = ($name_field,$name_field);3809}3810}3811if(defined$headrev&&$self->use_svm_props) {3812if($self->rewrite_root) {3813die"Can't have both 'useSvmProps' and 'rewriteRoot' ",3814"options set!\n";3815}3816if($self->rewrite_uuid) {3817die"Can't have both 'useSvmProps' and 'rewriteUUID' ",3818"options set!\n";3819}3820my($uuid,$r) =$headrev=~m{^([a-f\d\-]{30,}):(\d+)$}i;3821# we don't want "SVM: initializing mirror for junk" ...3822returnundefif$r==0;3823my$svm=$self->svm;3824if($uuidne$svm->{uuid}) {3825die"UUID mismatch on SVM path:\n",3826"expected:$svm->{uuid}\n",3827" got:$uuid\n";3828}3829my$full_url=$self->full_url;3830$full_url=~ s#^\Q$svm->{replace}\E(/|$)#$svm->{source}$1# or3831die"Failed to replace '$svm->{replace}' with ",3832"'$svm->{source}' in$full_url\n";3833# throw away username for storing in records3834 remove_username($full_url);3835$log_entry{metadata} ="$full_url\@$r$uuid";3836$log_entry{svm_revision} =$r;3837$email||="$author\@$uuid";3838$commit_email||="$author\@$uuid";3839}elsif($self->use_svnsync_props) {3840my$full_url=$self->svnsync->{url};3841$full_url.="/$self->{path}"iflength$self->{path};3842 remove_username($full_url);3843my$uuid=$self->svnsync->{uuid};3844$log_entry{metadata} ="$full_url\@$rev$uuid";3845$email||="$author\@$uuid";3846$commit_email||="$author\@$uuid";3847}else{3848my$url=$self->metadata_url;3849 remove_username($url);3850my$uuid=$self->rewrite_uuid||$self->ra->get_uuid;3851$log_entry{metadata} ="$url\@$rev".$uuid;3852$email||="$author\@".$uuid;3853$commit_email||="$author\@".$uuid;3854}3855$log_entry{name} =$name;3856$log_entry{email} =$email;3857$log_entry{commit_name} =$commit_name;3858$log_entry{commit_email} =$commit_email;3859 \%log_entry;3860}38613862sub fetch {3863my($self,$min_rev,$max_rev,@parents) =@_;3864my($last_rev,$last_commit) =$self->last_rev_commit;3865my($base,$head) =$self->get_fetch_range($min_rev,$max_rev);3866$self->ra->gs_fetch_loop_common($base,$head, [$self]);3867}38683869sub set_tree_cb {3870my($self,$log_entry,$tree,$rev,$date,$author) =@_;3871$self->{inject_parents} = {$rev=>$tree};3872$self->fetch(undef,undef);3873}38743875sub set_tree {3876my($self,$tree) = (shift,shift);3877my$log_entry= ::get_commit_entry($tree);3878unless($self->{last_rev}) {3879::fatal("Must have an existing revision to commit");3880}3881my%ed_opts= ( r =>$self->{last_rev},3882log=>$log_entry->{log},3883 ra =>$self->ra,3884 tree_a =>$self->{last_commit},3885 tree_b =>$tree,3886 editor_cb =>sub{3887$self->set_tree_cb($log_entry,$tree,@_) },3888 svn_path =>$self->{path} );3889if(!SVN::Git::Editor->new(\%ed_opts)->apply_diff) {3890print"No changes\nr$self->{last_rev} =$tree\n";3891}3892}38933894sub rebuild_from_rev_db {3895my($self,$path) =@_;3896my$r= -1;3897open my$fh,'<',$pathor croak "open:$!";3898binmode$fhor croak "binmode:$!";3899while(<$fh>) {3900length($_) ==41or croak "inconsistent size in ($_) != 41";3901chomp($_);3902++$r;3903next if$_eq('0' x 40);3904$self->rev_map_set($r,$_);3905print"r$r=$_\n";3906}3907close$fhor croak "close:$!";3908unlink$pathor croak "unlink:$!";3909}39103911sub rebuild {3912my($self) =@_;3913my$map_path=$self->map_path;3914my$partial= (-e $map_path&& ! -z $map_path);3915return unless::verify_ref($self->refname.'^0');3916if(!$partial&& ($self->use_svm_props||$self->no_metadata)) {3917my$rev_db=$self->rev_db_path;3918$self->rebuild_from_rev_db($rev_db);3919if($self->use_svm_props) {3920my$svm_rev_db=$self->rev_db_path($self->svm_uuid);3921$self->rebuild_from_rev_db($svm_rev_db);3922}3923$self->unlink_rev_db_symlink;3924return;3925}3926print"Rebuilding$map_path...\n"if(!$partial);3927my($base_rev,$head) = ($partial?$self->rev_map_max_norebuild(1) :3928(undef,undef));3929my($log,$ctx) =3930 command_output_pipe(qw/rev-list --pretty=raw --reverse/,3931($head?"$head..":"") .$self->refname,3932'--');3933my$metadata_url=$self->metadata_url;3934 remove_username($metadata_url);3935my$svn_uuid=$self->rewrite_uuid||$self->ra_uuid;3936my$c;3937while(<$log>) {3938if(m{^commit ($::sha1)$}) {3939$c=$1;3940next;3941}3942next unlesss{^\s*(git-svn-id:)}{$1};3943my($url,$rev,$uuid) = ::extract_metadata($_);3944 remove_username($url);39453946# ignore merges (from set-tree)3947next if(!defined$rev|| !$uuid);39483949# if we merged or otherwise started elsewhere, this is3950# how we break out of it3951if(($uuidne$svn_uuid) ||3952($metadata_url&&$url&& ($urlne$metadata_url))) {3953next;3954}3955if($partial&&$head) {3956print"Partial-rebuilding$map_path...\n";3957print"Currently at$base_rev=$head\n";3958$head=undef;3959}39603961$self->rev_map_set($rev,$c);3962print"r$rev=$c\n";3963}3964 command_close_pipe($log,$ctx);3965print"Done rebuilding$map_path\n"if(!$partial|| !$head);3966my$rev_db_path=$self->rev_db_path;3967if(-f $self->rev_db_path) {3968unlink$self->rev_db_pathor croak "unlink:$!";3969}3970$self->unlink_rev_db_symlink;3971}39723973# rev_map:3974# Tie::File seems to be prone to offset errors if revisions get sparse,3975# it's not that fast, either. Tie::File is also not in Perl 5.6. So3976# one of my favorite modules is out :< Next up would be one of the DBM3977# modules, but I'm not sure which is most portable...3978#3979# This is the replacement for the rev_db format, which was too big3980# and inefficient for large repositories with a lot of sparse history3981# (mainly tags)3982#3983# The format is this:3984# - 24 bytes for every record,3985# * 4 bytes for the integer representing an SVN revision number3986# * 20 bytes representing the sha1 of a git commit3987# - No empty padding records like the old format3988# (except the last record, which can be overwritten)3989# - new records are written append-only since SVN revision numbers3990# increase monotonically3991# - lookups on SVN revision number are done via a binary search3992# - Piping the file to xxd -c24 is a good way of dumping it for3993# viewing or editing (piped back through xxd -r), should the need3994# ever arise.3995# - The last record can be padding revision with an all-zero sha13996# This is used to optimize fetch performance when using multiple3997# "fetch" directives in .git/config3998#3999# These files are disposable unless noMetadata or useSvmProps is set40004001sub _rev_map_set {4002my($fh,$rev,$commit) =@_;40034004binmode$fhor croak "binmode:$!";4005my$size= (stat($fh))[7];4006($size%24) ==0or croak "inconsistent size:$size";40074008my$wr_offset=0;4009if($size>0) {4010sysseek($fh, -24, SEEK_END)or croak "seek:$!";4011my$read=sysread($fh,my$buf,24)or croak "read:$!";4012$read==24or croak "read only$readbytes (!= 24)";4013my($last_rev,$last_commit) =unpack(rev_map_fmt,$buf);4014if($last_commiteq('0' x40)) {4015if($size>=48) {4016sysseek($fh, -48, SEEK_END)or croak "seek:$!";4017$read=sysread($fh,$buf,24)or4018 croak "read:$!";4019$read==24or4020 croak "read only$readbytes (!= 24)";4021($last_rev,$last_commit) =4022unpack(rev_map_fmt,$buf);4023if($last_commiteq('0' x40)) {4024 croak "inconsistent .rev_map\n";4025}4026}4027if($last_rev>=$rev) {4028 croak "last_rev is higher!:$last_rev>=$rev";4029}4030$wr_offset= -24;4031}4032}4033sysseek($fh,$wr_offset, SEEK_END)or croak "seek:$!";4034syswrite($fh,pack(rev_map_fmt,$rev,$commit),24) ==24or4035 croak "write:$!";4036}40374038sub _rev_map_reset {4039my($fh,$rev,$commit) =@_;4040my$c= _rev_map_get($fh,$rev);4041$ceq$commitor die"_rev_map_reset(@_) commit$cdoes not match!\n";4042my$offset=sysseek($fh,0, SEEK_CUR)or croak "seek:$!";4043truncate$fh,$offsetor croak "truncate:$!";4044}40454046sub mkfile {4047my($path) =@_;4048unless(-e $path) {4049my($dir,$base) = ($path=~ m#^(.*?)/?([^/]+)$#);4050 mkpath([$dir])unless-d $dir;4051open my$fh,'>>',$pathor die"Couldn't create$path:$!\n";4052close$fhor die"Couldn't close (create)$path:$!\n";4053}4054}40554056sub rev_map_set {4057my($self,$rev,$commit,$update_ref,$uuid) =@_;4058defined$commitor die"missing arg3\n";4059length$commit==40or die"arg3 must be a full SHA1 hexsum\n";4060my$db=$self->map_path($uuid);4061my$db_lock="$db.lock";4062my$sig;4063$update_ref||=0;4064if($update_ref) {4065$SIG{INT} =$SIG{HUP} =$SIG{TERM} =$SIG{ALRM} =$SIG{PIPE} =4066$SIG{USR1} =$SIG{USR2} =sub{$sig=$_[0] };4067}4068 mkfile($db);40694070$LOCKFILES{$db_lock} =1;4071my$sync;4072# both of these options make our .rev_db file very, very important4073# and we can't afford to lose it because rebuild() won't work4074if($self->use_svm_props||$self->no_metadata) {4075$sync=1;4076 copy($db,$db_lock)or die"rev_map_set(@_): ",4077"Failed to copy: ",4078"$db=>$db_lock($!)\n";4079}else{4080rename$db,$db_lockor die"rev_map_set(@_): ",4081"Failed to rename: ",4082"$db=>$db_lock($!)\n";4083}40844085sysopen(my$fh,$db_lock, O_RDWR | O_CREAT)4086or croak "Couldn't open$db_lock:$!\n";4087$update_refeq'reset'? _rev_map_reset($fh,$rev,$commit) :4088 _rev_map_set($fh,$rev,$commit);4089if($sync) {4090$fh->flushor die"Couldn't flush$db_lock:$!\n";4091$fh->syncor die"Couldn't sync$db_lock:$!\n";4092}4093close$fhor croak $!;4094if($update_ref) {4095$_head=$self;4096my$note="";4097$note=" ($update_ref)"if($update_ref!~/^\d*$/);4098 command_noisy('update-ref','-m',"r$rev$note",4099$self->refname,$commit);4100}4101rename$db_lock,$dbor die"rev_map_set(@_): ","Failed to rename: ",4102"$db_lock=>$db($!)\n";4103delete$LOCKFILES{$db_lock};4104if($update_ref) {4105$SIG{INT} =$SIG{HUP} =$SIG{TERM} =$SIG{ALRM} =$SIG{PIPE} =4106$SIG{USR1} =$SIG{USR2} ='DEFAULT';4107kill$sig,$$ifdefined$sig;4108}4109}41104111# If want_commit, this will return an array of (rev, commit) where4112# commit _must_ be a valid commit in the archive.4113# Otherwise, it'll return the max revision (whether or not the4114# commit is valid or just a 0x40 placeholder).4115sub rev_map_max {4116my($self,$want_commit) =@_;4117$self->rebuild;4118my($r,$c) =$self->rev_map_max_norebuild($want_commit);4119$want_commit? ($r,$c) :$r;4120}41214122sub rev_map_max_norebuild {4123my($self,$want_commit) =@_;4124my$map_path=$self->map_path;4125stat$map_pathorreturn$want_commit? (0,undef) :0;4126sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4127binmode$fhor croak "binmode:$!";4128my$size= (stat($fh))[7];4129($size%24) ==0or croak "inconsistent size:$size";41304131if($size==0) {4132close$fhor croak "close:$!";4133return$want_commit? (0,undef) :0;4134}41354136sysseek($fh, -24, SEEK_END)or croak "seek:$!";4137sysread($fh,my$buf,24) ==24or croak "read:$!";4138my($r,$c) =unpack(rev_map_fmt,$buf);4139if($want_commit&&$ceq('0' x40)) {4140if($size<48) {4141return$want_commit? (0,undef) :0;4142}4143sysseek($fh, -48, SEEK_END)or croak "seek:$!";4144sysread($fh,$buf,24) ==24or croak "read:$!";4145($r,$c) =unpack(rev_map_fmt,$buf);4146if($ceq('0'x40)) {4147 croak "Penultimate record is all-zeroes in$map_path";4148}4149}4150close$fhor croak "close:$!";4151$want_commit? ($r,$c) :$r;4152}41534154sub rev_map_get {4155my($self,$rev,$uuid) =@_;4156my$map_path=$self->map_path($uuid);4157returnundefunless-e $map_path;41584159sysopen(my$fh,$map_path, O_RDONLY)or croak "open:$!";4160my$c= _rev_map_get($fh,$rev);4161close($fh)or croak "close:$!";4162$c4163}41644165sub _rev_map_get {4166my($fh,$rev) =@_;41674168binmode$fhor croak "binmode:$!";4169my$size= (stat($fh))[7];4170($size%24) ==0or croak "inconsistent size:$size";41714172if($size==0) {4173returnundef;4174}41754176my($l,$u) = (0,$size-24);4177my($r,$c,$buf);41784179while($l<=$u) {4180my$i=int(($l/24+$u/24) /2) *24;4181sysseek($fh,$i, SEEK_SET)or croak "seek:$!";4182sysread($fh,my$buf,24) ==24or croak "read:$!";4183my($r,$c) =unpack(rev_map_fmt,$buf);41844185if($r<$rev) {4186$l=$i+24;4187}elsif($r>$rev) {4188$u=$i-24;4189}else{# $r == $rev4190return$ceq('0' x 40) ?undef:$c;4191}4192}4193undef;4194}41954196# Finds the first svn revision that exists on (if $eq_ok is true) or4197# before $rev for the current branch. It will not search any lower4198# than $min_rev. Returns the git commit hash and svn revision number4199# if found, else (undef, undef).4200sub find_rev_before {4201my($self,$rev,$eq_ok,$min_rev) =@_;4202--$revunless$eq_ok;4203$min_rev||=1;4204my$max_rev=$self->rev_map_max;4205$rev=$max_revif($rev>$max_rev);4206while($rev>=$min_rev) {4207if(my$c=$self->rev_map_get($rev)) {4208return($rev,$c);4209}4210--$rev;4211}4212return(undef,undef);4213}42144215# Finds the first svn revision that exists on (if $eq_ok is true) or4216# after $rev for the current branch. It will not search any higher4217# than $max_rev. Returns the git commit hash and svn revision number4218# if found, else (undef, undef).4219sub find_rev_after {4220my($self,$rev,$eq_ok,$max_rev) =@_;4221++$revunless$eq_ok;4222$max_rev||=$self->rev_map_max;4223while($rev<=$max_rev) {4224if(my$c=$self->rev_map_get($rev)) {4225return($rev,$c);4226}4227++$rev;4228}4229return(undef,undef);4230}42314232sub _new {4233my($class,$repo_id,$ref_id,$path) =@_;4234unless(defined$repo_id&&length$repo_id) {4235$repo_id=$Git::SVN::default_repo_id;4236}4237unless(defined$ref_id&&length$ref_id) {4238$_prefix=''unlessdefined($_prefix);4239$_[2] =$ref_id=4240"refs/remotes/$_prefix$Git::SVN::default_ref_id";4241}4242$_[1] =$repo_id;4243my$dir="$ENV{GIT_DIR}/svn/$ref_id";42444245# Older repos imported by us used $GIT_DIR/svn/foo instead of4246# $GIT_DIR/svn/refs/remotes/foo when tracking refs/remotes/foo4247if($ref_id=~m{^refs/remotes/(.*)}) {4248my$old_dir="$ENV{GIT_DIR}/svn/$1";4249if(-d $old_dir&& ! -d $dir) {4250$dir=$old_dir;4251}4252}42534254$_[3] =$path=''unless(defined$path);4255 mkpath([$dir]);4256bless{4257 ref_id =>$ref_id, dir =>$dir,index=>"$dir/index",4258 path =>$path, config =>"$ENV{GIT_DIR}/svn/config",4259 map_root =>"$dir/.rev_map", repo_id =>$repo_id},$class;4260}42614262# for read-only access of old .rev_db formats4263sub unlink_rev_db_symlink {4264my($self) =@_;4265my$link=$self->rev_db_path;4266$link=~s/\.[\w-]+$//or croak "missing UUID at the end of$link";4267if(-l $link) {4268unlink$linkor croak "unlink:$linkfailed!";4269}4270}42714272sub rev_db_path {4273my($self,$uuid) =@_;4274my$db_path=$self->map_path($uuid);4275$db_path=~s{/\.rev_map\.}{/\.rev_db\.}4276or croak "map_path:$db_pathdoes not contain '/.rev_map.' !";4277$db_path;4278}42794280# the new replacement for .rev_db4281sub map_path {4282my($self,$uuid) =@_;4283$uuid||=$self->ra_uuid;4284"$self->{map_root}.$uuid";4285}42864287sub uri_encode {4288my($f) =@_;4289$f=~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg;4290$f4291}42924293sub uri_decode {4294my($f) =@_;4295$f=~ s#%([0-9a-fA-F]{2})#chr(hex($1))#eg;4296$f4297}42984299sub remove_username {4300$_[0] =~s{^([^:]*://)[^@]+@}{$1};4301}43024303package Git::SVN::Prompt;4304use strict;4305use warnings;4306require SVN::Core;4307use vars qw/$_no_auth_cache $_username/;43084309sub simple {4310my($cred,$realm,$default_username,$may_save,$pool) =@_;4311$may_save=undefif$_no_auth_cache;4312$default_username=$_usernameifdefined$_username;4313if(defined$default_username&&length$default_username) {4314if(defined$realm&&length$realm) {4315print STDERR "Authentication realm:$realm\n";4316 STDERR->flush;4317}4318$cred->username($default_username);4319}else{4320 username($cred,$realm,$may_save,$pool);4321}4322$cred->password(_read_password("Password for '".4323$cred->username."': ",$realm));4324$cred->may_save($may_save);4325$SVN::_Core::SVN_NO_ERROR;4326}43274328sub ssl_server_trust {4329my($cred,$realm,$failures,$cert_info,$may_save,$pool) =@_;4330$may_save=undefif$_no_auth_cache;4331print STDERR "Error validating server certificate for '$realm':\n";4332{4333no warnings 'once';4334# All variables SVN::Auth::SSL::* are used only once,4335# so we're shutting up Perl warnings about this.4336if($failures&$SVN::Auth::SSL::UNKNOWNCA) {4337print STDERR " - The certificate is not issued ",4338"by a trusted authority. Use the\n",4339" fingerprint to validate ",4340"the certificate manually!\n";4341}4342if($failures&$SVN::Auth::SSL::CNMISMATCH) {4343print STDERR " - The certificate hostname ",4344"does not match.\n";4345}4346if($failures&$SVN::Auth::SSL::NOTYETVALID) {4347print STDERR " - The certificate is not yet valid.\n";4348}4349if($failures&$SVN::Auth::SSL::EXPIRED) {4350print STDERR " - The certificate has expired.\n";4351}4352if($failures&$SVN::Auth::SSL::OTHER) {4353print STDERR " - The certificate has ",4354"an unknown error.\n";4355}4356}# no warnings 'once'4357printf STDERR4358"Certificate information:\n".4359" - Hostname:%s\n".4360" - Valid: from%suntil%s\n".4361" - Issuer:%s\n".4362" - Fingerprint:%s\n",4363map$cert_info->$_,qw(hostname valid_from valid_until4364 issuer_dname fingerprint);4365my$choice;4366prompt:4367print STDERR $may_save?4368"(R)eject, accept (t)emporarily or accept (p)ermanently? ":4369"(R)eject or accept (t)emporarily? ";4370 STDERR->flush;4371$choice=lc(substr(<STDIN> ||'R',0,1));4372if($choice=~/^t$/i) {4373$cred->may_save(undef);4374}elsif($choice=~/^r$/i) {4375return-1;4376}elsif($may_save&&$choice=~/^p$/i) {4377$cred->may_save($may_save);4378}else{4379goto prompt;4380}4381$cred->accepted_failures($failures);4382$SVN::_Core::SVN_NO_ERROR;4383}43844385sub ssl_client_cert {4386my($cred,$realm,$may_save,$pool) =@_;4387$may_save=undefif$_no_auth_cache;4388print STDERR "Client certificate filename: ";4389 STDERR->flush;4390chomp(my$filename= <STDIN>);4391$cred->cert_file($filename);4392$cred->may_save($may_save);4393$SVN::_Core::SVN_NO_ERROR;4394}43954396sub ssl_client_cert_pw {4397my($cred,$realm,$may_save,$pool) =@_;4398$may_save=undefif$_no_auth_cache;4399$cred->password(_read_password("Password: ",$realm));4400$cred->may_save($may_save);4401$SVN::_Core::SVN_NO_ERROR;4402}44034404sub username {4405my($cred,$realm,$may_save,$pool) =@_;4406$may_save=undefif$_no_auth_cache;4407if(defined$realm&&length$realm) {4408print STDERR "Authentication realm:$realm\n";4409}4410my$username;4411if(defined$_username) {4412$username=$_username;4413}else{4414print STDERR "Username: ";4415 STDERR->flush;4416chomp($username= <STDIN>);4417}4418$cred->username($username);4419$cred->may_save($may_save);4420$SVN::_Core::SVN_NO_ERROR;4421}44224423sub _read_password {4424my($prompt,$realm) =@_;4425my$password='';4426if(exists$ENV{GIT_ASKPASS}) {4427open(PH,"-|",$ENV{GIT_ASKPASS},$prompt);4428$password= <PH>;4429$password=~s/[\012\015]//;# \n\r4430close(PH);4431}else{4432print STDERR $prompt;4433 STDERR->flush;4434require Term::ReadKey;4435 Term::ReadKey::ReadMode('noecho');4436while(defined(my$key= Term::ReadKey::ReadKey(0))) {4437last if$key=~/[\012\015]/;# \n\r4438$password.=$key;4439}4440 Term::ReadKey::ReadMode('restore');4441print STDERR "\n";4442 STDERR->flush;4443}4444$password;4445}44464447package SVN::Git::Fetcher;4448use vars qw/@ISA $_ignore_regex $_preserve_empty_dirs $_placeholder_filename4449@deleted_gpath %added_placeholder $repo_id/;4450use strict;4451use warnings;4452use Carp qw/croak/;4453use File::Basename qw/dirname/;4454use IO::File qw//;44554456# file baton members: path, mode_a, mode_b, pool, fh, blob, base4457sub new {4458my($class,$git_svn,$switch_path) =@_;4459my$self= SVN::Delta::Editor->new;4460bless$self,$class;4461if(exists$git_svn->{last_commit}) {4462$self->{c} =$git_svn->{last_commit};4463$self->{empty_symlinks} =4464 _mark_empty_symlinks($git_svn,$switch_path);4465}44664467# some options are read globally, but can be overridden locally4468# per [svn-remote "..."] section. Command-line options will *NOT*4469# override options set in an [svn-remote "..."] section4470$repo_id=$git_svn->{repo_id};4471my$k="svn-remote.$repo_id.ignore-paths";4472my$v=eval{ command_oneline('config','--get',$k) };4473$self->{ignore_regex} =$v;44744475$k="svn-remote.$repo_id.preserve-empty-dirs";4476$v=eval{ command_oneline('config','--get','--bool',$k) };4477if($v&&$veq'true') {4478$_preserve_empty_dirs=1;4479$k="svn-remote.$repo_id.placeholder-filename";4480$v=eval{ command_oneline('config','--get',$k) };4481$_placeholder_filename=$v;4482}44834484# Load the list of placeholder files added during previous invocations.4485$k="svn-remote.$repo_id.added-placeholder";4486$v=eval{ command_oneline('config','--get-all',$k) };4487if($_preserve_empty_dirs&&$v) {4488# command() prints errors to stderr, so we only call it if4489# command_oneline() succeeded.4490my@v= command('config','--get-all',$k);4491$added_placeholder{ dirname($_) } =$_foreach@v;4492}44934494$self->{empty} = {};4495$self->{dir_prop} = {};4496$self->{file_prop} = {};4497$self->{absent_dir} = {};4498$self->{absent_file} = {};4499$self->{gii} =$git_svn->tmp_index_do(sub{ Git::IndexInfo->new});4500$self->{pathnameencoding} = Git::config('svn.pathnameencoding');4501$self;4502}45034504# this uses the Ra object, so it must be called before do_{switch,update},4505# not inside them (when the Git::SVN::Fetcher object is passed) to4506# do_{switch,update}4507sub _mark_empty_symlinks {4508my($git_svn,$switch_path) =@_;4509my$bool= Git::config_bool('svn.brokenSymlinkWorkaround');4510return{}if(!defined($bool)) || (defined($bool) && !$bool);45114512my%ret;4513my($rev,$cmt) =$git_svn->last_rev_commit;4514return{}unless($rev&&$cmt);45154516# allow the warning to be printed for each revision we fetch to4517# ensure the user sees it. The user can also disable the workaround4518# on the repository even while git svn is running and the next4519# revision fetched will skip this expensive function.4520my$printed_warning;4521chomp(my$empty_blob=`git hash-object -t blob --stdin < /dev/null`);4522my($ls,$ctx) = command_output_pipe(qw/ls-tree -r -z/,$cmt);4523local$/="\0";4524my$pfx=defined($switch_path) ?$switch_path:$git_svn->{path};4525$pfx.='/'iflength($pfx);4526while(<$ls>) {4527chomp;4528s/\A100644 blob $empty_blob\t//oornext;4529unless($printed_warning) {4530print STDERR "Scanning for empty symlinks, ",4531"this may take a while if you have ",4532"many empty files\n",4533"You may disable this with `",4534"git config svn.brokenSymlinkWorkaround ",4535"false'.\n",4536"This may be done in a different ",4537"terminal without restarting ",4538"git svn\n";4539$printed_warning=1;4540}4541my$path=$_;4542my(undef,$props) =4543$git_svn->ra->get_file($pfx.$path,$rev,undef);4544if($props->{'svn:special'}) {4545$ret{$path} =1;4546}4547}4548 command_close_pipe($ls,$ctx);4549 \%ret;4550}45514552# returns true if a given path is inside a ".git" directory4553sub in_dot_git {4554$_[0] =~m{(?:^|/)\.git(?:/|$)};4555}45564557# return value: 0 -- don't ignore, 1 -- ignore4558sub is_path_ignored {4559my($self,$path) =@_;4560return1if in_dot_git($path);4561return1ifdefined($self->{ignore_regex}) &&4562$path=~m!$self->{ignore_regex}!;4563return0unlessdefined($_ignore_regex);4564return1if$path=~m!$_ignore_regex!o;4565return0;4566}45674568sub set_path_strip {4569my($self,$path) =@_;4570$self->{path_strip} =qr/^\Q$path\E(\/|$)/iflength$path;4571}45724573sub open_root {4574{ path =>''};4575}45764577sub open_directory {4578my($self,$path,$pb,$rev) =@_;4579{ path =>$path};4580}45814582sub git_path {4583my($self,$path) =@_;4584if(my$enc=$self->{pathnameencoding}) {4585require Encode;4586 Encode::from_to($path,'UTF-8',$enc);4587}4588if($self->{path_strip}) {4589$path=~s!$self->{path_strip}!!or4590die"Failed to strip path '$path' ($self->{path_strip})\n";4591}4592$path;4593}45944595sub delete_entry {4596my($self,$path,$rev,$pb) =@_;4597returnundefif$self->is_path_ignored($path);45984599my$gpath=$self->git_path($path);4600returnundefif($gpatheq'');46014602# remove entire directories.4603my($tree) = (command('ls-tree','-z',$self->{c},"./$gpath")4604=~/\A040000 tree ([a-f\d]{40})\t\Q$gpath\E\0/);4605if($tree) {4606my($ls,$ctx) = command_output_pipe(qw/ls-tree4607-r --name-only -z/,4608$tree);4609local$/="\0";4610while(<$ls>) {4611chomp;4612my$rmpath="$gpath/$_";4613$self->{gii}->remove($rmpath);4614print"\tD\t$rmpath\n"unless$::_q;4615}4616print"\tD\t$gpath/\n"unless$::_q;4617 command_close_pipe($ls,$ctx);4618}else{4619$self->{gii}->remove($gpath);4620print"\tD\t$gpath\n"unless$::_q;4621}4622# Don't add to @deleted_gpath if we're deleting a placeholder file.4623push@deleted_gpath,$gpathunless$added_placeholder{dirname($path)};4624$self->{empty}->{$path} =0;4625undef;4626}46274628sub open_file {4629my($self,$path,$pb,$rev) =@_;4630my($mode,$blob);46314632goto out if$self->is_path_ignored($path);46334634my$gpath=$self->git_path($path);4635($mode,$blob) = (command('ls-tree','-z',$self->{c},"./$gpath")4636=~/\A(\d{6}) blob ([a-f\d]{40})\t\Q$gpath\E\0/);4637unless(defined$mode&&defined$blob) {4638die"$pathwas not found in commit$self->{c} (r$rev)\n";4639}4640if($modeeq'100644'&&$self->{empty_symlinks}->{$path}) {4641$mode='120000';4642}4643out:4644{ path =>$path, mode_a =>$mode, mode_b =>$mode, blob =>$blob,4645 pool => SVN::Pool->new, action =>'M'};4646}46474648sub add_file {4649my($self,$path,$pb,$cp_path,$cp_rev) =@_;4650my$mode;46514652if(!$self->is_path_ignored($path)) {4653my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4654delete$self->{empty}->{$dir};4655$mode='100644';46564657if($added_placeholder{$dir}) {4658# Remove our placeholder file, if we created one.4659 delete_entry($self,$added_placeholder{$dir})4660unless$patheq$added_placeholder{$dir};4661delete$added_placeholder{$dir}4662}4663}46644665{ path =>$path, mode_a =>$mode, mode_b =>$mode,4666 pool => SVN::Pool->new, action =>'A'};4667}46684669sub add_directory {4670my($self,$path,$cp_path,$cp_rev) =@_;4671goto out if$self->is_path_ignored($path);4672my$gpath=$self->git_path($path);4673if($gpatheq'') {4674my($ls,$ctx) = command_output_pipe(qw/ls-tree4675-r --name-only -z/,4676$self->{c});4677local$/="\0";4678while(<$ls>) {4679chomp;4680$self->{gii}->remove($_);4681print"\tD\t$_\n"unless$::_q;4682push@deleted_gpath,$gpath;4683}4684 command_close_pipe($ls,$ctx);4685$self->{empty}->{$path} =0;4686}4687my($dir,$file) = ($path=~ m#^(.*?)/?([^/]+)$#);4688delete$self->{empty}->{$dir};4689$self->{empty}->{$path} =1;46904691if($added_placeholder{$dir}) {4692# Remove our placeholder file, if we created one.4693 delete_entry($self,$added_placeholder{$dir});4694delete$added_placeholder{$dir}4695}46964697out:4698{ path =>$path};4699}47004701sub change_dir_prop {4702my($self,$db,$prop,$value) =@_;4703returnundefif$self->is_path_ignored($db->{path});4704$self->{dir_prop}->{$db->{path}} ||= {};4705$self->{dir_prop}->{$db->{path}}->{$prop} =$value;4706undef;4707}47084709sub absent_directory {4710my($self,$path,$pb) =@_;4711returnundefif$self->is_path_ignored($path);4712$self->{absent_dir}->{$pb->{path}} ||= [];4713push@{$self->{absent_dir}->{$pb->{path}}},$path;4714undef;4715}47164717sub absent_file {4718my($self,$path,$pb) =@_;4719returnundefif$self->is_path_ignored($path);4720$self->{absent_file}->{$pb->{path}} ||= [];4721push@{$self->{absent_file}->{$pb->{path}}},$path;4722undef;4723}47244725sub change_file_prop {4726my($self,$fb,$prop,$value) =@_;4727returnundefif$self->is_path_ignored($fb->{path});4728if($propeq'svn:executable') {4729if($fb->{mode_b} !=120000) {4730$fb->{mode_b} =defined$value?100755:100644;4731}4732}elsif($propeq'svn:special') {4733$fb->{mode_b} =defined$value?120000:100644;4734}else{4735$self->{file_prop}->{$fb->{path}} ||= {};4736$self->{file_prop}->{$fb->{path}}->{$prop} =$value;4737}4738undef;4739}47404741sub apply_textdelta {4742my($self,$fb,$exp) =@_;4743returnundefif$self->is_path_ignored($fb->{path});4744my$fh= $::_repository->temp_acquire('svn_delta');4745# $fh gets auto-closed() by SVN::TxDelta::apply(),4746# (but $base does not,) so dup() it for reading in close_file4747open my$dup,'<&',$fhor croak $!;4748my$base= $::_repository->temp_acquire('git_blob');47494750if($fb->{blob}) {4751my($base_is_link,$size);47524753if($fb->{mode_a}eq'120000'&&4754!$self->{empty_symlinks}->{$fb->{path}}) {4755print$base'link 'or die"print$!\n";4756$base_is_link=1;4757}4758 retry:4759$size= $::_repository->cat_blob($fb->{blob},$base);4760die"Failed to read object$fb->{blob}"if($size<0);47614762if(defined$exp) {4763seek$base,0,0or croak $!;4764my$got= ::md5sum($base);4765if($gotne$exp) {4766my$err="Checksum mismatch: ".4767"$fb->{path}$fb->{blob}\n".4768"expected:$exp\n".4769" got:$got\n";4770if($base_is_link) {4771warn$err,4772"Retrying... (possibly ",4773"a bad symlink from SVN)\n";4774$::_repository->temp_reset($base);4775$base_is_link=0;4776goto retry;4777}4778die$err;4779}4780}4781}4782seek$base,0,0or croak $!;4783$fb->{fh} =$fh;4784$fb->{base} =$base;4785[ SVN::TxDelta::apply($base,$dup,undef,$fb->{path},$fb->{pool}) ];4786}47874788sub close_file {4789my($self,$fb,$exp) =@_;4790returnundefif$self->is_path_ignored($fb->{path});47914792my$hash;4793my$path=$self->git_path($fb->{path});4794if(my$fh=$fb->{fh}) {4795if(defined$exp) {4796seek($fh,0,0)or croak $!;4797my$got= ::md5sum($fh);4798if($gotne$exp) {4799die"Checksum mismatch:$path\n",4800"expected:$exp\ngot:$got\n";4801}4802}4803if($fb->{mode_b} ==120000) {4804sysseek($fh,0,0)or croak $!;4805my$rd=sysread($fh,my$buf,5);48064807if(!defined$rd) {4808 croak "sysread:$!\n";4809}elsif($rd==0) {4810warn"$pathhas mode 120000",4811" but it points to nothing\n",4812"converting to an empty file with mode",4813" 100644\n";4814$fb->{mode_b} ='100644';4815}elsif($bufne'link ') {4816warn"$pathhas mode 120000",4817" but is not a link\n";4818}else{4819my$tmp_fh= $::_repository->temp_acquire(4820'svn_hash');4821my$res;4822while($res=sysread($fh,my$str,1024)) {4823my$out=syswrite($tmp_fh,$str,$res);4824defined($out) &&$out==$res4825or croak("write ",4826 Git::temp_path($tmp_fh),4827":$!\n");4828}4829defined$resor croak $!;48304831($fh,$tmp_fh) = ($tmp_fh,$fh);4832 Git::temp_release($tmp_fh,1);4833}4834}48354836$hash= $::_repository->hash_and_insert_object(4837 Git::temp_path($fh));4838$hash=~/^[a-f\d]{40}$/or die"not a sha1:$hash\n";48394840 Git::temp_release($fb->{base},1);4841 Git::temp_release($fh,1);4842}else{4843$hash=$fb->{blob}or die"no blob information\n";4844}4845$fb->{pool}->clear;4846$self->{gii}->update($fb->{mode_b},$hash,$path)or croak $!;4847print"\t$fb->{action}\t$path\n"if$fb->{action} && ! $::_q;4848undef;4849}48504851sub abort_edit {4852my$self=shift;4853$self->{nr} =$self->{gii}->{nr};4854delete$self->{gii};4855$self->SUPER::abort_edit(@_);4856}48574858sub close_edit {4859my$self=shift;48604861if($_preserve_empty_dirs) {4862my@empty_dirs;48634864# Any entry flagged as empty that also has an associated4865# dir_prop represents a newly created empty directory.4866foreachmy$i(keys%{$self->{empty}}) {4867push@empty_dirs,$iifexists$self->{dir_prop}->{$i};4868}48694870# Search for directories that have become empty due subsequent4871# file deletes.4872push@empty_dirs,$self->find_empty_directories();48734874# Finally, add a placeholder file to each empty directory.4875$self->add_placeholder_file($_)foreach(@empty_dirs);48764877$self->stash_placeholder_list();4878}48794880$self->{git_commit_ok} =1;4881$self->{nr} =$self->{gii}->{nr};4882delete$self->{gii};4883$self->SUPER::close_edit(@_);4884}48854886sub find_empty_directories {4887my($self) =@_;4888my@empty_dirs;4889my%dirs=map{ dirname($_) =>1}@deleted_gpath;48904891foreachmy$dir(sort keys%dirs) {4892next if$direq".";48934894# If there have been any additions to this directory, there is4895# no reason to check if it is empty.4896my$skip_added=0;4897foreachmy$t(qw/dir_prop file_prop/) {4898foreachmy$path(keys%{$self->{$t} }) {4899if(exists$self->{$t}->{dirname($path)}) {4900$skip_added=1;4901last;4902}4903}4904last if$skip_added;4905}4906next if$skip_added;49074908# Use `git ls-tree` to get the filenames of this directory4909# that existed prior to this particular commit.4910my$ls= command('ls-tree','-z','--name-only',4911$self->{c},"$dir/");4912my%files=map{$_=>1}split(/\0/,$ls);49134914# Remove the filenames that were deleted during this commit.4915delete$files{$_}foreach(@deleted_gpath);49164917# Report the directory if there are no filenames left.4918push@empty_dirs,$dirunless(scalar%files);4919}4920@empty_dirs;4921}49224923sub add_placeholder_file {4924my($self,$dir) =@_;4925my$path="$dir/$_placeholder_filename";4926my$gpath=$self->git_path($path);49274928my$fh= $::_repository->temp_acquire($gpath);4929my$hash= $::_repository->hash_and_insert_object(Git::temp_path($fh));4930 Git::temp_release($fh,1);4931$self->{gii}->update('100644',$hash,$gpath)or croak $!;49324933# The directory should no longer be considered empty.4934delete$self->{empty}->{$dir}ifexists$self->{empty}->{$dir};49354936# Keep track of any placeholder files we create.4937$added_placeholder{$dir} =$path;4938}49394940sub stash_placeholder_list {4941my($self) =@_;4942my$k="svn-remote.$repo_id.added-placeholder";4943my$v=eval{ command_oneline('config','--get-all',$k) };4944 command_noisy('config','--unset-all',$k)if$v;4945foreach(values%added_placeholder) {4946 command_noisy('config','--add',$k,$_);4947}4948}49494950package SVN::Git::Editor;4951use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;4952use strict;4953use warnings;4954use Carp qw/croak/;4955use IO::File;49564957sub new {4958my($class,$opts) =@_;4959foreach(qw/svn_path r ra tree_a tree_b log editor_cb/) {4960die"$_required!\n"unless(defined$opts->{$_});4961}49624963my$pool= SVN::Pool->new;4964my$mods= generate_diff($opts->{tree_a},$opts->{tree_b});4965my$types= check_diff_paths($opts->{ra},$opts->{svn_path},4966$opts->{r},$mods);49674968# $opts->{ra} functions should not be used after this:4969my@ce=$opts->{ra}->get_commit_editor($opts->{log},4970$opts->{editor_cb},$pool);4971my$self= SVN::Delta::Editor->new(@ce,$pool);4972bless$self,$class;4973foreach(qw/svn_path r tree_a tree_b/) {4974$self->{$_} =$opts->{$_};4975}4976$self->{url} =$opts->{ra}->{url};4977$self->{mods} =$mods;4978$self->{types} =$types;4979$self->{pool} =$pool;4980$self->{bat} = {''=>$self->open_root($self->{r},$self->{pool}) };4981$self->{rm} = { };4982$self->{path_prefix} =length$self->{svn_path} ?4983"$self->{svn_path}/":'';4984$self->{config} =$opts->{config};4985$self->{mergeinfo} =$opts->{mergeinfo};4986return$self;4987}49884989sub generate_diff {4990my($tree_a,$tree_b) =@_;4991my@diff_tree=qw(diff-tree -z -r);4992if($_cp_similarity) {4993push@diff_tree,"-C$_cp_similarity";4994}else{4995push@diff_tree,'-C';4996}4997push@diff_tree,'--find-copies-harder'if$_find_copies_harder;4998push@diff_tree,"-l$_rename_limit"ifdefined$_rename_limit;4999push@diff_tree,$tree_a,$tree_b;5000my($diff_fh,$ctx) = command_output_pipe(@diff_tree);5001local$/="\0";5002my$state='meta';5003my@mods;5004while(<$diff_fh>) {5005chomp$_;# this gets rid of the trailing "\0"5006if($stateeq'meta'&& /^:(\d{6})\s(\d{6})\s5007($::sha1)\s($::sha1)\s5008([MTCRAD])\d*$/xo) {5009push@mods, { mode_a =>$1, mode_b =>$2,5010 sha1_a =>$3, sha1_b =>$4,5011 chg =>$5};5012if($5=~/^(?:C|R)$/) {5013$state='file_a';5014}else{5015$state='file_b';5016}5017}elsif($stateeq'file_a') {5018my$x=$mods[$#mods]or croak "Empty array\n";5019if($x->{chg} !~/^(?:C|R)$/) {5020 croak "Error parsing$_,$x->{chg}\n";5021}5022$x->{file_a} =$_;5023$state='file_b';5024}elsif($stateeq'file_b') {5025my$x=$mods[$#mods]or croak "Empty array\n";5026if(exists$x->{file_a} &&$x->{chg} !~/^(?:C|R)$/) {5027 croak "Error parsing$_,$x->{chg}\n";5028}5029if(!exists$x->{file_a} &&$x->{chg} =~/^(?:C|R)$/) {5030 croak "Error parsing$_,$x->{chg}\n";5031}5032$x->{file_b} =$_;5033$state='meta';5034}else{5035 croak "Error parsing$_\n";5036}5037}5038 command_close_pipe($diff_fh,$ctx);5039 \@mods;5040}50415042sub check_diff_paths {5043my($ra,$pfx,$rev,$mods) =@_;5044my%types;5045$pfx.='/'iflength$pfx;50465047sub type_diff_paths {5048my($ra,$types,$path,$rev) =@_;5049my@p=split m#/+#, $path;5050my$c=shift@p;5051unless(defined$types->{$c}) {5052$types->{$c} =$ra->check_path($c,$rev);5053}5054while(@p) {5055$c.='/'.shift@p;5056next ifdefined$types->{$c};5057$types->{$c} =$ra->check_path($c,$rev);5058}5059}50605061foreachmy$m(@$mods) {5062foreachmy$f(qw/file_a file_b/) {5063next unlessdefined$m->{$f};5064my($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);5065if(length$pfx.$dir&& !defined$types{$dir}) {5066 type_diff_paths($ra, \%types,$pfx.$dir,$rev);5067}5068}5069}5070 \%types;5071}50725073sub split_path {5074return($_[0] =~ m#^(.*?)/?([^/]+)$#);5075}50765077sub repo_path {5078my($self,$path) =@_;5079if(my$enc=$self->{pathnameencoding}) {5080require Encode;5081 Encode::from_to($path,$enc,'UTF-8');5082}5083$self->{path_prefix}.(defined$path?$path:'');5084}50855086sub url_path {5087my($self,$path) =@_;5088if($self->{url} =~ m#^https?://#) {5089$path=~s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;5090}5091$self->{url} .'/'.$self->repo_path($path);5092}50935094sub rmdirs {5095my($self) =@_;5096my$rm=$self->{rm};5097delete$rm->{''};# we never delete the url we're tracking5098return unless%$rm;50995100foreach(keys%$rm) {5101my@d=split m#/#, $_;5102my$c=shift@d;5103$rm->{$c} =1;5104while(@d) {5105$c.='/'.shift@d;5106$rm->{$c} =1;5107}5108}5109delete$rm->{$self->{svn_path}};5110delete$rm->{''};# we never delete the url we're tracking5111return unless%$rm;51125113my($fh,$ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,5114$self->{tree_b});5115local$/="\0";5116while(<$fh>) {5117chomp;5118my@dn=split m#/#, $_;5119while(pop@dn) {5120delete$rm->{join'/',@dn};5121}5122unless(%$rm) {5123close$fh;5124return;5125}5126}5127 command_close_pipe($fh,$ctx);51285129my($r,$p,$bat) = ($self->{r},$self->{pool},$self->{bat});5130foreachmy$d(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {5131$self->close_directory($bat->{$d},$p);5132my($dn) = ($d=~ m#^(.*?)/?(?:[^/]+)$#);5133print"\tD+\t$d/\n"unless$::_q;5134$self->SUPER::delete_entry($d,$r,$bat->{$dn},$p);5135delete$bat->{$d};5136}5137}51385139sub open_or_add_dir {5140my($self,$full_path,$baton,$deletions) =@_;5141my$t=$self->{types}->{$full_path};5142if(!defined$t) {5143die"$full_pathnot known in r$self->{r} or we have a bug!\n";5144}5145{5146no warnings 'once';5147# SVN::Node::none and SVN::Node::file are used only once,5148# so we're shutting up Perl's warnings about them.5149if($t==$SVN::Node::none ||defined($deletions->{$full_path})) {5150return$self->add_directory($full_path,$baton,5151undef, -1,$self->{pool});5152}elsif($t==$SVN::Node::dir) {5153return$self->open_directory($full_path,$baton,5154$self->{r},$self->{pool});5155}# no warnings 'once'5156print STDERR "$full_pathalready exists in repository at ",5157"r$self->{r} and it is not a directory (",5158($t==$SVN::Node::file ?'file':'unknown'),"/$t)\n";5159}# no warnings 'once'5160exit1;5161}51625163sub ensure_path {5164my($self,$path,$deletions) =@_;5165my$bat=$self->{bat};5166my$repo_path=$self->repo_path($path);5167return$bat->{''}unless(length$repo_path);51685169my@p=split m#/+#, $repo_path;5170my$c=shift@p;5171$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{''},$deletions);5172while(@p) {5173my$c0=$c;5174$c.='/'.shift@p;5175$bat->{$c} ||=$self->open_or_add_dir($c,$bat->{$c0},$deletions);5176}5177return$bat->{$c};5178}51795180# Subroutine to convert a globbing pattern to a regular expression.5181# From perl cookbook.5182sub glob2pat {5183my$globstr=shift;5184my%patmap= ('*'=>'.*','?'=>'.','['=>'[',']'=>']');5185$globstr=~s{(.)} { $patmap{$1}||"\Q$1"}ge;5186return'^'.$globstr.'$';5187}51885189sub check_autoprop {5190my($self,$pattern,$properties,$file,$fbat) =@_;5191# Convert the globbing pattern to a regular expression.5192my$regex= glob2pat($pattern);5193# Check if the pattern matches the file name.5194if($file=~m/($regex)/) {5195# Parse the list of properties to set.5196my@props=split(/;/,$properties);5197foreachmy$prop(@props) {5198# Parse 'name=value' syntax and set the property.5199if($prop=~/([^=]+)=(.*)/) {5200my($n,$v) = ($1,$2);5201for($n,$v) {5202s/^\s+//;s/\s+$//;5203}5204$self->change_file_prop($fbat,$n,$v);5205}5206}5207}5208}52095210sub apply_autoprops {5211my($self,$file,$fbat) =@_;5212my$conf_t= ${$self->{config}}{'config'};5213no warnings 'once';5214# Check [miscellany]/enable-auto-props in svn configuration.5215if(SVN::_Core::svn_config_get_bool(5216$conf_t,5217$SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY,5218$SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS,52190)) {5220# Auto-props are enabled. Enumerate them to look for matches.5221my$callback=sub{5222$self->check_autoprop($_[0],$_[1],$file,$fbat);5223};5224 SVN::_Core::svn_config_enumerate(5225$conf_t,5226$SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS,5227$callback);5228}5229}52305231sub A {5232my($self,$m,$deletions) =@_;5233my($dir,$file) = split_path($m->{file_b});5234my$pbat=$self->ensure_path($dir,$deletions);5235my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5236undef, -1);5237print"\tA\t$m->{file_b}\n"unless$::_q;5238$self->apply_autoprops($file,$fbat);5239$self->chg_file($fbat,$m);5240$self->close_file($fbat,undef,$self->{pool});5241}52425243sub C {5244my($self,$m,$deletions) =@_;5245my($dir,$file) = split_path($m->{file_b});5246my$pbat=$self->ensure_path($dir,$deletions);5247my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5248$self->url_path($m->{file_a}),$self->{r});5249print"\tC\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5250$self->chg_file($fbat,$m);5251$self->close_file($fbat,undef,$self->{pool});5252}52535254sub delete_entry {5255my($self,$path,$pbat) =@_;5256my$rpath=$self->repo_path($path);5257my($dir,$file) = split_path($rpath);5258$self->{rm}->{$dir} =1;5259$self->SUPER::delete_entry($rpath,$self->{r},$pbat,$self->{pool});5260}52615262sub R {5263my($self,$m,$deletions) =@_;5264my($dir,$file) = split_path($m->{file_b});5265my$pbat=$self->ensure_path($dir,$deletions);5266my$fbat=$self->add_file($self->repo_path($m->{file_b}),$pbat,5267$self->url_path($m->{file_a}),$self->{r});5268print"\tR\t$m->{file_a} =>$m->{file_b}\n"unless$::_q;5269$self->apply_autoprops($file,$fbat);5270$self->chg_file($fbat,$m);5271$self->close_file($fbat,undef,$self->{pool});52725273($dir,$file) = split_path($m->{file_a});5274$pbat=$self->ensure_path($dir,$deletions);5275$self->delete_entry($m->{file_a},$pbat);5276}52775278sub M {5279my($self,$m,$deletions) =@_;5280my($dir,$file) = split_path($m->{file_b});5281my$pbat=$self->ensure_path($dir,$deletions);5282my$fbat=$self->open_file($self->repo_path($m->{file_b}),5283$pbat,$self->{r},$self->{pool});5284print"\t$m->{chg}\t$m->{file_b}\n"unless$::_q;5285$self->chg_file($fbat,$m);5286$self->close_file($fbat,undef,$self->{pool});5287}52885289sub T { shift->M(@_) }52905291sub change_file_prop {5292my($self,$fbat,$pname,$pval) =@_;5293$self->SUPER::change_file_prop($fbat,$pname,$pval,$self->{pool});5294}52955296sub change_dir_prop {5297my($self,$pbat,$pname,$pval) =@_;5298$self->SUPER::change_dir_prop($pbat,$pname,$pval,$self->{pool});5299}53005301sub _chg_file_get_blob ($$$$) {5302my($self,$fbat,$m,$which) =@_;5303my$fh= $::_repository->temp_acquire("git_blob_$which");5304if($m->{"mode_$which"} =~/^120/) {5305print$fh'link 'or croak $!;5306$self->change_file_prop($fbat,'svn:special','*');5307}elsif($m->{mode_a} =~/^120/&&$m->{"mode_$which"} !~/^120/) {5308$self->change_file_prop($fbat,'svn:special',undef);5309}5310my$blob=$m->{"sha1_$which"};5311return($fh,)if($blob=~/^0{40}$/);5312my$size= $::_repository->cat_blob($blob,$fh);5313 croak "Failed to read object$blob"if($size<0);5314$fh->flush==0or croak $!;5315seek$fh,0,0or croak $!;53165317my$exp= ::md5sum($fh);5318seek$fh,0,0or croak $!;5319return($fh,$exp);5320}53215322sub chg_file {5323my($self,$fbat,$m) =@_;5324if($m->{mode_b} =~/755$/&&$m->{mode_a} !~/755$/) {5325$self->change_file_prop($fbat,'svn:executable','*');5326}elsif($m->{mode_b} !~/755$/&&$m->{mode_a} =~/755$/) {5327$self->change_file_prop($fbat,'svn:executable',undef);5328}5329my($fh_a,$exp_a) = _chg_file_get_blob $self,$fbat,$m,'a';5330my($fh_b,$exp_b) = _chg_file_get_blob $self,$fbat,$m,'b';5331my$pool= SVN::Pool->new;5332my$atd=$self->apply_textdelta($fbat,$exp_a,$pool);5333if(-s $fh_a) {5334my$txstream= SVN::TxDelta::new ($fh_a,$fh_b,$pool);5335my$res= SVN::TxDelta::send_txstream($txstream,@$atd,$pool);5336if(defined$res) {5337die"Unexpected result from send_txstream:$res\n",5338"(SVN::Core::VERSION:$SVN::Core::VERSION)\n";5339}5340}else{5341my$got= SVN::TxDelta::send_stream($fh_b,@$atd,$pool);5342die"Checksum mismatch\nexpected:$exp_b\ngot:$got\n"5343if($gotne$exp_b);5344}5345 Git::temp_release($fh_b,1);5346 Git::temp_release($fh_a,1);5347$pool->clear;5348}53495350sub D {5351my($self,$m,$deletions) =@_;5352my($dir,$file) = split_path($m->{file_b});5353my$pbat=$self->ensure_path($dir,$deletions);5354print"\tD\t$m->{file_b}\n"unless$::_q;5355$self->delete_entry($m->{file_b},$pbat);5356}53575358sub close_edit {5359my($self) =@_;5360my($p,$bat) = ($self->{pool},$self->{bat});5361foreach(sort{$b=~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {5362next if$_eq'';5363$self->close_directory($bat->{$_},$p);5364}5365$self->close_directory($bat->{''},$p);5366$self->SUPER::close_edit($p);5367$p->clear;5368}53695370sub abort_edit {5371my($self) =@_;5372$self->SUPER::abort_edit($self->{pool});5373}53745375sub DESTROY {5376my$self=shift;5377$self->SUPER::DESTROY(@_);5378$self->{pool}->clear;5379}53805381# this drives the editor5382sub apply_diff {5383my($self) =@_;5384my$mods=$self->{mods};5385my%o= ( D =>0, C =>1, R =>2, A =>3, M =>4, T =>5);5386my%deletions;53875388foreachmy$m(@$mods) {5389if($m->{chg}eq"D") {5390$deletions{$m->{file_b}} =1;5391}5392}53935394foreachmy$m(sort{$o{$a->{chg}} <=>$o{$b->{chg}} }@$mods) {5395my$f=$m->{chg};5396if(defined$o{$f}) {5397$self->$f($m, \%deletions);5398}else{5399 fatal("Invalid change type:$f");5400}5401}54025403if(defined($self->{mergeinfo})) {5404$self->change_dir_prop($self->{bat}{''},"svn:mergeinfo",5405$self->{mergeinfo});5406}5407$self->rmdirsif$_rmdir;5408if(@$mods==0&& !defined($self->{mergeinfo})) {5409$self->abort_edit;5410}else{5411$self->close_edit;5412}5413returnscalar@$mods;5414}54155416package Git::SVN::Ra;5417use vars qw/@ISA $config_dir $_ignore_refs_regex $_log_window_size/;5418use strict;5419use warnings;5420my($ra_invalid,$can_do_switch,%ignored_err,$RA);54215422BEGIN{5423# enforce temporary pool usage for some simple functions5424no strict 'refs';5425formy$f(qw/rev_proplist get_latest_revnum get_uuid get_repos_root5426 get_file/) {5427my$SUPER="SUPER::$f";5428*$f=sub{5429my$self=shift;5430my$pool= SVN::Pool->new;5431my@ret=$self->$SUPER(@_,$pool);5432$pool->clear;5433wantarray?@ret:$ret[0];5434};5435}5436}54375438sub _auth_providers () {5439[5440 SVN::Client::get_simple_provider(),5441 SVN::Client::get_ssl_server_trust_file_provider(),5442 SVN::Client::get_simple_prompt_provider(5443 \&Git::SVN::Prompt::simple,2),5444 SVN::Client::get_ssl_client_cert_file_provider(),5445 SVN::Client::get_ssl_client_cert_prompt_provider(5446 \&Git::SVN::Prompt::ssl_client_cert,2),5447 SVN::Client::get_ssl_client_cert_pw_file_provider(),5448 SVN::Client::get_ssl_client_cert_pw_prompt_provider(5449 \&Git::SVN::Prompt::ssl_client_cert_pw,2),5450 SVN::Client::get_username_provider(),5451 SVN::Client::get_ssl_server_trust_prompt_provider(5452 \&Git::SVN::Prompt::ssl_server_trust),5453 SVN::Client::get_username_prompt_provider(5454 \&Git::SVN::Prompt::username,2)5455]5456}54575458sub escape_uri_only {5459my($uri) =@_;5460my@tmp;5461foreach(splitm{/},$uri) {5462s/([^~\w.%+-]|%(?![a-fA-F0-9]{2}))/sprintf("%%%02X",ord($1))/eg;5463push@tmp,$_;5464}5465join('/',@tmp);5466}54675468sub escape_url {5469my($url) =@_;5470if($url=~ m#^(https?)://([^/]+)(.*)$#) {5471my($scheme,$domain,$uri) = ($1,$2, escape_uri_only($3));5472$url="$scheme://$domain$uri";5473}5474$url;5475}54765477sub new {5478my($class,$url) =@_;5479$url=~s!/+$!!;5480return$RAif($RA&&$RA->{url}eq$url);54815482::_req_svn();54835484 SVN::_Core::svn_config_ensure($config_dir,undef);5485my($baton,$callbacks) = SVN::Core::auth_open_helper(_auth_providers);5486my$config= SVN::Core::config_get_config($config_dir);5487$RA=undef;5488my$dont_store_passwords=1;5489my$conf_t= ${$config}{'config'};5490{5491no warnings 'once';5492# The usage of $SVN::_Core::SVN_CONFIG_* variables5493# produces warnings that variables are used only once.5494# I had not found the better way to shut them up, so5495# the warnings of type 'once' are disabled in this block.5496if(SVN::_Core::svn_config_get_bool($conf_t,5497$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5498$SVN::_Core::SVN_CONFIG_OPTION_STORE_PASSWORDS,54991) ==0) {5500 SVN::_Core::svn_auth_set_parameter($baton,5501$SVN::_Core::SVN_AUTH_PARAM_DONT_STORE_PASSWORDS,5502bless(\$dont_store_passwords,"_p_void"));5503}5504if(SVN::_Core::svn_config_get_bool($conf_t,5505$SVN::_Core::SVN_CONFIG_SECTION_AUTH,5506$SVN::_Core::SVN_CONFIG_OPTION_STORE_AUTH_CREDS,55071) ==0) {5508$Git::SVN::Prompt::_no_auth_cache =1;5509}5510}# no warnings 'once'5511my$self= SVN::Ra->new(url => escape_url($url), auth =>$baton,5512 config =>$config,5513 pool => SVN::Pool->new,5514 auth_provider_callbacks =>$callbacks);5515$self->{url} =$url;5516$self->{svn_path} =$url;5517$self->{repos_root} =$self->get_repos_root;5518$self->{svn_path} =~ s#^\Q$self->{repos_root}\E(/|$)##;5519$self->{cache} = { check_path => { r =>0, data => {} },5520 get_dir => { r =>0, data => {} } };5521$RA=bless$self,$class;5522}55235524sub check_path {5525my($self,$path,$r) =@_;5526my$cache=$self->{cache}->{check_path};5527if($r==$cache->{r} &&exists$cache->{data}->{$path}) {5528return$cache->{data}->{$path};5529}5530my$pool= SVN::Pool->new;5531my$t=$self->SUPER::check_path($path,$r,$pool);5532$pool->clear;5533if($r!=$cache->{r}) {5534%{$cache->{data}} = ();5535$cache->{r} =$r;5536}5537$cache->{data}->{$path} =$t;5538}55395540sub get_dir {5541my($self,$dir,$r) =@_;5542my$cache=$self->{cache}->{get_dir};5543if($r==$cache->{r}) {5544if(my$x=$cache->{data}->{$dir}) {5545returnwantarray?@$x:$x->[0];5546}5547}5548my$pool= SVN::Pool->new;5549my($d,undef,$props) =$self->SUPER::get_dir($dir,$r,$pool);5550my%dirents=map{$_=> { kind =>$d->{$_}->kind} }keys%$d;5551$pool->clear;5552if($r!=$cache->{r}) {5553%{$cache->{data}} = ();5554$cache->{r} =$r;5555}5556$cache->{data}->{$dir} = [ \%dirents,$r,$props];5557wantarray? (\%dirents,$r,$props) : \%dirents;5558}55595560sub DESTROY {5561# do not call the real DESTROY since we store ourselves in $RA5562}55635564# get_log(paths, start, end, limit,5565# discover_changed_paths, strict_node_history, receiver)5566sub get_log {5567my($self,@args) =@_;5568my$pool= SVN::Pool->new;55695570# svn_log_changed_path_t objects passed to get_log are likely to be5571# overwritten even if only the refs are copied to an external variable,5572# so we should dup the structures in their entirety. Using an5573# externally passed pool (instead of our temporary and quickly cleared5574# pool in Git::SVN::Ra) does not help matters at all...5575my$receiver=pop@args;5576my$prefix="/".$self->{svn_path};5577$prefix=~ s#/+($)##;5578my$prefix_regex= qr#^\Q$prefix\E#;5579push(@args,sub{5580my($paths) =$_[0];5581return&$receiver(@_)unless$paths;5582$_[0] = ();5583foreachmy$p(keys%$paths) {5584my$i=$paths->{$p};5585# Make path relative to our url, not repos_root5586$p=~s/$prefix_regex//;5587my%s=map{$_=>$i->$_; }5588 qw/copyfrom_path copyfrom_rev action/;5589if($s{'copyfrom_path'}) {5590$s{'copyfrom_path'} =~s/$prefix_regex//;5591}5592$_[0]{$p} = \%s;5593}5594&$receiver(@_);5595});559655975598# the limit parameter was not supported in SVN 1.1.x, so we5599# drop it. Therefore, the receiver callback passed to it5600# is made aware of this limitation by being wrapped if5601# the limit passed to is being wrapped.5602if($SVN::Core::VERSION le'1.2.0') {5603my$limit=splice(@args,3,1);5604if($limit>0) {5605my$receiver=pop@args;5606push(@args,sub{ &$receiver(@_)if(--$limit>=0) });5607}5608}5609my$ret=$self->SUPER::get_log(@args,$pool);5610$pool->clear;5611$ret;5612}56135614sub trees_match {5615my($self,$url1,$rev1,$url2,$rev2) =@_;5616my$ctx= SVN::Client->new(auth => _auth_providers);5617my$out= IO::File->new_tmpfile;56185619# older SVN (1.1.x) doesn't take $pool as the last parameter for5620# $ctx->diff(), so we'll create a default one5621my$pool= SVN::Pool->new_default_sub;56225623$ra_invalid=1;# this will open a new SVN::Ra connection to $url15624$ctx->diff([],$url1,$rev1,$url2,$rev2,1,1,0,$out,$out);5625$out->flush;5626my$ret= (($out->stat)[7] ==0);5627close$outor croak $!;56285629$ret;5630}56315632sub get_commit_editor {5633my($self,$log,$cb,$pool) =@_;5634my@lock=$SVN::Core::VERSION ge'1.2.0'? (undef,0) : ();5635$self->SUPER::get_commit_editor($log,$cb,@lock,$pool);5636}56375638sub gs_do_update {5639my($self,$rev_a,$rev_b,$gs,$editor) =@_;5640my$new= ($rev_a==$rev_b);5641my$path=$gs->{path};56425643if($new&& -e $gs->{index}) {5644unlink$gs->{index}or die5645"Couldn't unlink index:$gs->{index}:$!\n";5646}5647my$pool= SVN::Pool->new;5648$editor->set_path_strip($path);5649my(@pc) =split m#/#, $path;5650my$reporter=$self->do_update($rev_b, (@pc?shift@pc:''),56511,$editor,$pool);5652my@lock=$SVN::Core::VERSION ge'1.2.0'? (undef) : ();56535654# Since we can't rely on svn_ra_reparent being available, we'll5655# just have to do some magic with set_path to make it so5656# we only want a partial path.5657my$sp='';5658my$final=join('/',@pc);5659while(@pc) {5660$reporter->set_path($sp,$rev_b,0,@lock,$pool);5661$sp.='/'iflength$sp;5662$sp.=shift@pc;5663}5664die"BUG: '$sp' != '$final'\n"if($spne$final);56655666$reporter->set_path($sp,$rev_a,$new,@lock,$pool);56675668$reporter->finish_report($pool);5669$pool->clear;5670$editor->{git_commit_ok};5671}56725673# this requires SVN 1.4.3 or later (do_switch didn't work before 1.4.3, and5674# svn_ra_reparent didn't work before 1.4)5675sub gs_do_switch {5676my($self,$rev_a,$rev_b,$gs,$url_b,$editor) =@_;5677my$path=$gs->{path};5678my$pool= SVN::Pool->new;56795680my$full_url=$self->{url};5681my$old_url=$full_url;5682$full_url.='/'.$pathiflength$path;5683my($ra,$reparented);56845685if($old_url=~ m#^svn(\+ssh)?://# ||5686($full_url=~ m#^https?://# &&5687 escape_url($full_url)ne$full_url)) {5688$_[0] =undef;5689$self=undef;5690$RA=undef;5691$ra= Git::SVN::Ra->new($full_url);5692$ra_invalid=1;5693}elsif($old_urlne$full_url) {5694 SVN::_Ra::svn_ra_reparent($self->{session},$full_url,$pool);5695$self->{url} =$full_url;5696$reparented=1;5697}56985699$ra||=$self;5700$url_b= escape_url($url_b);5701my$reporter=$ra->do_switch($rev_b,'',1,$url_b,$editor,$pool);5702my@lock=$SVN::Core::VERSION ge'1.2.0'? (undef) : ();5703$reporter->set_path('',$rev_a,0,@lock,$pool);5704$reporter->finish_report($pool);57055706if($reparented) {5707 SVN::_Ra::svn_ra_reparent($self->{session},$old_url,$pool);5708$self->{url} =$old_url;5709}57105711$pool->clear;5712$editor->{git_commit_ok};5713}57145715sub longest_common_path {5716my($gsv,$globs) =@_;5717my%common;5718my$common_max=scalar@$gsv;57195720foreachmy$gs(@$gsv) {5721my@tmp=split m#/#, $gs->{path};5722my$p='';5723foreach(@tmp) {5724$p.=length($p) ?"/$_":$_;5725$common{$p} ||=0;5726$common{$p}++;5727}5728}5729$globs||= [];5730$common_max+=scalar@$globs;5731foreachmy$glob(@$globs) {5732my@tmp=split m#/#, $glob->{path}->{left};5733my$p='';5734foreach(@tmp) {5735$p.=length($p) ?"/$_":$_;5736$common{$p} ||=0;5737$common{$p}++;5738}5739}57405741my$longest_path='';5742foreach(sort{length$b<=>length$a}keys%common) {5743if($common{$_} ==$common_max) {5744$longest_path=$_;5745last;5746}5747}5748$longest_path;5749}57505751sub gs_fetch_loop_common {5752my($self,$base,$head,$gsv,$globs) =@_;5753return if($base>$head);5754my$inc=$_log_window_size;5755my($min,$max) = ($base,$head<$base+$inc?$head:$base+$inc);5756my$longest_path= longest_common_path($gsv,$globs);5757my$ra_url=$self->{url};5758my$find_trailing_edge;5759while(1) {5760my%revs;5761my$err;5762my$err_handler=$SVN::Error::handler;5763$SVN::Error::handler =sub{5764($err) =@_;5765 skip_unknown_revs($err);5766};5767sub _cb {5768my($paths,$r,$author,$date,$log) =@_;5769[$paths,5770{ author =>$author, date =>$date,log=>$log} ];5771}5772$self->get_log([$longest_path],$min,$max,0,1,1,5773sub{$revs{$_[1]} = _cb(@_) });5774if($err) {5775print"Checked through r$max\r";5776}else{5777$find_trailing_edge=1;5778}5779if($errand$find_trailing_edge) {5780print STDERR "Path '$longest_path' ",5781"was probably deleted:\n",5782$err->expanded_message,5783"\nWill attempt to follow ",5784"revisions r$min.. r$max",5785"committed before the deletion\n";5786my$hi=$max;5787while(--$hi>=$min) {5788my$ok;5789$self->get_log([$longest_path],$min,$hi,57900,1,1,sub{5791$ok=$_[1];5792$revs{$_[1]} = _cb(@_) });5793if($ok) {5794print STDERR "r$min.. r$okOK\n";5795last;5796}5797}5798$find_trailing_edge=0;5799}5800$SVN::Error::handler =$err_handler;58015802my%exists=map{$_->{path} =>$_}@$gsv;5803foreachmy$r(sort{$a<=>$b}keys%revs) {5804my($paths,$logged) = @{$revs{$r}};58055806foreachmy$gs($self->match_globs(\%exists,$paths,5807$globs,$r)) {5808if($gs->rev_map_max>=$r) {5809next;5810}5811next unless$gs->match_paths($paths,$r);5812$gs->{logged_rev_props} =$logged;5813if(my$last_commit=$gs->last_commit) {5814$gs->assert_index_clean($last_commit);5815}5816my$log_entry=$gs->do_fetch($paths,$r);5817if($log_entry) {5818$gs->do_git_commit($log_entry);5819}5820$INDEX_FILES{$gs->{index}} =1;5821}5822foreachmy$g(@$globs) {5823my$k="svn-remote.$g->{remote}.".5824"$g->{t}-maxRev";5825 Git::SVN::tmp_config($k,$r);5826}5827if($ra_invalid) {5828$_[0] =undef;5829$self=undef;5830$RA=undef;5831$self= Git::SVN::Ra->new($ra_url);5832$ra_invalid=undef;5833}5834}5835# pre-fill the .rev_db since it'll eventually get filled in5836# with '0' x40 if something new gets committed5837foreachmy$gs(@$gsv) {5838next if$gs->rev_map_max>=$max;5839next ifdefined$gs->rev_map_get($max);5840$gs->rev_map_set($max,0 x40);5841}5842foreachmy$g(@$globs) {5843my$k="svn-remote.$g->{remote}.$g->{t}-maxRev";5844 Git::SVN::tmp_config($k,$max);5845}5846last if$max>=$head;5847$min=$max+1;5848$max+=$inc;5849$max=$headif($max>$head);5850}5851 Git::SVN::gc();5852}58535854sub get_dir_globbed {5855my($self,$left,$depth,$r) =@_;58565857my@x=eval{$self->get_dir($left,$r) };5858return unlessscalar@x==3;5859my$dirents=$x[0];5860my@finalents;5861foreachmy$de(keys%$dirents) {5862next if$dirents->{$de}->{kind} !=$SVN::Node::dir;5863if($depth>1) {5864my@args= ("$left/$de",$depth-1,$r);5865foreachmy$dir($self->get_dir_globbed(@args)) {5866push@finalents,"$de/$dir";5867}5868}else{5869push@finalents,$de;5870}5871}5872@finalents;5873}58745875# return value: 0 -- don't ignore, 1 -- ignore5876sub is_ref_ignored {5877my($g,$p) =@_;5878my$refname=$g->{ref}->full_path($p);5879return1ifdefined($g->{ignore_refs_regex}) &&5880$refname=~m!$g->{ignore_refs_regex}!;5881return0unlessdefined($_ignore_refs_regex);5882return1if$refname=~m!$_ignore_refs_regex!o;5883return0;5884}58855886sub match_globs {5887my($self,$exists,$paths,$globs,$r) =@_;58885889sub get_dir_check {5890my($self,$exists,$g,$r) =@_;58915892my@dirs=$self->get_dir_globbed($g->{path}->{left},5893$g->{path}->{depth},5894$r);58955896foreachmy$de(@dirs) {5897my$p=$g->{path}->full_path($de);5898next if$exists->{$p};5899next if(length$g->{path}->{right} &&5900($self->check_path($p,$r) !=5901$SVN::Node::dir));5902next unless$p=~/$g->{path}->{regex}/;5903$exists->{$p} = Git::SVN->init($self->{url},$p,undef,5904$g->{ref}->full_path($de),1);5905}5906}5907foreachmy$g(@$globs) {5908if(my$path=$paths->{"/$g->{path}->{left}"}) {5909if($path->{action} =~/^[AR]$/) {5910 get_dir_check($self,$exists,$g,$r);5911}5912}5913foreach(keys%$paths) {5914if(/$g->{path}->{left_regex}/&&5915!/$g->{path}->{regex}/) {5916next if$paths->{$_}->{action} !~/^[AR]$/;5917 get_dir_check($self,$exists,$g,$r);5918}5919next unless/$g->{path}->{regex}/;5920my$p=$1;5921my$pathname=$g->{path}->full_path($p);5922next if is_ref_ignored($g,$p);5923next if$exists->{$pathname};5924next if($self->check_path($pathname,$r) !=5925$SVN::Node::dir);5926$exists->{$pathname} = Git::SVN->init(5927$self->{url},$pathname,undef,5928$g->{ref}->full_path($p),1);5929}5930my$c='';5931foreach(split m#/#, $g->{path}->{left}) {5932$c.="/$_";5933next unless($paths->{$c} &&5934($paths->{$c}->{action} =~/^[AR]$/));5935 get_dir_check($self,$exists,$g,$r);5936}5937}5938values%$exists;5939}59405941sub minimize_url {5942my($self) =@_;5943return$self->{url}if($self->{url}eq$self->{repos_root});5944my$url=$self->{repos_root};5945my@components=split(m!/!,$self->{svn_path});5946my$c='';5947do{5948$url.="/$c"iflength$c;5949eval{5950my$ra= (ref$self)->new($url);5951my$latest=$ra->get_latest_revnum;5952$ra->get_log("",$latest,0,1,0,1,sub{});5953};5954}while($@&& ($c=shift@components));5955$url;5956}59575958sub can_do_switch {5959my$self=shift;5960unless(defined$can_do_switch) {5961my$pool= SVN::Pool->new;5962my$rep=eval{5963$self->do_switch(1,'',0,$self->{url},5964 SVN::Delta::Editor->new,$pool);5965};5966if($@) {5967$can_do_switch=0;5968}else{5969$rep->abort_report($pool);5970$can_do_switch=1;5971}5972$pool->clear;5973}5974$can_do_switch;5975}59765977sub skip_unknown_revs {5978my($err) =@_;5979my$errno=$err->apr_err();5980# Maybe the branch we're tracking didn't5981# exist when the repo started, so it's5982# not an error if it doesn't, just continue5983#5984# Wonderfully consistent library, eh?5985# 160013 - svn:// and file://5986# 175002 - http(s)://5987# 175007 - http(s):// (this repo required authorization, too...)5988# More codes may be discovered later...5989if($errno==175007||$errno==175002||$errno==160013) {5990my$err_key=$err->expanded_message;5991# revision numbers change every time, filter them out5992$err_key=~s/\d+/\0/g;5993$err_key="$errno\0$err_key";5994unless($ignored_err{$err_key}) {5995warn"W: Ignoring error from SVN, path probably ",5996"does not exist: ($errno): ",5997$err->expanded_message,"\n";5998warn"W: Do not be alarmed at the above message ",5999"git-svn is just searching aggressively for ",6000"old history.\n",6001"This may take a while on large repositories\n";6002$ignored_err{$err_key} =1;6003}6004return;6005}6006die"Error from SVN, ($errno): ",$err->expanded_message,"\n";6007}60086009package Git::SVN::Log;6010use strict;6011use warnings;6012use POSIX qw/strftime/;6013useconstant commit_log_separator => ('-' x 72) ."\n";6014use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline6015%rusers $show_commit $incremental/;6016my$l_fmt;60176018sub cmt_showable {6019my($c) =@_;6020return1ifdefined$c->{r};60216022# big commit message got truncated by the 16k pretty buffer in rev-list6023if($c->{l} &&$c->{l}->[-1]eq"...\n"&&6024$c->{a_raw} =~/\@([a-f\d\-]+)>$/) {6025@{$c->{l}} = ();6026my@log= command(qw/cat-file commit/,$c->{c});60276028# shift off the headers6029shift@logwhile($log[0]ne'');6030shift@log;60316032# TODO: make $c->{l} not have a trailing newline in the future6033@{$c->{l}} =map{"$_\n"}grep!/^git-svn-id: /,@log;60346035(undef,$c->{r},undef) = ::extract_metadata(6036(grep(/^git-svn-id: /,@log))[-1]);6037}6038returndefined$c->{r};6039}60406041sub log_use_color {6042return$color|| Git->repository->get_colorbool('color.diff');6043}60446045sub git_svn_log_cmd {6046my($r_min,$r_max,@args) =@_;6047my$head='HEAD';6048my(@files,@log_opts);6049foreachmy$x(@args) {6050if($xeq'--'||@files) {6051push@files,$x;6052}else{6053if(::verify_ref("$x^0")) {6054$head=$x;6055}else{6056push@log_opts,$x;6057}6058}6059}60606061my($url,$rev,$uuid,$gs) = ::working_head_info($head);6062$gs||= Git::SVN->_new;6063my@cmd= (qw/log --abbrev-commit --pretty=raw --default/,6064$gs->refname);6065push@cmd,'-r'unless$non_recursive;6066push@cmd, qw/--raw --name-status/if$verbose;6067push@cmd,'--color'if log_use_color();6068push@cmd,@log_opts;6069if(defined$r_max&&$r_max==$r_min) {6070push@cmd,'--max-count=1';6071if(my$c=$gs->rev_map_get($r_max)) {6072push@cmd,$c;6073}6074}elsif(defined$r_max) {6075if($r_max<$r_min) {6076($r_min,$r_max) = ($r_max,$r_min);6077}6078my(undef,$c_max) =$gs->find_rev_before($r_max,1,$r_min);6079my(undef,$c_min) =$gs->find_rev_after($r_min,1,$r_max);6080# If there are no commits in the range, both $c_max and $c_min6081# will be undefined. If there is at least 1 commit in the6082# range, both will be defined.6083return()if!defined$c_min|| !defined$c_max;6084if($c_mineq$c_max) {6085push@cmd,'--max-count=1',$c_min;6086}else{6087push@cmd,'--boundary',"$c_min..$c_max";6088}6089}6090return(@cmd,@files);6091}60926093# adapted from pager.c6094sub config_pager {6095if(! -t *STDOUT) {6096$ENV{GIT_PAGER_IN_USE} ='false';6097$pager=undef;6098return;6099}6100chomp($pager= command_oneline(qw(var GIT_PAGER)));6101if($pagereq'cat') {6102$pager=undef;6103}6104$ENV{GIT_PAGER_IN_USE} =defined($pager);6105}61066107sub run_pager {6108return unlessdefined$pager;6109pipe my($rfd,$wfd)orreturn;6110defined(my$pid=fork)or::fatal "Can't fork:$!";6111if(!$pid) {6112open STDOUT,'>&',$wfdor6113::fatal "Can't redirect to stdout:$!";6114return;6115}6116open STDIN,'<&',$rfdor::fatal "Can't redirect stdin:$!";6117$ENV{LESS} ||='FRSX';6118exec$pageror::fatal "Can't run pager:$!($pager)";6119}61206121sub format_svn_date {6122my$t=shift||time;6123my$gmoff= Git::SVN::get_tz($t);6124return strftime("%Y-%m-%d%H:%M:%S$gmoff(%a,%d%b%Y)",localtime($t));6125}61266127sub parse_git_date {6128my($t,$tz) =@_;6129# Date::Parse isn't in the standard Perl distro :(6130if($tz=~s/^\+//) {6131$t+= tz_to_s_offset($tz);6132}elsif($tz=~s/^\-//) {6133$t-= tz_to_s_offset($tz);6134}6135return$t;6136}61376138sub set_local_timezone {6139if(defined$TZ) {6140$ENV{TZ} =$TZ;6141}else{6142delete$ENV{TZ};6143}6144}61456146sub tz_to_s_offset {6147my($tz) =@_;6148$tz=~s/(\d\d)$//;6149return($1*60) + ($tz*3600);6150}61516152sub get_author_info {6153my($dest,$author,$t,$tz) =@_;6154$author=~s/(?:^\s*|\s*$)//g;6155$dest->{a_raw} =$author;6156my$au;6157if($::_authors) {6158$au=$rusers{$author} ||undef;6159}6160if(!$au) {6161($au) = ($author=~/<([^>]+)\@[^>]+>$/);6162}6163$dest->{t} =$t;6164$dest->{tz} =$tz;6165$dest->{a} =$au;6166$dest->{t_utc} = parse_git_date($t,$tz);6167}61686169sub process_commit {6170my($c,$r_min,$r_max,$defer) =@_;6171if(defined$r_min&&defined$r_max) {6172if($r_min==$c->{r} &&$r_min==$r_max) {6173 show_commit($c);6174return0;6175}6176return1if$r_min==$r_max;6177if($r_min<$r_max) {6178# we need to reverse the print order6179return0if(defined$limit&& --$limit<0);6180push@$defer,$c;6181return1;6182}6183if($r_min!=$r_max) {6184return1if($r_min<$c->{r});6185return1if($r_max>$c->{r});6186}6187}6188return0if(defined$limit&& --$limit<0);6189 show_commit($c);6190return1;6191}61926193sub show_commit {6194my$c=shift;6195if($oneline) {6196my$x="\n";6197if(my$l=$c->{l}) {6198while($l->[0] =~/^\s*$/) {shift@$l}6199$x=$l->[0];6200}6201$l_fmt||='A'.length($c->{r});6202print'r',pack($l_fmt,$c->{r}),' | ';6203print"$c->{c} | "if$show_commit;6204print$x;6205}else{6206 show_commit_normal($c);6207}6208}62096210sub show_commit_changed_paths {6211my($c) =@_;6212return unless$c->{changed};6213print"Changed paths:\n", @{$c->{changed}};6214}62156216sub show_commit_normal {6217my($c) =@_;6218print commit_log_separator,"r$c->{r} | ";6219print"$c->{c} | "if$show_commit;6220print"$c->{a} | ", format_svn_date($c->{t_utc}),' | ';6221my$nr_line=0;62226223if(my$l=$c->{l}) {6224while($l->[$#$l]eq"\n"&&$#$l>06225&&$l->[($#$l-1)]eq"\n") {6226pop@$l;6227}6228$nr_line=scalar@$l;6229if(!$nr_line) {6230print"1 line\n\n\n";6231}else{6232if($nr_line==1) {6233$nr_line='1 line';6234}else{6235$nr_line.=' lines';6236}6237print$nr_line,"\n";6238 show_commit_changed_paths($c);6239print"\n";6240print$_foreach@$l;6241}6242}else{6243print"1 line\n";6244 show_commit_changed_paths($c);6245print"\n";62466247}6248foreachmy$x(qw/raw stat diff/) {6249if($c->{$x}) {6250print"\n";6251print$_foreach@{$c->{$x}}6252}6253}6254}62556256sub cmd_show_log {6257my(@args) =@_;6258my($r_min,$r_max);6259my$r_last= -1;# prevent dupes6260 set_local_timezone();6261if(defined$::_revision) {6262if($::_revision =~/^(\d+):(\d+)$/) {6263($r_min,$r_max) = ($1,$2);6264}elsif($::_revision =~/^\d+$/) {6265$r_min=$r_max= $::_revision;6266}else{6267::fatal "-r$::_revision is not supported, use ",6268"standard 'git log' arguments instead";6269}6270}62716272 config_pager();6273@args= git_svn_log_cmd($r_min,$r_max,@args);6274if(!@args) {6275print commit_log_separator unless$incremental||$oneline;6276return;6277}6278my$log= command_output_pipe(@args);6279 run_pager();6280my(@k,$c,$d,$stat);6281my$esc_color=qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;6282while(<$log>) {6283if(/^${esc_color}commit (?:- )?($::sha1_short)/o) {6284my$cmt=$1;6285if($c&& cmt_showable($c) &&$c->{r} !=$r_last) {6286$r_last=$c->{r};6287 process_commit($c,$r_min,$r_max, \@k)or6288goto out;6289}6290$d=undef;6291$c= { c =>$cmt};6292}elsif(/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {6293 get_author_info($c,$1,$2,$3);6294}elsif(/^${esc_color}(?:tree|parent|committer) /o) {6295# ignore6296}elsif(/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {6297push@{$c->{raw}},$_;6298}elsif(/^${esc_color}[ACRMDT]\t/) {6299# we could add $SVN->{svn_path} here, but that requires6300# remote access at the moment (repo_path_split)...6301 s#^(${esc_color})([ACRMDT])\t#$1 $2 #o;6302push@{$c->{changed}},$_;6303}elsif(/^${esc_color}diff /o) {6304$d=1;6305push@{$c->{diff}},$_;6306}elsif($d) {6307push@{$c->{diff}},$_;6308}elsif(/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*6309$esc_color*[\+\-]*$esc_color$/x) {6310$stat=1;6311push@{$c->{stat}},$_;6312}elsif($stat&&/^ \d+ files changed, \d+ insertions/) {6313push@{$c->{stat}},$_;6314$stat=undef;6315}elsif(/^${esc_color} (git-svn-id:.+)$/o) {6316($c->{url},$c->{r},undef) = ::extract_metadata($1);6317}elsif(s/^${esc_color} //o) {6318push@{$c->{l}},$_;6319}6320}6321if($c&&defined$c->{r} &&$c->{r} !=$r_last) {6322$r_last=$c->{r};6323 process_commit($c,$r_min,$r_max, \@k);6324}6325if(@k) {6326($r_min,$r_max) = ($r_max,$r_min);6327 process_commit($_,$r_min,$r_max)foreachreverse@k;6328}6329out:6330close$log;6331print commit_log_separator unless$incremental||$oneline;6332}63336334sub cmd_blame {6335my$path=pop;63366337 config_pager();6338 run_pager();63396340my($fh,$ctx,$rev);63416342if($_git_format) {6343($fh,$ctx) = command_output_pipe('blame',@_,$path);6344while(my$line= <$fh>) {6345if($line=~/^\^?([[:xdigit:]]+)\s/) {6346# Uncommitted edits show up as a rev ID of6347# all zeros, which we can't look up with6348# cmt_metadata6349if($1!~/^0+$/) {6350(undef,$rev,undef) =6351::cmt_metadata($1);6352$rev='0'if(!$rev);6353}else{6354$rev='0';6355}6356$rev=sprintf('%-10s',$rev);6357$line=~s/^\^?[[:xdigit:]]+(\s)/$rev$1/;6358}6359print$line;6360}6361}else{6362($fh,$ctx) = command_output_pipe('blame','-p',@_,'HEAD',6363'--',$path);6364my($sha1);6365my%authors;6366my@buffer;6367my%dsha;#distinct sha keys63686369while(my$line= <$fh>) {6370push@buffer,$line;6371if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6372$dsha{$1} =1;6373}6374}63756376my$s2r= ::cmt_sha2rev_batch([keys%dsha]);63776378foreachmy$line(@buffer) {6379if($line=~/^([[:xdigit:]]{40})\s\d+\s\d+/) {6380$rev=$s2r->{$1};6381$rev='0'if(!$rev)6382}6383elsif($line=~/^author (.*)/) {6384$authors{$rev} =$1;6385$authors{$rev} =~s/\s/_/g;6386}6387elsif($line=~/^\t(.*)$/) {6388printf("%6s%10s%s\n",$rev,$authors{$rev},$1);6389}6390}6391}6392 command_close_pipe($fh,$ctx);6393}63946395package Git::SVN::Migration;6396# these version numbers do NOT correspond to actual version numbers6397# of git nor git-svn. They are just relative.6398#6399# v0 layout: .git/$id/info/url, refs/heads/$id-HEAD6400#6401# v1 layout: .git/$id/info/url, refs/remotes/$id6402#6403# v2 layout: .git/svn/$id/info/url, refs/remotes/$id6404#6405# v3 layout: .git/svn/$id, refs/remotes/$id6406# - info/url may remain for backwards compatibility6407# - this is what we migrate up to this layout automatically,6408# - this will be used by git svn init on single branches6409# v3.1 layout (auto migrated):6410# - .rev_db => .rev_db.$UUID, .rev_db will remain as a symlink6411# for backwards compatibility6412#6413# v4 layout: .git/svn/$repo_id/$id, refs/remotes/$repo_id/$id6414# - this is only created for newly multi-init-ed6415# repositories. Similar in spirit to the6416# --use-separate-remotes option in git-clone (now default)6417# - we do not automatically migrate to this (following6418# the example set by core git)6419#6420# v5 layout: .rev_db.$UUID => .rev_map.$UUID6421# - newer, more-efficient format that uses 24-bytes per record6422# with no filler space.6423# - use xxd -c24 < .rev_map.$UUID to view and debug6424# - This is a one-way migration, repositories updated to the6425# new format will not be able to use old git-svn without6426# rebuilding the .rev_db. Rebuilding the rev_db is not6427# possible if noMetadata or useSvmProps are set; but should6428# be no problem for users that use the (sensible) defaults.6429use strict;6430use warnings;6431use Carp qw/croak/;6432use File::Path qw/mkpath/;6433use File::Basename qw/dirname basename/;6434use vars qw/$_minimize/;64356436sub migrate_from_v0 {6437my$git_dir=$ENV{GIT_DIR};6438returnundefunless-d $git_dir;6439my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6440my$migrated=0;6441while(<$fh>) {6442chomp;6443my($id,$orig_ref) = ($_,$_);6444next unless$id=~ s#^refs/heads/(.+)-HEAD$#$1#;6445next unless-f "$git_dir/$id/info/url";6446my$new_ref="refs/remotes/$id";6447if(::verify_ref("$new_ref^0")) {6448print STDERR "W:$orig_refis probably an old ",6449"branch used by an ancient version of ",6450"git-svn.\n",6451"However,$new_refalso exists.\n",6452"We will not be able ",6453"to use this branch until this ",6454"ambiguity is resolved.\n";6455next;6456}6457print STDERR "Migrating from v0 layout...\n"if!$migrated;6458print STDERR "Renaming ref:$orig_ref=>$new_ref\n";6459 command_noisy('update-ref',$new_ref,$orig_ref);6460 command_noisy('update-ref','-d',$orig_ref,$orig_ref);6461$migrated++;6462}6463 command_close_pipe($fh,$ctx);6464print STDERR "Done migrating from v0 layout...\n"if$migrated;6465$migrated;6466}64676468sub migrate_from_v1 {6469my$git_dir=$ENV{GIT_DIR};6470my$migrated=0;6471return$migratedunless-d $git_dir;6472my$svn_dir="$git_dir/svn";64736474# just in case somebody used 'svn' as their $id at some point...6475return$migratedif-d $svn_dir&& ! -f "$svn_dir/info/url";64766477print STDERR "Migrating from a git-svn v1 layout...\n";6478 mkpath([$svn_dir]);6479print STDERR "Data from a previous version of git-svn exists, but\n\t",6480"$svn_dir\n\t(required for this version ",6481"($::VERSION) of git-svn) does not exist.\n";6482my($fh,$ctx) = command_output_pipe(qw/rev-parse --symbolic --all/);6483while(<$fh>) {6484my$x=$_;6485next unless$x=~ s#^refs/remotes/##;6486chomp$x;6487next unless-f "$git_dir/$x/info/url";6488my$u=eval{ ::file_to_s("$git_dir/$x/info/url") };6489next unless$u;6490my$dn= dirname("$git_dir/svn/$x");6491 mkpath([$dn])unless-d $dn;6492if($xeq'svn') {# they used 'svn' as GIT_SVN_ID:6493 mkpath(["$git_dir/svn/svn"]);6494print STDERR " -$git_dir/$x/info=> ",6495"$git_dir/svn/$x/info\n";6496rename"$git_dir/$x/info","$git_dir/svn/$x/info"or6497 croak "$!:$x";6498# don't worry too much about these, they probably6499# don't exist with repos this old (save for index,6500# and we can easily regenerate that)6501foreachmy$f(qw/unhandled.log index .rev_db/) {6502rename"$git_dir/$x/$f","$git_dir/svn/$x/$f";6503}6504}else{6505print STDERR " -$git_dir/$x=>$git_dir/svn/$x\n";6506rename"$git_dir/$x","$git_dir/svn/$x"or6507 croak "$!:$x";6508}6509$migrated++;6510}6511 command_close_pipe($fh,$ctx);6512print STDERR "Done migrating from a git-svn v1 layout\n";6513$migrated;6514}65156516sub read_old_urls {6517my($l_map,$pfx,$path) =@_;6518my@dir;6519foreach(<$path/*>) {6520if(-r "$_/info/url") {6521$pfx.='/'if$pfx&&$pfx!~ m!/$!;6522my$ref_id=$pfx. basename $_;6523my$url= ::file_to_s("$_/info/url");6524$l_map->{$ref_id} =$url;6525}elsif(-d $_) {6526push@dir,$_;6527}6528}6529foreach(@dir) {6530my$x=$_;6531$x=~s!^\Q$ENV{GIT_DIR}\E/svn/!!o;6532 read_old_urls($l_map,$x,$_);6533}6534}65356536sub migrate_from_v2 {6537my@cfg= command(qw/config -l/);6538return ifgrep/^svn-remote\..+\.url=/,@cfg;6539my%l_map;6540 read_old_urls(\%l_map,'',"$ENV{GIT_DIR}/svn");6541my$migrated=0;65426543foreachmy$ref_id(sort keys%l_map) {6544eval{ Git::SVN->init($l_map{$ref_id},'',undef,$ref_id) };6545if($@) {6546 Git::SVN->init($l_map{$ref_id},'',$ref_id,$ref_id);6547}6548$migrated++;6549}6550$migrated;6551}65526553sub minimize_connections {6554my$r= Git::SVN::read_all_remotes();6555my$new_urls= {};6556my$root_repos= {};6557foreachmy$repo_id(keys%$r) {6558my$url=$r->{$repo_id}->{url}ornext;6559my$fetch=$r->{$repo_id}->{fetch}ornext;6560my$ra= Git::SVN::Ra->new($url);65616562# skip existing cases where we already connect to the root6563if(($ra->{url}eq$ra->{repos_root}) ||6564($ra->{repos_root}eq$repo_id)) {6565$root_repos->{$ra->{url}} =$repo_id;6566next;6567}65686569my$root_ra= Git::SVN::Ra->new($ra->{repos_root});6570my$root_path=$ra->{url};6571$root_path=~ s#^\Q$ra->{repos_root}\E(/|$)##;6572foreachmy$path(keys%$fetch) {6573my$ref_id=$fetch->{$path};6574my$gs= Git::SVN->new($ref_id,$repo_id,$path);65756576# make sure we can read when connecting to6577# a higher level of a repository6578my($last_rev,undef) =$gs->last_rev_commit;6579if(!defined$last_rev) {6580$last_rev=eval{6581$root_ra->get_latest_revnum;6582};6583next if$@;6584}6585my$new=$root_path;6586$new.=length$path?"/$path":'';6587eval{6588$root_ra->get_log([$new],$last_rev,$last_rev,65890,0,1,sub{ });6590};6591next if$@;6592$new_urls->{$ra->{repos_root}}->{$new} =6593{ ref_id =>$ref_id,6594 old_repo_id =>$repo_id,6595 old_path =>$path};6596}6597}65986599my@emptied;6600foreachmy$url(keys%$new_urls) {6601# see if we can re-use an existing [svn-remote "repo_id"]6602# instead of creating a(n ugly) new section:6603my$repo_id=$root_repos->{$url} ||$url;66046605my$fetch=$new_urls->{$url};6606foreachmy$path(keys%$fetch) {6607my$x=$fetch->{$path};6608 Git::SVN->init($url,$path,$repo_id,$x->{ref_id});6609my$pfx="svn-remote.$x->{old_repo_id}";66106611my$old_fetch=quotemeta("$x->{old_path}:".6612"$x->{ref_id}");6613 command_noisy(qw/config --unset/,6614"$pfx.fetch",'^'.$old_fetch.'$');6615delete$r->{$x->{old_repo_id}}->6616{fetch}->{$x->{old_path}};6617if(!keys%{$r->{$x->{old_repo_id}}->{fetch}}) {6618 command_noisy(qw/config --unset/,6619"$pfx.url");6620push@emptied,$x->{old_repo_id}6621}6622}6623}6624if(@emptied) {6625my$file=$ENV{GIT_CONFIG} ||"$ENV{GIT_DIR}/config";6626print STDERR <<EOF;6627The following [svn-remote] sections in your config file ($file) are empty6628and can be safely removed:6629EOF6630print STDERR "[svn-remote\"$_\"]\n"foreach@emptied;6631}6632}66336634sub migration_check {6635 migrate_from_v0();6636 migrate_from_v1();6637 migrate_from_v2();6638 minimize_connections()if$_minimize;6639}66406641package Git::IndexInfo;6642use strict;6643use warnings;6644use Git qw/command_input_pipe command_close_pipe/;66456646sub new {6647my($class) =@_;6648my($gui,$ctx) = command_input_pipe(qw/update-index -z --index-info/);6649bless{ gui =>$gui, ctx =>$ctx, nr =>0},$class;6650}66516652sub remove {6653my($self,$path) =@_;6654if(print{$self->{gui} }'0 ',0 x 40,"\t",$path,"\0") {6655return++$self->{nr};6656}6657undef;6658}66596660sub update {6661my($self,$mode,$hash,$path) =@_;6662if(print{$self->{gui} }$mode,' ',$hash,"\t",$path,"\0") {6663return++$self->{nr};6664}6665undef;6666}66676668sub DESTROY {6669my($self) =@_;6670 command_close_pipe($self->{gui},$self->{ctx});6671}66726673package Git::SVN::GlobSpec;6674use strict;6675use warnings;66766677sub new {6678my($class,$glob,$pattern_ok) =@_;6679my$re=$glob;6680$re=~s!/+$!!g;# no need for trailing slashes6681my(@left,@right,@patterns);6682my$state="left";6683my$die_msg="Only one set of wildcard directories ".6684"(e.g. '*' or '*/*/*') is supported: '$glob'\n";6685formy$part(split(m|/|,$glob)) {6686if($part=~/\*/&&$partne"*") {6687die"Invalid pattern in '$glob':$part\n";6688}elsif($pattern_ok&&$part=~/[{}]/&&6689$part!~/^\{[^{}]+\}/) {6690die"Invalid pattern in '$glob':$part\n";6691}6692if($parteq"*") {6693die$die_msgif$stateeq"right";6694$state="pattern";6695push(@patterns,"[^/]*");6696}elsif($pattern_ok&&$part=~/^\{(.*)\}$/) {6697die$die_msgif$stateeq"right";6698$state="pattern";6699my$p=quotemeta($1);6700$p=~s/\\,/|/g;6701push(@patterns,"(?:$p)");6702}else{6703if($stateeq"left") {6704push(@left,$part);6705}else{6706push(@right,$part);6707$state="right";6708}6709}6710}6711my$depth=@patterns;6712if($depth==0) {6713die"One '*' is needed in glob: '$glob'\n";6714}6715my$left=join('/',@left);6716my$right=join('/',@right);6717$re=join('/',@patterns);6718$re=join('\/',6719grep(length,quotemeta($left),"($re)",quotemeta($right)));6720my$left_re=qr/^\/\Q$left\E(\/|$)/;6721bless{ left =>$left, right =>$right, left_regex =>$left_re,6722 regex =>qr/$re/,glob=>$glob, depth =>$depth},$class;6723}67246725sub full_path {6726my($self,$path) =@_;6727return(length$self->{left} ?"$self->{left}/":'') .6728$path. (length$self->{right} ?"/$self->{right}":'');6729}67306731__END__67326733Data structures:673467356736$remotes= {# returned by read_all_remotes()6737'svn'=> {6738# svn-remote.svn.url=https://svn.musicpd.org6739 url =>'https://svn.musicpd.org',6740# svn-remote.svn.fetch=mpd/trunk:trunk6741 fetch => {6742'mpd/trunk'=>'trunk',6743},6744# svn-remote.svn.tags=mpd/tags/*:tags/*6745 tags => {6746 path => {6747 left =>'mpd/tags',6748 right =>'',6749 regex =>qr!mpd/tags/([^/]+)$!,6750glob=>'tags/*',6751},6752ref=> {6753 left =>'tags',6754 right =>'',6755 regex =>qr!tags/([^/]+)$!,6756glob=>'tags/*',6757},6758}6759}6760};67616762$log_entry hashref as returned by libsvn_log_entry()6763{6764log=>'whitespace-formatted log entry6765',# trailing newline is preserved6766 revision =>'8',# integer6767 date =>'2004-02-24T17:01:44.108345Z',# commit date6768 author =>'committer name'6769};677067716772# this is generated by generate_diff();6773@mods= array of diff-index line hashes,each element represents one line6774 of diff-index output67756776diff-index line ($m hash)6777{6778 mode_a => first column of diff-index output,no leading ':',6779 mode_b => second column of diff-index output,6780 sha1_b => sha1sum of the final blob,6781 chg => change type [MCRADT],6782 file_a => original file name of a file (iff chg is'C'or'R')6783 file_b => new/current file name of a file (any chg)6784}6785;67866787# retval of read_url_paths{,_all}();6788$l_map= {6789# repository root url6790'https://svn.musicpd.org'=> {6791# repository path # GIT_SVN_ID6792'mpd/trunk'=>'trunk',6793'mpd/tags/0.11.5'=>'tags/0.11.5',6794},6795}67966797Notes:6798 I don't trust the each() function on unless I created%hashmyself6799 because the internal iterator may not have started at base.