#!/usr/bin/perl -w
#
# Script to help with fallout from #849041.
#
# usage:
#   dgit-badcommit-fixup --check
#   dgit-badcommit-fixup --test
#   dgit-badcommit-fixup --real

# Update procedure, from server operator's point of view:
#
# 1. Test in an offline tree that this DTRT
#
# 2. Announce a transition time.  Tell everyone that between
#    the transition time and their next upload, they must
#    run this script.
#
# 3. At the transition time, run this script in every repo.
#
# 4. Run the mirror script to push changes, if necessary.


use strict;

use POSIX;
use IPC::Open2;
use Data::Dumper;

our $our_version = 'UNRELEASED'; ###substituted###

my $real;

foreach my $a (@ARGV) {
    if ($a eq '--test') {
	$real = 0;
    } elsif ($a eq '--real') {
	$real = 1;
    } elsif ($a eq '--check') {
	$real = -1;
    } else {
	die "$a ?";
    }
}

die unless defined $real;

my $gcfpid = open2 \*GCFO, \*GCFI, 'git cat-file --batch' or die $!;

our %count;

no warnings qw(recursion);

sub runcmd {
    system @_ and die "@_ $! $?";
}

$!=0; $?=0;
my $bare = `git rev-parse --is-bare-repository`;
die "$? $!" if $?;
chomp $bare or die;

sub getobj ($$) {
    my ($obj, $type) = @_;
    print GCFI $obj, "\n" or die $!;
    my $x = <GCFO>;
    my ($gtype, $gsize) = $x =~ m/^\w+ (\w+) (\d+)\n/ or die "$obj ?";
    $gtype eq $type or die "$obj $gtype != $type ?";
    my $gdata;
    (read GCFO, $gdata, $gsize) == $gsize or die "$obj $!";
    $x = <GCFO>;
    $x eq "\n" or die "$obj ($_) $!";
    $count{inspected}++;
    return $gdata;
}

sub hashobj ($$) {
    my ($data,$type) = @_;
    my $gwopid = open2 \*GWO, \*GWI,
	"git hash-object -w -t $type --stdin"
	or die $!;
    print GWI $data or die $!;
    close GWI or die $!;
    $_ = <GWO>;
    close GWO or die $!;
    waitpid $gwopid,0 == $gwopid or die $!;
    die $? if $?;
    m/^(\w+)\n/ or die "$_ ?";
    $count{"rewritten $type"}++;
    return $1;
}

our %memo;

