--------
[verse]
(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
- [--ref=<ref>] [--ref-action=<mode>] <revision-range>
+ [--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>
DESCRIPTION
-----------
+
The default mode can be configured via the `replay.refAction` configuration variable.
+--linearize::
+ In this mode, each replayed commit is stacked on top of the
+ previously replayed one, so all replayed commits are flattened into
+ a single linear history.
++
+When a merge commit is encountered, the behavior of git-rebase(1)'s
+option `--no-rebase-merges` is imitated. All commits in the range
+reachable from the merge commit are replayed into a linear history, and
+the merge commit itself is dropped. A ref that pointed to a merge commit
+is updated to the merge's last replayed ancestor.
++
+Only a single branch can be linearized at a time: `--linearize` cannot
+be combined with multiple positive revisions or with `--contained`,
+because that would concatenate otherwise unrelated histories into one
+line. To linearize several branches, replay them in separate `git
+replay` invocations.
+
<revision-range>::
Range of commits to replay; see "Specifying Ranges" in
linkgit:git-rev-parse[1]. In `--advance=<branch>` or
const char *const replay_usage[] = {
N_("(EXPERIMENTAL!) git replay "
"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
- "[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
+ "[--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>"),
NULL
};
struct option replay_options[] = {
N_("mode"),
N_("control ref update behavior (update|print)"),
PARSE_OPT_NONEG),
+ OPT_BOOL(0, "linearize", &opts.linearize,
+ N_("drop merge commits, replaying only non-merge commits")),
OPT_END()
};
opts.contained, "--contained");
die_for_incompatible_opt2(!!opts.ref, "--ref",
!!opts.contained, "--contained");
+ die_for_incompatible_opt2(opts.linearize, "--linearize",
+ !!opts.contained, "--contained");
/* Parse ref action mode from command line or config */
ref_mode = get_ref_action_mode(repo, ref_action);
set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
&detached_head, &advance, &revert, &onto, &update_refs);
+ if (opts->linearize &&
+ update_refs && strset_get_size(update_refs) > 1) {
+ ret = error(_("'--linearize' cannot be used with multiple revision ranges"));
+ goto out;
+ }
+
if (opts->ref) {
struct object_id oid;
while ((commit = get_revision(revs))) {
const struct name_decoration *decoration;
- /*
- * Decide where to replay this commit on.
- * If the parent commit was replayed already, the replayed result
- * can be found in `replayed_commits`. Otherwise fall back to `onto`.
- * When reverting, commits are replayed in reverse order and thus
- * its parent isn't replayed yet. Therefore revert commits are
- * always replayed onto `last_commit`.
- */
- struct commit *parent = commit->parents ? commit->parents->item : NULL;
- struct commit *base = get_mapped_commit(replayed_commits, parent, onto);
-
- if (mode == REPLAY_MODE_REVERT)
- base = last_commit;
-
- if (commit->parents && commit->parents->next)
- die(_("replaying merge commits is not supported yet!"));
-
- last_commit = pick_regular_commit(revs->repo, commit, base,
- &merge_opt, &result,
- mode, opts->empty);
+ if (commit->parents && commit->parents->next) {
+ if (!opts->linearize)
+ die(_("replaying merge commits is not supported yet!"));
+ /*
+ * Drop the merge commit: do not pick it, leave
+ * `last_commit` unchanged, and fall through to the
+ * rest of the loop. As a result:
+ * - refs pointing to the merge commit will be updated
+ * to `last_commit`.
+ * - the next replayed commit uses `last_commit` as its
+ * `base`.
+ */
+ } else {
+ /*
+ * Decide where to replay this commit onto.
+ * If the parent commit was replayed already, the replayed result
+ * can be found in `replayed_commits`. Otherwise fall back to `onto`.
+ * When reverting, commits are replayed in reverse order and thus
+ * its parent isn't replayed yet. Therefore revert commits are
+ * always replayed onto `last_commit`.
+ * Also when opts->linearize is true, set the base to
+ * `last_commit` to create a single linear history.
+ */
+ struct commit *parent = commit->parents ? commit->parents->item : NULL;
+ struct commit *base = get_mapped_commit(replayed_commits, parent, onto);
+
+ if (opts->linearize || mode == REPLAY_MODE_REVERT)
+ base = last_commit;
+
+ last_commit = pick_regular_commit(revs->repo, commit, base,
+ &merge_opt, &result,
+ mode, opts->empty);
+ }
+
if (!last_commit)
break;
* Defaults to REPLAY_EMPTY_COMMIT_DROP.
*/
enum replay_empty_commit_action empty;
+
+ /*
+ * Whether to linearize the commits (i.e. drop merge commits).
+ */
+ int linearize;
};
/* This struct is used as an out-parameter by `replay_revisions()`. */
test_merge P O --no-ff &&
git switch main &&
+ git switch --orphan unrelated &&
+ test_commit unrelated-root &&
+
git switch -c conflict B &&
- test_commit C.conflict C.t conflict
+ test_commit C.conflict C.t conflict &&
+ git branch -D unrelated &&
+
+ git switch -c divergent-x main &&
+ test_commit X &&
+ git switch -c divergent-y main &&
+ test_commit Y &&
+ git switch divergent-x &&
+ test_merge Z divergent-y --no-ff
'
test_expect_success 'setup bare' '
test_grep "cannot be used with multiple revision ranges" err
'
+test_expect_success 'replay to rebase merge commit with --linearize' '
+ git replay --ref-action=print --linearize \
+ --onto main I..topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+
+ git log --format=%s $(cut -f 3 -d " " result) >actual &&
+ test_write_lines O N J M L B A >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'replay to rebase merge commit with --linearize down to the root commit' '
+ git replay --ref-action=print --linearize \
+ --onto unrelated-root topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+
+ git log --format=%s $(cut -f 3 -d " " result) >actual &&
+ test_write_lines O N J I B A unrelated-root >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'replay to cherry-pick merge commit with --linearize' '
+ git replay --ref-action=print --linearize \
+ --advance main I..topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+
+ git log --format=%s $(cut -f 3 -d " " result) >actual &&
+ test_write_lines O N J M L B A >expect &&
+ test_cmp expect actual &&
+
+ printf "update refs/heads/main " >expect &&
+ printf "%s " $(cut -f 3 -d " " result) >>expect &&
+ git rev-parse main >>expect &&
+ test_cmp expect result
+'
+
+test_expect_success 'replay --linearize produces the same patches' '
+ git replay --ref-action=print --linearize \
+ --onto main I..topic-with-merge >result &&
+
+ test_line_count = 1 result &&
+ tip=$(cut -f 3 -d " " result) &&
+
+ # range-diff does not care about the dropped merge,
+ # so the original commits (I..topic-with-merge)
+ # and the replayed chain (main..tip) must produce identical patches.
+ git range-diff I..topic-with-merge main..$tip >out &&
+ test_file_not_empty out &&
+ test_grep ! -v "=" out &&
+
+ git log --oneline main..$tip >out &&
+ test_line_count = 3 out
+'
+
+test_expect_success '--linearize rejects multiple revision ranges' '
+ test_must_fail git replay --ref-action=print --linearize \
+ --onto main ^B topic2 topic3 topic4 2>err &&
+ test_grep "cannot be used with multiple revision ranges" err
+'
+
+test_expect_success 'replay with --linearize of a divergent merge keeps both sides' '
+ git replay --ref-action=print --linearize \
+ --onto main main..divergent-x >result &&
+ test_line_count = 1 result &&
+ tip=$(cut -f 3 -d " " result) &&
+
+ # The merge Z is dropped, but both X and Y are linearized onto main;
+ # neither side is lost.
+ git log --format=%s main..$tip >actual &&
+ test_write_lines Y X >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '--linearize and --contained cannot be used together' '
+ test_must_fail git replay --ref-action=print --linearize --contained \
+ --onto main ^B topic-with-merge 2>err &&
+ test_grep "cannot be used together" err
+'
+
+test_expect_success 'replay --revert with --linearize reverts a range containing a merge' '
+ git replay --ref-action=print --revert=divergent-x --linearize \
+ main..divergent-x >result &&
+ test_line_count = 1 result &&
+ tip=$(cut -f 3 -d " " result) &&
+
+ git log --format=%s $tip >actual &&
+ test_write_lines \
+ "Revert \"X\"" "Revert \"Y\"" Z Y X M L B A >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git cat-file -e $tip:X.t &&
+ test_must_fail git cat-file -e $tip:Y.t
+'
+
test_done