dgit-badcommit-fixup 7.22 KB
Newer Older
Ian Jackson's avatar
Ian Jackson committed
1
#!/usr/bin/perl -w
Ian Jackson's avatar
Ian Jackson committed
2 3 4
#
# Script to help with fallout from #849041.
#
5
# usage:
6
#   dgit-badcommit-fixup --check
Ian Jackson's avatar
Ian Jackson committed
7 8
#   dgit-badcommit-fixup --test
#   dgit-badcommit-fixup --real
9

10 11 12 13 14 15 16 17 18 19 20 21
# 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.

22 23
END { $? = $Debian::Dgit::ExitStatus::desired // -1; };
use Debian::Dgit::ExitStatus;
24

Ian Jackson's avatar
Ian Jackson committed
25
use strict;
Ian Jackson's avatar
Ian Jackson committed
26

Ian Jackson's avatar
Ian Jackson committed
27 28
use POSIX;
use IPC::Open2;
Ian Jackson's avatar
Ian Jackson committed
29
use Data::Dumper;
Ian Jackson's avatar
Ian Jackson committed
30

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

33
my $real;
34

35 36 37 38 39
foreach my $a (@ARGV) {
    if ($a eq '--test') {
	$real = 0;
    } elsif ($a eq '--real') {
	$real = 1;
40 41
    } elsif ($a eq '--check') {
	$real = -1;
42 43 44 45 46 47
    } else {
	die "$a ?";
    }
}

die unless defined $real;
48

Ian Jackson's avatar
Ian Jackson committed
49
my $gcfpid = open2 \*GCFO, \*GCFI, 'git cat-file --batch' or die $!;
Ian Jackson's avatar
Ian Jackson committed
50

Ian Jackson's avatar
Ian Jackson committed
51
our %count;
Ian Jackson's avatar
Ian Jackson committed
52

Ian Jackson's avatar
Ian Jackson committed
53
no warnings qw(recursion);
Ian Jackson's avatar
Ian Jackson committed
54

55
sub runcmd {
56
    system @_ and die "@_ $! $?";
57 58 59 60 61
}

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

64 65 66 67 68 69 70 71 72 73 74
our @configs;
foreach my $k (qw(core.sharedRepository)) {
    $?=0; $!=0; my $v = `set -x; git config --local $k`;
    if (defined $v && $?==0 && chomp $v) {
	push @configs, [ $k, $v ];
    } elsif (defined $v && $?==256 && $v eq '') {
    } else {
	die "git-config --local $k => $v $? $! ?";
    }
}

Ian Jackson's avatar
Ian Jackson committed
75 76 77 78
sub getobj ($$) {
    my ($obj, $type) = @_;
    print GCFI $obj, "\n" or die $!;
    my $x = <GCFO>;
Ian Jackson's avatar
Ian Jackson committed
79
    my ($gtype, $gsize) = $x =~ m/^\w+ (\w+) (\d+)\n/ or die "$obj ?";
Ian Jackson's avatar
Ian Jackson committed
80 81
    $gtype eq $type or die "$obj $gtype != $type ?";
    my $gdata;
Ian Jackson's avatar
Ian Jackson committed
82 83 84 85
    (read GCFO, $gdata, $gsize) == $gsize or die "$obj $!";
    $x = <GCFO>;
    $x eq "\n" or die "$obj ($_) $!";
    $count{inspected}++;
Ian Jackson's avatar
Ian Jackson committed
86 87 88
    return $gdata;
}

Ian Jackson's avatar
Ian Jackson committed
89 90 91
sub hashobj ($$) {
    my ($data,$type) = @_;
    my $gwopid = open2 \*GWO, \*GWI,
92
	"git hash-object -w -t $type --stdin"
Ian Jackson's avatar
Ian Jackson committed
93 94 95 96 97 98 99 100 101 102 103 104
	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;
}

Ian Jackson's avatar
Ian Jackson committed
105 106
our %memo;

Ian Jackson's avatar
Ian Jackson committed
107
sub rewrite_commit ($);
Ian Jackson's avatar
Ian Jackson committed
108 109 110 111 112 113 114
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 = $';
115
    local $_ = $`;
Ian Jackson's avatar
Ian Jackson committed
116
    s{^(parent )(\w+)$}{ $1 . rewrite_commit($2) }gme;
117
    $count{'fix overwrite'} += s{^commiter }{committer }gm;
Ian Jackson's avatar
Ian Jackson committed
118 119 120 121 122 123
    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;
124
	$count{'fix import'}++;
Ian Jackson's avatar
Ian Jackson committed
125
    }
Ian Jackson's avatar
Ian Jackson committed
126
    my $newdata = $_.$msg;
Ian Jackson's avatar
Ian Jackson committed
127 128
    my $newobj;
    if ($newdata eq $olddata) {
Ian Jackson's avatar
Ian Jackson committed
129 130
	$newobj = $obj;
	$count{unchanged}++;
131
#print STDERR "UNCHANGED $obj\n";
Ian Jackson's avatar
Ian Jackson committed
132
    } else {
Ian Jackson's avatar
Ian Jackson committed
133
	$newobj = hashobj $newdata, 'commit';
134
#print STDERR "REWRITTEN $obj $newobj\n";
Ian Jackson's avatar
Ian Jackson committed
135 136 137 138 139
    }
    $$m= $newobj;
    return $newobj;
}

140 141 142 143 144 145
our @updates;

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

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
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';
}
164

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

168 169 170
    filter_updates();
    return $old unless @updates;

171 172 173 174 175 176 177 178
    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 $!;
179 180 181
    foreach my $c (@configs) {
	runcmd qw(git config), $c->[0], $c->[1];
    }
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196

    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 $!;
    }

197 198 199 200 201 202
    foreach my $oldc (keys %memo) {
	my $newc = $memo{$oldc};
	next if $oldc eq $newc;
	$map{$oldc} = $newc;
    }
    foreach my $up (@updates) { # catches tags
203
	$map{ $up->[1] } = $up->[2];
204 205 206
    }

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

213 214 215 216
    if (!$old) {
	runcmd qw(git add map);
    }

217 218 219 220 221
    runcmd qw(git commit -q), qw(-m), <<END, qw(map);
dgit-badcommit-fixup

[dgit-badcommit-fixup $our_version]
END
222 223 224 225 226 227 228 229 230 231 232 233 234 235

    $!=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;
}

236 237 238 239 240 241
$!=0; $?=0;
my $refs=`git for-each-ref`;
die "$? $!" if $?;

chomp $refs;

242 243
our $org_rewrite_map;

244 245 246 247
foreach my $rline (split /\n/, $refs) {
    my ($obj, $type, $refname) = 
	$rline =~ m/^(\w+)\s+(\w+)\s+(\S.*)/
	or die "$_ ?";
248
    if ($refname eq 'refs/dgit-rewrite/map') {
249
	$org_rewrite_map = $obj;
250
	next;
251
    }
252
    next if $refname =~ m{^refs/dgit-(?:badcommit|badfixuptest)/};
253 254 255 256 257 258 259 260 261 262 263

    $!=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;

264 265 266 267 268 269 270 271
    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;
272
    }
273 274 275
    push @updates, [ $refname, $obj, $rewrite ];
}

276 277
if ($bare eq 'true') {
    my $new_rewrite_map = edit_rewrite_map($org_rewrite_map);
278
    push @updates, [ 'refs/dgit-rewrite/map',
279
		     ($org_rewrite_map // '0'x40),
280 281
		     ($new_rewrite_map // '0'x40),
		     1 ];
282 283
}

284
filter_updates();
285

286
if (!@updates) {
287
    print Dumper(\%count), "all is well - nothing to do\n";
288
    finish 0;
289 290
}

291
#print Dumper(\@updates);
292

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

296
for my $up (@updates) {
297
    my ($ref, $old, $new, $nobackup) = @$up;
298 299
    my $otherref = $ref;
    $otherref =~ s{^refs/}{};
300
    if ($real > 0) {
301
	print U <<END or die $! unless $nobackup;
302
create refs/dgit-badcommit/$otherref $old
303 304
END
	print U <<END or die $!;
305 306
update $ref $new $old
END
307
    } elsif ($real==0) {
308 309 310
	print U <<END or die $!;
update refs/dgit-badfixuptest/$otherref $new
END
311 312
    } else {
	print "found trouble in history of $ref\n" or die $!;
313 314 315
    }
}

316 317 318 319 320
if ($real >= 0) {
    $?=0; $!=0;
    close U or die "$? $!";
    die $? if $?;
}
321 322 323

print Dumper(\%count);

324
if ($real >= 0) {
325
    print "old values saved in refs/dgit-badcommit/\n" or die $!;
326
} elsif ($real == 0) {
327
    print "testing output saved in refs/dgit-badfixuptest/\n" or die $!;
328 329
} else {
    print STDERR "found work to do, exiting status 2\n";
330
    finish 2;
331
}
332 333

finish 0;