Skip to content

Commit 8210d39

Browse files
committed
rebase --rebase-merges: add support for octopus merges
Previously, we introduced the `merge` command for use in todo lists, to allow to recreate and modify branch topology. For ease of implementation, and to make review easier, the initial implementation only supported merge commits with exactly two parents. This patch adds support for octopus merges, making use of the just-introduced `-F <file>` option for the `git merge` command: to keep things simple, we spawn a new Git command instead of trying to call a library function, also opening an easier door to enhance `rebase --rebase-merges` to optionally use a merge strategy different from `recursive` for regular merges: this feature would use the same code path as octopus merges and simply spawn a `git merge`. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent 59eb604 commit 8210d39

File tree

2 files changed

+159
-43
lines changed

2 files changed

+159
-43
lines changed

sequencer.c

Lines changed: 125 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2841,6 +2841,26 @@ static int do_reset(const char *name, int len, struct replay_opts *opts)
28412841
return ret;
28422842
}
28432843

2844+
static struct commit *lookup_label(const char *label, int len,
2845+
struct strbuf *buf)
2846+
{
2847+
struct commit *commit;
2848+
2849+
strbuf_reset(buf);
2850+
strbuf_addf(buf, "refs/rewritten/%.*s", len, label);
2851+
commit = lookup_commit_reference_by_name(buf->buf);
2852+
if (!commit) {
2853+
/* fall back to non-rewritten ref or commit */
2854+
strbuf_splice(buf, 0, strlen("refs/rewritten/"), "", 0);
2855+
commit = lookup_commit_reference_by_name(buf->buf);
2856+
}
2857+
2858+
if (!commit)
2859+
error(_("could not resolve '%s'"), buf->buf);
2860+
2861+
return commit;
2862+
}
2863+
28442864
static int do_merge(struct commit *commit, const char *arg, int arg_len,
28452865
int flags, struct replay_opts *opts)
28462866
{
@@ -2849,8 +2869,9 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
28492869
struct strbuf ref_name = STRBUF_INIT;
28502870
struct commit *head_commit, *merge_commit, *i;
28512871
struct commit_list *bases, *j, *reversed = NULL;
2872+
struct commit_list *to_merge = NULL, **tail = &to_merge;
28522873
struct merge_options o;
2853-
int merge_arg_len, oneline_offset, can_fast_forward, ret;
2874+
int merge_arg_len, oneline_offset, can_fast_forward, ret, k;
28542875
static struct lock_file lock;
28552876
const char *p;
28562877

@@ -2865,26 +2886,34 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
28652886
goto leave_merge;
28662887
}
28672888

