Posted in

Git Rebase – A Complete, Practical Guide

A hands-on reference for developers who want to actually understand rebase, not just copy commands.

1. The One-Sentence Definition

Rebasing means taking a series of commits and replaying them on top of a different base commit.

That’s it. The word “rebase” literally means give these commits a new base. Everything else in this guide is a detail of that single idea.

Where merge combines two histories by tying them together with a new merge commit, rebase moves your commits so they appear to have been written on top of the latest code – producing a clean, straight line of history.


2. Mental Model: Commits, Branches, and HEAD

Before rebase makes sense, three concepts must be crystal clear.

A commit is a snapshot of your files plus a pointer to its parent commit. Commits are immutable — once created, a commit’s ID (the SHA hash) never changes. This matters enormously: rebase does not move commits. It creates brand-new copies of them with new SHAs and throws the old ones away.

A branch is just a lightweight, movable pointer to one specific commit. When you add a commit, the branch pointer slides forward to the new commit. main, feature, develop — all just sticky notes pointing at a commit.

HEAD is a pointer to “where you are right now” — usually the branch you have checked out.

Throughout this guide, diagrams use this notation:

A---B---C        commits, older on the left, newer on the right
     \
      D---E      a branch that diverged after commit B

Each letter is a commit. The rightmost commit a branch points to is its tip.


3. Rebase vs. Merge — The Core Difference

You started a feature branch off main. While you worked, teammates pushed new commits to main. Now your branch is “behind.” You have two ways to integrate their work.

