Commits, branches, HEAD and remotes are just a few kinds of pointers. Once you can see them move, the commands stop feeling like magic spells.
Everything else on this page is a combination of these four things.
A snapshot of every tracked file, plus the ID of its parent commit. Arrows point backwards: children know their parents, never the other way.
A sticky note holding one commit ID. Committing moves the note forward. Creating a branch copies nothing.
"You are here." Normally it points at a branch, and that branch is the one that moves when you commit.
Your repo's memory of where the server's main was the last time you talked to it. It goes stale until you fetch.
Pick a scenario and press Next (or the → key). Watch which pointer moves on each command. Faded, dashed commits are unreachable: no branch points at them anymore, but Git still has them for a while.
Turn on Predict to guess what each command changes before you see it. Click any commit to see its other names, like HEAD~2.
git log
..
The same diverged history, combined two ways. Step through both at once.
A change travels left to right. git add copies the file into the staging area (Git calls it the index), which is the draft of your next snapshot. git commit freezes that draft into a commit. Try the buttons and watch git status.
With two files you can edit both, stage only one, and commit. The other stays behind for its own commit. That's how you keep each commit to one idea.
Git is a small database of four object types (blob, tree, commit, tag), each named by the hash of its contents, plus a folder of pointers.
A blob is a file's contents. A tree is a folder listing (names → blobs and subtrees). A commit is a tree plus parent, author and message. In commit 2 only app.js changed, so README.md's blob is reused as-is. That's why full snapshots stay cheap.
Git names a blob by the SHA-1 of "blob <size>\0" followed by the contents. Same content always gives the same name. This box computes the real hash; it will match git hash-object.
Check it: echo hello | git hash-object --stdin. Change one character and the whole hash changes, which is why editing an old commit always produces a new ID.
The .git folder is the repository. Your project files are just one checked-out snapshot of it.
ref: refs/heads/main. In detached HEAD it holds a commit hash instead.objects/pack.Commits never change. --amend, rebase and reset make new commits (or pick older ones) and move a branch to them. The old ones just become unreachable. That's why rewriting pushed commits hurts: your teammates still have the old ones.
Git keeps anything reachable from a branch, tag, HEAD or the reflog. Unreachable commits stay recoverable through git reflog for roughly 30 days by default, until git gc clears them out. If you committed it, you can almost always get it back.
HEAD~1 is the parent, HEAD~3 is three generations back along first parents. HEAD^2 is the second parent of a merge commit. main..feature means "commits on feature that main doesn't have".
git log main..feature # what would merge bring in? git diff HEAD~2 HEAD # changes in the last 2 commits
Git compares three snapshots: the merge base (last shared commit), yours, and theirs. A line changed on only one side is taken automatically. A line changed on both sides is a conflict, and Git writes both versions into the file for you to choose:
<<<<<<< HEAD const title = "Welcome back"; ======= const title = "Hello again"; >>>>>>> search
Edit the file, git add it, then git commit (or git rebase --continue). git merge --abort backs out completely. Try resolving one below ↓
If your branch is an ancestor of the other, merging just slides your sticky note forward. No new commit is made. If both sides have new commits, Git needs a merge commit with two parents. git merge --no-ff forces a merge commit anyway, and git pull --ff-only refuses anything except a slide.
Nothing updates on its own. git status saying "up to date with origin/main" only means up to date with the last fetch. git fetch is always safe: it downloads commits and moves origin/*, and never touches your branches or files.
It means HEAD points at a commit instead of a branch, which happens when you check out a hash, a tag, or stop mid-rebase. Looking around is fine. If you commit there, create a branch (git switch -c name) before you leave, or those commits end up unreachable.
Once a file is tracked, adding it to .gitignore does nothing. Stop tracking it with git rm --cached file and commit. The file stays on disk but leaves the repository from that commit on. Older commits still contain it, which matters for leaked secrets: rotate the secret.
You ran git merge search on main. main added "Pricing" to the menu and search added "Search" in the same spot, so Git stopped. Pick a side, or edit the file yourself, then stage and commit. Try committing too early, or staging with the markers still in.
Fixes for the mistakes everyone makes. Most of them work because Git almost never deletes a commit straight away. Where there's a playground step that shows it happening, there's a link to it.
git branch feature # keep the commit on a new branch git reset --hard HEAD~1 # move main back (commit or stash other work first) git switch feature
Only if you haven't pushed yet. If you have, git revert the commit on main instead.
git reset --soft HEAD~1 # changes stay staged git reset HEAD~1 # changes stay, unstaged
The commit leaves the branch, your work doesn't. Not for commits you've already pushed.
See it in the playground →git revert <commit> git push
Revert adds a new commit that does the opposite, so nobody's history gets rewritten.
See it in the playground →git add forgotten.js # if you forgot a file
git commit --amend -m "better message"
This makes a new commit in place of the old one. Only do it before you push.
See it in the playground →git reflog # find the hash from before git branch rescue <hash> # or: git reset --hard <hash>
The reflog lists every commit HEAD pointed at. Commits nothing else reaches stay in it for about 30 days by default.
See it in the playground →git reflog # or copy the hash from Git's warning
git branch rescue <hash>
Give the commit a branch and it's safe again.
See it in the playground →Deleted branch login (was 3f9a1c2). # Git printed the hash
git branch login 3f9a1c2
The hash scrolled away? If you ever had the branch checked out, git reflog still has it.
git pull --rebase git push
Someone pushed first. Bring their commits in, put yours on top, push again. Don't reach for --force.
git merge --abort git rebase --abort
Either one puts everything back to how it was before you started. To finish it instead, try the conflict demo.
git restore --staged . # unstage everything git restore . # undo edits to tracked files git clean -n # preview untracked files to delete git clean -fd # delete them
This one really can't be undone: work you never committed isn't in Git. git stash -u clears everything too, but keeps a copy just in case.
git rm --cached .env # stop tracking it, keep the file
echo ".env" >> .gitignore
git commit -m "stop tracking .env"
Rotate the secret first. Older commits still contain it, and if it was pushed, assume someone has it. Removing it from history takes git filter-repo and a force push, which rewrites history for everyone.
Each command is tagged by what it does to the picture. Filter by effect to see, for example, everything that can discard work.
checkout, new namesgit checkout did two unrelated jobs, so Git 2.23 (2019) split it into git switch (move HEAD) and git restore (put back file contents). Older tutorials and answers online still use checkout. It still works, so here's the translation.
| Old | New | What it does |
|---|---|---|
git checkout feature | git switch feature | Move to a branch |
git checkout -b new | git switch -c new | Create a branch and move to it |
git checkout a1b2c3 | git switch --detach a1b2c3 | Look at an old commit (detached HEAD) |
git checkout -- app.js | git restore app.js | Throw away unstaged edits to a file |
git reset HEAD app.js | git restore --staged app.js | Unstage a file, keep the edits |
git checkout main -- app.js | git restore --source=main --staged --worktree app.js | Take a file's version from another branch |
Run these once per computer. --global saves them in your user config (~/.gitconfig), so they apply to every repository.
git config --global user.name "Your Name" git config --global user.email you@example.com
Written into every commit you make. Set these first.
git config --global init.defaultBranch main
New repositories start on a branch called main instead of master.
git config --global pull.rebase true
git pull puts your local commits on top of what arrived, instead of making a merge commit. pull.ff only is the stricter choice: refuse anything except a fast-forward.
git config --global push.autoSetupRemote true
The first git push of a new branch just works, no -u origin name needed (Git 2.37+).
git config --global fetch.prune true
git fetch also removes origin/* branches that were deleted on the server.
git config --global merge.conflictStyle zdiff3
Conflicts also show the merge base's version, so you can see what each side actually changed (Git 2.35+).
git config --global rerere.enabled true
Git remembers how you resolved a conflict and reuses it if the same conflict comes back, which helps when you rebase often.
git config --global alias.lg "log --oneline --graph --all"
Now git lg draws the graph from the playground for your real repo.
git config --global core.editor "code --wait"
Commit messages and rebase -i open in VS Code instead of Vim.
Underlined words elsewhere on the page show these definitions when you hover or focus them.
git add copies changes into it and git commit turns it into a commit..git.-a) also stores a message, the tagger and a date.origin for the one you cloned from.origin/main: your repo's record of where that branch was on the remote the last time you fetched, pulled or pushed.git pull and git push use it, and git status compares against it ("ahead 2, behind 1"). git push -u sets it.git merge-base a b prints it.git gc removes them.git stash list shows them.