bigywater.tech
Git · 40

worktree:.git/worktrees 里记了什么

想同时在两个分支上干活,又不想 clone 两份仓库,git worktree 是怎么做到的?这篇看它在 .git/worktrees 里写了哪些文件、哪些东西共享、哪些各有一份,以及删除、移动、锁定时发生什么。

3 样独立
每个工作树各有自己的 HEAD、index 和 HEAD 的 reflog;objects 与 refs/heads 共享
互指
新目录的 .git 文件指向 .git/worktrees/<名>,管理目录里的 gitdir 又指回来
prunable
手工删掉工作树目录后,git worktree list 的标记,要 prune 才清理管理目录

新目录里的 .git

实测 · 本机执行输出
  $ git worktree add -q ../l36wt1 -b feature 2>&1; cat ../l36wt1/.git; echo "---"; ls .git/worktrees/l36wt1
  gitdir: /work/l36/.git/worktrees/l36wt1
  ---
  HEAD
  ORIG_HEAD
  commondir
  gitdir
  index
  logs
  refs

附加工作树的 .git 是一个只有一行的文件:gitdir: <主仓库>/.git/worktrees/<名>。git 在这个目录里找 HEAD、index 等。

管理目录

实测 · 本机执行输出
  $ cd .git/worktrees/l36wt1 && for f in HEAD gitdir commondir; do printf "%-10s" $f; cat $f; done; echo "index: $(wc -c < index) 字节"; ls logs
  HEAD      ref: refs/heads/feature
  gitdir    /work/l36wt1/.git
  commondir ../..
  index:      137 字节
  HEAD

.git/worktrees/<名>/ 里:HEAD 是这个工作树自己的 HEAD,index 是自己的暂存区,gitdir 是新目录 .git 文件的路径(反向指针),commondir 是 ../..,logs/HEAD 是自己的 HEAD reflog。

共享与独立

实测 · 本机执行输出
  $ git status --short; echo "(主工作树的暂存区没有 b.txt)"; git -C ../l36wt1 status --short
  (主工作树的暂存区没有 b.txt)
  A  b.txt
  $ git -C ../l36wt1 rev-parse --git-dir --git-common-dir | sed "s#.*/l36/#<主>/#; s#.*/ltmp/#<ltmp>/#" ; git rev-parse --git-dir --git-common-dir
  <主>/.git/worktrees/l36wt1
  <主>/.git
  .git
  .git
  $ git -C ../l36wt1 rev-parse --git-path HEAD --git-path refs/heads/main --git-path objects --git-path index | sed "s#.*/l36/#<主>/#; s#.*/ltmp/#<ltmp>/#"
  <主>/.git/worktrees/l36wt1/HEAD
  <主>/.git/refs/heads/main
  <主>/.git/objects
  <主>/.git/worktrees/l36wt1/index
  $ git branch -vv | sed -E "s/ [0-9a-f]{7} / <sha> /"
  + feature <sha> (/work/l36wt1) 第二个
  * main    <sha> 第二个
  在 wt1 里提交,主仓库马上就能看到这个分支的新提交(refs/heads 共享):
  $ git log --oneline feature | sed -E "s/^[0-9a-f]+ //" | head -2
  wt1提交
  第二个

对象库、refs/heads、配置都在主 .git 里共享:在一个工作树里提交,别的工作树马上能看到分支的新提交。HEAD、暂存区各自独立,在一个工作树里 add 不会影响另一个。

一个分支一个工作树

实测 · 本机执行输出
  $ git worktree add ../l36wt2 feature 2>&1 | head -2 | sed -E "s#'[^']*l36wt1'#'<wt1>'#"
  Preparing worktree (checking out 'feature')
  fatal: 'feature' is already used by worktree at '<wt1>'
  $ git worktree add -q --detach ../l36wt2 2>&1; git worktree list | sed -E "s#/[^ ]*/(l36[a-z0-9]*)#<路径>/\1#; s/[0-9a-f]{7}/<sha>/"
  <路径>/l36    <sha> [main]
  <路径>/l36wt1 <sha> [feature]
  <路径>/l36wt2 <sha> (detached HEAD)

同一个分支不能同时在两个工作树里检出,否则两边会同时移动它;想多开可以 --detach。

手工删除与 prune

实测 · 本机执行输出
  $ git worktree list | sed -E "s#/[^ ]*/(l36[a-z0-9]*)#<路径>/\1#; s/[0-9a-f]{7}/<sha>/"; ls .git/worktrees
  <路径>/l36    <sha> [main]
  <路径>/l36wt1 <sha> [feature]
  <路径>/l36wt2 <sha> (detached HEAD) prunable
  l36wt1
  l36wt2
  $ git worktree prune -v 2>&1 | sed -E "s#/[^ ]*/(l36[a-z0-9]*)#<路径>/\1#"; ls .git/worktrees
  Removing worktrees/l36wt2: gitdir file points to non-existent location
  l36wt1

手工删除工作树目录,主仓库不知道;git worktree list 标 prunable,git worktree prune 依据 gitdir 反向指针判断目录不存在,清掉管理目录。

lock、move、remove

实测 · 本机执行输出
  $ git worktree lock --reason "在移动硬盘上" ../l36wt1; ls .git/worktrees/l36wt1 | tr "\n" " "; echo; cat .git/worktrees/l36wt1/locked; echo; git worktree list | sed -E "s#/[^ ]*/(l36[a-z0-9]*)#<路径>/\1#; s/[0-9a-f]{7}/<sha>/"
  COMMIT_EDITMSG HEAD ORIG_HEAD commondir gitdir index locked logs refs
  在移动硬盘上
  
  <路径>/l36    <sha> [main]
  <路径>/l36wt1 <sha> [feature] locked
  $ git worktree unlock ../l36wt1; git worktree move ../l36wt1 ../l36mv; cat ../l36mv/.git | sed -E "s#.*/ltmp#<ltmp>#"; cat .git/worktrees/l36wt1/gitdir | sed -E "s#.*/ltmp#<ltmp>#"
  <ltmp>/l36/.git/worktrees/l36wt1
  <ltmp>/l36mv/.git
  $ git worktree remove ../l36mv 2>&1; ls .git/worktrees 2>&1 | head -1; git worktree list | wc -l
  ls: .git/worktrees: No such file or directory
         1

lock 在管理目录里放 locked 文件(内容是原因),防止在移动硬盘上的工作树因为暂时找不到被 prune;move 同时更新新目录的 .git 文件和管理目录里的 gitdir;remove 删除目录和管理文件。

分步演示

worktree
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。路径在输出里做了简化(<主>、<路径>);worktree 的管理目录文件列表来自 ls,内容来自 cat
    • 没有验证:每个工作树各自的 refs/bisect、refs/worktree/* 等 per-worktree 引用(只验证了 HEAD 与 index);worktree.guessRemote、extensions.worktreeConfig 的 per-worktree 配置;git worktree repair;bare 仓库配合 worktree;子模块在 worktree 里的行为;git gc 对其他工作树 HEAD 与 index 的保护
    京ICP备2026067502号-1京公网安备11010502063487号