Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

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-edit keeps the message
git commit --fixup <sha>
record a fix for an older commit; git rebase -i --autosquash folds it in
rebase.autoSquash true
set with git config so every rebase -i moves fixup!/squash! commits into place
git rebase --onto A B
move the commits after B onto A — 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 CMD after each replayed commit and stop at the first failure
git rebase --continue
resume after fixing and git add-ing a conflict; --abort restores 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 bad and git 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; -w ignores whitespace, -C follows 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 --all to 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-merges leaves 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; -x notes where it came from
git cherry-pick A^..B
copy a range including A; A..B would leave A out
git stash push -m msg
stash with a label; -u includes untracked files, -- path stashes 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 prune tidies up after one removed by hand
git switch -
back to the previous branch; git switch -c new creates 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; -x also 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 fetch cannot quietly defeat it (2.30+)
git pull --rebase
rebase onto upstream instead of a merge bubble; pull.rebase true makes 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 --unshallow fetches 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-includes

Fold 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 reset

HEAD 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 ../hotfix

The 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.py

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