2868-
oneline_offset = arg_len;
2869-
merge_arg_len = strcspn(arg, " \t\n");
2870-
p = arg + merge_arg_len;
2871-
p += strspn(p, " \t\n");
2872-
if (*p == '#' && (!p[1] || isspace(p[1]))) {
2873-
p += 1 + strspn(p + 1, " \t\n");
2874-
oneline_offset = p - arg;
2875-
} else if (p - arg < arg_len)
2876-
BUG("octopus merges are not supported yet: '%s'", p);
2877-
2878-
strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
2879-
merge_commit = lookup_commit_reference_by_name(ref_name.buf);
2880-
if (!merge_commit) {
2881-
/* fall back to non-rewritten ref or commit */
2882-
strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
2883-
merge_commit = lookup_commit_reference_by_name(ref_name.buf);
2889+
/*
2890+
* For octopus merges, the arg starts with the list of revisions to be
2891+
* merged. The list is optionally followed by '#' and the oneline.
2892+
*/
2893+
merge_arg_len = oneline_offset = arg_len;
2894+
for (p = arg; p - arg < arg_len; p += strspn(p, " \t\n")) {
2895+
if (!*p)
2896+
break;
2897+
if (*p == '#' && (!p[1] || isspace(p[1]))) {
2898+
p += 1 + strspn(p + 1, " \t\n");
2899+
oneline_offset = p - arg;
2900+
break;
2901+
}
2902+
k = strcspn(p, " \t\n");
2903+
if (!k)
2904+
continue;
2905+
merge_commit = lookup_label(p, k, &ref_name);
2906+
if (!merge_commit) {
2907+
ret = error(_("unable to parse '%.*s'"), k, p);
2908+
goto leave_merge;
2909+
}
2910+
tail = &commit_list_insert(merge_commit, tail)->next;
2911+
p += k;
2912+
merge_arg_len = p - arg;
28842913
}
28852914

2886-
if (!merge_commit) {
2887-
ret = error(_("could not resolve '%s'"), ref_name.buf);
2915+
if (!to_merge) {
2916+
ret = error(_("nothing to merge: '%.*s'"), arg_len, arg);
28882917
goto leave_merge;
28892918
}
28902919

@@ -2895,8 +2924,13 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
28952924
* "[new root]", let's simply fast-forward to the merge head.
28962925
*/
28972926
rollback_lock_file(&lock);
2898-
ret = fast_forward_to(&merge_commit->object.oid,
2899-
&head_commit->object.oid, 0, opts);
2927+
if (to_merge->next)
2928+
ret = error(_("octopus merge cannot be executed on "
2929+
"top of a [new root]"));
2930+
else
2931+
ret = fast_forward_to(&to_merge->item->object.oid,
2932+
&head_commit->object.oid, 0,
2933+
opts);
29002934
goto leave_merge;
29012935
}
29022936

@@ -2932,7 +2966,8 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
29322966
p = arg + oneline_offset;
29332967
len = arg_len - oneline_offset;
29342968
} else {
2935-
strbuf_addf(&buf, "Merge branch '%.*s'",
2969+
strbuf_addf(&buf, "Merge %s '%.*s'",
2970+
to_merge->next ? "branches" : "branch",
29362971
merge_arg_len, arg);
29372972
p = buf.buf;
29382973
len = buf.len;
@@ -2956,28 +2991,76 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
29562991
&head_commit->object.oid);
29572992

29582993
/*
2959-
* If the merge head is different from the original one, we cannot
2994+
* If any merge head is different from the original one, we cannot
29602995
* fast-forward.
29612996
*/
29622997
if (can_fast_forward) {
2963-
struct commit_list *second_parent = commit->parents->next;
2998+
struct commit_list *p = commit->parents->next;
29642999

2965-
if (second_parent && !second_parent->next &&
2966-
oidcmp(&merge_commit->object.oid,
2967-
&second_parent->item->object.oid))
3000+
for (j = to_merge; j && p; j = j->next, p = p->next)
3001+
if (oidcmp(&j->item->object.oid,
3002+
&p->item->object.oid)) {
3003+
can_fast_forward = 0;
3004+
break;
3005+
}
3006+
/*
3007+
* If the number of merge heads differs from the original merge
3008+
* commit, we cannot fast-forward.
3009+
*/
3010+
if (j || p)
29683011
can_fast_forward = 0;
29693012
}
29703013

2971-
if (can_fast_forward && commit->parents->next &&
2972-
!commit->parents->next->next &&
2973-
!oidcmp(&commit->parents->next->item->object.oid,
2974-
&merge_commit->object.oid)) {
3014+
if (can_fast_forward) {
29753015
rollback_lock_file(&lock);
29763016
ret = fast_forward_to(&commit->object.oid,
29773017
&head_commit->object.oid, 0, opts);
29783018
goto leave_merge;
29793019
}
29803020

3021+
if (to_merge->next) {
3022+
/* Octopus merge */
3023+
struct child_process cmd = CHILD_PROCESS_INIT;
3024+
3025+
if (read_env_script(&cmd.env_array)) {
3026+
const char *gpg_opt = gpg_sign_opt_quoted(opts);
3027+
3028+
ret = error(_(staged_changes_advice), gpg_opt, gpg_opt);
3029+
goto leave_merge;
3030+
}
3031+
3032+
cmd.git_cmd = 1;
3033+
argv_array_pushl(&cmd.args,
3034+
"merge",
3035+
"--strategy=octopus",
3036+
"--no-edit",
3037+
"--no-ff",
3038+
"--no-log",
3039+
"--no-stat",
3040+
"-F", git_path_merge_msg(),
3041+
NULL);
3042+
if (opts->gpg_sign)
3043+
argv_array_push(&cmd.args, opts->gpg_sign);
3044+
3045+
/* Add the tips to be merged */
3046+
for (j = to_merge; j; j = j->next)
3047+
argv_array_push(&cmd.args,
3048+
oid_to_hex(&j->item->object.oid));
3049+
3050+
strbuf_release(&ref_name);
3051+
unlink(git_path_cherry_pick_head());
3052+
rollback_lock_file(&lock);
3053+
3054+
rollback_lock_file(&lock);
3055+
ret = run_command(&cmd);
3056+
3057+
/* force re-reading of the cache */
3058+
if (!ret && (discard_cache() < 0 || read_cache() < 0))
3059+
ret = error(_("could not read index"));
3060+
goto leave_merge;
3061+
}
3062+
3063+
merge_commit = to_merge->item;
29813064
write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
29823065
git_path_merge_head(), 0);
29833066
write_message("no-ff", 5, git_path_merge_mode(), 0);
@@ -3040,6 +3123,7 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
30403123
leave_merge:
30413124
strbuf_release(&ref_name);
30423125
rollback_lock_file(&lock);
3126+
free_commit_list(to_merge);
30433127
return ret;
30443128
}
30453129

