For a merge, git finds the merge base. Normally, this is a single specific commit. (In some cases there are multiple merge-base candidates, and git handles these too, but let's not worry about them.) You can think of this as the point at which your branch and the other branch diverged:
<--older--time increases as we move right--newer-->
[see footnote 1]
o - o <-- your branch (master)
/
... - o - *
\
o - o - o <-- their branch (origin/master)
In this diagram, each little o
node denotes a commit. The merge-base is the commit marked *
instead of o
. The tip-most commit of your branch master
is the right-most o
on the top line, and the tip-most commit of their branch origin/master
is the right-most o
on the bottom line.
Let's call the tip-most commit of your branch A
, just for concreteness. (It has its own unique SHA-1 ID and that's its "real name", but let's just call it A
.) Let's call the merge base B
and the tip of their branch C
, and redraw that diagram:
o - A <-- your branch (master)
/
... - o - B
\
o - o - C <-- their branch (origin/master)
To perform a merge (when a merge is required), git starts by running a diff between commit B
and commit A
, as if you did git diff B A
(filling in the actual SHA-1 for B
and A
of course; and it uses an internal version of its diff code, rather than simply reading through literal git diff
text output). This diff tells git "what you changed" since the common base B
.
Next, git runs a git diff B C
(internal format again of course). This diff tells git "what they changed" since that same common base.
Finally, git tries to combine the two diffs. If you added a file and they didn't, git keeps your added file. If you changed the spelling of neighbor
to neighbour
in file README
and they didn't, it keeps your change. If they removed a file you didn't touch, or removed a line from a file you didn't touch, git keeps their change. If you both made the exact same change to weather.h
, git keeps one copy of that change (rather than two copies). Git applies all the "kept" changes to the base version and writes the result to your work-tree, and the merge is now done with no merge conflicts. The result, git believes, is ready to commit. (Note that this is true even if some change you made requires, say, that the system keep the file they deleted, which git has now deleted. That is, suppose they said "oh look, this file isn't used, let's remove it" and you said "oh, oops, I forgot to use that file, let's use it". Git doesn't know that these changes actually conflict: all it can tell is that the lines in the diff don't collide.)
The next step depends on whether you gave the --no-commit
flag.2 This suppresses the commit, no matter what. If you didn't suppress the commit, git merge
commits this no-apparent-conflict set of changes, as a "merge commit" that has two parents:
o - A ----- M <-- your branch (master)
/ /
... - o - B /
\ /
o - o - C <-- their branch (origin/master)
At this point, the working tree matches the new commit M
. While it wasn't checked out, it was checked in (committed) as a merge.
If you did say --no-commit
, or if there was a merge conflict, the merge stops and leaves you to fix and/or commit the merge. The changes are simply present in your work-directory. If and when you decide to commit the result, that will be a merge commit. Git knows this because it leaves a trace-file behind saying "in the middle of a merge". If you decided to cancel the merge with git merge --abort
, that removes the trace-file.
For a rebase, git goes through a similar process. However, instead of finding a merge-base, it finds the set of commits that you have that the upstream doesn't. We can draw that diagram yet again, but let's use different node names:
A - B <-- your branch (master)
/
... - o - o
\
C - D - E <-- their branch (origin/master)
In this case git finds your two commits (A
and B
) and copies them (using git cherry-pick
, more or less). The mechanism for this is potentially complicated, so let's just note here that each cherry-pick
operation requires updating your work-tree and making a new commit. If all goes well, we calll the copy of A
, A'
, and the copy of B
B'
; git moves the branch name to point to B'
; and the final drawing looks like this:
A - B [abandoned]
/
... - o - o A' - B' <-- your branch
\ /
C - D - E <-- their branch
Because each cherry-pick step updates your work-tree and makes a commit from it, your work-tree matches the final commit B'
, not because it got checked out, but because it got checked in (committed), exactly as in the merge case.