intermediate30 min read·Updated Dec 2024Expert reviewed
Git & Version Control
Git is not just 'save and upload' — it's a content-addressable object store with a directed acyclic graph of commits. Understanding the object model, branching strategies, rebase vs merge, and conflict resolution separates engineers who use Git from engineers who are afraid of it.
Git object model:
blob file content (no filename)
tree directory (filenames + refs)
commit snapshot: tree + parent + metadata
tag named pointer to a commit
Key commands:
git add -p interactive staging (hunk by hunk)
git diff --staged review what's staged before commit
git commit --amend rewrite last commit (don't do on pushed commits)
git rebase -i HEAD~N interactive rebase: squash, reword, reorder
git stash / git stash pop temporarily shelve changes
git bisect start/good/bad binary search for bug-introducing commit
git reflog history of where HEAD has been (lifesaver)
git worktree add ../path check out a second branch without stashing
git push --force-with-lease safer force-push (fails if remote has new commits)
git log --oneline --graph visualize branch history
Merge vs Rebase:
merge preserves history, creates merge commit
rebase linear history, new SHAs — never on shared branches
Conventional Commits:
feat(scope): description new feature (bumps minor)
fix(scope): description bug fix (bumps patch)
chore: description tooling, no user impact
BREAKING CHANGE: ... in footer — bumps major
Conflict markers:
<<<<<<< HEAD your changes
=======
>>>>>>> other-branch incoming changes
Resolve: edit file, git add, git merge/rebase --continue
Everything in Git is an object identified by a SHA-1 (soon SHA-256) hash:
blob — file content (just bytes, no filename)tree — directory listing (filenames + blob/tree references)commit — snapshot pointer: tree + parent commit(s) + author + messagetag — annotated reference to a commit
A commit doesn't store diffs — it stores a complete snapshot as a tree of blobs. Diffs are computed on the fly by comparing two trees. This is why git log -p on a huge repo is cheap: it compares parent and child tree objects.
A branch is just a file containing one SHA — the commit it points to. main = .git/refs/heads/main = c3d4e5f. When you commit, the branch ref updates to the new commit. Branches are cheap (a 41-byte file).
HEAD is a pointer to the currently checked-out branch (or a commit directly — "detached HEAD"). When you commit, HEAD → branch ref → new commit SHA.
The most important and most misunderstood Git operation pair.
Merge
Creates a merge commit with two parents. Preserves the true history of when branches diverged and converged.
Before: After merge:A - B - C (main) A - B - C - M (main) \ \ / D - E (feat) D - E
When to merge:
Public/shared branches (don't rewrite history others have pulled)
When you want to preserve the exact history of when work was done
Merging feature branches into main via PRs
Rebase
Re-applies your commits on top of the target branch. The commits get new SHAs (content may be the same, but parent differs). Creates a linear history — easier to read, easier to bisect.
Before: After `git rebase main` from feat:A - B - C (main) A - B - C (main) \ \ D - E (feat) D' - E' (feat)
D' and E' are new commits with the same changes but different parents. The original D and E are abandoned (garbage collected).
When to rebase:
Updating a local feature branch with upstream changes before a PR (instead of a merge commit)
Interactive rebase (git rebase -i) to clean up messy local commits before review
Never on shared branches — rewriting history that others have pulled forces force-pushes and breaks their local copies
Golden rule: Never rebase commits that have been pushed to a branch others are working from.
Merge vs rebase — practical decision
Situation
Use
Updating feature branch with latest main
git rebase main
Merging PR into main
Merge (or squash merge)
Cleaning up local WIP commits
git rebase -i HEAD~N
Public hotfix branch
Merge
Syncing a fork
git rebase upstream/main
Interactive rebase — the most useful command you're not using#
git rebase -i HEAD~5 opens an editor with the last 5 commits:
pick a1b2c3 Fix typo in headerpick d4e5f6 Add user authpick e5f6a7 WIPpick f6a7b8 More auth stuffpick a7b8c9 Tests for auth# Commands:# p, pick = use commit as-is# r, reword = use commit, but edit message# e, edit = use commit, but pause to amend# s, squash = meld into previous commit# f, fixup = like squash but discard this commit's message# d, drop = remove commit entirely
Common pattern: squash WIP commits before a PR, reword confusing messages.
Resolution: edit the file to the correct state (pick one side, combine, or rewrite), then git add <file> and git merge --continue (or git rebase --continue).
Simple. Requires CI on every PR. Works well for web services with continuous deployment.
Git Flow (complex release cycles)
main (production tags only)develop (integration) └─ feature/* → develop → release/x.x → main + develophotfix/* → main + develop
Overkill for most web apps. Still used in mobile/native release pipelines.
Trunk-Based Development (large teams/monorepos)
Everyone merges to main daily. Feature flags gate incomplete features. Requires extremely fast CI and strong code review culture. Used by Google, Meta.
1. Force-pushing to a shared branch without --force-with-lease#
git push --force will silently overwrite commits others pushed since your last fetch. git push --force-with-lease fails if the remote has changes you haven't fetched — much safer.
A 4,000-line commit is impossible to review and impossible to bisect. Make commits atomic: one logical change per commit. It's easier to squash later than to split.
A .env file committed to a public repo exposes credentials even if you delete it in the next commit — the blob still exists in history. Fix: git filter-repo (preferred) or BFG Repo Cleaner. And immediately rotate the exposed credentials.
Rebasing main or any branch others have checked out rewrites SHAs. Their local history diverges. They see "your branch and origin/main have diverged." Painful to untangle.
Tracking node_modules, build artifacts, .DS_Store, or .env files pollutes history. Set up global gitignore (~/.gitignore_global) for OS/editor artifacts and per-repo .gitignore for project artifacts.
Commit messages: type(scope): short description (Conventional Commits) — enables changelog generation and semantic versioning automation. First line ≤ 72 chars.
One commit = one logical change. Future you will thank you at git bisect.
Use git stash + git stash pop for WIP when switching contexts. Or even better: git worktree add for parallel branches without stashing.
Review your diff before committing: git diff --staged shows exactly what's going in. Never git add . blindly.
Tag releases: . Tags are permanent checkpoints that survive branch deletion.
git fetch is safe, git pull is not.git pull = git fetch + git merge (or rebase if configured). Prefer git fetch && git rebase origin/main for explicit control.
Shallow clones (git clone --depth=1) for CI pipelines where full history isn't needed — saves bandwidth and time.
git bisect is a binary search through history for the commit that introduced a bug. Incredibly powerful with good atomic commits.
Sparse checkout (git sparse-checkout) checks out only specific directories — essential for monorepos (e.g., only checkout ).
[ { "id": "git-q1", "type": "mcq", "prompt": "What does `git rebase main` do when run from a feature branch?", "options": [ "Merges main into the feature branch, creating a merge commit", "Re-applies the feature branch's commits on top of main's latest commit, creating new commit SHAs", "Fast-forwards the feature branch to main's HEAD", "Creates a new branch from main with the feature branch's changes" ], "correct": 1, "explanation": "Rebase replays your commits on top of the target, creating new commits with new SHAs (same diffs, different parents). No merge commit. The feature branch history becomes linear. The original commits are abandoned (eventually GC'd)."
git tag -a v1.0.0 -m "Release 1.0.0"
Protect main: require PRs + CI + at least one review before merge. No direct pushes.
packages/my-app
,
"difficulty": "medium"
},
{
"id": "git-q2",
"type": "mcq",
"prompt": "You accidentally committed a `.env` file with API keys to a public repo. You delete it in the next commit and push. Are the keys still exposed?",
"options": [
"No — deleting the file removes it from all history",
"Yes — the blob still exists in Git's object store and is accessible via the commit hash",
"No — GitHub automatically redacts secrets in commit history",
"Yes, but only for 24 hours until GitHub purges it"
],
"correct": 1,
"explanation": "Git stores content as blobs by content hash. The `.env` blob still exists in the repo's object store and is reachable via the commit that added it. Anyone who cloned or fetched the repo has it. Immediately rotate all exposed credentials, then use `git filter-repo` to purge the file from history.",
"difficulty": "hard"
},
{
"id": "git-q3",
"type": "mcq",
"prompt": "What is a 'detached HEAD' state in Git?",
"options": [
"HEAD points to a commit directly instead of a branch ref",
"The HEAD file is corrupt",
"You're on a branch with no commits",
"The remote branch has been deleted"
],
"correct": 0,
"explanation": "Normally HEAD → branch → commit. In detached HEAD, HEAD → commit directly (no branch in between). Commits you make won't belong to any branch — they'll be garbage collected unless you create a branch: `git checkout -b new-branch`.",
"difficulty": "medium"
},
{
"id": "git-q4",
"type": "predict-output",
"prompt": "You run `git log --oneline` and see:\n```\nc3d4e5f Add payment flow\nb2c3d4e Fix auth bug\na1b2c3d Initial commit\n```\nYou run `git rebase -i HEAD~2`. What commits appear in the editor?",
"correct": "The two most recent commits: `b2c3d4e Fix auth bug` and `c3d4e5f Add payment flow` (listed oldest first in the editor). HEAD~2 means 2 commits back from HEAD.",
"explanation": "HEAD~2 refers to the commit 2 steps before HEAD (a1b2c3d). Interactive rebase shows commits from HEAD~N+1 to HEAD, listed in reverse chronological order (oldest first in the editor so you read top-to-bottom).",