本节目标
这一节学习 Git 中非常重要、但新手容易害怕的命令:rebase。
你要理解这些问题:
什么是 rebase?
rebase 和 merge 有什么区别?
为什么 Gerrit 工作流里经常使用 rebase?
rebase 冲突怎么解决?
什么时候可以 rebase,什么时候不要 rebase?
本节重点命令:
git fetch
git rebase
git rebase --continue
git rebase --abort
git rebase --skip
git log --oneline --graph --all
1. rebase 是什么
rebase 中文常翻译为:
变基
这个名字听起来复杂,但它的核心思想很简单:
把你的提交移动到另一个分支的最新提交后面。
假设一开始主分支是:
A --- B --- C main
你从 C 创建了功能分支:
A --- B --- C main
\
D --- E feature/login
后来别人往 main 合入了新提交:
A --- B --- C --- F --- G main
\
D --- E feature/login
这时你的功能分支已经落后于 main。
如果执行:
git switch feature/login
git rebase main
结果会变成:
A --- B --- C --- F --- G main
\
D' --- E' feature/login
注意这里变成了 D' 和 E'。
这表示:
Git 把你的 D、E 两个提交重新放到了 main 最新提交 G 的后面。
2. rebase 的直观理解
可以把 rebase 理解成:
我先把自己的改动临时拿下来。
然后把分支更新到目标分支最新位置。
最后再把自己的改动一个一个重新放上去。
过程像这样:
1. 找到你和目标分支的共同祖先
2. 暂时取下你自己的提交
3. 把当前分支移动到目标分支最新提交
4. 重新应用你自己的每一个提交
所以 rebase 之后,提交历史会更像一条直线。
3. merge 和 rebase 的区别
假设现在历史是:
A --- B --- C --- F --- G main
\
D --- E feature/login
3.1 使用 merge
执行:
git switch feature/login
git merge main
结果可能是:
A --- B --- C --- F --- G
\ \
D --- E --- M feature/login
这里会产生一个合并提交 M。
特点:
保留真实分支历史。
会多一个 merge commit。
历史图可能更复杂。
3.2 使用 rebase
执行:
git switch feature/login
git rebase main
结果可能是:
A --- B --- C --- F --- G --- D' --- E' feature/login
特点:
提交历史更直线。
不会产生额外 merge commit。
会重写你本地分支上的提交。
4. 为什么 Gerrit 经常使用 rebase
Gerrit 很重视每一个 Change 的评审质量。
很多 Gerrit 团队希望提交历史像这样:
A --- B --- C --- D --- E --- F
而不是这样:
A --- B --- C --- M --- N
\ \ /
D --- E ----
原因是:
-
历史更清楚
-
每个 commit 更容易评审
-
不容易产生无意义的 merge commit
-
一个 Change 对应一个清晰的修改
-
Gerrit 页面上的 Patch Set 更容易理解
所以在 Gerrit 中,经常会看到这种流程:
git fetch origin
git switch feature/login
git rebase origin/master
git push origin HEAD:refs/for/master
如果目标分支是 develop:
git fetch origin
git switch feature/login
git rebase origin/develop
git push origin HEAD:refs/for/develop
5. git fetch 是什么
在 rebase 前,经常先执行:
git fetch origin
它的作用是:
从远程仓库获取最新提交信息,但不自动合并到当前分支。
和 git pull 的区别:
git fetch = 只下载远程更新,不动当前工作区
git pull = fetch + merge 或 fetch + rebase
新手阶段建议多用:
git fetch
再明确决定:
git merge origin/master
或者:
git rebase origin/master
这样你更清楚 Git 正在做什么。
6. rebase 的标准流程
假设你在 feature/login 分支开发,目标分支是 master。
标准流程:
git status
git fetch origin
git switch feature/login
git rebase origin/master
如果没有冲突,rebase 会直接完成。
然后你可以查看历史:
git log --oneline --graph --all
最后推送到 Gerrit:
git push origin HEAD:refs/for/master
7. rebase 时发生冲突怎么办
rebase 本质上是把你的提交一个一个重新应用到目标分支后面。
如果某个提交和目标分支冲突,Git 会停下来,让你解决。
你可能看到:
CONFLICT (content): Merge conflict in readme.md
error: could not apply abc1234... 修改登录说明
意思是:
Git 正在重新应用某个提交时发生冲突。
你需要解决冲突后继续 rebase。
标准流程:
git status
# 手动修改冲突文件
git add readme.md
git rebase --continue
注意:
rebase 冲突解决后,不是 git commit。
而是 git rebase --continue。
这是 rebase 和 merge 冲突处理的重要区别。
8. rebase 冲突解决流程
完整流程:
git rebase origin/master
# 如果出现冲突
git status
# 打开冲突文件,删除冲突标记,整理成最终正确内容
git add 冲突文件
git rebase --continue
如果后面还有其他提交冲突,Git 可能会再次停下来。
你继续重复:
git status
# 解决冲突
git add 冲突文件
git rebase --continue
直到 rebase 完成。
9. 放弃 rebase
如果 rebase 过程中发现问题太复杂,想回到 rebase 前的状态,可以执行:
git rebase --abort
它的作用是:
取消当前 rebase,回到 rebase 开始之前。
适合这些场景:
-
rebase 了错误的目标分支
-
冲突太多,想重新整理
-
不确定怎么解决
-
想先备份当前分支
新手遇到不确定情况时,git rebase --abort 是一个很重要的安全按钮。
10. 跳过当前提交
rebase 过程中还有一个命令:
git rebase --skip
它的作用是:
跳过当前正在应用的提交。
注意:
这个命令可能会丢掉当前提交的修改。
新手不要随便使用。
只有当你非常确定这个提交已经不需要了,才使用 --skip。
11. rebase 会重写提交历史
这是 rebase 最重要的风险点。
rebase 后,原来的提交:
D --- E
会变成:
D' --- E'
虽然代码内容可能一样,但 commit id 已经变了。
原因是 commit id 和这些信息有关:
-
提交内容
-
父提交
-
作者信息
-
时间信息
-
commit message
rebase 改变了提交的父节点,所以 commit id 会变化。
所以要记住:
rebase 会重写提交历史。
12. 什么时候可以 rebase
通常可以 rebase 的情况:
你的本地功能分支
还没有被别人基于它继续开发
还没有作为公共分支给别人使用
你只是想把自己的提交更新到目标分支最新位置
例如:
git switch feature/login
git rebase origin/master
这是常见且合理的。
13. 什么时候不要随便 rebase
不要随便 rebase 这些分支:
main
master
develop
release
多人共享分支
已经推送并被别人基于其开发的分支
原因是:
rebase 会改变提交历史。
如果别人已经基于旧历史开发,你重写历史会让别人很痛苦。
简单记忆:
自己的本地分支可以 rebase。
公共共享分支不要随便 rebase。
14. Gerrit 中的 rebase 和 Patch Set
在 Gerrit 中,一个 Change 通常依靠 Change-Id 识别。
如果你 rebase 后重新推送:
git push origin HEAD:refs/for/master
只要 commit message 里的 Change-Id 没变,Gerrit 通常会把它识别为同一个 Change 的新 Patch Set。
例如:
实现登录功能
Change-Id: Iabc1234567890abcdef1234567890abcdef1234
rebase 后仍然保留这个 Change-Id,推送后会生成新的 Patch Set,而不是新的 Change。
15. rebase 后推送失败怎么办
如果你 rebase 过一个已经推送到普通远程分支的分支,再执行:
git push
可能会失败。
因为远程分支历史和你本地历史不一致。
普通 Git 平台有时需要:
git push --force-with-lease
但注意:
不要随便 force push。
尤其不要对 main/master 使用 force push。
在 Gerrit 工作流里,你通常不是推送到普通分支,而是推送到:
git push origin HEAD:refs/for/master
所以很多时候不需要直接 force push 到目标分支。
16. merge 与 rebase 怎么选择
可以这样理解:
| 场景 | 推荐 |
|---|---|
| 想保留完整分支合并历史 | merge |
| 想让提交历史更线性 | rebase |
| 本地个人功能分支同步主分支 | rebase 常见 |
| 公共共享分支整合功能 | merge 常见 |
| Gerrit 单 Change 更新目标分支 | rebase 常见 |
简单记忆:
merge:把别人的历史合进来。
rebase:把自己的提交搬到别人最新历史后面。
17. 实战练习:体验 rebase
创建练习仓库:
mkdir git-rebase-demo
cd git-rebase-demo
git init
创建初始提交:
echo "base" > readme.txt
git add readme.txt
git commit -m "添加基础文件"
创建功能分支:
git switch -c feature/login
echo "login feature" > login.txt
git add login.txt
git commit -m "添加登录功能"
切回主分支:
git switch main
如果你的主分支叫 master:
git switch master
在主分支添加新提交:
echo "main update" >> readme.txt
git add readme.txt
git commit -m "更新主分支说明"
查看历史:
git log --oneline --graph --all
切回功能分支并 rebase:
git switch feature/login
git rebase main
如果主分支叫 master:
git rebase master
再次查看历史:
git log --oneline --graph --all
你会看到功能分支的提交被移动到了主分支最新提交后面。
18. 实战练习:制造 rebase 冲突
创建练习仓库:
mkdir git-rebase-conflict-demo
cd git-rebase-conflict-demo
git init
创建初始文件:
echo "version: base" > config.txt
git add config.txt
git commit -m "添加基础配置"
创建功能分支:
git switch -c feature/login
echo "version: login feature" > config.txt
git add config.txt
git commit -m "修改登录配置"
切回主分支:
git switch main
如果你的主分支叫 master:
git switch master
主分支也修改同一行:
echo "version: main update" > config.txt
git add config.txt
git commit -m "修改主分支配置"
切回功能分支:
git switch feature/login
执行 rebase:
git rebase main
如果主分支叫 master:
git rebase master
这时应该会出现冲突。
19. 实战练习:解决 rebase 冲突
查看状态:
git status
打开 config.txt,可能看到:
<<<<<<< HEAD
version: main update
=======
version: login feature
>>>>>>> 修改登录配置
手动改成最终正确内容,例如:
version: main update with login feature
然后执行:
git add config.txt
git rebase --continue
如果还有冲突,继续解决。
如果完成,查看历史:
git log --oneline --graph --all
20. rebase 和 amend 的关系
Gerrit 中常见组合是:
git rebase origin/master
git commit --amend
git push origin HEAD:refs/for/master
但顺序要根据场景判断。
如果只是根据评审意见修改当前 Change:
git add .
git commit --amend
git push origin HEAD:refs/for/master
如果目标分支已经更新,你需要先同步:
git fetch origin
git rebase origin/master
git push origin HEAD:refs/for/master
如果 rebase 后还要修改提交信息或整理代码,再使用:
git commit --amend
核心原则:
保持 Change-Id 不变。
保持提交内容清晰。
不要制造无意义的 merge commit。
21. 常见错误
错误一:在公共分支上 rebase
不要随便对这些分支做 rebase:
main
master
develop
release
尤其不要 rebase 后强推公共分支。
错误二:rebase 冲突后执行 git commit
rebase 冲突解决后应该执行:
git rebase --continue
不是:
git commit
错误三:不确定时乱用 --skip
git rebase --skip 可能丢掉当前提交。
新手不要随便用。
错误四:rebase 后 Change-Id 丢失
Gerrit 依赖 Change-Id 判断是否是同一个 Change。
如果 Change-Id 丢了,可能会生成新的 Change。
错误五:rebase 前工作区不干净
rebase 前先看:
git status
最好保证:
nothing to commit, working tree clean
22. 本节必须记住的命令
git fetch origin
git rebase origin/master
git rebase --continue
git rebase --abort
git rebase --skip
git log --oneline --graph --all
对应含义:
git fetch origin 获取远程最新信息
git rebase origin/master 把当前分支提交搬到 origin/master 后面
git rebase --continue 解决冲突后继续 rebase
git rebase --abort 放弃本次 rebase
git rebase --skip 跳过当前提交
git log --oneline --graph --all 查看提交历史图
23. 本节总结
这一节你学习了 rebase:
-
rebase 是把你的提交重新放到目标分支最新提交后面
-
rebase 可以让提交历史更直线
-
merge 会保留合并历史,rebase 会重写本地提交
-
Gerrit 中经常使用 rebase 来保持 Change 清晰
-
rebase 冲突解决后要用
git rebase --continue -
不确定时可以用
git rebase --abort -
不要随便 rebase 公共分支
-
不要随便 force push 公共分支
最重要的一句话:
自己的本地功能分支可以 rebase,公共共享分支不要随便 rebase。
下一节建议学习:
Git commit --amend、Gerrit Patch Set 与 Change-Id:如何根据评审意见更新同一个 Change。