What Problem Does Version Control Solve?
Every serious engineering project is a collaborative, evolving artifact. Without version control you have three problems:
- History — you can’t answer “what changed, when, and why?” for any file.
- Collaboration — two people editing the same file overwrite each other.
- Safety — a bad change can’t be undone; there is no “revert.”
Git solves all three with a content-addressable store of snapshots plus cheap, decentralized branching. It is the substrate for code review, CI/CD (every pipeline triggers off a push), rollbacks, and incident analysis — the single most transferable tool in this curriculum.
The Three-State Model
Git tracks your work through three states, and each maps to a distinct area:
| State | Area | Meaning |
|---|---|---|
| Modified | Working tree | The file on disk differs from what Git last recorded |
| Staged | Index (staging area) | You’ve marked the change to go into the next commit |
| Committed | Repository (HEAD) | The change is safely stored in Git’s history |
The flow is working tree → git add → index → git commit → HEAD. The staging area is Git’s killer feature: it lets you assemble a commit from parts of your changes, so each commit tells one coherent story.
Committing and Inspecting History
git status # which files are modified/staged
git diff # unstaged changes
git diff --staged # staged changes
git add <file> # stage a file (or -p to stage hunks interactively)
git commit -m "feat: add rate limiter" # snapshot the index
git log --oneline --graph --decorate # readable history
git show <commit> # what a specific commit changed
A commit is a snapshot: the content of every tracked file plus metadata (author, message, timestamp) and a pointer to its parent. Because a commit records the full tree (not a diff), Git can jump to any point in history instantly.
Branches Are Cheap Pointers
A branch is just a movable pointer to a commit. Creating one is O(1) — it’s a label, not a copy:
git switch -c feature/login # create + switch (git checkout -b in older syntax)
git switch main # switch back
git merge feature/login # integrate the branch
git branch -d feature/login # delete after merging
Merging combines histories. A fast-forward merge just moves the pointer when the branch has no divergent commits; a merge commit is created when two branches have diverged, joining them.
When Git can’t merge a region automatically it leaves conflict markers (<<<<<<<, =======, >>>>>>>) in the file for you to resolve by hand — then git add the resolved file and git commit.
Rewriting History
Sometimes you want to edit commits, not just add new ones:
git commit --amend # fix the last commit's message or absorb small changes
git rebase -i HEAD~3 # interactive: squash, reorder, reword the last 3 commits
git push --force-with-lease # publish a rewritten branch (use --force-with-lease, never --force)
Rule of thumb: rewriting local, unpushed history is normal and encouraged; rewriting shared history is dangerous because every collaborator’s clone diverges. Only force-push branches you own (topic branches), never main.
Remotes, Push, and Pull
A remote is another copy of the repository — typically a hosted one (GitHub, GitLab) that acts as the team’s source of truth:
git clone <url> # copy a remote repo locally (sets up 'origin')
git remote -v # list remotes
git pull # fetch + merge (fetch then merge origin/main into yours)
git push # publish your commits
git fetch # download objects without merging
git pull is shorthand for git fetch + a merge. On a diverged main, git pull --rebase replays your local commits on top of the fetched ones, producing linear history instead of merge commits — the common trunk-based style.
Collaboration Workflows
- GitHub Flow —
mainis always deployable; every change is a feature branch, a pull request, a code review, and a merge. Simple, dominant for web teams. - Trunk-based development — everyone commits to
main(or short-lived branches) multiple times a day behind feature flags. Enables continuous delivery; popular at scale. - Feature branching / Git Flow — long-lived
develop/release/hotfixbranches; heavier ceremony, mainly legacy or release-train teams.
Which you choose matters less than consistency. Pick one, write good commit messages (type: subject — feat:, fix:, refactor:, docs:, chore:), keep commits focused, and make main your deployable source of truth.
Undoing Mistakes Safely
| Mistake | Safe fix |
|---|---|
| Staged the wrong file | git restore --staged <file> |
| Discard uncommitted edits | git restore <file> |
| Committed too early | git commit --amend |
| Committed to the wrong branch | git switch <branch> then git cherry-pick <commit> |
| Broke something in history | git revert <commit> (safe, adds an undo commit) |
| Lost a commit entirely | git reflog → git reset --hard <sha> (recovery net) |
git revert is the shared-history-safe undo: it adds a new commit that inverts the bad one, so everyone can pull the fix. git reset moves your branch pointer and is only safe for local, unpublished work.
Practice Trajectory
- Initialize a repo, make five commits with
git status/git diff/git logbetween each — watch the three-state model in action. - Create a feature branch, commit three changes, then merge it into
mainwith both a fast-forward and a forced merge commit. - Introduce a conflict deliberately (two branches editing the same line) and resolve it by hand.
- Squash three messy commits into one with
git rebase -i, then verify history withgit log --oneline --graph. - Commit a mistake,
git revertit, and explain why revert (not reset) is the right tool for shared history.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Learning the toolchain | The three-state model + branching is 90% of daily Git |
| Solo work | Even alone, commits are your undo buffer and documentation |
| Team collaboration | Branch + PR + review keeps main green |
| Continuous delivery | Trunk-based + small commits + --force-with-lease discipline |
| Incident response | git log, git bisect, git revert are first responders |