diff --git a/CHANGELOG.md b/CHANGELOG.md index 648b51b..895940a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,41 @@ ## [Unreleased] +## [0.0.22] - 2026-10-24 — Graph 过滤持久化与激活指示 · Branches 就地更新 · 开发依赖升级 + +### Added + +- **Branches 视图分支就地更新(不 checkout)**:右键单个分支 **Update Branch**、多选批量更新、标题栏 Pull 按钮升级为选区感知(无选区保持原 Pull 语义,Commit 视图 Pull 不受影响)。底层原语 `git fetch refs/heads/:refs/heads/`——git 原生 fast-forward-only,diverged / ahead-only 分支被安全拒绝(附真实 stderr 原因),**永不改写本地提交**(src 侧全限定防 `+` 前缀上游名被解析为 force);当前分支自动改走 `repo.pull()`(含冲突处理通道);无上游分支透明跳过;无 remote 配置提示 `No remote repository configured` 提前返回(不误报 no upstream)。五桶对账汇总(updated / already up to date / pulled / skipped / failed,选中数 = 各桶之和),up-to-date 经前后 `objectname` 短 sha 前缀比对判定(fetch 静默 no-op 无输出;缩写长度随对象数伸缩,全等比对会误报)。upstream 解析不信任树节点数据(API 路径 `refToRaw` 不填 upstream/head),统一以一次 `for-each-ref` 取新鲜快照;`origin/x` 拆分复用 `resolveRemoteBranch` 最长前缀匹配(remote 名可含 `/`)。engine 纯逻辑 `planBranchUpdates` / `formatBranchUpdateResult` / `classifyFetchOutcome` 落于 `engine/ref/branch-update.ts`,全量单测覆盖。 + +### Fixed + +- **Graph 视图过滤条件不再随重启 / 切库丢失(全部 7 个维度按仓库持久化)**:authors、File Path、Grep、Merge Mode、Date Filter、Message Regex 此前仅存于内存,重启 VS Code 或切换仓库后即被清空。现在沿用 scope/dmode 的按仓库 workspaceState 范式持久化——authors 独立 key(`hyperGit.log.authors:`),其余 6 维组合 key(`hyperGit.log.filter:`,单 key 原子读写防半恢复态,dateFrom/dateTo 经 ISO 串往返),重启与切库往返后自动恢复为上次设置;过滤生效期间标题栏过滤图标点亮为填充态(Graph Filter Active,`$(filter-filled)`,经 `hyperGit.log.filterActive` context key 与常态图标互斥呈现)——恢复的过滤不再是无声约束;重开「Filter by Author…」即预勾选已选 exact 作者并继续增删(pattern 项不参与预勾选、重开后不可见,确认新选择会将其移除);空选确认与 Clear Filter 的「清除」同样持久化,重启不复活;脏持久化数据经 engine 纯函数逐字段校验免疫。**附带行为变更**:① SCM 变更列表右键「Show History」由整替过滤器改为与其余过滤命令一致的增量式——文件历史现与活跃过滤相交,而非静默清空后者(M3 期遗留不一致);② 任何过滤对话框(Filter Graph 聚合入口 / Author / Path / Grep / Merge Mode / Date / Message Regex)或 Commit 文件右键菜单打开期间发生切库后确认,不再把旧仓库语境的选择误写新仓库(rootAtStart 切库守卫全覆盖);③ issue #107 的「切库清空 host 级过滤」限制消除——每个仓库记忆自己的完整过滤条件;④ Filter by Path 选中当前仓库外的文件(多根工作区选他仓文件 / 单仓导航到仓外)时以告警拒绝,不再落入 `../…` 垃圾 path 过滤——该垃圾 path 会随持久化成为跨重启的图加载 fatal 错误态。 + +### Changed + +- 开发依赖升级(#153):eslint 10.12 → 10.13、typescript-eslint 8.71 → 8.72、vitest 5.0.4 → 5.0.5、ovsx 1.2 → 1.3——全部 devDependencies,对扩展运行时零影响。 + +## [0.0.21] - 2026-10-23 — Filter by Author 候选建议 · 多选过滤 · 元字符作者修复 + +### Added + +- **「Filter by Author…」升级为作者候选建议 + 多选过滤**:候选经 `git shortlog -sne` 按当前 Graph 视图 scope 取数(按提交数降序、姓名/邮箱均可模糊匹配),输入前缀(如 `Thr`)即在下拉中建议对应提交者,勾选即选、无需完整手输;依次勾选多个提交者翻译为多条 `--author`(git 取 OR)直接过滤 Graph 视图,空选确认 = 清除该维度、Esc = 取消不变。输入不等于任何作者姓名时动态插入「Use pattern」逃生口(原样正则透传),完整保留旧 InputBox 的任意 pattern 能力。engine 侧 `LogFilter.author` 单值改为 `authors: AuthorTerm[]`(exact 身份 / pattern 正则 tagged union,单一字段单一事实源);`scopeRefs` 提取为 scope → 引用范围参数的单一事实源(Graph 取数与作者候选共享);adapter 侧为仓库首个 `createQuickPick` 动态用法(busy 取数、勾选自维护回写、settle 幂等守卫、切库竞态守卫、空仓库降级)。新增 `engine/log/author-suggest.ts`(shortlog 取数参数 + 容错解析,current scope 显式 `HEAD` 防 stdin 挂起)。 + +### Fixed + +- **作者名含正则元字符时过滤 0 命中**:`git log --author` 缺省 POSIX BRE(非 ERE),旧实现把手输作者名原样作为正则传递——`dependabot[bot]` 中的 `[bot]` 被解释为字符类,匹配 0 条(本仓库实测:不转义 0 命中)。现在从候选列表选中的作者身份按 BRE 语义转义(仅 `\ . [ * ^ $`;`+ ? | ( ) {` 在 BRE 中是普通字符,加反斜杠反成操作符)并锚定全行 `^Name $`(同名异邮箱不混淆)。 + + +## [0.0.20] - 2026-10-17 — Delete Remote Branch 陈旧远程分支修复 · 删除失败可诊断 + +### Fixed + +- **Branches 视图「Delete Remote Branch」对陈旧远程分支误报失败且无原因**:PR 合并后远端自动删除的分支,本地 remote-tracking ref 在下次 `fetch --prune` 前一直保留,视图仍列出;右键删除时 `git push --delete` 被服务端拒绝(`remote ref does not exist`),且 `catch {}` 吞掉真实报错,只回显笼统的「N failed」。现在「服务端已无此 ref」被识别为目标态已达成——按成功收尾并清理本地陈旧 tracking ref(通知如实标注 *already gone on remote*);真失败(保护分支拒绝 / 认证 / 网络)则经 stderr 优先提取,在通知中逐分支附上真实原因,并联动 Show Output 查看完整输出。engine 纯逻辑 `isRemoteRefMissingError` / `formatRemoteDeleteResult` 落于 `engine/ref/remote-ref.ts`,全量单测覆盖。 + +### Changed + +- **错误文本提取单一事实源(`gitErrorText`)**:新增 `engine/git-state/git-error.ts` 统一「非空白 stderr 优先 → message → String 兜底」提取语义,`remote-commands` / `history-commands` 两处内联拷贝改为引用;Push 对话框 / Update Project / Merge 对话框的失败通知随之升级为 stderr 优先(真实原因直达,不再只显通用串)。 + ## [0.0.19] - 2026-10-17 — Branches 视图 PR 状态徽标 · 取数安全加固 · 开发依赖升级 ### Fixed diff --git a/docs/.agents/issue.md b/docs/.agents/issue.md index d1e3284..a6f6b55 100644 --- a/docs/.agents/issue.md +++ b/docs/.agents/issue.md @@ -164,3 +164,19 @@ - **处理方式**:webview state v3 在 `byRepo` 分区新增 `draft`(message + 三勾选);输入/勾选变更即时 `saveState()` 落盘(消除 debounce 尾丢);回灌仅限「重建首帧/切换仓库」两时机且先于 `reconcileChecked`(避免 state 推送往返覆盖当前编辑);提交成功清空草稿。未采用 `retainContextWhenHidden`(钉死整个 webview 内存,且不跨窗口 reload)。 - **后续防范**:webview 内用户编辑态一律显式入 `setState`(含 debounce 之外的即时落盘点);回灌逻辑必须有时机守卫(一次性/切换触发),并注意「先回灌 DOM、后执行任何从 DOM 取数回写 state」的顺序。 - **同类问题影响**:所有自绘 WebviewView 承载编辑器的扩展;凡输入经防抖上报宿主的模式都存在尾丢窗口。 + +## #19 Branches 视图 Delete Remote Branch 对陈旧远程分支删除失败且无原因提示 + +- **表因**:右键删除 `origin/rsi/fixture-terminology` 等远程分支直接报 `Deleted 0 remote branches, 1 failed: …`,无任何失败原因,此类分支「经常」删不掉。 +- **根因**:复合缺陷——① PR 合并后 GitHub 自动删除远端分支,本地 `refs/remotes/origin/...` 在下一次 `fetch --prune` 前一直陈旧,Branches 视图(数据源 `repo.state.refs`)仍列出它;② 对陈旧 ref 执行 `git push origin --delete ` 被服务端拒绝(git 2.54 实测 stderr:`error: unable to delete 'x': remote ref does not exist`);③ `remote-commands.ts` 删除循环的 `catch {}` 把 execGit 已透传挂载的 stderr 整个吞掉,最终只回显笼统的「N failed」,不可诊断。即「目标态已达成却按失败收尾」+「真失败也无原因」两个 Bug 叠加。 +- **处理方式**:删除循环改三桶(成功 / 服务端已无此 ref / 真失败)——「服务端已无此 ref」经 engine 纯逻辑 `isRemoteRefMissingError`(stderr 优先,匹配 `remote ref does not exist` / `delete of non-existent ref` 变体)识别,按达成收尾并把本地陈旧 tracking ref 纳入既有 `branch -D -r` 清理;真失败经 stderr 优先的 `gitErrorText`(`engine/git-state/git-error.ts` 单一事实源)提取原因,由 `formatRemoteDeleteResult` 汇总为通知(失败走 `showGitError`,联动 Show Output 看完整 stderr)。确认弹窗/受保护主干硬阻断/上游软警示逻辑不动。 +- **后续防范**:① 批量操作的 `catch` 必须保留可行动的失败原因(execGit 拒绝值已挂 `.stderr`,消费即可),`grep -rn 'catch {' src/adapter/` 应逐个确认非吞错;② remote-tracking ref 与服务端存在时滞,任何「对远程分支的操作」都要考虑「本地有、服务端无」的陈旧态——删除/比较/PR 匹配等场景遇到 `does not exist` 类报错应先判陈旧再判失败;③ 视图列表的「幽灵分支」根治靠 `fetch --prune`(Prune Remotes 命令已有),删除路径的容错只是补救。 +- **同类问题影响**:所有列出 remote-tracking ref 并提供远程删除的 Git GUI;凡把服务端拒绝一概当失败、不区分「目标态已达成」的实现,以及任何丢弃 stderr 的批量 CLI 包装。 + +## #20 Graph 视图 Filter by Author 过滤选择随重启丢失(未持久化) + +- **表因**:Graph 视图「Filter Graph… → Filter by Author…」多选作者后,重启 VS Code(或切换仓库再切回)过滤被清空,需从头重选。 +- **根因**:过滤状态仅存于 `LogWebviewProvider` 内存字段 `private filter: LogFilter = {}`,`setFilter/clearFilter` 只改内存不落盘;切库 handler 还会显式 `this.filter = {}`。同文件的 scope/dmode 早有按仓库 workspaceState 持久化范式(`loadRepoScopedPrefs`),filter 维度从未接入。另有一处硬冲突:`hyperGit.showHistory` 用 `setFilter({ path })` 整替 filter 对象(M3 期遗留,早于其余 5 个过滤命令统一的 `{ ...f, <维度> }` 增量范式),若接入持久化会静默清空已保存的 authors。 +- **处理方式**:① `engine/log/log-filter.ts` 新增纯函数 `parsePersistedAuthors`(JSON 串 → `AuthorTerm[]`,元素级剔除脏项,永不抛异常);② `LogWebviewProvider` 新增 `authorsKey(root)`(`hyperGit.log.authors:`,同 scope/dmode 命名范式)与唯一写入点 `persistAuthors`(`setFilter/clearFilter` 汇聚,`update(key, undefined)` 即删 key,空选清除同样持久化);恢复并入 `loadRepoScopedPrefs`(构造期 + 切库共用),切库语义由「全清」改为「清非持久维度 + 恢复新仓库持久值」;③ `showHistory` 改增量式 `setFilter({ ...getFilter(), path })`,与其余过滤命令同构;④ Review 收敛补充:`pickAuthors` settle 增切库守卫(rootAtStart 同范式——环境驱动切库不夺焦点、QuickPick 不隐藏,取数窗口外确认会把旧仓库勾选经 persistAuthors 误写新仓库持久化 key),候选集外的持久化 exact 作者以 ghost 项预勾选呈现(selectedItems 只能来自 items,scope 已切换 / shortlog 失败降级时缺项者确认即被静默丢出持久化 key),`multi-root-repo-selection.md` 已知限制同步新切库语义,wiring 级测试(`history-commands-filter-wiring.test.ts`)钉住 showHistory 增量式与 settle 守卫;同构守卫同步应用于 `logFilterPath`(对话框 await 期间切库会按新仓库 root 求出跨仓库垃圾相对路径——预存竞态,收敛门范围外观察,经用户裁定收口;越仓选择同产 `../…` 垃圾 path 且经持久化升级为跨重启粘性污染——增 rel 路径段级越仓校验(`'..'` / `'../'` 前缀;较 showBlame 的 `startsWith('..')` 不误杀仓内 `..` 前缀文件)+ 告警拦截,wiring 测试补越仓与不误杀用例);⑤ 其余 6 个过滤维度(path/grep/mergeMode/dateFrom/dateTo/messageRegex)经用户决策按同范式扩展持久化:组合 key `hyperGit.log.filter:`(单 key 原子读写防半恢复态),dateFrom/dateTo ISO 串往返,engine 层 `serializePersistableFilter/parsePersistedFilter` 逐字段校验(归一化:空串、mergeMode 'all' 不落盘),issue #107 的「切库清空 host 级过滤」限制随之消除(multi-root 文档已知限制同步移除,按仓库状态表与导语同步补录过滤 key);增量 Review 复核后同范式守卫补齐 grep/mergeMode/date/regex 四命令、commit 文件菜单(`handleFileMenu`——菜单 await 期间切库时 change 属旧仓库语境,任何动作不派发)与聚合入口 `logFilter`(其 Clear Filter 项直接执行,await 期间切库会删切换后仓库的持久化 key),全部过滤对话框与 Commit 文件菜单的 await 窗口至此收口(Graph 提交行右键菜单 `handleCommitMenu` 为预存未守卫路径,不在本条收口范围);wiring 测试同步补齐 date/regex(正常 + 切库守卫 + 清除分支)与 mergeMode 正常路径用例;⑥ 经用户决策采纳 Review 改进建议:过滤激活态可见指示——engine 纯函数 `hasActiveFilter`(与持久化归一化同口径:空串 / mergeMode 'all' / 空 authors 不算生效)驱动 `hyperGit.log.filterActive` context key(setFilter/clearFilter/loadRepoScopedPrefs 三处同步),标题栏过滤图标常态(`$(filter)`)/填充(`$(filter-filled)`)双命令互斥呈现(detailTree/detailFlat 同范式,`logFilterOn` 与 `logFilter` 共用聚合 handler)。 +- **后续防范**:① 视图 UI 状态凡期望跨会话存活,一律挂到既有的按仓库 memento 范式(key 带 repoRoot、读时校验、写时 undefined 删 key),勿新造存储;② 新增过滤维度命令必须沿用 `{ ...getFilter(), <维度> }` 增量式,整替 filter 会静默摧毁其他维度(可 grep `setFilter({` 检查是否带 spread);③ 持久化读写要过脏数据校验纯函数(engine 层),adapter 只做编排;④ 跨 await 的确认回调(QuickPick settle 等)写仓库语境数据前复核 repoRoot 未变——持久化把瞬态竞态升级为跨重启的粘性污染。 +- **同类问题影响**:其余 6 个过滤维度已随后续决策按同范式扩展持久化(见处理方式⑤);所有「内存态 + 仓库语境」的视图组件在接入持久化前都存在同样的重启丢失。 diff --git a/docs/.agents/knowledge-map.md b/docs/.agents/knowledge-map.md index 86993fb..e33a24e 100644 --- a/docs/.agents/knowledge-map.md +++ b/docs/.agents/knowledge-map.md @@ -25,11 +25,12 @@ - [Claude Code 配置](../docs/features/claude-code-config.md) — Agentic Git 预置:`hyperGit.claudeCode.executablePath` 设置 + `~/.claude/settings.json` 快捷入口(原生设置 + 命令;纯路径逻辑沉淀 `engine/agent/`)。 - [Agentic Git 偏好配置](../docs/features/agentic-git-preferences.md) — Agentic Git 预置:`hyperGit.agent.*` 四项(baseBranch + commit/createPr/review Preferences;多行文本域 + 内置默认模板,Commit 模板中文规约已英译)。 - [多根工作区仓库切换](../docs/features/multi-root-repo-selection.md) — Graph 标题栏切换仓库图标按钮 + `hyperGit.selectRepository` 切换全局活跃仓库:七视图级联跟随、changelist/收藏/Shelf/最近消息按仓库隔离、三重顺序不变量与安全迁移。 +- [分支就地更新](../features/branches-update-in-place.md) — Branches 视图不 checkout 更新本地分支:`git fetch refs/heads/:refs/heads/` 原生 fast-forward-only,右键/多选/标题栏选区感知三入口,五桶对账汇总,引擎排程 + 适配层执行。 - [实施状态总览](../docs/milestones/implementation-status.md) — M0-M5 交付记录 + API 限制 + M5 AI 设计 + 验证/发布(**实施看板**)。 - [工程实施方案](../docs/architecture/engineering-plan.md) — 路径 B 架构 + M0-M5 里程碑(**开发蓝图**)。 - [Git 功能矩阵](../docs/requirements/idea-feature-matrix.md) — 56 功能点 / 8 组(**验收基线**,参考 IDEA 等成熟实现)。 - [调研报告](../docs/research/README.md) — SCM 集成 / 工程蓝图 / 发布 CI / AI 接缝四路循证报告。 -- [发布说明](../releases/README.md) — 各正式版 Release Notes(GitHub Release 正文单一事实源;最新 [v0.0.19](../releases/v0.0.19.md))。 +- [发布说明](../releases/README.md) — 各正式版 Release Notes(GitHub Release 正文单一事实源;最新 [v0.0.22](../releases/v0.0.22.md))。 ## 图资产(docs/assets/) - [Mermaid 源图索引](../assets/mermaid/README.md) — 全项目 Mermaid 源图统一管理 + **溯源矩阵**(mermaid ↔ archify ↔ 嵌入文档的唯一权威映射)。 diff --git a/docs/README.md b/docs/README.md index 2456e5f..a1cad00 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,9 +13,10 @@ - [Log 提交详情面板](./features/log-commit-detail-panel.md) — 点击提交行于图右侧打开常驻详情面板(上半 Changed Files、下半提交信息);面板可见性 ⟺ 选中态,替代原悬停浮层。 - [Claude Code 配置](./features/claude-code-config.md) — Agentic Git 预置:Claude Code 可执行路径设置 + `~/.claude/settings.json` 快捷入口(原生设置 + 命令;M5 前置铺垫)。 - [Agentic Git 偏好配置](./features/agentic-git-preferences.md) — Agentic Git 预置:Base Branch + Commit / Create PR / Review 三项 agent 指令偏好(多行文本域 + 内置默认模板;M5 前置铺垫)。 +- [分支就地更新](./features/branches-update-in-place.md) — Branches 视图不 checkout 更新本地分支(fetch refspec 原生 fast-forward-only):右键 / 多选批量 / 标题栏选区感知三入口。 ## 发布说明 -- [Release Notes 目录](./releases/README.md) — 各正式版发布说明(GitHub Release 正文单一事实源);最新:[v0.0.19 Branches 视图 PR 状态徽标 · 取数安全加固 · 开发依赖升级](./releases/v0.0.19.md)。 +- [Release Notes 目录](./releases/README.md) — 各正式版发布说明(GitHub Release 正文单一事实源);最新:[v0.0.22 Graph 过滤持久化与激活指示 · Branches 就地更新 · 开发依赖升级](./releases/v0.0.22.md)。 ## 调研报告(循证依据) - [02 · VS Code SCM API 与 vscode.git 集成路径](./research/02-vscode-scm-integration.md) — 路径 B 决策依据、SCM 稳定/proposed API 边界、changelist 模型映射。 diff --git a/docs/features/branches-update-in-place.md b/docs/features/branches-update-in-place.md new file mode 100644 index 0000000..4912a2e --- /dev/null +++ b/docs/features/branches-update-in-place.md @@ -0,0 +1,44 @@ +# 分支就地更新(Update Branch Without Checkout) + +> Branches 视图中**不 checkout 即可把本地分支更新到其远程版本**:右键单个分支 Update、多选批量 Update、标题栏 Pull 按钮升级为选区感知(无选区时保持原 Pull 语义)。底层原语 `git fetch refs/heads/:refs/heads/`,git 原生 fast-forward-only,diverged / ahead-only 分支被安全拒绝,**永不改写本地提交**。 + +## 动机 + +对齐 IntelliJ IDEA 的 `GitUpdateSelectedBranchAction`:多分支并行开发时,让非当前分支跟上远程(如同步他人已合并 PR 的分支)此前必须 checkout → pull → checkout 回来;标题栏 Pull 也只作用于当前分支。本特性把「更新分支」从「必须身处该分支」中解耦。 + +## 核心原语:fetch refspec 而非 pull + +| 场景 | 行为 | +|---|---| +| 非当前分支、落后远程 | `git fetch origin refs/heads/feature/foo:refs/heads/feature/foo` → 本地 ref fast-forward,工作区零扰动 | +| 非当前分支、已同步 | fetch 静默 no-op(exit 0)→ 事后 sha 对比归入 `already up to date` 桶 | +| 非当前分支、diverged / ahead-only | git 拒绝(`! [rejected] ... (non-fast-forward)`)→ 失败桶,**无 force 通道** | +| 当前分支 | fetch refspec 必被 git 拒绝(`refusing to fetch into branch '…' checked out at '…'`)→ 排程为 `repo.pull()`(含冲突处理通道) | +| 分支被其他 worktree checkout | git 拒绝 → 失败桶附 stderr 原因 | + +副作用收益:带显式 refspec 的 fetch 同步刷新 remote-tracking ref,视图 ahead/behind 随之归真。refspec src 侧全限定 `refs/heads/`:`+` 开头的合法上游名(如 `origin/+foo`)裸写时首 `+` 会被 git 解析为 force 前缀,静默绕过 fast-forward-only。 + +## 三入口与目标解析链 + +单一命令 `hyperGit.branchUpdate`("Update Branch",`$(arrow-down)`),目标解析链:**右键实参 →(无实参的标题栏路径)`branchesView.selection` → 无选区等价 Pull(委托既有 `hyperGit.pull`)**;选区非空但不含本地分支(仅远程/标签/分组节点)时不动作——显式选区优先,不回退 Pull。 + +- **右键**(`view/item/context`,`viewItem == hyperGit.branch`,组 `1_branch@1.7`):单选作用于点击项;多选作用于整个选区(when 子句无 `!listMultiSelection`,与批量删除同契约),谓词过滤掉远程/标签节点,「点击在选区之外」以点击项为准(复用 `collectBranchRefs`)。当前分支节点同样显示 Update——路由到 pull,与 IDEA 行为一致。 +- **标题栏**(`view/title`,`navigation@3`,原全局 Pull 位):按钮读 Branches 视图选区(仅远程/标签/分组节点时不动作);Commit 视图的 `hyperGit.pull` 保留原语义,跨视图零影响。 + +## 分层:引擎排程、适配层执行(单一事实源) + +- **引擎纯逻辑** [`engine/ref/branch-update.ts`](../../src/engine/ref/branch-update.ts)(零 vscode 依赖、Vitest 覆盖): + - `planBranchUpdates(names, refByShort, headName, remotes)` 把选中短名排程为逐分支动作——`pull-current`(head 命中,fetch 必败故改走 pull)/ `fetch`(携带 remote、upstreamBranch、beforeSha)/ `skip-no-upstream`(无上游或上游无法归属已知 remote)。upstream 短名(`origin/x`)拆分复用 [`resolveRemoteBranch`](../../src/engine/ref/remote-ref.ts) 最长前缀匹配(remote 名可含 `/`)。 + - `formatBranchUpdateResult` 把五桶(updated / upToDate / pulled / skipped / failed)汇总为通知文案与级别——零计数段省略、失败附真实原因(stderr 优先、折叠单行、超 4 条截断),保证「选中数 = 各桶之和」可对账。 + - `classifyFetchOutcome` 以前缀语义比对前后短 sha 归类 updated / upToDate——`%(objectname:short)` 缩写长度随仓库对象数自动伸缩,批量 fetch 落库新对象可能使缩写变长,严格全等会把 up-to-date 误报为 updated;after 缺失(并发删除)保守计 updated。 +- **适配层** [`adapter/remote-commands.ts`](../../src/adapter/remote-commands.ts):命令编排。**不信任树节点数据**——API 路径的 `refToRaw` 不填 `upstream/head`,一律先以一次 `git for-each-ref`(`FOR_EACH_REF_FORMAT` + `parseForEachRef`)取全量新鲜快照再排程;逐分支执行(per-item try/catch,`branchDelete`/`deleteRemoteBranch` 批量范式),pull-current 失败先走 `handleGitConflict` 展示冲突 UI 但仍计入失败桶;事后一次 for-each-ref 经 `classifyFetchOutcome` 以 `objectname` 前后对比区分 updated / upToDate(fetch 静默 no-op 无输出可解析;读取失败保守计 updated)。 + +逐分支而非合并 refspec(`git fetch origin b1:… b2:…`):合并执行丢失逐分支错误归因——任一 refspec 被拒即整命令失败且 stderr 混排,无法落桶。 + +## 边界 + +- **detached HEAD**:标题栏无选区兜底时警示(对齐 push 的 detached 提示),不盲跑。 +- **无 remote**:`remotes` 为空(未配置/元数据未就绪)→ 提示 `No remote repository configured` 提前返回(对齐 pushDialog / deleteRemoteBranch 先例),不把有上游分支误报 `no upstream`。 +- **陈旧上游**:上游指向已删除的 remote(remotes 非空时)→ `resolveRemoteBranch` 返回 null → skip 桶透明提示 `no upstream`。 +- **快照竞态**:分支刚被并发删除 → 快照缺失 → skip 桶(不 crash、不误执行)。 +- **进度**:`runWithProgress`(Window 位置,非阻塞);execFile 无 kill 通道,不可取消(`task-progress.ts` 既有约束)。 diff --git a/docs/features/multi-root-repo-selection.md b/docs/features/multi-root-repo-selection.md index 0c51898..7e2fb4b 100644 --- a/docs/features/multi-root-repo-selection.md +++ b/docs/features/multi-root-repo-selection.md @@ -1,6 +1,6 @@ # 多根工作区仓库切换(Multi-root Repository Selection) -> 多根工作区(multi-root)含多个 Git 仓库时,**Graph 标题栏出现「切换仓库」图标按钮**(`$(repo)`,对标 Git Graph 的 repo selector;仓库路径常驻 `WebviewView.description` 副标题),Command Palette 亦可直达。切换的是**全局活跃仓库**:Graph / Branches / Commit(changelist)/ Stash / Shelf / Worktrees / 未提交角标**全部视图跟随**,changelist 分配、分支收藏、分支分组偏好、Shelf 存储、最近提交消息**按仓库隔离记忆**,重开窗口回到上次操作的仓库。 +> 多根工作区(multi-root)含多个 Git 仓库时,**Graph 标题栏出现「切换仓库」图标按钮**(`$(repo)`,对标 Git Graph 的 repo selector;仓库路径常驻 `WebviewView.description` 副标题),Command Palette 亦可直达。切换的是**全局活跃仓库**:Graph / Branches / Commit(changelist)/ Stash / Shelf / Worktrees / 未提交角标**全部视图跟随**,changelist 分配、分支收藏、分支分组偏好、Shelf 存储、最近提交消息、Graph 过滤条件**按仓库隔离记忆**,重开窗口回到上次操作的仓库。 ## 交互形态 @@ -51,6 +51,7 @@ | 分支分组偏好 | `hyperGit.branchesGrouping:${repoRoot}` | 同上 + context key 同步 | | Shelf 存储 | `shelves/./` | 目录随 `service.repoRoot` **动态求值**(零快照零双源) | | 最近提交消息 | `hyperGit.recentCommitMessages:${repoRoot}` | 动态 key + 旧全局 key 回落一次(平滑迁移) | +| Graph 过滤条件(authors + path/grep/mergeMode/date/regex) | `hyperGit.log.authors:${repoRoot}`、`hyperGit.log.filter:${repoRoot}` | `loadRepoScopedPrefs` 换 key 重载(构造期 + 切库,engine 纯函数校验脏数据) | | Graph 视图状态(scope/选中/详情模式) | webview `vscode.getState()` v2 `byRepo` 分区 | `graphData` 到达时按 `repoRoot` 换装载 | | Commit 勾选集/视图模式 | 同上 | 同上(勾选是相对路径集合,跨仓库本就错位) | @@ -69,6 +70,5 @@ ## 已知限制 -- 仓库切换时 Graph 的 host 级过滤条件(author/path/grep 等)清空——过滤条件是旧仓库语境的产物,跨仓库无意义; - 多仓库历史混仓 Shelf 数据归属迁移时的当前活跃仓库(无法按仓库归因); - CI 状态按 commit hash 缓存(跨仓库理论碰撞概率可忽略,行为正确)。 diff --git a/docs/releases/README.md b/docs/releases/README.md index 449f050..b055c17 100644 --- a/docs/releases/README.md +++ b/docs/releases/README.md @@ -8,6 +8,9 @@ | 版本 | 说明 | 日期 | |---|---|---| +| [v0.0.22](./v0.0.22.md) | Graph 过滤持久化与激活指示 · Branches 就地更新 · 开发依赖升级 | 2026-10-24 | +| [v0.0.21](./v0.0.21.md) | Filter by Author 候选建议 · 多选过滤 · 元字符作者修复 | 2026-10-23 | +| [v0.0.20](./v0.0.20.md) | Delete Remote Branch 陈旧远程分支修复 · 删除失败可诊断 | 2026-10-17 | | [v0.0.19](./v0.0.19.md) | Branches 视图 PR 状态徽标 · 取数安全加固 · 开发依赖升级 | 2026-10-17 | | [v0.0.18](./v0.0.18.md) | Commit / Branches 标题栏重构 · 批量 Discard Changes · 分支级 Push · 双市场发布重启 | 2026-09-10 | | [v0.0.17](./v0.0.17.md) | Log 提交详情常驻面板 · VS Code 1.136 最佳实践对齐 · 稳定性与体验修复 | 2026-09-09 | diff --git a/docs/releases/v0.0.20.md b/docs/releases/v0.0.20.md new file mode 100644 index 0000000..3b1c10a --- /dev/null +++ b/docs/releases/v0.0.20.md @@ -0,0 +1,55 @@ +# Hyper Git - Agentic Git v0.0.20 — Delete Remote Branch 陈旧远程分支修复 · 删除失败可诊断 + +> 本版核心:**修复 Branches 视图「Delete Remote Branch」对「服务端已不存在」的远程分支误报失败、且删除失败时不显示任何原因的问题**。PR 合并后被 GitHub 自动删除的分支,在下次 `fetch --prune` 前会以陈旧 remote-tracking ref 的形式留在 Branches 视图中;对它执行删除曾被服务端拒绝并回显笼统的「N failed」。本版把这类分支识别为「目标态已达成」——按成功收尾、就地清理本地陈旧 ref;真正的失败(保护分支拒绝 / 认证 / 网络)则在通知中逐分支附上 git 的真实报错,并可直达 Show Output 查看完整输出。 + +--- + +## ✨ 亮点速览 + +- **陈旧远程分支不再误报失败**:删除「服务端已无此 ref」的分支(典型:PR 合并后远端自动删除)时,通知如实标注 `N already gone on remote (pruned local tracking ref)` 并同步清理本地陈旧 tracking ref,视图中的「幽灵分支」就地消失。 +- **删除失败可诊断**:真失败不再只回显笼统的「N failed」——保护分支拒绝、认证失败、网络错误等真实原因(stderr 优先)逐分支附在通知中,超长折叠单行、超 4 条截断标注;配合 **Show Output** 按钮直达 Hyper Git Console 查看完整输出。 +- **错误文本提取单一事实源(`gitErrorText`)**:新增 engine 纯函数统一「非空白 stderr 优先 → message → 兜底」提取语义;Push 对话框 / Update Project / Merge 对话框的失败通知随之升级为 stderr 优先,真实原因直达。 +- **受保护主干与既有防护不变**:main / master 硬阻断、当前分支上游软警示、删除前 modal 确认等既有行为全部保留。 + +--- + +## 🧩 特性详述 + +### 1. Delete Remote Branch 三桶语义(#147) + +删除循环按结果三桶处理,通知如实计数: + +- **成功**:`Deleted remote branch origin/foo`(单条)/ `Deleted N remote branches`(多条); +- **服务端已无此 ref(already gone)**:`git push --delete` 被服务端以 `remote ref does not exist` 拒绝时,经 engine 分类器 `isRemoteRefMissingError`(匹配 git 2.x 规范文案及旧服务端变体,大小写不敏感)识别——该分支的目标态(服务端无此分支)已天然达成,按成功收尾并清理本地陈旧 tracking ref;通知单列 `N already gone on remote (pruned local tracking refs)`,不以误导性的「Deleted 0 …」开头; +- **真失败**:保护分支拒绝(如删除远端当前分支)、认证 / 网络错误等——失败原因经 `gitErrorText` 提取(stderr 优先),通知逐分支附 `branch (原因)`;失败级通知联动 **Show Output** 查看完整 stderr。 + +### 2. 错误文本提取单一事实源 `gitErrorText`(#147) + +- 新增 `engine/git-state/git-error.ts`(零 vscode 依赖,全量单测):统一 CLI 通道(`execGit` 透传的 `.stderr`)与 vscode.git API 通道(GitError)的提取语义——非空白 stderr 优先(trim 后返回),回退 message,最终 `String(e)` 兜底; +- `remote-commands` / `history-commands` 的两处内联实现改为引用该单一事实源;Push 对话框 / Update Project / Merge 对话框失败通知由通用 message 升级为 stderr 优先(如「无上游」「non-fast-forward」等失败原因直接可读); +- 存量 adapter 中约 13 处旧式内联 `errMsg`(只读 message)为预先存在的迁移债,将在后续版本逐步迁移,不影响本版行为。 + +--- + +## ⚙️ 升级与兼容 + +- **无破坏性变更**:全部既有命令、视图与配置项保留;删除确认弹窗、受保护主干(main / master)硬阻断、当前分支上游软警示行为与 v0.0.19 一致; +- **行为改进**:仅删除分支场景的通知文案与失败路径变化(更诚实、更可诊断);`already gone` 分支的本地 tracking ref 现会被主动清理(此前仅成功删除项清理)。 + +--- + +## 📦 安装 + +- **VS Code Marketplace**:搜索 `Hyper Git - Agentic Git` 直接安装或更新。 +- **Open VSX**:搜索 `Hyper Git - Agentic Git` 直接安装或更新(覆盖 Cursor / Windsurf / VSCodium 等 Open VSX 系编辑器)。 +- **手动**:从 [Releases](https://github.com/ThreeFish-AI/hyper-git/releases/tag/v0.0.20) 下载 `hyper-git-agentic-git-0.0.20.vsix` → 命令面板 `Extensions: Install from VSIX`。 + +**系统要求**:VS Code ≥ 1.85.0,且启用内置 Git 扩展(`vscode.git`,默认随附)。仅支持本地 git 仓库,不支持虚拟 / Web 工作区。 + +--- + +## 💬 反馈 + +欢迎在 [GitHub Issues](https://github.com/ThreeFish-AI/hyper-git/issues) 反馈问题与建议。完整工程视角变更记录见 [CHANGELOG](../../CHANGELOG.md)。 + +许可证:[MIT](https://github.com/ThreeFish-AI/hyper-git/blob/master/LICENSE)。 diff --git a/docs/releases/v0.0.21.md b/docs/releases/v0.0.21.md new file mode 100644 index 0000000..15cc7b0 --- /dev/null +++ b/docs/releases/v0.0.21.md @@ -0,0 +1,52 @@ +# Hyper Git - Agentic Git v0.0.21 — Filter by Author 候选建议 · 多选过滤 · 元字符作者修复 + +> 本版核心:**Graph 视图「Filter by Author…」从「裸输入框完整手输单个作者名」升级为「候选建议 + 多选」**。候选列表来自仓库真实提交作者(`git shortlog` 按当前视图范围取数、按提交数降序)——输入 `Thr` 即在下拉中建议 `ThreeFish`,姓名与邮箱均可模糊匹配,点选即填、无需手输完整 ID;依次勾选多个提交者即可按作者集合过滤 Graph 视图(OR 语义)。同时修复了作者名含正则元字符(如 `dependabot[bot]`)时过滤 0 命中的隐性缺陷。 + +--- + +## ✨ 亮点速览 + +- **前缀输入提示**:在过滤输入框键入姓名或邮箱前缀,下拉即时给出匹配的提交者(附提交数与邮箱),点选即选中——不再要求人工完整输入作者名。 +- **多选过滤**:依次勾选任意多个提交者,Graph 视图立即按所选作者集合过滤(任一命中即保留);空选确认 = 清除作者过滤,Esc = 取消不改。 +- **正则逃生口**:输入不等于任何作者姓名时,下拉头部动态提供「Use pattern "xxx"」项——手输任意正则原样透传,旧输入框的任意 pattern 能力完整保留,且可与选中身份混用(OR)。 +- **元字符作者修复**:`dependabot[bot]` 这类含 `[ ] . +` 的作者名此前按正则原样传递导致 0 命中;现在从候选选中的身份按字面精确匹配(BRE 转义 + 全行锚定 `Name `,同名异邮箱不混淆)。 + +--- + +## 🧩 特性详述 + +### 1. 作者候选建议与多选(#149) + +- **候选取数**:`git shortlog -sne` 按当前 Graph 视图 scope(All / Current / Checkpointer)取真实提交作者,按提交数降序、附邮箱——候选与视图所见范围一致,切换 scope 后重开即同步; +- **多选语义**:多个作者翻译为多条 `git log --author`(git 原生取 OR);与 Message / Path / Date / Merge 等其它过滤维度仍为 AND 组合,`Clear Graph Filter` 一键全清; +- **交互细节**:等待取数期间显示 busy 且可先输入;已勾选项在继续键入过滤时保持不丢;再次打开时预勾选当前已过滤的作者,便于增删;空仓库 / 查询失败时降级为仅 pattern 输入(近似旧行为)。 + +### 2. BRE 字面转义修复(#149) + +- `git log --author` 的 pattern 缺省按 POSIX BRE(Basic Regular Expression)解释:`dependabot[bot]` 中的 `[bot]` 是字符类、`.` 匹配任意字符——作者名原样传递即偏离字面语义,含 `[ ]` 的作者 0 命中、含 `.` 的作者可能误伤他人; +- 修复:从候选选中的作者身份按 BRE 语义转义(仅 `\ . [ * ^ $` 有特殊义;`+ ? | ( ) {` 在 BRE 中本就是普通字符,勿加反斜杠)并首尾锚定 `^Name $` 全行匹配——精确身份、同名异邮箱不混淆(本仓库实测:`dependabot[bot]` 修复前 0 命中、修复后 36 命中)。 + +--- + +## ⚙️ 升级与兼容 + +- **无破坏性变更**:全部既有命令、视图与配置项保留;其它过滤维度(Message / Path / Date / Merge / Regex)行为与组合语义与 v0.0.20 一致; +- **行为改进**:作者过滤条件仅存于内存(切换仓库自动清空,不持久化),升级后无遗留状态迁移问题;手输任意正则的能力经「Use pattern」项完整保留。 + +--- + +## 📦 安装 + +- **VS Code Marketplace**:搜索 `Hyper Git - Agentic Git` 直接安装或更新。 +- **Open VSX**:搜索 `Hyper Git - Agentic Git` 直接安装或更新(覆盖 Cursor / Windsurf / VSCodium 等 Open VSX 系编辑器)。 +- **手动**:从 [Releases](https://github.com/ThreeFish-AI/hyper-git/releases/tag/v0.0.21) 下载 `hyper-git-agentic-git-0.0.21.vsix` → 命令面板 `Extensions: Install from VSIX`。 + +**系统要求**:VS Code ≥ 1.85.0,且启用内置 Git 扩展(`vscode.git`,默认随附)。仅支持本地 git 仓库,不支持虚拟 / Web 工作区。 + +--- + +## 💬 反馈 + +欢迎在 [GitHub Issues](https://github.com/ThreeFish-AI/hyper-git/issues) 反馈问题与建议。完整工程视角变更记录见 [CHANGELOG](../../CHANGELOG.md)。 + +许可证:[MIT](https://github.com/ThreeFish-AI/hyper-git/blob/master/LICENSE)。 diff --git a/docs/releases/v0.0.22.md b/docs/releases/v0.0.22.md new file mode 100644 index 0000000..17e46bc --- /dev/null +++ b/docs/releases/v0.0.22.md @@ -0,0 +1,56 @@ +# Hyper Git - Agentic Git v0.0.22 — Graph 过滤持久化与激活指示 · Branches 就地更新 · 开发依赖升级 + +> 本版核心:**Graph 视图全部 7 个过滤维度按仓库持久化**(Author / File Path / Grep / Merge Mode / Date / Message Regex)——重启 VS Code 或切换仓库再切回,过滤条件自动恢复为上次设置,每个仓库记忆自己的完整过滤组合;过滤生效期间标题栏过滤图标点亮为填充态(Graph Filter Active),恢复的过滤不再是无声约束。同时 Branches 视图迎来**分支就地更新(不 checkout)**:右键 Update Branch 以 `git fetch` 原生 fast-forward-only 原语安全前移本地分支,永不改写本地提交。 + +--- + +## ✨ 亮点速览 + +- **过滤持久化**:任一过滤维度设置后即按仓库落盘(workspaceState),重启 / 切库往返自动恢复;空选确认与 Clear Filter 的「清除」同样持久化,重启不复活;脏持久化数据逐字段校验免疫,永不崩溃。 +- **过滤激活指示**:过滤生效期间标题栏过滤图标切换为填充态 `$(filter-filled)`(悬停标注 Graph Filter Active),与常态图标按激活态互斥呈现——重启恢复的过滤一眼可见。 +- **重开 Author 过滤即续选**:重开「Filter by Author…」预勾选已选作者(含不在当前候选集的持久化作者,以 ghost 项呈现)继续增删;pattern 项经「Use pattern」逃生口完整保留。 +- **分支就地更新**:右键单个分支 **Update Branch**、多选批量更新、标题栏 Pull 升级为选区感知——fast-forward-only 安全拒绝 diverged / ahead-only 分支(附真实原因),**永不改写本地提交**;五桶对账汇总(updated / up-to-date / pulled / skipped / failed)。 +- **切库守卫全覆盖**:任何过滤对话框(含 Filter Graph 聚合入口)或 Commit 文件右键菜单打开期间发生切库后确认,不再把旧仓库语境的选择误写新仓库。 +- **越仓选择拒绝**:Filter by Path 选中当前仓库外的文件时以告警拒绝,不再落入 `../…` 垃圾 path 过滤(该垃圾 path 会随持久化成为跨重启的图加载 fatal 错误态)。 + +--- + +## 🧩 特性详述 + +### 1. Graph 过滤按仓库持久化与激活指示(#152) + +- **存储范式**:沿用 scope / detail mode 的按仓库 workspaceState 范式——authors 独立 key(`hyperGit.log.authors:`),其余 6 维组合 key(`hyperGit.log.filter:`,单 key 原子读写防半恢复态,Date 经 ISO 串往返);脏数据经 engine 层纯函数逐字段校验(空串、mergeMode `all` 等归一化不落盘); +- **激活指示**:engine 纯函数 `hasActiveFilter`(激活判定与持久化归一化同口径)驱动 `hyperGit.log.filterActive` context key,标题栏过滤图标常态(`$(filter)`)/ 填充(`$(filter-filled)`)双命令互斥呈现,`alt+g` 键绑定不受影响; +- **附带行为变更**:① SCM 变更列表右键「Show History」由整替过滤器改为与其余过滤命令一致的增量式——文件历史与活跃过滤相交而非静默清空;② issue #107 的「切库清空 host 级过滤」限制消除——每个仓库记忆自己的完整过滤条件。 + +### 2. Branches 视图分支就地更新(#151) + +- **底层原语**:`git fetch refs/heads/:refs/heads/`——git 原生 fast-forward-only,diverged / ahead-only 分支被安全拒绝(附真实 stderr 原因);当前分支自动改走 `repo.pull()`(含冲突处理通道);无上游分支透明跳过; +- **选区感知 Pull**:Branches 标题栏 Pull 按钮在有多选分支时按选区执行就地更新(无选区保持原 Pull 语义,Commit 视图 Pull 不受影响); +- **五桶对账**:updated / already up to date / pulled / skipped / failed 汇总(选中数 = 各桶之和),up-to-date 经前后 `objectname` 短 sha 前缀比对判定;upstream 解析以一次 `for-each-ref` 取新鲜快照(不信任树节点数据),`origin/x` 拆分复用最长前缀匹配(remote 名可含 `/`)。 + +--- + +## ⚙️ 升级与兼容 + +- **无破坏性变更**:全部既有命令、视图与配置项保留;过滤组合语义(各维度 AND、多作者 OR)与 v0.0.21 一致; +- **行为改进**:过滤条件由「仅内存、切库清空」升级为「按仓库持久化」——升级后首次使用即开始记忆,无迁移步骤;此前会话内的所有过滤行为(空选清除、pattern 逃生口、Clear Filter)语义不变,仅新增跨重启存活; +- **开发依赖升级**(#153):eslint 10.13 / typescript-eslint 8.72 / vitest 5.0.5 / ovsx 1.3(全部 devDependencies,对运行时零影响)。 + +--- + +## 📦 安装 + +- **VS Code Marketplace**:搜索 `Hyper Git - Agentic Git` 直接安装或更新。 +- **Open VSX**:搜索 `Hyper Git - Agentic Git` 直接安装或更新(覆盖 Cursor / Windsurf / VSCodium 等 Open VSX 系编辑器)。 +- **手动**:从 [Releases](https://github.com/ThreeFish-AI/hyper-git/releases/tag/v0.0.22) 下载 `hyper-git-agentic-git-0.0.22.vsix` → 命令面板 `Extensions: Install from VSIX`。 + +**系统要求**:VS Code ≥ 1.85.0,且启用内置 Git 扩展(`vscode.git`,默认随附)。仅支持本地 git 仓库,不支持虚拟 / Web 工作区。 + +--- + +## 💬 反馈 + +欢迎在 [GitHub Issues](https://github.com/ThreeFish-AI/hyper-git/issues) 反馈问题与建议。完整工程视角变更记录见 [CHANGELOG](../../CHANGELOG.md)。 + +许可证:[MIT](https://github.com/ThreeFish-AI/hyper-git/blob/master/LICENSE)。 diff --git a/package.json b/package.json index 56b8ac3..5becf9e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "theme": "dark" }, "description": "Hyper Git brings IntelliJ IDEA's powerful Git & Commit modules to VS Code, enhanced with AI-powered autonomous agents for intelligent repository management. Experience Git in a higher dimension.", - "version": "0.0.19", + "version": "0.0.22", "publisher": "ThreeFish-AI", "license": "MIT", "preview": true, @@ -332,6 +332,12 @@ "category": "Hyper Git", "icon": "$(filter)" }, + { + "command": "hyperGit.logFilterOn", + "title": "Graph Filter Active — Filter Graph…", + "category": "Hyper Git", + "icon": "$(filter-filled)" + }, { "command": "hyperGit.logFilterAuthor", "title": "Filter by Author…", @@ -661,6 +667,12 @@ "category": "Hyper Git", "icon": "$(arrow-up)" }, + { + "command": "hyperGit.branchUpdate", + "title": "Update Branch", + "category": "Hyper Git", + "icon": "$(arrow-down)" + }, { "command": "hyperGit.resolveConflicts", "title": "Resolve Conflicts (3-way Merge Editor)…", @@ -943,7 +955,12 @@ }, { "command": "hyperGit.logFilter", - "when": "view == hyperGit.log", + "when": "view == hyperGit.log && !hyperGit.log.filterActive", + "group": "navigation@2" + }, + { + "command": "hyperGit.logFilterOn", + "when": "view == hyperGit.log && hyperGit.log.filterActive", "group": "navigation@2" }, { @@ -972,7 +989,7 @@ "group": "navigation@2" }, { - "command": "hyperGit.pull", + "command": "hyperGit.branchUpdate", "when": "view == hyperGit.branches", "group": "navigation@3" }, @@ -1108,6 +1125,11 @@ "when": "view == hyperGit.branches && viewItem =~ /^(hyperGit.branch|hyperGit.remoteBranch)$/ && !listMultiSelection", "group": "1_branch@1.5" }, + { + "command": "hyperGit.branchUpdate", + "when": "view == hyperGit.branches && viewItem == hyperGit.branch", + "group": "1_branch@1.7" + }, { "command": "hyperGit.compareWithCurrent", "when": "view == hyperGit.branches && viewItem =~ /^(hyperGit.branch|hyperGit.remoteBranch|hyperGit.tag)$/ && !listMultiSelection", @@ -1425,6 +1447,10 @@ "command": "hyperGit.log.detailFlat", "when": "view == hyperGit.log" }, + { + "command": "hyperGit.logFilterOn", + "when": "view == hyperGit.log" + }, { "command": "hyperGit.commit.detailTree", "when": "view == hyperGit.commit" @@ -1448,6 +1474,10 @@ { "command": "hyperGit.pushBranch", "when": "false" + }, + { + "command": "hyperGit.branchUpdate", + "when": "false" } ] }, @@ -1574,14 +1604,14 @@ "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^4.0.0", "esbuild": "^0.28.2", - "eslint": "^10.12.0", + "eslint": "^10.13.0", "mocha": "^12.0.3", - "ovsx": "^1.2.0", + "ovsx": "^1.3.0", "prettier": "^3.9.10", "rimraf": "^6.0.1", "typescript": "^6.0.3", - "typescript-eslint": "^8.71.2", + "typescript-eslint": "^8.72.0", "vite": "^8.3.4", - "vitest": "^5.0.4" + "vitest": "^5.0.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b57e2fb..6f7716d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,10 +111,10 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.12.0(supports-color@8.1.1)) + version: 10.0.1(eslint@10.13.0(supports-color@8.1.1)) '@stylistic/eslint-plugin': specifier: ^5.10.0 - version: 5.10.0(eslint@10.12.0(supports-color@8.1.1)) + version: 5.10.0(eslint@10.13.0(supports-color@8.1.1)) '@types/node': specifier: ^26.6.5 version: 26.6.5 @@ -131,14 +131,14 @@ importers: specifier: ^0.28.2 version: 0.28.2 eslint: - specifier: ^10.12.0 - version: 10.12.0(supports-color@8.1.1) + specifier: ^10.13.0 + version: 10.13.0(supports-color@8.1.1) mocha: specifier: ^12.0.3 version: 12.0.3 ovsx: - specifier: ^1.2.0 - version: 1.2.0(@types/node@26.6.5)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) + specifier: ^1.3.0 + version: 1.3.0(@types/node@26.6.5)(supports-color@8.1.1) prettier: specifier: ^3.9.10 version: 3.9.10 @@ -149,23 +149,17 @@ importers: specifier: ^6.0.3 version: 6.0.3 typescript-eslint: - specifier: ^8.71.2 - version: 8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + specifier: ^8.72.0 + version: 8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) vite: specifier: ^8.3.4 version: 8.3.4(@types/node@26.6.5)(esbuild@0.28.2) vitest: - specifier: ^5.0.4 - version: 5.0.4(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)) + specifier: ^5.0.5 + version: 5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)) packages: - '@azu/format-text@1.0.2': - resolution: {integrity: sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==} - - '@azu/style-format@1.0.1': - resolution: {integrity: sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==} - '@azure/abort-controller@2.1.2': resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} @@ -194,10 +188,6 @@ packages: resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} engines: {node: '>=20.0.0'} - '@azure/identity@4.13.1': - resolution: {integrity: sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==} - engines: {node: '>=20.0.0'} - '@azure/identity@4.13.3': resolution: {integrity: sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A==} engines: {node: '>=22.0.0'} @@ -218,22 +208,10 @@ packages: resolution: {integrity: sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==} engines: {node: '>=0.8.0'} - '@azure/msal-node@5.3.0': - resolution: {integrity: sha512-fXtJX811pX8y8QlrQqBSH6+plvWyKZDI0IxkheAcyAw9OtcpXyFivmTC7eGUqutLWaDlKXuQ3yOESD4zAmkjHg==} - engines: {node: '>=20'} - '@azure/msal-node@6.0.1': resolution: {integrity: sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==} engines: {node: '>=20'} - '@babel/code-frame@7.29.7': - resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.29.7': - resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} - engines: {node: '>=6.9.0'} - '@cacheable/memory@2.2.0': resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} @@ -781,18 +759,6 @@ packages: resolution: {integrity: sha512-OwuyRAe9Lj0GoFVBFzTS6bTAV4i+Xd68sYbNUmLnI1GVkQIqVb9mzNrpKSZkBFiQD73CdMB/uKYsvk2bxq/eZg==} engines: {node: '>= 10'} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@oxc-project/types@0.153.0': resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} @@ -895,35 +861,13 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@secretlint/config-creator@10.2.2': - resolution: {integrity: sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==} - engines: {node: '>=20.0.0'} - - '@secretlint/config-loader@10.2.2': - resolution: {integrity: sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==} - engines: {node: '>=20.0.0'} - '@secretlint/core@10.2.2': resolution: {integrity: sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==} engines: {node: '>=20.0.0'} - '@secretlint/formatter@10.2.2': - resolution: {integrity: sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==} - engines: {node: '>=20.0.0'} - - '@secretlint/node@10.2.2': - resolution: {integrity: sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==} - engines: {node: '>=20.0.0'} - '@secretlint/profiler@10.2.2': resolution: {integrity: sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==} - '@secretlint/resolver@10.2.2': - resolution: {integrity: sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==} - - '@secretlint/secretlint-formatter-sarif@10.2.2': - resolution: {integrity: sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==} - '@secretlint/secretlint-rule-no-dotenv@10.2.2': resolution: {integrity: sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==} engines: {node: '>=20.0.0'} @@ -940,31 +884,12 @@ packages: resolution: {integrity: sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==} engines: {node: '>=20.0.0'} - '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - '@stylistic/eslint-plugin@5.10.0': resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^9.0.0 || ^10.0.0 - '@textlint/ast-node-types@15.7.1': - resolution: {integrity: sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==} - - '@textlint/linter-formatter@15.7.1': - resolution: {integrity: sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==} - - '@textlint/module-interop@15.7.1': - resolution: {integrity: sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==} - - '@textlint/resolver@15.7.1': - resolution: {integrity: sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==} - - '@textlint/types@15.7.1': - resolution: {integrity: sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -983,48 +908,42 @@ packages: '@types/node@26.6.5': resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==} - '@types/normalize-package-data@2.4.4': - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - - '@types/sarif@2.1.7': - resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} - '@types/vscode@1.85.0': resolution: {integrity: sha512-CF/RBon/GXwdfmnjZj0WTUMZN5H6YITOfBCP4iEZlOtVQXuzw6t7Le7+cR+7JzdMrnlm7Mfp49Oj2TuSXIWo3g==} - '@typescript-eslint/eslint-plugin@8.71.2': - resolution: {integrity: sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==} + '@typescript-eslint/eslint-plugin@8.72.0': + resolution: {integrity: sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.71.2 + '@typescript-eslint/parser': ^8.72.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.71.2': - resolution: {integrity: sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==} + '@typescript-eslint/parser@8.72.0': + resolution: {integrity: sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.71.2': - resolution: {integrity: sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==} + '@typescript-eslint/project-service@8.72.0': + resolution: {integrity: sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.71.2': - resolution: {integrity: sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==} + '@typescript-eslint/scope-manager@8.72.0': + resolution: {integrity: sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.71.2': - resolution: {integrity: sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==} + '@typescript-eslint/tsconfig-utils@8.72.0': + resolution: {integrity: sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.71.2': - resolution: {integrity: sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==} + '@typescript-eslint/type-utils@8.72.0': + resolution: {integrity: sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1034,33 +953,33 @@ packages: resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.71.2': - resolution: {integrity: sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==} + '@typescript-eslint/types@8.72.0': + resolution: {integrity: sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.71.2': - resolution: {integrity: sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==} + '@typescript-eslint/typescript-estree@8.72.0': + resolution: {integrity: sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.71.2': - resolution: {integrity: sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==} + '@typescript-eslint/utils@8.72.0': + resolution: {integrity: sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.71.2': - resolution: {integrity: sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==} + '@typescript-eslint/visitor-keys@8.72.0': + resolution: {integrity: sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typespec/ts-http-runtime@0.3.6': resolution: {integrity: sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==} engines: {node: '>=20.0.0'} - '@vitest/mocker@5.0.4': - resolution: {integrity: sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==} + '@vitest/mocker@5.0.5': + resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==} peerDependencies: msw: ^2.4.9 || ^3.0.0 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1070,8 +989,8 @@ packages: vite: optional: true - '@vitest/spy@5.0.4': - resolution: {integrity: sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==} + '@vitest/spy@5.0.5': + resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==} '@vscode/test-electron@3.1.0': resolution: {integrity: sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==} @@ -1122,17 +1041,9 @@ packages: cpu: [x64] os: [win32] - '@vscode/vsce-sign@2.0.9': - resolution: {integrity: sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==} - '@vscode/vsce-sign@2.1.0': resolution: {integrity: sha512-9AQrqazrBgTgRSuwleLVXUrIUphY02/SFCh2TKYoLV/xifJAdblhdmEmw5gUrYSPQ3sRwNs9iyCMD14sATEE6g==} - '@vscode/vsce@3.9.2': - resolution: {integrity: sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==} - engines: {node: '>= 20'} - hasBin: true - '@vscode/vsce@4.0.0': resolution: {integrity: sha512-NImwuLaenMmb5D5Jer9/lzi/F9ZQUBOp8Azhj/BVYcTFgixv8KehFXqEUDjQlD2tAiw2E6dDGyjTuAB//di60A==} engines: {node: '>= 22'} @@ -1160,13 +1071,6 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv@8.20.0: - resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - - ansi-escapes@7.3.0: - resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} - engines: {node: '>=18'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1186,13 +1090,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - azure-devops-node-api@12.5.0: resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==} @@ -1200,19 +1097,10 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - binaryextensions@6.11.0: resolution: {integrity: sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==} engines: {node: '>=4'} - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - boundary@2.0.0: resolution: {integrity: sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==} @@ -1220,10 +1108,6 @@ packages: resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} @@ -1233,9 +1117,6 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -1255,10 +1136,6 @@ packages: resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} engines: {node: '>=18'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1266,20 +1143,10 @@ packages: chardet@2.2.0: resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} - cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - - cheerio@1.2.0: - resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} - engines: {node: '>=20.18.1'} - chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -1306,17 +1173,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} - commander@6.2.1: - resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} - engines: {node: '>= 6'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -1330,13 +1193,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1346,14 +1202,6 @@ packages: supports-color: optional: true - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1377,10 +1225,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1389,19 +1233,6 @@ packages: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1419,32 +1250,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - encoding-sniffer@0.2.1: - resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} - - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - - entities@7.0.1: - resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} - engines: {node: '>=0.12'} - entities@8.1.0: resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} engines: {node: '>=20.19.0'} - environment@1.1.0: - resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} - engines: {node: '>=18'} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1460,10 +1269,6 @@ packages: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} @@ -1489,8 +1294,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.12.0: - resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==} + eslint@10.13.0: + resolution: {integrity: sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1526,10 +1331,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} @@ -1537,22 +1338,12 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} - - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1565,10 +1356,6 @@ packages: file-entry-cache@11.1.5: resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - find-up-simple@1.0.1: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} @@ -1583,26 +1370,6 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} - follow-redirects@1.16.0: - resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - form-data@4.0.6: - resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} - engines: {node: '>= 6'} - - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - - fs-extra@11.3.5: - resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} - engines: {node: '>=14.14'} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1623,13 +1390,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -1642,10 +1402,6 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - globby@14.1.0: - resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} - engines: {node: '>=18'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1664,10 +1420,6 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - hashery@1.5.1: resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} @@ -1686,13 +1438,6 @@ packages: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} - hosted-git-info@7.0.2: - resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} - engines: {node: ^16.14.0 || >=18.0.0} - - htmlparser2@10.1.0: - resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1701,17 +1446,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1727,16 +1465,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - index-to-position@1.2.0: - resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} - engines: {node: '>=18'} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - is-ci@2.0.0: resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} hasBin: true @@ -1771,10 +1502,6 @@ packages: resolution: {integrity: sha512-AX2uU0HW+TxagTgQXOJY7+2fbFHemC7YFBwN1XqD8qQMKdtfbOC8OC3fUb4s5NU59a3662Dzwto8tWDdZYRXxg==} engines: {node: '>=12'} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - is-path-inside@4.0.0: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} @@ -1805,13 +1532,6 @@ packages: resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==} engines: {node: '>=4'} - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.2.0: - resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} - hasBin: true - js-yaml@5.4.2: resolution: {integrity: sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==} hasBin: true @@ -1819,23 +1539,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsonfile@6.2.1: - resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} - jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} @@ -1849,16 +1558,9 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - keytar@7.9.0: - resolution: {integrity: sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==} - keyv@5.6.0: resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1940,9 +1642,6 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} - linkify-it@5.0.1: - resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1968,19 +1667,10 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.truncate@4.4.2: - resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.5.1: resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} @@ -1992,10 +1682,6 @@ packages: magic-string@1.4.3: resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} - markdown-it@14.2.0: - resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} - hasBin: true - marked@18.0.13: resolution: {integrity: sha512-xTxVzZsBFwunP6HDmtBkabUQEYArnP7/rMDGmPj9SlrKlQ4i8MdYVow+nJL0eOqwpUqhzBoTBRADGN6uYwPyOw==} engines: {node: '>= 20'} @@ -2005,29 +1691,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - meow@14.1.0: resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -2037,24 +1704,14 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mocha@12.0.3: resolution: {integrity: sha512-beammieDdiWdkjANWeWPpkxUROYGi5FN88fPOkLiticz7HTg3Y+Pho3EYE6SUdcuWnZ3h2WD8EXCA55JF4o6wg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2075,30 +1732,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - node-abi@3.92.0: - resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} - engines: {node: '>=10'} - - node-addon-api@4.3.0: - resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} - - node-sarif-builder@3.4.0: - resolution: {integrity: sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==} - engines: {node: '>=20'} - - normalize-package-data@6.0.2: - resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} - engines: {node: ^16.14.0 || >=18.0.0} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -2111,9 +1747,6 @@ packages: resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -2130,8 +1763,8 @@ packages: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} - ovsx@1.2.0: - resolution: {integrity: sha512-12mauBqsLehlJ0cRiroQz4uKYVjblVS5OobgSJ278CdCtkDfRQD+SI3NQ00GNalDqzp5PDN11yx84miFUojeMw==} + ovsx@1.3.0: + resolution: {integrity: sha512-U5B1YCo+6knEipsEF9O+iO+uwK+vTm8qNQyvc5zzE/2P6Zfzjhw4Xj4UJ7WZ1aU/kVGI70FuUWxv/GVXK5JmXw==} engines: {node: '>=22.0.0'} hasBin: true @@ -2143,32 +1776,12 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-map@7.0.4: - resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} - engines: {node: '>=18'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - parse-json@8.3.0: - resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} - engines: {node: '>=18'} - - parse-semver@1.1.1: - resolution: {integrity: sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==} - - parse5-htmlparser2-tree-adapter@7.1.0: - resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} - - parse5-parser-stream@7.1.2: - resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} - - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -2184,20 +1797,12 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} - path-type@6.0.0: - resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} - engines: {node: '>=18'} - pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} - picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -2206,23 +1811,10 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - pluralize@2.0.0: - resolution: {integrity: sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==} - - pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - postcss@8.5.30: resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} engines: {node: ^10 || ^12 || >=14} - prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. - hasBin: true - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2238,13 +1830,6 @@ packages: proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2257,20 +1842,6 @@ packages: resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} engines: {node: '>=0.6'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - rc-config-loader@4.1.4: - resolution: {integrity: sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==} - - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - - read-pkg@9.0.1: - resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} - engines: {node: '>=18'} - read@1.0.7: resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==} engines: {node: '>=0.8'} @@ -2278,18 +1849,10 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - readdirp@5.1.1: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -2298,10 +1861,6 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@6.1.3: resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} engines: {node: 20 || >=22} @@ -2316,9 +1875,6 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -2332,15 +1888,6 @@ packages: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} - secretlint@10.2.2: - resolution: {integrity: sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==} - engines: {node: '>=20.0.0'} - hasBin: true - - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -2384,40 +1931,14 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - simple-invariant@2.0.1: resolution: {integrity: sha512-1sbhsxqI+I2tqlmjbz99GXNmZtr6tKIyEgGGnJw/MKGblalqk/XoOYYFJlBzTKZCxx8kLaD3FD5s9BEEjx5Pyg==} engines: {node: '>=10'} - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - - slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} - source-map-js@1.2.2: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} - spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-license-ids@3.0.23: - resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} - std-env@4.3.0: resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} @@ -2436,9 +1957,6 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2447,10 +1965,6 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - strip-json-comments@5.0.3: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} @@ -2458,36 +1972,10 @@ packages: structured-source@4.0.0: resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - supports-hyperlinks@3.2.0: - resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} - engines: {node: '>=14.18'} - - table@6.9.0: - resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} - engines: {node: '>=10.0.0'} - - tar-fs@2.1.4: - resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - - terminal-link@4.0.0: - resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==} - engines: {node: '>=18'} - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - textextensions@6.11.0: resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==} engines: {node: '>=4'} @@ -2504,14 +1992,6 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tmp@0.2.7: - resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} - engines: {node: '>=14.14'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -2521,9 +2001,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - tunnel@0.0.6: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} @@ -2532,15 +2009,11 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} - typescript-eslint@8.71.2: - resolution: {integrity: sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==} + typescript-eslint@8.72.0: + resolution: {integrity: sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2551,31 +2024,12 @@ packages: engines: {node: '>=14.17'} hasBin: true - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - underscore@1.13.8: resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} undici-types@8.9.0: resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} - engines: {node: '>=20.18.1'} - - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - - unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2585,9 +2039,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - version-range@4.15.0: resolution: {integrity: sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==} engines: {node: '>=4'} @@ -2635,20 +2086,20 @@ packages: yaml: optional: true - vitest@5.0.4: - resolution: {integrity: sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==} + vitest@5.0.5: + resolution: {integrity: sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 5.0.4 - '@vitest/browser-preview': 5.0.4 + '@vitest/browser-playwright': 5.0.5 + '@vitest/browser-preview': 5.0.5 '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 - '@vitest/coverage-istanbul': 5.0.4 - '@vitest/coverage-v8': 5.0.4 - '@vitest/ui': 5.0.4 + '@vitest/coverage-istanbul': 5.0.5 + '@vitest/coverage-v8': 5.0.5 + '@vitest/ui': 5.0.5 happy-dom: '*' jsdom: '*' vite: ^6.4.0 || ^7.0.0 || ^8.0.0 @@ -2676,15 +2127,6 @@ packages: jsdom: optional: true - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation - - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2706,9 +2148,6 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -2745,12 +2184,6 @@ packages: snapshots: - '@azu/format-text@1.0.2': {} - - '@azu/style-format@1.0.1': - dependencies: - '@azu/format-text': 1.0.2 - '@azure/abort-controller@2.1.2': dependencies: tslib: 2.8.1 @@ -2801,22 +2234,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/identity@4.13.1(supports-color@8.1.1)': - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1(supports-color@8.1.1) - '@azure/core-client': 1.10.2(supports-color@8.1.1) - '@azure/core-rest-pipeline': 1.24.0(supports-color@8.1.1) - '@azure/core-tracing': 1.3.1 - '@azure/core-util': 1.13.1(supports-color@8.1.1) - '@azure/logger': 1.3.0(supports-color@8.1.1) - '@azure/msal-browser': 5.15.0 - '@azure/msal-node': 5.3.0 - open: 10.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - '@azure/identity@4.13.3(supports-color@8.1.1)': dependencies: '@azure/abort-controller': 2.1.2 @@ -2849,24 +2266,11 @@ snapshots: '@azure/msal-common@16.14.1': {} - '@azure/msal-node@5.3.0': - dependencies: - '@azure/msal-common': 16.10.0 - jsonwebtoken: 9.0.3 - '@azure/msal-node@6.0.1': dependencies: '@azure/msal-common': 16.14.1 jsonwebtoken: 9.0.3 - '@babel/code-frame@7.29.7': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/helper-validator-identifier@7.29.7': {} - '@cacheable/memory@2.2.0': dependencies: '@cacheable/utils': 2.5.0 @@ -2957,14 +2361,14 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.12.0(supports-color@8.1.1))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.13.0(supports-color@8.1.1))': dependencies: - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.1(eslint@10.12.0(supports-color@8.1.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.13.0(supports-color@8.1.1))': dependencies: - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -2985,9 +2389,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.12.0(supports-color@8.1.1))': + '@eslint/js@10.0.1(eslint@10.13.0(supports-color@8.1.1))': optionalDependencies: - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) '@eslint/object-schema@3.0.5': {} @@ -3260,18 +2664,6 @@ snapshots: '@node-rs/crc32-win32-ia32-msvc': 1.10.7 '@node-rs/crc32-win32-x64-msvc': 1.10.7 - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - '@oxc-project/types@0.153.0': {} '@rolldown/binding-android-arm-eabi@1.2.13': @@ -3321,67 +2713,17 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@secretlint/config-creator@10.2.2': - dependencies: - '@secretlint/types': 10.2.2 - - '@secretlint/config-loader@10.2.2(supports-color@8.1.1)': + '@secretlint/core@10.2.2(supports-color@8.1.1)': dependencies: '@secretlint/profiler': 10.2.2 - '@secretlint/resolver': 10.2.2 '@secretlint/types': 10.2.2 - ajv: 8.20.0 debug: 4.4.3(supports-color@8.1.1) - rc-config-loader: 4.1.4(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - '@secretlint/core@10.2.2(supports-color@8.1.1)': - dependencies: - '@secretlint/profiler': 10.2.2 - '@secretlint/types': 10.2.2 - debug: 4.4.3(supports-color@8.1.1) - structured-source: 4.0.0 - transitivePeerDependencies: - - supports-color - - '@secretlint/formatter@10.2.2(supports-color@8.1.1)': - dependencies: - '@secretlint/resolver': 10.2.2 - '@secretlint/types': 10.2.2 - '@textlint/linter-formatter': 15.7.1(supports-color@8.1.1) - '@textlint/module-interop': 15.7.1 - '@textlint/types': 15.7.1 - chalk: 5.6.2 - debug: 4.4.3(supports-color@8.1.1) - pluralize: 8.0.0 - strip-ansi: 7.2.0 - table: 6.9.0 - terminal-link: 4.0.0 - transitivePeerDependencies: - - supports-color - - '@secretlint/node@10.2.2(supports-color@8.1.1)': - dependencies: - '@secretlint/config-loader': 10.2.2(supports-color@8.1.1) - '@secretlint/core': 10.2.2(supports-color@8.1.1) - '@secretlint/formatter': 10.2.2(supports-color@8.1.1) - '@secretlint/profiler': 10.2.2 - '@secretlint/source-creator': 10.2.2 - '@secretlint/types': 10.2.2 - debug: 4.4.3(supports-color@8.1.1) - p-map: 7.0.4 + structured-source: 4.0.0 transitivePeerDependencies: - supports-color '@secretlint/profiler@10.2.2': {} - '@secretlint/resolver@10.2.2': {} - - '@secretlint/secretlint-formatter-sarif@10.2.2': - dependencies: - node-sarif-builder: 3.4.0 - '@secretlint/secretlint-rule-no-dotenv@10.2.2': dependencies: '@secretlint/types': 10.2.2 @@ -3395,47 +2737,16 @@ snapshots: '@secretlint/types@10.2.2': {} - '@sindresorhus/merge-streams@2.3.0': {} - - '@stylistic/eslint-plugin@5.10.0(eslint@10.12.0(supports-color@8.1.1))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.13.0(supports-color@8.1.1))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.12.0(supports-color@8.1.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.13.0(supports-color@8.1.1)) '@typescript-eslint/types': 8.62.1 - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 picomatch: 4.0.4 - '@textlint/ast-node-types@15.7.1': {} - - '@textlint/linter-formatter@15.7.1(supports-color@8.1.1)': - dependencies: - '@azu/format-text': 1.0.2 - '@azu/style-format': 1.0.1 - '@textlint/module-interop': 15.7.1 - '@textlint/resolver': 15.7.1 - '@textlint/types': 15.7.1 - chalk: 4.1.2 - debug: 4.4.3(supports-color@8.1.1) - js-yaml: 4.2.0 - lodash: 4.18.1 - pluralize: 2.0.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - table: 6.9.0 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - '@textlint/module-interop@15.7.1': {} - - '@textlint/resolver@15.7.1': {} - - '@textlint/types@15.7.1': - dependencies: - '@textlint/ast-node-types': 15.7.1 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -3453,21 +2764,17 @@ snapshots: dependencies: undici-types: 8.9.0 - '@types/normalize-package-data@2.4.4': {} - - '@types/sarif@2.1.7': {} - '@types/vscode@1.85.0': {} - '@typescript-eslint/eslint-plugin@8.71.2(@typescript-eslint/parser@8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.72.0(@typescript-eslint/parser@8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.71.2 - '@typescript-eslint/type-utils': 8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.71.2 - eslint: 10.12.0(supports-color@8.1.1) + '@typescript-eslint/parser': 8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/type-utils': 8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.72.0 + eslint: 10.13.0(supports-color@8.1.1) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -3475,43 +2782,43 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/parser@8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.71.2 - '@typescript-eslint/types': 8.71.2 - '@typescript-eslint/typescript-estree': 8.71.2(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.71.2 + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.72.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.71.2(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/project-service@8.72.0(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.71.2(typescript@6.0.3) - '@typescript-eslint/types': 8.71.2 + '@typescript-eslint/tsconfig-utils': 8.72.0(typescript@6.0.3) + '@typescript-eslint/types': 8.72.0 debug: 4.4.3(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.71.2': + '@typescript-eslint/scope-manager@8.72.0': dependencies: - '@typescript-eslint/types': 8.71.2 - '@typescript-eslint/visitor-keys': 8.71.2 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/visitor-keys': 8.72.0 - '@typescript-eslint/tsconfig-utils@8.71.2(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.72.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.71.2 - '@typescript-eslint/typescript-estree': 8.71.2(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -3519,14 +2826,14 @@ snapshots: '@typescript-eslint/types@8.62.1': {} - '@typescript-eslint/types@8.71.2': {} + '@typescript-eslint/types@8.72.0': {} - '@typescript-eslint/typescript-estree@8.71.2(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.72.0(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.71.2(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.71.2(typescript@6.0.3) - '@typescript-eslint/types': 8.71.2 - '@typescript-eslint/visitor-keys': 8.71.2 + '@typescript-eslint/project-service': 8.72.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.72.0(typescript@6.0.3) + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/visitor-keys': 8.72.0 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.6 semver: 7.8.5 @@ -3536,20 +2843,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': + '@typescript-eslint/utils@8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@8.1.1)) - '@typescript-eslint/scope-manager': 8.71.2 - '@typescript-eslint/types': 8.71.2 - '@typescript-eslint/typescript-estree': 8.71.2(supports-color@8.1.1)(typescript@6.0.3) - eslint: 10.12.0(supports-color@8.1.1) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0(supports-color@8.1.1)) + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(supports-color@8.1.1)(typescript@6.0.3) + eslint: 10.13.0(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.71.2': + '@typescript-eslint/visitor-keys@8.72.0': dependencies: - '@typescript-eslint/types': 8.71.2 + '@typescript-eslint/types': 8.72.0 eslint-visitor-keys: 5.0.1 '@typespec/ts-http-runtime@0.3.6(supports-color@8.1.1)': @@ -3560,16 +2867,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/mocker@5.0.4(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2))': + '@vitest/mocker@5.0.5(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2))': dependencies: '@jridgewell/trace-mapping': 0.3.31 - '@vitest/spy': 5.0.4 + '@vitest/spy': 5.0.5 estree-walker: 3.0.3 magic-string: 1.4.3 optionalDependencies: vite: 8.3.4(@types/node@26.6.5)(esbuild@0.28.2) - '@vitest/spy@5.0.4': {} + '@vitest/spy@5.0.5': {} '@vscode/test-electron@3.1.0(supports-color@8.1.1)': dependencies: @@ -3608,18 +2915,6 @@ snapshots: '@vscode/vsce-sign-win32-x64@2.0.6': optional: true - '@vscode/vsce-sign@2.0.9': - optionalDependencies: - '@vscode/vsce-sign-alpine-arm64': 2.0.6 - '@vscode/vsce-sign-alpine-x64': 2.0.6 - '@vscode/vsce-sign-darwin-arm64': 2.0.6 - '@vscode/vsce-sign-darwin-x64': 2.0.6 - '@vscode/vsce-sign-linux-arm': 2.0.6 - '@vscode/vsce-sign-linux-arm64': 2.0.6 - '@vscode/vsce-sign-linux-x64': 2.0.6 - '@vscode/vsce-sign-win32-arm64': 2.0.6 - '@vscode/vsce-sign-win32-x64': 2.0.6 - '@vscode/vsce-sign@2.1.0': optionalDependencies: '@vscode/vsce-sign-alpine-arm64': 2.0.6 @@ -3632,42 +2927,6 @@ snapshots: '@vscode/vsce-sign-win32-arm64': 2.0.6 '@vscode/vsce-sign-win32-x64': 2.0.6 - '@vscode/vsce@3.9.2(supports-color@8.1.1)': - dependencies: - '@azure/identity': 4.13.1(supports-color@8.1.1) - '@secretlint/node': 10.2.2(supports-color@8.1.1) - '@secretlint/secretlint-formatter-sarif': 10.2.2 - '@secretlint/secretlint-rule-no-dotenv': 10.2.2 - '@secretlint/secretlint-rule-preset-recommend': 10.2.2 - '@vscode/vsce-sign': 2.0.9 - azure-devops-node-api: 12.5.0 - chalk: 4.1.2 - cheerio: 1.2.0 - cockatiel: 3.2.1 - commander: 12.1.0 - form-data: 4.0.6 - glob: 13.0.6 - hosted-git-info: 4.1.0 - jsonc-parser: 3.3.1 - leven: 3.1.0 - markdown-it: 14.2.0 - mime: 1.6.0 - minimatch: 10.2.6 - parse-semver: 1.1.1 - read: 1.0.7 - secretlint: 10.2.2(supports-color@8.1.1) - semver: 7.8.5 - tmp: 0.2.7 - typed-rest-client: 1.8.11 - url-join: 4.0.1 - xml2js: 0.5.0 - yauzl: 3.4.0 - yazl: 2.5.1 - optionalDependencies: - keytar: 7.9.0 - transitivePeerDependencies: - - supports-color - '@vscode/vsce@4.0.0(supports-color@8.1.1)': dependencies: '@azure/identity': 4.13.3(supports-color@8.1.1) @@ -3720,17 +2979,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.20.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - ansi-escapes@7.3.0: - dependencies: - environment: 1.1.0 - ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -3743,10 +2991,6 @@ snapshots: assertion-error@2.0.1: {} - astral-regex@2.0.0: {} - - asynckit@0.4.0: {} - azure-devops-node-api@12.5.0: dependencies: tunnel: 0.0.6 @@ -3754,44 +2998,22 @@ snapshots: balanced-match@4.0.4: {} - base64-js@1.5.1: - optional: true - binaryextensions@6.11.0: dependencies: editions: 6.22.0 - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - optional: true - - boolbase@1.0.0: {} - boundary@2.0.0: {} brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - browser-stdout@1.3.1: {} buffer-crc32@0.2.13: {} buffer-equal-constant-time@1.0.1: {} - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - optional: true - bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -3816,45 +3038,14 @@ snapshots: chai@6.3.0: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chalk@5.6.2: {} chardet@2.2.0: {} - cheerio-select@2.1.0: - dependencies: - boolbase: 1.0.0 - css-select: 5.2.2 - css-what: 6.2.2 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - - cheerio@1.2.0: - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.2.2 - encoding-sniffer: 0.2.1 - htmlparser2: 10.1.0 - parse5: 7.3.0 - parse5-htmlparser2-tree-adapter: 7.1.0 - parse5-parser-stream: 7.1.2 - undici: 7.28.0 - whatwg-mimetype: 4.0.0 - chokidar@5.0.0: dependencies: readdirp: 5.1.1 - chownr@1.1.4: - optional: true - ci-info@2.0.0: {} cli-cursor@5.0.0: @@ -3873,13 +3064,9 @@ snapshots: color-name@1.1.4: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - commander@12.1.0: {} - commander@6.2.1: {} + commander@14.0.3: {} core-util-is@1.0.3: {} @@ -3898,30 +3085,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-select@5.2.2: - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 - - css-what@6.2.2: {} - debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 optionalDependencies: supports-color: 8.1.1 - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - optional: true - - deep-extend@0.6.0: - optional: true - deep-is@0.1.4: {} default-browser-id@5.0.1: {} @@ -3945,30 +3114,10 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - delayed-stream@1.0.0: {} - detect-libc@2.1.2: {} diff@9.0.0: {} - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3987,26 +3136,8 @@ snapshots: emoji-regex@8.0.0: {} - encoding-sniffer@0.2.1: - dependencies: - iconv-lite: 0.6.3 - whatwg-encoding: 3.1.1 - - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - optional: true - - entities@4.5.0: {} - - entities@6.0.1: {} - - entities@7.0.1: {} - entities@8.1.0: {} - environment@1.1.0: {} - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -4017,13 +3148,6 @@ snapshots: dependencies: es-errors: 1.3.0 - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - esbuild@0.28.2: optionalDependencies: '@esbuild/aix-ppc64': 0.28.2 @@ -4068,9 +3192,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.12.0(supports-color@8.1.1): + eslint@10.13.0(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@8.1.1)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5(supports-color@8.1.1) '@eslint/config-helpers': 0.7.0 @@ -4131,31 +3255,14 @@ snapshots: esutils@2.0.3: {} - expand-template@2.0.3: - optional: true - expect-type@1.4.0: {} fast-deep-equal@3.1.3: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} - fast-uri@3.1.2: {} - - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 @@ -4164,10 +3271,6 @@ snapshots: dependencies: flat-cache: 6.1.23 - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - find-up-simple@1.0.1: {} find-up@5.0.0: @@ -4183,27 +3286,6 @@ snapshots: flatted@3.4.4: {} - follow-redirects@1.16.0(debug@4.4.3(supports-color@8.1.1)): - optionalDependencies: - debug: 4.4.3(supports-color@8.1.1) - - form-data@4.0.6: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.4 - mime-types: 2.1.35 - - fs-constants@1.0.0: - optional: true - - fs-extra@11.3.5: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.1 - universalify: 2.0.1 - fsevents@2.3.3: optional: true @@ -4229,13 +3311,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 - github-from-package@0.0.0: - optional: true - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -4251,15 +3326,6 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 - globby@14.1.0: - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.3 - ignore: 7.0.6 - path-type: 6.0.0 - slash: 5.1.0 - unicorn-magic: 0.3.0 - gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -4272,10 +3338,6 @@ snapshots: has-symbols@1.1.0: {} - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - hashery@1.5.1: dependencies: hookified: 1.15.1 @@ -4292,17 +3354,6 @@ snapshots: dependencies: lru-cache: 6.0.0 - hosted-git-info@7.0.2: - dependencies: - lru-cache: 10.4.3 - - htmlparser2@10.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 7.0.1 - http-proxy-agent@7.0.2(supports-color@8.1.1): dependencies: agent-base: 7.1.4 @@ -4317,17 +3368,10 @@ snapshots: transitivePeerDependencies: - supports-color - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 - ieee754@1.2.1: - optional: true - ignore@5.3.2: {} ignore@7.0.6: {} @@ -4336,13 +3380,8 @@ snapshots: imurmurhash@0.1.4: {} - index-to-position@1.2.0: {} - inherits@2.0.4: {} - ini@1.3.8: - optional: true - is-ci@2.0.0: dependencies: ci-info: 2.0.0 @@ -4367,8 +3406,6 @@ snapshots: dependencies: globalthis: 1.0.4 - is-number@7.0.0: {} - is-path-inside@4.0.0: {} is-unicode-supported@0.1.0: {} @@ -4391,32 +3428,16 @@ snapshots: editions: 6.22.0 textextensions: 6.11.0 - js-tokens@4.0.0: {} - - js-yaml@4.2.0: - dependencies: - argparse: 2.0.1 - js-yaml@5.4.2: dependencies: argparse: 2.0.1 json-schema-traverse@0.4.1: {} - json-schema-traverse@1.0.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} - json5@2.2.3: {} - jsonc-parser@3.3.1: {} - jsonfile@6.2.1: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - jsonwebtoken@9.0.3: dependencies: jws: 4.0.1 @@ -4448,18 +3469,10 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 - keytar@7.9.0: - dependencies: - node-addon-api: 4.3.0 - prebuild-install: 7.1.3 - optional: true - keyv@5.6.0: dependencies: '@keyv/serialize': 1.1.1 - leven@3.1.0: {} - levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -4518,10 +3531,6 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 - linkify-it@5.0.1: - dependencies: - uc.micro: 2.1.0 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4540,17 +3549,11 @@ snapshots: lodash.once@4.1.1: {} - lodash.truncate@4.4.2: {} - - lodash@4.18.1: {} - log-symbols@6.0.0: dependencies: chalk: 5.6.2 is-unicode-supported: 1.3.0 - lru-cache@10.4.3: {} - lru-cache@11.5.1: {} lru-cache@6.0.0: @@ -4561,55 +3564,22 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 - markdown-it@14.2.0: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.1 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - marked@18.0.13: {} math-intrinsics@1.1.0: {} - mdurl@2.0.0: {} - meow@14.1.0: {} - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.2 - - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - mime@1.6.0: {} mimic-function@5.0.1: {} - mimic-response@3.1.0: - optional: true - minimatch@10.2.6: dependencies: brace-expansion: 5.0.9 - minimist@1.2.8: - optional: true - minipass@7.1.3: {} - mkdirp-classic@0.5.3: - optional: true - mocha@12.0.3: dependencies: browser-stdout: 1.3.1 @@ -4637,45 +3607,14 @@ snapshots: nanoid@3.3.20: {} - napi-build-utils@2.0.0: - optional: true - natural-compare@1.4.0: {} - node-abi@3.92.0: - dependencies: - semver: 7.8.5 - optional: true - - node-addon-api@4.3.0: - optional: true - - node-sarif-builder@3.4.0: - dependencies: - '@types/sarif': 2.1.7 - fs-extra: 11.3.5 - - normalize-package-data@6.0.2: - dependencies: - hosted-git-info: 7.0.2 - semver: 7.8.5 - validate-npm-package-license: 3.0.4 - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - object-inspect@1.13.4: {} object-keys@1.1.1: {} obug@3.0.0: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - optional: true - onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -4708,21 +3647,17 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 - ovsx@1.2.0(@types/node@26.6.5)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1): + ovsx@1.3.0(@types/node@26.6.5)(supports-color@8.1.1): dependencies: '@inquirer/prompts': 7.10.1(@types/node@26.6.5) - '@vscode/vsce': 3.9.2(supports-color@8.1.1) - commander: 6.2.1 + '@vscode/vsce': 4.0.0(supports-color@8.1.1) + commander: 14.0.3 cross-keychain: 1.1.0(@types/node@26.6.5) - follow-redirects: 1.16.0(debug@4.4.3(supports-color@8.1.1)) is-ci: 2.0.0 - leven: 3.1.0 semver: 7.8.5 - tmp: 0.2.7 yauzl-promise: 4.0.0 transitivePeerDependencies: - '@types/node' - - debug - supports-color p-limit@3.1.0: @@ -4733,35 +3668,10 @@ snapshots: dependencies: p-limit: 3.1.0 - p-map@7.0.4: {} - package-json-from-dist@1.0.1: {} pako@1.0.11: {} - parse-json@8.3.0: - dependencies: - '@babel/code-frame': 7.29.7 - index-to-position: 1.2.0 - type-fest: 4.41.0 - - parse-semver@1.1.1: - dependencies: - semver: 5.7.2 - - parse5-htmlparser2-tree-adapter@7.1.0: - dependencies: - domhandler: 5.0.3 - parse5: 7.3.0 - - parse5-parser-stream@7.1.2: - dependencies: - parse5: 7.3.0 - - parse5@7.3.0: - dependencies: - entities: 6.0.1 - parse5@8.0.1: dependencies: entities: 8.1.0 @@ -4775,44 +3685,20 @@ snapshots: lru-cache: 11.5.1 minipass: 7.1.3 - path-type@6.0.0: {} - pend@1.2.0: {} picocolors@1.1.1: {} - picomatch@2.3.2: {} - picomatch@4.0.4: {} picomatch@4.0.7: {} - pluralize@2.0.0: {} - - pluralize@8.0.0: {} - postcss@8.5.30: dependencies: nanoid: 3.3.20 picocolors: 1.1.1 source-map-js: 1.2.2 - prebuild-install@7.1.3: - dependencies: - detect-libc: 2.1.2 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.92.0 - pump: 3.0.4 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.4 - tunnel-agent: 0.6.0 - optional: true - prelude-ls@1.2.1: {} prettier@3.9.10: {} @@ -4825,14 +3711,6 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 - pump@3.0.4: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - optional: true - - punycode.js@2.3.1: {} - punycode@2.3.1: {} qified@0.10.1: @@ -4844,33 +3722,6 @@ snapshots: es-define-property: 1.0.1 side-channel: 1.1.1 - queue-microtask@1.2.3: {} - - rc-config-loader@4.1.4(supports-color@8.1.1): - dependencies: - debug: 4.4.3(supports-color@8.1.1) - js-yaml: 4.2.0 - json5: 2.2.3 - require-from-string: 2.0.2 - transitivePeerDependencies: - - supports-color - - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - optional: true - - read-pkg@9.0.1: - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 6.0.2 - parse-json: 8.3.0 - type-fest: 4.41.0 - unicorn-magic: 0.1.0 - read@1.0.7: dependencies: mute-stream: 0.0.8 @@ -4885,17 +3736,8 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - optional: true - readdirp@5.1.1: {} - require-from-string@2.0.2: {} - restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -4903,8 +3745,6 @@ snapshots: retry@0.12.0: {} - reusify@1.1.0: {} - rimraf@6.1.3: dependencies: glob: 13.0.6 @@ -4933,10 +3773,6 @@ snapshots: run-applescript@7.1.0: {} - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -4945,20 +3781,6 @@ snapshots: sax@1.6.0: {} - secretlint@10.2.2(supports-color@8.1.1): - dependencies: - '@secretlint/config-creator': 10.2.2 - '@secretlint/formatter': 10.2.2(supports-color@8.1.1) - '@secretlint/node': 10.2.2(supports-color@8.1.1) - '@secretlint/profiler': 10.2.2 - debug: 4.4.3(supports-color@8.1.1) - globby: 14.1.0 - read-pkg: 9.0.1 - transitivePeerDependencies: - - supports-color - - semver@5.7.2: {} - semver@7.8.5: {} serialize-javascript@7.1.1: {} @@ -5003,42 +3825,10 @@ snapshots: signal-exit@4.1.0: {} - simple-concat@1.0.1: - optional: true - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - optional: true - simple-invariant@2.0.1: {} - slash@5.1.0: {} - - slice-ansi@4.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - source-map-js@1.2.2: {} - spdx-correct@3.2.0: - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.23 - - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.23 - - spdx-license-ids@3.0.23: {} - std-env@4.3.0: {} stdin-discarder@0.2.2: {} @@ -5059,11 +3849,6 @@ snapshots: dependencies: safe-buffer: 5.1.2 - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - optional: true - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -5072,60 +3857,16 @@ snapshots: dependencies: ansi-regex: 6.2.2 - strip-json-comments@2.0.1: - optional: true - strip-json-comments@5.0.3: {} structured-source@4.0.0: dependencies: boundary: 2.0.0 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - supports-color@8.1.1: dependencies: has-flag: 4.0.0 - supports-hyperlinks@3.2.0: - dependencies: - has-flag: 4.0.0 - supports-color: 7.2.0 - - table@6.9.0: - dependencies: - ajv: 8.20.0 - lodash.truncate: 4.4.2 - slice-ansi: 4.0.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - tar-fs@2.1.4: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.4 - tar-stream: 2.2.0 - optional: true - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - optional: true - - terminal-link@4.0.0: - dependencies: - ansi-escapes: 7.3.0 - supports-hyperlinks: 3.2.0 - - text-table@0.2.0: {} - textextensions@6.11.0: dependencies: editions: 6.22.0 @@ -5139,64 +3880,41 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 - tmp@0.2.7: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 tslib@2.8.1: {} - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - optional: true - tunnel@0.0.6: {} type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - type-fest@4.41.0: {} - typed-rest-client@1.8.11: dependencies: qs: 6.15.3 tunnel: 0.0.6 underscore: 1.13.8 - typescript-eslint@8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): + typescript-eslint@8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.71.2(@typescript-eslint/parser@8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/parser': 8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.71.2(supports-color@8.1.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.71.2(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) - eslint: 10.12.0(supports-color@8.1.1) + '@typescript-eslint/eslint-plugin': 8.72.0(@typescript-eslint/parser@8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.72.0(supports-color@8.1.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.72.0(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) + eslint: 10.13.0(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color typescript@6.0.3: {} - uc.micro@2.1.0: {} - underscore@1.13.8: {} undici-types@8.9.0: {} - undici@7.28.0: {} - - unicorn-magic@0.1.0: {} - - unicorn-magic@0.3.0: {} - - universalify@2.0.1: {} - uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -5205,11 +3923,6 @@ snapshots: util-deprecate@1.0.2: {} - validate-npm-package-license@3.0.4: - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - version-range@4.15.0: {} vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2): @@ -5224,10 +3937,10 @@ snapshots: esbuild: 0.28.2 fsevents: 2.3.3 - vitest@5.0.4(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)): + vitest@5.0.5(@types/node@26.6.5)(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.4(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)) + '@vitest/mocker': 5.0.5(vite@8.3.4(@types/node@26.6.5)(esbuild@0.28.2)) chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 @@ -5245,12 +3958,6 @@ snapshots: transitivePeerDependencies: - msw - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - which@2.0.2: dependencies: isexe: 2.0.0 @@ -5267,9 +3974,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrappy@1.0.2: - optional: true - wsl-utils@0.1.0: dependencies: is-wsl: 3.1.1 diff --git a/src/adapter/branch-selection.ts b/src/adapter/branch-selection.ts index 6593efb..da019d2 100644 --- a/src/adapter/branch-selection.ts +++ b/src/adapter/branch-selection.ts @@ -18,7 +18,7 @@ import type { BranchNode } from './tree/branches-tree'; */ export function selectedBranchRefs( clicked: BranchNode | undefined, - selection: BranchNode[] | undefined, + selection: readonly BranchNode[] | undefined, predicate: (ref: RawRef) => boolean, ): RawRef[] { const clickedRef = clicked?.kind === 'branch' ? clicked.ref : undefined; diff --git a/src/adapter/history-commands.ts b/src/adapter/history-commands.ts index 0a9b9af..9acdc52 100644 --- a/src/adapter/history-commands.ts +++ b/src/adapter/history-commands.ts @@ -14,6 +14,9 @@ import { selectedBranchRefs } from './branch-selection'; import { diffPrunedRefs, formatBranchDeleteConfirm, partitionByMerged, truncateNames } from '../engine/ref/cleanup'; import { diffShapeFromCode } from '../engine/diff/change-side'; import { resolveDiffSides } from './diff-sides'; +import { gitErrorText } from '../engine/git-state/git-error'; +import type { AuthorTerm } from '../engine/log/log-filter'; +import { buildAuthorListArgs, parseShortlog, type AuthorStat } from '../engine/log/author-suggest'; /** 注册 Log/Branches/Blame/History/Tags 相关命令。 */ export function registerHistoryCommands( @@ -23,38 +26,31 @@ export function registerHistoryCommands( favorites: BranchFavorites, ): vscode.Disposable[] { const subs: vscode.Disposable[] = []; - // vscode.git 在 git 非零退出时抛出 GitError,其 .message 为通用串「Failed to execute git」, - // 真实原因落在 .stderr。优先暴露 stderr,使「无上游」「non-fast-forward」等失败可读。 - const errMsg = (e: unknown): string => { - if (e && typeof e === 'object') { - const ge = e as { stderr?: unknown; message?: unknown }; - if (typeof ge.stderr === 'string' && ge.stderr.trim()) { - return ge.stderr.trim(); - } - if (typeof ge.message === 'string' && ge.message) { - return ge.message; - } - } - return String(e); - }; subs.push(vscode.commands.registerCommand('hyperGit.refreshLog', () => logTree.refresh())); subs.push(vscode.commands.registerCommand('hyperGit.refreshBranches', () => branchesTree.refresh())); subs.push( - vscode.commands.registerCommand('hyperGit.logFilterAuthor', async () => { - const author = await vscode.window.showInputBox({ prompt: 'Filter by Author', placeHolder: 'e.g. John' }); - const f = logTree.getFilter(); - logTree.setFilter({ ...f, author: author && author.trim() ? author.trim() : undefined }); - }), + vscode.commands.registerCommand('hyperGit.logFilterAuthor', () => pickAuthors(service, logTree)), ); subs.push( vscode.commands.registerCommand('hyperGit.logFilterPath', async () => { + // 切库竞态守卫(pickAuthors rootAtStart 同范式):对话框 await 期间切库时,所选文件 + // 会按切换后的仓库 root 求相对路径,得到跨仓库垃圾 path 过滤 → 仓库已变即放弃。 + const rootAtStart = service.repoRoot; const uri = await vscode.window.showOpenDialog({ canSelectMany: false }); const repo = service.repo; - if (uri?.[0] && repo) { + if (uri?.[0] && repo && service.repoRoot === rootAtStart) { const rel = path.relative(repo.rootUri.fsPath, uri[0].fsPath).split(path.sep).join('/'); + // 越仓选择(多根他仓文件 / 单仓外导航)同样产出 '../…' 垃圾 path,且随持久化跨重启 + // 粘性污染(git log fatal)→ 拒绝。按路径段判定:rel 已归一为 '/' 分隔,'..' 或 + // '../' 前缀才是越仓;'..' 仅作文件名前缀(如 '..drafts/note.md')是仓内合法文件,勿误杀 + // (showBlame 的 startsWith('..') 会对后者误杀,属预存差异,不在此处对齐)。 + if (rel === '..' || rel.startsWith('../') || path.isAbsolute(rel)) { + void vscode.window.showWarningMessage('This file is outside the current repository; cannot filter by path'); + return; + } const f = logTree.getFilter(); logTree.setFilter({ ...f, path: rel }); } @@ -63,24 +59,30 @@ export function registerHistoryCommands( subs.push(vscode.commands.registerCommand('hyperGit.logClearFilter', () => logTree.clearFilter())); - subs.push( - vscode.commands.registerCommand('hyperGit.logFilter', async () => { - // 过滤器聚合入口:QuickPick 分发到既有 6 个过滤命令(复用 handler,零重复实现)。 - const items = [ - { label: 'Filter by Author…', description: 'commits by author', action: 'hyperGit.logFilterAuthor' }, - { label: 'Filter by Path…', description: 'commits touching a path', action: 'hyperGit.logFilterPath' }, - { label: 'Filter by Message (grep)…', description: 'substring match', action: 'hyperGit.logFilterGrep' }, - { label: 'Filter by Message (regex)…', description: 'regular expression', action: 'hyperGit.logFilterRegex' }, - { label: 'Filter Merge Commits…', description: 'show or hide merge commits', action: 'hyperGit.logFilterMergeMode' }, - { label: 'Filter by Date…', description: 'commits within recent days', action: 'hyperGit.logFilterDate' }, - { label: 'Clear Filter', description: 'remove all active filters', action: 'hyperGit.logClearFilter' }, - ]; - const pick = await vscode.window.showQuickPick(items, { placeHolder: 'Filter commit log' }); - if (pick) { - await vscode.commands.executeCommand(pick.action); - } - }), - ); + // 过滤器聚合入口:QuickPick 分发到既有 6 个过滤命令(复用 handler,零重复实现)。 + // 切库竞态守卫(其余对话框同范式):await 期间切库即放弃——Clear Filter 项直接执行, + // 无守卫会删切换后仓库的持久化 key;过滤子命令也应在新仓库语境下重新发起。 + // 双命令(logFilter/logFilterOn,detailTree/detailFlat 同范式):常态与「过滤激活」图标经 + // hyperGit.log.filterActive context key 互斥呈现——持久化恢复的过滤不再是无声约束。 + const openFilterPicker = async (): Promise => { + const rootAtStart = service.repoRoot; + const items = [ + { label: 'Filter by Author…', description: 'commits by author', action: 'hyperGit.logFilterAuthor' }, + { label: 'Filter by Path…', description: 'commits touching a path', action: 'hyperGit.logFilterPath' }, + { label: 'Filter by Message (grep)…', description: 'substring match', action: 'hyperGit.logFilterGrep' }, + { label: 'Filter by Message (regex)…', description: 'regular expression', action: 'hyperGit.logFilterRegex' }, + { label: 'Filter Merge Commits…', description: 'show or hide merge commits', action: 'hyperGit.logFilterMergeMode' }, + { label: 'Filter by Date…', description: 'commits within recent days', action: 'hyperGit.logFilterDate' }, + { label: 'Clear Filter', description: 'remove all active filters', action: 'hyperGit.logClearFilter' }, + ]; + const pick = await vscode.window.showQuickPick(items, { placeHolder: 'Filter commit log' }); + if (!pick || service.repoRoot !== rootAtStart) { + return; + } + await vscode.commands.executeCommand(pick.action); + }; + subs.push(vscode.commands.registerCommand('hyperGit.logFilter', () => openFilterPicker())); + subs.push(vscode.commands.registerCommand('hyperGit.logFilterOn', () => openFilterPicker())); subs.push( vscode.commands.registerCommand('hyperGit.copyCommitHash', (node: LogNode) => { @@ -94,7 +96,8 @@ export function registerHistoryCommands( subs.push( vscode.commands.registerCommand('hyperGit.showHistory', async (change?: ChangeItem) => { if (change?.relativePath) { - logTree.setFilter({ path: change.relativePath }); + // 增量式与其余过滤命令同构:整替 filter 会静默清掉持久化的 authors(文件历史与作者过滤相交而非重置)。 + logTree.setFilter({ ...logTree.getFilter(), path: change.relativePath }); } await vscode.commands.executeCommand('hyperGit.log.focus'); }), @@ -115,7 +118,7 @@ export function registerHistoryCommands( await repo.createBranch(name.trim(), true); branchesTree.refresh(); } catch (e) { - void showGitError(`Failed to create branch: ${errMsg(e)}`); + void showGitError(`Failed to create branch: ${gitErrorText(e)}`); } } }), @@ -131,7 +134,7 @@ export function registerHistoryCommands( await repo.checkout(node.ref.shortName); branchesTree.refresh(); } catch (e) { - void showGitError(`Failed to checkout: ${errMsg(e)}`); + void showGitError(`Failed to checkout: ${gitErrorText(e)}`); } }), ); @@ -202,7 +205,7 @@ export function registerHistoryCommands( branchesTree.refresh(); } catch (e) { if (!(await handleGitConflict(service, 'Merge'))) { - void showGitError(`Failed to merge: ${errMsg(e)}`); + void showGitError(`Failed to merge: ${gitErrorText(e)}`); } } }), @@ -224,7 +227,7 @@ export function registerHistoryCommands( branchesTree.refresh(); } catch (e) { if (!(await handleGitConflict(service, 'Rebase'))) { - void showGitError(`Failed to rebase: ${errMsg(e)}`); + void showGitError(`Failed to rebase: ${gitErrorText(e)}`); } } }), @@ -251,7 +254,7 @@ export function registerHistoryCommands( const doc = await vscode.workspace.openTextDocument({ content: blame, language: 'plaintext' }); await vscode.window.showTextDocument(doc, { preview: true }); } catch (e) { - void showGitError(`Failed to Blame: ${errMsg(e)}`); + void showGitError(`Failed to Blame: ${gitErrorText(e)}`); } }), ); @@ -267,7 +270,7 @@ export function registerHistoryCommands( branchesTree.refresh(); } catch (e) { if (!(await handleGitConflict(service, 'Pull'))) { - void showGitError(`Failed to Pull: ${errMsg(e)}`); + void showGitError(`Failed to Pull: ${gitErrorText(e)}`); } } }), @@ -309,7 +312,7 @@ export function registerHistoryCommands( branchesTree.refresh(); void vscode.window.showInformationMessage(`Pushed ${head.name}`); } catch (e) { - void showGitError(`Failed to Push: ${errMsg(e)}`); + void showGitError(`Failed to Push: ${gitErrorText(e)}`); } }), ); @@ -352,7 +355,7 @@ export function registerHistoryCommands( branchesTree.refresh(); void vscode.window.showInformationMessage(`Pushed ${name}`); } catch (e) { - void showGitError(`Failed to Push: ${errMsg(e)}`); + void showGitError(`Failed to Push: ${gitErrorText(e)}`); } }), ); @@ -375,7 +378,7 @@ export function registerHistoryCommands( branchesTree.refresh(); logTree.refresh(); } catch (e) { - void showGitError(`Failed to Fetch: ${errMsg(e)}`); + void showGitError(`Failed to Fetch: ${gitErrorText(e)}`); } }), ); @@ -451,7 +454,7 @@ export function registerHistoryCommands( await repo.createBranch(name.trim(), true, source); branchesTree.refresh(); } catch (e) { - void showGitError(`Failed to create branch: ${errMsg(e)}`); + void showGitError(`Failed to create branch: ${gitErrorText(e)}`); } }), ); @@ -468,7 +471,7 @@ export function registerHistoryCommands( const doc = await vscode.workspace.openTextDocument({ content: `$ git diff --stat HEAD...${selected}\n\n${out}`, language: 'plaintext' }); await vscode.window.showTextDocument(doc, { preview: true }); } catch (e) { - void showGitError(`Failed to compare: ${errMsg(e)}`); + void showGitError(`Failed to compare: ${gitErrorText(e)}`); } }), ); @@ -507,7 +510,7 @@ export function registerHistoryCommands( branchesTree.refresh(); void vscode.window.showInformationMessage(`Created tag ${name.trim()} @ ${pick.target === 'HEAD' ? 'HEAD' : pick.target.slice(0, 7)}`); } catch (e) { - void showGitError(`Failed to create tag: ${errMsg(e)}`); + void showGitError(`Failed to create tag: ${gitErrorText(e)}`); } }), ); @@ -558,7 +561,7 @@ export function registerHistoryCommands( await repo.checkout(name); branchesTree.refresh(); } catch (e) { - void showGitError(`Failed to checkout tag: ${errMsg(e)}`); + void showGitError(`Failed to checkout tag: ${gitErrorText(e)}`); } }), ); @@ -587,7 +590,7 @@ export function registerHistoryCommands( try { await vscode.commands.executeCommand('vscode.diff', left, right, `${filePath} · ${hash.slice(0, 7)} (commit diff)`, { preview: true }); } catch (e) { - void showGitError(`Failed to open diff: ${errMsg(e)}`); + void showGitError(`Failed to open diff: ${gitErrorText(e)}`); } }), ); @@ -621,7 +624,7 @@ export function registerHistoryCommands( void vscode.window.showInformationMessage(`Reset (--${pick.label} ${hash.slice(0, 7)}) complete`); } catch (e) { if (!(await handleGitConflict(service, 'Reset'))) { - void showGitError(`Failed to Reset: ${errMsg(e)}`); + void showGitError(`Failed to Reset: ${gitErrorText(e)}`); } } }), @@ -646,7 +649,7 @@ export function registerHistoryCommands( branchesTree.refresh(); void vscode.window.showInformationMessage(`Created and checked out ${name.trim()} @ ${node.commit.hash.slice(0, 7)}`); } catch (e) { - void showGitError(`Failed to create branch: ${errMsg(e)}`); + void showGitError(`Failed to create branch: ${gitErrorText(e)}`); } }), ); @@ -669,7 +672,7 @@ export function registerHistoryCommands( branchesTree.refresh(); void vscode.window.showInformationMessage(`Created tag ${name.trim()} @ ${node.commit.hash.slice(0, 7)}`); } catch (e) { - void showGitError(`Failed to create tag: ${errMsg(e)}`); + void showGitError(`Failed to create tag: ${gitErrorText(e)}`); } }), ); @@ -687,14 +690,20 @@ export function registerHistoryCommands( }); await vscode.window.showTextDocument(doc, { preview: true }); } catch (e) { - void showGitError(`Failed to query: ${errMsg(e)}`); + void showGitError(`Failed to query: ${gitErrorText(e)}`); } }), ); subs.push( vscode.commands.registerCommand('hyperGit.logFilterGrep', async () => { + // 切库竞态守卫(logFilterPath/settle 同范式,grep/mergeMode/date/regex 四命令同): + // 对话框 await 期间切库即放弃,防旧仓库输入经 persistFilter 误写新仓库持久化 key。 + const rootAtStart = service.repoRoot; const grep = await vscode.window.showInputBox({ prompt: 'Filter by message text/regex (--grep)', placeHolder: 'e.g. fix|bug' }); + if (service.repoRoot !== rootAtStart) { + return; + } const f = logTree.getFilter(); logTree.setFilter({ ...f, grep: grep && grep.trim() ? grep.trim() : undefined }); }), @@ -702,6 +711,7 @@ export function registerHistoryCommands( subs.push( vscode.commands.registerCommand('hyperGit.logFilterMergeMode', async () => { + const rootAtStart = service.repoRoot; const pick = await vscode.window.showQuickPick( [ { label: 'All Commits', mode: 'all' as MergeMode }, @@ -710,7 +720,7 @@ export function registerHistoryCommands( ], { placeHolder: 'Merge commit filter mode' }, ); - if (!pick) { + if (!pick || service.repoRoot !== rootAtStart) { return; } const f = logTree.getFilter(); @@ -720,6 +730,7 @@ export function registerHistoryCommands( subs.push( vscode.commands.registerCommand('hyperGit.logFilterDate', async () => { + const rootAtStart = service.repoRoot; const now = Date.now(); const pick = await vscode.window.showQuickPick( [ @@ -730,7 +741,7 @@ export function registerHistoryCommands( ], { placeHolder: 'Commit date range' }, ); - if (!pick) { + if (!pick || service.repoRoot !== rootAtStart) { return; } const f = logTree.getFilter(); @@ -740,7 +751,11 @@ export function registerHistoryCommands( subs.push( vscode.commands.registerCommand('hyperGit.logFilterRegex', async () => { + const rootAtStart = service.repoRoot; const re = await vscode.window.showInputBox({ prompt: 'Filter by message regex (client-side)', placeHolder: 'e.g. ^feat:' }); + if (service.repoRoot !== rootAtStart) { + return; + } const f = logTree.getFilter(); logTree.setFilter({ ...f, messageRegex: re && re.trim() ? re.trim() : undefined }); }), @@ -749,6 +764,106 @@ export function registerHistoryCommands( return subs; } +/** + * 作者过滤选择器(仓库首个 createQuickPick 动态用法):候选来自 `git shortlog`(按当前视图 scope 取数、 + * 按提交数降序),QuickPick 内建模糊过滤实现「输入前缀 → 建议下拉」;canSelectMany 多选(git 对多条 + * `--author` 取 OR)。输入不等于任何作者姓名时头部动态插入「Use pattern」逃生口(原样正则透传, + * 保留旧 InputBox 的任意 pattern 能力);确认时空选 = 清除 author 维度,Esc = 不变。 + */ +async function pickAuthors(service: GitRepositoryService, logTree: LogFilterControl): Promise { + /** exact 项勾选键(同名异邮箱不串)。 */ + const key = (t: { name: string; email: string }) => `${t.name} <${t.email}>`; + const qp = vscode.window.createQuickPick(); + qp.canSelectMany = true; + qp.matchOnDescription = true; + qp.matchOnDetail = true; + qp.title = 'Filter by Author'; + qp.placeholder = 'Type to search authors; check to filter (empty selection clears)'; + // 先展示再取数:busy 遮盖等待;事件先于 await 注册,取数期间的键入即触发 rebuild。 + qp.busy = true; + qp.items = []; + qp.show(); + + // 勾选自维护:重建 items 会重置 selectedItems,须记录并在重建后回写。 + // 预勾选仅 exact 项(pattern 项随输入动态生成,无法预置);重建触发的程序化 selection + // 事件与用户点击不可区分,以时间窗抑制(程序化事件在赋值后即刻送达)。 + const persistedExact = logTree.getFilter().authors?.filter((t) => t.kind === 'exact') ?? []; + let checked = new Set(persistedExact.map(key)); + let patternOn = false; + let suppressUntil = 0; + let authors: readonly AuthorStat[] = []; + // 候选集外的持久化 exact 作者(scope 已切换 / shortlog 失败降级)以 ghost 项呈现: + // selectedItems 只能来自 items,缺项者确认时会被静默丢出持久化 key——ghost 预勾选保留、可取消移除。 + const ghostItems = (): AuthorPickItem[] => { + const present = new Set(authors.map((a) => key(a))); + return persistedExact + .filter((t) => !present.has(key(t))) + .map((t) => ({ label: t.name, description: 'not in current scope', detail: t.email, term: t })); + }; + const rebuild = (value: string): void => { + const v = value.trim(); + const patternItem: AuthorPickItem[] = + v && !authors.some((a) => a.name === v) + ? [{ label: `Use pattern "${v}"`, alwaysShow: true, description: 'regular expression (OR-ed)', term: { kind: 'pattern', pattern: v } }] + : []; + suppressUntil = Date.now() + 100; + qp.items = [...patternItem, ...baseItems(authors), ...ghostItems()]; + qp.selectedItems = qp.items.filter((i) => (i.term.kind === 'exact' ? checked.has(key(i.term)) : patternOn)); + }; + qp.onDidChangeValue(rebuild); + qp.onDidChangeSelection((sel) => { + if (Date.now() < suppressUntil) { + return; + } + patternOn = sel.some((i) => i.term.kind === 'pattern'); + checked = new Set(sel.flatMap((i) => (i.term.kind === 'exact' ? [key(i.term)] : []))); + }); + + // settle 幂等守卫:accept 后 onDidHide 仍会触发;undefined = 取消不改,[] = 清除 author 维度。 + // 取数窗口外仍可切库(环境驱动切换不夺焦点、QuickPick 不隐藏):确认时仓库已变则放弃写回, + // 防旧仓库勾选经 setFilter→persistAuthors 误写新仓库的持久化 key。 + const rootAtStart = service.repoRoot; + let settled = false; + const settle = (terms: readonly AuthorTerm[] | undefined): void => { + if (settled) { + return; + } + settled = true; + qp.dispose(); + if (terms && service.repoRoot === rootAtStart) { + logTree.setFilter({ ...logTree.getFilter(), authors: terms.length ? terms : undefined }); + } + }; + qp.onDidAccept(() => settle(qp.selectedItems.map((i) => i.term))); + qp.onDidHide(() => settle(undefined)); + + // 候选按当前视图 scope 一次取数(非逐键);切库竞态守卫见上方 rootAtStart; + // 空仓库 / unborn HEAD / 查询失败 → 空列表降级(仅 pattern 逃生口可用)。 + authors = parseShortlog(await service.execGit(['shortlog', ...buildAuthorListArgs(logTree.getScope())]).catch(() => '')); + if (service.repoRoot !== rootAtStart) { + qp.dispose(); + return; + } + // 取数完成以 qp.value(而非 '')重建:纳入等待期间的键入。 + rebuild(qp.value); + qp.busy = false; +} + +/** shortlog 统计 → QuickPick 项(label=姓名、description=提交数、detail=email 供模糊匹配)。 */ +function baseItems(authors: readonly AuthorStat[]): AuthorPickItem[] { + return authors.map((a) => ({ + label: a.name, + description: a.count === 1 ? '1 commit' : `${a.count} commits`, + detail: a.email, + term: { kind: 'exact', name: a.name, email: a.email }, + })); +} + +/** 作者候选 QuickPick 项(label=姓名、description=提交数、detail=email,term 为过滤写回项)。 */ +interface AuthorPickItem extends vscode.QuickPickItem { + readonly term: AuthorTerm; +} + /** * 列出当前 `refs/remotes/*` 短名(如 `origin/master`),供 prune 前后做差集。 * 解析 `git for-each-ref --format=%(refname:short) refs/remotes`:逐行去空白、去空行。 diff --git a/src/adapter/remote-commands.ts b/src/adapter/remote-commands.ts index 0c31fb7..a096c7b 100644 --- a/src/adapter/remote-commands.ts +++ b/src/adapter/remote-commands.ts @@ -8,26 +8,39 @@ import type { LogFilterControl } from './webview/log-webview'; import { truncateNames } from '../engine/ref/cleanup'; import { formatRemoteDeleteConfirm, + formatRemoteDeleteResult, + isRemoteRefMissingError, partitionRemoteByProtected, resolveRemoteBranch, type RemoteBranchTarget, + type RemoteDeleteFailure, } from '../engine/ref/remote-ref'; +import { FOR_EACH_REF_FORMAT, parseForEachRef, type RawRef } from '../engine/ref/for-each-ref'; +import { + planBranchUpdates, + formatBranchUpdateResult, + classifyFetchOutcome, + type BranchUpdateAction, + type BranchUpdateFailure, +} from '../engine/ref/branch-update'; import type { GitRepositoryService } from './git-repository-service'; - -const errMsg = (e: unknown): string => (e instanceof Error ? e.message : String(e)); +import { gitErrorText } from '../engine/git-state/git-error'; /** - * 注册远程同步对话框(Phase 3):Push 对话框 / Update Project / Merge 对话框。 + * 注册远程同步命令(Phase 3 起):Push 对话框 / Update Project / Merge 对话框 / 分支就地更新 / 删远程分支。 * * vscode.git 稳定 API 的 pull/push 缺策略/force/tags 选项,故经 execGit 实现: * - Push:force/force-with-lease/push tags(核心走 repo.push,tags 走 CLI)。 * - Update Project:pull --rebase / --no-rebase(策略)。 * - Merge:--ff-only / --no-ff / --squash + 自定义 message。 + * - Update Branch:`git fetch refs/heads/:refs/heads/`(原生 fast-forward-only, + * 不 checkout 就地更新本地分支;当前分支改走 repo.pull)。 */ export function registerRemoteCommands( service: GitRepositoryService, branchesTree: BranchesTreeProvider, logTree: LogFilterControl, + branchesView: vscode.TreeView, ): vscode.Disposable[] { const subs: vscode.Disposable[] = []; @@ -83,7 +96,7 @@ export function registerRemoteCommands( void vscode.window.showInformationMessage(`Pushed to ${remote} ${previewCount}`.trim()); } catch (e) { if (!(await handleGitConflict(service, 'Push'))) { - void showGitError(`Push failed: ${errMsg(e)}`); + void showGitError(`Push failed: ${gitErrorText(e)}`); } } }), @@ -114,8 +127,117 @@ export function registerRemoteCommands( void vscode.window.showInformationMessage('Update Project complete'); } catch (e) { if (!(await handleGitConflict(service, 'Update'))) { - void showGitError(`Update failed: ${errMsg(e)}`); + void showGitError(`Update failed: ${gitErrorText(e)}`); + } + } + }), + ); + + subs.push( + vscode.commands.registerCommand('hyperGit.branchUpdate', async (node?: BranchNode, nodes?: BranchNode[]) => { + const repo = service.repo; + if (!repo) { + return; + } + // 目标解析链:右键实参 →(无实参的标题栏路径)视图选区 → 无选区等价 Pull(当前分支); + // 选区非空但不含本地分支时不动作(显式选区优先,不回退 Pull)。仅本地、非 tag。 + const isLocal = (r: RawRef) => !r.isRemote && !r.isTag; + let targets = selectedBranchRefs(node, nodes, isLocal); + if (targets.length === 0 && !node) { + targets = selectedBranchRefs(undefined, branchesView.selection, isLocal); + } + if (targets.length === 0) { + if (node || branchesView.selection.length > 0) { + // 点击项/选区不含本地分支(右键路径防御——菜单门控下不可达;标题栏路径为 + // 仅远程/标签/分组节点的显式选区)。显式选区优先,不回退 Pull,避免静默改写工作区。 + return; + } + const headName = repo.state.HEAD?.name; + if (!headName) { + void vscode.window.showWarningMessage('Currently in detached HEAD; no branch to update'); + return; + } + // 无选区 → 语义兜底为 Pull 当前分支,复用既有命令(含冲突通道)。 + await vscode.commands.executeCommand('hyperGit.pull'); + return; + } + // remotes 未配置/未就绪时不排程(姊妹命令同守卫):避免把有上游分支误报 no upstream。 + const remotes = repo.state.remotes.map((r) => r.name); + if (remotes.length === 0) { + void vscode.window.showWarningMessage('No remote repository configured'); + return; + } + // 新鲜快照:树节点 ref.upstream/head 在 API 路径下为空(refToRaw 不填),禁止信任节点数据。 + let snapshot: ReadonlyMap; + try { + const out = await service.execGit(['for-each-ref', `--format=${FOR_EACH_REF_FORMAT}`, 'refs/heads']); + snapshot = new Map(parseForEachRef(out).map((r) => [r.shortName, r] as const)); + } catch (e) { + void showGitError(`Failed to read branch info: ${gitErrorText(e)}`); + return; + } + // 引擎纯逻辑排程 + 逐分支执行(per-item try/catch,三桶先例)。 + const headName = repo.state.HEAD?.name; + const plan = planBranchUpdates( + targets.map((r) => r.shortName), + snapshot, + headName, + remotes, + ); + const skipped = plan.filter((a): a is Extract => a.kind === 'skip-no-upstream').map((a) => a.name); + const updated: string[] = []; + const upToDate: string[] = []; + const pulled: string[] = []; + const failures: BranchUpdateFailure[] = []; + const failedNames = new Set(); + await runWithProgress('Updating branch(es)…', async () => { + for (const action of plan) { + if (action.kind === 'skip-no-upstream') { + continue; + } + try { + if (action.kind === 'pull-current') { + await repo.pull(); + pulled.push(action.name); + } else { + // src 侧全限定:`+` 开头的合法上游名裸写会被 git 解析为 force 前缀,绕过 fast-forward-only。 + await service.execGit(['fetch', action.remote, `refs/heads/${action.upstreamBranch}:refs/heads/${action.name}`]); + } + } catch (e) { + if (action.kind === 'pull-current') { + // 冲突 UI 先行展示;仍按失败计数,维持「选中数 = 各桶之和」的对账诚实。 + await handleGitConflict(service, 'Pull'); + } + failedNames.add(action.name); + failures.push({ name: action.name, reason: gitErrorText(e) }); + } } + }); + // 事后一次 for-each-ref:sha 对比分类 updated/upToDate(fetch 静默 no-op 无输出可解析)。 + try { + const out = await service.execGit(['for-each-ref', `--format=${FOR_EACH_REF_FORMAT}`, 'refs/heads']); + const after = new Map(parseForEachRef(out).map((r) => [r.shortName, r.objectname] as const)); + for (const a of plan) { + if (a.kind !== 'fetch' || failedNames.has(a.name)) { + continue; + } + (classifyFetchOutcome(a.beforeSha, after.get(a.name)) === 'updated' ? updated : upToDate).push(a.name); + } + } catch { + // 分类不可得 → 保守计 updated(fetch 已成功,仅失判定粒度)。 + for (const a of plan) { + if (a.kind === 'fetch' && !failedNames.has(a.name)) { + updated.push(a.name); + } + } + } + branchesTree.refresh(); + logTree.refresh(); + const result = formatBranchUpdateResult({ updated, upToDate, pulled, skipped, failures }); + if (result.level === 'info') { + void vscode.window.showInformationMessage(result.message); + } else { + void showGitError(result.message); } }), ); @@ -156,7 +278,7 @@ export function registerRemoteCommands( void vscode.window.showInformationMessage(`Merged ${target}`); } catch (e) { if (!(await handleGitConflict(service, 'Merge'))) { - void showGitError(`Merge failed: ${errMsg(e)}`); + void showGitError(`Merge failed: ${gitErrorText(e)}`); } } }), @@ -206,19 +328,26 @@ export function registerRemoteCommands( if (choice !== confirmLabel) { return; } - // 逐分支推删,收集失败(不调用 handleGitConflict——删除不产生合并冲突)。 - const failures: string[] = []; + // 逐分支推删,三桶收集(不调用 handleGitConflict——删除不产生合并冲突): + // 成功 / 服务端已无此 ref(陈旧 tracking ref 场景,目标态已达成按成功收尾)/ 真失败(保留原因)。 + const failures: RemoteDeleteFailure[] = []; const succeeded: RemoteBranchTarget[] = []; + const alreadyGone: RemoteBranchTarget[] = []; for (const t of deletable) { try { await service.execGit(['push', t.remote, '--delete', t.branch]); succeeded.push(t); - } catch { - failures.push(t.shortName); + } catch (e) { + if (isRemoteRefMissingError(e)) { + alreadyGone.push(t); + } else { + failures.push({ name: t.shortName, reason: gitErrorText(e) }); + } } } - // 仅对服务端删除成功项清理本地 remote-tracking ref(失败项保留,待重试或下次 fetch --prune)。 - for (const t of succeeded) { + // 对服务端已无此分支的全部目标(含 alreadyGone 的陈旧项)清理本地 remote-tracking ref; + // 真失败项保留,待重试或下次 fetch --prune。 + for (const t of [...succeeded, ...alreadyGone]) { try { await service.execGit(['branch', '-D', '-r', `${t.remote}/${t.branch}`]); } catch { @@ -227,14 +356,11 @@ export function registerRemoteCommands( } branchesTree.refresh(); logTree.refresh(); - if (failures.length === 0) { - void vscode.window.showInformationMessage( - succeeded.length === 1 ? `Deleted remote branch ${succeeded[0].shortName}` : `Deleted ${succeeded.length} remote branches`, - ); + const result = formatRemoteDeleteResult(succeeded, alreadyGone, failures); + if (result.level === 'info') { + void vscode.window.showInformationMessage(result.message); } else { - void vscode.window.showWarningMessage( - `Deleted ${succeeded.length} remote branches, ${failures.length} failed: ${truncateNames(failures)}`, - ); + void showGitError(result.message); } }), ); diff --git a/src/adapter/webview/commit-webview.ts b/src/adapter/webview/commit-webview.ts index 8f6b980..800582d 100644 --- a/src/adapter/webview/commit-webview.ts +++ b/src/adapter/webview/commit-webview.ts @@ -170,8 +170,11 @@ export class CommitWebviewProvider implements vscode.WebviewViewProvider, vscode { label: '$(diff-ignored) Add to .gitignore', command: 'hyperGit.ignorePath' }, { label: '$(discard) Discard Changes', command: 'hyperGit.discardChanges' }, ]; + // 切库守卫(rootAtStart 范式):菜单 await 期间切库时 change 属旧仓库语境(showHistory 的 + // path 随持久化落盘会污染新仓库),任何动作都放弃派发。 + const rootAtStart = this.service.repoRoot; const pick = await vscode.window.showQuickPick(actions.slice(), { placeHolder: relativePath }); - if (!pick) { + if (!pick || this.service.repoRoot !== rootAtStart) { return; } await vscode.commands.executeCommand(pick.command, change); diff --git a/src/adapter/webview/log-webview.ts b/src/adapter/webview/log-webview.ts index b11b020..bc51465 100644 --- a/src/adapter/webview/log-webview.ts +++ b/src/adapter/webview/log-webview.ts @@ -1,7 +1,16 @@ import * as vscode from 'vscode'; import type { GitRepositoryService } from '../git-repository-service'; import { parseNameStatus, statusLabel, parseShortStat } from '../../engine/log/commit-files'; -import { applyClientFilters, toClientFilter, type LogFilter } from '../../engine/log/log-filter'; +import { + applyClientFilters, + hasActiveFilter, + parsePersistedAuthors, + parsePersistedFilter, + serializePersistableFilter, + toClientFilter, + type AuthorTerm, + type LogFilter, +} from '../../engine/log/log-filter'; import { DEFAULT_LANE_PALETTE } from '../../engine/log/graph-color'; import { computeGraphLayout, maxLanes } from '../../engine/log/graph-layout'; import { getBaseStyles, GRAPH_ROW_H, GRAPH_LANE_W, ICON_CHEVRON_DOWN, ICON_CLOSE } from './shared-styles'; @@ -72,6 +81,8 @@ export interface LogFilterControl extends vscode.Disposable { setFilter(filter: LogFilter): void; clearFilter(): void; getFilter(): LogFilter; + /** 当前视图 scope(作者候选等按视图范围取数的消费方使用;视图状态,不入 LogFilter)。 */ + getScope(): LogScope; refresh(): void; } @@ -108,6 +119,14 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter private static dmodeKey(root: string): string { return `hyperGit.log.dmode:${root}`; } + /** authors 过滤按仓库持久化 key(同 scope/dmode 范式;值为 AuthorTerm[] JSON 串,脏数据经 parsePersistedAuthors 免疫)。 */ + private static authorsKey(root: string): string { + return `hyperGit.log.authors:${root}`; + } + /** 非 authors 过滤维度(path/grep/mergeMode/dateFrom/dateTo/messageRegex)组合持久化 key(整体 JSON 串,单 key 原子读写防半恢复态)。 */ + private static filterKey(root: string): string { + return `hyperGit.log.filter:${root}`; + } /** context key 同步(标题栏按钮显隐 / 勾选态由 when 子句驱动)。 */ private static setCtx(key: string, value: string | boolean): void { void vscode.commands.executeCommand('setContext', key, value); @@ -126,25 +145,29 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter t = setTimeout(() => this.refresh(), 400); }), ); - // 激活即恢复当前仓库的 scope/dmode 记忆并同步 context key(标题栏控件先于视图解析渲染)。 + // 激活即恢复当前仓库的 scope/dmode/过滤记忆并同步 context key(标题栏控件先于视图解析渲染)。 this.loadRepoScopedPrefs(); - // 活跃仓库切换(issue #107):host 级过滤条件是旧仓库语境的产物,跨仓库无意义 → 清空; - // scope/dmode 恢复该仓库的记忆值(无记忆回退 'all'/'flat');图数据重拉由上方 onDidChange 订阅驱动。 + // 活跃仓库切换(issue #107):过滤条件是旧仓库语境的产物,跨仓库无意义 → 整体重载为 + // 新仓库的持久过滤(无记忆回退不过滤);图数据重拉由上方 onDidChange 订阅驱动 + // (service 契约保证本事件先于 onDidChange fire)。 this.disposables.push( service.onDidChangeRepository(() => { - this.filter = {}; this.loadRepoScopedPrefs(); }), ); } - /** 装载当前仓库的 scope / dmode 偏好(memento → 内存 + context key)。 */ + /** 装载当前仓库的 scope / dmode / 过滤器偏好(memento → 内存 + context key;filter 整体重赋)。 */ private loadRepoScopedPrefs(): void { const root = this.service.repoRoot; this.scope = (root ? this.workspaceState.get(LogWebviewProvider.scopeKey(root)) : undefined) ?? 'all'; this.detailMode = (root ? this.workspaceState.get<'flat' | 'tree'>(LogWebviewProvider.dmodeKey(root)) : undefined) ?? 'flat'; LogWebviewProvider.setCtx('hyperGit.log.scope', this.scope); LogWebviewProvider.setCtx('hyperGit.log.tree', this.detailMode === 'tree'); + const persisted = root ? parsePersistedFilter(this.workspaceState.get(LogWebviewProvider.filterKey(root))) : undefined; + const authors = root ? parsePersistedAuthors(this.workspaceState.get(LogWebviewProvider.authorsKey(root))) : undefined; + this.filter = { ...(persisted ?? {}), ...(authors ? { authors } : {}) }; + LogWebviewProvider.setCtx('hyperGit.log.filterActive', hasActiveFilter(this.filter)); } /** 标题栏 Scope 子菜单选中项(hyperGit.log.scopeAll/Current/Checkpointer 命令入口)。 */ @@ -174,18 +197,50 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter setFilter(filter: LogFilter): void { this.filter = filter; + this.persistFilter(filter); + this.persistAuthors(filter.authors); + LogWebviewProvider.setCtx('hyperGit.log.filterActive', hasActiveFilter(filter)); this.refresh(); } clearFilter(): void { this.filter = {}; + this.persistFilter(undefined); + this.persistAuthors(undefined); + LogWebviewProvider.setCtx('hyperGit.log.filterActive', false); this.refresh(); } + /** + * authors 维度按仓库持久化(写入点汇聚于 setFilter/clearFilter):重启与切库后经 + * {@link loadRepoScopedPrefs} 恢复;repoRoot 未知时仅内存(激活后切库事件自然覆盖)。 + * `update(key, undefined)` 即删除条目 —— 清除同样持久化,重启不复活。 + */ + private persistAuthors(authors: readonly AuthorTerm[] | undefined): void { + const root = this.service.repoRoot; + if (!root) { + return; + } + void this.workspaceState.update(LogWebviewProvider.authorsKey(root), authors?.length ? JSON.stringify(authors) : undefined); + } + + /** 非 authors 过滤维度组合持久化(同 {@link persistAuthors} 范式;归一化后全空即删 key)。 */ + private persistFilter(filter: LogFilter | undefined): void { + const root = this.service.repoRoot; + if (!root) { + return; + } + void this.workspaceState.update(LogWebviewProvider.filterKey(root), serializePersistableFilter(filter)); + } + getFilter(): LogFilter { return this.filter; } + getScope(): LogScope { + return this.scope; + } + refresh(): void { clearTimeout(this.refreshTimer); this.refreshTimer = setTimeout(() => { diff --git a/src/engine/git-state/git-error.ts b/src/engine/git-state/git-error.ts new file mode 100644 index 0000000..fefb36f --- /dev/null +++ b/src/engine/git-state/git-error.ts @@ -0,0 +1,27 @@ +/** + * Git 错误文本提取的单一事实源(纯逻辑,零 vscode 依赖)。 + * + * CLI 通道({@link GitRepositoryService.execGit} 的约定)失败时把真实原因透传挂载在错误对象的 + * `.stderr`,`err.message` 只是 execFile 的通用串;vscode.git API 通道的 GitError 同样以 `.stderr` + * 承载真实原因。本函数统一两条通道的提取语义:非空白 stderr 优先(trim 后返回),回退 message, + * 最终 `String(e)` 兜底。新增代码与 engine 层分类一律引用本函数;存量 adapter 中尚有约 13 处 + * 只读 message 的旧式内联 `errMsg`(预先存在),待逐步迁移,禁止再新增内联拷贝。 + */ + +/** + * 从 git 失败的拒绝值中提取最可读的错误文本。 + * @param e execGit / vscode.git API 抛出的错误(任意 unknown) + * @returns 非空白 stderr(trim 后)> message > String(e) + */ +export function gitErrorText(e: unknown): string { + if (e && typeof e === 'object') { + const ge = e as { stderr?: unknown; message?: unknown }; + if (typeof ge.stderr === 'string' && ge.stderr.trim()) { + return ge.stderr.trim(); + } + if (typeof ge.message === 'string' && ge.message) { + return ge.message; + } + } + return String(e); +} diff --git a/src/engine/log/author-suggest.ts b/src/engine/log/author-suggest.ts new file mode 100644 index 0000000..0cf7a70 --- /dev/null +++ b/src/engine/log/author-suggest.ts @@ -0,0 +1,35 @@ +/** + * 作者候选查询(Filter by Author 多选):`git shortlog` 取数参数 + 输出解析(纯逻辑,零 vscode 依赖)。 + * + * shortlog 在 git 侧完成聚合计数与降序排序(巨库 stdout 仅数百行,优于全量 `log --format` 逐行回传), + * engine 仅保留一个容错解析函数;引用范围复用 {@link scopeRefs} 与 Graph 视图一致(单一事实源)。 + */ + +import { scopeRefs, type LogScope } from './log-query'; + +/** 作者统计(shortlog 一行:身份 + 提交数)。 */ +export interface AuthorStat { + readonly name: string; + readonly email: string; + readonly count: number; +} + +/** shortlog 参数:`-s` 仅计数、`-n` 按数降序、`-e` 含 email;引用范围随 scope。 + * `current` 的 refs 为空时必须补显式 `HEAD`——shortlog 无 rev 时非 tty 下会等 stdin 而挂起。 */ +export function buildAuthorListArgs(scope: LogScope): string[] { + const refs = scopeRefs(scope); + return ['-s', '-n', '-e', ...(refs.length > 0 ? refs : ['HEAD'])]; +} + +/** 解析 `git shortlog -se` 输出行(`\t `);坏行/空输入容错跳过。 + * git ident 禁止 name/email 含 `<` `>` 或换行,`` 段无切分歧义。 */ +export function parseShortlog(output: string): AuthorStat[] { + const stats: AuthorStat[] = []; + for (const line of output.split('\n')) { + const m = line.match(/^\s*(\d+)\t(.*) <([^<>]*)>\s*$/); + if (m) { + stats.push({ count: Number(m[1]), name: m[2], email: m[3] }); + } + } + return stats; +} diff --git a/src/engine/log/log-filter.ts b/src/engine/log/log-filter.ts index 5d6456d..8623fc0 100644 --- a/src/engine/log/log-filter.ts +++ b/src/engine/log/log-filter.ts @@ -1,7 +1,7 @@ /** * Log 客户端过滤器(纯逻辑,零 vscode 依赖)。 * - * author / path / grep 交给 `git log`(repo.log)服务端过滤;以下维度 git log 稳定 API 不支持或 + * authors / path / grep 交给 `git log`(repo.log)服务端过滤;以下维度 git log 稳定 API 不支持或 * 表达不便,改在客户端对已取回的提交做过滤:合并模式(parents)/ 日期范围(authorDate)/ message 正则。 * engine 不依赖 vscode,故以 {@link FilterableCommit} 抽象 commit 所需字段,由 adapter 映射。 */ @@ -25,11 +25,163 @@ export interface LogClientFilter { } /** - * Log 完整过滤器(单一事实源,engine 层):author/path/grep 交 git log 服务端; + * 作者过滤项(OR 语义,git 对多条 `--author` 取并集,可与 pattern 混选): + * - `exact`:从仓库作者列表选中的身份,经 {@link exactAuthorPattern} 锚定字面匹配(同名异邮箱不混淆); + * - `pattern`:手输正则原样透传(保留旧 InputBox 的任意 pattern 能力)。 + */ +export type AuthorTerm = + | { readonly kind: 'exact'; readonly name: string; readonly email: string } + | { readonly kind: 'pattern'; readonly pattern: string }; + +/** 元素级合法性判定(持久化 salvage 用):exact 项要求 name/email 均为非空串,pattern 项要求非空 pattern。 */ +function isAuthorTerm(x: unknown): x is AuthorTerm { + if (typeof x !== 'object' || x === null) { + return false; + } + const t = x as Record; + if (t.kind === 'exact') { + return typeof t.name === 'string' && t.name.length > 0 && typeof t.email === 'string' && t.email.length > 0; + } + return t.kind === 'pattern' && typeof t.pattern === 'string' && t.pattern.length > 0; +} + +/** + * 解析持久化的 authors 过滤(memento JSON 串 → {@link AuthorTerm} 列表): + * 非字符串 / 非法 JSON / 非数组返回 undefined;元素级剔除脏项(kind 未知或缺字段), + * 剔除后为空亦返回 undefined(与「空选 = 不过滤」的规范化一致)。脏数据免疫,永不抛异常。 + */ +export function parsePersistedAuthors(raw: unknown): readonly AuthorTerm[] | undefined { + if (typeof raw !== 'string' || raw.length === 0) { + return undefined; + } + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return undefined; + } + if (!Array.isArray(parsed)) { + return undefined; + } + const terms = parsed.filter(isAuthorTerm); + return terms.length ? terms : undefined; +} + +/** 非空字符串维度(空串规约为未设置)。 */ +function nonEmptyString(x: unknown): string | undefined { + return typeof x === 'string' && x.length > 0 ? x : undefined; +} + +/** ISO 串 → 合法 Date(非串 / 空 / 不可解析返回 undefined)。 */ +function validDate(x: unknown): Date | undefined { + if (typeof x !== 'string' || x.length === 0) { + return undefined; + } + const d = new Date(x); + return Number.isNaN(d.getTime()) ? undefined : d; +} + +/** + * 非 authors 过滤维度(path/grep/mergeMode/dateFrom/dateTo/messageRegex)的持久化序列化: + * 归一化后全空(undefined 维度、空串、mergeMode 'all')返回 undefined 即删 key;Date 经 ISO 串 + * 落盘。authors 独立成 key,不属本层(见 {@link parsePersistedAuthors})。 + */ +export function serializePersistableFilter(filter: LogFilter | undefined): string | undefined { + if (!filter) { + return undefined; + } + const out: { path?: string; grep?: string; mergeMode?: MergeMode; dateFrom?: string; dateTo?: string; messageRegex?: string } = {}; + if (filter.path) { + out.path = filter.path; + } + if (filter.grep) { + out.grep = filter.grep; + } + if (filter.mergeMode && filter.mergeMode !== 'all') { + out.mergeMode = filter.mergeMode; + } + if (filter.dateFrom) { + out.dateFrom = filter.dateFrom.toISOString(); + } + if (filter.dateTo) { + out.dateTo = filter.dateTo.toISOString(); + } + if (filter.messageRegex) { + out.messageRegex = filter.messageRegex; + } + const s = JSON.stringify(out); + return s === '{}' ? undefined : s; +} + +/** + * 解析持久化的非 authors 过滤维度({@link serializePersistableFilter} 的逆):非字符串 / 非法 + * JSON / 非对象返回 undefined;逐字段校验剔除脏项(字符串非空、mergeMode 合法、日期可解析), + * 全空 / 全脏返回 undefined(与「无过滤」规约一致)。脏数据免疫,永不抛异常。 + */ +export function parsePersistedFilter(raw: unknown): LogFilter | undefined { + if (typeof raw !== 'string' || raw.length === 0) { + return undefined; + } + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return undefined; + } + if (typeof parsed !== 'object' || parsed === null) { + return undefined; + } + const t = parsed as Record; + const path = nonEmptyString(t.path); + const grep = nonEmptyString(t.grep); + const messageRegex = nonEmptyString(t.messageRegex); + const dateFrom = validDate(t.dateFrom); + const dateTo = validDate(t.dateTo); + const mergeMode = t.mergeMode === 'merge-only' || t.mergeMode === 'no-merge' ? t.mergeMode : undefined; + const filter: LogFilter = { + ...(path ? { path } : {}), + ...(grep ? { grep } : {}), + ...(mergeMode ? { mergeMode } : {}), + ...(dateFrom ? { dateFrom } : {}), + ...(dateTo ? { dateTo } : {}), + ...(messageRegex ? { messageRegex } : {}), + }; + return Object.keys(filter).length ? filter : undefined; +} + +/** + * 判定过滤器是否存在生效维度(authors 非空,或经 {@link serializePersistableFilter} 归一化后 + * 仍有可持久化维度)——归一化口径与持久化一致(空串 / mergeMode 'all' / 空 authors 均不算生效), + * 驱动标题栏过滤图标激活态(hyperGit.log.filterActive context key)。 + */ +export function hasActiveFilter(filter: LogFilter | undefined): boolean { + if (!filter) { + return false; + } + return (filter.authors?.length ?? 0) > 0 || serializePersistableFilter(filter) !== undefined; +} + +/** BRE 特殊字符集:git `--author` 缺省 POSIX BRE(非 ERE),仅 `\ . [ * ^ $` 有特殊义; + * `+ ? | ( ) {` 在 BRE 中是普通字符,加反斜杠反成量词/分组操作符(实测 `\+` 使含 `+` 的邮箱归零命中),勿扩大转义集。 */ +const BRE_SPECIAL = /[.*[\]^$\\]/g; + +/** 按 BRE 语义转义字面串(`+ ? |` 等保持裸写,见 {@link BRE_SPECIAL})。 */ +export function escapeBreLiteral(s: string): string { + return s.replace(BRE_SPECIAL, '\\$&'); +} + +/** exact 身份 → 锚定字面 pattern:`--author` 对 `Name ` 整行匹配,首尾锚定 + 全字面转义。 */ +export function exactAuthorPattern(name: string, email: string): string { + return `^${escapeBreLiteral(`${name} <${email}>`)}$`; +} + +/** + * Log 完整过滤器(单一事实源,engine 层):authors/path/grep 交 git log 服务端; * mergeMode/dateFrom/dateTo/messageRegex 客户端(见 {@link toClientFilter})。 */ export interface LogFilter { - readonly author?: string; + /** 作者过滤项集合(多选 OR),见 {@link AuthorTerm};空/缺省即不过滤。 */ + readonly authors?: readonly AuthorTerm[]; readonly path?: string; readonly grep?: string; readonly mergeMode?: MergeMode; diff --git a/src/engine/log/log-query.ts b/src/engine/log/log-query.ts index 6965532..abd3afe 100644 --- a/src/engine/log/log-query.ts +++ b/src/engine/log/log-query.ts @@ -9,7 +9,7 @@ * (`row.authorDate`)对齐,肉眼所见与排列顺序永远一致。pathspec `-- ` 必须置于末尾。 */ -import type { LogFilter } from './log-filter'; +import { exactAuthorPattern, type LogFilter } from './log-filter'; import { LOG_GRAPH_FORMAT } from './log-line'; /** @@ -33,6 +33,21 @@ export interface LogQueryOptions { readonly skip?: number; } +/** scope → git 引用范围参数(单一事实源,{@link buildLogArgs} 与作者候选查询共享): + * `current` 为空数组 = git log 缺省 HEAD;shortlog 等调用方需自行补显式 HEAD。 */ +export function scopeRefs(scope: LogScope): string[] { + if (scope === 'checkpointer') { + // 原始完整视图:遍历 refs/ 全部引用,含 conductor 等工具的 checkpoint 快照与归档头。 + return ['--all']; + } + if (scope === 'all') { + // 干净视图:仅 heads/tags/remotes,排除 refs/conductor-* 等工具注入的内部引用, + // 以免其携带的游离/陈旧提交以游离泳道污染人类历史(`--all` 无法排除,prune 亦触不及)。 + return ['--branches', '--tags', '--remotes']; + } + return []; +} + /** * 构造 `git log` 参数向量(不含 `log` 字面量,host 拼接)。 * 顺序:`--author-date-order` → 范围 → 分页 → 服务端过滤 → `--format` → pathspec(末尾)。 @@ -40,21 +55,19 @@ export interface LogQueryOptions { export function buildLogArgs(filter: LogFilter | undefined, scope: LogScope, opts: LogQueryOptions): string[] { // 排序键须与视图日期列(row.authorDate)对齐;lane 算法仅依赖「子在父之上」不变量, // --author-date-order 与 --topo-order 均满足,且额外按作者日期倒序、跨支正确交错(详见文件头)。 - const args: string[] = ['--author-date-order']; - if (scope === 'checkpointer') { - // 原始完整视图:遍历 refs/ 全部引用,含 conductor 等工具的 checkpoint 快照与归档头。 - args.push('--all'); - } else if (scope === 'all') { - // 干净视图:仅 heads/tags/remotes,排除 refs/conductor-* 等工具注入的内部引用, - // 以免其携带的游离/陈旧提交以游离泳道污染人类历史(`--all` 无法排除,prune 亦触不及)。 - args.push('--branches', '--tags', '--remotes'); - } + const args: string[] = ['--author-date-order', ...scopeRefs(scope)]; args.push(`--max-count=${opts.maxCount}`); if (opts.skip && opts.skip > 0) { args.push(`--skip=${opts.skip}`); } - if (filter?.author && filter.author.trim()) { - args.push(`--author=${filter.author.trim()}`); + // authors → 多条 `--author=`(git 取 OR);去重防重复 flag,空 pattern 跳过(对齐「空串不产 flag」)。 + const seen = new Set(); + for (const term of filter?.authors ?? []) { + const v = term.kind === 'exact' ? exactAuthorPattern(term.name, term.email) : term.pattern.trim(); + if (v && !seen.has(v)) { + seen.add(v); + args.push(`--author=${v}`); + } } if (filter?.grep && filter.grep.trim()) { args.push(`--grep=${filter.grep.trim()}`); diff --git a/src/engine/ref/branch-update.ts b/src/engine/ref/branch-update.ts new file mode 100644 index 0000000..dd84347 --- /dev/null +++ b/src/engine/ref/branch-update.ts @@ -0,0 +1,148 @@ +/** + * 分支就地更新(不 checkout)的纯逻辑(零 vscode 依赖)。 + * + * 核心原语:`git fetch refs/heads/:refs/heads/`——git 原生 fast-forward-only, + * diverged / ahead-only 时拒绝回退(`! [rejected] ... (non-fast-forward)`),永不改写本地提交; + * 目标是当前分支(含被其他 worktree checkout)时 git 拒绝 + * (`refusing to fetch into branch '…' checked out at '…'`),须改走 `repo.pull()`。 + * 本模块提供: + * - {@link planBranchUpdates}:把选中本地分支短名 + for-each-ref 新鲜快照排程为逐分支动作 + * (pull-current / fetch / skip-no-upstream)。 + * - {@link formatBranchUpdateResult}:把逐分支结果五桶汇总为通知文案与级别。 + * - {@link classifyFetchOutcome}:事后以短 sha 前缀语义把单个 fetch 动作归类 updated / upToDate。 + * + * 与同层模块的关系:upstream 短名(`origin/x`)拆分复用 {@link resolveRemoteBranch} + * (remote 名可含 `/`);汇总文案复用 {@link truncateNames} 与 {@link collapseReason};均以轻量指针保持单一事实源。 + */ + +import type { RawRef } from './for-each-ref'; +import { resolveRemoteBranch } from './remote-ref'; +import { collapseReason, truncateNames } from './cleanup'; + +/** 单分支更新动作({@link planBranchUpdates} 的产物)。 */ +export type BranchUpdateAction = + | { readonly kind: 'pull-current'; readonly name: string } + | { + readonly kind: 'fetch'; + readonly name: string; + readonly remote: string; + readonly upstreamBranch: string; + /** 更新前 objectname 短 sha(事后对比区分 updated / upToDate——fetch 静默 no-op 无输出)。 */ + readonly beforeSha: string; + } + | { readonly kind: 'skip-no-upstream'; readonly name: string }; + +/** + * 把选中的本地分支排程为逐分支更新动作(纯逻辑)。 + * + * 判定规则(顺序即优先级): + * 1. `name === headName` → pull-current:fetch refspec 对当前分支必被 git 拒绝,改走 repo.pull() + * (有无上游都交由 pull 报错,统一失败通道); + * 2. 快照缺失 / 无 upstream / upstream 无法归属任何已知 remote(陈旧 remote 场景)→ skip-no-upstream; + * 3. 其余 → fetch,携带 {remote, upstreamBranch, beforeSha}。 + * + * @param names 选中本地分支短名(调用方已谓词过滤/去重;此处对重复名再防御性去重) + * @param refByShort for-each-ref 新鲜快照(key=短名,含 upstream/objectname)。 + * 树节点 ref 不可作为事实源——API 路径的 refToRaw 不填 upstream/head。 + * @param headName 当前分支名(repo.state.HEAD?.name,权威判定;detached HEAD 时 undefined) + * @param remotes 已知 remote 名列表(repo.state.remotes,upstream 拆分的事实源) + */ +export function planBranchUpdates( + names: readonly string[], + refByShort: ReadonlyMap, + headName: string | undefined, + remotes: readonly string[], +): BranchUpdateAction[] { + const seen = new Set(); + const plan: BranchUpdateAction[] = []; + for (const name of names) { + if (seen.has(name)) { + continue; + } + seen.add(name); + if (name === headName) { + plan.push({ kind: 'pull-current', name }); + continue; + } + const ref = refByShort.get(name); + const target = ref?.upstream ? resolveRemoteBranch(ref.upstream, remotes) : null; + if (!ref || !target) { + plan.push({ kind: 'skip-no-upstream', name }); + continue; + } + plan.push({ kind: 'fetch', name, remote: target.remote, upstreamBranch: target.branch, beforeSha: ref.objectname }); + } + return plan; +} + +/** 单分支更新失败:分支短名 + 从 stderr/message 提取的原因。 */ +export interface BranchUpdateFailure { + readonly name: string; + /** 失败原因(stderr 优先),可能多行。 */ + readonly reason: string; +} + +/** 逐分支更新结果五桶(skip 在排程期即可定桶,其余在执行后归类)。 */ +export interface BranchUpdateBuckets { + /** fetch 后 sha 变化(真更新)。 */ + readonly updated: readonly string[]; + /** fetch 成功但 sha 不变(远程无新提交)。 */ + readonly upToDate: readonly string[]; + /** 当前分支经 repo.pull() 更新。 */ + readonly pulled: readonly string[]; + /** 无上游 / 上游无法归属已知 remote,未执行。 */ + readonly skipped: readonly string[]; + readonly failures: readonly BranchUpdateFailure[]; +} + +/** 通知级别:无失败走 info;存在失败走 error(联动 Show Output)。 */ +export type BranchUpdateResultLevel = 'info' | 'error'; + +/** + * 把逐分支更新结果五桶汇总为通知文案与级别(纯逻辑)。 + * + * 诚实计数:各桶零计数段省略;失败项附真实原因(超 4 条截断标注); + * 选中数 = 五桶之和,保证用户可对账。 + */ +export function formatBranchUpdateResult(buckets: BranchUpdateBuckets): { + level: BranchUpdateResultLevel; + message: string; +} { + const { updated, upToDate, pulled, skipped, failures } = buckets; + const parts: string[] = []; + if (updated.length === 1) { + parts.push(`Updated branch ${updated[0]}`); + } else if (updated.length > 1) { + parts.push(`Updated ${updated.length} branches`); + } + if (upToDate.length === 1) { + parts.push(`${upToDate[0]} is already up to date`); + } else if (upToDate.length > 1) { + parts.push(`${upToDate.length} already up to date`); + } + if (pulled.length > 0) { + // head 至多一个,pull-current 桶恒 ≤1。 + parts.push(`Pulled current branch ${pulled[0]}`); + } + if (skipped.length > 0) { + parts.push(`${skipped.length} skipped (no upstream): ${truncateNames(skipped)}`); + } + if (failures.length === 0) { + return { level: 'info', message: parts.join('; ') }; + } + const entries = failures.map((f) => `${f.name} (${collapseReason(f.reason)})`); + parts.push(`${failures.length} failed: ${truncateNames(entries, 4)}`); + return { level: 'error', message: parts.join('; ') }; +} + +/** + * 事后把单个 fetch 动作归类为 updated / upToDate(纯逻辑)。 + * + * 前缀语义而非严格全等:`%(objectname:short)` 的缩写长度随仓库对象数自动伸缩, + * 批量 fetch 落库新对象可能使 after 短 sha 变长(如 7→8 位),同 commit 的前后 + * 短 sha 仍互为前缀;afterSha 缺失(分支被并发删除)保守计 updated——fetch 已 + * 成功,宁报更新不报无变化。 + */ +export function classifyFetchOutcome(beforeSha: string, afterSha: string | undefined): 'updated' | 'upToDate' { + return afterSha !== undefined && afterSha.startsWith(beforeSha) ? 'upToDate' : 'updated'; +} diff --git a/src/engine/ref/cleanup.ts b/src/engine/ref/cleanup.ts index 09cf993..10c7303 100644 --- a/src/engine/ref/cleanup.ts +++ b/src/engine/ref/cleanup.ts @@ -83,6 +83,12 @@ export function truncateNames(names: readonly string[], max = 8): string { return `${names.slice(0, max).join(', ')} … and ${names.length - max} more`; } +/** 失败原因折叠为单行(换行→'; ')并截断,避免通知气泡过长(单一事实源,勿在各结果格式化器内复制)。 */ +export function collapseReason(reason: string, max = 160): string { + const line = reason.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).join('; '); + return line.length > max ? `${line.slice(0, max - 1)}…` : line; +} + /** * 生成批量删除本地分支的确认文案与确认按钮文本(纯逻辑)。 * 诚实呈现强制删除风险:混合时分栏列出,确保 force 删除不被隐藏。 diff --git a/src/engine/ref/remote-ref.ts b/src/engine/ref/remote-ref.ts index 94974f9..e48746c 100644 --- a/src/engine/ref/remote-ref.ts +++ b/src/engine/ref/remote-ref.ts @@ -8,11 +8,14 @@ * 朴素 `split('/')[0]` 会把 `myorg/repo/feature` 错切成 `myorg`。 * - {@link partitionRemoteByProtected}:复用 {@link isProtectedBranch},把 main/master 等主干硬排除。 * - {@link formatRemoteDeleteConfirm}:生成 modal 确认文案,诚实传达服务器端不可逆 + 协作者影响。 + * - {@link isRemoteRefMissingError}:识别「服务端已无此 ref」的 push 失败(本地 tracking ref 陈旧场景)。 + * - {@link formatRemoteDeleteResult}:把逐分支删除结果三桶汇总为通知文案与级别。 * - * 与 cleanup.ts 的关系:复用其 `isProtectedBranch` / `truncateNames` 作为单一事实源(轻量指针,不复制定义)。 + * 与 cleanup.ts 的关系:复用其 `isProtectedBranch` / `truncateNames` / `collapseReason` 作为单一事实源(轻量指针,不复制定义)。 */ -import { isProtectedBranch, truncateNames } from './cleanup'; +import { collapseReason, isProtectedBranch, truncateNames } from './cleanup'; +import { gitErrorText } from '../git-state/git-error'; /** 解析后的远程分支定位。 */ export interface RemoteBranchTarget { @@ -94,3 +97,64 @@ export function formatRemoteDeleteConfirm( : `Delete ${n} remote branches: ${truncateNames(deletable.map((t) => t.shortName))}.\n${irreversible}`; return { detail: `${head}${body}`, confirmLabel: 'Delete' }; } + +/** 单条远程删除失败:分支短名 + 从 stderr/message 提取的原因。 */ +export interface RemoteDeleteFailure { + /** 远程分支短名,如 origin/foo。 */ + readonly name: string; + /** 失败原因(stderr 优先),可能多行。 */ + readonly reason: string; +} + +/** 「服务端已无此 ref」的报错文案(大小写不敏感)。git 2.x 规范文案 + 旧服务端变体。 */ +const REMOTE_REF_MISSING_PATTERNS: readonly RegExp[] = [/remote ref does not exist/i, /delete of non-existent ref/i]; + +/** + * 判定 `git push --delete ` 的失败是否属「服务端已无此 ref」。 + * + * 典型场景:PR 合并后远端自动删除分支,本地 remote-tracking ref 在下一次 `fetch --prune` + * 前一直陈旧——视图仍列出它,push --delete 却被服务端拒绝 + * (`error: unable to delete 'x': remote ref does not exist`)。此分支的目标态(服务端无此分支) + * 已天然达成,应按成功收尾并清理本地陈旧 ref,而非误报失败。 + * + * @param e execGit 的拒绝值:文本提取统一走 {@link gitErrorText}(非空白 stderr 优先,回退 message)。 + */ +export function isRemoteRefMissingError(e: unknown): boolean { + const text = gitErrorText(e); + return text.length > 0 && REMOTE_REF_MISSING_PATTERNS.some((re) => re.test(text)); +} + +/** 通知级别:仅成功/已不存在走 info;存在真失败走 error(联动 Show Output)。 */ +export type RemoteDeleteResultLevel = 'info' | 'error'; + +/** + * 把逐分支远程删除结果(成功 / 服务端已不存在 / 失败)三桶汇总为通知文案与级别(纯逻辑)。 + * + * 诚实计数:alreadyGone 单列(服务端本就无此分支,非本次删除),失败项附真实原因; + * 零计数段省略(如仅陈旧项时文案为「N already gone on remote …」,不以「Deleted 0 …」开头)。 + * @param succeeded 服务端删除成功的目标 + * @param alreadyGone 服务端已无此分支、按达成处理的目标(本地陈旧 tracking ref 已一并清理) + * @param failures 真失败项(含原因),展示超 4 条截断标注 + */ +export function formatRemoteDeleteResult( + succeeded: readonly RemoteBranchTarget[], + alreadyGone: readonly RemoteBranchTarget[], + failures: readonly RemoteDeleteFailure[], +): { level: RemoteDeleteResultLevel; message: string } { + const parts: string[] = []; + if (succeeded.length === 1) { + parts.push(`Deleted remote branch ${succeeded[0].shortName}`); + } else if (succeeded.length > 1) { + parts.push(`Deleted ${succeeded.length} remote branches`); + } + if (alreadyGone.length > 0) { + parts.push(`${alreadyGone.length} already gone on remote (pruned local tracking ref${alreadyGone.length > 1 ? 's' : ''})`); + } + if (failures.length === 0) { + return { level: 'info', message: parts.join('; ') }; + } + // 复用 truncateNames(本模块头注释声明的 SSOT):名称+原因条目信息密度高,收紧到 4 条。 + const entries = failures.map((f) => `${f.name} (${collapseReason(f.reason)})`); + parts.push(`${failures.length} failed: ${truncateNames(entries, 4)}`); + return { level: 'error', message: parts.join('; ') }; +} diff --git a/src/extension.ts b/src/extension.ts index ea8d1dc..4981348 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -220,7 +220,7 @@ export async function activate( ...registerGitCliCommands(service, branchesTree, logTree), ...registerPartialCommands(service, registry), ...registerAdvancedCommands(service, branchesTree), - ...registerRemoteCommands(service, branchesTree, logTree), + ...registerRemoteCommands(service, branchesTree, logTree, branchesView), ...registerMergeCommands(service), // 窗口 reload 后 rebase/merge 面板恢复(retainContextWhenHidden 仅保活不跨 reload,serializer 补齐)。 RebaseWebview.registerSerializer(service), diff --git a/tests/suite/extension.test.js b/tests/suite/extension.test.js index bd6d32a..ff1cdfb 100644 --- a/tests/suite/extension.test.js +++ b/tests/suite/extension.test.js @@ -51,6 +51,7 @@ suite('扩展冒烟测试', function () { 'hyperGit.pull', 'hyperGit.push', 'hyperGit.pushBranch', + 'hyperGit.branchUpdate', 'hyperGit.fetch', 'hyperGit.cherryPick', 'hyperGit.revertCommit', diff --git a/tests/unit/author-suggest.test.ts b/tests/unit/author-suggest.test.ts new file mode 100644 index 0000000..78e8baa --- /dev/null +++ b/tests/unit/author-suggest.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; +import { buildAuthorListArgs, parseShortlog } from '../../src/engine/log/author-suggest'; +import { scopeRefs } from '../../src/engine/log/log-query'; +import { escapeBreLiteral, exactAuthorPattern } from '../../src/engine/log/log-filter'; + +describe('buildAuthorListArgs — shortlog 取数', () => { + it('三 scope 引用范围与 Graph 视图一致(scopeRefs 单一事实源)', () => { + expect(buildAuthorListArgs('all')).toEqual(['-s', '-n', '-e', '--branches', '--tags', '--remotes']); + expect(buildAuthorListArgs('checkpointer')).toEqual(['-s', '-n', '-e', '--all']); + // current 必须显式 HEAD:shortlog 无 rev 时非 tty 下等 stdin 会挂起 + expect(buildAuthorListArgs('current')).toEqual(['-s', '-n', '-e', 'HEAD']); + }); + + it('scopeRefs 提取后行为不变(重构护栏)', () => { + expect(scopeRefs('all')).toEqual(['--branches', '--tags', '--remotes']); + expect(scopeRefs('checkpointer')).toEqual(['--all']); + expect(scopeRefs('current')).toEqual([]); + }); +}); + +describe('parseShortlog — 容错解析', () => { + it('标准行(前导空格 + countName ),保留输出顺序(-n 已按数降序)', () => { + const out = [' 96\tAurelius Huang ', ' 3\tThreeFish '].join('\n'); + expect(parseShortlog(out)).toEqual([ + { name: 'Aurelius Huang', email: 'threefish.ai@gmail.com', count: 96 }, + { name: 'ThreeFish', email: 'a@b.com', count: 3 }, + ]); + }); + + it('本仓库真实输出形态(含正则元字符作者名与 + 号邮箱)', () => { + const out = ' 36\tdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>'; + expect(parseShortlog(out)).toEqual([{ name: 'dependabot[bot]', email: '49699333+dependabot[bot]@users.noreply.github.com', count: 36 }]); + }); + + it('坏行跳过、空输入返回空数组(空仓库/降级路径)', () => { + expect(parseShortlog('')).toEqual([]); + expect(parseShortlog('\n')).toEqual([]); + expect(parseShortlog('garbage line without tab\n12\tno-email-here\n')).toEqual([]); + }); +}); + +describe('BRE 转义 — git --author 缺省 POSIX BRE(非 ERE)', () => { + it('仅转义 \\ . [ * ^ $;+ ? | ( ) { 裸写(加反斜杠反成量词/分组操作符)', () => { + expect(escapeBreLiteral('a.b[c]*^$\\d+?|({}')).toBe('a\\.b\\[c\\]\\*\\^\\$\\\\d+?|({}'); + expect(escapeBreLiteral('plain')).toBe('plain'); + }); + + it('exact 身份 → 锚定全行字面 pattern(--author 匹配 Name 整行)', () => { + expect(exactAuthorPattern('Jane', 'jane@x.com')).toBe('^Jane $'); + expect(exactAuthorPattern('dependabot[bot]', '49699333+nx@users.noreply.github.com')).toBe( + '^dependabot\\[bot\\] <49699333+nx@users\\.noreply\\.github\\.com>$', + ); + }); +}); diff --git a/tests/unit/branch-update.test.ts b/tests/unit/branch-update.test.ts new file mode 100644 index 0000000..57e44ba --- /dev/null +++ b/tests/unit/branch-update.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect } from 'vitest'; +import { planBranchUpdates, formatBranchUpdateResult, classifyFetchOutcome } from '../../src/engine/ref/branch-update'; +import type { RawRef } from '../../src/engine/ref/for-each-ref'; + +/** 构造测试用本地分支 RawRef(for-each-ref 新鲜快照口径,upstream/head 可用)。 */ +function ref(shortName: string, opts: Partial = {}): RawRef { + return { + refname: `refs/heads/${shortName}`, + shortName, + objectname: 'abc1234', + head: false, + isRemote: false, + isTag: false, + ...opts, + }; +} + +describe('planBranchUpdates', () => { + it('head 命中 → pull-current(fetch refspec 对当前分支必被 git 拒绝),即使配置了上游', () => { + const snapshot = new Map([['dev', ref('dev', { upstream: 'origin/dev' })]]); + const plan = planBranchUpdates(['dev'], snapshot, 'dev', ['origin']); + expect(plan).toEqual([{ kind: 'pull-current', name: 'dev' }]); + }); + + it('有上游 → fetch 动作携带 remote/upstreamBranch/beforeSha', () => { + const snapshot = new Map([['feature/foo', ref('feature/foo', { upstream: 'origin/feature/foo', objectname: 'deadbee' })]]); + const plan = planBranchUpdates(['feature/foo'], snapshot, 'main', ['origin']); + expect(plan).toEqual([ + { kind: 'fetch', name: 'feature/foo', remote: 'origin', upstreamBranch: 'feature/foo', beforeSha: 'deadbee' }, + ]); + }); + + it('remote 名含 / :最长前缀拆分(myorg/repo/feat → remote=myorg/repo, upstreamBranch=feat)', () => { + const snapshot = new Map([['feat', ref('feat', { upstream: 'myorg/repo/feat' })]]); + const plan = planBranchUpdates(['feat'], snapshot, undefined, ['origin', 'myorg/repo']); + expect(plan).toEqual([{ kind: 'fetch', name: 'feat', remote: 'myorg/repo', upstreamBranch: 'feat', beforeSha: 'abc1234' }]); + }); + + it('无上游 → skip-no-upstream', () => { + const snapshot = new Map([['orphan', ref('orphan')]]); + const plan = planBranchUpdates(['orphan'], snapshot, 'main', ['origin']); + expect(plan).toEqual([{ kind: 'skip-no-upstream', name: 'orphan' }]); + }); + + it('上游指向已不存在的 remote(陈旧配置)→ skip-no-upstream', () => { + const snapshot = new Map([['stale', ref('stale', { upstream: 'gone/stale' })]]); + const plan = planBranchUpdates(['stale'], snapshot, 'main', ['origin']); + expect(plan).toEqual([{ kind: 'skip-no-upstream', name: 'stale' }]); + }); + + it('快照缺失(分支刚被并发删除)→ skip-no-upstream;重复名去重', () => { + const snapshot = new Map([['a', ref('a', { upstream: 'origin/a' })]]); + const plan = planBranchUpdates(['a', 'a', 'ghost'], snapshot, 'main', ['origin']); + expect(plan).toEqual([ + { kind: 'fetch', name: 'a', remote: 'origin', upstreamBranch: 'a', beforeSha: 'abc1234' }, + { kind: 'skip-no-upstream', name: 'ghost' }, + ]); + }); +}); + +describe('formatBranchUpdateResult', () => { + it('单 updated → 点名分支,级别 info', () => { + const result = formatBranchUpdateResult({ updated: ['main'], upToDate: [], pulled: [], skipped: [], failures: [] }); + expect(result).toEqual({ level: 'info', message: 'Updated branch main' }); + }); + + it('多 updated → 计数;零计数段省略(无 pulled/skipped 不出现)', () => { + const result = formatBranchUpdateResult({ + updated: ['a', 'b'], + upToDate: ['c', 'd'], + pulled: [], + skipped: [], + failures: [], + }); + expect(result).toEqual({ level: 'info', message: 'Updated 2 branches; 2 already up to date' }); + }); + + it('单 upToDate → 点名;pulled/skipped 段并存', () => { + const result = formatBranchUpdateResult({ + updated: [], + upToDate: ['c'], + pulled: ['dev'], + skipped: ['orphan'], + failures: [], + }); + expect(result).toEqual({ + level: 'info', + message: 'c is already up to date; Pulled current branch dev; 1 skipped (no upstream): orphan', + }); + }); + + it('有失败 → 级别 error,条目 name (reason) 且多行 reason 折叠单行', () => { + const result = formatBranchUpdateResult({ + updated: ['a'], + upToDate: [], + pulled: [], + skipped: [], + failures: [{ name: 'b', reason: '! [rejected] b -> b (non-fast-forward)\nhint: …' }], + }); + expect(result.level).toBe('error'); + expect(result.message).toBe( + 'Updated branch a; 1 failed: b (! [rejected] b -> b (non-fast-forward); hint: …)', + ); + }); + + it('仅失败(无任何成功段)→ 文案不以空段开头', () => { + const result = formatBranchUpdateResult({ + updated: [], + upToDate: [], + pulled: [], + skipped: [], + failures: [{ name: 'x', reason: 'fatal: refusing to fetch into branch … checked out at …' }], + }); + expect(result).toEqual({ + level: 'error', + message: '1 failed: x (fatal: refusing to fetch into branch … checked out at …)', + }); + }); +}); + +describe('classifyFetchOutcome', () => { + it('同 commit 且缩写变长(对象数跨阈值,7→8 位)→ upToDate(前缀语义,非严格全等)', () => { + expect(classifyFetchOutcome('abc1234', 'abc12345')).toBe('upToDate'); + }); + + it('真更新(不同 commit)→ updated', () => { + expect(classifyFetchOutcome('abc1234', 'deadbee')).toBe('updated'); + }); + + it('afterSha 缺失(分支被并发删除)→ 保守 updated', () => { + expect(classifyFetchOutcome('abc1234', undefined)).toBe('updated'); + }); +}); diff --git a/tests/unit/branches-titlebar.test.ts b/tests/unit/branches-titlebar.test.ts index c615963..9c00472 100644 --- a/tests/unit/branches-titlebar.test.ts +++ b/tests/unit/branches-titlebar.test.ts @@ -88,4 +88,40 @@ describe('branches-titlebar(Branches 标题栏/右键菜单声明护栏)', ( when: 'false', }); }); + + it('Update Branch 命令定义齐备:title "Update Branch" 与 $(arrow-down)(同全局 Pull 图标)', () => { + expect(commandById.get('hyperGit.branchUpdate')).toMatchObject({ + title: 'Update Branch', + icon: '$(arrow-down)', + }); + }); + + it('view/title:标题栏 Pull 位(navigation@3)换成 Update Branch(选区感知,无选区等价 Pull),不再挂全局 Pull', () => { + const branchesEntries = titleEntries.filter((e) => e.when === 'view == hyperGit.branches'); + const byCommand = new Map(branchesEntries.map((e) => [e.command, e])); + expect(byCommand.get('hyperGit.branchUpdate')).toEqual({ + command: 'hyperGit.branchUpdate', + when: 'view == hyperGit.branches', + group: 'navigation@3', + }); + // Commit 视图的 hyperGit.pull 保留;Branches 标题栏不再出现(选区语义由 branchUpdate 承载)。 + expect(byCommand.get('hyperGit.pull'), 'Branches 标题栏不应再挂全局 Pull').toBeUndefined(); + }); + + it('view/item/context:本地分支右键提供 Update Branch(多选可用:when 无 !listMultiSelection),palette 不泄漏', () => { + expect( + itemEntries.find( + (e) => e.command === 'hyperGit.branchUpdate' && e.when === 'view == hyperGit.branches && viewItem == hyperGit.branch', + ), + '分支右键缺 branchUpdate 条目', + ).toEqual({ + command: 'hyperGit.branchUpdate', + when: 'view == hyperGit.branches && viewItem == hyperGit.branch', + group: '1_branch@1.7', + }); + expect(paletteEntries.find((e) => e.command === 'hyperGit.branchUpdate')).toEqual({ + command: 'hyperGit.branchUpdate', + when: 'false', + }); + }); }); diff --git a/tests/unit/codicon-validity.test.ts b/tests/unit/codicon-validity.test.ts index 7ec09f3..2a151be 100644 --- a/tests/unit/codicon-validity.test.ts +++ b/tests/unit/codicon-validity.test.ts @@ -28,6 +28,7 @@ const VALID_CODICONS: ReadonlySet = new Set([ 'edit', 'eye', 'filter', + 'filter-filled', 'folder-opened', 'gear', 'git-branch', diff --git a/tests/unit/history-commands-filter-wiring.test.ts b/tests/unit/history-commands-filter-wiring.test.ts new file mode 100644 index 0000000..0282ab4 --- /dev/null +++ b/tests/unit/history-commands-filter-wiring.test.ts @@ -0,0 +1,497 @@ +import { describe, it, expect, vi } from 'vitest'; + +/** 命令 handler 注册表 + 可脚本化 QuickPick(pickAuthors 全交互在此驱动,不依赖真实 quick input)。 */ +const h = vi.hoisted(() => ({ + handlers: new Map unknown>(), + qpListeners: new Map void>(), + qp: undefined as { selectedItems: unknown[] } | undefined, +})); + +vi.mock('vscode', () => ({ + commands: { + registerCommand: vi.fn((id: string, handler: (arg?: unknown) => unknown) => { + h.handlers.set(id, handler); + return { dispose: () => undefined }; + }), + executeCommand: vi.fn(() => Promise.resolve()), + }, + window: { + showOpenDialog: vi.fn(), + showInputBox: vi.fn(), + showQuickPick: vi.fn(), + showWarningMessage: vi.fn(), + createQuickPick: () => { + const qp = { + canSelectMany: false, + matchOnDescription: false, + matchOnDetail: false, + title: '', + placeholder: '', + busy: false, + items: [] as unknown[], + selectedItems: [] as unknown[], + value: '', + show: () => undefined, + dispose: () => undefined, + onDidChangeValue: (cb: (...args: unknown[]) => void) => { + h.qpListeners.set('value', cb); + return { dispose: () => undefined }; + }, + onDidChangeSelection: (cb: (...args: unknown[]) => void) => { + h.qpListeners.set('selection', cb); + return { dispose: () => undefined }; + }, + onDidAccept: (cb: (...args: unknown[]) => void) => { + h.qpListeners.set('accept', cb); + return { dispose: () => undefined }; + }, + onDidHide: (cb: (...args: unknown[]) => void) => { + h.qpListeners.set('hide', cb); + return { dispose: () => undefined }; + }, + }; + h.qp = qp; + return qp; + }, + }, +})); + +import * as vscode from 'vscode'; +import type { Memento } from 'vscode'; +import type { ChangeItem, GitRepositoryService } from '../../src/adapter/git-repository-service'; +import type { GitHubCiService } from '../../src/adapter/ci/github-ci-service'; +import type { AuthorTerm } from '../../src/engine/log/log-filter'; +import { LogWebviewProvider } from '../../src/adapter/webview/log-webview'; +import { registerHistoryCommands } from '../../src/adapter/history-commands'; +import { CommitWebviewProvider } from '../../src/adapter/webview/commit-webview'; +import { parsePersistedFilter } from '../../src/engine/log/log-filter'; + +/** 内存 Memento(对齐 workspaceState 语义:update(key, undefined) 即删除条目)。 */ +class MemMemento { + private readonly map = new Map(); + get(key: string): T | undefined { + return this.map.get(key) as T | undefined; + } + update(key: string, value: unknown): Thenable { + if (value === undefined) { + this.map.delete(key); + } else { + this.map.set(key, value); + } + return Promise.resolve(); + } +} + +/** 功能型 EventEmitter mock(branch-favorites.test.ts 同范式)。 */ +class FakeEmitter { + private listeners: Array<(a: A) => void> = []; + readonly event = (l: (a: A) => void): { dispose: () => void } => { + this.listeners.push(l); + return { dispose: () => { this.listeners = this.listeners.filter((x) => x !== l); } }; + }; + fire(a: A): void { + for (const l of [...this.listeners]) { + l(a); + } + } +} + +/** fake service:可变 repoRoot + shortlog 空结果 + 可手动 fire 的生命周期事件。 */ +function makeService(root: string | null) { + const repoChanged = new FakeEmitter<{ previousRoot: string | null; root: string | null }>(); + const changed = new FakeEmitter(); + const service = { + repoRoot: root, + execGit: async () => '', + /** repo 跟随 repoRoot(logFilterPath 以 repo.rootUri 求相对路径)。 */ + get repo(): { rootUri: { fsPath: string } } | undefined { + return this.repoRoot ? { rootUri: { fsPath: this.repoRoot } } : undefined; + }, + onDidChange: changed.event, + onDidChangeRepository: repoChanged.event, + fireRepoChange(next: string | null): void { + const previousRoot = this.repoRoot; + this.repoRoot = next; + repoChanged.fire({ previousRoot, root: next }); + }, + }; + return service; +} + +const FISH: AuthorTerm = { kind: 'exact', name: 'ThreeFish', email: 'fish@example.com' }; +const BOB: AuthorTerm = { kind: 'exact', name: 'Bob', email: 'bob@example.com' }; + +/** 真实 Provider(持久化真链路)+ 注册全部命令,返回触发入口。 */ +function makeWiring(root: string, memento: MemMemento) { + const service = makeService(root); + const provider = new LogWebviewProvider( + service as unknown as GitRepositoryService, + {} as GitHubCiService, + memento as unknown as Memento, + ); + registerHistoryCommands( + service as unknown as GitRepositoryService, + provider, + {} as never, + {} as never, + ); + const openPicker = () => (h.handlers.get('hyperGit.logFilterAuthor') as () => Promise)(); + const showHistory = (relativePath: string) => + (h.handlers.get('hyperGit.showHistory') as (c?: ChangeItem) => Promise)( + { relativePath } as unknown as ChangeItem, + ); + const accept = (items: readonly { term: AuthorTerm }[]) => { + // selectedItems 由 onDidAccept 闭包读取(() => settle(qp.selectedItems.map(...))), + // 真实 quick input 由框架维护该字段,此处先注入再触发 accept。 + h.qp!.selectedItems = [...items]; + h.qpListeners.get('accept')!(); + }; + /** 触发 logFilterPath:对话框挂起期间可注入事件(切库),再以所选文件路径放行。 */ + const filterPathWithDialog = (fsPath: string, during?: () => void) => { + let resolveDialog!: (v: unknown) => void; + vi.mocked(vscode.window.showOpenDialog).mockImplementationOnce( + () => new Promise((r) => { resolveDialog = r; }), + ); + const p = (h.handlers.get('hyperGit.logFilterPath') as () => Promise)(); + during?.(); + resolveDialog([{ fsPath }]); + return p; + }; + return { provider, service, openPicker, showHistory, accept, filterPathWithDialog }; +} + +/** 触发任一「对话框型」命令/流程:对话框挂起期间可注入事件(切库),再以所选值放行。 */ +function withPendingDialog( + mockFn: { mockImplementationOnce: (impl: () => Promise) => void }, + resolveValue: unknown, + run: () => Promise, + during?: () => void, +): Promise { + let resolveDialog!: (v: unknown) => void; + mockFn.mockImplementationOnce(() => new Promise((r) => { resolveDialog = r; })); + const p = run(); + during?.(); + resolveDialog(resolveValue); + return p; +} + +/** commit 文件右键菜单(handleFileMenu 直调,绕过 webview 消息层):getChanges 返回单个文件。 */ +function makeCommitMenu(root: string) { + const service = makeService(root); + (service as unknown as { getChanges: () => unknown[] }).getChanges = () => [{ relativePath: 'src/a.ts' }]; + const provider = new CommitWebviewProvider( + service as unknown as GitRepositoryService, + {} as never, + {} as never, + new MemMemento() as unknown as Memento, + ); + const menu = (path: string) => + (provider as unknown as { handleFileMenu: (p: string) => Promise }).handleFileMenu(path); + return { provider, service, menu }; +} + +describe('history-commands 过滤 wiring(真实 Provider 持久化链路)', () => { + it('showHistory 增量式:保留持久化 authors 并叠加 path,不删 memento key', async () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-a', JSON.stringify([FISH])); + const { provider, showHistory } = makeWiring('/repo-a', memento); + + await showHistory('src/a.ts'); + expect(provider.getFilter()).toEqual({ authors: [FISH], path: 'src/a.ts' }); + expect(memento.get('hyperGit.log.authors:/repo-a')).toBe(JSON.stringify([FISH])); + provider.dispose(); + }); + + it('pickAuthors 正常路径:确认勾选写入 filter 与持久化 key', async () => { + const memento = new MemMemento(); + const { provider, openPicker, accept } = makeWiring('/repo-a', memento); + + await openPicker(); + accept([{ term: FISH }]); + expect(provider.getFilter()).toEqual({ authors: [FISH] }); + expect(memento.get('hyperGit.log.authors:/repo-a')).toBe(JSON.stringify([FISH])); + provider.dispose(); + }); + + it('pickAuthors settle 切库守卫:取数完成后切库,确认不再把旧仓库勾选误写新仓库 key', async () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-b', JSON.stringify([BOB])); + const { provider, service, openPicker, accept } = makeWiring('/repo-a', memento); + + await openPicker(); + // 取数守卫已通过(await 完成);此后环境驱动切库(不夺焦点、QuickPick 仍开着)再确认。 + service.fireRepoChange('/repo-b'); + accept([{ term: FISH }]); + expect(provider.getFilter()).toEqual({ authors: [BOB] }); + expect(memento.get('hyperGit.log.authors:/repo-b')).toBe(JSON.stringify([BOB])); + provider.dispose(); + }); + + it('pickAuthors 候选集外的持久化 exact 作者:ghost 项预勾选,no-op 确认不丢持久化数据', async () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-a', JSON.stringify([FISH])); + const { provider, openPicker, accept } = makeWiring('/repo-a', memento); + + // shortlog 恒返回空(service.execGit → '')→ FISH 不在候选集,须以 ghost 项呈现且预勾选。 + await openPicker(); + const items = h.qp!.items as ReadonlyArray<{ term: AuthorTerm; description?: string }>; + const ghost = items.find((i) => i.term.kind === 'exact' && i.term.email === FISH.email); + expect(ghost).toBeDefined(); + expect(ghost!.description).toBe('not in current scope'); + expect((h.qp!.selectedItems as ReadonlyArray<{ term: AuthorTerm }>).some((i) => i.term === ghost!.term)).toBe(true); + + // no-op 确认(按当前预勾选原样 settle)不丢持久化 authors。 + accept(h.qp!.selectedItems as ReadonlyArray<{ term: AuthorTerm }>); + expect(provider.getFilter()).toEqual({ authors: [FISH] }); + expect(memento.get('hyperGit.log.authors:/repo-a')).toBe(JSON.stringify([FISH])); + provider.dispose(); + }); + + it('logFilterPath 正常路径:所选文件按当前仓库求相对路径,增量叠加不覆盖 authors', async () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-a', JSON.stringify([FISH])); + const { provider, filterPathWithDialog } = makeWiring('/repo-a', memento); + + await filterPathWithDialog('/repo-a/src/a.ts'); + expect(provider.getFilter()).toEqual({ authors: [FISH], path: 'src/a.ts' }); + provider.dispose(); + }); + + it('重启往返后 showHistory 增量叠加:恢复的完整持久过滤(authors + grep)再叠 path', async () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-a', JSON.stringify([FISH])); + memento.update('hyperGit.log.filter:/repo-a', JSON.stringify({ grep: 'fix' })); + const { provider, showHistory } = makeWiring('/repo-a', memento); + + await showHistory('src/a.ts'); + expect(provider.getFilter()).toEqual({ authors: [FISH], grep: 'fix', path: 'src/a.ts' }); + // path 随组合 key 一并持久化,重启后完整过滤存活 + expect(parsePersistedFilter(memento.get('hyperGit.log.filter:/repo-a'))).toEqual({ grep: 'fix', path: 'src/a.ts' }); + provider.dispose(); + }); + + it('logFilterPath 切库守卫:对话框期间切库,不再按新仓库 root 求出跨仓库垃圾 path', async () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-b', JSON.stringify([BOB])); + const { provider, service, filterPathWithDialog } = makeWiring('/repo-a', memento); + + await filterPathWithDialog('/repo-a/src/a.ts', () => service.fireRepoChange('/repo-b')); + // 无守卫时:path.relative('/repo-b', '/repo-a/src/a.ts') = '../repo-a/src/a.ts'(垃圾过滤)。 + expect(provider.getFilter()).toEqual({ authors: [BOB] }); + provider.dispose(); + }); + + it('logFilterPath 越仓守卫:所选文件在当前仓库外,告警放弃,不写内存与持久化 key', async () => { + const memento = new MemMemento(); + const { provider, filterPathWithDialog } = makeWiring('/repo-a', memento); + const warn = vi.mocked(vscode.window.showWarningMessage); + warn.mockClear(); + + // path.relative('/repo-a', '/outside/src/a.ts') = '../outside/src/a.ts'(越仓垃圾 path)。 + await filterPathWithDialog('/outside/src/a.ts'); + expect(provider.getFilter()).toEqual({}); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBeUndefined(); + expect(warn).toHaveBeenCalledWith('This file is outside the current repository; cannot filter by path'); + provider.dispose(); + }); + + it('logFilterPath 仓内 .. 前缀文件不误杀:按路径段判定越仓,仅 ../ 前缀拒绝', async () => { + const memento = new MemMemento(); + const { provider, filterPathWithDialog } = makeWiring('/repo-a', memento); + const warn = vi.mocked(vscode.window.showWarningMessage); + warn.mockClear(); + + // '..drafts/note.md' 是仓内合法文件名('..' 仅作前缀,非路径段),应正常写入 path 过滤。 + await filterPathWithDialog('/repo-a/..drafts/note.md'); + expect(provider.getFilter()).toEqual({ path: '..drafts/note.md' }); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBe(JSON.stringify({ path: '..drafts/note.md' })); + expect(warn).not.toHaveBeenCalled(); + provider.dispose(); + }); + + it('grep 正常路径:确认输入(含 trim)写入 filter 与组合 key', async () => { + const memento = new MemMemento(); + const { provider } = makeWiring('/repo-a', memento); + const grep = h.handlers.get('hyperGit.logFilterGrep') as () => Promise; + + await withPendingDialog(vi.mocked(vscode.window.showInputBox), ' fix ', grep); + expect(provider.getFilter()).toEqual({ grep: 'fix' }); + expect(parsePersistedFilter(memento.get('hyperGit.log.filter:/repo-a'))).toEqual({ grep: 'fix' }); + provider.dispose(); + }); + + it('grep 切库守卫:输入框期间切库,确认不再把旧仓库输入写入新仓库持久化 key', async () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-b', JSON.stringify([BOB])); + const { provider, service } = makeWiring('/repo-a', memento); + const grep = h.handlers.get('hyperGit.logFilterGrep') as () => Promise; + + await withPendingDialog(vi.mocked(vscode.window.showInputBox), 'fix', grep, () => service.fireRepoChange('/repo-b')); + expect(provider.getFilter()).toEqual({ authors: [BOB] }); + expect(memento.get('hyperGit.log.filter:/repo-b')).toBeUndefined(); + provider.dispose(); + }); + + it('mergeMode 切库守卫:选择器期间切库,确认不写入任何仓库的持久化 key', async () => { + const memento = new MemMemento(); + const { provider, service } = makeWiring('/repo-a', memento); + const mergeMode = h.handlers.get('hyperGit.logFilterMergeMode') as () => Promise; + + await withPendingDialog( + vi.mocked(vscode.window.showQuickPick), + { label: 'Merge Commits Only', mode: 'merge-only' }, + mergeMode, + () => service.fireRepoChange('/repo-b'), + ); + expect(provider.getFilter()).toEqual({}); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBeUndefined(); + expect(memento.get('hyperGit.log.filter:/repo-b')).toBeUndefined(); + provider.dispose(); + }); + + it('mergeMode 正常路径:选择 merge-only 写入 filter 与组合 key', async () => { + const memento = new MemMemento(); + const { provider } = makeWiring('/repo-a', memento); + const mergeMode = h.handlers.get('hyperGit.logFilterMergeMode') as () => Promise; + + await withPendingDialog( + vi.mocked(vscode.window.showQuickPick), + { label: 'Merge Commits Only', mode: 'merge-only' }, + mergeMode, + ); + expect(provider.getFilter()).toEqual({ mergeMode: 'merge-only' }); + expect(parsePersistedFilter(memento.get('hyperGit.log.filter:/repo-a'))).toEqual({ mergeMode: 'merge-only' }); + provider.dispose(); + }); + + it('date 正常路径:Last 7 Days 写入 dateFrom ≈ now-7d 并经组合 key ISO 往返', async () => { + const memento = new MemMemento(); + const { provider } = makeWiring('/repo-a', memento); + const date = h.handlers.get('hyperGit.logFilterDate') as () => Promise; + + const t0 = Date.now(); + await withPendingDialog(vi.mocked(vscode.window.showQuickPick), { label: 'Last 7 Days', days: 7 }, date); + const t1 = Date.now(); + const f = provider.getFilter(); + expect(f.dateFrom).toBeInstanceOf(Date); + // handler 内部捕获的 now ∈ [t0, t1](同 tick 执行时取等号),dateFrom = now - 7d。 + expect(f.dateFrom!.getTime()).toBeGreaterThanOrEqual(t0 - 7 * 86_400_000); + expect(f.dateFrom!.getTime()).toBeLessThanOrEqual(t1 - 7 * 86_400_000); + expect(parsePersistedFilter(memento.get('hyperGit.log.filter:/repo-a'))).toEqual({ dateFrom: f.dateFrom }); + provider.dispose(); + }); + + it('date 清除分支:Clear Date Filter 移除 dateFrom 并删组合 key', async () => { + const memento = new MemMemento(); + const { provider } = makeWiring('/repo-a', memento); + const date = h.handlers.get('hyperGit.logFilterDate') as () => Promise; + provider.setFilter({ ...provider.getFilter(), dateFrom: new Date('2026-01-01T00:00:00.000Z') }); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBeDefined(); + + await withPendingDialog(vi.mocked(vscode.window.showQuickPick), { label: 'Clear Date Filter', days: 0 }, date); + expect(provider.getFilter()).toEqual({}); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBeUndefined(); + provider.dispose(); + }); + + it('date 切库守卫:选择器期间切库,确认不写入任何仓库的持久化 key', async () => { + const memento = new MemMemento(); + const { provider, service } = makeWiring('/repo-a', memento); + const date = h.handlers.get('hyperGit.logFilterDate') as () => Promise; + + await withPendingDialog( + vi.mocked(vscode.window.showQuickPick), + { label: 'Last 7 Days', days: 7 }, + date, + () => service.fireRepoChange('/repo-b'), + ); + expect(provider.getFilter()).toEqual({}); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBeUndefined(); + expect(memento.get('hyperGit.log.filter:/repo-b')).toBeUndefined(); + provider.dispose(); + }); + + it('regex 正常路径:确认输入(含 trim)写入 filter 与组合 key', async () => { + const memento = new MemMemento(); + const { provider } = makeWiring('/repo-a', memento); + const regex = h.handlers.get('hyperGit.logFilterRegex') as () => Promise; + + await withPendingDialog(vi.mocked(vscode.window.showInputBox), ' ^feat: ', regex); + expect(provider.getFilter()).toEqual({ messageRegex: '^feat:' }); + expect(parsePersistedFilter(memento.get('hyperGit.log.filter:/repo-a'))).toEqual({ messageRegex: '^feat:' }); + provider.dispose(); + }); + + it('regex 清除分支:空输入确认移除 messageRegex 并删组合 key', async () => { + const memento = new MemMemento(); + const { provider } = makeWiring('/repo-a', memento); + const regex = h.handlers.get('hyperGit.logFilterRegex') as () => Promise; + provider.setFilter({ ...provider.getFilter(), messageRegex: '^feat:' }); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBeDefined(); + + // 空白输入经 re && re.trim() 规约为 undefined(清除维度),组合 key 归一化后全空即删。 + await withPendingDialog(vi.mocked(vscode.window.showInputBox), ' ', regex); + expect(provider.getFilter()).toEqual({}); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBeUndefined(); + provider.dispose(); + }); + + it('regex 切库守卫:输入框期间切库,确认不把旧仓库输入写入新仓库持久化 key', async () => { + const memento = new MemMemento(); + const { provider, service } = makeWiring('/repo-a', memento); + const regex = h.handlers.get('hyperGit.logFilterRegex') as () => Promise; + + await withPendingDialog(vi.mocked(vscode.window.showInputBox), '^feat:', regex, () => service.fireRepoChange('/repo-b')); + expect(provider.getFilter()).toEqual({}); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBeUndefined(); + expect(memento.get('hyperGit.log.filter:/repo-b')).toBeUndefined(); + provider.dispose(); + }); + + it('logFilter 聚合入口切库守卫:选择器期间切库,Clear Filter 不删新仓库的持久化 key', async () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-b', JSON.stringify([BOB])); + const { provider, service } = makeWiring('/repo-a', memento); + const filter = h.handlers.get('hyperGit.logFilter') as () => Promise; + + await withPendingDialog( + vi.mocked(vscode.window.showQuickPick), + { label: 'Clear Filter', description: 'remove all active filters', action: 'hyperGit.logClearFilter' }, + filter, + () => service.fireRepoChange('/repo-b'), + ); + // 守卫生效:B 的持久化 authors 未被清(无守卫时 clearFilter 会删 hyperGit.log.authors:/repo-b)。 + expect(memento.get('hyperGit.log.authors:/repo-b')).toBe(JSON.stringify([BOB])); + expect(provider.getFilter()).toEqual({ authors: [BOB] }); + provider.dispose(); + }); + + it('commit 文件菜单切库守卫:菜单期间切库,不派发携带旧仓库 relativePath 的动作', async () => { + const exec = vi.mocked(vscode.commands.executeCommand); + exec.mockClear(); + const { provider, service, menu } = makeCommitMenu('/repo-a'); + + await withPendingDialog( + vi.mocked(vscode.window.showQuickPick), + { label: '$(history) Show History', command: 'hyperGit.showHistory' }, + () => menu('src/a.ts'), + () => service.fireRepoChange('/repo-b'), + ); + expect(exec).not.toHaveBeenCalledWith('hyperGit.showHistory', expect.anything()); + provider.dispose(); + }); + + it('commit 文件菜单正常路径:确认后以原 change 派发所选命令', async () => { + const exec = vi.mocked(vscode.commands.executeCommand); + exec.mockClear(); + const { provider, menu } = makeCommitMenu('/repo-a'); + + await withPendingDialog( + vi.mocked(vscode.window.showQuickPick), + { label: '$(history) Show History', command: 'hyperGit.showHistory' }, + () => menu('src/a.ts'), + ); + expect(exec).toHaveBeenCalledWith('hyperGit.showHistory', { relativePath: 'src/a.ts' }); + provider.dispose(); + }); +}); diff --git a/tests/unit/log-filter.test.ts b/tests/unit/log-filter.test.ts index 2952075..4c69b01 100644 --- a/tests/unit/log-filter.test.ts +++ b/tests/unit/log-filter.test.ts @@ -1,5 +1,17 @@ import { describe, it, expect } from 'vitest'; -import { CHECKPOINT_SUBJECT_RE, applyClientFilters, isCheckpointSubject, safeRegex, type FilterableCommit, type LogClientFilter } from '../../src/engine/log/log-filter'; +import { + CHECKPOINT_SUBJECT_RE, + applyClientFilters, + hasActiveFilter, + isCheckpointSubject, + parsePersistedAuthors, + parsePersistedFilter, + safeRegex, + serializePersistableFilter, + type FilterableCommit, + type LogClientFilter, + type LogFilter, +} from '../../src/engine/log/log-filter'; const D = (iso: string): Date => new Date(iso); const C = (message: string, parents: string[], authorDate?: Date): FilterableCommit => ({ message, parents, authorDate }); @@ -114,3 +126,110 @@ describe('safeRegex', () => { expect(safeRegex('(')).toBeUndefined(); }); }); + +describe('parsePersistedAuthors', () => { + it('合法 exact + pattern 混合 JSON 串还原结构(round-trip)', () => { + const terms = [ + { kind: 'exact', name: 'ThreeFish', email: 'fish@example.com' }, + { kind: 'pattern', pattern: '^bot' }, + ] as const; + expect(parsePersistedAuthors(JSON.stringify(terms))).toEqual(terms); + }); + + it('非字符串输入返回 undefined', () => { + expect(parsePersistedAuthors(undefined)).toBeUndefined(); + expect(parsePersistedAuthors(42)).toBeUndefined(); + expect(parsePersistedAuthors({})).toBeUndefined(); + }); + + it('空串与非法 JSON 返回 undefined', () => { + expect(parsePersistedAuthors('')).toBeUndefined(); + expect(parsePersistedAuthors('not-json')).toBeUndefined(); + }); + + it('非数组 JSON 返回 undefined', () => { + expect(parsePersistedAuthors('{"kind":"exact"}')).toBeUndefined(); + }); + + it('元素脏项剔除,仅留合法项', () => { + const raw = JSON.stringify([ + { kind: 'exact', name: 'A', email: 'a@x.com' }, + { kind: 'exact', name: '', email: 'b@x.com' }, // name 空 + { kind: 'exact', name: 'C' }, // 缺 email + { kind: 'pattern', pattern: '' }, // pattern 空 + { kind: 'unknown' }, // kind 未知 + 'not-an-object', + ]); + expect(parsePersistedAuthors(raw)).toEqual([{ kind: 'exact', name: 'A', email: 'a@x.com' }]); + }); + + it('全脏 / 空数组返回 undefined(与「空选 = 不过滤」规范化一致)', () => { + expect(parsePersistedAuthors(JSON.stringify([{ kind: 'nope' }]))).toBeUndefined(); + expect(parsePersistedAuthors('[]')).toBeUndefined(); + }); +}); + +describe('serializePersistableFilter / parsePersistedFilter', () => { + const FULL: LogFilter = { + path: 'src/a.ts', + grep: 'fix', + mergeMode: 'no-merge', + dateFrom: new Date('2026-01-02T03:04:05.000Z'), + dateTo: new Date('2026-02-03T00:00:00.000Z'), + messageRegex: '^feat:', + }; + + it('全维度 round-trip(Date 经 ISO 串往返)', () => { + expect(parsePersistedFilter(serializePersistableFilter(FULL))).toEqual(FULL); + }); + + it('归一化:空对象、空串、mergeMode all、undefined 入参均不落盘', () => { + expect(serializePersistableFilter({})).toBeUndefined(); + expect(serializePersistableFilter(undefined)).toBeUndefined(); + expect(serializePersistableFilter({ path: '', grep: undefined, mergeMode: 'all' })).toBeUndefined(); + }); + + it('authors 不属本层:序列化忽略', () => { + expect(serializePersistableFilter({ authors: [{ kind: 'pattern', pattern: 'x' }] })).toBeUndefined(); + }); + + it('非字符串 / 非法 JSON / 非对象返回 undefined', () => { + expect(parsePersistedFilter(undefined)).toBeUndefined(); + expect(parsePersistedFilter(42)).toBeUndefined(); + expect(parsePersistedFilter('not-json')).toBeUndefined(); + expect(parsePersistedFilter('[]')).toBeUndefined(); + }); + + it('逐字段剔除脏项,仅留合法维度', () => { + const raw = JSON.stringify({ + path: 'ok.ts', + grep: '', + mergeMode: 'bogus', + dateFrom: 'not-a-date', + dateTo: '2026-02-03T00:00:00.000Z', + messageRegex: 7, + }); + expect(parsePersistedFilter(raw)).toEqual({ path: 'ok.ts', dateTo: new Date('2026-02-03T00:00:00.000Z') }); + }); + + it('全脏返回 undefined', () => { + expect(parsePersistedFilter(JSON.stringify({ grep: '', mergeMode: 'x', dateFrom: 'oops' }))).toBeUndefined(); + }); +}); + +describe('hasActiveFilter', () => { + it('未激活口径与持久化归一化一致:undefined / 空对象 / 空串 / mergeMode all / 空 authors', () => { + expect(hasActiveFilter(undefined)).toBe(false); + expect(hasActiveFilter({})).toBe(false); + expect(hasActiveFilter({ path: '', grep: undefined, mergeMode: 'all' })).toBe(false); + expect(hasActiveFilter({ authors: [] })).toBe(false); + }); + + it('任一维度生效即激活:authors(含 pattern)/ path / mergeMode / date / regex', () => { + expect(hasActiveFilter({ authors: [{ kind: 'pattern', pattern: 'x' }] })).toBe(true); + expect(hasActiveFilter({ path: 'src/a.ts' })).toBe(true); + expect(hasActiveFilter({ mergeMode: 'no-merge' })).toBe(true); + expect(hasActiveFilter({ dateFrom: D('2026-01-01T00:00:00.000Z') })).toBe(true); + expect(hasActiveFilter({ messageRegex: '^feat:' })).toBe(true); + }); +}); diff --git a/tests/unit/log-query.test.ts b/tests/unit/log-query.test.ts index 9627896..42f55fb 100644 --- a/tests/unit/log-query.test.ts +++ b/tests/unit/log-query.test.ts @@ -36,17 +36,60 @@ describe('buildLogArgs — 顺序与必选项', () => { }); describe('buildLogArgs — 服务端过滤翻译', () => { - it('author / grep 翻译为 flag(去空白)', () => { - const args = buildLogArgs({ author: ' Jane ', grep: 'fix' }, 'all', { maxCount: 300 }); - expect(args).toContain('--author=Jane'); - expect(args).toContain('--grep=fix'); + it('exact 作者翻译为锚定字面 pattern(首尾 ^$ + 全字面转义,同名异邮箱不混淆)', () => { + const args = buildLogArgs({ authors: [{ kind: 'exact', name: 'Jane', email: 'jane@x.com' }] }, 'all', { maxCount: 300 }); + expect(args).toContain('--author=^Jane $'); + }); + + it('多个 exact 作者产生多条 --author(git 取 OR)', () => { + const args = buildLogArgs( + { + authors: [ + { kind: 'exact', name: 'Jane', email: 'jane@x.com' }, + { kind: 'exact', name: 'Bob', email: 'bob@x.com' }, + ], + }, + 'all', + { maxCount: 300 }, + ); + const authorFlags = args.filter((a) => a.startsWith('--author=')); + expect(authorFlags).toHaveLength(2); + }); + + it('pattern 项原样透传(保留旧 InputBox 任意正则能力),与 exact 可混存且去重', () => { + const exact = { kind: 'exact', name: 'Jane', email: 'jane@x.com' } as const; + const args = buildLogArgs( + { authors: [{ kind: 'pattern', pattern: ' bot@example ' }, exact, exact] }, + 'all', + { maxCount: 300 }, + ); + const authorFlags = args.filter((a) => a.startsWith('--author=')); + expect(authorFlags).toContain('--author=bot@example'); + expect(authorFlags).toHaveLength(2); }); - it('空 author/grep 不产生 flag', () => { - const args = buildLogArgs({ author: ' ', grep: '' }, 'all', { maxCount: 300 }); + it('BRE 转义护栏:+ ? | 裸写(BRE 普通字符,\\+ 反成量词致归零命中),. [ ] \\ 转义', () => { + const args = buildLogArgs( + { authors: [{ kind: 'exact', name: 'dependabot[bot]', email: '49699333+nx@users.noreply.github.com' }] }, + 'all', + { maxCount: 300 }, + ); + const flag = args.find((a) => a.startsWith('--author=')); + expect(flag).toBe('--author=^dependabot\\[bot\\] <49699333+nx@users\\.noreply\\.github\\.com>$'); + }); + + it('空 pattern / 空数组 / 全空白不产生 flag', () => { // 用 `--author=` / `--grep=` 精确前缀,避免与既有的 `--author-date-order` 排序 flag 误判 - expect(args.some((a) => a.startsWith('--author='))).toBe(false); - expect(args.some((a) => a.startsWith('--grep='))).toBe(false); + expect(buildLogArgs({ authors: [] }, 'all', { maxCount: 300 }).some((a) => a.startsWith('--author='))).toBe(false); + expect( + buildLogArgs({ authors: [{ kind: 'pattern', pattern: ' ' }] }, 'all', { maxCount: 300 }).some((a) => a.startsWith('--author=')), + ).toBe(false); + expect(buildLogArgs({ grep: '' }, 'all', { maxCount: 300 }).some((a) => a.startsWith('--grep='))).toBe(false); + }); + + it('grep 翻译为 flag(去空白)', () => { + const args = buildLogArgs({ grep: ' fix ' }, 'all', { maxCount: 300 }); + expect(args).toContain('--grep=fix'); }); it('path 以 pathspec 形式置于参数末尾(--format 之后)', () => { diff --git a/tests/unit/log-webview-filter-persist.test.ts b/tests/unit/log-webview-filter-persist.test.ts new file mode 100644 index 0000000..b792d8f --- /dev/null +++ b/tests/unit/log-webview-filter-persist.test.ts @@ -0,0 +1,254 @@ +import { describe, it, expect, vi } from 'vitest'; + +vi.mock('vscode', () => ({ + // 构造路径仅触 setCtx(commands.executeCommand);webview 未 resolve 时 pushState 早退,无需其余 API。 + commands: { executeCommand: vi.fn(() => Promise.resolve()) }, +})); + +import * as vscode from 'vscode'; +import type { Memento } from 'vscode'; +import type { GitRepositoryService } from '../../src/adapter/git-repository-service'; +import type { GitHubCiService } from '../../src/adapter/ci/github-ci-service'; +import type { AuthorTerm } from '../../src/engine/log/log-filter'; +import { LogWebviewProvider } from '../../src/adapter/webview/log-webview'; +import { parsePersistedFilter, type LogFilter } from '../../src/engine/log/log-filter'; + +/** 内存 Memento(模拟 workspaceState 持久化往返;update(key, undefined) 即删除条目,对齐 Memento 语义)。 */ +class MemMemento { + private readonly map = new Map(); + get(key: string): T | undefined { + return this.map.get(key) as T | undefined; + } + update(key: string, value: unknown): Thenable { + if (value === undefined) { + this.map.delete(key); + } else { + this.map.set(key, value); + } + return Promise.resolve(); + } +} + +/** 功能型 EventEmitter mock(事件注册监听、fire 通知;branch-favorites.test.ts 同范式)。 */ +class FakeEmitter { + private listeners: Array<(a: A) => void> = []; + readonly event = (l: (a: A) => void): { dispose: () => void } => { + this.listeners.push(l); + return { dispose: () => { this.listeners = this.listeners.filter((x) => x !== l); } }; + }; + fire(a: A): void { + for (const l of [...this.listeners]) { + l(a); + } + } +} + +/** fake service:可变 repoRoot + 可手动 fire 的两个生命周期事件。 */ +function makeService(root: string | null) { + const repoChanged = new FakeEmitter<{ previousRoot: string | null; root: string | null }>(); + const changed = new FakeEmitter(); + const service = { + repoRoot: root, + onDidChange: changed.event, + onDidChangeRepository: repoChanged.event, + fireRepoChange(next: string | null): void { + const previousRoot = this.repoRoot; + this.repoRoot = next; + repoChanged.fire({ previousRoot, root: next }); + }, + }; + return service; +} + +const AUTHORS: readonly AuthorTerm[] = [ + { kind: 'exact', name: 'ThreeFish', email: 'fish@example.com' }, + { kind: 'pattern', pattern: '^bot' }, +]; + +function makeProvider(root: string | null, memento: MemMemento): { provider: LogWebviewProvider; service: ReturnType } { + const service = makeService(root); + const provider = new LogWebviewProvider( + service as unknown as GitRepositoryService, + {} as GitHubCiService, + memento as unknown as Memento, + ); + return { provider, service }; +} + +describe('LogWebviewProvider authors 持久化', () => { + it('构造期从 memento 恢复 authors', () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-a', JSON.stringify(AUTHORS)); + const { provider } = makeProvider('/repo-a', memento); + expect(provider.getFilter()).toEqual({ authors: AUTHORS }); + provider.dispose(); + }); + + it('setFilter 持久化往返:同 memento 重建 provider 恢复', () => { + const memento = new MemMemento(); + const { provider } = makeProvider('/repo-a', memento); + provider.setFilter({ authors: AUTHORS, grep: 'x' }); + expect(memento.get('hyperGit.log.authors:/repo-a')).toBe(JSON.stringify(AUTHORS)); + provider.dispose(); + + const { provider: revived } = makeProvider('/repo-a', memento); + expect(revived.getFilter()).toEqual({ authors: AUTHORS, grep: 'x' }); + revived.dispose(); + }); + + it('清除同样持久化:空选确认(authors: undefined)与 clearFilter 均删 key,重启不复活', () => { + const memento = new MemMemento(); + const { provider } = makeProvider('/repo-a', memento); + provider.setFilter({ ...provider.getFilter(), authors: AUTHORS }); + expect(memento.get('hyperGit.log.authors:/repo-a')).toBeDefined(); + provider.setFilter({ ...provider.getFilter(), authors: undefined }); + expect(memento.get('hyperGit.log.authors:/repo-a')).toBeUndefined(); + + provider.setFilter({ authors: AUTHORS }); + provider.clearFilter(); + expect(memento.get('hyperGit.log.authors:/repo-a')).toBeUndefined(); + provider.dispose(); + }); + + it('脏持久化数据免疫:视为未设置,不崩溃', () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-a', 'not-json'); + const { provider } = makeProvider('/repo-a', memento); + expect(provider.getFilter()).toEqual({}); + provider.dispose(); + }); + + it('切库重绑:恢复新仓库完整持久过滤(authors + 非 authors 维度各自记忆)', () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.authors:/repo-b', JSON.stringify([{ kind: 'exact', name: 'B', email: 'b@x.com' }])); + const { provider, service } = makeProvider('/repo-a', memento); + provider.setFilter({ authors: AUTHORS, grep: 'stale' }); + + service.fireRepoChange('/repo-b'); + expect(provider.getFilter()).toEqual({ authors: [{ kind: 'exact', name: 'B', email: 'b@x.com' }] }); + + // 往返切回 A:恢复 A 的 authors 与 grep(各自 key 均按仓库存活) + service.fireRepoChange('/repo-a'); + expect(provider.getFilter()).toEqual({ authors: AUTHORS, grep: 'stale' }); + provider.dispose(); + }); + + it('repoRoot 为 null 时仅内存,不写 memento', () => { + const memento = new MemMemento(); + const { provider } = makeProvider(null, memento); + provider.setFilter({ authors: AUTHORS }); + expect(memento.get('hyperGit.log.authors:null')).toBeUndefined(); + expect(provider.getFilter()).toEqual({ authors: AUTHORS }); + provider.dispose(); + }); +}); + +describe('LogWebviewProvider 非 authors 过滤维度持久化', () => { + const DIMS: LogFilter = { + path: 'src/a.ts', + grep: 'fix', + mergeMode: 'no-merge', + dateFrom: new Date('2026-01-02T03:04:05.000Z'), + messageRegex: '^feat:', + }; + + it('setFilter 全维度持久化往返:同 memento 重建 provider 恢复(Date 经 ISO 往返)', () => { + const memento = new MemMemento(); + const { provider } = makeProvider('/repo-a', memento); + provider.setFilter(DIMS); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBeDefined(); + provider.dispose(); + + const { provider: revived } = makeProvider('/repo-a', memento); + expect(revived.getFilter()).toEqual(DIMS); + revived.dispose(); + }); + + it('清除维度只删组合 key 中对应内容,authors key 独立存活', () => { + const memento = new MemMemento(); + const { provider } = makeProvider('/repo-a', memento); + provider.setFilter({ ...DIMS, authors: AUTHORS }); + // 空选确认(清 grep):组合 key 仅剩其余维度,authors key 不动 + provider.setFilter({ ...provider.getFilter(), grep: undefined }); + expect(memento.get('hyperGit.log.authors:/repo-a')).toBe(JSON.stringify(AUTHORS)); + const revived0 = parsePersistedFilter(memento.get('hyperGit.log.filter:/repo-a')); + expect(revived0).toEqual({ path: DIMS.path, mergeMode: DIMS.mergeMode, dateFrom: DIMS.dateFrom, messageRegex: DIMS.messageRegex }); + + // clearFilter:两 key 全删 + provider.clearFilter(); + expect(memento.get('hyperGit.log.filter:/repo-a')).toBeUndefined(); + expect(memento.get('hyperGit.log.authors:/repo-a')).toBeUndefined(); + provider.dispose(); + }); + + it('切库恢复新仓库组合 key,与 authors 合并', () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.filter:/repo-b', JSON.stringify({ grep: 'bot', mergeMode: 'merge-only' })); + memento.update('hyperGit.log.authors:/repo-b', JSON.stringify(AUTHORS)); + const { provider, service } = makeProvider('/repo-a', memento); + provider.setFilter({ path: 'old.ts' }); + + service.fireRepoChange('/repo-b'); + expect(provider.getFilter()).toEqual({ grep: 'bot', mergeMode: 'merge-only', authors: AUTHORS }); + provider.dispose(); + }); + + it('脏组合数据免疫:视为未设置,不崩溃', () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.filter:/repo-a', 'not-json'); + memento.update('hyperGit.log.authors:/repo-a', JSON.stringify(AUTHORS)); + const { provider } = makeProvider('/repo-a', memento); + expect(provider.getFilter()).toEqual({ authors: AUTHORS }); + provider.dispose(); + }); +}); + +describe('LogWebviewProvider 过滤激活 context key(标题栏图标双命令互斥)', () => { + it('setFilter / clearFilter 同步 hyperGit.log.filterActive', () => { + const memento = new MemMemento(); + const { provider } = makeProvider('/repo-a', memento); + const exec = vi.mocked(vscode.commands.executeCommand); + exec.mockClear(); + + provider.setFilter({ grep: 'fix' }); + expect(exec).toHaveBeenCalledWith('setContext', 'hyperGit.log.filterActive', true); + exec.mockClear(); + provider.clearFilter(); + expect(exec).toHaveBeenCalledWith('setContext', 'hyperGit.log.filterActive', false); + provider.dispose(); + }); + + it('构造期恢复持久过滤即激活(重启后图标点亮)', () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.filter:/repo-a', JSON.stringify({ grep: 'fix' })); + const { provider } = makeProvider('/repo-a', memento); + expect(vi.mocked(vscode.commands.executeCommand)).toHaveBeenCalledWith('setContext', 'hyperGit.log.filterActive', true); + provider.dispose(); + }); + + it('切库重载按新仓库过滤态同步(B 无持久过滤 → false,往返切回 → true)', () => { + const memento = new MemMemento(); + memento.update('hyperGit.log.filter:/repo-a', JSON.stringify({ grep: 'fix' })); + const { provider, service } = makeProvider('/repo-a', memento); + const exec = vi.mocked(vscode.commands.executeCommand); + exec.mockClear(); + + service.fireRepoChange('/repo-b'); + expect(exec).toHaveBeenCalledWith('setContext', 'hyperGit.log.filterActive', false); + exec.mockClear(); + service.fireRepoChange('/repo-a'); + expect(exec).toHaveBeenCalledWith('setContext', 'hyperGit.log.filterActive', true); + provider.dispose(); + }); + + it('归一化未激活态不点亮:空串 / mergeMode all / 空 authors 恢复后仍为 false', () => { + const memento = new MemMemento(); + const { provider } = makeProvider('/repo-a', memento); + const exec = vi.mocked(vscode.commands.executeCommand); + exec.mockClear(); + + provider.setFilter({ path: '', mergeMode: 'all', authors: [] }); + expect(exec).toHaveBeenCalledWith('setContext', 'hyperGit.log.filterActive', false); + provider.dispose(); + }); +}); diff --git a/tests/unit/ref-remote.test.ts b/tests/unit/ref-remote.test.ts index 5e66859..9c7586b 100644 --- a/tests/unit/ref-remote.test.ts +++ b/tests/unit/ref-remote.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from 'vitest'; -import { resolveRemoteBranch, partitionRemoteByProtected, formatRemoteDeleteConfirm } from '../../src/engine/ref/remote-ref'; +import { + resolveRemoteBranch, + partitionRemoteByProtected, + formatRemoteDeleteConfirm, + isRemoteRefMissingError, + formatRemoteDeleteResult, +} from '../../src/engine/ref/remote-ref'; describe('resolveRemoteBranch', () => { it('普通 remote:origin/foo → {origin, foo}', () => { @@ -109,3 +115,113 @@ describe('formatRemoteDeleteConfirm', () => { expect(r.detail).toContain('upstream'); }); }); + +describe('isRemoteRefMissingError', () => { + // 本机 git 2.54 对「服务端已无此分支」push --delete 的实测 stderr。 + const REAL_STDERR = 'error: unable to delete \'rsi/fixture-terminology\': remote ref does not exist\nerror: failed to push some refs to \'github.com:ThreeFish-AI/rsi.git\''; + + it('实测 stderr 文案(remote ref does not exist)→ true', () => { + const e = Object.assign(new Error('Command failed: git push'), { stderr: REAL_STDERR }); + expect(isRemoteRefMissingError(e)).toBe(true); + }); + + it('旧服务端变体(delete of non-existent ref)→ true', () => { + expect(isRemoteRefMissingError(Object.assign(new Error('x'), { stderr: ' ! [remote rejected] foo (delete of non-existent ref)' }))).toBe(true); + }); + + it('大小写不敏感', () => { + expect(isRemoteRefMissingError(Object.assign(new Error('x'), { stderr: 'Remote Ref Does Not Exist' }))).toBe(true); + }); + + it('无 stderr 时回退 message', () => { + expect(isRemoteRefMissingError(new Error('unable to delete: remote ref does not exist'))).toBe(true); + }); + + it('保护分支拒绝 → false(真失败)', () => { + const e = Object.assign(new Error('Command failed: git push'), { + stderr: 'remote: error: refusing to delete the current branch: refs/heads/main\n ! [remote rejected] main (remote failure)', + }); + expect(isRemoteRefMissingError(e)).toBe(false); + }); + + it('网络/认证错误等无关报错 → false', () => { + expect(isRemoteRefMissingError(Object.assign(new Error('x'), { stderr: 'fatal: Could not read from remote repository.' }))).toBe(false); + expect(isRemoteRefMissingError(new Error('Command failed: git push origin --delete foo'))).toBe(false); + }); + + it('非对象抛出值 / 空对象 / 空文本 → false(裸字符串走 String(e) 兜底,命中文案为 true)', () => { + expect(isRemoteRefMissingError('remote ref does not exist')).toBe(true); + expect(isRemoteRefMissingError('boom')).toBe(false); + expect(isRemoteRefMissingError(null)).toBe(false); + expect(isRemoteRefMissingError({})).toBe(false); + expect(isRemoteRefMissingError(Object.assign(new Error(''), { stderr: '' }))).toBe(false); + expect(isRemoteRefMissingError(Object.assign(new Error(''), { stderr: ' ' }))).toBe(false); + }); + + it('stderr 空白但 message 命中 → 回退 message(与 errMsg 守卫语义一致)', () => { + expect(isRemoteRefMissingError(Object.assign(new Error('push: remote ref does not exist'), { stderr: ' ' }))).toBe(true); + }); +}); + +describe('formatRemoteDeleteResult', () => { + const t = (shortName: string, remote = 'origin', branch = shortName.slice(remote.length + 1)) => ({ remote, branch, shortName }); + + it('仅单条成功 → 单数文案 + info', () => { + const r = formatRemoteDeleteResult([t('origin/foo')], [], []); + expect(r).toEqual({ level: 'info', message: 'Deleted remote branch origin/foo' }); + }); + + it('多条成功 → 复数计数 + info', () => { + const r = formatRemoteDeleteResult([t('origin/a'), t('origin/b')], [], []); + expect(r.level).toBe('info'); + expect(r.message).toBe('Deleted 2 remote branches'); + }); + + it('成功 + 服务端已不存在 → 诚实并列 already gone + info', () => { + const r = formatRemoteDeleteResult([t('origin/a')], [t('origin/rsi/fixture-terminology')], []); + expect(r.level).toBe('info'); + expect(r.message).toBe('Deleted remote branch origin/a; 1 already gone on remote (pruned local tracking ref)'); + }); + + it('仅 alreadyGone(本次报错场景:0 删除 1 陈旧)→ info 且不以 Deleted 0 开头', () => { + const r = formatRemoteDeleteResult([], [t('origin/x')], []); + expect(r.level).toBe('info'); + expect(r.message).toBe('1 already gone on remote (pruned local tracking ref)'); + }); + + it('alreadyGone 复数 → tracking refs', () => { + const r = formatRemoteDeleteResult([], [t('origin/x'), t('origin/y')], []); + expect(r.message).toContain('pruned local tracking refs'); + }); + + it('含失败 → error 级,附分支名与折叠后的单行原因', () => { + const r = formatRemoteDeleteResult([t('origin/a')], [], [ + { name: 'origin/main', reason: 'remote: error: refusing to delete the current branch\n ! [remote rejected] main (remote failure)' }, + ]); + expect(r.level).toBe('error'); + expect(r.message).toContain('1 failed: origin/main ('); + expect(r.message).toContain('refusing to delete the current branch; ! [remote rejected] main (remote failure))'); + expect(r.message).not.toContain('\n'); + }); + + it('失败原因超长 → 截断加省略号', () => { + const r = formatRemoteDeleteResult([], [], [{ name: 'origin/x', reason: `line\n${'e'.repeat(300)}` }]); + expect(r.message.length).toBeLessThan(300); + expect(r.message).toContain('…'); + }); + + it('失败超 4 条 → 截断标注 and N more', () => { + const failures = Array.from({ length: 6 }, (_, i) => ({ name: `origin/f${i}`, reason: 'boom' })); + const r = formatRemoteDeleteResult([], [], failures); + expect(r.level).toBe('error'); + expect(r.message).toContain('6 failed: '); + expect(r.message).toContain('… and 2 more'); + expect(r.message).not.toContain('origin/f5 ('); + }); + + it('三桶共存 → 一条消息完整覆盖', () => { + const r = formatRemoteDeleteResult([t('origin/a')], [t('origin/b')], [{ name: 'origin/c', reason: 'denied' }]); + expect(r.level).toBe('error'); + expect(r.message).toBe('Deleted remote branch origin/a; 1 already gone on remote (pruned local tracking ref); 1 failed: origin/c (denied)'); + }); +});