154

From what I read, both of them help us get a linear history.

From what I experimented, rebase works all the time. But merge --ff-only works only in scenarios where it can be fast forwarded.

I also noticed, git merge creates a merge commit, but if we use --ff-only, it gives a linear history which essentially is equal to git rebasing. So --ff-only kills the purpose of git merge, right?

So what is the actual difference between them?

Ivin
  • 4,435
  • 8
  • 46
  • 65
  • https://stackoverflow.com/questions/25430600/difference-between-git-pull-rebase-and-git-pull-ff-only -- looks like fast-forward will merge changes that have no overlap, and merge rebase will apply remote changes, then your local changes. It seems like that could produce code that no one person wrote or checked end-to-end. – Chris Dec 02 '22 at 15:05

3 Answers3

312

Note that git rebase has a different job than git merge (with or without --ff-only). What rebase does is to take existing commits and copy them. Suppose, for instance, that you're on branch1 and have made two commits A and B:

...-o--o--A--B   <-- HEAD=branch1
        \
         o--C    <-- branch2

and you decide that you'd rather have those two commits be on branch2 instead. You can:

  • get a list of changes you made in A (diff A against its parent)
  • get a list of changes you made in B (diff B against A)
  • switch to branch2
  • make the same changes you made in A and commit them, copying your commit message from A; let's call this commit A'
  • and then make the same changes you made in B and commit them, copying your commit message from B; let's call this B'.

There's a git command that does this diff-and-then-copy-and-commit for you: git cherry-pick. So:

git checkout branch2      # switch HEAD to branch2 (commit C)
git cherry-pick branch1^  # this copies A to A'
git cherry-pick branch1   # and this copies B to B'

Now you have this:

...-o--o--A--B         <-- branch1
        \
         o--C--A'-B'   <-- HEAD=branch2

Now you can switch back to branch1 and delete your original A and B, using git reset (I'll use --hard here, it's more convenient that way as it cleans up the work-tree too):

git checkout branch1
git reset --hard HEAD~2

This removes the original A and B,1 so now you have:

...-o--o               <-- HEAD=branch1
        \
         o--C--A'-B'   <-- branch2

Now you just need to re-check-out branch2 to continue working there.

