单一checkout的瓶颈

我正在开发一个macOS菜单栏应用。我有三个功能在待办清单中:一个消耗量的迷你图表、原生通知和一个桌面小组件。这三个功能都是独立的。我打算用Claude Code来完成这三个功能。

问题是:Claude Code在一个目录中工作。一个目录有一个分支。而git checkout就像一个单车道的环岛:一次只能通过一个。

如果我想同时推进这三个功能,我的传统选择是:

  1. Stash乒乓球git stash,切换分支,工作,git stash pop,祈祷没有冲突。重复直到发疯或退休,看哪个先到。

  2. 克隆仓库三次:可以工作,但现在我有三个.git/副本,三个独立的历史记录,每个都要执行git fetch。浪费。

  3. 接受串行生活:一个功能接着一个功能。安全,可预测,但慢得像手动归并排序。

都不好。但有第四个选择,自2015年以来git就有了,但几乎没人使用。

Worktrees:你已经安装的解决方案

一个worktree是第二个工作目录,共享同一个.git仓库。没有副本,没有克隆,没有黑魔法。

比喻:你的仓库是一个图书馆。到目前为止你有一张桌子,只能打开一本书。worktree是放更多桌子。每张桌子都打开着不同的书,但都从同一个书架取书。

~/code/miapp/                    ← 桌子1 (main)
     .git/                       ← 图书馆(只有一个)

~/code/miapp-sparkline/          ← 桌子2 (feature/sparkline)
     .git  ← 文件,不是文件夹(指向图书馆的指针)

~/code/miapp-notificaciones/     ← 桌子3 (feature/notifications)
     .git  ← 另一个指针

每个目录都是一个完整的checkout,包含所有文件。你可以在一个中编译,在另一个中运行测试,在第三个中让你的AI助手工作。同时进行。

创建只需一行命令

从你的主仓库:

1
2
git worktree add ../miapp-sparkline -b feature/sparkline
git worktree add ../miapp-notificaciones -b feature/notifications

就这样。两个新目录,每个在自己的分支上,共享整个git数据库。不需要克隆,不需要配置远程,不需要复制历史记录。

它们共享什么,不共享什么

这很重要。worktrees共享整个仓库:提交、分支、标签、远程、钩子、配置。如果你在sparkline的worktree中做了一个提交,你可以立即从notifications的worktree中看到它,不需要fetch或任何操作,因为它们是同一个数据库。

它们共享:

  • 磁盘上的文件(每张桌子都有自己的工作副本)
  • 暂存区(每个都有自己的git add
  • HEAD(每个指向自己的分支)

简单来说:“我正在处理什么"的状态对每个worktree是私有的。其他一切都是公共的。

与编码助手的工作流程

这里就变得有趣了。有了worktrees,你可以真正让几个助手同时在同一个项目上工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 终端1:在sparkline中的Claude Code
cd ~/code/miapp-sparkline
claude

# 终端2:在通知中的Claude Code
cd ~/code/miapp-notificaciones
claude

# 终端3:main保持完整,应用运行中
cd ~/code/miapp
make run

每个Claude实例都有自己的目录、自己的分支、自己的.build/。它们不会冲突。不会竞争index。不需要stash任何东西。

由于它们共享git数据库,当其中一个助手完成并推送时,其他的就能看到那个分支。

合并:和往常完全一样

worktrees不会改变合并流程。它们是分离目录中的正常分支:

1
2
3
4
5
6
7
8
9
# 选项A:本地合并
cd ~/code/miapp
git merge feature/sparkline
git merge feature/notifications

# 选项B:PRs(通常的做法)
cd ~/code/miapp-sparkline
git push -u origin feature/sparkline
# 在GitHub/Gitea中创建PR,审查,合并

完成后,清理:

1
2
git worktree remove ../miapp-sparkline
git branch -d feature/sparkline  # 如果已经合并

没人告诉你的坑

1. 一个分支,一个worktree

你不能同时在两个worktrees中checkout main。这是设计如此:避免两个目录修改同一个HEAD并损坏它。如果你需要main的第二个checkout,创建一个临时分支。

2. 第一次构建是从头开始

每个worktree都有自己的构建目录。第一次编译会很慢。之后,每个worktree维护自己独立的缓存,这正是相对于传统git checkout的优势(每次切换分支都会使缓存失效)。

3. 未跟踪的本地文件

你的.env.local、编辑器配置、不在git中的文件…不会被复制到新的worktree中。你需要重新创建它们或制作符号链接。

4. 在磁盘上有共享状态的应用

如果你的应用写数据到~/Library/Application Support/或类似的地方,来自不同worktrees的两个应用实例会竞争同一个文件。这不是worktree的问题,而是运行同一应用的两个实例的问题。解决方案:不同时运行两个,或者为每个构建参数化数据目录。

5. 不要手动删除目录

如果你使用rm -rf删除worktree而不是使用git worktree remove,git仍然认为分支被占用。执行git worktree prune来清理孤立的引用。

6. 远程仓库什么都不知道

worktrees是100%本地的。Gitea、GitHub、GitLab…没有远程知道它们的存在。它们只看到正常的git push和正常的分支。就像问你的服务器是否有你使用Vim或VS Code的问题:它不知道,不会影响它。

最佳实践

命名约定:将worktrees放在原仓库的同级目录,带有描述性后缀:

~/code/miapp/                    ← main
~/code/miapp-sparkline/          ← feature
~/code/miapp-notifications/      ← feature
~/code/miapp-hotfix-login/       ← hotfix

这样ls ~/code/miapp*一下子就能看到所有的。

每个功能一个worktree,不是为了好玩:为真正要并行工作的任务创建worktrees。如果你要一个接一个地做事情,带有checkout的正常分支就足够了。

完成后清理:被遗弃的worktrees就像没人删除的分支——它们累积起来会造成混乱。git worktree list是你的朋友。

不要从两个worktrees编辑同一个文件:技术上你可以,每个都有自己的副本。但如果两个都修改同一个文件,合并时会有冲突。尝试让功能接触代码的不同区域。

完整工作流程建议

对于那些想要有序工作流程的人,这是我使用的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 1. 为sprint的功能创建worktrees
cd ~/code/miapp
git worktree add ../miapp-feat-a -b feature/feat-a
git worktree add ../miapp-feat-b -b feature/feat-b

# 2. 在每个中启动一个助手
cd ~/code/miapp-feat-a && claude    # 终端1
cd ~/code/miapp-feat-b && claude    # 终端2

# 3. 随着完成而合并
cd ~/code/miapp-feat-a
git push -u origin feature/feat-a   # 创建PR

# 4. 清理已合并的内容
git worktree remove ../miapp-feat-a
git branch -d feature/feat-a

# 5. 查看剩余的活动worktrees
git worktree list

循环是:创建 → 并行工作 → push/PR → 合并 → 清理。每个worktree的生命周期和功能的生命周期一样长,不多不少。

总结

worktrees从git 2.5版本(2015年7月)就有了。超过十年了。大多数人仍然在做git stash,就像我们还在2010年一样。

随着编码助手的到来,瓶颈不再是你写代码的速度——而是你切换上下文的速度。而worktrees完全消除了这种上下文切换:你不切换分支,你切换目录。cd而不是checkout

这终究是我们一直应该做的。


总结git worktree add ../名称 -b 分支在同一个仓库上创建第二个工作目录。没有副本,没有stash,不使缓存失效。完美适用于让多个编码助手并行工作。完成时用git worktree remove清理。