dsh-ide-git 0.14.5 → 0.15.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/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +871 -81
- package/src/index.js +110 -18
package/src/index.js
CHANGED
|
@@ -46,7 +46,7 @@ try {
|
|
|
46
46
|
/** Rail action ids, in rail order. MUST mirror RAIL_SPECS in src/client.js. */
|
|
47
47
|
const RAIL_CONFIG_IDS = [
|
|
48
48
|
'refresh', 'tree', 'float', 'split', 'fullscreen', 'newBranch', 'checkout', 'delete',
|
|
49
|
-
'compare', 'diff', 'stash', 'tag', 'favorite', 'fetch', 'pull', 'push',
|
|
49
|
+
'compare', 'diff', 'stash', 'tag', 'favorite', 'fetch', 'pull', 'push', 'more',
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
/** Volatile refs (dsh >= 0.1.7 settings contract) or plain values on older hosts. */
|
|
@@ -84,6 +84,21 @@ export const Config = Schema === null ? undefined : railConfigSchema(Schema)
|
|
|
84
84
|
|
|
85
85
|
const ROUTE_PREFIX = '/dsh-ide-git/api'
|
|
86
86
|
const MAX_OUTPUT_BYTES = 8 * 1024 * 1024
|
|
87
|
+
|
|
88
|
+
/* Ring buffer of the most recent git runs — the panel's "Git output" view
|
|
89
|
+
(VS Code's scm showOutput equivalent). Bounded on both axes: entries and
|
|
90
|
+
per-entry text, so a hostile `log -p` cannot grow it without limit. */
|
|
91
|
+
const GIT_LOG_MAX_ENTRIES = 200
|
|
92
|
+
const GIT_LOG_TEXT_MAX = 4000
|
|
93
|
+
const gitLog = []
|
|
94
|
+
function pushGitLog(entry) {
|
|
95
|
+
gitLog.push(entry)
|
|
96
|
+
if (gitLog.length > GIT_LOG_MAX_ENTRIES) gitLog.splice(0, gitLog.length - GIT_LOG_MAX_ENTRIES)
|
|
97
|
+
}
|
|
98
|
+
function clipGitLogText(text) {
|
|
99
|
+
if (typeof text !== 'string' || text.length <= GIT_LOG_TEXT_MAX) return text
|
|
100
|
+
return text.slice(0, GIT_LOG_TEXT_MAX / 2) + '\n…[' + String(text.length - GIT_LOG_TEXT_MAX) + ' more chars]…\n' + text.slice(-GIT_LOG_TEXT_MAX / 2)
|
|
101
|
+
}
|
|
87
102
|
const MAX_DIFF_CHARS = 400 * 1024
|
|
88
103
|
const DEFAULT_TIMEOUT_MS = 30_000
|
|
89
104
|
const NETWORK_TIMEOUT_MS = 180_000
|
|
@@ -118,6 +133,7 @@ function gitBinary() {
|
|
|
118
133
|
*/
|
|
119
134
|
function runGit(cwd, args, options = {}) {
|
|
120
135
|
const timeoutMs = options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs
|
|
136
|
+
const startedAt = Date.now()
|
|
121
137
|
return new Promise((resolve, reject) => {
|
|
122
138
|
let child
|
|
123
139
|
try {
|
|
@@ -135,6 +151,7 @@ function runGit(cwd, args, options = {}) {
|
|
|
135
151
|
if (settled) return
|
|
136
152
|
settled = true
|
|
137
153
|
child.kill('SIGKILL')
|
|
154
|
+
pushGitLog({ at: new Date(startedAt).toISOString(), argv: args.join(' '), code: 'timeout', ms: timeoutMs, out: '', err: 'timed out after ' + String(timeoutMs) + 'ms' })
|
|
138
155
|
reject(new PanelError('git-timeout', 'git ' + args[0] + ' timed out after ' + timeoutMs + 'ms', 504))
|
|
139
156
|
}, timeoutMs)
|
|
140
157
|
child.stdout.on('data', (chunk) => {
|
|
@@ -155,6 +172,14 @@ function runGit(cwd, args, options = {}) {
|
|
|
155
172
|
if (settled) return
|
|
156
173
|
settled = true
|
|
157
174
|
clearTimeout(timer)
|
|
175
|
+
pushGitLog({
|
|
176
|
+
at: new Date(startedAt).toISOString(),
|
|
177
|
+
argv: args.join(' '),
|
|
178
|
+
code: code === null ? -1 : code,
|
|
179
|
+
ms: Date.now() - startedAt,
|
|
180
|
+
out: clipGitLogText(Buffer.concat(out).toString('utf8')),
|
|
181
|
+
err: clipGitLogText(Buffer.concat(err).toString('utf8')),
|
|
182
|
+
})
|
|
158
183
|
resolve({
|
|
159
184
|
code: code === null ? -1 : code,
|
|
160
185
|
signal: signal === null ? undefined : signal,
|
|
@@ -436,7 +461,7 @@ function withRepoLock(key, work) {
|
|
|
436
461
|
const WRITE_METHODS = new Set([
|
|
437
462
|
'stage', 'unstage', 'discard', 'commit', 'checkout', 'branchCreate', 'branchRename',
|
|
438
463
|
'branchDelete', 'merge', 'rebase', 'cherryPick', 'revert', 'reset', 'fetch', 'pull',
|
|
439
|
-
'push', 'stashPush', 'stashApply', 'stashDrop', 'tagCreate', 'tagDelete', 'undoApply',
|
|
464
|
+
'push', 'stashPush', 'stashApply', 'stashDrop', 'tagCreate', 'tagDelete', 'undoApply', 'clone',
|
|
440
465
|
])
|
|
441
466
|
|
|
442
467
|
function lockKeyOf(payload) {
|
|
@@ -574,23 +599,42 @@ async function operationOf(root) {
|
|
|
574
599
|
async function summary(payload) {
|
|
575
600
|
const cwd = cwdOf(payload)
|
|
576
601
|
const root = await repoRootOf(cwd, payload)
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
602
|
+
/* Two-phase changes (issue #11): with `stats: false` the two `diff
|
|
603
|
+
--numstat` passes — the most expensive step once a tree holds thousands
|
|
604
|
+
of changes — are skipped, `statsDeferred: true` announces that the +/-
|
|
605
|
+
numbers arrive through the `stats` method instead. The rows themselves
|
|
606
|
+
are complete without them; nothing is truncated. */
|
|
607
|
+
const deferStats = payload.stats === false
|
|
608
|
+
/* The branch chain (branch → upstream → ahead/behind) is sequential by
|
|
609
|
+
necessity; everything else is independent and runs concurrently, so a
|
|
610
|
+
large `status` scan no longer serialises behind five cheap calls
|
|
611
|
+
(issue #11: the sum of ~ten sequential commands became the max of two
|
|
612
|
+
chains). */
|
|
613
|
+
const [branchInfo, statusOut, unstagedStat, stagedStat, ignored, stashCount, worktrees, operation] = await Promise.all([
|
|
614
|
+
(async () => {
|
|
615
|
+
const branch = await currentBranchOf(root)
|
|
616
|
+
const detached = branch === 'HEAD'
|
|
617
|
+
const upstream = detached ? null : await upstreamOf(root, branch)
|
|
618
|
+
const tracking = upstream === null ? { ahead: 0, behind: 0 } : await aheadBehindOf(root, upstream, branch)
|
|
619
|
+
return { branch, detached, upstream, tracking }
|
|
620
|
+
})(),
|
|
621
|
+
git(root, ['status', '--porcelain=v1', '-z', '--untracked-files=all']),
|
|
622
|
+
deferStats ? Promise.resolve(new Map()) : numstatOf(root, []),
|
|
623
|
+
deferStats ? Promise.resolve(new Map()) : numstatOf(root, ['--cached']),
|
|
624
|
+
payload.ignored === true ? ignoredEntriesOf(root) : { entries: [], truncated: false },
|
|
625
|
+
stashCountOf(root),
|
|
626
|
+
worktreesOf(root),
|
|
627
|
+
operationOf(root),
|
|
628
|
+
])
|
|
582
629
|
const groups = classify(parsePorcelain(statusOut))
|
|
583
|
-
const ignored = payload.ignored === true ? await ignoredEntriesOf(root) : { entries: [], truncated: false }
|
|
584
|
-
const unstagedStat = await numstatOf(root, [])
|
|
585
|
-
const stagedStat = await numstatOf(root, ['--cached'])
|
|
586
630
|
return {
|
|
587
631
|
repoRoot: root,
|
|
588
632
|
cwd,
|
|
589
|
-
branch,
|
|
590
|
-
detached,
|
|
591
|
-
upstream,
|
|
592
|
-
ahead: tracking.ahead,
|
|
593
|
-
behind: tracking.behind,
|
|
633
|
+
branch: branchInfo.branch,
|
|
634
|
+
detached: branchInfo.detached,
|
|
635
|
+
upstream: branchInfo.upstream,
|
|
636
|
+
ahead: branchInfo.tracking.ahead,
|
|
637
|
+
behind: branchInfo.tracking.behind,
|
|
594
638
|
changes: {
|
|
595
639
|
staged: decorate(groups.staged, stagedStat),
|
|
596
640
|
unstaged: decorate(groups.unstaged, unstagedStat),
|
|
@@ -599,10 +643,29 @@ async function summary(payload) {
|
|
|
599
643
|
ignored: ignored.entries,
|
|
600
644
|
ignoredTruncated: ignored.truncated,
|
|
601
645
|
},
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
646
|
+
statsDeferred: deferStats === true,
|
|
647
|
+
stashCount: stashCount,
|
|
648
|
+
worktrees: worktrees,
|
|
649
|
+
operation: operation,
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/* The second phase of the two-phase changes (issue #11): the two numstat
|
|
654
|
+
passes, concurrent, packed as [path, additions, deletions, binary] rows so
|
|
655
|
+
they survive JSON. Paths are post-rename paths — exactly what `summary`
|
|
656
|
+
decorates its entries with. Read-only: deliberately not in WRITE_METHODS. */
|
|
657
|
+
async function stats(payload) {
|
|
658
|
+
const root = await repoRootOf(cwdOf(payload), payload)
|
|
659
|
+
const [unstaged, staged] = await Promise.all([
|
|
660
|
+
numstatOf(root, []),
|
|
661
|
+
numstatOf(root, ['--cached']),
|
|
662
|
+
])
|
|
663
|
+
const pack = (map) => {
|
|
664
|
+
const rows = []
|
|
665
|
+
for (const [path, stat] of map) rows.push([path, stat.additions, stat.deletions, stat.binary === true])
|
|
666
|
+
return rows
|
|
605
667
|
}
|
|
668
|
+
return { unstaged: pack(unstaged), staged: pack(staged) }
|
|
606
669
|
}
|
|
607
670
|
|
|
608
671
|
async function branches(payload) {
|
|
@@ -1052,6 +1115,32 @@ async function tagDelete(payload) {
|
|
|
1052
1115
|
return { tag }
|
|
1053
1116
|
}
|
|
1054
1117
|
|
|
1118
|
+
/* VS Code's SCM title "Clone..." — clone a URL into an absolute local path.
|
|
1119
|
+
The PARENT of the target must exist; git creates the last component itself
|
|
1120
|
+
and refuses a non-empty one, which is the safety net for typos. It writes a
|
|
1121
|
+
brand-new tree (never touches an existing repository), but it does write the
|
|
1122
|
+
filesystem, so it rides the write queue like everything else that mutates. */
|
|
1123
|
+
const CLONE_TIMEOUT_MS = 10 * 60 * 1000
|
|
1124
|
+
function requireUrl(value, label) {
|
|
1125
|
+
if (typeof value !== 'string' || value.trim() === '') throw badRequest(label + ' is required')
|
|
1126
|
+
const text = value.trim()
|
|
1127
|
+
if (text.startsWith('-') === true || /[\s\u0000-\u001f]/.test(text) === true || text.length > 2048) {
|
|
1128
|
+
throw badRequest(label + ' is not a valid repository URL')
|
|
1129
|
+
}
|
|
1130
|
+
return text
|
|
1131
|
+
}
|
|
1132
|
+
async function clone(payload) {
|
|
1133
|
+
const url = requireUrl(payload.url, 'url')
|
|
1134
|
+
const dir = requireAbsolute(payload.dir, 'dir')
|
|
1135
|
+
await git(path.dirname(dir), ['clone', url, dir], { timeoutMs: CLONE_TIMEOUT_MS })
|
|
1136
|
+
return { dir }
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/* The tail of the git run ring buffer (see runGit). Read-only, global. */
|
|
1140
|
+
async function gitLogs() {
|
|
1141
|
+
return { entries: gitLog.slice(-GIT_LOG_MAX_ENTRIES) }
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1055
1144
|
/** What this repository can still undo right now (newest last). */
|
|
1056
1145
|
async function undoList(payload) {
|
|
1057
1146
|
const cwd = cwdOf(payload)
|
|
@@ -1666,6 +1755,7 @@ const METHODS = {
|
|
|
1666
1755
|
'commit-efforts': commitEfforts,
|
|
1667
1756
|
repos,
|
|
1668
1757
|
summary,
|
|
1758
|
+
stats,
|
|
1669
1759
|
branches,
|
|
1670
1760
|
log,
|
|
1671
1761
|
commitDetail,
|
|
@@ -1693,6 +1783,8 @@ const METHODS = {
|
|
|
1693
1783
|
stashDrop,
|
|
1694
1784
|
tagCreate,
|
|
1695
1785
|
tagDelete,
|
|
1786
|
+
clone,
|
|
1787
|
+
gitLogs,
|
|
1696
1788
|
undoList,
|
|
1697
1789
|
undoApply,
|
|
1698
1790
|
}
|