Version control is the single most important skill a developer can build early. Most tutorials show you commands to copy-paste without making them stick.
This post ships a runnable terminal adventure — a bash script that creates a safe sandbox and walks you through each concept interactively. Type the real commands. Get immediate feedback. No cloud account, no setup, nothing to break.
Table of contents
Open Table of contents
Download and run
curl -fsSL https://shukyusof.com/git-101.sh | bash
Or save it first if you prefer to read before running:
curl -fsSL https://shukyusof.com/git-101.sh -o git-101.sh
bash git-101.sh
Requirements: Git installed, bash or zsh. Works on macOS, Linux, and WSL on Windows.
The script creates a sandbox at ~/git-101-sandbox. It never touches any of your existing projects.
Options
| Flag | What it does |
|---|---|
--level N | Jump straight to level N (1–14) |
--reset | Delete the sandbox and exit |
--help | Show usage |
During a challenge, type hint for a clue (−1 point) or skip to move on (−2 points).
Why Git?
Git is a distributed version control system. Every change is stored as a snapshot. You can branch to try ideas, merge to combine work, and recover from any mistake — because Git never forgets.
Every serious software project uses it. Learning Git pays dividends for your entire career.
Tier 1 — Beginner (Levels 1–5)
Level 1 — Init & commit
A repository is a folder Git watches. You create one once per project.
git init # start tracking this folder
git status # see what has changed
git add README.md # stage a file (mark it for the snapshot)
git commit -m "Add README" # save the snapshot with a message
Think of add as putting items into a box, and commit as sealing the box and labelling it. Git only packages what you explicitly place in the box.
Level 2 — Branching
A branch is a parallel copy of your codebase. Changes on a branch don’t affect others until you deliberately merge them.
git switch -c feature-login # create a branch and move to it
# ... make your changes ...
git add .
git commit -m "Add login feature"
git switch master # go back to the main branch
The golden rule: never work directly on master / main. Branches are free — use one for every piece of work.
Level 3 — Merging
When a feature is ready, merge its branch into the main branch.
git merge feature-login # bring feature-login into the current branch
git log --oneline --graph --all
If two branches changed the same lines, Git pauses for a merge conflict — you decide which version wins.
Level 4 — Remotes
A remote is another copy of your repo — usually on GitHub, GitLab, or Bitbucket.
git remote add origin <URL> # register the remote address as "origin"
git push -u origin master # upload commits; -u sets the default upstream
Level 5 — Pulling teammate changes
git pull # fetch + merge the remote's commits into your current branch
git pull is shorthand for git fetch (download) followed by git merge (apply).
Tier 2 — Intermediate (Levels 6–10)
Level 6 — Undoing mistakes
Git gives you several escape hatches:
| Situation | Command |
|---|---|
| Un-stage a file (keep changes on disk) | git restore --staged <file> |
| Discard working-directory changes | git restore <file> |
| Undo a commit by creating a reverse commit | git revert HEAD --no-edit |
| Undo last commit but keep changes staged | git reset --soft HEAD~1 |
| Wipe last commit and its changes entirely | git reset --hard HEAD~1 |
revert is safe for shared branches — it adds a new commit. reset --hard rewrites history — only use it on commits that haven’t been pushed.
Level 7 — Inspecting history
git diff # working dir vs last commit
git diff --staged # staged changes vs last commit
git log --oneline # compact history
git log --oneline --graph --all # ASCII branch graph
git blame app.js # who changed each line, and when
Common git log filters:
git log --author="Alice"
git log --since="2 weeks ago"
git log --grep="fix"
git log -p # show the full diff for each commit
Level 8 — .gitignore
.gitignore tells Git which files and patterns to ignore entirely:
node_modules/
.env
*.log
dist/
build/
.DS_Store
Rules use glob patterns. * matches anything. A trailing / means directory only. A leading ! negates (un-ignores) a pattern.
Once you commit .gitignore, the ignored files vanish from git status. They are never staged or committed.
Level 9 — Stashing
Stash temporarily shelves uncommitted changes so you can switch context:
git stash # shelve everything
git stash push -m 'feature wip' # shelve with a label
git stash list # show all stash entries
git stash pop # re-apply and remove the latest stash
git stash apply stash@{1} # re-apply a specific entry (keep it in list)
git stash drop stash@{0} # delete an entry without applying
git stash clear # delete all stash entries
Level 10 — Tagging
Tags mark specific commits as meaningful — usually version releases.
git tag v1.0 # lightweight tag (just a pointer)
git tag -a v1.1 -m "Release v1.1" # annotated tag (stores author, date, message)
git tag # list all tags
git push origin --tags # push all tags to the remote
git tag -d v1.0 # delete a local tag
Annotated tags are preferred for releases because they carry metadata and can be signed.
Tier 3 — Advanced (Levels 11–14)
Level 11 — Merge conflicts
Conflicts are unavoidable in team work. Here is the resolution flow:
git merge hotfix # → CONFLICT: automatic merge failed
Git marks the conflicting sections in the file:
<<<<<<< HEAD
console.log("master version");
=======
console.log("hotfix version");
>>>>>>> hotfix
Edit the file to keep what you want, then:
git add app.js # mark as resolved
git commit -m "Resolve merge conflict"
git branch -d hotfix # clean up
git mergetool opens a visual diff editor if you prefer a GUI.
Level 12 — Rebase
Rebase replays your branch’s commits on top of another branch, giving you a straight line of history instead of a merge bubble.
# On feature-rebase, after master has new commits:
git rebase master # move feature commits on top of master
# Then fast-forward master:
git switch master
git merge feature-rebase # fast-forward, no new commit
Interactive rebase — rewrite, squash, or reorder commits before sharing:
git rebase -i HEAD~3 # edit the last 3 commits
Commands in the interactive editor:
pick— keep as-issquash/s— combine with the previous commitfixup/f— like squash but discard the commit messagereword/r— keep the commit but edit the messagedrop/d— delete the commit entirely
Warning: never rebase commits that have already been pushed to a shared branch — it rewrites history and forces everyone else to re-sync.
Level 13 — Cherry-pick
Cherry-pick applies one specific commit from any branch onto HEAD:
git log experimental --oneline # find the SHA you want
git cherry-pick <sha> # apply it to the current branch
Classic use-cases:
- Port a security fix from
mainto an older release branch - Pull one feature commit without merging the whole branch
- Rescue a commit from an abandoned branch
Level 14 — Reflog & recovery
git reflog records every movement of HEAD — even ones caused by reset --hard.
git reflog # show all HEAD positions with SHAs
To recover a “lost” commit:
git reflog # find the SHA just before the reset
git reset --hard <lost-sha> # restore HEAD to that commit
You can also create a branch from a lost commit without moving HEAD:
git branch recovered <lost-sha>
Reflog entries expire after 90 days by default — act quickly if you lose something important.
What to learn next
Once you’re comfortable with all 14 levels:
| Topic | Command(s) |
|---|---|
| Bisecting bugs | git bisect start / bad / good / run |
| Worktrees | git worktree add |
| Submodules | git submodule add / update |
| Git hooks | .git/hooks/pre-commit, pre-push |
| Signing commits | git commit -S (GPG) |
| Shallow clones | git clone --depth 1 |
| Sparse checkout | git sparse-checkout |
Quick reference card
| Goal | Command |
|---|---|
| Start tracking | git init |
| Current state | git status |
| Stage file | git add <file> |
| Stage all | git add . |
| Snapshot | git commit -m "msg" |
| Compact history | git log --oneline |
| Branch graph | git log --oneline --graph --all |
| Who changed what | git blame <file> |
| Working dir diff | git diff |
| Staged diff | git diff --staged |
| Create+switch branch | git switch -c <name> |
| Switch branch | git switch <name> |
| Merge | git merge <branch> |
| Register remote | git remote add origin <URL> |
| Upload | git push |
| Download+apply | git pull |
| Download only | git fetch |
| Unstage | git restore --staged <file> |
| Discard changes | git restore <file> |
| Reverse a commit | git revert HEAD --no-edit |
| Undo commit (keep staged) | git reset --soft HEAD~1 |
| Wipe commit + changes | git reset --hard HEAD~1 |
| Shelve WIP | git stash |
| Restore WIP | git stash pop |
| Lightweight tag | git tag <name> |
| Annotated tag | git tag -a <name> -m "msg" |
| Rebase | git rebase <branch> |
| Interactive rebase | git rebase -i HEAD~N |
| Copy one commit | git cherry-pick <sha> |
| Safety net | git reflog |
The sandbox is yours to experiment in. Break things, reset with bash git-101.sh --reset, and start again — that is exactly how Git internalises.