Rebasing explained; merge or rebase?
Intermediate Git CLI VS Code UI IntelliJ UI GitLab UI GitHub UI
Why this matters
Rebase has a reputation: powerful, tidy, and dangerous. All three are true, and the danger is entirely predictable once you know that rebase does not move commits, it re-creates them. This lesson shows the mechanism, the force push it leads to, and the single rule that separates the safe use from the one that ruins a colleague's afternoon.
What rebase does
$ git switch docs/12-windows-install-steps
$ git rebase mainSuccessfully rebased and updated refs/heads/docs/12-windows-install-steps.$ git log --oneline --graph --all --decorate* 5f181fc (HEAD -> docs/12-windows-install-steps) docs: add Windows steps to installation guide (#12)
* 1a01760 (origin/main, origin/HEAD, main) feat: add Coast Walk trail (#29)
| * 510f1e0 (origin/docs/12-windows-install-steps) docs: add Windows steps to installation guide (#12)
|/
* 8fa922e (tag: v1.2.0) Initial import of trailguideRead that graph carefully, because it contains the whole lesson. There are two commits with the same message: 5f181fc, the new one sitting on top of main, and 510f1e0, the original, still pointed at by origin/docs/12-… on the remote. Your branch now has a history the remote does not share.
The push that follows
$ git pushhint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.Git is protecting the remote: your new commit is not a descendant of what is there. Following the hint and running git pull would be a mistake here, because it would merge the old commit back in and leave you with both. What you actually want is to replace the remote branch with your rewritten one:
$ git push --force-with-leaseTo https://gitlab.com/northwind-trails/trailguide.git
+ 510f1e0...5f181fc docs/12-windows-install-steps -> docs/12-windows-install-steps (forced update)The + and "forced update" mark a branch whose history was replaced.
The golden rule
Rebase only commits that exist nowhere but your own computer and your own branch.
The reason is mechanical, not stylistic. If a colleague has the old commits and you replace them, their next pull produces a repository containing both versions of your work, and every later merge tangles. That is why:
- Rebasing a feature branch that is yours onto
main: normal and useful, even after pushing, as long as nobody else has checked it out. - Rebasing
main, a release branch, or any branch two people work on: not a matter of taste. Do not. - If in doubt, ask, or merge instead. Merging is never wrong; it only makes the graph less tidy.
Merge or rebase?
Both bring main's newer work into your branch. Choose by what you value and by who else is involved:
| Question | Merge | Rebase |
|---|---|---|
| What does history look like? | Fork and join, the true shape | Straight line, as if you had started today |
| Do my commits keep their hashes? | Yes | No, they are re-created |
| Can I do it after pushing? | Yes, plain git push |
Yes, but needs --force-with-lease |
| Is someone else on this branch? | Fine | Do not rebase |
| Conflicts | Resolved once, in one commit | Possibly once per commit being replayed |
| Team rule | The safe default | Common where the team wants a linear main |
A practical compromise many teams use: merge main into your branch while you work, and let the platform squash the branch on merge. You never rebase, and main still ends up linear.
How to do it
$ git switch main && git pull
$ git switch docs/12-windows-install-steps
$ git rebase main
$ git push --force-with-leaseIf a conflict stops the rebase, Git pauses on the commit it could not apply; resolve, git add, then git rebase --continue, or git rebase --abort to return to where you started (lesson 11.5).
- Update
mainfirst: switch to it and pull. - Switch back to your branch and open the Command Palette (Ctrl+Shift+P / ⇧⌘P) → Git: Rebase Branch… → pick
main. - Push afterwards: the sync icon offers Force Push once the branch has diverged. Enable it first in Settings → Git: Allow Force Push, and set Git: Use Force Push With Lease (on by default in current versions).
- On a conflict the merge editor opens; the Source Control view then shows Continue to resume the rebase.
- Open the branches popup (status bar, bottom right), point at
main→ Rebase current onto 'main'. - For more control, Git → Rebase… opens a dialog with the onto-branch and options such as Interactive.
- Push with Git → Push; when the branch has diverged IntelliJ offers Force Push, and it uses
--force-with-leaseby default. - Conflicts open the three-way merge tool; the toolbar then offers Continue Rebasing or Abort Rebasing.
A merge request whose branch is behind shows a Rebase button when the project's merge method requires a linear history (fast-forward merge or semi-linear). Pressing it rebases the branch on the server and force-pushes it for you.
Afterwards your local copy is the stale one. Do not git pull, which would merge the old commits back; instead run git fetch and then git reset --hard origin/<branch> to match the server, having first made sure you have no uncommitted work.
The pull request's Update branch dropdown offers Update with rebase, and the merge button offers Rebase and merge, which replays the branch's commits onto main at merge time with new hashes and no merge commit.
As on GitLab, a server-side rebase leaves your local branch behind: git fetch then git reset --hard origin/<branch>.
Do and don't
Do
- Rebase your own unshared branch to keep the merge request clean.
- Update
mainimmediately before rebasing, so you land on the current base. - Use
--force-with-leasefor the push that follows, and read its output.
Don't
- Don't rebase
main, a release branch, or any branch a colleague has checked out. - Don't follow the "use 'git pull' before pushing again" hint after a rebase; it merges the old commits back.
- Don't rebase to fix a rejected push before checking why it was rejected; a colleague's new commits look the same at first glance.
Common mistakes
git pullafter rebasing. You end up with both the old and the new commits and a confusing merge. If it happens:git reset --hard origin/<branch>if the remote is right, or lesson 12.1 to recover.- Plain
--forceon a shared branch. Colleagues' commits vanish from the remote. Recoverable through their local copies and the reflog, but a bad afternoon. - A rebase with many conflicts. Each replayed commit can conflict separately.
git rebase --abortand merge instead; nobody will judge you. - Rebasing after the reviewer started reading. The commits they commented on no longer exist, and GitLab and GitHub show the threads as outdated. Rebase before review, not during.
Try it yourself
Goal: rebase a branch, see the hash change, and recognize the rejected push.
- From an up-to-date
main, createdocs/practice-rebase, make one commit, and push it with-u. - Switch to
main, make one commit there, push. - Switch back to the branch and note the commit's hash with
git log --oneline -1. - Run
git rebase main, thengit log --oneline -1again and compare the hash. - Run
git pushand read the rejection, thengit push --force-with-lease. - Run
git log --oneline --graph --all --decorate -4.
Expected result: the hash in step 4 differs from step 3 although the message is identical; step 5 is rejected first and then reports a "forced update".
Show solution
The hash changed because the commit has a new parent, and a commit's hash covers its parent (lesson 2.4). The rejection is Git refusing to discard the old commit on the remote without being told explicitly; --force-with-lease tells it, while still checking that nobody else has pushed. Clean up: git switch main, git branch -D docs/practice-rebase, git push origin --delete docs/practice-rebase.