All posts
·14 min read· rebase· interactive-rebase· commits· history· review

Git rebase to reorder commits: a step-by-step guide

Five commits ready to push, but in the wrong order for review? Here is exactly how git rebase -i reorders commits, what happens when reordering hits a conflict, and when you should and should not reorder.

You have five commits ready to push. Looking at the list, the order makes sense to you — you built things in the order they came to mind. But for a reviewer, the order is awful. The schema migration is third. The endpoint that uses the new schema is first. A reviewer reading top-to-bottom hits the endpoint commit and immediately asks "where is this column defined?"

You want to reorder before opening the PR. The migration should come first, the endpoint that uses it should come second, the tests for that endpoint third, and the small follow-ups last. A reviewer can then read in order and each commit makes sense on its own.

This is what git rebase -i is for. Reordering takes about thirty seconds once you know the keystrokes. This post walks through the exact steps, what to do when a reorder causes a conflict, and when reordering is a bad idea.

What git rebase -i HEAD~N actually does

A commit in Git is a small object that records a snapshot, a parent, an author, a committer, and a message. A branch is a ref pointing at the latest commit in a chain.

When you run git rebase -i HEAD~5, Git does this:

  1. Reads the last 5 commits on your branch.
  2. Opens an editor with a TODO list — one line per commit, in chronological order (oldest first).
  3. Waits for you to edit the TODO. You can change the order, drop lines, or change the command from pick to reword, edit, squash, fixup, or drop.
  4. When you save and close, Git rewinds the branch to the base (HEAD~5) and replays the commits in the new order you specified.

Each replayed commit gets a new SHA because its parent, timestamp, or contents may differ. The original commits are still in .git/objects/ (they live in the reflog), but the branch now points at the new chain.

The key mental model: rebase does not "move" commits. It creates new commits with the same patches applied in a new order, then moves the branch to the tip of the new chain.

The TODO file format

When the editor opens, you will see something like:

pick 3d8f2b1 docs: update README
pick 6c1a5e3 feat: payment scaffold
pick 9f4e2c8 feat: payment webhook handler
pick b4c8e2f feat: add retry queue
pick a1b2c3d test: webhook retry e2e

Read top-to-bottom — oldest commit first, newest last. (This is the opposite of git log, which shows newest first. The reason: rebase is going to replay top-to-bottom, and replay order is "oldest first".)

The commands you can use, summarised:

CommandShortWhat it does
pickpUse the commit as-is.
rewordrUse the commit, but stop to edit the message.
editeUse the commit, but stop after applying so you can amend it.
squashsMerge into the previous commit, combining both messages.
fixupfLike squash but discard this commit's message.
dropdRemove the commit entirely.

For reordering, you only need pick. The trick is in the line order — reorder the lines, and Git replays in the new order.

The full reference is in the git rebase docs.

A step-by-step walkthrough

Suppose git log --oneline -5 shows (newest first):

a1b2c3d test: webhook retry e2e
b4c8e2f feat: add retry queue
9f4e2c8 feat: payment webhook handler
6c1a5e3 feat: payment scaffold
3d8f2b1 docs: update README

The docs: update README commit is mixed in with feature work. You want it last, so the PR shows feature commits in build order, then the doc tweak at the end.

Step 1 — Start the rebase

git rebase -i HEAD~5

The editor opens with:

pick 3d8f2b1 docs: update README
pick 6c1a5e3 feat: payment scaffold
pick 9f4e2c8 feat: payment webhook handler
pick b4c8e2f feat: add retry queue
pick a1b2c3d test: webhook retry e2e

Step 2 — Reorder the lines

Move the docs: line to the bottom:

pick 6c1a5e3 feat: payment scaffold
pick 9f4e2c8 feat: payment webhook handler
pick b4c8e2f feat: add retry queue
pick a1b2c3d test: webhook retry e2e
pick 3d8f2b1 docs: update README

In Vim, that is dd on the line, then G to go to the bottom, then p to paste. In VS Code, it is cut-and-paste. The mechanics do not matter — what matters is that when you save, the line order is what you want.

Step 3 — Save and close

