Git Workflow
Git is the single tool you’ll use most. Get comfortable — the better you are at Git, the less anxious you’ll be about breaking things.
The core promise of Git: almost nothing is actually destructive. Commits stick around for 90 days in the reflog even after you “lose” them. Breathe. Then recover.
Mental model (the 30-second version)
Four places your code lives:
- Working directory — the files you see.
- Staging area (index) — changes added with
git add. - Local repo — committed history on your machine.
- Remote (GitHub) — the shared truth.
git add moves working → index. git commit moves index → local. git push moves local → remote. git pull is the reverse of push.
If you internalize those four arrows you’ll never be confused about where your changes are.
Daily commands
This is 95% of what you’ll use:
git status # what's changed
git diff # unstaged changes
git diff --staged # staged changes
git add <file> # stage a specific file
git add -p # stage chunk-by-chunk (use this!)
git commit -m "feat: add X" # commit staged changes
git push # send to remote
git pull --rebase # fetch + replay your commits on top
git log --oneline -20 # recent history
git switch -c feat/my-branch # create + switch branch
git switch main # switch to main
git add -p (patch mode) is the single best habit you can build. It forces you to read every change before committing — huge for review quality and catches stray console.logs.
Branching strategy
We do trunk-based development.
mainis always deployable.- Short-lived feature branches cut from
main. - Typical lifespan: hours to a couple of days. If a branch is alive for a week, you’re doing something wrong — split it.
- Merge back via Pull Request with squash merge.
Branch naming
type/short-slug — lowercase, hyphens:
feat/client-invoicesfix/auth-redirect-looprefactor/extract-db-clientchore/bump-node-22docs/update-readme
Add a ticket id if you have one: feat/ENG-142-client-invoices.
Commits
Conventional Commits
Every commit’s subject line starts with a type:
| Type | When | Example |
|---|---|---|
feat: | New user-facing capability | feat: add invoice export |
fix: | Bug fix | fix: auth redirect loops on expired token |
refactor: | Internal change, no behavior change | refactor: extract DB client into lib/db |
chore: | Tooling, deps, ops | chore: bump Next.js to 15.4 |
docs: | Docs only | docs: update onboarding checklist |
test: | Tests only | test: add integration test for /api/invoices |
perf: | Performance change | perf: cache expensive invoice query |
Subject line
- ≤ 72 characters.
- Imperative mood: “add X”, not “added X” or “adds X”. Read it as “If applied, this commit will ___”.
- No period at the end.
Body (optional)
Skip it for trivial changes. Write one when the why isn’t obvious:
fix: cache invoice list to unblock dashboard
The dashboard loads the invoice list on mount, which was
triggering a full-table scan on every page load. Dropped p95
from 2.3s to 80ms by caching for 60s — safe because invoices
are only created from the /new flow.
Don’t repeat what the diff already says. Explain context, tradeoffs, alternatives considered.
Before you commit
Always:
git status— know what’s staged.git diff --staged— read what you’re about to ship.- Did you stage something you didn’t mean to?
git restore --staged <file>.
Rebasing vs merging
We prefer rebase for keeping your branch current:
git fetch origin
git rebase origin/main
This replays your commits on top of the latest main. Result: clean linear history on your branch.
Don’t git merge main repeatedly into your branch. It creates merge commits that pollute review.
Never rebase shared branches. Rebasing rewrites history. If someone else has pulled your branch, rebasing will wreck their clone. Rule of thumb: only rebase branches that only you have touched.
Merging conflicts
After a rebase you’ll sometimes hit conflicts. The flow:
git rebase origin/main
# CONFLICT in app/page.tsx
- Open the file. Find the
<<<<<<<,=======,>>>>>>>markers. - Decide what the resolved code should be. Delete all three markers.
git add app/page.tsxgit rebase --continue- Repeat per conflict.
If you panic: git rebase --abort returns you to the state before you started. Nothing lost.
Squash merge
On GitHub, our merge button is set to squash and merge. Your whole branch becomes one commit on main.
- Your messy
wip,more wip,fix testcommits don’t pollute history. - The squashed commit’s message = your PR title + description.
- Write good PR titles. They become the commit log on
main.
Pull requests
Before you open one
- Rebased on latest
main. - Tests pass locally.
-
git loglooks reasonable (or you’ll squash anyway — fine). - You’ve self-reviewed the diff on GitHub (not just your editor).
Opening the PR
- Title: conventional-commit style.
feat: add invoice export. - Description: explain why. Link the ticket. Mention tradeoffs and what you skipped.
- Screenshots / screen recordings for UI changes. Use CleanShot or Loom.
- Draft PR if it’s not ready — opens conversation without requesting review.
PR template
If a project has a .github/PULL_REQUEST_TEMPLATE.md, GitHub fills it in automatically. Our default:
## Why
<1–3 sentences>
## What changed
<bullet list if non-obvious>
## How to verify
<steps a reviewer can actually run>
## Notes
<tradeoffs, follow-ups, things I skipped>
Closes #<ticket>
Size
Aim for < 400 changed lines. Bigger PRs get shallower reviews. If it must be big:
- Split into multiple PRs that stack.
- Feature-flag half-finished work.
- Narrate what to look at first.
Reviewers
- Default: 1 reviewer.
- Touching auth, payments, migrations, or infra → request a senior explicitly.
- Don’t ping the whole channel. Request 1–2 specific people.
Pulling down someone else’s PR
gh pr checkout 142 # via GitHub CLI
Browsing a PR locally is 10× better than reviewing in the browser for anything non-trivial. Run it. Poke at it.
Recovering from mistakes
Git almost never actually loses work. Your tools:
I committed to the wrong branch
git log # find your commit hash
git switch correct-branch
git cherry-pick <hash>
git switch wrong-branch
git reset --hard HEAD~1 # only if the commit was only on this branch
I committed something secret (API key, password)
- Rotate the secret immediately. Anything that touched git is compromised forever.
- Then clean the history:
git rebase -i <parent>, drop the commit. Or usegit filter-repo. - Force-push the branch (only if it’s your own branch, never
main).
I need my last commit back after a reset
git reflog # shows every HEAD move — your undo stack
git reset --hard HEAD@{2} # jump back to where you were
git reflog is the nuclear undo. It remembers 90 days. Almost nothing is truly gone.
I force-pushed over someone else’s work
Apologize. Then:
git reflog # on the other person's machine
git reset --hard <pre-push-hash>
git push --force-with-lease origin <branch>
Don’t force-push to branches other people share.
Never do these
- Never
git push --forcetomain. Branch protection should block you — if it doesn’t, fix the protection. - Never
git commit --amendon a commit already pushed to a shared branch. - Never
git rebasea branch someone else has pulled. - Never commit
.envfiles, even once. Rotate the secrets the moment you do. - Never
git add .without reviewing. Usegit add -por stage specific files.
Branch protection (what’s on main)
Every repo has these settings enforced:
- No direct push to
main. - PR required.
- At least 1 approval.
- CI must pass (typecheck, lint, test, build).
- Branch must be up to date with
mainbefore merge. - Stale approvals dismissed on new commits.
- No force push to
main, ever.
If any of these are off in a repo, open a PR to fix it. They exist for real reasons we’ve learned the hard way.
Quality-of-life
Global .gitignore
Set one for editor/OS junk so you don’t pollute project .gitignore files:
git config --global core.excludesFile ~/.gitignore_global
Content:
.DS_Store
.vscode/
.idea/
*.swp
Aliases that pay for themselves
git config --global alias.co checkout
git config --global alias.sw switch
git config --global alias.st status
git config --global alias.lg "log --oneline --graph --decorate -30"
git config --global alias.amend "commit --amend --no-edit"
GitHub CLI
gh makes a lot of this faster:
gh pr create --fill # creates PR from your branch
gh pr checkout 142 # check out PR #142 locally
gh pr view --web # open current branch's PR in browser
gh pr status # PRs that need your attention
gh run watch # watch the CI run
Install: brew install gh or your package manager.
When in doubt
Copy the repo somewhere before doing anything scary:
cp -r my-project my-project.backup
Then experiment in the original. Git undo is powerful but your nerves are limited — a 30-second copy is cheaper than a panic spiral.