# Power tools

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

Git & Collaboration roadmap · Going further · 19 min read · https://levelcode.org/roadmaps/git/power-tools

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:

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

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

> [!TIP]
> Stashes are easy to forget. Always add a `-m` description, and prefer a work-in-progress commit on a branch for anything you will not return to within the hour.

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

> [!IMPORTANT]
> The reflog cannot save work that was never committed, and it does not last forever. By default, entries for commits no longer reachable from any branch expire after 30 days, others after 90, and garbage collection then deletes the commits. It is also not pushed: a fresh clone has an empty reflog. Commit early, even on throwaway branches.

## 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 <ada@example.com>
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:

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

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

> [!WARNING]
> Client-side hooks are a convenience, not a guarantee. Anyone can skip them with `git commit --no-verify` or simply not install them. Rules that truly matter — tests pass, messages follow the convention — must also be enforced in CI and branch protection on the server.

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

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

## Quiz

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

   A. It is deleted, so the changes exist only in the conflicted files
   B. It is kept on the stack until you drop it
   C. It is converted into a commit
   D. It is moved to a new branch

   **Answer: B.** When pop cannot apply cleanly, Git keeps the entry ("The stash entry is kept in case you need it again"). Resolve the conflict, then run git stash drop yourself.

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

   A. 0
   B. 1
   C. 125
   D. 128

   **Answer: C.** 125 means skip. 0 means good, any other code from 1 to 127 means bad, and codes above 127 abort the bisect.

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

   A. Find the tip commit in git reflog and run git branch <name> <id>
   B. Clone the repository again
   C. Run git revert on the branch name
   D. It cannot be recovered once deleted with -D

   **Answer: A.** Deleting a branch only removes the label. The commits remain and the reflog records where you were, so creating a new branch at that ID restores it. A fresh clone would not have unpushed commits.

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

   A. Hooks only run on the server
   B. commit-msg hooks cannot read the message
   C. Hooks are pushed with the repository but run too late
   D. Hooks in .git/hooks are not shared, and anyone can bypass them with --no-verify, so CI must enforce the rule too

   **Answer: D.** Client-side hooks are local and optional. They give fast feedback, but only server-side checks such as CI and branch protection can guarantee a rule.
