dsh-gitbash-shell 0.26.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/README.md +37 -0
- package/cordis.patch.yml +5 -1
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/bash-path.js +371 -0
- package/src/client.js +407 -7
- package/src/index.js +178 -31
- package/src/shell.js +58 -10
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,80 @@
|
|
|
3
3
|
> 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
|
|
4
4
|
> 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
|
|
5
5
|
|
|
6
|
+
## v0.28.0 — 2026-09-26
|
|
7
|
+
|
|
8
|
+
**类型**:fix(issue #11 —— Git 没装在 `C:\Program Files\Git` 时,写死的默认 `bashPath` 让每条命令 `spawn … ENOENT`,而整个会话的命令能力归零且无提示)+ feat(fail-loud 引导:弹窗可填写路径 / 可跳转下载)
|
|
9
|
+
|
|
10
|
+
> 报障:https://github.com/KannaKuron/dsh-gitbash-shell/issues/11(Windows 11,Git 装在 `Q:\Git`;作者是另一台 DSH agent「蓝鲸」,诊断准确、自带 workaround)。两条用户口径逐字确立:**"默认用设置里填的 空的就去默认路径 没有就去用户环境path 和 系统path找 还没有就报错,弹窗让他去下载或者去设置里填写路径,而且要支持点击转跳过去"**;**"绝对不能因为找不到就用pwsh … 绝对不能用wsl"**。
|
|
11
|
+
|
|
12
|
+
### 一、默认值假设被推翻:解析链接管
|
|
13
|
+
- **起因**:`cordis.patch.yml` 把 `gitbash-executor.config.bashPath` 写成 `C:/Program Files/Git/bin/bash.exe`;Git 装在别处 ⇒ 每条命令 ENOENT。更糟的是本插件撤掉 `pwsh-sandbox` 而 dsh 每进程只允许一个 `ctx.shell`,于是**连原本可用的 pwsh 也一并没了**,且没有任何提示。
|
|
14
|
+
- **新解析链(`src/bash-path.js`,唯一实现;执行器 `src/shell.js` 与翻译层 `src/index.js` 共用同一 memo)**:
|
|
15
|
+
**设置里填的**(`gitbash-executor` 行 config > `gitbash-shell` 行/设置卡;用 `ctx.loader.resolve('gitbash-executor')` 让翻译层看到同一个显式值,显式值失败也不换别的) → **默认安装位置**(Program Files / Program Files (x86) / ProgramW6432 / LOCALAPPDATA) → **PATH 逐目录 `bash.exe`** → **PATH 上 `git.exe` 反推** → **注册表 `Path`**(HKCU + HKLM,`reg query` 走 argv 数组) → **失败**。
|
|
16
|
+
`cordis.patch.yml` 的 `bashPath` 默认改为**空串(=自动)**。
|
|
17
|
+
- **只认 Git for Windows(用户第二条口径)**:黑名单先于探测(`system32`/`windowsapps`/`msys`/`cygwin`/`wsl`,归一化后逐段匹配 —— System32 是头号陷阱,它永远在 PATH 上);正向判据 = `<root>/cmd|bin/git.exe` + `<root>/usr/bin/bash.exe` + (`<root>/mingw64` 或 `usr/bin/msys-2.0.dll`);指纹 = `git --version` 含 `.windows.`;**一票否决** = 实跑候选 `bash -c "uname -s"` 必须是 `MINGW32_NT-*`/`MINGW64_NT-*`(`Linux`/`MSYS_NT-*`/`CYGWIN_NT-*` 一律不合格)。全部候选失败 ⇒ `{ ok:false, path:'' }`,**绝不返回替补 shell**。
|
|
18
|
+
- **永不回退(用户第一条口径,写进 AGENTS §4h)**:`pwsh-sandbox` 仍是无条件 `disabled: true`(与 bash 健康无关);`src/shell.js` 每次 argv 都过 `requireBashPath()`,解析失败**直接 throw 带引导的错误**(不再让用户只看到裸 ENOENT);冒烟守卫断言 patch 里没有 `!!js` 条件、执行器源码不出现 `pwsh.exe`/`powershell.exe`/`cmd.exe`/`wsl.exe`/裸 `bash`。
|
|
19
|
+
|
|
20
|
+
### 二、fail-loud 与可点击引导
|
|
21
|
+
- **启动日志**:完整报告 —— 当前 `bashPath` 取到什么 / 按顺序探测过哪些位置(逐条 code+detail)/ 去哪里改 / 下载链接 / "本插件不会回退到 PowerShell"。
|
|
22
|
+
- **客户端弹窗**(`shell.overlay`,仅 win32 且 `ok:false`;同 boot 一次、`sessionStorage` 记关闭;21 语言新增 6 个 `bashmiss.*` 键):探测清单 + **内联 bashPath 编辑框(写 `configForms` 的同一字段,保存落盘到 profile 的 `cordis.patch.yml`)** + **「去下载 Git for Windows」**(`window.open`,Electron 下走外部浏览器)+ 关闭。
|
|
23
|
+
- **写入判定按宿主的布尔返回值(验收反馈修复,commit 见下)**:`ConfigForm.set()` 的契约是 `Promise<boolean>` = **Host 是否接受这次写入**。原先弹窗与设置卡的 bashPath 保存都**丢掉该返回值**、无条件显示「已保存」⇒ Host 拒绝时会**假成功**(与 §4h「绝不静默假成功」同一条纪律)。现在两处都按 `accepted === true` 决定成败,拒绝时显示 `bash.saveFailed`(21 语言,含"确认路径存在且是 Git for Windows 的 bash / 查看宿主日志"的下一步),并处理"没有 form 座位"这一同类情形(同样是拒绝而不是成功)。
|
|
24
|
+
- **数据通道**:host 半注册 `GET /dsh-gitbash-shell/api/status`(`ctx.inject(['webServer'])` 可选服务,家族既有范式同 dsh-ide-git),客户端**在 effect 里 fetch**(不在 apply 取服务,避开客户端半的取服务竞态);`gitBash` capability 同时**加法**扩展为 `{ active, bashPath, ok, source, configured, tried, downloadUrl }`。
|
|
25
|
+
- **深链实测结论**:官方设置页**没有**公开 API 能跳到指定插件的设置卡 —— `openSettings`/`openSection` 只发给 `settings.launcher`/`settings.onboarding` 占用者,面板开关状态是 `ui-settings-general` 私有的 store,`ctx.shortcuts` 也没有"按 id 执行命令"的入口。因此弹窗**自带编辑器**(用户不必去找设置页),文字指路作为兜底;上游若开放深链 API,这里可以直接换成按钮跳转。
|
|
26
|
+
|
|
27
|
+
### 三、验证
|
|
28
|
+
- `npm test` **93/93 绿**(新增 7 条:解析链显式优先且**失败不替换** / 默认→PATH 含陷阱拒绝 / 只认 Git(WSL+MSYS2+Cygwin+`.windows.`+`uname` 矩阵)/ 全落空报告含引导与下载链接 / **无回退守卫** / 弹窗形状与 21 语言 / 路由与 capability 形状);`tests/align-official.mjs` 四变体仍逐字节对齐。
|
|
29
|
+
- **真机(隔离 `DSH_HOME` + 插件副本强制 win32 门 + 无头 Chrome/CDP,macOS)**:
|
|
30
|
+
- 启动日志打出完整报告(15 条探测记录,含 `missing`/`bad-shape` 分类);
|
|
31
|
+
- `curl /dsh-gitbash-shell/api/status` → `{"ok":false,"platform":"win32",…}`;
|
|
32
|
+
- **弹窗渲染**:标题「找不到 Git Bash —— 命令无法执行」+ 按钮 `[保存, 去下载 Git for Windows, 关闭]` + **12 条探测清单** + 输入框;
|
|
33
|
+
- **下载动作**:「去下载」点击后 `window.open` 收到 `https://git-scm.com/download/win`;
|
|
34
|
+
- **内联编辑**:填入 `Q:/Git/bin/bash.exe` → 保存 → 卡片显示「已保存」,**宿主侧落盘** `profiles/verif/cordis.patch.yml` 的 `bashPath: Q:/Git/bin/bash.exe`;
|
|
35
|
+
- **一次性**:关闭后刷新页面不再出现(会话内已记)。
|
|
36
|
+
- 过程中自查修掉一个真缺陷:一次性判定原先写在 render 里(设标志的那次渲染之后的**任何 re-render 都会让弹窗消失**),改为在 effect 中决定 + 本地 state 呈现。
|
|
37
|
+
- **拒绝态实测(隔离实例 + 副本把 host 写入强制为 `false` + 无头 Chrome/CDP)**:`showsSaved=false`、`showsFailure=true`,界面显示
|
|
38
|
+
「写入失败: 保存未生效:宿主拒绝了这次写入 —— 确认路径存在且是 Git for Windows 的 bash,或查看宿主日志」,零 pageerror;
|
|
39
|
+
弹窗此时仍有「去下载 Git for Windows」与探测清单作为下一步。`npm test` **94/94**(新增第 8 条守卫:两处写入都必须看布尔返回值、不得出现无条件 `setBashSaved(true)`、21 语言失败文案齐)。
|
|
40
|
+
- **未验证(如实标注,需 issue 作者在 Windows 真机复验)**:注册表 `Path` 兜底、真实 Git 安装布局、WSL/WindowsApps/MSYS2 实机路径的拒绝行为(均为 fake io 单测 + 静态核对);深链不可用是上游能力缺口。
|
|
41
|
+
- 相关:issue #11 https://github.com/KannaKuron/dsh-gitbash-shell/issues/11
|
|
42
|
+
|
|
43
|
+
## v0.27.0 — 2026-09-25
|
|
44
|
+
|
|
45
|
+
**类型**:feat(新设置「子代理/队员是否也使用 Git Bash」,默认开启 —— 用户 2026-09-25 追加需求)
|
|
46
|
+
|
|
47
|
+
### 需求与语义
|
|
48
|
+
用户原话:「gitbash 插件也是专门给个设置是否让子代理或者子队员使用 gitbash,默认启动。」
|
|
49
|
+
|
|
50
|
+
- **开关 `subagentDialect`(本插件行 Config 的 volatile 布尔,**默认 `true`**;旧宿主 ≤0.1.6 在 settings 命名空间 `gitbash-shell` 声明同名字段,**缺键 = true**)。
|
|
51
|
+
- **开(默认)**:委托代理 —— 子代理(`subagent`/`subagent_fork`)、团队队员(`spawn_teammate` 走 continuable-subagent 的 `spawn`/`fork`)、**嵌套子代理** —— 与主代理享受同一条方言链路。
|
|
52
|
+
- **关**:方言只对主代理生效,委托出去的请求回退官方 shell 语义(提示词不改写、路径参数不翻译、结果/报错不回显为 MSYS、`run_code` 程序字面量不翻译、不下发 `DSH_PATH_DIALECT`)。
|
|
53
|
+
|
|
54
|
+
### 委托身份的判据(先查清链路,再动代码)
|
|
55
|
+
- 用**会话头**判定:`origin === 'subagent' || delegationDepth > 0` —— 与 dsh 自己的 `packages/deliverables/workspace-changes/src/index.ts:59-60` 同一对字段;由子代理驱动在 `packages/subagent/subagent/src/child-agent.ts:139-155` (`childSessionMeta`) 打上,`spawn`/`fork` 两种 provider 与嵌套深度都走它。
|
|
56
|
+
- 纯函数 `isDelegatedAgent(agent)` + 唯一判定点 `dialectApplies(dialect, agent)`;条件挂在 `assembleContext.agent`(`packages/core/agent/src/dispatch.ts:174` 的 `assembleContextFor` 传 `{ agent, scope }`)与 `exec.agent` / `execution.agent` 上。
|
|
57
|
+
- **未知形状一律视为"非委托"**(保留方言):判定不出来时按主代理处理。
|
|
58
|
+
|
|
59
|
+
### 覆盖到的五个消费点(全部接同一个 gate,避免"半方言")
|
|
60
|
+
① `system-prompt/assemble` 的源头改写与 run_code 句;② 指令 context 的 `text(context)` provider;③ `tools/execute` 的入参翻译 + 成功面回显;④ `tools/post-execute` 的失败面 `content`/`error.message`;⑤ `shellEnv.resolve(execution)` 的 `DSH_PATH_DIALECT` 事实。
|
|
61
|
+
|
|
62
|
+
### 能力边界(如实写明,写进 README/AGENTS)
|
|
63
|
+
dsh **每个进程只有一个 shell 执行器**(`ctx.shell` 是单例服务),所以 **Git Bash 二进制本身仍是全局的**,本开关管的是**方言/翻译层**;关闭后委托代理见到与写出的是 Windows 形式路径,而 Git Bash 同样接受 `C:/...`,行为自洽。
|
|
64
|
+
|
|
65
|
+
### 测试与证据
|
|
66
|
+
- 冒烟 **86/86 绿**:新增 3 条(① `isDelegatedAgent`/`dialectApplies` 矩阵:origin、depth 1/2、root、未知/抛错/`agent` 缺失,以及卡片写入断言;② OFF 时主代理保留、委托代理被挡 + 五个消费点都接 gate 的源码断言;③ 旧宿主/老配置**缺键 = true** 的回退);`tests/align-official.mjs` 四变体仍全绿(本版不动组合)。
|
|
67
|
+
- **真机两态(隔离 `DSH_HOME` + 新建 profile + 插件副本 `link:` 安装,真实 dsh 0.1.7-rc.2,本机 macOS)**:探针用 `ctx.agents.create()` 造**真**的 root / child(`origin:'subagent', delegationDepth:1`)/ nested(depth 2)三个 agent(实测会话头 `undefined` / `"subagent":1` / `"subagent":2`),再对每个 agent 调**真的** `ctx.systemPrompt.assemble({ agent, scope: agent })`:
|
|
68
|
+
|
|
69
|
+
| 开关 | main(root) | subagent | nested |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| **默认 ON** | 方言指令 = yes | **yes** | **yes** |
|
|
72
|
+
| **OFF**(行 Config `subagentDialect: false`) | yes | **no** | **no** |
|
|
73
|
+
|
|
74
|
+
⇒ 两态下委托代理的方言确实不同(指令 context 出现/消失即 gate 的可观测面),主代理两态都保留。
|
|
75
|
+
**平台说明**:macOS 上方言/执行器面本来被 `process.platform === 'win32'` 门控,本次把**插件副本**的两处组装门(`system-prompt/assemble` 与指令 context)强制打开以取得请求级证据(报告中如实标注);**win32 真机仍未覆盖**(执行器是单例,与方言开关无关,不受本版影响)。
|
|
76
|
+
- 设置卡:方言区新增一行(21 语言 `sub.label`/`sub.hint`),写入本行 `subagentDialect`;文案明确"二进制仍全局、此开关管方言/翻译层"。
|
|
77
|
+
|
|
78
|
+
- 相关:用户需求(2026-09-25 追加);实现不变量见 AGENTS.md §4g,验证装置见「验证清单」第 8 条。
|
|
79
|
+
|
|
6
80
|
## v0.26.1 — 2026-09-25
|
|
7
81
|
|
|
8
82
|
**类型**:fix(v0.26.0 的 workflow 互斥按「意图」而非「生效态」收敛:后端不可用时用户会白丢 workflow 能力)
|
package/README.md
CHANGED
|
@@ -101,6 +101,22 @@ dsh-ptc-cordis-preset 报告它的 `PTC 创造模式` 已经是 Git Bash 版。
|
|
|
101
101
|
0.24.x 的四个变体逐字不变;判定细节、生效时机与两侧同步方式见下方
|
|
102
102
|
[「与 dsh-ptc-cordis-preset 联动」](#与-dsh-ptc-cordis-preset-联动)。
|
|
103
103
|
|
|
104
|
+
### 子代理/队员是否也用 Git Bash:`subagentDialect`(默认开,v0.27.0)
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
- id: gitbash-shell
|
|
108
|
+
config:
|
|
109
|
+
subagentDialect: true # 默认;false = 方言只对主代理生效
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
默认开启时,子代理、团队队员(含嵌套子代理)与主代理走**同一条**方言链路:提示词里的路径同样是 `/c/...` 形式、
|
|
113
|
+
路径参数同样被翻译、结果与报错同样回显为 MSYS 形式、`run_code` 程序里的路径字面量同样被翻译,`DSH_PATH_DIALECT` 环境事实也照常下发。
|
|
114
|
+
关闭后这些**只对主代理生效**:委托出去的请求保持官方 shell 语义(提示词不改写、路径参数原样传递),适合"子代理要跑原生 Windows 工具链"的场景。
|
|
115
|
+
|
|
116
|
+
> 能力边界(如实说明):dsh **每个进程只有一个 shell 执行器**(`ctx.shell` 是单例服务),所以 Git Bash **二进制本身仍是全局的**,
|
|
117
|
+
> 这个开关管的是**方言/翻译层**,不是"换一个 shell"。关闭后委托代理见到与写出的是 Windows 形式路径(`C:/...`),Git Bash 同样接受,行为自洽。
|
|
118
|
+
> 委托身份的判据用会话头的 `origin === 'subagent'` / `delegationDepth > 0`(与 dsh 自身一致),判定不出来时按主代理处理(保留方言)。
|
|
119
|
+
|
|
104
120
|
配合 `agent-presets` 的 `default`,新会话直接落在 Git Bash 模式,免去每次在
|
|
105
121
|
模式选择器里翻找(原版 shipped 模式无法替换或隐藏——部署级、只读):
|
|
106
122
|
|
|
@@ -110,6 +126,27 @@ dsh-ptc-cordis-preset 报告它的 `PTC 创造模式` 已经是 Git Bash 版。
|
|
|
110
126
|
default: ptc-cordis # 或 standard-gitbash / minimal-gitbash
|
|
111
127
|
```
|
|
112
128
|
|
|
129
|
+
## bash 从哪里来:解析链与失败引导(v0.28.0,issue #11)
|
|
130
|
+
|
|
131
|
+
`bashPath` **默认留空 = 自动探测**;解析顺序就是契约:
|
|
132
|
+
|
|
133
|
+
1. **设置里填的**(显式值优先,失败也不会被别的路径顶替):
|
|
134
|
+
`gitbash-executor` 行 config 的 `bashPath` > `gitbash-shell` 行 / 设置卡的「Git Bash 路径」;
|
|
135
|
+
2. **默认安装位置**:`C:/Program Files/Git/bin/bash.exe`、`%ProgramFiles(x86)%\Git`、`%ProgramW6432%\Git`、
|
|
136
|
+
`%LOCALAPPDATA%\Programs\Git\bin\bash.exe`;
|
|
137
|
+
3. **PATH**(用户 PATH + 系统 PATH,进程内已合并)逐目录找 `bash.exe`;
|
|
138
|
+
4. **PATH 上的 `git.exe` 反推** `<gitdir>/../bin/bash.exe`;
|
|
139
|
+
5. **注册表 `Path`**(`HKCU\Environment`、`HKLM\...\Session Manager\Environment`)——覆盖"GUI 启动时 PATH 快照过期";
|
|
140
|
+
6. **全落空 ⇒ 报错 + 引导**(启动日志一份完整报告,客户端弹一次引导框)。
|
|
141
|
+
|
|
142
|
+
> **只认 Git for Windows 的 bash。** WSL(`C:\Windows\System32\bash.exe`,永远在 PATH 且排序靠前)、
|
|
143
|
+
> WindowsApps 别名、MSYS2、Cygwin 的 bash **一律拒绝**;候选还要通过 Git 布局(`usr/bin` + `cmd\git.exe` +
|
|
144
|
+
> `mingw64`)、`git --version` 的 `.windows.` 指纹,以及**实跑 `bash -c "uname -s"` 必须是 `MINGW*_NT-*`**。
|
|
145
|
+
>
|
|
146
|
+
> **本插件不会回退到 PowerShell / cmd / 任何其它 shell。** 找不到 Git Bash 就是找不到:设计如此 ——
|
|
147
|
+
> 装了本插件就是要用 Git Bash,偷偷换一个能跑的顶上是错的。失败时弹窗给两件事:在弹窗里**直接填 bashPath**
|
|
148
|
+
> (写入设置卡同一字段)或**去下载 Git for Windows**;同一 boot 只弹一次,可关闭。
|
|
149
|
+
|
|
113
150
|
## 配置(执行器)
|
|
114
151
|
|
|
115
152
|
`gitbash-executor` 行支持:
|
package/cordis.patch.yml
CHANGED
|
@@ -32,7 +32,11 @@
|
|
|
32
32
|
disabled: !!js process.platform !== 'win32'
|
|
33
33
|
config:
|
|
34
34
|
timeoutMs: 60000
|
|
35
|
-
|
|
35
|
+
# EMPTY = AUTO (v0.28.0, issue #11): the shared resolver walks
|
|
36
|
+
# setting > default install paths > PATH > registry PATH, and accepts
|
|
37
|
+
# ONLY Git for Windows' bash. A hard-coded C: path here made a Git
|
|
38
|
+
# installed elsewhere (e.g. Q:/Git) look like "dsh is broken".
|
|
39
|
+
bashPath: ""
|
|
36
40
|
|
|
37
41
|
# Row id 'gitbash-shell' (was 'gitbash-presets' through v0.23.0): dsh >= 0.1.7
|
|
38
42
|
# keys a plugin's live Config form and the one-shot legacy settings.yaml
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-gitbash-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "Windows 全模式 Git Bash:用 Git for Windows bash 替换 PowerShell 执行器,并物化 standard/minimal/code/cordis 的 Git Bash 变体 preset。双 era 适配 dsh <= 0.1.1(内置 id 'code')与 >= 0.1.2(改名 'ptc'),启动探测自动选择,preset id 永不变。联动:dsh-ptc-cordis-preset(两者同装时 PTC 创造模式自动物化为 Git Bash 版)+ dsh-better-sidebar(终端 UI 标签与模型 terminal_* 工具自动切到 Git Bash)。",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gitbash-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Windows 上让 agent 的每条 shell 命令改经 Git for Windows bash 执行;物化四个内置预设的 Git Bash 变体,统一 MSYS 盘根路径方言,侧栏终端接管可开关。· Run every agent shell command through Git for Windows bash on Windows; materialize Git Bash variants of the built-in presets, unify the MSYS drive-root path dialect, and adopt the sidebar terminal behind a switch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/bash-path.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-gitbash-shell — Git for Windows bash RESOLUTION (issue #11).
|
|
3
|
+
*
|
|
4
|
+
* One resolver, used by BOTH the executor (src/shell.js) and the path-dialect
|
|
5
|
+
* translation layer (src/index.js). Before v0.28.0 the translation layer
|
|
6
|
+
* probed PATH on its own while the executor kept whatever `bashPath` the patch
|
|
7
|
+
* hard-coded, so a Git installed outside `C:/Program Files/Git` produced
|
|
8
|
+
* "translation works, the shell does not exist" — the split this module ends.
|
|
9
|
+
*
|
|
10
|
+
* THE TWO HARD RULES (user, 2026-09-25; see AGENTS.md §4h):
|
|
11
|
+
* 1. Never substitute another shell. No pwsh, no cmd, no WSL bash, no
|
|
12
|
+
* "temporarily keep something runnable": an unresolved bash is a FAILURE
|
|
13
|
+
* with guidance, never a silent downgrade.
|
|
14
|
+
* 2. Only Git for Windows' own bash counts. WSL's starter
|
|
15
|
+
* (`C:\Windows\System32\bash.exe`, always on PATH), the WindowsApps
|
|
16
|
+
* alias, MSYS2's and Cygwin's bash are REJECTED — they speak a different
|
|
17
|
+
* path world, so using one is worse than finding none.
|
|
18
|
+
*
|
|
19
|
+
* The resolver is deliberately injectable: every filesystem/process fact
|
|
20
|
+
* arrives through `io`, so the whole chain is unit-testable off-Windows.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { execFileSync } from 'node:child_process'
|
|
24
|
+
import { existsSync } from 'node:fs'
|
|
25
|
+
import { dirname } from 'node:path'
|
|
26
|
+
|
|
27
|
+
/** The historical default — still the FIRST default-path candidate. */
|
|
28
|
+
export const DEFAULT_GIT_BASH = 'C:/Program Files/Git/bin/bash.exe'
|
|
29
|
+
|
|
30
|
+
/** Where a user without Git for Windows should go. */
|
|
31
|
+
export const GIT_BASH_DOWNLOAD_URL = 'https://git-scm.com/download/win'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Path fragments (on the NORMALIZED path) that disqualify a bash outright.
|
|
35
|
+
* `system32` and `windowsapps` are WSL's two delivery points; `msys`/`cygwin`
|
|
36
|
+
* are the sibling Unix emulations; `wsl` catches distro-installed shims.
|
|
37
|
+
*/
|
|
38
|
+
export const REJECTED_BASH_MARKERS = ['system32', 'windowsapps', 'msys', 'cygwin', 'wsl']
|
|
39
|
+
|
|
40
|
+
/** `git version 2.54.0.windows.1` — only Git for Windows prints this. */
|
|
41
|
+
export const GIT_WINDOWS_VERSION_MARKER = '.windows.'
|
|
42
|
+
|
|
43
|
+
/** The final veto: what `bash -c 'uname -s'` must print on Git for Windows. */
|
|
44
|
+
export const GIT_BASH_UNAME = /^MINGW(32|64)_NT-/
|
|
45
|
+
|
|
46
|
+
/** Exit codes/sources reported back to the caller. */
|
|
47
|
+
export const BASH_SOURCES = ['configured', 'default', 'path', 'path-git', 'registry']
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Normalize a Windows path for comparison: forward slashes, lowercase, no
|
|
51
|
+
* duplicate separators, no trailing separator. Short (8.3) names cannot be
|
|
52
|
+
* expanded without the filesystem, so callers on Windows pass the resolved
|
|
53
|
+
* path when they have one.
|
|
54
|
+
* @param {unknown} value - candidate path.
|
|
55
|
+
* @returns {string} the normalized form ('' for non-strings).
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeBashPath(value) {
|
|
58
|
+
if (typeof value !== 'string') return ''
|
|
59
|
+
return value.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/+$/, '').toLowerCase()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Does this path point at something that is NOT Git for Windows' bash? The
|
|
64
|
+
* blacklist runs BEFORE any filesystem probe, so a WSL starter on PATH is
|
|
65
|
+
* never even considered a candidate (issue #11's head trap: System32 is
|
|
66
|
+
* always on PATH and sorts first).
|
|
67
|
+
* @param {unknown} candidate - candidate path.
|
|
68
|
+
* @returns {string|undefined} the matched marker, or undefined when clean.
|
|
69
|
+
*/
|
|
70
|
+
export function rejectedBashMarker(candidate) {
|
|
71
|
+
const path = normalizeBashPath(candidate)
|
|
72
|
+
if (path === '') return undefined
|
|
73
|
+
const segments = path.split('/')
|
|
74
|
+
for (const marker of REJECTED_BASH_MARKERS) {
|
|
75
|
+
// Whole-segment matches only: "C:/tools/msys2/usr/bin/bash.exe" is caught by
|
|
76
|
+
// the `msys2` segment? No — by `msys` as a substring of that segment, which
|
|
77
|
+
// is exactly what we want (msys2, msys64, msys32). `system32`/`windowsapps`
|
|
78
|
+
// are full segments in practice; match the segment both ways.
|
|
79
|
+
for (const segment of segments) {
|
|
80
|
+
if (segment === marker || segment.includes(marker)) return marker
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The Git root a bash path implies (`<root>/bin/bash.exe` → `<root>`), or ''
|
|
88
|
+
* when the shape is wrong.
|
|
89
|
+
* @param {unknown} bashPath - candidate bash path.
|
|
90
|
+
* @returns {string} the normalized root, or ''.
|
|
91
|
+
*/
|
|
92
|
+
export function gitRootOfBash(bashPath) {
|
|
93
|
+
const path = normalizeBashPath(bashPath)
|
|
94
|
+
const parts = path.split('/')
|
|
95
|
+
if (parts.length < 2 || parts[parts.length - 1] !== 'bash.exe') return ''
|
|
96
|
+
return parts.slice(0, -2).join('/')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate one candidate as Git for Windows' bash. Every step is a hard
|
|
101
|
+
* requirement; the first failure decides the rejection code.
|
|
102
|
+
* @param {string} candidate - absolute candidate path.
|
|
103
|
+
* @param {object} io - injected facts (see {@link defaultIo}).
|
|
104
|
+
* @returns {{ok: true, path: string, root: string, gitExe: string}
|
|
105
|
+
* | {ok: false, code: string, detail: string}} the verdict.
|
|
106
|
+
*/
|
|
107
|
+
export function classifyBashCandidate(candidate, io) {
|
|
108
|
+
const marker = rejectedBashMarker(candidate)
|
|
109
|
+
if (marker !== undefined) {
|
|
110
|
+
return { ok: false, code: 'rejected-shell', detail: 'path matches the "' + marker + '" exclusion (WSL/MSYS2/Cygwin are not Git Bash)' }
|
|
111
|
+
}
|
|
112
|
+
const root = gitRootOfBash(candidate)
|
|
113
|
+
if (root === '') return { ok: false, code: 'bad-shape', detail: 'not a <git-root>/bin/bash.exe shape' }
|
|
114
|
+
if (!io.exists(candidate)) return { ok: false, code: 'missing', detail: 'file does not exist' }
|
|
115
|
+
// Git for Windows layout: a git binary beside it AND the MSYS runtime.
|
|
116
|
+
const gitExe = [root + '/cmd/git.exe', root + '/bin/git.exe'].find((path) => io.exists(path))
|
|
117
|
+
if (gitExe === undefined) return { ok: false, code: 'no-git-exe', detail: 'no cmd/git.exe or bin/git.exe under ' + root }
|
|
118
|
+
if (!io.exists(root + '/usr/bin/bash.exe')) return { ok: false, code: 'no-usr-bin', detail: 'no usr/bin/bash.exe under ' + root }
|
|
119
|
+
if (!(io.exists(root + '/mingw64') || io.exists(root + '/usr/bin/msys-2.0.dll'))) {
|
|
120
|
+
return { ok: false, code: 'no-mingw', detail: 'no mingw64/ and no usr/bin/msys-2.0.dll under ' + root }
|
|
121
|
+
}
|
|
122
|
+
// Version fingerprint: only Git for Windows prints `.windows.`.
|
|
123
|
+
const version = io.gitVersion(gitExe)
|
|
124
|
+
if (typeof version !== 'string' || !version.includes(GIT_WINDOWS_VERSION_MARKER)) {
|
|
125
|
+
return { ok: false, code: 'not-windows-git', detail: (version === undefined ? 'git --version failed' : 'git --version = "' + version + '"') + ' (no "' + GIT_WINDOWS_VERSION_MARKER + '" marker)' }
|
|
126
|
+
}
|
|
127
|
+
// The final veto: run the candidate itself.
|
|
128
|
+
const uname = io.uname(candidate)
|
|
129
|
+
if (typeof uname !== 'string' || !GIT_BASH_UNAME.test(uname.trim())) {
|
|
130
|
+
return { ok: false, code: 'not-git-bash', detail: "bash -c 'uname -s' = " + (uname === undefined ? 'failed' : JSON.stringify(uname)) + ' (not MINGW*_NT-*)' }
|
|
131
|
+
}
|
|
132
|
+
return { ok: true, path: candidate, root, gitExe }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The ordered candidate chain for one resolution, WITHOUT validation. Order is
|
|
137
|
+
* the contract (issue #11): explicit setting → default install locations →
|
|
138
|
+
* PATH → git.exe reverse lookup → registry PATH (the last one only helps when
|
|
139
|
+
* the process PATH snapshot predates a user's PATH edit).
|
|
140
|
+
* @param {object} input - resolution input.
|
|
141
|
+
* @param {string} input.configured - explicit path ('' = none).
|
|
142
|
+
* @param {object} io - injected facts.
|
|
143
|
+
* @returns {{path: string, source: string}[]} the ordered candidates.
|
|
144
|
+
*/
|
|
145
|
+
export function bashCandidates({ configured }, io) {
|
|
146
|
+
const out = []
|
|
147
|
+
const explicit = typeof configured === 'string' ? configured.trim() : ''
|
|
148
|
+
if (explicit !== '') {
|
|
149
|
+
out.push({ path: explicit, source: 'configured' })
|
|
150
|
+
return out // an explicit setting is the user's answer; never substitute it
|
|
151
|
+
}
|
|
152
|
+
for (const path of io.defaults()) out.push({ path, source: 'default' })
|
|
153
|
+
for (const path of io.pathBashCandidates()) out.push({ path, source: 'path' })
|
|
154
|
+
for (const path of io.gitReverseCandidates()) out.push({ path, source: 'path-git' })
|
|
155
|
+
for (const path of io.registryBashCandidates()) out.push({ path, source: 'registry' })
|
|
156
|
+
return out
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the Git for Windows bash to run. Pure w.r.t. every fact it needs —
|
|
161
|
+
* all of it arrives through `io`, which is why the whole chain (including the
|
|
162
|
+
* Windows-only registry step) is unit-testable anywhere.
|
|
163
|
+
* @param {object} [options] - resolution options.
|
|
164
|
+
* @param {string} [options.configured] - explicit `bashPath` from config.
|
|
165
|
+
* @param {object} [options.io] - injected facts; defaults to the real ones.
|
|
166
|
+
* @returns {{ok: boolean, path: string, root: string, gitExe: string, source: string,
|
|
167
|
+
* tried: {path: string, source: string, code: string, detail: string}[],
|
|
168
|
+
* configured: string}} the verdict, with the full probe list for guidance.
|
|
169
|
+
*/
|
|
170
|
+
export function resolveGitBash(options = {}) {
|
|
171
|
+
const io = options.io ?? defaultIo()
|
|
172
|
+
const configured = typeof options.configured === 'string' ? options.configured.trim() : ''
|
|
173
|
+
const tried = []
|
|
174
|
+
const rejected = []
|
|
175
|
+
for (const candidate of bashCandidates({ configured }, io)) {
|
|
176
|
+
let verdict
|
|
177
|
+
try {
|
|
178
|
+
verdict = classifyBashCandidate(candidate.path, io)
|
|
179
|
+
} catch (error) {
|
|
180
|
+
verdict = { ok: false, code: 'probe-failed', detail: String(error && error.message ? error.message : error) }
|
|
181
|
+
}
|
|
182
|
+
if (verdict.ok) {
|
|
183
|
+
return { ok: true, path: verdict.path, root: verdict.root, gitExe: verdict.gitExe, source: candidate.source, tried, rejected, configured }
|
|
184
|
+
}
|
|
185
|
+
tried.push({ path: candidate.path, source: candidate.source, code: verdict.code, detail: verdict.detail })
|
|
186
|
+
rejected.push(verdict.code)
|
|
187
|
+
}
|
|
188
|
+
return { ok: false, path: '', root: '', gitExe: '', source: '', tried, rejected, configured }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The fail-loud report for one resolution — the text a user reads in the host
|
|
193
|
+
* log when Git Bash could not be resolved. It states the design rather than
|
|
194
|
+
* blaming the environment: this plugin will NOT fall back to another shell.
|
|
195
|
+
* @param {object} resolution - a {@link resolveGitBash} result.
|
|
196
|
+
* @returns {string} the multi-line report.
|
|
197
|
+
*/
|
|
198
|
+
export function bashResolutionReport(resolution) {
|
|
199
|
+
const lines = []
|
|
200
|
+
if (resolution.ok) {
|
|
201
|
+
lines.push('Git Bash resolved from the ' + resolution.source + ' source: ' + resolution.path)
|
|
202
|
+
if (resolution.tried.length > 0) {
|
|
203
|
+
lines.push(' (probed first, rejected: ' + resolution.tried.map((row) => row.path + ' [' + row.code + ']').join(', ') + ')')
|
|
204
|
+
}
|
|
205
|
+
return lines.join('\n')
|
|
206
|
+
}
|
|
207
|
+
lines.push('Git Bash could NOT be resolved — the shell is unavailable by design, not broken: this plugin never falls back to PowerShell, cmd or a WSL/MSYS2/Cygwin bash.')
|
|
208
|
+
lines.push(resolution.configured === ''
|
|
209
|
+
? ' bashPath is empty, so the automatic chain ran (setting > default install paths > PATH > registry PATH).'
|
|
210
|
+
: ' bashPath is set to "' + resolution.configured + '" and that value was rejected; nothing else is substituted.')
|
|
211
|
+
if (resolution.tried.length === 0) lines.push(' probed: (nothing — no candidate was produced)')
|
|
212
|
+
for (const row of resolution.tried) lines.push(' probed [' + row.source + '] ' + row.path + ' -> ' + row.code + ' (' + row.detail + ')')
|
|
213
|
+
lines.push(' fix: set the "Git Bash path" field in this plugin\'s settings, or install Git for Windows: ' + GIT_BASH_DOWNLOAD_URL)
|
|
214
|
+
lines.push(' note: only Git for Windows\' own bash is accepted; WSL (/Windows/System32/bash.exe), the WindowsApps alias, MSYS2 and Cygwin are refused on purpose.')
|
|
215
|
+
return lines.join('\n')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The explicit `bashPath` a SETTINGS row carries, read the era-aware way (the
|
|
221
|
+
* exact double shape src/index.js uses for better-sidebar's namespace): on
|
|
222
|
+
* dsh <= 0.1.6 through `settings.get(ns).bashPath`, on >= 0.1.7 through
|
|
223
|
+
* `settings.describe()`'s own row. Never throws; a missing service reads ''.
|
|
224
|
+
* @param {object} ctx - a context exposing `settings`.
|
|
225
|
+
* @param {string} [ns] - the settings namespace/row id (default this plugin's).
|
|
226
|
+
* @returns {string} the configured path, '' when unset or unreadable.
|
|
227
|
+
*/
|
|
228
|
+
export function settingsBashPath(ctx, ns = 'gitbash-shell') {
|
|
229
|
+
try {
|
|
230
|
+
const settings = ctx && typeof ctx.get === 'function' ? ctx.get('settings') : undefined
|
|
231
|
+
if (!settings) return ''
|
|
232
|
+
if (typeof settings.get === 'function') {
|
|
233
|
+
const value = settings.get(ns)
|
|
234
|
+
return value && typeof value === 'object' && typeof value.bashPath === 'string' ? value.bashPath : ''
|
|
235
|
+
}
|
|
236
|
+
if (typeof settings.describe === 'function') {
|
|
237
|
+
const entry = settings.describe().find((row) => row !== null && typeof row === 'object' && row.ns === ns)
|
|
238
|
+
const value = entry === undefined ? undefined : entry.value
|
|
239
|
+
return value && typeof value === 'object' && typeof value.bashPath === 'string' ? value.bashPath : ''
|
|
240
|
+
}
|
|
241
|
+
return ''
|
|
242
|
+
} catch {
|
|
243
|
+
return ''
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Merge the explicit tiers of the chain: the row's OWN config wins over the
|
|
249
|
+
* settings row, which wins over empty (which means "run the automatic chain").
|
|
250
|
+
* The order is the contract (issue #11): an explicit answer is never
|
|
251
|
+
* substituted, and an empty one is not an answer at all.
|
|
252
|
+
* @param {unknown} ownRowValue - `gitbash-executor` row config's `bashPath`.
|
|
253
|
+
* @param {unknown} settingsValue - the `gitbash-shell` row/settings `bashPath`.
|
|
254
|
+
* @returns {string} the explicit path to resolve with.
|
|
255
|
+
*/
|
|
256
|
+
export function effectiveConfiguredBashPath(ownRowValue, settingsValue) {
|
|
257
|
+
const own = typeof ownRowValue === 'string' ? ownRowValue.trim() : ''
|
|
258
|
+
if (own !== '') return own
|
|
259
|
+
return typeof settingsValue === 'string' ? settingsValue.trim() : ''
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The `gitbash-executor` ROW's own `bashPath` — the highest-priority tier.
|
|
265
|
+
* Read through the Loader's public `resolve(id)` (the row id is part of this
|
|
266
|
+
* plugin's own bundle patch), so the translation layer sees the same explicit
|
|
267
|
+
* value the executor row carries instead of quietly resolving a different
|
|
268
|
+
* one. Absent row / absent config / any loader shape change reads ''.
|
|
269
|
+
* @param {object} ctx - a context exposing `loader`.
|
|
270
|
+
* @returns {string} the row's configured path, '' when unset.
|
|
271
|
+
*/
|
|
272
|
+
export function executorConfiguredBashPath(ctx) {
|
|
273
|
+
try {
|
|
274
|
+
const entry = ctx && ctx.loader && typeof ctx.loader.resolve === 'function'
|
|
275
|
+
? ctx.loader.resolve('gitbash-executor')
|
|
276
|
+
: undefined
|
|
277
|
+
const value = entry && entry.options ? entry.options.config : undefined
|
|
278
|
+
return value && typeof value.bashPath === 'string' ? value.bashPath : ''
|
|
279
|
+
} catch {
|
|
280
|
+
return ''
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The real `io`: filesystem probes, the Windows registry PATH read (argv
|
|
286
|
+
* arrays, never a shell string) and the two subprocess fingerprints.
|
|
287
|
+
* Every call is defensive — a probe that throws is a candidate that fails,
|
|
288
|
+
* never a crash.
|
|
289
|
+
* @returns {object} the production io.
|
|
290
|
+
*/
|
|
291
|
+
export function defaultIo() {
|
|
292
|
+
const run = (file, args) => {
|
|
293
|
+
try {
|
|
294
|
+
return String(execFileSync(file, args, { encoding: 'utf8', timeout: 5000, windowsHide: true })).trim()
|
|
295
|
+
} catch {
|
|
296
|
+
return undefined
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const registryPath = (hive, key) => {
|
|
300
|
+
const out = run('reg', ['query', hive + '\\' + key, '/v', 'Path'])
|
|
301
|
+
if (typeof out !== 'string') return []
|
|
302
|
+
const match = /Path\s+REG_(?:EXPAND_)?SZ\s+(.*)/i.exec(out)
|
|
303
|
+
if (match === null) return []
|
|
304
|
+
return match[1].split(';').map((entry) => entry.trim()).filter((entry) => entry !== '')
|
|
305
|
+
}
|
|
306
|
+
const dirs = () => {
|
|
307
|
+
const out = []
|
|
308
|
+
for (const key of ['ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432']) {
|
|
309
|
+
const base = process.env[key]
|
|
310
|
+
if (typeof base === 'string' && base.trim() !== '') out.push(base.replace(/\\/g, '/') + '/Git/bin/bash.exe')
|
|
311
|
+
}
|
|
312
|
+
const local = process.env.LOCALAPPDATA
|
|
313
|
+
if (typeof local === 'string' && local.trim() !== '') out.push(local.replace(/\\/g, '/') + '/Programs/Git/bin/bash.exe')
|
|
314
|
+
return out
|
|
315
|
+
}
|
|
316
|
+
const pathDirs = () => String(process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').map((entry) => entry.trim()).filter((entry) => entry !== '')
|
|
317
|
+
return {
|
|
318
|
+
exists: (path) => {
|
|
319
|
+
try { return existsSync(path) } catch { return false }
|
|
320
|
+
},
|
|
321
|
+
defaults: () => [DEFAULT_GIT_BASH, ...dirs()],
|
|
322
|
+
pathBashCandidates: () => pathDirs().map((dir) => dir.replace(/\\/g, '/') + '/bash.exe'),
|
|
323
|
+
gitReverseCandidates: () => pathDirs()
|
|
324
|
+
.filter((dir) => /(^|[/\\])cmd$/i.test(dir) || /(^|[/\\])bin$/i.test(dir))
|
|
325
|
+
.flatMap((dir) => {
|
|
326
|
+
const parent = dirname(dir.replace(/\\/g, '/'))
|
|
327
|
+
return [parent + '/bin/bash.exe', dirname(parent) + '/bin/bash.exe']
|
|
328
|
+
}),
|
|
329
|
+
registryBashCandidates: () => [
|
|
330
|
+
...registryPath('HKCU', 'Environment'),
|
|
331
|
+
...registryPath('HKLM', 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment'),
|
|
332
|
+
].flatMap((entry) => {
|
|
333
|
+
const dir = entry.replace(/%([^%]+)%/g, (whole, name) => {
|
|
334
|
+
const value = process.env[name] ?? process.env[name.toUpperCase()]
|
|
335
|
+
return typeof value === 'string' ? value : whole
|
|
336
|
+
})
|
|
337
|
+
return /bash\.exe$/i.test(dir) ? [dir] : [dir.replace(/\\/g, '/') + '/bash.exe']
|
|
338
|
+
}),
|
|
339
|
+
gitVersion: (gitExe) => run(gitExe, ['--version']),
|
|
340
|
+
uname: (bash) => run(bash, ['-c', 'uname -s']),
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Process-wide memo of the resolution. The translation layer and the executor
|
|
346
|
+
* MUST agree — a split ("translation resolved Q:/Git, the executor still looks
|
|
347
|
+
* at C:/...") is exactly issue #11 — so both read this one cache. A failed
|
|
348
|
+
* resolution is cached too: re-probing on every command would spawn `reg` and
|
|
349
|
+
* `bash` repeatedly, and the diagnosed failure is stable within a boot.
|
|
350
|
+
* @param {object} [options] - as {@link resolveGitBash}, plus `fresh` to bypass.
|
|
351
|
+
* @returns {object} the memoized resolution.
|
|
352
|
+
*/
|
|
353
|
+
const memoized = new Map()
|
|
354
|
+
|
|
355
|
+
export function resolveGitBashCached(options = {}) {
|
|
356
|
+
const key = (typeof options.configured === 'string' ? options.configured.trim() : '')
|
|
357
|
+
+ '\u0000' + (options.platform ?? process.platform)
|
|
358
|
+
if (options.fresh === true || !memoized.has(key)) {
|
|
359
|
+
memoized.set(key, resolveGitBash(options))
|
|
360
|
+
}
|
|
361
|
+
return memoized.get(key)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Drop the memo. Keyed by the configured value already, so a settings edit
|
|
366
|
+
* resolves afresh without this; the reset exists for tests and for a caller
|
|
367
|
+
* that knows the environment changed (e.g. a fresh registry read).
|
|
368
|
+
*/
|
|
369
|
+
export function resetGitBashMemo() {
|
|
370
|
+
memoized.clear()
|
|
371
|
+
}
|