Skip to content
C Codeloom
Git

Git Rebase Explained: When to Rebase and When to Merge

A deeper look at git rebase — the mechanics, interactive rebase with squash/fixup/edit/drop, the golden rule of published branches, conflict resolution, and rerere for repeated conflicts.

·10 min read · By Yash Kesharwani
Intermediate 12 min read

What you'll learn

  • The mechanics of how rebase replays commits, with hashes that change
  • Interactive rebase actions: pick, reword, edit, squash, fixup, drop
  • The golden rule — never rebase commits that have been shared
  • How to resolve conflicts during a rebase and continue or abort
  • Using rerere to skip repeated conflict resolutions

Prerequisites

  • You have used both merge and rebase at least once — see Merge vs Rebase
  • You are comfortable resolving merge conflicts

Merge keeps history honest; rebase keeps history readable. Once you have used both, the questions stop being “what does each one do” and start being “when do I use which, and how do I get out of it when something goes wrong.” This post is the practical answer.

Mechanics: what rebase actually does

Suppose you have this:

            E---F  (feature)
           /
A---B---C---D  (main)

Running git rebase main from feature does the following, step by step:

  1. Git identifies the commits unique to feature — here, E and F.
  2. Git temporarily stores them and resets feature to point at main’s tip (D).
  3. Git replays E onto D as a brand-new commit E' with a new hash but the same change.
  4. Git replays F onto E' as F'.
  5. If a step conflicts, Git pauses and asks you to resolve.

The result:

                    E'---F'  (feature)
                   /
A---B---C---D  (main)

The new commits E' and F' have different hashes from E and F. The content of the changes is the same; the parent commit is different, so the SHA is different. This is the entire reason for the golden rule below.

Merge vs rebase mechanics, side by side

A quick comparison:

OperationCreates new commits?Changes hashes?Shape of history
merge (fast-forward)NoNoLinear
merge (true merge)Yes — one merge commitNo (for existing commits)Diamond
rebaseYes — replays allYes (new hashes)Linear

The merge commit in a true merge has two parents; rebased commits have one. That single fact drives most of the trade-offs.

Interactive rebase

The everyday usefulness of rebase is not “put my branch on top of main” — most teams handle that with a pull or a merge. The killer feature is interactive rebase, which lets you rewrite a sequence of commits before sharing them.

git rebase -i HEAD~5

Git opens an editor showing the last five commits, oldest at the top:

pick a1b2c3d Add login form
pick e4f5g6h fix typo
pick i7j8k9l fix typo again
pick m1n2o3p Wire up auth context
pick q4r5s6t WIP — remember to remove console.log

You change the action on each line and save. Git replays the list using your instructions.

The actions

  • pick — keep the commit as-is. The default.
  • reword (or r) — keep the commit but open the editor to change its message.
  • edit (or e) — pause at this commit so you can amend it (change files, split it, etc.).
  • squash (or s) — combine this commit into the previous one. Git opens an editor to compose a combined message.
  • fixup (or f) — like squash, but discard this commit’s message and keep the previous one’s. The right choice for “fix typo” follow-ups.
  • drop (or d) — delete the commit entirely. Same effect as deleting the line.

So the example above becomes:

pick a1b2c3d Add login form
fixup e4f5g6h fix typo
fixup i7j8k9l fix typo again
pick m1n2o3p Wire up auth context
drop q4r5s6t WIP — remember to remove console.log

Three commits become two, the typos disappear into the original commit, and the WIP commit is gone. The branch now reads like a story you wrote on purpose.

Reordering

You can also reorder the lines in the editor and Git will replay them in the new order. Useful when two commits are conceptually backwards. Be aware that reordering can introduce conflicts if the commits touch related code.

Try it yourself. In a practice repo, make four commits on a feature branch: “Add header”, “fix typo in header”, “Add footer”, “fix typo in footer”. Run git rebase -i HEAD~4, change the typo commits to fixup, and watch the history collapse to two clean commits.

The golden rule

The single rule you must internalise:

Never rebase commits that you have already shared.

When you rebase, the commit hashes change. If a teammate already pulled your old commits and started building on them, their history now diverges from yours. The next pull/push produces duplicates and ugly merges that take time and trust to untangle.

Practical applications:

  • Rebasing your local, unpushed commits to tidy them up — safe.
  • Rebasing your private feature branch onto the latest main — safe.
  • Rebasing a shared main or any branch other people are committing to — never.
  • Rebasing a branch you have already pushed but only you work on — technically safe, but you will have to force-push, which is its own can of worms (see below).

If you must update a shared branch, merge instead — it is non-destructive.

Force-pushing safely

If you rebase a branch you have already pushed, the remote will reject a normal git push because your history no longer matches. You have to force-push. The safe form is:

