单一checkout的困境
我正在开发一个macOS菜单栏应用,有三个并行需求:消费趋势的走势图、原生通知和桌面小组件。这三个功能独立开发,都计划使用Claude Code实现。
问题在于:Claude Code需要在指定目录工作。而每个Git目录同一时间只能检出(checkout)一个分支。标准的git checkout就如同单车道环岛——每次只能通过一辆车。
如果想让三个功能并行开发,传统方案只有以下几种:
暂存乒乓:反复执行
git stash保存更改→切换分支→git stash pop恢复→祈祷不要出现冲突…如此循环直到崩溃或退休,视哪个先到来。克隆多个仓库:可行,但会产生三个完整的
.git/目录副本、三个独立历史记录,以及三倍git fetch操作。资源浪费严重。顺序开发:老老实实一个接一个开发。稳定可靠,但效率低得像手动执行归并排序。
这些方案都不理想。但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翻译。*