Skip to content

Commit d187425

Browse files
dschoTo1ne
authored andcommitted
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>
1 parent d2f8bf6 commit d187425

5 files changed

Lines changed: 109 additions & 10 deletions

File tree

Documentation/git-replay.adoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ SYNOPSIS
1010
--------
1111
[verse]
1212
(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
13-
[--ref=<ref>] [--ref-action=<mode>] <revision-range>
13+
[--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>
1414

1515
DESCRIPTION
1616
-----------
@@ -88,6 +88,12 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
8888
+
8989
The default mode can be configured via the `replay.refAction` configuration variable.
9090

91+
--linearize::
92+
In this mode, `git replay` imitates `git rebase --no-rebase-merges`,
93+
i.e. it cherry-picks only non-merge commits, each one on top of the
94+
previous one.
95+
This option is incompatible with `--revert`.
96+
9197
<revision-range>::
9298
Range of commits to replay; see "Specifying Ranges" in
9399
linkgit:git-rev-parse[1]. In `--advance=<branch>` or

builtin/replay.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ int cmd_replay(int argc,
8585
const char *const replay_usage[] = {
8686
N_("(EXPERIMENTAL!) git replay "
8787
"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
88-
"[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
88+
"[--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>"),
8989
NULL
9090
};
9191
struct option replay_options[] = {
@@ -111,6 +111,8 @@ int cmd_replay(int argc,
111111
N_("mode"),
112112
N_("control ref update behavior (update|print)"),
113113
PARSE_OPT_NONEG),
114+
OPT_BOOL(0, "linearize", &opts.linearize,
115+
N_("drop merge commits, replaying only non-merge commits")),
114116
OPT_END()
115117
};
116118

@@ -132,6 +134,8 @@ int cmd_replay(int argc,
132134
opts.contained, "--contained");
133135
die_for_incompatible_opt2(!!opts.ref, "--ref",
134136
!!opts.contained, "--contained");
137+
die_for_incompatible_opt2(!!opts.revert, "--revert",
138+
opts.linearize, "--linearize");
135139

136140
/* Parse ref action mode from command line or config */
137141
ref_mode = get_ref_action_mode(repo, ref_action);

replay.c

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,16 @@ static struct commit *pick_regular_commit(struct repository *repo,
277277
struct commit *onto,
278278
struct merge_options *merge_opt,
279279
struct merge_result *result,
280+
struct commit *replayed_base,
280281
bool reverse,
281282
enum replay_empty_commit_action empty)
282283
{
283-
struct commit *base, *replayed_base;
284+
struct commit *base;
284285
struct tree *pickme_tree, *base_tree, *replayed_base_tree;
285286

287+
if (replayed_base && reverse)
288+
BUG("Linearizing commits is not supported when replaying in reverse");
289+
286290
if (pickme->parents) {
287291
base = pickme->parents->item;
288292
base_tree = repo_get_commit_tree(repo, base);
@@ -291,7 +295,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
291295
base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
292296
}
293297

294-
replayed_base = get_mapped_commit(replayed_commits, base, onto);
298+
if (!replayed_base)
299+
replayed_base = get_mapped_commit(replayed_commits, base, onto);
295300
replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
296301
pickme_tree = repo_get_commit_tree(repo, pickme);
297302

@@ -430,12 +435,25 @@ int replay_revisions(struct rev_info *revs,
430435
while ((commit = get_revision(revs))) {
431436
const struct name_decoration *decoration;
432437

433-
if (commit->parents && commit->parents->next)
434-
die(_("replaying merge commits is not supported yet!"));
438+
if (commit->parents && commit->parents->next) {
439+
if (!opts->linearize)
440+
die(_("replaying merge commits is not supported yet!"));
441+
/*
442+
* Drop the merge commit: do not pick it and leave
443+
* last_commit unchanged, so its children (and any ref
444+
* pointing at it) are reparented onto the previous
445+
* non-merge commit, which the ref-update loop below uses.
446+
*/
447+
} else {
448+
struct commit *to_pick = reverse ? last_commit : onto;
449+
last_commit =
450+
pick_regular_commit(revs->repo, commit,
451+
replayed_commits, to_pick,
452+
&merge_opt, &result,
453+
opts->linearize ? last_commit : NULL,
454+
reverse, opts->empty);
455+
}
435456

436-
last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
437-
reverse ? last_commit : onto,
438-
&merge_opt, &result, reverse, opts->empty);
439457
if (!last_commit)
440458
break;
441459

replay.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ struct replay_revisions_options {
6262
* Defaults to REPLAY_EMPTY_COMMIT_DROP.
6363
*/
6464
enum replay_empty_commit_action empty;
65+
66+
/*
67+
* Whether to linearize the commits (i.e. drop merge commits).
68+
*/
69+
int linearize;
6570
};
6671

6772
/* This struct is used as an out-parameter by `replay_revisions()`. */

t/t3650-replay-basics.sh

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,12 @@ test_expect_success 'setup' '
5252
test_merge P O --no-ff &&
5353
git switch main &&
5454
55+
git switch --orphan unrelated &&
56+
test_commit unrelated-root &&
57+
5558
git switch -c conflict B &&
56-
test_commit C.conflict C.t conflict
59+
test_commit C.conflict C.t conflict &&
60+
git branch -D unrelated
5761
'
5862

5963
test_expect_success 'setup bare' '
@@ -97,6 +101,12 @@ test_expect_success '--advance and --contained cannot be used together' '
97101
test_grep "cannot be used together" actual
98102
'
99103

104+
test_expect_success '--revert and --linearize cannot be used together' '
105+
test_must_fail git replay --revert=main --linearize \
106+
topic1..topic2 2>actual &&
107+
test_grep "cannot be used together" actual
108+
'
109+
100110
test_expect_success 'cannot advance target ... ordering would be ill-defined' '
101111
echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
102112
test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
@@ -565,4 +575,60 @@ test_expect_success '--onto with --ref rejects multiple revision ranges' '
565575
test_grep "cannot be used with multiple revision ranges" err
566576
'
567577

578+
test_expect_success 'replay to rebase merge commit with --linearize' '
579+
git replay --ref-action=print --linearize \
580+
--onto main I..topic-with-merge >result &&
581+
582+
test_line_count = 1 result &&
583+
584+
git log --format=%s $(cut -f 3 -d " " result) >actual &&
585+
test_write_lines O N J M L B A >expect &&
586+
test_cmp expect actual
587+
'
588+
589+
test_expect_success 'replay to rebase merge commit with --linearize down to the root commit' '
590+
git replay --ref-action=print --linearize \
591+
--onto unrelated-root topic-with-merge >result &&
592+
593+
test_line_count = 1 result &&
594+
595+
git log --format=%s $(cut -f 3 -d " " result) >actual &&
596+
test_write_lines O N J I B A unrelated-root >expect &&
597+
test_cmp expect actual
598+
'
599+
600+
test_expect_success 'replay to cherry-pick merge commit with --linearize' '
601+
git replay --ref-action=print --linearize \
602+
--advance main I..topic-with-merge >result &&
603+
604+
test_line_count = 1 result &&
605+
606+
git log --format=%s $(cut -f 3 -d " " result) >actual &&
607+
test_write_lines O N J M L B A >expect &&
608+
test_cmp expect actual &&
609+
610+
printf "update refs/heads/main " >expect &&
611+
printf "%s " $(cut -f 3 -d " " result) >>expect &&
612+
git rev-parse main >>expect &&
613+
test_cmp expect result
614+
'
615+
616+
test_expect_success 'replay --linearize produces the same patches' '
617+
git replay --ref-action=print --linearize \
618+
--onto main I..topic-with-merge >result &&
619+
620+
test_line_count = 1 result &&
621+
tip=$(cut -f 3 -d " " result) &&
622+
623+
# range-diff does not care about the dropped merge,
624+
# so the original commits (I..topic-with-merge)
625+
# and the replayed chain (main..tip) must produce identical patches.
626+
git range-diff I..topic-with-merge main..$tip >out &&
627+
test_file_not_empty out &&
628+
! grep -v "=" out &&
629+
630+
git log --oneline main..$tip >out &&
631+
test_line_count = 3 out
632+
'
633+
568634
test_done

0 commit comments

Comments
 (0)