文章

【笔记】Git Worktree 的共享存储与隔离边界

【笔记】Git Worktree 的共享存储与隔离边界

git worktree 让同一个仓库同时拥有多个工作目录。它不是复制仓库,也不是创建轻量容器,而是把 Git 状态拆成“仓库共有”和“工作树私有”两部分。

本文依据 Git 官方 git-worktree 与 repository layout 文档,并在 Apple Git 2.39.5 上做最小实验。命令选项应以当前安装版本为准。

1. 中心模型:历史共享,现场隔离

一句话概括 worktree:

1
2
共享:对象、绝大多数 refs、仓库配置、hooks
隔离:工作目录、HEAD、index、HEAD reflog、进行中的操作状态

共享部分回答“这个仓库里有什么历史”;隔离部分回答“这个工作树此刻检出了什么、准备提交什么、正在执行什么操作”。

flowchart TB
    C[Common Git Directory]
    C --> O[objects]
    C --> R[共享 refs 与分支 reflog]
    C --> CFG[config / hooks]
    C --> A[主工作树私有状态]
    C --> W[worktrees/feature 私有状态]
    A --> A1[HEAD / index / logs/HEAD]
    W --> W1[HEAD / index / logs/HEAD]
    A1 --> WT1[主工作目录]
    W1 --> WT2[linked worktree 目录]

这个模型同时解释了两个现象:新 worktree 几乎不增加对象存储成本,但它的未提交修改和暂存区不会自动出现在其他 worktree。

2. 三个目录角色

假设主工作树是 /repo/main,linked worktree 是 /repo/feature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/repo/main/
├── .git/                         # common dir,也是主工作树 git dir
│   ├── objects/
│   ├── refs/
│   ├── config
│   ├── HEAD                      # 主工作树私有
│   ├── index                     # 主工作树私有
│   └── worktrees/
│       └── feature/
│           ├── HEAD              # linked worktree 私有
│           ├── index
│           ├── logs/HEAD
│           ├── commondir
│           └── gitdir
└── ...

/repo/feature/
├── .git                          # 文本文件,不是目录
└── ...

2.1 工作目录

工作目录保存检出的普通文件。不同 worktree 可以同时呈现不同 commit 或分支的文件树。

2.2 $GIT_DIR

$GIT_DIR 是当前工作树的管理目录。linked worktree 中,它通常指向:

1
/repo/main/.git/worktrees/feature

HEADindexlogs/HEAD 等路径相对这里解析,因此每个 worktree 各有一份。

2.3 $GIT_COMMON_DIR

$GIT_COMMON_DIR 指向共享管理目录,通常就是主仓库的 .git。对象库、绝大多数 refs、仓库配置和 hooks 从这里读取。

主工作树中 $GIT_DIR$GIT_COMMON_DIR 通常是同一个目录;linked worktree 中二者才分开。

3. 两跳寻址:gitfile 与 commondir

3.1 第一跳:.git 找到私有管理目录

linked worktree 根目录的 .git 是一个 gitfile:

1
gitdir: /repo/main/.git/worktrees/feature

Git 读取它后得到当前 $GIT_DIR。所以不能假设 .git 总是目录,也不要用普通文件操作硬编码 .git/index

3.2 第二跳:commondir 找到共享管理目录

私有管理目录中的 commondir 通常写着:

1
../..

它相对 $GIT_DIR 指回主仓库 .git,形成 $GIT_COMMON_DIR

3.3 gitdir 是反向联系

私有管理目录中还有名为 gitdir 的文件,内容指向 linked worktree 的 .git 文件。它让管理端知道对应工作目录在哪里,供 list、prune、move、repair 等生命周期操作检查和修复联系。

把关系画成双向图更直观:

1
2
3
4
5
6
7
8
linked/.git
    └── gitdir: .../.git/worktrees/<id>    # 工作树 → 私有管理目录

.git/worktrees/<id>/gitdir
    └── /path/to/linked/.git               # 私有管理目录 → 工作树

.git/worktrees/<id>/commondir
    └── ../..                               # 私有 → 共享管理目录

4. 哪些状态真正共享

4.1 对象库共享

commit、tree、blob 和 tag object 都在 common dir 的 objects/。任一 worktree 创建的新 commit,其他 worktree 立刻可以按对象 ID 读取,不需要复制或同步对象。

4.2 绝大多数 refs 共享

分支和 tag 通常位于 common dir 的 refs/packed-refs。在 worktree A 更新 refs/heads/feature 后,worktree B 下一次读取该 ref 就能看到新值。

但“所有 refs 都共享”并不准确。官方规则是:

  • HEAD 等直接放在 $GIT_DIR 下的 pseudo refs 通常是每 worktree 私有;
  • refs/ 下通常共享;
  • refs/bisectrefs/worktreerefs/rewritten 是例外,按 worktree 隔离。

4.3 分支 reflog 与 HEAD reflog 不同

共享分支的 reflog 位于 common dir,例如 logs/refs/heads/feature;某个工作树自身 HEAD 的移动轨迹位于它的 $GIT_DIR/logs/HEAD

因此,不能笼统地说整个 logs/ 共享或整个 logs/ 隔离。判断标准仍是:记录属于共享 ref,还是当前工作树的 HEAD。

4.4 配置默认共享,也可以启用 worktree 配置

仓库级 .git/config 默认共享。Git 也支持 extensions.worktreeConfig,启用后把特定配置放进 config.worktree,按工作树覆盖。故“config 一定全部共享”同样只是默认模型,不是完整规则。

5. 哪些状态必须隔离

5.1 HEAD

每个 worktree 可以检出不同分支或 detached commit,因此各自需要独立 HEAD

1
ref: refs/heads/feature

或:

1
<commit-id>

