Skip to content

rebase -r: support octopus merges #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Documentation/git-merge.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ SYNOPSIS
'git merge' [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]
[-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]
[--[no-]allow-unrelated-histories]
[--[no-]rerere-autoupdate] [-m <msg>] [<commit>...]
[--[no-]rerere-autoupdate] [-m <msg>] [-F <file>] [<commit>...]
'git merge' --abort
'git merge' --continue

Expand Down Expand Up @@ -75,6 +75,14 @@ The 'git fmt-merge-msg' command can be
used to give a good default for automated 'git merge'
invocations. The automated message can include the branch description.

-F <file>::
--file=<file>::
Read the commit message to be used for the merge commit (in
case one is created).
+
If `--log` is specified, a shortlog of the commits being merged
will be appended to the specified message.

--[no-]rerere-autoupdate::
Allow the rerere mechanism to update the index with the
result of auto-conflict resolution if possible.
Expand Down
14 changes: 8 additions & 6 deletions Documentation/git-rebase.txt
Original file line number Diff line number Diff line change
Expand Up @@ -879,8 +879,8 @@ rescheduled immediately, with a helpful message how to edit the todo list
(this typically happens when a `reset` command was inserted into the todo
list manually and contains a typo).

The `merge` command will merge the specified revision into whatever is
HEAD at that time. With `-C <original-commit>`, the commit message of
The `merge` command will merge the specified revision(s) into whatever
is HEAD at that time. With `-C <original-commit>`, the commit message of
the specified merge commit will be used. When the `-C` is changed to
a lower-case `-c`, the message will be opened in an editor after a
successful merge so that the user can edit the message.
Expand All @@ -889,10 +889,12 @@ If a `merge` command fails for any reason other than merge conflicts (i.e.
when the merge operation did not even start), it is rescheduled immediately.

At this time, the `merge` command will *always* use the `recursive`
merge strategy, with no way to choose a different one. To work around
this, an `exec` command can be used to call `git merge` explicitly,
using the fact that the labels are worktree-local refs (the ref
`refs/rewritten/onto` would correspond to the label `onto`, for example).
merge strategy for regular merges, and the `octopus` strategy for
octopus merges, strategy, with no way to choose a different one. To work
around this, an `exec` command can be used to call `git merge`
explicitly, using the fact that the labels are worktree-local refs (the
ref `refs/rewritten/onto` would correspond to the label `onto`, for
example).

Note: the first command (`label onto`) labels the revision onto which
the commits are rebased; The name `onto` is just a convention, as a nod
Expand Down
35 changes: 35 additions & 0 deletions builtin/merge.c
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,38 @@ static int option_parse_message(const struct option *opt,
return 0;
}

static int option_read_message(struct parse_opt_ctx_t *ctx,
const struct option *opt, int unset)
{
struct strbuf *buf = opt->value;
const char *arg;

if (unset)
BUG("--no-file?!?");

if (ctx->opt) {
arg = ctx->opt;
ctx->opt = NULL;
} else if (ctx->argc > 1) {
ctx->argc--;
arg = *++ctx->argv;
} else
return opterror(opt, "requires a value", 0);

if (buf->len) {
strbuf_complete_line(buf);
strbuf_addch(buf, '\n');
}
if (ctx->prefix && !is_absolute_path(arg))
arg = prefix_filename(ctx->prefix, arg);
if (strbuf_read_file(buf, arg, 0) < 0)
return error(_("could not read file '%s'"), arg);
strbuf_complete_line(buf);
have_message = 1;

return 0;
}

