Type real Git commands into a repository that does not exist, and watch the commit graph move.
Open it · The basics · All 33 levels · Cheat sheet · Sandbox
Most people learn Git by memorising four commands and then being frightened of the fifth.
That is not a knowledge problem, it is a feedback problem. git rebase is genuinely simple once
you have watched it happen; it is impossible to reason about from prose.
The thing that reliably teaches Git is seeing the graph change under your own commands. That normally needs a repository you are not afraid to break, which needs setup, which is where most people quit.
So there is no setup. The repository is simulated, the commands are real, and nothing here can touch anything on your machine.
Three panels and a prompt. The graph is the biggest thing on screen at all times, because the graph is the lesson and the text is the footnote. Nothing there is a mockup: that is a real level, mid-solve, with a real repository behind it.
| # | Chapter | What you walk out with | Levels |
|---|---|---|---|
| 01 | Commits | Files, staging, commits, and reading history | 3 |
| 02 | Branches | A branch is a pointer. Everything else follows | 4 |
| 03 | Merge | Fast-forward, real merges, and what a second parent means | 4 |
| 04 | Rebase | Replaying commits onto a new base, and --onto |
5 |
| 05 | Conflicts | Markers, --continue, --abort, ours versus theirs |
4 |
| 06 | Remotes | Fetch, pull, push, and why a push gets rejected | 5 |
| 07 | Undo | reset, revert, reflog, and which of them rewrite history |
5 |
| 08 | Boss level | No new commands. Real scenarios, start to finish | 3 |
Chapter 4 gets the most minutes because rebase is the reason this exists. Chapter 8 adds no new commands at all, because retrieval practice is what actually moves knowledge into long-term memory.
This is a teaching simulator, not a Git implementation. It models behaviour, not plumbing: there is no zlib, no packfile, no index format. What it does model, it models properly.
lib/engine/
state.ts the three trees, refs, reflog, stash, snapshot/restore
refs.ts HEAD~2, HEAD^2, origin/main, HEAD@{n}, stash@{n}, hash prefixes
diff3.ts a real LCS three-way merge, with conflict markers
sequencer.ts one implementation driving every paused operation
commands.ts the command surface
parse.ts tokeniser and dispatch
lib/graph/
layout.ts topological sort into lanes and rows
render.ts SVG built imperatively, animated by diffing layouts
Three decisions are worth calling out:
Commits store whole trees, not deltas. A commit is a full snapshot. This makes applyPatch,
checkoutTree and mergeBase a few lines each, and it is also how Git itself thinks about
commits, so the model the learner builds is the correct one.
One sequencer for every paused operation. Rebase, merge, cherry-pick and revert all pause the
same way and resume through the same code, so --continue, --abort and --skip behave
identically everywhere. That is the behaviour that transfers to a real terminal.
Goals match on structure and messages, never on hashes. They cannot match on hashes: after a rebase the hashes are supposed to be different. Goal checking is graph isomorphism over commit messages, with a predicate escape hatch for the handful of levels where messages repeat.
No command carries hand-written animation. The renderer keeps the previous layout in memory and diffs it against the new one, which leaves exactly three cases:
| Case | What you see |
|---|---|
| A node that moved | it slides |
| A node that is a copy of an older node | it lifts off the original and flies to its new base |
| A genuinely new node | it pops in |
That third case is why a rebase reads correctly the very first time you run one. Nobody choreographed it. It falls out of the data.
bun install
bun dev # http://localhost:3000bun test # 130 tests, engine plus every level
bun run lint
bun run build # 146 static pages, no server neededSet NEXT_PUBLIC_SITE_URL at build time so shared links resolve absolute preview images.
Two things are tested, because two things can break silently.
The engine, against real Git semantics: fast-forward versus true merge, rebase copying rather
than moving, all three reset modes, conflict markers and resolution, --force-with-lease refusing
when the lease is stale.
Every level, end to end. Each one is built from its setup, its own published solution is run through the parser, and the goal checker must pass. This has already caught two authoring bugs that would have been invisible in review: a level that completed the moment it loaded, and a level that could never be completed at all.
bun test
130 pass
0 fail
Levels live in lib/levels/ and are plain TypeScript, one object per level:
{
id: '4.3',
title: 'Transplant a branch',
brief: 'hotfix was cut from feature by mistake...',
setup: { commits: [...], branches: {...}, head: {...} },
steps: [{ t: 'Put hotfix on top of main', f: () => ... }],
goal: { commits: [...], branches: {...} },
hints: ['...', '...', '...'],
solution: ['git rebase --onto main feature hotfix'],
teach: 'Three arguments: where it lands, where to cut, what to move.',
}Add one, run bun test, and the suite will tell you whether your own solution actually solves it.
Issues and pull requests are welcome. If you are adding a command, add the level that teaches it in the same change: a command with no level is a command nobody will discover.
MIT. Do what you like with it.
Built with love by vanshcodeworks
learngit.vanshcodeworks.com · vanshcodeworks.com · GitHub · LinkedIn
A simulator, not a Git implementation. It teaches behaviour, not plumbing.
