dsh-gitbar-lite 0.2.1 → 0.4.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/README.en.md +34 -2
- package/README.md +27 -2
- package/docs/CONTRACT.md +1 -1
- package/lib/client.js +163 -18
- package/lib/host/git.js +33 -7
- package/lib/host/repo.js +144 -3
- package/lib/host/routes.js +18 -1
- package/lib/index.js +10 -0
- package/lib/shared.js +9 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -23,7 +23,9 @@ The column is narrow (300px minimum, and a narrow viewport makes it full-screen)
|
|
|
23
23
|
| Commit | Commits exactly what is ticked — because ticking wrote the index |
|
|
24
24
|
| Commit and Push | Commits, then pushes. **A failed push never claims the commit failed**: the commit happened, and the panel says "committed `abc1234`, but the push failed" |
|
|
25
25
|
| Context button | **Push** when local commits are unpushed, **Publish** when the branch has no upstream, **Fetch** otherwise |
|
|
26
|
-
|
|
|
26
|
+
| Fetch before push | A push runs `git fetch --prune` first, then pushes. **Being behind refuses the push** and says by how many — git never merges on a push, so it would be rejected either way, and knowing before the attempt beats being told after. A failed fetch does not block the push |
|
|
27
|
+
| Automatic fetch | GitHub Desktop's rhythm while the panel is visible and the repository has a remote: an hour by default, each tick re-asking whether to run, the next scheduled from the *completion* of the last, with a random skew so panels do not sync up. The pace is `autoFetchMs` (`0` to disable), and "was it fetched recently" is answered by `FETCH_HEAD`'s mtime — so a fetch in a terminal counts too |
|
|
28
|
+
| Panel regains focus | The file list refreshes when the panel becomes visible again; if a script is configured, it runs and fills the Summary |
|
|
27
29
|
| Discard | One file or all of them; the diff is archived *before* anything is restored, and the confirmation offers "delete for good" |
|
|
28
30
|
| Trash | Its own tab. Every discard keeps a diff, recorded **against the commit it was taken from** (`abc1234 initial commit`) |
|
|
29
31
|
| Diff preview | Double-click a file, or a trash entry, to open a diff tab in the column — **one tab, ever**, re-navigated in place |
|
|
@@ -127,6 +129,35 @@ The order is fixed and cannot be swapped:
|
|
|
127
129
|
> **A conflicted file is refused**, with one sentence. Running `git restore` to HEAD in the middle of a merge can
|
|
128
130
|
> disturb the merge state, and that risk is not worth saving a sentence.
|
|
129
131
|
|
|
132
|
+
### Filling the Summary from a script
|
|
133
|
+
|
|
134
|
+
Next to the Summary field is a sparkle button — rendered only when a script is configured — that fills the Summary from
|
|
135
|
+
the script's output. It also runs by itself when the panel regains focus: leave the sidebar open, go and let the agent
|
|
136
|
+
work in the conversation, come back, and the Summary is already there.
|
|
137
|
+
|
|
138
|
+
The script goes in the `summaryScript` config field. It runs with the **repository root as its working directory** and
|
|
139
|
+
is fed nothing on stdin, so "read the top version out of the CHANGELOG" is:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
grep -m1 '^## ' CHANGELOG.md | sed 's/^## //'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Whatever it prints becomes the Summary. To fill the Description as well, put a line reading `---` in the output:
|
|
146
|
+
everything after it is the Description.
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
echo "1.4.2"
|
|
150
|
+
echo "---"
|
|
151
|
+
echo "Bumped the patch version."
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
About the shell: on Windows the script prefers Git's own `sh` (so `grep`/`sed`/`awk` work) and puts its `usr/bin` back on
|
|
155
|
+
`PATH` — without that those tools report "command not found". Only a machine with no `sh` at all falls back to
|
|
156
|
+
`cmd.exe`, where the script must be written in cmd syntax. Elsewhere it is plain `sh`.
|
|
157
|
+
|
|
158
|
+
A failing script does not raise an error banner; its exit code shows as a brief notice, and its stderr is kept verbatim so
|
|
159
|
+
you can judge for yourself.
|
|
160
|
+
|
|
130
161
|
## Configuration
|
|
131
162
|
|
|
132
163
|
| field | default | |
|
|
@@ -139,6 +170,7 @@ The order is fixed and cannot be swapped:
|
|
|
139
170
|
| `trashLimit` | `200` | Trash entries kept; the oldest are dropped along with their diff files |
|
|
140
171
|
| `scanDepth` | `3` | Directory depth the picker scans below the workspace root |
|
|
141
172
|
| `autoFetchMs` | `300000` | How often the panel fetches on its own; `0` disables it, leaving the manual button |
|
|
173
|
+
| `summaryScript` | `""` | A script that fills the Summary. It runs with the repository root as its working directory and whatever it prints becomes the Summary; a line reading `---` starts the Description |
|
|
142
174
|
|
|
143
175
|
Every field is volatile, so a change on the settings page takes effect without a restart.
|
|
144
176
|
|
|
@@ -192,7 +224,7 @@ are in [`docs/CONTRACT.md`](./docs/CONTRACT.md).
|
|
|
192
224
|
|
|
193
225
|
```sh
|
|
194
226
|
npm test # unit + real-git e2e + artifact self-check
|
|
195
|
-
npm run test:e2e # the real-git suite alone (
|
|
227
|
+
npm run test:e2e # the real-git suite alone (22 sections building their own fixtures, 277 assertions)
|
|
196
228
|
npm run check # artifact self-check alone (catches boot-breaking mistakes without restarting DSH)
|
|
197
229
|
npm run test:live # probe a running DSH (not part of npm test; needs the app running)
|
|
198
230
|
```
|
package/README.md
CHANGED
|
@@ -23,7 +23,9 @@ DSH 的右栏本来就是一个常驻的标签区。Git 面板放这里,意味
|
|
|
23
23
|
| Commit | 只提交勾上的(因为勾选写进了索引) |
|
|
24
24
|
| Commit and Push | 提交后立刻推送;**推送失败不会说「提交失败」**——提交已经发生了,面板会明说「已提交 xxx,但推送失败」 |
|
|
25
25
|
| 上下文按钮 | 有本地未推送提交时是 **Push**,没有上游时是 **Publish**,否则是 **Fetch** |
|
|
26
|
-
|
|
|
26
|
+
| Push 前先 Fetch | 推送前会先 `git fetch --prune`,然后才推。**已经落后就拒绝推**并说明落后几个——git 的 push 从不合并,早晚都是被拒,先知道比推完再被拒有用。fetch 本身失败不阻断推送 |
|
|
27
|
+
| 自动 Fetch | 面板可见且仓库有远端时按 GitHub Desktop 的口径自己抓:默认 1 小时,每次到点重新判断,下一次从**上一次完成后**开始排,并加随机抖动防止多个面板同步。节奏是 `autoFetchMs`(`0` 关掉),并且用 `FETCH_HEAD` 的 mtime 判断"是不是刚抓过"——你在终端里抓也算 |
|
|
28
|
+
| 面板获得焦点 | 面板重新可见时刷新文件列表;配了脚本就顺带跑一次填 Summary |
|
|
27
29
|
| 舍弃更改 | 单个文件或「全部舍弃」;动手前先把这次改动的 diff 归档,确认框里可以勾「彻底删除」 |
|
|
28
30
|
| 回收站 | 独立 tab。每次舍弃存一份 diff,并记下**是对哪个提交(`abc1234 初始提交`)取的 diff** |
|
|
29
31
|
| diff 预览 | 双击文件,或点回收站里的一条,在右栏开一个 diff 标签页——**永远只占一个**,再点就复用 |
|
|
@@ -122,6 +124,28 @@ dsh plugin --profile <你的 profile> remove dsh-gitbar-lite
|
|
|
122
124
|
|
|
123
125
|
> **冲突中的文件会被拒绝舍弃**,只给一句提示。在合并中途 `git restore` 到 HEAD 有扰动合并状态的风险,不值得为省一句话去冒。
|
|
124
126
|
|
|
127
|
+
### 用脚本填 Summary
|
|
128
|
+
|
|
129
|
+
Summary 输入框旁边有一个星星按钮(只有配了脚本才出现),点一下就把脚本的输出填进去。**侧边栏面板重新获得焦点时也会自动跑一次**——把侧边栏开着,去对话里让 agent 干活,回来,Summary 已经填好了。
|
|
130
|
+
|
|
131
|
+
脚本写在配置的 `summaryScript` 里,以**仓库根目录为工作目录**运行,不喂任何 stdin。所以「取 CHANGELOG 最顶上的版本号」就是:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
grep -m1 '^## ' CHANGELOG.md | sed 's/^## //'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
输出即 Summary。要同时填 Description,就在输出里单独放一行 `---`,**该行之后**的全部内容是 Description:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
echo "1.4.2"
|
|
141
|
+
echo "---"
|
|
142
|
+
echo "Bumped the patch version."
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
关于 shell:Windows 上优先用 Git 自带的 `sh`(`grep`/`sed`/`awk` 都可用),并把它的 `usr/bin` 补回 `PATH`——否则那些工具会「command not found」。机器上没有 `sh` 才退回 `cmd.exe`,那时脚本要按 cmd 语法写。非 Windows 就是 `sh`。
|
|
146
|
+
|
|
147
|
+
脚本失败不弹错误横幅,退出码会以一条短暂提示显示;它的 stderr 原样保留,方便你自己判断。
|
|
148
|
+
|
|
125
149
|
## 配置
|
|
126
150
|
|
|
127
151
|
| 字段 | 默认 | 说明 |
|
|
@@ -134,6 +158,7 @@ dsh plugin --profile <你的 profile> remove dsh-gitbar-lite
|
|
|
134
158
|
| `trashLimit` | `200` | 回收站保留多少条;超出丢弃最旧的连同它的 diff 文件 |
|
|
135
159
|
| `scanDepth` | `3` | 「不是仓库时」扫描工作区找仓库的目录深度 |
|
|
136
160
|
| `autoFetchMs` | `300000` | 面板自己多久 fetch 一次;`0` = 关掉,只用手动按钮 |
|
|
161
|
+
| `summaryScript` | `""` | 填 Summary 的脚本。以仓库根为工作目录运行,输出即 Summary;单独一行 `---` 之后算 Description |
|
|
137
162
|
|
|
138
163
|
每个字段都是 volatile 的,所以在设置页改了立刻生效、不用重启。
|
|
139
164
|
|
|
@@ -179,7 +204,7 @@ lib/client.js 浏览器半边:三个 tab 类型 + 面板/回收站/di
|
|
|
179
204
|
|
|
180
205
|
```sh
|
|
181
206
|
npm test # 单测 + 真 git 的 e2e + 产物自检
|
|
182
|
-
npm run test:e2e # 只跑真 git 的 e2e(
|
|
207
|
+
npm run test:e2e # 只跑真 git 的 e2e(22 个小节,各自造自己的夹具,277 项断言)
|
|
183
208
|
npm run check # 只用产物自检(不启 DSH 也能发现会让 DSH 起不来的问题)
|
|
184
209
|
npm run test:live # 探测一个正在运行的 DSH(不在 npm test 里,需要跑着应用)
|
|
185
210
|
```
|
package/docs/CONTRACT.md
CHANGED
|
@@ -326,7 +326,7 @@ this package on the next upgrade.
|
|
|
326
326
|
| state files, the binding key, corruption tolerance, write serialization | `test/store.test.mjs` |
|
|
327
327
|
| the route carrier, single registration under a hostile service order, error classification | `test/index.test.mjs` |
|
|
328
328
|
| every registration identity, the wire literals, the dictionaries, and the hook-free components' rendered trees | `test/client.test.mjs` |
|
|
329
|
-
| the whole host stack against **real git**, mutation by mutation | `test/e2e.mjs`,
|
|
329
|
+
| the whole host stack against **real git**, mutation by mutation | `test/e2e.mjs`, 277 assertions over 22 sections that build their own fixtures |
|
|
330
330
|
| the manifest, the patch layer, the tarball's contents, the two copies of the wire contract, the dictionaries, the workflow | `test/check.mjs` |
|
|
331
331
|
| that the client bundle would not abort the Host boot | `scripts/check-client-bundle.mjs` |
|
|
332
332
|
|
package/lib/client.js
CHANGED
|
@@ -75,6 +75,7 @@ window.__ModuleLoader__.load({
|
|
|
75
75
|
TRASH_DELETE: 'trashDelete',
|
|
76
76
|
TRASH_CLEAR: 'trashClear',
|
|
77
77
|
TRASH_RESTORE: 'trashRestore',
|
|
78
|
+
RUN_SCRIPT: 'runScript',
|
|
78
79
|
PREFS: 'prefs',
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -167,6 +168,7 @@ window.__ModuleLoader__.load({
|
|
|
167
168
|
'commit.descriptionPlaceholder': 'Description(可选)',
|
|
168
169
|
'commit.expand': '展开 Description',
|
|
169
170
|
'commit.collapse': '收起 Description',
|
|
171
|
+
'commit.runScript': '用脚本填 Summary',
|
|
170
172
|
'commit.commit': 'Commit',
|
|
171
173
|
'commit.commitAndPush': 'Commit and Push',
|
|
172
174
|
'commit.needsSummary': '先写 Summary',
|
|
@@ -251,6 +253,7 @@ window.__ModuleLoader__.load({
|
|
|
251
253
|
'toast.trashCleared': '回收站已清空({n} 条)',
|
|
252
254
|
'toast.trashDeleted': '已删除那条记录',
|
|
253
255
|
'toast.trashRestored': '已恢复 {n} 个文件的更改',
|
|
256
|
+
'toast.scriptFailed': '脚本退出码 {code}',
|
|
254
257
|
'toast.pushFailed': '已提交 {commit},但推送失败',
|
|
255
258
|
}
|
|
256
259
|
|
|
@@ -298,6 +301,7 @@ window.__ModuleLoader__.load({
|
|
|
298
301
|
'commit.descriptionPlaceholder': 'Description (optional)',
|
|
299
302
|
'commit.expand': 'Show the Description field',
|
|
300
303
|
'commit.collapse': 'Hide the Description field',
|
|
304
|
+
'commit.runScript': 'Fill the Summary from the script',
|
|
301
305
|
'commit.commit': 'Commit',
|
|
302
306
|
'commit.commitAndPush': 'Commit and Push',
|
|
303
307
|
'commit.needsSummary': 'Write a Summary first',
|
|
@@ -382,6 +386,7 @@ window.__ModuleLoader__.load({
|
|
|
382
386
|
'toast.trashCleared': 'Trash emptied ({n} entries)',
|
|
383
387
|
'toast.trashDeleted': 'Entry deleted',
|
|
384
388
|
'toast.trashRestored': 'Restored the changes to {n} files',
|
|
389
|
+
'toast.scriptFailed': 'The script exited with {code}',
|
|
385
390
|
'toast.pushFailed': 'Committed {commit}, but the push failed',
|
|
386
391
|
}
|
|
387
392
|
|
|
@@ -576,6 +581,32 @@ window.__ModuleLoader__.load({
|
|
|
576
581
|
return t('trash.restoreNoContent')
|
|
577
582
|
}
|
|
578
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Split a script's output into the Summary and the Description.
|
|
586
|
+
*
|
|
587
|
+
* Everything it printed is the Summary, so a script that knows nothing about
|
|
588
|
+
* this still fills the box usefully. A line reading exactly `---` is the one
|
|
589
|
+
* way to say "the rest is the body": it is three characters a shell script
|
|
590
|
+
* can print, it cannot collide with prose the way a heading might, and it is
|
|
591
|
+
* already the separator Markdown and YAML front-matter use.
|
|
592
|
+
*
|
|
593
|
+
* @param {string} stdout
|
|
594
|
+
* @returns {{ summary: string, description: string }}
|
|
595
|
+
*/
|
|
596
|
+
function splitScriptOutput(stdout) {
|
|
597
|
+
const text = typeof stdout === 'string' ? stdout : ''
|
|
598
|
+
const lines = text.split(/\r?\n/)
|
|
599
|
+
const marker = lines.findIndex((line) => line.trim() === '---')
|
|
600
|
+
if (marker === -1) {
|
|
601
|
+
const copy = [...lines]
|
|
602
|
+
while (copy.length > 0 && copy[copy.length - 1].trim() === '') copy.pop()
|
|
603
|
+
return { summary: copy.join('\n'), description: '' }
|
|
604
|
+
}
|
|
605
|
+
const before = lines.slice(0, marker)
|
|
606
|
+
while (before.length > 0 && before[before.length - 1].trim() === '') before.pop()
|
|
607
|
+
return { summary: before.join('\n'), description: lines.slice(marker + 1).join('\n').replace(/^\n+/, '') }
|
|
608
|
+
}
|
|
609
|
+
|
|
579
610
|
/** How many of a status payload's files are ticked and commit-ready. */
|
|
580
611
|
function stagedCount(state) {
|
|
581
612
|
if (state === null || !Array.isArray(state.files)) return 0
|
|
@@ -756,6 +787,18 @@ window.__ModuleLoader__.load({
|
|
|
756
787
|
'M2.3 3.6V7.4H6.1',
|
|
757
788
|
], [])
|
|
758
789
|
|
|
790
|
+
/**
|
|
791
|
+
* The script mark: a sparkle, drawn to the same specification.
|
|
792
|
+
*
|
|
793
|
+
* DSH has no "generate" or "wand" glyph to copy, and a chevron would read as
|
|
794
|
+
* "expand" rather than "fill this in", so this is hand-drawn to match the
|
|
795
|
+
* house rule (16 box, `stroke` on each path).
|
|
796
|
+
*/
|
|
797
|
+
const WandGlyph = artwork('0 0 16 16', [
|
|
798
|
+
'M6.5 2.5L7.6 5.9L11 7L7.6 8.1L6.5 11.5L5.4 8.1L2 7L5.4 5.9Z',
|
|
799
|
+
'M12.2 9.6L12.8 11.2L14.4 11.8L12.8 12.4L12.2 14L11.6 12.4L10 11.8L11.6 11.2Z',
|
|
800
|
+
], [])
|
|
801
|
+
|
|
759
802
|
/** `IconRefreshOutlineRegular`. */
|
|
760
803
|
const RefreshGlyph = artwork(
|
|
761
804
|
'0 0 16 16',
|
|
@@ -1322,37 +1365,56 @@ window.__ModuleLoader__.load({
|
|
|
1322
1365
|
[sessionId, tab.signal],
|
|
1323
1366
|
)
|
|
1324
1367
|
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1368
|
+
// Refresh when the panel comes back. This is the behaviour the panel is
|
|
1369
|
+
// asked for, and it is also the one that matters: leave the sidebar open,
|
|
1370
|
+
// go and let the agent work in the conversation, come back — the file list
|
|
1371
|
+
// should already show what changed, and the Summary script should already
|
|
1372
|
+
// have run. `tab.visible` is exactly that transition.
|
|
1373
|
+
//
|
|
1374
|
+
// Note that GitHub Desktop does *not* refresh on window focus (its focus
|
|
1375
|
+
// handler only tracks the resize handle); it refreshes on repository switch
|
|
1376
|
+
// and on an hourly background rhythm. The panel follows Desktop's shape
|
|
1377
|
+
// where Desktop has one, and the user's explicit request here, because a
|
|
1378
|
+
// DSH sidebar lives next to a chat the agent is working in.
|
|
1327
1379
|
const wasVisible = useRef(false)
|
|
1328
1380
|
const loadedOnce = useRef(false)
|
|
1329
1381
|
useEffect(() => {
|
|
1330
1382
|
const visible = tab.visible !== false
|
|
1331
1383
|
if (!loadedOnce.current || visible) {
|
|
1332
1384
|
loadedOnce.current = true
|
|
1385
|
+
const returning = wasVisible.current === false && visible
|
|
1333
1386
|
wasVisible.current = visible
|
|
1334
1387
|
// Quiet when there is already something to show, so returning to the
|
|
1335
1388
|
// tab does not flash the loading state over a list the user recognises.
|
|
1336
|
-
void load({ quiet: state !== null })
|
|
1389
|
+
void load({ quiet: state !== null }).then(() => {
|
|
1390
|
+
// After the list, not before: the script may read the tree, and the
|
|
1391
|
+
// Summary should describe what is on screen.
|
|
1392
|
+
if (returning && hasScript) void runScript()
|
|
1393
|
+
})
|
|
1337
1394
|
}
|
|
1338
|
-
}, [tab.visible, sessionId, load])
|
|
1395
|
+
}, [tab.visible, sessionId, load, hasScript, runScript])
|
|
1339
1396
|
|
|
1340
|
-
// Fetch
|
|
1341
|
-
//
|
|
1397
|
+
// Fetch unattended, in the background, while the panel is being looked at.
|
|
1398
|
+
//
|
|
1399
|
+
// GitHub Desktop's shape (`BackgroundFetcher` in app-store), which this
|
|
1400
|
+
// copies: the interval is long (an hour by default, with a five-minute
|
|
1401
|
+
// floor when the server suggests one), each tick re-asks whether it should
|
|
1402
|
+
// run, the next tick is scheduled from the *completion* of the last one
|
|
1403
|
+
// rather than on a fixed period, and a failure is logged and forgotten.
|
|
1404
|
+
//
|
|
1405
|
+
// Three gates, each with a reason:
|
|
1342
1406
|
//
|
|
1343
1407
|
// * the tab must be visible — a hidden panel has no business touching the
|
|
1344
|
-
// network
|
|
1408
|
+
// network, and the host may have several sessions;
|
|
1345
1409
|
// * the repository must have a remote — otherwise `fetch` is a guaranteed
|
|
1346
1410
|
// E_NO_REMOTE, and a timer that only ever fails is noise;
|
|
1347
1411
|
// * nothing else may be in flight — a user mid-commit must not have an
|
|
1348
1412
|
// extra git child appear under them.
|
|
1349
1413
|
//
|
|
1350
|
-
//
|
|
1351
|
-
// the
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1354
|
-
// says nothing. Success refreshes the status, which is what moves
|
|
1355
|
-
// ahead/behind and the "last fetch" line.
|
|
1414
|
+
// The "has it been long enough" question is answered by the repository
|
|
1415
|
+
// itself: `lastFetchAt` is the mtime of `.git/FETCH_HEAD`, so a fetch run
|
|
1416
|
+
// in a terminal counts, and a repository the user fetched a minute ago is
|
|
1417
|
+
// left alone however long the panel stays open.
|
|
1356
1418
|
//
|
|
1357
1419
|
// The conditions are read through a ref rather than listed as dependencies
|
|
1358
1420
|
// on purpose: a dependency on `state` would restart the clock on every
|
|
@@ -1366,10 +1428,29 @@ window.__ModuleLoader__.load({
|
|
|
1366
1428
|
if (autoFetchMs === 0) return undefined
|
|
1367
1429
|
if (tab.visible === false) return undefined
|
|
1368
1430
|
|
|
1369
|
-
const
|
|
1431
|
+
const sinceLastFetch = () => {
|
|
1432
|
+
const at = latest.current.state?.lastFetchAt
|
|
1433
|
+
const when = Date.parse(typeof at === 'string' ? at : '')
|
|
1434
|
+
if (!Number.isFinite(when)) return Number.POSITIVE_INFINITY
|
|
1435
|
+
return Date.now() - when
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// Reschedule from completion, not on a fixed period, so a slow fetch
|
|
1439
|
+
// cannot stack up behind itself. The skew keeps two panels that happened
|
|
1440
|
+
// to open together from fetching in lockstep forever.
|
|
1441
|
+
let timer = null
|
|
1442
|
+
let stopped = false
|
|
1443
|
+
const schedule = () => {
|
|
1444
|
+
if (stopped) return
|
|
1445
|
+
const wait = Math.max(0, autoFetchMs - sinceLastFetch()) + Math.ceil(Math.random() * 30000)
|
|
1446
|
+
timer = setTimeout(tick, wait)
|
|
1447
|
+
}
|
|
1448
|
+
const tick = () => {
|
|
1370
1449
|
const current = latest.current
|
|
1371
|
-
if (current.busy !== null) return
|
|
1372
|
-
if (!Array.isArray(current.state?.remotes) || current.state.remotes.length === 0) return
|
|
1450
|
+
if (current.busy !== null) return schedule()
|
|
1451
|
+
if (!Array.isArray(current.state?.remotes) || current.state.remotes.length === 0) return schedule()
|
|
1452
|
+
if (sinceLastFetch() < autoFetchMs) return schedule()
|
|
1453
|
+
|
|
1373
1454
|
void rpc(ACTIONS.FETCH, { sessionId }, tab.signal)
|
|
1374
1455
|
.then(() => {
|
|
1375
1456
|
if (alive.current) void load({ quiet: true })
|
|
@@ -1377,8 +1458,14 @@ window.__ModuleLoader__.load({
|
|
|
1377
1458
|
.catch(() => {
|
|
1378
1459
|
// Deliberately swallowed: see the note above.
|
|
1379
1460
|
})
|
|
1380
|
-
|
|
1381
|
-
|
|
1461
|
+
.finally(schedule)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
schedule()
|
|
1465
|
+
return () => {
|
|
1466
|
+
stopped = true
|
|
1467
|
+
if (timer !== null) clearTimeout(timer)
|
|
1468
|
+
}
|
|
1382
1469
|
}, [autoFetchMs, tab.visible, tab.signal, sessionId, load])
|
|
1383
1470
|
|
|
1384
1471
|
// The column's refresh shortcut, when it has one.
|
|
@@ -1505,6 +1592,14 @@ window.__ModuleLoader__.load({
|
|
|
1505
1592
|
setSummary('')
|
|
1506
1593
|
setDescription('')
|
|
1507
1594
|
setDescriptionOpen(false)
|
|
1595
|
+
// Clear the "already auto-selected this set" guard, so the
|
|
1596
|
+
// select-by-default rule applies again to whatever is left. GitHub
|
|
1597
|
+
// Desktop's equivalent is that a rebuilt working-directory list has
|
|
1598
|
+
// every new file included; here the same effect comes from letting
|
|
1599
|
+
// the existing rule run once more. A clean tree makes it a no-op,
|
|
1600
|
+
// and a partly-ticked commit leaves the rest ticked, which is what
|
|
1601
|
+
// Desktop does.
|
|
1602
|
+
autoStaged.current = ''
|
|
1508
1603
|
const short = data.commit?.short ?? ''
|
|
1509
1604
|
if (andPush && data.push?.ok === false) {
|
|
1510
1605
|
flash('warn', `${t('toast.pushFailed', { commit: short })}:${errorText(t, data.push.error?.code, data.push.error?.message)}`)
|
|
@@ -1516,6 +1611,36 @@ window.__ModuleLoader__.load({
|
|
|
1516
1611
|
[run, summary, description, flash, t],
|
|
1517
1612
|
)
|
|
1518
1613
|
|
|
1614
|
+
// The user's own script, run to fill the Summary. Only offered when the
|
|
1615
|
+
// config actually has one, and quiet when it is not configured — a panel
|
|
1616
|
+
// that never sets it should look exactly as it did before this existed.
|
|
1617
|
+
const hasScript = typeof state?.summaryScript === 'string' && state.summaryScript.trim() !== ''
|
|
1618
|
+
|
|
1619
|
+
const runScript = useCallback(async () => {
|
|
1620
|
+
setBusy('script')
|
|
1621
|
+
try {
|
|
1622
|
+
const result = await rpc(ACTIONS.RUN_SCRIPT, { sessionId }, tab.signal)
|
|
1623
|
+
if (!alive.current) return
|
|
1624
|
+
if (result.ran !== true) return
|
|
1625
|
+
const { summary: filled, description: body } = splitScriptOutput(result.stdout)
|
|
1626
|
+
setSummary(filled)
|
|
1627
|
+
if (body !== '') {
|
|
1628
|
+
setDescription(body)
|
|
1629
|
+
setDescriptionOpen(true)
|
|
1630
|
+
}
|
|
1631
|
+
setError(null)
|
|
1632
|
+
if (result.exitCode !== 0) {
|
|
1633
|
+
flash('warn', t('toast.scriptFailed', { code: result.exitCode ?? '?' }))
|
|
1634
|
+
}
|
|
1635
|
+
} catch (failure) {
|
|
1636
|
+
if (alive.current && failure?.name !== 'AbortError') {
|
|
1637
|
+
setError({ code: failure.code ?? 'E_UNKNOWN', message: failure.message })
|
|
1638
|
+
}
|
|
1639
|
+
} finally {
|
|
1640
|
+
if (alive.current) setBusy(null)
|
|
1641
|
+
}
|
|
1642
|
+
}, [sessionId, tab.signal, flash, t])
|
|
1643
|
+
|
|
1519
1644
|
const runContext = useCallback(() => {
|
|
1520
1645
|
const action = contextAction(state)
|
|
1521
1646
|
void run('context', action, undefined, (data) => {
|
|
@@ -1705,6 +1830,25 @@ window.__ModuleLoader__.load({
|
|
|
1705
1830
|
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey) && canCommit) commit(false)
|
|
1706
1831
|
},
|
|
1707
1832
|
}),
|
|
1833
|
+
// Fills the Summary (and the Description) from the user's script. Only
|
|
1834
|
+
// rendered when there is one, so a panel that never configures it looks
|
|
1835
|
+
// exactly as it did before.
|
|
1836
|
+
hasScript
|
|
1837
|
+
? h(
|
|
1838
|
+
'button',
|
|
1839
|
+
{
|
|
1840
|
+
key: 'fill',
|
|
1841
|
+
type: 'button',
|
|
1842
|
+
className: 'gitbar-disclose',
|
|
1843
|
+
title: t('commit.runScript'),
|
|
1844
|
+
'aria-label': t('commit.runScript'),
|
|
1845
|
+
'data-gitbar-runscript': '',
|
|
1846
|
+
disabled: busy !== null,
|
|
1847
|
+
onClick: () => void runScript(),
|
|
1848
|
+
},
|
|
1849
|
+
h(WandGlyph, { size: 13 }),
|
|
1850
|
+
)
|
|
1851
|
+
: null,
|
|
1708
1852
|
h(
|
|
1709
1853
|
'button',
|
|
1710
1854
|
{
|
|
@@ -2405,6 +2549,7 @@ window.__ModuleLoader__.load({
|
|
|
2405
2549
|
formatRelative,
|
|
2406
2550
|
lastFetchLabel,
|
|
2407
2551
|
restoreReasonText,
|
|
2552
|
+
splitScriptOutput,
|
|
2408
2553
|
stagedCount,
|
|
2409
2554
|
hasConflict,
|
|
2410
2555
|
contextLabelKey,
|
package/lib/host/git.js
CHANGED
|
@@ -137,6 +137,32 @@ export function createGit(ctx, config) {
|
|
|
137
137
|
*/
|
|
138
138
|
async function run(args, options = {}) {
|
|
139
139
|
const executablePath = await executable(options.signal)
|
|
140
|
+
return spawnCommand(executablePath, args, options, `git ${args[0] ?? ''}`.trim())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Run something that is not git, with the same guarantees `run` gives git.
|
|
145
|
+
*
|
|
146
|
+
* The user's Summary script is the one caller: it is a shell, not a git
|
|
147
|
+
* subcommand, so `run` — which always prepends the git executable — cannot
|
|
148
|
+
* launch it. Everything else about the contract is shared on purpose: the same
|
|
149
|
+
* timeout, the same abort handling, the same bounded output, the same
|
|
150
|
+
* environment, and the same "a non-zero exit is data, not a throw" rule.
|
|
151
|
+
*
|
|
152
|
+
* @param {readonly string[]} argv the executable and its arguments, in full
|
|
153
|
+
* @param {object} [options] same as `run`
|
|
154
|
+
* @returns {Promise<{ exitCode: number | null, stdout: string, stderr: string, truncated: boolean }>}
|
|
155
|
+
*/
|
|
156
|
+
async function runAny(argv, options = {}) {
|
|
157
|
+
return spawnCommand(argv[0], argv.slice(1), options, String(argv[0] ?? ''))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Spawn one child and collect it. Every launch in this module goes through
|
|
162
|
+
* here, so there is exactly one place where the timeout, the abort ladder and
|
|
163
|
+
* the output ceiling are decided.
|
|
164
|
+
*/
|
|
165
|
+
async function spawnCommand(executablePath, args, options, label) {
|
|
140
166
|
const timeoutMs = Number.isFinite(config.timeoutMs) ? config.timeoutMs : 30000
|
|
141
167
|
// A zero/negative timeout means "no deadline of our own": still pass the
|
|
142
168
|
// caller's signal so an aborted request does not leave a child running.
|
|
@@ -164,11 +190,11 @@ export function createGit(ctx, config) {
|
|
|
164
190
|
})
|
|
165
191
|
} catch (error) {
|
|
166
192
|
// `spawn` refuses a spec it cannot honour *synchronously*: a signal that is
|
|
167
|
-
// already aborted, or
|
|
168
|
-
//
|
|
169
|
-
// unclassified -- a generic internal error for a request that
|
|
170
|
-
// abandoned. Report it the way the asynchronous abort below does.
|
|
171
|
-
throw new GitFailure(
|
|
193
|
+
// already aborted, or an argv/cwd/env/grace it considers invalid. Neither is
|
|
194
|
+
// a verdict about the work, and letting the throw escape would reach the
|
|
195
|
+
// route layer unclassified -- a generic internal error for a request that
|
|
196
|
+
// was simply abandoned. Report it the way the asynchronous abort below does.
|
|
197
|
+
throw new GitFailure(label, {
|
|
172
198
|
exitCode: null,
|
|
173
199
|
stdout: '',
|
|
174
200
|
stderr: abortReason(signal, timeout, timeoutMs) ?? String(error?.message ?? error),
|
|
@@ -178,7 +204,7 @@ export function createGit(ctx, config) {
|
|
|
178
204
|
const outcome = await handle.done
|
|
179
205
|
|
|
180
206
|
if (signal !== undefined && signal.aborted) {
|
|
181
|
-
throw new GitFailure(
|
|
207
|
+
throw new GitFailure(label, { exitCode: null, stdout: '', stderr: abortReason(signal, timeout, timeoutMs) })
|
|
182
208
|
}
|
|
183
209
|
|
|
184
210
|
const out = handle.collected.stdout?.readFrom(0) ?? { text: '', lossy: false }
|
|
@@ -237,5 +263,5 @@ export function createGit(ctx, config) {
|
|
|
237
263
|
return toplevel
|
|
238
264
|
}
|
|
239
265
|
|
|
240
|
-
return { executable, run, must, repoRootOf, isRepoRoot }
|
|
266
|
+
return { executable, run, runAny, must, repoRootOf, isRepoRoot }
|
|
241
267
|
}
|
package/lib/host/repo.js
CHANGED
|
@@ -199,6 +199,45 @@ async function fetchTimeOf(gitDirResult) {
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/** Resolved once per plugin instance: which shell a script is written for. */
|
|
203
|
+
let scriptShell = null
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The argv prefix that runs one script string.
|
|
207
|
+
*
|
|
208
|
+
* `sh` if the machine has one, and `cmd.exe /d /s /c` otherwise.
|
|
209
|
+
*
|
|
210
|
+
* Why the hunt for `sh` and not simply "use it on Unix, cmd on Windows": the
|
|
211
|
+
* script is text a user types into a settings field, and the shell it is written
|
|
212
|
+
* in decides whether `grep`, `sed` and `awk` work. Git for Windows ships a full
|
|
213
|
+
* `usr/bin` beside the git binary — but it does *not* add it to `PATH`, so a bare
|
|
214
|
+
* `sh` lookup fails on most Windows machines even though one is installed. The
|
|
215
|
+
* git binary's own location is therefore the place to look: it is the one
|
|
216
|
+
* executable this plugin is certain exists, and its neighbour `usr/bin/sh.exe`
|
|
217
|
+
* is the one that has the POSIX tools beside it.
|
|
218
|
+
*
|
|
219
|
+
* @param {object} git the runner from `createGit`, for executable resolution
|
|
220
|
+
* @param {AbortSignal} [signal]
|
|
221
|
+
* @returns {Promise<readonly string[]>}
|
|
222
|
+
*/
|
|
223
|
+
async function scriptPrefix(git, signal) {
|
|
224
|
+
if (scriptShell !== null) return scriptShell
|
|
225
|
+
if (process.platform !== 'win32') {
|
|
226
|
+
scriptShell = ['sh', '-c']
|
|
227
|
+
return scriptShell
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const gitPath = await git.executable('git', signal)
|
|
232
|
+
const candidate = resolve(dirname(dirname(gitPath)), 'usr', 'bin', 'sh.exe')
|
|
233
|
+
await stat(candidate)
|
|
234
|
+
scriptShell = [candidate, '-c']
|
|
235
|
+
} catch {
|
|
236
|
+
scriptShell = ['cmd.exe', '/d', '/s', '/c']
|
|
237
|
+
}
|
|
238
|
+
return scriptShell
|
|
239
|
+
}
|
|
240
|
+
|
|
202
241
|
/**
|
|
203
242
|
* Build the repository facade for one plugin instance.
|
|
204
243
|
*
|
|
@@ -316,6 +355,9 @@ export function createRepo(git, config) {
|
|
|
316
355
|
// it), so the rhythm it should fetch on travels with the status it is
|
|
317
356
|
// already reading. Zero means "never, on your own".
|
|
318
357
|
autoFetchMs,
|
|
358
|
+
// ...and so does the script, so the panel can offer the button only when
|
|
359
|
+
// there is a script to run.
|
|
360
|
+
summaryScript: typeof config.summaryScript === 'string' ? config.summaryScript : '',
|
|
319
361
|
}
|
|
320
362
|
}
|
|
321
363
|
|
|
@@ -394,8 +436,30 @@ export function createRepo(git, config) {
|
|
|
394
436
|
* @param {string} repoRoot
|
|
395
437
|
* @param {AbortSignal} [signal]
|
|
396
438
|
*/
|
|
439
|
+
/**
|
|
440
|
+
* Push the current branch, publishing it when it has no upstream yet.
|
|
441
|
+
*
|
|
442
|
+
* A fetch runs first. GitHub Desktop goes further — fetch, then pull, then push
|
|
443
|
+
* — but a pull rewrites the working tree, and this panel's whole model is that
|
|
444
|
+
* a tick is the real index: a merge commit appearing under the user between
|
|
445
|
+
* their tick and their push would quietly invalidate what they selected. So the
|
|
446
|
+
* fetch is followed by a check instead, and the push is refused with a sentence
|
|
447
|
+
* when the branch has fallen behind.
|
|
448
|
+
*
|
|
449
|
+
* That refusal is the honest outcome, not a limitation: git never merges on
|
|
450
|
+
* push, so a behind branch is rejected either way. What the fetch buys is that
|
|
451
|
+
* the answer is known *before* the push attempt, and that the upstream tracking
|
|
452
|
+
* ref the ahead/behind count is read from is current rather than however stale
|
|
453
|
+
* it was when the panel opened.
|
|
454
|
+
*
|
|
455
|
+
* The fetch itself is best-effort: if it fails (offline, no credential) the
|
|
456
|
+
* push still goes ahead, because the fetch's failure says nothing about whether
|
|
457
|
+
* the push can work, and silently refusing to push would be worse.
|
|
458
|
+
*
|
|
459
|
+
* @param {string} repoRoot
|
|
460
|
+
* @param {AbortSignal} [signal]
|
|
461
|
+
*/
|
|
397
462
|
async function push(repoRoot, signal) {
|
|
398
|
-
const { branch } = await statusOf(repoRoot, signal)
|
|
399
463
|
const remotesResult = await git.run(['remote'], { cwd: repoRoot, signal })
|
|
400
464
|
const remotes =
|
|
401
465
|
remotesResult.exitCode === 0
|
|
@@ -406,13 +470,36 @@ export function createRepo(git, config) {
|
|
|
406
470
|
: []
|
|
407
471
|
const remote = pickRemote(remotes)
|
|
408
472
|
|
|
473
|
+
let fetched = false
|
|
474
|
+
const preFetch = await git.run(['fetch', '--prune', remote], { cwd: repoRoot, signal, maxBytes: 1024 * 1024 })
|
|
475
|
+
if (preFetch.exitCode === 0) fetched = true
|
|
476
|
+
|
|
477
|
+
// Re-read after the fetch: the branch's upstream and its ahead/behind counts
|
|
478
|
+
// are exactly what the fetch may have changed.
|
|
479
|
+
const { branch } = await statusOf(repoRoot, signal)
|
|
480
|
+
|
|
481
|
+
// Only refuse when we *know* we are behind, which requires both an upstream
|
|
482
|
+
// and a fetch that actually told us something. Without an upstream this is a
|
|
483
|
+
// publish, and a failed fetch leaves the counts unknown — neither should be
|
|
484
|
+
// turned into a refusal.
|
|
485
|
+
if (fetched && branch.upstream !== null && branch.behind > 0) {
|
|
486
|
+
const refusal = new RepoError(
|
|
487
|
+
ERROR_CODES.PUSH_REJECTED,
|
|
488
|
+
`远端有新提交(落后 ${branch.behind} 个),先 Pull 或 Rebase 再推。`,
|
|
489
|
+
)
|
|
490
|
+
// Carried on the error so the route can report "we did look" alongside the
|
|
491
|
+
// refusal, rather than the caller having to guess whether a fetch happened.
|
|
492
|
+
refusal.fetched = true
|
|
493
|
+
throw refusal
|
|
494
|
+
}
|
|
495
|
+
|
|
409
496
|
const args =
|
|
410
497
|
branch.upstream === null ? ['push', '--set-upstream', remote, 'HEAD'] : ['push', remote]
|
|
411
498
|
const result = await git.run(args, { cwd: repoRoot, signal, maxBytes: 1024 * 1024 })
|
|
412
499
|
if (result.exitCode !== 0) {
|
|
413
500
|
throw classifyGitFailure({ ...result, message: `git push failed: ${result.stderr.trim()}` })
|
|
414
501
|
}
|
|
415
|
-
return { remote, published: branch.upstream === null, output: `${result.stdout}${result.stderr}`.trim() }
|
|
502
|
+
return { remote, published: branch.upstream === null, fetched, output: `${result.stdout}${result.stderr}`.trim() }
|
|
416
503
|
}
|
|
417
504
|
|
|
418
505
|
/**
|
|
@@ -943,5 +1030,59 @@ export function createRepo(git, config) {
|
|
|
943
1030
|
return { restored, skipped }
|
|
944
1031
|
}
|
|
945
1032
|
|
|
946
|
-
|
|
1033
|
+
/**
|
|
1034
|
+
* Run the user's Summary script and report what it printed.
|
|
1035
|
+
*
|
|
1036
|
+
* The script is text from the settings page, run with the repository root as
|
|
1037
|
+
* its working directory, so `grep -m1 '^## ' CHANGELOG.md | sed 's/^## //'` is
|
|
1038
|
+
* a valid script. It is handed nothing on stdin: the inputs worth naming in a
|
|
1039
|
+
* commit are files in the tree, and a script that wants the diff can ask git
|
|
1040
|
+
* for it itself.
|
|
1041
|
+
*
|
|
1042
|
+
* A non-zero exit is reported rather than thrown — a script that finds nothing
|
|
1043
|
+
* is an ordinary outcome, and the panel shows its stderr beside the button.
|
|
1044
|
+
*
|
|
1045
|
+
* @param {string} repoRoot
|
|
1046
|
+
* @param {AbortSignal} [signal]
|
|
1047
|
+
* @returns {Promise<{ ran: boolean, exitCode?: number, stdout?: string, stderr?: string }>}
|
|
1048
|
+
*/
|
|
1049
|
+
async function runScript(repoRoot, signal) {
|
|
1050
|
+
const script = typeof config.summaryScript === 'string' ? config.summaryScript.trim() : ''
|
|
1051
|
+
if (script === '') return { ran: false }
|
|
1052
|
+
|
|
1053
|
+
const shell = await scriptPrefix(git, signal)
|
|
1054
|
+
// Git for Windows' `sh` is a MinGW program whose tools (`grep`, `sed`, `awk`)
|
|
1055
|
+
// live in its own `usr/bin`, which git does *not* add to PATH — so a script
|
|
1056
|
+
// that uses them finds nothing unless that directory is put back. This is
|
|
1057
|
+
// what git's own `git-bash` wrapper does, and it is the difference between a
|
|
1058
|
+
// script that works and one that reports "grep: command not found".
|
|
1059
|
+
const env =
|
|
1060
|
+
process.platform === 'win32' && shell.length > 0 && shell[0].includes('usr')
|
|
1061
|
+
? { PATH: `${dirname(shell[0])}${process.env.PATH === undefined ? '' : `;${process.env.PATH}`}` }
|
|
1062
|
+
: {}
|
|
1063
|
+
|
|
1064
|
+
const result = await git.runAny([...shell, script], { cwd: repoRoot, signal, env })
|
|
1065
|
+
|
|
1066
|
+
return {
|
|
1067
|
+
ran: true,
|
|
1068
|
+
exitCode: result.exitCode,
|
|
1069
|
+
stdout: result.stdout,
|
|
1070
|
+
stderr: result.stderr,
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
readStatus,
|
|
1076
|
+
statusOf,
|
|
1077
|
+
setStaged,
|
|
1078
|
+
commit,
|
|
1079
|
+
push,
|
|
1080
|
+
fetch,
|
|
1081
|
+
fileDiff,
|
|
1082
|
+
discard,
|
|
1083
|
+
restore,
|
|
1084
|
+
canRestore,
|
|
1085
|
+
scanRepos,
|
|
1086
|
+
runScript,
|
|
1087
|
+
}
|
|
947
1088
|
}
|
package/lib/host/routes.js
CHANGED
|
@@ -261,7 +261,11 @@ export function createHandlers({ ctx, git, repo, store }) {
|
|
|
261
261
|
try {
|
|
262
262
|
push = { ok: true, ...(await repo.push(repoRoot, request.signal)) }
|
|
263
263
|
} catch (error) {
|
|
264
|
-
|
|
264
|
+
// `fetched` is carried out of a refusal because it is the difference
|
|
265
|
+
// between "the push was rejected, fetch and try again" and "we already
|
|
266
|
+
// looked, and the branch really is behind" — the same sentence reads very
|
|
267
|
+
// differently depending on which one it is.
|
|
268
|
+
push = { ok: false, error: describeError(error), fetched: error?.fetched === true }
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
const state = await repo.readStatus(repoRoot, request.signal)
|
|
@@ -305,6 +309,19 @@ export function createHandlers({ ctx, git, repo, store }) {
|
|
|
305
309
|
return { repoRoot, diff }
|
|
306
310
|
},
|
|
307
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Run the user's Summary script and hand back what it printed.
|
|
314
|
+
*
|
|
315
|
+
* The panel does the splitting, because the protocol is a presentation rule
|
|
316
|
+
* (`---` separates Summary from Description) rather than anything the
|
|
317
|
+
* repository needs to know about.
|
|
318
|
+
*/
|
|
319
|
+
async [ACTIONS.RUN_SCRIPT](payload, request) {
|
|
320
|
+
const { repoRoot } = await requireRepoRoot(payload, request.signal)
|
|
321
|
+
const result = await repo.runScript(repoRoot, request.signal)
|
|
322
|
+
return { repoRoot, ...result }
|
|
323
|
+
},
|
|
324
|
+
|
|
308
325
|
async [ACTIONS.TRASH_LIST](payload, request) {
|
|
309
326
|
const workspace = await requireWorkspace(ctx, stringField(payload, 'sessionId'))
|
|
310
327
|
const prefs = await store.readPrefs()
|
package/lib/index.js
CHANGED
|
@@ -76,6 +76,16 @@ export const Config = vol(
|
|
|
76
76
|
* to disable it; the button still works.
|
|
77
77
|
*/
|
|
78
78
|
autoFetchMs: z.number().default(300000),
|
|
79
|
+
/**
|
|
80
|
+
* A script that fills in the Summary, run with the repository root as its
|
|
81
|
+
* working directory. Empty means the feature is off.
|
|
82
|
+
*
|
|
83
|
+
* Whatever it prints is the Summary; a line reading `---` splits it, so
|
|
84
|
+
* everything after that line is the Description. Nothing is fed in — the
|
|
85
|
+
* inputs worth naming in a commit are files in the tree, and a script that
|
|
86
|
+
* wants the diff can ask git for it.
|
|
87
|
+
*/
|
|
88
|
+
summaryScript: z.string().default(''),
|
|
79
89
|
}),
|
|
80
90
|
)
|
|
81
91
|
|
package/lib/shared.js
CHANGED
|
@@ -54,6 +54,15 @@ export const ACTIONS = {
|
|
|
54
54
|
* that happened after the archive was taken.
|
|
55
55
|
*/
|
|
56
56
|
TRASH_RESTORE: 'trashRestore',
|
|
57
|
+
/**
|
|
58
|
+
* Run the user's summary script and return what it printed.
|
|
59
|
+
*
|
|
60
|
+
* The script is the user's own text from the config, run with the repository
|
|
61
|
+
* root as its working directory. The panel turns its output into a Summary
|
|
62
|
+
* (and a Description), which is why this returns the output rather than
|
|
63
|
+
* writing it anywhere itself.
|
|
64
|
+
*/
|
|
65
|
+
RUN_SCRIPT: 'runScript',
|
|
57
66
|
/** Read or write the small persisted UI preferences. */
|
|
58
67
|
PREFS: 'prefs',
|
|
59
68
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gitbar-lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A GitHub Desktop-style Git panel for DSH: changed files, tick-to-stage, Summary + Description, Commit and Commit and Push, discard-to-trash, and diff preview, in the built-in right sidebar.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|