tl;dr
Having a linear history between two branches can only be done by rebasing one branch on top of the other before merging. If you just merge two branches that have diverged, Git will join the two lines of history by creating a merge commit.
Merging with merge commits
In Git, a commit normally holds a reference to one parent. A merge commit is a special kind of commit that references two or more parents.
In your example, the merge commit C5
has two parents:
- The first parent is
C2
, that is the commit on the branch
where the other branch was merged to, i.e. branch1
- The second parent is
C4
, that is the commit on the branch that was merged, i.e. branch2
.
When you do git log
on branch1
Git will follow both lines of history showing you this:
C1--C2--C5 <- branch1
/
C3--C4 <- branch2
If you do git log
on branch2
, the lines of history will be swapped:
C3--C4 <- branch2
/
C1--C2--C5 <- branch1
Merging without merge commits
The scenario in which the commits between the two branches appear to be in the same line of history – with no merge commit involved – is the result of a rebase.
Rebasing branch2
on top of branch1
is an operation that's conceptually different from merging the two branches.
While merging is done with:
git checkout branch1
git merge branch2
Rebasing is done with:
git checkout branch2
git rebase branch1
which basically means:
Find all the commits that are reachable from branch2
but not from branch1
– in this case C3
and C4
– and apply them on top of the latest commit in branch1
.
So the final history will look like this:
C1--C2--C3--C4 <- branch2
^
branch1
Notice that branch1
still points to C2
– the same commit as it did before the rebase. Since the history between the two branches is now on the same line, merging them with:
git checkout branch1
git merge branch2
won't create a merge commit. Instead, Git will simply move the branch1
forward so that it points to the same commit as branch2
. This operation is called a fast-forward:
To phrase that another way, when you try to merge one commit with a
commit that can be reached by following the first commit’s history,
Git simplifies things by moving the pointer forward because there is
no divergent work to merge together – this is called a “fast-forward.”
Undoing a merge
The way you undo a merge will vary depending on whether the merge was done through a merge commit or not.
If there's a merge commit, you can undo the merge by simply moving branch1
to point to the merge commit's first parent:
git checkout branch1 # branch1 points at the merge commit C5
git reset --hard branch1^ # branch1 now points at C2
If the merge was done after a rebase, things are a bit trickier. You basically need to restore branch1
to point to the commit it did before the merge. If merging was the latest operation you did, you can use the special reference ORIG_HEAD
:
git reset --hard ORIG_HEAD
Otherwise you'll have to resort to the reflog:
git checkout branch1
git reflog # Find the commit that branch1 pointed to before the merge
git reset --hard HEAD@{n} # Move branch1 to point to that entry in the reflog