npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-git-vcs

v0.1.2

Published

DSH Web GUI 的 Git 版本管理插件:右侧栏新增「版本管理」页,功能与界面参照 IntelliJ IDEA 的 Version Control 工具窗(本地变更 / 提交 / 历史 / 分支 / 命令 / 贮藏)。

Readme

dsh-git-vcs

DSH Web GUI 的 Git 版本管理插件:在右侧栏新增「版本管理」页,功能与界面参照 IntelliJ IDEA 的 Version Control 工具窗(Alt+9)与 Commit 工具窗(Alt+0)——本地变更与勾选提交、提交树与提交详情、 分支 / 远程 / 贮藏管理、git 命令流水。

版本与兼容性

| 项目 | 值 | |------|-----| | 插件版本 | 0.1.2(package.json 的 version) | | 仓库 / Release | https://github.com/TiChuXiXi/dsh-git-vcs | | npm 包名 / 插件行 id | dsh-git-vcs / git-vcs | | 兼容的 DeepSeek Harness | @deepseek-ai/dsh ≥ 0.1.7-rc.2(写在 package.json 的 dsh.engines.dsh;开发与验证版本就是 0.1.7-rc.2) | | 依赖的官方插件 | @deepseek-ai/dsh-client-ui-sidebar-right(客户端半区 dsh.client.inject 声明;随 web-app 分发,本机为 0.1.7-rc.2) | | 依赖的 host 服务 | subprocess(跑 git)、connection(RPC 信任栅栏)、webServer(注册 /git-vcs 路由) | | 依赖的 client 能力 | sidebarRightTabs(注册 tab 类型)、slots(sidebar.right.pane.tab / .title)、标准 props sessionId + useSessions(取会话工作目录) | | git | 2.x;argv 调用(不进 shell),从 PATH 解析或由配置 gitPath 指定 | | Node.js | ESM;本机验证 v22.23.2 | | 构建 | 零构建:无编译步骤、无运行时依赖(host 半区不 import 任何 @deepseek-ai/* 值) |

最低版本取 0.1.7-rc.2 的原因:客户端半区取「会话工作目录」依赖 session 作用域 slot 的标准 prop sessionId —— DSH 0.1.7 的 SessionListState 移除了旧的 current 字段(0.1.5 时代用它定位 当前会话),不升级取不到 cwd、面板会停在「未加载」。host 侧走「自己注册 webServer 前缀路由」 这条通道,需要 webServer 与 connection 两个服务同时可注入(细节见文末「RPC 通道是怎么挂的」)。 升级 DSH 后先跑一遍 node scripts/verify-host.mjs。

更新记录

