12k
All articles

Git Worktree 详解:何时使用、为何使用

解析 Git worktree:与 stash 和第二个克隆的区别、适用场景,以及管理它们的常用命令。

OpenReplay Team
OpenReplay Team
Git Worktree 详解:何时使用、为何使用

Git worktree(工作树)是附加在同一个仓库上的额外工作目录,拥有自己独立检出的分支。这样你就能在两个文件夹中同时打开两个分支,而无需克隆任何东西。

大多数开发者都是在某次抓狂的过程中才意识到需要它:stash 被应用到了错误的分支上、切换分支后语言服务器卡在重新索引里、或者同一个仓库的第二个克隆副本始终无法保持同步。Worktree 能解决这三个问题,而且它自 Git 2.5(2015 年 7 月) 起就已内置,无需安装任何东西。

本文将介绍 worktree 在磁盘上究竟是什么、驱动这一特性的三条命令、它何时优于 stash、何时优于第二个克隆,以及使用它需要付出的代价。

核心要点

  • Git worktree 是附加在同一仓库上的第二个工作目录,拥有自己检出的分支;这个链接目录中的 .git 是一个指回主仓库的小文件,而非完整的 .git 文件夹。
  • 一条 git worktree add -b hotfix-bug ../hotfix-workspace main 就能取代整套 stash、checkout、建分支、修复、stash-pop 的流程。
  • 所有 worktree 共享同一个对象数据库和同一套 refs,因此一次 git fetch 就能更新所有 worktree,磁盘上也不会重复存储历史记录。
  • Git 跟踪的是已提交的文件,而非你的环境:已安装的依赖、.env 文件和构建缓存都会留在原目录中。
  • 默认情况下,Git 不允许同一个分支在两个 worktree 中同时检出;当你只需要构建或测试某个提交时,可以用 git worktree add -d 创建一个处于 detached-HEAD 状态的 worktree。

Git Worktree 在磁盘上究竟是什么?

链接式 worktree 只是一个存放已检出文件的目录,而不是仓库的副本。它顶层的 .git 是一个普通文件而非目录,指向主仓库 .git/worktrees/ 下的一个小型元数据文件夹——git worktree 官方文档 对这一布局有完整说明。所有笨重的部分都只保留一份:一个对象数据库、一份历史记录和一套 refs 服务于所有 worktree,只有寥寥几个文件属于单个 worktree,其中包括 HEAD 和索引(index)。

这一点足以解释 worktree 的其他所有特性。创建一个 worktree 的成本相当于一次 checkout,而不是一次 clone。在任何一个 worktree 中所做的提交,在其他所有 worktree 中都立即可见,因为底层只有一个仓库。

Worktree 取代了怎样的工作流?

没有 worktree 时,紧急插入的任务意味着必须挂起当前状态。下面是我们熟悉的流程,使用 git stash push 形式 以便消息能真正附加上去:

git stash push -m "wip: login form"
git checkout main
git checkout -b hotfix-bug
# fix, commit, push, wait for the merge
git checkout feature-login
git stash pop

五条命令、两次上下文切换,还有一次把 stash 弹到错误分支上的机会。而 worktree 版本只需一条命令:

git worktree add -b hotfix-bug ../hotfix-workspace main

这会创建一个同级文件夹,基于 main 创建 hotfix-bug 分支,并在该文件夹中将其检出。你原本的目录丝毫未变:还是那个分支、那些修改过的文件、那个打开着的编辑器。修复合并之后,git worktree remove ../hotfix-workspace 会删除该文件夹并注销它。

三条命令:add、list、remove

日常使用 worktree 只需三个子命令。git worktree add <path> <branch> 会在新路径上检出一个已有分支;在路径前加上 -b <new-branch> 则可以顺便创建该分支。将 add 指向一个尚不存在的路径,Git 会为你创建该目录。

git worktree list 会列出所有 worktree,主 worktree 排在最前,显示其路径、缩写的 commit 哈希,以及方括号中已检出的分支:

/home/you/project             abc1234 [feature-login]
/home/you/hotfix-workspace    def5678 [hotfix-bug]

git worktree remove <path> 用于删除一个 worktree,而 Git 的删除规则 相当严格:只要有一个未跟踪文件或一个被修改的已跟踪文件,命令就会中止,除非你加上 --force,但那会丢弃这些改动。Git 完全不允许删除主 worktree,而被锁定的 worktree 则需要两次 --force。如果你直接手动删除了 worktree 文件夹,可以用 git worktree prune 清理它遗留的元数据。

Worktree 何时优于 Stash?

Stash 会挂起你的工作,而 worktree 让它继续运行。这个区别决定了你该选哪一个。如果只是把两行改动搁置十分钟,stash 完全够用。但当你目录中的工作仍在运行时,它就是错误的工具:正在跑的测试套件或长时间构建、正在监听文件变化的开发服务器,或者一个切换分支后语言服务器会重新索引整个项目的编辑器。

