← All maps Git in 3D

A visual guide · snapshots, staging, branches, merge, rebase, squash, reset, reflog, remotes

Git, from the inside out

Thirteen pictures. Scroll, and the model on the right follows the text. Press a button and it runs real git commands against a simulated repository, with real SHA-1 hashes, and animates what they do. Once you see that git is only snapshots, arrows and sticky notes, rebase and squash stop being scary.

The whole of git in three sentences
  1. A commit is a snapshot of every file, named by a hash that also covers its parent. So history can grow, but a commit can never change.
  2. A branch is a sticky note pointing at one commit. HEAD is the note that says "you are here".
  3. Every command either makes new commits, moves notes, or both. Rebase, squash and amend never edit anything: they make copies and move a note.

Chapter 1

A commit is a photo of your whole folder

Most people picture git storing changes: a diff from one version to the next. It doesn't. Every commit is a full snapshot of every tracked file. That sounds wasteful until you see the trick: git stores things by their content.

There are three kinds of object, bottom to top in the model:

  • A blob is the content of one file. Just the bytes: no name, no date.
  • A tree is a folder listing: names, each pointing at a blob (or at another tree for a subfolder).
  • A commit is one tree plus its parent commit, an author, a time and a message.

Each object is named by the SHA-1 hash of its content. Same content, same name, so identical content is stored once. Commit a change to app.js and git writes one new blob, one new tree and one new commit; README's blob is simply pointed at again. Press the button and watch the old blob light up instead of being copied.

press a button to run it

Change one character and the whole id changes; that is what makes it a fingerprint. Check it on your machine: echo 'hello' | git hash-object --stdin prints the same ce01362… as above, on any computer in the world.

git cat-file -p HEAD           # the commit: tree, parent, author, message
git cat-file -p HEAD^{tree}    # the tree: one line per file, each a blob id
git count-objects -v           # how many objects your .git holds
RememberA commit is a snapshot, not a diff. Everything is named by the hash of its content, so identical files are stored once and nothing can change without its name changing.

Chapter 2

Three places a file lives

At any moment, one file can exist in three versions, and the model shows a card for each:

  • Working tree: your desk. The real files you open and edit.
  • Index, also called the staging area: a draft of the next snapshot. A tray you load before taking the photo.
  • Repository: the album in .git. The version shown is the one in the commit HEAD points at.

git add copies a file from the desk to the tray. git commit photographs the tray, not your desk, and glues the photo into the album. That is why you can commit part of your work: stage only what belongs together.

git status is nothing but a comparison of the three: desk ≠ tray shows as not staged, tray ≠ album as to be committed. Same colour, same content.

try them in order, or in any order

Going backwards: git restore --staged f copies the album's version back onto the tray (unstage). git restore f copies the tray's version back onto your desk and throws your edit away. It was never in a commit, so not even the reflog can bring it back.

RememberDesk → tray → album. add loads the tray, commit photographs the tray. status compares the three.

Chapter 3

Arrows point backwards, and every hash seals its whole past

Each commit stores its parent's hash. So arrows in git only point backwards: a commit knows its parent, but never its children. Walking history back is cheap; "the next commit" is not something git even records.

The parent's hash is part of the commit's content, so it is also part of the commit's name. Change anything in commit 2, even one letter of its message, and you have a different commit 2. Commit 3 stored the old hash of 2, so it must be rewritten too, then 4, then 5. Press the button: one typo fix makes four new commits.

watch the hashes of every later commit

