Git & GitHub · lesson 2 of 12
Branching, Merging, and Real Conflicts
Create branches, fast-forward merge one, force a real three-way merge conflict with another, and resolve it — all with actual git output.
By SkillByExample · Published
A merge either fast-forwards a pointer or creates a real merge commit, depending on whether the branches actually diverged. This lesson produces one of each, plus a genuine conflict — nothing here is simulated.
1) Branch, commit, and fast-forward
$ git checkout -b feature/farewell-punctuation
$ # ...edit app.py...
$ git commit -am "Extend farewell message"
$ git checkout main
$ git merge feature/farewell-punctuation
Updating f6a618b..80a7aa3
Fast-forward
app.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
main hadn’t moved since the branch point, so Git just slides the main label forward
to the feature branch’s tip. No merge commit is created — there’s nothing to reconcile.
2) Two branches edit the same place independently
$ git checkout -b feature/add-tests
$ # ...append test_greet() to app.py, commit...
$ git checkout main
$ # ...append a __main__ block to app.py, commit...
$ git log --oneline --all --graph
* 947c644 Add test for greet
| * dbb16bc Add script entry point
|/
* 80a7aa3 Extend farewell message
* f6a618b Add farewell function
* 86b2e4e Add greet function
Both branches added a function to the bottom of app.py, starting from the same
commit (80a7aa3). They’ve genuinely diverged — a fast-forward is no longer possible.
3) The merge conflicts
$ git merge feature/add-tests -m "Merge feature/add-tests into main"
Auto-merging app.py
CONFLICT (content): Merge conflict in app.py
Automatic merge failed; fix conflicts and then commit the result.
Git tries a three-way merge (common ancestor + both tips) automatically. It succeeds line-by-line everywhere except the region both branches touched, and stops there.
4) The conflict markers, in the file itself
$ cat app.py
def greet(name):
return f"Hello, {name}!"
def farewell(name):
return f"Goodbye, {name}! See you soon."
<<<<<<< HEAD
if __name__ == "__main__":
print(greet("World"))
=======
def test_greet():
assert greet("Ana") == "Hello, Ana!"
>>>>>>> feature/add-tests
HEAD (the branch you’re merging into — main) is on top; the incoming branch is on
the bottom. Git doesn’t know whether the test function or the entry point should come
first, or whether you want both.
5) Resolve by hand, then finish the merge
$ # ...edit app.py to keep both additions, remove the markers...
$ git add app.py
$ git commit --no-edit
$ git log --oneline --graph --all
* bd316aa Merge feature/add-tests into main
|\
| * 947c644 Add test for greet
* | dbb16bc Add script entry point
|/
* 80a7aa3 Extend farewell message
* f6a618b Add farewell function
* 86b2e4e Add greet function
Staging the resolved file tells Git the conflict is settled; committing creates a real
merge commit (bd316aa) with two parents — dbb16bc and 947c644 — which is how
the graph shows the branches rejoining.
Practice
-
Run
git statusin the middle of an unresolved conflict (before staging the fix). Read what it suggests:git add <file>to mark resolved, orgit merge --abortto bail out entirely.Solution
On branch main You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: app.pygit merge --abortresets everything back to before the merge started, as if it never ran — useful when a conflict is bigger than you expected. -
Create a second, deliberate conflict: on two branches, change the same line (not just nearby lines) to different text, then merge. Confirm the conflict markers show both versions of that exact line.
-
Instead of hand-editing, resolve a conflict with
git checkout --theirs -- app.pyto take the incoming branch’s version wholesale, thengit addand commit. -
Run
git log --graph --oneline main feature/add-tests(before merging) to see the two branches’ commits interleaved by graph column, without merging them yet. -
After the merge commit, run
git show bd316aa(using your own merge commit’s hash). Notice it shows a combined diff rather than a plain one — that’s how Git represents a merge commit’s content.