@youtyan/code-viewer 0.1.18 → 0.1.20

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.
@@ -82,7 +82,13 @@ function startDevAssetReload(options) {
82
82
  }
83
83
 
84
84
  // web-src/server/git.ts
85
- import { existsSync, lstatSync as lstatSync2, readdirSync, readFileSync } from "node:fs";
85
+ import {
86
+ existsSync,
87
+ lstatSync as lstatSync2,
88
+ readdirSync,
89
+ readFileSync
90
+ } from "node:fs";
91
+ import { join as join2 } from "node:path";
86
92
 
87
93
  // web-src/server/runtime.ts
88
94
  import { spawn, spawnSync } from "node:child_process";
@@ -235,9 +241,11 @@ async function writeWebResponse(res, response) {
235
241
  }
236
242
 
237
243
  // web-src/server/git.ts
238
- import { join as join2 } from "node:path";
239
244
  var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32;
240
245
  var WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000;
246
+ var DEFAULT_REF_COMMIT_LIMIT = 100;
247
+ var MAX_REF_COMMIT_LIMIT = 500;
248
+ var COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI";
241
249
  var DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
242
250
  "node_modules",
243
251
  ".venv",
@@ -291,11 +299,19 @@ function catFileBlobStream(oid, cwd) {
291
299
  }
292
300
  function objectSize(ref, path, cwd) {
293
301
  const res = run(["git", "cat-file", "-s", `${ref}:${path}`], cwd);
294
- return { code: res.code, size: Number(res.stdout.trim()) || 0, stderr: res.stderr };
302
+ return {
303
+ code: res.code,
304
+ size: Number(res.stdout.trim()) || 0,
305
+ stderr: res.stderr
306
+ };
295
307
  }
296
308
  function objectByteSize(oid, cwd) {
297
309
  const res = run(["git", "cat-file", "-s", oid], cwd);
298
- return { code: res.code, size: Number(res.stdout.trim()) || 0, stderr: res.stderr };
310
+ return {
311
+ code: res.code,
312
+ size: Number(res.stdout.trim()) || 0,
313
+ stderr: res.stderr
314
+ };
299
315
  }
300
316
  function objectId(ref, path, cwd) {
301
317
  const res = run(["git", "rev-parse", "--verify", `${ref}:${path}`], cwd);
@@ -316,30 +332,133 @@ function verifyTreeRef(ref, cwd) {
316
332
  return res.code === 0;
317
333
  }
318
334
  function refs(cwd) {
319
- const out = { branches: [], tags: [], commits: [], current: "" };
335
+ const out = {
336
+ branches: [],
337
+ tags: [],
338
+ commits: [],
339
+ current: ""
340
+ };
320
341
  const branches = run([
321
342
  "git",
322
343
  "for-each-ref",
323
344
  "--sort=-committerdate",
324
- "--format=%(refname:short)",
345
+ "--format=%(refname)%09%(refname:short)%09%(committerdate:iso-strict)",
325
346
  "refs/heads",
326
347
  "refs/remotes"
327
348
  ], cwd);
328
349
  if (branches.code === 0) {
329
- out.branches = branches.stdout.split(`
330
- `).filter((line) => line && line !== "origin/HEAD");
331
- }
332
- const tags = run(["git", "for-each-ref", "--sort=-creatordate", "--format=%(refname:short)", "refs/tags"], cwd);
333
- if (tags.code === 0)
334
- out.tags = tags.stdout.split(`
335
- `).filter(Boolean);
336
- const commits = run(["git", "log", "-50", "--format=%h\t%s\t%an\t%ar"], cwd);
337
- if (commits.code === 0)
338
- out.commits = commits.stdout.split(`
339
- `).filter(Boolean);
350
+ for (const line of branches.stdout.split(`
351
+ `)) {
352
+ const [fullName, name, when] = line.split("\t");
353
+ if (!fullName || !name || fullName.startsWith("refs/remotes/") && fullName.endsWith("/HEAD"))
354
+ continue;
355
+ out.branches.push({ name, when });
356
+ }
357
+ }
358
+ const tags = run([
359
+ "git",
360
+ "for-each-ref",
361
+ "--sort=-creatordate",
362
+ "--format=%(refname:short)%09%(creatordate:iso-strict)",
363
+ "refs/tags"
364
+ ], cwd);
365
+ if (tags.code === 0) {
366
+ for (const line of tags.stdout.split(`
367
+ `)) {
368
+ const [name, when] = line.split("\t");
369
+ if (!name)
370
+ continue;
371
+ out.tags.push({ name, when });
372
+ }
373
+ }
374
+ out.commits = refCommits(cwd, "", DEFAULT_REF_COMMIT_LIMIT);
340
375
  out.current = currentBranch(cwd) || "";
341
376
  return out;
342
377
  }
