A depth-first reference on Git: the object database underneath, the daily workflow on top, and the recovery tricks interviewers love to probe. Every section includes runnable shell transcripts plus a step-through diagram.
8 modules · FAANG-grade interview prep · built on the shared Phase-0 design system
Module 1
Git's Mental Model
Every Git command ultimately shuffles four immutable object types and a handful of mutable pointers called refs. Internalise this layer and every higher-level command stops feeling magical.
The four object types
At the bottom of Git sits a single content-addressable key-value store in .git/objects/. Every object is written once, named by the SHA-1 (or SHA-256 in modern repos) of its contents, and never mutated. Four flavours cover every artefact a developer ever handles:
blob - raw file bytes. No filename, no permissions. Two identical files anywhere in history share exactly one blob.
tree - a directory listing: a sorted list of (mode, name, blob-or-tree-sha) entries. A tree is itself an object with its own SHA.
commit - a snapshot pointer: one tree SHA (the root of the snapshot) plus 0+ parent commit SHAs, author, committer, message. Merges are commits with ≥2 parents.
tag (annotated) - a signed, named pointer to any object with its own message. Lightweight tags are just refs, not objects.
Every one of these objects is immutable. "Editing" a commit produces a new commit with a new SHA and the old one is orphaned.
# Inspect an object by SHA. Type first, then content.$ git cat-file -t 3b18e512
blob
$ git cat-file -p 3b18e512
hello world
# A tree lists its children.$ git cat-file -p HEAD^{tree}
100644 blob 3b18e512 README.md
040000 tree a1b2c3d4 src
# A commit references exactly one tree and 0+ parents.$ git cat-file -p HEAD
tree 9f2e...
parent 7c1a...
author Alice <alice@x.io> 1714000000 +0000
committer Alice <alice@x.io> 1714000000 +0000
fix: handle empty input
Refs, HEAD, and the reflog
Objects are immutable; refs are how you give them moving names. A ref is literally a text file under .git/refs/ containing a 40-char SHA.
Branches (refs/heads/main) - auto-advance when you commit on them.
Remote-tracking branches (refs/remotes/origin/main) - last SHA fetched from a remote; only moved by fetch/push.
Tags (refs/tags/v1.0) - by convention don't move.
HEAD - a special symbolic ref. Usually it holds ref: refs/heads/main (attached). Detached HEAD means it directly stores a SHA.
Every update to a ref is journaled in .git/logs/ - the reflog. That journal is a local, per-user 90-day safety net (see Module 6).
$ cat .git/HEAD
ref: refs/heads/main
$ cat .git/refs/heads/main
c3f9a01d5e7b8a2c4c...# All refs at a glance:$ git for-each-ref --format='%(refname) %(objectname:short)'
refs/heads/main c3f9a01
refs/remotes/origin/main c3f9a01
refs/tags/v1.0 9f2ee12
The commit DAG
A commit and its parent chain form a directed acyclic graph. Every ref is a named entry-point to this DAG; history is just "walk backwards from each ref".
3-commit object graph: blobs, trees, commits
Now watch what "committing" actually does to HEAD and the branch ref:
A commit moves HEAD and the branch ref forward
Packfiles and delta compression
Loose objects (one file per SHA under .git/objects/xx/yy...) are ergonomic but wasteful. Periodically Git packs them into .git/objects/pack/pack-<sha>.pack plus an index .idx. Inside a pack, objects are stored either whole or as a delta against another object in the same pack - this is a storage trick, not a change to the logical "snapshot" model.
$ git count-objects -v
count: 42 # loose objects
size: 192 # KiB on disk (loose)
in-pack: 18456 # objects already packed
packs: 1
size-pack: 71340 # KiB for the pack
prune-packable: 0
garbage: 0
# Force pack & delta compression$ git gc --aggressive
# Peek inside a pack$ git verify-pack -v .git/objects/pack/pack-*.idx | head
9f2e... commit 213 140 12
a1b2... tree 88 76 225
c3f9... blob 48112 12890 301 1 b0af...# delta against b0af...
Under the hood: why SHA-1 is not a security hazard here
Git uses SHA-1 as a content fingerprint, not as a security token. After the 2017 SHAttered collision Git added collision detection to libgit2/core Git (the sha1dc implementation), so a crafted collision is rejected at object-write time. Modern repos also support a pure SHA-256 mode (git init --object-format=sha256). In interviews: know that SHA-1 is used, know SHAttered exists, know collision-detection is shipped.
Module 2
Core Workflow: Three Trees
Every commit passes through three trees - working, index, HEAD. Master their transitions and every status message explains itself.
Working tree, index, HEAD
Git's day-to-day plumbing is best understood as three trees - three different snapshots of your repo living on disk simultaneously:
Working tree - the files you actually edit. Totally Git-unaware text on disk.
Index (aka staging area, aka cache) - a single binary file .git/index containing the tree you're about to commit.
HEAD - the tree of the last commit on the current branch.
Every status line in git status is measuring the diff between two of these trees. A file can be staged (index differs from HEAD), unstaged (working tree differs from index), both, or neither.
Command
Working tree
Index
HEAD
Refs
git add f
unchanged
← WT[f]
unchanged
unchanged
git commit
unchanged
unchanged
← index
branch moves
git restore --staged f
unchanged
← HEAD[f]
unchanged
unchanged
git restore f
← index[f]
unchanged
unchanged
unchanged
git checkout <branch>
← branch tree
← branch tree
← branch
HEAD moves
git reset --soft C
unchanged
unchanged
= C
branch = C
git reset --mixed C
unchanged
= C
= C
branch = C
git reset --hard C
= C
= C
= C
branch = C
add → commit lifecycle, visualised
Follow a single file through a typical edit-stage-commit-checkout cycle. The viz below tracks which of the three trees holds which version after each command.
File transitions: WT -> index -> HEAD
$ echo "hello" > readme.md # t=1: WT edited$ git status --short
M readme.md # WT!=index$ git add readme.md # t=2: WT -> index$ git status --short
M readme.md # index!=HEAD$ git commit -m "update readme" # t=3: index -> HEAD$ echo "oops" >> readme.md # t=4$ git restore readme.md # t=5: index -> WT$ git status --short
# clean
diff, status, log
Three inspection commands cover 95% of daily usage. Each is a read-only query against the trees / DAG:
# WT vs index (what you're about to stage)$ git diff
# index vs HEAD (what you're about to commit)$ git diff --cached # synonym: --staged# WT vs HEAD (total pending change)$ git diff HEAD
# Arbitrary commit-to-commit$ git diff main..feature # two-dot: symmetric diff$ git diff main...feature # three-dot: since merge base# Pretty one-liner history$ git log --oneline --graph --decorate --all
# Who owns each line (useful for post-incident forensics)$ git blame -L 42,60 src/server.ts
checkout, restore, switch
Historically git checkout was the swiss-army knife of dangerous: it moved HEAD and could nuke uncommitted changes without warning. Since Git 2.23 the command was split into two narrower verbs:
git switch <branch> - change which branch HEAD points at. Refuses to proceed if it would destroy uncommitted work.
git restore <path> - restore a file from a source tree. --staged restores into the index; without it, into the working tree.
checkout still works for script back-compat; new code should prefer the narrower verbs.
# Move HEAD / create a branch$ git switch main
$ git switch -c feature/login # create & switch# Unstage a file$ git restore --staged secrets.env
# Throw away working-tree edits$ git restore src/broken.ts
# Grab one file from another branch without switching$ git restore --source=origin/main -- config/prod.yaml
Module 3
Branch & Merge
A branch is a 40-char pointer in a text file. Merge vs rebase is a history-shape choice, not a correctness choice.
Branches are cheap pointers
In CVS/SVN, a branch was a server-side full-tree copy that cost minutes and disk. In Git it's a 41-byte text file (40 SHA chars + newline). That engineering choice is why Git's workflow norms are "many short-lived topic branches" while older SCMs were "one long-lived dev branch".
# Create a branch at the current HEAD - literally write a file$ git branch feature/login
$ cat .git/refs/heads/feature/login
c3f9a01d5e7b...# List + last-commit date$ git branch -vv --sort=-committerdate
* main c3f9a01 [origin/main] release: v1.4.0
feature/login a1b2c3d wip: form layout
hotfix/auth 8e7f6a5 [gone] // remote deleted
Fast-forward vs 3-way merge
Two distinct cases occur when you run git merge X while on branch Y:
Fast-forward (FF): if Y's tip is an ancestor of X's tip, Git just advances Y's ref to X. No new commit, no conflict possible. Linear history preserved.
True 3-way merge: Y and X have diverged since a common ancestor M ("merge base"). Git runs a 3-way diff over (M, Y-tip, X-tip) and creates a merge commit with two parents.
You can force either form: --ff-only refuses to proceed if FF impossible; --no-ff always creates a merge commit even when FF would suffice (useful for preserving topic-branch boundaries).
// Scenario 1: feature is ahead of main but main has moved nowhere new.
main: A---B---C
feature: C---D---E # feature @ E, main @ C$ git switch main
$ git merge feature // FF: main now points to E. Done.// Scenario 2: main also advanced; divergence.
main: A---B---C---F---G
feature: C---D---E
$ git merge feature // 3-way merge, new commit M with parents G and E.
Merge vs rebase, side-by-side
Rebase re-creates your commits on top of another base - every commit gets a new SHA. Merge keeps SHAs intact and adds a merge commit. The functional result (working tree) is typically the same; the history shape is radically different.
Merge: preserves divergence, adds merge commit MRebase: replays D', E' on top of G; old D, E abandoned
Aspect
Merge
Rebase
SHA preservation
Yes
No - every rebased commit gets a new SHA
History shape
DAG with merge commits
Linear
Safe on shared branches?
Yes
No - rewrites history, breaks others' clones
Conflict resolution
Once, in the merge commit
Potentially once per replayed commit
bisect quality
Merge commits can hide bad bisects
Each commit is a clean step
Typical policy
Merge into main; tag releases
Rebase feature onto main before opening PR
Conflict resolution
Conflicts only happen at the hunk level when the same lines were modified differently since the merge base. Git marks the file in the working tree with conflict markers and leaves index stage slots 1/2/3:
stage 1 = common ancestor ("base")
stage 2 = "ours" (HEAD side)
stage 3 = "theirs" (incoming side)
$ git merge feature
Auto-merging src/server.ts
CONFLICT (content): Merge conflict in src/server.ts
$ git status --short
UU src/server.ts // both modified$ cat src/server.ts | head
<<<<<<< HEAD
port = 8080
=======
port = 9090
>>>>>>> feature
# Inspect all three stages$ git show :1:src/server.ts // ancestor$ git show :2:src/server.ts // ours$ git show :3:src/server.ts // theirs# Resolve & continue$ $EDITOR src/server.ts
$ git add src/server.ts
$ git merge --continue // or: git rebase --continue# Bail out without changing anything$ git merge --abort
Module 4
Rewriting History
amend, rebase -i, cherry-pick, reset, revert - each rewrites a different slice of the ref/tree/WT trio. Knowing which keeps you out of "help I just deleted a week".
commit --amend
Replaces the current HEAD commit with a new one whose tree is the current index and whose parent is HEAD's parent. The old HEAD commit becomes unreachable (but reflog-recoverable).
amend rewrites C3 -> C3' in place
# Fix the message only$ git commit --amend -m "fix: clearer title"
# Add a forgotten file to the previous commit$ git add forgotten.ts
$ git commit --amend --no-edit
# Amend author on the last commit$ git commit --amend --author="Alice <alice@x.io>" --no-edit
Interactive rebase: squash, fixup, reword, drop
git rebase -i <base> opens an editor with one line per commit from base..HEAD. Reorder lines, change the action keyword, save - Git replays accordingly.
pick a1b2c3d feat: add login form
squash e4f5g6h fix form margin # fold into previous, keep both msgs
fixup i7j8k9l lint # fold, discard msg
reword m0n1o2p login uses JWT # keep commit, edit msg
pick q3r4s5t add integration test
drop u6v7w8x wip debug log # delete this commit// save & close -> Git replays top-to-bottom, creating all-new SHAs.
Interactive rebase: 6 commits squashed to 3
cherry-pick
Take the patch of one commit and apply it on top of the current HEAD. Useful for hotfixes that need to land on multiple release branches.
# Backport one commit from main to release/1.3$ git switch release/1.3
$ git cherry-pick a1b2c3d# Cherry-pick a range (exclusive..inclusive)$ git cherry-pick a1b2c3d..e4f5g6h# Keep provenance link in the commit message$ git cherry-pick -x a1b2c3d# adds: "(cherry picked from commit a1b2c3d)"# Conflict? Resolve & continue like a rebase$ git cherry-pick --continue # or --abort, or --skip
reset: soft, mixed, hard
All three always move the branch ref. They differ in what else they touch:
reset --soft vs --mixed vs --hard
# Undo last 3 commits but keep changes staged for re-commit$ git reset --soft HEAD~3
# Undo last commit, keep changes unstaged (default)$ git reset HEAD~1 # or: git reset --mixed HEAD~1# Nuclear option: throw away everything back to commit C$ git reset --hard c3f9a01# Unstage a file only (not same as restore --staged, but equivalent)$ git reset HEAD -- secrets.env
revert
git revert <commit> creates a new commit whose patch is the inverse of <commit>. History grows forward; nothing is rewritten. This is the only safe "undo" on a public branch.
# Undo commit X by creating X^-1 on top$ git revert a1b2c3d
[main 9f2e512] Revert "feat: add telemetry"
# Revert a merge commit (must specify "mainline" parent)$ git revert -m 1 a1b2c3d# Revert without committing - stack several reverts into one commit$ git revert --no-commit a1b2c3de4f5g6h$ git commit -m "revert: rollback feature pack"
Under the hood: revert of a revert
Reverting a revert does not cleanly reintroduce the original change's future improvements. If feature F was reverted as R, then later reintroduced as R', and F had a bugfix on top (F2), you must still bring F2 across separately. This is why teams use feature flags for risky rollouts rather than relying on revert/unrevert gymnastics.
Module 5
Remotes & Collaboration
Remotes are just ref-namespaces on another machine. Fetch copies bytes, pull is fetch+merge, push shoves bytes back.
Remote mechanics
A remote is a named URL + refspec. The first clone gives you one called origin. Every fetch updates remote-tracking branches under refs/remotes/origin/*; those are your read-only mirrors of what the remote said at the time you last asked.
Server-side enforcement is how a repo stays safe when clients misbehave. The stack from outermost to innermost:
Branch protection rules (GitHub/GitLab): require 1-2 reviewers, require CI green, require linear history, block force-pushes, block deletion.
CODEOWNERS: a text file mapping paths to required reviewers; combine with "require review from Code Owners".
Signed commits: GPG/SSH signatures verified by the forge - gives non-repudiation. git commit -S.
Required status checks: CI runs on the target tree including the merge simulation; if any required check is red, merge is blocked.
Merge queues (GitHub merge queue, Bors, Graphite): serialise concurrent merges so each PR re-tests against the latest main before landing.
# Local: sign every commit with your configured key$ git config --global commit.gpgsign true
$ git config --global user.signingkey <key-id>
$ git commit -S -m "feat: add SSO"
# SSH signatures (newer, no GPG keyring needed)$ git config --global gpg.format ssh
$ git config --global user.signingkey ~/.ssh/id_ed25519.pub
# Verify a commit$ git log --show-signature -1 HEAD
Module 6
Recovery: reflog, fsck, dangling
In Git nothing is ever truly gone until gc runs. The reflog is your 90-day undo journal; fsck finds the orphans.
The reflog - Git's local undo journal
Every time a ref (HEAD, a branch) moves, Git appends a line to .git/logs/<ref> containing <old-sha> <new-sha> <author> <time> <action>. That file is the reflog. Default retention is 90 days for reachable refs and 30 days for unreachable ones, set by gc.reflogExpire.
# Full HEAD reflog$ git reflog
c3f9a01 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
a1b2c3d HEAD@{1}: commit: wip debug
e4f5g6h HEAD@{2}: commit: feat: add login
c3f9a01 HEAD@{3}: checkout: moving from main to feat
# Reflog for a single branch$ git reflog show main
$ git log -g --oneline main
fsck, dangling, unreachable
Use git fsck to enumerate orphaned objects. Anything in .git/objects/ that isn't reachable from any ref, the reflog, or the index is dangling. fsck won't delete - only gc does.
Recovering from an accidental reset --hard
# The recovery drill$ git reset --hard HEAD~2 # oh no$ git reflog
c3f9a01 HEAD@{0}: reset: moving to HEAD~2
e4f5g6h HEAD@{1}: commit: wip # this is what I lost$ git branch recover HEAD@{1}
$ git merge recover # or cherry-pick specific SHAs# Enumerate dangling objects (no ref anywhere)$ git fsck --lost-found --unreachable --dangling
dangling commit e4f5g6h...
dangling blob 9f2e5120...# Inspect an orphan$ git show e4f5g6h
gc and the grace period
git gc packs loose objects and deletes unreachable ones whose reflog entries have expired. Auto-gc fires inside various commands when thresholds are crossed; manual git gc --prune=now is the nuclear option that bypasses the grace period.
fsck re-computes the SHA of every object and verifies it matches the filename; it checks that every tree entry points at an object of the correct type; it traces every ref's parent chain to catch cycles or broken pointers. On a multi-GB repo this is non-trivial but still O(pack size) at ~hundreds of MB/s on modern disks.
Module 7
Advanced Toolbox
Hooks, submodules, worktrees, sparse-checkout, LFS, bisect, blame, stash - every one shows up in a real interview if you claim to "know git well".
Hooks
Executable scripts under .git/hooks/ that Git invokes at well-known lifecycle points. Sample files ship with .sample suffix; rename to activate. Hooks are local by default (not cloned with the repo); to distribute them, check them into a tracked directory and point core.hooksPath at it.
Hook
Fires
Can reject?
Typical use
pre-commit
before commit creation
Yes
lint, format, unit tests
prepare-commit-msg
before editor opens
No (rewrite msg)
auto-prefix with ticket ID
commit-msg
after msg written
Yes
enforce conventional commits
pre-push
before push executes
Yes
run full test suite
pre-receive (server)
before any ref updates
Yes
policy gates on the forge
post-receive (server)
after refs updated
No
trigger CI, notify Slack
update (server)
per-ref before update
Yes
per-branch permissions
# Example: pre-commit that blocks committing .env files$ cat .git/hooks/pre-commit
#!/usr/bin/env bash
if git diff --cached --name-only | grep -qE '\.env(\.|$)'; then
echo "refusing to commit a .env file" >&2
exit 1
fi
$ chmod +x .git/hooks/pre-commit
# Share hooks across clones: commit them to repo, point Git at them$ git config core.hooksPath .githooks
Submodules
A submodule is a pinned SHA of one repo embedded inside another. The outer repo tracks "submodule path X is at commit Y of URL Z"; it does not track Y's contents. That pinning is both submodules' power and their pain.
# Add a submodule and pin its SHA$ git submodule add https://github.com/foo/bar libs/bar
$ cat .gitmodules
[submodule "libs/bar"]
path = libs/bar
url = https://github.com/foo/bar
# Clone something WITH submodules in one shot$ git clone --recurse-submodules https://github.com/acme/app
# After plain clone, initialise & fetch submodules$ git submodule update --init --recursive
# Update all submodules to the latest commit of their tracked branch$ git submodule update --remote --merge
# Run a command in every submodule$ git submodule foreach 'git switch main && git pull'
Worktrees
git worktree lets one repo have multiple working directories checked out to different refs simultaneously. Useful when a hotfix interrupts you mid-feature and you don't want to stash or clone again.
# Add a new worktree with its own branch$ git worktree add ../app-hotfix -b hotfix/auth origin/main
$ cd ../app-hotfix && edit && commit && push
$ cd - ; git worktree remove ../app-hotfix
# List all worktrees for this repo$ git worktree list
/home/a/app c3f9a01 [main]
/home/a/app-hotfix 8e7f6a5 [hotfix/auth]
Sparse checkout & partial clone
On a monorepo you rarely need every directory. Sparse checkout skips the populate step for excluded paths; partial clone skips downloading blobs outside your working set.
# Partial clone: skip blob download at clone time$ git clone --filter=blob:none https://github.com/acme/monorepo
$ cd monorepo
# Enable sparse-checkout (cone mode = simpler, path-prefix only)$ git sparse-checkout init --cone
$ git sparse-checkout set services/api libs/shared
// only these two paths are checked out; everything else excluded$ git sparse-checkout list
services/api
libs/shared
$ git sparse-checkout disable # back to full working tree
Large File Storage (LFS)
Git's object format assumes files are small enough to hash, diff, and transfer in bulk. For binaries (video, datasets, PSDs) git-lfs replaces the blob in-repo with a tiny pointer file; the actual bytes live on an LFS server and are fetched on checkout.
Bisect finds the first commit that introduced a regression by binary-searching the commit DAG. You mark the bad commit (usually HEAD) and a known-good commit; Git checks out the midpoint; you test; mark good/bad; repeat. With N commits between the endpoints, O(log₂N) probes pin the culprit.
git bisect over a 16-commit range
$ git bisect start
$ git bisect bad # current HEAD is broken$ git bisect good v1.2.0 # last release known good
Bisecting: 7 revisions left to test after this (roughly 3 steps)
[a1b2c3d] feat: refactor auth middleware
$ npm test
$ git bisect bad
$ npm test
$ git bisect good
// ...log(7) ~ 3 steps...e4f5g6h is the first bad commit
# Automate with a test script$ git bisect run npm test
# Finish$ git bisect reset
blame and stash
Blame is a per-line commit attribution. It follows line moves across files (within a commit) and across renames (with -C -C -C). The common interview use is post-incident: "this line broke prod, who wrote it and why?".
Stash is a side-stack of dirty working-tree snapshots. Each stash push creates a commit-like object under refs/stash; subsequent pushes build a stack that you can list, apply, or drop.
$ git stash push -m "wip: trying new layout"
Saved working directory and index state On feat/login: wip: trying new layout
$ git stash list
stash@{0}: On feat/login: wip: trying new layout
stash@{1}: On main: quick experiment
$ git stash show -p stash@{0} # view the diff$ git stash apply stash@{0} # apply & keep on stack$ git stash pop # apply & drop top$ git stash drop stash@{1} # delete explicitly# Stash only selected files$ git stash push -m "db only" -- src/db/*
# Stash untracked & ignored too$ git stash push -u -a -m "full snapshot"
Module 8
Interview Cheat Sheet
Dense, searchable, printable. Enough to survive a whiteboard when the problem is "here's a broken repo, fix it".
Daily commands
Inspection
git status -sb - short status with branch tracking
git log --oneline --graph --decorate --all
git show <sha> - full diff of one commit
git diff / --cached / HEAD
git blame -L N,M <file>
git shortlog -sn - commit count per author
git reflog - local ref-movement journal
Staging & commit
git add -p - interactive hunk stage
git restore --staged <f> - unstage
git restore <f> - discard WT edits
git commit -m "..."
git commit --amend --no-edit
git commit --fixup=<sha>
Branching
git switch <b> / -c <new>
git branch -vv / --sort=-committerdate
git branch -d <b> / -D (force)
git merge <b> / --no-ff / --squash
git rebase <base> / -i / --autosquash
git cherry-pick <sha> / -x
Undo matrix
Situation
Command
Safe on shared?
Typo in commit message (not pushed)
git commit --amend -m
No
Forgot to stage a file before last commit
git add f && git commit --amend --no-edit
No
Unstage a file
git restore --staged f
Yes
Throw away WT edits
git restore f
Yes (local only)
Undo last N local commits, keep edits staged
git reset --soft HEAD~N
No
Undo last N local commits, keep edits in WT
git reset HEAD~N
No
Throw away last N commits + edits (!!!)
git reset --hard HEAD~N
No
Revert a pushed commit safely
git revert <sha>
Yes
Revert a pushed merge
git revert -m 1 <sha>
Yes
Recover after reset --hard
git reflog + git branch save HEAD@{n}
n/a
Recover a branch you deleted
git reflog show <branch> + git branch <name> <sha>
HEAD current commit
HEAD^ first parent of HEAD
HEAD^2 second parent (merge only)
HEAD~3 3 commits back along first-parent
main@{yesterday} main where it pointed yesterday
HEAD@{5} HEAD 5 reflog entries ago
main..feature commits reachable from feature but not main
main...feature symmetric difference (either side but not both)
:/word latest commit whose message contains 'word'
:1:path conflict stage 1 of path (base)
:2:path conflict stage 2 (ours)
:3:path conflict stage 3 (theirs)
Common interview prompts, one-line answers
Plumbing
"Does git store diffs?" → No, snapshots + delta compression in packs.
"What are the 4 object types?" → blob, tree, commit, (annotated) tag.
"What is a ref?" → Text file under .git/refs/ holding a SHA.
"What is HEAD?" → Symbolic ref, usually pointing at a branch.
"Detached HEAD?" → HEAD stores a SHA not a branch name.
Flow
"merge vs rebase?" → merge preserves SHAs+DAG; rebase rewrites SHAs to linearise.
"FF vs 3-way?" → FF if HEAD is ancestor; else merge commit with 2 parents.
"--force vs --force-with-lease?" → lease refuses if remote moved since last fetch.
"fetch vs pull?" → pull = fetch + (merge|rebase); fetch alone is read-only.
"Golden rule of rebase?" → Never rebase shared history.
Recovery
"Undid too much with reset --hard" → git reflog, branch from HEAD@{n}.
"Revert vs reset?" → revert creates forward-inverse commit; reset rewrites history.
"Dangling commit?" → object with no ref; git fsck --lost-found.
"Shared repo hygiene?" → branch protection, signed commits, merge queue.
"Fast history server?" → reachability bitmaps, commit-graph file.
Deeper readings
Pro Git book (free, git-scm.com/book) - esp. ch. 10 "Internals".
"Reset Demystified" by Matthew McCullough - the canonical reset reference.
Scott Chacon's "Git from the Inside Out" talk - 45 min, worth every second.
GitHub's Scaling Git blog post series - partial clone, bitmaps, reachability.
"Think Like (a) Git" by Sam Livingston-Gray - beautifully built-up mental model.
Rules (enforced at Wave-4 checksum sweep):
- Zero external dependencies.
- IIFE-wrapped, single global: window.VizLib.
- Respects prefers-reduced-motion.
- ARIA-live captions on every viz.
- Lazy-init via IntersectionObserver.
- No build step; copy-paste as-is.
- NO innerHTML usage (XSS-safe by construction).
Declarative spec shape lives in a `
Place AFTER the existing per-page sidebar/search script, BEFORE
.
Responsibilities:
- #reading-progress width tracker
- Keyboard shortcuts: j/k/t/? and '/' focus TOC search
- '.copy-btn' injection on every
- Theme toggle (html.theme-light) with localStorage
- #kbd-help open/close
- #sub-toc build from current page's