The Git Model: Commits, Branches, HEAD & Tags¶
Everything else in Git — merging, rebasing, undoing a mistake — is a variation on one idea: Git stores a chain of snapshots, and a handful of lightweight pointers tell you where you are in that chain. Get the model right and every command stops feeling like a magic incantation.
TL;DR — in 30 seconds:
- A commit is a full snapshot of the project (not a diff), pointing back at its parent commit — the whole history is a chain (a DAG, once branches and merges exist).
- A branch is just a movable pointer to one commit — creating a branch is instant because no files are copied.
- HEAD points at "where you currently are" — normally at a branch name, which in turn points at a commit.
1. Commits are snapshots, not diffs¶
A common misconception is that Git stores a series of diffs (like some older version-control tools did).
It doesn't: every commit is a full snapshot of every tracked file at that point, plus a pointer to its
parent commit (the commit it was made on top of). Git only shows you diffs when you ask (git diff,
git log -p) — internally, each commit is complete on its own.
flowchart RL
C3["commit c3<br/>snapshot"] -->|parent| C2["commit c2<br/>snapshot"]
C2 -->|parent| C1["commit c1<br/>snapshot"]
Because every commit is self-contained, checking out any commit gives you the exact state of the project at that moment — no replaying of diffs required.
2. Branches are pointers, not copies¶
A branch is a small file containing one thing: the hash of the commit it currently points to. Creating
a branch (git branch feature) does not copy any files — it just writes a new pointer at your current
commit. That's why branching in Git is near-instant, even on a huge repository.
flowchart LR
subgraph History["commit chain"]
C1["c1"] --> C2["c2"] --> C3["c3"]
end
main["main"] -.-> C3
feature["feature"] -.-> C3
When you commit while on a branch, Git creates the new commit, then moves that branch's pointer forward to the new commit — the branch is always "whatever commit I last pointed at," updated automatically as you commit.
3. HEAD — where you currently are¶
HEAD is a pointer to whichever branch (or commit) you currently have checked out. Almost always, HEAD points at a branch name, and that branch points at a commit — so HEAD moves indirectly, by moving the branch it's attached to.
git checkout <branch>(orgit switch <branch>) moves HEAD to point at that branch instead.git checkout <commit-hash>moves HEAD to point directly at a commit, with no branch attached — this is called detached HEAD. You can look around and even make experimental commits there, but nothing points at them once you switch away, so they become unreachable and are eventually garbage-collected unless you create a branch to keep them (git branch rescue-me).
flowchart LR
HEAD["HEAD"] -.->|"attached to"| main["main"]
main --> C3["c3 (latest commit)"]
4. Tags — a fixed pointer, not a moving one¶
A tag looks like a branch (both are named pointers to a commit) but behaves differently: a tag never
moves. It marks one specific commit permanently — the conventional use is marking a release (v1.2.0).
| Branch | Tag | |
|---|---|---|
| Moves as you commit? | Yes — follows the latest commit | No — fixed forever at the commit it was created on |
| Typical use | Active line of work | A release marker, a point-in-time reference |
| Create | git branch <name> |
git tag -a v1.0.0 -m "message" (annotated) or git tag v1.0.0 (lightweight) |
Prefer an annotated tag (-a) for anything you'll share or reference later — it stores the tagger,
date, and message as a real object, where a lightweight tag is just a bare pointer with no metadata.
Common gotchas
- Assuming a branch is a full copy of the project. Fix: it's one pointer to a commit — the actual files live in the shared commit history, so switching branches just changes which snapshot your working directory reflects.
- Not noticing you're in detached HEAD state. Fix:
git statussays so explicitly ("HEAD detached at ..."); if you've made a commit there you want to keep,git branch <name>before switching away, or it becomes unreachable. - Confusing a tag with a branch because both "point at a commit." Fix: ask "does this move as I commit more?" — a branch does, a tag never does once created.
Check yourself — you create a new branch and immediately delete the old one. Did you lose any commits?
No — a branch is only a pointer. The commits themselves are untouched; deleting a branch just removes that one pointer. The commits stay reachable as long as some other ref (another branch, a tag) still points at them, directly or through history.
Check yourself — why is git branch <name> almost instant even on a huge repository, while a full checkout of another commit can take longer?
Creating a branch only writes a small pointer file — no project files are touched. Checking out a different commit has to update your actual working directory to match that snapshot, which is real file I/O proportional to how much changed.
5. How this connects¶
- This course's next page,
02-branching-and-merging.md— branching and merging are built entirely out of the pointer model here: a merge is just Git finding the two branches' common ancestor commit and combining what changed since. 04-undoing-changes-safely.md—git resetworks by moving a branch pointer backward; understanding that a branch is just a pointer is what makes reset's behavior (and its risk) predictable instead of scary.- Module 00 (Setup & ways of working) — the "never commit on
maindirectly" rule and its 3-step recovery are a direct application of this page's model:mainis a branch (a pointer), and the recovery works by pointing a new branch at the same commit before movingmain's pointer back.
You can defend this when you can explain, from the pointer model alone, why creating a branch is instant and why deleting one doesn't delete any commits.