12 · Git Worktree 与异步工作区操作¶
Herdr 把 workspace 与 Git worktree 深度连接,目标是让多个 Agent 在同一仓库不同分支并行工作。这里最大的风险不是 git worktree add 语法,而是:Git 子进程很慢、完成会乱序、目录可能有未提交内容,错误的强制清理甚至会删到用户无关目录。
12.1 领域关系¶
flowchart LR
Repo["source repository"] --> Main["main checkout"]
Repo --> WT1["linked worktree A"]
Repo --> WT2["linked worktree B"]
Main --> W0["Herdr workspace 1"]
WT1 --> W1["Herdr workspace 2 / Agent A"]
WT2 --> W2["Herdr workspace 3 / Agent B"]
Workspace 的 worktree membership 记录:
- source repository/root;
- checkout path 与 canonical checkout key;
- branch/ref;
- 是否 linked worktree;
- Git common/admin identity;
- 对 UI/API 有用的显示元数据。
基础 Git 命令和解析位于 src/worktree.rs,领域/API 协调在 src/app/worktrees.rs 与 src/app/api/worktrees/。
12.2 Git 发现不进入 render¶
为了显示 repo、branch、dirty 等状态,最简单但错误的实现是每帧运行 git status。其成本会乘以:
Herdr 把发现拆进 src/workspace/git/:
discovery.rs:从 cwd 找 repo/worktree identity;status.rs:读取 branch/dirty 等缓存;config.rs:Git 配置相关解析;git_refresh.rs:后台刷新与事件回流。
render 只读取已经准备好的 workspace metadata。
12.3 Checkout key 解决路径别名¶
同一目录可能通过相对路径、symlink、大小写差异或 .. 表达。异步操作 map 若直接用原始字符串,会允许同一 checkout 同时创建/删除两次。
Herdr 使用 canonical_or_original 一类 best-effort 规范化生成 checkout_key:
- canonicalize 成功时使用稳定路径;
- 目标尚未创建时保留可比较的原路径表示;
- key 同时用于 pending create/remove 冲突检查;
- completion event 携带同一 key 回来验证。
best-effort 很关键:创建前路径不存在,强制要求 canonicalize 会让合法 create 无法开始。
12.4 创建 worktree 的阶段¶
sequenceDiagram
participant U as UI/API
participant A as App loop
participant P as Pending maps
participant B as Background thread
participant G as git
U->>A: create(branch/path/source)
A->>A: validate repo/branch/path/conflicts
A->>P: reserve checkout_key + operation_id
A->>B: spawn command task
B->>G: git worktree add ...
G-->>B: status/stdout/stderr
B-->>A: AppEvent(result, key, operation_id)
A->>P: still current?
alt current and success
A->>A: create/focus workspace + membership
A->>P: release reservation
A->>A: persist + publish worktree.created
else stale or failure
A->>P: release only matching reservation
A->>A: report error / ignore stale completion
end
Git 子进程不阻塞单写者。App 只在开始和完成两个短阶段改变状态。
12.5 Operation ID 防止乱序完成¶
仅使用 checkout_key -> pending bool 不够:
若迟到结果只按 key 查 pending,会误清除或提交 op 8。Herdr 维护单调 next_api_worktree_operation_id,pending map 保存 key/id;completion 必须同时匹配。
accept(result) iff
pending_by_checkout[result.key] == result.operation_id
AND(删除时)pending_by_workspace[result.workspace] == result.operation_id
这是第 8、10 章 sequence/identity 模式在后台文件系统操作上的同类应用。
12.6 默认路径与 branch slug¶
创建 UI/API 需要把 branch 映射成可用 checkout 目录。slug 逻辑要处理:
/、空格和平台非法字符;- 同名目标目录;
- 已存在 worktree;
- repo root 与默认 worktree parent;
- 用户显式 path 优先;
- 分支已存在 vs 需要新建。
最终仍把 argv 数组交给 git,不把 branch/path 拼成 shell command,避免 quoting 与注入问题。
12.7 Porcelain 解析¶
工作树列表使用:
而不是解析面向人的对齐输出。parse_worktree_list_porcelain 按空行分 record,读取 worktree path、HEAD、branch、bare/detached/locked/prunable 等字段。
Porcelain 是更稳定的机器接口,但 parser 仍应:
- 忽略可扩展未知字段;
- 处理 detached worktree 无 branch;
- 不假设路径无空格;
- 保留足够原始信息用于错误诊断;
- 对缺失关键
worktree行丢弃/报错,而非制造空路径。
12.8 Dirty 删除的双层防线¶
非强制删除前,Herdr 用 git status --porcelain 检查 modified/untracked。即使预检 clean,真正 git worktree remove 前也可能新产生修改,因此仍解析 Git 返回的 “use --force to delete it” 错误并转换成稳定 dirty_worktree_requires_force。
flowchart TD
R["remove request"] --> F{"force?"}
F -->|否| D{"status --porcelain dirty?"}
D -->|是| Ask["返回需要 force / UI 二次确认"]
D -->|否| Git["git worktree remove"]
Git --> Err{"Git 报 dirty race?"}
Err -->|是| Ask
Err -->|否| Done["完成或其他错误"]
F -->|是| Shutdown["先处理 workspace runtimes"]
Shutdown --> ForceGit["git worktree remove --force"]
ForceGit --> Done
预检改善体验,Git 自身是最终权威。两层缺一不可。
12.9 删除前为何处理 Terminal runtimes¶
Workspace 中的 shell/Agent cwd 位于待删 checkout。强制删除目录时仍有进程使用它,会导致:
- child 后续相对路径失败;
- Windows 文件锁让删除失败;
- UI 仍展示一个不存在的 workspace;
- 恢复 snapshot 指向已删 cwd。
因此强制删除(Windows 路径通常更严格)会先按策略 shutdown 对应 terminal runtimes,再执行 Git/目录操作。非强制 dirty 拒绝则不应提前杀进程;必须在确定用户已授权 destructive removal 后才做不可逆关闭。
12.10 Forced orphan recovery 的安全验证¶
git worktree remove --force 可能遇到“目录存在但 Git 已不再注册”的残留。Herdr 有恢复路径清理 orphan checkout,但不能直接 remove_dir_all(path)。
安全证明链大致是:
- 目标确实是预期 checkout path;
- 读取目标
.git文件; - 解析其
gitdir:指向; - 计算 source repo 的 common
worktrees/admin 目录; - canonical/best-effort 验证
.git指向位于该目录之下; - 确认没有无关 replacement directory;
- 只有 force 路径才进行 orphan cleanup。
git_common_worktrees_dir 和相关测试专门保护这条边界。
为什么要防 replacement directory¶
后台 remove 运行期间,用户可能在同一路径创建新目录。若 completion 盲删路径,会删掉与原 worktree 无关的新数据。测试 forced_worktree_remove_recovery_keeps_unrelated_replacement_directory 正是在锁定这个时间竞态。
12.11 完成事件与 UI/API 统一¶
UI modal 和 API 都可能启动 worktree 操作。它们应复用底层 command/result,但完成后的呈现不同:
- API 需要结构化 response/error code;
- UI 需要关闭/推进 modal、显示 notification、可能自动 focus;
- 两者都要更新 workspace membership、pending maps、snapshot 和 EventHub;
- stale completion 都必须忽略,不能因调用入口不同放宽。
src/app/api/worktrees/deferred.rs 把 API 的 deferred response sender 与 operation identity 一起保存,让后台完成后回到 App loop 再答复原连接。
12.12 Git 状态刷新与缓存 key¶
多个 workspace 可能指向同一 repo/common dir,不应各自重复跑完全相同的发现。cache key 需要区分:
- repo-level 信息(common dir、配置);
- checkout-level 信息(branch、dirty、HEAD);
- linked worktree identity;
- cwd 只是 repo 子目录时的归属。
后台结果同样要验证 key/当前 workspace metadata,避免用户切换 cwd 后旧 refresh 覆盖新 repo 状态。
12.13 失败矩阵¶
| 失败 | 状态处理 |
|---|---|
| branch/path 校验失败 | 不 reserve,不启动线程 |
| 同 checkout 已有 pending op | 稳定 conflict error |
git 不存在/非 repo |
释放匹配 reservation,保留 workspace |
| create Git 成功但 workspace 创建失败 | 报告部分成功并提供可清理路径,不能伪装全失败 |
| 非 force dirty | 不关闭 runtime,要求显式确认 |
| force 前 runtime shutdown 失败 | 根据平台/所有权策略停止,避免半删 |
| 迟到 completion | 不改当前 pending/state |
| orphan 指针无法证明归属 | 拒绝递归清理 |
“Git 成功但 App 后处理失败”是典型跨系统部分提交。最理想是补偿 git worktree remove,但补偿本身也可能失败;至少错误必须承认外部资源已创建,并给出确切路径和命令。
12.14 测试策略¶
src/worktree.rs 的测试会创建临时真实 Git repo,覆盖:
- porcelain parsing;
- clean/dirty detection;
- add/remove command argv;
- force flag;
- dirty error 分类;
- unregistered orphan recovery;
- unrelated replacement directory 保留。
App/API 层测试进一步覆盖:
- operation id 与 checkout key;
- pending create/remove 冲突;
- UI 二次 force confirmation;
- 删除后 workspace/terminal lifecycle;
- EventHub/response 的 success/error 形状。
12.15 本章结论¶
- Git/FS 操作离开主循环,开始和完成才串行修改 state;
- checkout key 防路径别名,operation id 防迟到完成;
- dirty 预检优化体验,Git 错误仍是最终竞态防线;
- 强制删除必须在授权后处理 runtime,并证明 orphan 目标归属于 repo;
- 外部命令与内存状态跨界时,要显式处理“外部成功、内部失败”的部分提交。
下一章把同一控制平面延伸到远端:Herdr 如何通过 SSH 探测平台、安装匹配二进制、建立 bridge,并把 foreground/observer/尺寸和协议版本语义带过网络边界。