dsh-checkout-guard
你正要写入的这个工作副本,是不是你以为的那个?
一个 DSH 插件,在你编辑、提交、推送之前回答这个问题,而不是之后。它读取一个 git checkout,告诉你:它相对远端处在什么位置、这个答案有多陈旧、你下一个提交会 署谁的名、暂存区里已经躺着什么、以及本机是否还有同一个仓库的另一份 clone 已经 跑到了你前面。
只读。不显式要求就不联网。从不占用 index 锁。
---
为什么
这里每一项检查都对应一次真实发生过的事故,通常发生在同时操作多个仓库的 agent 身上:
| 出了什么事 | 你事后才看到 |
|---|---|
| 在落后 8 个提交的 checkout 里改代码 | 你的"修复"把别人的工作退回去了,或者推送被拒 |
| 同一个仓库有两份 clone,你在陈旧的那份里 | 改动凭空消失,forge 上永远看不到 |
| 并发进程把 HEAD 切到了别的分支 | 你的提交落在一个你没听说过的分支上 |
你 git add 之前暂存区里已经有东西 | 你的提交打包进了别人的半成品 |
仓库没有本地 user.email | 一个工作邮箱被永久钉在公开提交上 |
| detached HEAD | 提交存在过,然后不存在了,全程无提示 |
这些都不会报错。git 完全照你说的做了;是你在误读的状态下说的。这就是本插件覆盖的 全部问题域。
安装
npm install dsh-checkout-guard然后在 profile 的 cordis.yml 追加:
- id: checkout-guard
name: 'dsh-checkout-guard'需要 Node 22.19+ 或 24+。运行期唯一依赖是 PATH 上有 git。
工具
checkout_guard —— 单个工作副本,深度检查
{ "path": "/abs/path/to/repo", "expectBranch": "main" }{
"branch": { "current": "main", "detached": false, "upstream": "origin/main", "linkedWorktree": false },
"sync": { "ahead": 1, "behind": 1, "fetchAgeSeconds": 255600, "fetchedNow": false },
"remote": { "normalized": "github.com/acme/widget", "host": "github.com", "publicForge": true },
"identity":{ "email": "you@example.com", "source": "global", "repositoryOverride": false },
"workingTree": { "staged": [], "unstaged": ["package.json"], "untracked": [], "clean": false },
"duplicateCheckouts": [ { "path": "/elsewhere/widget", "branch": "main", "ahead": 3, "aheadOfThisOne": true } ],
"blockers": [ "diverged from origin/main by 1 commit(s) (and 1 ahead) ...",
"another checkout of the same remote is ahead of this one: /elsewhere/widget ..." ],
"warnings": [ "remote refs were last updated 71h ago ..." ],
"verdict": "blocked",
"safeToWrite": false
}调用方只需要看 safeToWrite 一个字段。blockers 是继续下去会丢失或错置工作的 状态;warnings 是值得知道、但不必然是错的情况。
| 参数 | 作用 |
|---|---|
path(必填) | 工作副本的绝对路径,或它内部任意子目录 |
fetch | 先联系远端,让 ahead/behind 是当下的。唯一联网的选项,默认 false |
expectBranch | 你认为自己在哪个分支。不符即 blocker |
expectIdentity | 你期望的提交署名邮箱。不符即 blocker |
remote | 对比哪个远端(默认 origin) |
staleAfterHours | 远端 ref 多久算陈旧(默认 24) |
findDuplicates / duplicateRoots / maxDepth / maxRepos | 控制重复 clone 的搜索范围 |
expectBranch 和 expectIdentity 是把"报告"变成"断言"的开关。不传,插件只描述; 传了,插件会拒绝。
checkout_guard_scan —— 某个根目录下的所有工作副本
{ "roots": ["/abs/path/to/projects"], "maxDepth": 3 }每个 checkout 返回一行——分支、upstream、ahead/behind、未提交数量、上次 fetch 距今多久——外加:
needsAttention:落后、分叉、detached、或没有 upstream 的那些duplicateRemotes:按远端分组的路径,一眼看出同一个仓库有几份 clone
先跑这个找出值得细看的 checkout,再把路径逐个交给 checkout_guard。
刻意不做的事
- 不告诉你远端是否已归档。 那需要调 forge 的 API,本插件不发带认证的 HTTP
请求。remote.normalized 会给你 host/owner/repo,你可以自己去查。
- 不修任何东西。 不 pull、不 rebase、不 stash、不 checkout。"落后"和"分叉"
的正确恢复方式不一样,在两者之间做选择不该由一个 guard 替你决定。
- 不评判你的身份。 它只报告署名来自哪份配置,并且仅在涉及公开 forge 且仓库
没有本地覆盖时才警告。你传 expectIdentity,它就严格按你给的值比对——它从不 猜测你的邮箱"应该"是什么。
安全性质
- 不对任何仓库写入。 唯一可能修改东西的命令是
git fetch --dry-run,仅在
fetch: true 时执行,且只动 FETCH_HEAD。
- 每次调用都带
GIT_OPTIONAL_LOCKS=0。 普通的git status会刷新并重写
index,从而占用 .git/index.lock。一个立足于"此刻可能有另一个 agent 在这个 checkout 里"的 guard,绝不能反过来卡住那个 agent。
GIT_TERMINAL_PROMPT=0、空GIT_ASKPASS。 对私有远端 fetch 会快速失败,
而不是停在一个没人看得见的凭据提示上。
- URL 归一化时先剥掉内嵌凭据,凭据不可能进入结果对象或日志。
- 路径白名单。 两侧都做
realpath,所以放在允许目录里的软链接无法读到外面。
配置
- id: checkout-guard
name: 'dsh-checkout-guard'
config:
roots:
- /Users/you/projectsroots 限定插件能看的一切范围。不设时默认为你的 home 目录。
开发
npm install
npm test # 在临时目录里对真实 git 仓库跑的单元测试
npm run test:boot # 把插件装载进真实 cordis Context没有构建步骤。 src/index.js 逐字节就是发布出去的入口。编译到 lib/ 再把 产物提交上去的插件,得永远维持两者同步,而所有 import src/ 的测试都会通过—— 与此同时,安装者真正运行的那份产物是过期的。去掉构建,就去掉了这整类问题。
单元测试创建真实仓库——真实远端、真实推送、真实分叉——而不是给 git 打桩。手写的 测试替身会按作者的预期作答,而作者对仓库状态的预期,恰恰就是被测对象本身。
tests/boot.test.mjs 在裸 clone 下打印 SKIP 并退出 0,不挡贡献者;在 CI 设置的 DSH_BOOT_STRICT=1 下退出 1。一个被跳过却报告为绿的集成测试,比没有测试更糟。
English · MIT