单一checkout的困境

我正在开发一个macOS菜单栏应用,有三个并行需求:消费趋势的走势图、原生通知和桌面小组件。这三个功能独立开发,都计划使用Claude Code实现。

问题在于:Claude Code需要在指定目录工作。而每个Git目录同一时间只能检出(checkout)一个分支。标准的git checkout就如同单车道环岛——每次只能通过一辆车。

如果想让三个功能并行开发,传统方案只有以下几种:

  1. 暂存乒乓:反复执行git stash保存更改→切换分支→git stash pop恢复→祈祷不要出现冲突…如此循环直到崩溃或退休,视哪个先到来。

  2. 克隆多个仓库:可行,但会产生三个完整的.git/目录副本、三个独立历史记录,以及三倍git fetch操作。资源浪费严重。

  3. 顺序开发:老老实实一个接一个开发。稳定可靠,但效率低得像手动执行归并排序。

这些方案都不理想。但Git其实自2015年起就内置了第四种解决方案——只是鲜为人知。

工作树(Worktree):现成的解决方案

Git工作树允许为同一仓库创建多个工作目录,无需克隆完整仓库。没有文件复制,没有魔法技巧。

类比说明:将Git仓库视作图书馆。原先你只有一张阅览桌,每次只能打开一本书。而工作树相当于增添多张阅览桌,每张桌子可打开不同的书,但所有书都来自同一个书库。

~/code/miapp/                    ← 主工作区(main分支)
     .git/                       ← 核心书库(唯一存在)

~/code/miapp-sparkline/          ← 工作树1(feature/sparkline分支)
     .git  ← 符号链接文件(指向主书库)

~/code/miapp-notificaciones/     ← 工作树2(feature/notifications分支)
     .git  ← 另一符号链接

每个目录都是完整的检出副本,包含所有文件。你可以在一个目录编译代码,在另一个目录运行测试,同时让AI编程代理在第三个目录工作——真正并行不悖。

一行命令创建工作树

在项目主目录执行:

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

就这么简单。两个新目录立即创建完成,各自对应独立分支,共享同一Git数据库。无需克隆操作,无需配置远程仓库,无需复制历史记录。

共享与隔离机制

关键在于理解:所有工作树完全共享Git仓库数据——包括提交历史、分支、标签、远程配置和钩子脚本。当你在sparkline工作树提交代码,notifications工作树可即时看到此次提交,无需执行fetch操作。

但以下内容是独立的:

  • 工作目录文件(每个工作树有自己的文件副本)
  • 暂存区(各自独立的git add状态)
  • HEAD指针(指向不同分支)

简言之:每个工作树的"当前工作状态"是私有的,其他所有仓库数据都是共享的。

与AI编程代理的协作流程

这才是工作树的精髓所在。通过工作树,你可以让多个AI编程代理真正并行工作:

# 终端1:在sparkline分支运行Claude Code
cd ~/code/miapp-sparkline
claude

# 终端2:在notifications分支运行Claude Code
cd ~/code/miapp-notificaciones
claude

# 终端3:保持主分支运行
cd ~/code/miapp
make run

每个Claude实例都在独立目录、独立分支下操作,拥有独立的.build/缓存目录。彼此互不干扰,无需竞争暂存区,更不需要反复执行stash操作。

由于它们共享Git数据库,当任一代理完成工作并执行push操作,其他工作树都能即时看到更新。

合并操作:与传统流程一致

工作树的合并操作与传统Git完全相同:

# 方案A:本地合并
cd ~/code/miapp
git merge feature/sparkline
git merge feature/notifications

# 方案B:PR流程(推荐)
cd ~/code/miapp-sparkline
git push -u origin feature/sparkline
# 然后在GitHub/Gitea创建PR并进行代码审查

工作完成后,清理操作也很简单:

git worktree remove ../miapp-sparkline
git branch -d feature/sparkline  # 如果分支已合并

注意事项

1. 分支独占性

同一分支不能同时在多个工作树检出。这是故意设计的限制,避免多个工作目录修改同一分支导致冲突。如需多次检出main分支,应创建临时分支。

2. 首次构建较慢

每个工作树都有独立的构建目录。首次构建需要完整重建,但后续构建会利用独立缓存——这正是相对传统checkout操作的优势所在(传统方式切换分支时常导致缓存失效)。

3. 非追踪文件处理

本地配置文件如.env.local、编辑器配置等未纳入Git管理的文件,不会自动复制到新建工作树。你需要手动复制或创建符号链接。

4. 共享状态问题

若应用程序会在~/Library/Application Support/等目录写入状态数据,从不同工作树运行的两个实例会竞争同一文件。这不是工作树本身的问题,而是多实例运行的通用限制。解决方案:避免同时运行,或者为每个构建配置独立数据目录。

5. 不要手动删除目录

如果直接用rm -rf删除工作树目录而非执行git worktree remove,Git会误认为该分支仍被占用。此时需运行git worktree prune清理孤儿引用。

6. 远程仓库无感知

工作树纯属本地概念。Gitea、GitHub等远程仓库完全感知不到工作树的存在,所有push操作都表现为标准Git流程。就像它们不关心你使用Vim还是VS Code编辑器一样——无关紧要。

最佳实践

命名规范:将工作树目录作为主仓库的同级目录,采用描述性后缀:

~/code/miapp/                    ← 主分支
~/code/miapp-sparkline/          ← 功能分支
~/code/miapp-notifications/      ← 功能分支
~/code/miapp-hotfix-login/       ← 热修复分支

这样通过ls ~/code/miapp*即可一览所有相关工作树。

按需创建工作树:只为真正需要并行的任务创建工作树。如果是顺序开发,使用常规分支切换即可。

及时清理:残留的工作树就像无人清理的过时分支——只会造成混乱。善用git worktree list查看活动工作树。

避免重复编辑:技术上虽然允许多个工作树修改同一文件,但合并时必然产生冲突。建议不同工作树专注于代码库的不同区域。

完整工作流示例

以下是我采用的标准化工作流程:

# 1. 为迭代周期创建对应工作树
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. 查看现存工作树
git worktree list

这个循环的核心是:创建→并行开发→提交PR→合并→清理。每个工作树的生命周期与其功能分支完全同步。

结论

Git工作树功能自2.5版本(2015年7月)就已存在。十余年来,大多数人仍在使用git stash这种2010年代的解决方案。

在AI编程代理时代,开发效率的瓶颈不再是代码编写速度,而是上下文切换的效率。工作树方案彻底消除了这种切换成本——你不再需要切换分支,只需切换工作目录。用cd命令替代checkout操作。

这本质上,才是我们应该始终坚持的最佳实践。


总结:git worktree add ../目录名 -b 分支名命令可创建共享同一仓库的多个工作目录。无需复制仓库,无需暂存操作,保持构建缓存有效——完美支持多个AI编程代理并行工作。完成后用git worktree remove清理即可。

---

*本文原文为西班牙语,借助AI翻译。*