I’ve struggled with my git workflows. In the past, I found that I have a bad habit of dramatically editing my projects when I really should fork them. I’ve been putting some effort into building better git habits to help me avoid these situations. I started off being familiar with git init, git commit and git push, but stash and other commands were beyond my grasp. I’ve done some prompt engineering to develop guardrails for the types of development decisions that should be handled with some of the more advanced git use cases. Maybe these will help you!
A couple of safety nets for immediate use:
- Always be able to undo:
- See anything you’ve done:
git reflog - Lightweight “save point”:
git tag backup-$(date +%Y%m%d-%H%M%S)
Portable snapshot (off-repo backup):git bundle create backup.bundle --all
- See anything you’ve done:
- WIP parking lot: prefer WIP commits on a throwaway branch over
stashwhen work will last more than a few minutes. You can do this with the following command:
# from anywhere with uncommitted changes
b="wip/$(date +%Y%m%d-%H%M%S)"; \
git switch -c "$b" && git add -A && git commit -m "WIP: parked" --no-verify && git switch -
1. “Am I rewriting the product?” → Fork vs Branch
- Use a fork (new repo) when:
- You’re changing project direction, licensing, or governance.
- You’ll diverge long-term from upstream (different roadmap) and want to pull upstream occasionally but not merge back regularly.
- You need independent release cadence and issue tracking.
- ✨ Tools:
git remote add upstream <url>, thengit fetch upstreamand selective cherry-picks back.
- Use a new branch (same repo) when:
- It’s still the same product, just a big feature or refactor.
- You want CI, PR review, and discoverability to stay in the same place.
- ✨ Tools:
git switch -c feature/refactor-auth, maybe behind a feature flag.
Quick rule: If you’d be uncomfortable merging it back “as-is,” consider a fork. If you’d merge it behind a flag after review, it’s a branch.
2) “Am I about to experiment wildly?” → Throwaway branch + worktree
- Create a scratch branch you can nuke anytime:
git switch -c spike/new-idea # or keep working tree separate so you don't juggle unstaged changes: git worktree add ../proj-spike spike/new-idea - If it works, cherry-pick useful commits onto a clean feature branch:
git log --oneline # find hashes git switch feature/refactor git cherry-pick <hash1> <hash2> - If it fails:
git switch main && git branch -D spike/new-idea && git worktree remove ../proj-spike
When to prefer git worktree: When you want two branches checked out simultaneously (e.g., bugfix and main) without stashing.
3) “My working tree is messy, I need to hop branches” → Stash vs WIP commit
- Use
stashfor quick context switches and truly throwaway partial work:git stash push -m "WIP: parser tweak" # saves staged+unstaged git switch main && git pull git switch feature/parser git stash pop # apply and drop (use `apply` to keep in stash)- Keep it organized:
git stash list,git stash show -p stash@{2} - Partial stash:
git stash -p
- Keep it organized:
- Use a WIP commit if:
Rule of thumb: Minutes → stash. Hours/days → WIP commit.
4) “I’ve started a big refactor on top of stale main” → Rebase early, merge late
- Keep your feature branch fresh to minimize painful conflicts later:
git fetch origin git rebase origin/main # replay your commits onto latest main # if conflicts: resolve, then git rebase --continue - Prefer rebase for private branches; prefer merge for shared/history-sensitive branches.
Guardrail: If the branch is already public and teammates might have based work on it, avoid rebasing it; use git merge origin/main.
5) “I need to land part of a large change safely” → Split & cherry-pick
- Break work into small, reviewable commits and land enabling changes first:
- Extract a pure “rename/move” commit (no logic change).
- Land new interfaces behind feature flags with no callers.
- Use
git cherry-pickto move those low-risk commits into separate PRs:git cherry-pick <hash> # keep author/date and exact diff
6) “I must keep risky code from reaching users” → Feature flags + release branches
- Main stays releasable; incomplete work guarded by flags.
- Release branches cut from main when stabilizing:
git switch -c release/1.4.0- Only bug fixes cherry-picked into release branch.
- Tag final release:
git tag -a v1.4.0 -m "Release 1.4.0" && git push --tags
7) “My history is noisy; I want it clean before merging” → Interactive rebase
- Squash fixups, rename messages, reorder commits:
git fetch origin git rebase -i origin/main # Use: pick / reword / squash / fixup - Use
--autosquashwithfixup!commits:git commit --fixup <hash> git rebase -i --autosquash origin/main
Guardrail: Only rewrite history on branches no one else has pulled.
8) “I need to find where a bug was introduced” → Bisect
git bisect start
git bisect bad HEAD
git bisect good v1.3.2 # or a known-good commit
# Git checks out midpoints; you run tests and mark them:
git bisect good | bad
git bisect reset
Automate with a test script: git bisect run ./ci/test.sh
9) “I want to share part of the repo or vendor another repo” → Subtree vs submodule
- Subtree (simple, self-contained code copy you occasionally sync):
- Pros: no extra checkout step for consumers; normal commits.
- Cons: merges can be larger; history mixed.
- Submodule (true nested repo):
- Pros: clean separation, track exact external revisions.
- Cons: extra steps for users/CI (
--recurse-submodules), more footguns.
Guardrail: If your consumers shouldn’t think about extra steps, prefer subtree.
10) “Repo is huge; I only need a slice” → Sparse checkout
git sparse-checkout init --cone
git sparse-checkout set src/api docs
Great for monorepos or to focus on one component.
11) Everyday branch hygiene (golden rules)
- Create a branch early for any work > 15 minutes.
git switch -c feature/<short-purpose> - Sync daily:
git fetch && git rebase origin/main(if private). - Commit small, purposeful changes with present-tense messages.
- Keep main green; hide incomplete features behind flags.
- Use throwaway spikes for experiments; keep or delete sans guilt.
- Tag releases and cut release branches for stabilization.
- Never rebase shared branches; merge instead.
Minimal command playbook (copy/paste friendly)
# Start a feature
git switch -c feature/login-oauth
# Work... then sync with latest main (private branch)
git fetch origin
git rebase origin/main
# Park work temporarily
git stash push -m "WIP: oauth redirect"
# or (longer): WIP commit
git add -A && git commit -m "WIP: oauth redirect not wired"
# Create a spike in a separate working directory
git worktree add ../proj-oauth-spike spike/oauth
# ...experiment...
git worktree remove ../proj-oauth-spike && git branch -D spike/oauth
# Prepare a clean history before PR
git rebase -i origin/main # squash/fixup
# Split out a safe helper into a separate PR
git cherry-pick <hash-of-helper-commit>
# Release flow
git switch -c release/1.5.0
git tag -a v1.5.0 -m "Release 1.5.0"
git push origin release/1.5.0 --tags
# Disaster recovery
git reflog # find the good state
git reset --hard <hash>
Helpful .gitconfig aliases (speeds up the guardrails)
[alias]
co = checkout
sw = switch
br = branch
st = status -sb
lg = log --oneline --decorate --graph --all
rb = rebase
rbi = rebase -i
fp = fetch --prune
pop = stash pop
ap = stash apply
aa = add -A
cm = commit -m
fix = commit --fixup
autosquash = !git rebase -i --autosquash
unstage = reset HEAD --
wip = !git add -A && git commit -m 'WIP'
What to do when you “feel the drift”
Use this quick decision tree:
- “This is becoming a different product/vision.” → Fork.
- “This is a big refactor or feature but same product.” → Feature branch, guard with flags.
- “I want to try something risky fast.” → Spike branch (ideally via
worktree), later cherry-pick. - “I must context-switch now.”
- Short: stash
- Long: WIP commit
- Short: stash
- “History is messy before merge.” → Interactive rebase (private only).
- “Need to ship, but not everything is ready.” → Release branch + cherry-pick fixes.
- “Bug appeared somewhere in history.” → bisect.
