Discussion:
[RFC/PATCH] git-merge: implement --ff-only-merge option.
Sergey Organov
2014-10-07 16:35:10 UTC
Permalink
This option allows to create merge commit when fast-forward is
possible, and abort otherwise. I.e. it's equivalent to --ff-only,
except that it finally creates merge commit instead of
fast-forwarding.

One may also consider this option to be equivalent to --no-ff with
additional check that the command without --no-ff would indeed result
in fast-forward.

Useful to incorporate topic branch as single merge commit, ensuring
the left-side of the merge has no changes (our-diff-empty-merge).

Signed-off-by: Sergey Organov <***@gmail.com>
---
builtin/merge.c | 39 ++++++++++++++++++++++++++++++---------
1 file changed, 30 insertions(+), 9 deletions(-)

diff --git a/builtin/merge.c b/builtin/merge.c
index dff043d..39d0f1e 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -79,7 +79,8 @@ static const char *pull_twohead, *pull_octopus;
enum ff_type {
FF_NO,
FF_ALLOW,
- FF_ONLY
+ FF_ONLY,
+ FF_ONLY_MERGE
};

static enum ff_type fast_forward = FF_ALLOW;
@@ -206,6 +207,9 @@ static struct option builtin_merge_options[] = {
{ OPTION_SET_INT, 0, "ff-only", &fast_forward, NULL,
N_("abort if fast-forward is not possible"),
PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, FF_ONLY },
+ { OPTION_SET_INT, 0, "ff-only-merge", &fast_forward, NULL,
+ N_("create merge commit when fast-forward is possible, abort otherwise"),
+ PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, FF_ONLY_MERGE },
OPT_RERERE_AUTOUPDATE(&allow_rerere_auto),
OPT_BOOL(0, "verify-signatures", &verify_signatures,
N_("Verify that the named commit has a valid GPG signature")),
@@ -591,6 +595,8 @@ static int git_merge_config(const char *k, const char *v, void *cb)
fast_forward = boolval ? FF_ALLOW : FF_NO;
} else if (v && !strcmp(v, "only")) {
fast_forward = FF_ONLY;
+ } else if (v && !strcmp(v, "merge")) {
+ fast_forward = FF_ONLY_MERGE;
} /* do not barf on values from future versions of git */
return 0;
} else if (!strcmp(k, "merge.defaulttoupstream")) {
@@ -866,7 +872,7 @@ static int finish_automerge(struct commit *head,

free_commit_list(common);
parents = remoteheads;
- if (!head_subsumed || fast_forward == FF_NO)
+ if (!head_subsumed || (fast_forward == FF_NO || fast_forward == FF_ONLY_MERGE))
commit_list_insert(head, &parents);
strbuf_addch(&merge_msg, '\n');
prepare_to_commit(remoteheads);
@@ -1162,6 +1168,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
if (squash) {
if (fast_forward == FF_NO)
die(_("You cannot combine --squash with --no-ff."));
+ if (fast_forward == FF_ONLY_MERGE)
+ die(_("You cannot combine --squash with --ff-only-merge."));
option_commit = 0;
}

@@ -1206,7 +1214,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
"empty head"));
if (squash)
die(_("Squash commit into empty head not supported yet"));
- if (fast_forward == FF_NO)
+ if (fast_forward == FF_NO || fast_forward == FF_ONLY_MERGE)
die(_("Non-fast-forward commit does not make sense into "
"an empty head"));
remoteheads = collect_parents(head_commit, &head_subsumed, argc, argv);
@@ -1292,6 +1300,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
setenv(buf.buf, merge_remote_util(commit)->name, 1);
strbuf_reset(&buf);
if (fast_forward != FF_ONLY &&
+ fast_forward != FF_ONLY_MERGE &&
merge_remote_util(commit) &&
merge_remote_util(commit)->obj &&
merge_remote_util(commit)->obj->type == OBJ_TAG)
@@ -1312,7 +1321,8 @@ int cmd_merge(int argc, const char **argv, const char *prefix)

