Three-way merge: when both lines have moved
The interactive lesson needs JavaScript. The text below is the complete lesson.
After this lesson you can
- explain what a merge base is, and find it with
git merge-baseapply - predict when a merge produces a new merge commit with two parents understand
- read a merged history with
git log --oneline --graph, and useHEAD^1andHEAD^2to find each side of a merge apply
Understand: Three snapshots, two parents
Now both lines have new commits after A. main has B, and feature has C. Neither is an ancestor of the other.
Git walks back from both tips and finds the nearest common ancestor. It is called the merge base. Here it is A, where the lines parted.
Then Git compares three snapshots: the base, our side and their side. A change made on only one side goes into the result. That is why this is called a three-way merge.
The result is stored in a new commit, a merge commit, with two parents. main moves to it. feature stays put, and B and C are unchanged.
The order of the parents matters. The first parent is the commit you were on, B. The second parent is the one you merged in, C. Git calls them HEAD^1 and HEAD^2.
A merge without error messages only means the texts fitted together. It does not prove that the program works. Run the tests before you share the result.
Watch: A real merge commit
We are on main. The graph forks: main and feature each have a new commit after the first one.
$ git log --oneline --graph --all
We ask Git for the merge base. The answer is a commit ID. Compare it with the log: it is the first commit, where the lines parted.
$ git merge-base main feature
Now we merge feature. --no-edit accepts the message Git suggests. Watch the graph: a new commit with two parents appears, and main moves there.
$ git merge --no-edit feature
Read the graph from the top. The merge commit is at the top. Just below, the line splits in two: one path to each parent. At the bottom they meet again in the merge base.
$ git log --oneline --graph
The first parent, HEAD^1, is where we stood before the merge: Add contact page from main.
$ git log --oneline -1 HEAD^1
The second parent, HEAD^2, is what we merged in: Add login form from feature.
$ git log --oneline -1 HEAD^2
Tasks
- Show the graph with all branches.
- Find the common ancestor of
mainandfeature. - Merge
featureintomain. - Show the second parent of the merge commit.
Practise: Transfer: a new merge
Both main and api have a new commit. You are on api.
Tasks
- Merge
apiintomain. - Create a branch
before-apithat points to the commitmainwas on just before the merge.
Check yourself: Recall it
What is the merge base?
- The newest commit on
main - The nearest commit that both branches share in their history
- The very first commit in the repository
You were on main and ran git merge feature, and Git made a merge commit. What is HEAD^2 now?
- The commit two steps back on
main - The last commit from
feature, which was merged in - The commit
mainpointed to before the merge
What happens to commits B and C when Git makes the merge commit M?
- They are combined into M and disappear
- They are unchanged. M points to both as parents.
- They are copied onto
main
A merge went through without conflicts. What do you know?
- That the program works
- That Git could combine the text changes. The program still has to be tested.
- That nobody changed the same files
Review: Both sides changed README.md before the merge, and HEAD is the merge commit. You want to see README.md as it was on main just before the merge. Which command shows that?
git show HEAD^2:README.mdgit show HEAD^1:README.mdgit diff HEAD^1 HEAD
Common misconceptions
- “A merge copies or rewrites the commits.” The existing commits are unchanged. The merge commit just points to both.
- “
HEAD^2means two commits back.”^2picks the second parent. It is~2that goes back two generations. - “A merge without conflicts means the code works.” Git checks that the texts can be combined, not that the program behaves correctly.
Read more
The three-way rule
When both lines have moved, Git compares three snapshots for every file, and within a file for every changed region: the merge base, ours (the branch you are on) and theirs (the branch you merge in).
| Base | Ours | Theirs | Result |
|---|---|---|---|
| A | A | A | A |
| A | B | A | B — only we changed it |
| A | A | C | C — only they changed it |
| A | B | B | B — the same change on both sides |
| A | B | C | conflict — next lesson |
The base is what makes this work. Comparing only ours and theirs, Git could see that the two versions differ, but not who changed what. With the base it knows which side made the change and can take it. That is why changes in different files, and even in different parts of the same file, usually merge by themselves. Different changes on the same or neighbouring lines become a conflict.
Finding the base
$ git merge-base main feature
df6c7db3a1e0c9f5b8d2e7a4c6f1b9d0e3a5c7f2
The answer is the nearest commit both branches can reach, measured along the graph, not by date. git log --oneline main..feature lists what feature has that main lacks, which is exactly what the merge will bring in, and git diff main...feature with three dots shows the changes on feature since the base. In unusual histories where branches have been merged back and forth, there can be several equally good bases. Git's default strategy, ort, then merges those bases first. You rarely need to think about it.
Anatomy of a merge commit
$ git merge feature
Merge made by the 'ort' strategy.
login.txt | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 login.txt
$ git show --no-patch --format='%h %p %s' HEAD
c3d971a cecf599 2379bbb Merge branch 'feature'
%p prints the parents. The first, cecf599, is where main was. The second, 2379bbb, is the tip of feature. git cat-file -p HEAD shows the same thing as one tree line followed by two parent lines: a merge commit is an ordinary commit with one snapshot, the combined result, and two parents. Neither branch's commits are copied or changed.
In a real terminal Git opens your editor with the message "Merge branch 'feature'". Save to accept it, or use --no-edit to skip the editor, or -m to write your own.
Navigating parents: ^ and ~
| Expression | Means |
|---|---|
HEAD^ or HEAD^1 |
first parent: where you stood |
HEAD^2 |
second parent: what you merged in |
HEAD~2 |
two steps back, always following first parents (same as HEAD^^) |
HEAD^2~1 |
one step back along the merged branch |
^ picks which parent. ~ goes back through generations.
Reading a merged history
* c3d971a Merge branch 'feature'
|\
| * 2379bbb Style login form
| * 27a6985 Add login form
* | cecf599 Add docs
|/
* df6c7db Initial snapshot
The left column is the first-parent line: the story of main. Useful views:
git log --oneline --first-parentshows only the stepsmaintook itself, here c3d971a, cecf599 and df6c7db.git log --mergesshows only merge commits.git diff HEAD^1 HEADshows what the merge brought intomain.
Test before you commit the merge
A merge without conflicts only means the texts fitted together. Example: on main someone renames the function greet() to welcome(). On feature someone else adds a new call to greet() in another file. Git merges both cleanly, and the program no longer runs. Run the tests after every merge, before you push.
If you want to inspect or test the combined result before it is recorded, use git merge --no-commit --no-ff feature. Git merges, stops before committing and prints Automatic merge went well; stopped before committing as requested. Test, then finish with git commit, or back out with git merge --abort. Without --no-ff, a possible fast-forward still happens.
Strategy options, used with care
-X oursor-X theirs: when a region conflicts, take our or their version automatically. Non-conflicting changes from both sides are still merged. Convenient, but you skip the decision a conflict asks for.-s oursis something else entirely: it records a merge but keeps your snapshot unchanged and throws away all their changes. Rarely what you want.- Merging several branches at once,
git merge a b c, makes an "octopus" merge with more than two parents. It refuses if anything conflicts.
Undoing a merge
- While a merge is still in progress, because of a conflict or
--no-commit:git merge --abort. - A completed merge you have not shared:
ORIG_HEADholds where the branch was; the reset lesson shows how to go back. - A merge that is already pushed:
git revert -m 1 <merge>makes a new commit that undoes it.-m 1says that the first parent's side is the one to keep. See the revert lesson.
Official documentation
- git merge, including merge strategies
- git merge-base
- gitrevisions, for
^and~ - Pro Git: Basic Branching and Merging
Per-Arne's Git Good!