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
| Situation | Prefer |
|---|---|
Integrating latest main into your private feature branch | Rebase — 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 it | Merge — never rebase shared history |
| You want an honest record of when branches actually diverged | Merge |
| You want history that reads like a clear story | Rebase |
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-leaseover--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:
- Find the merge base — the last commit
featureandmainhave in common (commitEin our earlier example). - Collect your commits — every commit on
featureafter the merge base, in order (A,B,C). - Move to the new base — check out the tip of
main(G) in a detached state. - Replay each commit — apply
Aas a patch → createA'; applyB→ createB'; applyC→ createC'. Each replay is essentially acherry-pick. - Move the branch pointer — point
featureat 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 trueThis 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
| Command | Short | What it does |
|---|---|---|
pick | p | Keep the commit as-is |
reword | r | Keep the commit, but edit its message |
edit | e | Pause here so you can amend the commit’s contents |
squash | s | Merge into the previous commit; combine both messages |
fixup | f | Merge into the previous commit; discard this message |
drop | d | Delete the commit entirely |
reorder | — | Just 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:
squashopens an editor so you can write a combined message from both commits.fixupsilently 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-rebaseIf the rebase goes wrong,
git reset --hard backup-before-rebaserestores 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.
