Git 高级技巧:cherry-pick、rebase、bisect 等进阶操作

Git 高级技巧:cherry-pick、rebase、bisect 等进阶操作

学会了 addcommitpushpull 和分支合并,你已经能应付日常开发了。但 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 在仓库里引用另一个仓库 多项目共享公共组件库

这些高级技巧不需要每天都用,但关键时刻能救命。建议把这张表格收藏起来,遇到对应场景的时候再查具体用法。用得多了自然就记住了。