我记得刚接手那个日订单量 30 万 + 的电商订单系统时,团队 12 个人都在同一个 develop 分支上干活。每天早上打开 Git 日志,就像看一锅乱炖:Merge branch 'develop' of xxx into develop、fix bug、update code…… 这种提交信息占了 70% 以上。更要命的是,每次上线前合并 release 分支,总会冒出一堆没意义的合并提交,有次大促前紧急修复支付回调接口,我花了整整 40 分钟才从一堆 Merge 提交里翻出真正的改动点。
后来我带着团队把 git pull 换成了 git pull --rebase,再配合严格的 Commit 规范,情况彻底变了。这里先解释下为什么不用 git pull:默认情况下 git pull 等于 git fetch + git merge,每次拉代码都会生成一个合并提交。假设你在本地改了 2 个文件,远程也有 1 个提交,执行 git pull 后日志里就会多一个 Merge 节点。团队人一多,日志就会变成一张网,根本没法追溯历史。而 git pull --rebase 是先把你本地的提交“暂存”,拉取远程最新代码,再把你的提交“重新应用”上去,整个过程没有多余的合并节点,日志是一条干净的直线。
我们当时定的规则很简单:本地提交没推送到远程前,拉代码必须用 git pull --rebase;已经推送到远程的提交,绝对不允许再 rebase(避免改写公共历史)。为了降低大家的记忆成本,我在项目根目录加了个 .gitconfig 示例,让新人 clone 代码后直接复制就能用:
这里提一下 --autostash 参数,我之前遇到过好几次:本地改了半天的代码还没提交,突然要拉远程代码,默认 git pull --rebase 会报错说工作区不干净。加上 --autostash 后,Git 会自动把未提交的改动暂存,拉完代码再恢复,省得手动 git stash 再 git stash pop,我们团队用了这个参数后,这类小问题直接降到了 0。
光有 rebase 还不够,提交信息乱的问题得解决。我们参考了 Conventional Commits 规范,但做了简化,只要求必须带 feat(新功能)、fix(修复)、refactor(重构)、test(测试)四个前缀,后面跟冒号和简短描述。比如支付模块的 bug 修复,必须写成 fix: 修复支付宝回调签名验证失败问题,而不是 fix bug。为了让大家不用记格式,我写了个简单的 commit-msg 钩子,放在 .git/hooks/ 目录下,不符合格式的直接拒绝提交:
这个钩子上线后,前两周还有人提交被拒,第三周开始就没人踩雷了。后来有次线上订单状态同步延迟,我直接用 git log --grep="fix: 订单状态" 搜,10 秒就找到了两周前修复同步逻辑的提交,要是放在以前,我得翻至少 20 分钟的日志。
这里要提醒下,rebase 不是万能的。我们团队有个新人,把自己已经推送到远程的 3 个提交 rebase 了,导致其他同事拉代码时冲突了一大片。后来我专门在周会上强调:只有本地未推送的提交才能 rebase,已经推给别人的代码,绝对不能改写历史。如果本地提交太多太乱,可以用 git rebase -i HEAD~3 合并成 1 个提交再推送,我们当时规定单个功能最多保留 2 个提交,这样日志既干净,又不会丢失关键信息。
去年我们公司把 6 个独立的微服务仓库合并成了一个 Monorepo,总代码量 120 万行,.git 目录大小直接飙到了 8.7GB。之前用单个仓库时,新人 clone 一次要等 25 分钟,CI 流水线里每次拉代码也要 18 分钟,严重拖慢了发版速度。后来我研究了 Git 2.45.0(2024 年 5 月发布的版本,每 3 个月更新一次,这个版本对部分克隆做了性能优化)的新特性,用 Partial Clone(部分克隆)和 Sparse Checkout(稀疏检出)把 clone 时间压到了 3 分钟以内,CI 拉代码时间也降到了 2 分钟。
先讲 Partial Clone。默认 git clone 会把远程仓库的所有对象(提交、树、文件内容)都下载到本地,哪怕你只需要其中 1 个目录的代码。而 Partial Clone 允许你只下载必要的对象,其他对象等用到的时候再按需拉取。我们当时仓库里有很多历史版本的二进制文件(比如之前的设计稿、旧版本的 jar 包),占了 6GB 的空间,这些文件新人根本用不上。我配置了部分克隆后,第一次 clone 只下载了最近 3 个月的提交和当前分支的文件,历史大文件直接跳过。
具体配置很简单,clone 的时候加 --filter 参数就行。我们当时用的是 blob:none 过滤规则,意思是只下载提交和树对象,文件内容(blob)等用到的时候再拉。命令如下:
这里 --no-checkout 是先不检出文件,等配置好稀疏检出再检,避免先下载所有文件再过滤浪费时间。我们实测下来,用这个命令 clone 后,.git 目录直接从 8.7GB 降到了 1.2GB,clone 时间从 25 分钟变成了 2 分 40 秒。
再说说 Sparse Checkout。我们 Monorepo 里有 12 个模块,每个开发同学只负责 1-2 个模块,之前 clone 完所有代码,本地磁盘占了 12GB,很多代码根本不会碰。用稀疏检出后,大家只拉自己负责的模块,磁盘占用直接降到了 2GB 左右。Git 2.45.0 对稀疏检出的性能做了优化,之前切换模块要 10 秒,现在只要 2 秒。我平时负责订单和支付模块,配置稀疏检出的完整流程是这样的:
这里 --cone 模式是 Git 2.45.0 推荐的模式,比之前的模式快 30% 左右,它只匹配目录层级,不会递归匹配所有文件,适合 Monorepo 的场景。我之前试过不用 --cone 模式,配置复杂不说,切换模块时还会卡 5 秒以上,换成 --cone 后这个问题就消失了。
有个实际的问题我印象很深:之前 CI 流水线里每次构建都要 git clone 完整仓库,18 分钟里 12 分钟都在拉代码。后来我把 CI 脚本改成了部分克隆 + 稀疏检出,只拉构建需要的模块,比如前端构建只拉 src/web 目录,后端构建只拉 src/order 和 src/payment。修改后的 CI 脚本片段如下:
加了 --depth=1 做浅克隆,只拉最近 1 次提交,CI 拉代码时间直接降到了 1 分 50 秒,整个构建流程从 25 分钟缩短到了 10 分钟以内。我们当时统计过,每天 CI 流水线运行 40 次,每天节省的时间加起来有 6 个小时,相当于多了一个人力。
不过要注意,Partial Clone 有个小问题:如果你需要查看很久以前的历史提交,或者需要用到被过滤掉的大文件,Git 会临时去远程拉取,可能会慢 1-2 秒。但我们团队几乎没人需要看 3 个月前的大文件历史,所以这个代价完全可以接受。Git 2.45.0 还优化了按需拉取的性能,之前拉一个 100MB 的历史文件要 8 秒,现在只要 3 秒,基本感知不到延迟。
去年双 11 大促前,我们同时维护着 3 个版本:正在上线的 release/v3.2、修复线上 bug 的 hotfix/v3.1.5、开发新功能的 develop。之前每次切换分支都要 git stash 保存本地改动,切换完再恢复,有次我在 develop 分支改了半天的订单逻辑,切换到 hotfix 分支时忘了 stash,结果改动的代码被覆盖了,我花了 1 个多小时才重新写出来。后来我开始用 git worktree,同一个仓库可以同时打开多个工作目录,每个目录对应一个分支,再也不用频繁切换了。
Git Worktree 的原理很简单:它会在你的仓库目录下创建多个工作树,共享同一个 .git 目录里的对象和引用。比如你可以在 ~/project/monorepo 放 develop 分支的代码,在 ~/project/monorepo-hotfix 放 hotfix 分支的代码,两个目录完全独立,改其中一个不会影响另一个。我现在的习惯是,同时维护 2 个以上分支时,一定会用 worktree,而不是切换分支。
创建 worktree 的命令非常直接,我平时用的命令是这样的:
上次线上支付接口突然报错,QPS 从 500 掉到了 100,我直接打开 hotfix 工作树,10 秒就开始改代码,改完提交、构建、上线,整个过程只用了 15 分钟。要是放在以前,我得先 stash develop 的改动,切换分支,改完再切回来恢复,至少多花 5 分钟,而这 5 分钟里可能就有几千个订单支付失败。
worktree 用久了,我发现另一个问题:我们的 Monorepo 有 1200 多个分支,每次执行 git branch 都要等 3 秒,执行 git push 有时候会卡 2 秒。查了下 Git 2.45.0 的文档,发现是默认的引用存储后端(loose 模式)在大规模引用场景下性能不好——每个引用都是一个单独的文件,1200 个分支就有 1200 个文件,读写都慢。Git 2.45.0 推荐用新的 reftable 后端,把所有引用存到一个紧凑的二进制文件里,读写速度能提升 5 倍以上。
我先在测试环境试了 reftable 后端,迁移命令很简单,不需要改任何工作流:
迁移后我测了几个常用命令的速度:之前 git branch 要 3 秒,现在 0.5 秒;git push 之前卡 2 秒,现在几乎秒响应;git fetch 拉 100 个新分支,之前要 8 秒,现在 2 秒。我们团队 12 个人都迁移后,每天节省的等待时间加起来至少有 30 分钟。不过要注意,reftable 后端是 Git 2.45.0 才稳定的,之前的版本可能有兼容性问题,所以一定要先升级到 2.45.0 及以上再用。
我还做过一个对比测试:同时维护 3 个分支(develop、hotfix、release),用传统切换分支的方式,每次切换平均要 12 秒(包括 stash、切换、恢复),一天切换 20 次就是 4 分钟;用 worktree 的话,打开一次工作树要 5 秒,之后切换目录只要 1 秒,一天切换 20 次总共 25 秒。再加上 reftable 后端节省的 1 秒/命令,一天下来能多 5 分钟写代码。
有个小细节要提醒:worktree 创建的工作树,用完记得删掉,不然会占用磁盘空间。删除命令很简单:
我们团队之前有个同学创建了 5 个 worktree 忘了删,占了 10GB 磁盘,后来我写了个定时脚本,每周检查一次未使用的 worktree,自动发提醒,这个问题就解决了。
现在我们的标准配置是:Git 2.45.0 + reftable 后端 + worktree 管理多分支。之前同时维护 3 个版本要手忙脚乱,现在每个版本对应一个工作树,切换目录就行,再也没出现过代码覆盖的问题。而且 reftable 后端让引用操作快了很多,尤其是 CI 流水线里拉分支、推代码的步骤,整体速度提升了 40% 左右。
那是一个周五下午五点半,我正准备合上电脑去接孩子,钉钉突然炸了。运维那边急吼吼地喊,说生产环境发版失败,Jenkins 报找不到 release/2024.05 分支。我心里咯噔一下,当时刚给一个新来的后端演示完 git push origin --delete release/2024.05 清理旧分支的操作,估计是他手滑把正在用的给干掉了。
当时那个 release/2024.05 分支上挂着大概 1.2 万个提交,是我们电商系统核心交易域的发布分支,QPS 峰值能到 8000+。如果找不回来,意味着我们要从 master 重新拉代码,还得手动 cherry-pick 过去两周的 40 多个 hotfix,这基本上就得通宵了。
我第一反应就是 git reflog。很多刚入行的同学以为 git log 就是看历史的全部,其实 reflog (Reference Log) 才是 Git 的“黑匣子”。它记录的是本地仓库里 HEAD 指针每一次移动的历史。哪怕你 reset --hard 了,或者删除了分支,只要你的本地仓库没被物理删除,那些 commit 对象还在对象数据库里,只是暂时变成了“悬空”状态。
我立马切到本地之前拉过那个分支的机器上。因为 Git 2.45.0 现在对引用存储做了优化,用的是 Reftable 后端,查找历史引用的速度其实比以前快了不少,但我当时没空管那个,直接就是一顿操作。
首先,我得找到那个分支最后指向的那个 commit hash。
从 HEAD@{1} 往前推,我找到了那个分支最后一次提交的 SHA-1 值 3b9c7d2。
接下来就是关键的恢复步骤。我不能直接 git checkout 3b9c7d2,那样你是在“分离头指针”状态,不是分支。我需要重建那个分支指针。
但这还没完。我得确认那个新来的同事是不是在他本地也删了。果不其然,他本地也没了。我让他执行了同样的 reflog 操作,但他发现他的记录里没有那个 3b9c7d2,因为他当时是在 master 上执行删除操作的,并没有在那个分支上提交过新东西,所以他的 HEAD 历史里没有那个分支的详细移动轨迹。
这时候就得用另一个神器 git fsck(文件系统检查)。这个命令能扫描本地数据库中所有未被任何分支或标签引用的对象。
虽然 fsck 能找到,但在 1.2 万个提交的大仓库里,靠这个找简直是大海捞针。这也是为什么我强烈建议大家在做任何破坏性操作前,先建一个备份分支,或者至少确认 reflog 里有记录。
为什么这事儿这么严重?因为我们那个仓库用了 Partial Clone (部分克隆) 技术。当时为了优化 CI 流水线速度,我们配置了 git clone --filter=blob:none,本地只拉取了最近 7 天的 commit 历史。如果那个分支的最后一次提交超过了 7 天,且没有被其他分支引用,那么在 CI 的浅克隆环境里,那个 commit 对象可能压根就没下载下来,这就真的只能去服务器备份里找了。
那次事故后,我给团队定了个规矩:凡是 release/* 分支,删除权限收归运维,且删除前必须先在 Jira 里关联归档任务。另外,我们利用了 Git 2.45.0 新支持的 Built-in FSMonitor,配合 core.fsmonitor 配置,虽然主要是为了加速 git status(从原来的 800ms 降到 120ms 左右),但也顺带监控了 .git/refs 目录的变动,算是多了一层保险。
前阵子我们团队接手了一个遗留的单体巨石应用(Monorepo),里面塞了订单、商品、用户三个大模块,代码量大概 200 多万行。每次 git log 查看历史,那提交信息简直惨不忍睹,什么 "fix bug"、"update"、"改好了" 这种我见得太多了。这种信息对后续排查问题毫无帮助,尤其是当我们需要回溯为什么某个接口突然变慢的时候。
我现在的做法是不再硬憋那几句描述,而是直接把改动扔给 AI。我们现在用的是基于 Git 2.45.0 环境,配合 Copilot 或者本地的 Ollama 跑个小型模型来做这个事。
具体的操作流程是这样的:我改完代码,暂存之后,不直接写 git commit -m "xxx"。我会先跑一个脚本,这个脚本会提取我的暂存区改动,然后发送给 AI 模型,让它生成遵循 Conventional Commits 规范的 Message。
比如我刚改完一个库存扣减的逻辑,防止超卖:
以前我得自己想怎么描述,现在我会用下面这个脚本(或者直接在 IDE 里用插件):
运行这个脚本后,AI 给了我一个 fix(stock): 增加分布式锁防止高并发下库存超卖。这比我手写的好多了,因为它能精准地概括出我改动的意图,而不仅仅是描述代码动作。
再说说冲突解决。以前遇到 git merge 冲突,特别是那种改了同一个文件几十行的情况,我得一行行对着看。现在 AI 能帮上大忙,特别是针对 Reftable 后端带来的大规模引用场景,虽然冲突不多,但一旦涉及跨版本的合并就很头疼。
有一次我们要把 feature/new-payment 合并回 master。那个支付模块的文件 payment_processor.java 被两个人同时大改。
打开文件,冲突标记长这样:
以前我得手动判断是保留风控还是保留日志。现在我直接把这段冲突代码连同上下文一起丢给 AI,提示词是:“这段代码在合并时产生了冲突,HEAD 版本是增加日志,feature 版本是增加风控检查。请帮我合并,确保既保留风控逻辑,也保留日志,并优化代码结构。”
AI 给出的结果通常是这样的:
这种处理方式让我省去了很多上下文切换的时间。不过有一点要注意,AI 合并后的代码,我都会再跑一遍单元测试。毕竟 Git 的合并算法是基于文本行的,而 AI 是基于语义理解的。在 2024 年到 2026 年这个趋势里,我觉得 AI 辅助开发集成 会越来越深,以后可能 git merge 命令直接内置一个 --ai-resolve 参数,自动调用模型去解决这种非极端的冲突。
但我也发现,AI 生成的 Commit Message 有时候太“聪明”了,它会过度概括。比如我明明只是改了个配置文件的端口号,它给我生成个 refactor(config): 优化网络服务配置以提升可维护性。这种时候我就得手动改回去,别让后来的人看了摸不着头脑。所以,AI 是辅助,最终的审核权一定还是在人手里。
去年我接手了一个老牌电商后台的重构,业务复杂到离谱,十几个子系统硬塞在一个仓库里。当时为了提升构建速度,我脑子一热,直接上了 Git 2.45.0 的 Partial Clone 配合 Sparse Checkout。
刚开始确实爽,克隆代码从原来的十几分钟变成了几十秒。但问题很快就来了。团队里有个新同事,他的本地环境配置有点问题,在执行 git fetch 的时候因为只拉取了部分对象,导致他在做一个跨模块的大特性时,本地漏掉了一个公共库的提交。结果就是,他在本地跑得好好的,一提交到 CI 就报错,而且报错信息极其诡异,不是代码问题,而是 Git 提示找不到某个 blob。
我当时排查了半天才发现,是因为他本地的 promisor remote 配置没生效,导致部分历史对象缺失。最后我不得不让他强制 git fetch --unshallow 把整个仓库拉下来,才解决了这个问题。
经过这次折腾,我对这些高级特性有了更清醒的认识:
* Sparse Checkout 真的香,但有门槛。 如果你的团队还在用老版本的 Git(低于 2.25),或者大家对 Git 内部原理不太熟,千万别强推。否则你不是在写代码,你是在当 Git 客服。
* Monorepo 不是银弹。 很多团队其实没到那个量级,为了跟风搞 Monorepo 反而增加了工具链的负担。
* Rebase 虽好,别在公共分支用。 我现在的策略是:个人开发分支随便 rebase,但一旦涉及多人协作的分支,老老实实 merge,保持历史图的完整性比那点整洁更重要。
Git 这东西,别光看文档里那些酷炫的命令。我建议你先在本地搭个测试仓库,把 reflog、worktree 这些命令玩坏几次。特别是 git reflog,它真的是你的后悔药,但前提是你要知道怎么用它去“穿越”回那个还没搞砸的时间点。别等到代码丢了才去查怎么恢复,那时候心态崩了,手一抖可能就彻底没了。