侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 178 篇文章
  • 累计收到 7 条评论

两个分支同时改,worktree 让它们各占一个目录

2026-10-12 / 0 评论 / 11 阅读

两个分支同时改,worktree 让它们各占一个目录

手上有两条分支要同时动:一条修 bug 修到一半,另一条等着回一个紧急的评审意见。反复 git stash 加切分支,最容易把没写完的半成品留在栈里忘了取。git worktree 给的是另一种办法,同一个仓库挂出两个目录,每个目录有自己的工作区和一个签出的分支。

下面这套动作全部在一台机器的临时目录里跑完,命令和输出都是原样贴的。

第 1 步,起一个能提交的仓库

仓库里先放一个 app.js,内容 console.log('v1'),提交一次,当前分支 main。这一步看到 git branch --show-current 输出 main 就可以往下走。

第 2 步,挂出第二个目录

一条命令同时做两件事:在上一级目录建一个 app-hotfix,并在里面签出一个新分支 fix/login。

== 2. 建一个工作树,同时签出另一个分支 ==
$ git worktree add -b fix/login ../app-hotfix
HEAD is now at d1fc010 v1
Preparing worktree (new branch 'fix/login')
   退出码 0

$ git worktree list
/private/tmp/verify-git-worktree/app         d1fc010 [main]
/private/tmp/verify-git-worktree/app-hotfix  d1fc010 [fix/login]

git worktree list 的输出就是这套东西的全貌,两行,每行一个目录加它签出的分支。主目录还在 main 上,新目录在 fix/login 上,两边指向同一个提交。这一步的检查点:list 里出现两行,第二行的分支名是新分支。

第 3 步,看两个目录里的 .git

== 3. 两个目录里的 .git 长得不一样 ==
$ ls -ld .git(主目录)
drwxr-xr-x .git
$ ls -ld .git(工作树)
-rw-r--r-- .git
$ cat .git(工作树里只有一行指针)
gitdir: /private/tmp/verify-git-worktree/app/.git/worktrees/app-hotfix

主目录里 .git 是目录,工作树里它是一个普通文件,内容只有一行,指向主仓库 .git/worktrees/app-hotfix。这一点上,一开始的判断是错的,以为工作树会复制一份仓库元数据,看到这行指针才明白:所有工作树共享同一个对象库和分支列表,只是各自占一个工作目录。

共享这件事的后果是双向的。一边提交,另一边 git log --all 立刻能看到;一边删分支,另一边也会发现分支没了。省掉的是仓库体积,不是隔离。

第 4 步,在两个目录里各写各的

在工作树里改 app.js、提交,然后回主目录看文件。

== 4. 在工作树里改代码并提交,主目录完全不受影响 ==
$ git add -A && git commit -qm 'fix login'
(无输出)
   退出码 0

$ git log --oneline --all --decorate
39ee0c4 (fix/login) fix login
d1fc010 (HEAD -> main) v1
   退出码 0

$ cat app.js(主目录)
console.log('v1');
$ cat app.js(工作树)
console.log('fix/login');

git log --oneline --all 一条 fix/login 一条 main,主目录里的 app.js 还是 v1,工作树里是新内容。两个目录的工作区文件是各自独立的,切分支不会动到另一个目录里的未提交改动。这是这套做法最实际的价值:bug 改一半的现场不用收,直接去另一个目录干活。

第 5 步,撞一次分支占用的报错

把已经签出的 main 再挂一个目录,git 会直接拒绝,并指出占用的目录是哪一个。

== 5. 已经签出的分支不能再挂一个工作树 ==
$ git worktree add ../app-dupe main
Preparing worktree (checking out 'main')
fatal: 'main' is already used by worktree at '/private/tmp/verify-git-worktree/app'

退出码 128。换成 -b 交给它自己建一个新分支就没事,后面那两行是建完立刻 git worktree remove 清掉的。这条限制的好处是明确:一个分支在一个时刻只有一个工作目录,不会出现两处同时改同一个分支的情况,那种情况下的冲突比切分支更难处理。

第 6 步,目录删了登记还在

工作树的目录被直接删掉(rm -rf)之后,主仓库里的登记不会自动清掉,git worktree list 会把它标成 prunable。

== 6. 悬空的工作树目录删掉之后,登记还在 ==
$ git worktree list
/private/tmp/verify-git-worktree/app         d1fc010 [main]
/private/tmp/verify-git-worktree/app-hotfix  39ee0c4 [fix/login] prunable
   退出码 0

$ git worktree prune
(无输出)
   退出码 0

$ git worktree list
/private/tmp/verify-git-worktree/app  d1fc010 [main]
   退出码 0

$ git branch
  app-dupe2
  fix/login
* main

正确的收尾是用 git worktree remove <目录>,它会检查有没有未提交的改动;已经手工删掉的用 git worktree prune 清登记。列表里最后那几行还说明一件事:app-dupe2 这个分支在它的工作树被 remove 之后依然存在,删工作树不删分支。

和复制一份仓库的差别

复制一份仓库也能得到两个目录,代价是对象库来两份、以后两边的提交要互相加远端才能同步。worktree 是共享对象库、各自工作区;分支不能重名是它的限制,也是它省事的地方。

要留意的是工作目录里的依赖不会被共享。工作树是新的目录,node_modules 要自己装一遍,编译产物也是一样。临时开一个目录改两行、改完就 remove 的场景,这套很轻;长期并行维护几条分支的场景,装依赖这一步的成本要算进去。

一句取舍:需要同时改两条分支时我会用 worktree,需要在一个干净的仓库里验证别人的提交时,复制的隔离更彻底。两边各有各的场合。

    🤞 分享