Module 6 — Merging (git merge, merge conflicts)
Updated 8 September 2026
🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)
Part A — The shape of a merge
A1. Three commits define every merge
git merge <branch> means: bring that branch's work into the branch I am on. To do it, Git locates three snapshots: ours — the tip of the current branch; theirs — the tip of the branch being merged; and the merge base — the common ancestor you spotted in Module 5's graph, the last commit both sides share. git merge-base A B prints it any time. The base is what makes merging computation rather than guesswork: comparing each side against the base tells Git exactly what each branch changed — and "what changed on each side since we parted" is precisely the information needed to combine them.
One vocabulary note before the exercises: merging happens into the branch you are on. git merge sourdough while on main changes main only — sourdough is read, not written. Getting this direction backwards is the most common merge mistake in real teams.
A2. Fast-forward: the merge that isn't one
Special case first. If the branch being merged is strictly ahead — your current tip is the merge base, because nothing was committed on your side since the fork — then there is nothing to combine. The other branch's history already contains yours. Git just slides your branch pointer forward to their tip: a fast-forward. No new commit, no combination, a pure Module 3 pointer move. This is exactly the main/sourdough shape before Module 5's rye commit — and it is the everyday case when you finish a small feature branch nobody else raced against.
🧪 Exercise 6.1 — a fast-forward, end to end
cd ~/git-course/kitchen # on main
git switch -c glaze # branch, add one commit...
echo "brush with milk for shine" > glaze.txt
git add glaze.txt && git commit -m "Add milk glaze note"
git switch main # ...and merge it back immediately
git merge glaze
git branch -d glaze # merged: -d is now happy (Module 5's seatbelt)✅ Expected result — click to reveal
Updating 8b89f2f..a48960b
Fast-forward
glaze.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 glaze.txt
Deleted branch glaze (was a48960b).What to read out of it: the word Fast-forward is the whole story — no Merge made by..., no new commit. Updating 8b89f2f..a48960b names the pointer move: main slid from its old tip to glaze's tip (your hashes differ). Run git log --oneline -2 and see there is no merge commit — history stays a straight line, as if the work had been done on main all along. Note also branch -d succeeded without protest: glaze's commit is now reachable from main, so deleting the pointer strands nothing.
Two cars leave a junction. If your car never moved, "merging" with the car ahead just means driving forward along the road it already traveled — you fast-forward over its route; no maneuvering needed, because there is only one route. But if both cars drove different roads, rejoining requires a real junction: somebody has to interleave the two paths into one, and that junction is a real place on the map afterwards (the merge commit, Part B).
Where the analogy stops working. Cars merge in real time and the maneuver leaves no trace. Git's junction is recorded: the merge commit permanently holds pointers to both roads, so anyone later can see exactly which two lines of work met, when, and what each contributed. History is not just reunited — the reunion itself becomes history. That permanence is why teams sometimes force a junction to exist even when fast-forwarding was possible (--no-ff, Part D).
🎯 Interview questions — Part A
🎯 "What is a fast-forward merge?" — asked verbatim in Greenroom's Git Interview Questions, Jun 2026
A fast-forward happens when the target branch has no commits of its own since the fork — its tip is the merge base. There is nothing to combine, so Git simply moves the current branch's pointer forward to the merged branch's tip: no merge commit, no content computation, and history remains linear. It is the default when possible; git merge --ff-only demands it (aborting if a real merge would be needed — useful in scripts that must never create surprise merge commits), and --no-ff forbids it, forcing a merge commit even when unnecessary.
The details that separate candidates: an average answer says "when Git just moves the pointer." A strong answer states the precondition precisely (current tip is an ancestor of the merged tip — checkable with git merge-base) and knows all three dials: default --ff, --ff-only for guarding automation, --no-ff for preserving feature-branch grouping. The judgment layer: linear history reads cleanly but dissolves the feature's identity into individual commits; a forced merge commit keeps "these N commits were one feature" queryable and revertable as a unit — which is why many teams configure PR merges as no-ff. Knowing that trade-off is what the question is really probing.
Part B — The three-way merge
B1. Combining diverged branches
Now the real case: main and sourdough have both moved since their base. Git performs a three-way merge: for each file, compare base→ours and base→theirs. A change made by only one side is taken automatically — the other side is silent about it, and silence means "no opinion". Changes to different parts of the same file both go in. Only when both sides changed the same lines differently does Git stop and ask you (Part C). The result is committed as a merge commit with two parents — the two tips it reunited. The default merge machinery ("strategy") in modern Git is called ort; its name appears in the output, and knowing it is a small interview flex.
Before merging, preview what each side brought: git log --oneline main...sourdough — the three-dot log, finally taught: commits reachable from either side but not both, i.e. everything since the base, both lanes. (This is the log meaning of three dots; diff's differs — Part D.)
🧪 Exercise 6.2 — find the base, preview both sides, merge
cd ~/git-course/kitchen # on main
git merge-base main sourdough # the common ancestor's hash
git log --oneline main...sourdough # what each side did since then
git merge sourdough --no-edit # merge; --no-edit accepts the default message
git log --oneline --graph | head -6✅ Expected result — click to reveal
60d31c040e8012b22f58184a86ae865e681ebdbc
a48960b Add milk glaze note
8a00eea Add sourdough starter notes
8b89f2f Add rye variant notes
Merge made by the 'ort' strategy.
starter.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 starter.txt
* 2a1edbe Merge branch 'sourdough'
|\
| * 8a00eea Add sourdough starter notes
* | a48960b Add milk glaze note
* | 8b89f2f Add rye variant notes
|/What to read out of it: the merge-base hash is Module 5's fork commit (Add copy of serving info — confirm with git show -s on it). The three-dot log lists both lanes' work: two commits on main's side (glaze, rye), one on sourdough's. Then Merge made by the 'ort' strategy — a true three-way merge, not a fast-forward — and the stat shows what arriving theirs changes brought (starter.txt). Without --no-edit your editor would have opened, prefilled with Merge branch 'sourdough' — accepting the default is normal for routine merges. In the graph: the lanes now close — |\ at the top is the reunion, the mirror image of Module 5's |/ fork. Both histories are intact inside the merged one.
B2. The merge commit — a commit with two parents
Open the merge commit with Module 3's tools and the whole design is visible — and Module 4's mysterious ^ operator finally gets its job: HEAD^1 is a merge's first parent (the branch you were on), HEAD^2 the second (the branch merged in). First-parent lineage is what "the main branch's own history" means — git log --first-parent walks it, showing one entry per merge instead of every feature's internals.
Diagram source
flowchart RL
M["merge commit 2a1edbe<br>two parents"] -->|"parent 1 (ours)"| A["a48960b<br>glaze note (main)"]
M -->|"parent 2 (theirs)"| S["8a00eea<br>starter notes (sourdough)"]
A --> R["8b89f2f<br>rye variant"]
R --> BASE["60d31c0<br>merge base"]
S --> BASE🧪 Exercise 6.3 — dissect the merge commit
cd ~/git-course/kitchen
git cat-file -p HEAD | head -3 # the raw object: count the parent lines
git rev-parse --short HEAD^1 # first parent — where main stood
git rev-parse --short HEAD^2 # second parent — where sourdough stood
git log --oneline --merges # list only merge commits✅ Expected result — click to reveal
tree 445fbcaf16248bc072a9dc2c42e3bc3ccff3c5f3
parent a48960bb1354066f6185b4652cb5c92ed2f17b18
parent 8a00eea65da6a9400936412283b8d66eed2d35b4
a48960b
8a00eea
2a1edbe Merge branch 'sourdough'What to read out of it: two parent lines — the only structural difference between a merge commit and any other commit (Module 3's format, one line longer). ^1 and ^2 resolve to those parents in order: parent 1 = your side, parent 2 = theirs; this ordering is guaranteed, which is why tooling can rely on --first-parent to reconstruct "what landed on main, feature by feature." Everything else about the commit is ordinary: one tree (the combined snapshot), author, message. A merge is not a special object — it is a commit that admits to having two histories.
🎯 Interview questions — Part B
🎯 "What does the command git merge do?" — asked verbatim in WeCreateProblems' 100+ GIT Interview Questions, 2026
git merge <branch> incorporates that branch's work into the current branch. Two outcomes: if the current tip is an ancestor of the target's tip, Git fast-forwards — moves the pointer, creates nothing. Otherwise it performs a three-way merge: using the common ancestor (merge base) as reference, it combines each side's changes and records the result as a merge commit with two parents. Changes only one side made apply automatically; overlapping edits to the same lines raise conflicts for the human to resolve. The merged-in branch itself is unchanged — merging writes only to the branch you are on.
The details that separate candidates: an average answer says "it combines two branches." A strong answer names the merge base as the third input — three-way, not two-way — because that is what makes the combination well-defined, and states the directionality (into the current branch). Precision extras that land: the two-parent structure and ^1/^2 ordering, --no-edit/default message behavior, and that modern Git's default strategy is ort (replacing recursive around Git 2.34) — a detail that signals current, hands-on knowledge.
🎯 "What is the difference between fast-forward and three-way merges?" — asked verbatim in WeCreateProblems' 100+ GIT Interview Questions, 2026
The deciding fact is where the merge base sits. Fast-forward: the current tip is the base (no local commits since the fork) — nothing to combine, so Git moves the branch pointer to the other tip; no new commit; history stays linear. Three-way: both branches moved since the base — Git combines base→ours and base→theirs changes and records a merge commit with two parents; history shows the fork and the join. Conflicts can only occur in the three-way case, since fast-forward never combines anything.
The details that separate candidates: an average answer describes the outputs ("one makes a merge commit, one doesn't"). A strong answer gives the precondition that selects between them (ancestry of the current tip) and can flip each into the other: --no-ff forces a merge commit where a fast-forward was possible; --ff-only refuses the three-way case entirely. Then the workflow consequence: fast-forwards keep history linear but erase branch grouping; forced merge commits preserve "this set of commits was one unit" — the axis on which teams standardize their PR merge settings (Module 9).
Part C — Conflicts, mechanically
C1. Why conflicts happen, and what one looks like
A conflict is not an error and not damage. It is Git reporting, precisely: both sides changed the same lines of the same file, relative to the base, in different ways — and choosing between human intentions is not my job. Everything else merges silently; only genuine overlap stops the machine. When it stops, Git does three things: writes conflict markers into the affected file showing both versions; lists the file as both modified in status; and pauses the merge mid-flight — no merge commit exists yet, and you are expected to either finish the job or abort it. Nothing has been lost or corrupted at this point; both versions sit in the file, and both commits still exist untouched.
The markers read like this: everything between <<<<<<< HEAD and ======= is your side's version; between ======= and >>>>>>> <branch> is theirs. Your task is to replace the whole marked block with the text that should be there — one side, the other, or something better than both.
🧪 Exercise 6.4 — manufacture a conflict on purpose
cd ~/git-course/kitchen # on main
git switch -c crust
echo "oven: 230C, 20 min, steam tray on bottom rack" > baking.txt
git commit -a -m "Crust experiments: hotter and shorter with steam"
git switch main
echo "oven: 220C, 30 min for large loaves" > baking.txt
git commit -a -m "Adjust time for large loaves"
git merge crust # both sides changed the same line
echo "exit code: $?"
git status | head -8
cat baking.txt✅ Expected result — click to reveal (the merge stops on purpose)
Auto-merging baking.txt
CONFLICT (content): Merge conflict in baking.txt
Automatic merge failed; fix conflicts and then commit the result.
exit code: 1
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: baking.txt
<<<<<<< HEAD
oven: 220C, 30 min for large loaves
=======
oven: 230C, 20 min, steam tray on bottom rack
>>>>>>> crustWhat to read out of it: CONFLICT (content) names the type and file; exit 1 means "paused", not "broken". Status is, as always, a cheat sheet: both legal moves are printed — fix-and-commit, or --abort. The file itself now contains both versions between markers: HEAD side first (your 220C line), crust side second. Both branches' commits are untouched; the conflict lives only in your working tree and index. Read the two versions as intentions: one tuned for large loaves, one for crust — and notice that no algorithm could know the right combination. That is the entire reason conflicts exist.
Two editors take copies of a document. One rewrites paragraph three for clarity; the other rewrites paragraph three for legal accuracy. The publisher can accept both editors' other changes mechanically — they touched different paragraphs — but paragraph three lands on the desk as two competing rewrites stapled together with a note: "pick or combine." Nobody's work is lost; a decision is simply owed.
Where the analogy stops working. A publisher might judge which rewrite is better. Git never does — it has no opinion, no AI arbitration, no "newest wins". Overlap goes to a human, every time, deterministically. This is a feature with a sharp edge: Git will also happily accept a bad human resolution (including committing the marker lines themselves — see the trap below). The machine guarantees you decide; it does not guarantee you decide well.
C2. The two exits: abort or resolve
A paused merge has exactly two exits, and knowing the abort exists is what keeps conflicts unstressful. Exit 1 — walk away: git merge --abort rewinds working tree and index to the pre-merge state, exactly; try again later, or merge smaller pieces. Exit 2 — resolve: edit each conflicted file to its final text (markers gone), git add it — which in a paused merge means "this file is resolved" — and git commit to conclude; the prepared message even lists the conflicted files. For whole-file decisions there are shortcuts: git checkout --ours <file> / --theirs <file> takes one side wholesale (config files often merge this way), then add as usual.
🧪 Exercise 6.5 — abort first (know your escape hatch), then resolve for real
cd ~/git-course/kitchen
git merge --abort # exit 1: pretend it never happened
cat baking.txt # your version is back, alone
git merge crust # bring the conflict back...
echo "oven: 230C for 20 min with steam, then 220C 10 more min for large loaves" > baking.txt
git add baking.txt # mark resolved
git commit --no-edit # conclude the merge
git log --oneline --graph | head -5
git branch -d crust✅ Expected result — click to reveal
oven: 220C, 30 min for large loaves
Auto-merging baking.txt
CONFLICT (content): Merge conflict in baking.txt
Automatic merge failed; fix conflicts and then commit the result.
[main 890f90f] Merge branch 'crust'
* 890f90f Merge branch 'crust'
|\
| * 321d9d4 Crust experiments: hotter and shorter with steam
* | 0be8c41 Adjust time for large loaves
|/
Deleted branch crust (was 321d9d4).What to read out of it: after --abort, baking.txt is exactly your pre-merge version — the escape hatch is total, which is why you should never fear starting a merge. Round two: same conflict, but this time you wrote a resolution that combines both intentions (hot start for crust, then lower for large loaves) — better than either side, which is the resolution ideal. git add flipped the file from both modified to staged; commit --no-edit accepted the prepared message and minted the merge commit — two parents, same anatomy as B2. History shows the conflict happened and how it ended. Total damage: none.
🎯 Interview questions — Part C
🎯 "What is a conflict in Git, and how do you resolve it?" — asked verbatim in WeCreateProblems' 100+ GIT Interview Questions, 2026
A conflict occurs during a merge (or rebase/cherry-pick, which reuse the same machinery) when both sides changed the same lines of the same file differently relative to their common ancestor — Git combines everything non-overlapping automatically and stops only where intentions genuinely collide. The paused state: conflict markers in the file (<<<<<<< ours, =======, >>>>>>> theirs), both modified in status, no merge commit yet. Resolution: edit each file to its intended final content, git add it to mark it resolved, then git commit to conclude — or git merge --abort to return exactly to the pre-merge state. git checkout --ours/--theirs <file> takes one side wholesale when that is the right answer.
The details that separate candidates: an average answer describes markers and "edit, add, commit." A strong answer explains the mechanism (three-way comparison against the base; conflict = overlapping base-relative changes) — because it predicts when conflicts will and won't happen — and includes operational judgment: --abort as the always-available exit, git diff --check against leftover markers, and the honest point that a resolution can be worse than either side — resolving means re-deciding the intention, ideally with the other author. Mentioning that "ours/theirs" flip meaning during a rebase (Module 10) is an advanced flourish interviewers notice.
🎯 "How do you resolve a merge conflict?" — asked verbatim in Greenroom's Git Interview Questions, Jun 2026
The mechanical sequence: run the merge; on CONFLICT, open each file git status lists as both modified; for each marked block, replace everything from <<<<<<< to >>>>>>> with the correct final text (one side, or a synthesis); git add each finished file; git commit to complete the merge. Verify before committing: git diff --cached --check (leftover markers in what is actually staged — plain --check only sees unstaged files), build/tests (the merged combination may compile on neither side's assumptions). Escape at any point with git merge --abort. In editors/IDEs the same flow appears as side-by-side "accept ours/theirs/both" UI — same operations, nicer view.
The details that separate candidates: an average answer recites edit-add-commit. A strong answer treats resolution as re-deciding intent — naming the practice of involving the other change's author when the collision is semantic, and testing the merged result because "both sides were correct alone" does not compose. Process maturity signals: small frequent merges to shrink conflict size, rerere (Module 15) to auto-replay recurring resolutions, and knowing that hosts block PR merges on conflicts so the resolution usually happens by updating the feature branch first (Module 9's flow).
Part D — Controlling merges
D1. --no-ff: forcing the junction to exist
Fast-forward is efficient but it erases a boundary: after one, nothing marks where the feature branch began and ended — its commits blend into the line as if branching never happened. git merge --no-ff <branch> refuses the shortcut and creates a merge commit even when fast-forwarding was possible. Why teams want that: the merge commit is the feature's receipt — one node saying "these commits arrived together, from this branch, at this time," making history browsable feature-by-feature (git log --first-parent --oneline becomes a changelog) and the whole feature revertable as one unit (git revert -m 1 <merge> — Module 7 explains revert; note the pattern now). Most hosted "merge pull request" buttons run exactly this.
🧪 Exercise 6.6 — the same tiny branch, merged with a receipt
cd ~/git-course/kitchen # on main
git switch -c seeds
echo "top with sesame and poppy" > seeds.txt
git add seeds.txt && git commit -m "Add seed topping note"
git switch main
git merge --no-ff seeds --no-edit # a fast-forward was possible; forbid it
git log --oneline --graph | head -4
git branch -d seeds✅ Expected result — click to reveal
Merge made by the 'ort' strategy.
seeds.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 seeds.txt
* 01756f0 Merge branch 'seeds'
|\
| * df8bc5e Add seed topping note
|/
Deleted branch seeds (was df8bc5e).What to read out of it: compare directly with Exercise 6.1 — same shape of work (one commit, strictly ahead), opposite result: Merge made by... instead of Fast-forward, and the graph shows a bubble — out and back in one hop. That bubble is pure bookkeeping: it contains no changes of its own (its tree equals seeds' tree) but records the grouping forever. Whether bubbles are signal or noise is a genuine team-taste question; what matters is that you now control which one you get: --no-ff forces the bubble, --ff-only forbids the three-way, default takes whichever applies.
D2. Three dots, resolved at last
Module 4 told you to postpone ... — you now own every concept it needs. The two meanings, side by side. git log A...B (symmetric difference): commits reachable from either A or B but not both — "everything since the fork, both lanes," your Exercise 6.2 preview tool. git diff A...B: the diff from the merge base of A and B, to B — that is, only what B's side changed, ignoring everything A did meanwhile. That second form is the important one professionally: it is the diff a pull request shows — "what would this branch contribute?" — as opposed to git diff A B, which mixes in everything A changed too and routinely confuses reviewers into thinking a branch "touches" files it never edited.
| Syntax | For log | For diff |
|---|---|---|
| A..B | Reachable from B, not A (one lane) | Same as diff A B — dots ignored |
| A...B | Reachable from exactly one of A/B (both lanes since fork) | merge-base(A,B) → B: only B's side's changes — the PR diff |
🎯 Interview questions — Part D
🎯 "How do you perform a three-way merge?" — asked verbatim in WeCreateProblems' 100+ GIT Interview Questions, 2026
Operationally: switch to the receiving branch (git switch main) and run git merge <feature>; when the branches have truly diverged, Git automatically performs the three-way merge — you do not select it. Internally: Git finds the merge base (git merge-base shows it), computes base→ours and base→theirs changes, auto-combines everything non-overlapping, raises conflicts for overlaps (resolve: edit, add, commit; or --abort), and concludes with a two-parent merge commit. Useful preflight: git log --oneline A...B to see both sides' commits, git diff main...feature to see exactly what the feature would contribute.
The details that separate candidates: an average answer gives the two commands and stops. A strong answer makes clear that "three-way" names the inputs (base + two tips), not a flag — Git chooses it whenever fast-forward is impossible — and can walk the internal steps in order. Adding the preflight habits (three-dot log and diff before merging) and the strategy name (ort, with -X ours/-X theirs as per-conflict-side preferences distinct from the whole-file checkout --ours/--theirs) demonstrates someone who merges deliberately rather than hopefully.
Part E — Production practice
E1. Symptom → cause → diagnosis → fix
Click the symptom you're seeing.
⚠️ CONFLICT (content): Merge conflict in <file> and panic
What is really happening: Both sides changed the same lines since the base; Git paused mid-merge — a state, not damage.
Diagnose: git status (lists files + both exits) · git diff (shows the colliding hunks)
The fix: Resolve (edit → add → commit) or git merge --abort — both are always available.
⚠️ <<<<<<< HEAD found in production / in a committed file
What is really happening: A conflict was "resolved" by committing the markers themselves.
Diagnose: git log -1 -- <file> to find the bad resolution · git diff --check elsewhere
The fix: Fix the file and commit; add a CI grep for ^<<<<<<< so it cannot recur.
⚠️ Merged the wrong direction (feature has main's history, main unchanged)
What is really happening: You ran the merge while standing on the feature branch — merge writes to the current branch only.
Diagnose: git log --oneline --graph --all · git branch -v
The fix: It is often harmless (feature updated from main is normal); if unwanted, undo on the feature branch (Module 7) and redo from main.
⚠️ fatal: Not possible to fast-forward, aborting.
What is really happening: --ff-only (flag or pull.ff=only config) met genuinely diverged branches.
Diagnose: git log --oneline A...B — see both sides' commits
The fix: Decide deliberately: a real merge, or rebase (Module 10); the guard did its job.
⚠️ Feature's commits invisible as a unit after merging; log is an undifferentiated line
What is really happening: Fast-forward merges dissolved branch grouping.
Diagnose: git log --oneline --graph (no bubbles anywhere)
The fix: Adopt --no-ff for feature merges (or the host's merge-commit setting); use git log --first-parent as the feature-level view.
⚠️ Same conflict re-appears every time a long-lived branch is updated
What is really happening: The branch keeps diverging from the same hot lines; each merge re-collides.
Diagnose: git merge-base drift · conflict file frequency across merges
The fix: Merge/update more often; split hot files; enable rerere to auto-replay past resolutions (Module 15).
E2. Capstone — four tickets
Worked answer: git switch main && git merge --no-ff release/2.4 — the forced merge commit is the record: it carries who merged (committer identity, Module 1), when (timestamp), what (^2 names the release tip; the message names the branch), all hash-chained (Module 3) so it cannot be silently altered. Strengthen it: write a real message (-m "Merge release/2.4 for the Q3 audit window"), and sign the merge commit or tag the result (Module 12) if the audit needs cryptographic attribution. This is the general pattern: when process requires evidence that an integration happened, fast-forward is the wrong tool precisely because it leaves no artifact.
Worked answer: the decision tree, in order. (1) Read both sides as intentions — markers show yours and theirs; git log -1 -- settings.yml on their side gets the message explaining why theirs exists (Module 2's discipline cashing out at 3am). (2) If the intentions are independent (different keys, adjacent lines): combine both, git diff --check, run whatever validates the file (lint, config check), add, commit, ship. (3) If they genuinely collide semantically: prefer the resolution that keeps production safe now — usually keeping their already-deployed behavior plus your fix — and open a ticket to revisit with the teammate in the morning; never guess away a colleague's intention silently. (4) If unsure even of that: git merge --abort, and hotfix on top of the teammate's version instead (branch from current main, reapply your one-line fix there) — sidestepping the conflict entirely. Escalation over improvisation; abort is always on the table.
Worked answer: wrong diff form. git diff main feature compares two snapshots, so everything main received since the fork (other people's 38 files) appears — inverted, as if the feature "removes" it. The question reviews actually ask is "what does this branch contribute over the base," which is git diff main...feature — merge-base to feature tip: 2 files, as expected. (Hosts compute PR diffs this way, which is why the web UI already showed 2.) Two follow-ups worth adding: if the branch is very stale, even the three-dot diff can mislead about how the changes will land — updating the branch from main (merge or rebase, Module 10) refreshes the base; and the team habit of running git diff --stat main...HEAD before pushing catches "accidentally committed junk" early.
Worked answer: attack all three causes. Cadence: month-old branches carry a month of divergence; institute merging main into features weekly (or rebasing, Module 10) and aim features at under a week of life — conflict size scales with divergence, so this alone usually halves the pain. Surface area: the three hot files are architectural conflict magnets — split routes.py per module and config.yaml per service/environment so simultaneous edits stop overlapping textually. Special cases: the lockfile should almost never be hand-merged — take one side wholesale and regenerate (git checkout --ours package-lock.json && npm install), and codify that in the runbook; optionally define a custom merge driver for it (Module 14's attributes). Instrument the result: count conflicted merges per month before and after. This ticket is the interview's real merging question — the strong answer is process and architecture, with Git mechanics as the vocabulary.
E3. Documentation reference
| Topic | Official source | What it covers |
|---|---|---|
| git merge | git-merge manual | ff rules, --no-ff/--ff-only, --abort, conflict presentation |
| Branching & merging walkthrough | Git Book §3.2 — Basic Branching and Merging | The exact fork→merge→conflict→resolve story, with figures |
| Merge strategies | merge-strategies manual | ort and friends; -X ours/-X theirs options |
| Revision/range grammar | gitrevisions manual | ^1/^2, .. and ... for log |
| git diff three-dot | git-diff manual | A...B = merge-base(A,B) → B; --check for markers |
E4. Self-assessment
Try each question aloud, from memory, before opening its answer — the gap between your answer and the hidden one is what to study.
1. Name the three commits that define a merge, and the command that prints the third one.
Ours — the tip of the current branch; theirs — the tip of the branch being merged; and the merge base — the common ancestor, the last commit both sides share. git merge-base A B prints the base any time. The base is what makes merging a computation rather than guesswork: comparing each side against it tells Git exactly what each branch changed since they parted.
2. State the exact precondition for a fast-forward. What does Git do, and what does it not create?
The branch being merged is strictly ahead — your current tip is the merge base, because nothing was committed on your side since the fork. Git just slides your branch pointer forward to their tip: a pure pointer move. It creates no new commit and combines nothing — history stays a straight line, as if the work had been done on your branch all along.
3. Which branch does git merge X modify — and which does it never modify?
Merging happens into the branch you are on: git merge X changes only the current branch. X itself is read, not written — it is never modified. Getting this direction backwards is the most common merge mistake in real teams.
4. In the three-way computation, what happens to a change only one side made? Why?
It is taken automatically. Git compares base→ours and base→theirs for each file; if only one side changed something, the other side is silent about it, and silence means "no opinion." Only when both sides changed the same lines differently does Git stop and ask a human.
5. Precisely: when does a conflict occur? (Same file is not enough — finish the sentence.)
A conflict occurs when both sides changed the same lines of the same file, relative to the base, in different ways. Changes to different parts of the same file both go in silently. A conflict is Git reporting that choosing between human intentions is not its job — it is a paused state, not an error or damage.
6. Read the markers: what is between <<<<<<< HEAD and =======? Between ======= and >>>>>>>?
Between <<<<<<< HEAD and ======= is your side's version (ours — the branch you are on). Between ======= and >>>>>>> <branch> is theirs — the branch being merged in. Your task is to replace the whole marked block with the text that should be there: one side, the other, or something better than both.
7. The two exits from a paused merge — commands and end states.
Exit 1 — walk away: git merge --abort rewinds the working tree and index to the exact pre-merge state, as if the merge never started. Exit 2 — resolve: edit each conflicted file to its final text (markers gone), git add it to mark it resolved, and git commit to conclude — ending with a two-parent merge commit. Knowing the abort exists is what keeps conflicts unstressful.
8. What does git add mean during a conflicted merge?
In a paused merge, git add <file> means "this file is resolved" — it flips the file from both modified to staged. Once every conflicted file is added, git commit concludes the merge with the prepared message, which even lists the conflicted files.
9. What are a merge commit's ^1 and ^2, and what does git log --first-parent show?
^1 is the merge's first parent — the branch you were on when you merged; ^2 is the second parent — the branch merged in. This ordering is guaranteed, which is why tooling can rely on it. git log --first-parent walks first-parent lineage — the receiving branch's own history — showing one entry per merge instead of every feature's internal commits.
10. Why would a team force --no-ff on merges that could fast-forward? Name two concrete payoffs.
Because a fast-forward erases the feature's boundary — its commits blend into the line as if branching never happened. The forced merge commit is the feature's receipt. Two payoffs: history becomes browsable feature-by-feature (git log --first-parent --oneline reads like a changelog), and the whole feature is revertable as one unit (git revert -m 1 <merge>). Most hosted "merge pull request" buttons do exactly this.
11. git diff main feature vs git diff main...feature — which one is the PR diff, and what does the other one pollute the view with?
git diff main...feature is the PR diff: it compares from the merge base of the two to feature, showing only what the feature's side changed. git diff main feature compares two snapshots, so it mixes in everything main changed meanwhile — routinely confusing reviewers into thinking a branch "touches" files it never edited.
12. Your team hits hour-long conflicts monthly. Name the two structural levers before "get better at resolving."
Latency (cadence): branches that live for weeks diverge further from the base, so every merge carries more overlap — merge small and often, and update long-lived branches frequently. Surface area: hot files everyone edits (a shared config, a giant routes file) conflict constantly — splitting them makes overlap physically rarer. Conflict frequency is a team-architecture signal, not a Git skill problem.
E5. Sources
🗒️ Cheat sheet — Module 6
| Command | What it does |
|---|---|
| git merge <branch> | Bring that branch's work into the current branch (ff if possible, else three-way) |
| git merge-base A B | Print the common ancestor — the third input of every merge |
| git merge --no-ff <branch> · --ff-only | Force a merge commit even when ff was possible · allow only ff, abort otherwise |
| git merge --abort | Rewind a conflicted merge to the exact pre-merge state |
| git checkout --ours <f> · --theirs <f> | Take one whole side of a conflicted file (then git add it) |
| git diff --check · git diff --cached --check | Flag leftover conflict markers: unstaged · staged (use the --cached form after git add) |
| git commit --no-edit | Conclude a merge accepting the prepared message |
| git log --oneline A...B | Both lanes since the fork (symmetric difference) — pre-merge preview |
| git diff A...B | merge-base(A,B) → B: what B's side contributes — the PR diff |
| git log --merges · git log --first-parent | Only merge commits · the receiving branch's own lane, one entry per feature |
| HEAD^1 · HEAD^2 | A merge's first parent (your side) · second parent (theirs) |
Key concepts: every merge = three snapshots: base, ours, theirs · merge writes only to the current branch · fast-forward = current tip is the base → pointer slide, no commit; three-way = both moved → two-parent merge commit (ort strategy) · one-sided changes merge silently; only same-lines-changed-differently conflicts · conflict = paused state, not damage: markers in file, both modified, two exits (resolve or --abort) · add = "resolved"; beware committing markers (diff --check) · --no-ff buys a grouping/receipt commit; --first-parent reads history feature-by-feature · log A...B = both lanes; diff A...B = theirs-only since base · conflict frequency is architecture + cadence, not skill.