Git 01 — The Mental Model: Commits, Trees & Refs
You typed git commit -m "fix" five hundred times and you still couldn’t draw what it did. That gap is the entire reason git feels scary — you are operating a machine you can’t see. This module makes the machine visible: a commit is a snapshot with a pointer to its parent, branches are sticky notes on commits, and “staging” is a real place your files pass through. Once you can draw it, git stops being magic and the fear dissolves.
The Goal
By the end of this module you can:
- Define a commit precisely — a snapshot plus a pointer to its parent(s) plus an identity hash
- Draw the commit graph (a DAG) and place HEAD, a branch, and a tag on it
- Explain branches, tags, and HEAD as nothing but movable labels (refs) pointing at commits
- Describe the staging area as the photographer setting up the shot before the snapshot is taken
- Name the three trees — working directory, index, HEAD — and say which command moves a file between them
- Peek inside
.gitwithgit cat-fileand confirm with your own eyes that a commit really is a snapshot
The Lesson
A commit is a snapshot, not a diff
The single most common wrong belief about git is that a commit stores the changes you made — the diff. It does not. A commit stores a full snapshot of every tracked file at the moment you committed. Git is smart about not duplicating unchanged files on disk, but conceptually each commit is a complete photograph of your project, not a list of edits.
Each commit also carries:
| Part of a commit | What it is |
|---|---|
| Snapshot | The complete state of all tracked files at commit time |
| Parent pointer | The hash of the commit that came before it (a merge commit has two parents) |
| Metadata | Author, committer, date, and your commit message |
| Hash (the ID) | A 40-character SHA-1 like a1b2c3d... computed from all of the above |
That hash is the commit’s name, and it’s computed from the commit’s contents — including the parent’s hash. Change one byte of any file, or the message, or the parent, and the hash changes completely. This is why git history is tamper-evident: every commit’s ID depends on its entire ancestry. You met this same content-addressing idea nowhere else yet, but the consequence is simple — a commit is permanent and identified by what’s inside it.
Commits form a chain — the graph
Because every commit points at its parent, commits form a chain running backwards through time. The first commit has no parent; every commit after it points at the one before. Draw it and the whole of git appears:
flowchart RL
C3["C3 snapshot - added tests"] --> C2["C2 snapshot - added README"]
C2 --> C1["C1 snapshot - first commit"]
Read the arrows as “child points to parent.” C3 knows about C2, C2 knows about C1, C1 is the root. Git never stores a “next” pointer — it only ever points backward. To show you history forward, git starts at the newest commit and walks the parent pointers back.
This structure has a name: a DAG — a directed acyclic graph. Directed because the pointers have a direction (always toward the parent). Acyclic because you can never loop back to where you started — history doesn’t form circles. When branches enter the picture in module 02, the chain forks and rejoins, but it stays a DAG. Hold that word; interviewers use it.
Branches, tags, and HEAD are just movable labels
Here is the realization that does most of the fear-dissolving. A branch is not a copy of your files. A branch is not a folder. A branch is a single pointer — a 40-character hash in a tiny file — naming one commit. That’s the whole thing. main is a sticky note stuck on a commit.
flowchart RL
C3["C3"] --> C2["C2"]
C2 --> C1["C1"]
MAIN["main"] -.points at.-> C3
HEAD["HEAD"] -.points at.-> MAIN
TAG["tag v1.0"] -.points at.-> C1
Three kinds of label, all pointing at commits:
| Label (ref) | What it is | Does it move? |
|---|---|---|
Branch (main, feature-x) | A pointer to the latest commit on that line of work | Yes — every commit you make moves the current branch forward to the new commit |
Tag (v1.0) | A pointer to a specific commit you want to bookmark forever (a release) | No — a tag stays put; it’s a permanent marker |
| HEAD | A pointer to the branch you’re currently on (so, indirectly, to your current commit) | Yes — git checkout/git switch moves HEAD to a different branch |
When you run git commit, three things happen in order: git writes the new snapshot as a commit whose parent is your current commit, then moves the current branch label forward to the new commit, and HEAD comes along because it points at the branch. That’s all a commit does to the graph — make a node, slide one label. When you “create a branch,” git writes one tiny file with a hash in it. Creating a branch is nearly free, which is why git encourages you to make them constantly.
HEAD is worth one extra sentence because it’s the thing interviewers poke at. HEAD answers the question “where am I?” Normally HEAD points at a branch name (you are “on” main). If you git checkout a raw commit hash instead of a branch, HEAD points straight at a commit with no branch in between — that’s the famous detached HEAD state, and now you know it’s not a disaster, just HEAD pointing at a commit instead of at a branch.
The staging area — the photographer setting up the shot
Most version-control tools have two places: your files, and the saved history. Git has three, and the middle one is the staging area (also called the index), the part beginners find most confusing until they see why it exists.
Think of committing as taking a photograph:
- Your working directory is the room — messy, with people half-dressed and props scattered. This is the actual files on your disk that you edit.
- The staging area / index is you arranging exactly who and what will be in the next photo. You pick this person, move that prop into frame, leave the mess in the corner out of shot.
git add filemeans “put this file, as it looks right now, into the frame.” - The commit is pressing the shutter. It photographs exactly what’s in the frame — the staging area — not the whole messy room.
Why have the middle step at all? Because real work is messy. You fix a bug and leave a half-written experiment in the same file’s neighbour. Staging lets you commit only the bug fix — frame just that — and leave the experiment out of the photo. It’s the difference between “save everything I touched” and “save exactly this coherent change.” That control is the entire point of the index, and it’s why git add is a separate command from git commit instead of being folded into it.
The three trees
Put names to the three places and you can explain every basic git command as “moving a file between two of these”:
flowchart LR
WD["Working Directory - the files you edit"] -->|git add| IDX["Index - the staged snapshot for the next commit"]
IDX -->|git commit| HEADT["HEAD - the latest commit on your branch"]
HEADT -->|git checkout| WD
| Tree | What it holds | You change it with |
|---|---|---|
| Working directory | The real files on disk you’re editing right now | Your editor — typing, saving, deleting |
| Index (staging area) | The proposed next snapshot | git add (stage), git restore --staged (unstage) |
| HEAD | The snapshot of your latest commit | git commit (writes a new one), git checkout/git reset (point at a different one) |
git status is just git comparing these three and telling you where the differences are:
- A file different between working directory and index shows as “Changes not staged for commit” — you edited it but haven’t
git add-ed the new version. - A file different between index and HEAD shows as “Changes to be committed” — it’s staged, waiting for the shutter.
When you read module 04 on undo and recovery, every flavour of reset will make instant sense, because each one just decides how many of these three trees to move. The whole undo story is built on this picture, so make sure it’s solid before you move on.
Check The Concept
How This Shows Up At Work
- The detached HEAD scare. A teammate runs
git checkout <hash>to inspect an old version, sees the giant “you are in detached HEAD” warning, and panics that they broke the repo. You explain in one sentence — HEAD is pointing at a commit instead of a branch, justgit switch -to get back — and you look like the person who actually understands the tool. - The “where am I?” before any risky command. Before a merge, rebase, or reset, experienced engineers reflexively run
git statusandgit log --oneline --graphto see where HEAD and the branches sit. Knowing the graph is what makes them confident; not knowing it is what makes juniors afraid to press enter. - Composing a clean commit in review. A reviewer asks you to split one messy commit into “the fix” and “the refactor.” Because you understand staging, you stage and commit them separately instead of giving up. Clean, single-purpose commits are a senior signal that shows up in every code review.
- The interview opener. “What is a commit? What is HEAD? What is a branch?” is the warm-up at half of backend interviews. Answering with the snapshot-plus-parent and movable-label model — not a list of commands — tells the interviewer you reason about git instead of memorizing it.
Build This
Everything here runs in PowerShell on a brand-new throwaway repo, so you can experiment freely without touching anything real.
- Make a fresh repo and look at the empty machine:
mkdir git-lab
cd git-lab
git init
git status
git init creates the hidden .git folder — that folder is the repository; everything else is just your working directory. git status reports “No commits yet” and an empty staging area.
- Create a file, then watch it move through the three trees:
"line one" | Out-File -Encoding utf8 notes.txt
git status # notes.txt is "Untracked" - in the working directory, unknown to the index
git add notes.txt
git status # now "Changes to be committed" - it moved into the index
git commit -m "Add notes"
git status # "nothing to commit, working tree clean" - all three trees agree
You just walked a file from working directory → index → HEAD by hand. Say the tree name out loud at each step.
- Make two more commits so you have a chain to look at:
"line two" | Add-Content notes.txt
git commit -am "Add second line"
"line three" | Add-Content notes.txt
git commit -am "Add third line"
(-am stages all tracked changes and commits in one step — fine once a file is already tracked.)
- See the graph — this is the picture this whole module is about:
git log --oneline --graph
Expected, roughly:
* c0ffee3 (HEAD -> main) Add third line
* bada55c Add second line
* 1234abc Add notes
Read it top to bottom = newest to oldest. The * column is the chain of commits. (HEAD -> main) on the top line is the two labels from the lesson: HEAD points at main, main points at the newest commit. You are looking at the DAG and the refs, live.
- Peek inside a commit and prove it’s a snapshot. Copy your newest hash from the log, then:
git cat-file -t c0ffee3 # prints "commit" - the object type
git cat-file -p c0ffee3 # prints the commit's contents
The second command prints something like:
tree 9a8b7c6...
parent bada55c...
author Darshan ...
committer Darshan ...
Add third line
Read it line by line: tree is the snapshot of your files (a tree object), parent is the pointer back to the previous commit — exactly the model from the lesson, in raw form. Follow the tree to see the snapshot itself:
git cat-file -p 9a8b7c6 # use YOUR tree hash from above
It lists notes.txt and a blob hash — the actual file content. You have now seen, with your own eyes, that a commit is a snapshot plus a parent pointer. The magic is gone for good.
- Break it on purpose — see staging do its one job. Edit the file in two unrelated ways, then stage only one:
"keeper change" | Add-Content notes.txt
git add notes.txt
"experiment I do not want yet" | Add-Content notes.txt
git status
git status now shows notes.txt under both “Changes to be committed” (the keeper, staged) and “Changes not staged for commit” (the experiment, still only in the working directory). The same file, two versions, two trees. Commit and confirm only the staged version is photographed:
git commit -m "Keep only the keeper change"
git show HEAD # the experiment line is NOT in this commit
git status # the experiment is still waiting in your working directory
That is the staging area’s entire purpose, proven on your own machine.
Interview Practice
These are warm-up questions at Indian product companies — Razorpay, Zoho, Freshworks, and most startups open with at least one. Answer with the model, not a command list.
Model answers
1. What is a commit? A commit is a full snapshot of all tracked files at the moment it was made, plus a pointer to its parent commit (or two parents for a merge), plus metadata — author, date, message. All of that is hashed into a 40-character SHA that becomes the commit’s ID. Because the hash includes the parent’s hash, history is tamper-evident: changing anything changes every descendant’s ID. The key correction I make is that a commit is a snapshot, not a diff — git computes diffs on the fly by comparing two snapshots.
2. What is HEAD? HEAD is a pointer that answers “where am I right now?” Normally it points at a branch name, and that branch points at a commit, so HEAD indirectly points at your current commit. When you switch branches, HEAD moves to the new branch. If you check out a raw commit hash instead of a branch, HEAD points directly at that commit — that’s detached HEAD.
3. What’s the difference between a branch and a tag? Both are refs — pointers to a commit. A branch moves: every commit you make on it slides the branch pointer forward to the new commit. A tag is fixed: it bookmarks one specific commit permanently, which is why tags are used for releases like v1.0. Creating either is cheap because both are just a small file holding a commit hash.
4. What is the staging area and why does git have it?
The staging area, or index, is the middle of git’s three trees — between your working directory and the committed history. git add copies a file’s current state into it; git commit photographs exactly what’s in it. It exists so you can compose a single, coherent commit even when your working directory has several unrelated changes — stage just the bug fix, leave the experiment out of the commit.
5. Explain the three trees.
Working directory: the real files you edit. Index: the proposed next snapshot you build with git add. HEAD: the snapshot of your latest commit. git status just reports the differences between these three — working-vs-index changes are “not staged,” index-vs-HEAD changes are “to be committed.” Most git commands are explained as moving a file between two of these trees.
6. What does detached HEAD mean, and is it dangerous?
It means HEAD points directly at a commit instead of at a branch. It’s not dangerous in itself — you can look around and even commit. The only risk is that commits made there aren’t pointed to by any branch, so if you switch away without creating a branch, they become hard to find (though the reflog can still recover them). To get back, git switch - or check out a real branch.
7. Why is the commit hash computed from the commit’s contents? Content-addressing: the SHA is derived from the snapshot, the parent hash, and the metadata. This gives each commit a unique fingerprint and makes the whole chain tamper-evident — you can’t alter an old commit without changing its hash and therefore every commit after it. It’s also how git de-duplicates: identical content yields the same hash and is stored once.
Where to Practice
| Resource | What to do | How long |
|---|---|---|
| learngitbranching.js.org | Do the “Introduction Sequence” — it draws the commit graph as you type, exactly the picture this module teaches | 40 min |
| git-scm.com (Pro Git book, free online) | Read chapter 1.3 “Git Basics” and 10.2 “Git Objects” — the official explanation of snapshots and the three trees | 30 min |
| git-scm.com docs | Read the git cat-file and git status man pages with your own git-lab repo open beside them | 20 min |
Check Yourself
- In one sentence, what does a single commit store?
- Why is git history called a DAG — what do “directed” and “acyclic” each mean here?
- What are the three kinds of ref, and which ones move?
- Name the three trees and the command that moves a file from the first to the second.
- You edited a file but didn’t
git addit. Which two trees disagree, and what doesgit statuscall it? - What is detached HEAD, in terms of where HEAD is pointing?
- Why does creating a branch cost almost nothing?
- What does
git cat-file -p <commit-hash>show you, and which two lines prove the snapshot-plus-parent model?
Answers
- A complete snapshot of all tracked files at that moment, plus a pointer to its parent commit (or two for a merge) and metadata, all hashed into the commit ID.
- Directed: the pointers have one direction, always from a commit to its parent. Acyclic: you can never loop back to a commit you came from — history forms no cycles.
- Branches, tags, and HEAD. Branches move (forward on every commit) and HEAD moves (when you switch branches); tags stay fixed on the commit they mark.
- Working directory, index (staging area), HEAD.
git addmoves a file from the working directory into the index. - The working directory and the index disagree.
git statuscalls it “Changes not staged for commit.” - HEAD pointing directly at a commit hash instead of at a branch name — so new commits don’t move any branch.
- A branch is just a tiny file containing one commit hash; creating it writes that one file, nothing is copied.
- It prints the commit’s contents —
tree,parent, author/committer, and message. Thetreeline is the snapshot of your files; theparentline is the pointer back to the previous commit. Together they are the snapshot-plus-parent model in raw form.
Explain it out loud: Facing an empty chair, draw three commits on imaginary paper and narrate what happens to the graph and the labels when you run git commit — the new node, its parent pointer, the branch sliding forward, HEAD coming along. Then explain where a file sits before git add, after git add, and after git commit, naming all three trees. If you stall, that’s the section to re-read.
Still Unclear?
Copy-paste any of these into Claude — they deepen understanding without doing the work for you:
I just learned a git commit is a snapshot plus a parent pointer, not a diff.
Explain how git can show me a diff between two commits so fast if it only
stores snapshots, and what a "tree" and a "blob" object are. Use my git-lab
repo as the running example. Don't give me commands to run blindly - explain
the objects first, then quiz me.
Play an interviewer at an Indian product company. Ask me "what is HEAD",
"what is a branch", and "what's the difference between the working directory,
the index, and HEAD" one at a time. After each answer, tell me what a strong
answer includes that I missed, then ask a harder follow-up.
Walk me through exactly what changes inside the .git folder when I run
git add then git commit - which objects get created, what the index file is,
and how the branch ref file changes. I want to understand the machinery, not
just the commands.
Why AI Can’t Do This For You
AI can run git commit and even untangle a mess for you — but the moment you trust it blindly, you’re back to operating a machine you can’t see. The day a git reset --hard eats your afternoon’s work, or a force-push erases a teammate’s commits, the person who can picture the graph is the one who knows whether the work is recoverable and exactly which ref to move. AI doesn’t know your reflog, your branch layout, or what you did ten minutes ago.
This mental model is the cheapest insurance in your whole toolkit. It takes one focused session to build and it pays out every single time git surprises you for the rest of your career. The commands are the part AI can do; knowing what they do to the graph is the part that makes you the engineer people ask when their repo breaks.
Module done? Add it to today’s tracker