How-to

git rebase: how it works, interactive mode, and squashing commits

Updated: · Skillaria.Top

git rebase moves the current branch's commits onto a new base: Git temporarily sets your commits aside, moves the branch to the tip of the target, and reapplies them as new commits. This gives you linear history without merge commits, and with git rebase -i you can squash, reword or drop commits. Only rewrite commits that haven't been published to others yet.

What git rebase does

git rebase "transplants" your branch onto a different point in history. Say you branched feature off main a week ago, and main has moved on since then. After a rebase, your branch looks as if you'd just started working — from the latest main commit.

git switch feature
git fetch origin
git rebase origin/main

Under the hood there are three steps. Git finds the common ancestor of the branches, saves the changes from each of your commits after it in order, moves the branch to origin/main, and reapplies the saved changes one by one. The result is new commits with the same content and messages but different hashes.

Before:  A---B---C---F---G   main
                  \
                   D---E     feature

After:   A---B---C---F---G   main
                          \
                           D'---E'   feature

The primes on D' and E' aren't decoration. These are different objects with different hashes and a different parent. That's where the main rule of rebase comes from, covered below.

The golden rule: don't rebase published commits

Rebase rewrites history. As long as the commits exist only on your machine, that's safe. But if teammates have already pulled the branch, after a rebase they'll still have the old D and E, you'll have the new D' and E', and on the next pull history gets duplicated and conflicts have to be resolved again.

The practical rule: use rebase on your local branches and on personal feature branches that only one person works on. Shared branches — main, develop, release branches — are never rebased.

How to push a branch after a rebase

If the branch was already on the server, a regular git push will be rejected: your local history no longer continues the remote one. You need a force push, and the safe kind:

git push --force-with-lease

Unlike --force, the --force-with-lease flag refuses to overwrite the branch if commits you haven't seen have appeared on the server. That way you won't accidentally wipe out someone else's work.

Resolving conflicts during a rebase

Since commits are applied one at a time, a conflict can happen on any of them — even several times in a row. Git stops and tells you which commit it couldn't apply.

$ git rebase origin/main
Auto-merging src/api.js
CONFLICT (content): Merge conflict in src/api.js
error: could not apply 4e1f9a2... Add request caching
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".

What to do:

  1. Open the conflicted file, pick the right version, and remove the <<<<<<<, =======, >>>>>>> markers.
  2. Mark the file as resolved: git add src/api.js. No need to commit.
  3. Continue: git rebase --continue. Git applies the next commit.

Other commands for controlling the process:

CommandWhat it does
git rebase --continueContinue after resolving a conflict
git rebase --skipSkip the current commit entirely (its changes will be lost)
git rebase --abortStop the rebase and return the branch to its original state

Watch out for the "ours" vs "theirs" confusion. During a rebase, HEAD is the base you're moving the branch onto, and the "incoming" changes are your own commits. So during a rebase git checkout --ours takes the version from main, and --theirs takes yours. With merge it's the other way around.

Recurring conflicts: rerere

If you regularly rebase a long-lived branch and keep resolving the same conflict over and over, enable git config --global rerere.enabled true. Git will remember how you resolved the conflict and apply the resolution itself next time.

Interactive rebase: cleaning up history before publishing

The most valuable part of rebase is interactive mode. It lets you tidy up your commits before showing them in review: squash "fix", "fix typo", "another fix" into one meaningful commit, rewrite messages, reorder or drop what's not needed.

git rebase -i HEAD~4        # the last 4 commits
git rebase -i origin/main   # all commits on the branch that aren't in main

Git opens an editor with a list of commits — oldest to newest (unlike git log):

pick 1a2b3c4 Add login form
pick 5d6e7f8 fix
pick 9a0b1c2 Email validation
pick 3d4e5f6 typo in validation

# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash" but keep only the previous commit's log message
# d, drop = remove commit

Change the word at the start of a line, save, and close the editor — Git runs the plan from top to bottom.

CommandEffect
pickKeep the commit as is
rewordKeep the changes but rewrite the message
editStop at the commit to change its contents (git commit --amend) or split it into several
squashMeld into the previous commit, combine and edit the messages
fixupMeld into the previous commit, discard this commit's message
dropRemove the commit along with its changes

Reordering lines reorders commits; deleting a line is the same as drop.

Git squash: how to combine commits into one

There's no separate git squash command in Git. You can squash commits in three ways, and interactive rebase is the most flexible of them.

Option 1. Interactive rebase

For the example above, let's turn four commits into two clean ones:

