mirror of
https://github.com/git/git.git
synced 2024-11-18 06:54:55 +01:00
285 lines
6 KiB
Text
285 lines
6 KiB
Text
|
#!/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();
|