@youtyan/code-viewer 0.2.11 → 0.3.0
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 +18 -1
- package/dist/code-viewer.js +323 -28
- package/package.json +1 -1
- package/web/app.js +11912 -10764
- package/web/index.html +2 -22
- package/web/style.css +341 -28
package/README.md
CHANGED
|
@@ -14,6 +14,12 @@ Requires Node.js 20 or newer when installed from npm. Development uses
|
|
|
14
14
|
pills" that copy `@path#start-end` for AI agents.
|
|
15
15
|
- Browse commit history per branch and open any commit's changed files and
|
|
16
16
|
diff, with shareable `/history?ref=<branch>&commit=<sha>` links.
|
|
17
|
+
- Open per-file Blame and History tabs on a file detail page (GitHub-style):
|
|
18
|
+
Blame groups consecutive lines from the same commit with an Older→Newer
|
|
19
|
+
colour bar and lets you jump to the originating commit; History embeds the
|
|
20
|
+
same commit list and diff renderer used by `/history` inside the file's
|
|
21
|
+
tab shell, filtered to that path. Both tabs keep the Repository sidebar
|
|
22
|
+
visible.
|
|
17
23
|
- Open files directly from the repository or diff view, including large
|
|
18
24
|
generated files (virtualized source viewer with copy/open-full-view).
|
|
19
25
|
- Preview Markdown with a table of contents, task lists, Mermaid diagrams
|
|
@@ -111,6 +117,15 @@ resolved inside the repository, code blocks are highlighted with Shiki, and
|
|
|
111
117
|
Mermaid diagrams are rendered lazily in the browser (click any diagram to
|
|
112
118
|
open it in a lightbox).
|
|
113
119
|
|
|
120
|
+
A file detail page lays out four tabs — **Preview**, **Code**, **Blame**,
|
|
121
|
+
**History** — modelled after the GitHub file view. `Code` is the default and
|
|
122
|
+
`?preview=1` opts in to the Markdown / media preview. `Blame` and `History`
|
|
123
|
+
each have their own canonical URL (`view=blame`, `view=history`), so deep
|
|
124
|
+
links and the browser back/forward stay in sync. The Blame tab reuses the
|
|
125
|
+
source view's row component, so line numbers, drag-selection of `line=`
|
|
126
|
+
ranges, syntax highlighting and the Viewer Settings code font size all match
|
|
127
|
+
the Code tab.
|
|
128
|
+
|
|
114
129
|
Very large text files use a virtualized source viewer. Only visible rows are
|
|
115
130
|
rendered, and the page includes controls to copy the full file or reopen it in
|
|
116
131
|
the full non-virtual view.
|
|
@@ -227,7 +242,9 @@ Open Datastores in the global navigation to access:
|
|
|
227
242
|
whether each entry came from the browser or the CLI.
|
|
228
243
|
- **ER diagram** — auto-generated entity-relationship diagram showing
|
|
229
244
|
foreign-key relationships between tables.
|
|
230
|
-
- **Schema view** — table columns
|
|
245
|
+
- **Schema view** — table columns (with column comments when the database
|
|
246
|
+
defines them), indexes, foreign keys, triggers, and DDL. Tables themselves
|
|
247
|
+
surface a comment column on the database table list.
|
|
231
248
|
- **Global search** — full-text search across all tables and text columns of
|
|
232
249
|
a database.
|
|
233
250
|
- **Snapshots and diffs** — take point-in-time snapshots of selected tables
|
package/dist/code-viewer.js
CHANGED
|
@@ -759,6 +759,16 @@ import {
|
|
|
759
759
|
statSync
|
|
760
760
|
} from "node:fs";
|
|
761
761
|
import { join as join2 } from "node:path";
|
|
762
|
+
function normalizeBlameRef(ref, base) {
|
|
763
|
+
const rawRef = ref || "worktree";
|
|
764
|
+
if (base === "worktree" && rawRef !== "worktree") {
|
|
765
|
+
return { base: "HEAD", ref: rawRef };
|
|
766
|
+
}
|
|
767
|
+
if (base === "HEAD" && rawRef === "worktree") {
|
|
768
|
+
return { base: "HEAD", ref: "HEAD" };
|
|
769
|
+
}
|
|
770
|
+
return { base, ref: rawRef };
|
|
771
|
+
}
|
|
762
772
|
function run(args, cwd) {
|
|
763
773
|
return runSync(args, cwd);
|
|
764
774
|
}
|
|
@@ -1063,6 +1073,13 @@ function commitHistory(cwd, options) {
|
|
|
1063
1073
|
const skip = Math.max(0, Math.floor(options.skip) || 0);
|
|
1064
1074
|
const limit = Math.max(1, Math.min(Math.floor(options.limit) || 1, MAX_HISTORY_LIMIT));
|
|
1065
1075
|
const { filterArgs, pathspec, shaTerm } = historyQueryArgs(options.query || "");
|
|
1076
|
+
const pathFilter = (options.path || "").trim();
|
|
1077
|
+
const pathArgs = [];
|
|
1078
|
+
if (pathFilter && !pathFilter.includes("\x00") && !pathFilter.startsWith("-")) {
|
|
1079
|
+
if (!pathFilter.endsWith("/"))
|
|
1080
|
+
pathArgs.push("--follow");
|
|
1081
|
+
pathArgs.push("--", pathFilter);
|
|
1082
|
+
}
|
|
1066
1083
|
const res = run([
|
|
1067
1084
|
"git",
|
|
1068
1085
|
"log",
|
|
@@ -1072,7 +1089,8 @@ function commitHistory(cwd, options) {
|
|
|
1072
1089
|
`--format=${HISTORY_FORMAT}`,
|
|
1073
1090
|
...filterArgs,
|
|
1074
1091
|
verified.stdout.trim(),
|
|
1075
|
-
...pathspec
|
|
1092
|
+
...pathspec,
|
|
1093
|
+
...pathArgs
|
|
1076
1094
|
], cwd);
|
|
1077
1095
|
if (res.code !== 0)
|
|
1078
1096
|
return { commits: [], hasMore: false, error: "git log failed" };
|
|
@@ -1173,6 +1191,130 @@ function numstatZ(args, cwd) {
|
|
|
1173
1191
|
function isToolInternalPath(path) {
|
|
1174
1192
|
return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
|
|
1175
1193
|
}
|
|
1194
|
+
function syntheticUncommittedBlameFromWorktree(cwd, path) {
|
|
1195
|
+
const filePath = join2(cwd, path);
|
|
1196
|
+
try {
|
|
1197
|
+
const stat = statSync(filePath);
|
|
1198
|
+
if (!stat.isFile())
|
|
1199
|
+
return { lines: [], commits: {}, error: "not a file" };
|
|
1200
|
+
const text = readFileSync(filePath, "utf8");
|
|
1201
|
+
const normalized = text.replace(/\r\n/g, `
|
|
1202
|
+
`).replace(/\r/g, `
|
|
1203
|
+
`);
|
|
1204
|
+
const lineCount = normalized.length ? normalized.endsWith(`
|
|
1205
|
+
`) ? normalized.length - 1 === 0 ? 1 : normalized.split(`
|
|
1206
|
+
`).length - 1 : normalized.split(`
|
|
1207
|
+
`).length : 1;
|
|
1208
|
+
const lines = [];
|
|
1209
|
+
for (let i = 1;i <= lineCount; i++) {
|
|
1210
|
+
lines.push({ lineNo: i, sha: BLAME_ZERO_SHA, isUncommitted: true });
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
lines,
|
|
1214
|
+
commits: {
|
|
1215
|
+
[BLAME_ZERO_SHA]: {
|
|
1216
|
+
sha: BLAME_ZERO_SHA,
|
|
1217
|
+
author: "Not Committed Yet",
|
|
1218
|
+
authorMail: "",
|
|
1219
|
+
authorTime: 0,
|
|
1220
|
+
summary: "Working tree",
|
|
1221
|
+
isUncommitted: true
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1224
|
+
isUntracked: true,
|
|
1225
|
+
isSynthetic: true
|
|
1226
|
+
};
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
|
|
1229
|
+
return { lines: [], commits: {}, error: "file not found" };
|
|
1230
|
+
}
|
|
1231
|
+
return { lines: [], commits: {}, error: "file not readable" };
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
function blame(cwd, options) {
|
|
1235
|
+
const path = options.path;
|
|
1236
|
+
if (!path || path.includes("\x00") || path.startsWith("-")) {
|
|
1237
|
+
return { lines: [], commits: {}, error: "invalid path" };
|
|
1238
|
+
}
|
|
1239
|
+
const normalized = normalizeBlameRef(options.ref, options.base);
|
|
1240
|
+
const args = ["git", "blame", "--porcelain"];
|
|
1241
|
+
if (normalized.base === "HEAD") {
|
|
1242
|
+
if (normalized.ref.startsWith("-") || normalized.ref.includes("\x00"))
|
|
1243
|
+
return { lines: [], commits: {}, error: "invalid ref" };
|
|
1244
|
+
args.push(normalized.ref);
|
|
1245
|
+
}
|
|
1246
|
+
args.push("--", path);
|
|
1247
|
+
const res = run(args, cwd);
|
|
1248
|
+
if (res.code !== 0) {
|
|
1249
|
+
if (normalized.base === "worktree") {
|
|
1250
|
+
return syntheticUncommittedBlameFromWorktree(cwd, path);
|
|
1251
|
+
}
|
|
1252
|
+
return {
|
|
1253
|
+
lines: [],
|
|
1254
|
+
commits: {},
|
|
1255
|
+
error: res.stderr.trim() || "blame failed"
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
const lines = [];
|
|
1259
|
+
const commits = {};
|
|
1260
|
+
const rawLines = res.stdout.split(`
|
|
1261
|
+
`);
|
|
1262
|
+
let i = 0;
|
|
1263
|
+
while (i < rawLines.length) {
|
|
1264
|
+
const headerLine = rawLines[i];
|
|
1265
|
+
if (!headerLine) {
|
|
1266
|
+
i++;
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
const headerMatch = /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/.exec(headerLine);
|
|
1270
|
+
if (!headerMatch) {
|
|
1271
|
+
i++;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
const sha = headerMatch[1];
|
|
1275
|
+
const finalLine = Number(headerMatch[3]);
|
|
1276
|
+
i++;
|
|
1277
|
+
let commit = commits[sha];
|
|
1278
|
+
if (!commit) {
|
|
1279
|
+
commit = {
|
|
1280
|
+
sha,
|
|
1281
|
+
author: "",
|
|
1282
|
+
authorMail: "",
|
|
1283
|
+
authorTime: 0,
|
|
1284
|
+
summary: "",
|
|
1285
|
+
isUncommitted: sha === BLAME_ZERO_SHA
|
|
1286
|
+
};
|
|
1287
|
+
commits[sha] = commit;
|
|
1288
|
+
}
|
|
1289
|
+
while (i < rawLines.length && !rawLines[i].startsWith("\t")) {
|
|
1290
|
+
const metaLine = rawLines[i++];
|
|
1291
|
+
if (!metaLine)
|
|
1292
|
+
continue;
|
|
1293
|
+
const sp = metaLine.indexOf(" ");
|
|
1294
|
+
const key = sp >= 0 ? metaLine.slice(0, sp) : metaLine;
|
|
1295
|
+
const val = sp >= 0 ? metaLine.slice(sp + 1) : "";
|
|
1296
|
+
if (key === "author" && !commit.author)
|
|
1297
|
+
commit.author = val;
|
|
1298
|
+
else if (key === "author-mail" && !commit.authorMail)
|
|
1299
|
+
commit.authorMail = val.replace(/^</, "").replace(/>$/, "");
|
|
1300
|
+
else if (key === "author-time" && !commit.authorTime)
|
|
1301
|
+
commit.authorTime = Number(val) || 0;
|
|
1302
|
+
else if (key === "summary" && !commit.summary)
|
|
1303
|
+
commit.summary = val;
|
|
1304
|
+
}
|
|
1305
|
+
if (i < rawLines.length && rawLines[i].startsWith("\t"))
|
|
1306
|
+
i++;
|
|
1307
|
+
if (Number.isFinite(finalLine) && finalLine > 0) {
|
|
1308
|
+
lines.push({
|
|
1309
|
+
lineNo: finalLine,
|
|
1310
|
+
sha,
|
|
1311
|
+
isUncommitted: sha === BLAME_ZERO_SHA
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
lines.sort((a, b) => a.lineNo - b.lineNo);
|
|
1316
|
+
return { lines, commits };
|
|
1317
|
+
}
|
|
1176
1318
|
function untracked(cwd, path = "") {
|
|
1177
1319
|
const args = ["git", "ls-files", "--others", "--exclude-standard"];
|
|
1178
1320
|
if (path)
|
|
@@ -1509,7 +1651,7 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
|
|
|
1509
1651
|
lineTruncated
|
|
1510
1652
|
};
|
|
1511
1653
|
}
|
|
1512
|
-
var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
|
|
1654
|
+
var BLAME_ZERO_SHA = "0000000000000000000000000000000000000000", WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
|
|
1513
1655
|
var init_git = __esm(() => {
|
|
1514
1656
|
init_runtime();
|
|
1515
1657
|
DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
|
|
@@ -4016,12 +4158,41 @@ function sanitizeDbUiPrefs(raw) {
|
|
|
4016
4158
|
}
|
|
4017
4159
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
4018
4160
|
}
|
|
4161
|
+
function sanitizeDbUiExpandedTables(raw) {
|
|
4162
|
+
if (!isRecord(raw))
|
|
4163
|
+
return;
|
|
4164
|
+
const out = {};
|
|
4165
|
+
let scopeCount = 0;
|
|
4166
|
+
for (const [scopeRaw, tablesRaw] of Object.entries(raw)) {
|
|
4167
|
+
if (scopeCount >= MAX_DB_UI_EXPANDED_SCOPES)
|
|
4168
|
+
break;
|
|
4169
|
+
const scope = safeObjectKey(scopeRaw);
|
|
4170
|
+
if (!scope)
|
|
4171
|
+
continue;
|
|
4172
|
+
const tables = normalizeStringList(tablesRaw, {
|
|
4173
|
+
maxItems: MAX_DB_UI_TABLES,
|
|
4174
|
+
maxLen: MAX_KEY_LEN,
|
|
4175
|
+
sort: true
|
|
4176
|
+
});
|
|
4177
|
+
if (!tables || tables.length === 0)
|
|
4178
|
+
continue;
|
|
4179
|
+
out[scope] = tables;
|
|
4180
|
+
scopeCount++;
|
|
4181
|
+
}
|
|
4182
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
4183
|
+
}
|
|
4019
4184
|
function sanitizeDbUiState(raw) {
|
|
4020
4185
|
if (!isRecord(raw))
|
|
4021
4186
|
return emptyDbUiState();
|
|
4022
4187
|
const prefs = sanitizeDbUiPrefs(raw.prefs);
|
|
4188
|
+
const expandedTables = sanitizeDbUiExpandedTables(raw.expandedTables);
|
|
4023
4189
|
if (!isRecord(raw.columnWidths)) {
|
|
4024
|
-
|
|
4190
|
+
const out2 = emptyDbUiState();
|
|
4191
|
+
if (expandedTables)
|
|
4192
|
+
out2.expandedTables = expandedTables;
|
|
4193
|
+
if (prefs)
|
|
4194
|
+
out2.prefs = prefs;
|
|
4195
|
+
return out2;
|
|
4025
4196
|
}
|
|
4026
4197
|
const columnWidths = {};
|
|
4027
4198
|
let dbCount = 0;
|
|
@@ -4062,6 +4233,8 @@ function sanitizeDbUiState(raw) {
|
|
|
4062
4233
|
dbCount++;
|
|
4063
4234
|
}
|
|
4064
4235
|
const out = { version: 1, columnWidths };
|
|
4236
|
+
if (expandedTables)
|
|
4237
|
+
out.expandedTables = expandedTables;
|
|
4065
4238
|
if (prefs)
|
|
4066
4239
|
out.prefs = prefs;
|
|
4067
4240
|
return out;
|
|
@@ -4079,12 +4252,30 @@ function mergeDbUiPrefs(current, patch) {
|
|
|
4079
4252
|
}
|
|
4080
4253
|
return Object.keys(next).length > 0 ? next : undefined;
|
|
4081
4254
|
}
|
|
4255
|
+
function mergeDbUiExpandedTables(current, patch) {
|
|
4256
|
+
if (!isRecord(patch))
|
|
4257
|
+
return current;
|
|
4258
|
+
const next = { ...current ?? {} };
|
|
4259
|
+
for (const [scope, tablesRaw] of Object.entries(patch)) {
|
|
4260
|
+
if (tablesRaw === null) {
|
|
4261
|
+
delete next[scope];
|
|
4262
|
+
continue;
|
|
4263
|
+
}
|
|
4264
|
+
next[scope] = tablesRaw;
|
|
4265
|
+
}
|
|
4266
|
+
return sanitizeDbUiExpandedTables(next);
|
|
4267
|
+
}
|
|
4082
4268
|
function mergeDbUiState(current, patch) {
|
|
4083
4269
|
if (!isRecord(patch))
|
|
4084
4270
|
return current;
|
|
4085
4271
|
const mergedPrefs = "prefs" in patch ? mergeDbUiPrefs(current.prefs, patch.prefs) : current.prefs;
|
|
4272
|
+
const mergedExpandedTables = "expandedTables" in patch ? mergeDbUiExpandedTables(current.expandedTables, patch.expandedTables) : current.expandedTables;
|
|
4086
4273
|
if (!isRecord(patch.columnWidths)) {
|
|
4087
4274
|
const merged = { ...current, version: 1 };
|
|
4275
|
+
if (mergedExpandedTables)
|
|
4276
|
+
merged.expandedTables = mergedExpandedTables;
|
|
4277
|
+
else
|
|
4278
|
+
delete merged.expandedTables;
|
|
4088
4279
|
if (mergedPrefs)
|
|
4089
4280
|
merged.prefs = mergedPrefs;
|
|
4090
4281
|
else
|
|
@@ -4124,6 +4315,7 @@ function mergeDbUiState(current, patch) {
|
|
|
4124
4315
|
...current,
|
|
4125
4316
|
...patch,
|
|
4126
4317
|
columnWidths,
|
|
4318
|
+
expandedTables: mergedExpandedTables,
|
|
4127
4319
|
prefs: mergedPrefs,
|
|
4128
4320
|
version: 1
|
|
4129
4321
|
});
|
|
@@ -4155,7 +4347,7 @@ async function patchDbUiState(root, patch) {
|
|
|
4155
4347
|
return { state: next, result: next };
|
|
4156
4348
|
});
|
|
4157
4349
|
}
|
|
4158
|
-
var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
|
|
4350
|
+
var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, MAX_DB_UI_EXPANDED_SCOPES = 500, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
|
|
4159
4351
|
var init_state_store = __esm(() => {
|
|
4160
4352
|
init_json_store();
|
|
4161
4353
|
DB_UI_BOOL_PREF_KEYS = ["s3TooltipEnabled", "inferFkRails"];
|
|
@@ -5268,9 +5460,12 @@ function createDockerAdapter(config) {
|
|
|
5268
5460
|
const tableLiteral = table.replace(/'/g, "''");
|
|
5269
5461
|
if (config.kind === "postgresql") {
|
|
5270
5462
|
const schemaLiteral = postgresSchemaLiteral();
|
|
5271
|
-
return `SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, CASE WHEN pk.column_name IS NULL THEN 'NO' ELSE 'YES' END FROM information_schema.columns c LEFT JOIN (SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema AND tc.table_name = kcu.table_name WHERE tc.table_schema = ${schemaLiteral} AND tc.table_name = '${tableLiteral}' AND tc.constraint_type = 'PRIMARY KEY') pk ON pk.column_name = c.column_name WHERE c.table_schema = ${schemaLiteral} AND c.table_name = '${tableLiteral}' ORDER BY c.ordinal_position`;
|
|
5463
|
+
return `SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, CASE WHEN pk.column_name IS NULL THEN 'NO' ELSE 'YES' END, COALESCE(d.description, '') FROM information_schema.columns c JOIN pg_namespace n ON n.nspname = c.table_schema JOIN pg_class cls ON cls.relnamespace = n.oid AND cls.relname = c.table_name LEFT JOIN pg_attribute a ON a.attrelid = cls.oid AND a.attname = c.column_name AND a.attnum > 0 AND NOT a.attisdropped LEFT JOIN pg_description d ON d.objoid = cls.oid AND d.objsubid = a.attnum LEFT JOIN (SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema AND tc.table_name = kcu.table_name WHERE tc.table_schema = ${schemaLiteral} AND tc.table_name = '${tableLiteral}' AND tc.constraint_type = 'PRIMARY KEY') pk ON pk.column_name = c.column_name WHERE c.table_schema = ${schemaLiteral} AND c.table_name = '${tableLiteral}' ORDER BY c.ordinal_position`;
|
|
5272
5464
|
}
|
|
5273
|
-
return `SELECT column_name, column_type, is_nullable, column_default, column_key FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = '${tableLiteral}' ORDER BY ordinal_position`;
|
|
5465
|
+
return `SELECT column_name, column_type, is_nullable, column_default, column_key, column_comment FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = '${tableLiteral}' ORDER BY ordinal_position`;
|
|
5466
|
+
}
|
|
5467
|
+
function columnCommentFromInfoValue(value) {
|
|
5468
|
+
return value ? value : null;
|
|
5274
5469
|
}
|
|
5275
5470
|
function columnsFromInfoRows(rows) {
|
|
5276
5471
|
if (config.kind === "postgresql") {
|
|
@@ -5279,7 +5474,8 @@ function createDockerAdapter(config) {
|
|
|
5279
5474
|
type: row[1],
|
|
5280
5475
|
nullable: row[2] === "YES",
|
|
5281
5476
|
primaryKey: row[4] === "YES",
|
|
5282
|
-
defaultValue: row[3] === "" ? null : row[3]
|
|
5477
|
+
defaultValue: row[3] === "" ? null : row[3],
|
|
5478
|
+
comment: columnCommentFromInfoValue(row[5])
|
|
5283
5479
|
}));
|
|
5284
5480
|
}
|
|
5285
5481
|
return rows.map((row) => ({
|
|
@@ -5287,7 +5483,8 @@ function createDockerAdapter(config) {
|
|
|
5287
5483
|
type: row[1],
|
|
5288
5484
|
nullable: row[2] === "YES",
|
|
5289
5485
|
primaryKey: row[4] === "PRI",
|
|
5290
|
-
defaultValue: row[3] === "NULL" ? null : row[3]
|
|
5486
|
+
defaultValue: row[3] === "NULL" ? null : row[3],
|
|
5487
|
+
comment: columnCommentFromInfoValue(row[5])
|
|
5291
5488
|
}));
|
|
5292
5489
|
}
|
|
5293
5490
|
async function fetchColumnsAsyncUncached(table, signal) {
|
|
@@ -5386,10 +5583,10 @@ function createDockerAdapter(config) {
|
|
|
5386
5583
|
let sql;
|
|
5387
5584
|
if (config.kind === "postgresql") {
|
|
5388
5585
|
const inList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
|
|
5389
|
-
sql = `SELECT table_name, column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = ${postgresSchemaLiteral()} AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
|
|
5586
|
+
sql = `SELECT c.table_name, c.column_name, c.data_type, c.is_nullable, c.column_default, COALESCE(d.description, '') FROM information_schema.columns c JOIN pg_namespace n ON n.nspname = c.table_schema JOIN pg_class cls ON cls.relnamespace = n.oid AND cls.relname = c.table_name LEFT JOIN pg_attribute a ON a.attrelid = cls.oid AND a.attname = c.column_name AND a.attnum > 0 AND NOT a.attisdropped LEFT JOIN pg_description d ON d.objoid = cls.oid AND d.objsubid = a.attnum WHERE c.table_schema = ${postgresSchemaLiteral()} AND c.table_name IN (${inList}) ORDER BY c.table_name, c.ordinal_position`;
|
|
5390
5587
|
} else {
|
|
5391
5588
|
const inList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
|
|
5392
|
-
sql = `SELECT table_name, column_name, column_type, is_nullable, column_default, column_key FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
|
|
5589
|
+
sql = `SELECT table_name, column_name, column_type, is_nullable, column_default, column_key, column_comment FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
|
|
5393
5590
|
}
|
|
5394
5591
|
try {
|
|
5395
5592
|
const queryResult = await execAsync(sql, signal);
|
|
@@ -5425,7 +5622,8 @@ function createDockerAdapter(config) {
|
|
|
5425
5622
|
type: row[2],
|
|
5426
5623
|
nullable: row[3] === "YES",
|
|
5427
5624
|
primaryKey: pkCols.has(row[1]),
|
|
5428
|
-
defaultValue: row[4] === "" ? null : row[4]
|
|
5625
|
+
defaultValue: row[4] === "" ? null : row[4],
|
|
5626
|
+
comment: columnCommentFromInfoValue(row[5])
|
|
5429
5627
|
}));
|
|
5430
5628
|
} else {
|
|
5431
5629
|
cols = rows.map((row) => ({
|
|
@@ -5433,7 +5631,8 @@ function createDockerAdapter(config) {
|
|
|
5433
5631
|
type: row[2],
|
|
5434
5632
|
nullable: row[3] === "YES",
|
|
5435
5633
|
primaryKey: row[5] === "PRI",
|
|
5436
|
-
defaultValue: row[4] === "NULL" ? null : row[4]
|
|
5634
|
+
defaultValue: row[4] === "NULL" ? null : row[4],
|
|
5635
|
+
comment: columnCommentFromInfoValue(row[6])
|
|
5437
5636
|
}));
|
|
5438
5637
|
}
|
|
5439
5638
|
columnCache.set(tbl, cols);
|
|
@@ -5526,12 +5725,7 @@ function createDockerAdapter(config) {
|
|
|
5526
5725
|
async getFilteredTablePageWithMeta(table, options, signal) {
|
|
5527
5726
|
const id = tableIdentifier(table);
|
|
5528
5727
|
const columnsPromise = tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table, signal));
|
|
5529
|
-
|
|
5530
|
-
try {
|
|
5531
|
-
columns = await columnsPromise;
|
|
5532
|
-
} catch (err) {
|
|
5533
|
-
throw err;
|
|
5534
|
-
}
|
|
5728
|
+
const columns = await columnsPromise;
|
|
5535
5729
|
const columnNames = columns.map((column) => column.name);
|
|
5536
5730
|
const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames), config.kind);
|
|
5537
5731
|
const where = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), config.kind, filterExactColumns(options.exact, columnNames)).where;
|
|
@@ -11607,7 +11801,7 @@ function fileToMeta(file, range, extraQs) {
|
|
|
11607
11801
|
untracked: file.untracked || false
|
|
11608
11802
|
};
|
|
11609
11803
|
}
|
|
11610
|
-
function computePayload(extras, range) {
|
|
11804
|
+
function computePayload(extras, range, pathFilter = "") {
|
|
11611
11805
|
if (isSameWorktreeRange(range)) {
|
|
11612
11806
|
return {
|
|
11613
11807
|
files: [],
|
|
@@ -11623,8 +11817,9 @@ function computePayload(extras, range) {
|
|
|
11623
11817
|
const files = fileMeta(fullArgs, cwd, false);
|
|
11624
11818
|
if (includeUntracked(range, refs2))
|
|
11625
11819
|
files.push(...untrackedMeta(cwd));
|
|
11626
|
-
files.
|
|
11627
|
-
|
|
11820
|
+
const filteredFiles = pathFilter ? files.filter((file) => file.path === pathFilter || file.old_path === pathFilter) : files;
|
|
11821
|
+
filteredFiles.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
11822
|
+
filteredFiles.forEach((file, i) => {
|
|
11628
11823
|
file.order = i + 1;
|
|
11629
11824
|
});
|
|
11630
11825
|
const extraQs = {};
|
|
@@ -11634,7 +11829,7 @@ function computePayload(extras, range) {
|
|
|
11634
11829
|
if (e === "--ignore-blank-lines")
|
|
11635
11830
|
extraQs.ignore_blank = "1";
|
|
11636
11831
|
}
|
|
11637
|
-
const meta =
|
|
11832
|
+
const meta = filteredFiles.map((file) => fileToMeta(file, range, extraQs));
|
|
11638
11833
|
const totals = meta.reduce((acc, file) => {
|
|
11639
11834
|
acc.additions += file.additions || 0;
|
|
11640
11835
|
acc.deletions += file.deletions || 0;
|
|
@@ -11661,9 +11856,12 @@ function handleDiffJson(url) {
|
|
|
11661
11856
|
from: url.searchParams.get("from") || "",
|
|
11662
11857
|
to: url.searchParams.get("to") || ""
|
|
11663
11858
|
};
|
|
11664
|
-
const
|
|
11859
|
+
const path = url.searchParams.get("path") || "";
|
|
11860
|
+
if (path && !safePath(path))
|
|
11861
|
+
return text("invalid path", 400);
|
|
11862
|
+
const key = `${range.from}|${range.to}|${url.searchParams.get("ignore_ws") || ""}|${url.searchParams.get("ignore_blank") || ""}|${path}`;
|
|
11665
11863
|
if (url.searchParams.get("nocache") === "1") {
|
|
11666
|
-
const payload2 = computePayload(extras, range);
|
|
11864
|
+
const payload2 = computePayload(extras, range, path);
|
|
11667
11865
|
const sig = JSON.stringify({ ...payload2, generation: undefined });
|
|
11668
11866
|
const cached2 = metaCache.get(key);
|
|
11669
11867
|
if (!cached2 || cached2.sig !== sig) {
|
|
@@ -11689,7 +11887,7 @@ function handleDiffJson(url) {
|
|
|
11689
11887
|
"Cache-Control": "no-store"
|
|
11690
11888
|
}
|
|
11691
11889
|
});
|
|
11692
|
-
const payload = computePayload(extras, range);
|
|
11890
|
+
const payload = computePayload(extras, range, path);
|
|
11693
11891
|
const body = JSON.stringify(payload);
|
|
11694
11892
|
setTimedCacheEntry(metaCache, key, {
|
|
11695
11893
|
body,
|
|
@@ -12106,15 +12304,109 @@ function handleLog(url) {
|
|
|
12106
12304
|
const ref = url.searchParams.get("ref") || "HEAD";
|
|
12107
12305
|
const skip = Number(url.searchParams.get("skip") || "0");
|
|
12108
12306
|
const limit = Number(url.searchParams.get("limit") || "50");
|
|
12307
|
+
const path = url.searchParams.get("path") || "";
|
|
12308
|
+
if (path && !safePath(path))
|
|
12309
|
+
return text("invalid path", 400);
|
|
12109
12310
|
const result = commitHistory(cwd, {
|
|
12110
12311
|
ref,
|
|
12111
12312
|
skip: Number.isFinite(skip) ? skip : 0,
|
|
12112
12313
|
limit: Number.isFinite(limit) ? limit : 50,
|
|
12113
|
-
query: url.searchParams.get("q") || ""
|
|
12314
|
+
query: url.searchParams.get("q") || "",
|
|
12315
|
+
...path ? { path } : {}
|
|
12114
12316
|
});
|
|
12115
12317
|
if (result.error)
|
|
12116
12318
|
return text(result.error, 400);
|
|
12117
|
-
|
|
12319
|
+
const wantsWorktreeHead = path && skip === 0 && (ref === "worktree" || url.searchParams.get("worktree") === "1");
|
|
12320
|
+
let commits = result.commits;
|
|
12321
|
+
let hasWorktree = false;
|
|
12322
|
+
if (wantsWorktreeHead) {
|
|
12323
|
+
const status = runSync([
|
|
12324
|
+
"git",
|
|
12325
|
+
"-c",
|
|
12326
|
+
"core.quotepath=false",
|
|
12327
|
+
"status",
|
|
12328
|
+
"--porcelain=v1",
|
|
12329
|
+
"-z",
|
|
12330
|
+
"--untracked-files=normal",
|
|
12331
|
+
"--",
|
|
12332
|
+
path
|
|
12333
|
+
], cwd);
|
|
12334
|
+
if (status.code === 0 && status.stdout.length > 0) {
|
|
12335
|
+
const parts = status.stdout.split("\x00").filter(Boolean);
|
|
12336
|
+
if (parts.length > 0) {
|
|
12337
|
+
hasWorktree = true;
|
|
12338
|
+
commits = [
|
|
12339
|
+
{
|
|
12340
|
+
sha: "worktree",
|
|
12341
|
+
subject: "未コミット変更 (Working tree)",
|
|
12342
|
+
author: "",
|
|
12343
|
+
when: "",
|
|
12344
|
+
parents: [],
|
|
12345
|
+
body: ""
|
|
12346
|
+
},
|
|
12347
|
+
...commits
|
|
12348
|
+
];
|
|
12349
|
+
}
|
|
12350
|
+
}
|
|
12351
|
+
}
|
|
12352
|
+
return json2({
|
|
12353
|
+
commits,
|
|
12354
|
+
hasMore: result.hasMore,
|
|
12355
|
+
generation,
|
|
12356
|
+
...hasWorktree ? { hasWorktree: true } : {}
|
|
12357
|
+
});
|
|
12358
|
+
}
|
|
12359
|
+
function blamePathKey(p) {
|
|
12360
|
+
try {
|
|
12361
|
+
const st = statSync3(join13(cwd, p));
|
|
12362
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
12363
|
+
} catch {
|
|
12364
|
+
return "missing";
|
|
12365
|
+
}
|
|
12366
|
+
}
|
|
12367
|
+
function rememberBlame(key, value) {
|
|
12368
|
+
if (blameCache.has(key))
|
|
12369
|
+
blameCache.delete(key);
|
|
12370
|
+
blameCache.set(key, value);
|
|
12371
|
+
while (blameCache.size > BLAME_CACHE_MAX) {
|
|
12372
|
+
const oldest = blameCache.keys().next();
|
|
12373
|
+
if (oldest.done)
|
|
12374
|
+
break;
|
|
12375
|
+
blameCache.delete(oldest.value);
|
|
12376
|
+
}
|
|
12377
|
+
}
|
|
12378
|
+
function handleFileBlame(url) {
|
|
12379
|
+
const path = url.searchParams.get("path") || "";
|
|
12380
|
+
if (!safePath(path))
|
|
12381
|
+
return text("invalid path", 400);
|
|
12382
|
+
const ref = url.searchParams.get("ref") || "worktree";
|
|
12383
|
+
if (!ref || ref.startsWith("-") || ref.includes("\x00"))
|
|
12384
|
+
return text("invalid ref", 400);
|
|
12385
|
+
const rawBase = url.searchParams.get("base");
|
|
12386
|
+
const requestedBase = rawBase === "HEAD" ? "HEAD" : rawBase === "worktree" ? "worktree" : ref === "worktree" ? "worktree" : "HEAD";
|
|
12387
|
+
const normalized = normalizeBlameRef(ref, requestedBase);
|
|
12388
|
+
const { base } = normalized;
|
|
12389
|
+
let cacheKey;
|
|
12390
|
+
if (base === "worktree") {
|
|
12391
|
+
cacheKey = `worktree|${path}|${blamePathKey(path)}`;
|
|
12392
|
+
} else {
|
|
12393
|
+
const resolved = runSync(["git", "rev-parse", "--verify", `${normalized.ref}^{commit}`], cwd);
|
|
12394
|
+
if (resolved.code !== 0)
|
|
12395
|
+
return text("unknown ref", 400);
|
|
12396
|
+
cacheKey = `HEAD|${path}|${resolved.stdout.trim()}`;
|
|
12397
|
+
}
|
|
12398
|
+
const cached = blameCache.get(cacheKey);
|
|
12399
|
+
if (cached) {
|
|
12400
|
+
if (blameCache.has(cacheKey)) {
|
|
12401
|
+
blameCache.delete(cacheKey);
|
|
12402
|
+
blameCache.set(cacheKey, cached);
|
|
12403
|
+
}
|
|
12404
|
+
return json2({ ...cached, base, ref, generation });
|
|
12405
|
+
}
|
|
12406
|
+
const result = blame(cwd, { path, ref: normalized.ref, base });
|
|
12407
|
+
if (!result.error)
|
|
12408
|
+
rememberBlame(cacheKey, result);
|
|
12409
|
+
return json2({ ...result, base, ref, generation });
|
|
12118
12410
|
}
|
|
12119
12411
|
function handleFileDiff(url) {
|
|
12120
12412
|
const path = url.searchParams.get("path") || "";
|
|
@@ -13123,7 +13415,7 @@ function restartWorktreeWatch() {
|
|
|
13123
13415
|
}
|
|
13124
13416
|
worktreeWatch = startScopedWorktreeWatch();
|
|
13125
13417
|
}
|
|
13126
|
-
var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
|
|
13418
|
+
var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, blameCache, BLAME_CACHE_MAX = 64, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
|
|
13127
13419
|
var init_preview = __esm(async () => {
|
|
13128
13420
|
init_routes();
|
|
13129
13421
|
init_annotations();
|
|
@@ -13191,6 +13483,7 @@ var init_preview = __esm(async () => {
|
|
|
13191
13483
|
sseClients = new Set;
|
|
13192
13484
|
sseKeepalives = new Map;
|
|
13193
13485
|
fileCache = new Map;
|
|
13486
|
+
blameCache = new Map;
|
|
13194
13487
|
metaCache = new Map;
|
|
13195
13488
|
fileListCache = new Map;
|
|
13196
13489
|
lineIndexCache = new Map;
|
|
@@ -13223,6 +13516,8 @@ var init_preview = __esm(async () => {
|
|
|
13223
13516
|
return handleRefCommits(url);
|
|
13224
13517
|
if (url.pathname === "/_log")
|
|
13225
13518
|
return handleLog(url);
|
|
13519
|
+
if (url.pathname === "/_file_blame")
|
|
13520
|
+
return handleFileBlame(url);
|
|
13226
13521
|
if (url.pathname === "/file_diff")
|
|
13227
13522
|
return handleFileDiff(url);
|
|
13228
13523
|
if (url.pathname === "/file_range")
|