pick 1a2b3c4 Add login form
fixup 5d6e7f8 fix
pick 9a0b1c2 Email validation
fixup 3d4e5f6 typo in validation

The first commit in a group must stay pick: squash and fixup meld a commit into the previous one, and the very first has nothing to meld into.

Option 2. Fixup commits and autosquash

If you know up front which commit you're fixing, create the fix already marked:

git commit --fixup=1a2b3c4
git rebase -i --autosquash origin/main

Git places the fixup commit under the right one and marks it as fixup — all you have to do is save the plan. To make this the default, run git config --global rebase.autoSquash true.

Option 3. Squash the whole branch into one commit

git reset --soft $(git merge-base HEAD origin/main)
git commit -m "Email login with validation"

reset --soft moves the branch pointer back to where it branched off but keeps all the changes staged — you just make one commit. Other options are git merge --squash when merging into the main branch, or the "Squash and merge" button in a pull request.

Useful rebase variants

git pull --rebase

A regular pull merges the remote branch into yours and creates a merge commit if you committed in parallel with teammates. git pull --rebase instead replays your local commits on top of the latest remote ones. To make it the default: git config --global pull.rebase true. This is exactly the safe zone for rebase — nobody has seen your local commits yet.

--onto: move part of a branch

You branched feature-b off feature-a, and now you want feature-b to start directly from main, without the feature-a commits:

git rebase --onto main feature-a feature-b

Read it as: take the commits in feature-b that aren't in feature-a and transplant them onto main.

--exec: check every commit

git rebase -i --exec "npm test" origin/main

Tests run after each applied commit; if they fail, the rebase stops at the problematic commit. Handy when it matters that every commit in history builds.

--update-refs: a stack of dependent branches

If you maintain a chain of branches that depend on each other, the --update-refs flag (Git 2.38+) moves the intermediate branches too when you rebase the top one.

How to undo a rebase

While it's in progress — git rebase --abort. If the rebase has already finished and you don't like the result, the old commits haven't gone anywhere: the reference log remembers them.

$ git reflog
e5f6a7b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e5f6a7b HEAD@{1}: rebase (pick): Email validation
...
c3d4e5f HEAD@{5}: commit: typo in validation

$ git reset --hard HEAD@{5}

Find the entry from before the rebase started in git reflog and reset the branch to it. Right after a rebase there's also a shortcut: git reset --hard ORIG_HEAD. The reflog is stored locally and keeps entries for about 90 days by default, so there's no need to panic after a botched rebase.

Common mistakes

  • Rebasing a shared branch and force pushing — teammates' history falls apart. The fix: coordinate, restore the branch from the reflog of whoever has the correct version, and block force pushes to protected branches on the server.
  • Using --force instead of --force-with-lease and overwriting a commit a teammate managed to push to the same branch.
  • Squash on the first line of the interactive plan — Git responds with the error "cannot 'squash' without a previous commit".
  • Starting a rebase with a dirty working tree. Git refuses to start. Commit your changes, stash them with git stash, or use git rebase --autostash.
  • Resolving a conflict and running git commit instead of git rebase --continue. You end up with an extra commit; after --continue you can remove it with another interactive rebase.

Wrapping up

git rebase moves your commits onto a new base and recreates them, so history ends up linear and clean. Interactive mode turns draft commits into a tidy history: squash and fixup combine fixes, reword corrects messages, drop removes what's not needed. There's one main rule — only rebase what you haven't published to others yet, and after rebasing your remote branch, push it with --force-with-lease. Rebasing and cleaning up commit history are covered hands-on in the Git course.

FAQ

Does git rebase change the code?

Without conflicts, the final file contents are the same as with a merge, but the commits themselves are recreated with new hashes and new parents. If you picked a different version during a conflict, the intermediate commits will differ from the originals.

How do I rebase all the way back to the first commit in the repository?

Use git rebase -i --root: the plan will include all commits, including the root one. You rarely need it, for example to rewrite the message of the very first commit.

Does rebase keep the commit author and date?

The author and author date are kept, while the committer and commit date change to the current ones. The --committer-date-is-author-date flag keeps the commit date equal to the author date.

Can I rebase merge commits?

By default, rebase flattens history and drops merge commits. To preserve the merge structure, use git rebase --rebase-merges.

How can I tell if a rebase is in progress?

git status shows "interactive rebase in progress" or "You are currently rebasing" and suggests the available commands. Many shells and IDEs also show it in the prompt.

Learn this topic on video

This topic is covered in depth with practice in the course “Git from Scratch to Confident Developer”.

Go to course