Here's a toy script that finds directory renames. I'm sure there are a ton of corner cases it doesn't handle (like directory renames inside of directory renames). My test case was the very trivial:
mkdir repo && cd repo && git init
mkdir subdir
for i in 1 2 3; do
echo content $i >subdir/file$i
done
git add subdir
git commit -m initial git mv subdir new
git commit -m move
git checkout -b other HEAD^
echo content 4 >subdir/file4
git add subdir
git commit -m new
git merge --no-commit master
perl ../find-dir-rename.pl
git commit
At which point you should see the merged commit with new/file4.
Script is below.
-- >8 -- #!/usr/bin/perl # # Find renamed directories, and move any files in the "old" # directory into the "new". # # usage: # git merge --no-commit <whatever> # find-dir-rename # git commit
use strict;
foreach my $r (renamed_dirs()) {
move_dir_contents($r->{from}, $r->{to});
}
exit 0;sub renamed_dirs {
my $base = `git merge-base HEAD MERGE_HEAD`;
chomp $base;
return grep {
$_->{score} == 1
} (renamed_dirs_between($base, 'HEAD'),
renamed_dirs_between($base, 'MERGE_HEAD'));
}sub renamed_dirs_between {
my ($base, $commit) = @_; my %sources;
foreach my $pair (renamed_files($base, $commit)) {
my $d1 = dir_of($pair->[0]);
my $d2 = dir_of($pair->[1]);
next unless defined($d1) && defined($d2); $sources{$d1}->{total}++;
$sources{$d1}->{dests}->{$d2}++;
} return map {
my $from = $_;
map {
{
from => $from,
to => $_,
score => $sources{$from}->{dests}->{$_} / $sources{$from}->{total},
}
} keys(%{$sources{$from}->{dests}});
} removed_directories($base, $commit);
}sub dir_of {
local $_ = shift;
s{/[^/]+$}{} or return undef;
return $_;
}sub renamed_files {
my ($from, $to) = @_;
open(my $fh, '-|', qw(git diff-tree -r -M), $from, $to)
or die "unable to open diff-tree: $!";
return map {
chomp;
m/ R\d+\t([^\t]+)\t(.*)/ ? [$1 => $2] : ()
} <$fh>;
}sub removed_directories {
my ($base, $commit) = @_;
my %new_dirs = map { $_ => 1 } directories($commit);
return grep { !exists $new_dirs{$_} } directories($base);
}sub directories {
my $commit = shift;
return uniq(
map {
s{/[^/]+$}{} ? $_ : ()
} files($commit)
);
}sub files {
my $commit = shift;
open(my $fh, '-|', qw(git ls-tree -r), $commit)
or die "unable to open ls-tree: $!";
return map {
chomp;
s/^[^\t]*\t//;
$_
} <$fh>;
}sub uniq {
my %seen;
return grep { !$seen{$_}++ } @_;
}sub move_dir_contents {
my ($from, $to) = @_; my @files = glob("$from/*");
return unless @files; system(qw(git mv), @files, "$to/")
and die "unable to move $from/* to $to";
rmdir($from); # ignore error since there may be untracked files
}