merge-build 7.65 KB
Newer Older
1 2
#!/usr/bin/perl 

Kevin Atkinson's avatar
Kevin Atkinson committed
3 4 5 6
#
# See http://users.emulab.net/trac/emulab/wiki/MergeBuild
#

7
use Cwd;
8
use File::Temp qw(mktemp);
9

10 11 12 13 14 15 16 17
use warnings;
use strict;

sub cd ($) {
    print "cd $_[0]\n";
    chdir $_[0] or die "Unable to cd to $_[0]\n";
}

18 19
sub echo_command (@) {
    print "@_\n";
20 21
}

22
sub sys (@) {
23 24 25 26 27 28 29 30
    my $ops = ref $_[0] ? shift : {};
    echo_command @_ unless $ops->{quiet};
    system @_;
    my $failed = $? != 0;
    my $ret = $? >> 8;
    if ($failed && $ops->{ok_exit_code}) {
	$failed = 0 if $ret == $ops->{ok_exit_code};
    }
31
    die "ERROR: @_ failed\n" if $failed && !$ops->{ignore_errors};
32 33 34 35 36 37
    return $ret;
}

sub sysq (@) {
    my $ops = ref $_[0] ? shift : {};
    sys {%$ops, 'quiet'=>1}, @_;
38 39
}

40 41
sub sysp (@) {
    my $cmd = join(' ', @_, '|');
42
    #print "$cmd\n";
43 44 45 46 47
    open F, $cmd or die $!;
    local $/ = undef;
    my $output = <F>;
    close F;
    die "ERROR: $cmd faild\n" unless $? == 0;
48
    chop $output;
49 50 51 52 53 54 55 56
    return $output;
}

sub usage() {
    print "usage: merge-build prep|reset|sync\n";
    exit(1);
}

57 58 59
#
# Read config file
#
60 61 62 63 64 65
open F, ".merge-build" or die "Unable to open .merge-build\n";
my $source_tree;
my $merge_tree;
my @branches;
my $branches_str;
my $merge_branch = "merge";
66 67
my $merge_base = "merge-base";
my $auto_reset = 1;
68 69 70 71 72 73 74 75 76 77 78 79
while (<F>) {
    s/#.+//;
    next unless /\S/;
    my ($key, $value) = /^\s*(\S+)\s+(.+?)\s*$/ or die "Bad Line\n";
    if ($key eq 'source-tree') {
	$source_tree = $value;
    } elsif ($key eq 'merge-tree') {
	$merge_tree = $value;
    } elsif ($key eq 'branches') {
	@branches = split /\s+/, $value;
    } elsif ($key eq 'merge-branch') {
	$merge_branch = $value;
80 81 82 83 84 85 86 87 88 89
    } elsif ($key eq 'merge-base') {
	$merge_base = $value;
    } elsif ($key eq 'auto-reset') {
	if ($value eq 'yes') { 
	    $auto_reset = 1;
	} elsif ($value eq 'no') {
	    $auto_reset = 0;
	} else {
	    die "Expected \"yes\" or \"no\" for auto-reset value\n";
	}
90 91 92 93 94 95 96 97
    } else {
	die "Unknown key in .merge-build: $key\n";
    }
}
sub missing_key($) {die "Missing key in .merge-build: $_[0]\n";}
missing_key('source-tree') unless defined $source_tree;
missing_key('merge-tree') unless defined $merge_tree;
missing_key('branches') unless @branches;
98 99
@branches = map {"origin/$_"} @branches;
my $branch_str = join ' ', @branches;
100

101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
#
# Figure out where we are
#
sub same_file($$) {
    my ($dev0,$ino0) = stat $_[0];
    my ($dev1,$ino1) = stat $_[1];
    return defined $dev0 && defined $dev1 && $dev0 == $dev1 && $ino0 == $ino1;
}
sub where_am_i() {
    my $cwd = cwd();
    if (same_file($cwd,"$cwd/$source_tree/$merge_tree")) {
	return 'merge';
    } elsif (same_file($cwd,"$cwd/$merge_tree/$source_tree")) {
	return 'source';
    } else {
	die "I don't know where I am! Check that source-tree and merge-tree are correct.\n";
    }
}
sub cd_source() {
    cd $source_tree if where_am_i() eq 'merge';
    die unless where_am_i() eq 'source';
}
sub cd_merge() {
    cd $merge_tree if where_am_i() eq 'source';
    die unless where_am_i() eq 'merge';
}
127

128 129 130 131
#
#
#

132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
#
#
#

sub check_for_conflicts() {
    my @files = split /\n/, sysp "git ls-files -u";
    my %files;
    foreach (@files) {
	/\S+\s+\S+\s+\S+\s+(\S+)/ or die;
	$files{$1} = 1;
    }
    my @ok;
    my @unresolved;
    foreach my $f (keys %files) {
	local $/ = undef;
	open F, $f or die $!;
	local $_ = <F>;
	if (/^={7}|^<{7}|^>{7}/m) {push @unresolved, $f}
	else                      {push @ok, $f}
    }
    sys 'git', 'add', '--', @ok if @ok;
    return @unresolved;
}