git push --force-with-lease

--force-with-lease only overwrites the remote if it is at the commit you last fetched — that is, if no one else pushed to it in the meantime. If someone did, the push fails and you can investigate. Plain git push --force skips this check and will silently clobber your teammate’s work.

Habit: use --force-with-lease always. Treat --force as a hostile command.

Resolving conflicts during a rebase

When rebase hits a conflict, it stops mid-replay:

CONFLICT (content): Merge conflict in src/auth.js
error: could not apply <hash>... Wire up auth context
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

The workflow is the same shape as a merge conflict, but you may resolve the same area multiple times if successive commits touch it. That is normal — each replayed commit is applied independently.

The three options at any pause:

git rebase --continue    # after fixing the conflict and `git add`-ing files
git rebase --skip        # drop the current commit and move to the next
git rebase --abort       # cancel the whole rebase and return to the starting state

--abort is your safety net. If a rebase is going sideways, abort, take a breath, and consider whether merge is the right call instead.

A practical conflict tip

When the same lines conflict in commit after commit, it usually means an unrelated reformatting (whitespace, lint fix) landed on main and your branch is replaying old versions of the same lines. You have two choices:

  1. Squash your branch first (interactive rebase your branch onto itself) so only one commit needs to be rebased onto main. One conflict resolution instead of five.
  2. Use git rerere — the next section.

rerere: remembering conflict resolutions

git rerere (“reuse recorded resolution”) is one of Git’s best-kept secrets. Once enabled, Git remembers how you resolved a given conflict and replays the same resolution if the exact conflict appears again.

Enable it once per repo (or globally):

git config --global rerere.enabled true

That is it. Now when you resolve a conflict during a rebase, Git records the resolution. Next time you rebase the same branch (or a similar branch), Git applies the recorded resolution automatically and tells you so:

Resolved 'src/auth.js' using previous resolution.

You still review the result and git add the file before continuing — rerere does not blindly commit. But the tedious mechanical work of repeating the same resolution is gone. This is especially valuable on long-running feature branches that are rebased onto main repeatedly.

Try it yourself. In a practice repo, enable rerere, then create a conflict between main and a feature branch by editing the same line on both. Rebase, resolve, and complete. Reset both branches, recreate the same conflict, rebase again — Git resolves it for you and prints the “previous resolution” message.

A small worked example

Walk through a realistic flow. You started a payments branch a week ago. Five commits in, you want to update onto main and tidy up before opening a pull request.

# 1. Get the latest main
git fetch origin

# 2. Update your branch on top of it
git switch payments
git rebase origin/main

# (Resolve any conflicts, --continue / --abort as needed.)

# 3. Tidy up: squash WIP commits, drop debug commits
git rebase -i origin/main

# 4. Push the rewritten branch (force-with-lease because hashes changed)
git push --force-with-lease

By the time the pull request is open, your branch is a small number of well-described commits sitting cleanly on top of the latest main. The reviewer sees only your real changes, not a tangle of integration noise.

When to merge instead

Reach for merge — and not rebase — when:

  • The branch has been pulled by collaborators and rewriting history would hurt them.
  • The history of “this branch existed and was integrated on date X” is worth recording.
  • Your team has a merge-only policy (some projects use squash-merge in the host UI and keep main linear that way).
  • The conflict resolution would be easier as a single merge commit than as a chain of replayed conflicts.

A common professional pattern: rebase locally to keep your branch clean while you work, then squash-merge into main via the GitHub/GitLab UI. You get the linear history without exposing collaborators to hash changes.

A short decision guide

SituationUse
Tidying up your own unpushed commitsrebase -i
Catching up your private feature branch with mainrebase
Integrating a finished feature into main (team has no policy)either; follow convention
Working on a branch with collaboratorsmerge
Already shared the commits — and now you want to “clean them up”leave them; merge new fixes
In doubtmerge

The two questions that decide it: has this commit been shared with anyone yet, and would changing its hash inconvenience them? If both are no, rebase is fair game.

Recap

You now know:

  • Rebase replays commits as new commits with new hashes; merge preserves them under a new merge commit
  • Interactive rebase actions: pick, reword, edit, squash, fixup, drop — plus reordering
  • The golden rule: never rebase commits that have been shared
  • Force-push with --force-with-lease, never plain --force
  • During a rebase you can --continue, --skip, or --abort at any pause
  • git rerere remembers conflict resolutions and reapplies them automatically

Next steps

Cleaning up history is a small subset of the larger discipline of working well with branches and pull requests. The next post in the workflow series puts these tools into a daily routine.

→ Next: A Daily Git Workflow That Scales From Solo to Team

Questions or feedback? Email codeloomdevv@gmail.com.