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?
Merging is a safe option that preserves the entire history of your repository, while rebasing creates a linear history by moving your feature branch onto the tip of main .
With --no-ff , create a merge commit in all cases, even when the merge could instead be resolved as a fast-forward. With --ff-only , resolve the merge as a fast-forward when possible.
How Git Pull –ff-only Works. Fortunately, Git gives us an option that prevents the accidental creation of new shas in your repository. With git pull --ff-only , Git will update your branch only if it can be “fast-forwarded” without creating new commits.
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:
A
(diff A
against its parent)B
(diff B
against A
)branch2
A
and commit them, copying your commit message from A
; let's call this commit A'
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With