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 <toon@iotcl.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Toon Claes <toon@iotcl.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Johannes Schindelin
2026-06-10 16:49:14 +02:00
committed by Junio C Hamano
parent dbec23af84
commit ae260e765e
5 changed files with 63 additions and 7 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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()`. */

View File

@@ -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