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.
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
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.
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.
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.
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.)
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.
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>.
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.
--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
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.
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.
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.
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.
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:
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.
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.
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.
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:
| flag | moves | your changes end up |
|---|---|---|
| --soft | the branch | staged, ready to commit again |
| --mixed (default) | branch + index | in your files, unstaged |
| --hard | branch + index + working tree | gone from the files (the commit is still in the reflog; uncommitted edits are not) |
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.
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.
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.
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.
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.
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.
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.