Skip to content

Commit 40a1530

Browse files
rchyenaEric Wong
authored andcommitted
git-svn: New flag to emulate empty directories
Adds a --preserve-empty-dirs flag to the clone operation that will detect empty directories in the remote Subversion repository and create placeholder files in the corresponding local Git directories. This allows "empty" directories to exist in the history of a Git repository. Also adds the --placeholder-file flag to control the name of any placeholder files created. Default value is ".gitignore". Signed-off-by: Ray Chen <rchen@cs.umd.edu> Acked-by: Eric Wong <normalperson@yhbt.net>
1 parent 4b5eac7 commit 40a1530

File tree

3 files changed

+308
-4
lines changed

3 files changed

+308
-4
lines changed

Documentation/git-svn.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,17 @@ Skip "branches" and "tags" of first level directories;;
157157
affecting the working tree; and the 'rebase' command will be
158158
able to update the working tree with the latest changes.
159159

160+
--preserve-empty-dirs;;
161+
Create a placeholder file in the local Git repository for each
162+
empty directory fetched from Subversion. This includes directories
163+
that become empty by removing all entries in the Subversion
164+
repository (but not the directory itself). The placeholder files
165+
are also tracked and removed when no longer necessary.
166+
167+
--placeholder-filename=<filename>;;
168+
Set the name of placeholder files created by --preserve-empty-dirs.
169+
Default: ".gitignore"
170+
160171
'rebase'::
161172
This fetches revisions from the SVN parent of the current HEAD
162173
and rebases the current (uncommitted to SVN) work against it.