sub rewrite_commit ($);
sub rewrite_commit ($) {
    my ($obj) = @_;
    my $m = \ $memo{$obj};
    return $$m if defined $$m;
    my $olddata = getobj $obj, 'commit';
    $olddata =~ m/(?<=\n)(?=\n)/ or die "$obj ?";
    my $msg = $';
    local $_ = $`;
    s{^(parent )(\w+)$}{ $1 . rewrite_commit($2) }gme;
    $count{'fix overwrite'} += s{^commiter }{committer }gm;
    if (!m{^author }m && !m{^committer }m) {
	m{^parent (\w+)}m or die "$obj ?";
	my $parent = getobj $1, 'commit';
	$parent =~ m/^(?:.+\n)+(author .*\ncommitter .*\n)/;
	m/\n$/ or die "$obj ?";
	$_ .= $1;
	$count{'fix import'}++;
    }
    my $newdata = $_.$msg;
    my $newobj;
    if ($newdata eq $olddata) {
	$newobj = $obj;
	$count{unchanged}++;
#print STDERR "UNCHANGED $obj\n";
    } else {
	$newobj = hashobj $newdata, 'commit';
#print STDERR "REWRITTEN $obj $newobj\n";
    }
    $$m= $newobj;
    return $newobj;
}

our @updates;

sub filter_updates () {
    @updates = grep { $_->[1] ne $_->[2] } @updates;
}

sub rewrite_tag ($) {
    my ($obj) = @_;
    $_ = getobj $obj, 'tag';
    m/^type (\w+)\n/m or die "$obj ?";
    if ($1 ne 'commit') {
	$count{"oddtags $1"}++;
	return $obj;
    }
    m/^object (\w+)\n/m or die "$obj ?";
    my $oldref = $1;
    my $newref = rewrite_commit $oldref;
    if ($oldref eq $newref) {
	return $obj;
    }
    s/^(object )\w+$/ $1.$newref /me or die "$obj ($_) ?";
    s/^-----BEGIN PGP SIGNATURE-----\n.*^-----END PGP SIGNATURE-----\n$//sm;
    return hashobj $_, 'tag';
}

sub edit_rewrite_map ($) {
    my ($old) = @_;

    filter_updates();
    return $old unless @updates;

    my $td = 'dgit-broken-fixup.tmp';
    runcmd qw(rm -rf), $td;
    mkdir $td, 0700 or die "$td $!";
    chdir $td or die $!;
    runcmd qw(git init -q);
    runcmd qw(git config gc.auto 0);
    runcmd qw(rm -rf .git/objects);
    symlink "../../objects", ".git/objects" or die $!;

    my %map;

    if ($old) {
	runcmd qw(git checkout -q), $old;
	open M, "map" or die $!;
	while (<M>) {
	    m/^(\w+)(?:\s+(\w+))?$/ or die;
	    $map{$1} = $2;
	    $count{rewrite_map_previous}++;
	}
	M->error and die $!;
	close M or die $!;
    }

    foreach my $oldc (keys %memo) {
	my $newc = $memo{$oldc};
	next if $oldc eq $newc;
	$map{$oldc} = $newc;
    }
    foreach my $up (@updates) { # catches tags
	$map{ $up->[1] } = $up->[2];
    }

    open M, ">", "map" or die $!;
    printf M "%s%s\n",
	$_, (defined $map{$_} ? " $map{$_}" : "")
	or die $!
	foreach keys %map;
    close M or die $!;

    if (!$old) {
	runcmd qw(git add map);
    }

    runcmd qw(git commit -q), qw(-m), <<END, qw(map);
dgit-badcommit-fixup

[dgit-badcommit-fixup $our_version]
END

    $!=0; $?=0;
    my $new = `git rev-parse HEAD`;
    die "$? $!" if $?;
    chomp $new or die;

    chdir '..' or die $!;
    runcmd qw(rm -rf), $td;

    $count{rewrite_map_updated}++;

    return $new;
}

$!=0; $?=0;
my $refs=`git for-each-ref`;
die "$? $!" if $?;

chomp $refs;

our $org_rewrite_map;

foreach my $rline (split /\n/, $refs) {
    my ($obj, $type, $refname) = 
	$rline =~ m/^(\w+)\s+(\w+)\s+(\S.*)/
	or die "$_ ?";
    if ($refname eq 'refs/dgit-rewrite/map') {
	$org_rewrite_map = $obj;
	next;
    }
    next if $refname =~ m{^refs/dgit-(?:badcommit|badfixuptest)/};

    $!=0; $?=0;
    system qw(sh -ec),
	'exec >/dev/null git symbolic-ref -q "$1"', qw(x),
	$refname;
    if ($?==0) {
	$count{symrefs_ignored}++;
	next;
    }
    die "$? $!" unless $?==256;

    my $rewrite;
    if ($type eq 'commit') {
	$rewrite = rewrite_commit($obj);
    } elsif ($type eq 'tag') {
	$rewrite = rewrite_tag($obj);
    } else {
	warn "ref $refname refers to $type\n";
	next;
    }
    push @updates, [ $refname, $obj, $rewrite ];
}

if ($bare eq 'true') {
    my $new_rewrite_map = edit_rewrite_map($org_rewrite_map);
    push @updates, [ 'refs/dgit-rewrite/map',
		     ($org_rewrite_map // '0'x40),
		     ($new_rewrite_map // '0'x40),
		     1 ];
}

filter_updates();

if (!@updates) {
    print Dumper(\%count), "all is well - nothing to do\n";
    exit 0;
}

#print Dumper(\@updates);

open U, "|git update-ref -m 'dgit bad commit fixup' --stdin" or die $!
    if $real >= 0;

for my $up (@updates) {
    my ($ref, $old, $new, $nobackup) = @$up;
    my $otherref = $ref;
    $otherref =~ s{^refs/}{};
    if ($real > 0) {
	print U <<END or die $! unless $nobackup;
create refs/dgit-badcommit/$otherref $old
END
	print U <<END or die $!;
update $ref $new $old
END
    } elsif ($real==0) {
	print U <<END or die $!;
update refs/dgit-badfixuptest/$otherref $new
END
    } else {
	print "found trouble in history of $ref\n" or die $!;
    }
}

if ($real >= 0) {
    $?=0; $!=0;
    close U or die "$? $!";
    die $? if $?;
}

print Dumper(\%count);

if ($real >= 0) {
    print "old values saved in refs/dgit-badcommit/\n" or die $!;
} elsif ($real == 0) {
    print "testing output saved in refs/dgit-badfixuptest/\n" or die $!;
} else {
    print STDERR "found work to do, exiting status 2\n";
    exit 2;
}
