Branches, merges and the commit graph
Branching, merging and rebasing explained as what they are — operations on a directed graph of snapshots. With that picture, choosing between merge and rebase becomes obvious.
Chapter one established that a commit is a snapshot with pointers to its parents. That makes your history a directed acyclic graph, and every branching operation is a manipulation of it.
The graph
Start with a linear history. Each letter is a commit; arrows point to parents.
A ── B ── C ← main
Create a branch. Nothing is copied; a new pointer is written:
A ── B ── C ← main, feature
Commit twice on feature:
A ── B ── C ← main
╲
D ── E ← feature
Meanwhile someone commits to main:
A ── B ── C ── F ← main
╲
D ── E ← feature
The branches have diverged. Everything that follows is about how to bring them back together.
Fast-forward: no merge at all
If main has not moved, merging feature into it is just moving a pointer:
before: A ── B ── C ← main
╲
D ── E ← feature
after: A ── B ── C ── D ── E ← main, feature
git switch main
git merge feature # Fast-forward
No merge commit, no new history. Git prints Fast-forward and you are done.
Merge: preserve what happened
When both have moved, git merge creates a commit with two parents:
A ── B ── C ── F ────── M ← main
╲ ╱
D ── E ───╯ ← feature
git switch main
git merge feature
M records the fact that two lines of development existed and were joined. The history is
accurate but not linear.
Rebase: rewrite for a clean line
git rebase takes your commits and re-creates them on a new base:
before: A ── B ── C ── F ← main
╲
D ── E ← feature
after: A ── B ── C ── F ← main
╲
D' ── E' ← feature
git switch feature
git rebase main
Then merging is a fast-forward, and the history is a straight line.
Choosing between them
The rule that works in practice:
Beyond that it is a team preference:
| Merge | Rebase | |
|---|---|---|
| History | Accurate, shows real topology | Linear, easier to read |
git log |
Interleaved, branchy | Sequential |
git bisect |
Harder — merge commits complicate it | Easier |
| Conflicts | Resolved once | Possibly once per commit |
| Shared branches | Safe | Dangerous |
A common, sensible policy: rebase your feature branch onto main before opening a PR (clean, linear, easy to review), then merge the PR (the merge commit records when it landed).
Resolving a conflict
A conflict happens when both sides changed the same region. Git marks it and stops:
<<<<<<< HEAD
const timeout = 5000;
=======
const timeout = 30000;
>>>>>>> feature
HEAD is where you are; below the ======= is what is coming in. Edit the file into the state
you actually want — deleting all three markers — then:
git add src/config.js
git merge --continue # or: git rebase --continue
To abandon and get back to where you started:
git merge --abort
git rebase --abort
Remote branches are a local cache
origin/main is not the branch on the server. It is your local record of where main was on
the server the last time you communicated with it.
git fetch # update origin/* — changes nothing in your working tree
git status # "Your branch is behind origin/main by 3 commits"
git merge origin/main # now actually integrate
git pull is git fetch followed by git merge (or git rebase, with the config from chapter
two). Splitting them is often better: fetch, look at what arrived, then decide.
Interactive rebase: editing your own history
Before sharing a branch, clean it up:
git rebase -i main
You get an editable list:
pick a1b2c3d Add user model
pick e4f5g6h fix typo
pick i7j8k9l WIP
pick m0n1o2p Actually implement login
Change the verbs:
pick— keep as isreword— change the messagesquash— fold into the previous commit, combining messagesfixup— fold into the previous commit, discard this messagedrop— delete the commitedit— stop here so you can amend it
Turning four messy commits into one clean one makes the PR reviewable and the history bisectable. This is the single highest-value Git skill beyond the basics.
Next: the commands this all turns into, day to day.