git-svn: make Git::SVN::Fetcher a separate file
This patch removes a chunk of code (the Git::SVN::Fetcher consumer of libsvn's tree delta protocol) from git-svn.perl and documents its interface so the hurried reader does not have to read that code right away. Signed-off-by: Jonathan Nieder <jrnieder@gmail.com> Signed-off-by: Eric Wong <normalperson@yhbt.net>
Jonathan Nieder committed
May 28, 2012 at 02:03 UTC
a6180325e885e2226d68144000e8bb18a906a2a6
3 files changed
+605
-505
git-svn.perl
+2
-505
@@ -69,7 +69,6 @@ sub _req_svn {
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';
72
-push @Git::SVN::Fetcher::ISA, 'SVN::Delta::Editor';
72
use Carp qw/croak/;
73
use Digest::MD5;
74
use IO::File qw//;
@@ -80,6 +79,7 @@ use File::Find;
79
use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
80
use IPC::Open3;
81
use Git;
82
+use Git::SVN::Fetcher qw//;
83
use Git::SVN::Prompt qw//;
84
use Memoize; # core since 5.8.0, Jul 2002
85
@@ -89,7 +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 Git::SVN::Fetcher
92
+ for my $package ( qw(Git::SVN::Editor
93
Git::SVN::Migration Git::SVN::Log Git::SVN),
94
__PACKAGE__) {
95
*{"${package}::$_"} = \&{"Git::$_"};
@@ -4328,509 +4328,6 @@ sub remove_username {
4328
$_[0] =~ s{^([^:]*://)[^@]+@}{$1};
4329
}
4330
4331
-package Git::SVN::Fetcher;
4332
-use vars qw/@ISA $_ignore_regex $_preserve_empty_dirs $_placeholder_filename
4333
- @deleted_gpath %added_placeholder $repo_id/;
4334
-use strict;
4335
-use warnings;
4336
-use Carp qw/croak/;
4337
-use File::Basename qw/dirname/;
4338
-use IO::File qw//;
4339
-
4340
-# file baton members: path, mode_a, mode_b, pool, fh, blob, base
4341
-sub new {
4342
- my ($class, $git_svn, $switch_path) = @_;
4343
- my $self = SVN::Delta::Editor->new;
4344
- bless $self, $class;
4345
- if (exists $git_svn->{last_commit}) {
4346
- $self->{c} = $git_svn->{last_commit};
4347
- $self->{empty_symlinks} =
4348
- _mark_empty_symlinks($git_svn, $switch_path);
4349
- }
4350
-
4351
- # some options are read globally, but can be overridden locally
4352
- # per [svn-remote "..."] section. Command-line options will *NOT*
4353
- # override options set in an [svn-remote "..."] section
4354
- $repo_id = $git_svn->{repo_id};
4355
- my $k = "svn-remote.$repo_id.ignore-paths";
4356
- my $v = eval { command_oneline('config', '--get', $k) };
4357
- $self->{ignore_regex} = $v;
4358
-
4359
- $k = "svn-remote.$repo_id.preserve-empty-dirs";
4360
- $v = eval { command_oneline('config', '--get', '--bool', $k) };
4361
- if ($v && $v eq 'true') {
4362
- $_preserve_empty_dirs = 1;
4363
- $k = "svn-remote.$repo_id.placeholder-filename";
4364
- $v = eval { command_oneline('config', '--get', $k) };
4365
- $_placeholder_filename = $v;
4366
- }
4367
-
4368
- # Load the list of placeholder files added during previous invocations.
4369
- $k = "svn-remote.$repo_id.added-placeholder";
4370
- $v = eval { command_oneline('config', '--get-all', $k) };
4371
- if ($_preserve_empty_dirs && $v) {
4372
- # command() prints errors to stderr, so we only call it if
4373
- # command_oneline() succeeded.
4374
- my @v = command('config', '--get-all', $k);
4375
- $added_placeholder{ dirname($_) } = $_ foreach @v;
4376
- }
4377
-
4378
- $self->{empty} = {};
4379
- $self->{dir_prop} = {};
4380
- $self->{file_prop} = {};
4381
- $self->{absent_dir} = {};
4382
- $self->{absent_file} = {};
4383
- $self->{gii} = $git_svn->tmp_index_do(sub { Git::IndexInfo->new });
4384
- $self->{pathnameencoding} = Git::config('svn.pathnameencoding');
4385
- $self;
4386
-}
4387
-
4388
-# this uses the Ra object, so it must be called before do_{switch,update},
4389
-# not inside them (when the Git::SVN::Fetcher object is passed) to
4390
-# do_{switch,update}
4391
-sub _mark_empty_symlinks {
4392
- my ($git_svn, $switch_path) = @_;
4393
- my $bool = Git::config_bool('svn.brokenSymlinkWorkaround');
4394
- return {} if (!defined($bool)) || (defined($bool) && ! $bool);
4395
-
4396
- my %ret;
4397
- my ($rev, $cmt) = $git_svn->last_rev_commit;
4398
- return {} unless ($rev && $cmt);
4399
-
4400
- # allow the warning to be printed for each revision we fetch to
4401
- # ensure the user sees it. The user can also disable the workaround
4402
- # on the repository even while git svn is running and the next
4403
- # revision fetched will skip this expensive function.
4404
- my $printed_warning;
4405
- chomp(my $empty_blob = `git hash-object -t blob --stdin < /dev/null`);
4406
- my ($ls, $ctx) = command_output_pipe(qw/ls-tree -r -z/, $cmt);
4407
- local $/ = "\0";
4408
- my $pfx = defined($switch_path) ? $switch_path : $git_svn->{path};
4409
- $pfx .= '/' if length($pfx);
4410
- while (<$ls>) {
4411
- chomp;
4412
- s/\A100644 blob $empty_blob\t//o or next;
4413
- unless ($printed_warning) {
4414
- print STDERR "Scanning for empty symlinks, ",
4415
- "this may take a while if you have ",
4416
- "many empty files\n",
4417
- "You may disable this with `",
4418
- "git config svn.brokenSymlinkWorkaround ",
4419
- "false'.\n",
4420
- "This may be done in a different ",
4421
- "terminal without restarting ",
4422
- "git svn\n";
4423
- $printed_warning = 1;
4424
- }
4425
- my $path = $_;
4426
- my (undef, $props) =
4427
- $git_svn->ra->get_file($pfx.$path, $rev, undef);
4428
- if ($props->{'svn:special'}) {
4429
- $ret{$path} = 1;
4430
- }
4431
- }
4432
- command_close_pipe($ls, $ctx);
4433
- \%ret;
4434
-}
4435
-
4436
-# returns true if a given path is inside a ".git" directory
4437
-sub in_dot_git {
4438
- $_[0] =~ m{(?:^|/)\.git(?:/|$)};
4439
-}
4440
-
4441
-# return value: 0 -- don't ignore, 1 -- ignore
4442
-sub is_path_ignored {
4443
- my ($self, $path) = @_;
4444
- return 1 if in_dot_git($path);
4445
- return 1 if defined($self->{ignore_regex}) &&
4446
- $path =~ m!$self->{ignore_regex}!;
4447
- return 0 unless defined($_ignore_regex);
4448
- return 1 if $path =~ m!$_ignore_regex!o;
4449
- return 0;
4450
-}
4451
-
4452
-sub set_path_strip {
4453
- my ($self, $path) = @_;
4454
- $self->{path_strip} = qr/^\Q$path\E(\/|$)/ if length $path;
4455
-}
4456
-
4457
-sub open_root {
4458
- { path => '' };
4459
-}
4460
-
4461
-sub open_directory {
4462
- my ($self, $path, $pb, $rev) = @_;
4463
- { path => $path };
4464
-}
4465
-
4466
-sub git_path {
4467
- my ($self, $path) = @_;
4468
- if (my $enc = $self->{pathnameencoding}) {
4469
- require Encode;
4470
- Encode::from_to($path, 'UTF-8', $enc);
4471
- }
4472
- if ($self->{path_strip}) {
4473
- $path =~ s!$self->{path_strip}!! or
4474
- die "Failed to strip path '$path' ($self->{path_strip})\n";
4475
- }
4476
- $path;
4477
-}
4478
-
4479
-sub delete_entry {
4480
- my ($self, $path, $rev, $pb) = @_;
4481
- return undef if $self->is_path_ignored($path);
4482
-
4483
- my $gpath = $self->git_path($path);
4484
- return undef if ($gpath eq '');
4485
-
4486
- # remove entire directories.
4487
- my ($tree) = (command('ls-tree', '-z', $self->{c}, "./$gpath")
4488
- =~ /\A040000 tree ([a-f\d]{40})\t\Q$gpath\E\0/);
4489
- if ($tree) {
4490
- my ($ls, $ctx) = command_output_pipe(qw/ls-tree
4491
- -r --name-only -z/,
4492
- $tree);
4493
- local $/ = "\0";
4494
- while (<$ls>) {
4495
- chomp;
4496
- my $rmpath = "$gpath/$_";
4497
- $self->{gii}->remove($rmpath);
4498
- print "\tD\t$rmpath\n" unless $::_q;
4499
- }
4500
- print "\tD\t$gpath/\n" unless $::_q;
4501
- command_close_pipe($ls, $ctx);
4502
- } else {
4503
- $self->{gii}->remove($gpath);
4504
- print "\tD\t$gpath\n" unless $::_q;
4505
- }
4506
- # Don't add to @deleted_gpath if we're deleting a placeholder file.
4507
- push @deleted_gpath, $gpath unless $added_placeholder{dirname($path)};
4508
- $self->{empty}->{$path} = 0;
4509
- undef;
4510
-}
4511
-
4512
-sub open_file {
4513
- my ($self, $path, $pb, $rev) = @_;
4514
- my ($mode, $blob);
4515
-
4516
- goto out if $self->is_path_ignored($path);
4517
-
4518
- my $gpath = $self->git_path($path);
4519
- ($mode, $blob) = (command('ls-tree', '-z', $self->{c}, "./$gpath")
4520
- =~ /\A(\d{6}) blob ([a-f\d]{40})\t\Q$gpath\E\0/);
4521
- unless (defined $mode && defined $blob) {
4522
- die "$path was not found in commit $self->{c} (r$rev)\n";
4523
- }
4524
- if ($mode eq '100644' && $self->{empty_symlinks}->{$path}) {
4525
- $mode = '120000';
4526
- }
4527
-out:
4528
- { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob,
4529
- pool => SVN::Pool->new, action => 'M' };
4530
-}
4531
-
4532
-sub add_file {
4533
- my ($self, $path, $pb, $cp_path, $cp_rev) = @_;
4534
- my $mode;
4535
-
4536
- if (!$self->is_path_ignored($path)) {
4537
- my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
4538
- delete $self->{empty}->{$dir};
4539
- $mode = '100644';
4540
-
4541
- if ($added_placeholder{$dir}) {
4542
- # Remove our placeholder file, if we created one.
4543
- delete_entry($self, $added_placeholder{$dir})
4544
- unless $path eq $added_placeholder{$dir};
4545
- delete $added_placeholder{$dir}
4546
- }
4547
- }
4548
-
4549
- { path => $path, mode_a => $mode, mode_b => $mode,
4550
- pool => SVN::Pool->new, action => 'A' };
4551
-}
4552
-
4553
-sub add_directory {
4554
- my ($self, $path, $cp_path, $cp_rev) = @_;
4555
- goto out if $self->is_path_ignored($path);
4556
- my $gpath = $self->git_path($path);
4557
- if ($gpath eq '') {
4558
- my ($ls, $ctx) = command_output_pipe(qw/ls-tree
4559
- -r --name-only -z/,
4560
- $self->{c});
4561
- local $/ = "\0";
4562
- while (<$ls>) {
4563
- chomp;
4564
- $self->{gii}->remove($_);
4565
- print "\tD\t$_\n" unless $::_q;
4566
- push @deleted_gpath, $gpath;
4567
- }
4568
- command_close_pipe($ls, $ctx);
4569
- $self->{empty}->{$path} = 0;
4570
- }
4571
- my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
4572
- delete $self->{empty}->{$dir};
4573
- $self->{empty}->{$path} = 1;
4574
-
4575
- if ($added_placeholder{$dir}) {
4576
- # Remove our placeholder file, if we created one.
4577
- delete_entry($self, $added_placeholder{$dir});
4578
- delete $added_placeholder{$dir}
4579
- }
4580
-
4581
-out:
4582
- { path => $path };
4583
-}
4584
-
4585
-sub change_dir_prop {
4586
- my ($self, $db, $prop, $value) = @_;
4587
- return undef if $self->is_path_ignored($db->{path});
4588
- $self->{dir_prop}->{$db->{path}} ||= {};
4589
- $self->{dir_prop}->{$db->{path}}->{$prop} = $value;
4590
- undef;
4591
-}
4592
-
4593
-sub absent_directory {
4594
- my ($self, $path, $pb) = @_;
4595
- return undef if $self->is_path_ignored($path);
4596
- $self->{absent_dir}->{$pb->{path}} ||= [];
4597
- push @{$self->{absent_dir}->{$pb->{path}}}, $path;
4598
- undef;
4599
-}
4600
-
4601
-sub absent_file {
4602
- my ($self, $path, $pb) = @_;
4603
- return undef if $self->is_path_ignored($path);
4604
- $self->{absent_file}->{$pb->{path}} ||= [];
4605
- push @{$self->{absent_file}->{$pb->{path}}}, $path;
4606
- undef;
4607
-}
4608
-
4609
-sub change_file_prop {
4610
- my ($self, $fb, $prop, $value) = @_;
4611
- return undef if $self->is_path_ignored($fb->{path});
4612
- if ($prop eq 'svn:executable') {
4613
- if ($fb->{mode_b} != 120000) {
4614
- $fb->{mode_b} = defined $value ? 100755 : 100644;
4615
- }
4616
- } elsif ($prop eq 'svn:special') {
4617
- $fb->{mode_b} = defined $value ? 120000 : 100644;
4618
- } else {
4619
- $self->{file_prop}->{$fb->{path}} ||= {};
4620
- $self->{file_prop}->{$fb->{path}}->{$prop} = $value;
4621
- }
4622
- undef;
4623
-}
4624
-
4625
-sub apply_textdelta {
4626
- my ($self, $fb, $exp) = @_;
4627
- return undef if $self->is_path_ignored($fb->{path});
4628
- my $fh = $::_repository->temp_acquire('svn_delta');
4629
- # $fh gets auto-closed() by SVN::TxDelta::apply(),
4630
- # (but $base does not,) so dup() it for reading in close_file
4631
- open my $dup, '<&', $fh or croak $!;
4632
- my $base = $::_repository->temp_acquire('git_blob');
4633
-
4634
- if ($fb->{blob}) {
4635
- my ($base_is_link, $size);
4636
-
4637
- if ($fb->{mode_a} eq '120000' &&
4638
- ! $self->{empty_symlinks}->{$fb->{path}}) {
4639
- print $base 'link ' or die "print $!\n";
4640
- $base_is_link = 1;
4641
- }
4642
- retry:
4643
- $size = $::_repository->cat_blob($fb->{blob}, $base);
4644
- die "Failed to read object $fb->{blob}" if ($size < 0);
4645
-
4646
- if (defined $exp) {
4647
- seek $base, 0, 0 or croak $!;
4648
- my $got = ::md5sum($base);
4649
- if ($got ne $exp) {
4650
- my $err = "Checksum mismatch: ".
4651
- "$fb->{path} $fb->{blob}\n" .
4652
- "expected: $exp\n" .
4653
- " got: $got\n";
4654
- if ($base_is_link) {
4655
- warn $err,
4656
- "Retrying... (possibly ",
4657
- "a bad symlink from SVN)\n";
4658
- $::_repository->temp_reset($base);
4659
- $base_is_link = 0;
4660
- goto retry;
4661
- }
4662
- die $err;
4663
- }
4664
- }
4665
- }
4666
- seek $base, 0, 0 or croak $!;
4667
- $fb->{fh} = $fh;
4668
- $fb->{base} = $base;
4669
- [ SVN::TxDelta::apply($base, $dup, undef, $fb->{path}, $fb->{pool}) ];
4670
-}
4671
-
4672
-sub close_file {
4673
- my ($self, $fb, $exp) = @_;
4674
- return undef if $self->is_path_ignored($fb->{path});
4675
-
4676
- my $hash;
4677
- my $path = $self->git_path($fb->{path});
4678
- if (my $fh = $fb->{fh}) {
4679
- if (defined $exp) {
4680
- seek($fh, 0, 0) or croak $!;
4681
- my $got = ::md5sum($fh);
4682
- if ($got ne $exp) {
4683
- die "Checksum mismatch: $path\n",
4684
- "expected: $exp\n got: $got\n";
4685
- }
4686
- }
4687
- if ($fb->{mode_b} == 120000) {
4688
- sysseek($fh, 0, 0) or croak $!;
4689
- my $rd = sysread($fh, my $buf, 5);
4690
-
4691
- if (!defined $rd) {
4692
- croak "sysread: $!\n";
4693
- } elsif ($rd == 0) {
4694
- warn "$path has mode 120000",
4695
- " but it points to nothing\n",
4696
- "converting to an empty file with mode",
4697
- " 100644\n";
4698
- $fb->{mode_b} = '100644';
4699
- } elsif ($buf ne 'link ') {
4700
- warn "$path has mode 120000",
4701
- " but is not a link\n";
4702
- } else {
4703
- my $tmp_fh = $::_repository->temp_acquire(
4704
- 'svn_hash');
4705
- my $res;
4706
- while ($res = sysread($fh, my $str, 1024)) {
4707
- my $out = syswrite($tmp_fh, $str, $res);
4708
- defined($out) && $out == $res
4709
- or croak("write ",
4710
- Git::temp_path($tmp_fh),
4711
- ": $!\n");
4712
- }
4713
- defined $res or croak $!;
4714
-
4715
- ($fh, $tmp_fh) = ($tmp_fh, $fh);
4716
- Git::temp_release($tmp_fh, 1);
4717
- }
4718
- }
4719
-
4720
- $hash = $::_repository->hash_and_insert_object(
4721
- Git::temp_path($fh));
4722
- $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
4723
-
4724
- Git::temp_release($fb->{base}, 1);
4725
- Git::temp_release($fh, 1);
4726
- } else {
4727
- $hash = $fb->{blob} or die "no blob information\n";
4728
- }
4729
- $fb->{pool}->clear;
4730
- $self->{gii}->update($fb->{mode_b}, $hash, $path) or croak $!;
4731
- print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $::_q;
4732
- undef;
4733
-}
4734
-
4735
-sub abort_edit {
4736
- my $self = shift;
4737
- $self->{nr} = $self->{gii}->{nr};
4738
- delete $self->{gii};
4739
- $self->SUPER::abort_edit(@_);
4740
-}
4741
-
4742
-sub close_edit {
4743
- my $self = shift;
4744
-
4745
- if ($_preserve_empty_dirs) {
4746
- my @empty_dirs;
4747
-
4748
- # Any entry flagged as empty that also has an associated
4749
- # dir_prop represents a newly created empty directory.
4750
- foreach my $i (keys %{$self->{empty}}) {
4751
- push @empty_dirs, $i if exists $self->{dir_prop}->{$i};
4752
- }
4753
-
4754
- # Search for directories that have become empty due subsequent
4755
- # file deletes.
4756
- push @empty_dirs, $self->find_empty_directories();
4757
-
4758
- # Finally, add a placeholder file to each empty directory.
4759
- $self->add_placeholder_file($_) foreach (@empty_dirs);
4760
-
4761
- $self->stash_placeholder_list();
4762
- }
4763
-
4764
- $self->{git_commit_ok} = 1;
4765
- $self->{nr} = $self->{gii}->{nr};
4766
- delete $self->{gii};
4767
- $self->SUPER::close_edit(@_);
4768
-}
4769
-
4770
-sub find_empty_directories {
4771
- my ($self) = @_;
4772
- my @empty_dirs;
4773
- my %dirs = map { dirname($_) => 1 } @deleted_gpath;
4774
-
4775
- foreach my $dir (sort keys %dirs) {
4776
- next if $dir eq ".";
4777
-
4778
- # If there have been any additions to this directory, there is
4779
- # no reason to check if it is empty.
4780
- my $skip_added = 0;
4781
- foreach my $t (qw/dir_prop file_prop/) {
4782
- foreach my $path (keys %{ $self->{$t} }) {
4783
- if (exists $self->{$t}->{dirname($path)}) {
4784
- $skip_added = 1;
4785
- last;
4786
- }
4787
- }
4788
- last if $skip_added;
4789
- }
4790
- next if $skip_added;
4791
-
4792
- # Use `git ls-tree` to get the filenames of this directory
4793
- # that existed prior to this particular commit.
4794
- my $ls = command('ls-tree', '-z', '--name-only',
4795
- $self->{c}, "$dir/");
4796
- my %files = map { $_ => 1 } split(/\0/, $ls);
4797
-
4798
- # Remove the filenames that were deleted during this commit.
4799
- delete $files{$_} foreach (@deleted_gpath);
4800
-
4801
- # Report the directory if there are no filenames left.
4802
- push @empty_dirs, $dir unless (scalar %files);
4803
- }
4804
- @empty_dirs;
4805
-}
4806
-
4807
-sub add_placeholder_file {
4808
- my ($self, $dir) = @_;
4809
- my $path = "$dir/$_placeholder_filename";
4810
- my $gpath = $self->git_path($path);
4811
-
4812
- my $fh = $::_repository->temp_acquire($gpath);
4813
- my $hash = $::_repository->hash_and_insert_object(Git::temp_path($fh));
4814
- Git::temp_release($fh, 1);
4815
- $self->{gii}->update('100644', $hash, $gpath) or croak $!;
4816
-
4817
- # The directory should no longer be considered empty.
4818
- delete $self->{empty}->{$dir} if exists $self->{empty}->{$dir};
4819
-
4820
- # Keep track of any placeholder files we create.
4821
- $added_placeholder{$dir} = $path;
4822
-}
4823
-
4824
-sub stash_placeholder_list {
4825
- my ($self) = @_;
4826
- my $k = "svn-remote.$repo_id.added-placeholder";
4827
- my $v = eval { command_oneline('config', '--get-all', $k) };
4828
- command_noisy('config', '--unset-all', $k) if $v;
4829
- foreach (values %added_placeholder) {
4830
- command_noisy('config', '--add', $k, $_);
4831
- }
4832
-}
4833
-
4331
package Git::SVN::Editor;
4332
use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;
4333
use strict;
perl/Git/SVN/Fetcher.pm
new
+602
@@ -0,0 +1,602 @@
1
+package Git::SVN::Fetcher;
2
+use vars qw/@ISA $_ignore_regex $_preserve_empty_dirs $_placeholder_filename
3
+ @deleted_gpath %added_placeholder $repo_id/;
4
+use strict;
5
+use warnings;
6
+use SVN::Delta;
7
+use Carp qw/croak/;
8
+use File::Basename qw/dirname/;
9
+use IO::File qw//;
10
+use Git qw/command command_oneline command_noisy command_output_pipe
11
+ command_input_pipe command_close_pipe
12
+ command_bidi_pipe command_close_bidi_pipe/;
13
+BEGIN {
14
+ @ISA = qw(SVN::Delta::Editor);
15
+}
16
+
17
+# file baton members: path, mode_a, mode_b, pool, fh, blob, base
18
+sub new {
19
+ my ($class, $git_svn, $switch_path) = @_;
20
+ my $self = SVN::Delta::Editor->new;
21
+ bless $self, $class;
22
+ if (exists $git_svn->{last_commit}) {
23
+ $self->{c} = $git_svn->{last_commit};
24
+ $self->{empty_symlinks} =
25
+ _mark_empty_symlinks($git_svn, $switch_path);
26
+ }
27
+
28
+ # some options are read globally, but can be overridden locally
29
+ # per [svn-remote "..."] section. Command-line options will *NOT*
30
+ # override options set in an [svn-remote "..."] section
31
+ $repo_id = $git_svn->{repo_id};
32
+ my $k = "svn-remote.$repo_id.ignore-paths";
33
+ my $v = eval { command_oneline('config', '--get', $k) };
34
+ $self->{ignore_regex} = $v;
35
+
36
+ $k = "svn-remote.$repo_id.preserve-empty-dirs";
37
+ $v = eval { command_oneline('config', '--get', '--bool', $k) };
38
+ if ($v && $v eq 'true') {
39
+ $_preserve_empty_dirs = 1;
40
+ $k = "svn-remote.$repo_id.placeholder-filename";
41
+ $v = eval { command_oneline('config', '--get', $k) };
42
+ $_placeholder_filename = $v;
43
+ }
44
+
45
+ # Load the list of placeholder files added during previous invocations.
46
+ $k = "svn-remote.$repo_id.added-placeholder";
47
+ $v = eval { command_oneline('config', '--get-all', $k) };
48
+ if ($_preserve_empty_dirs && $v) {
49
+ # command() prints errors to stderr, so we only call it if
50
+ # command_oneline() succeeded.
51
+ my @v = command('config', '--get-all', $k);
52
+ $added_placeholder{ dirname($_) } = $_ foreach @v;
53
+ }
54
+
55
+ $self->{empty} = {};
56
+ $self->{dir_prop} = {};
57
+ $self->{file_prop} = {};
58
+ $self->{absent_dir} = {};
59
+ $self->{absent_file} = {};
60
+ $self->{gii} = $git_svn->tmp_index_do(sub { Git::IndexInfo->new });
61
+ $self->{pathnameencoding} = Git::config('svn.pathnameencoding');
62
+ $self;
63
+}
64
+
65
+# this uses the Ra object, so it must be called before do_{switch,update},
66
+# not inside them (when the Git::SVN::Fetcher object is passed) to
67
+# do_{switch,update}
68
+sub _mark_empty_symlinks {
69
+ my ($git_svn, $switch_path) = @_;
70
+ my $bool = Git::config_bool('svn.brokenSymlinkWorkaround');
71
+ return {} if (!defined($bool)) || (defined($bool) && ! $bool);
72
+
73
+ my %ret;
74
+ my ($rev, $cmt) = $git_svn->last_rev_commit;
75
+ return {} unless ($rev && $cmt);
76
+
77
+ # allow the warning to be printed for each revision we fetch to
78
+ # ensure the user sees it. The user can also disable the workaround
79
+ # on the repository even while git svn is running and the next
80
+ # revision fetched will skip this expensive function.
81
+ my $printed_warning;
82
+ chomp(my $empty_blob = `git hash-object -t blob --stdin < /dev/null`);
83
+ my ($ls, $ctx) = command_output_pipe(qw/ls-tree -r -z/, $cmt);
84
+ local $/ = "\0";
85
+ my $pfx = defined($switch_path) ? $switch_path : $git_svn->{path};
86
+ $pfx .= '/' if length($pfx);
87
+ while (<$ls>) {
88
+ chomp;
89
+ s/\A100644 blob $empty_blob\t//o or next;
90
+ unless ($printed_warning) {
91
+ print STDERR "Scanning for empty symlinks, ",
92
+ "this may take a while if you have ",
93
+ "many empty files\n",
94
+ "You may disable this with `",
95
+ "git config svn.brokenSymlinkWorkaround ",
96
+ "false'.\n",
97
+ "This may be done in a different ",
98
+ "terminal without restarting ",
99
+ "git svn\n";
100
+ $printed_warning = 1;
101
+ }
102
+ my $path = $_;
103
+ my (undef, $props) =
104
+ $git_svn->ra->get_file($pfx.$path, $rev, undef);
105
+ if ($props->{'svn:special'}) {
106
+ $ret{$path} = 1;
107
+ }
108
+ }
109
+ command_close_pipe($ls, $ctx);
110
+ \%ret;
111
+}
112
+
113
+# returns true if a given path is inside a ".git" directory
114
+sub in_dot_git {
115
+ $_[0] =~ m{(?:^|/)\.git(?:/|$)};
116
+}
117
+
118
+# return value: 0 -- don't ignore, 1 -- ignore
119
+sub is_path_ignored {
120
+ my ($self, $path) = @_;
121
+ return 1 if in_dot_git($path);
122
+ return 1 if defined($self->{ignore_regex}) &&
123
+ $path =~ m!$self->{ignore_regex}!;
124
+ return 0 unless defined($_ignore_regex);
125
+ return 1 if $path =~ m!$_ignore_regex!o;
126
+ return 0;
127
+}
128
+
129
+sub set_path_strip {
130
+ my ($self, $path) = @_;
131
+ $self->{path_strip} = qr/^\Q$path\E(\/|$)/ if length $path;
132
+}
133
+
134
+sub open_root {
135
+ { path => '' };
136
+}
137
+
138
+sub open_directory {
139
+ my ($self, $path, $pb, $rev) = @_;
140
+ { path => $path };
141
+}
142
+
143
+sub git_path {
144
+ my ($self, $path) = @_;
145
+ if (my $enc = $self->{pathnameencoding}) {
146
+ require Encode;
147
+ Encode::from_to($path, 'UTF-8', $enc);
148
+ }
149
+ if ($self->{path_strip}) {
150
+ $path =~ s!$self->{path_strip}!! or
151
+ die "Failed to strip path '$path' ($self->{path_strip})\n";
152
+ }
153
+ $path;
154
+}
155
+
156
+sub delete_entry {
157
+ my ($self, $path, $rev, $pb) = @_;
158
+ return undef if $self->is_path_ignored($path);
159
+
160
+ my $gpath = $self->git_path($path);
161
+ return undef if ($gpath eq '');
162
+
163
+ # remove entire directories.
164
+ my ($tree) = (command('ls-tree', '-z', $self->{c}, "./$gpath")
165
+ =~ /\A040000 tree ([a-f\d]{40})\t\Q$gpath\E\0/);
166
+ if ($tree) {
167
+ my ($ls, $ctx) = command_output_pipe(qw/ls-tree
168
+ -r --name-only -z/,
169
+ $tree);
170
+ local $/ = "\0";
171
+ while (<$ls>) {
172
+ chomp;
173
+ my $rmpath = "$gpath/$_";
174
+ $self->{gii}->remove($rmpath);
175
+ print "\tD\t$rmpath\n" unless $::_q;
176
+ }
177
+ print "\tD\t$gpath/\n" unless $::_q;
178
+ command_close_pipe($ls, $ctx);
179
+ } else {
180
+ $self->{gii}->remove($gpath);
181
+ print "\tD\t$gpath\n" unless $::_q;
182
+ }
183
+ # Don't add to @deleted_gpath if we're deleting a placeholder file.
184
+ push @deleted_gpath, $gpath unless $added_placeholder{dirname($path)};
185
+ $self->{empty}->{$path} = 0;
186
+ undef;
187
+}
188
+
189
+sub open_file {
190
+ my ($self, $path, $pb, $rev) = @_;
191
+ my ($mode, $blob);
192
+
193
+ goto out if $self->is_path_ignored($path);
194
+
195
+ my $gpath = $self->git_path($path);
196
+ ($mode, $blob) = (command('ls-tree', '-z', $self->{c}, "./$gpath")
197
+ =~ /\A(\d{6}) blob ([a-f\d]{40})\t\Q$gpath\E\0/);
198
+ unless (defined $mode && defined $blob) {
199
+ die "$path was not found in commit $self->{c} (r$rev)\n";
200
+ }
201
+ if ($mode eq '100644' && $self->{empty_symlinks}->{$path}) {
202
+ $mode = '120000';
203
+ }
204
+out:
205
+ { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob,
206
+ pool => SVN::Pool->new, action => 'M' };
207
+}
208
+
209
+sub add_file {
210
+ my ($self, $path, $pb, $cp_path, $cp_rev) = @_;
211
+ my $mode;
212
+
213
+ if (!$self->is_path_ignored($path)) {
214
+ my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
215
+ delete $self->{empty}->{$dir};
216
+ $mode = '100644';
217
+
218
+ if ($added_placeholder{$dir}) {
219
+ # Remove our placeholder file, if we created one.
220
+ delete_entry($self, $added_placeholder{$dir})
221
+ unless $path eq $added_placeholder{$dir};
222
+ delete $added_placeholder{$dir}
223
+ }
224
+ }
225
+
226
+ { path => $path, mode_a => $mode, mode_b => $mode,
227
+ pool => SVN::Pool->new, action => 'A' };
228
+}
229
+
230
+sub add_directory {
231
+ my ($self, $path, $cp_path, $cp_rev) = @_;
232
+ goto out if $self->is_path_ignored($path);
233
+ my $gpath = $self->git_path($path);
234
+ if ($gpath eq '') {
235
+ my ($ls, $ctx) = command_output_pipe(qw/ls-tree
236
+ -r --name-only -z/,
237
+ $self->{c});
238
+ local $/ = "\0";
239
+ while (<$ls>) {
240
+ chomp;
241
+ $self->{gii}->remove($_);
242
+ print "\tD\t$_\n" unless $::_q;
243
+ push @deleted_gpath, $gpath;
244
+ }
245
+ command_close_pipe($ls, $ctx);
246
+ $self->{empty}->{$path} = 0;
247
+ }
248
+ my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
249
+ delete $self->{empty}->{$dir};
250
+ $self->{empty}->{$path} = 1;
251
+
252
+ if ($added_placeholder{$dir}) {
253
+ # Remove our placeholder file, if we created one.
254
+ delete_entry($self, $added_placeholder{$dir});
255
+ delete $added_placeholder{$dir}
256
+ }
257
+
258
+out:
259
+ { path => $path };
260
+}
261
+
262
+sub change_dir_prop {
263
+ my ($self, $db, $prop, $value) = @_;
264
+ return undef if $self->is_path_ignored($db->{path});
265
+ $self->{dir_prop}->{$db->{path}} ||= {};
266
+ $self->{dir_prop}->{$db->{path}}->{$prop} = $value;
267
+ undef;
268
+}
269
+
270
+sub absent_directory {
271
+ my ($self, $path, $pb) = @_;
272
+ return undef if $self->is_path_ignored($path);
273
+ $self->{absent_dir}->{$pb->{path}} ||= [];
274
+ push @{$self->{absent_dir}->{$pb->{path}}}, $path;
275
+ undef;
276
+}
277
+
278
+sub absent_file {
279
+ my ($self, $path, $pb) = @_;
280
+ return undef if $self->is_path_ignored($path);
281
+ $self->{absent_file}->{$pb->{path}} ||= [];
282
+ push @{$self->{absent_file}->{$pb->{path}}}, $path;
283
+ undef;
284
+}
285
+
286
+sub change_file_prop {
287
+ my ($self, $fb, $prop, $value) = @_;
288
+ return undef if $self->is_path_ignored($fb->{path});
289
+ if ($prop eq 'svn:executable') {
290
+ if ($fb->{mode_b} != 120000) {
291
+ $fb->{mode_b} = defined $value ? 100755 : 100644;
292
+ }
293
+ } elsif ($prop eq 'svn:special') {
294
+ $fb->{mode_b} = defined $value ? 120000 : 100644;
295
+ } else {
296
+ $self->{file_prop}->{$fb->{path}} ||= {};
297
+ $self->{file_prop}->{$fb->{path}}->{$prop} = $value;
298
+ }
299
+ undef;
300
+}
301
+
302
+sub apply_textdelta {
303
+ my ($self, $fb, $exp) = @_;
304
+ return undef if $self->is_path_ignored($fb->{path});
305
+ my $fh = $::_repository->temp_acquire('svn_delta');
306
+ # $fh gets auto-closed() by SVN::TxDelta::apply(),
307
+ # (but $base does not,) so dup() it for reading in close_file
308
+ open my $dup, '<&', $fh or croak $!;
309
+ my $base = $::_repository->temp_acquire('git_blob');
310
+
311
+ if ($fb->{blob}) {
312
+ my ($base_is_link, $size);
313
+
314
+ if ($fb->{mode_a} eq '120000' &&
315
+ ! $self->{empty_symlinks}->{$fb->{path}}) {
316
+ print $base 'link ' or die "print $!\n";
317
+ $base_is_link = 1;
318
+ }
319
+ retry:
320
+ $size = $::_repository->cat_blob($fb->{blob}, $base);
321
+ die "Failed to read object $fb->{blob}" if ($size < 0);
322
+
323
+ if (defined $exp) {
324
+ seek $base, 0, 0 or croak $!;
325
+ my $got = ::md5sum($base);
326
+ if ($got ne $exp) {
327
+ my $err = "Checksum mismatch: ".
328
+ "$fb->{path} $fb->{blob}\n" .
329
+ "expected: $exp\n" .
330
+ " got: $got\n";
331
+ if ($base_is_link) {
332
+ warn $err,
333
+ "Retrying... (possibly ",
334
+ "a bad symlink from SVN)\n";
335
+ $::_repository->temp_reset($base);
336
+ $base_is_link = 0;
337
+ goto retry;
338
+ }
339
+ die $err;
340
+ }
341
+ }
342
+ }
343
+ seek $base, 0, 0 or croak $!;
344
+ $fb->{fh} = $fh;
345
+ $fb->{base} = $base;
346
+ [ SVN::TxDelta::apply($base, $dup, undef, $fb->{path}, $fb->{pool}) ];
347
+}
348
+
349
+sub close_file {
350
+ my ($self, $fb, $exp) = @_;
351
+ return undef if $self->is_path_ignored($fb->{path});
352
+
353
+ my $hash;
354
+ my $path = $self->git_path($fb->{path});
355
+ if (my $fh = $fb->{fh}) {
356
+ if (defined $exp) {
357
+ seek($fh, 0, 0) or croak $!;
358
+ my $got = ::md5sum($fh);
359
+ if ($got ne $exp) {
360
+ die "Checksum mismatch: $path\n",
361
+ "expected: $exp\n got: $got\n";
362
+ }
363
+ }
364
+ if ($fb->{mode_b} == 120000) {
365
+ sysseek($fh, 0, 0) or croak $!;
366
+ my $rd = sysread($fh, my $buf, 5);
367
+
368
+ if (!defined $rd) {
369
+ croak "sysread: $!\n";
370
+ } elsif ($rd == 0) {
371
+ warn "$path has mode 120000",
372
+ " but it points to nothing\n",
373
+ "converting to an empty file with mode",
374
+ " 100644\n";
375
+ $fb->{mode_b} = '100644';
376
+ } elsif ($buf ne 'link ') {
377
+ warn "$path has mode 120000",
378
+ " but is not a link\n";
379
+ } else {
380
+ my $tmp_fh = $::_repository->temp_acquire(
381
+ 'svn_hash');
382
+ my $res;
383
+ while ($res = sysread($fh, my $str, 1024)) {
384
+ my $out = syswrite($tmp_fh, $str, $res);
385
+ defined($out) && $out == $res
386
+ or croak("write ",
387
+ Git::temp_path($tmp_fh),
388
+ ": $!\n");
389
+ }
390
+ defined $res or croak $!;
391
+
392
+ ($fh, $tmp_fh) = ($tmp_fh, $fh);
393
+ Git::temp_release($tmp_fh, 1);
394
+ }
395
+ }
396
+
397
+ $hash = $::_repository->hash_and_insert_object(
398
+ Git::temp_path($fh));
399
+ $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n";
400
+
401
+ Git::temp_release($fb->{base}, 1);
402
+ Git::temp_release($fh, 1);
403
+ } else {
404
+ $hash = $fb->{blob} or die "no blob information\n";
405
+ }
406
+ $fb->{pool}->clear;
407
+ $self->{gii}->update($fb->{mode_b}, $hash, $path) or croak $!;
408
+ print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $::_q;
409
+ undef;
410
+}
411
+
412
+sub abort_edit {
413
+ my $self = shift;
414
+ $self->{nr} = $self->{gii}->{nr};
415
+ delete $self->{gii};
416
+ $self->SUPER::abort_edit(@_);
417
+}
418
+
419
+sub close_edit {
420
+ my $self = shift;
421
+
422
+ if ($_preserve_empty_dirs) {
423
+ my @empty_dirs;
424
+
425
+ # Any entry flagged as empty that also has an associated
426
+ # dir_prop represents a newly created empty directory.
427
+ foreach my $i (keys %{$self->{empty}}) {
428
+ push @empty_dirs, $i if exists $self->{dir_prop}->{$i};
429
+ }
430
+
431
+ # Search for directories that have become empty due subsequent
432
+ # file deletes.
433
+ push @empty_dirs, $self->find_empty_directories();
434
+
435
+ # Finally, add a placeholder file to each empty directory.
436
+ $self->add_placeholder_file($_) foreach (@empty_dirs);
437
+
438
+ $self->stash_placeholder_list();
439
+ }
440
+
441
+ $self->{git_commit_ok} = 1;
442
+ $self->{nr} = $self->{gii}->{nr};
443
+ delete $self->{gii};
444
+ $self->SUPER::close_edit(@_);
445
+}
446
+
447
+sub find_empty_directories {
448
+ my ($self) = @_;
449
+ my @empty_dirs;
450
+ my %dirs = map { dirname($_) => 1 } @deleted_gpath;
451
+
452
+ foreach my $dir (sort keys %dirs) {
453
+ next if $dir eq ".";
454
+
455
+ # If there have been any additions to this directory, there is
456
+ # no reason to check if it is empty.
457
+ my $skip_added = 0;
458
+ foreach my $t (qw/dir_prop file_prop/) {
459
+ foreach my $path (keys %{ $self->{$t} }) {
460
+ if (exists $self->{$t}->{dirname($path)}) {
461
+ $skip_added = 1;
462
+ last;
463
+ }
464
+ }
465
+ last if $skip_added;
466
+ }
467
+ next if $skip_added;
468
+
469
+ # Use `git ls-tree` to get the filenames of this directory
470
+ # that existed prior to this particular commit.
471
+ my $ls = command('ls-tree', '-z', '--name-only',
472
+ $self->{c}, "$dir/");
473
+ my %files = map { $_ => 1 } split(/\0/, $ls);
474
+
475
+ # Remove the filenames that were deleted during this commit.
476
+ delete $files{$_} foreach (@deleted_gpath);
477
+
478
+ # Report the directory if there are no filenames left.
479
+ push @empty_dirs, $dir unless (scalar %files);
480
+ }
481
+ @empty_dirs;
482
+}
483
+
484
+sub add_placeholder_file {
485
+ my ($self, $dir) = @_;
486
+ my $path = "$dir/$_placeholder_filename";
487
+ my $gpath = $self->git_path($path);
488
+
489
+ my $fh = $::_repository->temp_acquire($gpath);
490
+ my $hash = $::_repository->hash_and_insert_object(Git::temp_path($fh));
491
+ Git::temp_release($fh, 1);
492
+ $self->{gii}->update('100644', $hash, $gpath) or croak $!;
493
+
494
+ # The directory should no longer be considered empty.
495
+ delete $self->{empty}->{$dir} if exists $self->{empty}->{$dir};
496
+
497
+ # Keep track of any placeholder files we create.
498
+ $added_placeholder{$dir} = $path;
499
+}
500
+
501
+sub stash_placeholder_list {
502
+ my ($self) = @_;
503
+ my $k = "svn-remote.$repo_id.added-placeholder";
504
+ my $v = eval { command_oneline('config', '--get-all', $k) };
505
+ command_noisy('config', '--unset-all', $k) if $v;
506
+ foreach (values %added_placeholder) {
507
+ command_noisy('config', '--add', $k, $_);
508
+ }
509
+}
510
+
511
+1;
512
+__END__
513
+
514
+Git::SVN::Fetcher - tree delta consumer for "git svn fetch"
515
+
516
+=head1 SYNOPSIS
517
+
518
+ use SVN::Core;
519
+ use SVN::Ra;
520
+ use Git::SVN;
521
+ use Git::SVN::Fetcher;
522
+ use Git;
523
+
524
+ my $gs = Git::SVN->find_by_url($url);
525
+ my $ra = SVN::Ra->new(url => $url);
526
+ my $editor = Git::SVN::Fetcher->new($gs);
527
+ my $reporter = $ra->do_update($SVN::Core::INVALID_REVNUM, '',
528
+ 1, $editor);
529
+ $reporter->set_path('', $old_rev, 0);
530
+ $reporter->finish_report;
531
+ my $tree = $gs->tmp_index_do(sub { command_oneline('write-tree') });
532
+
533
+ foreach my $path (keys %{$editor->{dir_prop}) {
534
+ my $props = $editor->{dir_prop}{$path};
535
+ foreach my $prop (keys %$props) {
536
+ print "property $prop at $path changed to $props->{$prop}\n";
537
+ }
538
+ }
539
+ foreach my $path (keys %{$editor->{empty}) {
540
+ my $action = $editor->{empty}{$path} ? 'added' : 'removed';
541
+ print "empty directory $path $action\n";
542
+ }
543
+ foreach my $path (keys %{$editor->{file_prop}) { ... }
544
+ foreach my $parent (keys %{$editor->{absent_dir}}) {
545
+ my @children = @{$editor->{abstent_dir}{$parent}};
546
+ print "cannot fetch directory $parent/$_: not authorized?\n"
547
+ foreach @children;
548
+ }
549
+ foreach my $parent (keys %{$editor->{absent_file}) { ... }
550
+
551
+=head1 DESCRIPTION
552
+
553
+This is a subclass of C<SVN::Delta::Editor>, which means it implements
554
+callbacks to act as a consumer of Subversion tree deltas. This
555
+particular implementation of those callbacks is meant to store
556
+information about the resulting content which B<git svn fetch> could
557
+use to populate new commits and new entries for F<unhandled.log>.
558
+More specifically:
559
+
560
+=over
561
+
562
+=item * Additions, removals, and modifications of files are propagated
563
+to git-svn's index file F<$GIT_DIR/svn/$refname/index> using
564
+B<git update-index>.
565
+
566
+=item * Changes in Subversion path properties are recorded in the
567
+C<dir_prop> and C<file_prop> fields (which are hashes).
568
+
569
+=item * Addition and removal of empty directories are indicated by
570
+entries with value 1 and 0 respectively in the C<empty> hash.
571
+
572
+=item * Paths that are present but cannot be conveyed (presumably due
573
+to permissions) are recorded in the C<absent_file> and
574
+C<absent_dirs> hashes. For each key, the corresponding value is
575
+a list of paths under that directory that were present but
576
+could not be conveyed.
577
+
578
+=back
579
+
580
+The interface is unstable. Do not use this module unless you are
581
+developing git-svn.
582
+
583
+=head1 DEPENDENCIES
584
+
585
+L<SVN::Delta> from the Subversion perl bindings,
586
+the core L<Carp>, L<File::Basename>, and L<IO::File> modules,
587
+and git's L<Git> helper module.
588
+
589
+C<Git::SVN::Fetcher> has not been tested using callers other than
590
+B<git-svn> itself.
591
+
592
+=head1 SEE ALSO
593
+
594
+L<SVN::Delta>.
595
+
596
+=head1 INCOMPATIBILITIES
597
+
598
+None reported.
599
+
600
+=head1 BUGS
601
+
602
+None.
perl/Makefile.PL
+1
@@ -27,6 +27,7 @@ MAKE_FRAG
27
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/Prompt.pm' => '$(INST_LIBDIR)/Git/SVN/Prompt.pm',
32
);
33