Step 10 of 10 · optional

Power tools

Park work with stash, copy commits with cherry-pick, recover anything with reflog, find bugs with bisect and automate checks with hooks.

19 min 7 sections

A handful of less famous commands turn awkward situations into thirty-second fixes: an urgent bug arrives mid-feature, a fix must land on an old release, a branch was deleted by mistake, or a regression appeared somewhere in the last two hundred commits. This lesson covers those five tools — stash, cherry-pick, reflog, bisect and hooks — with real sessions so you know exactly what to expect.

git stash: park unfinished work

You are halfway through a change when a teammate asks you to look at a bug on another branch. Your work is not ready to commit. Stash saves your uncommitted changes onto a stack and cleans your working tree:

bash
git stash push -m "half-done multiply"
text
Saved working directory and index state On main: half-done multiply

git stash alone does the same with an automatic message (WIP on main: 129587a ...). Now you can switch branches, fix the bug, and come back. Then reapply the work:

bash
git stash list
git stash pop
text
stash@{0}: On main: half-done multiply
stash@{1}: WIP on main: 129587a Change 12

pop applies the most recent entry and removes it from the stack. The other commands you need:

CommandEffect
git stash push -uAlso stash untracked files (by default they are left behind)
git stash push -- path/Stash only some paths
git stash show -p stash@{1}Show an entry's diff
git stash apply stash@{1}Apply an entry but keep it on the stack
git stash drop stash@{1}Delete one entry
git stash branch new-branchCreate a branch at the commit you stashed from, apply the stash there, and drop it

If popping conflicts with changes made since, Git leaves conflict markers as in a merge and keeps the entry safe:

text
CONFLICT (content): Merge conflict in f
The stash entry is kept in case you need it again.

Resolve the files, then git stash drop once you are happy.

git cherry-pick: copy a commit

Cherry-pick takes the changes introduced by one commit and applies them as a new commit on your current branch. The classic use is backporting: a fix lands on main, and it must also go into the release/2.3 branch customers are running.

text
Before:
main          A ── B ── F ── C       (F = "fix(cart): round totals")
                    \
release/2.3          R1 ── R2

After git cherry-pick F on release/2.3:
main          A ── B ── F ── C
                    \
release/2.3          R1 ── R2 ── F'
bash
git switch release/2.3
git cherry-pick -x 9c41d07
text
[release/2.3 4f2a9c1] fix(cart): round totals to two decimal places
 Date: Sun Oct 11 10:02:13 2026 +0000
 1 file changed, 1 insertion(+), 1 deletion(-)

F' has the same changes and message but a different parent, so a new ID. -x appends (cherry picked from commit 9c41d07...) to the message, keeping the link traceable — use it whenever the original commit is public.

You can pick several commits (git cherry-pick a1b2c3d e4f5a6b) or a range (git cherry-pick main~3..main, meaning the last three commits of main). Conflicts are handled like a rebase: resolve, git add, then git cherry-pick --continue, or --abort to back out.

Cherry-pick is a scalpel, not a workflow: if you are copying many commits between branches, you probably want a merge or rebase instead.

git reflog: Git's undo history

Every time HEAD moves — commit, switch, reset, rebase, merge, pull — Git appends a line to the reflog. It is local to your repository, and it is how you recover commits no branch points to anymore.

bash
git reflog
text
661ccd8 (HEAD -> main) HEAD@{0}: cherry-pick: Fix add() regression
129587a HEAD@{1}: checkout: moving from hotfix to main
661ccd8 HEAD@{2}: commit: Fix add() regression
129587a HEAD@{3}: checkout: moving from main to hotfix

HEAD@{2} means "where HEAD was two moves ago". (git switch is recorded as checkout: too.) Each branch also has its own reflog — git reflog show feature/login — and you can address entries by time, such as main@{yesterday} or HEAD@{2.hours.ago}.

Recovering a deleted branch

bash
git branch -D feature/login
text
Deleted branch feature/login (was 7e80f6c).

The commits still exist; only the label is gone, and the message even printed the ID. If it has scrolled away, find it in the reflog, then recreate the branch:

bash
git reflog | grep -A1 "moving from feature/login"   # the second line is the branch's last tip
git branch feature/login 7e80f6c

The same technique undoes a bad reset --hard, a botched rebase or an unwanted amend: find the entry for the state you want, and git reset --hard HEAD@{n} (on the right branch) or create a branch pointing at it to inspect first.

git bisect: binary search for bugs

A test that passed at v1.2.0 fails today, 200 commits later. Bisect does a binary search: you mark one good and one bad commit, Git checks out the midpoint, you say whether it is good or bad, and it halves the range each time. 200 commits need about 8 steps.

bash
git bisect start
git bisect bad              # the current commit is broken
git bisect good v1.2.0      # this one was fine

On the smaller 12-commit history from the Practice below, the first step looks like this:

text
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[677573a620dd88700e7aaba536d47062259fa488] Change 6

Git has checked out a commit in the middle (in detached HEAD). Test it and report the result with git bisect good or git bisect bad. If a commit cannot be tested (it does not build, for an unrelated reason), use git bisect skip. Eventually Git names the culprit:

text
989fc5bd773070854b35ea3f8a3029c8ad1e1c71 is the first bad commit
commit 989fc5bd773070854b35ea3f8a3029c8ad1e1c71
Author: Ada Lovelace <[email protected]>
Date:   Sun Oct 11 08:04:44 2026 +0000

    Change 7

 math.js  | 2 +-
 notes.js | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

