A visual field guide

Git, drawn out.

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.

$ git log --oneline --graph
Hold these four ideas

The whole mental model

Everything else on this page is a combination of these four things.

commit

A snapshot of every tracked file, plus the ID of its parent commit. Arrows point backwards: children know their parents, never the other way.

branch

A sticky note holding one commit ID. Committing moves the note forward. Creating a branch copies nothing.

HEAD

"You are here." Normally it points at a branch, and that branch is the one that moves when you commit.

origin/main

Your repo's memory of where the server's main was the last time you talked to it. It goes stale until you fetch.

Step through it

The playground

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.

branch HEAD detached HEAD remote-tracking stash tag arrows point child → parent
git log ..

Merge vs rebase, side by side

The same diverged history, combined two ways. Step through both at once.

merge
rebase
Where your changes live

Working folder, staging, commits

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.

Working folder

The files you see and edit

Staging area

The draft of the next commit

Repository

Commits, inside .git
Lake depth

Under the hood

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 commit points to a tree, a tree points to blobs

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.

parent COMMIT1a2b3c "init" COMMIT4d5e6f "fix" TREE9a01bc TREEc7d8e9 BLOB · app.js v1e3f4a5 BLOB · README.mdb6c7d8 BLOB · app.js v2f0a1b2 same content → same hash → stored once

The name is the hash

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.

What's inside the .git folder

The .git folder is the repository. Your project files are just one checked-out snapshot of it.

HEAD
One line, usually ref: refs/heads/main. In detached HEAD it holds a commit hash instead.
refs/heads/main
A branch: a file containing a single 40-character commit hash. That's all a branch is.
refs/remotes/origin/main
Remote-tracking ref. Updated only by fetch, pull and push.
refs/tags/v1.0
A tag: like a branch that never moves.
refs/stash
The latest stash entry, which is a commit.
objects/
Every blob, tree and commit, compressed and filed by hash. Later packed into objects/pack.
index
The staging area, as a binary file listing the next snapshot.
logs/
The reflog: a history of where HEAD and each branch have pointed.
config
This repo's settings, including remote URLs.
hooks/
Scripts Git can run on events like commit or push.
One level deeper

Concepts that make things click

Rewriting history means replacing it

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.

Reachability decides what survives

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.

Walking history: ~ and ^

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

Three-way merge and conflicts

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 ↓

Fast-forward vs merge commit

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.

origin/main is a memory, not a live view

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.

Detached HEAD isn't an error

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.

.gitignore only affects untracked files

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.

Try it: resolve a conflict

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.

app.js
Fix the file
Then tell Git
When something goes wrong

Oh no, I…

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.

…committed to the wrong branch
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.

See it in the playground →
…want to undo my last commit but keep the changes
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 →
…need to undo a commit that's already pushed
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 →
…want to fix my last commit's message, or add a forgotten file
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 →
…lost commits after a reset, rebase or amend
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 →
…committed in detached HEAD, then switched away
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 a branch I still needed
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.

…got my push rejected
git pull --rebase
git push

Someone pushed first. Bring their commits in, put yours on top, push again. Don't reach for --force.

See it in the playground →
…am stuck in a merge or rebase that's going badly
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.

…want to throw away all my uncommitted changes
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.

Try it in the three areas demo →
…committed a password or API key
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.

Field guide

Commands worth knowing

Each command is tagged by what it does to the picture. Filter by effect to see, for example, everything that can discard work.

Old checkout, new names

git 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.

OldNewWhat it does
git checkout featuregit switch featureMove to a branch
git checkout -b newgit switch -c newCreate a branch and move to it
git checkout a1b2c3git switch --detach a1b2c3Look at an old commit (detached HEAD)
git checkout -- app.jsgit restore app.jsThrow away unstaged edits to a file
git reset HEAD app.jsgit restore --staged app.jsUnstage a file, keep the edits
git checkout main -- app.jsgit restore --source=main --staged --worktree app.jsTake a file's version from another branch

Settings worth changing once

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.

Words Git uses

Glossary

Underlined words elsewhere on the page show these definitions when you hover or focus them.

working tree
The files you see and edit: one commit checked out, plus your changes. This page calls it the working folder.
index (staging area)
The draft of your next commit. git add copies changes into it and git commit turns it into a commit.
commit
A snapshot of every tracked file, plus its parent commit(s), author, date and message. It never changes once made.
hash
The 40-character ID Git computes from an object's contents (SHA-1 by default). Same content, same hash. The first 7 characters are usually enough to name a commit.
ref
A name that points at a commit: a branch, a tag, a remote-tracking branch or HEAD. On disk it's a small file in .git.
branch
A ref that moves. When you commit while it's checked out, it moves to the new commit.
HEAD
Where you are. Usually it points at a branch, and that branch is the one that moves when you commit.
detached HEAD
HEAD points straight at a commit instead of a branch. New commits there belong to no branch.
tag
A ref that stays on one commit, used for releases. An annotated tag (-a) also stores a message, the tagger and a date.
remote
A saved name for another copy of the repository, usually origin for the one you cloned from.
remote-tracking branch
A ref like origin/main: your repo's record of where that branch was on the remote the last time you fetched, pulled or pushed.
upstream
The remote branch your local branch is paired with. Plain git pull and git push use it, and git status compares against it ("ahead 2, behind 1"). git push -u sets it.
ancestor
Any commit you reach by following parent arrows backwards. B is an ancestor of D if D's history includes B.
merge base
The most recent commit two branches share. A three-way merge compares both sides against it. git merge-base a b prints it.
fast-forward
Moving a branch forward to a later commit in its own history. No new commit is needed.
reachable
A commit is reachable if you can get to it from a branch, tag or HEAD by following parents. Unreachable commits stay recoverable through the reflog for a while, until git gc removes them.
reflog
A local log of every commit HEAD and each branch has pointed at. It's never pushed, so it only covers your own repo.
stash
Uncommitted changes saved away as special commits. git stash list shows them.
rebase
Replaying commits on top of a different base. The replayed commits are new commits with new hashes.
blob, tree
The objects Git stores besides commits: a blob is one file's contents, a tree is a folder listing that points to blobs and other trees.