← Curriculum 08 · Version Control · Git ⏱ 40 min

Module 08 · Tooling · All tracks

Version Control · Git

Most Git pain comes from memorising commands without the model underneath. Build the model and the commands become obvious — including the ones that scare people.

40 min deep read 🎯 7 sections 📊 2 diagrams

By the end you'll be able to explain, with conviction:

  • The three-area model that makes every Git command make sense.
  • Merge vs rebase — and when each is the right call.
  • How to undo things safely, and why revert beats reset on shared branches.

1The Git mental model — three areas

Almost every Git command just moves changes between three places. See the places, and the commands stop being magic.

Your work lives in three areas: the working directory (the files you edit), the staging area / index (a holding pen for the exact changes you're about to commit), and the repository (the permanent, hashed history of commits). The staging area is the part people skip — and it's the whole point. It lets you craft a commit deliberately, choosing which changes go together rather than dumping everything at once.

flowchart LR WD[Working Directory] -- git add --> SA[Staging Area] SA -- git commit --> R[(Local Repo)] R -- git push --> RemoteRepo[(Remote)] RemoteRepo -- git fetch / pull --> R R -- git checkout --> WD
add stages, commit records, push shares. Every command is just moving changes across this line.

A commit is a snapshot of the whole tree plus a pointer to its parent — which is why history is a graph, and why branches are astonishingly cheap. A branch is just a movable label pointing at a commit. Internalise that one sentence and rebasing, fast-forwarding, and detached HEAD all stop being mysterious.

🧭 Why it matters

"A branch is a pointer to a commit; HEAD is a pointer to where you are" is the single most clarifying idea in Git. Most "how do I undo this" questions dissolve once you think in terms of moving pointers.

2Branching strategies — GitFlow vs trunk-based

A branching strategy is a team agreement about how work flows into the main line. Two dominate, and they trade off in opposite directions.

GitFlow uses long-lived branches: main, develop, plus feature/*, release/*, and hotfix/*. It's structured and supports scheduled, versioned releases — but the long-lived branches drift apart and merging gets painful. It fits software with explicit release versions (desktop apps, libraries).

Trunk-based development keeps everyone on main with very short-lived branches merged daily, guarded by CI and feature flags to hide unfinished work. It minimises merge pain and is the backbone of continuous delivery — which is why most web teams have moved to it. The cost is discipline: it demands strong automated tests because half-built code is constantly hitting main.

💬 Interview angle

"GitFlow suits versioned, scheduled releases; trunk-based suits continuous delivery. The core tradeoff is integration frequency — short-lived branches mean small, frequent merges and fewer conflicts, but they only work if your test suite can be trusted."

3Merge vs rebase

Both combine work from two branches; they differ in what they do to history. Merge creates a new "merge commit" that ties the two histories together — it's non-destructive and preserves exactly what happened, at the cost of a branchy graph. Rebase replays your commits one by one on top of the target branch, producing a clean linear history — at the cost of rewriting those commits (they get new hashes).

gitGraph commit id: "A" commit id: "B" branch feature commit id: "C" commit id: "D" checkout main commit id: "E" merge feature
A merge preserves the branch shape and ties it off with a merge commit; a rebase would instead replay C and D after E as a straight line.

The decision rule that keeps you safe: rebase to tidy your own local, unpushed work; merge to integrate shared work. The golden rule of rebasing — never rebase commits that others have already pulled — follows directly, because rewriting shared history forces everyone else into a painful reconciliation.

Common trap

Rebasing a shared branch and force-pushing is how you ruin a teammate's afternoon. Rewriting history is fine on a private branch and dangerous on a public one — that distinction is the whole answer.

4Resolving conflicts calmly

A conflict isn't an error — it's Git honestly admitting it can't decide which of two changes to the same lines should win, and handing you the choice. Panicking is the only real mistake.

Git marks the contested region with <<<<<<<, =======, and >>>>>>>. Your job is to read both sides, decide what the code should be (often a blend, not one side wholesale), delete the markers, then stage the resolved file and continue. The calm habit is to resolve in small, frequent integrations rather than one giant end-of-feature merge — the smaller the divergence, the smaller the conflict.

💬 Interview angle

"A conflict just means two changes touched the same lines and Git won't guess — I read both sides, decide the correct result, and remove the markers. The real fix is upstream: integrate often so divergences stay small."

5Undoing safely — reset vs revert, stash, cherry-pick

The undo commands separate people who fear Git from people who don't.

  • git revert — creates a new commit that undoes a previous one. History is preserved, nothing is rewritten. The safe choice on shared branches.
  • git reset — moves the branch pointer backwards. --soft keeps your changes staged, --mixed (default) unstages them, --hard discards them entirely. It rewrites history, so reserve it for local, unpushed commits.
  • git stash — shelves uncommitted changes so you can switch context (e.g. an urgent fix), then stash pop brings them back.
  • git cherry-pick — copies a single commit from one branch onto another. Ideal for pulling one specific fix into a release branch without merging everything.

Common trap

Reaching for reset --hard on a branch others share rewrites public history and loses work. On anything shared, revert is the answer — it undoes the effect while keeping an honest record.

6The PR workflow

The pull request is where version control meets collaboration. The standard loop: branch off main → commit focused work → push → open a PR → automated CI runs (build, tests, linters) → teammates review → address feedback → squash-or-merge once approved and green. Branch protection rules enforce that nothing reaches main without passing checks and review.

The senior habits mirror Module 02's review etiquette: keep the PR small and single-purpose, write a description that explains the why and how to verify, and treat a red CI run as a hard stop, not a suggestion. The PR is also a permanent record of why a change was made — worth writing for the engineer who reads it in a year.

Go deeper

"Squash and merge" collapses a messy feature branch into one clean commit on main — tidy history, but you lose the granular steps. "Merge commit" keeps every commit. Teams pick one for consistency; knowing the tradeoff (clean history vs full detail) is the interview-ready point.

7Tags, releases, and semver

A tag is a permanent, named pointer to a specific commit — unlike a branch, it doesn't move. Tags mark releases so you can always return to exactly what shipped. Semantic Versioning gives those tags meaning with MAJOR.MINOR.PATCH:

  • MAJOR — breaking changes; existing users must adapt.
  • MINOR — new, backward-compatible features.
  • PATCH — backward-compatible bug fixes.

The value of semver is that the version number becomes a promise: a consumer reading 2.4.1 → 2.5.0 knows new features arrived but nothing broke, while 2.x → 3.0 warns them to read the migration notes. It turns upgrades from a gamble into an informed decision.

💬 Interview angle

"A tag pins a release to an exact commit, and semver makes the number a promise — patch is safe, minor adds features compatibly, major signals a breaking change. It lets consumers upgrade with eyes open."

Recap — what you can now teach

  • Git has three areas — working dir, staging, repo — and a branch is just a pointer to a commit.
  • GitFlow for versioned releases, trunk-based for continuous delivery; the tradeoff is integration frequency.
  • Rebase your local work, merge shared work — never rebase commits others have pulled.
  • A conflict is Git asking you to choose; the real fix is integrating often.
  • revert on shared branches, reset only locally; stash to shelve, cherry-pick to copy one commit.
  • Tags pin releases; semver makes the version a compatibility promise.

Self-check

Say each answer out loud before revealing it.

What is a Git branch, really?

When do you rebase, and when do you merge?

Why prefer revert over reset on a shared branch?

What does the staging area give you that committing directly wouldn't?

What does a MINOR version bump promise under semver?