Skip to content
SK
← All posts
gitworkflow

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.