The git commands worth knowing
The handful of git commands that carry a normal week — and the four that get you out of trouble when something has gone wrong.
Most days you need about eight git commands. The rest of git is there for the
afternoon when something has gone wrong, and that's the part worth learning
before you need it — hunting for reset flags with a broken branch in front of
you is a bad time to find out what --hard throws away.
This is the set I actually type, roughly in the order a change moves through it.
Seeing where you are
git status is fine, but the short form fits on one screen and tells you the
same thing:
git status -sb
-s is the short format — two columns, staged on the left, unstaged on the
right — and -b keeps the branch line so you still see how far ahead of the
remote you are.
For history, the full git log is unreadable. This is the version worth
aliasing:
git log --oneline --graph --decorate -20
One line per commit, the branch topology drawn on the left, tags and branch
names attached. Add --all when you want to see how your branch relates to
everything else rather than just its own ancestry.
And the distinction that trips people up early — two different diffs:
git diff # working tree vs staged: what you have NOT staged yet
git diff --staged # staged vs last commit: what your next commit will contain
If git diff prints nothing after you've staged everything, that's not a bug.
Staging with some care
Staging whole files is a habit worth breaking:
git add -p
It walks you through each hunk and asks. You end up splitting unrelated changes
into separate commits, and — more usefully — you read your own diff before it
becomes permanent. I catch a stray console.log this way most weeks.
To back out, restore is the modern pair, and it says what it does far better
than the old checkout -- and reset HEAD spellings did:
git restore --staged src/file.ts # unstage, keep the edits
git restore src/file.ts # discard the edits entirely
That second one has no undo. Git never had the change, so there is nothing to recover — it's the one command here that can genuinely lose work.
Committing
Nothing exotic, but two flags earn their place:
git commit -m "message"
git commit --amend # fold staged changes into the last commit
git commit --amend --no-edit # ... and keep the existing message
--amend is for the commit you made ten seconds ago that was missing a file.
It does not edit the old commit — it replaces it with a new one that has a new
hash. Harmless on a local commit. On a commit you've already pushed, it means a
force push, which brings us to the rule at the bottom of this post.
Undoing things
This is the part worth actually understanding, because the three resets differ in exactly one respect: how far back they drag your files.
git reset --soft HEAD~1 # undo the commit; changes stay staged
git reset HEAD~1 # undo the commit; changes stay, unstaged (--mixed, the default)
git reset --hard HEAD~1 # undo the commit and destroy the changes
--soft is the one you want after committing too early — it hands the changes
straight back to you, still staged, ready to recommit. --hard is the only
destructive one, and it's the reason people are scared of reset in general.
On a shared branch, don't reset at all. Reverse the change by adding a new commit instead:
git revert <commit>
revert makes a commit that undoes another commit. History grows rather than
changes, so nobody else's clone breaks. It's the correct way to undo something
that's already on main.
The safety net
If you have rewritten history and lost a commit, you have almost certainly not lost it:
git reflog
Every position HEAD has held is recorded here for about 90 days — including
the commits an amend or a hard reset "removed". Find the hash and get back to
it:
git reset --hard abc1234 # or: git switch -c recovered abc1234
Learn this one before you need it. It's the single reason a bad git accident is usually a five-minute problem.
Branches and pulling
git switch -c feature/thing # create and move to a new branch
git switch main # move to an existing one
switch was split out of checkout precisely because checkout did two
unrelated jobs — moving between branches, and throwing away file changes. Use
switch for branches and restore for files, and the ambiguity disappears.
For updating a branch, the default git pull creates a merge commit every time
the remote has moved on, which litters history on a busy repo:
git pull --rebase
Your local commits get replayed on top of what everyone else pushed, and you get a straight line instead. Set it as the default and forget about it:
git config --global pull.rebase true
Setting work aside
git stash push -m "half-done navbar"
git stash list
git stash pop
Always pass -m. A stash list of six entries called WIP on main is useless a
week later.
Stash is fine for a quick interruption. If you're going to be on the other branch for a while, a second working directory is better — same repo, same history, two checkouts on disk at once, no stashing at all:
git worktree add ../portfolio-hotfix main
Finding out why
Three commands answer nearly every "why is this line here" question:
git log -S "someFunction" --oneline # commits that added or removed that string
git blame src/file.ts # who last touched each line, and in which commit
git log --oneline -- src/file.ts # the history of one file
log -S — the pickaxe — is the one people don't know about. It searches
changes rather than commit messages, so it finds the commit that introduced a
string even when the message says nothing useful.
When the question is "which commit broke this", git bisect does a binary
search through history for you:
git bisect start
git bisect bad # current commit is broken
git bisect good v1.2.0 # this old one was fine
# git checks out the midpoint; test it, then say:
git bisect good # or: git bisect bad
git bisect reset # when you have your answer
Ten commits between good and bad means about three tests. A thousand means about ten.
The one rule
Rewriting history that only exists on your machine is free. Rewriting history other people have pulled means their next pull conflicts with itself, and someone loses work reconciling it.
So: amend, rebase and reset freely on your own unpushed branch. Once it's
on a shared branch, revert instead. If you genuinely must force-push a branch
that's yours alone, use the flag that checks nobody pushed while you weren't
looking:
git push --force-with-lease
Plain --force overwrites the remote whatever state it's in.
--force-with-lease refuses if the remote has commits you haven't seen. There
is no good reason to type the first one.