dsh-gitbar-lite 0.3.0 → 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 +4 -2
- package/README.md +4 -2
- package/docs/CONTRACT.md +1 -1
- package/lib/client.js +8 -0
- package/lib/host/repo.js +47 -2
- package/lib/host/routes.js +5 -1
- 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 |
|
|
@@ -222,7 +224,7 @@ are in [`docs/CONTRACT.md`](./docs/CONTRACT.md).
|
|
|
222
224
|
|
|
223
225
|
```sh
|
|
224
226
|
npm test # unit + real-git e2e + artifact self-check
|
|
225
|
-
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)
|
|
226
228
|
npm run check # artifact self-check alone (catches boot-breaking mistakes without restarting DSH)
|
|
227
229
|
npm run test:live # probe a running DSH (not part of npm test; needs the app running)
|
|
228
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 标签页——**永远只占一个**,再点就复用 |
|
|
@@ -202,7 +204,7 @@ lib/client.js 浏览器半边:三个 tab 类型 + 面板/回收站/di
|
|
|
202
204
|
|
|
203
205
|
```sh
|
|
204
206
|
npm test # 单测 + 真 git 的 e2e + 产物自检
|
|
205
|
-
npm run test:e2e # 只跑真 git 的 e2e(
|
|
207
|
+
npm run test:e2e # 只跑真 git 的 e2e(22 个小节,各自造自己的夹具,277 项断言)
|
|
206
208
|
npm run check # 只用产物自检(不启 DSH 也能发现会让 DSH 起不来的问题)
|
|
207
209
|
npm run test:live # 探测一个正在运行的 DSH(不在 npm test 里,需要跑着应用)
|
|
208
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
|
@@ -1592,6 +1592,14 @@ window.__ModuleLoader__.load({
|
|
|
1592
1592
|
setSummary('')
|
|
1593
1593
|
setDescription('')
|
|
1594
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 = ''
|
|
1595
1603
|
const short = data.commit?.short ?? ''
|
|
1596
1604
|
if (andPush && data.push?.ok === false) {
|
|
1597
1605
|
flash('warn', `${t('toast.pushFailed', { commit: short })}:${errorText(t, data.push.error?.code, data.push.error?.message)}`)
|
package/lib/host/repo.js
CHANGED
|
@@ -436,8 +436,30 @@ export function createRepo(git, config) {
|
|
|
436
436
|
* @param {string} repoRoot
|
|
437
437
|
* @param {AbortSignal} [signal]
|
|
438
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
|
+
*/
|
|
439
462
|
async function push(repoRoot, signal) {
|
|
440
|
-
const { branch } = await statusOf(repoRoot, signal)
|
|
441
463
|
const remotesResult = await git.run(['remote'], { cwd: repoRoot, signal })
|
|
442
464
|
const remotes =
|
|
443
465
|
remotesResult.exitCode === 0
|
|
@@ -448,13 +470,36 @@ export function createRepo(git, config) {
|
|
|
448
470
|
: []
|
|
449
471
|
const remote = pickRemote(remotes)
|
|
450
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
|
+
|
|
451
496
|
const args =
|
|
452
497
|
branch.upstream === null ? ['push', '--set-upstream', remote, 'HEAD'] : ['push', remote]
|
|
453
498
|
const result = await git.run(args, { cwd: repoRoot, signal, maxBytes: 1024 * 1024 })
|
|
454
499
|
if (result.exitCode !== 0) {
|
|
455
500
|
throw classifyGitFailure({ ...result, message: `git push failed: ${result.stderr.trim()}` })
|
|
456
501
|
}
|
|
457
|
-
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() }
|
|
458
503
|
}
|
|
459
504
|
|
|
460
505
|
/**
|
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)
|
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",
|