Always finish with git bisect reset, which returns you to the branch you started on.

Automating it with bisect run

If a command can tell good from bad, Git can do the whole search unattended. The command must exit with 0 for good, 125 to skip, and any other code from 1 to 127 for bad:

bash
git bisect start HEAD v1.2.0      # shorthand: bad commit, then good commit
git bisect run npm test -- math.test.js
git bisect reset
text
running 'npm' 'test' '--' 'math.test.js'
...
989fc5bd773070854b35ea3f8a3029c8ad1e1c71 is the first bad commit
bisect found first bad commit

If the test that detects the bug is new, keep it in an untracked script or outside the repository, so it is still present as Git checks out older commits. Bisect is also why atomic commits matter: finding a 3,000-line "various changes" commit does not help much.

Hooks: automate checks

Hooks are scripts Git runs at certain points. They live in .git/hooks/, must be executable, and are named after the event. If a hook exits non-zero, the operation is aborted. The most useful client-side hooks:

HookRunsTypical use
pre-commitBefore a commit is createdLint or format staged files, block debug code
commit-msgAfter the message is written; receives its file path as $1Enforce a message convention
pre-pushBefore a push sends anythingRun the test suite

Here is a pre-commit hook that blocks commits adding a debugger statement:

bash
#!/bin/sh
# .git/hooks/pre-commit — block commits that add a `debugger` statement.
if git diff --cached -U0 | grep -q '^+.*debugger'; then
  echo "pre-commit: remove 'debugger' statements before committing" >&2
  exit 1
fi

And a commit-msg hook that enforces Conventional Commits:

bash
#!/bin/sh
# .git/hooks/commit-msg — $1 is the file holding the proposed message.
pattern='^(feat|fix|docs|refactor|test|chore)(\([a-z0-9-]+\))?!?: .+'
if ! head -n 1 "$1" | grep -Eq "$pattern"; then
  echo "commit-msg: use 'type(scope): summary', e.g. 'fix(cart): round totals'" >&2
  exit 1
fi
bash
chmod +x .git/hooks/pre-commit .git/hooks/commit-msg
git commit -am "updated stuff"
text
commit-msg: use 'type(scope): summary', e.g. 'fix(cart): round totals'

Sharing hooks with the team

.git/ is never committed, so hooks there are personal. To share them, commit them to a folder in the repository and point Git at it — each developer runs this once:

bash
git config core.hooksPath .githooks

Many JavaScript projects use tools such as Husky or lint-staged, which set this up during npm install.

Practice

  1. In a new repository, commit math.js containing export const add = (a, b) => a + b;, then make twelve more commits that each append a comment line to notes.js. In the seventh of those, also change a + b to a - b.
  2. Write an untracked script check.sh that exits 0 if math.js contains a + b and 1 otherwise, and use git bisect run to find the bad commit.
  3. On a new branch hotfix, fix the bug and commit. Switch to main and bring the fix over with cherry-pick.
  4. Delete hotfix with -D, then recover it using the reflog.
Show solution
bash
git init calc && cd calc
echo 'export const add = (a, b) => a + b;' > math.js
git add math.js && git commit -m "Add add()"
for i in $(seq 1 12); do
  echo "// note $i" >> notes.js
  [ "$i" = 7 ] && sed -i 's/a + b/a - b/' math.js    # macOS: sed -i ''
  git add . && git commit -m "Change $i"
done

printf '#!/bin/sh\ngrep -q "a + b" math.js\n' > check.sh
chmod +x check.sh                  # untracked, so it survives checkouts

git bisect start HEAD HEAD~12      # bad, good
git bisect run ./check.sh          # "... is the first bad commit" → Change 7
git bisect reset

git switch -c hotfix
sed -i 's/a - b/a + b/' math.js
git commit -am "fix(math): restore addition"
git switch main
git cherry-pick -x hotfix
git log --oneline -2               # the cherry-picked fix is on main

git branch -D hotfix               # prints "(was <id>)"
git reflog | grep "fix(math)"      # use the "commit:" line, not "cherry-pick:"
git branch hotfix <id>
git log --oneline -1 hotfix

grep exits 0 when it finds the text and 1 when it does not, which is exactly the good/bad contract bisect run expects. HEAD~12 is the first commit, since there are twelve commits after it.

Key takeaways

  • git stash push -m parks uncommitted work; pop reapplies and removes it, apply keeps it, -u includes untracked files.
  • git cherry-pick -x copies a commit onto another branch with a new ID, ideal for backporting fixes.
  • The reflog records every move of HEAD, so deleted branches and reset or rebased commits can be recovered for weeks.
  • git bisect binary-searches history for the first bad commit; bisect run automates it using exit codes.
  • Hooks in .git/hooks automate local checks; share them via core.hooksPath, and enforce critical rules in CI too.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.You run git stash, and later git stash pop produces a conflict. What happens to the stash entry?

  2. 2.During git bisect run ./test.sh, what exit code tells Git that the current commit cannot be tested and should be skipped?

  3. 3.You force-deleted a branch with unmerged commits an hour ago. What is the best way to recover it?

  4. 4.Your team wants every commit message on main to follow a convention. Why is a commit-msg hook alone not enough?

Concepts in this lesson

One-page notes on each idea, for review or a quick lookup later.

Finished reading?

Mark it complete to fill in your roadmap.