* [PATCH 0/3] Teach git-replay(1) to linearize merge commits
@ 2026-06-08 18:37 Toon Claes
2026-06-08 18:37 ` [PATCH 1/3] replay: refactor enum replay_mode into a bool Toon Claes
` (3 more replies)
0 siblings, 4 replies; 11+ messages in thread
From: Toon Claes @ 2026-06-08 18:37 UTC (permalink / raw)
To: git; +Cc: Toon Claes, Johannes Schindelin
As an alternative to dscho's patch series to replay merges[1], add
option to git-replay(1) to linearize merges. This mimics wath
git-rebase(1) does too with --no-rebase-merges (the default).
The first two patches do some refactoring. The third patch implements
the actual change. I was kindly helped by dscho to implement this
change.
The --linearize option is only added to git-replay(1) and not to
git-history(1) because in my opinion doesn't make much sense to do so,
but I'm happy to hear if anyone disagrees.
This series might conflict with Kristoffer's series to make
documentation changes[2], but should be trivial to resolve. And I don't
think there's a conflict with Patrick's series on adding "drop" to
git-history(1)[3].
dscho's series to replay merges[1] need a bit of rework to fit on top of
this, but I'm happy to help figuring that out.
[1]: <pull.2106.git.1778107405.gitgitgadget@gmail.com>
[2]: <V2_CV_doc_replay_config.767@msgid.xyz>
[3]: <20260603-b4-pks-history-drop-v2-0-742cb5b5176d@pks.im>
Signed-off-by: Toon Claes <toon@iotcl.com>
---
Johannes Schindelin (1):
replay: offer an option to linearize the commit topology
Toon Claes (2):
replay: refactor enum replay_mode into a bool
replay: add helper to put entry into mapped_commits
Documentation/git-replay.adoc | 5 ++
builtin/replay.c | 4 ++
replay.c | 109 +++++++++++++++++++++++-------------------
replay.h | 5 ++
t/t3650-replay-basics.sh | 22 +++++++++
5 files changed, 97 insertions(+), 48 deletions(-)
---
base-commit: 9ac3f193c05c2237e2b14ebaa1149e9fc8a1abe0
change-id: 20260604-toon-git-replay-drop-merges-807fa008d395
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH 1/3] replay: refactor enum replay_mode into a bool
2026-06-08 18:37 [PATCH 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
@ 2026-06-08 18:37 ` Toon Claes
2026-06-08 18:37 ` [PATCH 2/3] replay: add helper to put entry into mapped_commits Toon Claes
` (2 subsequent siblings)
3 siblings, 0 replies; 11+ messages in thread
From: Toon Claes @ 2026-06-08 18:37 UTC (permalink / raw)
To: git; +Cc: Toon Claes
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
possible 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 pocessed 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 avoid adding code paths that
check for invalid vaues of the enum, and shortens code where the value
is checked with a ternary operator.
Signed-off-by: Toon Claes <toon@iotcl.com>
---
replay.c | 59 +++++++++++++++++++++++++----------------------------------
1 file changed, 25 insertions(+), 34 deletions(-)
diff --git a/replay.c b/replay.c
index 4ef8abb607..1f8e5b083b 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;
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH 2/3] replay: add helper to put entry into mapped_commits
2026-06-08 18:37 [PATCH 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-08 18:37 ` [PATCH 1/3] replay: refactor enum replay_mode into a bool Toon Claes
@ 2026-06-08 18:37 ` Toon Claes
2026-06-08 18:37 ` [PATCH 3/3] replay: offer an option to linearize the commit topology Toon Claes
2026-06-10 14:49 ` [PATCH v2 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
3 siblings, 0 replies; 11+ messages in thread
From: Toon Claes @ 2026-06-08 18:37 UTC (permalink / raw)
To: git; +Cc: Toon Claes
The function replay_revisions() in replay.c is rather lengthy. Extract
the logic to put commit entry into mapped_commits into a helper
function.
Signed-off-by: Toon Claes <toon@iotcl.com>
---
replay.c | 31 ++++++++++++++++++++-----------
1 file changed, 20 insertions(+), 11 deletions(-)
diff --git a/replay.c b/replay.c
index 1f8e5b083b..7921d7dba3 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)
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH 3/3] replay: offer an option to linearize the commit topology
2026-06-08 18:37 [PATCH 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-08 18:37 ` [PATCH 1/3] replay: refactor enum replay_mode into a bool Toon Claes
2026-06-08 18:37 ` [PATCH 2/3] replay: add helper to put entry into mapped_commits Toon Claes
@ 2026-06-08 18:37 ` Toon Claes
2026-06-08 19:29 ` Junio C Hamano
2026-06-10 14:49 ` [PATCH v2 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
3 siblings, 1 reply; 11+ messages in thread
From: Toon Claes @ 2026-06-08 18:37 UTC (permalink / raw)
To: git; +Cc: Toon Claes, Johannes Schindelin
From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
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
linearized the commit history into a sequence of the
regular (single-parent) commits.
Add option `--linearize` to git-replay(1) do the same.
Co-authored-by: Toon Claes <toon@iotcl.com>
---
Documentation/git-replay.adoc | 5 +++++
builtin/replay.c | 4 ++++
replay.c | 25 +++++++++++++++++++------
replay.h | 5 +++++
t/t3650-replay-basics.sh | 22 ++++++++++++++++++++++
5 files changed, 55 insertions(+), 6 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.
+
<revision-range>::
Range of commits to replay; see "Specifying Ranges" in
linkgit:git-rev-parse[1]. In `--advance=<branch>` 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 7921d7dba3..3e36908131 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,20 @@ int replay_revisions(struct rev_info *revs,
while ((commit = get_revision(revs))) {
const struct name_decoration *decoration;
- if (commit->parents && commit->parents->next)
+ if (opts->linearize && (!commit->parents || commit->parents->next))
+ ; /* map current commit to the same as the previous commit */
+ else if (commit->parents && commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
+ 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..c781a3bb1b 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -565,4 +565,26 @@ test_expect_success '--onto with --ref rejects multiple revision ranges' '
test_grep "cannot be used with multiple revision ranges" err
'
+test_expect_success 'linearize the commit topology' '
+ test_tick &&
+ N=$(git commit-tree -m N -p L -p I L:) &&
+ N=$(git commit-tree -m N-child -p $N L:) &&
+ git update-ref refs/heads/N $N &&
+
+ git replay --ref-action=print --linearize \
+ --onto A B..refs/heads/N >out &&
+
+ test_line_count = 1 out &&
+ read N1 N2 N3 N4 <out &&
+
+ cat >expect <<-EOF &&
+ * N-child
+ * I
+ * L
+ o A
+ EOF
+ git log --format=%s --graph --boundary A...$N3 >actual &&
+ test_cmp expect actual
+'
+
test_done
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH 3/3] replay: offer an option to linearize the commit topology
2026-06-08 18:37 ` [PATCH 3/3] replay: offer an option to linearize the commit topology Toon Claes
@ 2026-06-08 19:29 ` Junio C Hamano
2026-06-10 14:26 ` Toon Claes
0 siblings, 1 reply; 11+ messages in thread
From: Junio C Hamano @ 2026-06-08 19:29 UTC (permalink / raw)
To: Toon Claes; +Cc: git, Johannes Schindelin
Toon Claes <toon@iotcl.com> writes:
> From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
>
> 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
> linearized the commit history into a sequence of the
> regular (single-parent) commits.
"linearized" -> "linearizes"?
>
> Add option `--linearize` to git-replay(1) do the same.
"do the same" -> "to do the same"?
> Co-authored-by: Toon Claes <toon@iotcl.com>
There is no sign-off by any of the authors?
> @@ -430,12 +435,20 @@ int replay_revisions(struct rev_info *revs,
> while ((commit = get_revision(revs))) {
> const struct name_decoration *decoration;
>
> - if (commit->parents && commit->parents->next)
> + if (opts->linearize && (!commit->parents || commit->parents->next))
> + ; /* map current commit to the same as the previous commit */
This uses the same treatment on either root commits or merge
commits? If this were a mistake and this wants to handle merges but
not roots, shouldn't it be more like
if (opts->linearize && (commit->parents && commit->parents->next))
; /* map the merge to the previous */
> + else if (commit->parents && commit->parents->next)
> die(_("replaying merge commits is not supported yet!"));
And because the next one is also about merges, perhaps the early
part of this if/else if cascade can be written
if (commit->parents && commit->parents->next) {
/* We have a merge */
if (!opts->linearize)
die(_("can't replay a merge (yet)"));
; /* map current to the previous */
} else {
...
wouldn't it?
If the "map current to prev" is applicable to root, any root are
mapped to the last_commit in the above, and if we saw a root as the
first thing in the loop, last_commit is NULL, we do not do anything
here, and after the if/else if/else cascade, we see last_commit is
NULL and break out of the loop.
> + 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..c781a3bb1b 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -565,4 +565,26 @@ test_expect_success '--onto with --ref rejects multiple revision ranges' '
> test_grep "cannot be used with multiple revision ranges" err
> '
>
> +test_expect_success 'linearize the commit topology' '
> + test_tick &&
> + N=$(git commit-tree -m N -p L -p I L:) &&
> + N=$(git commit-tree -m N-child -p $N L:) &&
> + git update-ref refs/heads/N $N &&
> +
> + git replay --ref-action=print --linearize \
> + --onto A B..refs/heads/N >out &&
> +
> + test_line_count = 1 out &&
> + read N1 N2 N3 N4 <out &&
> +
> + cat >expect <<-EOF &&
> + * N-child
> + * I
> + * L
> + o A
> + EOF
> + git log --format=%s --graph --boundary A...$N3 >actual &&
> + test_cmp expect actual
> +'
Perhaps we would want to have a test that replays all the way down
to the root commit?
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH 3/3] replay: offer an option to linearize the commit topology
2026-06-08 19:29 ` Junio C Hamano
@ 2026-06-10 14:26 ` Toon Claes
0 siblings, 0 replies; 11+ messages in thread
From: Toon Claes @ 2026-06-10 14:26 UTC (permalink / raw)
To: Junio C Hamano, Johannes Schindelin; +Cc: git
Junio C Hamano <gitster@pobox.com> writes:
> Toon Claes <toon@iotcl.com> writes:
>
>> From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
>>
>> 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
>> linearized the commit history into a sequence of the
>> regular (single-parent) commits.
>
> "linearized" -> "linearizes"?
Thanks.
>>
>> Add option `--linearize` to git-replay(1) do the same.
>
> "do the same" -> "to do the same"?
Ack.
>> Co-authored-by: Toon Claes <toon@iotcl.com>
>
> There is no sign-off by any of the authors?
My bad. I'll add mine.
@Johannes, can I re-add yours? I've removed it because I've made some
changes on top of the patch you wrote, but if you agree, I'll add your
Sign-off back.
>> @@ -430,12 +435,20 @@ int replay_revisions(struct rev_info *revs,
>> while ((commit = get_revision(revs))) {
>> const struct name_decoration *decoration;
>>
>> - if (commit->parents && commit->parents->next)
>> + if (opts->linearize && (!commit->parents || commit->parents->next))
>> + ; /* map current commit to the same as the previous commit */
>
> This uses the same treatment on either root commits or merge
> commits? If this were a mistake and this wants to handle merges but
> not roots, shouldn't it be more like
>
> if (opts->linearize && (commit->parents && commit->parents->next))
> ; /* map the merge to the previous */
>
>> + else if (commit->parents && commit->parents->next)
>> die(_("replaying merge commits is not supported yet!"));
>
> And because the next one is also about merges, perhaps the early
> part of this if/else if cascade can be written
>
> if (commit->parents && commit->parents->next) {
> /* We have a merge */
> if (!opts->linearize)
> die(_("can't replay a merge (yet)"));
> ; /* map current to the previous */
> } else {
> ...
>
> wouldn't it?
The way it was written in v1 was maybe a bit too smart and hard to
follow. I agree with your suggestion and will adopt this (with some
tweaks) in the next version.
> If the "map current to prev" is applicable to root, any root are
> mapped to the last_commit in the above, and if we saw a root as the
> first thing in the loop, last_commit is NULL, we do not do anything
> here, and after the if/else if/else cascade, we see last_commit is
> NULL and break out of the loop.
Yes, good observation. I did not test this.
> Perhaps we would want to have a test that replays all the way down
> to the root commit?
I'll add it.
--
Cheers,
Toon
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v2 0/3] Teach git-replay(1) to linearize merge commits
2026-06-08 18:37 [PATCH 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
` (2 preceding siblings ...)
2026-06-08 18:37 ` [PATCH 3/3] replay: offer an option to linearize the commit topology Toon Claes
@ 2026-06-10 14:49 ` Toon Claes
2026-06-10 14:49 ` [PATCH v2 1/3] replay: refactor enum replay_mode into a bool Toon Claes
` (2 more replies)
3 siblings, 3 replies; 11+ messages in thread
From: Toon Claes @ 2026-06-10 14:49 UTC (permalink / raw)
To: git; +Cc: Toon Claes, Johannes Schindelin, Johannes Schindelin
As an alternative to dscho's patch series to replay merges[1], add
option to git-replay(1) to linearize merges. This mimics wath
git-rebase(1) does too with --no-rebase-merges (the default).
The first two patches do some refactoring. The third patch implements
the actual change. I was kindly helped by dscho to implement this
change.
The --linearize option is only added to git-replay(1) and not to
git-history(1) because in my opinion doesn't make much sense to do so,
but I'm happy to hear if anyone disagrees.
This series might conflict with Kristoffer's series to make
documentation changes[2], but should be trivial to resolve. And I don't
think there's a conflict with Patrick's series on adding "drop" to
git-history(1)[3].
dscho's series to replay merges[1] need a bit of rework to fit on top of
this, but I'm happy to help figuring that out. We've been discussing to
either name the option --flatten or --linearize, but I've decided on
"linearize" because the documentation of git-rebase(1) also mentions
"linearize".
[1]: <pull.2106.git.1778107405.gitgitgadget@gmail.com>
[2]: <V2_CV_doc_replay_config.767@msgid.xyz>
[3]: <20260603-b4-pks-history-drop-v2-0-742cb5b5176d@pks.im>
Signed-off-by: Toon Claes <toon@iotcl.com>
---
Changes in v2:
- Restructured the conditions to detect merge commits and added a line
of comment why the loop continues.
- Rewrote tests to use the history from the setup step and added a few
test cases.
- Re-added Johannes's Signed-off-by trailer. Johannes gave me the
patches with this trailer, and if I understand correctly, I can keep
it. Please let me know if that wrong.
- Link to v1: https://patch.msgid.link/20260608-toon-git-replay-drop-merges-v1-0-e3ee71fce7b4@iotcl.com
---
Johannes Schindelin (1):
replay: offer an option to linearize the commit topology
Toon Claes (2):
replay: refactor enum replay_mode into a bool
replay: add helper to put entry into mapped_commits
Documentation/git-replay.adoc | 5 ++
builtin/replay.c | 4 ++
replay.c | 114 ++++++++++++++++++++++++------------------
replay.h | 5 ++
t/t3650-replay-basics.sh | 26 ++++++++++
5 files changed, 105 insertions(+), 49 deletions(-)
Range-diff versus v1:
1: 7f3bc6f425 ! 1: 0975b142e3 replay: refactor enum replay_mode into a bool
@@ Commit message
- The value `REPLAY_MODE_REVERT` is used when option `--revert` is
passed to git-replay(1). When using this value the commits are
- possible in reverse order and the inverse of the changes are applied.
+ 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 pocessed in normal
- order, and the changes are applied as-is.
+ `--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 avoid adding code paths that
- check for invalid vaues of the enum, and shortens code where the value
+ 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 <toon@iotcl.com>
2: 0868871c78 ! 2: db88193624 replay: add helper to put entry into mapped_commits
@@ Commit message
replay: add helper to put entry into mapped_commits
The function replay_revisions() in replay.c is rather lengthy. Extract
- the logic to put commit entry into mapped_commits into a helper
- function.
+ 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 <toon@iotcl.com>
3: a432ae753b ! 3: d0c220ec8e replay: offer an option to linearize the commit topology
@@ Commit message
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
- linearized the commit history into a sequence of the
+ linearizes the commit history into a sequence of the
regular (single-parent) commits.
- Add option `--linearize` to git-replay(1) do the same.
+ Add option `--linearize` to git-replay(1) to do the same.
Co-authored-by: Toon Claes <toon@iotcl.com>
+ Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
+ Signed-off-by: Toon Claes <toon@iotcl.com>
## Documentation/git-replay.adoc ##
@@ Documentation/git-replay.adoc: incompatible with `--contained` (which is a modifier for `--onto` only).
@@ replay.c: int replay_revisions(struct rev_info *revs,
const struct name_decoration *decoration;
- if (commit->parents && commit->parents->next)
-+ if (opts->linearize && (!commit->parents || commit->parents->next))
-+ ; /* map current commit to the same as the previous commit */
-+ else if (commit->parents && commit->parents->next)
- die(_("replaying merge commits is not supported yet!"));
-+ else {
+- 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,
@@ t/t3650-replay-basics.sh: test_expect_success '--onto with --ref rejects multipl
test_grep "cannot be used with multiple revision ranges" err
'
-+test_expect_success 'linearize the commit topology' '
-+ test_tick &&
-+ N=$(git commit-tree -m N -p L -p I L:) &&
-+ N=$(git commit-tree -m N-child -p $N L:) &&
-+ git update-ref refs/heads/N $N &&
++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
++'
+
-+ git replay --ref-action=print --linearize \
-+ --onto A B..refs/heads/N >out &&
++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 out &&
-+ read N1 N2 N3 N4 <out &&
++ test_line_count = 1 result &&
+
-+ cat >expect <<-EOF &&
-+ * N-child
-+ * I
-+ * L
-+ o A
-+ EOF
-+ git log --format=%s --graph --boundary A...$N3 >actual &&
++ 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
+'
+
---
base-commit: 9ac3f193c05c2237e2b14ebaa1149e9fc8a1abe0
change-id: 20260604-toon-git-replay-drop-merges-807fa008d395
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v2 1/3] replay: refactor enum replay_mode into a bool
2026-06-10 14:49 ` [PATCH v2 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
@ 2026-06-10 14:49 ` Toon Claes
2026-06-10 14:49 ` [PATCH v2 2/3] replay: add helper to put entry into mapped_commits Toon Claes
2026-06-10 14:49 ` [PATCH v2 3/3] replay: offer an option to linearize the commit topology Toon Claes
2 siblings, 0 replies; 11+ messages in thread
From: Toon Claes @ 2026-06-10 14:49 UTC (permalink / raw)
To: git; +Cc: Toon Claes
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 <toon@iotcl.com>
---
replay.c | 59 +++++++++++++++++++++++++----------------------------------
1 file changed, 25 insertions(+), 34 deletions(-)
diff --git a/replay.c b/replay.c
index 4ef8abb607..1f8e5b083b 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;
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 2/3] replay: add helper to put entry into mapped_commits
2026-06-10 14:49 ` [PATCH v2 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-10 14:49 ` [PATCH v2 1/3] replay: refactor enum replay_mode into a bool Toon Claes
@ 2026-06-10 14:49 ` Toon Claes
2026-06-10 14:49 ` [PATCH v2 3/3] replay: offer an option to linearize the commit topology Toon Claes
2 siblings, 0 replies; 11+ messages in thread
From: Toon Claes @ 2026-06-10 14:49 UTC (permalink / raw)
To: git; +Cc: Toon Claes
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 <toon@iotcl.com>
---
replay.c | 31 ++++++++++++++++++++-----------
1 file changed, 20 insertions(+), 11 deletions(-)
diff --git a/replay.c b/replay.c
index 1f8e5b083b..7921d7dba3 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)
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 3/3] replay: offer an option to linearize the commit topology
2026-06-10 14:49 ` [PATCH v2 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-10 14:49 ` [PATCH v2 1/3] replay: refactor enum replay_mode into a bool Toon Claes
2026-06-10 14:49 ` [PATCH v2 2/3] replay: add helper to put entry into mapped_commits Toon Claes
@ 2026-06-10 14:49 ` Toon Claes
2026-06-10 17:02 ` Junio C Hamano
2 siblings, 1 reply; 11+ messages in thread
From: Toon Claes @ 2026-06-10 14:49 UTC (permalink / raw)
To: git; +Cc: Toon Claes, Johannes Schindelin, Johannes Schindelin
From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
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 <toon@iotcl.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Toon Claes <toon@iotcl.com>
---
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.
+
<revision-range>::
Range of commits to replay; see "Specifying Ranges" in
linkgit:git-rev-parse[1]. In `--advance=<branch>` 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 7921d7dba3..81033fb889 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
--
2.53.0.1323.g189a785ab5
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v2 3/3] replay: offer an option to linearize the commit topology
2026-06-10 14:49 ` [PATCH v2 3/3] replay: offer an option to linearize the commit topology Toon Claes
@ 2026-06-10 17:02 ` Junio C Hamano
0 siblings, 0 replies; 11+ messages in thread
From: Junio C Hamano @ 2026-06-10 17:02 UTC (permalink / raw)
To: Toon Claes; +Cc: git, Johannes Schindelin
Toon Claes <toon@iotcl.com> writes:
> From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
>
> 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 <toon@iotcl.com>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> Signed-off-by: Toon Claes <toon@iotcl.com>
> ---
> 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(-)
>
> @@ -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.
> + */
In the review response during the previous iteration, I commented
that (1) the original excluded only merges, but (2) your version
excluded both merges and the root commits the same way. Your
response was:
The way it was written in v1 was maybe a bit too smart and hard to
follow. I agree with your suggestion and will adopt this (with some
tweaks) in the next version.
which I took as saying "it may be confusing, but it correctly
expresses what we want to do", meaning "yes, roots and merges should
be handled the same way". But the above no longer treats roots the
same way as merges. I think that is intended, but just wanted to
double check.
> 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;
> };
OK.
> 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 &&
As with other test pieces, this "git replay" command line is overly
long and hides the important bit which is that the range being
replayed is *not* actually down to the root, which is A (it excludes
A). Intended?
> +
> + 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
Thanks.
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-06-10 17:02 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-08 18:37 [PATCH 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-08 18:37 ` [PATCH 1/3] replay: refactor enum replay_mode into a bool Toon Claes
2026-06-08 18:37 ` [PATCH 2/3] replay: add helper to put entry into mapped_commits Toon Claes
2026-06-08 18:37 ` [PATCH 3/3] replay: offer an option to linearize the commit topology Toon Claes
2026-06-08 19:29 ` Junio C Hamano
2026-06-10 14:26 ` Toon Claes
2026-06-10 14:49 ` [PATCH v2 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-10 14:49 ` [PATCH v2 1/3] replay: refactor enum replay_mode into a bool Toon Claes
2026-06-10 14:49 ` [PATCH v2 2/3] replay: add helper to put entry into mapped_commits Toon Claes
2026-06-10 14:49 ` [PATCH v2 3/3] replay: offer an option to linearize the commit topology Toon Claes
2026-06-10 17:02 ` Junio C Hamano
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox