Git 高级技巧:cherry-pick、rebase、bisect 等进阶操作
学会了 add、commit、push、pull 和分支合并,你已经能应付日常开发了。但 Git 还有很多强大的进阶功能,知道这些能让你在遇到复杂情况时游刃有余。本文整理了五个最实用的高级技巧,每一个都是我在实际项目中高频使用的。
cherry-pick:只挑选一个提交
你在 bugfix 分支上修了一个 bug,现在想把这次修复也应用到 main 分支上。但 bugfix 分支上还有其他不想带到 main 的提交(半成品功能、实验性改动),合并整个分支不合适。
cherry-pick 就是为这个场景设计的。它能把指定的某次提交"复制"到当前分支:
git checkout main
git cherry-pick abc1234
abc1234 是你要复制的 commit ID(完整 hash 或者前7位缩写都行)。执行之后,main 分支上会多一次提交,内容和 abc1234 完全一样(改动相同),但提交 ID 不同(因为 Git 的 ID 包含了父提交信息、作者、时间戳等元数据)。
常见踩坑:cherry-pick 有可能遇到冲突。如果提示冲突,手动解决之后 git cherry-pick --continue 继续即可。如果是想放弃这次 cherry-pick,用 git cherry-pick --abort 取消。
实用场景:维护多个版本的产品时特别有用。比如你同时维护 v1 和 v2 两个大版本都要修同一个安全漏洞,修一次 cherry-pick 到另一个分支比手动复制代码靠谱多了。再比如你在 feature 分支上不小心提交了一个 hotfix,也可以用 cherry-pick 把这次 hotfix 单独拿到 main 分支上。
交互式 rebase:整理你的提交历史
你开发一个功能的过程中可能提交了好几次:第一次写了框架,第二次修了个 typo,第三次又改了接口参数。提交记录看起来很乱。
交互式 rebase 让你在推送之前重新整理提交历史:
git rebase -i HEAD~3
这会打开默认编辑器,显示最近三次提交(从新到旧),类似这样:
pick abc1234 增加用户登录API
pick def5678 fix typo
pick ghi9012 补充单元测试退出逻辑
你可以对每行做这些操作:
- pick:保留这个提交不变
- reword:保留提交内容,但修改提交说明文字
- squash(缩写
s):把这个提交的改动合并到上一个提交里,两个提交说明也会合并 - drop(缩写
d):删除这个提交(改动也一起丢弃) - edit:暂停 rebase,让你修改这个提交的代码
比如你想把后面两次提交合并到第一次提交里,就把后面两个 pick 改成 squash(或简写 s),保存退出。Git 会弹出一个新的编辑器让你重新写一条合并后的提交说明。整理完后的提交历史就变成一条干净的:增加用户登录API(含单元测试和研究typo)。
整理好的历史记录别人读起来舒服很多。理想情况下,一个功能对应一条提交,说明清晰,改动完整,别人一眼就能知道你做了什么。
重要警告:rebase 改写历史之后,push 需要加 --force(或更安全的 --force-with-lease)。但只在你自己的私有分支上这么做,公共分支(main、develop)上有其他人正在基于这些提交工作,一旦你 force push 会让他们的工作基础全部错乱。团队协作中最让人头疼的事之一就是有人 force push 了公共分支。
bisect:快速定位 bug 是哪次提交引入的
一个功能上周还好好的,这周坏了。中间提交了几十次,你怀疑是某次提交引入的 bug,但不知道到底是哪次。手动一个一个回退测试太慢了。
git bisect 用二分查找帮你快速定位。它的原理和有序数组中二分查找目标值一样:
git bisect start
git bisect bad # 当前版本是有 bug 的
git bisect good abc123 # abc123 这个版本是好的
Git 会自动切换到中间的那个版本。你测试一下——如果这个版本还是好的:
git bisect good
如果这个版本已经出现 bug 了:
git bisect bad
Git 会继续缩小范围,每次排除一半的可能性。假设中间有100个提交,手动测试需要100步,bisect 最多只要7步就能找到。找到之后:
git bisect reset
回到原来的状态。
自动化进阶用法:如果你有一个测试脚本,可以直接用 git bisect run ./test.sh 让 Git 自动运行测试脚本来判断 good 或 bad。脚本返回0表示 good,返回非0表示 bad。Git 会全自动定位到那个引入 bug 的提交,比手动测试快得多。
reflog:你的后悔药
你删了一个分支,过两天发现还需要恢复。或者你 reset 了一个提交,后来发现不该删。或者你误操作 git rebase 导致一些提交看起来消失了。
git reflog 记录了 HEAD 的所有移动历史,包括那些"已经消失"的提交——即使它们不在任何分支上被引用了,reflog 里依然可以找到它们的 ID:
git reflog
输出类似:
abc1234 HEAD@{0}: checkout: moving from feature-x to main
def5678 HEAD@{1}: commit: 修复了链接超时问题
ghi9012 HEAD@{2}: commit: 增加重试机制
每条记录前面都有一个 ID,找到你想恢复的那个,复制它的 ID:
git checkout def5678 # 先看一下是不是你要的提交
git checkout -b recovered-branch
或者更直接地:
git reset --hard abc1234
提交就回来了。不过 reset --hard 会丢弃当前工作区的改动,使用前务必保存。
reflog 默认保留 90 天,所以不是永久的——但大部分情况下,90 天够用了。如果你想做更长期的备份,可以用 git branch backup 创建一个指向当前状态的备份分支。
一个真实经历:我有次在凌晨调试问题时执行了 git push origin :feature-branch(这个命令的意思是删除远程分支),等我反应过来的时候远程分支已经消失了。但本地的 reflog 记录还在,轻松恢复。从那次之后,我都会在反复折腾之前确保有安全网。
submodule:管理子项目
一个大项目有时会依赖另一个独立的项目。比如你的前端项目要引用一个内部组件库,这个组件库在另一个 Git 仓库里,有自己独立的提交历史。
git submodule 允许你把一个 Git 仓库嵌入到另一个仓库里:
git submodule add https://github.com/xxx/component-lib.git libs/component-lib
这会在 libs/component-lib 目录下创建一个指向那个仓库的引用(本质上是一个特殊的记录文件)。别人克隆你的项目后,执行:
git submodule update --init --recursive
就能把子项目下载下来。如果子项目里还有子模块,--recursive 会自动递归初始化。
常用命令:
git submodule status:查看子模块状态和当前版本git submodule update --remote:更新子模块到最新的远程版本git diff --submodule:查看子模块是否有更新
submodule 用起来有点烦——主要问题是每次切换分支后可能需要手动更新子模块,而且别人第一次克隆项目容易忘记 --init 参数。但在必须拆分成独立仓库的场景下(比如组件库被多个项目共用),submodule 是最标准的解决方案。如今的替代方案还有 Git Subtree(把子项目代码直接合并进主仓库)和各大语言的包管理器(npm、pip等),按团队实际情况选择。
其他实用命令速查
- git blame 文件名:查看文件的每一行是谁、什么时候改的。看到一段奇怪的代码,用 blame 找到责任人,再去问清楚背景
- git tag v1.0.0 abc123:给重要的提交打上标签,用来标记版本发布点。标签比 commit ID 好记,而且语义化(v1.0.0 比 a3f9b2d 有意义得多)
- git diff:查看工作区和暂存区的区别。
git diff --cached查看暂存区和仓库的区别,git diff HEAD查看所有未提交改动 - git stash:临时保存当前未提交的改动,切换到别的分支工作,回来再
git stash pop恢复。非常适合被打断需要临时切换任务的场景 - git cherry-pick -n abc123:cherry-pick 但不自动提交,先把改动放到暂存区,你又检查或者修改后再手动提交
总结
| 技巧 | 一句话说明 | 适用场景 |
|---|---|---|
| cherry-pick | 复制某次提交到当前分支 | 跨分支搬运个别修复 |
| rebase -i | 重写最近的提交历史 | 推送前整理零散的提交记录 |
| bisect | 二分查找找到引入bug的提交 | 大版本之间定位问题来源 |
| reflog | 记录HEAD的所有移动 | 误删或误操作后恢复提交 |
| submodule | 在仓库里引用另一个仓库 | 多项目共享公共组件库 |
这些高级技巧不需要每天都用,但关键时刻能救命。建议把这张表格收藏起来,遇到对应场景的时候再查具体用法。用得多了自然就记住了。