@@ -3877,7 +3961,6 @@ static int make_script_with_merges(struct pretty_print_context *pp,
38773961
*/
38783962
while ((commit = get_revision(revs))) {
38793963
struct commit_list *to_merge;
3880-
int is_octopus;
38813964
const char *p1, *p2;
38823965
struct object_id *oid;
38833966
int is_empty;
@@ -3909,11 +3992,6 @@ static int make_script_with_merges(struct pretty_print_context *pp,
39093992
continue;
39103993
}
39113994

3912-
is_octopus = to_merge && to_merge->next;
3913-
3914-
if (is_octopus)
3915-
BUG("Octopus merges not yet supported");
3916-
39173995
/* Create a label */
39183996
strbuf_reset(&label);
39193997
if (skip_prefix(oneline.buf, "Merge ", &p1) &&
@@ -3935,13 +4013,17 @@ static int make_script_with_merges(struct pretty_print_context *pp,
39354013
strbuf_addf(&buf, "%s -C %s",
39364014
cmd_merge, oid_to_hex(&commit->object.oid));
39374015

3938-
/* label the tip of merged branch */
3939-
oid = &to_merge->item->object.oid;
3940-
strbuf_addch(&buf, ' ');
4016+
/* label the tips of merged branches */
4017+
for (; to_merge; to_merge = to_merge->next) {
4018+
oid = &to_merge->item->object.oid;
4019+
strbuf_addch(&buf, ' ');
4020+
4021+
if (!oidset_contains(&interesting, oid)) {
4022+
strbuf_addstr(&buf, label_oid(oid, NULL,
4023+
&state));
4024+
continue;
4025+
}
39414026

3942-
if (!oidset_contains(&interesting, oid))
3943-
strbuf_addstr(&buf, label_oid(oid, NULL, &state));
3944-
else {
39454027
tips_tail = &commit_list_insert(to_merge->item,
39464028
tips_tail)->next;
39474029

t/t3430-rebase-merges.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,38 @@ test_expect_success 'labels that are object IDs are rewritten' '
329329
! grep "^label $third$" .git/ORIGINAL-TODO
330330
'
331331

332+
test_expect_success 'octopus merges' '
333+
git checkout -b three &&
334+
test_commit before-octopus &&
335+
test_commit three &&
336+
git checkout -b two HEAD^ &&
337+
test_commit two &&
338+
git checkout -b one HEAD^ &&
339+
test_commit one &&
340+
test_tick &&
341+
GIT_AUTHOR_NAME="Hank" GIT_AUTHOR_EMAIL="[email protected]" \
342+
git merge -m "Tüntenfüsch" two three &&
343+
344+
: fast forward if possible &&
345+
before="$(git rev-parse --verify HEAD)" &&
346+
test_tick &&
347+
git rebase -i -r HEAD^^ &&
348+
test_cmp_rev HEAD $before &&
349+
350+
test_tick &&
351+
git rebase -i --force -r HEAD^^ &&
352+
test "Hank" = "$(git show -s --format=%an HEAD)" &&
353+
test "$before" != $(git rev-parse HEAD) &&
354+
test_cmp_graph HEAD^^.. <<-\EOF
355+
*-. Tüntenfüsch
356+
|\ \
357+
| | * three
358+
| * | two
359+
| |/
360+
* | one
361+
|/
362+
o before-octopus
363+
EOF
364+
'
365+
332366
test_done

0 commit comments

Comments
 (0)