@youtyan/code-viewer 0.6.0 → 0.6.2

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.
@@ -1438,7 +1438,19 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
1438
1438
  return "internal";
1439
1439
  return omitDirNames.has(name) ? "heavy" : undefined;
1440
1440
  }
1441
- function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames) {
1441
+ function worktreeSubmodulePaths(cwd) {
1442
+ if (!existsSync(join2(cwd, ".gitmodules")))
1443
+ return new Set;
1444
+ const res = run(["git", "config", "--file", ".gitmodules", "--get-regexp", "\\.path$"], cwd);
1445
+ if (res.code !== 0)
1446
+ return new Set;
1447
+ return new Set(res.stdout.split(`
1448
+ `).map((line) => {
1449
+ const split = line.indexOf(" ");
1450
+ return split >= 0 ? normalizeTreePath(line.slice(split + 1)) : "";
1451
+ }).filter(Boolean));
1452
+ }
1453
+ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames, submodulePaths) {
1442
1454
  if (excludeNames.has(name.toLowerCase()))
1443
1455
  return {
1444
1456
  name,
@@ -1448,23 +1460,24 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, exc
1448
1460
  const entryPath = base ? `${base}/${name}` : name;
1449
1461
  const type = isDirectory ? hasDotGitEntry(join2(dir, name)) ? "commit" : "tree" : "blob";
1450
1462
  const omittedReason = type === "tree" ? omittedWorktreeDirectoryReason(name, omitDirNames) : undefined;
1463
+ const submodule = type === "commit" && submodulePaths.has(entryPath) ? true : undefined;
1464
+ const baseEntry = submodule ? { name, path: entryPath, type, submodule } : { name, path: entryPath, type };
1451
1465
  return omittedReason ? {
1452
- name,
1453
- path: entryPath,
1454
- type,
1466
+ ...baseEntry,
1455
1467
  children_omitted: true,
1456
1468
  children_omitted_reason: omittedReason
1457
- } : { name, path: entryPath, type };
1469
+ } : baseEntry;
1458
1470
  }
1459
1471
  function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
1460
1472
  const base = normalizeTreePath(path);
1461
1473
  const root = join2(cwd, base);
1462
1474
  const omitDirNameSet = new Set(omitDirNames);
1463
1475
  const excludeNameSet = new Set(excludeNames.map((name) => name.toLowerCase()));
1476
+ const submodulePaths = worktreeSubmodulePaths(cwd);
1464
1477
  let directEntries;
1465
1478
  try {
1466
1479
  const dirents = readdirSync(root, { withFileTypes: true });
1467
- directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet)).filter((entry) => entry.path));
1480
+ directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet, submodulePaths)).filter((entry) => entry.path));
1468
1481
  } catch {
1469
1482
  return [];
1470
1483
  }
@@ -3765,6 +3778,93 @@ Default (non --json) output:
3765
3778
  ]);
3766
3779
  });
3767
3780
 
3781
+ // web-src/core/routes.ts
3782
+ function assertNever(value) {
3783
+ throw new Error(`unhandled route: ${JSON.stringify(value)}`);
3784
+ }
3785
+ function formatLineTarget(line) {
3786
+ return typeof line === "number" ? String(line) : `${line.start}-${line.end}`;
3787
+ }
3788
+ function buildRoute(route) {
3789
+ switch (route.screen) {
3790
+ case "repo": {
3791
+ const params = new URLSearchParams;
3792
+ if (route.ref && route.ref !== "worktree")
3793
+ params.set("ref", route.ref);
3794
+ if (route.path)
3795
+ params.set("path", route.path);
3796
+ const qs = params.toString();
3797
+ return `/${qs ? `?${qs}` : ""}`;
3798
+ }
3799
+ case "file":
3800
+ if (route.view === "blob") {
3801
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=blob" + (route.preview ? "&preview=1" : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
3802
+ }
3803
+ if (route.view === "blame") {
3804
+ const ref = route.ref || "worktree";
3805
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(ref) + "&view=blame" + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
3806
+ }
3807
+ if (route.view === "history") {
3808
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=history" + (route.commit ? `&commit=${encodeURIComponent(route.commit)}` : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
3809
+ }
3810
+ return "/file?path=" + encodeURIComponent(route.path) + "&ref=" + encodeURIComponent(route.ref || "worktree") + "&from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
3811
+ case "diff":
3812
+ return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.path ? `&path=${encodeURIComponent(route.path)}` : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
3813
+ case "help": {
3814
+ const params = new URLSearchParams;
3815
+ if (route.lang && route.lang !== "en")
3816
+ params.set("lang", route.lang);
3817
+ if (route.section && route.section !== "overview")
3818
+ params.set("section", route.section);
3819
+ const qs = params.toString();
3820
+ return `/help${qs ? `?${qs}` : ""}`;
3821
+ }
3822
+ case "history": {
3823
+ const params = new URLSearchParams;
3824
+ if (route.ref && route.ref !== "HEAD")
3825
+ params.set("ref", route.ref);
3826
+ if (route.commit)
3827
+ params.set("commit", route.commit);
3828
+ const qs = params.toString();
3829
+ return `/history${qs ? `?${qs}` : ""}`;
3830
+ }
3831
+ case "database": {
3832
+ const params = new URLSearchParams;
3833
+ if (route.db)
3834
+ params.set("db", route.db);
3835
+ if (route.schema)
3836
+ params.set("schema", route.schema);
3837
+ if (route.table)
3838
+ params.set("table", route.table);
3839
+ if (route.tab)
3840
+ params.set("tab", route.tab);
3841
+ if (route.diffBefore)
3842
+ params.set("diffBefore", route.diffBefore);
3843
+ if (route.diffAfter)
3844
+ params.set("diffAfter", route.diffAfter);
3845
+ const qs = params.toString();
3846
+ return `/database${qs ? `?${qs}` : ""}`;
3847
+ }
3848
+ case "unknown":
3849
+ return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
3850
+ default:
3851
+ return assertNever(route);
3852
+ }
3853
+ }
3854
+ var SPA_PATHS, APP_ENTRY_PATHS;
3855
+ var init_routes = __esm(() => {
3856
+ SPA_PATHS = [
3857
+ "/todif",
3858
+ "/todiff",
3859
+ "/file",
3860
+ "/help",
3861
+ "/history",
3862
+ "/database",
3863
+ "/doctor"
3864
+ ];
3865
+ APP_ENTRY_PATHS = ["/", "/index.html"];
3866
+ });
3867
+
3768
3868
  // web-src/server/query-cli.ts