378
+ function clampCommitLimit(max) {
379
+ return Math.max(1, Math.min(max, MAX_REF_COMMIT_LIMIT));
380
+ }
381
+ function parseCommitLog(stdout) {
382
+ const parts = stdout.split("\x00");
383
+ const commits = [];
384
+ for (let index = 0;index < parts.length; ) {
385
+ if (!parts[index]) {
386
+ index++;
387
+ continue;
388
+ }
389
+ const sha = parts[index++] || "";
390
+ const subject = parts[index++] || "";
391
+ const author = parts[index++] || "";
392
+ const when = parts[index++] || "";
393
+ if (sha)
394
+ commits.push({ sha, subject, author, when });
395
+ }
396
+ return commits;
397
+ }
398
+ function commitLogArgs(limit) {
399
+ return [
400
+ "git",
401
+ "log",
402
+ "--all",
403
+ "-z",
404
+ `--max-count=${limit}`,
405
+ `--format=${COMMIT_FORMAT}`
406
+ ];
407
+ }
408
+ function mergeCommitResults(limit, ...groups) {
409
+ const seen = new Set;
410
+ const merged = [];
411
+ for (const commits of groups) {
412
+ for (const commit of commits) {
413
+ if (!commit.sha || seen.has(commit.sha))
414
+ continue;
415
+ seen.add(commit.sha);
416
+ merged.push(commit);
417
+ if (merged.length >= limit)
418
+ return merged;
419
+ }
420
+ }
421
+ return merged;
422
+ }
423
+ function runCommitLog(cwd, args) {
424
+ const commits = run(args, cwd);
425
+ return commits.code === 0 ? parseCommitLog(commits.stdout) : [];
426
+ }
427
+ function refCommits(cwd, query = "", max = DEFAULT_REF_COMMIT_LIMIT) {
428
+ const limit = clampCommitLimit(max);
429
+ const trimmed = query.trim().slice(0, 200).replace(/\0/g, "");
430
+ const hashMatches = [];
431
+ if (/^[0-9a-f]{4,40}$/i.test(trimmed)) {
432
+ const verified = run(["git", "rev-parse", "--verify", `${trimmed}^{commit}`], cwd);
433
+ const single = run([
434
+ "git",
435
+ "log",
436
+ "-z",
437
+ "-1",
438
+ `--format=${COMMIT_FORMAT}`,
439
+ verified.code === 0 && verified.stdout.trim() ? verified.stdout.trim() : trimmed
440
+ ], cwd);
441
+ if (single.code === 0 && single.stdout.trim()) {
442
+ hashMatches.push(...parseCommitLog(single.stdout));
443
+ }
444
+ }
445
+ if (!trimmed) {
446
+ return runCommitLog(cwd, commitLogArgs(limit));
447
+ }
448
+ const subjectMatches = runCommitLog(cwd, [
449
+ ...commitLogArgs(limit),
450
+ "--regexp-ignore-case",
451
+ "--fixed-strings",
452
+ `--grep=${trimmed}`
453
+ ]);
454
+ const authorMatches = runCommitLog(cwd, [
455
+ ...commitLogArgs(limit),
456
+ "--regexp-ignore-case",
457
+ "--fixed-strings",
458
+ `--author=${trimmed}`
459
+ ]);
460
+ return mergeCommitResults(limit, hashMatches, subjectMatches, authorMatches);
461
+ }
343
462
  function nameStatus(args, cwd) {
344
463
  const res = run([
345
464
  "git",
@@ -366,7 +485,12 @@ function nameStatus(args, cwd) {
366
485
  const oldPath = parts[i++] || "";
367
486
  const path = parts[i++] || "";
368
487
  if (path)
369
- files.push({ status: kind, old_path: oldPath, path, similarity: Number(status.slice(1)) || undefined });
488
+ files.push({
489
+ status: kind,
490
+ old_path: oldPath,
491
+ path,
492
+ similarity: Number(status.slice(1)) || undefined
493
+ });
370
494
  } else {
371
495
  const path = parts[i++] || "";
372
496
  if (path)
@@ -512,7 +636,11 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
512
636
  continue;
513
637
  walk(full, entryPath, depth + 1);
514
638
  } else if (entry.isFile() || entry.isSymbolicLink()) {
515
- if (!pushRecursiveEntry({ name: entry.name, path: entryPath, type: "blob" }))
639
+ if (!pushRecursiveEntry({
640
+ name: entry.name,
641
+ path: entryPath,
642
+ type: "blob"
643
+ }))
516
644
  return;
517
645
  }
518
646
  }
@@ -545,7 +673,11 @@ function gitTreeEntries(ref, path, cwd, recursive) {
545
673
  if (!match)
546
674
  return null;
547
675
  const entryPath = match[2];
548
- return { name: entryPath.split("/").pop() || entryPath, path: entryPath, type: match[1] };
676
+ return {
677
+ name: entryPath.split("/").pop() || entryPath,
678
+ path: entryPath,
679
+ type: match[1]
680
+ };
549
681
  }).filter((entry) => !!entry);
550
682
  if (recursive)
551
683
  entries.sort((a, b) => a.path.localeCompare(b.path));
@@ -566,7 +698,11 @@ function worktreeFiles(cwd) {
566
698
  function listTree(ref, path, cwd, options = {}) {
567
699
  const base = normalizeTreePath(path);
568
700
  if (ref === "worktree") {
569
- return { code: 0, entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames), stderr: "" };
701
+ return {
702
+ code: 0,
703
+ entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames),
704
+ stderr: ""
705
+ };
570
706
  }
571
707
  const direct = gitTreeEntries(ref, base, cwd, false);
572
708
  if (direct.code !== 0 || !options.recursive)
@@ -574,7 +710,11 @@ function listTree(ref, path, cwd, options = {}) {
574
710
  const recursive = gitTreeEntries(ref, base, cwd, true);
575
711
  if (recursive.code !== 0)
576
712
  return recursive;
577
- return { code: 0, entries: combineDirectAndRecursiveFiles(direct.entries, recursive.entries), stderr: "" };
713
+ return {
714
+ code: 0,
715
+ entries: combineDirectAndRecursiveFiles(direct.entries, recursive.entries),
716
+ stderr: ""
717
+ };
578
718
  }
579
719
  function untrackedMeta(cwd) {
580
720
  return untracked(cwd).map((path) => {
@@ -589,7 +729,14 @@ function untrackedMeta(cwd) {
589
729
  lines = data.toString("utf8").split(`
590
730
  `).length - 1;
591
731
  }
592
- return { path, status: "A", additions: binary ? 0 : lines, deletions: 0, binary, untracked: true };
732
+ return {
733
+ path,
734
+ status: "A",
735
+ additions: binary ? 0 : lines,
736
+ deletions: 0,
737
+ binary,
738
+ untracked: true
739
+ };
593
740
  });
594
741
  }
595
742
  function fileMeta(args, cwd, includeUntracked = false) {
@@ -990,7 +1137,12 @@ function buildFileSearchList(ref, generation, entries) {
990
1137
  }
991
1138
  function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
992
1139
  const safePaths = paths.length ? paths : ["."];
993
- const omitGlobs = omitDirNames.flatMap((name) => ["--glob", `!${name}/**`, "--glob", `!**/${name}/**`]);
1140
+ const omitGlobs = omitDirNames.flatMap((name) => [
1141
+ "--glob",
1142
+ `!${name}/**`,
1143
+ "--glob",
1144
+ `!**/${name}/**`
1145
+ ]);
994
1146
  const args = [
995
1147
  "rg",
996
1148
  "--no-config",
@@ -1029,7 +1181,12 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
1029
1181
  const preview = parsed[4];
1030
1182
  if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames))
1031
1183
  continue;
1032
- matches.push({ path, line: lineNo, column, preview: preview.slice(0, 500) });
1184
+ matches.push({
1185
+ path,
1186
+ line: lineNo,
1187
+ column,
1188
+ preview: preview.slice(0, 500)
1189
+ });
1033
1190
  }
