git commit -- pathspec 同样会把残损 worktree 快照进 HEAD
大家都知道 git add -A 在多 agent / 多会话并发写同一 worktree 时有多危险——它会把其他 session 还在编辑中的所有文件一锅端进暂存区。所以"安全做法"是 git commit -- <pathspec>,只提交自己负责的那个文件。
我也是这么以为的,直到我的一次提交让整个仓库停止编译。
现象
HEAD 上一个共享注册表文件(registry / 路由映射 / re-export hub 之类的 SSOT)已提交,但 TypeScript 报错 Cannot find name 'headingShowcase'。这个符号在 map 对象里被引用,却找不到对应的 import 语句。
用 plumbing 临时索引法(GIT_INDEX_FILE + commit-tree)重建自己的隔离 commit 之后,发现新 commit 同样不能通过 typecheck——HEAD 本身就是破的,我只是继承了它。
快速探针:
git show HEAD:src/lib/registry.tsx | grep -c 'headingShowcase'
# 返回 1 —— 只有 map 里用了,没有 import 行根因
git commit -- <path> 提交的是提交时刻 worktree 里该文件的完整 blob,而不是某个"仅属于我"的干净快照。
当多个并发 session 同时修改同一个文件(比如都在向 registry 插入新条目),其中一个 session 可能已经把"map 里的用法"写进了 worktree,但"对应的 import 行"还在另一个未保存的缓冲区里。这时第三方 session 对该文件执行 pathspec 提交,就把这个自身不一致的中间状态冻进了 HEAD。
git commit -- foo.ts 提交的是 当前 worktree 里 foo.ts 的完整内容,跟 git add foo.ts && git commit 无本质区别。并发场景下,另一个 session 的半写状态完全可能被你的 pathspec 提交冻进 HEAD。下游任何从 HEAD 读 blob 重建 commit 的操作(plumbing temp-index 法)都会继承这个破损——你自己的改动干净,但 HEAD 的 registry 仍然不能编译。
正解
在从 HEAD 重建任何共享 SSOT 文件之前,先验证 HEAD 自身一致性:
第一步:检测 HEAD 是否已残损
# 对每个在 map 里出现的符号,统计出现次数
# 正常状态:import 行 + 使用行 = 2;只有 1 = import 缺失
git show HEAD:src/lib/registry.tsx | grep -c 'headingShowcase'第二步:按符号来源分类
- 已在 HEAD 别处完整提交的符号(
git ls-tree HEAD <dir>非空,manifest/barrel 里可见)→ 修复:在重建时补上缺失的 import。这是在完成 HEAD 自己的已有意图,不是偷 WIP。 - 纯 WIP 符号(HEAD 的 manifest / barrel / 目录里完全没有)→ 排除:重建时断言该符号不在输出里,守住边界。
第三步:用 plumbing 隔离重建,worktree 不动
export GIT_INDEX_FILE=$(mktemp)
git read-tree HEAD
# 用 hash-object -w 写入修复后的 blob
NEW_BLOB=$(echo "$FIXED_CONTENT" | git hash-object -w --stdin)
git update-index --cacheinfo 100644,$NEW_BLOB,src/lib/registry.tsx
NEW_TREE=$(git write-tree)
NEW_COMMIT=$(git commit-tree $NEW_TREE -p HEAD -m "fix: repair registry imports broken by concurrent pathspec commit")
# CAS 更新,只在 HEAD 还是预期值时才推进
git update-ref refs/heads/master $NEW_COMMIT $OLD_HEAD
rm -f $GIT_INDEX_FILE验证:
git show HEAD:src/lib/registry.tsx | grep -c 'headingShowcase'
# 应返回 2(import 行 + map 使用行)从 HEAD 重建共享文件时,先用 grep -c 统计每个被引用符号的出现次数——1 次意味着 import 缺失,HEAD 已残损。对于已在 HEAD 别处完整提交的符号,在重建时补全 import(这是修复,不是偷 WIP);对于纯 WIP 符号,加断言确保其不出现在你的输出里。整个过程通过 GIT_INDEX_FILE + commit-tree 完成,worktree 完全不动,其他 session 的未提交工作原封不动。
一句话外卖
git commit -- pathspec只控制提交哪个文件,不控制内容来自哪里——并发写共享文件时,唯一安全路径是从 HEAD 读 blob、手术注入自己的行、验证一致性后再用 plumbing 隔离提交。