Week 4: Advanced Git — History & Recovery

This is the week that turns Git from "the thing that occasionally blocks a push" into a real productivity tool. You'll stash work in progress, cherry-pick individual commits, clean up messy history before it's shared, and learn the command that has saved more "lost" work than any other: reflog.

Module 4 of 5 Week 4 of 5 ~2–3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Stash work in progress and cherry-pick specific commits between branches
  • Clean up local commit history with amend and interactive rebase
  • Recover "lost" commits with reflog and find a bug's origin with bisect

1. Stashing Work in Progress

You're mid-edit and need to switch branches urgently — maybe to fix an emergency bug — but your changes aren't ready to commit. git stash shelves your uncommitted changes and restores a clean working directory, so you can switch freely:

terminal
git stash push -m "wip: pricing table redesign"
# Saved working directory and index state On feature/pricing: wip: pricing table redesign

git switch main
# ...fix the emergency bug, commit, push...

git switch feature/pricing
git stash list
# stash@{0}: On feature/pricing: wip: pricing table redesign

git stash pop     # reapplies the stash AND removes it from the stash list
# git stash apply reapplies without removing it, if you want to keep the stash around

A stash can conflict on pop/apply just like a merge can — resolve it the same way, by hand, then stage the result.

2. Cherry-Picking & Tagging Releases

git cherry-pick <commit> applies one specific commit from anywhere in history onto your current branch — useful for pulling a hotfix into a release branch without merging everything else that's landed since:

terminal
git log --oneline main
# a1b2c3d Fix critical checkout bug
# 9f8e7d6 Add new dashboard widget
# ...

git switch release/v2.1
git cherry-pick a1b2c3d
# Applies ONLY the checkout fix onto release/v2.1, as a new commit

Tags mark a specific commit as significant — almost always a release — with a name that doesn't move as new commits land, unlike a branch:

terminal
git tag -a v2.1.0 -m "Release 2.1.0"
git push origin v2.1.0
git tag                     # list all tags
git checkout v2.1.0         # inspect the repo exactly as it was at that release

3. Rewriting History: amend & Interactive Rebase

Made a typo in your last commit message, or forgot a file? --amend fixes the most recent commit in place instead of adding a new one:

terminal
git add forgotten-file.js
git commit --amend -m "Add cart total calculation, with tests"
# Replaces the previous commit entirely -- new hash, same position in history

For anything further back than the last commit, interactive rebase lets you reorder, squash, reword or drop a range of commits before you share them:

terminal
git rebase -i HEAD~4
# Opens an editor listing the last 4 commits, oldest first:
#
# pick a1b2c3d Add cart component
# pick 9f8e7d6 fix typo
# pick 1a2b3c4 fix typo again
# pick 7d6e5f4 Add cart tests
#
# Change "pick" to "squash" (or "s") on the two typo-fix commits to fold
# them into the commit above, then save and close -- Git prompts for a
# combined commit message next.
The golden rule of rewriting history

Never amend or interactively rebase commits that have already been pushed and pulled by someone else — it rewrites hashes, and their local history will silently diverge from yours. It's safe on a branch that's entirely yours and not yet shared.

4. Recovering with reflog & Bisecting Bugs

git reflog is a local, personal log of every place HEAD has pointed — every commit, checkout, rebase and reset — even ones no branch or tag references anymore. If you think you've lost a commit, it's almost certainly still here:

terminal
git reset --hard HEAD~1   # oops -- accidentally discarded the last commit

git reflog
# 7d6e5f4 HEAD@{0}: reset: moving to HEAD~1
# a1b2c3d HEAD@{1}: commit: Add cart total calculation
# ...

git reset --hard a1b2c3d  # restores the "lost" commit

git bisect finds the exact commit that introduced a bug via binary search — invaluable once a project has hundreds of commits between "known good" and "known broken":

terminal
git bisect start
git bisect bad                  # current commit is broken
git bisect good v2.0.0          # this tag was known to work
# Git checks out a commit halfway between them:
#   Bisecting: 8 revisions left to test

# ...test the app...
git bisect good   # or `git bisect bad`, depending on the result
# Repeat -- Git narrows the range each time until it names the exact
# commit that introduced the bug.

git bisect reset  # done -- return to your original branch

5. Git Hooks: Automating Checks

Hooks are scripts Git runs automatically at points in the workflow — .git/hooks/pre-commit runs before a commit is created, and can reject it (non-zero exit code) to enforce quality gates locally:

.git/hooks/pre-commit
#!/bin/sh
# Block commits that still contain a debugger statement.
if git diff --cached | grep -q "debugger;"; then
  echo "Error: remove 'debugger;' before committing."
  exit 1
fi

Make it executable with chmod +x .git/hooks/pre-commit. Hooks live in .git/hooks/, which isn't tracked or shared by default — teams typically use a tool like Husky to commit hook configuration into the repo itself so everyone gets the same checks automatically.

6. Hands-on Exercise

Hands-on

Clean up a messy branch before opening a PR

Practice the exact cleanup real reviewers expect before they'll look at your diff.

Requirements:

  1. On a feature branch, make five small commits, including at least two that are trivial fixups (e.g. "fix typo", "oops").
  2. Use git rebase -i to squash the fixup commits into the commits they're fixing, ending with 2–3 clean commits.
  3. Amend the final commit's message to be more descriptive using git commit --amend.
  4. Stash an unrelated uncommitted change, switch to main, switch back, and pop the stash to confirm it survived.
  5. Tag the current commit as v0.1.0-practice with an annotated tag.
  6. Deliberately discard your last commit with git reset --hard HEAD~1, then use git reflog to recover it.
Hint

If an interactive rebase goes wrong mid-way, git rebase --abort restores the branch to exactly how it was before you started — the same safety net as merge --abort.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What's the difference between git stash pop and git stash apply?

Both reapply a stashed set of changes to your working directory. pop then removes that entry from the stash list; apply leaves it there, so you can reapply the same stash again elsewhere (e.g. onto a different branch) later.

Q2

When is git cherry-pick the right tool instead of git merge?

When you want exactly one specific commit from another branch — not everything on it. A classic case is pulling a single hotfix commit into a release branch without dragging in every other unrelated change that's since landed on the source branch.

Q3

Why is it unsafe to git commit --amend a commit that's already been pushed and pulled by a teammate?

--amend creates a brand-new commit hash to replace the old one. Anyone who already pulled the original commit still has it in their history; when they next pull, they'll see your amended commit as a separate, conflicting piece of history rather than a clean update.

Q4

You ran git reset --hard and lost a commit that wasn't backed up anywhere else. Is it actually gone?

Almost certainly not, at least for a while. git reflog keeps a local record of everywhere HEAD has pointed, including commits no branch references anymore. Finding the commit's hash in the reflog and running git reset --hard <hash> recovers it, as long as Git hasn't garbage-collected it yet (typically a 30–90 day window).