Week 2: Branching & Merging

Real work almost never happens in a straight line — you fix a bug while a feature is half-built, or two people touch the same file in parallel. This week covers branches as Git's mechanism for that: how to create and switch between them, how merges bring lines of work back together, and what to do when they collide.

Module 2 of 5 Week 2 of 5 ~2–3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Create, switch between and delete branches confidently
  • Explain the difference between a fast-forward and a three-way merge
  • Resolve a merge conflict by hand, and describe when rebase is the better tool

1. Why Branch?

A branch is just a movable pointer to a commit — creating one is instant and cheap because it doesn't copy any files. main (or historically master) is the conventional default branch; everything else is a parallel line of history that starts from wherever you branched off.

The usual reason to branch: isolate work in progress from the stable, deployable state of main. You build a feature or fix a bug on its own branch, and only bring it into main once it's done and reviewed — that way main stays something you can deploy from at any moment.

2. Creating & Switching Branches

terminal
git branch                       # list local branches, * marks the current one
git branch feature/login         # create a new branch (doesn't switch to it)
git switch feature/login         # switch to it
git switch -c feature/signup     # create AND switch in one step

git branch -d feature/login      # delete a branch (only if merged)
git branch -D feature/login      # force-delete, even if unmerged

git switch is the modern, purpose-built command for changing branches (Git 2.23+). You'll also see git checkout <branch> in older tutorials and codebases — it does the same job, but checkout is an older, overloaded command that also does several unrelated things (like restoring files), which is exactly why switch was split out.

terminal
git switch -c feature/signup
echo "signup form" > signup.html
git add signup.html
git commit -m "Add signup page skeleton"

git switch main
cat signup.html
# cat: signup.html: No such file or directory

That last line is the point: main genuinely doesn't have signup.html yet. Each branch has its own independent working directory state, computed from its own commit history.

3. Fast-Forward vs. Three-Way Merges

git merge <branch>, run from the branch you want to merge into, combines another branch's history into the current one. There are two outcomes:

Fast-forward merge — if main hasn't moved since you branched off it, Git just slides the main pointer forward to your branch's latest commit. No new commit is created:

terminal
git switch main
git merge feature/signup
# Updating 3f9a1c2..7b2e910
# Fast-forward
#  signup.html | 1 +

Three-way merge — if main has moved (someone else merged something), Git looks at three commits — the two branch tips plus their common ancestor — and creates a new merge commit with two parents, combining both histories:

terminal
git switch main
git merge feature/login
# Merge made by the 'ort' strategy.
#  login.html | 1 +
# (a new merge commit is created, with two parent commits)

Both are equally valid — a fast-forward simply means there was nothing to reconcile. If you always want an explicit merge commit for traceability even when a fast-forward is possible, use git merge --no-ff.

4. Resolving Merge Conflicts

A conflict happens when both branches changed the same lines of the same file. Git can't guess which version you want, so it pauses the merge and marks the file for you to resolve by hand:

terminal
git merge feature/pricing
# Auto-merging pricing.html
# CONFLICT (content): Merge conflict in pricing.html
# Automatic merge failed; fix conflicts and then commit the result.
pricing.html
<h1>Pricing</h1>
<<<<<<< HEAD
<p>Starting at $9/month</p>
=======
<p>Starting at $12/month, billed annually</p>
>>>>>>> feature/pricing

<<<<<<< HEAD down to ======= is your current branch's version; ======= down to >>>>>>> feature/pricing is the incoming branch's version. Edit the file down to what it should say — deleting the marker lines entirely — then stage and commit:

terminal
# after manually editing pricing.html to the final text:
git add pricing.html
git commit -m "Merge feature/pricing, resolve pricing copy conflict"
Stuck mid-merge? Bail out safely

git merge --abort puts the repo back exactly how it was before you ran git merge, as if it never happened. Use it any time a conflict resolution goes sideways — there's no penalty for starting over.

5. Rebase Basics: Rebase vs. Merge

git rebase main, run from a feature branch, replays that branch's commits one by one on top of main's latest state — instead of creating a merge commit, it rewrites the feature branch's history so it looks like it was built starting from where main is now:

terminal
git switch feature/signup
git rebase main
# Successfully rebased and updated refs/heads/feature/signup.

The result: a clean, linear history with no merge commits — often preferred for feature branches before they're merged, because it reads like the work happened in a straight line. The trade-off is that rebase rewrites commit hashes, so it's only safe on branches nobody else has already pulled and built on top of. Merge is always safe on shared branches; rebase is a local cleanup tool. Week 4 covers rewriting history in more depth.

6. Hands-on Exercise

Hands-on

Create a deliberate conflict, then resolve it

The fastest way to stop fearing merge conflicts is to cause one on purpose and walk through fixing it.

Requirements:

  1. In a repo with one committed file about.md, create branch a and change its first line; commit.
  2. Switch back to main, create branch b from it, change the same first line to something different; commit.
  3. Merge a into main — this should fast-forward or merge cleanly.
  4. Merge b into main — this should now conflict on that line.
  5. Resolve the conflict by hand, choosing or combining the wording, then stage and commit the merge.
  6. Run git log --oneline --graph --all and identify which commit is the merge commit (it has two parent lines in the graph).
Hint

If you get lost mid-conflict, git merge --abort resets everything and lets you try again — there's no wrong way to practice this.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

When does Git perform a fast-forward merge instead of creating a merge commit?

When the branch being merged into (e.g. main) hasn't had any new commits since the other branch diverged from it. There's nothing to reconcile, so Git just moves the branch pointer forward to match — no new commit is created, unless --no-ff is passed to force one.

Q2

What exactly triggers a merge conflict?

Both branches changing the same lines of the same file (or one deleting a file the other edited). Changes to different files, or even different lines of the same file, merge automatically without a conflict — Git only stops and asks when it genuinely can't tell which version you want.

Q3

What's the key risk of running git rebase on a branch other people have already pulled?

Rebase rewrites commit hashes as it replays them. Anyone who already has the old commits will have a history that's diverged from yours, causing confusing conflicts and duplicate commits the next time they sync. Rule of thumb: rebase branches only you are working on; merge for anything shared.

Q4

How do you back out of a merge that's mid-conflict and start over?

git merge --abort — it restores the repository to exactly the state it was in before the merge started, discarding any partial conflict resolution. There's no risk in aborting and retrying; nothing is lost from history.