两次 clone 不等于 worktree —— 熟悉的陌生人最可怕。
8 月 18 日的 AI 课程叫《git 真用起来》。笑来老师讲,git 是一台有平行世界的时光机,创造平行世界有两种办法:一种叫 branch,大家都熟;另一种叫 worktree,更独立、更隔绝 —— 因为它连目录都换了。你可以在同一个项目的若干个互不干扰的目录里同时工作,需要合并的时候用 rebase 收回来。
课里举的例子是笑来老师自己写电子书阅读器:library view 拆成三块,book view 拆成四块,编辑器里开七个标签,每个标签挂一个 worktree,七个硅基劳动力同时干活。这边跑起来了,去看看那边;那边也在跑,再看看另一边 —— 七个里面总有要干的事。
听课的时候我心里一动。在此之前,我并不了解 worktree,但需求是真实存在的:ficus 要挂着最新开发分支 feat-v1.7.6 一路向前,历史版本的修复又得在 feat-v1.7.4 上做。于是有了 ficus 和 ficus-fix 两个目录,各挂各的分支,各干各的。我以为多个目录(多工作区)就是 worktree,我已经在用了。
不过我还是开了个会话,把两个目录都 @ 给 AI,问它:这两个目录现在是 git worktree 的方式使用吗?问的时候没有半点犹豫 —— 我真心以为自己在用它,也希望 AI 回答”是”。
AI 核实之后,回复的大意是:这两个目录不是真正的 git worktree,而是两个独立的完整克隆。
判断的依据很简单:真正的 worktree,它的 .git 是一个文件,内容只有一行,指向主仓库;而 ficus-fix/.git 是一个完整的目录。两边各自执行 git worktree list,都只看得到自己。它们唯一的交集,是共享同一个 origin。
两个独立克隆的实际影响:
- 跨目录不同步 —— 在 ficus-fix 里提交的修复,ficus 目录看不见,必须 push 之后再去 fetch;
- 没有分支互斥 —— 同一个分支可以被两边同时检出,各改各的,冲突留到未来某个措手不及的时刻爆炸;
- 对象库各存一份 —— 磁盘双倍,fetch 双倍,什么都双倍。
我甚至不知道 worktree 的存在,用第二个克隆满足了同样的需求,等真正遇到这个词时,就顺理成章地把它当成了自己已有的做法 —— 连核实都省了。以为自己知道,是比不知道更危险的状态:不知道的人会去查,以为自己知道的人,连查的动作都不会有。如果不是这次笑来老师的提醒,如果不是 AI 一句”这两个目录不是真正的 worktree”,我可能也就一直这么用着,多同步一次,费事点就费事点,困扰就困扰吧 —— 我以为这已经是最优解了。
事后想,这个误会几乎注定发生:多克隆和 worktree 从外面看几乎一模一样 —— 两个目录、两个分支、各干各的,日常用起来除了多一次 push 和 fetch,也没太大区别。区别全藏在 .git 里面:一个是完整仓库,一个是只有一行的链接文件。外观相同、机制不同的东西,最容易长成熟悉的陌生人。
既然知道用错了,就把它改过来。我说:需要使用 worktree,为我执行相关的操作。
AI 没有直接动手。它先做了一轮安全核查,确认删掉旧目录不会丢任何东西:
- 工作区干净,没有 stash;
- ficus-fix 里有 14 个本地分支,逐个和 origin 对比 —— 其中 feat-v1.6.0、feat-v1.7.5、feat-v1.7.x、master 这几个和 origin 不一致,需要分辨是「本地超前」(有未推送提交,删了就丢数据)还是「本地落后」(只是过时)—— 核查结果是全部 0 超前,不一致的仅仅因为过时;
- 2 个 tag 与 origin 完全一致。
结论:ficus-fix 里没有任何 origin 上不存在的提交,可以安全删除重建。
这一段是整个过程中最值得学的。删一个用了很久的目录之前,耐心把 14 个分支一个个和远程对账,把”会不会丢数据”证明清楚,而不是靠感觉说”应该没事”。这种枯燥的核查,正是人类绝对不会有耐心做完、而硅基劳动力毫不在意的活。
核查通过,执行就两步:删掉旧的独立克隆,从主仓库挂一个真正的 worktree:
1 | git worktree add /Users/airhead/WorkSpace/PolarData/ficus-fix feat-v1.7.4 |
验证结果:ficus-fix/.git 变成了链接文件,内容只有一行 gitdir: .../ficus/.git/worktrees/ficus-fix;两边执行 git worktree list 互相可见 —— 它们终于承认彼此属于同一个仓库;ficus 主目录的一切未受影响。转换完成时的状态:
1 | /Users/airhead/WorkSpace/PolarData/ficus 1dee48ef3 [feat-v1.7.6] ← 主 worktree |
真正挂上 worktree 之后,行为立刻不一样了。实时同步:在 ficus-fix 里提交的修复,ficus 目录立刻可见 —— 共享对象库,不需要再绕远程一圈。分支互斥:ficus 占着 feat-v1.7.6,ficus-fix 就不能再检出这个分支 —— 听起来像限制,其实正是 worktree 的价值:同一个分支永远不会被两个目录同时改,减小签错分支的可能性。
在 worktree 里切分支,和普通仓库没有任何区别:
1 | cd ficus-fix |
那句 git pull --ff-only 是 AI 特意提醒的:老版本分支常年不动,切过去时本地往往是落后的,git switch 不会自动更新 —— 不先快进,修复就可能打在过时的代码上。
最后,我把自己的使用模式说给它听,它写进了长期记忆:
ficus 只向前推进 —— 始终检出最新开发分支;ficus-fix 通常需要切换其他分支 —— 在各历史版本之间做修复。
两者的分工天然错开,以后的会话它会默认按这个模式工作:修复类任务先确认目标分支,落到 ficus-fix;新功能开发,跟随 ficus 的最新分支。这大概就是课程里说的”和自己协作”的另一种形态 —— 不仅 git 记得这个结构,AI 也记得。
写这篇文章的时候,我特意把 ficus-fix 已经切到 feat-v1.7.5 上。
回头看,这件事里有几个课程内容的直接印证。
worktree 是更独立、更隔绝的平行世界。 真正挂上之后才理解”隔绝”二字的准确:目录隔离、分支互斥 —— 但对象库共享。隔绝的是工作现场,共享的是历史。这个设计比我之前”两个克隆”的伪平行世界高明得多。
以前最有经验的工程师掌握的技巧,也不见得比 AI 强。 判断”两个克隆还是 worktree”只需要看 .git 是文件还是目录。课程说 AI 能把 git 的潜力开发到几乎 100%,我还缺很多。
花更多的时间做计划,而不是操作。 这件事里我花时间的部分,是决定”ficus 向前推进、ficus-fix 来回切换”这个分工 —— 这是 what 和 why;至于怎么核查、怎么转换、怎么验证,全部是 how,交给硅基劳动力就可以了。
熟悉的陌生人,要一个一个抓出来。 我在 worktree 摔的这一跤,路径有点绕:先是不了解它,用”再克隆一份”满足了多工作区的需求;听到这个词的时候,又把自己已有的做法直接映射了过去 —— 我以为多个目录(多个工作区)就是 worktree。还好 AI 核查的成本很低,让我了解我用错了。往后每学一个新词,除了问它是什么、怎么用,值得再问一句:帮我去现有的环境里核实一下,我是不是真的在用它。
课程最后说:你一定要用上 git 的 worktree 功能,这样的话,你就算是把这个工具用透了,比绝大多数人强。
现在,我的项目里,ficus 和 ficus-fix 终于真正是同一个仓库的两个平行世界了。