Skip to content

Branching, Merging & Merge vs. Rebase

Branches only earn their keep when you bring the work back together. There are two ways to combine history — merge and rebase — and picking the wrong one for the situation is one of the most common sources of Git confusion on a team.

TL;DR — in 30 seconds:

  • Merging creates a new merge commit that joins two branches' history together — nothing is rewritten, the branch's original commits stay exactly as they were.
  • Rebasing replays your branch's commits one by one on top of another branch, producing new commit hashes — history becomes linear, but the original commits are rewritten.
  • A conflict happens when the same lines changed on both sides; Git stops and asks you to resolve it by hand — it never guesses.

1. Merging: combine, don't rewrite

git merge feature (while on main) finds the common ancestor of main and feature, then combines the changes made on each side since that point. If both branches changed different things, Git combines them automatically and creates a merge commit — a commit with two parents — recording that the join happened.

flowchart LR
    C1["c1"] --> C2["c2 (common ancestor)"]
    C2 --> M1["main: c3"]
    C2 --> F1["feature: c4"]
    M1 --> Merge["merge commit c5<br/>(two parents)"]
    F1 --> Merge

Merging never rewrites existing commits — feature's commits keep their original hashes, and the merge commit is simply added on top. This makes merge safe on shared/public branches: anyone else who already has those commits is unaffected.

2. Rebasing: replay, don't join

git rebase main (while on feature) takes feature's commits, temporarily sets them aside, moves feature to point at the tip of main, then replays each of feature's commits on top, one at a time. The result looks like feature was built starting from main's latest commit all along — a straight line, no merge commit.

flowchart LR
    subgraph Before
        C1["c1"] --> C2["c2"] --> M1["main: c3"]
        C2 --> F1["feature: c4 (old hash)"]
    end
    subgraph After["after: git rebase main"]
        C1b["c1"] --> C2b["c2"] --> M1b["main: c3"] --> F1b["feature: c4' (new hash)"]
    end

Rebasing rewrites history — every replayed commit gets a brand-new hash, even if its content is identical. That's the core trade-off: a cleaner, linear log, at the cost of the original commits no longer existing under their old identity.

3. Merge vs. rebase — which one, when

Merge Rebase
History shape Preserves branch structure + a merge commit Linear — looks like it was built straight on top
Rewrites commits? No Yes — new hashes for every replayed commit
Safe on a branch others have pulled? Yes No — rewriting shared history breaks everyone else's copy
Typical use Bringing a finished feature branch back into main Updating your own, not-yet-shared branch with main's latest changes before opening a PR

The rule that avoids almost every rebase disaster: never rebase a branch other people have already pulled from. Rebasing your own local, unpushed (or not-yet-reviewed) branch to catch up with main is routine and safe; rebasing a branch that's already the base of someone else's work rewrites commits out from under them.

4. Resolving a conflict

A conflict happens when the same lines of the same file changed differently on both sides being combined (merge or rebase) — Git can't decide which version is "right," so it stops and asks you to. The file is left with conflict markers:

<<<<<<< HEAD
your version of the line
=======
their version of the line
>>>>>>> feature

To resolve it:

  1. Open the file and decide what the line(s) should actually say — keep one side, the other, or a combination; then delete the <<<<<<<, =======, >>>>>>> marker lines themselves.
  2. git add <file> — tells Git this file's conflict is resolved.
  3. Merge: git commit (Git pre-fills a merge-commit message). Rebase: git rebase --continue (it moves on to replay the next commit; a rebase can pause for a conflict more than once, once per commit that touches the conflicting lines).
  4. If you get lost mid-resolution: git merge --abort or git rebase --abort cleanly backs out and returns you to exactly where you started, no partial state left behind.

Common gotchas

  • Rebasing a branch other people have already pulled. Fix: check first — if anyone else might have that branch, merge instead; only rebase branches that are still yours alone.
  • Panicking mid-conflict and leaving marker lines (<<<<<<<) in the committed file. Fix: always re-read the resolved file before git add-ing it — a leftover marker is usually a syntax error waiting to be discovered later.
  • Assuming a conflict means you did something wrong. Fix: a conflict just means two changes overlapped — it's Git asking a question it genuinely can't answer alone, not an error you caused.
  • Force-pushing after a rebase without checking who else has the branch. Fix: git push --force (or the safer --force-with-lease) is required after a rebase because the history changed — but only do it on a branch that's yours alone, per the rule above.
Check yourself — a teammate already pulled your feature branch and started their own commits on it. Should you rebase it onto the latest main?

No — rebasing rewrites your branch's commit hashes, and your teammate's branch still points at the old ones. Their next pull would see diverged, seemingly unrelated history. Merge main into your branch instead — merging doesn't rewrite anything, so their copy stays compatible.

Check yourself — mid-rebase, you realize you picked the wrong resolution for a conflict. What's the safe way out?

git rebase --abort — it cleanly returns the branch to exactly its pre-rebase state, as if the rebase never started, so you can re-approach it (or resolve the conflict correctly on the next attempt) with no partial state left behind.

5. How this connects

  • This course's previous page, 01-the-git-model.md — merge and rebase are both just different ways of moving branch pointers and combining commit snapshots; the mechanics here only make sense once that model is solid.
  • This course's next page, 03-remotes-and-the-github-flow.md — the GitHub flow's "keep your feature branch up to date with main" step is exactly where the merge-vs-rebase choice from this page gets applied in practice, before opening a PR.
  • Module 00 (Setup & ways of working) — that module's "never commit on main" rite is the simplest possible branch; this page is what happens once two branches need to come back together.

You can defend this when you can explain, in your own words, why rebase is unsafe on a shared branch but merge isn't, and can resolve a conflict without leaving a marker line behind.