threads / rfc / 12635

patchRe: [RFC/PATCH] Fast forward strategies allow, never, and only

Subject: Re: [RFC/PATCH] Fast forward strategies allow, never, and only

## tl;dr

5 messages between Mar 11, 2008 and Mar 12, 2008. Diffs are folded; open one to read it.

replies: 4people: 4as markdown or json

colin@horizon.com· Mar 11, 2008, 09:35 UTC · lore
> What's lacking is "why this is a good idea".

Seconded. A long time ago (and I'm too lazy to find a link), Linus explained why disabling fast-forward merges was almost always a Bad Idea, and nobody has come up with a good reason why you'd want one since.

But from memory, suppose that you have two developers, each working on their own branch:

     a--a--a <-- A's head
    /
o--o
    \
     b--b--b <-- B's head

Then suppose that they merge back and forth to get to the same state. With fast-forward merges, it will go like this:

A merges from B:
     a--a--a
    /       \
o--o         o <-- A's head
    \       /
     b--b--b <-- B's head
Then B merges from A:
     a--a--a
    /       \
o--o         o <-- Both heads
    \       /
     b--b--b

And look, they are in sync and can go on to develop from a common base version. Future merges will do nothing.

If, instead, you have every merge generate a commit, then you get:
     a--a--a
    /       \
o--o         o <-- A's head
    \       / \
     b--b--b---o <-- B's head
     a--a--a
    /       \
o--o         o---o <-- A's head
    \       / \ /
     b--b--b---o <-- B's head
     a--a--a
    /       \
o--o         o---o <-- A's head
    \       / \ / \
     b--b--b---o---o <-- B's head

.. and it never ends. All of the merged commits are identical trees, but if you insist on creating a new commit object each time, you can generate an infinite number of bogus commits, and more to the point, A and B will never actually agree on the current HEAD commit.

With more developers, you can make even more of a mess.

What use does the "--ff=never" option have except to generate this cruft? Flexibility is useful only as long as it provides the ability to do something desirable. There's no point to having a button that should never be pushed.

Lars Hjemli· Mar 11, 2008, 10:09 UTC · re: colin@horizon.com · lore
On Tue, Mar 11, 2008 at 10:35 AM,  <colin@horizon.com> wrote:
Show 5 quoted lines
> > What's lacking is "why this is a good idea".
>
>  Seconded.  A long time ago (and I'm too lazy to find a link), Linus
>  explained why disabling fast-forward merges was almost always a Bad Idea,
>  and nobody has come up with a good reason why you'd want one since.
The reason for --no-ff was twofold:
* theoretical: when you want to record the integration of a topic branch
* practical: when merging git-svn branches in git, git-svn dcommit
would update the wrong svn 'branch' if the merge was a fast-forward

I originally needed --no-ff due to the 'practical' aspects (I used git-svn when working with the day-job svn repository), but now that we've switched to git (Hurray!) I'm still using --no-ff for the 'theoretical' reason: our topic branches tend to be named after bugtracker tickets, so by recording the merge of such a branch we get a very explicit note in our git log about when each ticket was resolved.

YMMV.

-- larsh

Bruce Stephens· Mar 11, 2008, 12:24 UTC · re: colin@horizon.com · lore
colin@horizon.com writes:
>> What's lacking is "why this is a good idea".
[...]
Show 11 quoted lines
> .. and it never ends.  All of the merged commits are identical trees, but
> if you insist on creating a new commit object each time, you can generate
> an infinite number of bogus commits, and more to the point, A and B will
> never actually agree on the current HEAD commit.
>
> With more developers, you can make even more of a mess.
>
> What use does the "--ff=never" option have except to generate this cruft?
> Flexibility is useful only as long as it provides the ability to do
> something desirable.  There's no point to having a button that should
> never be pushed.

IIUC what the new option is about is (optionally) forbidding merges. So it's orthogonal to the existing --no-ff and --ff merge options.

So you *don't* get that kind of criss-crossing: if you've got a local commit, the merge fails. So you have to use rebase. So it's not making the history more complex, it's linearizing it.

Now surely you don't always want to do that, but it seems like a very convenient option that you can generally have on, and switch off when you intend to do a merge.

Bruce Stephens· Mar 11, 2008, 12:33 UTC · re: Bruce Stephens · lore
Bruce Stephens <bruce.stephens@isode.com> writes:
> colin@horizon.com writes:
[...]
> IIUC what the new option is about is (optionally) forbidding merges.
> So it's orthogonal to the existing --no-ff and --ff merge options.
I'm wrong.  My apologies.
[...]
Junio C Hamano· Mar 12, 2008, 01:57 UTC · re: colin@horizon.com · lore
colin@horizon.com writes:
Show 20 quoted lines
>      a--a--a
>     /       \
> o--o         o---o <-- A's head
>     \       / \ /
>      b--b--b---o <-- B's head
>
>      a--a--a
>     /       \
> o--o         o---o <-- A's head
>     \       / \ / \
>      b--b--b---o---o <-- B's head
>
> .. and it never ends.  All of the merged commits are identical trees, but
> if you insist on creating a new commit object each time, you can generate
> an infinite number of bogus commits, and more to the point, A and B will
> never actually agree on the current HEAD commit.
>
> With more developers, you can make even more of a mess.
>
> What use does the "--ff=never" option have except to generate this cruft?

Judicious use of non-fast-forward has a justification that is not too unreasonable. That is, when you want to treat one lineage of history as "more special than others".

If your workflow is always to branch from the special branch ("master") when working on even a miniscule topic and merge that back to "master", if you happen to have worked only on a single topic and the "master" was never advanced during the time you worked on that topic, merging the topic back to "master" will result in a fast-forward. When you look back that history, you won't be able to tell where the topic started and ended by following the ancestry chain of the "master" branch.

Using "never fast forward" policy on such a special branch will be a way to make sure that all commits on the first-parent ancestry of that special branch will be merges from something else, and by computing $it^1..$it^2 for a merge commit $it on the special branch, which merges the topic fully into it, you can tell what commits the topic consisted of.

When you have repeated merges from a topic to that special branch, this computation needs to be a bit more than just $it^1..$it^2 of the last merge commit that merges the topic into "master". E.g. you would have two "should have been fast forward but artificially made into a real merge for the purpose of peeing in the snow" like this:

           o---o---o---o---o "topic"
          /     \           \
      ---o-------*-----------* "master"
 
By following the first-parent ancestry of "master", you can tell that the
first two changes on "topic" were accepted earlier and then three fixups
on top were incorporated much later, which is not something you can do if
you allowed fast-forward merge into "master".  Computing this history is
somewhat expensive but it is doable.  You have to follow the commit
ancestry of "topic", and for each commit you find, you would need to see
which commit on the first-parent ancestry of "master" can reach it
(e.g. the three topmost ones on "topic" can be reachable only by the last
merge on "master", while the remaining two can be reached by the previous
merge on "master").

In other words, if there is a globally special "master" history where everybody meets, forcing an artificial merge can have value. However, for this to work, you can never commit anything directly on such a special "master" branch, because directly committing on "master" is equivalent to fork a small topic branch that has a single commit on it, and immediately merging it back with a fast-forward merge to "master". So an artificial merge can have value but that value can be had only with a disciplined workflow.

Last night I pulled a topic from Shawn which was a series of updates to the bash completion script. It was based on the tip of 'master' and resulted in a fast forward. In git.git circle, it happens that my "master" history is not special at all. I have "trivially correct fixups" directly committed on "master" all the time, and fast-forwarding to the tip of bash completion updates Shawn collected for me was exactly that, with only different committer. So even though I act as the top-level integrator for git.git history, there was no reason to do non-fast-forward merge at that point. My tree is not that special.

On the other hand, I probably _could_ use non-ff to manage "next", which will fork off of the tip of "master" after every major release. In order to treat the first topic that will be merged into "next" just like other later topics, it should be merged without fast-forward. The latter topics will never fast-forward (because topics fork off of "master" or "maint" and never from "next" itself) but the very first one can (because "master" and "next" will be at the same at that point), and allowing fast-forward would mean the first topic after a major release is treated differently from others. This is possible only because there is a fairly strict discipline of not committing anything directly on top of "next" and not forking off of it.

← back to recent threads