Git rewinds your branch to HEAD~5 (the commit before all five), then replays each commit in the new order. The terminal prints something like:

Successfully rebased and updated refs/heads/feature/payments.

Step 4 — Verify

git log --oneline -5

You will see (newest first):

e7f8a9b docs: update README
c5d6e7f test: webhook retry e2e
b3c4d5e feat: add retry queue
a1b2c3d feat: payment webhook handler
9f4e2c8 feat: payment scaffold

The order is what you wanted. Note the SHAs have changed for every commit whose position relative to its parent moved. The old commits still exist in .git/objects/ — you can see them in git reflog — but the branch points at the new chain.

When reordering causes a conflict

Reordering only works cleanly when the commits are independent. Two commits that touch the same lines of the same file will conflict when you try to apply them in a different order.

Example: commit A adds field_a to a SQL schema. Commit B adds field_b right after field_a. The patch for commit B references field_a as context. If you reorder so B applies first, the context is missing — there is no field_a yet — and Git pauses with a merge conflict.

The output looks like:

Applying: feat: add field_b
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
CONFLICT (content): Merge conflict in schema.sql
error: could not apply b4c8e2f... feat: add field_b
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".

You have three options:

Resolve the conflict. Edit schema.sql to add field_b directly (without field_a as context). Then:

git add schema.sql
git rebase --continue

The next commit (A, adding field_a) will also need a small resolution if its patch context is now different. Git will pause again. Resolve, --add, --continue. Repeat until the rebase finishes.

Abort the rebase. If the conflict is messier than expected:

git rebase --abort

Your branch returns to exactly where it was before the rebase. The original commits are intact. You can try again later or decide the reorder is not worth it.

Step back from the reorder. Sometimes the conflict is a signal that the commits should not be reordered — the second commit really does depend on the first. Abort, leave the order as it was, and write a clear PR description explaining the order instead.

The general rule: conflicts during reorder mean the commits depend on each other. Sometimes you can resolve those dependencies by hand; sometimes you should accept the order Git is telling you the commits naturally have.

--autosquash for fixup commits

A related workflow: you are mid-feature and notice a bug in commit number 3 of your 5-commit branch. Instead of editing the commit message manually, you make a "fixup" commit:

git commit --fixup=9f4e2c8

This creates a new commit with the message fixup! feat: payment webhook handler. When you later run:

git rebase -i --autosquash HEAD~6

The TODO file opens with the fixup commit pre-positioned right after the commit it fixes, and pre-marked as fixup:

pick 6c1a5e3 feat: payment scaffold
pick 9f4e2c8 feat: payment webhook handler
fixup d2e3f4a fixup! feat: payment webhook handler
pick b4c8e2f feat: add retry queue
pick a1b2c3d test: webhook retry e2e
pick 3d8f2b1 docs: update README

Save and close, and the fixup is squashed into the target commit without you having to find it manually.

This is the cleanest way to fix something mid-feature without leaving messy "fix typo from earlier" commits in your final history. The full pattern is covered in the Squashing & Autosquash lesson in the Advanced Power track (linked in the sidebar).

You can also set autosquash as a default in ~/.gitconfig:

[rebase]
  autoSquash = true

Then plain git rebase -i HEAD~6 will autosquash automatically.

When NOT to reorder

Three cases where reordering is the wrong move:

The commits have been pushed and reviewed. Reordering changes SHAs. Reviewers' approvals were tied to specific SHAs. Most PR tools mark the approval as stale when the SHA changes — reviewers have to re-approve. On a small PR with one reviewer, that is fine. On a hot PR with five reviewers and CI runs that take 20 minutes, it is a real cost.

The branch is shared. If a teammate has pulled your branch and made commits on top, your reorder forces them to either rebase their work onto your new history or end up with a divergent local copy. Either way, it is friction you did not need to add.

The base branch is public. Never run git rebase -i on main, develop, or any other long-lived branch with other people committing to it. The standard rule:

Do not rewrite history that other people have pulled.

This is documented in the Pro Git book on rebasing and is the single most important guard rail around rebase -i.

For everything else — local feature branches, branches you have not yet pushed, branches only you are working on — reordering is cheap and useful. Use it freely.