3769
3869
  var exports_query_cli = {};
3770
3870
  __export(exports_query_cli, {
@@ -5149,6 +5249,18 @@ function buildSnapshotPollCommand(serverUrl, db, schema) {
5149
5249
  const schemaArg = schema ? ` --schema ${shellSingleQuote(schema)}` : "";
5150
5250
  return `${cli} snapshot list --db ${shellSingleQuote(db)}${schemaArg} --json`;
5151
5251
  }
5252
+ function buildSnapshotDiffUrl(serverUrl, dbId, schema, beforeId, afterId) {
5253
+ const path = buildRoute({
5254
+ screen: "database",
5255
+ db: dbId,
5256
+ schema,
5257
+ tab: "snapshot",
5258
+ diffBefore: beforeId,
5259
+ diffAfter: afterId,
5260
+ range: { from: "", to: "" }
5261
+ });
5262
+ return new URL(path, serverUrl).toString();
5263
+ }
5152
5264
  function buildDiffRowsCommand(serverUrl, before, after, table) {
5153
5265
  const cli = `code-viewer query --server ${shellSingleQuote(serverUrl)}`;
5154
5266
  return `${cli} diff rows --before ${shellSingleQuote(before)} ` + `--after ${shellSingleQuote(after)} --table ${shellSingleQuote(table)} --json`;
@@ -5301,6 +5413,7 @@ async function runDiffTables(serverUrl, command) {
5301
5413
  const body = await requestJson(serverUrl, `/_db/snapshot/diff/tables${qs}`, "GET", undefined, "diff tables");
5302
5414
  const enriched = {
5303
5415
  ...body,
5416
+ diffUrl: buildSnapshotDiffUrl(serverUrl, body.dbId, body.schema, body.beforeId, body.afterId),
5304
5417
  tables: body.tables.map((t) => ({
5305
5418
  ...t,
5306
5419
  diffRowsCommand: buildDiffRowsCommand(serverUrl, body.beforeId, body.afterId, t.tableName)
@@ -5312,8 +5425,10 @@ async function runDiffTables(serverUrl, command) {
5312
5425
  }
5313
5426
  if (!enriched.tables.length) {
5314
5427
  console.log("no tables in diff");
5428
+ console.log(`# view in browser: ${enriched.diffUrl}`);
5315
5429
  return;
5316
5430
  }
5431
+ console.log(`# view in browser: ${enriched.diffUrl}`);
5317
5432
  for (const t of enriched.tables) {
5318
5433
  const cov = t.coverage === "both" ? "" : ` (${t.coverage})`;
5319
5434
  console.log(`${t.tableName} +${t.insertedCount} ~${t.updatedCount} -${t.deletedCount} =${t.unchangedCount}${cov}`);
@@ -5570,10 +5685,13 @@ no separate stored diff entity, so you always pass both snapshot ids.
5570
5685
  code-viewer query snapshot list --db app.db --json
5571
5686
 
5572
5687
  5. View the diff (per-table summary, then per-row detail). diff tables
5573
- prints each per-table summary line plus a paste-safe "# diff rows: ..."
5574
- hint right below it, and --json adds a diffRowsCommand field to each
5575
- tables[] element, so you can drill into row detail without rebuilding
5576
- the command yourself:
5688
+ prints a "# view in browser: <url>" hint up top (opens the same diff in
5689
+ the human's browser, Database > Snapshot tab), then each per-table
5690
+ summary line plus a paste-safe "# diff rows: ..." hint right below it.
5691
+ --json adds the same browser link as a diffUrl field and a
5692
+ diffRowsCommand field on each tables[] element, so you can hand the
5693
+ human a direct link or drill into row detail without rebuilding the
5694
+ command yourself:
5577
5695
  code-viewer query diff tables --before snap-abc123 --after snap-def456 --json
5578
5696
  code-viewer query diff rows --before snap-abc123 --after snap-def456 \\
5579
5697
  --table users --json
@@ -5760,10 +5878,13 @@ object bytes (text-shaped objects are previewable via \`s3 text\`).
5760
5878
  - diff tables: human-readable lines (default) plus a paste-safe
5761
5879
  "# diff rows: code-viewer query --server '<url>' diff rows --before '<id>'
5762
5880
  --after '<id>' --table '<table>' --json" comment line right below each table,
5763
- so AI/human can drill into row detail without rebuilding the command. With
5764
- --json the full /_db/snapshot/diff/tables payload is emitted and each
5765
- tables[] element gains an additive diffRowsCommand field with the same
5766
- literal. server URL / snapshot ids / table names are POSIX single-quoted.
5881
+ so AI/human can drill into row detail without rebuilding the command. A
5882
+ "# view in browser: <url>" hint is printed once up top — opens the same
5883
+ before/after comparison in the human's browser (Database > Snapshot tab).
5884
+ With --json the full /_db/snapshot/diff/tables payload is emitted, the
5885
+ top-level diffUrl field carries the same browser link, and each tables[]
5886
+ element gains an additive diffRowsCommand field with the same literal.
5887
+ server URL / snapshot ids / table names are POSIX single-quoted.
5767
5888
  - snapshot create: prints "snapshot started" immediately with the snapshotId.
5768
5889
  The no-wait output also includes a paste-safe poll command that pins
5769
5890
  --server '<url>' and single-quotes db/schema so AI/human paste does not
@@ -5843,6 +5964,7 @@ object bytes (text-shaped objects are previewable via \`s3 text\`).
5843
5964
  server only scans text-like columns (faster, cheaper).
5844
5965
  `, VALUE_FLAGS2, BOOL_FLAGS2, DEFAULT_SEARCH_TIMEOUT_SEC = 60, DEFAULT_SNAPSHOT_WAIT_TIMEOUT_SEC = 120, REDIS_ACTION_ALLOWLIST, NON_REDIS_SUBCOMMAND_ALLOWLIST, ES_ACTION_ALLOWLIST, S3_ACTION_ALLOWLIST;
5845
5966
  var init_query_cli = __esm(() => {
5967
+ init_routes();
5846
5968
  init_cli_helpers();
5847
5969
  init_cli_helpers();
5848
5970
  VALUE_FLAGS2 = new Set([
@@ -10039,6 +10161,11 @@ var init_source_meta = __esm(() => {
10039
10161
  "conf",
10040
10162
  "env",
10041
10163
  "properties",
10164
+ "rules",
10165
+ "rule",
10166
+ "prompt",
10167
+ "prompts",
10168
+ "instructions",
10042
10169
  "gitignore",
10043
10170
  "dockerignore",
10044
10171
  "editorconfig",
@@ -11775,6 +11902,15 @@ function parseDockerDbId(dbId) {
11775
11902
  return null;
11776
11903
  return { serviceName: rest, relDir: "", database };
11777
11904
  }
11905
+ function canonicalizeDockerDbId(dbId) {
11906
+ const parsed = parseDockerDbId(dbId);
11907
+ if (!parsed)
11908
+ return null;
11909
+ const database = parsed.database ? `:${parsed.database}` : "";
11910
+ if (!parsed.relDir)
11911
+ return `docker:${parsed.serviceName}${database}`;
11912
+ return `docker:${parsed.serviceName}@${encodeURIComponent(parsed.relDir)}${database}`;
11913
+ }
11778
11914
  async function findDockerServiceByDbIdAsync(cwd, dbId, kind, omitDirNames, signal) {
11779
11915
  const parsed = parseDockerDbId(dbId);
11780
11916
  if (!parsed)
@@ -12194,6 +12330,9 @@ function sanitizeSettings(raw) {
12194
12330
  const annotationPanelOpen = optionalBoolean(raw.annotationPanelOpen);
12195
12331
  if (annotationPanelOpen !== undefined)
12196
12332
  out.annotationPanelOpen = annotationPanelOpen;
12333
+ const annotationPanelWidth = optionalNumber(raw.annotationPanelWidth, 260, 720);
12334
+ if (annotationPanelWidth !== undefined)
12335
+ out.annotationPanelWidth = annotationPanelWidth;
12197
12336
  const annotationFollow = optionalBoolean(raw.annotationFollow);
12198
12337
  if (annotationFollow !== undefined)
12199
12338
  out.annotationFollow = annotationFollow;
@@ -14175,6 +14314,9 @@ async function getStoreDb(cwd) {
14175
14314
  try {
14176
14315
  storeDb.exec("ALTER TABLE snapshots ADD COLUMN schema_name TEXT");
14177
14316
  } catch {}
14317
+ try {
14318
+ storeDb.exec("ALTER TABLE snapshot_tables ADD COLUMN revision_id TEXT");
14319
+ } catch {}
14178
14320
  return storeDb;
14179
14321
  }
14180
14322
  function makeId2(prefix) {
@@ -14183,32 +14325,163 @@ function makeId2(prefix) {
14183
14325
  function hashPayload(payloadJson) {
14184
14326
  return createHash5("sha256").update(payloadJson).digest("hex");
14185
14327
  }
14328
+ function hashLengthPrefixed(hasher, value) {
14329
+ hasher.update(`${Buffer.byteLength(value, "utf8")}:`);
14330
+ hasher.update(value);
14331
+ hasher.update(`
14332
+ `);
14333
+ }
14334
+ function deleteOrphanPayloads(db) {
14335
+ db.prepare(`DELETE FROM snapshot_payloads
14336
+ WHERE NOT EXISTS (
14337
+ SELECT 1 FROM snapshot_rows
14338
+ WHERE snapshot_rows.payload_hash = snapshot_payloads.payload_hash
14339
+ )
14340
+ AND NOT EXISTS (
14341
+ SELECT 1 FROM snapshot_table_revision_rows
14342
+ WHERE snapshot_table_revision_rows.payload_hash = snapshot_payloads.payload_hash
14343
+ )`).run();
14344
+ }
14345
+ function dockerDbIdFilterValues(dbId) {
14346
+ const values = [dbId];
14347
+ const canonical = canonicalizeDockerDbId(dbId);
14348
+ if (canonical)
14349
+ values.push(canonical);
14350
+ const parsed = parseDockerDbId(dbId);
14351
+ if (parsed?.relDir) {
14352
+ const database = parsed.database ? `:${parsed.database}` : "";
14353
+ values.push(`docker:${parsed.serviceName}@${parsed.relDir}${database}`);
14354
+ }
14355
+ return [...new Set(values)];
14356
+ }
14357
+ function getSnapshotScopeRow(db, snapshotId) {
14358
+ const row = db.prepare("SELECT db_id, COALESCE(schema_name, 'public') AS schema_name FROM snapshots WHERE id = ?").get(snapshotId);
14359
+ if (!row)
14360
+ throw new Error(`snapshot not found: ${snapshotId}`);
14361
+ return { dbId: row.db_id, schema: row.schema_name };
14362
+ }
14186
14363
  async function createSnapshot(cwd, dbId, kind, tables, note, schema) {
14187
14364
  const db = await getStoreDb(cwd);
14188
14365
  const id = makeId2("snap");
14189
- db.prepare("INSERT INTO snapshots (id, db_id, schema_name, kind, note, created_at, status) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, dbId, schema ?? null, kind, note, new Date().toISOString(), "running");
14366
+ const storedDbId = canonicalizeDockerDbId(dbId) ?? dbId;
14367
+ db.prepare("INSERT INTO snapshots (id, db_id, schema_name, kind, note, created_at, status) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, storedDbId, schema ?? null, kind, note, new Date().toISOString(), "running");
14190
14368
  for (const t of tables) {
14191
14369
  db.prepare("INSERT INTO snapshot_tables (snapshot_id, table_name) VALUES (?, ?)").run(id, t);
14192
14370
  }
14193
14371
  return id;
14194
14372
  }
14195
- async function addSnapshotTableData(cwd, snapshotId, tableName, pkColumns, rows) {
14373
+ async function beginSnapshotTableRevision(cwd, snapshotId, tableName, pkColumns) {
14374
+ const db = await getStoreDb(cwd);
14375
+ const id = makeId2("rev");
14376
+ const scope = getSnapshotScopeRow(db, snapshotId);
14377
+ db.exec("BEGIN");
14378
+ try {
14379
+ db.prepare("INSERT OR IGNORE INTO snapshot_tables (snapshot_id, table_name) VALUES (?, ?)").run(snapshotId, tableName);
14380
+ db.prepare(`INSERT INTO snapshot_table_revisions
14381
+ (id, source_snapshot_id, db_id, schema_name, table_name, row_count, table_hash, hash_version, pk_columns_json, status, created_at)
14382
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, snapshotId, scope.dbId, scope.schema, tableName, 0, "", SNAPSHOT_TABLE_HASH_VERSION, JSON.stringify(pkColumns), "building", new Date().toISOString());
14383
+ db.exec("COMMIT");
14384
+ } catch (err) {
14385
+ try {
14386
+ db.exec("ROLLBACK");
14387
+ } catch {}
14388
+ throw err;
14389
+ }
14390
+ return id;
14391
+ }
14392
+ async function addSnapshotTableRows(cwd, revisionId, rows) {
14393
+ if (rows.length === 0)
14394
+ return;
14196
14395
  const db = await getStoreDb(cwd);
14197
- const tableHasher = createHash5("sha256");
14198
- const insertRow = db.prepare("INSERT OR IGNORE INTO snapshot_rows (snapshot_id, table_name, row_key_hash, row_key_json, row_hash, payload_hash) VALUES (?, ?, ?, ?, ?, ?)");
14396
+ const insertRow = db.prepare("INSERT OR IGNORE INTO snapshot_table_revision_rows (revision_id, row_key_hash, row_key_json, row_hash, payload_hash) VALUES (?, ?, ?, ?, ?)");
14199
14397
  const insertPayload = db.prepare("INSERT OR IGNORE INTO snapshot_payloads (payload_hash, payload_json) VALUES (?, ?)");
14200
- const updateTable = db.prepare("UPDATE snapshot_tables SET row_count = ?, table_hash = ?, pk_columns_json = ? WHERE snapshot_id = ? AND table_name = ?");
14201
14398
  db.exec("BEGIN");
14202
14399
  try {
14203
14400
  for (const row of rows) {
14204
14401
  const rowKeyHash = createHash5("sha256").update(row.rowKeyJson).digest("hex");
14205
14402
  const payloadHash = hashPayload(row.payloadJson);
14206
- tableHasher.update(row.rowHash);
14207
- insertRow.run(snapshotId, tableName, rowKeyHash, row.rowKeyJson, row.rowHash, payloadHash);
14403
+ insertRow.run(revisionId, rowKeyHash, row.rowKeyJson, row.rowHash, payloadHash);
14208
14404
  insertPayload.run(payloadHash, row.payloadJson);
14209
14405
  }
14210
- const tableHash = tableHasher.digest("hex");
14211
- updateTable.run(rows.length, tableHash, JSON.stringify(pkColumns), snapshotId, tableName);
14406
+ db.exec("COMMIT");
14407
+ } catch (err) {
14408
+ try {
14409
+ db.exec("ROLLBACK");
14410
+ } catch {}
14411
+ throw err;
14412
+ }
14413
+ }
14414
+ function computeRevisionTableHash(db, revisionId) {
14415
+ const hasher = createHash5("sha256");
14416
+ hashLengthPrefixed(hasher, `snapshot-table-v${SNAPSHOT_TABLE_HASH_VERSION}`);
14417
+ let rowCount = 0;
14418
+ let last;
14419
+ for (;; ) {
14420
+ const rows = last ? db.prepare(`SELECT row_key_hash, row_key_json, row_hash, payload_hash
14421
+ FROM snapshot_table_revision_rows
14422
+ WHERE revision_id = ?
14423
+ AND (row_key_hash > ? OR (row_key_hash = ? AND row_key_json > ?))
14424
+ ORDER BY row_key_hash, row_key_json
14425
+ LIMIT ?`).all(revisionId, last.row_key_hash, last.row_key_hash, last.row_key_json, SNAPSHOT_REVISION_HASH_BATCH_SIZE) : db.prepare(`SELECT row_key_hash, row_key_json, row_hash, payload_hash
14426
+ FROM snapshot_table_revision_rows
14427
+ WHERE revision_id = ?
14428
+ ORDER BY row_key_hash, row_key_json
14429
+ LIMIT ?`).all(revisionId, SNAPSHOT_REVISION_HASH_BATCH_SIZE);
14430
+ if (rows.length === 0)
14431
+ break;
14432
+ for (const row of rows) {
14433
+ hashLengthPrefixed(hasher, row.row_key_hash);
14434
+ hashLengthPrefixed(hasher, row.row_key_json);
14435
+ hashLengthPrefixed(hasher, row.row_hash);
14436
+ hashLengthPrefixed(hasher, row.payload_hash);
14437
+ rowCount++;
14438
+ last = {
14439
+ row_key_hash: row.row_key_hash,
14440
+ row_key_json: row.row_key_json
14441
+ };
14442
+ }
14443
+ if (rows.length < SNAPSHOT_REVISION_HASH_BATCH_SIZE)
14444
+ break;
14445
+ }
14446
+ return {
14447
+ rowCount,
14448
+ tableHash: `${SNAPSHOT_TABLE_HASH_PREFIX}${hasher.digest("hex")}`
14449
+ };
14450
+ }
14451
+ async function finalizeSnapshotTableRevision(cwd, snapshotId, tableName, revisionId) {
14452
+ const db = await getStoreDb(cwd);
14453
+ const revision = db.prepare(`SELECT db_id, schema_name, table_name, pk_columns_json
14454
+ FROM snapshot_table_revisions
14455
+ WHERE id = ?`).get(revisionId);
14456
+ if (!revision)
14457
+ throw new Error(`snapshot table revision not found: ${revisionId}`);
14458
+ const { rowCount, tableHash } = computeRevisionTableHash(db, revisionId);
14459
+ const existing = db.prepare(`SELECT id
14460
+ FROM snapshot_table_revisions
14461
+ WHERE id != ?
14462
+ AND db_id = ?
14463
+ AND schema_name = ?
14464
+ AND table_name = ?
14465
+ AND hash_version = ?
14466
+ AND table_hash = ?
14467
+ AND row_count = ?
14468
+ AND pk_columns_json = ?
14469
+ AND status = 'done'
14470
+ ORDER BY created_at ASC
14471
+ LIMIT 1`).get(revisionId, revision.db_id, revision.schema_name, revision.table_name, SNAPSHOT_TABLE_HASH_VERSION, tableHash, rowCount, revision.pk_columns_json);
14472
+ const adoptedRevisionId = existing?.id ?? revisionId;
14473
+ db.exec("BEGIN");
14474
+ try {
14475
+ if (existing) {
14476
+ db.prepare("DELETE FROM snapshot_table_revisions WHERE id = ?").run(revisionId);
14477
+ } else {
14478
+ db.prepare(`UPDATE snapshot_table_revisions
14479
+ SET row_count = ?, table_hash = ?, status = 'done', source_snapshot_id = NULL
14480
+ WHERE id = ?`).run(rowCount, tableHash, revisionId);
14481
+ }
14482
+ db.prepare(`UPDATE snapshot_tables
14483
+ SET row_count = ?, table_hash = ?, pk_columns_json = ?, revision_id = ?
14484
+ WHERE snapshot_id = ? AND table_name = ?`).run(rowCount, tableHash, revision.pk_columns_json, adoptedRevisionId, snapshotId, tableName);
14212
14485
  db.exec("COMMIT");
14213
14486
  } catch (err) {
14214
14487
  try {
@@ -14220,7 +14493,18 @@ async function addSnapshotTableData(cwd, snapshotId, tableName, pkColumns, rows)
14220
14493
  async function finalizeSnapshot(cwd, snapshotId, error) {
14221
14494
  const db = await getStoreDb(cwd);
14222
14495
  if (error) {
14223
- db.prepare("UPDATE snapshots SET status = 'error', error_message = ? WHERE id = ?").run(error, snapshotId);
14496
+ db.exec("BEGIN");
14497
+ try {
14498
+ db.prepare("DELETE FROM snapshot_table_revisions WHERE source_snapshot_id = ? AND status = 'building'").run(snapshotId);
14499
+ deleteOrphanPayloads(db);
14500
+ db.prepare("UPDATE snapshots SET status = 'error', error_message = ? WHERE id = ?").run(error, snapshotId);
14501
+ db.exec("COMMIT");
14502
+ } catch (err) {
14503
+ try {
14504
+ db.exec("ROLLBACK");
14505
+ } catch {}
14506
+ throw err;
14507
+ }
14224
14508
  } else {
14225
14509
  db.prepare("UPDATE snapshots SET status = 'done' WHERE id = ?").run(snapshotId);
14226
14510
  }
@@ -14230,8 +14514,9 @@ async function listSnapshots(cwd, dbId, schema) {
14230
14514
  const conditions = [];
14231
14515
  const params = [];
14232
14516
  if (dbId) {
14233
- conditions.push("db_id = ?");
14234
- params.push(dbId);
14517
+ const dbIdValues = dockerDbIdFilterValues(dbId);
14518
+ conditions.push(dbIdValues.length === 1 ? "db_id = ?" : `db_id IN (${dbIdValues.map(() => "?").join(", ")})`);
14519
+ params.push(...dbIdValues);
14235
14520
  }
14236
14521
  if (schema !== undefined) {
14237
14522
  conditions.push("COALESCE(schema_name, 'public') = ?");
@@ -14264,11 +14549,14 @@ async function deleteSnapshot(cwd, snapshotId) {
14264
14549
  db.exec("BEGIN");
14265
14550
  try {
14266
14551
  db.prepare("DELETE FROM snapshots WHERE id = ?").run(snapshotId);
14267
- db.prepare(`DELETE FROM snapshot_payloads
14268
- WHERE NOT EXISTS (
14269
- SELECT 1 FROM snapshot_rows
14270
- WHERE snapshot_rows.payload_hash = snapshot_payloads.payload_hash
14271
- )`).run();
14552
+ db.prepare(`DELETE FROM snapshot_table_revisions
14553
+ WHERE status = 'done'
14554
+ AND NOT EXISTS (
14555
+ SELECT 1 FROM snapshot_tables
14556
+ WHERE snapshot_tables.revision_id = snapshot_table_revisions.id
14557
+ )`).run();
14558
+ db.prepare("DELETE FROM snapshot_table_revisions WHERE source_snapshot_id = ? AND status = 'building'").run(snapshotId);
14559
+ deleteOrphanPayloads(db);
14272
14560
  db.exec("COMMIT");
14273
14561
  } catch (err) {
14274
14562
  try {
@@ -14278,15 +14566,22 @@ async function deleteSnapshot(cwd, snapshotId) {
14278
14566
  }
14279
14567
  }
14280
14568
  function getSnapshotScope(db, snapshotId) {
14281
- const row = db.prepare("SELECT db_id, COALESCE(schema_name, 'public') AS schema_name FROM snapshots WHERE id = ?").get(snapshotId);
14282
- if (!row)
14283
- throw new Error(`snapshot not found: ${snapshotId}`);
14284
- return { dbId: row.db_id, schema: row.schema_name };
14569
+ return getSnapshotScopeRow(db, snapshotId);
14570
+ }
14571
+ async function getSnapshotScopeById(cwd, snapshotId) {
14572
+ const db = await getStoreDb(cwd);
14573
+ const scope = getSnapshotScope(db, snapshotId);
14574
+ return {
14575
+ dbId: canonicalizeDockerDbId(scope.dbId) ?? scope.dbId,
14576
+ schema: scope.schema
14577
+ };
14285
14578
  }
14286
14579
  function assertSameSnapshotScope(db, beforeId, afterId) {
14287
14580
  const before = getSnapshotScope(db, beforeId);
14288
14581
  const after = getSnapshotScope(db, afterId);
14289
- if (before.dbId !== after.dbId || before.schema !== after.schema) {
14582
+ const beforeDbId = canonicalizeDockerDbId(before.dbId) ?? before.dbId;
14583
+ const afterDbId = canonicalizeDockerDbId(after.dbId) ?? after.dbId;
14584
+ if (beforeDbId !== afterDbId || before.schema !== after.schema) {
14290
14585
  throw new Error(`cannot compare snapshots from different database/schema (${before.dbId}:${before.schema} vs ${after.dbId}:${after.schema})`);
14291
14586
  }
14292
14587
  }
@@ -14338,16 +14633,16 @@ async function computeDiffTables(cwd, beforeId, afterId) {
14338
14633
  continue;
14339
14634
  }
14340
14635
  const insertedCount = db.prepare(`SELECT COUNT(*) AS cnt
14341
- FROM snapshot_rows a
14342
- LEFT JOIN snapshot_rows b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
14636
+ FROM snapshot_rows_resolved a
14637
+ LEFT JOIN snapshot_rows_resolved b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
14343
14638
  WHERE a.snapshot_id = ? AND a.table_name = ? AND b.row_key_hash IS NULL`).get(beforeId, table, afterId, table).cnt;
14344
14639
  const deletedCount = db.prepare(`SELECT COUNT(*) AS cnt
14345
- FROM snapshot_rows b
14346
- LEFT JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14640
+ FROM snapshot_rows_resolved b
14641
+ LEFT JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14347
14642
  WHERE b.snapshot_id = ? AND b.table_name = ? AND a.row_key_hash IS NULL`).get(afterId, table, beforeId, table).cnt;
14348
14643
  const updatedCount = db.prepare(`SELECT COUNT(*) AS cnt
14349
- FROM snapshot_rows b
14350
- INNER JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14644
+ FROM snapshot_rows_resolved b
14645
+ INNER JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14351
14646
  WHERE b.snapshot_id = ? AND b.table_name = ? AND b.row_hash != a.row_hash`).get(afterId, table, beforeId, table).cnt;
14352
14647
  const unchangedCount = b.row_count - deletedCount - updatedCount;
14353
14648
  results.push({
@@ -14373,8 +14668,8 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
14373
14668
  assertSameSnapshotScope(db, beforeId, afterId);
14374
14669
  const allDiffRows = [];
14375
14670
  const inserted = db.prepare(`SELECT a.row_key_json, a.payload_hash
14376
- FROM snapshot_rows a
14377
- LEFT JOIN snapshot_rows b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
14671
+ FROM snapshot_rows_resolved a
14672
+ LEFT JOIN snapshot_rows_resolved b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
14378
14673
  WHERE a.snapshot_id = ? AND a.table_name = ? AND b.row_key_hash IS NULL
14379
14674
  ORDER BY a.row_key_json`).all(beforeId, table, afterId, table);
14380
14675
  for (const r of inserted) {
@@ -14386,8 +14681,8 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
14386
14681
  });
14387
14682
  }
14388
14683
  const deleted = db.prepare(`SELECT b.row_key_json, b.payload_hash
14389
- FROM snapshot_rows b
14390
- LEFT JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14684
+ FROM snapshot_rows_resolved b
14685
+ LEFT JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14391
14686
  WHERE b.snapshot_id = ? AND b.table_name = ? AND a.row_key_hash IS NULL
14392
14687
  ORDER BY b.row_key_json`).all(afterId, table, beforeId, table);
14393
14688
  for (const r of deleted) {
@@ -14399,8 +14694,8 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
14399
14694
  });
14400
14695
  }
14401
14696
  const updated = db.prepare(`SELECT b.row_key_json, b.payload_hash AS before_ph, a.payload_hash AS after_ph
14402
- FROM snapshot_rows b
14403
- INNER JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14697
+ FROM snapshot_rows_resolved b
14698
+ INNER JOIN snapshot_rows_resolved a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
14404
14699
  WHERE b.snapshot_id = ? AND b.table_name = ? AND b.row_hash != a.row_hash
14405
14700
  ORDER BY b.row_key_json`).all(afterId, table, beforeId, table);
14406
14701
  for (const r of updated) {
@@ -14454,10 +14749,45 @@ CREATE TABLE IF NOT EXISTS snapshot_tables (
14454
14749
  row_count INTEGER NOT NULL DEFAULT 0,
14455
14750
  table_hash TEXT NOT NULL DEFAULT '',
14456
14751
  pk_columns_json TEXT NOT NULL DEFAULT '[]',
14752
+ revision_id TEXT,
14457
14753
  PRIMARY KEY (snapshot_id, table_name),
14458
- FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE
14754
+ FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE,
14755
+ FOREIGN KEY (revision_id) REFERENCES snapshot_table_revisions(id)
14756
+ );
14757
+
14758
+ CREATE TABLE IF NOT EXISTS snapshot_table_revisions (
14759
+ id TEXT PRIMARY KEY,
14760
+ source_snapshot_id TEXT,
14761
+ db_id TEXT NOT NULL,
14762
+ schema_name TEXT NOT NULL,
14763
+ table_name TEXT NOT NULL,
14764
+ row_count INTEGER NOT NULL DEFAULT 0,
14765
+ table_hash TEXT NOT NULL DEFAULT '',
14766
+ hash_version INTEGER NOT NULL DEFAULT 2,
14767
+ pk_columns_json TEXT NOT NULL DEFAULT '[]',
14768
+ status TEXT NOT NULL DEFAULT 'building',
14769
+ created_at TEXT NOT NULL
14770
+ );
14771
+
14772
+ CREATE INDEX IF NOT EXISTS idx_snapshot_table_revisions_lookup
14773
+ ON snapshot_table_revisions(db_id, schema_name, table_name, hash_version, table_hash, row_count, pk_columns_json, status);
14774
+
14775
+ CREATE TABLE IF NOT EXISTS snapshot_table_revision_rows (
14776
+ revision_id TEXT NOT NULL,
14777
+ row_key_hash TEXT NOT NULL,
14778
+ row_key_json TEXT NOT NULL,
14779
+ row_hash TEXT NOT NULL,
14780
+ payload_hash TEXT NOT NULL,
14781
+ PRIMARY KEY (revision_id, row_key_hash),
14782
+ FOREIGN KEY (revision_id) REFERENCES snapshot_table_revisions(id) ON DELETE CASCADE
14459
14783
  );
14460
14784
 
14785
+ CREATE INDEX IF NOT EXISTS idx_snapshot_table_revision_rows_revision_key
14786
+ ON snapshot_table_revision_rows(revision_id, row_key_hash);
14787
+
14788
+ CREATE INDEX IF NOT EXISTS idx_snapshot_table_revision_rows_payload_hash
14789
+ ON snapshot_table_revision_rows(payload_hash);
14790
+
14461
14791
  CREATE TABLE IF NOT EXISTS snapshot_rows (
14462
14792
  snapshot_id TEXT NOT NULL,
14463
14793
  table_name TEXT NOT NULL,
@@ -14475,14 +14805,40 @@ CREATE TABLE IF NOT EXISTS snapshot_payloads (
14475
14805
  );
14476
14806
 
14477
14807
  -- deleteSnapshot の orphan cleanup (snapshot_payloads を残さない) は
14478
- -- snapshot_rows.payload_hash で逆引きする。index が無いと
14479
- -- snapshot_payloads 件数 × snapshot_rows 全件の相関スキャンになる。
14808
+ -- legacy rows と revision rows の payload_hash で逆引きする。index が無いと
14809
+ -- snapshot_payloads 件数 × rows 全件の相関スキャンになる。
14480
14810
  CREATE INDEX IF NOT EXISTS idx_snapshot_rows_payload_hash
14481
14811
  ON snapshot_rows(payload_hash);
14482
14812
 
14483
- `, storeDb = null, storeDbPath = null;
14813
+ DROP VIEW IF EXISTS snapshot_rows_resolved;
14814
+
14815
+ CREATE VIEW snapshot_rows_resolved AS
14816
+ SELECT
14817
+ st.snapshot_id AS snapshot_id,
14818
+ st.table_name AS table_name,
14819
+ rr.row_key_hash AS row_key_hash,
14820
+ rr.row_key_json AS row_key_json,
14821
+ rr.row_hash AS row_hash,
14822
+ rr.payload_hash AS payload_hash
14823
+ FROM snapshot_tables st
14824
+ INNER JOIN snapshot_table_revision_rows rr
14825
+ ON rr.revision_id = st.revision_id
14826
+ WHERE st.revision_id IS NOT NULL
14827
+ UNION ALL
14828
+ SELECT
14829
+ snapshot_id,
14830
+ table_name,
14831
+ row_key_hash,
14832
+ row_key_json,
14833
+ row_hash,
14834
+ payload_hash
14835
+ FROM snapshot_rows;
14836
+
14837
+ `, storeDb = null, storeDbPath = null, SNAPSHOT_TABLE_HASH_VERSION = 2, SNAPSHOT_TABLE_HASH_PREFIX, SNAPSHOT_REVISION_HASH_BATCH_SIZE = 1000;
14484
14838
  var init_snapshot_store = __esm(() => {
14839
+ init_discovery();
14485
14840
  init_sqlite_driver();
14841
+ SNAPSHOT_TABLE_HASH_PREFIX = `v${SNAPSHOT_TABLE_HASH_VERSION}:`;
14486
14842
  });
14487
14843
 
14488
14844
  // web-src/server/database/sources/types.ts
@@ -14505,7 +14861,6 @@ async function runSnapshot(cwd, source, dbId, containers, note, onProgress, opti
14505
14861
  const container = containers[i];
14506
14862
  throwIfAborted(options.signal, "snapshot cancelled");
14507
14863
  onProgress?.({ container, done: false, index: i, total });
14508
- const collected = [];
14509
14864
  let pkColumns = [];
14510
14865
  if (snapshotSource.model === "sql") {
14511
14866
  try {
@@ -14517,16 +14872,28 @@ async function runSnapshot(cwd, source, dbId, containers, note, onProgress, opti
14517
14872
  }
14518
14873
  }
14519
14874
  throwIfAborted(options.signal, "snapshot cancelled");
14875
+ const revisionId = await beginSnapshotTableRevision(cwd, snapshotId, container, pkColumns);
14876
+ const pendingRows = [];
14877
+ const flushRows = async () => {
14878
+ if (pendingRows.length === 0)
14879
+ return;
14880
+ const rows = pendingRows.splice(0, pendingRows.length);
14881
+ await addSnapshotTableRows(cwd, revisionId, rows);
14882
+ };
14520
14883
  for await (const item of snapshotSource.iterateForSnapshot(container, options.signal)) {
14521
14884
  throwIfAborted(options.signal, "snapshot cancelled");
14522
- collected.push({
14885
+ pendingRows.push({
14523
14886
  rowKeyJson: item.keyJson,
14524
14887
  rowHash: item.rowHash,
14525
14888
  payloadJson: item.payloadJson
14526
14889
  });
14890
+ if (pendingRows.length >= SNAPSHOT_FLUSH_THRESHOLD) {
14891
+ await flushRows();
14892
+ }
14527
14893
  }
14528
14894
  throwIfAborted(options.signal, "snapshot cancelled");
14529
- await addSnapshotTableData(cwd, snapshotId, container, pkColumns, collected);
14895
+ await flushRows();
14896
+ await finalizeSnapshotTableRevision(cwd, snapshotId, container, revisionId);
14530
14897
  }
14531
14898
  await finalizeSnapshot(cwd, snapshotId);
14532
14899
  onProgress?.({ container: "", done: true, index: total, total });
@@ -15083,6 +15450,38 @@ async function handleTable(cwd, url, omitDirNames, signal) {
15083
15450
  return handleError("database", "read table", err, signal);
15084
15451
  }
15085
15452
  }
15453
+ async function handleTableCount(cwd, url, omitDirNames, signal) {
15454
+ const r = await resolveDb(cwd, url.searchParams.get("db"), omitDirNames, url.searchParams.get("schema"), signal);
15455
+ if (r instanceof Response)
15456
+ return r;
15457
+ const table = url.searchParams.get("table");
15458
+ if (!table)
15459
+ return textError("missing table parameter", 400);
15460
+ try {
15461
+ const adapter = await getAdapter(r, cwd, signal);
15462
+ const { result, executedSql } = await captureSql(async () => {
15463
+ const db = asAsync(adapter);
15464
+ const tables = await db.tables(signal);
15465
+ const entry = tables.find((candidate) => candidate.name === table);
15466
+ if (!entry)
15467
+ throw new Error(`unknown table: ${table}`);
15468
+ if (entry.type !== "table")
15469
+ return { rowCount: null };
15470
+ const counts = await db.tableRowCounts([table], signal);
15471
+ return { rowCount: counts.get(table) ?? null };
15472
+ });
15473
+ const body = {
15474
+ dbId: r.dbId,
15475
+ ...r.schema ? { schema: r.schema } : {},
15476
+ table,
15477
+ rowCount: result.rowCount,
15478
+ executedSql
15479
+ };
15480
+ return json(body);
15481
+ } catch (err) {
15482
+ return handleError("database", "read table count", err, signal);
15483
+ }
15484
+ }
15086
15485
  function makeHistoryId() {
15087
15486
  return makeId("qh");
15088
15487
  }
@@ -15821,7 +16220,14 @@ async function handleDiffTables(cwd, url) {
15821
16220
  return textError("missing before or after parameter", 400);
15822
16221
  try {
15823
16222
  const tables = await computeDiffTables(cwd, beforeId, afterId);
15824
- return json({ beforeId, afterId, tables });
16223
+ const scope = await getSnapshotScopeById(cwd, beforeId);
16224
+ return json({
16225
+ beforeId,
16226
+ afterId,
16227
+ dbId: scope.dbId,
16228
+ schema: scope.schema,
16229
+ tables
16230
+ });
15825
16231
  } catch (err) {
15826
16232
  return handleError("database", "compute diff", err);
15827
16233
  }
@@ -16004,6 +16410,10 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
16004
16410
  methods: ["GET"],
16005
16411
  handler: () => handleTable(cwd, url, omitDirNames, req.signal)
16006
16412
  },
16413
+ "/_db/table-count": {
16414
+ methods: ["GET"],
16415
+ handler: () => handleTableCount(cwd, url, omitDirNames, req.signal)
16416
+ },
16007
16417
  "/_db/columns": {
16008
16418
  methods: ["GET"],
16009
16419
  handler: () => handleColumns(cwd, url, omitDirNames, req.signal)
@@ -17288,21 +17698,6 @@ function normalizeNewDirectoryName(name) {
17288
17698
  return trimmed;
17289
17699
  }
17290
17700
 
17291
- // web-src/core/routes.ts
17292
- var SPA_PATHS, APP_ENTRY_PATHS;
17293
- var init_routes = __esm(() => {
17294
- SPA_PATHS = [
17295
- "/todif",
17296
- "/todiff",
17297
- "/file",
17298
- "/help",
17299
- "/history",
17300
- "/database",
17301
- "/doctor"
17302
- ];
17303
- APP_ENTRY_PATHS = ["/", "/index.html"];
17304
- });
17305
-
17306
17701
  // web-src/server/cache.ts
17307
17702
  import { lstatSync as lstatSync3 } from "node:fs";
17308
17703
  import { join as join14 } from "node:path";
@@ -19637,6 +20032,8 @@ function fileMetadataForTarget(target, path) {
19637
20032
  function attachTreeEntryMetadata(target, entry) {
19638
20033
  if (entry.type === "tree")
19639
20034
  return { ...entry, ...directoryMetadata(target, entry.path) };
20035
+ if (entry.type === "commit" && !entry.submodule && (target === "worktree" || target === ""))
20036
+ return { ...entry, ...directoryMetadata(target, entry.path) };
19640
20037
  if (entry.type !== "blob")
19641
20038
  return entry;
19642
20039
  return { ...entry, ...fileMetadataForTarget(target, entry.path) };