Learning Objectives
By the end of this lesson you will be able to:
- Perform a complete rebase of your own branch onto an updated
main - Resolve a conflict during a rebase and continue
- Explain why the branch must be force-pushed afterwards
- Use
--force-with-leaseand say what it protects you from
Before You Start
Two habits, every single time.
git status
The working tree must be clean. A rebase moves you across several commits; uncommitted changes either block it or come along for the ride confusingly. Commit them or stash them first.
git fetch origin
git log --oneline main..HEAD
The second command lists exactly the commits that are about to be replayed. Read it. If it contains commits you did not write, you are about to rewrite someone else's work — stop and read the next lesson.
The Rebase
git switch test/PAY-4417
git rebase origin/main
Successfully rebased and updated refs/heads/test/PAY-4417.
That is the happy path, and most of the time it is what you get. Your three commits now sit on top of the current main, with new IDs.
git log --oneline --graph -6
* 4c1f8ab (HEAD -> test/PAY-4417) test: add PAY-4417 boundary cases
* 9d3e072 test: extract payment fixture builder
* 1a77c60 test: failing case for zero-amount refund
* e2b90d1 (origin/main, main) feat: upgrade payment SDK to 4.2
One straight line, and your work is at the top.
When a Conflict Interrupts It
A rebase applies commits one at a time, so a conflict stops it partway:
Auto-merging tests/payments/refund.spec.ts
CONFLICT (content): Merge conflict in tests/payments/refund.spec.ts
error: could not apply 9d3e072... test: extract payment fixture builder
Note what it says: could not apply this one commit. The first commit already landed. You are standing at a partly rebuilt branch.
The loop is always the same three steps:
# 1. Fix the conflict markers in the listed file, exactly as in a merge conflict
# 2. Stage the resolved file
git add tests/payments/refund.spec.ts
# 3. Carry on
git rebase --continue
Then Git moves to the next commit and either finishes or stops again. A ten-commit rebase can stop several times; the loop does not change.
Two important differences from a merge conflict:
- You do not commit.
git rebase --continuewrites the commit for you, reusing the original message. - The "ours"/"theirs" labels are reversed from what you expect. During a rebase, ours is the branch you are rebasing onto (
main) and theirs is your own commit being replayed. This confuses everybody once. Read the content, not the labels.
The Escape Hatch
At any point during a rebase, in any state, no matter how wrong it has gone:
git rebase --abort
This puts the branch back exactly as it was before you started. It always works, and it costs nothing. If a rebase starts producing conflicts you do not understand, abort it, and merge instead.
Pushing Afterwards
Your local branch and origin/test/PAY-4417 now disagree — the remote still has the original commits, and yours are copies. A normal push is refused:
git push
! [rejected] test/PAY-4417 -> test/PAY-4417 (non-fast-forward)
Git is doing its job. It only accepts a push that adds to what is on the remote, and yours replaces it. You have to say explicitly that replacing is what you mean:
git push --force-with-lease
--force-with-lease vs --force
--force says: overwrite the remote branch with mine, whatever is there. If a colleague pushed to that branch ten minutes ago, their commit is now gone, and there is nothing on the remote pointing at it.
--force-with-lease says: overwrite it, but only if it is still at the commit I last saw. If someone else pushed in the meantime, the push is rejected and nothing is lost.
! [rejected] test/PAY-4417 -> test/PAY-4417 (stale info)
That rejection is the feature. Fetch, look at what arrived, and decide.
Pro Tip: Make
--force-with-leasea habit and never type bare--forceagain. Some teams configure an alias, others setpush.useForceIfIncludes. The two commands feel identical right up to the day they do not.
The Whole Sequence
git switch test/PAY-4417
git status # clean?
git fetch origin
git log --oneline main..HEAD # what am I about to replay?
git rebase origin/main
# ...resolve, git add, git rebase --continue, as needed
git log --oneline --graph -8 # sanity check
git push --force-with-lease
Nine lines, and the middle five are the whole skill.
Key Takeaways
- Rebase from a clean working tree, after a
git fetch git log --oneline main..HEADshows exactly what is about to be replayed- Conflicts stop the rebase per commit: fix,
git add,git rebase --continue - Never
git commitduring a rebase —--continuewrites the commit - During a rebase "ours" is the target branch and "theirs" is your commit
git rebase --abortrestores the branch completely, at any point- A rebased branch needs
git push --force-with-lease, never bare--force