How does git branching and merging actually work?
Every programmer eventually hits the same moment of confusion: you make a branch, someone else makes a branch, and somehow git manages to combine your changes into one coherent history without either of you touching the other's code. It looks like magic. It isn't — it's a simple idea about snapshots and common ancestors, applied very consistently.
A commit is a snapshot, not a diff
The first misconception to clear up: git doesn't store your history as a chain of changes. Each commit is a full snapshot of your project at that moment (internally it's smarter than copying every file, but conceptually that's the model), plus a pointer back to the commit that came before it. String those pointers together and you get a line running backward through time — your history.
A branch, then, is almost nothing. It's just a movable label pointing at one commit. When you create a branch, git doesn't duplicate any files — it just writes down "this name points here." When you commit on that branch, the label moves forward to the new commit. That's the whole trick: branches are cheap because they're just pointers, not copies of your codebase.
Branching: two labels, one shared past
Say you're on main at commit C, and you create a branch called feature.
Both labels now point at C. You start committing on feature, and its label
walks forward — D, then E — while main stays put at C. Nothing about main
has changed; you've simply been laying down new snapshots that only the
feature label can see.
Meanwhile, someone else keeps committing on main itself, moving its label
forward too — say to F. Now you have two branches that agree on their shared
history up through C, and then diverge: one path goes C → D → E, the other
goes C → F. That point where they last agreed, C, is called the common
ancestor, and it's the key to everything that happens next.
Merging: three snapshots, one comparison
To merge feature back into main, git doesn't need to understand your code
or replay every commit one by one. It only needs three snapshots: the common
ancestor (C), the tip of main (F), and the tip of feature (E). For every
file, it asks the same question: what changed between C and F, and what
changed between C and E? If only one side touched a given file, git takes
that side's version. If both sides changed the same lines, that's a
conflict, and it asks you to pick the outcome by hand. Everything else
resolves automatically.
The result is a new commit with two parents — one pointing back to F, one
back to E — which is how the graph shows a merge as two lines flowing into
one. main's label moves to this new commit, and from that point on, the
history contains both sets of work.
Fast-forwards: when there's nothing to merge
There's a special case worth knowing about. If main hadn't moved at all
while you were working — it's still sitting at C — then merging feature in
doesn't require combining anything. Git can just slide the main label
forward to E, since E already contains everything C had plus your new work.
This is called a fast-forward, and it's why some merges show up in your
history as a clean straight line instead of a diamond shape with two parents.
Why conflicts happen (and why they're not really "git's fault")
A conflict isn't a sign that something went wrong — it's git being honest
about a situation it can't resolve safely. If you changed line 12 of a file
on feature and someone else changed the same line 12 on main, there's
no way to combine both edits without a human deciding what the line should
actually say. Git flags exactly that spot, leaves both versions visible in
the file, and waits. Everywhere else in the file that only one side touched,
it has already merged for you without a word.
The takeaway
Branches are just labels on commits, commits are snapshots linked to their
parents, and merging is a three-way comparison against the last point two
branches agreed on. Once you see the graph this way — labels sliding along a
web of snapshots — the commands stop feeling like incantations and start
feeling like exactly what they say: branch makes a label, commit moves it
forward, merge reconciles two labels' paths back to where they split.
This explainer was made with LineLapse — type a topic, get a hand-drawn video. Make your own →