Git 变基(Rebase)的工作原理:一个一个重新应用提交
1. 变基的核心过程
比喻:重播电影
想象你有一部电影(提交历史):
- 原版电影:A → B → C → D
- 你想从 B 开始重拍:变基到 B
- Git 会:暂停电影 → 从B开始重新拍摄每个场景 → 生成新电影
2. 详细步骤分解
原始状态
提交历史: A ← B ← C ← D ← E (HEAD/master)
↑
你要变基到这里(比如B)
变基过程
bash
git rebase B
Git 内部执行:
步骤1:保存要移动的提交
bash
# Git 会:
1. 找到 B 之后的所有提交:[C, D, E]
2. 把这些提交"拆下来"临时保存
步骤2:重置到目标提交
bash
# Git 会:
3. 把 HEAD 移动到 B(快退到B时刻)
4. 工作区变成 B 时的状态
步骤3:一个一个重新应用
bash
# Git 会:
5. 取出 C,尝试应用到当前状态
6. 如果成功,创建新的 C'
7. 取出 D,尝试应用到 C' 上
8. 如果成功,创建新的 D'
9. 取出 E,尝试应用到 D' 上
10. 如果成功,创建新的 E'
步骤4:更新分支指针
bash
# 最终:
旧历史:A ← B ← C ← D ← E
新历史:A ← B ← C' ← D' ← E'
3. "一个一个重新应用"的具体含义
对于每个提交,Git 做这些事:
python
# 伪代码表示变基过程
def rebase_commit(old_commit, current_state):
# 1. 获取这个提交的更改
changes = get_changes(old_commit)
# 2. 尝试将更改应用到当前状态
try:
new_state = apply_changes(current_state, changes)
# 3. 创建新的提交
new_commit = create_commit(
message = old_commit.message,
author = old_commit.author,
changes = changes,
parent = current_state.last_commit
)
return new_commit, new_state
except ConflictError:
# 4. 如果有冲突,暂停并等待用户解决
pause_and_wait_for_user_to_resolve()
4. 实际例子:你的飞行项目
你的提交历史
bash
# 原始提交链
3394272 ← 31cea98 ← 8f5b36e ← ... ← cc67b56 ← ... ← b095acf
当你执行 git rebase -i 3394272
bash
# Git 的内部操作:
1. 保存这些提交:[31cea98, 8f5b36e, ..., cc67b56, ..., b095acf]
2. 重置到 3394272(工作区变成那时的状态)
3. 开始重新应用:
- 尝试应用 31cea98 → 创建新的提交 31cea98'
- 尝试应用 8f5b36e → 创建新的提交 8f5b36e'
- ...
- 现在正在尝试应用 cc67b56
- 如果成功 → 创建 cc67b56'
- 然后继续下一个...
5. 为什么需要"重新应用"而不是"移动"?
原因:提交是基于父提交的
bash
# 每个提交都包含:
1. 当前文件状态
2. 与父提交的差异(diff)
3. 父提交的引用
# 所以不能直接移动,因为:
# 提交 C 是基于 B 的差异
# 如果要放到 A 后面,需要重新计算差异
示例:计算差异
提交 B: 文件内容 = "Hello"
提交 C: 在 B 基础上添加 " World" → 内容 = "Hello World"
差异: + " World"
现在要放到 A 后面:
提交 A: 文件内容 = "Hi"
应用 C 的差异 (+ " World") → 内容 = "Hi World"
6. 交互式变基的特殊处理
当你有修改指令时:
bash
git rebase -i 3394272
# 编辑器中:
pick 31cea98 完善mybatis-plus代码生成器
squash 8f5b36e 编写了docker脚本...
drop 98b50e9 忽略target文件夹
reword cc67b56 登录、分页查询、新增员工
Git 的处理:
bash
# 对于每个指令:
1. pick: 正常重新应用
2. squash: 应用更改,但不立即提交,合并到前一个提交
3. drop: 跳过这个提交(不应用)
4. reword: 应用更改,但让你修改提交信息
7. 遇到冲突时的暂停
为什么停在 cc67b56?
bash
# 可能原因:
1. cc67b56 修改了某个文件
2. 之前的某个提交(比如 31cea98')也修改了同一行
3. Git 不知道该如何合并
# 所以 Git 说:
"我正在应用 cc67b56,但这里有个冲突,你来决定怎么处理"
8. 可视化理解
变基前:
时间线:○──○──○──○──○──○
提交: A B C D E F
标签: ↑目标
变基过程:
1. 拆下 C,D,E,F: A B [C][D][E][F]
2. 回到 B: A B
3. 重新贴 C: A B ── C'
4. 重新贴 D: A B ── C' ── D'
5. 重新贴 E: A B ── C' ── D' ── E'
6. 重新贴 F: A B ── C' ── D' ── E' ── F'
9. 实际执行的 Git 命令
Git 底层命令
bash
# 变基大致相当于:
# 1. 创建临时分支
git checkout -b temp-branch 3394272
# 2. 逐个 cherry-pick
git cherry-pick 31cea98
git cherry-pick 8f5b36e
# ...
git cherry-pick cc67b56 # ← 停在这里
# ...
# 3. 移动分支指针
git branch -f master temp-branch
git checkout master
10. 关键要点总结
- 不是移动,是重演:每个提交都被重新创建
- 顺序重要:必须按顺序应用,因为每个提交基于前一个
- 可能冲突:重演时可能与当前状态冲突
- 创建新提交:即使内容相同,也是新的提交对象(不同哈希)
- 原提交还在 :直到垃圾回收前,原提交还在
.git/objects中
简单说: Git 像导演一样,从某个场景开始,按照剧本(提交历史)重新拍摄每一个镜头(提交),生成新电影(新历史)。