+sub gbp_pq_export ($$$) {
+ my ($bname, $base, $tip) = @_;
+ # must be run in a workarea. $bname and patch-queue/$bname
+ # ought not to exist. Leaves you on patch-queue/$bname with
+ # the patches staged but not committed.
+ printdebug "gbp_pq_export $bname $base $tip\n";
+ runcmd @git, qw(checkout -q -b), $bname, $base;
+ runcmd @git, qw(checkout -q -b), "patch-queue/$bname", $tip;
+ my @gbp_cmd = (qw(gbp pq export));
+ my $r = system shell_cmd 'exec >../gbp-pq-err 2>&1', @gbp_cmd;
+ if ($r) {
+ { local ($!,$?); copy('../gbp-pq-err', \*STDERR); }
+ failedcmd @gbp_cmd;
+ }
+ runcmd @git, qw(add -f debian/patches) if stat_exists 'debian/patches';
+}
+
+
+# xxx allow merge resolution separately from laundering, before git merge
+#
+# xxx general gdr docs highlight forbidden things
+# xxx general gdr docs list allowable things ?
+# xxx general gdr docs explicitly forbid some rebase
+#
+# xxx provide a way for the user to help
+# xxx (eg, provide wreckage provide way to continue)
+
+# later/rework?
+# use git-format-patch?
+# our own patch identification algorithm?
+# this is an alternative strategy
+
+sub merge_failed ($$;@) {
+ my ($wrecknotes, $emsg, @xmsgs) = @_;
+ my @m;
+ push @m, "Merge resolution failed: $emsg";
+ push @m, @xmsgs;
+
+ changedir $maindir;
+
+ my @updates;
+ merge_wreckage_cleaning \@updates;
+ run_ref_updates_now "merge failed", \@updates;
+
+ @updates = ();
+ keys %$wrecknotes;
+ while (my ($k,$v) = each %$wrecknotes) {
+ push @updates, "create $wrecknoteprefix/$k $v";
+ }
+ run_ref_updates_now "merge failed", \@updates;
+ push @m, "Wreckage left in $wrecknoteprefix/*.";
+
+ push @m, "See git-debrebase(1) section FAILED MERGES for suggestions.";
+
+ # use finish rather than fail, in case we are within an eval
+ # (that can happen inside walk!)
+ print STDERR "\n";
+ print STDERR "$us: $_\n" foreach @m;
+ finish 15;
+}
+
+sub mwrecknote ($$$) {
+ my ($wrecknotes, $reftail, $commitish) = @_;
+ confess unless defined $commitish;
+ printdebug "mwrecknote $reftail $commitish\n";
+ $wrecknotes->{$reftail} = $commitish;
+}
+
+sub merge_attempt_cmd {
+ my $wrecknotes = shift @_;
+ debugcmd '+', @_;
+ $!=0; $?=-1;
+ if (system @_) {
+ merge_failed $wrecknotes,
+ failedcmd_waitstatus(),
+ "failed command: @_";
+ }
+}
+
+sub merge_series_patchqueue_convert ($$$);
+
+sub merge_series ($$$;@) {
+ my ($newbase, $wrecknotes, $base_q, @input_qs) = @_;
+ # $base_q{SeriesBase} $input_qs[]{SeriesBase}
+ # $base_q{SeriesTip} $input_qs[]{SeriesTip}
+ # ^ specifies several patch series (currently we only support exactly 2)
+ # return value is a commit which is the result of
+ # merging the two versions of the same topic branch
+ # $input_q[0] and $input_q[1]
+ # with respect to the old version
+ # $base_q
+ # all onto $newbase.
+
+ # Creates, in *_q, a key MR for its private use
+
+ printdebug "merge_series newbase=$newbase\n";
+
+ $input_qs[$_]{MR}{S} = $_ foreach (0..$#input_qs);
+ $base_q->{MR}{S} = 'base';
+
+ my %prereq;
+ # $prereq{<patch filename>}{<possible prereq}{<S>} = 1 or absent
+ # $prereq{<patch filename>}{<possible prereq} exists or not (later)
+ # $prereq{<patch filename>} exists or not (even later)
+
+ my $merged_pq;
+
+ my $mwrecknote = sub { &mwrecknote($wrecknotes, @_); };
+
+ my $attempt_cmd = sub { &merge_attempt_cmd($wrecknotes, @_); };
+
+ local $workarea;
+ fresh_workarea "merge";
+ my $seriesfile = "debian/patches/series";
+ in_workarea sub {
+ playtree_setup();
+ foreach my $q ($base_q, reverse @input_qs) {
+ my $s = $q->{MR}{S};
+ gbp_pq_export "p-$s", $q->{SeriesBase}, $q->{SeriesTip};
+ my @earlier;
+ if (open S, $seriesfile) {
+ while (my $patch = <S>) {
+ chomp $patch or die $!;
+ $prereq{$patch} //= {};
+ foreach my $earlier (@earlier) {
+ $prereq{$patch}{$earlier}{$s}++ and die;
+ }
+ push @earlier, $patch;
+ stat "debian/patches/$patch" or die "$patch ?";
+ }
+ S->error and die "$seriesfile $!";
+ close S;
+ } else {
+ die "$seriesfile $!" unless $!==ENOENT;
+ }
+ read_tree_upstream $newbase, 1;
+ my $pec = make_commit [ grep { defined } $base_q->{MR}{PEC} ], [
+ "Convert $s to patch queue for merging",
+ "[git-debrebase merge-innards patch-queue import:".
+ " $q->{SeriesTip}]"
+ ];
+ printdebug "merge_series pec $pec ";
+ runcmd @git, qw(rm -q --ignore-unmatch --cached), $seriesfile;
+ $pec = make_commit [ $pec ], [
+ "Drop series file from $s to avoid merge trouble",
+ "[git-debrebase merge-innards patch-queue prep:".
+ " $q->{SeriesTip}]"
+ ];
+
+ read_tree_debian $newbase;
+ if (@earlier) {
+ read_tree_subdir 'debian/patches', "$pec:debian/patches";
+ } else {
+ rm_subdir_cached 'debian/patches';
+ }
+ $pec = make_commit [ $pec ], [
+ "Update debian/ (excluding patches) to final to avoid re-merging",
+ "debian/ was already merged and we need to just take that.",
+ "[git-debrebase merge-innards patch-queue packaging:".
+ " $q->{SeriesTip}]"
+ ];
+
+ printdebug "pec' $pec\n";
+ runcmd @git, qw(reset -q --hard), $pec;
+ $q->{MR}{PEC} = $pec;
+ $mwrecknote->("$q->{LeftRight}-patchqueue", $pec);
+ }
+ # now, because of reverse, we are on $input_q->{MR}{OQC}
+ runcmd @git, qw(checkout -q -b merge);
+ printdebug "merge_series merging...\n";
+ my @mergecmd = (@git, qw(merge --quiet --no-edit), "p-1");
+
+ $attempt_cmd->(@mergecmd);
+
+ printdebug "merge_series merge ok, series...\n";
+ # We need to construct a new series file
+ # Firstly, resolve prereq
+ foreach my $f (sort keys %prereq) {
+ printdebug "merge_series patch\t$f\t";
+ if (!stat_exists "debian/patches/$f") {
+ print DEBUG " drop\n" if $debuglevel;
+ # git merge deleted it; that's how we tell it's not wanted
+ delete $prereq{$f};
+ next;
+ }
+ print DEBUG " keep\n" if $debuglevel;
+ foreach my $g (sort keys %{ $prereq{$f} }) {
+ my $gfp = $prereq{$f}{$g};
+ printdebug "merge_series prereq\t$f\t-> $g\t";
+ if (!!$gfp->{0} == !!$gfp->{1}
+ ? $gfp->{0}
+ : !$gfp->{base}) {
+ print DEBUG "\tkeep\n" if $debuglevel;
+ } else {
+ print DEBUG "\tdrop\n" if $debuglevel;
+ delete $prereq{$f}{$g};
+ }
+ }
+ }
+
+ my $unsat = sub {
+ my ($f) = @_;
+ return scalar keys %{ $prereq{$f} };
+ };
+
+ my $nodate = time + 1;
+ my %authordate;
+ # $authordate{<patch filename>};
+ my $authordate = sub {
+ my ($f) = @_;
+ $authordate{$f} //= do {
+ open PF, "<", "debian/patches/$f" or die "$f $!";
+ while (<PF>) {
+ return $nodate if m/^$/;
+ last if s{^Date: }{};
+ }
+ chomp;
+ return cmdoutput qw(date +%s -d), $_;
+ };
+ };
+
+ open NS, '>', $seriesfile or die $!;
+
+ while (keys %prereq) {
+ my $best;
+ foreach my $try (sort keys %prereq) {
+ if ($best) {
+ next if (
+ $unsat->($try) <=> $unsat->($best) or
+ $authordate->($try) <=> $authordate->($best) or
+ $try cmp $best
+ ) >= 0;
+ }
+ $best = $try;
+ }
+ printdebug "merge_series series next $best\n";
+ print NS "$best\n" or die $!;
+ delete $prereq{$best};
+ foreach my $gp (values %prereq) {
+ delete $gp->{$best};
+ }
+ }
+
+ runcmd @git, qw(add), $seriesfile;
+ runcmd @git, qw(commit --quiet -m), 'Merged patch queue form';
+ $merged_pq = git_rev_parse 'HEAD';
+ $mwrecknote->('merged-patchqueue', $merged_pq);
+ };
+ return merge_series_patchqueue_convert
+ $wrecknotes, $newbase, $merged_pq;
+}
+
+sub merge_series_patchqueue_convert ($$$) {
+ my ($wrecknotes, $newbase, $merged_pq) = @_;
+
+ my $result;
+ in_workarea sub {
+ playtree_setup();
+ printdebug "merge_series series gbp pq import\n";
+ runcmd @git, qw(checkout -q -b mergec), $merged_pq;
+
+ merge_attempt_cmd($wrecknotes, qw(gbp pq import));
+
+ # OK now we are on patch-queue/merge, and we need to rebase
+ # onto the intended parent and drop the patches from each one
+
+ printdebug "merge_series series ok, building...\n";
+ my $build = $newbase;
+ my @lcmd = (@git, qw(rev-list --reverse mergec..patch-queue/mergec));
+ foreach my $c (grep /./, split /\n/, cmdoutput @lcmd) {
+ my $commit = git_cat_file $c, 'commit';
+ printdebug "merge_series series ok, building $c\n";
+ read_tree_upstream $c, 0, $newbase;
+ my $tree = cmdoutput @git, qw(write-tree);
+ $commit =~ s{^parent (\S+)$}{parent $build}m or confess;
+ $commit =~ s{^tree (\S+)$}{tree $tree}m or confess;
+ open C, ">", "../mcommit" or die $!;
+ print C $commit or die $!;
+ close C or die $!;
+ $build = cmdoutput @git, qw(hash-object -w -t commit ../mcommit);
+ }
+ $result = $build;
+ mwrecknote($wrecknotes, 'merged-result', $result);
+
+ runcmd @git, qw(update-ref refs/heads/result), $result;
+
+ runcmd @git, qw(checkout -q -b debug);
+ runcmd @git, qw(commit --allow-empty -q -m M-INDEX);
+ runcmd @git, qw(add .);
+ runcmd @git, qw(commit --allow-empty -q -m M-WORKTREE);
+ my $mdebug = git_rev_parse 'HEAD';
+ printdebug sprintf "merge_series done debug=%s\n", $mdebug;
+ mwrecknote($wrecknotes, 'merged-debug', $mdebug);
+ };
+ printdebug "merge_series returns $result\n";
+ return $result;
+}
+