This is what git rebase does: it "moves" commits (though not by actually moving them, because it can't: in git, a commit can never be changed, so even just changing the parent-ID requires copying it to new and slightly different commit).

In other words, while git cherry-pick is an automated diff-and-redo of one commit, git rebase is an automated process of redoing multiple commits, plus, at the end, moving labels around to "forget" or hide-away the originals.

The above illustrates moving commits from one local branch branch1 to another local branch branch2, but git uses the exact same process to move commits when you have a remote-tracking branch that acquires some new commits when you do a git fetch (including the fetch that is the first step of git pull). You might start by working on branch feature, that has an upstream of origin/feature, and make a couple of commits of your own:

...-o        <-- origin/feature
     \
      A--B   <-- HEAD=feature

But then you decide you should see what has happened upstream, so you run git fetch,2 and, aha, someone upstream wrote a commit C:

...-o--C     <-- origin/feature
     \
      A--B   <-- HEAD=feature

At this point you can simply rebase your feature's A and B onto C, giving:

...-o--C     <-- origin/feature
        \
         A'-B'  <-- HEAD=feature

These are copies of your original A and B, with the originals being thrown away (but see footnote 1) after the copies are complete.


Sometimes there's nothing to rebase, i.e., no work that you yourself did. That is, the graph before the fetch look like this:

...-o      <-- origin/feature
           `-- HEAD=feature

If you then git fetch and commit C comes in, though, you're left with your feature branch pointing to the old commit, while origin/feature has moved forward:

...-o--C   <-- origin/feature
     `---- <-- HEAD=feature

This is where git merge --ff-only comes in: if you ask to merge your current branch feature with origin/feature, git sees that it's possible to just slide the arrow forward, as it were, so that feature points directly to commit C. No actual merge is required.

If you had your own commits A and B, though, and you asked to merge those with C, git would do a real merge, making a new merge commit M:

...-o--C        <-- origin/feature
     \   `-_
      A--B--M   <-- feature

Here, --ff-only will stop and give you an error. Rebase, on the other hand, can copy A and B to A' and B' and then hide away the original A and B.

So, in short (ok, too late :-) ), they simply do different things. Sometimes the result is the same, and sometimes it's not. If it's OK to copy A and B, you can use git rebase; but if there's some good reason not to copy them, you can use git merge, perhaps with --ff-only, to merge-or-fail as appropriate.


1Git actually keeps the originals for some time—normally a month in this case—but it hides them away. The easiest way to find them is with git's "reflogs", which keep a history of where each branch pointed, and where HEAD pointed, before each change that updated the branch and/or HEAD.

Eventually the reflog history entries expire, at which point these commits become eligible for garbage collection.

2Or, again, you can use git pull, which is a convenience script that starts by running git fetch. Once the fetch is done, the convenience script runs either git merge or git rebase, depending on how you configure and run it.

aquaraga
  • 4,138
  • 23
  • 29
torek
  • 448,244
  • 59
  • 642
  • 775
  • When you are merging a PR from the development branch to the master(prod) what is the recommended strategy? – Shanika Ediriweera Mar 27 '19 at 00:56
  • @ShanikaEdiriweera: Recommended by whom, and for what purpose? (Various places I have been have had different goals and hence used different mechanisms.) Without knowing your situation, I cannot give useful advice. – torek Mar 27 '19 at 01:05
  • I have a dev branch and all developers work on their features and make PRs to dev branch. Here I accept with no-ff to have a merge commit. (is this correct workflow?) Now we have finished a complete production ready version and have made a PR from dev to master branch. I am not sure which strategy to use when merging into the master? – Shanika Ediriweera Mar 27 '19 at 01:13
  • 1
    That sounds like a place to do a standard merge, probably with `--no-ff` here as well. – torek Mar 27 '19 at 01:19
  • A minor tweak would be to describe cherry-pick and rebase as copying (rather than moving) commits to make it clearer what happens and that the source branch is not modified. You kind of explain by saying it "moves commits, but not by actually moving them", but just saying they are copied I think would be simpler and more accurate. – studgeek May 21 '20 at 15:14
  • In your first example where you say `Now you can switch back to branch1 and delete your original A and B` , what if you don't reset your commits? Does the history look like the same git commit originated in two separate branches? I feel like I have done that a lot; seen dejavu commits it in gitk or similar history viewers. I feel like it then confuses the automatic merge conflict resolution, or maybe it doesn't matter and it all gets cleaned up in the reflog eventually? – Davos Jun 11 '20 at 11:59
  • If you do not delete your original A and B commits, they remain. They are *different commits* from the copies: commit identity is (usually; the exceptions won't apply here) the commit's hash ID. So now `branch1` contains the original two commits, and `branch2` contains the copies. If you later to go merge, this may work smoothly anyway, or not. In particular, if you had to *modify* the commits as you copied them with rebase, they are almost guaranteed to conflict with the originals. Either way, as long as you leave the originals in, they remain: Git only deletes a commit if you make it do so. – torek Jun 11 '20 at 13:36
  • It seems a more efficient way for git to operate is to point the changes in a redundant commit by mapping the old and new hash IDs, i.e. `A` and `A'`, to the same storage location rather than making a full copy of the code when rebasing on top of incoming changes. – GViz Aug 17 '22 at 21:15
  • 1
    @GViz: the actual content of a commit is only the metadata: the source snapshot is a `tree` entry *in* the metadata, and if this tree is a duplicate of an earlier tree, the two trees are in fact literally shared. Since trees hold subtrees, this same kind of sharing works even if some parts of some commits are different (the common subtrees get shared and the differing subtrees differ; as differences "bubble up", the top level tree will be different in this case). – torek Aug 18 '22 at 07:34
  • It's for answers like this one that Stack Overflow is the best internet invention since internet, thanks for this wonderful lecture, @torek – Artur Mello Jun 28 '23 at 19:24
17

Yes, there is a difference. git merge --ff-only will abort if it cannot fast forward, and takes a commit (normally a branch) to merge in. It will only create a merge commit if it can't fast forward (i.e. will never do so with --ff-only).

git rebase rewrites history on the current branch, or can be used to rebase an existing branch onto an existing branch. In that instance it won't create a merge commit because it's rebasing, rather than merging.

abligh
  • 24,573
  • 4
  • 47
  • 84
  • Which is to say, --ff-only checks and fails to merge if its not possible without rewriting history. Whereas rebase does it by re writing history. Is it? – Ivin Jan 25 '15 at 20:13
  • 3
    Not quite. Merges do not rewrite history, and thus `git merge` never rewrites history (with or without `--ff-only`). `--ff-only` prevents any merge other than a fast-forward merge (no merges rewrite history). Merges other than fast-forward merges are in general simply applying the change-sets out of order (that's a simplification, but good enough to go with), plus recording metadata that a changeset has been merged; no history rewriting involved. – abligh Jan 25 '15 at 21:42
17

Yes, --ff-only will always fail where a plain git merge would fail, and might fail where a plain git merge would succeed. That's the point - if you're trying to keep a linear history, and the merge can't be done that way, you want it to fail.

An option that adds failure cases to a command isn't useless; it's a way of validating a precondition, so if the current state of the system isn't what you expect, you don't make the problem worse.

  • 2
    So is it like, --ff-only is used to validate the scenario and create a linear history if possible; whereas rebase would be more like, create a linear history regardless of the scenario. Is it? – Ivin Jan 25 '15 at 19:36
  • 7
    Yes, and if you rebase something that wasn't clean enough to `merge --ff-only` you are removing commits from one place in the history tree and reinserting them (with modifications) at a different place. This is called "rewriting history" and it's fine if you're on an unpublished branch, but after you've published your branch and other people have cloned it, rewriting history causes problems for them. –  Jan 25 '15 at 20:01