git-svn: make Git::SVN::Editor a separate file

This makes the git-svn script shorter and less scary for beginners to read through for the first time. Take the opportunity to explain the purpose and basic interface of the Git::SVN::Editor class while at it. Signed-off-by: Jonathan Nieder <jrnieder@gmail.com> Signed-off-by: Eric Wong <normalperson@yhbt.net>

Jonathan Nieder committed Jun 9, 2012 at 17:25 UTC 8f9facfe94536209fbcfd6151cbcc1cd67fd6e0a
4 files changed +541 -471
git-svn.perl
+2 -470
@@ -68,7 +68,6 @@ sub _req_svn {
68 }
69 my $can_compress = eval { require Compress::Zlib; 1};
70 push @Git::SVN::Ra::ISA, 'SVN::Ra';
71 -push @Git::SVN::Editor::ISA, 'SVN::Delta::Editor';
71 use Carp qw/croak/;
72 use Digest::MD5;
73 use IO::File qw//;
@@ -79,6 +78,7 @@ use File::Find;
78 use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
79 use IPC::Open3;
80 use Git;
81 +use Git::SVN::Editor qw//;
82 use Git::SVN::Fetcher qw//;
83 use Git::SVN::Prompt qw//;
84 use Memoize; # core since 5.8.0, Jul 2002
@@ -89,8 +89,7 @@ BEGIN {
89 foreach (qw/command command_oneline command_noisy command_output_pipe
90 command_input_pipe command_close_pipe
91 command_bidi_pipe command_close_bidi_pipe/) {
92 - for my $package ( qw(Git::SVN::Editor
93 - Git::SVN::Migration Git::SVN::Log Git::SVN),
92 + for my $package ( qw(Git::SVN::Migration Git::SVN::Log Git::SVN),
93 __PACKAGE__) {
94 *{"${package}::$_"} = \&{"Git::$_"};
95 }
@@ -1066,7 +1065,6 @@ sub cmd_branch {
1065 " with the --destination argument.\n";
1066 }
1067 foreach my $g (@{$allglobs}) {
1069 - # Git::SVN::Editor could probably be moved to Git.pm..
1068 my $re = Git::SVN::Editor::glob2pat($g->{path}->{left});
1069 if ($_branch_dest =~ /$re/) {
1070 $glob = $g;
@@ -4328,472 +4326,6 @@ sub remove_username {
4326 $_[0] =~ s{^([^:]*://)[^@]+@}{$1};
4327 }
4328
4331 -package Git::SVN::Editor;
4332 -use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;
4333 -use strict;
4334 -use warnings;
4335 -use Carp qw/croak/;
4336 -use IO::File;
4337 -
4338 -sub new {
4339 - my ($class, $opts) = @_;
4340 - foreach (qw/svn_path r ra tree_a tree_b log editor_cb/) {
4341 - die "$_ required!\n" unless (defined $opts->{$_});
4342 - }
4343 -
4344 - my $pool = SVN::Pool->new;
4345 - my $mods = generate_diff($opts->{tree_a}, $opts->{tree_b});
4346 - my $types = check_diff_paths($opts->{ra}, $opts->{svn_path},
4347 - $opts->{r}, $mods);
4348 -
4349 - # $opts->{ra} functions should not be used after this:
4350 - my @ce = $opts->{ra}->get_commit_editor($opts->{log},
4351 - $opts->{editor_cb}, $pool);
4352 - my $self = SVN::Delta::Editor->new(@ce, $pool);
4353 - bless $self, $class;
4354 - foreach (qw/svn_path r tree_a tree_b/) {
4355 - $self->{$_} = $opts->{$_};
4356 - }
4357 - $self->{url} = $opts->{ra}->{url};
4358 - $self->{mods} = $mods;
4359 - $self->{types} = $types;
4360 - $self->{pool} = $pool;
4361 - $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
4362 - $self->{rm} = { };
4363 - $self->{path_prefix} = length $self->{svn_path} ?
4364 - "$self->{svn_path}/" : '';
4365 - $self->{config} = $opts->{config};
4366 - $self->{mergeinfo} = $opts->{mergeinfo};
4367 - return $self;
4368 -}
4369 -
4370 -sub generate_diff {
4371 - my ($tree_a, $tree_b) = @_;
4372 - my @diff_tree = qw(diff-tree -z -r);
4373 - if ($_cp_similarity) {
4374 - push @diff_tree, "-C$_cp_similarity";
4375 - } else {
4376 - push @diff_tree, '-C';
4377 - }
4378 - push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
4379 - push @diff_tree, "-l$_rename_limit" if defined $_rename_limit;
4380 - push @diff_tree, $tree_a, $tree_b;
4381 - my ($diff_fh, $ctx) = command_output_pipe(@diff_tree);
4382 - local $/ = "\0";
4383 - my $state = 'meta';
4384 - my @mods;
4385 - while (<$diff_fh>) {
4386 - chomp $_; # this gets rid of the trailing "\0"
4387 - if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
4388 - ($::sha1)\s($::sha1)\s
4389 - ([MTCRAD])\d*$/xo) {
4390 - push @mods, { mode_a => $1, mode_b => $2,
4391 - sha1_a => $3, sha1_b => $4,
4392 - chg => $5 };
4393 - if ($5 =~ /^(?:C|R)$/) {
4394 - $state = 'file_a';
4395 - } else {
4396 - $state = 'file_b';
4397 - }
4398 - } elsif ($state eq 'file_a') {
4399 - my $x = $mods[$#mods] or croak "Empty array\n";
4400 - if ($x->{chg} !~ /^(?:C|R)$/) {
4401 - croak "Error parsing $_, $x->{chg}\n";
4402 - }
4403 - $x->{file_a} = $_;
4404 - $state = 'file_b';
4405 - } elsif ($state eq 'file_b') {
4406 - my $x = $mods[$#mods] or croak "Empty array\n";
4407 - if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
4408 - croak "Error parsing $_, $x->{chg}\n";
4409 - }
4410 - if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
4411 - croak "Error parsing $_, $x->{chg}\n";
4412 - }
4413 - $x->{file_b} = $_;
4414 - $state = 'meta';
4415 - } else {
4416 - croak "Error parsing $_\n";
4417 - }
4418 - }
4419 - command_close_pipe($diff_fh, $ctx);
4420 - \@mods;
4421 -}
4422 -
4423 -sub check_diff_paths {
4424 - my ($ra, $pfx, $rev, $mods) = @_;
4425 - my %types;
4426 - $pfx .= '/' if length $pfx;
4427 -
4428 - sub type_diff_paths {
4429 - my ($ra, $types, $path, $rev) = @_;
4430 - my @p = split m#/+#, $path;
4431 - my $c = shift @p;
4432 - unless (defined $types->{$c}) {
4433 - $types->{$c} = $ra->check_path($c, $rev);
4434 - }
4435 - while (@p) {
4436 - $c .= '/' . shift @p;
4437 - next if defined $types->{$c};
4438 - $types->{$c} = $ra->check_path($c, $rev);
4439 - }
4440 - }
4441 -
4442 - foreach my $m (@$mods) {
4443 - foreach my $f (qw/file_a file_b/) {
4444 - next unless defined $m->{$f};
4445 - my ($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);
4446 - if (length $pfx.$dir && ! defined $types{$dir}) {
4447 - type_diff_paths($ra, \%types, $pfx.$dir, $rev);
4448 - }
4449 - }
4450 - }
4451 - \%types;
4452 -}
4453 -
4454 -sub split_path {
4455 - return ($_[0] =~ m#^(.*?)/?([^/]+)$#);
4456 -}
4457 -
4458 -sub repo_path {
4459 - my ($self, $path) = @_;
4460 - if (my $enc = $self->{pathnameencoding}) {
4461 - require Encode;
4462 - Encode::from_to($path, $enc, 'UTF-8');
4463 - }
4464 - $self->{path_prefix}.(defined $path ? $path : '');
4465 -}
4466 -
4467 -sub url_path {
4468 - my ($self, $path) = @_;
4469 - if ($self->{url} =~ m#^https?://#) {
4470 - $path =~ s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;
4471 - }
4472 - $self->{url} . '/' . $self->repo_path($path);
4473 -}
4474 -
4475 -sub rmdirs {
4476 - my ($self) = @_;
4477 - my $rm = $self->{rm};
4478 - delete $rm->{''}; # we never delete the url we're tracking
4479 - return unless %$rm;
4480 -
4481 - foreach (keys %$rm) {
4482 - my @d = split m#/#, $_;
4483 - my $c = shift @d;
4484 - $rm->{$c} = 1;
4485 - while (@d) {
4486 - $c .= '/' . shift @d;
4487 - $rm->{$c} = 1;
4488 - }
4489 - }
4490 - delete $rm->{$self->{svn_path}};
4491 - delete $rm->{''}; # we never delete the url we're tracking
4492 - return unless %$rm;
4493 -
4494 - my ($fh, $ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,
4495 - $self->{tree_b});
4496 - local $/ = "\0";
4497 - while (<$fh>) {
4498 - chomp;
4499 - my @dn = split m#/#, $_;
4500 - while (pop @dn) {
4501 - delete $rm->{join '/', @dn};
4502 - }
4503 - unless (%$rm) {
4504 - close $fh;
4505 - return;
4506 - }
4507 - }
4508 - command_close_pipe($fh, $ctx);
4509 -
4510 - my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat});
4511 - foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
4512 - $self->close_directory($bat->{$d}, $p);
4513 - my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#);
4514 - print "\tD+\t$d/\n" unless $::_q;
4515 - $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p);
4516 - delete $bat->{$d};
4517 - }
4518 -}
4519 -
4520 -sub open_or_add_dir {
4521 - my ($self, $full_path, $baton, $deletions) = @_;
4522 - my $t = $self->{types}->{$full_path};
4523 - if (!defined $t) {
4524 - die "$full_path not known in r$self->{r} or we have a bug!\n";
4525 - }
4526 - {
4527 - no warnings 'once';
4528 - # SVN::Node::none and SVN::Node::file are used only once,
4529 - # so we're shutting up Perl's warnings about them.
4530 - if ($t == $SVN::Node::none || defined($deletions->{$full_path})) {
4531 - return $self->add_directory($full_path, $baton,
4532 - undef, -1, $self->{pool});
4533 - } elsif ($t == $SVN::Node::dir) {
4534 - return $self->open_directory($full_path, $baton,
4535 - $self->{r}, $self->{pool});
4536 - } # no warnings 'once'
4537 - print STDERR "$full_path already exists in repository at ",
4538 - "r$self->{r} and it is not a directory (",
4539 - ($t == $SVN::Node::file ? 'file' : 'unknown'),"/$t)\n";
4540 - } # no warnings 'once'
4541 - exit 1;
4542 -}
4543 -
4544 -sub ensure_path {
4545 - my ($self, $path, $deletions) = @_;
4546 - my $bat = $self->{bat};
4547 - my $repo_path = $self->repo_path($path);
4548 - return $bat->{''} unless (length $repo_path);
4549 -
4550 - my @p = split m#/+#, $repo_path;
4551 - my $c = shift @p;
4552 - $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''}, $deletions);
4553 - while (@p) {
4554 - my $c0 = $c;
4555 - $c .= '/' . shift @p;
4556 - $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0}, $deletions);
4557 - }
4558 - return $bat->{$c};
4559 -}
4560 -
4561 -# Subroutine to convert a globbing pattern to a regular expression.
4562 -# From perl cookbook.
4563 -sub glob2pat {
4564 - my $globstr = shift;
4565 - my %patmap = ('*' => '.*', '?' => '.', '[' => '[', ']' => ']');
4566 - $globstr =~ s{(.)} { $patmap{$1} || "\Q$1" }ge;
4567 - return '^' . $globstr . '$';
4568 -}
4569 -
4570 -sub check_autoprop {
4571 - my ($self, $pattern, $properties, $file, $fbat) = @_;
4572 - # Convert the globbing pattern to a regular expression.
4573 - my $regex = glob2pat($pattern);
4574 - # Check if the pattern matches the file name.
4575 - if($file =~ m/($regex)/) {
4576 - # Parse the list of properties to set.
4577 - my @props = split(/;/, $properties);
4578 - foreach my $prop (@props) {
4579 - # Parse 'name=value' syntax and set the property.
4580 - if ($prop =~ /([^=]+)=(.*)/) {
4581 - my ($n,$v) = ($1,$2);
4582 - for ($n, $v) {
4583 - s/^\s+//; s/\s+$//;
4584 - }
4585 - $self->change_file_prop($fbat, $n, $v);
4586 - }
4587 - }
4588 - }
4589 -}
4590 -
4591 -sub apply_autoprops {
4592 - my ($self, $file, $fbat) = @_;
4593 - my $conf_t = ${$self->{config}}{'config'};
4594 - no warnings 'once';
4595 - # Check [miscellany]/enable-auto-props in svn configuration.
4596 - if (SVN::_Core::svn_config_get_bool(
4597 - $conf_t,
4598 - $SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY,
4599 - $SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS,
4600 - 0)) {
4601 - # Auto-props are enabled. Enumerate them to look for matches.
4602 - my $callback = sub {
4603 - $self->check_autoprop($_[0], $_[1], $file, $fbat);
4604 - };
4605 - SVN::_Core::svn_config_enumerate(
4606 - $conf_t,
4607 - $SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS,
4608 - $callback);
4609 - }
4610 -}
4611 -
4612 -sub A {
4613 - my ($self, $m, $deletions) = @_;
4614 - my ($dir, $file) = split_path($m->{file_b});
4615 - my $pbat = $self->ensure_path($dir, $deletions);
4616 - my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
4617 - undef, -1);
4618 - print "\tA\t$m->{file_b}\n" unless $::_q;
4619 - $self->apply_autoprops($file, $fbat);
4620 - $self->chg_file($fbat, $m);
4621 - $self->close_file($fbat,undef,$self->{pool});
4622 -}
4623 -
4624 -sub C {
4625 - my ($self, $m, $deletions) = @_;
4626 - my ($dir, $file) = split_path($m->{file_b});
4627 - my $pbat = $self->ensure_path($dir, $deletions);
4628 - my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
4629 - $self->url_path($m->{file_a}), $self->{r});
4630 - print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
4631 - $self->chg_file($fbat, $m);
4632 - $self->close_file($fbat,undef,$self->{pool});
4633 -}
4634 -
4635 -sub delete_entry {
4636 - my ($self, $path, $pbat) = @_;
4637 - my $rpath = $self->repo_path($path);
4638 - my ($dir, $file) = split_path($rpath);
4639 - $self->{rm}->{$dir} = 1;
4640 - $self->SUPER::delete_entry($rpath, $self->{r}, $pbat, $self->{pool});
4641 -}
4642 -
4643 -sub R {
4644 - my ($self, $m, $deletions) = @_;
4645 - my ($dir, $file) = split_path($m->{file_b});
4646 - my $pbat = $self->ensure_path($dir, $deletions);
4647 - my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
4648 - $self->url_path($m->{file_a}), $self->{r});
4649 - print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
4650 - $self->apply_autoprops($file, $fbat);
4651 - $self->chg_file($fbat, $m);
4652 - $self->close_file($fbat,undef,$self->{pool});
4653 -
4654 - ($dir, $file) = split_path($m->{file_a});
4655 - $pbat = $self->ensure_path($dir, $deletions);
4656 - $self->delete_entry($m->{file_a}, $pbat);
4657 -}
4658 -
4659 -sub M {
4660 - my ($self, $m, $deletions) = @_;
4661 - my ($dir, $file) = split_path($m->{file_b});
4662 - my $pbat = $self->ensure_path($dir, $deletions);
4663 - my $fbat = $self->open_file($self->repo_path($m->{file_b}),
4664 - $pbat,$self->{r},$self->{pool});
4665 - print "\t$m->{chg}\t$m->{file_b}\n" unless $::_q;
4666 - $self->chg_file($fbat, $m);
4667 - $self->close_file($fbat,undef,$self->{pool});
4668 -}
4669 -
4670 -sub T { shift->M(@_) }
4671 -
4672 -sub change_file_prop {
4673 - my ($self, $fbat, $pname, $pval) = @_;
4674 - $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool});
4675 -}
4676 -
4677 -sub change_dir_prop {
4678 - my ($self, $pbat, $pname, $pval) = @_;
4679 - $self->SUPER::change_dir_prop($pbat, $pname, $pval, $self->{pool});
4680 -}
4681 -
4682 -sub _chg_file_get_blob ($$$$) {
4683 - my ($self, $fbat, $m, $which) = @_;
4684 - my $fh = $::_repository->temp_acquire("git_blob_$which");
4685 - if ($m->{"mode_$which"} =~ /^120/) {
4686 - print $fh 'link ' or croak $!;
4687 - $self->change_file_prop($fbat,'svn:special','*');
4688 - } elsif ($m->{mode_a} =~ /^120/ && $m->{"mode_$which"} !~ /^120/) {
4689 - $self->change_file_prop($fbat,'svn:special',undef);
4690 - }
4691 - my $blob = $m->{"sha1_$which"};
4692 - return ($fh,) if ($blob =~ /^0{40}$/);
4693 - my $size = $::_repository->cat_blob($blob, $fh);
4694 - croak "Failed to read object $blob" if ($size < 0);
4695 - $fh->flush == 0 or croak $!;
4696 - seek $fh, 0, 0 or croak $!;
4697 -
4698 - my $exp = ::md5sum($fh);
4699 - seek $fh, 0, 0 or croak $!;
4700 - return ($fh, $exp);
4701 -}
4702 -
4703 -sub chg_file {
4704 - my ($self, $fbat, $m) = @_;
4705 - if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
4706 - $self->change_file_prop($fbat,'svn:executable','*');
4707 - } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
4708 - $self->change_file_prop($fbat,'svn:executable',undef);
4709 - }
4710 - my ($fh_a, $exp_a) = _chg_file_get_blob $self, $fbat, $m, 'a';
4711 - my ($fh_b, $exp_b) = _chg_file_get_blob $self, $fbat, $m, 'b';
4712 - my $pool = SVN::Pool->new;
4713 - my $atd = $self->apply_textdelta($fbat, $exp_a, $pool);
4714 - if (-s $fh_a) {
4715 - my $txstream = SVN::TxDelta::new ($fh_a, $fh_b, $pool);
4716 - my $res = SVN::TxDelta::send_txstream($txstream, @$atd, $pool);
4717 - if (defined $res) {
4718 - die "Unexpected result from send_txstream: $res\n",
4719 - "(SVN::Core::VERSION: $SVN::Core::VERSION)\n";
4720 - }
4721 - } else {
4722 - my $got = SVN::TxDelta::send_stream($fh_b, @$atd, $pool);
4723 - die "Checksum mismatch\nexpected: $exp_b\ngot: $got\n"
4724 - if ($got ne $exp_b);
4725 - }
4726 - Git::temp_release($fh_b, 1);
4727 - Git::temp_release($fh_a, 1);
4728 - $pool->clear;
4729 -}
4730 -
4731 -sub D {
4732 - my ($self, $m, $deletions) = @_;
4733 - my ($dir, $file) = split_path($m->{file_b});
4734 - my $pbat = $self->ensure_path($dir, $deletions);
4735 - print "\tD\t$m->{file_b}\n" unless $::_q;
4736 - $self->delete_entry($m->{file_b}, $pbat);
4737 -}
4738 -
4739 -sub close_edit {
4740 - my ($self) = @_;
4741 - my ($p,$bat) = ($self->{pool}, $self->{bat});
4742 - foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {
4743 - next if $_ eq '';
4744 - $self->close_directory($bat->{$_}, $p);
4745 - }
4746 - $self->close_directory($bat->{''}, $p);
4747 - $self->SUPER::close_edit($p);
4748 - $p->clear;
4749 -}
4750 -
4751 -sub abort_edit {
4752 - my ($self) = @_;
4753 - $self->SUPER::abort_edit($self->{pool});
4754 -}
4755 -
4756 -sub DESTROY {
4757 - my $self = shift;
4758 - $self->SUPER::DESTROY(@_);
4759 - $self->{pool}->clear;
4760 -}
4761 -
4762 -# this drives the editor
4763 -sub apply_diff {
4764 - my ($self) = @_;
4765 - my $mods = $self->{mods};
4766 - my %o = ( D => 0, C => 1, R => 2, A => 3, M => 4, T => 5 );
4767 - my %deletions;
4768 -
4769 - foreach my $m (@$mods) {
4770 - if ($m->{chg} eq "D") {
4771 - $deletions{$m->{file_b}} = 1;
4772 - }
4773 - }
4774 -
4775 - foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
4776 - my $f = $m->{chg};
4777 - if (defined $o{$f}) {
4778 - $self->$f($m, \%deletions);
4779 - } else {
4780 - fatal("Invalid change type: $f");
4781 - }
4782 - }
4783 -
4784 - if (defined($self->{mergeinfo})) {
4785 - $self->change_dir_prop($self->{bat}{''}, "svn:mergeinfo",
4786 - $self->{mergeinfo});
4787 - }
4788 - $self->rmdirs if $_rmdir;
4789 - if (@$mods == 0 && !defined($self->{mergeinfo})) {
4790 - $self->abort_edit;
4791 - } else {
4792 - $self->close_edit;
4793 - }
4794 - return scalar @$mods;
4795 -}
4796 -
4329 package Git::SVN::Ra;
4330 use vars qw/@ISA $config_dir $_ignore_refs_regex $_log_window_size/;
4331 use strict;
perl/Git/SVN/Editor.pm new
+536
@@ -0,0 +1,536 @@
1 +package Git::SVN::Editor;
2 +use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;
3 +use strict;
4 +use warnings;
5 +use SVN::Core;
6 +use SVN::Delta;
7 +use Carp qw/croak/;
8 +use IO::File;
9 +use Git qw/command command_oneline command_noisy command_output_pipe
10 + command_input_pipe command_close_pipe
11 + command_bidi_pipe command_close_bidi_pipe/;
12 +BEGIN {
13 + @ISA = qw(SVN::Delta::Editor);
14 +}
15 +
16 +sub new {
17 + my ($class, $opts) = @_;
18 + foreach (qw/svn_path r ra tree_a tree_b log editor_cb/) {
19 + die "$_ required!\n" unless (defined $opts->{$_});
20 + }
21 +
22 + my $pool = SVN::Pool->new;
23 + my $mods = generate_diff($opts->{tree_a}, $opts->{tree_b});
24 + my $types = check_diff_paths($opts->{ra}, $opts->{svn_path},
25 + $opts->{r}, $mods);
26 +
27 + # $opts->{ra} functions should not be used after this:
28 + my @ce = $opts->{ra}->get_commit_editor($opts->{log},
29 + $opts->{editor_cb}, $pool);
30 + my $self = SVN::Delta::Editor->new(@ce, $pool);
31 + bless $self, $class;
32 + foreach (qw/svn_path r tree_a tree_b/) {
33 + $self->{$_} = $opts->{$_};
34 + }
35 + $self->{url} = $opts->{ra}->{url};
36 + $self->{mods} = $mods;
37 + $self->{types} = $types;
38 + $self->{pool} = $pool;
39 + $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
40 + $self->{rm} = { };
41 + $self->{path_prefix} = length $self->{svn_path} ?
42 + "$self->{svn_path}/" : '';
43 + $self->{config} = $opts->{config};
44 + $self->{mergeinfo} = $opts->{mergeinfo};
45 + return $self;
46 +}
47 +
48 +sub generate_diff {
49 + my ($tree_a, $tree_b) = @_;
50 + my @diff_tree = qw(diff-tree -z -r);
51 + if ($_cp_similarity) {
52 + push @diff_tree, "-C$_cp_similarity";
53 + } else {
54 + push @diff_tree, '-C';
55 + }
56 + push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
57 + push @diff_tree, "-l$_rename_limit" if defined $_rename_limit;
58 + push @diff_tree, $tree_a, $tree_b;
59 + my ($diff_fh, $ctx) = command_output_pipe(@diff_tree);
60 + local $/ = "\0";
61 + my $state = 'meta';
62 + my @mods;
63 + while (<$diff_fh>) {
64 + chomp $_; # this gets rid of the trailing "\0"
65 + if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
66 + ($::sha1)\s($::sha1)\s
67 + ([MTCRAD])\d*$/xo) {
68 + push @mods, { mode_a => $1, mode_b => $2,
69 + sha1_a => $3, sha1_b => $4,
70 + chg => $5 };
71 + if ($5 =~ /^(?:C|R)$/) {
72 + $state = 'file_a';
73 + } else {
74 + $state = 'file_b';
75 + }
76 + } elsif ($state eq 'file_a') {
77 + my $x = $mods[$#mods] or croak "Empty array\n";
78 + if ($x->{chg} !~ /^(?:C|R)$/) {
79 + croak "Error parsing $_, $x->{chg}\n";
80 + }
81 + $x->{file_a} = $_;
82 + $state = 'file_b';
83 + } elsif ($state eq 'file_b') {
84 + my $x = $mods[$#mods] or croak "Empty array\n";
85 + if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
86 + croak "Error parsing $_, $x->{chg}\n";
87 + }
88 + if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
89 + croak "Error parsing $_, $x->{chg}\n";
90 + }
91 + $x->{file_b} = $_;
92 + $state = 'meta';
93 + } else {
94 + croak "Error parsing $_\n";
95 + }
96 + }
97 + command_close_pipe($diff_fh, $ctx);
98 + \@mods;
99 +}
100 +
101 +sub check_diff_paths {
102 + my ($ra, $pfx, $rev, $mods) = @_;
103 + my %types;
104 + $pfx .= '/' if length $pfx;
105 +
106 + sub type_diff_paths {
107 + my ($ra, $types, $path, $rev) = @_;
108 + my @p = split m#/+#, $path;
109 + my $c = shift @p;
110 + unless (defined $types->{$c}) {
111 + $types->{$c} = $ra->check_path($c, $rev);
112 + }
113 + while (@p) {
114 + $c .= '/' . shift @p;
115 + next if defined $types->{$c};
116 + $types->{$c} = $ra->check_path($c, $rev);
117 + }
118 + }
119 +
120 + foreach my $m (@$mods) {
121 + foreach my $f (qw/file_a file_b/) {
122 + next unless defined $m->{$f};
123 + my ($dir) = ($m->{$f} =~ m#^(.*?)/?(?:[^/]+)$#);
124 + if (length $pfx.$dir && ! defined $types{$dir}) {
125 + type_diff_paths($ra, \%types, $pfx.$dir, $rev);
126 + }
127 + }
128 + }
129 + \%types;
130 +}
131 +
132 +sub split_path {
133 + return ($_[0] =~ m#^(.*?)/?([^/]+)$#);
134 +}
135 +
136 +sub repo_path {
137 + my ($self, $path) = @_;
138 + if (my $enc = $self->{pathnameencoding}) {
139 + require Encode;
140 + Encode::from_to($path, $enc, 'UTF-8');
141 + }
142 + $self->{path_prefix}.(defined $path ? $path : '');
143 +}
144 +
145 +sub url_path {
146 + my ($self, $path) = @_;
147 + if ($self->{url} =~ m#^https?://#) {
148 + $path =~ s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg;
149 + }
150 + $self->{url} . '/' . $self->repo_path($path);
151 +}
152 +
153 +sub rmdirs {
154 + my ($self) = @_;
155 + my $rm = $self->{rm};
156 + delete $rm->{''}; # we never delete the url we're tracking
157 + return unless %$rm;
158 +
159 + foreach (keys %$rm) {
160 + my @d = split m#/#, $_;
161 + my $c = shift @d;
162 + $rm->{$c} = 1;
163 + while (@d) {
164 + $c .= '/' . shift @d;
165 + $rm->{$c} = 1;
166 + }
167 + }
168 + delete $rm->{$self->{svn_path}};
169 + delete $rm->{''}; # we never delete the url we're tracking
170 + return unless %$rm;
171 +
172 + my ($fh, $ctx) = command_output_pipe(qw/ls-tree --name-only -r -z/,
173 + $self->{tree_b});
174 + local $/ = "\0";
175 + while (<$fh>) {
176 + chomp;
177 + my @dn = split m#/#, $_;
178 + while (pop @dn) {
179 + delete $rm->{join '/', @dn};
180 + }
181 + unless (%$rm) {
182 + close $fh;
183 + return;
184 + }
185 + }
186 + command_close_pipe($fh, $ctx);
187 +
188 + my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat});
189 + foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
190 + $self->close_directory($bat->{$d}, $p);
191 + my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#);
192 + print "\tD+\t$d/\n" unless $::_q;
193 + $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p);
194 + delete $bat->{$d};
195 + }
196 +}
197 +
198 +sub open_or_add_dir {
199 + my ($self, $full_path, $baton, $deletions) = @_;
200 + my $t = $self->{types}->{$full_path};
201 + if (!defined $t) {
202 + die "$full_path not known in r$self->{r} or we have a bug!\n";
203 + }
204 + {
205 + no warnings 'once';
206 + # SVN::Node::none and SVN::Node::file are used only once,
207 + # so we're shutting up Perl's warnings about them.
208 + if ($t == $SVN::Node::none || defined($deletions->{$full_path})) {
209 + return $self->add_directory($full_path, $baton,
210 + undef, -1, $self->{pool});
211 + } elsif ($t == $SVN::Node::dir) {
212 + return $self->open_directory($full_path, $baton,
213 + $self->{r}, $self->{pool});
214 + } # no warnings 'once'
215 + print STDERR "$full_path already exists in repository at ",
216 + "r$self->{r} and it is not a directory (",
217 + ($t == $SVN::Node::file ? 'file' : 'unknown'),"/$t)\n";
218 + } # no warnings 'once'
219 + exit 1;
220 +}
221 +
222 +sub ensure_path {
223 + my ($self, $path, $deletions) = @_;
224 + my $bat = $self->{bat};
225 + my $repo_path = $self->repo_path($path);
226 + return $bat->{''} unless (length $repo_path);
227 +
228 + my @p = split m#/+#, $repo_path;
229 + my $c = shift @p;
230 + $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''}, $deletions);
231 + while (@p) {
232 + my $c0 = $c;
233 + $c .= '/' . shift @p;
234 + $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0}, $deletions);
235 + }
236 + return $bat->{$c};
237 +}
238 +
239 +# Subroutine to convert a globbing pattern to a regular expression.
240 +# From perl cookbook.
241 +sub glob2pat {
242 + my $globstr = shift;
243 + my %patmap = ('*' => '.*', '?' => '.', '[' => '[', ']' => ']');
244 + $globstr =~ s{(.)} { $patmap{$1} || "\Q$1" }ge;
245 + return '^' . $globstr . '$';
246 +}
247 +
248 +sub check_autoprop {
249 + my ($self, $pattern, $properties, $file, $fbat) = @_;
250 + # Convert the globbing pattern to a regular expression.
251 + my $regex = glob2pat($pattern);
252 + # Check if the pattern matches the file name.
253 + if($file =~ m/($regex)/) {
254 + # Parse the list of properties to set.
255 + my @props = split(/;/, $properties);
256 + foreach my $prop (@props) {
257 + # Parse 'name=value' syntax and set the property.
258 + if ($prop =~ /([^=]+)=(.*)/) {
259 + my ($n,$v) = ($1,$2);
260 + for ($n, $v) {
261 + s/^\s+//; s/\s+$//;
262 + }
263 + $self->change_file_prop($fbat, $n, $v);
264 + }
265 + }
266 + }
267 +}
268 +
269 +sub apply_autoprops {
270 + my ($self, $file, $fbat) = @_;
271 + my $conf_t = ${$self->{config}}{'config'};
272 + no warnings 'once';
273 + # Check [miscellany]/enable-auto-props in svn configuration.
274 + if (SVN::_Core::svn_config_get_bool(
275 + $conf_t,
276 + $SVN::_Core::SVN_CONFIG_SECTION_MISCELLANY,
277 + $SVN::_Core::SVN_CONFIG_OPTION_ENABLE_AUTO_PROPS,
278 + 0)) {
279 + # Auto-props are enabled. Enumerate them to look for matches.
280 + my $callback = sub {
281 + $self->check_autoprop($_[0], $_[1], $file, $fbat);
282 + };
283 + SVN::_Core::svn_config_enumerate(
284 + $conf_t,
285 + $SVN::_Core::SVN_CONFIG_SECTION_AUTO_PROPS,
286 + $callback);
287 + }
288 +}
289 +
290 +sub A {
291 + my ($self, $m, $deletions) = @_;
292 + my ($dir, $file) = split_path($m->{file_b});
293 + my $pbat = $self->ensure_path($dir, $deletions);
294 + my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
295 + undef, -1);
296 + print "\tA\t$m->{file_b}\n" unless $::_q;
297 + $self->apply_autoprops($file, $fbat);
298 + $self->chg_file($fbat, $m);
299 + $self->close_file($fbat,undef,$self->{pool});
300 +}
301 +
302 +sub C {
303 + my ($self, $m, $deletions) = @_;
304 + my ($dir, $file) = split_path($m->{file_b});
305 + my $pbat = $self->ensure_path($dir, $deletions);
306 + my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
307 + $self->url_path($m->{file_a}), $self->{r});
308 + print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
309 + $self->chg_file($fbat, $m);
310 + $self->close_file($fbat,undef,$self->{pool});
311 +}
312 +
313 +sub delete_entry {
314 + my ($self, $path, $pbat) = @_;
315 + my $rpath = $self->repo_path($path);
316 + my ($dir, $file) = split_path($rpath);
317 + $self->{rm}->{$dir} = 1;
318 + $self->SUPER::delete_entry($rpath, $self->{r}, $pbat, $self->{pool});
319 +}
320 +
321 +sub R {
322 + my ($self, $m, $deletions) = @_;
323 + my ($dir, $file) = split_path($m->{file_b});
324 + my $pbat = $self->ensure_path($dir, $deletions);
325 + my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
326 + $self->url_path($m->{file_a}), $self->{r});
327 + print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $::_q;
328 + $self->apply_autoprops($file, $fbat);
329 + $self->chg_file($fbat, $m);
330 + $self->close_file($fbat,undef,$self->{pool});
331 +
332 + ($dir, $file) = split_path($m->{file_a});
333 + $pbat = $self->ensure_path($dir, $deletions);
334 + $self->delete_entry($m->{file_a}, $pbat);
335 +}
336 +
337 +sub M {
338 + my ($self, $m, $deletions) = @_;
339 + my ($dir, $file) = split_path($m->{file_b});
340 + my $pbat = $self->ensure_path($dir, $deletions);
341 + my $fbat = $self->open_file($self->repo_path($m->{file_b}),
342 + $pbat,$self->{r},$self->{pool});
343 + print "\t$m->{chg}\t$m->{file_b}\n" unless $::_q;
344 + $self->chg_file($fbat, $m);
345 + $self->close_file($fbat,undef,$self->{pool});
346 +}
347 +
348 +sub T { shift->M(@_) }
349 +
350 +sub change_file_prop {
351 + my ($self, $fbat, $pname, $pval) = @_;
352 + $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool});
353 +}
354 +
355 +sub change_dir_prop {
356 + my ($self, $pbat, $pname, $pval) = @_;
357 + $self->SUPER::change_dir_prop($pbat, $pname, $pval, $self->{pool});
358 +}
359 +
360 +sub _chg_file_get_blob ($$$$) {
361 + my ($self, $fbat, $m, $which) = @_;
362 + my $fh = $::_repository->temp_acquire("git_blob_$which");
363 + if ($m->{"mode_$which"} =~ /^120/) {
364 + print $fh 'link ' or croak $!;
365 + $self->change_file_prop($fbat,'svn:special','*');
366 + } elsif ($m->{mode_a} =~ /^120/ && $m->{"mode_$which"} !~ /^120/) {
367 + $self->change_file_prop($fbat,'svn:special',undef);
368 + }
369 + my $blob = $m->{"sha1_$which"};
370 + return ($fh,) if ($blob =~ /^0{40}$/);
371 + my $size = $::_repository->cat_blob($blob, $fh);
372 + croak "Failed to read object $blob" if ($size < 0);
373 + $fh->flush == 0 or croak $!;
374 + seek $fh, 0, 0 or croak $!;
375 +
376 + my $exp = ::md5sum($fh);
377 + seek $fh, 0, 0 or croak $!;
378 + return ($fh, $exp);
379 +}
380 +
381 +sub chg_file {
382 + my ($self, $fbat, $m) = @_;
383 + if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
384 + $self->change_file_prop($fbat,'svn:executable','*');
385 + } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
386 + $self->change_file_prop($fbat,'svn:executable',undef);
387 + }
388 + my ($fh_a, $exp_a) = _chg_file_get_blob $self, $fbat, $m, 'a';
389 + my ($fh_b, $exp_b) = _chg_file_get_blob $self, $fbat, $m, 'b';
390 + my $pool = SVN::Pool->new;
391 + my $atd = $self->apply_textdelta($fbat, $exp_a, $pool);
392 + if (-s $fh_a) {
393 + my $txstream = SVN::TxDelta::new ($fh_a, $fh_b, $pool);
394 + my $res = SVN::TxDelta::send_txstream($txstream, @$atd, $pool);
395 + if (defined $res) {
396 + die "Unexpected result from send_txstream: $res\n",
397 + "(SVN::Core::VERSION: $SVN::Core::VERSION)\n";
398 + }
399 + } else {
400 + my $got = SVN::TxDelta::send_stream($fh_b, @$atd, $pool);
401 + die "Checksum mismatch\nexpected: $exp_b\ngot: $got\n"
402 + if ($got ne $exp_b);
403 + }
404 + Git::temp_release($fh_b, 1);
405 + Git::temp_release($fh_a, 1);
406 + $pool->clear;
407 +}
408 +
409 +sub D {
410 + my ($self, $m, $deletions) = @_;
411 + my ($dir, $file) = split_path($m->{file_b});
412 + my $pbat = $self->ensure_path($dir, $deletions);
413 + print "\tD\t$m->{file_b}\n" unless $::_q;
414 + $self->delete_entry($m->{file_b}, $pbat);
415 +}
416 +
417 +sub close_edit {
418 + my ($self) = @_;
419 + my ($p,$bat) = ($self->{pool}, $self->{bat});
420 + foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {
421 + next if $_ eq '';
422 + $self->close_directory($bat->{$_}, $p);
423 + }
424 + $self->close_directory($bat->{''}, $p);
425 + $self->SUPER::close_edit($p);
426 + $p->clear;
427 +}
428 +
429 +sub abort_edit {
430 + my ($self) = @_;
431 + $self->SUPER::abort_edit($self->{pool});
432 +}
433 +
434 +sub DESTROY {
435 + my $self = shift;
436 + $self->SUPER::DESTROY(@_);
437 + $self->{pool}->clear;
438 +}
439 +
440 +# this drives the editor
441 +sub apply_diff {
442 + my ($self) = @_;
443 + my $mods = $self->{mods};
444 + my %o = ( D => 0, C => 1, R => 2, A => 3, M => 4, T => 5 );
445 + my %deletions;
446 +
447 + foreach my $m (@$mods) {
448 + if ($m->{chg} eq "D") {
449 + $deletions{$m->{file_b}} = 1;
450 + }
451 + }
452 +
453 + foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
454 + my $f = $m->{chg};
455 + if (defined $o{$f}) {
456 + $self->$f($m, \%deletions);
457 + } else {
458 + fatal("Invalid change type: $f");
459 + }
460 + }
461 +
462 + if (defined($self->{mergeinfo})) {
463 + $self->change_dir_prop($self->{bat}{''}, "svn:mergeinfo",
464 + $self->{mergeinfo});
465 + }
466 + $self->rmdirs if $_rmdir;
467 + if (@$mods == 0 && !defined($self->{mergeinfo})) {
468 + $self->abort_edit;
469 + } else {
470 + $self->close_edit;
471 + }
472 + return scalar @$mods;
473 +}
474 +
475 +1;
476 +__END__
477 +
478 +Git::SVN::Editor - commit driver for "git svn set-tree" and dcommit
479 +
480 +=head1 SYNOPSIS
481 +
482 + use Git::SVN::Editor;
483 + use Git::SVN::Ra;
484 +
485 + my $ra = Git::SVN::Ra->new($url);
486 + my %opts = (
487 + r => 19,
488 + log => "log message",
489 + ra => $ra,
490 + config => SVN::Core::config_get_config($svn_config_dir),
491 + tree_a => "$commit^",
492 + tree_b => "$commit",
493 + editor_cb => sub { print "Committed r$_[0]\n"; },
494 + mergeinfo => "/branches/foo:1-10",
495 + svn_path => "trunk"
496 + );
497 + Git::SVN::Editor->new(\%opts)->apply_diff or print "No changes\n";
498 +
499 + my $re = Git::SVN::Editor::glob2pat("trunk/*");
500 + if ($branchname =~ /$re/) {
501 + print "matched!\n";
502 + }
503 +
504 +=head1 DESCRIPTION
505 +
506 +This module is an implementation detail of the "git svn" command.
507 +Do not use it unless you are developing git-svn.
508 +
509 +This module adapts the C<SVN::Delta::Editor> object returned by
510 +C<SVN::Delta::get_commit_editor> and drives it to convey the
511 +difference between two git tree objects to a remote Subversion
512 +repository.
513 +
514 +The interface will change as git-svn evolves.
515 +
516 +=head1 DEPENDENCIES
517 +
518 +Subversion perl bindings,
519 +the core L<Carp> and L<IO::File> modules,
520 +and git's L<Git> helper module.
521 +
522 +C<Git::SVN::Editor> has not been tested using callers other than
523 +B<git-svn> itself.
524 +
525 +=head1 SEE ALSO
526 +
527 +L<SVN::Delta>,
528 +L<Git::SVN::Fetcher>.
529 +
530 +=head1 INCOMPATIBILITIES
531 +
532 +None reported.
533 +
534 +=head1 BUGS
535 +
536 +None.
perl/Git/SVN/Fetcher.pm
+2 -1
@@ -591,7 +591,8 @@ B<git-svn> itself.
591
592 =head1 SEE ALSO
593
594 -L<SVN::Delta>.
594 +L<SVN::Delta>,
595 +L<Git::SVN::Editor>.
596
597 =head1 INCOMPATIBILITIES
598
perl/Makefile.PL
+1
@@ -28,6 +28,7 @@ my %pm = (
28 'Git.pm' => '$(INST_LIBDIR)/Git.pm',
29 'Git/I18N.pm' => '$(INST_LIBDIR)/Git/I18N.pm',
30 'Git/SVN/Fetcher.pm' => '$(INST_LIBDIR)/Git/SVN/Fetcher.pm',
31 + 'Git/SVN/Editor.pm' => '$(INST_LIBDIR)/Git/SVN/Editor.pm',
32 'Git/SVN/Prompt.pm' => '$(INST_LIBDIR)/Git/SVN/Prompt.pm',
33 );
34