← Back to Blog

Git Beyond Commit and Push: Reflog, Bisect, Worktree and Interactive Rebase

September 2026~13 min read

The Safety Net You Forgot You Had: Reflog

Most developers learn Git as "commit, push, pull, merge." That's enough to get by, and it's also why a single bad reset --hard can ruin an afternoon. The first tool worth internalizing is reflog.

Git keeps a local journal of every position HEAD has ever occupied — every commit, checkout, rebase, reset, and merge. This journal is the reflog, and it is the reason "I deleted my work" is almost never true.

# Show every move HEAD has made, newest first
git reflog

# You'll see entries like:
# 4f2a1b3 HEAD@{0}: reset: moving to HEAD~3
# 9c8d7e6 HEAD@{1}: commit: add payment retry logic
# 9c8d7e6 HEAD@{2}: checkout: moving from main to feature/retry

# Jump back to where you were before the disaster
git reset --hard 9c8d7e6

# Or resurrect it as a branch so you don't lose your footing again
git branch rescue-payment-work 9c8d7e6

Two important caveats. First, the reflog is local — it is not pushed, and a fresh clone has none. Second, entries expire: reachable history is kept for 90 days, unreachable objects for 30. Within that window, though, you are effectively immune to your own mistakes.

The practical habit: when something goes wrong, stop typing Git commands and run git reflog first. Every subsequent command you run pushes the state you want to recover one line further down.

Finding the Commit That Broke Everything

"It worked last week" is the most common bug report in software. If the codebase has a hundred commits since then, reading them one by one is a waste of a day. git bisect does a binary search instead — with 1000 commits, it finds the culprit in about 10 steps.

# Start the search
git bisect start

# Mark the current state as broken
git bisect bad HEAD

# Mark a commit you know was fine (a tag, a hash, or a date)
git bisect good v2.4.0

# Git checks out a commit in the middle. Test it, then tell Git:
git bisect good    # or: git bisect bad
# ...repeat until Git prints "X is the first bad commit"

git bisect reset    # always remember to clean up

The real power shows up when you automate the test itself. If you have any command that exits 0 on success and non-zero on failure — a unit test, a build, a curl against a local server — hand it to bisect and walk away:

git bisect start HEAD v2.4.0
git bisect run npm test

# Ten minutes later:
# 7a3f9c1 is the first bad commit
# commit 7a3f9c1
# Author: someone
# Date:   Tue Sep 8 14:22:10 2026
#     refactor: move timeout config into client

I've used this to find regressions in codebases I had never seen before. It doesn't require understanding the system — only a reliable pass/fail signal. That's a remarkably low bar for the amount of time it saves.

Two Branches at Once Without Stashing

The usual way to switch context is git stash, switch branch, do the work, switch back, and pray the stash applies cleanly. git worktree removes the whole dance by checking out a branch into a separate directory, sharing the same repository.

# Check out the hotfix branch into a sibling folder
git worktree add ../hotfix-202 hotfix/login-500

cd ../hotfix-202
# Full working directory, own node_modules if you want, same .git
npm install && npm test

# When you're done
git worktree remove ../hotfix-202

Where this really earns its keep is long-running work. I keep a worktree for the production branch so I can diff behavior between versions without touching my feature branch, and another for reviewing large pull requests — the PR gets its own folder, and my in-progress work stays exactly where it was.

One rule: a branch can only be checked out in one worktree at a time. If Git refuses, something already has it, which is usually valuable information.

Rewriting History Before Anyone Sees It

Interactive rebase has a reputation for being dangerous. It's dangerous in exactly one situation: rewriting commits that other people have already pulled. On your own unpushed branch, it's the difference between a history that reads like a lab notebook and one that reads like documentation.

# Rework the last 5 commits
git rebase -i HEAD~5

# pick   4f2a1b3 add user endpoint
# pick   9c8d7e6 fix typo
# pick   1a2b3c4 add validation
# pick   5d6e7f8 fix typo again
# pick   8h9i0j1 update tests

The verbs worth knowing beyond the defaults:

You can also let Git prepare the todo list for you. Commit with --fixup and rebase with --autosquash, and the fix lands next to the commit it fixes, already marked:

# You know the typo belongs to 4f2a1b3
git commit --fixup 4f2a1b3

git rebase -i --autosquash HEAD~6
# Git has already arranged and labelled it for you

Moving a Single Commit

Merging takes everything; cherry-picking takes one commit. It's the right tool when a fix on main needs to reach a release branch, but the rest of main doesn't.

# Apply one commit onto the current branch
git cherry-pick 7a3f9c1

# Apply several, or a range
git cherry-pick 7a3f9c1 8b2c3d4
git cherry-pick main~3..main~1

# -x records where it came from — always use this on shared branches
git cherry-pick -x 7a3f9c1

Cherry-picking creates a new commit with a new hash. That means the same logical fix can exist twice in history, and merging those branches later can produce confusing conflicts. Use it deliberately, prefer -x so the trail is visible, and reach for a proper merge when you're moving more than a commit or two.

Searching History Like a Database

git log is far more than a list of commits. These four queries cover most of what you'd ever want to ask:

# Which commits changed the number of retries? (pickaxe: count of occurrences)
git log -S "MAX_RETRIES" --oneline

# Which commits touched a line matching this regex?
git log -G "retry\(.*\)" --oneline

# Which commits mention the ticket in the message?
git log --grep="PAY-142" --oneline

# Full history of one function, with diffs
git log -L :calculateBackoff:src/retry.js

The last one is the hidden gem: -L traces a single function or line range across renames and shows you every change that ever touched it. When you're staring at unfamiliar code and wondering "why on earth is it written this way," that flag usually answers the question in seconds.

Combine with --author, --since, and path filters to narrow further: git log --since="3 weeks ago" -- src/payments/.

Reset, Revert, and Restore Are Not the Same

Confusing these three causes more lost work than any other Git concept. The distinction:

# Undo a commit that's already on main, safely
git revert 7a3f9c1

# Unstage a file but keep the edits
git restore --staged config.js

# Throw away local edits to one file
git restore config.js

# Undo the last local commit but keep everything staged
git reset --soft HEAD~1

Rule of thumb I've stuck to for years: if it's been pushed, revert; if it's still yours, reset.

Aliases That Pay for Themselves

You don't need an elaborate dotfiles repository. Four aliases cover the vast majority of daily friction:

git config --global alias.lg "log --graph --oneline --decorate --all"
git config --global alias.undo "reset --soft HEAD~1"
git config --global alias.st "status -sb"
git config --global alias.amend "commit --amend --no-edit"

# And one safety setting: never --force, only --force-with-lease
git config --global alias.pushf "push --force-with-lease"

--force-with-lease refuses to overwrite work that appeared on the remote since your last fetch. It is --force with a seatbelt, and there is no good reason to use the plain version.

Habits That Prevent the Disaster

Tools are the fallback; habits are the actual fix. These cost seconds and have saved me hours:

Git rewards the people who understand that nothing is really deleted. Once you believe that — because you've watched reflog bring something back — the whole tool stops feeling fragile and starts feeling like the safety harness it was designed to be.

Related Articles