git-svn.perl

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ BEGIN
8989
$_prefix, $_no_checkout, $_url, $_verbose,
9090
$_git_format, $_commit_url, $_tag, $_merge_info);
9191
$Git::SVN::_follow_parent = 1;
92+
$SVN::Git::Fetcher::_placeholder_filename = ".gitignore";
9293
$_q ||= 0;
9394
my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
9495
'config-dir=s' => \$Git::SVN::Ra::config_dir,
@@ -139,6 +140,10 @@ BEGIN
139140
%fc_opts } ],
140141
clone => [ \&cmd_clone, "Initialize and fetch revisions",
141142
{ '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,
142147
%fc_opts, %init_opts } ],
143148
init => [ \&cmd_init, "Initialize a repo for tracking" .
144149
" (requires URL argument)",
@@ -386,6 +391,12 @@ sub do_git_init_db {
386391
my $ignore_regex = \$SVN::Git::Fetcher::_ignore_regex;
387392
command_noisy('config', "$pfx.ignore-paths", $$ignore_regex)
388393
if defined $$ignore_regex;
394+
395+
if (defined $SVN::Git::Fetcher::_preserve_empty_dirs) {
396+
my $fname = \$SVN::Git::Fetcher::_placeholder_filename;
397+
command_noisy('config', "$pfx.preserve-empty-dirs", 'true');
398+
command_noisy('config', "$pfx.placeholder-filename", $$fname);
399+
}
389400
}
390401

391402
sub init_subdir {
@@ -4080,12 +4091,13 @@ sub _read_password {
40804091
}
40814092

40824093
package SVN::Git::Fetcher;
4083-
use vars qw/@ISA/;
4094+
use vars qw/@ISA $_ignore_regex $_preserve_empty_dirs $_placeholder_filename
4095+
@deleted_gpath %added_placeholder $repo_id/;
40844096
use strict;
40854097
use warnings;
40864098
use Carp qw/croak/;
4099+
use File::Basename qw/dirname/;
40874100
use IO::File qw//;
4088-
use vars qw/$_ignore_regex/;
40894101

40904102
# file baton members: path, mode_a, mode_b, pool, fh, blob, base
40914103
sub new {
@@ -4097,8 +4109,34 @@ sub new {
40974109
$self->{empty_symlinks} =
40984110
_mark_empty_symlinks($git_svn, $switch_path);
40994111
}
4100-
$self->{ignore_regex} = eval { command_oneline('config', '--get',
4101-
"svn-remote.$git_svn->{repo_id}.ignore-paths") };
4112+
4113+
# some options are read globally, but can be overridden locally
4114+
# per [svn-remote "..."] section. Command-line options will *NOT*
4115+
# override options set in an [svn-remote "..."] section
4116+
$repo_id = $git_svn->{repo_id};
4117+
my $k = "svn-remote.$repo_id.ignore-paths";
4118+
my $v = eval { command_oneline('config', '--get', $k) };
4119+
$self->{ignore_regex} = $v;
4120+
4121+
$k = "svn-remote.$repo_id.preserve-empty-dirs";
4122+
$v = eval { command_oneline('config', '--get', '--bool', $k) };
4123+
if ($v && $v eq 'true') {
4124+
$_preserve_empty_dirs = 1;
4125+
$k = "svn-remote.$repo_id.placeholder-filename";
4126+
$v = eval { command_oneline('config', '--get', $k) };
4127+
$_placeholder_filename = $v;
4128+
}
4129+
4130+
# Load the list of placeholder files added during previous invocations.
4131+
$k = "svn-remote.$repo_id.added-placeholder";
4132+
$v = eval { command_oneline('config', '--get-all', $k) };
4133+
if ($_preserve_empty_dirs && $v) {
4134+
# command() prints errors to stderr, so we only call it if
4135+
# command_oneline() succeeded.
4136+
my @v = command('config', '--get-all', $k);
4137+
$added_placeholder{ dirname($_) } = $_ foreach @v;
4138+
}
4139+
41024140
$self->{empty} = {};
41034141
$self->{dir_prop} = {};
41044142
$self->{file_prop} = {};
@@ -4227,6 +4265,8 @@ sub delete_entry {
42274265
$self->{gii}->remove($gpath);
42284266
print "\tD\t$gpath\n" unless $::_q;
42294267
}
4268+
# Don't add to @deleted_gpath if we're deleting a placeholder file.
4269+
push @deleted_gpath, $gpath unless $added_placeholder{dirname($path)};
42304270
$self->{empty}->{$path} = 0;
42314271
undef;
42324272
}
@@ -4259,7 +4299,15 @@ sub add_file {
42594299
my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
42604300
delete $self->{empty}->{$dir};
42614301
$mode = '100644';
4302+
4303+
if ($added_placeholder{$dir}) {
4304+
# Remove our placeholder file, if we created one.
4305+
delete_entry($self, $added_placeholder{$dir})
4306+
unless $path eq $added_placeholder{$dir};
4307+
delete $added_placeholder{$dir}
4308+
}
42624309
}
4310+
42634311
{ path => $path, mode_a => $mode, mode_b => $mode,
42644312
pool => SVN::Pool->new, action => 'A' };
42654313
}
@@ -4277,13 +4325,21 @@ sub add_directory {
42774325
chomp;
42784326
$self->{gii}->remove($_);
42794327
print "\tD\t$_\n" unless $::_q;
4328+
push @deleted_gpath, $gpath;
42804329
}
42814330
command_close_pipe($ls, $ctx);
42824331
$self->{empty}->{$path} = 0;
42834332
}
42844333
my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#);
42854334
delete $self->{empty}->{$dir};
42864335
$self->{empty}->{$path} = 1;
4336+
4337+
if ($added_placeholder{$dir}) {
4338+
# Remove our placeholder file, if we created one.
4339+
delete_entry($self, $added_placeholder{$dir});
4340+
delete $added_placeholder{$dir}
4341+
}
4342+
42874343
out:
42884344
{ path => $path };
42894345
}
@@ -4447,12 +4503,96 @@ sub abort_edit {
44474503

44484504
sub close_edit {
44494505
my $self = shift;
4506+
4507+
if ($_preserve_empty_dirs) {
4508+
my @empty_dirs;
4509+
4510+
# Any entry flagged as empty that also has an associated
4511+
# dir_prop represents a newly created empty directory.
4512+
foreach my $i (keys %{$self->{empty}}) {
4513+
push @empty_dirs, $i if exists $self->{dir_prop}->{$i};
4514+
}
4515+
4516+
# Search for directories that have become empty due subsequent
4517+
# file deletes.
4518+
push @empty_dirs, $self->find_empty_directories();
4519+
4520+
# Finally, add a placeholder file to each empty directory.
4521+
$self->add_placeholder_file($_) foreach (@empty_dirs);
4522+
4523+
$self->stash_placeholder_list();
4524+
}
4525+
44504526
$self->{git_commit_ok} = 1;
44514527
$self->{nr} = $self->{gii}->{nr};
44524528
delete $self->{gii};
44534529
$self->SUPER::close_edit(@_);
44544530
}
44554531

4532+
sub find_empty_directories {
4533+
my ($self) = @_;
4534+
my @empty_dirs;
4535+
my %dirs = map { dirname($_) => 1 } @deleted_gpath;
4536+
4537+
foreach my $dir (sort keys %dirs) {
4538+
next if $dir eq ".";
4539+
4540+
# If there have been any additions to this directory, there is
4541+
# no reason to check if it is empty.
4542+
my $skip_added = 0;
4543+
foreach my $t (qw/dir_prop file_prop/) {
4544+
foreach my $path (keys %{ $self->{$t} }) {
4545+
if (exists $self->{$t}->{dirname($path)}) {
4546+
$skip_added = 1;
4547+
last;
4548+
}
4549+
}
4550+
last if $skip_added;
4551+
}
4552+
next if $skip_added;
4553+
4554+
# Use `git ls-tree` to get the filenames of this directory
4555+
# that existed prior to this particular commit.
4556+
my $ls = command('ls-tree', '-z', '--name-only',
4557+
$self->{c}, "$dir/");
4558+
my %files = map { $_ => 1 } split(/\0/, $ls);
4559+
4560+
# Remove the filenames that were deleted during this commit.
4561+
delete $files{$_} foreach (@deleted_gpath);
4562+
4563+
# Report the directory if there are no filenames left.
4564+
push @empty_dirs, $dir unless (scalar %files);
4565+
}
4566+
@empty_dirs;
4567+
}
4568+
4569+
sub add_placeholder_file {
4570+
my ($self, $dir) = @_;
4571+
my $path = "$dir/$_placeholder_filename";
4572+
my $gpath = $self->git_path($path);
4573+
4574+
my $fh = $::_repository->temp_acquire($gpath);
4575+
my $hash = $::_repository->hash_and_insert_object(Git::temp_path($fh));
4576+
Git::temp_release($fh, 1);
4577+
$self->{gii}->update('100644', $hash, $gpath) or croak $!;
4578+
4579+
# The directory should no longer be considered empty.
4580+
delete $self->{empty}->{$dir} if exists $self->{empty}->{$dir};
4581+
4582+
# Keep track of any placeholder files we create.
4583+
$added_placeholder{$dir} = $path;
4584+
}
4585+
4586+
sub stash_placeholder_list {
4587+
my ($self) = @_;
4588+
my $k = "svn-remote.$repo_id.added-placeholder";
4589+
my $v = eval { command_oneline('config', '--get-all', $k) };
4590+
command_noisy('config', '--unset-all', $k) if $v;
4591+
foreach (values %added_placeholder) {
4592+
command_noisy('config', '--add', $k, $_);
4593+
}
4594+
}
4595+
44564596
package SVN::Git::Editor;
44574597
use vars qw/@ISA $_rmdir $_cp_similarity $_find_copies_harder $_rename_limit/;
44584598
use strict;

0 commit comments

Comments
 (0)