Git Advanced (Rebase, Bisect, Worktree)
Beyond add/commit/push — rewrite history cleanly, hunt a bug with binary search, and juggle branches without stashing.
Everything here rests on one model: commits are immutable snapshots, a branch is a movable pointer to one of them, and “rewriting history” really means making new commits and moving the pointer. The old commits are not destroyed at once. git reflog logs every position HEAD and each branch have held, kept for weeks by default, which is why nearly every mistake on this page can be undone.
The real danger is sharing rewritten commits, because anyone who built on the old hashes has to untangle them. Rewrite freely on private branches, revert on shared ones, and publish rewrites with --force-with-lease.
Rebase & rewrite
- git rebase main
- replay your commits on top of main — linear history, no merge commit
- git rebase -i HEAD~5
- squash, reword, reorder or drop the last 5 commits in an editor
- git commit --amend
- fold staged changes into the last commit;
--no-editkeeps the message - git commit --fixup <sha>
- record a fix for an older commit;
git rebase -i --autosquashfolds it in - rebase.autoSquash true
- set with
git configso everyrebase -imoves fixup!/squash! commits into place - git rebase --onto A B
- move the commits after
BontoA— transplant a branch cut from the wrong base - git rebase --update-refs
- also move stacked branches that point into the rebased range (Git 2.38+)
- git rebase -x CMD main
- run
CMDafter each replayed commit and stop at the first failure - git rebase --continue
- resume after fixing and
git add-ing a conflict;--abortrestores the start - rerere.enabled true
- set with
git config: Git records conflict fixes and replays them next time - Golden rule
- Never rebase commits you’ve pushed and shared — it rewrites hashes.
Find the bad commit
- git bisect start
- binary-search history: mark
git bisect badandgit bisect good <sha> - git bisect run npm test
- automate it: exit 0 = good, 1–127 = bad, 125 = skip this commit
- git bisect skip · reset
- skip a commit you cannot test · finish and return to where you started
- git blame -L 10,20 file
- who last touched lines 10–20;
-wignores whitespace,-Cfollows moved code - git blame --ignore-rev X
- look past a formatting commit; list such commits in
.git-blame-ignore-revs - git log -S "name"
- pickaxe: commits that added or removed that string — when it appeared or vanished
- git log -G "regex"
- commits whose diff touches lines matching a regex, even if occurrence counts stay equal
- git log -L :func:file
- the history of one function, commit by commit
- git log --follow file
- one file’s history, continuing across renames
- git show <sha>:path
- print a file as it was at that commit — no checkout needed
Read history
- git log --oneline --graph
- branch topology at a glance; add
--allto include every branch - git log main..feat
- commits on feat that are not on main — what a merge would bring in
- git diff main...feat
- three dots: changes on feat since it forked — what the pull request shows
- git range-diff old...new
- compare two versions of a rebased branch, e.g.
origin/feat...feat - git shortlog -sn
- commit counts per author;
--no-mergesleaves merge commits out - git log --author=ann
- filter by author; combine with
--since="2 weeks ago"and-- path/
Move work around
- git cherry-pick <sha>
- copy one commit onto the current branch;
-xnotes where it came from - git cherry-pick A^..B
- copy a range including
A;A..Bwould leave A out - git stash push -m msg
- stash with a label;
-uincludes untracked files,-- pathstashes just that path - git stash pop · apply
- restore and drop · restore and keep;
stash@{2}picks an older stash - git stash list · show -p
- see what is stashed;
git stash show -p stash@{1}prints one as a diff - git stash branch new
- turn a stash into a branch from the commit it was made on — sidesteps conflicts
- git worktree add ../hf
- a second checkout in its own folder — no stashing; a branch lives in one worktree at a time
- git worktree list
- every checkout of this repo and the branch each one holds
- git worktree remove DIR
- delete a worktree;
git worktree prunetidies up after one removed by hand - git switch -
- back to the previous branch;
git switch -c newcreates a branch
Undo safely
- git restore --staged f
- unstage a file, keeping the edits
- git restore file
- discard unstaged edits to the file (gone for good)
- git restore -s HEAD~2 f
- bring back a file as it was two commits ago
- git revert <sha>
- new commit that inverts an old one — the safe undo for shared history
- git revert -m 1 <merge>
- undo a merge commit; merging that branch again later means reverting this revert
- git reset --soft HEAD~1
- uncommit, keep the changes staged
- git reset HEAD~1
- uncommit and unstage (the default
--mixed); edits stay in your files - git reset --hard <sha>
- move the branch and throw away every uncommitted change — stash first if unsure
- ORIG_HEAD
- where HEAD was before the last merge, rebase or reset:
git reset --hard ORIG_HEAD - git reflog
- every place HEAD has been; find the lost commit, then
git reset --hard <sha> - git clean -n · -fd
- preview · delete untracked files and directories;
-xalso deletes ignored files - git fsck --lost-found
- list dangling commits — how a dropped stash is recovered
Share rewritten history
- --force-with-lease
- force-push only if the remote is where you last saw it — no clobbering a teammate
- --force-if-includes
- add to the lease so a background
git fetchcannot quietly defeat it (2.30+) - git pull --rebase
- rebase onto upstream instead of a merge bubble;
pull.rebase truemakes it the default - git push -u origin HEAD
- publish the current branch under the same name and track it
- git checkout --theirs f
- take one side of a conflicted file; during a rebase ours and theirs are swapped
Big repos
- git clone --depth 1
- shallow clone of the latest commit;
git fetch --unshallowfetches the rest - --filter=blob:none
- partial clone: full history, file contents fetched on demand — good for monorepos
- git sparse-checkout set
- check out only listed directories, e.g.
git sparse-checkout set apps/web - git maintenance start
- schedule background prefetch, gc and commit-graph upkeep for this repo
Recipes
Clean up a feature branch before review
git fetch origin
git rebase -i --autosquash origin/main
git range-diff origin/feature...feature
git push --force-with-lease --force-if-includesFold fixups and reword commits against the latest main, check the rewrite commit by commit, then publish it without overwriting anyone else’s push.
Find the commit that broke the tests
git bisect start HEAD v1.4.0
git bisect run npm test
git bisect resetHEAD is known bad and v1.4.0 known good, so bisect tests about log2(n) midpoints and names the first bad commit. Make the test script exit 125 on commits that cannot build.
Undo a bad rebase or reset
git reflog -n 10
git reset --hard HEAD@{3}Pick the entry from just before the operation (here, three moves ago) and point the branch back at it. --hard discards uncommitted work, so stash it first.
Ship a hotfix without stashing your feature
git worktree add -b hotfix ../hotfix origin/main
cd ../hotfix
git commit -am "Fix crash on empty input"
git push -u origin hotfix
cd - && git worktree remove ../hotfixThe hotfix gets its own directory and branch, so the half-finished feature stays exactly as it was. A branch can be checked out in only one worktree at a time.
Recover a deleted file from history
git log --diff-filter=D --oneline -- path/to/file.py
git restore --source=<sha>^ -- path/to/file.pyThe first command finds the commit that deleted the file; restoring from its parent (^) brings back the last version that existed.
Questions about Git Advanced (Rebase, Bisect, Worktree) Cheat Sheet
What is the difference between git reset and git revert?
git reset moves the current branch pointer to another commit, optionally resetting the index (mixed, the default) and the working tree (hard). Commits after that point drop out of the branch, so it rewrites history. git revert leaves history alone and adds a new commit that applies the inverse of an earlier one. Use reset for local work nobody else has pulled, and revert for anything already pushed to a shared branch, because a reset there forces everyone else to reconcile their copies after your force-push.
When should I rebase instead of merge?
Rebase when you want a clean, linear history on a branch only you work on: replaying your commits on top of the latest main makes the pull request easier to review and easier to bisect later. Merge when the branch is shared or long-lived, or when you want history to record exactly when work came together. The rule that keeps teams out of trouble is never to rebase commits someone else may have built on. Many teams rebase privately and then merge or squash-merge the finished branch into main.
How do I undo a git rebase?
Right after the rebase, ORIG_HEAD still points at where the branch was before it started, so git reset --hard ORIG_HEAD restores it. If you have run other commands since, use git reflog: it lists every position HEAD has held, with entries such as rebase (start). Find the line just before the rebase began and reset to that entry, for example HEAD@{5}. Both forms discard uncommitted work, so stash or commit it first. If you had already force-pushed the rebased branch, push the restored one with --force-with-lease.
What does git push --force-with-lease do?
It force-pushes only if the remote branch still points where your last fetch saw it. If a teammate pushed in the meantime, the push is refused instead of silently deleting their commits, which is what a plain --force would do. The protection can be defeated by a background fetch, for example from an editor, because that updates your view of the remote without you ever looking at the new commits. Adding --force-if-includes, available since Git 2.30, closes that gap by also requiring the remote tip to be in your local history.