5.2 index

index 是工作目录与下一次 commit 之间的暂存快照。两个工作树可以有完全不同的文件内容和暂存选择,所以必须拥有不同 index。

这意味着 git add 只改变当前 worktree 的暂存区;它不会把另一个 worktree 的同名文件也加入暂存。

5.3 进行中的操作状态

merge、rebase、cherry-pick、bisect 等流程依赖当前工作树的状态文件和 refs。它们原则上随 worktree 隔离,否则一个工作树的中间状态会覆盖另一个。

具体文件属于实现布局,脚本应优先使用 git rev-parse --git-path <path> 定位,不要自己拼接主仓库 .git 路径。

6. 为什么同一分支默认不能检出两次

对象和分支 ref 是共享的,但 index 与工作目录各自独立。若两个 worktree 同时检出并提交同一分支:

  1. A 的 index 基于旧分支头准备提交;
  2. B 更新共享分支 ref;
  3. A 的工作目录和 index 不会随之同步;
  4. A 再提交就可能在意料之外移动同一个 ref。

所以 Git 默认检查其他 worktree 的 HEAD,发现目标分支已被占用就拒绝 checkout:

1
fatal: 'feature' is already checked out at '/path/to/feature'

这是一道一致性护栏,不是文件系统做不到。强制选项可以绕过部分保护,但不会让两个 index 自动协调。

detached HEAD 不占用分支名,因此多个 worktree 可以指向同一 commit。共享的是起点对象,不是后续的分支更新目标。

7. git worktree add 做了什么

简化后的创建过程是:

  1. 解析目标路径、commit-ish 和新分支选项;
  2. 检查目标分支是否已被其他 worktree 占用;
  3. 在 common dir 的 worktrees/<id>/ 建立私有管理记录;
  4. 在新工作目录写入 .git gitfile;
  5. 写入双方指针 gitdircommondir
  6. 创建私有 HEAD 和 index,并从共享对象库填充工作目录。

<id> 通常从目标目录 basename 派生,冲突时可能追加数字。它是内部管理标识,不应当成稳定业务 ID,也不保证等于分支名。

8. 生命周期:lock、move、remove、prune、repair

8.1 lock

git worktree lock 主要用于工作目录暂时不可访问的情况,例如可移动磁盘或未挂载的网络盘。锁定会阻止管理记录被 prune;当前 Git 也会阻止普通 move/remove,除非解锁或使用相应强制选项。

它不是编辑锁,不阻止进程修改工作区文件,也不替代并发协作规则。

8.2 move

git worktree move 移动 linked worktree,并同步更新双向路径关系。直接用文件系统移动目录可能让工作树 .git 与管理记录彼此失联。

主工作树不能用这个命令移动;包含 submodule 等情况也可能受限制,应以命令报错为准。

8.3 remove

git worktree remove 同时移除 linked 工作目录和相应管理记录。默认只移除干净 worktree;未跟踪文件、已修改文件、submodule 或 lock 会要求处理状态或显式强制。

8.4 prune

如果用户绕过 Git 直接删除工作目录,common dir 中仍会残留 worktrees/<id>git worktree prune 检查反向 gitdir 指向的位置,按过期策略删除失效管理记录。

先用 dry-run 查看更安全:

1
git worktree prune -n -v

被 lock 的记录不会按普通失联 worktree 清理。

8.5 repair

主仓库或 linked worktree 被手工移动后,双方记录可能仍指向旧路径。git worktree repair [<path>...] 用于重新建立这些联系。它修复管理路径,不会替用户恢复已经丢失的工作区内容。

9. Worktree 不是哪些东西

9.1 不是 clone

clone 拥有独立对象库、refs 和配置;worktree 共享同一个仓库身份。删除共享对象或重写共享 refs 会影响所有 worktree。

9.2 不是 submodule

submodule 是另一个仓库,只是在父仓库中记录一个 gitlink commit。它可能也用 .git 文件指向父仓库 .git/modules/,但不会通过 worktree 的 commondir 共享父仓库 refs。

9.3 不是进程或文件隔离

不同 worktree 的普通文件目录不同,但它们仍可能共享数据库、端口、缓存、环境变量和外部服务。并行开发时仍要为运行时资源做单独隔离。

9.4 不是提交隔离

新 commit 对象和分支 ref 都进入共享仓库。某 worktree 创建或删除分支,其他 worktree 立刻可见;只是各自 HEAD 和 index 不会被自动切换。

10. 排障时的最小命令集

先让 Git 告诉我们路径,不要猜:

1
2
3
4
5
6
git rev-parse --show-toplevel
git rev-parse --absolute-git-dir
git rev-parse --git-common-dir
git rev-parse --git-path HEAD
git rev-parse --git-path index
git worktree list --porcelain

判断问题时按三层检查:

  1. 工作目录是否仍存在、是否干净;
  2. .git gitfile 与 worktrees/<id>/gitdir 是否互相指向;
  3. commondir 是否仍能定位共享仓库。

若只是路径移动造成联系断裂,优先 git worktree repair;若工作目录已确认消失,先 dry-run 再 prune;不要直接手删 .git/worktrees/ 下的记录。

11. 复习索引

  1. $GIT_DIR 表示当前 worktree 的私有管理目录;
  2. $GIT_COMMON_DIR 表示所有 worktree 共用的仓库管理目录;
  3. gitfile 从工作目录指向私有目录,commondir 再指向共享目录;
  4. 对象和大部分 refs 共享,HEAD、index 与操作现场隔离;
  5. 同分支 checkout 保护是为防止两个独立 index 竞争同一个共享 ref;
  6. lock 防误 prune,move/remove 维护双向关系,prune 清残留,repair 修路径。
本文由作者按照 CC BY 4.0 进行授权