worktree 解决的是「两件事同时改一个仓库」,不是「备份」也不是「多版本」。 三棵树共享一份历史,主树零改动;改同一行照样冲突,但冲突比互相覆盖好。 异步协作的判据:对方隔八小时才看到,能不能只靠你留下的东西接着干。
上一篇把配置分清了。这一篇解决「同时」这个字。
一个人的时候,切换任务最省事的办法是 git stash;
两个人的时候,stash 就不够用了——你不能把别人的工作也 stash 掉。
Demo 真建三棵工作树:主树、feat-a、feat-b。
在 wt1 改文件,主树一行不动;两棵树改同一行,合并时冲突。
用的是真 git worktree,列出三棵树的命令输出也在里面。
一、worktree 解决的到底是什么
先排除三种误解。
| 以为是 | 实际不是 |
|---|---|
| 多版本备份 | 它不备份,删了树就没了 |
| 多分支同时可见 | 一次只看一棵,靠切目录 |
| 替代 stash | stash 是暂存,它是并行 |
它真正解决的是「同一份历史的多个工作区」。
三棵树共享一个 .git,所以提交历史、分支、tag 都是同一套——而文件目录是分开的。
这跟「复制一份目录」的差别在这里。
复制出来的目录不共享历史,你在副本里提交,主目录看不到;合并要手工搬文件。
worktree 出来的树共享历史,git log 是同一份,合并就是 git merge。
跟 stash 的差别更明显。
stash 是「把我现在的东西收起来」,它假设只有一个人在改。
两个人同时改的时候,stash 会互相覆盖——而且它不报错,你以为收起来了,其实收的是别人的。
二、一个仓库三棵树
Demo 里的三棵树:主树跟主干,wt1 在 feat-a,wt2 在 feat-b。
git worktree list 一次列全,每条带路径、commit 和分支名。
在 wt1 里把版本号改成 1.1.0,主树的 app.py 还是 1.0.0。
这不是「没同步」,这是设计如此——它们本来就是两个工作区。
然后两棵树改同一行,合并时冲突:CONFLICT (content): Merge conflict in app.py。
注意冲突不是 worktree 带来的,是「两棵树改了同一行」带来的。
worktree 只是让这个冲突在合并那一刻显形——而显形比互相覆盖好得多。
所以三棵树的实际用途是这样的:
主树跟主干,随时能发版;任务树一个任务一棵,做完就扔;实验树试不确定的方案。
第二行那个「做完就扔」要真扔。
留着十棵做完的树,下次你自己都分不清哪棵是哪棵——而且它们还占着分支名。
三、异步协作的信息流
| 环节 | 留下什么 | 给谁看 |
|---|---|---|
| 派活 | 一份任务描述 | 执行的人 |
| 执行 | 一棵独立的工作树 | — |
| 交付 | 一个分支或 PR | 审查的人 |
| 合并 | 一条提交信息 | 后来的人 |
四步里最容易省的是第一步。 「你自己看着办」在同步协作里还能靠口头补,异步协作里就断了。 为什么异步更依赖第一步? 因为同步时你有机会追问,异步时你追问要等八小时。 一句「看着办」在同步里成本是五分钟,在异步里成本是一天。 所以异步协作的判据可以写得很具体: 对方在另一个时区,隔八小时才看到你的消息,他能不能只靠你留下的东西接着干。 能,交接成功;不能,缺的那句话就是你的活。 第二行那个「一棵独立的工作树」也有讲究。 它是「不打扰别人」的具体实现——你在自己的树里怎么改都行,别人的树和主树都不受影响。 这比「大家都在主目录里,靠自律不冲突」可靠得多。
四、动手 Demo:Worktree:一个仓库三棵树,互不干扰
真建三棵工作树:主树、feat-a、feat-b。先验证主树零改动,再让两棵树改同一行看冲突。
git worktree add、list、merge 都是真跑的,中间那些 commit 也是真提交。
repo = os.path.join(root, "repo")
gitinit(repo) # 先有一次提交
git(repo, "worktree", "add", wt1, "-b", "feat-a")
git(repo, "worktree", "add", wt2, "-b", "feat-b")
put(wt1, "app.py", "VERSION = '1.1.0'\nprint('A', VERSION)\n")
print(get(repo, "app.py").split("\n")[0]) # 主树仍是 1.0.0
git(repo, "merge", "feat-a", "--no-edit")
c, o = git(repo, "merge", "feat-b", "--no-edit")
print("冲突" if "CONFLICT" in o else "干净合并")
实跑输出:
--------------------------------------------------------------------
Worktree:一个仓库三棵树,互不干扰
--------------------------------------------------------------------
团队化之后最先撞上的不是流程,是「两个人同时改同一个仓库」。
① 建两棵工作树
--------------------------------------------------------------------
I:/Desktop/自我教学/.workbuddy/scripts/ch50_dev/_tmp50/r13wt/repo b988447 [main]
I:/Desktop/自我教学/.workbuddy/scripts/ch50_dev/_tmp50/r13wt/wt1 b988447 [feat-a]
I:/Desktop/自我教学/.workbuddy/scripts/ch50_dev/_tmp50/r13wt/wt2 b988447 [feat-b]
三棵树:主树在 feat 分支之外,wt1 在 feat-a,wt2 在 feat-b。
② 在 wt1 里改,主树零改动
--------------------------------------------------------------------
| 位置 | 内容 |
|---|---|
| 主树 app.py | VERSION = '1.0.0' |
| wt1 app.py | VERSION = '1.1.0' |
主树一行没动。这就是 worktree 跟「复制一份目录」的区别:复制出来的目录不共享历史,合并要手工搬。
③ 两棵树改同一行,合并时冲突
--------------------------------------------------------------------
合并 feat-b 的结果:冲突
CONFLICT (content): Merge conflict in app.py
! 冲突不是 worktree 带来的,是「两棵树改了同一行」带来的。
worktree 只是让这个冲突在合并时显形——而显形比互相覆盖好。
④ 异步协作的信息流
--------------------------------------------------------------------
| 环节 | 留下什么 | 给谁看 |
|---|---|---|
| 派活 | 一份任务描述 | 执行的人 |
| 执行 | 一棵独立的工作树 | — |
| 交付 | 一个分支或 PR | 审查的人 |
| 合并 | 一条提交信息 | 后来的人 |
四步里最容易省的是第一步。「你自己看着办」在同步协作里还能靠口头补,异步协作里就断了。
! 异步协作的判据:对方在另一个时区、隔八小时才看到你的消息,他能不能只靠你留下的东西接着干。
能,就算交接成功。
⑤ 三棵树各自的用途
--------------------------------------------------------------------
| 树 | 用来干什么 | 能不能长期留着 |
|---|---|---|
| 主树 | 跟着主干走,随时能发版 | 能 |
| 任务树 | 一个任务一棵,做完就扔 | 不能 |
| 实验树 | 试一个不确定的方案 | 不建议 |
第二行那个「做完就扔」要真扔。留着十棵做完的树,下次你自己都分不清哪棵是哪棵。
worktree list 那三行要看清楚:三棵树,commit 哈希一样,分支名不同。
哈希一样说明它们指向同一个历史起点——这就是「共享历史」的具体样子。
第二段那张两行的表是全篇的关键:主树 1.0.0,wt1 是 1.1.0。
这一行就是「隔离」的证据。
注意它跟「备份」的区别:备份是同一份内容的两份拷贝,这里是同一份历史的两份内容。
冲突那段只有一行输出,但它值得停一下。
CONFLICT (content): Merge conflict in app.py 这句话的意思是「我拦住了」。
如果没有 worktree,两拨人改同一个文件,常见结局是后保存的覆盖前面的——而覆盖不报错。
最后那句「做完就扔」不是随口说的。
worktree 的成本是会累积的:每个任务树占一个分支名、一份工作目录。
不清理,三个月后你自己都说不清哪棵是哪棵。
30 秒实验
- 在你不重要的仓库上试一次
git worktree add。 建完在两边各改一个文件,看主树动没动。 - 数一下你现在有几个分支是「做完但没删」的。 它们和没清理的 worktree 是同一类垃圾。
- 写一份任务描述给一个假设隔八小时才看到的人。 写完检查一遍:里面有没有「你自己看着办」。
常见误区
- 「worktree 就是多克隆一份。」
克隆不共享历史,合并要手工搬。
worktree 共享同一个
.git。 - 「有了 worktree 就不会冲突了。」 两棵树改同一行照样冲突。 它保证的是「冲突会显形」,不是「没有冲突」。
- 「异步协作靠自觉。」 自觉在同步时够用,异步时不够。 判据是「对方隔八小时能不能接着干」,不是「我们关系好」。
自测三问(点开对答案)
1. worktree 和「复制一份目录」的差别在哪? —— **共享历史。** **worktree 出来的树在同一个 `.git` 下,合并就是 `git merge`;复制出来的要手工搬文件。** 2. 为什么说冲突「显形比覆盖好」? —— **因为覆盖不报错。** **两拨人改同一个文件,后保存的盖掉前面的,你到下周才发现。** 3. 异步协作的判据是什么? —— **对方隔八小时才看到你的消息,能不能只靠你留下的东西接着干。** **能,交接成功;不能,缺的那句话就是你的活。**最后一篇把整章收成四步:先一个人,再两个人,再三人小组,最后全组。
- ☐ (本篇完,下一篇 13.4 小团队试点的落地顺序)