dsh-ide-git 0.13.2 → 0.13.4
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 +691 -40
- package/src/index.js +39 -34
package/src/index.js
CHANGED
|
@@ -1101,10 +1101,10 @@ async function compare(payload) {
|
|
|
1101
1101
|
/** Directories never descended into while looking for repositories. */
|
|
1102
1102
|
const REPO_SCAN_SKIP = new Set(['node_modules', 'dist', 'build', 'out', 'target', 'vendor', 'venv', '.venv', '__pycache__', 'coverage', 'tmp', 'temp'])
|
|
1103
1103
|
|
|
1104
|
-
/*
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1104
|
+
/* `git submodule status --recursive` already reports every nesting level of a
|
|
1105
|
+
repository in ONE call (paths arrive fully qualified), so the submodule walk
|
|
1106
|
+
costs exactly one `status` per checkout; the lists stay bounded so a
|
|
1107
|
+
pathological tree cannot flood the picker. */
|
|
1108
1108
|
const SUBMODULE_LIST_LIMIT = 400
|
|
1109
1109
|
const REPO_LIST_LIMIT = 60
|
|
1110
1110
|
|
|
@@ -1146,18 +1146,21 @@ function repoRow(root, submodulePath, kind) {
|
|
|
1146
1146
|
*
|
|
1147
1147
|
* Git itself is the source of truth: `git submodule status --recursive` lists
|
|
1148
1148
|
* the registered paths (relative to the parent repository) even for submodules
|
|
1149
|
-
* that were never initialised, and
|
|
1150
|
-
*
|
|
1149
|
+
* that were never initialised, and `--recursive` walks EVERY nesting level in
|
|
1150
|
+
* that one call — deeper entries arrive as fully qualified paths
|
|
1151
|
+
* ("outer/inner"), so one `status` per checkout is the whole discovery. (An
|
|
1152
|
+
* earlier revision also re-ran `status` inside every confirmed submodule; those
|
|
1153
|
+
* calls could only re-list checkouts the recursive pass had already reported,
|
|
1154
|
+
* which multiplied the process count on a workspace full of checkouts without
|
|
1155
|
+
* producing a single new row.) A path that is present on disk and passes
|
|
1151
1156
|
* `rev-parse --show-toplevel` is a real, independent working tree — the parent
|
|
1152
1157
|
* only ever sees it as one gitlink line, so this is what makes it selectable
|
|
1153
1158
|
* as a repository of its own (the SourceTree behavior).
|
|
1154
1159
|
*
|
|
1155
|
-
* The
|
|
1156
|
-
*
|
|
1157
|
-
* all run at once, because a repository with a dozen submodules otherwise
|
|
1158
|
-
* costs a dozen sequential processes on every scan.
|
|
1160
|
+
* The working-tree checks all run at once, because a repository with a dozen
|
|
1161
|
+
* submodules otherwise costs a dozen sequential processes on every scan.
|
|
1159
1162
|
*/
|
|
1160
|
-
async function collectSubmoduleRepos(root
|
|
1163
|
+
async function collectSubmoduleRepos(root) {
|
|
1161
1164
|
const found = []
|
|
1162
1165
|
const listing = await runGit(root, ['submodule', 'status', '--recursive'])
|
|
1163
1166
|
if (listing.code !== 0) return found
|
|
@@ -1174,42 +1177,42 @@ async function collectSubmoduleRepos(root, depth = 1, prefix = '') {
|
|
|
1174
1177
|
/* An uninitialised submodule has no `.git` yet: git still registers it, but
|
|
1175
1178
|
there is no working tree to bind the panel to. */
|
|
1176
1179
|
if (!existsSync(path.join(absolute, '.git'))) continue
|
|
1177
|
-
candidates.push({ absolute, rel
|
|
1180
|
+
candidates.push({ absolute, rel })
|
|
1178
1181
|
}
|
|
1179
1182
|
const tops = await Promise.all(candidates.map((entry) => runGit(entry.absolute, ['rev-parse', '--show-toplevel'])))
|
|
1180
|
-
const next = []
|
|
1181
1183
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
1182
1184
|
const top = tops[index]
|
|
1183
1185
|
if (top.code !== 0) continue
|
|
1184
1186
|
const reported = top.stdout.trim().replace(/\\/g, '/').replace(/\/+$/, '')
|
|
1185
1187
|
found.push({ root: reported === '' ? candidates[index].absolute : reported, rel: candidates[index].rel })
|
|
1186
|
-
if (depth < SUBMODULE_MAX_DEPTH) next.push(candidates[index])
|
|
1187
|
-
}
|
|
1188
|
-
if (depth < SUBMODULE_MAX_DEPTH) {
|
|
1189
|
-
const deeper = await Promise.all(next.map((entry) => collectSubmoduleRepos(entry.absolute, depth + 1, `${entry.rel}/`)))
|
|
1190
|
-
for (const list of deeper) {
|
|
1191
|
-
for (const entry of list) {
|
|
1192
|
-
if (found.length >= SUBMODULE_LIST_LIMIT) break
|
|
1193
|
-
found.push({ root: entry.root, rel: entry.rel })
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
1188
|
}
|
|
1197
1189
|
return found
|
|
1198
1190
|
}
|
|
1199
1191
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1192
|
+
/* The whole discovery pass is cached in-process per cwd: one scan walks the
|
|
1193
|
+
tree and spawns a git process per checkout, while the client re-POSTs `repos`
|
|
1194
|
+
on every panel mount — a warm answer is what keeps switching back to the Git
|
|
1195
|
+
page instant on a workspace full of checkouts. Entries are { at, rows };
|
|
1196
|
+
REPOS_CACHE_TTL_MS is a module-level `let` so tests can pin it to 0 and
|
|
1197
|
+
observe the always-rescan path.
|
|
1198
|
+
|
|
1199
|
+
The cache is deliberately NOT invalidated when a write method succeeds:
|
|
1200
|
+
`repos` is a read-only discovery of which checkouts EXIST, and no write
|
|
1201
|
+
method changes that set — a commit or a new branch does not conjure a
|
|
1202
|
+
repository. A clone landing inside the 15s window simply stays invisible
|
|
1203
|
+
until the next scan; that is an accepted trade, the client's background
|
|
1204
|
+
refresh is the safety net. */
|
|
1205
|
+
export const repoScanCache = new Map()
|
|
1206
|
+
export let REPOS_CACHE_TTL_MS = 15000
|
|
1206
1207
|
|
|
1207
1208
|
async function scanRepos(cwd) {
|
|
1208
1209
|
const cached = repoScanCache.get(cwd)
|
|
1209
1210
|
/* Hand back fresh row objects: the cache is shared by every caller, and a
|
|
1210
1211
|
consumer that decorates or reorders what it received must not write
|
|
1211
1212
|
through into the next request's answer. */
|
|
1212
|
-
if (cached !== undefined &&
|
|
1213
|
+
if (cached !== undefined && Date.now() - cached.at < REPOS_CACHE_TTL_MS) {
|
|
1214
|
+
return { cached: true, rows: cached.rows.map((row) => ({ ...row })) }
|
|
1215
|
+
}
|
|
1213
1216
|
const list = []
|
|
1214
1217
|
const self = await runGit(cwd, ['rev-parse', '--show-toplevel'])
|
|
1215
1218
|
const isRepo = self.code === 0
|
|
@@ -1239,8 +1242,8 @@ async function scanRepos(cwd) {
|
|
|
1239
1242
|
const capped = rows.slice(0, REPO_LIST_LIMIT).map((row) => ({ ...row }))
|
|
1240
1243
|
const branches = await Promise.all(capped.map((row) => branchOrNull(row.path)))
|
|
1241
1244
|
for (let index = 0; index < capped.length; index += 1) capped[index].branch = branches[index]
|
|
1242
|
-
repoScanCache.set(cwd, {
|
|
1243
|
-
return capped.map((row) => ({ ...row }))
|
|
1245
|
+
repoScanCache.set(cwd, { at: Date.now(), rows: capped })
|
|
1246
|
+
return { cached: false, rows: capped.map((row) => ({ ...row })) }
|
|
1244
1247
|
}
|
|
1245
1248
|
|
|
1246
1249
|
/** Depth-capped search for repositories nested in a workspace. */
|
|
@@ -1280,8 +1283,10 @@ async function branchOrNull(root) {
|
|
|
1280
1283
|
*/
|
|
1281
1284
|
async function repos(payload) {
|
|
1282
1285
|
const cwd = cwdOf(payload)
|
|
1283
|
-
const rows = await scanRepos(cwd)
|
|
1284
|
-
|
|
1286
|
+
const { cached, rows } = await scanRepos(cwd)
|
|
1287
|
+
/* `cached: true` rides a cache hit only; the client ignores it today, it just
|
|
1288
|
+
makes the answer say where it came from. */
|
|
1289
|
+
return { cwd, isRepo: rows.some((row) => row.kind === 'workspace'), repos: rows, ...(cached ? { cached: true } : {}) }
|
|
1285
1290
|
}
|
|
1286
1291
|
|
|
1287
1292
|
async function version() {
|