A Rebase Is N Merges
One-line summary
A commit object contains its parent hash. If the parent changes, the commit hash necessarily changes. In other words, a rebase is not a command that moves commits but a command that rewrites them.
Why this is needed
"Do not rebase commits you have already pushed" is slightly inaccurate. The essence is not whether you pushed, but whether that commit object exists in someone else's repository, and whether someone's work is stacked on top of it. So the precise form of the golden rule is this: do not rewrite a commit that has other people's work stacked on top of it.
Rebasing to tidy up a remote branch that only you use is not a problem. The problem is when someone else is working on top of it.
How it works
The reason the same conflict repeats in a rebase also comes from the structure. A merge compares only three points, the two endpoints and the common ancestor, but a rebase re-applies the commits one by one and does a new three-way merge each time. A rebase is not one merge but N merges. So if you rebase five commits, you may see the same conflict five times. If you turn on rerere.enabled true, it remembers a conflict resolution you have solved once and applies it again.
A conflict state is a state in which the index holds three candidates for one path (1 common ancestor / 2 ours / 3 theirs). This is why merge.conflictStyle zdiff3 is recommended. To merge correctly, you need to know not what the other side changed but what both started from.
An interactive rebase has six verbs. pick (keep), reword (message only), edit (modify content), squash (combine and combine messages too), fixup (combine and discard the message), drop (delete).
--force-with-lease checks whether the remote-tracking reference is the same as the value I last saw. But it has a hole. If the IDE has just fetched, the reference has already been updated, so even though a commit I have not seen has come in, the check passes. Supplement it with push.useForceIfIncludes true.
What it looks like in the field
The most common mistake when a conflict occurs during a rebase is to pick one side wholesale. If you move quickly with --ours or --theirs, the other person's work silently disappears. Moreover, during a rebase the meanings of ours and theirs feel opposite to intuition. This is because the side of the commit being replayed is theirs.
Another is not knowing how to stop. git rebase --abort returns exactly to the state before the start. When you get stuck, it is almost always better to stop and plan again than to push on by force.
When to rebase and when to merge
There is one rule. Do not rewrite commits that others are looking at.
| Situation | What to use |
|---|---|
| Put my local branch on top of the latest main | rebase — the history gets clean |
| A shared branch that has already been pushed | merge — rewriting breaks others' history |
| When putting a PR into main | Team rule (unify on one of squash, rebase or merge) |
| Undoing (what has been made public) | revert — cancels with a new commit |
If you must rewrite an already pushed branch, use --force-with-lease. Unlike --force, it pushes only when the remote is the same as the state I last saw. If someone pushed in the meantime, it is rejected, so it does not erase other people's commits.
Tools that reduce conflicts
rerere — when it meets the same conflict again, it applies it automatically as solved before. It is valuable when you rebase a long branch several times.
git config --global rerere.enabled true
--onto — moves only the base of a branch. Use it when moving a branch that split off from a feature branch onto main.
A---B---C main
D---E feature
F---G hotfix ← D,E 없이 F,G 만 main 위로
git rebase --onto main feature hotfix
autosquash — when fixing review comments, mark them with git commit --fixup <해시> and fold them all at once with git rebase -i --autosquash. You do not have to pick by hand which commit to merge into.
Recovering when an accident happens
Even if you make a mistake during a rebase, you can usually recover. Git does not delete commits right away.
git reflog # HEAD 가 거쳐 온 모든 자리
git reset --hard HEAD@{5} # 그중 하나로 되돌아간다
# 리베이스 중이라면 그냥 그만둘 수 있다
git rebase --abort
The reflog is kept for 90 days by default. "The commit disappeared" is usually not true; it is just that the branch pointing to it is gone. If you know the hash, you can get it back.
git fsck --lost-found finds even objects that nothing points to. It is the last resort when they are not even in the reflog.
What you will do in the next lab
You will make two diverged branches and directly compare the hashes before and after a rebase, make a straight-line history by fast-forward, deliberately create a conflict and resolve it preserving both sides' changes, and once deliberately abort to confirm restoration to the original state. Then with an interactive rebase you combine three commits into one and discard one, and finally put the golden rule into your own sentences.