@youtyan/code-viewer 0.1.0 → 0.1.1
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/README.md +12 -5
- package/package.json +4 -3
- package/web/app.js +1393 -90
- package/web/favicon.png +0 -0
- package/web/index.html +36 -26
- package/web/style.css +737 -59
- package/web-src/server/dev.ts +95 -0
- package/web-src/server/git.ts +104 -3
- package/web-src/server/preview.ts +88 -6
- package/web-src/server/range.ts +8 -0
- package/web-src/server/runtime.d.ts +6 -1
- package/web-src/types.ts +18 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
4
|
+
import { join, normalize } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const ROOT = normalize(join(import.meta.dir, '..', '..'));
|
|
7
|
+
const SERVER_ROOT = join(ROOT, 'web-src', 'server');
|
|
8
|
+
const DEFAULT_DEV_PORT = 64160;
|
|
9
|
+
|
|
10
|
+
type ChildProcess = {
|
|
11
|
+
kill(signal?: string): void;
|
|
12
|
+
exited: Promise<number>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let server: ChildProcess | null = null;
|
|
16
|
+
let build: ChildProcess | null = null;
|
|
17
|
+
let restarting = false;
|
|
18
|
+
let firstStart = true;
|
|
19
|
+
|
|
20
|
+
function withDefaultPort(args: string[]) {
|
|
21
|
+
if (args.includes('--port')) return args;
|
|
22
|
+
return ['--port', String(DEFAULT_DEV_PORT), ...args];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function withoutOpen(args: string[]) {
|
|
26
|
+
return args.filter((arg) => arg !== '--open');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function serverArgs() {
|
|
30
|
+
const args = withDefaultPort(process.argv.slice(2));
|
|
31
|
+
return firstStart ? args : withoutOpen(args);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function watchedFiles() {
|
|
35
|
+
return readdirSync(SERVER_ROOT)
|
|
36
|
+
.filter((name) => name.endsWith('.ts') && name !== 'runtime.d.ts')
|
|
37
|
+
.map((name) => join(SERVER_ROOT, name))
|
|
38
|
+
.concat(join(ROOT, 'web-src', 'types.ts'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function watchSignature() {
|
|
42
|
+
return watchedFiles()
|
|
43
|
+
.map((file) => `${file}:${statSync(file).mtimeMs}`)
|
|
44
|
+
.join('|');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function startBuild() {
|
|
48
|
+
build = Bun.spawn([
|
|
49
|
+
'bun', 'build', '--watch', '--target=browser', '--format=iife',
|
|
50
|
+
'--outfile=web/app.js', 'web-src/app.ts',
|
|
51
|
+
], { cwd: ROOT, stdout: 'inherit', stderr: 'inherit' }) as ChildProcess;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function startServer() {
|
|
55
|
+
const args = serverArgs();
|
|
56
|
+
firstStart = false;
|
|
57
|
+
server = Bun.spawn([
|
|
58
|
+
'bun', 'run', 'web-src/server/preview.ts', ...args,
|
|
59
|
+
], { cwd: ROOT, stdout: 'inherit', stderr: 'inherit' }) as ChildProcess;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function restartServer() {
|
|
63
|
+
if (restarting) return;
|
|
64
|
+
restarting = true;
|
|
65
|
+
const old = server;
|
|
66
|
+
server = null;
|
|
67
|
+
if (old) {
|
|
68
|
+
old.kill();
|
|
69
|
+
await old.exited.catch(() => 1);
|
|
70
|
+
}
|
|
71
|
+
startServer();
|
|
72
|
+
restarting = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shutdown() {
|
|
76
|
+
if (server) server.kill();
|
|
77
|
+
if (build) build.kill();
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
process.on('SIGINT', shutdown);
|
|
82
|
+
process.on('SIGTERM', shutdown);
|
|
83
|
+
|
|
84
|
+
console.log(`code-viewer dev server watching ${SERVER_ROOT}`);
|
|
85
|
+
startBuild();
|
|
86
|
+
startServer();
|
|
87
|
+
|
|
88
|
+
let sig = watchSignature();
|
|
89
|
+
setInterval(() => {
|
|
90
|
+
const next = watchSignature();
|
|
91
|
+
if (next === sig) return;
|
|
92
|
+
sig = next;
|
|
93
|
+
console.log('server source changed; restarting preview server');
|
|
94
|
+
restartServer();
|
|
95
|
+
}, 500);
|
package/web-src/server/git.ts
CHANGED
|
@@ -13,6 +13,12 @@ export type GitFileMeta = {
|
|
|
13
13
|
untracked?: boolean;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export type GitTreeEntry = {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
type: 'tree' | 'blob' | 'commit';
|
|
20
|
+
};
|
|
21
|
+
|
|
16
22
|
function run(args: string[], cwd: string): { code: number; stdout: string; stderr: string } {
|
|
17
23
|
const proc = Bun.spawnSync(args, { cwd, stdout: 'pipe', stderr: 'pipe' });
|
|
18
24
|
return {
|
|
@@ -36,6 +42,13 @@ export function show(ref: string, path: string, cwd: string): { code: number; st
|
|
|
36
42
|
return run(['git', 'show', `${ref}:${path}`], cwd);
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
export function verifyTreeRef(ref: string, cwd: string): boolean {
|
|
46
|
+
if (!ref || ref === 'worktree') return false;
|
|
47
|
+
if (ref.startsWith('-')) return false;
|
|
48
|
+
const res = run(['git', 'rev-parse', '--verify', `${ref}^{tree}`], cwd);
|
|
49
|
+
return res.code === 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
export function refs(cwd: string): { branches: string[]; tags: string[]; commits: string[]; current: string } {
|
|
40
53
|
const out = { branches: [] as string[], tags: [] as string[], commits: [] as string[], current: '' };
|
|
41
54
|
const branches = run([
|
|
@@ -106,11 +119,99 @@ export function numstatZ(args: string[], cwd: string): GitFileMeta[] {
|
|
|
106
119
|
return files;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
export function untracked(cwd: string): string[] {
|
|
110
|
-
const
|
|
122
|
+
export function untracked(cwd: string, path = ''): string[] {
|
|
123
|
+
const args = ['git', 'ls-files', '--others', '--exclude-standard'];
|
|
124
|
+
if (path) args.push('--', `${path}/`);
|
|
125
|
+
const res = run(args, cwd);
|
|
111
126
|
return res.code === 0 ? res.stdout.split('\n').filter(Boolean) : [];
|
|
112
127
|
}
|
|
113
128
|
|
|
129
|
+
function normalizeTreePath(path: string): string {
|
|
130
|
+
return path.replace(/^\/+|\/+$/g, '');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function directChildren(paths: string[], basePath: string): GitTreeEntry[] {
|
|
134
|
+
const base = normalizeTreePath(basePath);
|
|
135
|
+
const prefix = base ? `${base}/` : '';
|
|
136
|
+
const entries = new Map<string, GitTreeEntry>();
|
|
137
|
+
for (const rawPath of paths) {
|
|
138
|
+
if (!rawPath || (prefix && !rawPath.startsWith(prefix))) continue;
|
|
139
|
+
const rest = prefix ? rawPath.slice(prefix.length) : rawPath;
|
|
140
|
+
if (!rest) continue;
|
|
141
|
+
const slash = rest.indexOf('/');
|
|
142
|
+
const name = slash >= 0 ? rest.slice(0, slash) : rest;
|
|
143
|
+
const childPath = prefix + name;
|
|
144
|
+
const type = slash >= 0 ? 'tree' : 'blob';
|
|
145
|
+
const existing = entries.get(childPath);
|
|
146
|
+
if (!existing || existing.type !== 'tree') entries.set(childPath, { name, path: childPath, type });
|
|
147
|
+
}
|
|
148
|
+
return [...entries.values()].sort((a, b) => {
|
|
149
|
+
if (a.type !== b.type) return a.type === 'tree' ? -1 : 1;
|
|
150
|
+
return a.name.localeCompare(b.name);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function worktreeEntries(cwd: string, path: string): GitTreeEntry[] {
|
|
155
|
+
return listTree('worktree', path, cwd).entries;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function worktreeFiles(cwd: string): GitTreeEntry[] {
|
|
159
|
+
return listTree('worktree', '', cwd, { recursive: true }).entries;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function treeEntries(ref: string, path: string, cwd: string): { code: number; entries: GitTreeEntry[]; stderr: string } {
|
|
163
|
+
return listTree(ref, path, cwd);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function treeFiles(ref: string, cwd: string): { code: number; entries: GitTreeEntry[]; stderr: string } {
|
|
167
|
+
return listTree(ref, '', cwd, { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function listTree(
|
|
171
|
+
ref: string,
|
|
172
|
+
path: string,
|
|
173
|
+
cwd: string,
|
|
174
|
+
options: { recursive?: boolean } = {},
|
|
175
|
+
): { code: number; entries: GitTreeEntry[]; stderr: string } {
|
|
176
|
+
const base = normalizeTreePath(path);
|
|
177
|
+
if (ref === 'worktree') {
|
|
178
|
+
const trackedArgs = ['git', '-c', 'core.quotepath=false', 'ls-files', '-z'];
|
|
179
|
+
if (!options.recursive) trackedArgs.push('--', base ? `${base}/` : '.');
|
|
180
|
+
const tracked = run(trackedArgs, cwd);
|
|
181
|
+
const paths = tracked.code === 0 ? tracked.stdout.split('\0').filter(Boolean) : [];
|
|
182
|
+
paths.push(...untracked(cwd, options.recursive ? '' : base));
|
|
183
|
+
const uniquePaths = [...new Set(paths)].filter(Boolean);
|
|
184
|
+
const entries = options.recursive
|
|
185
|
+
? uniquePaths.sort().map((entryPath) => ({
|
|
186
|
+
name: entryPath.split('/').pop() || entryPath,
|
|
187
|
+
path: entryPath,
|
|
188
|
+
type: 'blob' as const,
|
|
189
|
+
}))
|
|
190
|
+
: directChildren(uniquePaths, base);
|
|
191
|
+
return { code: 0, entries, stderr: '' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const args = ['git', '-c', 'core.quotepath=false', 'ls-tree'];
|
|
195
|
+
if (options.recursive) args.push('-r');
|
|
196
|
+
args.push('-z', '--full-tree', ref, '--');
|
|
197
|
+
if (!options.recursive && base) args.push(`${base}/`);
|
|
198
|
+
const res = run(args, cwd);
|
|
199
|
+
if (res.code !== 0) return { code: res.code, entries: [], stderr: res.stderr };
|
|
200
|
+
const allowedTypes = options.recursive ? 'blob|commit' : 'tree|blob|commit';
|
|
201
|
+
const entries = res.stdout.split('\0').filter(Boolean).map((rec) => {
|
|
202
|
+
const match = rec.match(new RegExp(`^\\d+\\s+(${allowedTypes})\\s+[0-9a-fA-F]+\\t(.+)$`));
|
|
203
|
+
if (!match) return null;
|
|
204
|
+
const entryPath = match[2];
|
|
205
|
+
return { name: entryPath.split('/').pop() || entryPath, path: entryPath, type: match[1] as GitTreeEntry['type'] };
|
|
206
|
+
}).filter((entry): entry is GitTreeEntry => !!entry);
|
|
207
|
+
entries.sort((a, b) => {
|
|
208
|
+
if (options.recursive) return a.path.localeCompare(b.path);
|
|
209
|
+
if (a.type !== b.type) return a.type === 'tree' ? -1 : 1;
|
|
210
|
+
return a.name.localeCompare(b.name);
|
|
211
|
+
});
|
|
212
|
+
return { code: 0, entries, stderr: '' };
|
|
213
|
+
}
|
|
214
|
+
|
|
114
215
|
export function untrackedMeta(cwd: string): GitFileMeta[] {
|
|
115
216
|
return untracked(cwd).map((path) => {
|
|
116
217
|
const full = join(cwd, path);
|
|
@@ -187,7 +288,7 @@ export function truncateToNHunks(diffText: string, n: number): {
|
|
|
187
288
|
return { text: diffText, totalHunks: 0, renderedHunks: 0, lineCount: (diffText.match(/\n/g) || []).length };
|
|
188
289
|
}
|
|
189
290
|
const renderedHunks = Math.min(n, hunks.length);
|
|
190
|
-
const text = header + hunks.slice(0, renderedHunks).join('');
|
|
291
|
+
const text = header + hunks.slice(0, renderedHunks).join('\n');
|
|
191
292
|
return {
|
|
192
293
|
text,
|
|
193
294
|
totalHunks: hunks.length,
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
4
4
|
import { basename, extname, join, normalize, relative } from 'node:path';
|
|
5
|
-
import
|
|
5
|
+
import { APP_ENTRY_PATHS, SPA_PATHS } from '../routes';
|
|
6
|
+
import type { DiffMeta, FileDiffResponse, FileMeta, FileRangeResponse, RepoTreeResponse } from '../types';
|
|
6
7
|
import * as git from './git';
|
|
8
|
+
import { isSameWorktreeRange } from './range';
|
|
7
9
|
|
|
8
10
|
const ROOT = normalize(join(import.meta.dir, '..', '..'));
|
|
9
11
|
const WEB_ROOT = join(ROOT, 'web');
|
|
@@ -18,6 +20,7 @@ const SIZE_LARGE = 20000;
|
|
|
18
20
|
let generation = 1;
|
|
19
21
|
let cwd = git.repoRoot(process.cwd()) || process.cwd();
|
|
20
22
|
let cliArgs = DEFAULT_ARGS;
|
|
23
|
+
let listenPort = 0;
|
|
21
24
|
|
|
22
25
|
const enc = new TextEncoder();
|
|
23
26
|
const sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();
|
|
@@ -32,7 +35,7 @@ function parseCli() {
|
|
|
32
35
|
console.log(`code-viewer ${VERSION}
|
|
33
36
|
|
|
34
37
|
Usage:
|
|
35
|
-
code-viewer [--cwd <repo>] [--open] [git-diff-args...]
|
|
38
|
+
code-viewer [--cwd <repo>] [--port <port>] [--open] [git-diff-args...]
|
|
36
39
|
|
|
37
40
|
Examples:
|
|
38
41
|
code-viewer --open
|
|
@@ -51,6 +54,14 @@ Examples:
|
|
|
51
54
|
process.exit(1);
|
|
52
55
|
}
|
|
53
56
|
cwd = git.repoRoot(next) || cwd;
|
|
57
|
+
} else if (arg === '--port') {
|
|
58
|
+
const next = process.argv[++i];
|
|
59
|
+
const parsed = Number(next);
|
|
60
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
61
|
+
console.error('--port requires a TCP port number');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
listenPort = parsed;
|
|
54
65
|
} else if (arg === '--open') {
|
|
55
66
|
setTimeout(() => openBrowser(`http://127.0.0.1:${server.port}/`), 0);
|
|
56
67
|
} else {
|
|
@@ -88,8 +99,7 @@ function requestAllowed(req: Request) {
|
|
|
88
99
|
|
|
89
100
|
function staticFile(pathname: string): Response | null {
|
|
90
101
|
const map: Record<string, [string, string]> = {
|
|
91
|
-
'/': ['
|
|
92
|
-
'/index.html': ['index.html', 'text/html; charset=utf-8'],
|
|
102
|
+
'/favicon.png': ['favicon.png', 'image/png'],
|
|
93
103
|
'/style.css': ['style.css', 'text/css; charset=utf-8'],
|
|
94
104
|
'/app.js': ['app.js', 'application/javascript; charset=utf-8'],
|
|
95
105
|
'/vendor/diff2html/diff2html.min.css': ['vendor/diff2html/diff2html.min.css', 'text/css; charset=utf-8'],
|
|
@@ -98,6 +108,9 @@ function staticFile(pathname: string): Response | null {
|
|
|
98
108
|
'/vendor/highlight.js/styles/github.min.css': ['vendor/highlight.js/styles/github.min.css', 'text/css; charset=utf-8'],
|
|
99
109
|
'/vendor/highlight.js/styles/github-dark.min.css': ['vendor/highlight.js/styles/github-dark.min.css', 'text/css; charset=utf-8'],
|
|
100
110
|
};
|
|
111
|
+
for (const spaPath of [...APP_ENTRY_PATHS, ...SPA_PATHS]) {
|
|
112
|
+
map[spaPath] = ['index.html', 'text/html; charset=utf-8'];
|
|
113
|
+
}
|
|
101
114
|
const spec = map[pathname];
|
|
102
115
|
if (!spec) return null;
|
|
103
116
|
const full = join(WEB_ROOT, spec[0]);
|
|
@@ -180,6 +193,16 @@ function fileToMeta(file: git.GitFileMeta, range: { from?: string; to?: string }
|
|
|
180
193
|
}
|
|
181
194
|
|
|
182
195
|
function computePayload(extras: string[], range: { from?: string; to?: string }): DiffMeta {
|
|
196
|
+
if (isSameWorktreeRange(range)) {
|
|
197
|
+
return {
|
|
198
|
+
files: [],
|
|
199
|
+
totals: { files: 0, additions: 0, deletions: 0 },
|
|
200
|
+
range: 'worktree .. worktree',
|
|
201
|
+
project: basename(cwd),
|
|
202
|
+
branch: git.currentBranch(cwd) || undefined,
|
|
203
|
+
generation,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
183
206
|
const { args, refs } = buildRangeArgs(range);
|
|
184
207
|
const fullArgs = [...extras, ...args];
|
|
185
208
|
const files = git.fileMeta(fullArgs, cwd, false);
|
|
@@ -231,7 +254,12 @@ function handleDiffJson(url: URL) {
|
|
|
231
254
|
}
|
|
232
255
|
|
|
233
256
|
function safePath(path: string) {
|
|
234
|
-
|
|
257
|
+
if (!path || path.startsWith('/') || path.startsWith('\\') || path.includes('\0')) return false;
|
|
258
|
+
return !path.split(/[\\/]+/).includes('..');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function safeRepoPath(path: string) {
|
|
262
|
+
return path === '' || safePath(path);
|
|
235
263
|
}
|
|
236
264
|
|
|
237
265
|
function safeWorktreePath(path: string): string | null {
|
|
@@ -245,6 +273,42 @@ function safeWorktreePath(path: string): string | null {
|
|
|
245
273
|
return realFull;
|
|
246
274
|
}
|
|
247
275
|
|
|
276
|
+
function readReadme(target: string, dirPath: string): RepoTreeResponse['readme'] {
|
|
277
|
+
const candidates = ['README.md', 'readme.md', 'README.markdown', 'README'];
|
|
278
|
+
for (const name of candidates) {
|
|
279
|
+
const path = dirPath ? `${dirPath}/${name}` : name;
|
|
280
|
+
if (target === 'worktree' || target === '') {
|
|
281
|
+
const full = safeWorktreePath(path);
|
|
282
|
+
if (!full) continue;
|
|
283
|
+
try {
|
|
284
|
+
return { path, text: readFileSync(full, 'utf8') };
|
|
285
|
+
} catch {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const res = git.show(target, path, cwd);
|
|
290
|
+
if (res.code === 0) return { path, text: res.stdout };
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function handleTree(url: URL) {
|
|
296
|
+
const target = url.searchParams.get('ref') || url.searchParams.get('target') || 'worktree';
|
|
297
|
+
const path = (url.searchParams.get('path') || '').replace(/^\/+|\/+$/g, '');
|
|
298
|
+
if (!safeRepoPath(path)) return text('invalid path', 400);
|
|
299
|
+
if (target !== 'worktree' && !git.verifyTreeRef(target, cwd)) return text('invalid target', 400);
|
|
300
|
+
const recursive = url.searchParams.get('recursive') === '1';
|
|
301
|
+
const entries = git.listTree(target, path, cwd, { recursive }).entries;
|
|
302
|
+
return json({
|
|
303
|
+
ref: target,
|
|
304
|
+
path,
|
|
305
|
+
project: basename(cwd),
|
|
306
|
+
branch: git.currentBranch(cwd) || undefined,
|
|
307
|
+
entries,
|
|
308
|
+
readme: readReadme(target, path),
|
|
309
|
+
} satisfies RepoTreeResponse);
|
|
310
|
+
}
|
|
311
|
+
|
|
248
312
|
function handleFileDiff(url: URL) {
|
|
249
313
|
const path = url.searchParams.get('path') || '';
|
|
250
314
|
if (!safePath(path)) return text('invalid path', 400);
|
|
@@ -253,6 +317,21 @@ function handleFileDiff(url: URL) {
|
|
|
253
317
|
if (url.searchParams.get('ignore_blank') === '1') extras.push('--ignore-blank-lines');
|
|
254
318
|
const isUntracked = url.searchParams.get('untracked') === '1';
|
|
255
319
|
const range = { from: url.searchParams.get('from') || '', to: url.searchParams.get('to') || '' };
|
|
320
|
+
if (isSameWorktreeRange(range)) {
|
|
321
|
+
return json({
|
|
322
|
+
path,
|
|
323
|
+
old_path: url.searchParams.get('old_path') || '',
|
|
324
|
+
status: url.searchParams.get('status') || '',
|
|
325
|
+
mode: url.searchParams.get('mode') || 'full',
|
|
326
|
+
diff: '',
|
|
327
|
+
hunk_count: 0,
|
|
328
|
+
rendered_hunk_count: 0,
|
|
329
|
+
line_count: 0,
|
|
330
|
+
truncated: false,
|
|
331
|
+
binary: false,
|
|
332
|
+
generation,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
256
335
|
const { args } = buildRangeArgs(range);
|
|
257
336
|
const oldPath = url.searchParams.get('old_path');
|
|
258
337
|
const cacheKey = isUntracked
|
|
@@ -305,6 +384,7 @@ function handleFileRange(url: URL) {
|
|
|
305
384
|
if (!full) return text('no file', 404);
|
|
306
385
|
content = readFileSync(full, 'utf8');
|
|
307
386
|
} else {
|
|
387
|
+
if (!git.verifyTreeRef(ref, cwd)) return text('invalid ref', 400);
|
|
308
388
|
const res = git.show(ref, path, cwd);
|
|
309
389
|
if (res.code !== 0) return text('not in ref', 404);
|
|
310
390
|
content = res.stdout;
|
|
@@ -322,6 +402,7 @@ function handleRawFile(url: URL) {
|
|
|
322
402
|
const ref = url.searchParams.get('ref') || 'worktree';
|
|
323
403
|
let body: BodyInit;
|
|
324
404
|
if (ref !== 'worktree' && ref !== '') {
|
|
405
|
+
if (!git.verifyTreeRef(ref, cwd)) return text('invalid ref', 400);
|
|
325
406
|
const res = git.show(ref, path, cwd);
|
|
326
407
|
if (res.code !== 0) return text('not in ref', 404);
|
|
327
408
|
body = res.stdout;
|
|
@@ -355,13 +436,14 @@ parseCli();
|
|
|
355
436
|
|
|
356
437
|
const server = Bun.serve({
|
|
357
438
|
hostname: '127.0.0.1',
|
|
358
|
-
port:
|
|
439
|
+
port: listenPort,
|
|
359
440
|
fetch(req) {
|
|
360
441
|
if (!requestAllowed(req)) return text('forbidden', 403);
|
|
361
442
|
const url = new URL(req.url);
|
|
362
443
|
const staticResponse = staticFile(url.pathname);
|
|
363
444
|
if (staticResponse) return staticResponse;
|
|
364
445
|
if (url.pathname === '/diff.json') return handleDiffJson(url);
|
|
446
|
+
if (url.pathname === '/_tree') return handleTree(url);
|
|
365
447
|
if (url.pathname === '/file_diff') return handleFileDiff(url);
|
|
366
448
|
if (url.pathname === '/file_range') return handleFileRange(url);
|
|
367
449
|
if (url.pathname === '/_file') return handleRawFile(url);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
declare const Bun: {
|
|
2
|
-
spawn(args: string[], opts?: Record<string, unknown>):
|
|
2
|
+
spawn(args: string[], opts?: Record<string, unknown>): {
|
|
3
|
+
kill(signal?: string): void;
|
|
4
|
+
exited: Promise<number>;
|
|
5
|
+
};
|
|
3
6
|
spawnSync(args: string[], opts?: Record<string, unknown>): {
|
|
4
7
|
exitCode: number;
|
|
5
8
|
stdout: Uint8Array;
|
|
@@ -16,6 +19,7 @@ declare const process: {
|
|
|
16
19
|
argv: string[];
|
|
17
20
|
cwd(): string;
|
|
18
21
|
platform: 'darwin' | 'win32' | string;
|
|
22
|
+
on(event: 'SIGINT' | 'SIGTERM', listener: () => void): void;
|
|
19
23
|
exit(code?: number): never;
|
|
20
24
|
};
|
|
21
25
|
|
|
@@ -25,6 +29,7 @@ interface ImportMeta {
|
|
|
25
29
|
|
|
26
30
|
declare module 'node:fs' {
|
|
27
31
|
export function existsSync(path: string): boolean;
|
|
32
|
+
export function readdirSync(path: string): string[];
|
|
28
33
|
export function readFileSync(path: string): Buffer;
|
|
29
34
|
export function readFileSync(path: string, encoding: BufferEncoding): string;
|
|
30
35
|
export function realpathSync(path: string): string;
|
package/web-src/types.ts
CHANGED
|
@@ -33,6 +33,24 @@ export type DiffMeta = {
|
|
|
33
33
|
generation?: number;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
export type RepoTreeEntry = {
|
|
37
|
+
name: string;
|
|
38
|
+
path: string;
|
|
39
|
+
type: 'tree' | 'blob' | 'commit';
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type RepoTreeResponse = {
|
|
43
|
+
ref: string;
|
|
44
|
+
path: string;
|
|
45
|
+
project: string;
|
|
46
|
+
branch?: string;
|
|
47
|
+
entries: RepoTreeEntry[];
|
|
48
|
+
readme?: {
|
|
49
|
+
path: string;
|
|
50
|
+
text: string;
|
|
51
|
+
} | null;
|
|
52
|
+
};
|
|
53
|
+
|
|
36
54
|
export type FileDiffResponse = {
|
|
37
55
|
path: string;
|
|
38
56
|
old_path?: string;
|