git/git-rerere.perl

285 строки
6.0 KiB
Perl
Executable File

#!/usr/bin/perl
#
# REuse REcorded REsolve. This tool records a conflicted automerge
# result and its hand resolution, and helps to resolve future
# automerge that results in the same conflict.
#
# To enable this feature, create a directory 'rr-cache' under your
# .git/ directory.
use Digest;
use File::Path;
use File::Copy;
my $git_dir = $::ENV{GIT_DIR} || ".git";
my $rr_dir = "$git_dir/rr-cache";
my $merge_rr = "$git_dir/rr-cache/MERGE_RR";
my %merge_rr = ();
sub read_rr {
if (!-f $merge_rr) {
%merge_rr = ();
return;
}
my $in;
local $/ = "\0";
open $in, "<$merge_rr" or die "$!: $merge_rr";
while (<$in>) {
chomp;
my ($name, $path) = /^([0-9a-f]{40})\t(.*)$/s;
$merge_rr{$path} = $name;
}
close $in;
}
sub write_rr {
my $out;
open $out, ">$merge_rr" or die "$!: $merge_rr";
for my $path (sort keys %merge_rr) {
my $name = $merge_rr{$path};
print $out "$name\t$path\0";
}
close $out;
}
sub compute_conflict_name {
my ($path) = @_;
my @side = ();
my $in;
open $in, "<$path" or die "$!: $path";
my $sha1 = Digest->new("SHA-1");
my $hunk = 0;
while (<$in>) {
if (/^<<<<<<< .*/) {
$hunk++;
@side = ([], undef);
}
elsif (/^=======$/) {
$side[1] = [];
}
elsif (/^>>>>>>> .*/) {
my ($one, $two);
$one = join('', @{$side[0]});
$two = join('', @{$side[1]});
if ($two le $one) {
($one, $two) = ($two, $one);
}
$sha1->add($one);
$sha1->add("\0");
$sha1->add($two);
$sha1->add("\0");
@side = ();
}
elsif (@side == 0) {
next;
}
elsif (defined $side[1]) {
push @{$side[1]}, $_;
}
else {
push @{$side[0]}, $_;
}
}
close $in;
return ($sha1->hexdigest, $hunk);
}
sub record_preimage {
my ($path, $name) = @_;
my @side = ();
my ($in, $out);
open $in, "<$path" or die "$!: $path";
open $out, ">$name" or die "$!: $name";
while (<$in>) {
if (/^<<<<<<< .*/) {
@side = ([], undef);
}
elsif (/^=======$/) {
$side[1] = [];
}
elsif (/^>>>>>>> .*/) {
my ($one, $two);
$one = join('', @{$side[0]});
$two = join('', @{$side[1]});
if ($two le $one) {
($one, $two) = ($two, $one);
}
print $out "<<<<<<<\n";
print $out $one;
print $out "=======\n";
print $out $two;
print $out ">>>>>>>\n";
@side = ();
}
elsif (@side == 0) {
print $out $_;
}
elsif (defined $side[1]) {
push @{$side[1]}, $_;
}
else {
push @{$side[0]}, $_;
}
}
close $out;
close $in;
}
sub find_conflict {
my $in;
local $/ = "\0";
my $pid = open($in, '-|');
die "$!" unless defined $pid;
if (!$pid) {
exec(qw(git ls-files -z -u)) or die "$!: ls-files";
}
my %path = ();
my @path = ();
while (<$in>) {
chomp;
my ($mode, $sha1, $stage, $path) =
/^([0-7]+) ([0-9a-f]{40}) ([123])\t(.*)$/s;
$path{$path} |= (1 << $stage);
}
close $in;
while (my ($path, $status) = each %path) {
if ($status == 14) { push @path, $path; }
}
return @path;
}
sub merge {
my ($name, $path) = @_;
record_preimage($path, "$rr_dir/$name/thisimage");
unless (system('git merge-file', map { "$rr_dir/$name/${_}image" }
qw(this pre post))) {
my $in;
open $in, "<$rr_dir/$name/thisimage" or
die "$!: $name/thisimage";
my $out;
open $out, ">$path" or die "$!: $path";
while (<$in>) { print $out $_; }
close $in;
close $out;
return 1;
}
return 0;
}
sub garbage_collect_rerere {
# We should allow specifying these from the command line and
# that is why the caller gives @ARGV to us, but I am lazy.
my $cutoff_noresolve = 15; # two weeks
my $cutoff_resolve = 60; # two months
my @to_remove;
while (<$rr_dir/*/preimage>) {
my ($dir) = /^(.*)\/preimage$/;
my $cutoff = ((-f "$dir/postimage")
? $cutoff_resolve
: $cutoff_noresolve);
my $age = -M "$_";
if ($cutoff <= $age) {
push @to_remove, $dir;
}
}
if (@to_remove) {
rmtree(\@to_remove);
}
}
-d "$rr_dir" || exit(0);
read_rr();
if (@ARGV) {
my $arg = shift @ARGV;
if ($arg eq 'clear') {
for my $path (keys %merge_rr) {
my $name = $merge_rr{$path};
if (-d "$rr_dir/$name" &&
! -f "$rr_dir/$name/postimage") {
rmtree(["$rr_dir/$name"]);
}
}
unlink $merge_rr;
}
elsif ($arg eq 'status') {
for my $path (keys %merge_rr) {
print $path, "\n";
}
}
elsif ($arg eq 'diff') {
for my $path (keys %merge_rr) {
my $name = $merge_rr{$path};
system('diff', ((@ARGV == 0) ? ('-u') : @ARGV),
'-L', "a/$path", '-L', "b/$path",
"$rr_dir/$name/preimage", $path);
}
}
elsif ($arg eq 'gc') {
garbage_collect_rerere(@ARGV);
}
else {
die "$0 unknown command: $arg\n";
}
exit 0;
}
my %conflict = map { $_ => 1 } find_conflict();
# MERGE_RR records paths with conflicts immediately after merge
# failed. Some of the conflicted paths might have been hand resolved
# in the working tree since then, but the initial run would catch all
# and register their preimages.
for my $path (keys %conflict) {
# This path has conflict. If it is not recorded yet,
# record the pre-image.
if (!exists $merge_rr{$path}) {
my ($name, $hunk) = compute_conflict_name($path);
next unless ($hunk);
$merge_rr{$path} = $name;
if (! -d "$rr_dir/$name") {
mkpath("$rr_dir/$name", 0, 0777);
print STDERR "Recorded preimage for '$path'\n";
record_preimage($path, "$rr_dir/$name/preimage");
}
}
}
# Now some of the paths that had conflicts earlier might have been
# hand resolved. Others may be similar to a conflict already that
# was resolved before.
for my $path (keys %merge_rr) {
my $name = $merge_rr{$path};
# We could resolve this automatically if we have images.
if (-f "$rr_dir/$name/preimage" &&
-f "$rr_dir/$name/postimage") {
if (merge($name, $path)) {
print STDERR "Resolved '$path' using previous resolution.\n";
# Then we do not have to worry about this path
# anymore.
delete $merge_rr{$path};
next;
}
}
# Let's see if we have resolved it.
(undef, my $hunk) = compute_conflict_name($path);
next if ($hunk);
print STDERR "Recorded resolution for '$path'.\n";
copy($path, "$rr_dir/$name/postimage");
# And we do not have to worry about this path anymore.
delete $merge_rr{$path};
}
# Write out the rest.
write_rr();