Git’s mental model: commits, branches, HEAD
Most people learn Git as four memorised commands — add, commit, push, pull — and are lost the moment
anything goes wrong, because they never built a picture of what those commands actually do. This module
fixes that. If you understand Git's mental model — what a commit is, what a branch is, what HEAD points at —
then branching, merging, resolving conflicts and undoing mistakes all become obvious rather than terrifying.
This first lesson builds that model. It assumes you have used git a little; the goal is to make it make
sense.
Git stores snapshots, not changes
The first idea, and the one that unlocks everything else: a commit is a snapshot of your entire project at a moment in time, not a "change" or a "diff". When you commit, Git records what every tracked file looked like right then. (It stores this efficiently under the hood, but the mental model is: each commit is a full snapshot.) A project's history is therefore a sequence of snapshots, each pointing back to the one before it — its parent.
Each commit has:
- A unique hash (a long id like
a1b2c3d) — how you refer to it. - A parent (the commit before it), which links the snapshots into a chain.
- The snapshot of all files, plus a message, author and timestamp.
So the history is a chain of commits, newest pointing back to oldest. Picturing commits as snapshots linked by parent pointers is the foundation; hold it and the rest follows.
The three areas: working directory, staging, repository
Git has three places your files can be, and understanding them explains add and commit:
- The working directory — your actual files on disk, as you edit them.
- The staging area (the "index") — a holding area where you assemble the next commit.
git addmoves changes here. - The repository — the committed history.
git committakes what is staged and records it as a new snapshot.
So the flow is: edit files (working directory) → git add chosen changes (staging) → git commit (repository).
The staging area is the bit beginners find odd, but it is useful: it lets you commit some of your changes
and not others, so each commit is a coherent, single idea rather than a dump of everything you touched. Check
where things are with:
git status # what is modified, staged, untracked — read this constantly
git diff # changes in the working directory, not yet staged
git diff --staged # changes that ARE staged, i.e. what your next commit will contain
git status is the command you run more than any other — it always tells you the current state and often what
to do next.
Branches are just pointers
Here is the idea that makes branching un-scary: a branch is simply a movable pointer to a commit. Not a
copy of your files, not a heavy thing — just a label pointing at one commit. main is a pointer; when you
commit on main, the pointer moves forward to the new commit. Create a branch and you create another pointer
at the same commit:
git branch feature # create a new pointer 'feature' at the current commit
git switch feature # move onto it (older syntax: git checkout feature)
git switch -c feature # create and switch in one step
Because a branch is just a cheap pointer, creating one is instant and costs nothing — which is why Git workflows use branches liberally (the branching lesson). When you understand that a branch is a pointer, "make a branch, work on it, merge it" stops being mysterious: you are just moving pointers around a graph of snapshots.
HEAD: where you are
One more pointer: HEAD points at where you currently are — normally at the branch you are on (which in
turn points at a commit). So HEAD → main → a1b2c3d means "I am on main, which is at commit a1b2c3d". When
you switch branches, HEAD moves to point at the other branch. When you commit, the current branch pointer (and
HEAD with it) moves to the new commit.
This lets you refer to commits relative to where you are, which is enormously useful:
HEAD # the current commit
HEAD~1 # one commit before HEAD (its parent)
HEAD~2 # two commits before
You will use HEAD~1 constantly for "the previous commit" — when undoing, comparing, or resetting (the
undoing lesson). Knowing HEAD is "you are here" makes those commands readable.
Why this model makes everything else easy
Everything in the rest of this module is just moving these pointers over the graph of snapshots:
- Branching — make a new pointer and commit on it.
- Merging — combine two branches' histories, moving a pointer to a new commit that has two parents.
- Conflicts — happen when the same lines changed on both branches, so Git cannot auto-combine the snapshots.
- Undoing — move HEAD or a branch pointer to an earlier commit (
reset), or make a new commit that undoes an old one (revert).
None of that requires memorisation once you see it as pointers over a commit graph. That is the whole reason to learn the model first: Git stops being a set of spells you fear breaking and becomes a small, logical system you can reason about — which is exactly what you need when something goes wrong on a real project under pressure.
Check your work
Commits are snapshots, not diffs: each records the whole project at a moment, with a hash (id), a parent (the commit before), a snapshot, and metadata. History is a chain of snapshots linked by parent pointers.
Three areas: working directory (files on disk) → git add → staging/index (assembling the next
commit) → git commit → repository (history). Staging lets each commit be one coherent idea. Read state
with git status; git diff (unstaged) / git diff --staged (what the next commit holds).
A branch is a movable pointer to a commit — cheap and instant, not a copy. Committing moves the pointer
forward; git branch/git switch -c create one. This is why branching is used liberally.
HEAD = "you are here" — points at your current branch → current commit. HEAD~1/HEAD~2 = previous
commits. Switching moves HEAD; committing moves the branch (and HEAD) to the new commit.
Everything else is moving pointers over the snapshot graph — branch, merge (a commit with two parents),
conflict (same lines changed both sides), undo (reset moves a pointer; revert adds an undoing commit). The
model removes the fear.
Practice
- In a repo, edit a file, run
git status,git addit, rungit statusagain, andgit commit; describe what moved between the three areas. - Use
git diffandgit diff --stagedand explain the difference between what each shows. - Run
git log --onelineand explain how the commits chain together via parents. - Create a branch with
git switch -c, commit on it, and explain what happened to the branch pointer and HEAD. - Explain, in your own words, why "a branch is just a pointer" makes creating branches cheap.
- Use
git show HEADandgit show HEAD~1and explain what HEAD and HEAD~1 refer to.
Official documentation
- Pro Git — Git basics (snapshots) — Why Git stores snapshots, not differences.
- Pro Git — Recording changes (staging) — The working directory, staging area and commits.
- Pro Git — Branches in a nutshell — Branches as pointers, and HEAD.
Next: branching and merging.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship