想同时在两个分支上干活,又不想 clone 两份仓库,git worktree 是怎么做到的?这篇看它在 .git/worktrees 里写了哪些文件、哪些东西共享、哪些各有一份,以及删除、移动、锁定时发生什么。
$ 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。
$ 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 反向指针判断目录不存在,清掉管理目录。
$ 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
1lock 在管理目录里放 locked 文件(内容是原因),防止在移动硬盘上的工作树因为暂时找不到被 prune;move 同时更新新目录的 .git 文件和管理目录里的 gitdir;remove 删除目录和管理文件。