How Git Works¶
What You'll Learn¶
- How Git stores your project as content-addressed objects
- Why a commit is a snapshot, not a diff
- What the working tree, staging area, and repository each hold
- Why branches are cheap, what
HEADpoints to, and how remotes fit in
Mental Model¶
Git is a database of immutable snapshots linked into a graph. Branches are movable name tags on that graph.
HEADsays which tag you're standing on. Almost every Git command either adds snapshots or moves tags.
Once this clicks, "dangerous" commands like reset and rebase become understandable: they move tags and create new snapshots, and the old snapshots are still there for a while.
The Object Database¶
Everything Git stores lives in .git/objects, named by a hash of its content.
| Object | Stores | Points to |
|---|---|---|
| blob | The contents of one file (no name, no permissions) | Nothing |
| tree | A directory listing: names, modes, and the hashes of blobs and subtrees | Blobs and trees |
| commit | Author, committer, message, timestamp | One tree (the snapshot) and zero or more parent commits |
| tag (annotated) | A named, optionally signed, pointer with a message | Usually a commit |
flowchart LR
C2["commit b7e41c<br/>Add retries"] -->|parent| C1["commit 3fa09d<br/>Initial API"]
C2 --> T2["tree"]
T2 --> B1["blob: README.md"]
T2 --> T3["tree: src/"]
T3 --> B2["blob: app.py (new version)"]
C1 --> T1["tree"]
T1 --> B1
T1 --> T4["tree: src/"]
T4 --> B3["blob: app.py (old version)"]
Unchanged files are shared between snapshots — README.md is stored once. That's why snapshots are cheap.
See it yourself¶
mkdir git-internals && cd git-internals && git init -q
echo "hello" > greeting.txt
git add greeting.txt
git commit -qm "Add greeting"
git cat-file -p HEAD # the commit object
tree 6d0cf2a3c1b56a1c3a0f4a0d0c7f3f3c5c0a4a2e
author Priya Shah <priya@example.com> 1757840400 +0000
committer Priya Shah <priya@example.com> 1757840400 +0000
Add greeting
git cat-file -p 'HEAD^{tree}' # the tree: one blob named greeting.txt
git cat-file -p HEAD:greeting.txt # the blob: hello
Because an object's name is the hash of its content, changing anything — a file, a message, a parent — produces a different hash. That's what makes history tamper-evident, and why rewriting one old commit changes every commit after it.
Three Places Your Changes Live¶
flowchart LR
W["Working tree<br/>files on disk"] -->|"git add"| I["Staging area (index)<br/>the next snapshot"]
I -->|"git commit"| R["Repository<br/>committed snapshots"]
R -->|"git restore / git switch"| W
I -->|"git restore --staged"| W
| Area | Holds | Inspect with |
|---|---|---|
| Working tree | Files as you're editing them | git status, git diff |
| Staging area | Exactly what the next commit will contain | git diff --staged |
| Repository | Every commit | git log |
The staging area lets you commit part of your changes:
git add -p app.py # stage selected hunks interactively
git commit -m "Add request timeout"
# the rest of your edits stay uncommitted
Branches Are Pointers¶
A branch is a file containing one commit hash:
Creating a branch writes a new 41-byte file. Committing on a branch moves that pointer to the new commit.
gitGraph
commit id: "3fa09d"
commit id: "b7e41c"
branch feature/retry
checkout feature/retry
commit id: "c91d02"
commit id: "e44a7b"
checkout main
commit id: "f10b3c"
mainpoints tof10b3c.feature/retrypoints toe44a7b.- Both share history up to
b7e41c, their merge base.
HEAD¶
HEAD is a pointer to what you have checked out — normally a branch, not a commit:
When you commit, Git writes the commit, then moves the branch HEAD points to.
Detached HEAD¶
If you check out a commit or tag directly, HEAD points at a commit instead of a branch:
You can look around and even commit, but no branch moves with you. When you switch away, those commits aren't on any branch. Keep them by creating one:
Remotes and Remote-Tracking Branches¶
A remote is another copy of the repository, usually named origin.
origin/main is your local record of where main was on the remote the last time you talked to it. It only updates when you fetch (or pull).
| Command | Does |
|---|---|
git fetch |
Download new commits and update origin/*. Never touches your branches or files. |
git pull |
fetch, then merge or rebase origin/<branch> into your current branch |
git push |
Upload your commits and move the branch on the remote |
Fetching is always safe, so git fetch followed by a look at git log origin/main is a good habit before changing anything.
Tags¶
git tag -a v2.4.0 -m "Release 2.4.0" # annotated: author, date, message; can be signed
git push origin v2.4.0
A tag is a pointer that doesn't move. Use annotated tags for releases, and never move a published release tag — CI systems, caches, and people depend on it meaning one exact commit.
Common Mistakes¶
- Thinking of commits as diffs, and being confused when a rebase "changes every commit hash".
- Believing
git fetchchanges your working files, or thatorigin/mainupdates itself. - Committing in detached
HEADstate and losing track of the work when switching branches. - Staging everything with
git add .and committing unrelated changes, secrets, or build output together. - Moving or deleting published tags that pipelines and deployments reference.
Interview Questions¶
- What are the four types of Git objects, and how do they relate?
- What is a branch in Git, physically?
- What does
HEADpoint to, and what is a detachedHEAD? - What's the difference between
mainandorigin/main? - Why does rewriting an old commit change the hashes of every later commit?
Next¶
Continue to Everyday Workflow.