Most engineers learn git by accident — copy a snippet from Stack Overflow, run it, watch what happens. That works for the first few weeks. Then someone force-pushes over your branch, or a merge conflict resolution drops half a feature, and the gaps in your mental model start charging interest.
This post is the floor. Not every git command — just the ones you actually reach for in a normal week, organised by what you’re trying to do. The deeper bits (custom merge drivers, sparse checkouts, sub-trees) you can learn when you need them; you don’t need them yet.
TL;DR
- Git is a content-addressable store + a small graph of commits + three “places” (working tree, index, HEAD).
- Stop guessing at staging —
git statusandgit diffare your two most-used commands by an order of magnitude.- Branching is cheap, recovering is cheap, history is permanent.
git reflogwill save you more than any tutorial.- Use
rebase -ito clean up your own branch before merging; never to rewrite shared history.- Force-push only with
--force-with-lease. Plain--forceis how teams lose work.- Set up a handful of aliases (
co,st,lg) once; the time saved compounds.
The Mental Model in Three Pieces
Before any commands, three concepts that explain almost everything else.
| Concept | What it really is | Where it lives |
|---|---|---|
| Working tree | The files on disk you can edit. | Your folder. |
| Index (a.k.a. “staging area”) | A next commit being assembled. Files added to it are queued for the next git commit. | .git/index |
| HEAD | A pointer to the current branch tip (which is itself a pointer to a commit). | .git/HEAD |
Almost every command moves data between these three places. git add moves working tree → index. git commit moves index → new commit, advances HEAD. git restore moves backwards. Once you see this, the commands stop feeling random.
Setup and Inspect
The first commands of every project, and the ones you run literally every day.
| Command | What it does |
|---|---|
git config --global user.name "Your Name" | Set your name (once per machine). |
git config --global user.email you@example.com | Set your email (must match GitHub for verified commits). |
git config --global init.defaultBranch main | New repos start on main instead of master. |
git config --global pull.rebase true | git pull rebases by default — fewer noisy merge commits. |
git config --global push.autoSetupRemote true | First push to a new branch auto-creates the upstream tracking. |
git status | What’s changed, staged, untracked. Run constantly. |
git diff | Unstaged changes. |
git diff --staged (or --cached) | Staged changes. |
git log --oneline -10 | Last 10 commits, one line each. |
git show <hash> | A specific commit’s diff + metadata. |
# Worth doing once on a fresh machine
git config --global core.editor "code --wait" # or vim, helix, etc.
git config --global core.autocrlf input # macOS/Linux
git config --global core.autocrlf true # Windows
git config --global rerere.enabled true # remember conflict resolutions
git status and git diff are far and away the commands you’ll run most. Don’t try to remember what you changed — ask git.
Stage and Commit
Most “I broke git” stories start here.
| Command | What it does |
|---|---|
git add <path> | Stage a specific file or directory. |
git add -p | Patch-add — pick which hunks to stage. Best command in the entire toolkit. |
git add -u | Stage all modifications and deletions of tracked files (skip untracked). |
git commit -m "msg" | Commit staged changes with a one-line message. |
git commit | Open editor for a multi-line commit message — the right way for non-trivial changes. |
git commit --amend | Replace the last commit (squash in new staged changes, fix the message). |
git restore <path> | Discard unstaged changes in <path>. |
git restore --staged <path> | Unstage <path> (keep the working-tree changes). |
# The pattern most engineers want most of the time:
git add -p # interactively stage just the relevant hunks
git status # confirm what's about to commit
git commit # write a real message
git add -p is the single most under-used command. It teaches you to commit one logical change at a time, which makes review easier and history cleaner.
git commit --amend is fine only before you’ve pushed. Once a commit is on the remote, amending it rewrites history — which everyone else now has to deal with.
Branching
Branches are cheap. Make them.
| Command | What it does |
|---|---|
git branch | List local branches; current one starred. |
git branch -a | List local + remote branches. |
git switch <branch> | Switch to an existing branch. |
git switch -c <branch> | Create AND switch to a new branch. |
git switch - | Switch to the previously-checked-out branch (like cd -). |
git branch -d <branch> | Delete a merged branch (safe). |
git branch -D <branch> | Delete a branch even if not merged (dangerous, useful). |
git branch -m <new-name> | Rename current branch. |
Old commands you’ll see in tutorials:
git checkout -b feature/xandgit checkout main. They still work, butgit switch(for branches) andgit restore(for files) split the overloadedcheckoutinto two clear verbs. Prefer them.
# The everyday branch dance
git switch main
git pull # update local main
git switch -c feature/login-fix # branch off the latest
# ...work, commit, push...
git push # autoSetupRemote handles the upstream
Merging vs Rebasing
The two ways to integrate one branch into another. Both have their place.
git merge | git rebase | |
|---|---|---|
| What it does | Combines two branches with a merge commit recording the join. | Replays your branch’s commits on top of another. |
| History shape | Shows the actual history (branched, merged). | Linear, as if your work always sat on top of the latest. |
| Best for | Integrating completed features back into shared branches. | Cleaning up your own branch before sharing it. |
| Conflicts | Resolved once, in a single commit. | Potentially resolved per replayed commit. |
| Rule of thumb | Use on shared/protected branches. | Use on your private branch only — never rewrite shared history. |
# Merge a feature branch into main
git switch main
git pull
git merge --no-ff feature/login-fix # explicit merge commit; clear in history
# Rebase YOUR branch onto the latest main (keeps history linear)
git switch feature/login-fix
git fetch origin
git rebase origin/main
# resolve conflicts as they appear, then:
git rebase --continue
If a rebase goes sideways, git rebase --abort puts everything back exactly how it was. Stop using --continue when you’re confused; abort, breathe, try again.
Working with Remotes
Where your code goes to be other people’s problem.
| Command | What it does |
|---|---|
git clone <url> | Copy a remote repo locally. |
git remote -v | List configured remotes. |
git fetch | Download new commits/branches from remote without merging. |
git pull | git fetch + git merge (or rebase, if you set pull.rebase). |
git push | Send your local commits to the upstream branch. |
git push -u origin <branch> | First push of a new branch — sets upstream tracking. |
git push --force-with-lease | Force-push, but only if the remote hasn’t moved since you last fetched. |
Never use git push --force on a shared branch. It overwrites whatever is there, including commits a teammate just pushed. --force-with-lease is the safety-belted version: it will refuse if the remote tip isn’t what you expect. Use it.
# A safe force-push after rebase
git fetch origin
git rebase origin/main
git push --force-with-lease
Recovering From Mistakes
Almost any git mistake is recoverable as long as the commits weren’t garbage-collected (which takes ~30 days). The trick is git reflog.
| Command | What it does |
|---|---|
git reflog | Log of every move HEAD has made — including the commits “lost” by reset/rebase. |
git reset --hard <hash> | Set current branch (and working tree) to a specific commit. Destructive — discards uncommitted work. |
git reset --soft <hash> | Move branch tip; keep working tree and index intact. |
git reset --mixed <hash> (default) | Move branch tip; keep working tree, unstage everything. |
git revert <hash> | Create a new commit that undoes <hash>. Safe on shared history. |
git stash | Tuck away uncommitted changes; clean working tree. |
git stash pop | Re-apply (and remove) the most recent stash. |
git stash list | See all stashes. |
git restore --source=<hash> <path> | Pull a specific file’s contents from any commit. |
# I just rebased and lost everything!
git reflog # find the commit hash before the rebase
git reset --hard HEAD@{12} # restore — your branch is back
# I committed a bug; I need to revert without rewriting history
git revert <bad-commit-hash>
git push
# I'm in the middle of a feature and need to switch branches NOW
git stash
git switch hotfix-branch
# ...later...
git switch -
git stash pop
reset --hard is the most dangerous command in normal use. Always check git status first to confirm you have nothing uncommitted you care about — and remember reflog will still save you for ~30 days even if you forget.
History — Who Did What, When, Why
| Command | What it does |
|---|---|
git log --oneline --graph --all | Visualise all branches as a graph. Set this up as an alias. |
git log -p <path> | Full diff of every commit that touched <path>. |
git log -S "function_name" | All commits that added/removed a string. Pickaxe — invaluable. |
git log --grep="bug" | Commits whose message contains “bug”. |
git log --author="Alice" | Commits by Alice. |
git log --since="2 weeks ago" | Commits in the last 2 weeks. |
git blame <file> | Who last touched each line, and when. |
git bisect start | Begin a binary search across history for the commit that introduced a bug. |
git show <hash>:<path> | Print a file’s contents at a specific commit, without checking it out. |
# Find when a function was deleted
git log -S "calculateInvoice" -- src/billing.ts
# Find the commit that introduced a bug
git bisect start
git bisect bad # current HEAD is broken
git bisect good v1.4.2 # this older version works
# git checks out the midpoint; you test, then mark good or bad
git bisect good
git bisect bad
# ...repeat...
git bisect reset # done; back to where you started
Bisect is magical when the bug appeared “sometime in the last two weeks” and you don’t know where. Even with hundreds of commits, you’ll find it in ≤ log₂(N) steps.
Cleanup — Before You Open the PR
Don’t open a PR with 23 commits called “wip”, “fix”, “actually fix”, “lint”. Clean up first.
| Command | What it does |
|---|---|
git rebase -i HEAD~5 | Interactive rebase of the last 5 commits — squash, reorder, reword, drop. |
git commit --fixup=<hash> | Make a commit marked as a fixup of an earlier commit. |
git rebase -i --autosquash <base> | Rebase that auto-squashes --fixup commits into their targets. |
git clean -fd | Delete untracked files and directories. Irreversible — check with -n (dry run) first. |
git clean -fdx | Same, plus ignored files (e.g., node_modules, .env). |
# Clean up your branch before requesting review
git rebase -i origin/main # interactive: squash, reword, reorder
git push --force-with-lease
# The fixup workflow (clean as you go)
git commit -m "Add login form"
# ...later, fixing a bug in the same logical change...
git commit --fixup=HEAD~2
git rebase -i --autosquash HEAD~3
git rebase -i --autosquash is the workflow most professional engineers use: keep messy commits as you work, then squash them into clean logical units before opening the PR.
Things You Should Set Up Once
A handful of aliases pay back the time investment within a week.
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.sw switch
git config --global alias.br branch
git config --global alias.cm "commit -m"
git config --global alias.last "log -1 HEAD"
git config --global alias.lg "log --oneline --graph --decorate --all"
git config --global alias.unstage "reset HEAD --"
git config --global alias.amend "commit --amend --no-edit"
git config --global alias.fixup "commit --fixup"
git config --global alias.rb-onto "rebase -i --autosquash"
Now git st, git lg, git fixup HEAD~2, etc.
Two more global settings that earn their keep:
git config --global rerere.enabled true # remember conflict resolutions; replay them next time
git config --global merge.conflictStyle zdiff3 # 3-way conflict markers — much easier to read
Workflow Cheat Sheet — “I want to…”
| I want to… | Command |
|---|---|
| Start a fresh feature | git switch main && git pull && git switch -c feature/x |
| See what changed | git status then git diff (and git diff --staged) |
| Commit just one logical change | git add -p then git commit |
| Pull in main without a merge bubble | git pull --rebase (or set pull.rebase=true) |
| Update my feature branch with main | git fetch && git rebase origin/main |
| Undo my last commit but keep the changes | git reset --soft HEAD~1 |
| Throw away local work and match origin exactly | git fetch && git reset --hard origin/<branch> |
| Recover work I think I “lost” | git reflog then git reset --hard HEAD@{n} |
| Save WIP and switch branches | git stash && git switch other-branch |
| Find when line X was added | git log -S "X" -- path/to/file |
| Find who last touched a line | git blame path/to/file |
| Find the commit that broke things | git bisect start |
| Squash 5 messy commits into 1 clean one | git rebase -i HEAD~5 |
| Force-push safely after rebase | git push --force-with-lease |
| Revert a bug commit on shared history | git revert <hash> |
| Delete an old merged branch | git branch -d <branch> then git push origin --delete <branch> |
Things You Should Almost Never Do
| Command | Why |
|---|---|
git push --force | Use --force-with-lease instead. Plain --force overwrites teammates’ work. |
git reset --hard without git status first | Discards uncommitted changes. Always look first. |
git rebase on a shared branch | Rewrites public history; everyone else has to recover. Rebase your own branch only. |
git clean -fd without -n first | Deletes files that aren’t in any commit. Always do a dry run. |
git commit --amend after pushing | Same as rebasing public history — forces teammates to clean up. |
git checkout . | Deletes uncommitted work in tracked files with no warning. Prefer git restore (clearer verb) and look at git status first. |
Closing Checklist
When you sit down to work on a branch:
-
git status— know the starting state - Branch off the latest:
git switch main && git pull && git switch -c feature/x - Stage with
git add -pto commit one logical change at a time -
git commitwith a real message (subject + body, “why” not “what”) - Before opening the PR:
git rebase -i origin/main, squash messy commits, force-with-lease - On any reset/rebase:
git reflogis your safety net for ~30 days - Delete merged branches locally and on the remote when done
If anything ever goes sideways: git status, then git reflog, then walk back. You almost never need to nuke a clone.
Further Reading
- Pro Git (Chacon & Straub) — free at git-scm.com/book. The canonical reference; chapters 2–7 cover everything in this post in more depth.
git help <command>andgit help --all— the official docs are surprisingly readable.- Atlassian’s git tutorials — good visualisations for merge vs rebase.
- oh-shit-git.com — concise recovery recipes for every “I broke it” situation.
- Learn Git Branching — interactive sandbox for visualising what each command actually does to the commit graph.
Git’s reputation for being hard comes mostly from learning it command-by-command instead of model-first. Hold the working tree → index → HEAD picture in your head, run git status constantly, trust git reflog to bail you out — and the rest is just vocabulary.