使用 worktree,这些状态都不会被打扰,因为你根本不会碰原目录。你只需在第二个编辑器窗口中打开新文件夹,在那里处理插入的任务,回来时一切都还在原处。正是这种隔离性,使得那些并行运行 AI 编码代理的工具将 worktree 作为其工作模型。如果想看更主观的观点,matklad 的文章 描述了一位开发者固定使用五个 worktree、并将它们映射到并发的「活动」而非分支上;不过这只是个人配置,不必当成通用实践。

选 Worktree 还是第二个克隆?

只要两个目录都需要跟踪同一个仓库,worktree 就优于第二个克隆。由于所有 worktree 共享一个对象存储和一套 refs,一次 git fetch 就能更新全部,从一个 worktree 推送的分支在其他 worktree 中立即可见,而且新增 worktree 不会在磁盘上重复任何历史记录。第二个克隆 则完全不具备这些优势:两个对象存储都要各自 fetch、两套 refs 会逐渐脱节,磁盘占用还翻倍。

在两种情况下克隆仍然是正确选择:当这份工作确实属于另一个远端仓库时(比如你单独交互的某个 fork),或者当你想要一个与主仓库完全隔离的一次性实验环境、连共享 refs 都不希望有时。

StashWorktree第二个克隆
工作可继续运行
创建成本瞬时一次 checkout完整克隆
磁盘上的历史记录共享共享重复
一次 fetch 即可更新

Worktree 的代价是什么?

Git 跟踪的是已提交的文件,而非你的环境。这是每个新 worktree 的主要开销:

  • 已安装的依赖不会跟着走。一个 npm 或 pip 项目的全新 worktree 通常需要先执行自己的安装步骤才能构建。
  • 未跟踪的本地文件会留在原地:.env 文件、本地配置和构建缓存都留在原目录中,因为 Git 对它们毫无记录。
  • 在仓库内部创建的 worktree 文件夹会显示为未跟踪的杂物,需要加入 .gitignore。使用同级目录(../hotfix-workspace)则可以完全避免这个问题。
  • 默认情况下,一个分支同一时间只能在一个 worktree 中被检出。虽然存在绕过方式(add--forceswitch--ignore-other-worktrees),但如果你只是需要构建或测试某个提交,更干净的做法是使用 detached-HEAD worktree:
git worktree add -d ../build-check 1a2b3c4

-d 标志让新文件夹中的 HEAD 处于分离状态,因此不会占用任何分支,上述限制也就不会触发。在已有的 worktree 中执行 git switch -d <commit> 也能达到同样效果。

你手头同时进行的事够多吗?

判断是否该采用它的测试很简单。当你手头还有真正未提交的工作时,是否经常被打断?切换分支是否会触发你能明显感觉到的依赖重装或语言服务器重新索引?你是否已经在维护同一仓库的第二个克隆?只要有两个「是」,worktree 在第一周就能回本。一个「是」都没有,那么 stash 加分支其实完全够用。不妨从一个开始:下次在开发功能途中遇到紧急修复时,用 git worktree add 代替 git stash,等修复合并后再删除该文件夹。

常见问题

Git worktree 能用于包含子模块(submodule)的仓库吗?

部分可以。Git 官方手册在 BUGS 说明中标注子模块支持尚未完成,并建议不要同时在多个位置检出同一个超级项目(superproject)。实际使用中,内部含有子模块的链接式 worktree 无法用 git worktree move 迁移,而用 git worktree remove 删除它时需要加上 force 标志。如果你的仓库高度依赖子模块,第二个克隆是更安全的选择。

stash 和分支会在各个 git worktree 之间共享吗?

会。refs/ 下的所有内容对每个 worktree 都是共享的,因此无论你身处哪个 worktree,分支、标签和 stash 看起来都一样。有三个命名空间例外,它们只属于单个 worktree:refs/bisect、refs/worktree 和 refs/rewritten。除此之外,只有 worktree 自身的指针是私有的,其中包括 HEAD 和它的索引。因此,你在一个 worktree 中创建的 stash,可以在同一仓库的任何其他 worktree 中被列出并弹出。

创建 worktree 之后还能把它移动到别的文件夹吗?

可以。把 worktree 及你希望它所在的路径传给 git worktree move,Git 会一次性移动文件夹并重写其元数据。有两种情况它无法处理:主 worktree,以及任何内部含有子模块的链接式 worktree。被锁定的 worktree 需要两次 force 标志。如果你已经自己把文件夹拖到了别处,git worktree repair 可以重新建立连接。

如果我执行 git worktree add 时只给了路径、没有指定分支会怎样?

Git 会把路径的最后一段作为分支名,从 HEAD 开始创建该分支,并在新文件夹中检出。因此 git worktree add ../hotfix 会给你一个名为 hotfix 的分支,检出在 ../hotfix。如果该名称已被占用,Git 会转而检出这个已有分支——前提是当前没有其他 worktree 正持有它。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.