Great work!

XP to next level

BugEater
EN

Rebasing Your Branch onto a Fresh main

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-lease and 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 --continue writes 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-lease a habit and never type bare --force again. Some teams configure an alias, others set push.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..HEAD shows exactly what is about to be replayed
  • Conflicts stop the rebase per commit: fix, git add, git rebase --continue
  • Never git commit during a rebase — --continue writes the commit
  • During a rebase "ours" is the target branch and "theirs" is your commit
  • git rebase --abort restores the branch completely, at any point
  • A rebased branch needs git push --force-with-lease, never bare --force

Quiz

A rebase stops with a conflict. You fix the file and stage it. What comes next?

Why does a normal git push fail after you rebase a branch you had already pushed?

What does --force-with-lease add over a plain --force?

Halfway through a rebase, the conflicts stop making sense. What is the safest move?