Skip to main content
Git & GitHub · lesson 2 of 12
Git & GitHub beginner 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

git branching merge-conflict

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

  1. Run git status in the middle of an unresolved conflict (before staging the fix). Read what it suggests: git add <file> to mark resolved, or git merge --abort to 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.py

    git merge --abort resets everything back to before the merge started, as if it never ran — useful when a conflict is bigger than you expected.

  2. 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.

  3. Instead of hand-editing, resolve a conflict with git checkout --theirs -- app.py to take the incoming branch’s version wholesale, then git add and commit.

  4. 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.

  5. 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.

Frequently Asked Questions

Why did the first merge in this lesson not create a merge commit?
Because main hadn't moved since the feature branch was created, Git could just slide main's pointer forward to the feature branch's tip — a fast-forward. No new commit is needed because there's nothing to reconcile.
Why did the second merge conflict but the first one didn't?
The second merge combined two branches that both edited the end of the same file after diverging from a common ancestor. Git can't guess which addition should come first, so it stops and asks a human to decide — that's what CONFLICT (content) means.
What do the <<<<<<<, =======, and >>>>>>> markers mean?
They delimit the two conflicting versions inside the file itself: everything between <<<<<<< HEAD and ======= is your current branch's version, and everything between ======= and >>>>>>> <branch> is the incoming branch's version. Editing the file to the final desired content and removing the markers is how you resolve it.
Do I have to use the markers, or can I pick a side entirely?
You can take one side wholesale with git checkout --ours -- app.py or --theirs, or edit by hand to combine both, which is what this lesson does. Either way you still git add the resolved file and run git commit to finish the merge.