Re: detecting rename->commit->modify->commit
- From
Jeff King <peff@peff.net>
- Date
- May 8, 2008, 18:17 UTC
- Message-ID
- <20080508181723.GA30449@sigill.intra.peff.net>
- In-Reply-To
- <20080501231427.GD21731@sigill.intra.peff.net>
On Thu, May 01, 2008 at 07:14:27PM -0400, Jeff King wrote:
> 1. write a proof-of-concept that shows directory renaming after the > fact (e.g., take a conflicted merge, scan the diff for directory > renames, and then fix up the files). That way it is available, but > doesn't impact git at all.
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 initialgit 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
}