diff options
Diffstat (limited to 'dgit-badcommit-fixup')
-rwxr-xr-x | dgit-badcommit-fixup | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/dgit-badcommit-fixup b/dgit-badcommit-fixup new file mode 100755 index 0000000..8b202c0 --- /dev/null +++ b/dgit-badcommit-fixup @@ -0,0 +1,315 @@ +#!/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; +} |