Starting point (both scenarios):

          A---B---C   feature  (your work)
         /
    D---E---F---G      main    (teammates' new work: F, G)

Your feature branched off main at commit E. Since then, main gained F and G.

Option A — Merge

git checkout feature
git merge main

Merge creates a new merge commit (M) that has two parents, tying the histories together:

          A---B---C---M   feature
         /           /
    D---E---F---G-----     main

Nothing is rewritten. A, B, C keep their original SHAs. The history honestly records “these two lines of development met here.” The downside: with many branches, the graph becomes a tangle of criss-crossing merge commits.

Option B — Rebase

git checkout feature
git rebase main

Rebase takes your commits (A, B, C), sets them aside, moves feature to the tip of main (G), and replays your commits one by one on top:

    D---E---F---G---A'---B'---C'   feature
                    \
                     main is at G

Notice A', B', C' — the prime marks are deliberate. These are new commits with the same changes but new SHAs and a new parent. The original A, B, C are now unreferenced and will eventually be garbage-collected.

The result is a linear history: it looks as though you started your feature after your teammates finished, even though you didn’t.

When to use which

SituationPrefer
Integrating latest main into your private feature branchRebase — clean, linear
Merging a finished feature into shared main (via PR)Merge (often a squash-merge)
The branch has already been pushed and others are using itMerge — never rebase shared history
You want an honest record of when branches actually divergedMerge
You want history that reads like a clear storyRebase

A very common professional workflow: rebase your feature branch onto main to keep it current and clean, then merge it into main via a pull request.


4. Your First Rebase (Step by Step)

Let’s do the classic “my feature is behind main” rebase concretely.

1. Confirm where you are.

git checkout feature
git log --oneline --graph --all

You see your feature commits and that main has moved ahead.

2. Run the rebase.

git rebase main

This says: “Replay the commits on my current branch (feature) on top of main.”

3. One of two things happens.

  • Clean case: Git replays every commit with no conflicts and prints Successfully rebased and updated refs/heads/feature. You’re done.
  • Conflict case: Git pauses on the first conflicting commit and asks you to resolve it (covered in Section 6).

4. Verify the result.

git log --oneline --graph

You should now see one straight line: main‘s commits followed by your feature commits.

5. If the branch was previously pushed, your local history now diverges from the remote (because commits were rewritten), so you must force-push:

git push --force-with-lease

Always prefer --force-with-lease over --force. It refuses to overwrite the remote if someone else has pushed in the meantime, protecting you from clobbering a teammate’s work.


5. What Rebase Actually Does Under the Hood

Understanding the internal algorithm removes almost all the mystery. When you run git rebase main from feature:

  1. Find the merge base — the last commit feature and main have in common (commit E in our earlier example).
  2. Collect your commits — every commit on feature after the merge base, in order (A, B, C).
  3. Move to the new base — check out the tip of main (G) in a detached state.
  4. Replay each commit — apply A as a patch → create A'; apply B → create B'; apply C → create C'. Each replay is essentially a cherry-pick.
  5. Move the branch pointer — point feature at the final new commit (C').

Because step 4 applies each change against new surrounding code, a commit that applied cleanly originally can now conflict — that’s normal and expected.

The key takeaways: rebase replays, it does not move; every replayed commit gets a new SHA; and the original commits are orphaned (recoverable via reflog, see Section 11).


6. Handling Conflicts During a Rebase

Conflicts during rebase feel scarier than merge conflicts because Git stops mid-sequence, but the loop is simple and always the same.

When Git hits a conflict, it stops and tells you:

CONFLICT (content): Merge conflict in src/app.js
error: could not apply b7f3a2c... Add validation to form

You are now paused on that one commit. Resolve it in three steps, then repeat:

Step 1 — Fix the conflicted files. Open them; Git has inserted markers:

<<<<<<< HEAD
const timeout = 5000;        // the version already on the new base
=======
const timeout = 3000;        // your commit's version
>>>>>>> b7f3a2c (Add validation to form)

Edit the file so it contains exactly what you want the final code to be, and delete all three marker lines (<<<<<<<, =======, >>>>>>>).

Step 2 — Stage the resolved files.

git add src/app.js

Step 3 — Continue the rebase.

git rebase --continue

Git applies the next commit in the sequence. If it conflicts too, repeat steps 1–3. When there are no more commits, the rebase finishes.

Your three escape hatches at any pause

git rebase --continue   # I've resolved this commit; move on
git rebase --skip       # Drop this commit entirely and move on
git rebase --abort      # Cancel everything; return to exactly where I started

--abort is your safety net. It restores the branch to its pre-rebase state with zero damage, no matter how many conflicts deep you are. If a rebase ever feels out of control, abort and rethink.

Tip: Enable rerere (“reuse recorded resolution”) once and Git will remember how you resolved a given conflict and auto-apply it next time the same conflict appears:

git config --global rerere.enabled true

This is a huge time-saver when rebasing long-lived branches repeatedly.


7. Interactive Rebase — Rewriting History Cleanly

This is where rebase becomes a superpower. Interactive rebase lets you rewrite a series of your own commits — squash them, reorder them, reword messages, split them, or delete them — before sharing your work.

Start it by telling Git how far back to go:

git rebase -i HEAD~4      # edit the last 4 commits

or rebase interactively relative to another branch:

git rebase -i main        # edit every commit on this branch since it left main

Git opens an editor listing the commits oldest first (the reverse of git log):

pick a1b2c3d Add login form
pick d4e5f6a Fix typo in login form
pick 7g8h9i0 Add password validation
pick j1k2l3m WIP debugging

You change the word at the start of each line to tell Git what to do, then save and close the editor.

The commands you can use

CommandShortWhat it does
pickpKeep the commit as-is
rewordrKeep the commit, but edit its message
editePause here so you can amend the commit’s contents
squashsMerge into the previous commit; combine both messages
fixupfMerge into the previous commit; discard this message
dropdDelete the commit entirely
reorderJust move the lines up/down to change commit order

Example: cleaning up before a pull request

Your four messy commits should become two clean ones. You edit the list to:

pick a1b2c3d Add login form
fixup d4e5f6a Fix typo in login form
pick 7g8h9i0 Add password validation
fixup j1k2l3m WIP debugging

Before:

a1b2c3d Add login form
d4e5f6a Fix typo in login form
7g8h9i0 Add password validation
j1k2l3m WIP debugging

After (the typo fix folds silently into the form commit; the WIP folds into validation):

X1X1X1X Add login form
Y2Y2Y2Y Add password validation

Two clean, self-contained commits – perfect for review.

squash vs. fixup – the one distinction people forget

Both combine a commit into the one above it. The difference is only about the message:

  • squash opens an editor so you can write a combined message from both commits.
  • fixup silently discards the squashed commit’s message and keeps only the earlier one’s.

Use fixup for “oops, small fix” commits whose messages are worthless. Use squash when both messages contain something worth keeping.

edit – splitting or amending a commit mid-history

Mark a commit edit and Git pauses after applying it. Now you can:

# Amend its content or message:
git commit --amend

# Or split it into two: undo the commit but keep changes staged,
# then re-commit in pieces:
git reset HEAD~
git add file1.js && git commit -m "First logical change"
git add file2.js && git commit -m "Second logical change"

# When finished, resume:
git rebase --continue

Autosquash — the pro move

While working, mark fix commits to target a specific earlier commit:

git commit --fixup=a1b2c3d      # this fix belongs to commit a1b2c3d

Then let Git arrange everything automatically:

git rebase -i --autosquash main

Git pre-orders the fixup lines directly beneath their targets, so you just save and close. This keeps a messy working process while still producing pristine final history.


8. git rebase --onto — Surgical Rebasing

--onto is the precision tool for when plain rebase isn’t specific enough. Its full form is:

git rebase --onto <new-base> <old-base> <branch>

Read it as: “Take the commits in <branch> that come after <old-base>, and replant them onto <new-base>.”

The classic use case: you branched off the wrong branch

You created feature off develop, but it should have come off main. develop has commits you do not want to drag along.

Before:

    D---E---F           develop
         \
          X---Y---Z     feature   (you want only X, Y, Z)
                        (you do NOT want E, F)
    A---B---C           main

You want to move only X, Y, Z onto main, dropping any dependence on develop:

git rebase --onto main develop feature
  • new-base = main (where you want the commits to land)
  • old-base = develop (the point after which commits should be taken)
  • branch = feature (which branch to move)

After:

    D---E---F           develop  (untouched)

    A---B---C---X'---Y'---Z'   feature, main is at C

Only your three commits moved; develop‘s commits were left behind entirely.

Second use case: dropping commits from the middle of a branch

Remove commit B while keeping C and D:

Before:  A---B---C---D   feature

git rebase --onto A B feature

After:   A---C'---D'      feature   (B is gone)

You’re saying “replay everything after B on top of A,” which skips B cleanly.


9. git pull --rebase — Keeping History Linear

A regular git pull is git fetch + git merge. When your local branch and the remote have both moved, that merge creates a needless merge commit like “Merge branch ‘main’ of origin into main” — clutter that adds no information.

git pull --rebase does git fetch + git rebase instead. It replays your local commits on top of the freshly fetched remote commits, avoiding the merge bubble:

git pull --rebase

Make it your default everywhere so you never think about it again:

git config --global pull.rebase true

Before pull:

              X---Y      origin/main (teammates pushed)
             /
    A---B---C---D---E     your local main (you committed D, E)

After git pull --rebase:

    A---B---C---X---Y---D'---E'     local main, in sync, no merge commit

Your local commits D, E cleanly follow the incoming X, Y.


10. The Golden Rule of Rebasing

Never rebase commits that exist outside your local repository and that other people may have based work on.

In plainer terms: do not rebase a shared/public branch. Do not rebase main or develop that your team pulls from. Do not rebase a feature branch that a colleague has already checked out and started committing on.

Why it’s dangerous: rebase rewrites commits into new ones with new SHAs. If a teammate already has the old commits, their Git and yours now disagree about history. When they next pull, Git sees two divergent versions of the “same” work, and reconciling it creates duplicated commits and painful conflicts. You’ve effectively pulled the rug out from under everyone downstream.

The safe boundary: rebase freely on commits that are only yours and only local — your private feature branch before anyone else touches it. Once work is shared and others build on it, integrate with merge instead.

The one accepted exception: rebasing your own feature branch and force-pushing it to update your own pull request, when you know no one else is committing to it. Coordinate with your team, and always use --force-with-lease.


11. Recovering When Things Go Wrong

Rebase feels risky mainly because people don’t know it’s almost always reversible. Two tools make you nearly bulletproof.

git rebase --abort (during a rebase)

If you’re mid-rebase and it’s going badly, this instantly restores your branch to its exact pre-rebase state:

git rebase --abort

Zero damage. Use it liberally.

git reflog (after a rebase already finished)

Even after a rebase completes, the original commits still physically exist — they’re just no longer pointed to by a branch. The reflog records every position HEAD has held:

git reflog
a1b2c3d HEAD@{0}: rebase finished: returning to refs/heads/feature
X1Y2Z3a HEAD@{1}: rebase: Add password validation
...
9f8e7d6 HEAD@{5}: checkout: moving to feature
c4d5e6f HEAD@{6}: commit: Add password validation   ← the pre-rebase tip

Find the entry from before the rebase (here HEAD@{6}) and reset your branch back to it:

git reset --hard HEAD@{6}
# or reset to the SHA directly:
git reset --hard c4d5e6f

Your branch is now exactly as it was before the rebase — the rewrite is undone.

Safety habit: before any big or risky rebase, drop a bookmark first:

git branch backup-before-rebase

If the rebase goes wrong, git reset --hard backup-before-rebase restores everything instantly, and you can delete the backup branch afterward.


12. Real-World Workflows

Workflow 1 — Keep a feature branch current with main

git checkout feature
git fetch origin
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

Do this regularly so your final PR merges cleanly and your branch never drifts far from main.

Workflow 2 — Clean up commits before opening a PR

git rebase -i main
# squash/fixup the noise, reword messages into clear statements
git push --force-with-lease

Reviewers see a tidy set of logical commits instead of “WIP”, “fix”, “fix again”.

Workflow 3 — Daily sync without merge clutter

git config --global pull.rebase true   # one-time setup
git pull                               # now rebases automatically

Workflow 4 — Move a branch you started off the wrong base

git rebase --onto main develop feature
git push --force-with-lease

13. Command Cheat Sheet

# --- Basic ---
git rebase main                       # replay current branch onto main
git rebase origin/main                # replay onto the remote's main

# --- During a rebase ---
git rebase --continue                 # proceed after resolving conflicts
git rebase --skip                     # drop the current commit and continue
git rebase --abort                    # cancel; restore pre-rebase state

# --- Interactive ---
git rebase -i HEAD~5                  # edit the last 5 commits
git rebase -i main                    # edit all commits since main
git rebase -i --autosquash main       # auto-arrange --fixup/--squash commits

# --- Surgical ---
git rebase --onto NEW OLD branch      # replant branch's post-OLD commits onto NEW

# --- Pull with rebase ---
git pull --rebase                     # fetch + rebase instead of fetch + merge
git config --global pull.rebase true  # make it the default

# --- Safety & recovery ---
git branch backup-before-rebase       # bookmark before a risky rebase
git reflog                            # find pre-rebase HEAD position
git reset --hard HEAD@{n}             # jump back to a reflog entry
git push --force-with-lease           # safely push a rewritten branch

# --- Helpful config ---
git config --global rerere.enabled true   # remember conflict resolutions

14. Common Pitfalls & FAQ

“My commit hashes changed after rebasing — did I lose work?” No. Rebase creates new commits with the same content but new SHAs and new parents. The changes are all there; only the identifiers changed. The old commits still exist in the reflog if you need them.

“Git says I need to force-push after rebasing. Is that safe?” On your own feature branch that no one else is using, yes — use git push --force-with-lease. On a shared branch, no. See the Golden Rule.

“I got the same conflict on several commits in a row — why?” Rebase replays commits one at a time, so a conflict in an area that multiple commits touch surfaces once per commit. Enable rerere so Git reuses your first resolution automatically.

“When would I ever choose merge over rebase?” When the branch is shared, when you want an honest historical record of when work diverged, or when integrating a finished feature into main (a merge or squash-merge commit is the natural record of “this feature landed”). Rebase is for cleaning up and syncing your own in-progress work; merge is for combining shared, finished work.

“Can I rebase without moving to a new base — just to reorder or squash?” Yes. git rebase -i replays commits onto the same base but lets you rewrite them along the way. That’s the pure “rewrite my own history” use case.

“I’m terrified of losing work. What’s the safest habit?” Two things: run git branch backup-before-rebase before starting, and remember git rebase --abort cancels an in-progress rebase completely. Between those two, rebase is nearly impossible to permanently mess up.


The Bottom Line

Rebase does exactly one thing — replays commits onto a new base — and everything else (interactive editing, --onto, pull --rebase) is a variation on that theme. Use it to keep your private branches clean and linear, obey the Golden Rule about shared history, keep --abort and reflog in your back pocket, and rebase stops being scary and becomes one of the most useful tools in Git.

Must Read

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading