Interactive Rebase Mastery
Complete guide to mastering git interactive rebase for cleaning commit history, squashing, splitting, reordering, and producing clean merge-ready branches
Interactive Rebase Mastery
Interactive rebase is the single most powerful tool in Git for rewriting commit history. It lets you squash, reorder, split, edit, and drop commits before they reach a shared branch -- turning a messy stream of WIP saves into a clean, reviewable narrative. If you only learn one advanced Git command, make it git rebase -i.
This is not a gentle introduction. We are going deep into every interactive rebase command, real conflict resolution workflows, recovery techniques, and the automation patterns that will change how you work with Git permanently.
Prerequisites
- Git 2.34+ installed (for
--autosquash,--autostash, and--update-refssupport) - A terminal you are comfortable with (bash, zsh, PowerShell, or Git Bash)
- Solid understanding of Git fundamentals: commits, branches, HEAD, staging area
- Your
core.editorconfigured in.gitconfig(vim, VS Code, nano -- whatever you prefer) - A throwaway repository to practice in (never learn rebase on a production branch with shared history)
Configure your editor if you have not already:
# VS Code
git config --global core.editor "code --wait"
# vim
git config --global core.editor vim
# nano
git config --global core.editor nano
Interactive Rebase Fundamentals
When you run git rebase -i <base>, Git opens your editor with a list of commits from <base> to HEAD. Each commit gets a command prefix that tells Git what to do with it.
$ git log --oneline -6
f4a1b2c Add input validation to user endpoint
d3e9c8a WIP save
b7a6f5e Fix typo in error message
a1c2d3e Add user creation endpoint
9e8f7d6 Set up Express router
8a7b6c5 Initialize project with package.json
Now run the interactive rebase against the initial commit:
$ git rebase -i 8a7b6c5
Your editor opens with:
pick 9e8f7d6 Set up Express router
pick a1c2d3e Add user creation endpoint
pick b7a6f5e Fix typo in error message
pick d3e9c8a WIP save
pick f4a1b2c Add input validation to user endpoint
# Rebase 8a7b6c5..f4a1b2c onto 8a7b6c5 (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
The commits are listed oldest to newest -- the opposite of git log. This matters. Let me walk through each command.
pick
pick uses the commit as-is. This is the default. If you save the file without changing anything, Git replays every commit unchanged and you end up with the same history.
reword
reword applies the commit but opens your editor to change the commit message. The code changes stay identical. Use this when the commit content is fine but the message is lazy or unclear.
reword a1c2d3e Add user creation endpoint
After saving the todo list, Git will stop and open your editor for that commit's message. You fix it and save. Rebase continues.
edit
edit stops the rebase after applying a commit. You get dropped back to the command line with that commit applied but the rebase paused. This lets you:
- Amend the commit (add or remove files, change code)
- Split the commit into multiple smaller commits
- Run tests to verify the state at that point
edit a1c2d3e Add user creation endpoint
When Git stops:
Stopped at a1c2d3e... Add user creation endpoint
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
squash
squash combines a commit with the one above it (the previous commit in the todo list). Git opens your editor with both commit messages concatenated so you can write a combined message.
pick a1c2d3e Add user creation endpoint
squash b7a6f5e Fix typo in error message
This produces one commit with both sets of changes. The editor will show:
# This is a combination of 2 commits.
# This is the 1st commit message:
Add user creation endpoint
# This is the commit message #2:
Fix typo in error message
You edit this into a single coherent message and save.
fixup
fixup is squash without the message editing. It combines the commit with the one above it and keeps only the previous commit's message. This is the one you will use most often -- it is perfect for folding small fixes into the commits they belong to.
pick a1c2d3e Add user creation endpoint
fixup b7a6f5e Fix typo in error message
Result: one commit with the message "Add user creation endpoint" containing both sets of changes.
Since Git 2.32, you can use fixup -C to keep the fixup commit's message instead, or fixup -c to keep it but open the editor.
drop
drop removes the commit entirely. The changes in that commit disappear from history. You can also just delete the line from the todo list -- same effect.
drop d3e9c8a WIP save
exec
exec runs a shell command between commits. If the command fails (nonzero exit code), the rebase pauses. This is incredibly useful for running tests at each point in history.
pick 9e8f7d6 Set up Express router
exec npm test
pick a1c2d3e Add user creation endpoint
exec npm test
If tests fail after any commit, the rebase stops so you can investigate.
Reordering and Combining Commits
The most common interactive rebase operation is rearranging commits so related changes are adjacent, then squashing them together.
Say your log looks like this after a morning of hacking:
$ git log --oneline -7
g7h8i9j Add tests for user validation
f4a1b2c Fix validation bug found during testing
e5d6c7b Update README with API docs
d3e9c8a Add user input validation
c2b3a4f Fix missing require in user route
b1a2c3d Add user creation route
a0b1c2d Set up Express project
The validation fix (f4a1b2c) should be squashed into the validation commit (d3e9c8a). The missing require fix (c2b3a4f) belongs with the route creation (b1a2c3d). The README update (e5d6c7b) should come last.
$ git rebase -i a0b1c2d
Original todo list:
pick b1a2c3d Add user creation route
pick c2b3a4f Fix missing require in user route
pick d3e9c8a Add user input validation
pick e5d6c7b Update README with API docs
pick f4a1b2c Fix validation bug found during testing
pick g7h8i9j Add tests for user validation
Rearranged and squashed:
pick b1a2c3d Add user creation route
fixup c2b3a4f Fix missing require in user route
pick d3e9c8a Add user input validation
fixup f4a1b2c Fix validation bug found during testing
pick g7h8i9j Add tests for user validation
pick e5d6c7b Update README with API docs
Now you have four clean commits instead of six messy ones. The bug fixes are folded into the commits they belong to. The README update is moved to the end where it logically belongs.
Splitting Commits into Smaller Pieces
Sometimes a commit does too much. You added a new endpoint AND refactored the middleware AND updated the config -- all in one commit. Interactive rebase lets you split it.
Use the edit command on the commit you want to split:
edit d3e9c8a Add validation and refactor middleware and update config
When Git pauses at that commit:
# Undo the commit but keep the changes staged
$ git reset HEAD~1
# Now all the changes from that commit are in your working tree, unstaged.
# Stage and commit them in logical groups:
$ git add src/middleware/auth.js src/middleware/errorHandler.js
$ git commit -m "Refactor auth and error handling middleware"
$ git add src/routes/users.js src/validators/userSchema.js
$ git commit -m "Add input validation to user endpoint"
$ git add config/default.json
$ git commit -m "Add validation config settings"
# Continue the rebase
$ git rebase --continue
One bloated commit becomes three focused ones. Each commit has a single responsibility and can be reviewed, reverted, or cherry-picked independently.
If you want to split changes within a single file, use git add -p (patch mode) to stage individual hunks:
$ git add -p src/routes/users.js
Git will show you each changed hunk and ask whether to stage it. Type y to stage, n to skip, s to split the hunk into smaller pieces.
Editing Commit Contents Mid-Rebase
The edit command is not just for splitting. You can use it to change the actual code in a historical commit.
Say you realize your third commit introduced a debug console.log that you forgot to remove. Instead of adding a new "remove debug log" commit, rewrite history:
$ git rebase -i HEAD~5
Mark the offending commit with edit:
pick a1c2d3e Set up user routes
edit b2c3d4f Add authentication middleware
pick c3d4e5g Add error handling
pick d4e5f6h Add tests
pick e5f6g7i Update documentation
When Git stops at the auth middleware commit:
# Remove the debug log
$ code src/middleware/auth.js # edit the file
# Stage the fix
$ git add src/middleware/auth.js
# Amend the commit (the changes fold into the existing commit)
$ git commit --amend --no-edit
# Continue
$ git rebase --continue
The debug log never existed in your history. The commit is clean as if you never made the mistake.
Autosquash and Fixup Commits Workflow
This is the workflow that changed everything for me. Instead of tracking which commits need to be squashed later, you tell Git at commit time.
When you find a bug that belongs in an earlier commit, commit the fix with --fixup:
# You realize your "Add user validation" commit had a bug
$ git log --oneline -5
e5f6g7i Add tests
d4e5f6h Update docs
c3d4e5g Add error handling
b2c3d4f Add user validation
a1c2d3e Set up routes
# Fix the bug
$ code src/validators/user.js
# Commit with --fixup pointing to the target commit
$ git add src/validators/user.js
$ git commit --fixup b2c3d4f
This creates a commit with the message fixup! Add user validation. Now when you rebase:
$ git rebase -i --autosquash a1c2d3e
Git automatically reorders the todo list and marks the fixup commit with fixup:
pick b2c3d4f Add user validation
fixup 1a2b3c4 fixup! Add user validation
pick c3d4e5g Add error handling
pick d4e5f6h Update docs
pick e5f6g7i Add tests
You can also use --squash instead of --fixup if you want to edit the combined message:
$ git commit --squash b2c3d4f
To make autosquash the default for all interactive rebases:
$ git config --global rebase.autosquash true
With this enabled, you never have to manually move fixup commits around. Just commit with --fixup, and git rebase -i handles the rest.
Rebase Onto and Transplanting Branches
Sometimes you branched off the wrong base, or you need to move a chain of commits from one branch to another. --onto handles this.
The syntax is:
$ git rebase --onto <new-base> <old-base> <branch>
Scenario: Wrong Parent Branch
You started feature/auth from feature/users instead of main. Now feature/users got reverted and you need feature/auth to sit directly on main.
main: A --- B --- C
\
feature/users: D --- E
\
feature/auth: F --- G --- H
You want F, G, H on top of C (main), without D and E:
$ git rebase --onto main feature/users feature/auth
Result:
main: A --- B --- C
\
feature/auth: F' --- G' --- H'
The commits F, G, H are replayed on top of main, completely detached from feature/users.
Scenario: Extracting a Range of Commits
You can also use --onto to remove commits from the middle of a branch. If commit D introduced a bug and you want to drop it:
A --- B --- C --- D --- E --- F
$ git rebase --onto C D F
Result:
A --- B --- C --- E' --- F'
Commit D is gone. E and F are replayed on top of C.
Handling Conflicts During Interactive Rebase
Conflicts during rebase are inevitable when you reorder or squash commits that touch the same files. Here is how to handle them efficiently.
When a conflict occurs, Git stops and tells you:
CONFLICT (content): Merge conflict in src/routes/users.js
error: could not apply b2c3d4f... Add user validation
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
The workflow:
# 1. See which files have conflicts
$ git status
Unmerged paths:
both modified: src/routes/users.js
# 2. Open the file and resolve the conflict markers
$ code src/routes/users.js
The conflict markers look like this:
var express = require('express');
var router = express.Router();
<<<<<<< HEAD
router.post('/users', function(req, res) {
var user = req.body;
db.createUser(user, function(err, result) {
=======
router.post('/users', validateUser, function(req, res) {
var user = sanitize(req.body);
db.createUser(user, function(err, result) {
>>>>>>> b2c3d4f (Add user validation)
Pick the right version (or combine them), remove the markers:
var express = require('express');
var router = express.Router();
router.post('/users', validateUser, function(req, res) {
var user = sanitize(req.body);
db.createUser(user, function(err, result) {
Then continue:
$ git add src/routes/users.js
$ git rebase --continue
If you get completely stuck, you can always abort:
$ git rebase --abort
This restores everything to exactly where you were before the rebase started. No harm done.
Conflict Strategy: Take Ours or Theirs
When you know for certain which version you want, you can resolve entire files without manual editing:
# Keep the version from the branch being rebased onto
$ git checkout --ours src/routes/users.js
$ git add src/routes/users.js
# Keep the version from the commit being replayed
$ git checkout --theirs src/routes/users.js
$ git add src/routes/users.js
Note: during rebase, --ours and --theirs are swapped compared to merge. In rebase, --ours is the branch you are rebasing onto (the upstream), and --theirs is the commit being replayed (your changes). This is confusing. It trips up experienced developers regularly. Remember: rebase replays your commits on top of the target, so from Git's perspective, the target is "ours" and your commit is "theirs."
Rebase vs Merge: Commit Strategies and Team Conventions
Interactive rebase is not just a tool -- it represents a philosophy about commit history. Here is my opinionated take on when to use it and when not to.
When to Rebase
Before opening a pull request. Clean up your WIP commits, squash related changes, reword lazy messages. Your reviewer should see a coherent story, not your stream of consciousness.
When updating a feature branch from main. Instead of git merge main which creates a merge commit, use git rebase main to keep your commits on top. Linear history is easier to read, easier to bisect, and easier to revert.
When you own the branch. If you are the only person working on a feature branch, rebase freely. There is zero risk.
When Not to Rebase
After pushing to a shared branch that others are working on. Rebase rewrites commit SHAs. If someone else has pulled your commits, their history diverges from yours. This creates a mess that ranges from annoying to catastrophic.
On the main branch. Never. Period. Main's history is sacred and shared.
When the merge history matters. Some teams want to see exactly when branches were integrated. Merge commits preserve that information. Rebase erases it.
Team Conventions
For teams, I recommend this convention:
1. Developers rebase their own feature branches before merging
2. The merge to main uses --no-ff to create a merge commit
3. The merge commit shows WHEN the feature was integrated
4. The rebased commits show WHAT was done, in clean logical steps
This gives you the best of both worlds: clean per-feature history with clear integration points.
A practical Git alias for this workflow:
$ git config --global alias.sync '!git fetch origin && git rebase origin/main'
$ git config --global alias.ship '!git checkout main && git merge --no-ff @{-1} && git push'
Recovering from Rebase Mistakes with Reflog
Rebase rewrites history. What if you mess up? Reflog is your safety net.
Git's reflog records every time HEAD moves. Every commit, merge, rebase, checkout, reset -- all of it. It is a local-only history of where your HEAD has been.
$ git reflog
f4a1b2c (HEAD -> feature/users) HEAD@{0}: rebase (finish): returning to refs/heads/feature/users
f4a1b2c HEAD@{1}: rebase (squash): Add user validation
d3e9c8a HEAD@{2}: rebase (start): checkout main
a9b8c7d HEAD@{3}: commit: WIP validation
e5f6a7b HEAD@{4}: commit: Fix typo
b2c3d4f HEAD@{5}: commit: Add user validation
See HEAD@{3} through HEAD@{5}? Those are your original commits before the rebase. To undo the entire rebase and go back to exactly where you were:
$ git reset --hard HEAD@{3}
Boom. Your branch is restored to its pre-rebase state. All the original commits are back.
The reflog entries persist for 90 days by default (configurable with gc.reflogExpire). So even if you do not realize your rebase went wrong until next week, you can still recover.
Another approach is to create a backup branch before any dangerous rebase:
# Before rebasing
$ git branch backup/feature-users
# Do the rebase
$ git rebase -i main
# If something goes wrong
$ git reset --hard backup/feature-users
# If everything is fine, delete the backup
$ git branch -d backup/feature-users
I do this reflexively before any complex rebase. The cost is one line of typing. The insurance is priceless.
Automating Rebase Patterns with exec
The exec command in interactive rebase runs a shell command after a commit is applied. If the command exits with a nonzero status, the rebase pauses. This turns interactive rebase into a powerful automation tool.
Running Tests at Every Commit
Ensure every commit in your history is independently buildable and testable:
$ git rebase -i main --exec "npm test"
This adds exec npm test after every pick line automatically. If tests fail at any commit, you can fix it right there:
# Tests failed. Fix the issue.
$ code src/routes/users.js
$ git add src/routes/users.js
$ git commit --amend --no-edit
$ git rebase --continue
Linting Every Commit
$ git rebase -i main --exec "npx eslint src/"
Running Multiple Commands
$ git rebase -i main --exec "npm run lint && npm test && npm run build"
Custom Validation Scripts
You can write Node.js scripts that validate commit state and use them with exec:
// scripts/validate-commit.js
var fs = require('fs');
var path = require('path');
var packagePath = path.join(__dirname, '..', 'package.json');
var pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
// Verify package.json is valid
if (!pkg.name || !pkg.version) {
console.error('ERROR: package.json missing required fields');
process.exit(1);
}
// Verify no debug logs left in source
var srcDir = path.join(__dirname, '..', 'src');
var files = fs.readdirSync(srcDir, { recursive: true });
var violations = [];
files.forEach(function(file) {
if (typeof file !== 'string' || !file.endsWith('.js')) return;
var filePath = path.join(srcDir, file);
var content = fs.readFileSync(filePath, 'utf8');
if (content.includes('console.log')) {
violations.push(filePath);
}
});
if (violations.length > 0) {
console.error('ERROR: console.log found in:');
violations.forEach(function(v) {
console.error(' ' + v);
});
process.exit(1);
}
console.log('Commit validation passed');
process.exit(0);
$ git rebase -i main --exec "node scripts/validate-commit.js"
Interactive Rebase with --autostash
If you have uncommitted changes in your working tree, Git refuses to rebase:
error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.
The --autostash flag handles this automatically:
$ git rebase -i main --autostash
Git stashes your working tree changes before the rebase starts and pops them back when it finishes. If the stash pop produces conflicts, Git tells you and you resolve them.
Make this the default so you never have to think about it:
$ git config --global rebase.autoStash true
With this set, every rebase automatically stashes and restores your uncommitted work. One less thing to think about.
Rebase Safety with --fork-point
When rebasing a feature branch onto an updated main branch, there is a subtle problem. If main has been rebased or had commits removed (via force push from another developer), a naive git rebase main might replay commits that were already incorporated, causing phantom conflicts.
The --fork-point option uses the reflog to find the actual point where your branch diverged, even if the upstream has been rewritten:
$ git rebase --fork-point main
This is the default behavior for git rebase since Git 2.0, but understanding what it does matters for troubleshooting. If you are seeing unexpected conflicts when rebasing onto a branch that was force-pushed, --fork-point is likely the issue.
To explicitly disable it (for example, when rebasing onto a completely different branch):
$ git rebase --no-fork-point main
--update-refs
Git 2.38 introduced --update-refs, which is a game-changer for stacked branches. If you have multiple branches in a chain:
main --- A
\
B --- C (feature/step-1)
\
D --- E (feature/step-2)
\
F --- G (feature/step-3)
Normally, rebasing feature/step-1 onto an updated main would leave feature/step-2 and feature/step-3 pointing to their old commits. You would have to rebase each one manually.
With --update-refs:
$ git checkout feature/step-3
$ git rebase --update-refs main
Git updates all the intermediate branch pointers as it replays the commits. All three feature branches land on the correct commits after one rebase.
Enable it globally:
$ git config --global rebase.updateRefs true
Complete Working Example: Cleaning Up a Feature Branch
Let me walk through a realistic scenario end to end. You have been building a user management API on a feature branch. After a few days of development, your history looks like this:
$ git log --oneline feature/user-api
h8i9j0k Add rate limiting to user endpoints
g7h8i9j WIP - rate limiting
f6g7h8i Fix: forgot to export validator
e5f6g7h console.log debugging session (REMOVE LATER)
d4e5f6g Add user input validation
c3d4e5f Fix missing semicolons
b2c3d4e Add user CRUD endpoints
a1b2c3d Set up user route module
That is eight commits but only four logical changes. Let us clean this up.
$ git rebase -i a1b2c3d~1
The ~1 means "start from one commit before a1b2c3d", which includes a1b2c3d in the rebase. Your editor opens with:
pick a1b2c3d Set up user route module
pick b2c3d4e Add user CRUD endpoints
pick c3d4e5f Fix missing semicolons
pick d4e5f6g Add user input validation
pick e5f6g7h console.log debugging session (REMOVE LATER)
pick f6g7h8i Fix: forgot to export validator
pick g7h8i9j WIP - rate limiting
pick h8i9j0k Add rate limiting to user endpoints
Here is the plan:
- Keep "Set up user route module" as-is
- Squash "Fix missing semicolons" into "Add user CRUD endpoints"
- Drop the console.log debugging commit entirely
- Squash "Fix: forgot to export validator" into "Add user input validation"
- Squash "WIP - rate limiting" into "Add rate limiting to user endpoints" and reorder so the final version comes first
Edit the todo list:
pick a1b2c3d Set up user route module
pick b2c3d4e Add user CRUD endpoints
fixup c3d4e5f Fix missing semicolons
pick d4e5f6g Add user input validation
fixup f6g7h8i Fix: forgot to export validator
drop e5f6g7h console.log debugging session (REMOVE LATER)
pick h8i9j0k Add rate limiting to user endpoints
fixup g7h8i9j WIP - rate limiting
Wait -- notice I moved the fixup for the validator export right after the validation commit and moved the drop for the console.log commit after both validation-related commits. I also moved the final rate limiting commit before its WIP predecessor so fixup squashes upward correctly.
Actually, let me reconsider. Fixup squashes into the commit above it (the preceding pick). So the WIP commit needs to be AFTER the final rate limiting commit to get squashed into it. But the WIP commit was created BEFORE the final commit. This means we need to reorder them:
pick a1b2c3d Set up user route module
pick b2c3d4e Add user CRUD endpoints
fixup c3d4e5f Fix missing semicolons
pick d4e5f6g Add user input validation
fixup f6g7h8i Fix: forgot to export validator
drop e5f6g7h console.log debugging session (REMOVE LATER)
pick h8i9j0k Add rate limiting to user endpoints
fixup g7h8i9j WIP - rate limiting
Save and close the editor. Git processes each line:
Successfully rebased and updated refs/heads/feature/user-api.
Now check the result:
$ git log --oneline feature/user-api
d1e2f3g Add rate limiting to user endpoints
c0d1e2f Add user input validation
b9c0d1e Add user CRUD endpoints
a8b9c0d Set up user route module
Four clean, focused commits. No WIP, no fixups, no debugging garbage. Each commit does one thing and has a clear message. This is ready for code review.
Let me also reword that first commit to be more descriptive:
$ git rebase -i a8b9c0d~1
reword a8b9c0d Set up user route module
pick b9c0d1e Add user CRUD endpoints
pick c0d1e2f Add user input validation
pick d1e2f3g Add rate limiting to user endpoints
Git opens the editor for the commit message. Change it:
Initialize user management module with Express router
Set up the Express router, middleware chain, and module exports
for the /api/users endpoint. Includes error handling wrapper.
Save. The rebase completes. Now let us verify everything still works:
$ git rebase -i HEAD~4 --exec "npm test"
Executing: npm test
User API
✓ should create a user (45ms)
✓ should validate required fields (12ms)
✓ should rate limit excessive requests (102ms)
3 passing (159ms)
Executing: npm test
...
Successfully rebased and updated refs/heads/feature/user-api.
All tests pass at every commit. The branch is clean and ready to merge.
Common Issues and Troubleshooting
1. "Cannot rebase: You have unstaged changes"
error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.
You have modified files in your working tree. Either commit them, stash them, or use --autostash:
$ git stash
$ git rebase -i main
$ git stash pop
# Or just:
$ git rebase -i main --autostash
2. "Could not apply ... interactive rebase in progress"
error: could not apply a1b2c3d... Add user validation
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: If you prefer to skip this patch, run "git rebase --skip".
This is a conflict during rebase. Resolve it:
$ git status # see which files conflict
$ code <conflicted-file> # resolve the markers
$ git add <conflicted-file> # mark as resolved
$ git rebase --continue # proceed
Do NOT run git commit after resolving -- git rebase --continue handles the commit.
3. "No rebase in progress?"
fatal: No rebase in progress?
You ran git rebase --continue or --abort but there is no active rebase. Check if it already completed or never started. If you are in a detached HEAD state from a failed rebase, check git reflog to find your branch.
4. "Refusing to rebase onto a branch that is not an ancestor"
fatal: refusing to merge unrelated histories
This happens when you try to rebase onto a branch that shares no common ancestor. If this is intentional (transplanting commits between unrelated repos), use:
$ git rebase --onto target-branch --root
5. "The following untracked working tree files would be overwritten"
error: The following untracked working tree files would be overwritten by checkout:
src/newfile.js
Please move or remove them before you merge.
A commit being replayed creates a file that already exists as untracked in your working tree. Either delete the untracked file, stash it, or commit it before rebasing.
6. Commits Appear Duplicated After Push
$ git push origin feature/users
To github.com:user/repo.git
! [rejected] feature/users -> feature/users (non-fast-forward)
You rebased commits that were already pushed. The remote has the old SHAs, your local has new SHAs. Either:
# Force push (ONLY if you own this branch and no one else is using it)
$ git push --force-with-lease origin feature/users
Always use --force-with-lease instead of --force. It fails if the remote has commits you do not have locally, preventing you from overwriting someone else's work.
Best Practices
Never rebase shared branches. If other developers have pulled your commits, rebasing rewrites SHAs and causes divergent histories. Only rebase branches you own exclusively.
Always use
--force-with-leaseinstead of--force. It is the same as force push but with a safety check -- it fails if the remote branch has moved since your last fetch. This prevents accidentally overwriting a colleague's work.Create backup branches before complex rebases. One command:
git branch backup/my-feature. If the rebase goes sideways,git reset --hard backup/my-featuregets you back instantly. Delete the backup when you are confident the rebase succeeded.Set
rebase.autosquash trueglobally. This makesgit commit --fixupandgit commit --squashactually useful by automatically reordering fixup commits during interactive rebase. Without this, you have to manually move them every time.Set
rebase.autoStash trueglobally. Eliminates the "you have unstaged changes" error by automatically stashing and popping your working tree changes around the rebase.Run tests at every commit with
--exec. After cleaning up history, verify every commit independently compiles and passes tests:git rebase -i main --exec "npm test". A history where individual commits break the build is worse than a messy history.Use
--update-refsfor stacked branches. If you work with branch chains (common in stacked PR workflows), enablerebase.updateRefs trueglobally. It saves you from manually rebasing every branch in the chain.Keep commits atomic. Each commit should represent one logical change that could be reverted independently. If you need to split a commit, use
editandgit reset HEAD~1to unstage, then re-commit in logical pieces.Reword before merging, not after. Take 30 seconds to reword lazy commit messages before your PR. "Fix stuff" and "WIP" are not acceptable on a shared branch. Your future self will thank you when running
git bisectorgit blame.Learn the reflog. Before you panic about a botched rebase, run
git reflog. Your old commits are almost certainly still there.git reset --hard HEAD@{n}is the universal undo button for any rebase mistake.
References
- Git Documentation: git-rebase -- The official reference for all rebase options and flags
- Git Documentation: git-reflog -- Understanding the reflog for recovery
- Pro Git Book: Rewriting History -- Comprehensive chapter on interactive rebase from the Pro Git book
- Git Documentation: git-commit --fixup -- Fixup and autosquash commit options
- Git 2.38 Release Notes: --update-refs -- The update-refs feature for stacked branches
- Atlassian Git Tutorials: Rewriting History -- Practical visual guide to rebasing