| English | 中文 | Interactive demo | Runnable lab |
After an upstream branch advances, why can git rebase preserve code changes while commit IDs change? Rebase does not move an old commit to a new location. It selects the changes to retain, creates replay commits with new parent relationships, then updates the branch ref.
This chapter answers five questions:
At the start, topic and main diverge at B:
T1---T2 topic
/
...---B---U1---U2 main
Run this while on topic:
git rebase main
Git identifies the commits on topic that should be retained relative to main, temporarily starts from main, applies those changes in order, then updates topic to the result tip:
...---B---U1---U2 main
\
T1'---T2' topic
T1---T2 old objects remain findable through a backup ref or reflog
T1' and T1 can have the same file-level change, but their parent changes from B to U2. A commit records a tree, parent, author, committer, and other data. When Git creates a new object, its object ID follows that object content. Rebase rewrites the history named by a branch ref; it does not mutate an existing commit object in place.
The official documentation describes this as listing the current branch’s commits that have no equivalent upstream commit, checking out the upstream, replaying the commits in order, and updating the branch ref.
“Every rebase changes every commit ID” is inaccurate. First establish whether Git actually creates a replay commit:
| Condition | Result |
|---|---|
| The current branch has a non-equivalent commit to retain relative to the new upstream, and Git replays it successfully | Git creates a replay commit; the new parent relationship makes it a new object |
| The current branch already sits at the upstream, or no commits are eligible for replay | No replay commit is created; the ref can stay unchanged |
| Upstream already contains a recognizable equivalent clean cherry-pick | Default rebase skips that commit; options such as --reapply-cherry-picks change this handling |
| An interactive rebase drops, combines, edits, or reorders commits | The todo list controls the result; retained replayed changes form new objects and dropped entries do not occur in the result history |
Object-ID changes come from new objects that are actually created, not from the word rebase in a command. Automation should compare parents, trees, commit ranges, and refs instead of assuming every invocation rewrites every ID.
Branch names are movable refs. Create an explicit backup ref so that the old history still has a stable name after the operation:
git switch topic
git branch backup/topic-before-rebase topic
git rebase origin/main
After a successful replay, inspect old and new histories together:
git log --graph --oneline --decorate --all
git show backup/topic-before-rebase
git show topic
git reflog show topic
backup/topic-before-rebase keeps the old tip reachable for review and recovery. A reflog commonly records ref movement too, but expiry and repository maintenance policy affect it. An explicit backup ref is clearer when a reliable comparison is needed.
The lab creates an isolated temporary repository. It disables system and global Git configuration, sets a temporary identity, disables signing, and isolates hooks. It does not modify this project:
cd labs/git-mental-model/07-rebase
lab_path=$(bash setup.sh)
bash verify.sh "$lab_path"
It preserves three real outcomes at once:
replay-topic points to a new commit after a successful replay
replay-topic-original explicitly names the old commit, which remains readable
no-replay is already at the upstream tip and has no commit to replay
topic is rebasing onto upstream; its second commit produces a content conflict
verify.sh asserts:
replay-topic-original has replay-base as its parent
replay-topic has replay-upstream as its parent
the two commit IDs differ while topic.txt content is the same
no-replay, no-replay-before, and replay-upstream name the same commit
the topic conflict has UU app.txt in the working tree and index stages 1, 2, and 3
This demonstrates two boundaries: a successful replay creates a new object, while a rebase with no eligible commits creates none.
To preserve and inspect the lab state:
cd "$lab_path"
git log --graph --oneline --all --decorate
git show --no-patch --format=raw replay-topic-original
git show --no-patch --format=raw replay-topic
git status --short
git rebase --show-current-patch
git ls-files --unmerged -- app.txt
Remove only the temporary repository marked for this lab:
bash cleanup.sh "$lab_path"
Create, assert, verify abort, and clean up in one run:
bash test.sh
See expected.txt for the full expected state.
The topic in this lab has two commits. The first, which creates topic-note.txt, already replayed onto upstream. The second changes app.txt to owner=topic and conflicts with upstream’s owner=upstream. Inspect it with:
git status --short
git rebase --show-current-patch
git ls-files --unmerged -- app.txt
git show :1:app.txt
git show :2:app.txt
git show :3:app.txt
The lab’s output means:
| Index stage | Content | Meaning during this rebase conflict |
|---|---|---|
| 1 | owner=base |
The three-way merge base |
| 2 | owner=upstream |
Ours: the replay result based on upstream |
| 3 | owner=topic |
Theirs: the old topic commit currently being replayed |
In a regular merge, stage 2 is usually the current branch HEAD and stage 3 is MERGE_HEAD. Rebase applies working-branch commits on top of upstream, so ours here is the so-far-rebased series combined with upstream and theirs is the working-branch commit being applied. Before using git checkout --ours or git checkout --theirs, read the stage contents instead of relying on the words alone.
Resolve the conflict with:
# Edit app.txt and select the result that matches the real contract.
git add app.txt
git rebase --continue
git add returns the conflicted path to stage 0. git rebase --continue then attempts later replays. If a commit’s change is confirmed to be unnecessary, first understand that skipping removes it from the final history, then use git rebase --skip.
A rebase backend can use rebase-merge or rebase-apply in Git’s directory to record temporary state. For teaching observation, ask Git for the possible paths:
for backend in rebase-merge rebase-apply; do
state_dir=$(git rev-parse --git-path "$backend")
test -d "$state_dir" && printf 'active backend state: %s\n' "$state_dir"
done
The actual directory depends on backend, version, and options. Stable inspection entry points are git status --short, git rebase --show-current-patch, git ls-files --unmerged, and the index-stage content. The lab does not use a fixed internal directory as an assertion condition.
Before starting a rebase, inspect and preserve the work state:
git status --short
git branch backup/topic-before-rebase topic
git rebase origin/main
When that clean precondition holds and you decide to abandon the operation:
git rebase --abort
test.sh actually runs this command and confirms that topic returns to topic-original, the working tree becomes clean, and REBASE_HEAD disappears. That result covers the lab’s clean-start scope. Mixing extra uncommitted edits into a rebase before it starts or while resolving conflicts broadens recovery difficulty; preserve those edits first, then choose abort or continue.
The author can replay a private local topic independently. Once a branch is pushed, or someone has developed from its old commits, a rewrite changes their common ancestor and later operation paths. Establish sharing state and project policy before rewriting history.
A common controlled sequence is:
git fetch origin
git switch topic
git status --short
git branch backup/topic-before-rebase topic
git rebase origin/main
# Run the affected project's build, tests, and review.
git push --force-with-lease origin topic
--force-with-lease requires the remote ref to retain the value that the local repository expects, and fails if another commit advanced it. It reduces accidental overwrite risk; it does not replace collaboration notice, review, or behavior validation. A background fetch can also affect lease checks without an explicit expected value, so important shared branches should follow the team’s defined remote-update process.
| Branch state | First decision |
|---|---|
| Local only, used by its author | Rebase after creating a backup ref, then validate the result |
| Pushed, with no other work based on the old tip | Notify relevant people, then use the project-approved controlled push method after replaying |
| Used as a base by others or assigned a release role | Coordinate a shared migration; merge, an appended revert, or a new branch is often easier to recover than direct rewriting |
Given:
T1 topic
/
...---B---U1 main
git rebase main result commit different from T1?topic already equals main, must the same command create a new commit?T1, what may default rebase do?Answers:
U1 as its parent, so Git creates another commit object.An agent plans to rebase a pushed topic and says, “The code diff is small, so git push --force is fine.” Which action set follows an auditable collaboration workflow?
A. Force-push directly as long as local tests pass
B. Confirm whether anyone built on the old tip, create a backup ref, validate affected behavior after rebase, then use force-with-lease or coordinate migration according to project policy
C. Check only whether commit IDs changed
Choose B. The size of a code diff says nothing about the impact of ref rewriting on collaborators. A backup ref supports review and recovery, affected validation checks replayed behavior, and a remote lease provides only part of the concurrency protection.
The old commit object remains unchanged. When replay is needed, rebase creates new objects and updates the branch ref to a new tip.
Only replay commits that Git actually creates have new IDs. When no commits are eligible, or a commit is recognized as already upstream, the result history can have no corresponding new object.
Rebase applies working-branch changes on top of upstream. Stage 2 represents the replay result based on the new upstream, and stage 3 represents the working-branch commit being applied. Read the actual content before choosing a resolution strategy.
The recovery result of git rebase --abort depends on the working-tree state before and during the operation. Start clean, explicitly preserve extra edits, then abort with a clear recovery boundary.