From ea0af2fe30c60ffff021a7a25f25bfae76a967ec Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Wed, 10 Jun 2026 16:49:12 +0200 Subject: [PATCH 1/3] replay: refactor enum replay_mode into a bool In 2760ee4983 (replay: add --revert mode to reverse commit changes, 2026-03-26) the enum `replay_mode` was introduced. This has two possible values: - The value `REPLAY_MODE_REVERT` is used when option `--revert` is passed to git-replay(1). When using this value the commits are processed in reverse order and the inverse of the changes are applied. - The value `REPLAY_MODE_PICK` is used when either option `--onto` or `--advance` is used. In both cases the commits are processed in normal order, and the changes are applied as-is. Since there are only two possible values of this enum, simplify the code by converting the enum into a bool. This avoids adding code paths that check for invalid values of the enum, and shortens code where the value is checked with a ternary operator. Signed-off-by: Toon Claes Signed-off-by: Junio C Hamano --- replay.c | 59 ++++++++++++++++++++++++-------------------------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/replay.c b/replay.c index da531d5bc6..1b4453af3f 100644 --- a/replay.c +++ b/replay.c @@ -18,11 +18,6 @@ */ #define the_repository DO_NOT_USE_THE_REPOSITORY -enum replay_mode { - REPLAY_MODE_PICK, - REPLAY_MODE_REVERT, -}; - static const char *short_commit_name(struct repository *repo, struct commit *commit) { @@ -81,7 +76,7 @@ static struct commit *create_commit(struct repository *repo, struct tree *tree, struct commit *based_on, struct commit *parent, - enum replay_mode mode) + bool reverse) { struct object_id ret; struct object *obj = NULL; @@ -98,15 +93,13 @@ static struct commit *create_commit(struct repository *repo, commit_list_insert(parent, &parents); extra = read_commit_extra_headers(based_on, exclude_gpgsig); - if (mode == REPLAY_MODE_REVERT) { + if (reverse) { generate_revert_message(&msg, based_on, repo); /* For revert, use current user as author (NULL = use default) */ - } else if (mode == REPLAY_MODE_PICK) { + } else { find_commit_subject(message, &orig_message); strbuf_addstr(&msg, orig_message); author = get_author(message); - } else { - BUG("unexpected replay mode %d", mode); } reset_ident_date(); if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents, @@ -269,7 +262,7 @@ static struct commit *pick_regular_commit(struct repository *repo, struct commit *onto, struct merge_options *merge_opt, struct merge_result *result, - enum replay_mode mode, + bool reverse, enum replay_empty_commit_action empty) { struct commit *base, *replayed_base; @@ -287,7 +280,21 @@ static struct commit *pick_regular_commit(struct repository *repo, replayed_base_tree = repo_get_commit_tree(repo, replayed_base); pickme_tree = repo_get_commit_tree(repo, pickme); - if (mode == REPLAY_MODE_PICK) { + if (reverse) { + /* Revert: swap base and pickme to reverse the diff */ + const char *pickme_name = short_commit_name(repo, pickme); + merge_opt->branch1 = short_commit_name(repo, replayed_base); + merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); + merge_opt->ancestor = pickme_name; + + merge_incore_nonrecursive(merge_opt, + pickme_tree, + replayed_base_tree, + base_tree, + result); + + free((char *)merge_opt->branch2); + } else { /* Cherry-pick: normal order */ merge_opt->branch1 = short_commit_name(repo, replayed_base); merge_opt->branch2 = short_commit_name(repo, pickme); @@ -303,22 +310,6 @@ static struct commit *pick_regular_commit(struct repository *repo, result); free((char *)merge_opt->ancestor); - } else if (mode == REPLAY_MODE_REVERT) { - /* Revert: swap base and pickme to reverse the diff */ - const char *pickme_name = short_commit_name(repo, pickme); - merge_opt->branch1 = short_commit_name(repo, replayed_base); - merge_opt->branch2 = xstrfmt("parent of %s", pickme_name); - merge_opt->ancestor = pickme_name; - - merge_incore_nonrecursive(merge_opt, - pickme_tree, - replayed_base_tree, - base_tree, - result); - - free((char *)merge_opt->branch2); - } else { - BUG("unexpected replay mode %d", mode); } merge_opt->ancestor = NULL; merge_opt->branch2 = NULL; @@ -341,7 +332,7 @@ static struct commit *pick_regular_commit(struct repository *repo, } } - return create_commit(repo, result->tree, pickme, replayed_base, mode); + return create_commit(repo, result->tree, pickme, replayed_base, reverse); } void replay_result_release(struct replay_result *result) @@ -381,13 +372,13 @@ int replay_revisions(struct rev_info *revs, char *revert; const char *ref; struct object_id old_oid; - enum replay_mode mode = REPLAY_MODE_PICK; + bool reverse; int ret; advance = xstrdup_or_null(opts->advance); revert = xstrdup_or_null(opts->revert); - if (revert) - mode = REPLAY_MODE_REVERT; + reverse = !!revert; + set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &detached_head, &advance, &revert, &onto, &update_refs); @@ -430,8 +421,8 @@ int replay_revisions(struct rev_info *revs, die(_("replaying merge commits is not supported yet!")); last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, - mode == REPLAY_MODE_REVERT ? last_commit : onto, - &merge_opt, &result, mode, opts->empty); + reverse ? last_commit : onto, + &merge_opt, &result, reverse, opts->empty); if (!last_commit) break; From dbec23af84b6e2e171d0f2f55ae41882c2f4d2ce Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Wed, 10 Jun 2026 16:49:13 +0200 Subject: [PATCH 2/3] replay: add helper to put entry into mapped_commits The function replay_revisions() in replay.c is rather lengthy. Extract the logic to put a commit entry into mapped_commits into a helper function put_mapped_commit(). While at it, rename mapped_commit() to get_mapped_commit() to pair with this new function. Signed-off-by: Toon Claes Signed-off-by: Junio C Hamano --- replay.c | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/replay.c b/replay.c index 1b4453af3f..1256237bed 100644 --- a/replay.c +++ b/replay.c @@ -243,9 +243,9 @@ static void set_up_replay_mode(struct repository *repo, strset_clear(&rinfo.positive_refs); } -static struct commit *mapped_commit(kh_oid_map_t *replayed_commits, - struct commit *commit, - struct commit *fallback) +static struct commit *get_mapped_commit(kh_oid_map_t *replayed_commits, + struct commit *commit, + struct commit *fallback) { khint_t pos; if (!commit) @@ -256,6 +256,21 @@ static struct commit *mapped_commit(kh_oid_map_t *replayed_commits, return kh_value(replayed_commits, pos); } +static void put_mapped_commit(kh_oid_map_t *replayed_commits, + struct commit *commit, + struct commit *new_commit) +{ + khint_t pos; + int ret; + + pos = kh_put_oid_map(replayed_commits, commit->object.oid, &ret); + if (ret == 0) + BUG("Duplicate rewritten commit: %s\n", + oid_to_hex(&commit->object.oid)); + + kh_value(replayed_commits, pos) = new_commit; +} + static struct commit *pick_regular_commit(struct repository *repo, struct commit *pickme, kh_oid_map_t *replayed_commits, @@ -276,7 +291,7 @@ static struct commit *pick_regular_commit(struct repository *repo, base_tree = lookup_tree(repo, repo->hash_algo->empty_tree); } - replayed_base = mapped_commit(replayed_commits, base, onto); + replayed_base = get_mapped_commit(replayed_commits, base, onto); replayed_base_tree = repo_get_commit_tree(repo, replayed_base); pickme_tree = repo_get_commit_tree(repo, pickme); @@ -414,8 +429,6 @@ int replay_revisions(struct rev_info *revs, replayed_commits = kh_init_oid_map(); while ((commit = get_revision(revs))) { const struct name_decoration *decoration; - khint_t pos; - int hr; if (commit->parents && commit->parents->next) die(_("replaying merge commits is not supported yet!")); @@ -427,11 +440,7 @@ int replay_revisions(struct rev_info *revs, break; /* Record commit -> last_commit mapping */ - pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr); - if (hr == 0) - BUG("Duplicate rewritten commit: %s\n", - oid_to_hex(&commit->object.oid)); - kh_value(replayed_commits, pos) = last_commit; + put_mapped_commit(replayed_commits, commit, last_commit); /* Update any necessary branches */ if (ref) From ae260e765ed356a57eb35f7c013a4addaa962c93 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 10 Jun 2026 16:49:14 +0200 Subject: [PATCH 3/3] replay: offer an option to linearize the commit topology One of the stated goals of git-replay(1) is to allow implementing the git-rebase(1) functionality on the server side. The default mode of git-rebase(1) is to act as if `--no-rebase-merges` was given. This mode drops merge commits instead of replaying them, and linearizes the commit history into a sequence of the regular (single-parent) commits. Add option `--linearize` to git-replay(1) to do the same. Co-authored-by: Toon Claes Signed-off-by: Johannes Schindelin Signed-off-by: Toon Claes Signed-off-by: Junio C Hamano --- Documentation/git-replay.adoc | 5 +++++ builtin/replay.c | 4 ++++ replay.c | 30 +++++++++++++++++++++++------- replay.h | 5 +++++ t/t3650-replay-basics.sh | 26 ++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index a32f72aead..41c96c7061 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -88,6 +88,11 @@ incompatible with `--contained` (which is a modifier for `--onto` only). + The default mode can be configured via the `replay.refAction` configuration variable. +--linearize:: + In this mode, `git replay` imitates `git rebase --no-rebase-merges`, + i.e. it cherry-picks only non-merge commits, each one on top of the + previous one. + :: Range of commits to replay; see "Specifying Ranges" in linkgit:git-rev-parse[1]. In `--advance=` or diff --git a/builtin/replay.c b/builtin/replay.c index 39e3a86f6c..fedfe46dc6 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -111,6 +111,8 @@ int cmd_replay(int argc, N_("mode"), N_("control ref update behavior (update|print)"), PARSE_OPT_NONEG), + OPT_BOOL(0, "linearize", &opts.linearize, + N_("ignore merge commits instead of replaying them")), OPT_END() }; @@ -132,6 +134,8 @@ int cmd_replay(int argc, opts.contained, "--contained"); die_for_incompatible_opt2(!!opts.ref, "--ref", !!opts.contained, "--contained"); + die_for_incompatible_opt2(!!opts.revert, "--revert", + opts.linearize, "--linearize"); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); diff --git a/replay.c b/replay.c index 1256237bed..0a10846cb4 100644 --- a/replay.c +++ b/replay.c @@ -277,12 +277,16 @@ static struct commit *pick_regular_commit(struct repository *repo, struct commit *onto, struct merge_options *merge_opt, struct merge_result *result, + struct commit *replayed_base, bool reverse, enum replay_empty_commit_action empty) { - struct commit *base, *replayed_base; + struct commit *base; struct tree *pickme_tree, *base_tree, *replayed_base_tree; + if (replayed_base && reverse) + BUG("Linearizing commits is not supported when replaying in reverse"); + if (pickme->parents) { base = pickme->parents->item; base_tree = repo_get_commit_tree(repo, base); @@ -291,7 +295,8 @@ static struct commit *pick_regular_commit(struct repository *repo, base_tree = lookup_tree(repo, repo->hash_algo->empty_tree); } - replayed_base = get_mapped_commit(replayed_commits, base, onto); + if (!replayed_base) + replayed_base = get_mapped_commit(replayed_commits, base, onto); replayed_base_tree = repo_get_commit_tree(repo, replayed_base); pickme_tree = repo_get_commit_tree(repo, pickme); @@ -430,12 +435,23 @@ int replay_revisions(struct rev_info *revs, while ((commit = get_revision(revs))) { const struct name_decoration *decoration; - if (commit->parents && commit->parents->next) - die(_("replaying merge commits is not supported yet!")); + if (commit->parents && commit->parents->next) { + if (!opts->linearize) + die(_("replaying merge commits is not supported yet!")); + /* + * When linearizing, a merge commit itself is not picked, + * but refs that point to it might need updating. + */ + } else { + struct commit *to_pick = reverse ? last_commit : onto; + last_commit = + pick_regular_commit(revs->repo, commit, + replayed_commits, to_pick, + &merge_opt, &result, + opts->linearize ? last_commit : NULL, + reverse, opts->empty); + } - last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, - reverse ? last_commit : onto, - &merge_opt, &result, reverse, opts->empty); if (!last_commit) break; diff --git a/replay.h b/replay.h index 1851a07705..07e6fdcca3 100644 --- a/replay.h +++ b/replay.h @@ -62,6 +62,11 @@ struct replay_revisions_options { * 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()`. */ diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index 3353bc4a4d..64e0731188 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -565,4 +565,30 @@ test_expect_success '--onto with --ref rejects multiple revision ranges' ' test_grep "cannot be used with multiple revision ranges" err ' +test_expect_success 'replay merge commit fails' ' + echo "fatal: replaying merge commits is not supported yet!" >expect && + test_must_fail git replay --ref-action=print --onto main I..P 2>actual && + test_cmp expect actual +' + +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 root commit' ' + git replay --ref-action=print --linearize --onto main A..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 M L B A >expect && + test_cmp expect actual +' + test_done