Git, learned properly.
Snapshots, branches, and the shortcuts that make it fast.
Git is a time machine for your project. Every time you tell it to, it takes a snapshot of all your files — and it never forgets one. Once you understand three ideas — the snapshot, the three areas, and the branch — everything else is vocabulary. This guide builds those three ideas in order, with live playgrounds, then hands you the alias kit that makes daily Git nearly effortless.
The mental model: snapshots, not diffs
Most beginners imagine Git as a pile of "changes." It isn't. Every commit is a complete snapshot of your entire project at one moment, plus a little metadata: who made it, when, a message explaining why, and a pointer to the snapshot that came before it. Chain those pointers together and you get your project's history — a line of commits stretching back to the very first one.
Each commit gets a unique ID — a 40-character SHA-1 hash like a1f9e2c…. You almost never type the whole thing; the first 7 characters are enough. Those little hex labels in the sidebar? That's what commit IDs look like in the wild.
Three facts to internalize before touching the keyboard:
- Git is local. The entire history lives in a hidden
.gitfolder inside your project. Committing, branching, viewing history — none of it needs the network. GitHub is an optional copy of your repository, not the repository itself. - Commits are permanent-ish. Once something is committed, it is astonishingly hard to lose. Even "deleted" commits linger for weeks and can be recovered (see Undoing things). The corollary: commit early, commit often — a commit is a save point, and save points are free.
- Branches are just labels. A branch is a 41-byte file containing a commit ID. That's it. Creating one is instant and costs nothing, which is why Git users branch constantly and casually.
The save-point analogy. If you've ever played a game with manual saves, you already understand Git: git commit is "save game," branches are separate save slots, and git checkout loads one. The only new idea is that Git keeps every save you ever made, forever.
Install & one-time setup
On macOS, Git ships with Apple's Command Line Tools. If you've ever compiled anything, you already have it. Otherwise, one command triggers the installer:
# Check whether git is present (and its version)
git --version
git version 2.50.1 (Apple Git-155)
# If missing, this pops up the Command Line Tools installer
xcode-select --install
# Prefer a newer Git than Apple's? Homebrew has it:
brew install gitTell Git who you are
Every commit is stamped with an author. Set your identity once, globally, and Git uses it for every repository on the machine. While you're in there, set two more defaults that save friction later:
git config --global user.name "Stephen"
git config --global user.email "you@example.com"
# Name new repos' first branch "main" (matches GitHub)
git config --global init.defaultBranch main
# Pick your editor for commit messages (nano is beginner-friendly)
git config --global core.editor "nano"
# ...or, since you're an Emacs person:
git config --global core.editor "emacs"
# Colorized output everywhere (usually on by default)
git config --global color.ui auto
# See everything you've configured
git config --global --listAll of this lands in a plain-text file at ~/.gitconfig. You can edit it directly — and later in this guide you'll paste an alias block into it that turns long commands into two-letter shortcuts.
Your first repository: the core loop
Ninety percent of daily Git is a four-command loop: status → add → commit → repeat. Let's build a repository around your actual project — say, the static site in ~/Sites/mysite — and run the loop once, slowly.
cd ~/Sites/mysite
git init
Initialized empty Git repository in /Users/stephen/Sites/mysite/.git/That's the whole ceremony. Git created a hidden .git directory — the database where every snapshot will live. Your files are untouched; Git is simply watching now. Ask it what it sees:
git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
index.html
style.cssUntracked means Git has never been told these files matter. Stage them — that is, put them in the box that the next snapshot will photograph:
git add index.html style.css
# or stage everything in the current directory at once:
git add .
git status
Changes to be committed:
new file: index.html
new file: style.cssgit commit -m "Initial commit: site skeleton"
[main (root-commit) c2e88af] Initial commit: site skeleton
2 files changed, 48 insertions(+)Done — snapshot c2e88af exists forever. Now the loop: edit style.css, and git status reports it as modified. Stage it, commit it, and you have a second save point. That rhythm — work, status, add, commit — is the heartbeat of everything else in this guide.
The shortcut you'll use most: git commit -am "message" stages every already-tracked modified file and commits in one step. It does not pick up brand-new files — those still need an explicit git add once.
If you run git commit without -m, Git opens your editor for the message. In vim (a common default) that surprises people: type your message, then press Esc and type :wq to save and exit. Setting core.editor to nano or emacs in the setup section avoids the ambush entirely.
The three areas: why add exists at all
Newcomers often ask why committing takes two steps. The answer is Git's most distinctive design decision: between your files and the permanent history sits a middle layer called the staging area (also "the index"). It's a loading dock where you assemble exactly the snapshot you want, before making it permanent.
Working directory
Your real files, as they are right now. Messy. In progress.
Staging area
The next snapshot, under construction. You choose what goes in.
Repository
Permanent history. Snapshots that can always be recovered.
Why bother? Because real work is messy. You fixed a bug and half-finished a new feature in the same afternoon. The staging area lets you add just the bug-fix files and commit them as one clean, focused snapshot — leaving the half-finished feature uncommitted until it's ready. Focused commits are what make history readable and reversible.
Try it below. Edit files, stage some of them, commit, and watch changes flow left to right through the three areas.
Playground · The three areas
interactiveWorking directory
Staging area
Repository
Seeing what's where: the two diffs
With three areas come two gaps, and Git gives you a diff for each:
# Working directory vs. staging area — "what have I changed but not staged?"
git diff
# Staging area vs. last commit — "what exactly will my next commit contain?"
git diff --staged
# Unstage a file you added by mistake (file itself is untouched)
git restore --staged style.cssReading history
A history you can't read is a history you can't use. git log is the raw tool; the trick is knowing the three or four flag combinations that turn its firehose into something scannable.
# Full detail: hash, author, date, message — one screen per commit
git log
# One line per commit — the everyday view
git log --oneline
e5a7d90 Add contact page
d94c1b6 Style the navigation bar
c2e88af Initial commit: site skeleton
# The full picture: every branch, drawn as an ASCII graph
# (this becomes indispensable once you branch — alias it!)
git log --oneline --graph --all --decorate
# Only what touched one file, with the actual changes shown
git log -p --follow style.css# Show everything about one commit (message + full diff)
git show d94c1b6
# Who last touched each line of a file, and in which commit?
git blame index.htmlHEAD, which you'll see everywhere, simply means "the commit you're standing on right now" — almost always the latest commit of your current branch. HEAD~1 is its parent, HEAD~2 its grandparent, and so on.
Branching & merging: Git's superpower
A branch lets you leave the main line of history untouched while you experiment on a parallel one. Redesigning your site's CSS? Branch. Trying a risky refactor of a Perl script? Branch. If it works, merge it back; if it doesn't, delete the branch and main never knew. Because a branch is just a movable label pointing at a commit, all of this is instant.
# Create a branch AND switch to it (the everyday command)
git switch -c new-navbar
# ...edit, add, commit as usual. Commits now land on new-navbar...
git commit -am "Rebuild navbar as flexbox"
# See all branches (* marks where you are)
git branch
main
* new-navbar
# Jump back to main — your working files instantly change to match!
git switch main
# Happy with the branch? Merge it into main, then tidy up
git merge new-navbar
git branch -d new-navbarOlder tutorials use git checkout -b name and git checkout main for these jobs. They still work — switch (2019+) is just the clearer, modern spelling. checkout survives because it also does other, unrelated things, which is exactly why it was split up.
The two shapes of a merge
- Fast-forward. If
mainhasn't moved since you branched, Git just slides themainlabel forward to your branch's tip. No new commit; history stays a straight line. - Merge commit. If both branches gained commits, Git weaves them together with a new commit that has two parents. The graph shows a genuine fork and rejoin.
When both branches edit the same line: conflicts
Conflicts sound scary; they're actually just Git refusing to guess. It pauses the merge and marks the disputed lines directly in the file:
git merge new-navbar
CONFLICT (content): Merge conflict in style.css
Automatic merge failed; fix conflicts and then commit the result.
# Inside style.css you'll find markers like this:
<<<<<<< HEAD
nav { background: #14161a; } ← main's version
=======
nav { background: #1c1f26; } ← the branch's version
>>>>>>> new-navbar
# 1. Edit the file: keep what you want, DELETE all marker lines
# 2. Then tell Git it's resolved:
git add style.css
git commit # Git supplies a merge message
# Panicking? Abort and return to exactly before the merge:
git merge --abortNow drive the graph yourself. Commit, branch, switch, and merge below — the DAG (directed acyclic graph, Git's actual data structure) redraws live, and the merge button demonstrates both fast-forward and true merges depending on the shape you've built.
Playground · Commit graph
interactiverepo initialized
Undoing things: the safety net
This is the section that turns anxiety into confidence. Git has a graduated set of undo tools, from "fix a typo in my last commit message" to "recover a branch I deleted an hour ago." Learn them in order of severity.
| You want to… | Command | Danger level |
|---|---|---|
| Discard unstaged edits to a file | git restore file.css | ⚠ destroys uncommitted work in that file |
| Unstage a file (keep the edits) | git restore --staged file.css | safe |
| Fix the last commit's message / add a forgotten file | git commit --amend | safe locally; avoid after pushing |
| Undo a commit publicly (make a new commit that reverses it) | git revert a1f9e2c | safe — history only grows |
| Move the branch back, keep changes staged | git reset --soft HEAD~1 | safe — nothing is lost |
| Move back, keep changes as unstaged edits (default) | git reset HEAD~1 | safe — nothing is lost |
| Erase the last commit and all its changes | git reset --hard HEAD~1 | ⚠⚠ working files overwritten |
| Recover after any disaster | git reflog | read-only — pure rescue |
Amend: the polish tool
# Typo in the message?
git commit --amend -m "Style the navigation bar"
# Forgot to include a file? Stage it, then fold it in silently:
git add forgotten.css
git commit --amend --no-editReflog: the true safety net
Here is the fact that makes Git nearly loss-proof: the reflog records every place HEAD has ever been on your machine — every commit, switch, reset, and merge — for ~90 days, even for commits no branch points to anymore. "Destroyed" work is usually one reflog lookup away:
git reflog
3b91c60 HEAD@{0}: reset: moving to HEAD~1 ← the mistake
0a9fc37 HEAD@{1}: commit: Add contact form ← the "lost" commit!
e5a7d90 HEAD@{2}: commit: Add contact page
# Point the branch right back at it — fully restored
git reset --hard 0a9fc37The one real rule: never rewrite history you've already pushed and others may have pulled (--amend, reset on shared branches). For anything public, use git revert — it undoes by adding a commit, so everyone's history stays consistent. Solo repos: rewrite freely.
Remotes & GitHub
Everything so far lived on your Mac. A remote is a copy of the repository somewhere else — GitHub, GitLab, another machine — that you synchronize with. By convention your primary remote is named origin. Two verbs move commits across the wire: push (send yours up) and pull (bring theirs down).
Starting from either direction
# On github.com: New Repository → name it → create (add nothing to it)
# Then connect and push:
git remote add origin git@github.com:stephen/mysite.git
git push -u origin main
# -u links local main ↔ origin/main; afterwards plain `git push` sufficesgit clone https://github.com/octocat/Spoon-Knife.git
cd Spoon-Knife
# clone = copy the entire history + set up origin automaticallyThe daily sync
# Before starting work: get the latest
git pull
# ...work, add, commit locally as many times as you like...
# When ready to share:
git push
# pull = fetch (download) + merge (integrate). To only look first:
git fetch
git log --oneline main..origin/main # what's new upstream?Auth note: GitHub no longer accepts account passwords over HTTPS. Either use SSH keys (generate with ssh-keygen -t ed25519, paste the .pub file into GitHub → Settings → SSH keys) or install the GitHub CLI and run gh auth login, which handles everything.
The classic collaboration flow — a pull request — is just branching over the network: push a feature branch (git push -u origin new-navbar), open a PR on GitHub, discuss, and merge there. Your branching skills transfer one-to-one.
Stash & .gitignore: the two conveniences
git stash — the pocket
Mid-edit with a messy working directory, and you suddenly need to switch branches? stash sweeps all uncommitted changes into a pocket, leaving the directory clean. Pop them back out whenever:
git stash # pocket everything; directory now clean
git switch main # go do the urgent thing…
git switch new-navbar # …come back
git stash pop # changes restored, stash removed
git stash list # stashes stack — see them all
git stash push -m "half-done navbar" # label one for later.gitignore — the bouncer
Some files should never be committed: OS litter, build output, editor droppings, secrets. List patterns in a file named .gitignore at the repo root and Git stops even mentioning them. A solid starter for Mac-based web work:
# macOS
.DS_Store
# Editors
*~
\#*\#
.vscode/
# Dependencies & build output
node_modules/
dist/
*.log
# Secrets — never commit credentials
.envCommit the .gitignore itself — it's part of the project. If a file was already committed before you ignored it, remove it from tracking (keeping the file on disk) with git rm --cached .DS_Store, then commit.
Shortcuts & aliases: making Git fast
Fluent Git users don't type faster — they type less. Three layers of shortcuts stack on each other: Git's built-in abbreviations, Git aliases, and shell aliases.
Layer 1 · built-in abbreviations you already have
| Shortcut | Expands to | Notes |
|---|---|---|
git commit -am "msg" | add all tracked + commit | the workhorse; skips new files |
git switch - | switch to previous branch | like cd - — toggle between two branches |
git add -p | stage hunks interactively | y/n through each change; surgical commits |
git commit --amend --no-edit | fold staged changes into last commit | the "oops, one more thing" |
HEAD~1, HEAD~2 | 1, 2 commits before HEAD | works anywhere a commit ID does |
a1f9e2c | full 40-char hash | any unique prefix (≥4 chars) works |
git log -3 | last 3 commits only | any number |
Layer 2 · Git aliases (the big one)
Git lets you define your own subcommands. Paste this block into ~/.gitconfig — it's a curated, battle-tested kit, and every alias below is used in anger by working developers:
[alias]
# — the daily four —
st = status -sb # compact status w/ branch info
co = switch # git co main
cm = commit -m # git cm "message"
aa = add -A # stage everything, incl. new files
# — history, readable —
lg = log --oneline --graph --decorate -15
lga = log --oneline --graph --decorate --all
last = log -1 HEAD --stat # what did I just commit?
# — branches —
br = branch
new = switch -c # git new feature-x
# — safety & fixing —
unstage = restore --staged
oops = commit --amend --no-edit
undo = reset --soft HEAD~1 # un-commit, keep everything staged
# — remotes —
pu = push
pl = pull --ff-only # pull, but never create surprise mergesOr install any single alias from the command line, no editing required:
git config --global alias.lg "log --oneline --graph --decorate -15"The payoff, in one before/after:
# Before # After
git status git st
git switch -c fix-footer git new fix-footer
git add -A git aa
git commit -m "Fix footer overflow" git cm "Fix footer overflow"
git log --oneline --graph --decorate git lg
git switch main && git merge fix-footer git co - && git merge fix-footerLayer 3 · shell aliases (zsh)
For the absolute maximum laziness, alias at the shell level in ~/.zshrc so even the word git disappears:
alias g='git'
alias gs='git status -sb'
alias gl='git log --oneline --graph --decorate -15'
alias gp='git push'
# Reload: source ~/.zshrc — now it's just g cm "message"macOS's zsh ships with a huge git tab-completion system. Type git sw⇥ to complete switch, or git switch ⇥⇥ to list branch names. If completion seems inert, add autoload -Uz compinit && compinit to ~/.zshrc.
Drill it: command reflexes
Fluency is recognition speed. Ten quick situations — pick the command you'd reach for:
Playground · What do you type?
interactiveGood habits: the taste layer
- Commit one idea at a time. "Fix navbar overflow" is a commit. "Fix navbar, add contact page, tweak fonts" is three. Small commits make
revert,blame, and code review all work dramatically better.git add -pexists precisely to untangle mixed work. - Write messages in the imperative: "Add contact form", not "added contact form". Convention: a summary line ≤ 50 characters; if more explanation is needed, a blank line and then paragraphs. The summary answers what; the body answers why.
- Run
git statusbefore and after everything. It's free, it's instant, and it always tells you exactly where you stand — most beginner confusion evaporates on contact withstatus. - Branch for anything nontrivial. The cost is one command; the benefit is that
mainalways works. - Pull before you push on shared repos; integrate others' work locally, where conflicts are easy to fix.
- Never commit secrets. API keys and passwords live in
.env-style files listed in.gitignore. History is forever — a pushed secret must be treated as leaked and rotated.
Where to go deeper: the free official book Pro Git (chapters 1–3 cover everything here with more theory), and git help <command> for any command's full manual — try git help log.
Cheat sheet
The whole guide on one screen. Print-worthy.
Core loop
- git init
- start tracking this folder
- git status
- where am I? (run constantly)
- git add <f> / .
- stage file(s) for next snapshot
- git add -p
- stage change-by-change
- git commit -m ""
- take the snapshot
- git commit -am ""
- stage tracked + commit
- git diff / --staged
- unstaged / staged changes
Branch & merge
- git branch
- list branches (* = current)
- git switch -c x
- create branch x, jump to it
- git switch x / -
- go to x / toggle previous
- git merge x
- fold x into current branch
- git merge --abort
- back out of a conflict
- git branch -d x
- delete merged branch
- git stash / pop
- pocket / restore WIP
Undo & rescue
- git restore f
- discard unstaged edits ⚠
- git restore --staged f
- unstage, keep edits
- git commit --amend
- fix last commit
- git revert <id>
- safe public undo
- git reset HEAD~1
- un-commit, keep edits
- git reset --hard <id>
- jump branch here ⚠⚠
- git reflog
- everywhere HEAD has been
History & remotes
- git log --oneline
- compact history
- … --graph --all
- full branch picture
- git show <id>
- one commit in detail
- git clone <url>
- copy a remote repo
- git remote add origin
- connect to GitHub
- git push -u origin main
- first push (links branches)
- git pull / push
- sync down / up
- git fetch
- download without merging