| 版本 | 主要变化 | |------|----------| | 0.1.2 | 适配 DSH 0.1.7:会话工作目录改从 session 作用域的标准 prop sessionId + useSessions 选择器取(0.1.7 的 SessionListState 移除了旧的 current 字段,旧写法导致面板永远停在「(未加载)」);两个探针夹具同步成 0.1.7 的真机 props 形态,避免"夹具与真机漂移、自检全绿而真机红" | | 0.1.1 | 提交树覆盖所有分支并支持滚到底续拉(issue #1,--all + --skip,单页 50 / 上限 2000 行);提交树从单轨画法改成多泳道图(对齐 git log --graph / IDEA,分叉与汇合画在真正的分叉提交上);修掉两处真机几何错位(斜线层 SVG 高度按 viewBox 比例算成 100px、连线比圆点偏右 1px);目录不是仓库时整页换成居中空态 + 一键 git init(issue #2,可选初始分支名);新增真浏览器几何/空态自检 scripts/dom-probe.mjs(18 条断言)与「客户端调用的端点 host 侧都存在」的结构性断言 | | 0.1.0 | 首个发布版:右侧栏「版本管理」页(Local Changes / Log / Console / Branches / Remotes / Stash)、按文件勾选提交、推送与凭据兜底、自挂 /git-vcs RPC 路由 |

插件说明

提供什么

  • 一个右侧栏 tab 类型:版本管理(同时在右侧栏引导页注册一个胶囊入口,order: 20), 内容全部由本插件渲染;分栏、全屏、展开/收起由官方 @deepseek-ai/dsh-client-ui-sidebar-right (右侧栏的停靠面与 header 展开控件)提供,本插件不碰布局。
  • 一条 host RPC 通道 /git-vcs(29 个端点,见下表):所有 git 调用都在 host 进程里以 argv 形式执行。
  • 六页功能:Local Changes / Log / Console / Branches / Remotes / Stash。
  • 目录不是仓库时:整页换成居中的空态 + 「在当前目录初始化 Git 仓库」按钮(repo/init)。

形态与文件

单包双半区、零构建(手写闭包工厂,无 tsdown、无运行时依赖):

| 半区 | 文件 | 职责 | |------|------|------| | host | index.js | 用 ctx.subprocess 执行 git;自己注册 webServer 前缀路由 /git-vcs 暴露端点;写操作门禁;repo/snapshot 一次刷新一次往返 | | 浏览器 | lib/client.js | 注册右侧栏 tab 类型 + 正文(sidebar.right.pane.tab[.title]);IDEA 风格 React UI(React.createElement,无 JSX) | | 组合层 | cordis.patch.yml | 插件行 git-vcs:inject: [subprocess, connection, webServer] + config |

作用范围

当前会话的工作目录(session.cwd),语义等同 IDEA 的「项目根」——换会话即换仓库; 也可以在面板顶部手填绝对路径后点「打开仓库」。配置 repoRoot 可把它限定在某个仓库根之下。

RPC 端点

浏览器侧统一 ctx.connection.rpc.call('/git-vcs', endpoint, payload);失败返回稳定错误码 (git-vcs/bad-request、git-vcs/not-a-repo、git-vcs/write-disabled、git-vcs/auth-required、 git-vcs/network、git-vcs/timeout、git-vcs/git-failed …)。

| 端点 | 作用 | 门禁 | |------|------|------| | repo/info | 仓库根、分支、upstream、ahead/behind、git 版本、remote.origin.url、插件配置回显 | — | | repo/snapshot | 一次刷新所需的全部数据(status + log + 本地/远程分支 + stash + 版本 + remote),host 侧并发探测;all: true 时提交列表覆盖所有分支 | — | | repo/init | 把目录初始化为 git 仓库(初始分支名可传 branch);已是仓库根返回 already: true,仓库内子目录直接拒绝 | write | | status | git status --porcelain=v2 → 按「冲突 / 已暂存 / 已修改 / 未跟踪」分组 | — | | diff | 单文件统一 diff;staged 取 index、untracked 走 --no-index、其余取工作区 | — | | log | 提交列表(limit / skip 分页、all 覆盖所有分支、可按 branch / path 过滤) | — | | show / show/file | 提交元数据 + 文件列表 / 单个文件的 diff(按需拉取) | — | | branches | 本地 + 远程分支(hash、upstream、ahead/behind;过滤 origin/HEAD 符号引用) | — | | remote/list | git remote -v | — | | remote/add | 新增远程(可同时给出用户名/密码,加完即存凭据) | write | | stash | action: list / push / pop / apply / drop | push/pop/apply/drop → write | | console/list | git 命令流水(argv、退出码、耗时、stderr、是否截断) | — | | stage / unstage | git add / git restore --staged | write | | commit | git commit(可 amend);带 paths 时先 git add -- <paths> 再按 pathspec 提交 | write | | checkout / branch/create / merge | 切换或新建分支、合并 | write | | branch/delete | 删除分支(-d / -D) | dangerous | | fetch / pull | 抓取(--prune)/ pull --no-edit(Update Project) | write | | push | 推送:直推 → SSL 后端兜底 → 认证兜底 | push | | credential/approve | 把用户名/密码(Token)存进 git 凭据助手 | write | | discard / revert / reset / cherry-pick / remote/remove | 丢弃改动、回滚提交、重置、拣选、删远程 | dangerous(remote/remove 为 write) |

安装

1. 从 npm 安装(推荐)

$env:Path = "C:\Users\zdz20\AppData\Roaming\npm;" + $env:Path
dsh plugin --profile web add dsh-git-vcs
dsh --profile web --dump-config | Select-String "dsh-git-vcs"   # 必须看到 "# == dsh-git-vcs" 层

装具体版本用 dsh plugin --profile web add [email protected](pnpm 对已存在的依赖会认为"已是最新", 只有带 @版本 才替换 spec)。本机 registry 是 npmmirror,必要时加 --registry=https://registry.npmjs.org。

更新到新版本

已装过旧版本时,用下面任意一条(@latest 最省事,写死版本最可控):

dsh plugin --profile web add dsh-git-vcs@latest        # 或 [email protected] 这种具体版本
dsh --profile web --dump-config | Select-String "dsh-git-vcs"   # 仍应看到 "# == dsh-git-vcs" 层

然后重启 dsh web(host 半区是进程内代码),浏览器里再 Ctrl+Shift+R。

先看自己当前是哪一种 spec,能省一次试错:

$p = "$env:USERPROFILE\.dsh\profiles\web"
(Get-Content "$p\package.json" -Raw | ConvertFrom-Json).dependencies.'dsh-git-vcs'
(Get-Content "$p\node_modules\dsh-git-vcs\package.json" -Raw | ConvertFrom-Json).version

| 当前 spec | 命令 | 结果 | |-----------|------|------| | 任意(0.1.0 精确 / ^0.1.0) | add dsh-git-vcs@latest 或 @<版本> | ✅ 升级,spec 一并改写 | | ^0.1.0(当初用裸 add dsh-git-vcs 装的) | update dsh-git-vcs | ✅ 升到最新的 0.1.x | | 0.1.0(当初用 add [email protected] 装的) | update dsh-git-vcs | ❌ 精确 spec 下 pnpm 不动它 | | 同上 | add dsh-git-vcs(不带版本) | ❌ 「已满足」→ 仍停在旧版本 |

其它注意点:

  • 命令别在插件源码目录里跑:那里的项目级 .npmrc 用 ${NPM_TOKEN},未设该变量时 pnpm 会警告 Failed to replace env in config(无害,但吵)。
  • npmmirror 会滞后几分钟到几小时;npm view dsh-git-vcs version --registry https://registry.npmmirror.com 还看不到新版本时,加 --registry=https://registry.npmjs.org 直接走官方源。
  • 当初若是本地路径装的(link:…),同一条 add dsh-git-vcs@<版本> 会把它换成 registry 版本。
  • dsh.profile.bundles 里已经有 dsh-git-vcs,更新不需要再动它。

2. 本地路径安装(开发调试)

dsh plugin --profile web add D:\zxh\code\git-plugin

3. 改完代码怎么生效

| 改了哪里 | 生效方式 | |----------|----------| | index.js(host)、cordis.patch.yml | 重启 dsh web(bundle 层在进程启动时加载,刷新页面不够) | | lib/client.js(浏览器) | 刷新页面即可 |

期望日志(启动时各一行,缺任意一行说明能力没挂上):

[dsh-git-vcs] host 半区激活:subprocess=就绪 connection=就绪
[dsh-git-vcs] RPC 通道已注册:/git-vcs

4. 跨盘符安装坑(历史/本地开发才会遇到)

只有「本地路径安装」(第 2 种)且插件目录与 profile 不同盘符时才会踩到;从 npm 安装不受影响。

插件目录在 D:,profile 在 C:\Users\zdz20\.dsh\profiles\web。pnpm 对跨盘符的 link: / file: 算不出相对路径,会生成目标被拼错的坏 junction;于是 dsh 的 bundle 对账读不到 node_modules/dsh-git-vcs/package.json,判定 "declares no dsh.bundle",插件只当普通依赖装、 不进 dsh.profile.bundles 层(现象:dsh plugin add 末尾提示 declares no dsh.bundle, --dump-config 里没有自己的层)。

三条可行路线,任选其一:

  1. 手动建正确 junction,再让 dsh 自己对账(推荐,改动最小)
    $p = "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-git-vcs"
    cmd /c rmdir "$p"                                                     # 先删掉坏 junction(rmdir 只删链接)
    cmd /c mklink /J "$p" "D:\zxh\code\git-plugin"
    dsh plugin --profile web install                                       # 对账:自动把 dsh-git-vcs 追加进 bundles
    dsh --profile web --dump-config | Select-String "dsh-git-vcs"
  2. 把插件挪到 C: 盘同一盘符,再 dsh plugin --profile web add <新路径>(例如 %USERPROFILE%\code\dsh-git-vcs)。
  3. 发布成 npm 包后按包名安装(dsh plugin --profile web add dsh-git-vcs),走 registry 就没有跨盘符问题。

清理残留(若第一次安装已经把坏依赖写进 profile 的 package.json):

dsh plugin --profile web remove dsh-git-vcs
# remove 若也失败,手工删链接与依赖条目:
cmd /c rmdir "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-git-vcs"
# 然后编辑 ~\.dsh\profiles\web\package.json,去掉 dependencies.dsh-git-vcs,并确认 dsh.profile.bundles 里没有它

使用方法

右侧栏 tab 条的「+」或引导页胶囊 → 版本管理。首次加入后右侧栏默认页会从「工作区文件」变为 引导页(官方规则:引导入口多于一个时打开引导页),页面上有两个胶囊:工作区文件 / 版本管理。

目录还不是 Git 仓库时

面板只渲染一块居中空态,只有三样东西:Git 图标 + 「当前目录不是 Git 仓库」+ 目录路径, 下面一个 「在当前目录初始化 Git 仓库」 按钮——页签、工具栏、提交树等一律不显示 (那些内容在非仓库下全是空壳),也没有多余的长篇说明。

点按钮先展开初始分支名:可自由输入,另有 main / master 两个一键备选(回车即确认), 点「初始化」才真的执行 git init;成功后空态自动消失、面板自己长回完整功能(不必再点 Refresh)。 分支名之所以要用户过一眼:git config init.defaultBranch 因机器而异(本机就是 master), 替用户决定会得到一个意料之外的分支名。不传 branch 时 host 侧的兜底顺序是 入参 → init.defaultBranch → main。

host 侧的语义边界(repo/init,受 allowWrite 门禁):

| 目录状态 | 行为 | |----------|------| | 不是仓库 | git init -b <branch>;git < 2.28 无 -b 时退回 git init + symbolic-ref HEAD | | 已经是仓库根 | 不重复初始化,返回 already: true(重复点击 / 竞态都安全) | | 在别的仓库内部(子目录) | 拒绝并报 git-vcs/bad-request,避免凭空造出嵌套仓库 |

工具栏

| 控件 | 说明 | |------|------| | 仓库路径 + 「打开仓库」 | 默认取会话工作目录,也可手填绝对路径 | | 分支胶囊 | 显示当前分支(detached 显示 DETACHED),点击跳到 Branches 页;右侧显示 ↑N ↓M(相对 upstream 的领先/落后) | | Refresh | 重新拉一次 repo/snapshot | | Fetch | git fetch --prune | | Update Project | git pull --no-edit | | Push | 推送当前分支(仅 allowPush=true 时可用) | | 凭据 | 随时配置当前 upstream 所属远程的用户名 / 密码(Token) |

Local Changes

按「冲突 / 已暂存(Index)/ 已修改(Working Tree)/ 未跟踪文件」分组,行内状态字母 (蓝=修改、绿=新增、灰=删除、红=冲突);差异默认不显示,点文件才在右侧展开,右上角 × 关闭。

两种「选择」在面板里是分开的、互不影响: 勾选框决定本次提交包含哪些文件(提交走 git add -- <勾选的文件> + git commit -m <信息> -- <勾选的文件>:先暂存再按 pathspec 提交,未跟踪文件也能直接勾选提交, 且忽略索引里其它内容); 「暂存 / 取消暂存」是 git add / git restore --staged,把改动放进或移出索引(Index), 供你在终端或别的工具里按标准 git 工作流使用。MM 状态的文件会同时出现在「已暂存」和「已修改」两个分组里。

  • 差异按行所属分组取:已暂存组走 diff --cached、其余走工作区 diff;未跟踪文件必须带 untracked=true (未跟踪文件不在 index 里,普通 git diff -- <path> 恒为空,只有 host 的 --no-index 分支拿得到"新文件"差异); 命中 .gitignore 的文件不发请求,直接说明没有可展示的差异。
  • 选中项会随快照校验:条目消失(文件被别处提交/回滚)即收起差异面板与底部操作条, 条目只是换了分组(刚点了暂存)则跟随到新分组,避免右侧继续显示已经不存在的差异。
  • 差异面板:行号 gutter、↑/↓ 在改动块之间跳转(当前改动的高亮只画在 gutter 上)、 改动计数、长行自动换行(不出现横向滚动,红/绿底色保留)。

提交区

提交信息、Amend(allowDangerous 控制)、Commit,行内还有「全选 / 全不选」与「清空」。 每一行改动前面的勾选框(零构建下自己画的方块)只切换勾选、不打开右侧差异; 只提交勾选的文件,未跟踪文件同样可选。 没有可提交的改动、或一个文件都没勾时 Commit 置灰(悬停说明原因),不会"点了才报错"。 Amend 语义是修补上一次提交,忽略勾选(按钮文案里已注明)。

Log

列顺序对齐 IDEA:时间 · 提交树 · Message · Author · Commit。

  • 提交树列:多泳道图,画法对齐 IDEA 与 git log --graph——每条分支一条自己的泳道, 分叉/汇合画在真正分叉的那次提交上(基于历史提交拉出来的分支不会被画成当前分支的延续)。 泳道算法:维护「每条泳道下一个期待的提交(= 上一条提交的父提交)」, 没有泳道期待本提交就是分支起点(新开一条泳道),多条泳道同时期待本提交就在本行汇合; 第一个父提交留在原泳道,合并提交的其余父提交优先并进「已经在等这个提交」的那条泳道, 只有真的没有泳道在等它时才新开一条(否则会画出一条向右突出、下一行又折回来的假分叉线, 窗口里若该父提交在下文很远处,那条假泳道还会一路挂到列表底部)。 并入已有泳道时,那条斜线会跨行拉直、画到目标提交的圆点上沿(目标在下面 1–3 行以内时; 更远或不在窗口里则仍按"斜线到本行底 → 目标泳道的竖直段接着走"画)——这样与它汇入同一个节点的 竖直段、汇入斜线全部落在同一个点上;否则几条线会在行交界与节点上沿分别收口,看起来"错开了十几像素"。 已经取全(没有更多可续拉的行)时,末行上仍指向窗口外的连线不再绘制——不留断头线。 圆点用 SVG 真圆(外径 10px、圆环 2px),HEAD 提交实心、其余空心; 泳道颜色按分支名排序取模分配(当前分支优先,跨渲染稳定),圆环用深色、连线用同色系不透明浅色 (不透明才敢让上下两段各越界 1px 重叠消接缝,半透明会叠出更深的色带)。 竖直段用 div 画,斜线用px 精确的横带里的 SVG 画(preserveAspectRatio="none" + non-scaling-stroke):
    • 上带:行顶(-1px) → 圆心 - 4px,画「汇入」斜线(终点落在圆点上沿);
    • 下带:圆心 + 4px → 行底(+1px),画「岔出」斜线(起点落在圆点下沿);
    • 跨行带:圆心 + 4px → 目标提交那一行的 圆心 - 4px(高度 calc(rows*100% - 8px)), 画上面那条"并入已有泳道"的斜线;
    • 竖直段的上行也恰好是 高度 = calc(50% - 3px)(从 top:-1px 起算),与上带共用同一段几何, 下行从 calc(50% + 4px) 起 —— 于是「同一个节点出来的线」端点由构造保证重合在一点。 早先斜线止点用百分比近似(50% ± 18% ≈ 假设行高 28px 的 5px),与竖直段的精确值差 1–2px, 真机上就是"同一节点出来的两条线错开了几像素";换成 px 横带后与行高无关,任何字号下都对齐。 节点右侧标出指向该提交的分支标签(同色系描边 + 淡底,本地实线 / 远程虚线,当前 HEAD 分支加粗, origin/HEAD 符号引用跳过),一眼看出每个分支停在哪次提交;标签区统一对齐在泳道区右侧(不跟着泳道左右跳)。 为免长分支名把整列撑宽:单个标签最长 96px(超出省略号截断,title 仍是完整名)、 一行最多 3 个(更多折成一个 +N);完整的全部分支清单在提交详情里的**「分支 · N」一行**(不截断)。 该列宽度按泳道数与截断后的标签内容自适应(64–240px),表头手柄可拖动覆盖它, 双击手柄恢复自适应(与拖动列宽的交互一致)。
  • 行交互:鼠标移到任意提交行即高亮(零构建下没有 CSS :hover,用状态模拟),光标为 pointer,点击展开该提交详情。
  • 右键菜单(屏蔽浏览器原生菜单):按功能分组并带分组标题与分隔线,支持多级子菜单 (父项 hover 或点击展开,按可用宽度自动向左翻转;点菜单外 / Esc 关闭):
    • 查看:展开提交详情
    • 复制:复制完整 ID / 短 ID / 提交信息 / 作者与邮箱
    • 分支 ▸:基于此提交新建分支(自动展开详情并聚焦分支名输入框)、检出此提交(分离 HEAD)、 把此提交合并到当前分支
    • 修改历史(危险) ▸:拣选(Cherry-Pick)、回滚(Revert)、重置到此提交 ▸(Soft / Mixed / Hard,Hard 走二次确认)
    • 受 allowWrite / allowDangerous 门禁的项自动置灰禁用。
  • Commit 列:显示短哈希,直接点击短哈希即复制完整提交 ID(无独立按钮)。 复制结果走右下角浮动提示条(绝对定位、不参与流式布局,出现/消失不会顶动高度造成抖动;2 秒后自动消失, 失败为红底);提交详情里的完整哈希同样可点。
  • 列宽:时间 / 提交树 / Message / Author / Commit 五列表头右侧都有拖动手柄(col-resize,pointer capture, 不依赖 window 监听),拖动范围各自钳制(92–360 / 56–320 / 120–900 / 60–280 / 64–220px; 提交树列双击手柄回到自适应); Message 列默认吃掉剩余宽度,被拖动后改为固定宽度,总宽超出面板即横向滚动。
  • 范围与懒加载:列表覆盖所有分支(本地 + 远程)的节点,不只是当前分支——host 侧走 git log --exclude=refs/stash --all,stash 的提交被挡在外面(它有独立页签)。 一页 50 条,滚动到底自动续拉下一页(--skip 接着上一页的窗口取,按 hash 去重), 最多渲染 2000 行;刷新(含自动刷新)按「已加载条数」一次拉回,滚到深处不会因为刷新弹回第一页。 列表底部的续拉提示行本身可点(面板很高、50 行撑不出滚动条时的那条入口)。 刻意不用 --date-order / --topo-order:这两种排序要先把整段可达历史读进内存排序, 大仓库上首屏会明显变慢,而默认的反向时间序是流式的——它才是「按需加载」成立的前提。

提交详情

默认不显示,点条目才展开;面板在 Log 下方(上下布局),顶边有拖拽条可改高度 (默认 300px,上拖变高,钳制 140–720px 且不超过容器 80%),右上角 × 关闭。

详情内含 Revert / Cherry-Pick / Reset --soft / Reset --hard、「新分支名」→ 基于该提交建分支(不切换), 以及变更文件列表——默认不展示任何文件的差异,文件行有 hover 高亮、光标 pointer, 点某个文件才按需拉取该文件的 diff(show/file 端点),再点一次收起。 点提交时只取元数据与文件列表(show + noPatch),不再一次性传输整次提交的 patch; 文件差异区同样有行号 gutter、↑/↓ 改动块跳转与改动计数。

Branches

本地 / 远程分支。当前分支用首列 * + 绿色加粗分支名标识(不做"选中"背景高亮——这里没有选中态), 行只在 hover 时变色、光标为默认箭头(操作都在行内按钮上,行本身不可点)。

远程分支列表会过滤符号引用——refs/remotes/origin/HEAD 的 %(refname:short) 会退化成 origin, 不过滤就会和真正的 origin/main 一起显示成两条。

每行操作:本地分支 推送(有 upstream 时 git push <remote> <branch>;没有 upstream 时按钮变成 「推送并设 upstream」,走 git push --set-upstream <remote> <branch>,remote 取自该分支的 upstream, 缺省 origin)、切换、合并、删除;顶部还有「新建并切换」。推送与工具栏 Push 同一道门禁(allowPush)。

Remotes

git remote -v 的全部远程与 fetch / push 地址;顶部表单可填名字 + URL(+ 可选 push URL)→ Add Remote, 并且可顺手填用户名 / 密码·Token(可选)——填了会在加好远程之后一并存进凭据助手 (保存失败只警告,不影响远程已经加好的事实)。每个远程行有 Remove 危险按钮(点击走内联二次确认)。

Stash(贮藏)

git stash 的入口 —— 把当前未提交的改动整体收起来、工作区回到干净状态,之后用 「应用(apply,保留记录)/ 弹出(pop,取回并删除记录)/ 删除(drop)」处理; 与 Local Changes 里的「暂存」不是一回事(那是 git add 放进索引)。

  • 当前实现用 git stash push [-m 说明],不带 -u:未跟踪的新文件不会被收走,会留在工作区。
  • 列表里的引用是 stash@{n} 数字选择器(由列表下标合成,与 git 编号一致)——不能用 %gd 配 --date=iso-strict,那会生成 stash@{2026-09-15T…} 时间戳选择器,而 git 对它会打印 Dropped …、 退出码 0,却什么也不删;host 现在还会核对操作前后的贮藏条数,把这种静默失败变成明确错误。

Console

面板发起的每条 git 命令(argv、退出码、耗时、stderr、是否截断)。

反馈与状态

  • 所有提示(写操作结果、复制反馈、失败与 git 报错)统一走右下角浮动 toast(绝对定位、不参与流式布局), 没有顶部错误横幅;失败的完整命令与 stderr 可在 Console 页回看。
  • 底部状态栏只在忙碌时出现,显示当前阶段(读取仓库 / 暂存中 / 提交中 / 刷新列表…)。
  • 所有等待都有文案:顶部 2px 进度条 + 阶段文案 + 分区占位(读取差异… / 读取提交详情… / 读取远程仓库…)。

性能约定(改动前先读)

面板流畅度取决于「客户端→宿主往返次数」与「git 进程创建次数」,两者在 Windows 上都是几十到上百毫秒级:

  • 一次刷新只发一次 RPC:repo/snapshot 在 host 侧把 status / log / for-each-ref(本地+远程合并) / stash / 版本 / remote 六项并发探测后一次返回(含 Console 流水)。旧实现要 6 次往返、约 16 次串行进程创建, 单次刷新 1.5–2.5s;现在是 1 次往返、6 次同波进程。
  • 缓存:git 可执行文件路径(resolveExecutable)、仓库根(rev-parse --show-toplevel,按路径)、 git --version、remote.origin.url 各只取一次;init 会写回仓库根缓存。

推送与认证

推送不是"配好就一直能推"或"永远推不了",而是按下面这条链路逐级处理:

  1. 直接推:仓库已配好 remote 就直接 git push(当前分支到它的 upstream),或按分支行推指定的 git push <remote> <branch>。
  2. SSL 后端兜底:若失败原因是 schannel 拿不到凭据上下文(SEC_E_NO_CREDENTIALS 一类), 自动换 -c http.sslBackend=openssl 重试一次(第一次失败不会改动远端,重试是安全的), 成功后 Console 里能看到这一次用了 openssl。也可用请求参数 sslBackend 固定后端。
  3. 缺认证信息:失败被归类为 git-vcs/auth-required 时,面板弹出认证表单 (分支页顶部一行:用户名 + 密码/Token + 「保存并推送」)。提交走 credential/approve:
    • 用 git credential approve 把凭据交给你机器上的凭据助手保存(store → ~/.git-credentials, 或 manager → Windows 凭据管理器),存成主机级(https://user:[email protected]), 所以同一主机下的仓库以后都不用再填;保存后用 git credential fill 回读校验, 因为 approve 即使没人接收也返回 0;
    • 密码只走 stdin,不进 argv;凭据类命令在 Console 流水里 argv 会脱敏、输出显示为"已隐藏";
    • 若助手不可用(例如受限环境里 msys sh 起不来),结果里 stored: false,此时凭据记在本进程内存中, 本次会话内的推送会用内嵌 URL 的兜底方式完成(同样脱敏)。
  4. 其余失败:网络不通、被拒(push-rejected)、无权限等原样报错(右下角 toast + Console 页可回看原文), 插件不猜、不重试。

添加远程时就能认证:Remotes 页的 Add Remote 多了可选的「用户名 / 密码·Token」, 对齐 IDEA「添加新远程时进行认证,认证通过就全局存起来」,避免"先加远程、再推一次、失败了才填账号"。

SSH 远程不做认证:git@host:owner/repo.git 走密钥/agent,插件不参与;失败按第 4 条报错。

allowPush 默认就是 true(与 IDEA 一致:配好 remote 就能直接推)。不想让插件碰远端时, 在 cordis.patch.yml 里置 false,Push / 推送按钮会置灰并说明原因。 沙箱环境里凭据助手可能起不来(sh.exe: couldn't create signal pipe),此时第 3 条的会话内存兜底就是主要通路。

配置

插件行的 config(见 cordis.patch.yml,改后触发热替换):

| 字段 | 默认 | 说明 | |------|------|------| | allowWrite | true | 关闭后所有写操作(暂存/提交/分支/贮藏…)直接报 git-vcs/write-disabled | | allowPush | true | 是否允许 push;置 false 时按钮禁用并提示 | | allowDangerous | true | 是否允许 reset / revert / cherry-pick / 删分支 / 回滚文件 | | gitPath | '' | git 可执行文件绝对路径,空 = 从 PATH 解析 | | repoRoot | '' | 限定可操作的仓库根;空 = 允许任意会话工作目录 | | diffContextLines | 3 | 统一 diff 上下文行数(钳制 0–200) | | maxOutputBytes | 2097152 | 单条命令输出内存上限(超出保留尾部并标记截断,钳制 4096–64MiB) | | timeoutMs | 120000 | 单条命令超时(钳制 1s–30min) | | autoRefreshSeconds | 0 | 面板自动刷新间隔,0 = 关闭(钳制 0–3600) | | consoleLimit | 200 | 命令流水保留条数(钳制 10–2000) |

本插件刻意零运行时依赖:host 半区不 import 任何 @deepseek-ai/* 值,subprocess / connection / webServer 都通过 inject 声明 + ctx.get() 读取;因此配置没有走 Schemastery Config schema, 而是在代码内 normalizeConfig() 合并默认值并做范围钳制(未知键忽略、类型不符即用默认值)。

安全边界

  • git 一律以 argv 形式执行(ctx.subprocess.spawn,不经 shell),路径统一放在 -- 之后,无注入面。
  • cwd 必须是绝对路径、存在且是目录;默认还要求它是 git 工作树(rev-parse --show-toplevel); 配置 repoRoot 后限定在其之下。
  • 执行环境禁用交互提示(GIT_TERMINAL_PROMPT=0 / GIT_ASKPASS=echo / SSH_ASKPASS=echo), 避免凭据提示挂起。
  • 破坏性操作在 UI 内需二次确认(内联确认条),并有 allowWrite / allowDangerous / allowPush 三级开关。
  • RPC 路由自带信任栅栏:复用 ctx.connection.requestRejection(req)(Host/Origin 校验 + 浏览器会话认证), 未通过返回 401 / 403。

已知限制(v1)

  • 提交树是多泳道图(每条分支一条泳道、分叉/汇合画在真正的分叉提交上),但: tag 暂不画标签芯片,因此「只被 tag 指向」的提交会以无标签节点出现; 续拉出更多提交后如果又冒出别的分支,泳道区会变宽(整表重排一次列宽),已显示行的泳道位置不变。 单次刷新最多渲染 2000 行(更早的历史靠滚到底续拉,超过上限后停止续拉)。
  • 差异视图是统一 diff 文本,没有并排 diff、没有按 hunk/行勾选提交(Partial Commit)。
  • git stash push 不带 -u,未跟踪文件不会被收走(面板暂无开关)。
  • Remotes 页增删远程后不会自动 fetch:track 关系已写进 .git/config,是否抓取由用户在工具栏点 Fetch。
  • 没有 changelist 分组、没有 Shelf、没有多 VCS root、没有编辑器 gutter 标记(DSH 无编辑器面板可挂)。
  • 没有文件系统监听:自动刷新依赖 autoRefreshSeconds 或手动刷新。
  • 状态只在内存:刷新页面即重置(与官方右侧栏一致)。
  • 初始化只做 git init,不会顺带建 .gitignore / README / 首次提交(IDEA 会问是否加这些)。
  • 空态下如果 allowWrite=false,按钮置灰并提示原因(初始化属于写操作)。

自检与排障

| 脚本 | 用途 | |------|------| | node scripts\verify-host.mjs [仓库] | 不需要挂 profile,直接在宿主域内跑全部 host 端点(真实 git、临时仓库)。覆盖解析结果、错误码门禁、allowPush 门禁、相对路径/非仓库拒绝、未跟踪文件 diff、勾选提交的 pathspec 回归、提交树覆盖所有分支 + skip 分页不串页、repo/init 全部分支(建仓库 / 幂等 / 指定分支 / 非法分支名 / 嵌套拒绝 / 未出生分支 oid 归一 / allowWrite 门禁),最后再从 lib/client.js 的字面量反查客户端调用的每个端点在 host 侧都存在等 64 条断言 | | node scripts\render-probe.mjs | 浏览器半区的离线自检:本机没装 react,脚本自带一个迷你 React 把面板真挂起来,跑「打开仓库 → 切 Log → 滚到底续拉 → 刷新」,断言提交树渲染出其它分支独有的提交、泳道拓扑(tip 上方不画线、分支各有泳道、斜线画在真正的分叉提交上、上下行竖直段与斜线带共用同一段 calc(50% ± 4px) 几何)、续拉按 hash 去重、刷新不缩回一页(15 条断言,视觉部分仍需真机目测) | | node scripts\render-probe.mjs --real [仓库] | 追加与参考实现对照的检查:把真实仓库的提交喂给同一份客户端代码渲染,再与 git log --graph 逐提交比对泳道下标与分叉行位置(两边用同一批提交、同一顺序),不一致就报出来 | | node scripts\dom-probe.mjs | 浏览器半区的真机几何自检:起 headless Chrome/Edge 渲染真实 lib/client.js(夹具数据),通过 CDP 取回渲染后的像素几何,断言泳道竖线与圆点同心、斜线带边界精确落在「圆心 ± 4px」与行高上、同一个节点出来的线端落在同一高度(容差 0.75px);另有一组合并线回归夹具(拓扑照抄 certbase-offline:合并的第二父提交早被主干泳道期待)断言合并线直接并进主干泳道、泳道数不虚增、没有"一个节点都没有"的泳道、末行没有断头线、单标签 ≤ 96px、一行最多 3 个标签 + +N、提交树列宽 ≤ 240px、提交详情「分支」一行列出全部分支、跨行的合并斜线落在目标节点的圆点上沿;并单独加载一份「非仓库」快照断言空态行为:功能内容一条都不渲染、空态块铺满且按钮居中、无多余长文案、点按钮先展开分支名(默认 main、备选 main/master、点备选能改输入值)、确认后才发 repo/init 且带上所选分支、成功后功能面板自己长回来(33 条断言)。先 npm install --prefix .npm-cache/domprobe [email protected] [email protected];缺浏览器/React 会自动跳过。沙箱禁止命名管道(Chrome 的 mojo IPC 会直接 FATAL),需放宽权限或沙箱外运行 | | node scripts\dom-probe.mjs --repo <仓库> | 追加真实仓库模式:把该仓库 --all 的整棵提交树喂进同一份客户端代码,在真浏览器里逐行量「没有节点的泳道」「向右突出又折返的合并线」「提交树列宽 ≤ 240px」「同一节点出来的线端落在同一高度」「合并的跨行斜线落在目标提交的圆点上沿」(再 +5 条断言,并会打印被检查的节点清单)。例:node scripts\dom-probe.mjs --repo D:\zxh\code\certbase-offline | | node scripts\graph-sim.mjs <仓库> [条数] [old\|new] | 泳道算法离线仿真(纯 ASCII,不起浏览器):把真实仓库的提交喂给旧/新两版布局算法,逐行画出泳道并统计「无节点泳道」「外突折返尖角」。定位合并线画法问题时用它最快 | | node scripts\preview-check.mjs | 在 node:vm 沙箱里装载真实 host 半区,按 host-runner 的 cloneJson 规则校验每个端点的信封是否无损 JSON,并确认 RPC 通道注册成功 | | node scripts\status-probe.mjs [仓库] | 用插件自己的 status / diff 读当前工作区,逐条打印 index/worktree 标记与三种 diff 长度——排查「列表说改了、差异却是空」 | | node scripts\web-rpc-probe.mjs [插件目录] | 在隔离的 DSH_HOME 里用完整 web 组合(base + web-app + 本插件)起临时实例(端口 3199),抓 host 日志并对 /git-vcs 做免认证探测:401 = 路由在(与 /api 一致)、405 = 路由不在(被静态兜底接手)。不传目录用当前源码;传目录可以验证"别人从 npm 装到的那份",例如 node scripts\web-rpc-probe.mjs "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-git-vcs" | | node scripts\live-probe.mjs [--keep] | 真机联调探针(诊断工具,不判红):在隔离 DSH_HOME 里起真实例(端口 3211),用 headless Chrome 打开真实页面(启动日志里带 token 的 URL),打印插件启动行、页面概览与「版本管理」相关的可交互元素。装配层问题(标准 props、会话作用域、认证后的 RPC 通路)只有真实例才暴露,而这类链路的 DOM 自动化(首启弹层 → 会话界面 → 展开右侧栏 → 点开 tab)做成硬断言会长期假红,所以只输出供人工判读 |

浏览器半区的布局/视觉由 scripts\dom-probe.mjs 用 headless 浏览器量像素来守(泳道几何、圆点同心、 斜线带边界与竖直段端点重合); 配色与列宽拖拽的手感仍需真机目测。数据流转由 scripts\render-probe.mjs 覆盖。

面板请求全部失败,报 HTTP 405:说明 /git-vcs 路由没挂上——查启动日志里有没有 subprocess=就绪 connection=就绪 与 RPC 通道已注册:/git-vcs。缺失的原因是插件行的 inject 没声明全(少了 webServer 会直接抛 cannot get property "webServer" without inject,整棵树加载失败), 或用 inject: [] + 一次性 ctx.get() 在服务就绪前就激活了。可直接用 scripts/web-rpc-probe.mjs 复现。

面板能打开、却一直显示「(未加载)」并提示"填写仓库路径后点「打开仓库」":说明客户端没拿到 会话工作目录(不是 host 通道的问题——repo/snapshot 压根没发出去)。DSH 0.1.7 起会话清单快照 SessionListState 只有 ids / byId / phase / projectionsBySession:0.1.5 时代用来定位"当前会话" 的 current 字段已被移除,改由 session 作用域 slot 把当前会话作为标准 prop sessionId 交给组件 (官方 dsh-client-ui-sidebar-files 在 0.1.7 里就是 useSessions((s) => s.byId[sessionId]?.cwd))。 lib/client.js 的 sessionCwdOf() 以 sessionId 为准、旧版本回退 current;改动这里时两个探针夹具 (render-probe.mjs / dom-probe.mjs)的标准 props 必须一起照抄真机形态,否则会出现"自检全绿、 真机红"(0.1.2 修的就是这一条)。

卸载

dsh plugin --profile web remove dsh-git-vcs

RPC 通道是怎么挂的(维护者向)

浏览器侧用 ctx.connection.rpc.call('/git-vcs', endpoint, payload);host 侧自己注册一条 webServer 的 prefix 路由 /git-vcs,线格式与 Connection RPC 一致 (请求 {type:"client-request",rpcId,method,payload} → 响应 {type:"server-response",rpcId,result}), 并复用 ctx.connection.requestRejection(req) 做信任栅栏(Host/Origin + 浏览器会话认证)。

不用官方那两个入口的原因(@deepseek-ai/dsh 0.1.5-rc.1 实测):

  • connection.rpc.handle(channel, handler):它把路由注册到 connection 服务自己 ctx 的 webServer 上, 而 web-app 的 connection 行只 inject: [webRuntime],任何第三方调用都会 cannot get property "webServer" without inject,整棵插件树加载失败。
  • connection.rpc.intercept('/api', …):官方推荐的共享通道,但是单占位—— api-gateway 已经占了,再注册会抛 already has an interceptor。

host 行的 inject 必须是真实存在的三个服务:subprocess(跑 git)、connection(信任栅栏)、 webServer(注册路由),模块级 export const inject 与插件行级 inject 都要写 (行级 inject 管模块级管不到的那一层)。早期用 inject: [] + 一次性 ctx.get() 会在提供方就绪前激活, 日志表现为 host 半区激活:subprocess=缺席 connection=缺席 → 通道没挂上 → 浏览器侧 405。

开发期曾用动态 Cordis 插件(gitvcs-3)做真机预览,但它依赖 harness.handle / host.call, 与正式版的 webServer 通道不是同一条路,已由本地链接安装取代。