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:

text
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

bash
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 --list

Setting 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:

bash
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/login

git 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

bash
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

text
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

bash
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       # rename

switch 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

bash
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 line

Make an alias for that first one — you’ll type it constantly:

bash
git config --global alias.lg "log --oneline --graph --all --decorate"

Syncing With the Remote

bash
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 rebase

Never 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.

bash
# 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:

bash
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:

bash
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 stash

Stash 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:

bash
# 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 main

Merge 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:

bash
git rebase -i HEAD~4

You get an editor with your last 4 commits:

text
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:

text
<<<<<<< HEAD
const timeout = 3000;
=======
const timeout = 5000;
>>>>>>> feat/x

Edit the file to the version you want, delete the <<<, ===, >>> markers, then:

bash
git add file.js             # mark this conflict resolved
git rebase --continue       # or: git merge --continue
git rebase --abort          # bail out, back to where you started

When in doubt, --abort is always safe. It puts everything back.

A Team Feature-Branch Workflow

The process I follow on real teams:

bash
# 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-export

Short-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

gitignore
# 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.