for (i = 0; i < use_strategies_nr; i++) {
if (use_strategies[i]->attr & NO_FAST_FORWARD)
- fast_forward = FF_NO;
+ if(fast_forward != FF_ONLY_MERGE)
+ fast_forward = FF_NO;
if (use_strategies[i]->attr & NO_TRIVIAL)
allow_trivial = 0;
}
@@ -1342,9 +1352,19 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
*/
finish_up_to_date("Already up-to-date.");
goto done;
- } else if (fast_forward != FF_NO && !remoteheads->next &&
- !common->next &&
- !hashcmp(common->item->object.sha1, head_commit->object.sha1)) {
+ } else if (fast_forward != FF_NO &&
+ !remoteheads->next &&
+ !common->next &&
+ !hashcmp(common->item->object.sha1, head_commit->object.sha1)) {
+
+ if (fast_forward == FF_ONLY_MERGE) {
+ /*
+ * We are going to fast-forward, but options force us to create
+ * merge commit instead.
+ */
+ goto commit;
+ }
+
/* Again the most common case of merging one remote. */
struct strbuf msg = STRBUF_INIT;
struct commit *commit;
@@ -1389,7 +1409,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
* only one common.
*/
refresh_cache(REFRESH_QUIET);
- if (allow_trivial && fast_forward != FF_ONLY) {
+ if (allow_trivial && fast_forward != FF_ONLY && fast_forward != FF_ONLY_MERGE) {
/* See if it is really trivial. */
git_committer_info(IDENT_STRICT);
printf(_("Trying really trivial in-index merge...\n"));
@@ -1430,9 +1450,10 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
}
}

- if (fast_forward == FF_ONLY)
+ if (fast_forward == FF_ONLY || fast_forward == FF_ONLY_MERGE)
die(_("Not possible to fast-forward, aborting."));

+commit:
/* We are going to make a new commit. */
git_committer_info(IDENT_STRICT);
--
1.9.3
Junio C Hamano
2014-10-07 18:37:46 UTC
Permalink
Post by Sergey Organov
This option allows to create merge commit when fast-forward is
possible, and abort otherwise. I.e. it's equivalent to --ff-only,
except that it finally creates merge commit instead of
fast-forwarding.
One may also consider this option to be equivalent to --no-ff with
additional check that the command without --no-ff would indeed result
in fast-forward.
Useful to incorporate topic branch as single merge commit, ensuring
the left-side of the merge has no changes (our-diff-empty-merge).
---
The workflow this implements sounds like "because we can", not
"because it will help us do X and Y and Z". Why would it be useful
to limit the history to a shape where all merges are the ones that
could have been fast-forwarded?

I cannot justify that sensibly myself, which in turn makes the
feature smell to me that it is encouraging a wrong workflow.
Sergey Organov
2014-10-07 20:24:23 UTC
Permalink
Post by Junio C Hamano
Post by Sergey Organov
This option allows to create merge commit when fast-forward is
possible, and abort otherwise. I.e. it's equivalent to --ff-only,
except that it finally creates merge commit instead of
fast-forwarding.
One may also consider this option to be equivalent to --no-ff with
additional check that the command without --no-ff would indeed result
in fast-forward.
Useful to incorporate topic branch as single merge commit, ensuring
the left-side of the merge has no changes (our-diff-empty-merge).
---
The workflow this implements sounds like "because we can", not
"because it will help us do X and Y and Z".
Well, I do have full time job, and while I think I can instantly invent
quite a few things from the "because we can" camp, I usually don't.
Post by Junio C Hamano
Why would it be useful to limit the history to a shape where all
merges are the ones that could have been fast-forwarded?
Except by true merge, how else can I express with git that 'n'
consequitive commits constitute single logical change (being originally
some topic branch)? Now I just want such special kind of merge to be
entirely trivial merge on one side. i.e. perfectly clean merge every
time.

Moreover, as topic branches are usually rebased before merge anyway,
why shouldn't I have simple capability to enforce it?
Post by Junio C Hamano
I cannot justify that sensibly myself, which in turn makes the
feature smell to me that it is encouraging a wrong workflow.
What's wrong, exactly, in enforcing rebasing of topic branches before
merge? Basically, I need --ff-only, only I don't want git to forget that
this entire set of commits is logically single unit. Neither do I want
to loose the structure of commits that --squash offers.

-- Sergey.
Junio C Hamano
2014-10-07 20:27:30 UTC
Permalink
Post by Sergey Organov
Post by Junio C Hamano
Why would it be useful to limit the history to a shape where all
merges are the ones that could have been fast-forwarded?
Except by true merge, how else can I express with git that 'n'
consequitive commits constitute single logical change (being originally
some topic branch)?
You are justifying --no-ff, aren't you?
Post by Sergey Organov
Moreover, as topic branches are usually rebased before merge anyway,
why shouldn't I have simple capability to enforce it?
Because rebasing immediately before is considered a bad manner,
i.e. encouraging a wrong workflow?
Sergey Organov
2014-10-07 20:42:25 UTC
Permalink
Post by Junio C Hamano
Post by Sergey Organov
Post by Junio C Hamano
Why would it be useful to limit the history to a shape where all
merges are the ones that could have been fast-forwarded?
Except by true merge, how else can I express with git that 'n'
consequitive commits constitute single logical change (being originally
some topic branch)?
You are justifying --no-ff, aren't you?
Yes, and I already said it's close. But I don't want such merge commit
to contain any non-trivialities. Currently I check it manually before
issuing "merge --no-ff" and was hoping for some automation.
Post by Junio C Hamano
Post by Sergey Organov
Moreover, as topic branches are usually rebased before merge anyway,
why shouldn't I have simple capability to enforce it?
Because rebasing immediately before is considered a bad manner,
i.e. encouraging a wrong workflow?
Why? What is wrong about it?

Please also notice that I don't try to impose this on anybody who does
consider it wrong workflow.
--
Sergey.
Junio C Hamano
2014-10-07 21:38:03 UTC
Permalink
Post by Sergey Organov
Post by Junio C Hamano
Because rebasing immediately before is considered a bad manner,
i.e. encouraging a wrong workflow?
Why? What is wrong about it?
Searching the kernel archive for messages that talks about "rebase"
and "pull-request" from Linus would tell us why it is frowned upon
in a prominent early adopter project of Git.

You destroy what you have been testing and replace it with an
untested one. If you merge, and if the result of the merge is
broken, at least you would have something that used to work at its
second parent (i.e. the tip of your topic).
Post by Sergey Organov
Please also notice that I don't try to impose this on anybody who does
consider it wrong workflow.
I know ;-). I didn't say anything about "imposing", did I?

