webdev.complete
🌿 Git & GitHub
🛠️Dev Toolbelt
Lesson 50 of 117
30 min

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.

bash
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)
Stash is per machine
Stashes are local. They don't push to GitHub. If you need to share work in progress, make a WIP commit and push that to a branch instead.

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.

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

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

Rebase only what you haven't pushed (or only your own branches)
Rebasing rewrites history. Doing it on a branch others have pulled will cause confusion. The rule of thumb: rebase your own feature branches before opening a PR, never rebase 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."

bash
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 reflog
Reflog entries last about 90 days
Plenty of time to realize the mistake. If you ever think you've lost work, before doing anything else, run git 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:

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

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

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

Never force-push to main
Even with --force-with-lease. Shared branches are sacred. If you accidentally pushed something bad, revert it with a new commit (git revert) instead.

The GitHub Pull Request culture

Most teams don't merge directly to main. The flow looks like this:

  1. Branch off main (git switch -c feature/x).
  2. Make commits, push the branch.
  3. Open a Pull Request (PR) on GitHub.
  4. CI runs your tests. Teammates review the diff and leave comments.
  5. You push fixes. CI runs again. Reviewers approve.
  6. 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:

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

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

bash
# 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"
Just be consistent
Your team might use a slightly different style. The big win is agreeing on something, not the specific format. Tools like commitlint can enforce it on commit.

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

Quiz1 / 4

You accidentally ran 'git reset --hard' and lost three commits. What's the first command to try?

Recap

  • git stash parks WIP. git cherry-pick grabs 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-lease over --force. Never force-push to main.
  • PR culture + gh CLI = how teams ship together. Conventional Commits make history readable.
  • revert for shared branches (new commit that undoes). reset for local cleanup (rewrites history).