Free lesson · Rewriting History and Recovering From Mistakes

Rebase: replaying your work

Rebase has a reputation for danger. That reputation comes from one specific misuse — rewriting shared history — and not from the operation itself, which is one of the most useful things Git can do.

What it does

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

git switch feature; git rebase main

After:
  A---B---C---F---D'---E'   ← feature
              ↑
             main

Git finds the commits unique to feature (D and E), sets the branch aside, moves to main's tip, then replays each one as a new commit. D′ and E′ contain the same changes but have different hashes — different parents, so different content, so different hash.

The result is a linear history that looks as though you started your work from the current main all along.

Rebase versus merge for keeping up to date

 git merge maingit rebase main
History shapeMerge commits appear on your branchLinear
HashesUnchangedAll rewritten
Safe when sharedYesNo
ConflictsOnce, all at the endPotentially once per commit
Records what really happenedYesNo — it presents a tidied story
The golden rule of rebasing

Never rebase commits that others have based work on. Your unpushed local branch: rebase freely. Your own pushed feature branch that nobody else uses: rebase and force-with-lease, it is fine and normal. A shared branch, or main: never. Rewriting hashes under someone else forces them to reconcile a diverged history, and they will usually get it wrong and duplicate commits.

Conflicts during a rebase

Because rebase replays commits one at a time, a conflict can occur at each one:

git rebase main
# CONFLICT in src/auth.js

# fix the file, then
git add src/auth.js
git rebase --continue

# or skip this commit entirely
git rebase --skip

# or give up and restore the original branch
git rebase --abort
"ours" and "theirs" are reversed here

During a rebase Git is replaying your commits onto the upstream, so it considers the upstream "ours" and your work "theirs" — the opposite of a merge. Rely on the actual content rather than the labels, and turn on merge.conflictstyle = zdiff3 so you can see the common ancestor too.

Interactive rebase

This is where rebase becomes a genuine craft tool:

git rebase -i main            # everything since main
git rebase -i HEAD~5          # the last 5 commits
git rebase -i --root          # the entire history

Git opens an editor with a script:

pick a3f9c2e Add login form
pick b4e8d1f wip
pick c5f9e2a fix typo
pick d6a0f3b Add validation
pick e7b1a4c oops forgot file

# p, pick   = keep as is
# r, reword = keep the change, edit the message
# e, edit   = stop here so you can amend or split it
# s, squash = merge into the previous commit, combine messages
# f, fixup  = merge into the previous commit, discard this message
# d, drop   = remove the commit entirely
# Reorder lines to reorder commits.

Rewriting it as:

pick a3f9c2e Add login form
fixup b4e8d1f wip
fixup c5f9e2a fix typo
pick d6a0f3b Add validation
fixup e7b1a4c oops forgot file

…produces two clean commits from five messy ones.

The autosquash workflow

The tidiest way to handle review corrections. When fixing something that belongs to an earlier commit:

git commit --fixup=a3f9c2e
# creates a commit titled "fixup! Add login form"

# later, fold them all in automatically
git rebase -i --autosquash main

Git positions each fixup under its target and pre-marks it. Set git config --global rebase.autosquash true and it happens on every interactive rebase.

Splitting a commit

git rebase -i HEAD~3
# mark the commit 'edit'

git reset HEAD~                  # uncommit it, keep the changes
git add -p                       # stage the first logical piece
git commit -m "First half"
git add .
git commit -m "Second half"
git rebase --continue

Force-pushing after a rebase

Your local branch and the remote have now genuinely diverged, so a normal push is rejected. That rejection is Git protecting you, not an obstacle to route around:

git push --force-with-lease
Always --force-with-lease

--force says "make the remote look like me, whatever is there". If a colleague pushed while you were rebasing, their commits are gone — silently, with no error. --force-with-lease first checks the remote is still where you last saw it and refuses otherwise. There is no situation where plain --force is meaningfully better; make the safe one a habit, or alias it.

This is one lesson of 52

Git & GitHub Actions: Version Control and CI/CD in Depth continues from here — 5 lessons are free to read like this one, and the rest come with the course. Enrolled readers also get an AI tutor that has read the lesson they are on.

See the full course