This is the most useful fact in all of git: commits cannot be edited. Amend, rebase and squash build new commits and move the label. The old ones still exist, unreferenced, until garbage collection removes them weeks later. (It is the same cascade as Docker's build cache in the containers guide: one change invalidates everything after it.)

RememberHash = content + parent's hash. Touch one commit and every descendant gets a new hash. "Rewriting history" really means making new commits.

Chapter 4

A branch is a sticky note. HEAD says where you are.

A branch is not a folder or a copy of your code. It is a tiny file holding one commit hash: cat .git/refs/heads/main prints 40 hex characters and a newline. Forty-one bytes. That is why creating a branch is instant, even in a huge repository.

HEAD is one more note, and normally it points at a branch, not at a commit: cat .git/HEAD prints ref: refs/heads/main. When you commit, git makes the new commit with HEAD's commit as parent and then moves the branch HEAD is on. Every other note stays put.

Press the buttons in order and watch which note moves.

start with git branch feature

Detached HEAD means HEAD points straight at a commit instead of at a branch. You can look around and even commit, but no branch moves with you. Switch away and those commits are left with no name: git prints a warning and they start to sink. Give them a name before you leave with git switch -c <name>.

RememberBranch = a sticky note on a commit. HEAD = the "you are here" note, usually stuck on a branch note. Committing moves only the note HEAD is on.

Chapter 5

Merge: slide the note, or tie a knot

git merge feature means "bring everything feature has into the branch I'm on". There are two cases, and git picks automatically.

Fast-forward. If main has nothing that feature lacks (main is an ancestor of feature), there is nothing to combine. Git just slides main's note forward. No new commit.

Three-way merge. If both have moved on, git finds the merge base, the newest commit both share (it glows), and compares each tip with it. Changes from either side are combined into a merge commit with two parents. That knot is the record that two lines of work joined.

each button rebuilds its own starting point first

--no-ff forces a merge commit even when a fast-forward was possible. Some teams like every feature to show up as its own bubble in history; others prefer a straight line. Neither is wrong.

git merge-base main feature        # which commit is the merge base?
git log --oneline --graph --all    # draw the knots in your terminal
RememberAncestor? Slide the note (fast-forward). Diverged? Compare both with the merge base and tie them with a two-parent commit.

Chapter 6

A conflict is git asking you a question

Git merges line by line against the merge base. If only one side changed a region, it takes that change. If both changed the same lines in different ways, it cannot know which you want, so it stops and asks.

It writes both versions into the file between markers. <<<<<<< HEAD starts your side (the branch you're on), ======= separates, and >>>>>>> feature ends theirs. Until you answer, the merge is paused and git status lists the file under Unmerged paths.

README.md (working tree)
start with git merge feature

Answering takes three steps: edit the file to what it should say (and delete the markers), git add it to mark it resolved, git commit to finish the merge. Changed your mind? git merge --abort puts everything back as it was.

A setting worth turning on: git config --global merge.conflictstyle zdiff3 also shows the merge base's version between the markers, so you can see what both sides started from.

RememberConflict = both sides changed the same lines. Edit → add → commit. "Ours" is where you are; "theirs" is what you're bringing in.

Chapter 7

Rebase: replay your commits on top of a new base

Your feature branch started from an older main, and main has moved on. Instead of tying a knot, rebase takes the commits that are only on your branch and replays them, one at a time, on top of main's tip. As if you had started your work today.

Replay means copy. Each replayed commit has a new parent, so it gets a new hash (chapter 3). Watch: the copies rise out of the originals, and the originals sink. They are not deleted. ORIG_HEAD and the reflog still point at them, which is why undoing a rebase is one command.

start with git rebase main

Afterwards main can fast-forward to feature: one straight line, no merge commit. That is the whole appeal of rebase: a history that reads like a story.

Two things to know. Conflicts happen per replayed commit: resolve, git add, git rebase --continue (or --skip, or --abort). And during a rebase, ours and theirs swap: "ours" (HEAD) is the new base you're building on, and "theirs" is your own commit being replayed. Everyone trips over this once.

The golden ruleNever rebase commits that other people already have. Rebase replaces commits with copies; if a teammate built on the originals, their history and yours now disagree, and the next pull brings back duplicates. Rebase your own unpushed work freely. On shared branches, merge.
RememberRebase = copy my commits onto a new base, then move my note. New parents, new hashes, originals left in the reflog.

Chapter 8

Squash and rebase -i: tidy up before anyone sees it

Real work produces commits like "wip", "fix typo" and "really fix typo". Before you open a pull request you can rewrite them into a clean story with git rebase -i main. Git opens a todo list, one line per commit, oldest first. You change the verbs, save, and git replays the list from the top.

pick
keep the commit as it is.
reword
keep it, but change its message.
squash
melt it into the commit above, and combine both messages.
fixup
melt it into the commit above, and throw its message away.
drop
leave it out entirely.
(move)
reorder lines to reorder commits.

This is that todo list, for the branch in the model. Edit it, or pick a preset, then run it:

git-rebase-todo · oldest first
pick a preset, then run

Same truth as before: a squash makes one new commit whose snapshot is the final state, and the originals sink. Shortcuts worth knowing: git commit --fixup <commit> followed by git rebase -i --autosquash main fills in the todo list for you. git merge --squash feature stages all of feature's changes as one uncommitted change on main, with no merge commit and no link back; GitHub's "Squash and merge" button does the same on the server. And git reset --soft HEAD~3 && git commit squashes your last three commits with no todo list at all.

Rememberrebase -i = a todo list that git replays. squash and fixup melt a commit into the one above. Squash the noise; keep commits that each make sense on their own.

Chapter 9

Cherry-pick: copy one commit somewhere else

Sometimes you need one commit, not a whole branch: a crash fix sitting in the middle of an unfinished feature. git cherry-pick <hash> takes the change that commit made (its diff against its parent) and applies it where you are, as a new commit. Same message, same author, new parent, so a new hash. It is a one-commit rebase.

the fix is the middle commit on feature

Later, when feature is rebased onto main, git notices the fix is already there (same patch) and drops the duplicate: patch contents already upstream. With a merge both copies stay in history, harmless but noisy. git cherry-pick -x appends "(cherry picked from commit …)" to the message, so the trail stays visible.

Remembercherry-pick = copy one commit's change onto HEAD, as a new commit.

Chapter 10

Undo in three strengths, and the safe one

git reset <commit> moves the branch HEAD is on to another commit. The flag decides how many of the three areas from chapter 2 follow it. Watch the three trays under the graph:

flagmovesyour changes end up
--softthe branchstaged, ready to commit again
--mixed (default)branch + indexin your files, unstaged
--hardbranch + index + working treegone from the files (the commit is still in the reflog; uncommitted edits are not)
each button starts from v3 again

reset rewrites history: the branch now points backwards and the newer commits sink. For a commit that is already pushed and shared, use git revert instead. It adds a new commit that does the exact opposite. Nothing is rewritten, so nobody else's history breaks.

Remembersoft = move the note. mixed = note + tray. hard = note + tray + desk. Already shared? revert, don't reset.

Chapter 11

The reflog: nothing you committed is really lost

Every time HEAD moves (commit, switch, reset, rebase, merge, amend), git writes one line to the reflog: where HEAD was, where it went, and why. It is local to your machine and kept for 90 days by default (30 for commits that nothing reaches any more).

That makes every history-rewriting command reversible. Reset too far? Rebase went wrong? Deleted a branch that was never merged? The commits are still in .git, and the reflog tells you their hashes.

break something first

The limit: the reflog records commits. Edits you never committed or stashed (lost to git restore, git checkout -- file, or a reset --hard over a dirty folder) have no hash, and no tool can bring them back. Commit early and often; you can always squash later.

RememberThe reflog is git's flight recorder. Committed = recoverable for weeks. Never committed = gone.

Chapter 12

Remotes: your laptop, GitHub, and the bookmarks in between

A remote is just another repository, usually on GitHub, normally named origin. It holds the same kind of objects with the same hashes: a commit is the same commit wherever it lives.

Your laptop also keeps remote-tracking branches like origin/main (dashed in the model). Each is a bookmark: where main was on GitHub the last time you asked. It only moves when you talk to origin.

fetch
download new commits and move the bookmarks. Your branches and files don't change. Always safe.
pull
fetch, then merge (or, with --rebase, rebase) your branch onto its bookmark.
push
upload your commits and move the branch on origin, but only if that is a fast-forward there.
try: teammate pushes, you commit, git push (rejected), git pull --rebase, git push

Force push replaces the branch on origin even when that isn't a fast-forward, dropping whatever was only there. Watch Sam's commit vanish from GitHub. --force-with-lease refuses if origin moved since your last fetch. Careful though: a fetch also refreshes the lease, so fetch-then-lease can still wipe a teammate's work. Add --force-if-includes for the real safety net.

Rememberorigin/main is a bookmark of GitHub's main as of your last fetch. fetch is always safe. push only fast-forwards unless you force it.

Chapter 13

The whole picture, in one breath

Your files are stored as snapshots: blobs and trees inside commits. Each commit is sealed by a hash that covers its parent, so history can grow but never change. Branches and HEAD are sticky notes. add loads the tray; commit photographs it. merge slides a note or ties two lines with a two-parent commit. rebase, cherry-pick, amend and squash make new commits that copy old changes, then move a note; the originals wait in the reflog. reset moves a note and, if you ask, your tray and desk; revert adds an opposite commit. fetch, pull and push copy commits between repositories and move notes there.

press play and watch the whole day

RememberSnapshots, arrows, sticky notes. Every command makes new commits, moves notes, or both.
Now make it stick.
The lab gives you a real terminal on a simulated repo, a live 3D graph, and missions that check your work: merge, resolve, rebase, squash, rescue, push.
Open the git lab →
1/13 Snapshotsdrag to orbit
The 3D models need WebGL and a connection to cdn.jsdelivr.net. The text works on its own.

I want to…

SituationCommandWhat it does to the picture
Throw away my edits to one filegit restore app.jsDesk ← tray. The edit is gone for good.
Unstage a filegit restore --staged app.jsTray ← album. The desk keeps your edit.
Fix the last commit's message, or add a forgotten filegit add file && git commit --amendA new commit replaces the tip; the old one sinks.
Undo the last commit, keep its changes stagedgit reset --soft HEAD~1Only the note moves back.
Undo the last commit, keep the changes as editsgit reset HEAD~1Note and tray move back.
Undo a commit that's already pushedgit revert <hash>Adds an opposite commit. Nothing rewritten.
Squash my last 3 commitsgit reset --soft HEAD~3 && git commit or git rebase -i HEAD~3One new commit with the final snapshot.
Update my branch with the latest maingit fetch && git rebase origin/main (or git merge origin/main)Copies your commits on top, or ties a knot.
I committed on main but meant a new branchgit branch feature && git reset --hard origin/main && git switch featureNew note on your commits; main's note goes back.
Move one commit to another branchgit switch other && git cherry-pick <hash>Copy lands on other; drop the original with reset or rebase -i.
Get back a commit or branch I lostgit reflog, then git branch rescue <hash>A new note on a sunken commit brings it back up.
Undo a rebase or reset I just didgit reset --hard ORIG_HEAD (or HEAD@{1})The note jumps back to the old tip.
Park my work to switch branchesgit stash … git stash popDesk and tray go into a drawer, then come back.
Stop a merge or rebase halfwaygit merge --abort / git rebase --abortEverything back as before you started.
See what my branch has that main doesn'tgit log --oneline main..featureCommits reachable from feature, not from main.
Find which commit broke somethinggit bisect start, bad, good <hash>Binary search down the arrows.
Replace a pushed branch after rebasing it (your own branch only)git push --force-with-lease --force-if-includesMoves origin's note to your copies, if no one else pushed.

Merge, rebase or squash?

mergerebasesquash (merge --squash, or rebase -i)
History shapea knot: both lines kept, joined by a two-parent commitone straight lineone commit per feature
Rewrites commits?noyes: copies with new hashesyes: many commits become one
Safe on shared branches?yesno (golden rule)only for your own branch
Conflictsat most once, all togetheronce per replayed commitonce, all together
Keeps the small steps?yesyesno: bisect and revert get coarser
Use it forintegrating shared branches; preserving what really happenedupdating your own branch before a pull requestturning messy work into one clean commit

How dangerous is this command?

Safe: read-only or adds

  • status, log, diff, show, reflog
  • fetch
  • branch, switch, tag (creating)
  • add, commit, stash
  • merge, revert, cherry-pick

Rewrites history: undo with the reflog

  • commit --amend
  • rebase, rebase -i
  • reset (soft, mixed; hard for committed work)
  • branch -D
  • push --force: the old tip may survive only on someone else's machine

Destroys uncommitted work: no undo

  • restore <file>, checkout -- <file>
  • reset --hard with uncommitted edits
  • clean -fd (untracked files)
  • stash drop / stash clear

Ten traps everyone hits once

  1. Rebasing a shared branch. Teammates' history and yours stop agreeing; duplicates appear on the next pull. Rebase only commits you alone have.
  2. Plain --force. It drops whatever someone else pushed since you last looked. Use --force-with-lease --force-if-includes.
  3. Confusing ours and theirs in a rebase. During a rebase, "ours" is the base and "theirs" is your own commit.
  4. git restore on the wrong file. Uncommitted edits have no hash, so no reflog. Commit or stash before experiments.
  5. Committing in detached HEAD and switching away. The commit loses its only name. Read the warning; git switch -c first.
  6. Thinking git branch -d deletes work. It deletes a 41-byte note. The commits remain, and the reflog knows them.
  7. Expecting fetch to update your files. It only moves origin/* bookmarks. Merge or rebase to bring the changes in.
  8. Huge, rare merges. Conflicts grow with the time between merges. Integrate small and often.
  9. Committing a secret and "fixing" it with a new commit. It is still in history, and possibly already cloned. Rotate the secret; rewriting history is a cleanup, not a fix.
  10. Squashing everything always. One giant commit makes git bisect and git revert useless. Squash the noise, keep meaningful steps.

Glossary: every word, one line each

repository
The .git folder: all objects, refs, the index and the reflog. Your files are just a checkout of one snapshot.
object
Anything stored in the object database: blob, tree, commit or annotated tag. Named by the hash of its content.
blob
The content of one file, without its name.
tree
A folder listing: names, file modes, and the blob or tree each name points to.
commit
A tree, zero or more parents, author, committer, dates and a message.
hash / SHA-1
The 40-hex-character fingerprint of an object's content. Short forms like a1b2c3d work while unambiguous. Newer repos can use SHA-256.
parent
The commit a commit was built on. Root commits have none; merge commits have two or more.
working tree
The files on your disk that you edit.
index / staging area
The draft of the next commit, filled by git add.
untracked
A file in the working tree that is in neither the index nor HEAD.
ref
A name that points at a commit: branch, tag, remote-tracking branch, HEAD.
branch
A movable ref in refs/heads/; moves forward when you commit on it.
HEAD
The ref that says what you have checked out. Usually points at a branch.
detached HEAD
HEAD pointing straight at a commit. New commits belong to no branch.
tag
A ref that never moves, for releases. Annotated tags are objects with a message and can be signed.
remote
A named URL of another repository, usually origin.
remote-tracking branch
origin/main: your local record of a branch on the remote, as of the last fetch.
upstream
The remote-tracking branch your branch compares with and pulls from by default (git push -u sets it).
fast-forward
Moving a branch forward along existing arrows. No new commit needed.
merge base
The best common ancestor of two commits. The reference point of a three-way merge.
three-way merge
Combining two tips by comparing each with their merge base.
merge commit
A commit with two (or more) parents.
conflict
Both sides changed the same region differently. Git writes markers and waits for you.
ours / theirs
In a merge: your branch / the branch coming in. In a rebase: reversed.
rebase
Replaying commits on a new base, creating copies with new hashes.
interactive rebase
rebase -i: the same, driven by a todo list you edit (pick, reword, squash, fixup, drop, edit).
squash
Combining several commits into one.
fixup
A squash that keeps the message of the commit above.
autosquash
rebase -i --autosquash moves fixup! commits next to their targets automatically.
cherry-pick
Applying one commit's change on top of HEAD as a new commit.
amend
Replacing the tip commit with a new one.
reset
Moving the current branch to another commit; --soft, --mixed or --hard decide whether the index and working tree follow.
revert
A new commit that undoes an earlier one.
restore
Copy a file's version from the index or a commit into the working tree or index.
stash
A stack where uncommitted changes can be parked and taken out again.
reflog
The local log of every position HEAD (and each branch) has had.
ORIG_HEAD
Where HEAD was before the last reset, rebase or merge. The quick undo.
reachable
A commit you can get to by following arrows from some ref. Unreachable ones are eventually garbage-collected.
gc
Garbage collection: packs objects and deletes unreachable ones once their reflog entries expire.
patch-id
A fingerprint of a commit's change. How rebase spots that a commit is already upstream.
fetch / pull / push
Download and move bookmarks / fetch then merge or rebase / upload and move the branch on the remote.
force push
A push that moves the remote branch even when it is not a fast-forward.
pull request
A GitHub feature, not a git one: a request to merge one branch into another, with review.
.gitignore
Patterns of files git should treat as invisible (build output, secrets, dependencies).

Check yourself

Does git store diffs between versions?

No. Each commit points at a full snapshot (a tree). Unchanged files reuse the same blob, which is why snapshots are cheap. (Pack files compress similar objects later, but that is a storage detail.)

You fix a typo in the message of a commit five commits back. How many new commits does that create?

Five: that commit and every one after it, because each one's hash includes its parent's hash.

What does git branch feature actually write?

One small file, refs/heads/feature, containing the current commit's hash. HEAD does not move.

When is a merge a fast-forward?

When the branch you're on is an ancestor of the one you merge. Git just moves the label.

After git rebase main, where are your original commits?

Still in the repository, unreachable. ORIG_HEAD and the reflog point at them until they expire.

What's the difference between squash and fixup in a rebase todo list?

Both melt the commit into the one above. squash combines the messages; fixup keeps only the message above.

Which reset keeps your changes staged?

--soft. --mixed keeps them as unstaged edits; --hard throws them out of the files.

A commit with a bug is already on the shared main branch. Reset or revert?

Revert. It adds an undo commit, so nobody's history is rewritten.

You ran git reset --hard HEAD~3 by mistake. How do you get the commits back?

git reset --hard ORIG_HEAD, or find the old tip with git reflog and reset to HEAD@{1}.

Your push was rejected with "fetch first". What happened, and what do you do?

Someone pushed commits you don't have, so your push would not be a fast-forward. Run git pull --rebase (or merge), then push.

Why does git fetch never cause conflicts?

It only downloads objects and moves origin/* bookmarks. Your branches, index and files are untouched.

During a rebase conflict, which side is "theirs"?

Your own commit being replayed. "Ours" is the new base.

Read it once, then do it once.
Fourteen missions in the lab, from a first commit to rescuing a deleted branch and surviving a rejected push.
Open the git lab →
Git in 3D Last updated 17 October 2026