Having an option to make it easy to do something undesirable gives
people an excuse to say "See Git has this option to let me do that
easily, it is an officially sanctioned and encouraged workflow".
Sergey Organov
2014-10-08 10:02:41 UTC
Permalink
Post by Junio C Hamano
Post by Sergey Organov
Post by Junio C Hamano
Because rebasing immediately before is considered a bad manner,
i.e. encouraging a wrong workflow?
Why? What is wrong about it?
Searching the kernel archive for messages that talks about "rebase"
and "pull-request" from Linus would tell us why it is frowned upon
in a prominent early adopter project of Git.
I don't argue it's wrong for work-flows used for Linux kernel.

Even in general, once pull-request is issued, it's bad idea to rebase
your work, as you've already published your history.

I don't see how it is relevant to my proposal though. It was bad idea in
general to rewrite published history before this patch, it still is with
this patch, and the patch itself doesn't encourage anybody to rebase
published history as it is about true merge, not about rebase.
Post by Junio C Hamano
You destroy what you have been testing and replace it with an untested
one. If you merge, and if the result of the merge is broken, at least
you would have something that used to work at its second parent (i.e.
the tip of your topic).
This seems to apply to rebasing in general. I have nothing against
work-flows that don't routinely use rebasing, but there are other
work-flows around as well.

In my workflows, I do test the result of rebase, and I'd simply revert
the rebase if the result appears to be broken. When the result is OK, I
do the final merge. If it is perfectly trivial merge, it needs no
testing at all as it doesn't change content. It's this latter condition
that my suggested new option for "git merge" tries to ensure.

Hopefully we are not going into arguing whose work flow is better.
Post by Junio C Hamano
Post by Sergey Organov
Please also notice that I don't try to impose this on anybody who does
consider it wrong workflow.
I know ;-). I didn't say anything about "imposing", did I?
Having an option to make it easy to do something undesirable gives
people an excuse to say "See Git has this option to let me do that
easily, it is an officially sanctioned and encouraged workflow".
Whatever you can do "easily" with my patch, you can do "easily" with
--no-ff right now, except that with my option instead of --no-ff, you
won't get non-trivial merges you didn't intend.

So I rather see the message of my proposal as: provided you rebase your
(topic) branches anyway, you can use this feature to keep essential
information in the history and avoid complications of non-trivial
merges.
--
Sergey.
Loading...