不把手头的活儿塞进 stash,而是再开一张桌子
一句话总结
git worktree 可以让一个仓库(一个 .git)挂载多个工作目录。
提交、分支和对象全部共享,只有检出的位置不同。因此可以在不把手头工作推到任何地方的情况下,打开另一个分支。
为什么需要它
周五下午经常出现这样的事:你在功能分支上改了一半十二个文件,这时生产环境出了故障,需要往发布分支上打热修复。通常的做法是这样的。
git stash
git switch release
# 고치고 커밋하고 올리고
git switch feature
git stash pop
这个流程有三个陷阱。第一,git stash 默认不会收纳未跟踪的文件。新建的文件即使切换了分支也仍留在原地,结果混进了不相干的分支。第二,stash 是没有名字的栈,堆上两三个之后就分不清谁是谁,而且 stash pop 一旦冲突,就得当场处理恢复。第三,一切换分支,构建产物和依赖目录就整体失效。在大型项目里,这次重新构建耗费的时间比热修复本身还长。
也可以用 git clone 再建一个副本。这样上面三个问题都解决了,但要把整段历史再下载一份,而且两个副本的分支开始各走各的。一边创建的提交在另一边没有,最后两者之间还得再做 push 和 fetch。
git worktree 正好填补了这两者之间的空白:对象库只有一个,所以不用再下载历史;目录有两个,所以构建产物不会混在一起。
工作原理
执行 git worktree add <경로> <브랜치>(占位符依次为路径与分支名)会产生两样东西。
- 新目录中检出该分支,其中的
.git不是目录而是文件,内容只有一行:gitdir: /원래저장소/.git/worktrees/<이름>。 - 原仓库的
.git/worktrees/<이름>/下会生成只属于这个工作树的管理文件,例如HEAD、index、gitdir。
这里有一个重要事实:HEAD 和索引每个工作树各有一份,但分支引用(refs/heads/*)由整个仓库共享。 所以想在两个位置检出同一个分支时,git 会拒绝。
$ git worktree add /tmp/dup main
fatal: 'main' is already used by worktree at '/home/me/repo'
这不是不友好,而是保护。分支引用只有一个,工作树却有两个,那么一边提交的瞬间,另一边就会坐在一个不是自己创建的提交之上,索引和实际文件随之错位。如果确实想在两个位置查看同一个提交,可以用 --detach 不带分支地挂载。
清理分三种命令。
| 情况 | 命令 | 作用 |
|---|---|---|
| 工作结束,干净地收尾 | git worktree remove <경로> |
同时删除目录和管理文件 |
目录已经被 rm -rf 删掉 |
git worktree prune |
只删除残留的管理文件 |
| 像外接磁盘那样暂时看不见 | git worktree lock |
锁定,防止 prune 动它 |
只用 rm -rf 删掉目录的话,git worktree list 里那个位置会一直显示为 prunable。在这种状态下用同一个名字再次 add,会得到“已经存在”的提示。而且 prune 只删除管理文件——在那个位置创建的分支仍然保留,所以要连分支一起清理,得另外执行 git branch -d。
在现场相遇的样子
- 常驻挂载一个发布分支。热修复请求一来,只要
cd进那个目录就行,功能分支上改了一半的文件完全不用动。 - 做评审时,把同事的分支挂载到另一个位置,与自己的分支并排打开。两个编辑器窗口分别查看不同的分支,对比起来容易得多。
- 在一个位置跑耗时很长的测试时,在另一个位置继续工作。对象库是共享的,但索引各自独立,互不干扰。
- CI Runner 用多个分支同时构建同一个仓库时,也用 worktree 代替 clone。磁盘和网络只需承担一份历史。
也有要注意的地方。工作树中运行 git gc 时,为了保住其他工作树引用的对象,git 必须知道这些位置;但如果有位置被 rm -rf 删掉后没有 prune,这些信息就过期了。所以最好不要拖延清理。
下一项实验要做什么
挂载三个工作树,亲眼看看两次打开同一个分支时被拒绝的情形。保留未保存的修改不动,单独提交一个热修复;确认目录被 rm -rf 后会显示 prunable,以及 prune 之后分支依然保留。最后还会看到被锁定的工作树如何拒绝 remove。
官方文档是 git-worktree。