On this page
Git rebase is the command people are told both to love and to fear, and both reactions come from the same fact: it rewrites history. Used on your own unpushed branch, it gives you a clean, linear series of commits that reviewers can read. Used on commits other people already have, it creates the kind of mess that ends with someone running git reset --hard and losing an afternoon. This part explains what rebase actually does to your commits, how to squash and reorder them with interactive rebase, what changes when a rebase hits a conflict, and how to push the result without overwriting a teammate's work.
It builds on Part 3, where merging combined two histories. Rebase reaches a similar end state a different way. Everything below was run on Git 2.43.0 in a scratch repository with a local bare repository as the remote.
What rebase does: it replays commits
Say you branched feature/coupon from main, made two commits, and meanwhile main gained a commit:
* f8be553 Apply coupon (feature/coupon)
* 488d7d4 Add coupon field
| * f8ad49d Add Other (main)
|/
* 65992c6 Add CartNow run, from the feature branch:
git switch feature/coupon
git rebase main* d83f9ba Apply coupon
* 7fda115 Add coupon field
* f8ad49d Add Other
* 65992c6 Add CartGit took each of your commits, re-applied its changes on top of the new tip of main, and moved the branch pointer to the result. Look at the hashes: 488d7d4 became 7fda115 and f8be553 became d83f9ba. Same changes and same messages, but different commits, because a commit's hash covers its parent (from Part 1). The old commits still exist for now, but nothing on a branch points at them.
Merge or rebase?
| merge | rebase | |
|---|---|---|
| History shape | Keeps both lines and adds a merge commit | Straight line, as if you started from the latest main |
| Existing commits | Unchanged | Replaced with new commits (new hashes) |
| Conflicts | Resolved once, in the merge commit | Possibly resolved once per replayed commit |
| Safe on shared commits | Yes | No |
Neither is "correct". Teams choose based on whether they value an exact record of what happened (merge) or a readable history (rebase).
The one rule: don't rebase commits other people have
Rebase is safe on commits that exist only in your clone. Once you have pushed a branch and someone has based work on it, rewriting those commits means their copy and yours no longer share history, and they will have to untangle it. A workable rule: rebase your own feature branch freely until others depend on it; never rebase main or any branch several people push to.
Undoing a rebase
If a rebase goes somewhere you don't like, Git has kept a pointer to where the branch was. Right after the rebase above, ORIG_HEAD pointed at f8be553, the old tip:
git reset --hard ORIG_HEADThe branch returns to its pre-rebase state. Be aware that ORIG_HEAD is overwritten by other commands (merge, reset, another rebase), so if you've done anything since, use the reflog instead. git reflog lists every step the rebase took and the position before it, and you can reset to any of those entries. Part 5 goes into reflog recovery in detail.
Interactive rebase: clean up before you share
git rebase -i lets you edit the list of commits before they are replayed. To rework everything since main:
git rebase -i mainGit opens your editor with a todo list, oldest commit first, followed by help text. This is the real list of commands from Git 2.43.0:
| Command | What it does |
|---|---|
pick | Use the commit as is |
reword | Use the commit but edit its message |
edit | Stop at this commit so you can amend it |
squash | Meld into the previous commit and combine the messages |
fixup | Like squash, but keep only the previous commit's message |
drop | Remove the commit |
exec | Run a shell command after this commit (for example your test suite) |
Lines are executed top to bottom, so reordering lines reorders commits. The help text also warns: if you remove a line, that commit will be lost. Use drop when you mean it so the intent is visible.
Example. After a messy afternoon the branch has four commits: commit 1, commit 2, commit 3 and commit 4. In the todo list I changed the first to reword, the second to drop and the fourth to squash. The result was two commits, a reworded first one and one combining 3 and 4, and the file added by the dropped commit was gone from the working tree.
Fixup commits and autosquash
A faster habit for small corrections: when you notice a problem in an earlier commit, commit the fix with a marker instead of editing history immediately.
git commit --fixup a1b2c3d # a1b2c3d = the commit being corrected
# ...later, before opening the pull request:
git rebase -i --autosquash mainThe fixup commit is named fixup! Add helper. With --autosquash Git pre-arranges the todo list, moving it directly under the commit it fixes and marking it fixup. I tested this with a file fixed after two later commits: the resulting history had the original Add helper commit containing the corrected file, and the later commit remained after it. To make it the default for interactive rebases: git config --global rebase.autoSquash true.
If your fixup touches the same lines as a later commit, the replay can conflict. That is normal; resolve it as described next.
Conflicts during a rebase
Because rebase replays commits one at a time, a conflict stops it at the commit being applied, and you may meet several. You get these commands:
git rebase --continueafter resolving and staging the filesgit rebase --skipto drop the current commit entirelygit rebase --abortto return to the state before the rebase started
Could not apply b9df546... Count returns 2 on branch
...
You are currently rebasing branch 'feature/count' on 'b2b5be8'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)Resolve the markers exactly as in Part 3, git add the file, then continue. Note that git rebase --continue may open an editor for the commit message; set GIT_EDITOR=true for that single command if you want to keep the message as is.
"Ours" and "theirs" are reversed
This is the part that surprises everyone. In a merge, "ours" is the branch you are on. In a rebase, Git is replaying your commits onto the other branch, so the roles swap. I checked this with a conflict where main returned 1 and my branch returned 2:
<<<<<<< HEAD
public function count(): int { return 1; }
=======
public function count(): int { return 2; }
>>>>>>> b9df546 (Count returns 2 on branch)git restore --ours Cart.phpproducedreturn 1;, the version frommain.git restore --theirs Cart.phpproducedreturn 2;, which is your own commit being replayed.
So during a rebase, "ours" means the branch you are rebasing onto, and "theirs" means your own work. Read the marker label (HEAD versus the commit message) rather than trusting the words.
Moving a branch with --onto
Sometimes a branch was started from the wrong place. Say topicB was branched from topicA, and topicA is not going to be merged, but you want topicB's own commit on main:
git rebase --onto main topicA topicBRead it as "take the commits after topicA up to topicB, and replay them onto main". In my test topicB ended up with just its own commit on top of main, with topicA's commit gone from its history.
Pulling with rebase
When your local branch and the remote branch both have new commits, a plain git pull creates a merge commit. git pull --rebase instead replays your local commits on top of what you fetched, keeping a straight line. You can make it your default with git config --global pull.rebase true. It is safe for the same reason as before: your local, unpushed commits are the ones being rewritten.
Pushing after a rebase
After rebasing a branch you already pushed, your local history and the remote's have diverged, and a normal push is refused:
! [rejected] f3 -> f3 (non-fast-forward)
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart.The hint suggests git pull, which is the wrong fix here because it would merge your old commits back in. The right tool is a forced push, but use the safer variant:
git push --force-with-lease + d0b464f...ff4a732 f3 -> f3 (forced update)Plain --force overwrites whatever is on the remote. --force-with-lease only overwrites if the remote branch is still where your last fetch saw it. When a teammate pushed in the meantime, it refuses:
! [rejected] f3 -> f3 (stale info)One limit worth knowing: the lease compares against your remote-tracking reference. If you run git fetch (which updates that reference) and don't look at what arrived, the lease will then pass and you can still overwrite the new commits. Check git log origin/f3 before forcing, or fetch only when you intend to review.
Common problems
| Symptom | Cause and fix |
|---|---|
| Push rejected after rebase (non-fast-forward) | History was rewritten. Use git push --force-with-lease on a branch only you push to. |
| Same conflict repeated for each commit | Rebase replays commits one by one. Squash the branch first, or enable rerere (Part 3). |
| "ours"/"theirs" look backwards | They are swapped during rebase. "ours" is the branch you're rebasing onto. |
| Commits seem to have disappeared | Check git reflog and reset to the entry before the rebase. |
| Teammates' history is now messy | You rebased shared commits. Coordinate, and have them rebase their work onto the new branch. |
| Not sure how to stop | git rebase --abort restores the branch to its pre-rebase state. |
Before you move on
Rebase only commits that nobody else has. Commit with --fixup and tidy with --autosquash before asking for review. Remember that ours and theirs swap during a rebase. Push rewritten branches with --force-with-lease, never plain --force. And if a rebase goes wrong, git rebase --abort or the reflog gets you back.
References: git-rebase, git-push (see --force-with-lease) and git-pull in the official Git documentation.
Related reading: Part 1: Git Staging Area Explained, Part 2: Git Commit Best Practices and Part 3: Git Merge Conflicts.
Free resources for this topic
Get one practical tip every week
New tutorials, fixes and free cheat sheets from DEV Talk, straight to your inbox. No spam, unsubscribe in one click.
Free download: Git cheat sheet (PDF)
Have a question about this post?
Ask the community: other developers and the author can answer.
Get notified about new articles
One click, no email needed. Turn it off anytime with the bell at the top.
Get new articles on your iPhone
Add DEV Talk to your Home Screen, open it, then tap the bell.
Enjoying the content?
If this article helped you, you can support the author with a coffee. It keeps new posts coming.
Comments 0
No comments yet
Be the first to share your thoughts.