Repository navigation
Releases: alanhaskey/dsh-gitmate
Release list
GitMate 1.15.0
图片和 PDF 现在可以直接在面板里看。
之前为什么看不到
图片和 PDF 没有补丁可读 —— git 只报告 binary —— 所以点开这类文件原来只有一句 binary file; contents omitted,等于没有内容。
现在
点开这类文件时,面板直接读出工作区里的当前文件来显示:
| 支持 | png jpg jpeg gif webp avif bmp ico svg pdf |
| 显示方式 | 图片用 <img>(SVG 也走 <img>,脚本不会执行),PDF 用 <embed> |
| 变更行 | 照旧在预览下方显示,两者不冲突 |
| 上限 | 单文件 6 MB(base64 之后更大,不该把一个 40 MB 的截图塞进面板) |
只看工作区那一份。 已提交的那份要经 shell 取回,二进制会被当文本损坏——所以只暂存、或已删除的文件会明说没有可预览的内容,而不是显示坏图。
blob: URL 在替换和卸载时都会 revoke —— 一个 object URL 会一直持有整个文件。
写测试时抓到一个真 bug
仓库内的绝对路径被误判为越界。
git rev-parse 报的是解析后的根(/private/var/…),而拼出来的路径是 /var/… —— /var 是指向 /private/var 的符号链接,于是词法比较判定"逃出仓库",一个完全合法的路径被拒。
现在两边都先 realpath 再比较。这个 bug 只在符号链接系统上出现(macOS 的 /var、/tmp 都是),光看代码不会发现。
验证
node test/smoke.mjs # 647 项断言新增 13 项:PNG 的 MIME 与字节往返一致(不是"返回了什么",是"解回来还是原文件")、PDF/SVG 类型、文本被拒、越界被拒、缺失文件与目录的处理、6 MB 上限(含体积提示)、面板路由可达,以及渲染层断言预览标题确实出现在屏幕上。
GitMate 1.14.0
支持一个目录下的多个仓库。
症状
工作区根目录常常只是"一堆项目的容器" —— 它自己不是仓库,下面有 alpha/、beta/。这种目录下"是不是仓库"对每一个项目都答"不是",于是面板只会提示初始化,看不见下面已经有的仓库。
现在
打开这样的目录,面板会先找仓库:限深 3 层、跳过 node_modules / vendor / dist / build 等、最多 40 个,列出名字和相对路径让你选。选中之后整个面板就操作那一个 —— 状态、提交、分支、历史、忽略规则、AI 审阅全部一起跟随。
顶部会出现一个仓库标签,点它回到列表。
几个刻意的地方:
| 情况 | 行为 |
|---|---|
| 根目录本身就是仓库 | 只列出它自己,不把里面的子模块当成可选项 |
.git 是文件(worktree / 子模块指针) |
也算一个仓库根 |
| 找到仓库后 | 不再往下钻 |
| 根目录下一个仓库都没有 | 才显示原来的初始化卡片(多一个"在根目录新建"的次要选项) |
安全边界
请求可以带 repo 指定用哪个仓库,但只接受会话目录之内的路径:
/etc→ 忽略,回落到会话目录../..爬出去 → 忽略,回落到会话目录
这两条有测试。工具体也一样 —— Agent 传一个子目录,git rev-parse 自己会找到仓库根。
验证
node test/smoke.mjs # 634 项断言新增 15 项:发现(worktree 指针 / 跳过规则 / 深度边界 / 根目录是仓库时只列自己)、路由列出、根目录不是仓库、指定一个后按它作答、外面的路径和爬出去的路径被忽略、以及一次真实操作确实落在选中的仓库里。渲染守卫也新增了"多个仓库"这条界面路径,断言屏幕上出现的是仓库名字而不是"这里还不是仓库"。
顺带修的测试缺陷
渲染测试原本用硬编码的下标取面板的状态槽 —— 这次插入两个 state 就让所有下标指错了槽位,而测试只是"没抛错"。现在改成按名字取(slotOf),名字写错会直接抛。
GitMate 1.13.0
这一版不改功能,只让包可以直接发布到 npm —— 因为新机器从市场安装时报了:
git 无法在无人值守的情况下完成 SSH 认证。如果你的 SSH key 设了密码,请用 ssh-agent(ssh-add)…
这不是插件的 bug,是那个网络的 bug
我读了市场(dshmarket)的安装代码,路径是:
installTargetFor(entry)
→ entry.npm (若有)
→ entry.tarball (若有)
→ github:owner/repo ← 你的条目走这条
而 github: 由 pnpm 解析成 codeload 的 HTTPS tarball,SSH 只是 HTTPS 失败之后的兜底。所以在那台机器上是 codeload 先失败,才落到 SSH,又被市场的 BatchMode=yes 快速失败 —— 报的是这个组合。
佐证:这个包没有安装脚本、没有依赖,装的时候只是解一个 tarball;那条 tarball URL 从别的网络访问是 200 / 162 KB。所以插件内容不可能是原因。
手边的绕法
# 走 github.com 的 HTTPS,而不是 codeload(不同的网络路径)
dsh plugin --profile desktop add "git+https://github.com/alanhaskey/dsh-gitmate.git#v1.13.0"或者按市场自己的建议配 ssh-agent / GIT_SSH_COMMAND。
长期修法:发布到 npm(本次已备好)
市场的条目格式里不允许手写 npm: 字段(contributing 明说会被校验拒绝)—— 它从 registry 自动采集映射,所以发布是唯一途径。发布之后安装走 registry,不再碰 GitHub 与 SSH。
已确认就绪:
| 项 | 状态 |
|---|---|
repository / homepage / bugs |
已指向本仓库(市场关联的硬要求)✓ |
npm 包名 dsh-gitmate |
registry 上是空的 ✓ |
prepublishOnly |
已加:发布前先跑 617 项断言 ✓ |
npm pack --dry-run |
16 个文件、117.6 kB,只有 lib/、locale/、icon、cordis.patch.yml、两个 README、LICENSE ✓ |
发布本身需要账号凭证,命令是:
cd /path/to/dsh-gitmate
npm login
npm publish # 有 2FA 时加 --otp=XXXXXX发布后市场会自动把条目关联到 npm,之后所有安装都从 registry 走。
GitMate 1.12.1
上一版改完就发了,我一次都没看过它长什么样 —— 结果基本没生效。这次先写了个渲染器,用同一套几何生成 SVG 转成 PNG 看了一眼,才看到真问题。
一、每行之间固定留一条缝
每行的图形画在一个 24px 高的固定盒子里,而一行实际高得多(展开提交时更高)—— 线条只覆盖每行顶部 24px。
上一版我用"把线画出盒子 5px"来补 ✗ 真实缝隙有十几到上百像素,5px 毫无意义。
现在:竖直段是钉在行底的元素(top + bottom:0),行多高它就有多长;曲线只出现在节点上下的一小段里。行高再怎么变都接得上。
(渲染器里把首行拉到 130px、其余 26px 验证过 —— 线笔直穿过整个展开区。)
二、收敛的泳道从不释放
一个提交可以同时是两条泳道的父提交(merge 的两边都指向它)。buildGraph 只释放被认领的那条,其余永远等一个已经发出的提交,在图上拖着一条没有提交的长线 —— 这才是"悬空"最直观的来源。
现在: 认领之后把其余等价泳道一并释放,它们在这一行收进节点(画 merge 曲线)。
顺带
- 根提交不再把自己的线画到行底
- 线条分三档粗细:本提交自己的线最重、分支曲线次之、路过的泳道最轻
验证
node test/smoke.mjs # 617 项断言测试重写为新的不变量:
- 收敛的泳道被释放、最后一行
after为空 - 每条
bottom:'end'的竖线对应的泳道确实延续到下一行("跑过死泳道"即为错) - 每行继承的泳道必须从
top:0起(否则出接缝) - 根提交停在节点、分支从节点下方起笔并由曲线接上
- 真仓库 merge 后:有曲线、无游离线、无接缝
把"释放收敛泳道"那几行删掉,三条断言立刻报错 —— 与渲染器里看到的一致。
GitMate 1.12.0
提交图(左侧的树)重画。两个具体缺陷,都不是"审美"问题:
一、上下接不上
每一行是一个 只有 24px 高的 SVG,而这一行还有 4px 上下内边距 + 1px 下边框。线条画到 SVG 边缘就停了 —— 两行之间固定留一条 9px 的缝。
现在穿过某行的泳道一律画到行外(从 -5 到 29),盖住这一行自己的内边距和边框,下一行再从 -5 接上。相邻两行因此是重叠的,而不是相接的。
二、多分支时是悬空竖线
某个提交新开出的父分支(merge 的第二父),原来只画了一条从行中到行底的竖线,和节点之间没有任何连接 —— 看起来就是断的。
现在:
| 情况 | 画法 |
|---|---|
| 泳道穿过此行 | 竖直,画到行外 |
| 此行新开出的父分支 | 从节点出发画曲线(branch) |
| 泳道在此结束 | 从行首曲线收进节点(merge) |
顺带:线条分层次
原来是所有线一样灰一样粗 —— 多分支就是一片栅栏。现在同一色相三档:
- 本提交自己的线最重(
border-l2,1.8px) - 分支 / 汇聚曲线次之(
label-secondary62%) - 只是路过的泳道最轻(38%,1.3px)
全部用已发布的主题 token 调制,不引入新颜色。
架构:几何搬到 Host
新增纯函数 graphSegments(row),history 端点把每行的 {kind, lane, from, to, d} 随数据下发,面板只负责画。
这样"相邻行是否接上"就从只能靠看变成了可以断言的数字。
验证
node test/smoke.mjs # 616 项断言新增 13 项。核心是一条属性断言:遍历每对相邻行,凡是在两行都出现的泳道,上一行必须画到 ROW_HEIGHT+pad、下一行必须从 -pad 起。
还包括:真仓库里做出一次 merge,断言历史里确实有 branch 段、且每条泳道都接得上。把 pad 改回 0,四条断言立刻报错。
顺带修了测试自己的两处问题:fixture 里 merge 的参数写成了 source(应为 branch),merge 静默失败而我从未断言它成功;以及断言辅助函数把 history 的行对象当成了段数组。
GitMate 1.11.2
上一版修了"历史按钮该禁",但设置点进去看不到 —— 这个版本修的是后半截。
症状
面板显示「这里还不是 Git 仓库」,设置按钮可以点 ✓,但页面没变:初始化卡片还挡在那儿。
原因
主区域的组装顺序是:
不是仓库 → 渲染初始化卡片
是仓库 → 按 view 渲染(设置 / 历史 / 改动…)
于是设置视图在非仓库下永远不渲染 —— 按钮亮了,页面没变。
修法
设置渲染在初始化卡片之上。 理由是设置属于插件而不是仓库,而且 .gitignore 方案在仓库存在之前就有用(gitignorePlan 本来就是照着纯目录设计的,不是照着仓库)。
验证
node test/smoke.mjs # 604 项断言这次的断言不看源码,看屏幕:渲染守卫现在会收集渲染出来的文本节点,然后断言:
- 「不是仓库」→ 屏幕上有初始化卡片的那句话 ✓
- 「不是仓库 + 请求设置」→ 屏幕上有设置内容,且没有那句话 ✓
把旧组装放回去,断言立刻报:
FAIL and asking for settings shows settings, not the initialise card
— "notRepo stashes 1 wip now stashPop stashApply notRepo notRepoHint init"
为什么这个 bug 上次没被测出来:之前的断言只检查"渲染不抛错",而初始化卡片确实渲染得好好的——错的是它和谁一起渲染。收集屏幕文本之后,这类"点击没反应"的问题第一次变得可测。
(写这条断言时我先用 notRepo 判断卡片,但 hero 里的分支位置在非仓库时也显示 notRepo,太粗 —— 改用只在卡片里出现的 notRepoHint。测试数据也有两处欠说明,一并修了。)
GitMate 1.11.1
不是仓库时,顶上的按钮该禁的禁、该留的留。
修的问题
面板明明显示「这里还不是 Git 仓库」,上面的分支图表(历史)按钮却还能点 —— 点进去只有一个空视图。
同步按钮早就有 || !status?.isRepo 的守卫,历史按钮漏了。
改动
| 按钮 | 不是仓库时 |
|---|---|
| 同步 | 禁用(原本就对) |
| 历史(分支图表) | 现在禁用 |
| 设置 | 刻意保持可点 |
设置不加守卫是有意的:面板不按预期工作时,人第一个想去的就是设置 —— 把它一起禁掉,等于在最需要的时候锁门。
顺带:仓库在面板开着时消失(切换了文件夹、.git 被删掉)会让视图退回改动列表,而不是让历史按钮亮着一个显示不出来的页面。
验证
node test/smoke.mjs # 601 项断言新增 3 项断言:历史按钮的 disabled 必须包含 status?.isRepo、设置按钮的 disabled 必须仍是那个单纯的 busy 判断(这条是防回归的关键 —— 它保证后人不会"顺手统一"把设置也禁掉)、以及退回逻辑存在。
渲染守卫新增两条路径:「不是仓库 + 请求历史视图」和「不是仓库 + 请求设置视图」——前者正是这个 bug 的状态。
GitMate 1.11.0
跨平台收尾。先说清查结论:真正跟平台相关的只有两处,都在 Host 侧,其余全部与平台无关。
清查结果
| 项 | 状态 |
|---|---|
| 命令引号 | PowerShell 单引号翻倍 / POSIX 闭合-转义-重开,按 process.platform 选择 ✓ |
| 环境变量 | 走 shell 请求的 env 字段,而不是 POSIX 的 VAR=value 前缀 ✓ |
| 路径 | 全部 node:path ✓ |
| 写文件 | 只写操作的那个仓库里的 .gitignore;lib/ 之外不写任何东西 ✓ |
| 执行位 | 不写任何需要执行位的文件 ✓ |
git init |
-b <branch>,与平台无关 ✓ |
| 沙箱识别 | DSH 那段拒绝文本三平台一致,所以识别逻辑本来就通用 ✓ |
这次补的两处
1. 沙箱起不来时的建议,按平台给。
原来一律说"Windows 上最常见的原因是 %TEMP% 不可写"——对 Linux 用户是错的指引。现在:
- Linux → 缺 bubblewrap,或内核不支持 Landlock
- macOS →
sandbox-exec不可用 - Windows →
%TEMP%不可写 - 其它平台 → 中性建议
(createHostText 接受 platform 参数,可注入,所以四个分支在一台 macOS 上就能全部测到。)
2. git 装不上时提示去确认 PATH —— 容器或刚装好的系统上这是最常见的原因。
验证
node test/smoke.mjs # 598 项断言新增 13 项,其中最关键的一组是恶意文件名端到端:往仓库里放一个同时含空格、单引号、$、分号的文件名,然后 stage → status → commit → diff 全走一遍。
这是引号唯一会以**"数据丢失"**而不是"报错"形式出问题的地方,而且三个平台共用同一条代码路径——所以它值得一个端到端断言,而不是只测 quoteFor 的字符串输出。
另外四个平台分支各自的建议、以及"每个分支都保留原始原因"都有断言。
GitMate 1.10.2
这一版不是修那个 Windows 问题的 —— 那台机器上的根因(DSH 自己跑在 Low 完整性级别,什么都写不了)与插件无关,下面是核对结果和一个小改进。
先核对两件事
1. v1.10.1 标签是正确的。 它指向 96665ca9d9ad,该提交树里的 package.json 版本正是 1.10.1。所以 …#v1.10.1 拉到的是对的版本。
2. 插件在安装和激活时什么都不写、也不跑 git。 我查过 apply():没有 writeFileSync、没有 mkdirSync、没有任何 git 调用——它只注册工具和路由。dependencies: null,scripts 只有 test。所以"装上插件"这个动作本身不可能触发沙箱、也不可能导致进程变成 Low 完整性。
这一版改了什么
有一件事确实是我的:面板每 5 秒探测一次仓库,而每次探测都会重走一遍沙箱启动,每一次都以同样的方式失败。
而那台机器上的证据显示,失败的那次 provisioning 会让整次运行的写权限退化——也就是说反复触发不只是浪费,可能还在加重问题。
现在 status 带回 unavailable 时轮询就停下,错误卡片上的「重试」是唯一的再次尝试入口:由你决定什么时候重试,而不是面板每 5 秒替你决定一次。
(用了一个 ref 存这个标记,避免为了读它而把 interval 拆掉重建——那会让每次状态更新都重置计时器。)
验证
node test/smoke.mjs # 585 项断言新增 3 项:轮询确实在 unavailable 时返回、错误卡片带重试按钮、运行指示仍然存在。
升级路径(在 Low 完整性修好之前装不上任何东西)
dsh plugin add 连 profile 里的锁文件都创建不了(package.json.lock → EPERM),所以它失败在 pnpm 启动之前,与插件内容无关。顺序是:
- 彻底退出 DSH Desktop(含托盘图标,任务管理器确认没有残留进程),然后从开始菜单正常启动——不要从任何 agent/沙箱的终端里启动它;
- 在 DSH 里跑
whoami /groups,确认是 Medium Mandatory Level(现在是 Low); - 在普通 PowerShell 里(不要在 DSH 内部)执行升级;
- 完整重启 DSH,再刷新页面。
第 1 步是前提:它没修好之前,装什么都装不上,面板的写操作也仍然会失败。
GitMate 1.10.1
修一个让插件在 Windows 上一安装就无法使用的问题。
症状
打开面板就是:
无法运行 git
could not run git here: sandbox mode "workspace-write" is requested but no sandbox backend is usable on this host … Runner failure: windows-acl-run: EPERM: operation not permitted, mkdtemp 'C:\Users\…\AppData\Local\Temp\dsh-XXXXXX'
根因:面板没有跟随会话的权限
DSH 的解析顺序是:
mode = 请求里的 mode ?? 该 session 的 sandbox/mode 覆盖 ?? 部署默认值
工具调用在 1.9.0 已经跟随会话了,但面板那条路径没有 ✗ ——它的每个请求都带着 sessionId,却没有拿它去解析策略,于是永远落到部署默认值 workspace-write。
而这台机器的 workspace-write 后端起不来(mkdtemp → EPERM,%TEMP% 不可写)——于是面板一个 git 命令都跑不了,而且用户在自己的会话里把权限设成什么都不管用 ✗
修法
面板的会话不是猜的:它就是这个会话的标签页,cwdForSession(sessionId) 一直用它解析仓库目录。现在同一个会话也用来解析权限,dispatcher 在开头选一次 core,所有端点自然沿用。
于是:把会话切到 danger-full-access(或修好 %TEMP%),面板立刻能用。
(这也修正了早先一份任务文档里的建议——它说"HTTP 请求没有 session,只能用部署默认值"。但面板的请求是带 sessionId 的,而且已经用它定位仓库;用同一个会话解析权限既不是猜测,也正是"策略属于调用方"该有的样子。)
验证
node test/smoke.mjs # 583 项断言新增 3 项:面板请求确实带上会话的策略、确实是从那个会话解析的、没选过模式的会话按文档的优先级回落到部署默认值。
顺带修了测试替身的一处失真:假的 session 对象没有 id,而真实的 Session 有 id,策略服务正是读它。补上之后断言立刻从 { mode: 'workspace-write' } 变成会话真正的 danger-full-access —— 也就是说,这个 bug 差点因为替身失真而测不出来。