Git · 2 of 11
Two branches merge instantly nine times out of ten, then one day Git stops and asks you to resolve a conflict by hand. Understanding what a branch actually is — a movable pointer, not a copy of the project — explains both cases from the same underlying mechanism.

A teammate once asked me why merging their branch into main finished instantly with no message, while merging mine a day later stopped with a wall of <<<<<<< markers across three files.
Same command, same repository, wildly different outcome.
The answer isn't luck — it's entirely determined by whether the two branches' histories diverged. Once you can see that picture in your head, you can usually predict which kind of merge you're about to get before you even run the command.
The biggest misconception people carry over from other tools: a branch is not a copy of your project. Creating a branch does not duplicate any files.
git branch feature-login
This creates a new label — feature-login — pointing at whatever commit HEAD currently sits on. That's the entire operation. It's a 41-byte file containing a commit hash. Both main and feature-login now point at the exact same commit. HEAD is still on main — you haven't switched yet.
HEAD itself is just another pointer, normally pointing at whatever branch you're on. Switching branches:
git checkout feature-login
# or the cleaner modern form:
git switch feature-login
Committing on feature-login moves that branch's pointer forward to the new commit. main stays where it was — still pointing at the commit where you branched from. The two labels now point at different places in the commit graph.
If main hasn't moved since feature-login branched off it, merging is trivial: main's pointer just moves forward to match feature-login's.
No new commit is created. No conflict is possible. Because there was never a second line of history to reconcile — just a straight line with a branch label sitting partway along it.
git switch main
git merge feature-login
# Fast-forward
Git just moves main's pointer forward to match feature-login. Both labels now point at the same commit. Nothing is being "combined" — main is just catching up.
If main also had new commits after feature-login branched off it (someone else merged something — the completely normal case on any active team), a fast-forward is impossible.
Git does this with a three-way merge, using three snapshots:
For each file, Git compares what changed between the ancestor and each side:
git switch main
git merge feature-login
If there's no overlap, Git creates a merge commit with two parents — one pointing at main's tip and the other at feature-login's. That's how Git records that two separate lines of work were combined at this point.
If both sides touched the same lines, Git stops and leaves inline markers:
const MAX_RETRIES = 5;
<<<<<<< HEAD and ======= → your current branch's version======= and >>>>>>> feature-login → what's coming inYou edit the file to whatever the correct final version is — one side, the other, or a blend — delete the marker lines entirely, then:
git add config.js # tells Git "this conflict is resolved"
git commit # reuses the merge message Git already prepared
<<<<<<< line in the file and commit it, that's now in your code. Run git diff --staged before committing to double-check.If a merge goes sideways and you want out entirely:
git merge --abort
This resets everything back to exactly how it was before you ran git merge. I've used this more than once — stepping back, reading what actually changed on each side, and re-merging with clearer eyes always goes better than pushing through a resolution you're not confident about.
git rebase solves the same problem as a three-way merge but with a different result. Instead of creating a merge commit, rebase takes your branch's commits, sets them aside, and replays them one by one on top of the other branch's current tip — as if you'd branched off now instead of whenever you actually did.
git switch feature-login
git rebase main
Git takes your feature commits, detaches them, and replays each one on top of where main is right now — as if you'd just branched from main's current tip. The result is clean, linear history with no merge commits.
The cost: your branch's commits get new hashes. This is a rewrite. Fine on a branch only you're working on. A genuine problem on a branch anyone else has already pulled — their local history no longer matches yours, and reconciling that is worse than the conflict you were trying to avoid.
Rule: Rebase freely before you share your branch. Merge (don't rebase) after it's been pulled by others.
Sometimes you don't want to merge an entire branch — just one specific commit. Usually a bug fix that needs to land on a release branch without pulling in unrelated work-in-progress.
git cherry-pick a1b2c3d
Replays the changes from commit a1b2c3d onto your current branch as a brand-new commit. Can conflict exactly like a merge can — same conflict-marker workflow applies.
Once a feature branch is merged, it's just a pointer sitting at a commit that's already part of main's history:
git branch -d feature-login
-d refuses to delete a branch with unmerged commits (a safety check). -D force-deletes regardless — useful when you deliberately abandoned a branch, dangerous if you didn't mean to (though the commits are usually recoverable via git reflog, covered later).
The fast-forward vs. three-way distinction is the one mental model that makes merge behavior predictable. Before merging anything, ask yourself: has main moved since this branch was created? If no → fast-forward. If yes → three-way merge, possibly with conflicts.
Rebase came up here as an alternative, but its more common real-world use is cleaning up your own commits before sharing them — squashing WIP commits into one meaningful commit, fixing typos in commit messages. The next chapter covers interactive rebase in depth.
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.