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/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
- /* The submodule walk spawns one short `git submodule status` per level, so the
1105
- depth is capped the same way the directory scan caps its own, and every list
1106
- stays bounded so a pathological tree cannot flood the picker. */
1107
- const SUBMODULE_MAX_DEPTH = 2
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 nested ones are reached by recursing into
1150
- * each listed path. A path that is present on disk and passes
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 per-level `submodule status` calls are serial (a level's paths are only
1156
- * known once its parent answered), but the working-tree checks are not: they
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, depth = 1, prefix = '') {
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: `${prefix}${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
- /** The whole discovery pass, cached briefly per cwd: it walks the tree and
1201
- * spawns a process per checkout, while the panel polls several times a minute.
1202
- * A submodule appearing or disappearing is a deliberate reconfiguration, so a
1203
- * one-minute staleness is the right trade. */
1204
- const repoScanCache = new Map()
1205
- const REPO_SCAN_TTL_MS = 60_000
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 && cached.expires > Date.now()) return cached.rows.map((row) => ({ ...row }))
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, { rows: capped, expires: Date.now() + REPO_SCAN_TTL_MS })
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
- return { cwd, isRepo: rows.some((row) => row.kind === 'workspace'), repos: rows }
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() {