CHAPTER 08

Merging & conflicts

Two weeks apart, two people touched the exact same line of CSS. Git can't guess which of you is right — and it knows better than to try.

Two weeks later, the redesign is ready

Rachit's redesign branch is done — properly done, reviewed by Anvesha, looks great. Time to bring it into main. Except main hasn't been sitting still for those two weeks. Three small fixes landed on it while he was heads-down: the event date fix from Chapter 7, a broken-link fix, and — this is the one that matters — Anvesha adjusted the announcement bar's background color last Tuesday, to make the August-event banner stand out more.

Rachit's redesign branch also touched the announcement bar. Restyled it completely, as part of the new color scheme, on the same lines Anvesha edited on main. Neither of them knew, because they were on different branches the whole time. This is the first moment where "combine two branches" stops being automatic.

The easy 90% first: fast-forward

Go back to Chapter 7's date fix for a second — remember how merging fix/event-date back into main was just one command, instantly? That's because at that moment, main hadn't moved at all since the fix branch was created. Git didn't need to combine anything — it just slid the main label forward to point at the same commit fix/event-date already had. That's called a fast-forward, and it's the case every merge secretly hopes to be.

The 3-way merge: both sides moved, but not on the same lines

redesign/homepage is a different situation — main gained three new commits after the branch split off. Git needs to figure out how to combine two lines of history that both moved. For the broken-link fix and the date fix, this is still easy: those commits touched files the redesign never went near. Git merges them automatically and creates one new commit — a merge commit — whose parents are both branch tips at once:

THE SHAPE OF A CLEAN 3-WAY MERGE
                           D — E  (redesign commits)─────┐
                          /              \
A ── B ── C ─────── F ── G  (3 small fixes)── M   ← main

M has two parents: E (the tip of the redesign) and G (the tip of main). Both histories are preserved, side by side, forever.

But the announcement bar is different — it's the one file both lines of work actually reached into.

The one file Git won't guess on

bringing the redesign in
$ git switch main
$ git merge redesign/homepage
Auto-merging src/styles.css
CONFLICT (content): Merge conflict in src/styles.css
Automatic merge failed; fix conflicts and then commit the result.

This isn't Git failing — it's Git correctly refusing to guess. Both changes are legitimate; both were made on purpose; they genuinely disagree. Open the file, and Git has marked exactly where:

.announcement-bar {
<<<<<<< HEAD
  background: #f3e6c8;   /* Anvesha's Tuesday fix, on main */
  color: #211d16;
=======
  background: linear-gradient(90deg, #b8231f, #7d1815);  /* the redesign */
  color: #fffdf6;
>>>>>>> redesign/homepage
}

Everything between <<<<<<< HEAD and ======= is what main has right now. Everything between ======= and >>>>>>> is what's arriving from the redesign. Neither is "correct" as far as Git's concerned — the decision needs a human who actually understands the intent behind both changes.

You message Anvesha and Rachit. Two minutes later: keep the redesign's gradient, since that's the whole point of the new look, but her Tuesday insight — that the banner needed to stand out more — is worth honoring too, so the gradient gets slightly brightened. You edit the file to reflect that decision and delete the marker lines entirely:

.announcement-bar {
  background: linear-gradient(90deg, #d1332e, #9c1f1a);
  color: #fffdf6;
}
telling Git you're done deciding
$ git add src/styles.css # marks this file resolved
$ git status # confirm nothing else is still conflicted
$ git commit # completes the merge; Git pre-fills the message

If, halfway through, the resolution had turned out to be more complicated than a two-minute group-chat decision — say the redesign secretly depended on something the Tuesday fix broke — git merge --abort throws away the entire in-progress merge and puts everything back exactly as it was before you typed git merge. No harm in trying and backing out.

Most conflicts are avoidable, not inevitable

This particular conflict happened because redesign/homepage lived for two full weeks without ever checking back in with main. If Rachit had merged main into his branch every few days along the way, he'd have hit this exact same disagreement with Anvesha's change — just early, small, and easy, instead of arriving all at once at the end alongside everything else.

THE HABIT THAT PREVENTS MOST OF THIS
On a branch that's going to live more than a day or two, periodically run git merge main (or git fetch && git merge origin/main once Chapter 11 introduces remotes) into your branch. Small, frequent conflicts resolved as they happen are dramatically easier than one large conflict resolved at the very end.

Two ways to shape the merge itself

Chapter 14 covers exactly when Accelerate uses which — it maps directly onto whether a PR was written by one person or several.

CHECK
Halfway through resolving the announcement-bar conflict, you realize you don't actually understand Anvesha's original intent well enough to merge it properly. What's the right move?
merge --abort is built exactly for this — it cleanly restores the pre-merge state so you can go get the information you're missing and try again with a clear head, instead of guessing or leaving broken markers in shipped code.

The merge lands. The redesign is live. But something about how you resolved it is bothering you slightly — Rachit's original two weeks of work now show up in history as one tangled merge commit, mixed in with three unrelated fixes, instead of a clean, readable line of "here's what the redesign did, step by step." There's a way to tidy that up before it ever reaches this point next time — and it involves rewriting commits, which comes with its own very specific danger.