Workflows & Recovery
Rebase, cherry-pick, stash, reflog. GitHub PRs and reviews.
Once basic git stops scaring you, the next level unlocks: rewriting history, recovering from disasters, and working with other humans on the same codebase. This lesson is the "intermediate to senior" leap.
Stashing: parking work in progress
You're halfway through a feature when an urgent bug report comes in. You can't commit half-broken code, and you don't want to lose your changes. git stash tucks them away.
git status
# Changes not staged for commit:
# modified: src/checkout.ts
git stash push -m "WIP: refactoring checkout"
# Saved working directory and index state On feature/checkout: WIP: refactoring checkout
git status
# nothing to commit, working tree clean
# ... fix the urgent bug, switch branches, do whatever ...
git stash list
# stash@{0}: On feature/checkout: WIP: refactoring checkout
git stash pop # apply the most recent stash and remove it
# (or 'git stash apply' to keep the stash around)Cherry-pick: grab one commit from another branch
You shipped a hotfix on main. The same fix is needed on arelease/v3branch. Don't rewrite it, just cherry-pick it.
git switch release/v3
git cherry-pick b4f8a91
# [release/v3 c9d2e10] Fix off-by-one in pagination
# 1 file changed, 2 insertions(+), 2 deletions(-)Cherry-picking copies the commit's changes onto your current branch as a new commit (different SHA, same content). If there are conflicts, resolve them, then git cherry-pick --continue.
Interactive rebase: clean up before pushing
While iterating on a feature, you'll make commits like "wip, wip2, fix typo, oops, actual feature." Before opening a PR, you want to squash and reorder these into a clean story. git rebase -i is the surgery tool.
git rebase -i main
# opens your editor with something like:
#
# pick a1b2c3d wip add api skeleton
# pick e4f5g6h wip2 finish api
# pick i7j8k9l fix typo
# pick m1n2o3p oops, missing import
# pick q4r5s6t feature complete: user auth
#
# Commands:
# p, pick = keep commit as-is
# r, reword = keep commit, edit message
# s, squash = merge into previous commit, keep both messages
# f, fixup = like squash, but discard this message
# d, drop = remove commit
#
# (rearrange lines to reorder commits)Change those picks to squash or fixup to merge commits together, reword to edit a message, or rearrange the lines to reorder. Save and close. Git replays the commits per your instructions.
main.Reflog: git's undo log
Here's a secret that will save you one day: git almost never throws anything away. Every move HEAD makes is logged in the reflog, even commits that look "lost."
git reflog
# b4f8a91 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
# c9d2e10 HEAD@{1}: commit: Big mistake commit
# d4f8a01 HEAD@{2}: commit: Important work
# e5a9b22 HEAD@{3}: checkout: moving from main to feature/x
# panic: you ran 'git reset --hard' and lost commits
git reset --hard d4f8a01 # back to "Important work" from the refloggit reflog.Recovering from a bad reset (a real scenario)
You ran git reset --hard HEAD~3thinking it would only undo locally, but it threw away three commits of work you actually needed. Here's the recovery:
# step 1: find the commit you want back
git reflog
# 8a1c0f2 HEAD@{0}: reset: moving to HEAD~3 <-- the bad action
# 9d2e10a HEAD@{1}: commit: WIP partial fix <-- want this back
# c4f8a91 HEAD@{2}: commit: Working feature <-- and this
# ...
# step 2: jump back to the good state
git reset --hard 9d2e10a
# or, if you want a softer move (keep them as uncommitted changes)
git reset --soft 9d2e10a
# done - your commits are backForce pushing: the dangerous one
After a rebase, your local history doesn't match the remote anymore, so a plain git push will be rejected. You need to overwrite the remote. The naive command is git push --force. Don't use it. Use the safe variant:
git push --force-with-lease
# pushes only if the remote is exactly where you last saw it--force-with-lease protects you against this nightmare: a teammate pushed to your branch while you were rebasing, and --force would erase their work. With --force-with-lease, git refuses and you get a chance to pull and rebase again.
git revert) instead.The GitHub Pull Request culture
Most teams don't merge directly to main. The flow looks like this:
- Branch off main (
git switch -c feature/x). - Make commits, push the branch.
- Open a Pull Request (PR) on GitHub.
- CI runs your tests. Teammates review the diff and leave comments.
- You push fixes. CI runs again. Reviewers approve.
- You (or a bot) merges the PR. The branch can be deleted.
This isn't bureaucracy. It's how a team of strangers can ship software without breaking each other's work. The PR is the artifact: a discussion, a diff, a CI result, all in one place.
The gh CLI: GitHub from the terminal
ghis the official GitHub CLI. It moves PR work out of the browser tab. Some of the moves you'll use constantly:
gh auth login # one-time setup
gh pr create --fill # open a PR from current branch
gh pr list # see open PRs
gh pr view 1234 # view PR #1234 in your terminal
gh pr checkout 1234 # check out a PR locally to review
gh pr merge 1234 --squash # merge it
gh pr diff 1234 # see the diff
gh issue create --title "Bug: ..." --body "..."
gh repo clone vercel/next.js
gh run watch # follow CI for the current PRConventional Commits: messages with meaning
A loose convention many teams adopt: type(scope): short description. It makes history skimmable and enables tools to auto-generate changelogs and version numbers.
# common types: feat, fix, docs, refactor, test, chore, perf, ci
git commit -m "feat(auth): add password reset flow"
git commit -m "fix(checkout): handle empty cart"
git commit -m "docs: update README with deploy steps"
git commit -m "refactor(api): extract user repo into module"
git commit -m "chore(deps): bump next to 15.1.0"
# add a "!" or "BREAKING CHANGE:" footer for breaking changes
git commit -m "feat(api)!: rename /users to /accounts"Reverting vs resetting
Two ways to undo, with very different implications:
- git revert <sha> creates a new commit that undoes the changes from the target commit. History is preserved. Safe on shared branches.
- git reset <sha> moves HEAD backwards, rewriting history. Only safe on your local, unshared branches.
# bad commit went out to production - undo it safely
git revert HEAD
# [main d8a1c2e] Revert "Add broken feature"
# Reset has three modes
git reset --soft HEAD~1 # undo the commit, keep changes staged
git reset --mixed HEAD~1 # undo the commit, keep changes unstaged (default)
git reset --hard HEAD~1 # undo the commit AND discard changes (dangerous)Quick quiz
You accidentally ran 'git reset --hard' and lost three commits. What's the first command to try?
Recap
git stashparks WIP.git cherry-pickgrabs one commit from another branch.git rebase -icleans up local history before pushing. Don't rebase shared branches.- Reflog is your safety net. Lost commits are almost always still there.
--force-with-leaseover--force. Never force-push to main.- PR culture +
ghCLI = how teams ship together. Conventional Commits make history readable. revertfor shared branches (new commit that undoes).resetfor local cleanup (rewrites history).