The Rebasing Process
What Will We Learn?
By the end of this chapter, you’ll know:
- What actually happens under the hood when you run
git rebase - A full step-by-step walkthrough you can follow along with on your own repo
- How to handle conflicts when Git can’t replay a commit cleanly
- How to write good commit messages for the commits that come out of a rebase
We’re sticking to plain git rebase here — interactive rebase (git rebase -i, squashing, rewording, reordering) gets its own dedicated chapter next, since it deserves the space.
Rebasing Process: General Overview
When you run git rebase main while sitting on feature, Git doesn’t magically “move” your branch. It does something closer to this, one commit at a time:
- Find the common ancestor of
featureandmain(the last commit they both share). - Take every commit on
featuresince that ancestor and set it aside as a patch — basically a diff plus the original commit message and author. - Reset
featureto point at the tip ofmain. - Replay each saved patch on top, one by one, creating a brand-new commit each time.
flowchart TB
subgraph before["Before Rebase"]
direction RL
B1((B)) --> A1((A))
E1((E)) ----> B1
C1((C)) --> B1
D1((D)) --> C1
main1["main"] -.-> E1
feat1["feature"] -.-> D1
end
subgraph after["After Rebase — feature onto main"]
direction RL
main2["main"] -.-> E2
subgraph feat
B2((B)) --> A2((A))
E2((E)) --> B2
C2((C')) --> E2
D2((D')) --> C2
feat2["feature"] -.-> D2
end
end
before -.->|"git rebase main"| after
style main1 fill:#888,stroke:#888,color:#fff
style feat1 fill:#c2255c,stroke:#c2255c,color:#fff
style main2 fill:#888,stroke:#888,color:#fff
style feat2 fill:#c2255c,stroke:#c2255c,color:#fff
If a patch applies cleanly, Git moves on to the next one automatically. If it doesn’t — because main changed something in the same lines your commit touches — Git pauses and hands control back to you. We’ll walk through exactly what that looks like in the conflicts section below.
One thing worth internalizing now: Git replays commits in order, one at a time, not all at once as a single combined diff. That’s why you can hit a conflict on commit C, resolve it, and then hit another conflict on commit D right after — Git is working through your commits individually, not merging your whole branch in one shot.
Step By Step Practical Walkthrough
Let’s use the same main / feature setup from the previous chapter: main has commits A → B → E, and feature branched off at B with commits C → D.
1. Make sure your working directory is clean.
git statusRebase will refuse to start (or get confusing fast) if you’ve got uncommitted changes lying around. Commit or stash them first:
git stash2. Update your local main.
git checkout main
git pullThis is what pulls in commit E — the new work on main you want your branch rebased on top of.
3. Switch to your feature branch and start the rebase.
git checkout feature
git rebase mainIf everything applies cleanly, you’ll see something like:
Successfully rebased and updated refs/heads/feature.At this point feature now has C' and D' sitting on top of E, exactly like the diagram above.
4. Restore your stashed changes, if you stashed any.
git stash pop5. Push your rebased branch.
Since the commit hashes changed, a normal push will be rejected — Git sees your local and remote histories as having “diverged,” even though you know better:
git push --force-with-lease--force-with-lease is the safer sibling of --force: it double-checks that nobody else pushed to the branch since you last fetched, and refuses to overwrite their work if they did. Plain --force doesn’t check anything — it just steamrolls whatever is on the remote.
How to Deal With Conflicts?
Sooner or later, a replayed commit will touch a line that main already changed, and Git will stop mid-rebase:
CONFLICT (content): Merge conflict in app.js
error: could not apply 1a2b3c4... Fix login validation
Resolve all conflicts manually, mark them as resolved with
"git add <file>", then run "git rebase --continue".Here’s the loop you’ll repeat for each conflicting commit:
flowchart TD
A["Git pauses on a conflicting commit"] --> B["Open the file, find conflict markers"]
B --> C["Edit the code to the correct final version"]
C --> D["git add the resolved file(s)"]
D --> E["git rebase --continue"]
E -->|"more conflicts"| A
E -->|"clean"| F["Rebase finishes"]
A few practical notes for that loop:
Conflict markers look like this in the affected file:
<<<<<<< HEAD const maxRetries = 5; ======= const maxRetries = 3; >>>>>>> 1a2b3c4 (Fix login validation)Everything between
<<<<<<< HEADand=======is what’s already in the code you’re rebasing onto (main, in this case). Everything between=======and>>>>>>>is what your commit was trying to introduce. Edit the block down to what the code should actually be, then delete the marker lines.git statusis your friend mid-rebase. It tells you exactly which files still have unresolved conflicts.You’re not limited to
--continue. Two other useful escape hatches:git rebase --skip— drop the current commit entirely and move to the next one (useful if the commit’s change is no longer relevant, e.g. it’s already been superseded by something onmain).git rebase --abort— bail out completely and return to exactly where you were before the rebase started. There’s no shame in aborting if things get too tangled; you can always try again with a clearer head.
Test after every resolved conflict, not just at the end. Resolving a conflict is you making a judgment call about what the “correct” code is — it’s easy to resolve it in a way that compiles but is logically wrong.
Commit Message For Rebase
Rebase replays your original commits, so by default it keeps your original commit messages untouched — you don’t need to write anything new just because the commit got a new hash.
That said, a couple of situations are worth knowing about:
- If a rebase would produce an empty commit (because the change is already present on
main), Git will typically skip it automatically and tell you so. No message needed — there’s nothing left to commit. - If you resolve a conflict and want to note why you resolved it a certain way, you can amend the message during
--continueby adding-e, or just fix it up afterward. A short note likeFix login validation (resolved rebase conflict with retry logic in main)is more useful to future-you than silence. - Resist the urge to rewrite history “while you’re in there.” Squashing, rewording, or reordering commits is entirely doable — but that’s a job for interactive rebase, which we’re covering next, with its own set of conventions for writing clean commit messages when you’re deliberately collapsing several commits into one.
For now: trust that your original messages carry over, and only touch them when a conflict resolution genuinely changes what a commit is doing.