dsh-remote-vs 0.9.3 → 0.11.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 +13 -10
- package/README.md +14 -9
- package/cordis.patch.yml +10 -5
- package/lib/client.js +3319 -1093
- package/lib/index.js +211 -9
- package/lib/types/client/api.d.ts +12 -0
- package/lib/types/client/composer.d.ts +5 -1
- package/lib/types/client/exec-output.d.ts +58 -0
- package/lib/types/client/i18n.d.ts +1 -1
- package/lib/types/client/scm/CommitPane.d.ts +73 -0
- package/lib/types/client/scm/ExecPanel.d.ts +34 -0
- package/lib/types/client/scm/HistoryPanel.d.ts +12 -4
- package/lib/types/client/scm/actions.d.ts +50 -3
- package/lib/types/client/scm/commit-model.d.ts +119 -0
- package/lib/types/client/store.d.ts +9 -3
- package/lib/types/client/vsc/kit.d.ts +16 -0
- package/lib/types/git-format.d.ts +5 -0
- package/lib/types/git-ops.d.ts +57 -2
- package/lib/types/index.d.ts +5 -0
- package/lib/types/pool.d.ts +9 -0
- package/lib/types/protocol.d.ts +23 -0
- package/lib/types/timeouts.d.ts +17 -2
- package/package.json +37 -37
- package/src/client/SettingsHosts.tsx +2 -1
- package/src/client/api.ts +61 -0
- package/src/client/composer.ts +11 -2
- package/src/client/exec-output.ts +129 -0
- package/src/client/i18n.ts +133 -4
- package/src/client/scm/CommitPane.tsx +726 -0
- package/src/client/scm/ExecPanel.tsx +124 -0
- package/src/client/scm/HistoryPanel.tsx +483 -104
- package/src/client/scm/ScmPanel.tsx +247 -9
- package/src/client/scm/actions.ts +93 -10
- package/src/client/scm/commit-model.ts +317 -0
- package/src/client/store.tsx +13 -6
- package/src/client/vsc/kit.tsx +71 -0
- package/src/client/vsc/vsc.css +647 -0
- package/src/git-format.ts +7 -3
- package/src/git-ops.ts +83 -7
- package/src/index.ts +10 -2
- package/src/pool.ts +84 -4
- package/src/protocol.ts +14 -0
- package/src/routes.ts +135 -1
- package/src/timeouts.ts +18 -2
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers behind the streaming exec surface (the network verbs' live log).
|
|
3
|
+
*
|
|
4
|
+
* The host route answers as server-sent events so a 20-minute push can show its
|
|
5
|
+
* progress (`remote: …`, hook output, pack lines) while it runs, instead of one
|
|
6
|
+
* line after the fact. Everything here is pure so the local self-check can pin
|
|
7
|
+
* the frame splitting (a chunk boundary may fall anywhere) and the output cap
|
|
8
|
+
* without a browser or a host.
|
|
9
|
+
*/
|
|
10
|
+
import { DEFAULT_NETWORK_TIMEOUT_MS, MAX_TIMEOUT_MS } from '../timeouts.ts'
|
|
11
|
+
import { fetchCommand, pullCommand, pushCommand, pushRefspecCommand } from '../git-ops.ts'
|
|
12
|
+
|
|
13
|
+
/** What a streamed push should do: remembered refspec ('' = plain) + remote. */
|
|
14
|
+
export interface PushTarget {
|
|
15
|
+
refspec: string
|
|
16
|
+
remote: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The commands one network verb runs, in order.
|
|
21
|
+
*
|
|
22
|
+
* Pure so the gate can pin the sequence: sync is pull-then-push (the historical
|
|
23
|
+
* order), and a push with a refspec must not fall back to the bare form.
|
|
24
|
+
*/
|
|
25
|
+
export function networkCommands(root: string, kind: 'fetch' | 'pull' | 'push' | 'sync', push: PushTarget): string[] {
|
|
26
|
+
if (kind === 'fetch') return [fetchCommand(root)]
|
|
27
|
+
if (kind === 'pull') return [pullCommand(root)]
|
|
28
|
+
const pushCmd = push.refspec === '' ? pushCommand(root) : pushRefspecCommand(root, push.remote, push.refspec)
|
|
29
|
+
if (kind === 'push') return [pushCmd]
|
|
30
|
+
// Sync keeps its historical order, and honours a refspec when one is given
|
|
31
|
+
// (the UI currently passes '' for sync, but the API must not quietly drop it).
|
|
32
|
+
return [pullCommand(root), pushCmd]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Live-log cap: keep the TAIL, because the head of a long push is never the point. */
|
|
36
|
+
export const EXEC_OUTPUT_CAP = 200_000
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fold carriage-return progress rewrites for DISPLAY.
|
|
40
|
+
*
|
|
41
|
+
* git's progress lines are written as `Counting: 1%\rCounting: 2%\r…` and a
|
|
42
|
+
* `<pre>` would render every rewrite as its own line (hundreds of them for one
|
|
43
|
+
* push). Only the content after the last `\r` of a line survives.
|
|
44
|
+
*
|
|
45
|
+
* Fold at RENDER time, over the complete accumulated text — not inside
|
|
46
|
+
* `appendExecOutput`. A chunk can end mid-rewrite (git writes bare `\r`s at
|
|
47
|
+
* arbitrary boundaries), and folding a fragment loses the state that the next
|
|
48
|
+
* fragment continues: measured against real `git push --progress` stderr, the
|
|
49
|
+
* per-chunk fold glued `Counting: 1%Counting: 2%…` into one long line.
|
|
50
|
+
*/
|
|
51
|
+
export function foldExecOutput(text: string): string {
|
|
52
|
+
if (!text.includes('\r')) return text
|
|
53
|
+
return text
|
|
54
|
+
.split('\n')
|
|
55
|
+
.map(line => {
|
|
56
|
+
if (!line.includes('\r')) return line
|
|
57
|
+
// A trailing `\r` (progress finished, then newline) does not erase what
|
|
58
|
+
// was drawn before it: the visible text is the last NON-EMPTY rewrite.
|
|
59
|
+
const parts = line.split('\r')
|
|
60
|
+
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
61
|
+
const part = parts[index] as string
|
|
62
|
+
if (part !== '') return part
|
|
63
|
+
}
|
|
64
|
+
return ''
|
|
65
|
+
})
|
|
66
|
+
.join('\n')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Append one decoded chunk, keeping the TAIL.
|
|
71
|
+
*
|
|
72
|
+
* The raw `\r` bytes are preserved on purpose: the view folds them once, over
|
|
73
|
+
* the whole accumulated log, so a rewrite split across two chunks still folds
|
|
74
|
+
* correctly (`foldExecOutput`).
|
|
75
|
+
*/
|
|
76
|
+
export function appendExecOutput(previous: string, chunk: string, cap = EXEC_OUTPUT_CAP): string {
|
|
77
|
+
if (chunk === '') return previous
|
|
78
|
+
const combined = previous + chunk
|
|
79
|
+
if (combined.length <= cap) return combined
|
|
80
|
+
return combined.slice(combined.length - cap)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Split an SSE buffer into complete frames and the incomplete tail.
|
|
85
|
+
*
|
|
86
|
+
* A chunk can end in the middle of `data: {...}` or between the two newlines
|
|
87
|
+
* that terminate a frame, so the caller must carry `rest` into the next read.
|
|
88
|
+
*/
|
|
89
|
+
export function takeSseFrames(buffer: string): { frames: string[]; rest: string } {
|
|
90
|
+
const parts = buffer.split('\n\n')
|
|
91
|
+
const rest = parts.pop() ?? ''
|
|
92
|
+
return { frames: parts.filter(frame => frame.trim() !== ''), rest }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One SSE frame's JSON payload; `undefined` for comments (heartbeats), empty
|
|
97
|
+
* frames and malformed JSON — a broken heartbeat must not kill the stream.
|
|
98
|
+
*/
|
|
99
|
+
export function parseSseFrame(frame: string): unknown | undefined {
|
|
100
|
+
const data = frame
|
|
101
|
+
.split('\n')
|
|
102
|
+
.filter(line => line.startsWith('data:'))
|
|
103
|
+
.map(line => line.slice(5).trimStart())
|
|
104
|
+
.join('\n')
|
|
105
|
+
if (data === '') return undefined
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(data) as unknown
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `m:ss` (or `h:mm:ss`) for the running timer. */
|
|
114
|
+
export function formatElapsed(ms: number): string {
|
|
115
|
+
const total = Math.max(0, Math.floor(ms / 1000))
|
|
116
|
+
const seconds = total % 60
|
|
117
|
+
const minutes = Math.floor(total / 60) % 60
|
|
118
|
+
const hours = Math.floor(total / 3600)
|
|
119
|
+
const pad = (value: number): string => String(value).padStart(2, '0')
|
|
120
|
+
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The network verbs' budget: at least 20 minutes, or twice the configured git
|
|
125
|
+
* budget when that is larger, never past the host's 30-minute ceiling.
|
|
126
|
+
*/
|
|
127
|
+
export function networkBudgetMs(gitTimeoutMs: number): number {
|
|
128
|
+
return Math.min(MAX_TIMEOUT_MS, Math.max(DEFAULT_NETWORK_TIMEOUT_MS, gitTimeoutMs * 2))
|
|
129
|
+
}
|
package/src/client/i18n.ts
CHANGED
|
@@ -20,6 +20,13 @@ export const DYNAMIC_KEYS = [
|
|
|
20
20
|
'scm.hunkStaged',
|
|
21
21
|
'scm.hunkUnstaged',
|
|
22
22
|
'scm.hunkReverted',
|
|
23
|
+
// Labels reached through a lookup table in `scm/CommitPane.tsx`
|
|
24
|
+
// (`TAB_LABELS`, a hand-maintained literal map — NOT generated from
|
|
25
|
+
// HISTORY_TABS; a fourth tab needs an entry here too), not through literal
|
|
26
|
+
// translator calls. The scanner below must not mistake them for orphans.
|
|
27
|
+
'history.details',
|
|
28
|
+
'history.content',
|
|
29
|
+
'history.diff',
|
|
23
30
|
] as const
|
|
24
31
|
|
|
25
32
|
const zh: Record<string, string> = {
|
|
@@ -138,7 +145,7 @@ const zh: Record<string, string> = {
|
|
|
138
145
|
'route.off': '关闭(直通本地)',
|
|
139
146
|
'route.shadow': '影子(记日志仍走本地)',
|
|
140
147
|
'route.enforce': '强制(真路由)',
|
|
141
|
-
'route.hint': '切换即时生效并写入 profile
|
|
148
|
+
'route.hint': '切换即时生效并写入 profile,无需重启;默认「强制(真路由)」。',
|
|
142
149
|
'route.saving': '保存中…',
|
|
143
150
|
'route.tombstones': '墓碑',
|
|
144
151
|
'route.aliasPath': '本地别名目录(原生工作区入口)',
|
|
@@ -286,7 +293,27 @@ const zh: Record<string, string> = {
|
|
|
286
293
|
'scm.discoverFound': '工作区内发现的仓库',
|
|
287
294
|
'scm.discoverEmpty': '工作区根 3 层内没发现仓库 —— 请手动进入目录',
|
|
288
295
|
'scm.pushTitle': '推送到远端',
|
|
289
|
-
'scm.pushText': '
|
|
296
|
+
'scm.pushText': '填写推送 refspec(Gerrit 用 HEAD:refs/for/<分支>)。留空 = 普通 git push(推到当前分支 upstream);非空值会被记住。',
|
|
297
|
+
'scm.pushRefspecPlaceholder': 'HEAD:refs/for/master',
|
|
298
|
+
'scm.pushRefspecDone': '已推送:{refspec}',
|
|
299
|
+
// -- streaming network verbs (fetch/pull/push/sync live output)
|
|
300
|
+
'exec.running': '执行中',
|
|
301
|
+
'exec.waiting': '等待远端输出…',
|
|
302
|
+
'exec.copy': '复制输出',
|
|
303
|
+
'exec.copied': '输出已复制',
|
|
304
|
+
'exec.cancel': '终止远端 git',
|
|
305
|
+
'exec.cancelHint': '会杀掉远端进程组(TERM→KILL),操作可能只完成一半;取消前请确认',
|
|
306
|
+
'exec.collapse': '收起日志',
|
|
307
|
+
'exec.expand': '展开日志',
|
|
308
|
+
'exec.log': '执行输出',
|
|
309
|
+
'exec.noOutput': '(无输出)',
|
|
310
|
+
'exec.unknownResult': '执行结果未知(输出流中断)',
|
|
311
|
+
'exec.unaryResult': '老 Host:一次性结果(未观测退出码)',
|
|
312
|
+
'exec.done': '完成',
|
|
313
|
+
'exec.failed': '失败',
|
|
314
|
+
'exec.cancelled': '已取消',
|
|
315
|
+
'exec.exitCode': '退出码',
|
|
316
|
+
'exec.timedOut': '超时结束',
|
|
290
317
|
'scm.syncTitle': '同步远端',
|
|
291
318
|
'scm.syncText': '将执行 git pull 然后 git push。',
|
|
292
319
|
'scm.syncDone': '同步完成',
|
|
@@ -334,6 +361,47 @@ const zh: Record<string, string> = {
|
|
|
334
361
|
'scm.checkoutCommitTitle': '检出提交(分离头指针)',
|
|
335
362
|
'scm.checkoutCommitText': '将把远端工作区切到 {hash},处于分离头指针状态。',
|
|
336
363
|
'history.filter': '过滤提交(信息 / 作者 / 哈希)',
|
|
364
|
+
'history.details': '提交详情',
|
|
365
|
+
'history.content': '内容',
|
|
366
|
+
'history.diff': '差异',
|
|
367
|
+
'history.paneLabel': '提交详情面板',
|
|
368
|
+
'history.close': '关闭提交详情',
|
|
369
|
+
'history.copyHash': '复制完整哈希',
|
|
370
|
+
'history.parents': '父提交',
|
|
371
|
+
'history.contentHint': '单击在差异中查看,右键或行尾按钮在编辑器中打开',
|
|
372
|
+
'history.noFiles': '这个提交没有改动文件。',
|
|
373
|
+
'history.noTextDiff': '没有文本改动(二进制文件,或仅文件模式变化)。',
|
|
374
|
+
'history.binaryFile': '二进制文件,无法显示差异。',
|
|
375
|
+
'history.fileNotRendered': '已达渲染上限,该文件未展开显示(点文件头可展开)。',
|
|
376
|
+
'history.openInEditor': '在编辑器中打开(双栏对比)',
|
|
377
|
+
'history.revealInExplorer': '在文件目录中打开',
|
|
378
|
+
'history.openInTerminal': '在终端中打开',
|
|
379
|
+
'history.patchTruncated': '补丁较大,已截断显示。',
|
|
380
|
+
'history.foldFile': '折叠该文件',
|
|
381
|
+
'history.unfoldFile': '展开该文件',
|
|
382
|
+
'history.reload': '重新读取',
|
|
383
|
+
'history.patchNotLoaded': '补丁还没有读取。',
|
|
384
|
+
'history.resizeHint': '拖拽调整提交详情面板高度',
|
|
385
|
+
'history.filesCount': '{n} 个文件',
|
|
386
|
+
'history.filesCountFiltered': '{matched}/{total} 个文件',
|
|
387
|
+
'history.copyMessage': '复制提交信息',
|
|
388
|
+
'history.copyFilePatch': '复制该文件补丁',
|
|
389
|
+
'history.copyCommitPatch': '复制整个补丁',
|
|
390
|
+
'history.copiedMessage': '已复制提交信息',
|
|
391
|
+
'history.copiedHash': '已复制完整哈希',
|
|
392
|
+
'history.copiedSubject': '已复制提交标题',
|
|
393
|
+
'history.copiedFilePatch': '已复制该文件补丁',
|
|
394
|
+
'history.copiedCommitPatch': '已复制整个提交的补丁',
|
|
395
|
+
'history.copySubject': '复制提交标题',
|
|
396
|
+
'history.filterFiles': '过滤文件(路径)',
|
|
397
|
+
'history.noMatchFiles': '没有匹配的文件。',
|
|
398
|
+
'history.viewInDiff': '在差异中查看',
|
|
399
|
+
'history.wrap': '自动换行',
|
|
400
|
+
'history.prevFile': '上一个文件',
|
|
401
|
+
'history.nextFile': '下一个文件',
|
|
402
|
+
'history.expand': '展开详情面板',
|
|
403
|
+
'history.restore': '还原列表布局',
|
|
404
|
+
'history.diffToolbar': '补丁工具',
|
|
337
405
|
'editor.empty': '没有打开的文件。在资源管理器里双击文件,或在源代码管理里点击更改。',
|
|
338
406
|
'editor.saveHint': '保存',
|
|
339
407
|
'editor.preview': '预览',
|
|
@@ -494,7 +562,7 @@ const en: Record<string, string> = {
|
|
|
494
562
|
'route.off': 'Off (local passthrough)',
|
|
495
563
|
'route.shadow': 'Shadow (log only, still local)',
|
|
496
564
|
'route.enforce': 'Enforce (real routing)',
|
|
497
|
-
'route.hint': 'Applies immediately and persists to the profile; no restart needed.',
|
|
565
|
+
'route.hint': 'Applies immediately and persists to the profile; no restart needed. Default: enforce (real routing).',
|
|
498
566
|
'route.saving': 'Saving…',
|
|
499
567
|
'route.tombstones': 'tombstones',
|
|
500
568
|
'route.aliasPath': 'Local alias dir (native workspace entry)',
|
|
@@ -642,7 +710,27 @@ const en: Record<string, string> = {
|
|
|
642
710
|
'scm.discoverFound': 'Repositories found in the workspace',
|
|
643
711
|
'scm.discoverEmpty': 'No repository within 3 levels of the workspace root — enter a folder manually',
|
|
644
712
|
'scm.pushTitle': 'Push to remote',
|
|
645
|
-
'scm.pushText': '
|
|
713
|
+
'scm.pushText': 'Enter a push refspec (Gerrit: HEAD:refs/for/<branch>). Empty = plain git push to the current branch upstream; a non-empty value is remembered.',
|
|
714
|
+
'scm.pushRefspecPlaceholder': 'HEAD:refs/for/master',
|
|
715
|
+
'scm.pushRefspecDone': 'Pushed: {refspec}',
|
|
716
|
+
// -- streaming network verbs (fetch/pull/push/sync live output)
|
|
717
|
+
'exec.running': 'Running',
|
|
718
|
+
'exec.waiting': 'Waiting for remote output…',
|
|
719
|
+
'exec.copy': 'Copy output',
|
|
720
|
+
'exec.copied': 'Output copied',
|
|
721
|
+
'exec.cancel': 'Stop the remote git',
|
|
722
|
+
'exec.cancelHint': 'Kills the remote process group (TERM→KILL); the operation may be half-done — confirm first',
|
|
723
|
+
'exec.collapse': 'Collapse the log',
|
|
724
|
+
'exec.expand': 'Expand the log',
|
|
725
|
+
'exec.log': 'Execution output',
|
|
726
|
+
'exec.noOutput': '(no output)',
|
|
727
|
+
'exec.unknownResult': 'Result unknown (the output stream was interrupted)',
|
|
728
|
+
'exec.unaryResult': 'Old Host: one-shot result (exit code not observed)',
|
|
729
|
+
'exec.done': 'Done',
|
|
730
|
+
'exec.failed': 'Failed',
|
|
731
|
+
'exec.cancelled': 'Cancelled',
|
|
732
|
+
'exec.exitCode': 'exit code',
|
|
733
|
+
'exec.timedOut': 'timed out',
|
|
646
734
|
'scm.syncTitle': 'Sync with remote',
|
|
647
735
|
'scm.syncText': 'Runs git pull, then git push.',
|
|
648
736
|
'scm.syncDone': 'Sync finished',
|
|
@@ -690,6 +778,47 @@ const en: Record<string, string> = {
|
|
|
690
778
|
'scm.checkoutCommitTitle': 'Check out commit (detached HEAD)',
|
|
691
779
|
'scm.checkoutCommitText': 'The remote working tree switches to {hash} in detached HEAD state.',
|
|
692
780
|
'history.filter': 'Filter commits (message / author / hash)',
|
|
781
|
+
'history.details': 'Commit details',
|
|
782
|
+
'history.content': 'Contents',
|
|
783
|
+
'history.diff': 'Diff',
|
|
784
|
+
'history.paneLabel': 'Commit details pane',
|
|
785
|
+
'history.close': 'Close commit details',
|
|
786
|
+
'history.copyHash': 'Copy full hash',
|
|
787
|
+
'history.parents': 'Parents',
|
|
788
|
+
'history.contentHint': 'Click to view the diff, right-click or use the row button to open in the editor',
|
|
789
|
+
'history.noFiles': 'This commit changes no file.',
|
|
790
|
+
'history.noTextDiff': 'No text change (binary file, or file mode only).',
|
|
791
|
+
'history.binaryFile': 'Binary file: no diff to show.',
|
|
792
|
+
'history.fileNotRendered': 'Display row cap reached: this file is folded (click its header to unfold).',
|
|
793
|
+
'history.openInEditor': 'Open in editor (side-by-side)',
|
|
794
|
+
'history.revealInExplorer': 'Reveal in the file explorer',
|
|
795
|
+
'history.openInTerminal': 'Open in terminal',
|
|
796
|
+
'history.patchTruncated': 'Large patch: the display is truncated.',
|
|
797
|
+
'history.foldFile': 'Fold this file',
|
|
798
|
+
'history.unfoldFile': 'Unfold this file',
|
|
799
|
+
'history.reload': 'Reload',
|
|
800
|
+
'history.patchNotLoaded': 'The patch has not been loaded yet.',
|
|
801
|
+
'history.resizeHint': 'Drag to resize the commit details pane',
|
|
802
|
+
'history.filesCount': '{n} files',
|
|
803
|
+
'history.filesCountFiltered': '{matched}/{total} files',
|
|
804
|
+
'history.copyMessage': 'Copy commit message',
|
|
805
|
+
'history.copyFilePatch': 'Copy file patch',
|
|
806
|
+
'history.copyCommitPatch': 'Copy whole patch',
|
|
807
|
+
'history.copiedMessage': 'Commit message copied',
|
|
808
|
+
'history.copiedHash': 'Full hash copied',
|
|
809
|
+
'history.copiedSubject': 'Subject copied',
|
|
810
|
+
'history.copiedFilePatch': 'File patch copied',
|
|
811
|
+
'history.copiedCommitPatch': 'Commit patch copied',
|
|
812
|
+
'history.copySubject': 'Copy subject',
|
|
813
|
+
'history.filterFiles': 'Filter files (path)',
|
|
814
|
+
'history.noMatchFiles': 'No files match.',
|
|
815
|
+
'history.viewInDiff': 'Show in diff',
|
|
816
|
+
'history.wrap': 'Wrap long lines',
|
|
817
|
+
'history.prevFile': 'Previous file',
|
|
818
|
+
'history.nextFile': 'Next file',
|
|
819
|
+
'history.expand': 'Expand the details pane',
|
|
820
|
+
'history.restore': 'Restore the list layout',
|
|
821
|
+
'history.diffToolbar': 'Patch tools',
|
|
693
822
|
'editor.empty': 'No open file. Double-click a file in the explorer, or click a change in source control.',
|
|
694
823
|
'editor.saveHint': 'save',
|
|
695
824
|
'editor.preview': 'Preview',
|