Git worktree 从多工作区到分支占用与审查隔离
先定位当前任务,再对照操作前提、结果与容易混淆的边界。
章节速览9 节 · 展开选择
正在写一个功能,暂存区里已有一部分准备提交的修改,这时又需要检查另一个版本。只切换分支可能打断当前工作;复制整个目录又容易混淆哪份修改属于哪个仓库。
Git worktree提供的是同一仓库的多个工作区:文件与暂存区可以分开,提交对象和普通分支仍然共享。这篇接续Git差异审查,通过一个保留全部实验目录的小仓库,实际核对“哪些独立、哪些共享”,再用脱离HEAD的工作区审查固定提交。
每条Git命令都使用-C明确工作区。主工作区保持已暂存与未暂存修改,另开目录处理新分支。
用完整提交ID创建脱离HEAD的审查目录。固定版本不等于只读,也不等于已经通过构建。
一个仓库,为什么可以有多个HEAD
先回想工作区、暂存区与提交:文件是你正在编辑的内容,索引决定下一次提交的快照,HEAD说明当前检出的分支或提交。多工作区让每个目录分别拥有这些当前状态,并通过仓库公共目录共享对象库与普通分支引用。
| 内容 | 主工作区与链接工作区的关系 | 操作上的含义 |
|---|---|---|
| 工作区中的文件 | 各目录独立 | 在topic中编辑,不会自动改写main目录的同名文件 |
| 索引,也就是暂存区 | 各工作区独立 | 在topic中add,不会替main暂存 |
| HEAD与HEAD引用日志 | 各工作区独立 | 两处可以指向不同分支;各自记录切换过程 |
| 提交、树、blob等对象 | 同一仓库共享 | topic中提交的新对象,可从main目录直接show |
| refs/heads下的普通分支 | 共享 | topic分支推进后,各工作区查询该分支会看到同一新位置 |
| 仓库公共config | 默认共享 | 修改local配置可能影响其他工作区,不等于目录私有设置 |
这里的共享规则来自官方worktree手册。不要把“所有引用都共享”当成规则:refs/bisect、refs/worktree等有例外。日常核对普通分支与HEAD已经足够,本文不直接编辑内部引用文件。
命令1:建立一个全新的教学仓库
下面适用于PowerShell和支持worktree的Git;本地验证版本为Git 2.41.0.windows.3。先选一个可写的练习目录作为当前位置,命令会在其中建立带随机名称的新子目录。所有变量在同一PowerShell会话中继续使用。每条命令执行后先确认成功;失败时停止,不把剩余命令直接粘贴运行。
命令1|新建而非覆盖:仅用于新生成的教学目录,不对已有项目执行初始化。提交使用命令级教学身份,不修改全局配置;示例不需要远端、凭据或推送。
1 | $demoRoot = Join-Path (Get-Location) ('worktree-demo-' + [Guid]::NewGuid().ToString('N')) |
把第一次提交称为A。正常情况下最后一次status没有修改行。$demoBase是本次实际生成的完整对象ID;创建时间、换行与对象格式都可能影响ID,所以文章不用一个固定哈希冒充所有读者的输出。
命令2:让main保持两层修改,再开topic
为了观察索引独立性,先在main追加一行并暂存,再追加另一行但不暂存。此时同一个文件在索引和工作区中各有一层变化。随后以已核对的A为起点,显式新建分支codex/demo-topic。
1 | Add-Content -LiteralPath (Join-Path $demoMain 'notes.txt') -Value 'main-staged=B' |
main的短状态应为MM notes.txt:第一列M表示索引相对HEAD变了,第二列M表示文件相对索引又变了。topic则从提交A检出干净快照,不会继承main尚未提交的两行文字。新工作区的来源是指定提交,不是另一个目录当前肉眼可见的文件内容。
| 位置 | HEAD所代表的已提交内容 | 索引中的notes.txt | 工作区中的notes.txt |
|---|---|---|---|
| main | A:只有base=A | base=A加main-staged=B | 再加main-unstaged=C |
| topic刚建立 | A:只有base=A | 只有base=A | 只有base=A |
自检1:main有未提交修改,topic一定会带上吗?
不会。本文显式从已提交的demoBase创建工作区;main的索引和文件变化留在main。若需要把其中的改动带到topic,必须另行设计并审查转移方式,不能假设worktree自动复制未提交内容。
命令3:在topic提交,回main核对共享对象
1 | Add-Content -LiteralPath (Join-Path $demoTopic 'notes.txt') -Value 'topic=D' |
把新提交称为D,它的父提交是A。从main目录执行show能够读到D,且文件包含topic=D,说明对象库确实共享。与此同时main的两层diff仍分别显示B和C,没有被topic的add或commit替换。
分支codex/demo-topic指向D,main仍指向A。使用不同工作区不会自动合并分支;“对象已经能读到”与“我的分支已经包含它”是两件事。审查提交内容可以用show,判断祖先关系还应使用merge-base等对应命令;不能拿同一仓库中对象存在,代替集成状态检查。
flowchart LR A[主工作区保留 B 与 C] --> B[从提交 A 建立 topic] B --> C[topic 独立暂存并提交 D] C --> D[普通分支推进至 D] D --> E[main 可读取 D 的对象] E --> F[main 文件和索引仍保持原样]
同一分支为什么会拒绝被再次检出
普通情况下,Git会拒绝把已经被某个工作区检出的分支再用于另一个工作区。否则两个不同索引与文件状态都以同一个可移动分支为当前分支,一个目录提交推进分支后,另一个目录的HEAD基准也跟着变化,容易产生难以理解的状态。
在本例中,codex/demo-topic已由topic占用。只读检查可先用git worktree list --porcelain定位;验证程序也实际尝试了不带强制选项的重复add并检查非零退出、原工作区状态与分支位置未变。面对占用拒绝,先选择新的任务分支或脱离HEAD审查,而不是把强制选项加上去使报错消失。
自检2:同一提交能出现在多个工作区吗?
能。这里受保护的是分支占用,不是提交对象只能检出一次。多个脱离HEAD的工作区可以检查同一提交;新分支也可以从同一提交开始,各自拥有独立的分支名字与后续移动。
命令4:用固定提交建立审查工作区
命令4|脱离HEAD审查:这里的demoTip必须已成功解析并核对为D。新目录固定检出这个提交,不占用codex/demo-topic;它仍是可写目录。
1 | git -C $demoMain worktree add --detach $demoReview $demoTip |
HEAD应解析成D,状态表明处于脱离HEAD,diff只显示topic新增的行。路径检查区分当前文件目录、该工作区的Git私有管理目录和仓库公共目录;这些命令的含义见文献2。链接工作区根部的.git通常是指向管理位置的文件,不应把它误当成损坏仓库或手工改写它。
若topic之后继续提交E,review仍指向D,直到你在review显式执行切换、提交等操作。固定对象ID解决的是版本漂移;它没有冻结系统依赖、数据库、环境变量或远端接口。要比较两个版本的行为,还需要对齐这些外部条件并记录实际测试结果。
在review里也能修改、暂存、提交。脱离HEAD的提交不会自动推进codex/demo-topic;如果确实要保留新的审查修改,应先核对当前对象,再新建明确分支。可参考reflog与提交找回理解可移动日志,但不要把日志可能存在当作日常保存策略。
flowchart TD
A[收到另一个版本的任务] --> B{需要继续开发吗}
B -->|需要| C[新建明确任务分支与工作区]
B -->|只审查固定版本| D[核对完整提交后脱离检出]
C --> E[确认目录 分支 索引与外部依赖]
D --> E
E --> F[审查差异并运行对应验证]
F --> G[记录对象ID与实际结果]
多开目录后,哪些问题仍需自己处理
仓库配置默认共享。git config --local的local是仓库级,不等于当前工作区私有。Git提供extensions.worktreeConfig与worktree配置机制,但启用前要核对版本、已有core.worktree/core.bare等设置和团队工具;本文不启用或迁移这些设置。语义见文献3。
构建依赖和缓存通常不是Git对象。如果它们位于各自目录中,可能各有一份;如果工具写入同一个全局缓存、共享数据库或端口,仍会相互影响。忽略规则也不会自动把主目录的未跟踪文件复制过来。启动服务前核对端口、输出目录和配置路径,比看到两个文件夹就假定完全独立可靠得多。
工作区列表是一张仓库管理记录,不是磁盘备份。本文实验保留main、topic、review及所有管理元数据,未执行目录移除或清理。真实项目结束任务时,应先审查未提交、未跟踪、忽略文件及仅在脱离HEAD可达的提交,再按团队的目录生命周期规则处理;不要直接删目录后把管理记录不一致当成Git失效。
自检3:review固定了D,测试是否就可重复?
只固定了Git提交。依赖版本、数据、运行时、端口和外部服务仍可能变化;审查记录需要同时保存环境与实际结果。worktree本身不提供容器、进程或网络隔离。
自检4:topic的提交已能从main读取,是否等于已经合入main?
不等于。对象库共享使对象可读,但main分支仍留在A。需要另外核对祖先与分支关系,再按项目流程集成;目录数量也不代表分支数量或已发布版本数量。
实验能证明的范围与下一步
本篇验证在新建的独立教学仓库内运行真实Git,核对对象父关系、分支占用、三个索引与文件内容、main两层diff不变、公共目录一致与私有目录不同。还在topic继续提交E,确认review仍为D,并创建脱离HEAD提交以核对普通分支未移动。所有实验产物保留,不接触博客部署仓库。
它证明的是当前本地Git版本下这组操作的具体语义,不能代替子模块、稀疏检出、网络盘或大型依赖项目的专项验证。需要解释变化从哪个版本开始,可衔接Git bisect;要撤销已发布改动,则继续读Git revert。
参考文献与对应位置
- Git官方:git-worktree,2.39.0手册条目(适用于本文验证的2.41系列基础命令)。 DESCRIPTION、add、REFS与DETAILS。核对链接工作区、独立HEAD/索引、共享普通分支及占用拒绝;本文命令输出另由本地实验核对。返回命令1。
- Git官方:git-rev-parse,2.41.0。 路径和修订解析选项。用于绝对管理路径、公共目录与提交对象核对。返回命令4。
- Git官方:git-config,2.41.0;git-worktree配置说明。 extensions.worktreeConfig、工作区配置边界。用于默认共享与可选扩展的区别;本文不调整已有仓库配置。