1034
1191
  return matches;
1035
1192
  }
@@ -1728,6 +1885,12 @@ function handleGrep(url) {
1728
1885
  return text("invalid target", 400);
1729
1886
  return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames));
1730
1887
  }
1888
+ function handleRefCommits(url) {
1889
+ const query = url.searchParams.get("q") || "";
1890
+ const parsedMax = Number(url.searchParams.get("max") || "");
1891
+ const max = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : undefined;
1892
+ return json({ commits: refCommits(cwd, query, max) });
1893
+ }
1731
1894
  function handleFileDiff(url) {
1732
1895
  const path = url.searchParams.get("path") || "";
1733
1896
  if (!safePath(path))
@@ -2300,6 +2463,8 @@ var server = await startServer({
2300
2463
  return handleFiles(url);
2301
2464
  if (url.pathname === "/_grep")
2302
2465
  return handleGrep(url);
2466
+ if (url.pathname === "/_commits")
2467
+ return handleRefCommits(url);
2303
2468
  if (url.pathname === "/file_diff")
2304
2469
  return handleFileDiff(url);
2305
2470
  if (url.pathname === "/file_range")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,12 +32,12 @@
32
32
  "check": "bun run typecheck",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "check:bundle": "bun build --target=browser --format=iife --outfile=/tmp/code-viewer-app.js web-src/app.ts && cmp /tmp/code-viewer-app.js web/app.js && bun build --target=browser --format=esm --outfile=/tmp/code-viewer-mermaid.js web-src/mermaid-entry.ts && cmp /tmp/code-viewer-mermaid.js web/mermaid.js && bun build --target=browser --format=esm --outfile=/tmp/code-viewer-shiki.js web-src/shiki-entry.ts && cmp /tmp/code-viewer-shiki.js web/shiki.js",
35
- "check:format": "biome format biome.jsonc package.json web-src/app.ts web-src/server/preview.ts web-src/test/source-fixture.ts web-src/test/source-fixture.test.ts web-src/test/view-file-button.test.ts web-src/test/open-path.test.ts web-src/test/markdown-preview.test.ts web-src/test/search-server.test.ts web-src/test/upload-files.test.ts web-src/test/git-truncate.test.ts web-src/test/asset-version-removal.test.ts web-src/test/sidebar-folder-icon.test.ts web-src/test/mark-viewed-button.test.ts",
35
+ "check:format": "biome format .",
36
36
  "dev": "bun run web-src/server/dev.ts",
37
37
  "preview": "bun run web-src/server/dev.ts",
38
38
  "preview:raw": "bun run web-src/server/preview.ts",
39
39
  "test": "bun test",
40
- "lint": "biome lint web-src/server package.json biome.jsonc",
40
+ "lint": "biome lint web-src package.json biome.jsonc",
41
41
  "verify": "bun run check && bun run lint && bun run check:format && bun run build && bun run check:bundle && bun run test && node --check web/app.js && node --check web/mermaid.js && node --check web/shiki.js && node --check dist/code-viewer.js && node dist/code-viewer.js --help && node scripts/node-smoke.mjs",
42
42
  "pack:dry": "npm pack --dry-run",
43
43
  "prepack": "bun run build",