156 157
sub save_uncommited() {
    cd_source();
158
    print "Saving Uncommited Changes ...\n";
159 160 161 162 163 164
    sysq "git update-index -q --refresh";
    local $ENV{GIT_INDEX_FILE} = mktemp ".git/index.XXXXXX";
    sysq "cp -p .git/index $ENV{GIT_INDEX_FILE}";
    sysq "git add -A";
    sysq "git reset -q .merge-build";
    my $tree_id = sysp "git write-tree";
165
    my $date = localtime();
Kevin Atkinson's avatar
Kevin Atkinson committed
166 167 168 169 170
    my $msg = "Uncommitted changes: $date";
    my $stash_id = sysp "echo '$msg' | git commit-tree $tree_id -p HEAD";
    unlink($ENV{GIT_INDEX_FILE});
    sysq ": >> .git/logs/build-stash"; # make sure the reflogs are kept.
    sysq "git update-ref -m '$msg' build-stash $stash_id";
171
    print "... done, commit id $stash_id\n";
172 173 174
    return $stash_id;
}

175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
sub apply_uncommited($) {
    my ($stash_id) = @_;
    my $ret = sys({ok_exit_code=>1},
                  "git merge-recursive -- HEAD $stash_id");
    if ($ret == 1) {
	sys "git rerere";
	my @unresolved = check_for_conflicts();
	if (@unresolved) {
	    print "Fix conflicts in:\n";
	    print '  ', join (' ', @unresolved), "\n";
	    print "and run \"git rerere\"\n";
	    exit 1;
	}
    }
}

191 192 193 194
sub merge_base(@) {
    cd_merge();
    my $res = shift @_;
    while (@_) {
195 196
        my $other = shift @_;
        $res = sysp "git merge-base $res $other";
197 198 199 200
    }
    return $res;
}

201 202 203 204 205
sub init($) {
    my ($cmd) = @_;
    cd_merge();
    my $base = merge_base @branches;
    sys "git $cmd $base";
206
    #sys "git clean -f -d";
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
    sys "git commit -q --allow-empty -m Marker";
    sys "git tag -f merge-base";
}

sub notall { $_ || return 1 for @_; 0 }
sub auto_reset() {
    cd_merge();
    my $merge_base = sysp "git rev-parse merge-base";
    my $new_base = merge_base @branches;
    open F, "git rev-list $branch_str ^$new_base |" or die $!;
    my %source_commits;
    while (<F>) {
	chop;
  	$source_commits{$_} = 1;
    }
    $source_commits{$merge_base} = 1;
    close F or die $!;
    cd_merge();
    $/ = "\0";
    open F, "git log --pretty=raw -z --first-parent --topo-order merge-base..HEAD | " or die $!;
    my $reset_point;
    my $last;
    while (<F>) {
	my ($commit) = /^commit (.+)$/m or die;
	next if $source_commits{$commit};
	my @parents = /^parent (.+)$/mg;
	shift @parents;
	next unless @parents;
	$last = $commit;
	$reset_point = $commit if notall map {$source_commits{$_}} @parents;
    }
    close F or die $!;
    if ($reset_point) {
	if ($reset_point eq $last) {
	    print "Starting over\n"; 
	    init("reset -q --hard");
	} else {
	    print "Need to reset to $reset_point^\n"; 
245
	    sys "git reset -q --hard $reset_point^";
246
	    #sys "git clean -f -d";
247 248 249 250
	}
    }
}

251 252 253 254
#
# Finally, do the work
#
my $op = shift @ARGV || '';
255
if ($op eq 'prep') {
256 257 258 259 260 261 262 263 264 265
    if (-e $merge_tree) {
	print "Fixing up existing merge tree...\n";
	cd_merge();
	sys "git checkout `git rev-parse HEAD`";
	sys {ignore_errors=>1}, "git branch -D $merge_branch";
    } else {
	sys "git clone -sn . $merge_tree";
	cd_merge();
    }
    sys "git config rerere.enabled true";
266
    init("checkout -q -b $merge_branch");
267 268 269 270 271 272
    sys "ln -s $source_tree/.merge-build" unless -e "$merge_tree/.merge-build";
    print "Writing .git/info/exclude\n";
    open F, ">.git/info/exclude";
    print F ".merged\n";
    print F ".merge-build\n";
    close F;
273
} elsif ($op eq 'reset') {
274
    cd_merge();
275 276 277 278 279 280
    sys "git fetch -q origin";
    init("reset -q --hard");
} elsif ($op eq 'auto-reset') {
    cd_merge();
    sys "git fetch -q origin";
    auto_reset();
281
} elsif ($op eq 'sync') {
282
    cd_source();
283
    my $stash_id = save_uncommited();
284
    cd_merge();
285
    sys "git fetch -q origin";
286
    auto_reset() if $auto_reset;
287
    sys "git reset -q --hard";
288
    #sys "git clean -f -d";
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
    foreach (@branches) {
	unlink ".git/MERGE_HEAD";
	my $cmd = "git merge -q $_";
	echo_command $cmd;
	open F, "$cmd |" or die $!;
	my $conflicts = 0;
	while (<F>) {
	    print;
	    $conflicts = 1 if /fix conflicts and then commit the result/;
	}
	close F;
	my $failed = $? != 0;
	$failed = 0 if $conflicts && ($? & 127) == 0 && -e ".git/MERGE_HEAD";
	die "ERROR: $cmd faild\n" if $failed;
	if ($conflicts) {
	    my @unresolved = check_for_conflicts();
	    if (@unresolved) {
		exit (1);
	    } else {
		print "All conflicts resolved with rerere, continuing.\n";
		sys "git commit -q -F .git/MERGE_MSG";
	    }
	}
    }
    apply_uncommited($stash_id);
314 315 316 317
    sys "touch .merged";
} else {
    usage();
}