dsh-checkout-guard
开发工具 活跃维护

dsh-checkout-guard

LeslieWylie/dsh-checkout-guard

轻量级Git工作副本预检工具,可在执行写操作前校验当前工作副本的分支身份、与远端的漂移状态及过期时长,提前拦截误写错误分支的潜在风险,安装后无需额外配置即可直接使用。

0
Stars 标星
0
Forks 分支
0
Watchers 关注
0
Open Issues
JavaScript
主要语言
MIT
开源协议
33 KB
仓库大小
1 个月前
最后推送
一键安装扩展 / 插件指令
dsh plugin --profile web add github:LeslieWylie/dsh-checkout-guard
git clone https://github.com/LeslieWylie/dsh-checkout-guard.git
git clone git@github.com:LeslieWylie/dsh-checkout-guard.git
README.md main

dsh-checkout-guard

Is the working copy you are about to write to the one you think it is?

A DSH plugin that answers that before you edit, commit, or push — not after. It
reads a git checkout and tells you where it sits relative to its remote, how
stale that answer is, who your next commit will be attributed to, what work is
already sitting in the index, and whether another clone of the same repository
on this machine has moved ahead of the one you are in.

Read-only. Offline unless you ask for a fetch. Never takes the index lock.


Why

Every check here exists because a specific mistake actually happened, usually to
an agent working across many repositories at once:

What goes wrong What you see afterwards
You edit a checkout that is 8 commits behind Your "fix" reverts work, or your push is rejected after the fact
Two clones of one repository, you are in the stale one Changes vanish; the repo on the forge never shows them
A concurrent process moved HEAD to another branch Your commit lands on a branch you have never heard of
Something was already staged before you ran git add Your commit packages somebody else's work in progress
No repository-local user.email A work address is permanently attached to a public commit
Detached HEAD The commits exist, then don't, and nothing warned you

None of these throw. Git does exactly what you asked; you asked in a state you
had misread. That is the entire problem space this plugin covers.

Install

npm install dsh-checkout-guard

Then append to your profile's cordis.yml:

- id: checkout-guard
  name: 'dsh-checkout-guard'

Node 22.19+ or 24+. The only runtime requirement is git on PATH.

Tools

checkout_guard — one working copy, in depth

{ "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); you are reading code the remote has already moved past, and a push will be rejected",
    "another checkout of the same remote is ahead of this one: /elsewhere/widget (main, 3 ahead vs 1); you may be editing the stale copy"
  ],
  "warnings": [
    "remote refs were last updated 71h ago; ahead/behind is computed from that snapshot, not from the remote as it is now (re-run with fetch: true)"
  ],
  "verdict": "blocked",
  "safeToWrite": false
}

safeToWrite is the one field a caller needs. blockers are states where
proceeding loses or misplaces work; warnings are things worth knowing that
are not automatically wrong.

Parameter Effect
path (required) Absolute path to the working copy, or any directory inside it
fetch Contact the remote first so ahead/behind is current. The only networked option, default false
expectBranch The branch you believe you are on. A mismatch is a blocker
expectIdentity The email you expect to author with. A mismatch is a blocker
remote Remote to compare against (default origin)
staleAfterHours When remote refs count as stale (default 24)
findDuplicates / duplicateRoots / maxDepth / maxRepos Control the search for other clones

expectBranch and expectIdentity are how you turn a report into an assertion.
Without them the plugin describes; with them it refuses.

checkout_guard_scan — every working copy under a root

{ "roots": ["/abs/path/to/projects"], "maxDepth": 3 }

Returns one row per checkout — branch, upstream, ahead/behind, uncommitted
counts, fetch age — plus:

  • needsAttention: the ones that are behind, diverged, detached, or have no upstream
  • duplicateRemotes: paths grouped by remote, so two clones of one repository are visible at a glance

Run this first to find out which checkouts deserve a closer look, then hand
those paths to checkout_guard.

What it deliberately does not do

  • It does not tell you a remote is archived. That needs the forge's API, and
    this plugin does not make authenticated HTTP calls. remote.normalized gives
    you host/owner/repo to check yourself.
  • It does not fix anything. No pull, no rebase, no stash, no checkout. The
    recovery for "behind" and the recovery for "diverged" are different, and
    picking between them is not a decision a guard should make for you.
  • It does not judge your identity. It reports which config your author line
    comes from, and warns only when a public forge is involved and the repository
    has no local override. If you pass expectIdentity, it compares against
    exactly what you named — it never guesses what your address should be.

Safety properties

  • No writes to any repository. The only command that can modify anything is
    git fetch --dry-run, which runs only when fetch: true and touches nothing
    but FETCH_HEAD.
  • GIT_OPTIONAL_LOCKS=0 on every call. Ordinary git status refreshes and
    rewrites the index, taking .git/index.lock. A guard whose whole premise is
    "another agent may be in this checkout right now" must not be able to block
    that agent.
  • GIT_TERMINAL_PROMPT=0, empty GIT_ASKPASS. A fetch against a private
    remote fails fast instead of parking on a credential prompt nobody can see.
  • Credentials are stripped during URL normalization, before a remote can
    reach a result object or a log line.
  • Path allowlist. Both sides are realpath'd, so a symlink planted inside
    an allowed root cannot read outside it.

Configuration

- id: checkout-guard
  name: 'dsh-checkout-guard'
  config:
    roots:
      - /Users/you/projects

roots bounds everything the plugin will look at. Unset, it defaults to your
home directory.

Development

npm install
npm test          # unit tests against real git repos in a temp dir
npm run test:boot # loads the plugin into a real cordis Context

There is no build step. src/index.js is the published entry point, byte
for byte. Plugins that compile to lib/ and commit the output have to keep the
two in sync forever, and every test that imports src/ passes while the
artifact an installer runs is stale. Removing the build removes the whole class.

The unit tests create real repositories — real remotes, real pushes, real
divergence — rather than stubbing git. A hand-written double answers the way its
author expects, and the author's expectation about repository state is precisely
what is under test.

tests/boot.test.mjs exits 0 with a SKIP from a bare clone so contributors are
not blocked, and exits 1 under DSH_BOOT_STRICT=1, which CI sets. A skipped
integration test reported as green is worse than no test at all.

中文 · MIT