static struct strategy *get_strategy(const char *name)
{
int i;
Expand Down Expand Up @@ -228,6 +260,9 @@ static struct option builtin_merge_options[] = {
OPT_CALLBACK('m', "message", &merge_msg, N_("message"),
N_("merge commit message (for a non-fast-forward merge)"),
option_parse_message),
{ OPTION_LOWLEVEL_CALLBACK, 'F', "file", &merge_msg, N_("path"),
N_("read message from file"), PARSE_OPT_NONEG,
(parse_opt_cb *) option_read_message },
OPT__VERBOSITY(&verbosity),
OPT_BOOL(0, "abort", &abort_current_merge,
N_("abort the current in-progress merge")),
Expand Down
168 changes: 125 additions & 43 deletions sequencer.c
Original file line number Diff line number Diff line change
Expand Up @@ -2841,6 +2841,26 @@ static int do_reset(const char *name, int len, struct replay_opts *opts)
return ret;
}

static struct commit *lookup_label(const char *label, int len,
struct strbuf *buf)
{
struct commit *commit;

strbuf_reset(buf);
strbuf_addf(buf, "refs/rewritten/%.*s", len, label);
commit = lookup_commit_reference_by_name(buf->buf);
if (!commit) {
/* fall back to non-rewritten ref or commit */
strbuf_splice(buf, 0, strlen("refs/rewritten/"), "", 0);
commit = lookup_commit_reference_by_name(buf->buf);
}

if (!commit)
error(_("could not resolve '%s'"), buf->buf);

return commit;
}

static int do_merge(struct commit *commit, const char *arg, int arg_len,
int flags, struct replay_opts *opts)
{
Expand All @@ -2849,8 +2869,9 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
struct strbuf ref_name = STRBUF_INIT;
struct commit *head_commit, *merge_commit, *i;
struct commit_list *bases, *j, *reversed = NULL;
struct commit_list *to_merge = NULL, **tail = &to_merge;
struct merge_options o;
int merge_arg_len, oneline_offset, can_fast_forward, ret;
int merge_arg_len, oneline_offset, can_fast_forward, ret, k;
static struct lock_file lock;
const char *p;

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

oneline_offset = arg_len;
merge_arg_len = strcspn(arg, " \t\n");
p = arg + merge_arg_len;
p += strspn(p, " \t\n");
if (*p == '#' && (!p[1] || isspace(p[1]))) {
p += 1 + strspn(p + 1, " \t\n");
oneline_offset = p - arg;
} else if (p - arg < arg_len)
BUG("octopus merges are not supported yet: '%s'", p);

strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
merge_commit = lookup_commit_reference_by_name(ref_name.buf);
if (!merge_commit) {
/* fall back to non-rewritten ref or commit */
strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
merge_commit = lookup_commit_reference_by_name(ref_name.buf);
/*
* For octopus merges, the arg starts with the list of revisions to be
* merged. The list is optionally followed by '#' and the oneline.
*/
merge_arg_len = oneline_offset = arg_len;
for (p = arg; p - arg < arg_len; p += strspn(p, " \t\n")) {
if (!*p)
break;
if (*p == '#' && (!p[1] || isspace(p[1]))) {
p += 1 + strspn(p + 1, " \t\n");
oneline_offset = p - arg;
break;
}
k = strcspn(p, " \t\n");
if (!k)
continue;
merge_commit = lookup_label(p, k, &ref_name);
if (!merge_commit) {
ret = error(_("unable to parse '%.*s'"), k, p);
goto leave_merge;
}
tail = &commit_list_insert(merge_commit, tail)->next;
p += k;
merge_arg_len = p - arg;
}

if (!merge_commit) {
ret = error(_("could not resolve '%s'"), ref_name.buf);
if (!to_merge) {
ret = error(_("nothing to merge: '%.*s'"), arg_len, arg);
goto leave_merge;
}

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

Expand Down Expand Up @@ -2932,7 +2966,8 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
p = arg + oneline_offset;
len = arg_len - oneline_offset;
} else {
strbuf_addf(&buf, "Merge branch '%.*s'",
strbuf_addf(&buf, "Merge %s '%.*s'",
to_merge->next ? "branches" : "branch",
merge_arg_len, arg);
p = buf.buf;
len = buf.len;
Expand All @@ -2956,28 +2991,76 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
&head_commit->object.oid);

/*
* If the merge head is different from the original one, we cannot
* If any merge head is different from the original one, we cannot
* fast-forward.
*/
if (can_fast_forward) {
struct commit_list *second_parent = commit->parents->next;
struct commit_list *p = commit->parents->next;

if (second_parent && !second_parent->next &&
oidcmp(&merge_commit->object.oid,
&second_parent->item->object.oid))
for (j = to_merge; j && p; j = j->next, p = p->next)
if (oidcmp(&j->item->object.oid,
&p->item->object.oid)) {
can_fast_forward = 0;
break;
}
/*
* If the number of merge heads differs from the original merge
* commit, we cannot fast-forward.
*/
if (j || p)
can_fast_forward = 0;
}

if (can_fast_forward && commit->parents->next &&
!commit->parents->next->next &&
!oidcmp(&commit->parents->next->item->object.oid,
&merge_commit->object.oid)) {
if (can_fast_forward) {
rollback_lock_file(&lock);
ret = fast_forward_to(&commit->object.oid,
&head_commit->object.oid, 0, opts);
goto leave_merge;
}

if (to_merge->next) {
/* Octopus merge */
struct child_process cmd = CHILD_PROCESS_INIT;

if (read_env_script(&cmd.env_array)) {
const char *gpg_opt = gpg_sign_opt_quoted(opts);

ret = error(_(staged_changes_advice), gpg_opt, gpg_opt);
goto leave_merge;
}

cmd.git_cmd = 1;
argv_array_pushl(&cmd.args,
"merge",
"--strategy=octopus",
"--no-edit",
"--no-ff",
"--no-log",
"--no-stat",
"-F", git_path_merge_msg(),
NULL);
if (opts->gpg_sign)
argv_array_push(&cmd.args, opts->gpg_sign);

/* Add the tips to be merged */
for (j = to_merge; j; j = j->next)
argv_array_push(&cmd.args,
oid_to_hex(&j->item->object.oid));

strbuf_release(&ref_name);
unlink(git_path_cherry_pick_head());
rollback_lock_file(&lock);

rollback_lock_file(&lock);
ret = run_command(&cmd);

/* force re-reading of the cache */
if (!ret && (discard_cache() < 0 || read_cache() < 0))
ret = error(_("could not read index"));
goto leave_merge;
}

merge_commit = to_merge->item;
write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
git_path_merge_head(), 0);
write_message("no-ff", 5, git_path_merge_mode(), 0);
Expand Down Expand Up @@ -3040,6 +3123,7 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
leave_merge:
strbuf_release(&ref_name);
rollback_lock_file(&lock);
free_commit_list(to_merge);
return ret;
}

Expand Down Expand Up @@ -3877,7 +3961,6 @@ static int make_script_with_merges(struct pretty_print_context *pp,
*/
while ((commit = get_revision(revs))) {
struct commit_list *to_merge;
int is_octopus;
const char *p1, *p2;
struct object_id *oid;
int is_empty;
Expand Down Expand Up @@ -3909,11 +3992,6 @@ static int make_script_with_merges(struct pretty_print_context *pp,
continue;
}

is_octopus = to_merge && to_merge->next;

if (is_octopus)
BUG("Octopus merges not yet supported");

/* Create a label */
strbuf_reset(&label);
if (skip_prefix(oneline.buf, "Merge ", &p1) &&
Expand All @@ -3935,13 +4013,17 @@ static int make_script_with_merges(struct pretty_print_context *pp,
strbuf_addf(&buf, "%s -C %s",
cmd_merge, oid_to_hex(&commit->object.oid));

/* label the tip of merged branch */
oid = &to_merge->item->object.oid;
strbuf_addch(&buf, ' ');
/* label the tips of merged branches */
for (; to_merge; to_merge = to_merge->next) {
oid = &to_merge->item->object.oid;
strbuf_addch(&buf, ' ');

if (!oidset_contains(&interesting, oid)) {
strbuf_addstr(&buf, label_oid(oid, NULL,
&state));
continue;
}

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

Expand Down
34 changes: 34 additions & 0 deletions t/t3430-rebase-merges.sh
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,38 @@ test_expect_success 'labels that are object IDs are rewritten' '
! grep "^label $third$" .git/ORIGINAL-TODO
'

test_expect_success 'octopus merges' '
git checkout -b three &&
test_commit before-octopus &&
test_commit three &&
git checkout -b two HEAD^ &&
test_commit two &&
git checkout -b one HEAD^ &&
test_commit one &&
test_tick &&
GIT_AUTHOR_NAME="Hank" GIT_AUTHOR_EMAIL="[email protected]" \
git merge -m "Tüntenfüsch" two three &&

: fast forward if possible &&
before="$(git rev-parse --verify HEAD)" &&
test_tick &&
git rebase -i -r HEAD^^ &&
test_cmp_rev HEAD $before &&

test_tick &&
git rebase -i --force -r HEAD^^ &&
test "Hank" = "$(git show -s --format=%an HEAD)" &&
test "$before" != $(git rev-parse HEAD) &&
test_cmp_graph HEAD^^.. <<-\EOF
*-. Tüntenfüsch
|\ \
| | * three
| * | two
| |/
* | one
|/
o before-octopus
EOF
'

test_done