The reorder–fixup workflow in practice

The way most experienced Git users work is a loose pattern:

  1. Start a feature branch.
  2. Commit early and often. Small commits are fine. Order does not matter yet.
  3. When you find a problem in an earlier commit, git commit --fixup=<sha> to mark a fix.
  4. Before pushing, run git rebase -i --autosquash <base> to:
    • Squash all fixups into their targets.
    • Reorder commits into the sequence that makes sense for review.
    • Optionally reword any messages that are unclear.

This produces a PR with 3–7 well-ordered, well-named commits, each of which compiles, tests pass, and tells a clear story. Reviewers like it. You like it. Bisect later likes it.

The Perfect Commit lesson (in the sidebar) covers this rhythm in depth — the workflow itself is more valuable than any single rebase command.

A quick comparison — merge vs rebase

Reordering uses rebase. It is worth a quick reminder that rebase and merge are different tools for different jobs.

QuestionUse rebaseUse merge
Cleaning up your own branch before PRYesNo
Integrating someone else's changesSometimes (see below)Yes, the default
Producing a linear history for reviewYesNo
Preserving the exact moment of mergeNoYes
Working on a shared branchNoYes

Rebase is for history shaping. Merge is for history joining. Reordering is purely history shaping, and it stays local until you push.

For the broader question of when to rebase and when to merge, see Choosing a Git workflow.

Three common myths

Myth 1: "Rebase is dangerous." Wrong, with one caveat. Rebase is dangerous on shared branches because it rewrites history that other people may have pulled. On your own local feature branch — where nobody else has fetched the SHAs — rebase is cheap and reversible. The git reflog keeps your old commits for ~30 days, so a botched rebase is undoable.

Myth 2: "Reordering commits is the same as squashing them." Wrong. Reorder changes the sequence; squash merges commits together. You can do both in the same rebase TODO file (squash some, reorder the rest), but they are independent operations.

Myth 3: "Rebase loses your work if it fails halfway." Wrong. A rebase that hits a conflict pauses; it does not destroy. You can run git rebase --abort to return to exactly the state before the rebase started. Even after a successful rebase, the original commits are reachable via git reflog for at least 30 days. The only way to lose work in a rebase is to manually git reset --hard on top of it without checking — and even then, the reflog has your back.

A safety habit — tag before you reorder

Even though the reflog will save you from a botched rebase, a small habit makes recovery one keystroke instead of three:

git tag pre-reorder
git rebase -i HEAD~5
# ... reorder, save, close ...
# if anything looks wrong:
git reset --hard pre-reorder
git tag -d pre-reorder

The tag is just a ref pointing at the pre-rebase tip. Recovery is git reset --hard <tag>. Once you confirm the new order is what you wanted, delete the tag.

Some people alias this:

[alias]
  save = !sh -c 'git tag save/$(date +%s)' -
  saves = tag --list 'save/*'

git save drops a timestamped tag at the current commit. git saves lists them. Cheap insurance for anything destructive.

What about preserving commit dates?

When you reorder commits, Git replays each one with a new committer date (the time of the replay). The original author date is preserved. So:

Most tools display the author date by default, so PR diffs and git log look unchanged. But git log --pretty=fuller shows both, and you may notice the committer date is "now" for every reordered commit.

If you need to preserve committer dates as well — for example, you care about the exact chronology in git log --date-order — use --committer-date-is-author-date:

git rebase -i --committer-date-is-author-date HEAD~5

This is rarely needed in practice, but worth knowing.

Try it in a live terminal

Open the reorder commits scenario for a seeded branch with five commits in the wrong order. Run the interactive rebase, drag the lines, see the new SHAs, verify the order — all in a real terminal underneath, with no real PR at risk. The first deliberate reorder is what makes the next "ugh, the order is wrong" thought a thirty-second fix instead of a five-minute wrestle.

What to read next

If you remember one thing from this post: reordering commits is a TODO file with lines in a different order. Save, close, done. The hard part is deciding whether the order matters enough to do it — and once you have built the habit, the answer is usually yes, because reviewers (and future-you reading git log) will thank you.