跳转至

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。其成本会乘以:

workspaces × panes × refresh rate × process spawn cost

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 不够:

op 7 开始创建 /repo/a
op 7 失败/取消
op 8 对同一路径重新开始
op 7 的后台结果迟到

若迟到结果只按 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 解析

工作树列表使用:

git worktree list --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)。

安全证明链大致是:

  1. 目标确实是预期 checkout path;
  2. 读取目标 .git 文件;
  3. 解析其 gitdir: 指向;
  4. 计算 source repo 的 common worktrees/ admin 目录;
  5. canonical/best-effort 验证 .git 指向位于该目录之下;
  6. 确认没有无关 replacement directory;
  7. 只有 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 本章结论

  1. Git/FS 操作离开主循环,开始和完成才串行修改 state;
  2. checkout key 防路径别名,operation id 防迟到完成;
  3. dirty 预检优化体验,Git 错误仍是最终竞态防线;
  4. 强制删除必须在授权后处理 runtime,并证明 orphan 目标归属于 repo;
  5. 外部命令与内存状态跨界时,要显式处理“外部成功、内部失败”的部分提交。

下一章把同一控制平面延伸到远端:Herdr 如何通过 SSH 探测平台、安装匹配二进制、建立 bridge,并把 foreground/observer/尺寸和协议版本语义带过网络边界。