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.
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:
git stash push -m "half-done multiply"Saved working directory and index state On main: half-done multiplygit 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:
git stash list
git stash popstash@{0}: On main: half-done multiply
stash@{1}: WIP on main: 129587a Change 12pop applies the most recent entry and removes it from the stack. The other commands you need:
| Command | Effect |
|---|---|
git stash push -u | Also 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-branch | Create 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:
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.
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'git switch release/2.3
git cherry-pick -x 9c41d07[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.
git reflog661ccd8 (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 hotfixHEAD@{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
git branch -D feature/loginDeleted 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:
git reflog | grep -A1 "moving from feature/login" # the second line is the branch's last tip
git branch feature/login 7e80f6cThe 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.
git bisect start
git bisect bad # the current commit is broken
git bisect good v1.2.0 # this one was fineOn the smaller 12-commit history from the Practice below, the first step looks like this:
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[677573a620dd88700e7aaba536d47062259fa488] Change 6Git 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:
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:
git bisect start HEAD v1.2.0 # shorthand: bad commit, then good commit
git bisect run npm test -- math.test.js
git bisect resetrunning 'npm' 'test' '--' 'math.test.js'
...
989fc5bd773070854b35ea3f8a3029c8ad1e1c71 is the first bad commit
bisect found first bad commitIf 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:
| Hook | Runs | Typical use |
|---|---|---|
pre-commit | Before a commit is created | Lint or format staged files, block debug code |
commit-msg | After the message is written; receives its file path as $1 | Enforce a message convention |
pre-push | Before a push sends anything | Run the test suite |
Here is a pre-commit hook that blocks commits adding a debugger statement:
#!/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
fiAnd a commit-msg hook that enforces Conventional Commits:
#!/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
fichmod +x .git/hooks/pre-commit .git/hooks/commit-msg
git commit -am "updated stuff"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:
git config core.hooksPath .githooksMany JavaScript projects use tools such as Husky or lint-staged, which set this up during npm install.
Practice
- In a new repository, commit
math.jscontainingexport const add = (a, b) => a + b;, then make twelve more commits that each append a comment line tonotes.js. In the seventh of those, also changea + btoa - b. - Write an untracked script
check.shthat exits 0 ifmath.jscontainsa + band 1 otherwise, and usegit bisect runto find the bad commit. - On a new branch
hotfix, fix the bug and commit. Switch tomainand bring the fix over with cherry-pick. - Delete
hotfixwith-D, then recover it using the reflog.
Show solution
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 hotfixgrep 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 -mparks uncommitted work;popreapplies and removes it,applykeeps it,-uincludes untracked files.git cherry-pick -xcopies 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 bisectbinary-searches history for the first bad commit;bisect runautomates it using exit codes.- Hooks in
.git/hooksautomate local checks; share them viacore.hooksPath, and enforce critical rules in CI too.
Check yourself
Quiz
0/4 correct · 4 left
1.You run git stash, and later git stash pop produces a conflict. What happens to the stash entry?
2.During git bisect run ./test.sh, what exit code tells Git that the current commit cannot be tested and should be skipped?
3.You force-deleted a branch with unmerged commits an hour ago. What is the best way to recover it?
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.