Git 02 — Branching, Merging & Rebasing
You’re three days into a feature when a production bug needs a fix now. Without branches you’d be stuck — your half-done work tangled with the urgent fix. With branches, you stash the feature on its own line, fix the bug on main, and come back. This module is where git stops being a save button and becomes the thing that lets a whole team work on the same code without stepping on each other.
The Goal
By the end of this module you can:
- Explain why creating a branch is nearly free — it’s one new pointer, not a copy of your files
- Tell a fast-forward merge from a 3-way merge by looking at the graph, and predict which one git will do
- Read a merge commit and name its two parents
- Describe rebase as “replay my commits onto a new base” and say exactly why the SHAs change
- Recite the golden rule of rebase and explain the wreckage that follows breaking it
- Choose merge or rebase for a real situation and defend the choice
The Lesson
A branch is a cheap movable label
From the mental model module you know the key fact: a branch is not a folder, not a copy, not a heavy thing. A branch is a 41-byte file containing the SHA of one commit. That’s it. main is a label that points at a commit; feature is another label pointing at a (possibly different) commit. The special label HEAD points at the branch you’re currently on.
When you run git branch feature, git writes one new pointer at the commit you’re standing on. No files are copied. This is why git branching is instant even on a repo with a million files — Subversion and the older tools copied directories and people learned to fear branching. In git, a branch is the cheapest thing there is.
flowchart LR
A[A] --> B[B]
B --> C[C]
main[main label] -.points at.-> C
feature[feature label] -.points at.-> C
HEAD[HEAD] -.on branch.-> main
Right after git branch feature, both labels point at the same commit C. Nothing has diverged yet. Commit once on feature and only the feature label moves forward — main stays put. The label you’re on (where HEAD points) is the one that advances when you commit.
Diverging — the situation that makes merge interesting
Real work diverges. You commit F1, F2 on your feature branch. Meanwhile a teammate commits C on main. Now the two labels point at different commits, and both descend from a shared ancestor B:
flowchart LR
A[A] --> B[B]
B --> F1[F1]
F1 --> F2[F2]
B --> C[C]
main[main] -.-> C
feature[feature] -.-> F2
This is divergence: two lines of work that share history up to B, then split. Bringing them back together is what merge and rebase both do — they just do it differently, and the difference shows up in the graph forever.
Fast-forward merge — when there’s nothing to combine
The simplest case first. Suppose main didn’t move — no teammate commit C, main is still at B. You finished F1, F2 on feature. You merge feature into main:
git checkout main
git merge feature
Git looks and sees: main (at B) is a direct ancestor of feature (at F2). There’s no divergence — feature is simply ahead. So git doesn’t create anything. It just slides the main label forward to F2. This is a fast-forward: no merge commit, no new history, the label fast-forwards along the existing line.
flowchart LR
A[A] --> B[B]
B --> F1[F1]
F1 --> F2[F2]
main[main moved to F2] -.-> F2
feature[feature] -.-> F2
A fast-forward is possible only when the target branch hasn’t moved since you branched. The result is a perfectly straight line — you can’t tell from the graph that a branch ever existed. That’s sometimes good (clean history) and sometimes bad (you lose the record that these commits were a unit). Teams that want the record force a merge commit with git merge --no-ff.
3-way merge — when both sides moved
Now the real case: main moved to C and your feature has F1, F2. There is no way to fast-forward — main isn’t an ancestor of feature anymore; they genuinely diverged. Git does a 3-way merge, named for the three commits it looks at:
| The three | Role |
|---|---|
The shared ancestor B | The common starting point (the “merge base”) |
main’s tip C | What changed on one side |
feature’s tip F2 | What changed on the other side |
Git compares both sides against the common ancestor, combines the non-overlapping changes automatically, and records the result as a brand-new commit with two parents — a merge commit:
flowchart LR
A[A] --> B[B]
B --> F1[F1]
F1 --> F2[F2]
B --> C[C]
F2 --> M[M merge commit]
C --> M
main[main] -.-> M
The merge commit M is the only kind of commit with two parents. It says, honestly and permanently: two lines of work happened in parallel and here is where they rejoined. Nothing gets rewritten — F1, F2, and C all keep their original identity and SHA. The graph forks and rejoins, recording reality.
When the two sides changed the same lines, git can’t guess which to keep and stops with a merge conflict — it hands you the file with both versions marked and waits for your decision. That’s not an error; it’s git refusing to silently lose work. You’ve already got the full playbook for this in the real-error doc needs-merge-resolve-index — read it now if you haven’t, because conflict resolution is where most beginners panic.
Rebase — replay your commits onto a new base
Rebase solves the same problem (your feature diverged from main) with a completely different philosophy. Instead of tying the two histories together with a merge commit, rebase moves your branch’s starting point to the tip of main and replays your commits there, one by one, as if you had branched from the new tip all along.
git checkout feature
git rebase main
What git actually does, step by step:
- Set your feature commits (
F1,F2) aside as a list of changes. - Move the
featurelabel’s base tomain’s tip,C. - Replay
F1ontoC— producingF1', a new commit with the same change but a new parent (C) and therefore a new SHA. - Replay
F2ontoF1'— producingF2'.
flowchart LR
A[A] --> B[B]
B --> C[C]
C --> F1p[F1 prime]
F1p --> F2p[F2 prime]
main[main] -.-> C
feature[feature] -.-> F2p
The result is a straight line: A-B-C-F1'-F2'. No fork, no merge commit, as if your work happened after C all along. The history reads like a clean story instead of a tangle.
But look closely at what happened: F1' and F2' are not F1 and F2. They’re new commits with new SHAs. The originals are abandoned (git keeps them in the reflog for a while, but they’re off the branch). Rebase rewrites history. That single fact is the source of both its beauty and its one deadly rule.
The golden rule — never rebase shared/pushed history
Never rebase commits that you have already pushed or that anyone else might have.
Here’s the wreckage if you break it. You push feature with commits F1, F2. A teammate pulls and builds on top of them. Now you rebase feature — F1, F2 become F1', F2' with new SHAs, and you force-push. Your teammate’s repo still has the original F1, F2 plus their own work on top. When they pull, git sees two completely different histories that look like they diverged, and they get a horrible tangle of duplicated commits and conflicts. You’ve rewritten history out from under someone standing on it.
The rule in practice is simple:
| Situation | Safe to rebase? |
|---|---|
| Local commits you haven’t pushed | Yes — clean them up freely |
| Your own feature branch, not yet shared | Yes |
| A branch you pushed but nobody else uses | Risky — only if you’re certain you’re alone, and you’ll force-push |
main, or any branch teammates pull | Never. Rewriting shared history breaks everyone |
Rebase is for tidying your own private history before you share it. Once it’s shared, it belongs to the team, and you only ever add to it (merge), never rewrite it.
Merge vs rebase — the honest comparison
There is no universally correct answer, and anyone who tells you “always rebase” or “always merge” is selling a religion. Here’s the real trade:
| Merge | Rebase | |
|---|---|---|
| History shape | Forked graph with merge commits — true and sometimes messy | Straight line — clean and readable |
| Rewrites commits? | No, originals preserved | Yes, new SHAs |
| Safe on shared branches? | Yes, always | No, never |
| Records that a branch existed | Yes (the merge commit) | No (looks like one line) |
| Conflict resolution | Once, in the merge commit | Possibly once per replayed commit |
| Good for | Integrating shared branches; preserving real history | Cleaning up your local work before pushing; keeping a tidy feature branch updated with main |
A common sane workflow that uses both: rebase your local feature branch onto the latest main while you work (so your branch stays current and conflicts are small), then merge it into main with a merge commit when it’s done (so the integration is recorded and you never rewrite shared history). Rebase to keep your private branch clean; merge to join it to the shared trunk.
The conflict-per-commit point bites in practice: a merge resolves all conflicts in one shot at the merge commit. A rebase replays each commit separately, so if F1 and F2 both touch the conflicting lines, you may resolve the same conflict twice. For a long-diverged branch, a merge is often less painful.
See It Move
The same diverged starting point, reunited two ways. Use the merge / rebase buttons at the top of the visualizer to toggle modes — run it through once in merge, then again in rebase, and watch how the graph ends up.
Step through both modes and notice:
- In merge mode, the final commit
Mhas two arrows coming into it — that’s the two-parent merge commit, andF1,F2,Call keep their identity. - In rebase mode, the original
F1,F2fade to ghosts andF1',F2'appear on the main line — same changes, new commits, new SHAs. That fading is “history rewritten.” - Merge ends in a fork-and-rejoin; rebase ends in one straight line. That visual difference is the entire merge-vs-rebase debate in one picture.
- The rebase ghosts are exactly why the golden rule exists: if anyone was standing on the original
F1/F2, you just abandoned the commit out from under them.
Check The Concept
How This Shows Up At Work
- The PR that won’t merge cleanly. Your branch is two weeks old and
mainhas moved a hundred commits. You rebase your branch onto the latestmainlocally, resolve the small conflicts, and your PR goes from a tangled mess to a clean diff. Knowing this is the difference between a 5-minute fix and a panicked afternoon. - The “who broke main” review. A teammate force-pushes a rebase to
mainand half the team’s morning is gone re-syncing repos. In the retro, the person who can explain why — shared history was rewritten — is the one who writes the team’s branching policy. - The interview whiteboard. “Explain merge vs rebase” is one of the most common git interview questions for backend roles. Answering with the graph — two-parent merge commit vs replayed new SHAs — and the golden rule puts you ahead of candidates who memorized “rebase is cleaner.”
- The hotfix. Production is down, your feature is half-done. You branch the fix off
main, ship it, merge it, then rebase your feature onto the newmain. Branches are what make this calm instead of chaos.
Build This
All in PowerShell. You’re going to create a branch, diverge it from main, merge it, then make a second branch and rebase it — so you see both joins with your own eyes. Use a throwaway repo so nothing real is at risk.
- Make a fresh repo and a first commit on
main:
mkdir git-branch-lab
cd git-branch-lab
git init
git branch -M main
"line A" | Out-File -Encoding utf8 notes.txt
git add notes.txt
git commit -m "A: first commit"
"line B" | Add-Content notes.txt
git commit -am "B: second commit"
git log --oneline --graph now shows two commits on one line. main points at B.
- Branch off and make it diverge. Create
feature, commit on it, then go back tomainand commit there too:
git switch -c feature
"feature work F1" | Add-Content notes.txt
git commit -am "F1: feature commit"
"feature work F2" | Add-Content notes.txt
git commit -am "F2: feature commit"
git switch main
"main work C" | Add-Content other.txt
git add other.txt
git commit -m "C: main moved"
Now both branches moved since the split. Confirm the fork:
git log --oneline --graph --all
You should see main and feature diverging from commit B — the graph forks. This is the exact picture from the Lesson.
- Merge it (3-way merge). You’re on
main; mergefeaturein:
git merge feature
Because you touched notes.txt on both sides, you’ll get a conflict. Open notes.txt — you’ll see <<<<<<<, =======, >>>>>>> markers. Edit the file so it has both lines and delete all three marker lines, then:
git add notes.txt
git commit --no-edit
Run git log --oneline --graph --all. See the merge commit where the two lines rejoin — and confirm it has two parents:
git show --no-patch --format="%h parents: %p" HEAD
Two SHAs after parents: — that’s your merge commit.
- Now rebase a second branch. Make a new branch off
main, diverge it, but this time replay instead of merge:
git switch -c feature2
"feature2 work G1" | Add-Content other.txt
git commit -am "G1: feature2 commit"
git switch main
"more main work" | Add-Content notes.txt
git commit -am "D: main moved again"
Record feature2’s current SHA so you can see it change:
git switch feature2
git log --oneline -1
git rebase main
git log --oneline -1
The G1 commit’s SHA is different before and after the rebase — same change, new commit, replayed onto the new main tip. Run git log --oneline --graph --all and notice feature2 is now a straight line continuing from main, with no fork.
- Break it on purpose — feel the golden rule (safely). Still local, so no one gets hurt:
git switch feature2
git log --oneline -3
git rebase main # run it again - notice it says "up to date" or replays again
Now imagine feature2 was pushed and a teammate had the old G1 SHA from step 4. Their repo points at a commit that no longer exists on your branch. That’s the tangle the golden rule prevents. (You can confirm the old commit still exists locally via git reflog — your safety net, covered in Git 04.)
Interview Practice
These are the git questions actually asked in backend and fintech interviews at Indian product companies. Say the answer out loud before opening each one.
1. What is the difference between merge and rebase?
Both integrate one branch into another, but differently. Merge creates a new merge commit with two parents that ties the two histories together — nothing is rewritten, the graph forks and rejoins, and it’s always safe on shared branches. Rebase replays your commits one by one onto the tip of the target branch, creating new commits with new SHAs — the result is a clean straight line, but because it rewrites history you must never rebase commits anyone else has. Rule of thumb: rebase your private local branch to keep it clean; merge to integrate into a shared branch.
2. What is a fast-forward merge?
When the branch you’re merging into is a direct ancestor of the branch you’re merging — meaning the target hasn’t moved since you branched — git doesn’t create a merge commit. It just slides the branch label forward to the tip of the other branch. The result is a straight line. A fast-forward is only possible when there’s nothing to actually combine. You can force a merge commit anyway with git merge --no-ff to preserve the record that a branch existed.
3. Why does rebase change commit SHAs?
A commit’s SHA is a hash of its content plus its parent commit (plus author, date, message). Rebase replays each commit onto a new base, so each replayed commit gets a new parent — and a new parent means a different hash, hence a new SHA. The original commits aren’t modified; brand-new ones are created and the originals are abandoned (still recoverable from the reflog for a while). This is the precise reason rebase counts as rewriting history.
4. What is the golden rule of rebasing?
Never rebase commits you’ve already pushed or that anyone else might have. Rewriting shared history changes the SHAs out from under teammates who built on the originals — when they pull, git sees two diverging histories and they get duplicated commits and painful conflicts. Rebase is only for cleaning up your own local, unshared work before you push it.
5. What is a merge commit and how is it special?
A merge commit is the commit git creates to record a merge. It’s the only kind of commit with two parents — one tip from each branch being joined. It carries no changes of its own usually; its job is to tie two lines of history together so the graph honestly shows that parallel work rejoined here. You can see its parents with git show --no-patch --format=%p HEAD — two SHAs means it’s a merge commit.
6. How do you resolve a merge conflict?
A conflict happens when both branches changed the same lines and git can’t decide which to keep. Git marks the file with <<<<<<<, =======, >>>>>>> separating your version from theirs. You open the file, edit it so the final content is correct, and delete all three marker lines. Then git add <file> to mark it resolved and git commit (or git rebase --continue if mid-rebase) to finish. If you want to bail out entirely, git merge --abort restores the pre-merge state. A conflict isn’t an error — it’s git asking you to make a decision it can’t make safely.
7. When would you prefer merge over rebase, and vice versa?
Prefer rebase for your own feature branch while you work — to pull in the latest main and keep your branch a clean, linear story before review. Prefer merge when integrating a shared branch into main, because it’s safe (never rewrites history) and the merge commit records that the feature was a unit. A long-diverged branch is often easier to merge than rebase, because merge resolves all conflicts once whereas rebase may make you resolve the same conflict on each replayed commit.
8. Your feature branch is far behind main. Walk me through getting it current.
First git fetch to update my knowledge of the remote (it changes no files — see the fetch-vs-pull point). Then, since the branch is private and unshared, git rebase origin/main to replay my commits onto the latest main, resolving any conflicts as they come up with git rebase --continue. If the branch were already shared with teammates, I’d git merge origin/main instead to avoid rewriting shared history. Either way I verify with git log --oneline --graph --all before pushing.
Where to Practice
| Resource | What to do there | How long |
|---|---|---|
| learngitbranching.js.org | The single best free tool for this — do the “Introduction Sequence” and the “Ramping Up” branching/merging/rebase levels. You drag commits and watch the graph move | 60 min |
| git-scm.com | Read Pro Git chapter 3.2 “Basic Branching and Merging” and 3.6 “Rebasing” — free, official, with the same graphs | 40 min |
| Your own git-branch-lab repo | Redo the Build This from memory without looking — branch, diverge, merge, then rebase a second branch | 30 min |
Check Yourself
- In one sentence, what is a branch physically, and why is creating one nearly free?
- What’s the exact condition under which git can do a fast-forward merge?
- What makes a merge commit different from every other commit?
- Describe what
git rebase maindoes to your feature commits, step by step. - Why do the SHAs change during a rebase?
- State the golden rule of rebasing and explain the wreckage if you break it.
- Give one situation where you’d choose merge and one where you’d choose rebase.
- A merge stops with conflict markers in a file. What are the steps to finish it?
Answers
- A branch is a tiny file holding the SHA of one commit — a movable pointer. Creating one just writes a new pointer; no files are copied, so it’s instant.
- When the branch you’re merging into is a direct ancestor of the branch you’re merging — i.e. the target hasn’t moved since you branched, so there’s nothing to combine. Git just slides the label forward.
- It has two parents — one from each branch being joined. Every other commit has exactly one parent (or none, for the first commit).
- Git sets your feature commits aside as changes, moves the branch base to
main’s tip, then replays each commit there in order, creating a new commit per original. - A commit’s SHA hashes its content plus its parent. Replaying gives each commit a new parent, so a new hash — a brand-new commit. The originals are abandoned.
- Never rebase commits you’ve pushed or that others may have. If you do, their SHAs change while teammates still hold the originals — on their next pull git sees diverging histories and they get duplicated commits and ugly conflicts.
- Merge: integrating a finished feature into shared
main(safe, records the branch). Rebase: cleaning up / updating your own private feature branch before pushing (linear, but never on shared history). - Open the file, edit so the final content is correct, delete the
<<<<<<<,=======,>>>>>>>marker lines, thengit add <file>andgit committo complete the merge.
Explain it out loud: Explain to an empty chair the difference between merging and rebasing the same diverged branch — draw the two resulting graphs in the air (fork-and-rejoin vs straight line), say why rebase changes the SHAs, and state the golden rule. If you can’t draw both graphs from memory, replay the visualizer in both modes once more.
Why AI Can’t Do This For You
AI can type git rebase main and even resolve a simple conflict. But it cannot see that this branch was already pushed and a teammate is standing on it — so it’ll cheerfully suggest the rebase that wrecks everyone’s morning. The judgment of merge here, rebase there, force-push never depends on facts about your team and your remote that live nowhere in the prompt.
And when a rebase goes wrong and you’re staring at duplicated commits at 11pm, the person who understands the graph — what a SHA is, what got replayed, what the reflog still holds — is the one who recovers it. No prompt rebuilds that mental model; only branching, diverging, and merging it yourself until the graph is something you see does.
Module done? Add it to today’s tracker