Git Workflow & Cheat Sheet: A Practical Quick Reference
Git Workflow & Cheat Sheet: A Practical Quick Reference
Git has a hundred subcommands, but daily work runs on maybe fifteen of them. This is a guide to the workflow I actually use — plus a cheat sheet of the commands worth keeping within reach. Not the internals, not the theory. The keystrokes.
The Mental Model
Git has three places a change can live, and almost every command moves work between them:
Working Directory → Staging Area (Index) → Repository (commits)
(you edit) (git add) (git commit)
A fourth place — the remote — is a copy of the repository on a server. push sends commits up, fetch brings them down. Keep this picture in your head and most of Git stops being mysterious.
One-Time Setup
git config --global user.name "Charles Tang"
git config --global user.email "you@example.com"
# Use main as the default branch name for new repos
git config --global init.defaultBranch main
# Better diffs and a sane default editor
git config --global core.editor "code --wait"
git config --global pull.rebase true # rebase instead of merge on pull
# See your config
git config --listSetting pull.rebase true is opinionated but worth it: it keeps history linear instead of littering it with “Merge branch ‘main’” noise.
The Daily Loop
Ninety percent of days look like this:
git status # what's changed?
git pull # get the latest
git checkout -b feat/login # branch for the work
# ... edit files ...
git add -p # stage hunks interactively
git commit -m "Add login form validation"
git push -u origin feat/logingit add -p is underused. It walks you through each change hunk by hunk, so you commit exactly what you mean to — not the stray console.log you forgot to delete.
Staging & Committing
git add file.js # stage one file
git add . # stage everything in current dir
git add -p # stage interactively, hunk by hunk
git restore --staged file.js # unstage (keep the edit)
git commit -m "Message" # commit staged changes
git commit -am "Message" # stage tracked files + commit, one step
git commit --amend # rewrite the last commit
git commit --amend --no-edit # add to last commit, keep its message--amend is your friend for “oops, forgot a file” — but only before you push. Amending published history forces everyone else to deal with the rewrite.
Writing a Good Message
Add rate limiting to login endpoint
Brute-force attempts were unthrottled. Cap at 5 tries
per IP per minute, returning 429 after that.
Subject in the imperative (“Add”, not “Added”), under ~50 characters, then a blank line, then the why. The diff already shows the what.
Branching
git branch # list local branches
git branch -a # include remote branches
git checkout -b feat/x # create and switch
git switch feat/x # switch (modern, clearer)
git switch -c feat/x # create and switch (modern)
git branch -d feat/x # delete (safe — refuses if unmerged)
git branch -D feat/x # delete (force)
git branch -m old new # renameswitch and restore are the modern split of the overloaded checkout. switch changes branches; restore changes files. Use them — they’re harder to misfire.
Inspecting History
git log --oneline --graph --all # the view I keep open
git log -p file.js # history of one file, with diffs
git log --since="2 weeks ago"
git show <commit> # full detail of one commit
git diff # working dir vs staged
git diff --staged # staged vs last commit
git diff main..feat/x # between two branches
git blame file.js # who last touched each lineMake an alias for that first one — you’ll type it constantly:
git config --global alias.lg "log --oneline --graph --all --decorate"Syncing With the Remote
git fetch # download remote changes, don't apply
git pull # fetch + integrate into current branch
git push # upload commits
git push -u origin feat/x # push and set upstream (first time)
git push --force-with-lease # safe force push after a rebaseNever use plain git push --force. Use --force-with-lease instead: it refuses to push if someone else has added commits you haven’t seen, so you can’t silently clobber a teammate’s work.
Undoing Things
This is the part everyone fears. Most of it is reversible.
# Discard uncommitted changes to a file
git restore file.js
# Unstage a file (keep the edit)
git restore --staged file.js
# Undo the last commit, keep the changes staged
git reset --soft HEAD~1
# Undo the last commit, keep changes unstaged
git reset HEAD~1
# Nuke the last commit and its changes — destructive
git reset --hard HEAD~1
# Undo a commit by making a new, inverse commit (safe on shared history)
git revert <commit>The rule of thumb: reset rewrites history, revert adds to it. On a shared branch, always revert. reset --hard is the only one that throws work away — treat it with respect.
The Safety Net: reflog
Even after a reset --hard, your commits aren’t gone for ~30 days. reflog lists every position HEAD has been:
git reflog
# pick the hash from before the mistake
git reset --hard HEAD@{2}This has saved me more times than I’ll admit. If you think you lost a commit, check the reflog before panicking.
Stashing
For when you need to switch context with half-finished work:
git stash # shelve changes, clean working dir
git stash -u # include untracked files
git stash list # see your stashes
git stash pop # reapply most recent, remove from stack
git stash apply # reapply but keep in stack
git stash drop # delete a stashStash is a quick drawer, not a filing cabinet. If something lives in a stash for more than a day, it should be a branch.
Merge vs Rebase
Two ways to bring a branch up to date with main:
# Merge — preserves history exactly, adds a merge commit
git switch feat/x
git merge main
# Rebase — replays your commits on top of main, linear history
git switch feat/x
git rebase mainMerge keeps a true record of what happened, at the cost of a tangled graph. Rebase gives you a clean straight line, at the cost of rewriting your commit hashes.
My rule: rebase your own feature branch to keep it tidy before review; never rebase anything others have pulled. Rewriting shared history is how you ruin a Friday.
Cleaning Up Before Review
Interactive rebase lets you squash, reorder, and reword commits:
git rebase -i HEAD~4You get an editor with your last 4 commits:
pick a1b2c3d Add login form
squash e4f5g6h Fix typo
squash h7i8j9k Fix another typo
reword l0m1n2o Wire up validation
squash folds a commit into the one above it; reword lets you fix a message. Ship one clean commit instead of five messy ones.
Resolving Conflicts
A conflict isn’t an error — it’s Git asking you to make a decision it can’t:
<<<<<<< HEAD
const timeout = 3000;
=======
const timeout = 5000;
>>>>>>> feat/x
Edit the file to the version you want, delete the <<<, ===, >>> markers, then:
git add file.js # mark this conflict resolved
git rebase --continue # or: git merge --continue
git rebase --abort # bail out, back to where you startedWhen in doubt, --abort is always safe. It puts everything back.
A Team Feature-Branch Workflow
The process I follow on real teams:
# 1. Start fresh from main
git switch main
git pull
# 2. Branch for the work
git switch -c feat/user-export
# 3. Work in small commits
git add -p && git commit -m "Add CSV export service"
# 4. Push and open a PR
git push -u origin feat/user-export
# 5. Keep current with main while you work
git fetch origin
git rebase origin/main
# 6. Tidy history before review
git rebase -i origin/main
# 7. After the PR merges, clean up
git switch main
git pull
git branch -d feat/user-exportShort-lived branches, small commits, rebase to stay current, squash before merge. The longer a branch lives, the worse the conflicts — keep them measured in days, not weeks.
Common Gotchas
Committed to the wrong branch. Don’t panic. git reset --soft HEAD~1 to uncommit, git stash, switch to the right branch, git stash pop, commit again.
Pushed a secret. Removing it in a later commit isn’t enough — it’s still in history. You need git filter-repo (or BFG) to scrub it, a force-push, and you should rotate the secret regardless. Assume it’s compromised.
detached HEAD. You checked out a commit instead of a branch. Harmless if you’re just looking. If you made commits there, git switch -c rescue-branch to keep them before switching away.
pull keeps making merge commits. You haven’t set pull.rebase true. Set it, or use git pull --rebase.
Big file committed by accident. Once it’s in history it bloats every clone forever. Catch it early with a reset, or scrub it with filter-repo. Better: a .gitignore that’s right from commit one.
.gitignore Essentials
# Dependencies
node_modules/
# Build output
dist/
public/
# Environment & secrets
.env
.env.local
# Editor & OS cruft
.vscode/
.DS_Store
Add it in the first commit. Untracking something later (git rm --cached) works, but the file still sits in history.
The Cheat Sheet, Condensed
| Task | Command |
|---|---|
| Status | git status |
| Stage interactively | git add -p |
| Commit | git commit -m "..." |
| Fix last commit | git commit --amend |
| New branch | git switch -c feat/x |
| Switch branch | git switch main |
| Update from remote | git pull |
| Push first time | git push -u origin feat/x |
| Safe force push | git push --force-with-lease |
| Pretty history | git log --oneline --graph --all |
| Discard file edit | git restore file.js |
| Unstage file | git restore --staged file.js |
| Undo commit, keep work | git reset --soft HEAD~1 |
| Undo a shared commit | git revert <commit> |
| Shelve work | git stash / git stash pop |
| Recover lost commit | git reflog |
| Rebase onto main | git rebase main |
| Clean up commits | git rebase -i HEAD~N |
Closing Thought
Git rewards a small, consistent set of habits more than deep knowledge of its plumbing. Commit often and small. Branch for everything. Pull before you start. Write messages your future self can read. And remember the reflog exists — almost nothing in Git is truly lost, which means almost no mistake is worth panicking over.