@xbzbing/dsh-git-panel 0.1.0 → 0.2.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 +7 -5
- package/README_EN.md +7 -5
- package/cordis.patch.yml +1 -1
- package/lib/client.js +37 -27
- package/lib/host/actions.d.ts +2 -3
- package/lib/host/core.d.ts +20 -1
- package/lib/host/git.d.ts +2 -0
- package/lib/host/index.js +319 -139
- package/lib/host/parser.d.ts +8 -1
- package/lib/host/types.d.ts +24 -4
- package/lib/host/validate.d.ts +10 -0
- package/lib/host/version.d.ts +6 -4
- package/package.json +4 -3
package/lib/host/index.js
CHANGED
|
@@ -77,19 +77,22 @@ function createGitRunner(subprocess, timeoutMs, maxBytes) {
|
|
|
77
77
|
try {
|
|
78
78
|
outcome = await handle.done;
|
|
79
79
|
} catch (error) {
|
|
80
|
+
const cancelled2 = opts.signal?.aborted === true && !controller.signal.aborted;
|
|
80
81
|
if (controller.signal.aborted || opts.signal?.aborted === true) {
|
|
81
|
-
return { exitCode: null, stdout: "", stderr: "", timedOut:
|
|
82
|
+
return { exitCode: null, stdout: "", stderr: "", timedOut: !cancelled2, cancelled: cancelled2, stdoutLossy: false };
|
|
82
83
|
}
|
|
83
84
|
throw error;
|
|
84
85
|
}
|
|
85
86
|
const stdout = handle.collected.stdout?.readFrom(0);
|
|
86
87
|
const stderr = handle.collected.stderr?.readFrom(0);
|
|
87
88
|
const resolved = await resolveStdout(stdout);
|
|
89
|
+
const cancelled = opts.signal?.aborted === true && !controller.signal.aborted;
|
|
88
90
|
return {
|
|
89
91
|
exitCode: outcome.exitCode,
|
|
90
92
|
stdout: resolved.text,
|
|
91
93
|
stderr: stderr?.text ?? "",
|
|
92
|
-
timedOut: controller.signal.aborted
|
|
94
|
+
timedOut: controller.signal.aborted,
|
|
95
|
+
cancelled,
|
|
93
96
|
stdoutLossy: resolved.lossy
|
|
94
97
|
};
|
|
95
98
|
} finally {
|
|
@@ -172,12 +175,13 @@ function parseGraphLog(stdout) {
|
|
|
172
175
|
if (rec.trim() === "") continue;
|
|
173
176
|
const parts = rec.split("");
|
|
174
177
|
if (parts.length < 7) continue;
|
|
175
|
-
const [hash, shortHash, parentsRaw, author, dateIso, decoration
|
|
178
|
+
const [hash, shortHash, parentsRaw, author, dateIso, decoration] = parts;
|
|
179
|
+
const subject = parts.slice(6).join("");
|
|
176
180
|
const parents = (parentsRaw ?? "").trim() === "" ? [] : parentsRaw.trim().split(/\s+/);
|
|
177
181
|
out.push({
|
|
178
182
|
hash: hash ?? "",
|
|
179
183
|
shortHash: shortHash ?? "",
|
|
180
|
-
subject
|
|
184
|
+
subject,
|
|
181
185
|
author: author ?? "",
|
|
182
186
|
dateIso: dateIso ?? "",
|
|
183
187
|
parents,
|
|
@@ -226,23 +230,38 @@ function parseBranches(stdout) {
|
|
|
226
230
|
}
|
|
227
231
|
return out;
|
|
228
232
|
}
|
|
233
|
+
function parseTags(stdout) {
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const line of stdout.split("\n")) {
|
|
236
|
+
if (line.trim() === "") continue;
|
|
237
|
+
const [name, shortHash = ""] = line.split("\0");
|
|
238
|
+
if (name) out.push({ name, shortHash: shortHash === "" ? null : shortHash });
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
229
242
|
function parseNameStatus(stdout) {
|
|
230
243
|
const out = [];
|
|
231
244
|
const tokens = stdout.split("\0");
|
|
232
|
-
|
|
245
|
+
let i = 0;
|
|
246
|
+
while (i < tokens.length) {
|
|
233
247
|
const tok = tokens[i];
|
|
234
|
-
if (tok === void 0 || tok === "")
|
|
248
|
+
if (tok === void 0 || tok === "") {
|
|
249
|
+
i += 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
235
252
|
const code = tok[0];
|
|
236
|
-
if (code === void 0 || !/[AMDRCTU]/.test(code))
|
|
253
|
+
if (code === void 0 || !/[AMDRCTU]/.test(code)) break;
|
|
237
254
|
if (code === "R" || code === "C") {
|
|
238
255
|
const newPath = tokens[i + 2];
|
|
256
|
+
if (newPath === void 0) break;
|
|
257
|
+
out.push({ path: newPath, status: statusOf(code) });
|
|
258
|
+
i += 3;
|
|
259
|
+
} else {
|
|
260
|
+
const path = tokens[i + 1];
|
|
261
|
+
if (path === void 0) break;
|
|
262
|
+
if (path !== "") out.push({ path, status: statusOf(code) });
|
|
239
263
|
i += 2;
|
|
240
|
-
if (newPath) out.push({ path: newPath, status: statusOf(code) });
|
|
241
|
-
continue;
|
|
242
264
|
}
|
|
243
|
-
const path = tokens[i + 1];
|
|
244
|
-
i += 1;
|
|
245
|
-
if (path) out.push({ path, status: statusOf(code) });
|
|
246
265
|
}
|
|
247
266
|
return out;
|
|
248
267
|
}
|
|
@@ -261,6 +280,25 @@ function sumNumstat(stdout) {
|
|
|
261
280
|
return { insertions, deletions };
|
|
262
281
|
}
|
|
263
282
|
|
|
283
|
+
// src/host/validate.ts
|
|
284
|
+
import { isAbsolute, normalize } from "node:path";
|
|
285
|
+
function isSafePath(path) {
|
|
286
|
+
if (path === "" || isAbsolute(path)) return false;
|
|
287
|
+
const norm = normalize(path);
|
|
288
|
+
if (norm === ".." || norm.startsWith("../") || norm.startsWith("..\\")) return false;
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
var REV_META = /[\x00-\x20\x7f~^:?*[\\]/;
|
|
292
|
+
function isSafeRev(input) {
|
|
293
|
+
if (input === "" || input.startsWith("-")) return false;
|
|
294
|
+
if (REV_META.test(input)) return false;
|
|
295
|
+
if (input.includes("..") || input.includes("@{")) return false;
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
function isSafeBranchName(name) {
|
|
299
|
+
return isSafeRev(name) && !name.startsWith("/");
|
|
300
|
+
}
|
|
301
|
+
|
|
264
302
|
// src/host/core.ts
|
|
265
303
|
var DEFAULT_CONFIG = {
|
|
266
304
|
timeoutMs: 8e3,
|
|
@@ -271,7 +309,7 @@ var DEFAULT_CONFIG = {
|
|
|
271
309
|
};
|
|
272
310
|
function normalizeConfig(raw) {
|
|
273
311
|
const c = raw ?? {};
|
|
274
|
-
const num = (v, d) => typeof v === "number" && Number.isFinite(v) && v >=
|
|
312
|
+
const num = (v, d) => typeof v === "number" && Number.isFinite(v) && v >= 1 ? Math.floor(v) : d;
|
|
275
313
|
return {
|
|
276
314
|
timeoutMs: num(c.timeoutMs, DEFAULT_CONFIG.timeoutMs),
|
|
277
315
|
maxBytes: num(c.maxBytes, DEFAULT_CONFIG.maxBytes),
|
|
@@ -284,6 +322,7 @@ function readBool(value, fallback) {
|
|
|
284
322
|
const raw = value !== null && typeof value === "object" && "get" in value && typeof value.get === "function" ? value.get() : value;
|
|
285
323
|
return typeof raw === "boolean" ? raw : fallback;
|
|
286
324
|
}
|
|
325
|
+
var NEG_CACHE_MS = 15e3;
|
|
287
326
|
async function resolveWorkspace(deps, sessionId) {
|
|
288
327
|
let cwd = deps.sessions.liveCwd(sessionId);
|
|
289
328
|
if (cwd === void 0 || cwd === "") {
|
|
@@ -295,16 +334,26 @@ async function resolveWorkspace(deps, sessionId) {
|
|
|
295
334
|
}
|
|
296
335
|
const cached = deps.rootCache?.get(cwd);
|
|
297
336
|
if (cached !== void 0) return { ok: true, root: cached };
|
|
337
|
+
const negAt = deps.rootNegCache?.get(cwd);
|
|
338
|
+
if (negAt !== void 0) {
|
|
339
|
+
if (Date.now() < negAt) return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
|
|
340
|
+
deps.rootNegCache?.delete(cwd);
|
|
341
|
+
}
|
|
298
342
|
const top = await runCommand(deps.run, ["git", "rev-parse", "--show-toplevel"], cwd, "toplevel", deps.signal);
|
|
299
343
|
if ("failure" in top) {
|
|
300
344
|
return { ok: false, failure: { ok: false, error: mapRunFailure(top.failure) } };
|
|
301
345
|
}
|
|
346
|
+
if (top.run.cancelled) return { ok: false, failure: { ok: false, error: { code: "cancelled" } } };
|
|
302
347
|
if (top.run.timedOut) return { ok: false, failure: { ok: false, error: { code: "timeout" } } };
|
|
303
348
|
if (top.run.exitCode !== 0) {
|
|
349
|
+
deps.rootNegCache?.set(cwd, Date.now() + NEG_CACHE_MS);
|
|
304
350
|
return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
|
|
305
351
|
}
|
|
306
352
|
const raw = top.run.stdout.trim();
|
|
307
|
-
if (raw === "")
|
|
353
|
+
if (raw === "") {
|
|
354
|
+
deps.rootNegCache?.set(cwd, Date.now() + NEG_CACHE_MS);
|
|
355
|
+
return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
|
|
356
|
+
}
|
|
308
357
|
let root = raw;
|
|
309
358
|
try {
|
|
310
359
|
root = await deps.fs.realpath(raw);
|
|
@@ -326,6 +375,11 @@ function mapRunFailure(failure) {
|
|
|
326
375
|
const message = failure instanceof Error ? failure.message : String(failure);
|
|
327
376
|
return { code: "git-unavailable", detail: message };
|
|
328
377
|
}
|
|
378
|
+
function mapWorkspaceFailure(failure) {
|
|
379
|
+
const error = failure.error;
|
|
380
|
+
const message = "detail" in error ? error.detail : void 0;
|
|
381
|
+
return { code: error.code, ...message !== void 0 ? { message } : {} };
|
|
382
|
+
}
|
|
329
383
|
async function snapshotForSession(deps, config, sessionId) {
|
|
330
384
|
const workspace = await resolveWorkspace(deps, sessionId);
|
|
331
385
|
if (!workspace.ok) {
|
|
@@ -336,26 +390,30 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
336
390
|
return failure;
|
|
337
391
|
}
|
|
338
392
|
const root = workspace.root;
|
|
339
|
-
const [branchRes, headRes, statusRes, aheadBehindRes, lastCommitRes] = await Promise.all([
|
|
393
|
+
const [branchRes, headRes, statusRes, aheadBehindRes, lastCommitRes, worktreeNumRes, stagedNumRes] = await Promise.all([
|
|
340
394
|
runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "HEAD"], root, "branch", deps.signal),
|
|
341
395
|
runCommand(deps.run, ["git", "rev-parse", "--short", "HEAD"], root, "head", deps.signal),
|
|
342
396
|
runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z"], root, "status", deps.signal),
|
|
343
397
|
runCommand(deps.run, ["git", "rev-list", "--count", "--left-right", "@{upstream}...HEAD"], root, "aheadBehind", deps.signal),
|
|
344
|
-
runCommand(deps.run, ["git", "log", "-1", "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI"], root, "lastCommit", deps.signal)
|
|
398
|
+
runCommand(deps.run, ["git", "log", "-1", "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI"], root, "lastCommit", deps.signal),
|
|
399
|
+
runCommand(deps.run, ["git", "diff", "--numstat"], root, "numstat-worktree", deps.signal),
|
|
400
|
+
runCommand(deps.run, ["git", "diff", "--numstat", "--cached"], root, "numstat-staged", deps.signal)
|
|
345
401
|
]);
|
|
346
402
|
const branch = "run" in branchRes && branchRes.run.exitCode === 0 ? branchRes.run.stdout.trim() || null : null;
|
|
347
403
|
const unborn = "run" in headRes && headRes.run.exitCode !== 0;
|
|
348
404
|
const head = unborn ? null : "run" in headRes ? headRes.run.stdout.trim() || null : null;
|
|
349
405
|
let allChanges = [];
|
|
406
|
+
let statusLossy = false;
|
|
350
407
|
if ("run" in statusRes && !statusRes.run.timedOut && statusRes.run.exitCode === 0) {
|
|
351
408
|
allChanges = parseStatus(statusRes.run.stdout);
|
|
409
|
+
statusLossy = statusRes.run.stdoutLossy;
|
|
352
410
|
}
|
|
353
|
-
const truncated = allChanges.length > config.maxChanges;
|
|
354
|
-
const changes =
|
|
411
|
+
const truncated = allChanges.length > config.maxChanges || statusLossy;
|
|
412
|
+
const changes = allChanges.length > config.maxChanges ? allChanges.slice(0, config.maxChanges) : allChanges;
|
|
355
413
|
let staged = 0;
|
|
356
414
|
let modified = 0;
|
|
357
415
|
let untracked = 0;
|
|
358
|
-
for (const c of
|
|
416
|
+
for (const c of allChanges) {
|
|
359
417
|
if (c.status === "untracked") untracked++;
|
|
360
418
|
else if (c.staged) staged++;
|
|
361
419
|
else modified++;
|
|
@@ -380,12 +438,35 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
380
438
|
};
|
|
381
439
|
}
|
|
382
440
|
}
|
|
441
|
+
const wt = "run" in worktreeNumRes && worktreeNumRes.run.exitCode === 0 ? sumNumstat(worktreeNumRes.run.stdout) : { insertions: 0, deletions: 0 };
|
|
442
|
+
const stg = "run" in stagedNumRes && stagedNumRes.run.exitCode === 0 ? sumNumstat(stagedNumRes.run.stdout) : { insertions: 0, deletions: 0 };
|
|
443
|
+
let untrackedInsertions = 0;
|
|
444
|
+
const untrackedPaths = allChanges.filter((c) => c.status === "untracked" && !c.isDirectory).map((c) => c.path);
|
|
445
|
+
const safeUntracked = untrackedPaths.filter(isSafePath).slice(0, 200);
|
|
446
|
+
if (safeUntracked.length > 0) {
|
|
447
|
+
const perFile = await Promise.all(
|
|
448
|
+
safeUntracked.map((p) => runCommand(deps.run, ["git", "diff", "--numstat", "--no-index", "--", "/dev/null", p], root, "numstat-untracked", deps.signal))
|
|
449
|
+
);
|
|
450
|
+
for (const r of perFile) if ("run" in r) untrackedInsertions += sumNumstat(r.run.stdout).insertions;
|
|
451
|
+
}
|
|
452
|
+
const lastChangeAt = await maxChangeMtime(deps, root, allChanges);
|
|
453
|
+
const distinct = new Set(allChanges.map((c) => c.path));
|
|
454
|
+
const stats = {
|
|
455
|
+
fileCount: distinct.size,
|
|
456
|
+
staged,
|
|
457
|
+
modified,
|
|
458
|
+
untracked,
|
|
459
|
+
insertions: wt.insertions + stg.insertions + untrackedInsertions,
|
|
460
|
+
deletions: wt.deletions + stg.deletions,
|
|
461
|
+
lastChangeAt,
|
|
462
|
+
headCommittedAt: lastCommit?.dateIso ?? null
|
|
463
|
+
};
|
|
383
464
|
const snapshot = {
|
|
384
465
|
root,
|
|
385
466
|
branch,
|
|
386
467
|
head,
|
|
387
468
|
unborn,
|
|
388
|
-
dirty:
|
|
469
|
+
dirty: allChanges.length > 0,
|
|
389
470
|
staged,
|
|
390
471
|
modified,
|
|
391
472
|
untracked,
|
|
@@ -393,6 +474,7 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
393
474
|
behind,
|
|
394
475
|
lastCommit,
|
|
395
476
|
changes,
|
|
477
|
+
stats,
|
|
396
478
|
truncated,
|
|
397
479
|
refreshIntervalMs: config.refreshIntervalMs,
|
|
398
480
|
showInputPill: config.showInputPill,
|
|
@@ -417,13 +499,7 @@ async function maxChangeMtime(deps, root, changes, cap = 200) {
|
|
|
417
499
|
}
|
|
418
500
|
|
|
419
501
|
// src/host/actions.ts
|
|
420
|
-
import {
|
|
421
|
-
function isSafePath(path) {
|
|
422
|
-
if (path === "" || isAbsolute(path)) return false;
|
|
423
|
-
const norm = normalize(path);
|
|
424
|
-
if (norm === ".." || norm.startsWith("../") || norm.startsWith("..\\")) return false;
|
|
425
|
-
return true;
|
|
426
|
-
}
|
|
502
|
+
import { join as join2, sep } from "node:path";
|
|
427
503
|
function withPaths(prefixes, paths) {
|
|
428
504
|
if (paths.length === 0) return { error: "invalid-path", message: "no paths given" };
|
|
429
505
|
for (const path of paths) {
|
|
@@ -445,55 +521,66 @@ function planAction(action, unborn) {
|
|
|
445
521
|
return withPaths([["git", "restore", "--"]], action.paths);
|
|
446
522
|
case "commit": {
|
|
447
523
|
const message = action.message.trim();
|
|
448
|
-
const
|
|
449
|
-
if (message === "" &&
|
|
450
|
-
const
|
|
524
|
+
const amend = action.amend === true;
|
|
525
|
+
if (message === "" && !amend) return { error: "empty-message" };
|
|
526
|
+
const amendFlag = amend ? ["--amend"] : [];
|
|
527
|
+
const msgArgs = message === "" ? ["--no-edit"] : ["-m", message];
|
|
451
528
|
if (action.paths === void 0 || action.paths.length === 0) {
|
|
452
529
|
return { argv: [["git", "commit", ...amendFlag, ...msgArgs]] };
|
|
453
530
|
}
|
|
454
531
|
const staged = withPaths([["git", "add", "--"]], action.paths);
|
|
455
532
|
if ("error" in staged) return staged;
|
|
456
533
|
const commitCmd = ["git", "commit", ...amendFlag, ...msgArgs, "--", ...action.paths];
|
|
457
|
-
for (const path of action.paths) {
|
|
458
|
-
if (!isSafePath(path)) return { error: "invalid-path", message: `unsafe path: ${path}` };
|
|
459
|
-
}
|
|
460
534
|
return { argv: [...staged.argv, commitCmd] };
|
|
461
535
|
}
|
|
462
536
|
case "branch-checkout":
|
|
463
|
-
return {
|
|
537
|
+
if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe branch name: ${action.name}` };
|
|
538
|
+
return { argv: [["git", "checkout", "--end-of-options", action.name]] };
|
|
464
539
|
case "fetch":
|
|
465
540
|
return { argv: [["git", "fetch", "--all", "--prune"]] };
|
|
466
541
|
}
|
|
467
542
|
}
|
|
468
543
|
async function runAction(deps, config, request) {
|
|
469
544
|
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
470
|
-
if (!workspace.ok) {
|
|
471
|
-
const error = workspace.failure.error;
|
|
472
|
-
return { ok: false, error: { code: error.code, message: "detail" in error ? error.detail : void 0 } };
|
|
473
|
-
}
|
|
545
|
+
if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
|
|
474
546
|
const root = workspace.root;
|
|
475
547
|
const headProbe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "head-probe", deps.signal);
|
|
476
548
|
const unborn = !("run" in headProbe) || headProbe.run.exitCode !== 0;
|
|
549
|
+
if (request.action.kind === "discard") {
|
|
550
|
+
const removed = await discardUntracked(deps, config, root, request.sessionId, request.action.paths);
|
|
551
|
+
if (removed !== null) {
|
|
552
|
+
if (!removed.ok) return removed.result;
|
|
553
|
+
if (removed.remainingTracked.length === 0) {
|
|
554
|
+
const snapshot2 = await snapshotForSession(deps, config, request.sessionId);
|
|
555
|
+
if (!snapshot2.ok) return { ok: false, error: { code: "git-error", message: "snapshot after action failed" } };
|
|
556
|
+
return { ok: true, snapshot: snapshot2.value, output: "" };
|
|
557
|
+
}
|
|
558
|
+
request = { ...request, action: { kind: "discard", paths: removed.remainingTracked } };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
477
561
|
const plan = planAction(request.action, unborn);
|
|
478
562
|
if ("error" in plan) return { ok: false, error: { code: plan.error, ...plan.message ? { message: plan.message } : {} } };
|
|
479
563
|
let lastOutput = "";
|
|
480
|
-
for (
|
|
564
|
+
for (let step = 0; step < plan.argv.length; step += 1) {
|
|
565
|
+
const argv = plan.argv[step];
|
|
481
566
|
const outcome = await runCommand(deps.run, argv, root, "action", deps.signal);
|
|
567
|
+
const where = plan.argv.length > 1 ? ` (step ${step + 1}/${plan.argv.length}: ${argv.join(" ")})` : "";
|
|
482
568
|
if ("failure" in outcome) {
|
|
483
569
|
const message = outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure);
|
|
484
|
-
return { ok: false, error: { code: "git-unavailable", message } };
|
|
570
|
+
return { ok: false, error: { code: "git-unavailable", message: message + where } };
|
|
485
571
|
}
|
|
572
|
+
if (outcome.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
486
573
|
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
487
574
|
lastOutput = outcome.run.stdout || outcome.run.stderr;
|
|
488
575
|
if (outcome.run.exitCode !== 0) {
|
|
489
576
|
const stderr = outcome.run.stderr;
|
|
490
577
|
if (/nothing to commit|no changes added/i.test(stderr + lastOutput)) {
|
|
491
|
-
return { ok: false, error: { code: "git-error", message: stderr.trim() || "nothing to commit" } };
|
|
578
|
+
return { ok: false, error: { code: "git-error", message: (stderr.trim() || "nothing to commit") + where } };
|
|
492
579
|
}
|
|
493
580
|
if (/would be overwritten by checkout|local changes/i.test(stderr)) {
|
|
494
|
-
return { ok: false, error: { code: "local-changes-block", message: stderr.trim() } };
|
|
581
|
+
return { ok: false, error: { code: "local-changes-block", message: stderr.trim() + where } };
|
|
495
582
|
}
|
|
496
|
-
return { ok: false, error: { code: "git-error", message: stderr.trim() || `git exited ${outcome.run.exitCode}` } };
|
|
583
|
+
return { ok: false, error: { code: "git-error", message: (stderr.trim() || `git exited ${outcome.run.exitCode}`) + where } };
|
|
497
584
|
}
|
|
498
585
|
}
|
|
499
586
|
const snapshot = await snapshotForSession(deps, config, request.sessionId);
|
|
@@ -502,9 +589,39 @@ async function runAction(deps, config, request) {
|
|
|
502
589
|
}
|
|
503
590
|
return { ok: true, snapshot: snapshot.value, output: lastOutput.trim() };
|
|
504
591
|
}
|
|
592
|
+
async function discardUntracked(deps, config, root, sessionId, paths) {
|
|
593
|
+
const snap = await snapshotForSession(deps, config, sessionId);
|
|
594
|
+
if (!snap.ok) return null;
|
|
595
|
+
const untrackedSet = new Set(snap.value.changes.filter((c) => c.status === "untracked").map((c) => c.path));
|
|
596
|
+
const untracked = paths.filter((p) => untrackedSet.has(p));
|
|
597
|
+
if (untracked.length === 0) return null;
|
|
598
|
+
const tracked = paths.filter((p) => !untrackedSet.has(p));
|
|
599
|
+
let rootReal;
|
|
600
|
+
try {
|
|
601
|
+
rootReal = await deps.fs.realpath(root);
|
|
602
|
+
} catch {
|
|
603
|
+
return { ok: false, result: { ok: false, error: { code: "git-error", message: "repository root unavailable" } } };
|
|
604
|
+
}
|
|
605
|
+
for (const path of untracked) {
|
|
606
|
+
if (!isSafePath(path)) return { ok: false, result: { ok: false, error: { code: "invalid-path", message: `unsafe path: ${path}` } } };
|
|
607
|
+
const target = join2(root, path);
|
|
608
|
+
let targetReal;
|
|
609
|
+
try {
|
|
610
|
+
targetReal = await deps.fs.realpath(target);
|
|
611
|
+
} catch {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (targetReal !== rootReal && !targetReal.startsWith(rootReal + sep)) {
|
|
615
|
+
return { ok: false, result: { ok: false, error: { code: "invalid-path", message: `path escapes repository: ${path}` } } };
|
|
616
|
+
}
|
|
617
|
+
await deps.fs.remove(target).catch(() => {
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
return { ok: true, remainingTracked: tracked };
|
|
621
|
+
}
|
|
505
622
|
|
|
506
623
|
// src/host/queries.ts
|
|
507
|
-
import { join as
|
|
624
|
+
import { join as join3, sep as sep2 } from "node:path";
|
|
508
625
|
|
|
509
626
|
// src/host/types.ts
|
|
510
627
|
var IMAGE_MIME = {
|
|
@@ -533,16 +650,7 @@ function isHexLike(text) {
|
|
|
533
650
|
}
|
|
534
651
|
async function runQuery(deps, config, request) {
|
|
535
652
|
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
536
|
-
if (!workspace.ok) {
|
|
537
|
-
const error = workspace.failure.error;
|
|
538
|
-
return {
|
|
539
|
-
ok: false,
|
|
540
|
-
error: {
|
|
541
|
-
code: error.code === "not-a-git-repo" || error.code === "cwd-unavailable" || error.code === "session-not-found" || error.code === "timeout" || error.code === "git-unavailable" ? error.code : "git-error",
|
|
542
|
-
..."detail" in error ? { message: error.detail } : {}
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
}
|
|
653
|
+
if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
|
|
546
654
|
const root = workspace.root;
|
|
547
655
|
const q = request.query;
|
|
548
656
|
try {
|
|
@@ -551,6 +659,8 @@ async function runQuery(deps, config, request) {
|
|
|
551
659
|
return await queryHistory(deps, root, q);
|
|
552
660
|
case "diff":
|
|
553
661
|
return await queryDiff(deps, root, q);
|
|
662
|
+
case "file-lines":
|
|
663
|
+
return await queryFileLines(deps, root, q);
|
|
554
664
|
case "image-diff":
|
|
555
665
|
return await queryImageDiff(deps, config, root, q);
|
|
556
666
|
case "show":
|
|
@@ -564,7 +674,7 @@ async function runQuery(deps, config, request) {
|
|
|
564
674
|
case "last-commit-message":
|
|
565
675
|
return await queryLastCommitMessage(deps, root);
|
|
566
676
|
case "worktree-stats":
|
|
567
|
-
return await queryWorktreeStats(deps, config,
|
|
677
|
+
return await queryWorktreeStats(deps, config, request.sessionId);
|
|
568
678
|
}
|
|
569
679
|
} catch (error) {
|
|
570
680
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -572,18 +682,13 @@ async function runQuery(deps, config, request) {
|
|
|
572
682
|
}
|
|
573
683
|
}
|
|
574
684
|
async function queryHistory(deps, root, q) {
|
|
575
|
-
const
|
|
685
|
+
const limit = Number.isFinite(q.limit) ? Math.min(500, Math.max(1, Math.trunc(q.limit))) : 100;
|
|
686
|
+
const skip = Number.isFinite(q.skip) ? Math.max(0, Math.trunc(q.skip)) : 0;
|
|
687
|
+
const args = ["git", "log", GRAPH_FORMAT, `--max-count=${limit}`, `--skip=${skip}`];
|
|
576
688
|
const search = q.search?.trim() ?? "";
|
|
577
689
|
const hexJump = search !== "" && isHexLike(search);
|
|
578
690
|
const countArgs = ["git", "rev-list", "--count"];
|
|
579
691
|
if (!hexJump) {
|
|
580
|
-
if (q.ref !== void 0 && q.ref !== "") {
|
|
581
|
-
args.push(q.ref);
|
|
582
|
-
countArgs.push(q.ref);
|
|
583
|
-
} else {
|
|
584
|
-
args.push("--all");
|
|
585
|
-
countArgs.push("--all");
|
|
586
|
-
}
|
|
587
692
|
if (search !== "") {
|
|
588
693
|
args.push("-i", "-E", `--grep=${search}`);
|
|
589
694
|
countArgs.push("-i", "-E", `--grep=${search}`);
|
|
@@ -596,22 +701,35 @@ async function queryHistory(deps, root, q) {
|
|
|
596
701
|
args.push(`--since=${q.since}`);
|
|
597
702
|
countArgs.push(`--since=${q.since}`);
|
|
598
703
|
}
|
|
704
|
+
if (q.ref !== void 0 && q.ref !== "") {
|
|
705
|
+
if (!isSafeRev(q.ref)) return { ok: false, error: { code: "invalid-name", message: `unsafe ref: ${q.ref}` } };
|
|
706
|
+
args.push("--end-of-options", q.ref);
|
|
707
|
+
countArgs.push("--end-of-options", q.ref);
|
|
708
|
+
} else {
|
|
709
|
+
args.push("--all");
|
|
710
|
+
countArgs.push("--all");
|
|
711
|
+
}
|
|
599
712
|
} else {
|
|
600
|
-
args.push(search);
|
|
713
|
+
args.push("--end-of-options", search);
|
|
601
714
|
}
|
|
602
715
|
const [res, countRes] = await Promise.all([
|
|
603
716
|
runCommand(deps.run, args, root, "history", deps.signal),
|
|
604
717
|
hexJump ? Promise.resolve(null) : runCommand(deps.run, countArgs, root, "history-count", deps.signal)
|
|
605
718
|
]);
|
|
606
719
|
if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
|
|
720
|
+
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
607
721
|
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
608
722
|
if (res.run.exitCode !== 0) {
|
|
609
|
-
|
|
723
|
+
const stderr = res.run.stderr.trim();
|
|
724
|
+
if (hexJump || /unknown revision|bad revision|does not have any commits|ambiguous argument/i.test(stderr)) {
|
|
725
|
+
return { ok: true, value: { kind: "history", commits: [], total: 0 } };
|
|
726
|
+
}
|
|
727
|
+
return { ok: false, error: { code: "git-error", message: stderr || `git exited ${res.run.exitCode}` } };
|
|
610
728
|
}
|
|
611
729
|
const commits = parseGraphLog(res.run.stdout);
|
|
612
730
|
let total = -1;
|
|
613
731
|
if (hexJump) {
|
|
614
|
-
total =
|
|
732
|
+
total = -1;
|
|
615
733
|
} else if (countRes !== null && "run" in countRes && countRes.run.exitCode === 0) {
|
|
616
734
|
const n = Number(countRes.run.stdout.trim());
|
|
617
735
|
if (Number.isFinite(n)) total = n;
|
|
@@ -626,22 +744,75 @@ async function queryDiff(deps, root, q) {
|
|
|
626
744
|
if (q.base === "staged") {
|
|
627
745
|
args = ["git", "diff", unified, "--cached", "--", q.path];
|
|
628
746
|
} else if (q.base === "commit") {
|
|
629
|
-
|
|
747
|
+
if (!isSafeRev(q.commit)) return { ok: false, error: { code: "invalid-name", message: `unsafe commit: ${q.commit}` } };
|
|
748
|
+
args = ["git", "show", unified, "--end-of-options", q.commit, "--", q.path];
|
|
630
749
|
} else {
|
|
631
750
|
args = ["git", "diff", unified, "--", q.path];
|
|
632
751
|
}
|
|
633
752
|
const res = await runCommand(deps.run, args, root, "diff", deps.signal);
|
|
634
753
|
if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
|
|
754
|
+
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
635
755
|
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
636
756
|
let text = res.run.stdout;
|
|
637
757
|
if (q.base === "worktree" && text.trim() === "") {
|
|
638
|
-
const
|
|
639
|
-
|
|
758
|
+
const tracked = await runCommand(deps.run, ["git", "ls-files", "--error-unmatch", "--", q.path], root, "diff-tracked-probe", deps.signal);
|
|
759
|
+
const isUntracked = !("run" in tracked) || tracked.run.exitCode !== 0;
|
|
760
|
+
if (isUntracked) {
|
|
761
|
+
const noIndex = await runCommand(deps.run, ["git", "diff", unified, "--no-index", "--", "/dev/null", q.path], root, "diff-untracked", deps.signal);
|
|
762
|
+
if ("run" in noIndex) text = noIndex.run.stdout;
|
|
763
|
+
}
|
|
640
764
|
}
|
|
641
765
|
return { ok: true, value: { kind: "diff", path: q.path, text } };
|
|
642
766
|
}
|
|
767
|
+
async function queryFileLines(deps, root, q) {
|
|
768
|
+
if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
|
|
769
|
+
const start = Number.isFinite(q.start) ? Math.max(1, Math.floor(q.start)) : 1;
|
|
770
|
+
const end = Number.isFinite(q.end) ? Math.max(start, Math.floor(q.end)) : start;
|
|
771
|
+
let args;
|
|
772
|
+
if (q.base === "commit") {
|
|
773
|
+
if (!isSafeRev(q.commit)) return { ok: false, error: { code: "invalid-name", message: `unsafe commit: ${q.commit}` } };
|
|
774
|
+
args = ["git", "show", "--end-of-options", `${q.commit}:${q.path}`];
|
|
775
|
+
} else {
|
|
776
|
+
args = ["git", "show", `--end-of-options`, `:${q.path}`];
|
|
777
|
+
if (q.base === "worktree") args = ["git", "cat-file", "-p", `:${q.path}`];
|
|
778
|
+
}
|
|
779
|
+
const res = await runCommand(deps.run, args, root, "file-lines", deps.signal);
|
|
780
|
+
if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
|
|
781
|
+
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
782
|
+
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
783
|
+
if (res.run.exitCode !== 0) {
|
|
784
|
+
if (q.base === "worktree") {
|
|
785
|
+
const wt = await readWorktreeText(deps, root, q.path);
|
|
786
|
+
if (wt !== null) return sliceLines(q.path, wt, start, end);
|
|
787
|
+
}
|
|
788
|
+
return { ok: false, error: { code: "git-error", message: res.run.stderr.trim() || "no such blob" } };
|
|
789
|
+
}
|
|
790
|
+
return sliceLines(q.path, res.run.stdout, start, end);
|
|
791
|
+
}
|
|
792
|
+
async function readWorktreeText(deps, root, path) {
|
|
793
|
+
try {
|
|
794
|
+
const file = join3(root, path);
|
|
795
|
+
const real = await deps.fs.realpath(file);
|
|
796
|
+
const rootReal = await deps.fs.realpath(root);
|
|
797
|
+
if (real !== rootReal && !real.startsWith(rootReal + sep2)) return null;
|
|
798
|
+
const buf = await deps.fs.readFile(file);
|
|
799
|
+
return buf.toString("utf8");
|
|
800
|
+
} catch {
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function sliceLines(path, content, start, end) {
|
|
805
|
+
const all = content.split("\n");
|
|
806
|
+
if (all.length > 0 && all[all.length - 1] === "") all.pop();
|
|
807
|
+
const from = Math.min(start, all.length + 1);
|
|
808
|
+
const to = Math.min(end, all.length);
|
|
809
|
+
const lines = from <= to ? all.slice(from - 1, to) : [];
|
|
810
|
+
const eof = to >= all.length;
|
|
811
|
+
return { ok: true, value: { kind: "file-lines", path, start: from, lines, eof } };
|
|
812
|
+
}
|
|
643
813
|
async function queryImageDiff(deps, config, root, q) {
|
|
644
814
|
if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
|
|
815
|
+
if (q.base === "commit" && !isSafeRev(q.commit)) return { ok: false, error: { code: "invalid-name", message: `unsafe commit: ${q.commit}` } };
|
|
645
816
|
const mime = imageMimeFor(q.path);
|
|
646
817
|
if (mime === null) return { ok: true, value: { kind: "image-diff", path: q.path, mime } };
|
|
647
818
|
const oldSpec = q.base === "staged" ? `HEAD:${q.path}` : q.base === "commit" ? `${q.commit}^1:${q.path}` : `:${q.path}`;
|
|
@@ -694,7 +865,7 @@ async function blobSide(deps, gitDir, oid, cap) {
|
|
|
694
865
|
if (!("run" in nameRes) || nameRes.run.exitCode !== 0) return void 0;
|
|
695
866
|
const name = nameRes.run.stdout.trim();
|
|
696
867
|
if (!/^[A-Za-z0-9._-]+$/.test(name)) return void 0;
|
|
697
|
-
const file =
|
|
868
|
+
const file = join3(gitDir, name);
|
|
698
869
|
try {
|
|
699
870
|
const buf = await deps.fs.readFile(file);
|
|
700
871
|
if (buf.length > cap) return { tooLarge: true };
|
|
@@ -708,7 +879,10 @@ async function blobSide(deps, gitDir, oid, cap) {
|
|
|
708
879
|
}
|
|
709
880
|
async function worktreeSide(deps, root, path, cap) {
|
|
710
881
|
try {
|
|
711
|
-
const file =
|
|
882
|
+
const file = join3(root, path);
|
|
883
|
+
const real = await deps.fs.realpath(file);
|
|
884
|
+
const rootReal = await deps.fs.realpath(root);
|
|
885
|
+
if (real !== rootReal && !real.startsWith(rootReal + sep2)) return void 0;
|
|
712
886
|
const info = await deps.fs.stat(file);
|
|
713
887
|
if (info.size > cap) return { tooLarge: true };
|
|
714
888
|
const buf = await deps.fs.readFile(file);
|
|
@@ -719,12 +893,14 @@ async function worktreeSide(deps, root, path, cap) {
|
|
|
719
893
|
}
|
|
720
894
|
}
|
|
721
895
|
async function queryShow(deps, root, ref) {
|
|
896
|
+
if (!isSafeRev(ref)) return { ok: false, error: { code: "invalid-name", message: `unsafe ref: ${ref}` } };
|
|
722
897
|
const metaFormat = "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%b";
|
|
723
898
|
const [metaRes, statRes] = await Promise.all([
|
|
724
|
-
runCommand(deps.run, ["git", "show", "-s", metaFormat, ref], root, "show-meta", deps.signal),
|
|
725
|
-
runCommand(deps.run, ["git", "show", "--name-status", "-z", "--format=", ref], root, "show-stat", deps.signal)
|
|
899
|
+
runCommand(deps.run, ["git", "show", "-s", metaFormat, "--end-of-options", ref], root, "show-meta", deps.signal),
|
|
900
|
+
runCommand(deps.run, ["git", "show", "--name-status", "-z", "--format=", "--end-of-options", ref], root, "show-stat", deps.signal)
|
|
726
901
|
]);
|
|
727
902
|
if (!("run" in metaRes)) return { ok: false, error: { code: "git-unavailable" } };
|
|
903
|
+
if (metaRes.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
728
904
|
if (metaRes.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
729
905
|
if (metaRes.run.exitCode !== 0) {
|
|
730
906
|
return { ok: false, error: { code: "git-error", message: metaRes.run.stderr.trim() || "unknown ref" } };
|
|
@@ -740,7 +916,7 @@ async function queryShow(deps, root, ref) {
|
|
|
740
916
|
author: parts[3] ?? "",
|
|
741
917
|
dateIso: parts[4] ?? ""
|
|
742
918
|
};
|
|
743
|
-
body = (
|
|
919
|
+
body = parts.slice(5).join("").trim();
|
|
744
920
|
}
|
|
745
921
|
const stats = "run" in statRes && statRes.run.exitCode === 0 ? parseNameStatus(statRes.run.stdout) : [];
|
|
746
922
|
return { ok: true, value: { kind: "show", ref, commit, body, stats } };
|
|
@@ -765,11 +941,7 @@ async function queryBranches(deps, root) {
|
|
|
765
941
|
}
|
|
766
942
|
async function queryTags(deps, root) {
|
|
767
943
|
const res = await runCommand(deps.run, ["git", "for-each-ref", "--sort=-creatordate", "--format=%(refname:short)%00%(objectname:short)", "refs/tags"], root, "tags", deps.signal);
|
|
768
|
-
const tags = "run" in res && res.run.exitCode === 0 ? res.run.stdout
|
|
769
|
-
if (line.trim() === "") return [];
|
|
770
|
-
const [name, shortHash = ""] = line.split("\0");
|
|
771
|
-
return name ? [{ name, shortHash: shortHash === "" ? null : shortHash }] : [];
|
|
772
|
-
}) : [];
|
|
944
|
+
const tags = "run" in res && res.run.exitCode === 0 ? parseTags(res.run.stdout) : [];
|
|
773
945
|
return { ok: true, value: { kind: "tags", tags } };
|
|
774
946
|
}
|
|
775
947
|
async function queryAuthors(deps, root) {
|
|
@@ -782,48 +954,18 @@ async function queryLastCommitMessage(deps, root) {
|
|
|
782
954
|
const message = "run" in res && res.run.exitCode === 0 ? res.run.stdout.replace(/\n+$/, "") : "";
|
|
783
955
|
return { ok: true, value: { kind: "last-commit-message", message } };
|
|
784
956
|
}
|
|
785
|
-
async function queryWorktreeStats(deps, config,
|
|
957
|
+
async function queryWorktreeStats(deps, config, sessionId) {
|
|
786
958
|
const snapshot = await snapshotForSession(deps, config, sessionId);
|
|
787
959
|
if (!snapshot.ok) return { ok: false, error: { code: "git-error", message: "snapshot failed" } };
|
|
788
|
-
|
|
789
|
-
const distinct = new Set(snap.changes.map((c) => c.path));
|
|
790
|
-
const [worktreeNum, stagedNum, headTime] = await Promise.all([
|
|
791
|
-
runCommand(deps.run, ["git", "diff", "--numstat"], root, "numstat-worktree", deps.signal),
|
|
792
|
-
runCommand(deps.run, ["git", "diff", "--numstat", "--cached"], root, "numstat-staged", deps.signal),
|
|
793
|
-
runCommand(deps.run, ["git", "log", "-1", "--format=%aI"], root, "head-time", deps.signal)
|
|
794
|
-
]);
|
|
795
|
-
const a = "run" in worktreeNum && worktreeNum.run.exitCode === 0 ? sumNumstat(worktreeNum.run.stdout) : { insertions: 0, deletions: 0 };
|
|
796
|
-
const b = "run" in stagedNum && stagedNum.run.exitCode === 0 ? sumNumstat(stagedNum.run.stdout) : { insertions: 0, deletions: 0 };
|
|
797
|
-
let untrackedInsertions = 0;
|
|
798
|
-
const untrackedPaths = snap.changes.filter((c) => c.status === "untracked" && !c.isDirectory).map((c) => c.path);
|
|
799
|
-
if (untrackedPaths.length > 0) {
|
|
800
|
-
const safe = untrackedPaths.filter(isSafePath).slice(0, 200);
|
|
801
|
-
if (safe.length > 0) {
|
|
802
|
-
const noIndex = await runCommand(deps.run, ["git", "diff", "--numstat", "--no-index", "--", "/dev/null", ...safe], root, "numstat-untracked", deps.signal);
|
|
803
|
-
if ("run" in noIndex) untrackedInsertions = sumNumstat(noIndex.run.stdout).insertions;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
const lastChangeAt = await maxChangeMtime(deps, root, snap.changes);
|
|
807
|
-
const headCommittedAt = "run" in headTime && headTime.run.exitCode === 0 ? headTime.run.stdout.trim() || null : null;
|
|
808
|
-
const stats = {
|
|
809
|
-
fileCount: distinct.size,
|
|
810
|
-
staged: snap.staged,
|
|
811
|
-
modified: snap.modified,
|
|
812
|
-
untracked: snap.untracked,
|
|
813
|
-
insertions: a.insertions + b.insertions + untrackedInsertions,
|
|
814
|
-
deletions: a.deletions + b.deletions,
|
|
815
|
-
lastChangeAt,
|
|
816
|
-
headCommittedAt
|
|
817
|
-
};
|
|
818
|
-
return { ok: true, value: { kind: "worktree-stats", stats } };
|
|
960
|
+
return { ok: true, value: { kind: "worktree-stats", stats: snapshot.value.stats } };
|
|
819
961
|
}
|
|
820
962
|
|
|
821
963
|
// src/host/version.ts
|
|
822
964
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
823
|
-
import { dirname, join as
|
|
965
|
+
import { dirname, join as join4 } from "node:path";
|
|
824
966
|
import { fileURLToPath } from "node:url";
|
|
825
967
|
function manifestPath() {
|
|
826
|
-
return
|
|
968
|
+
return join4(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
827
969
|
}
|
|
828
970
|
function stringField(value) {
|
|
829
971
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
@@ -841,9 +983,10 @@ function compareVersions(a, b) {
|
|
|
841
983
|
const split = (value) => {
|
|
842
984
|
const trimmed = value.trim().replace(/^v/i, "");
|
|
843
985
|
const [core, ...rest] = trimmed.split("-");
|
|
986
|
+
const preRaw = rest.join("-");
|
|
844
987
|
return {
|
|
845
988
|
core: core.split(".").map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part)),
|
|
846
|
-
|
|
989
|
+
pre: preRaw === "" ? [] : preRaw.split(".")
|
|
847
990
|
};
|
|
848
991
|
};
|
|
849
992
|
const left = split(a);
|
|
@@ -855,17 +998,39 @@ function compareVersions(a, b) {
|
|
|
855
998
|
if (l > r) return 1;
|
|
856
999
|
if (l < r) return -1;
|
|
857
1000
|
}
|
|
858
|
-
if (left.
|
|
859
|
-
|
|
1001
|
+
if (left.pre.length === 0 && right.pre.length === 0) return 0;
|
|
1002
|
+
if (left.pre.length === 0) return 1;
|
|
1003
|
+
if (right.pre.length === 0) return -1;
|
|
1004
|
+
const preLen = Math.max(left.pre.length, right.pre.length);
|
|
1005
|
+
for (let i = 0; i < preLen; i += 1) {
|
|
1006
|
+
const lp = left.pre[i];
|
|
1007
|
+
const rp = right.pre[i];
|
|
1008
|
+
if (lp === void 0) return -1;
|
|
1009
|
+
if (rp === void 0) return 1;
|
|
1010
|
+
const ln = Number.parseInt(lp, 10);
|
|
1011
|
+
const rn = Number.parseInt(rp, 10);
|
|
1012
|
+
const lNum = Number.isFinite(ln) && String(ln) === lp;
|
|
1013
|
+
const rNum = Number.isFinite(rn) && String(rn) === rp;
|
|
1014
|
+
if (lNum && rNum) {
|
|
1015
|
+
if (ln !== rn) return ln > rn ? 1 : -1;
|
|
1016
|
+
} else if (lp !== rp) return lp > rp ? 1 : -1;
|
|
1017
|
+
}
|
|
1018
|
+
return 0;
|
|
860
1019
|
}
|
|
1020
|
+
var manifestCache;
|
|
861
1021
|
async function readManifest(path) {
|
|
862
1022
|
const raw = await readFile2(path, "utf8");
|
|
863
1023
|
const parsed = JSON.parse(raw);
|
|
864
1024
|
if (typeof parsed !== "object" || parsed === null) throw new Error("package.json is not an object");
|
|
865
1025
|
return parsed;
|
|
866
1026
|
}
|
|
1027
|
+
async function loadManifest(path) {
|
|
1028
|
+
if (path !== void 0) return readManifest(path);
|
|
1029
|
+
if (manifestCache === void 0) manifestCache = await readManifest(manifestPath());
|
|
1030
|
+
return manifestCache;
|
|
1031
|
+
}
|
|
867
1032
|
async function readVersionInfo(options = {}) {
|
|
868
|
-
const manifest = await
|
|
1033
|
+
const manifest = await loadManifest(options.manifestPath);
|
|
869
1034
|
const current = stringField(manifest.version) ?? "0.0.0";
|
|
870
1035
|
const repo = parseRepository(manifest.repository);
|
|
871
1036
|
const repositoryUrl = repo === void 0 ? void 0 : `https://github.com/${repo.owner}/${repo.repo}`;
|
|
@@ -876,16 +1041,26 @@ async function readVersionInfo(options = {}) {
|
|
|
876
1041
|
checkedRemote: false
|
|
877
1042
|
};
|
|
878
1043
|
}
|
|
1044
|
+
function safeReleaseUrl(url) {
|
|
1045
|
+
if (url === void 0) return void 0;
|
|
1046
|
+
return /^https:\/\/github\.com\//i.test(url) ? url : void 0;
|
|
1047
|
+
}
|
|
1048
|
+
var REMOTE_CACHE_MS = 10 * 60 * 1e3;
|
|
1049
|
+
var remoteCache;
|
|
879
1050
|
async function checkLatestVersion(options = {}) {
|
|
1051
|
+
if (options.manifestPath === void 0 && options.fetchFn === void 0 && remoteCache !== void 0 && Date.now() - remoteCache.at < REMOTE_CACHE_MS) {
|
|
1052
|
+
return remoteCache.info;
|
|
1053
|
+
}
|
|
880
1054
|
const base = await readVersionInfo({ manifestPath: options.manifestPath });
|
|
881
|
-
const repo = parseRepository((await
|
|
1055
|
+
const repo = parseRepository((await loadManifest(options.manifestPath)).repository);
|
|
882
1056
|
if (repo === void 0) {
|
|
883
1057
|
return { ...base, checkedRemote: false, error: "repository is not configured" };
|
|
884
1058
|
}
|
|
885
1059
|
const fetchFn = options.fetchFn ?? fetch;
|
|
886
1060
|
try {
|
|
887
1061
|
const response = await fetchFn(`https://api.github.com/repos/${repo.owner}/${repo.repo}/releases/latest`, {
|
|
888
|
-
headers: { accept: "application/vnd.github+json", "user-agent": "dsh-git-panel" }
|
|
1062
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "dsh-git-panel" },
|
|
1063
|
+
signal: AbortSignal.timeout(5e3)
|
|
889
1064
|
});
|
|
890
1065
|
if (!response.ok) {
|
|
891
1066
|
return { ...base, checkedRemote: true, error: `GitHub responded with ${response.status}` };
|
|
@@ -897,13 +1072,15 @@ async function checkLatestVersion(options = {}) {
|
|
|
897
1072
|
return { ...base, checkedRemote: true, error: "GitHub response did not include a release tag" };
|
|
898
1073
|
}
|
|
899
1074
|
const latest = tag.replace(/^v/i, "");
|
|
900
|
-
|
|
1075
|
+
const info = {
|
|
901
1076
|
...base,
|
|
902
1077
|
checkedRemote: true,
|
|
903
1078
|
latest,
|
|
904
1079
|
updateAvailable: compareVersions(latest, base.current) > 0,
|
|
905
|
-
releaseUrl: htmlUrl ?? `${base.repositoryUrl
|
|
1080
|
+
releaseUrl: safeReleaseUrl(htmlUrl) ?? (base.repositoryUrl !== void 0 ? `${base.repositoryUrl}/releases/latest` : void 0)
|
|
906
1081
|
};
|
|
1082
|
+
if (options.manifestPath === void 0 && options.fetchFn === void 0) remoteCache = { at: Date.now(), info };
|
|
1083
|
+
return info;
|
|
907
1084
|
} catch (error) {
|
|
908
1085
|
return { ...base, checkedRemote: true, error: error instanceof Error ? error.message : "Unable to reach GitHub" };
|
|
909
1086
|
}
|
|
@@ -924,27 +1101,25 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
|
|
|
924
1101
|
}
|
|
925
1102
|
buildDeps(ctx, config) {
|
|
926
1103
|
const rootCache = /* @__PURE__ */ new Map();
|
|
1104
|
+
const rootNegCache = /* @__PURE__ */ new Map();
|
|
927
1105
|
const get = (key) => ctx.get(key);
|
|
1106
|
+
const fs = {
|
|
1107
|
+
realpath,
|
|
1108
|
+
stat: async (p) => stat(p),
|
|
1109
|
+
readFile: readFile3,
|
|
1110
|
+
remove: async (p) => {
|
|
1111
|
+
await rm(p, { force: true });
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
928
1114
|
const subprocess = get("subprocess");
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
throw new Error("subprocess service unavailable");
|
|
933
|
-
} },
|
|
934
|
-
fs: { realpath, stat: async (p) => stat(p), readFile: readFile3, remove: async (p) => {
|
|
935
|
-
await rm(p, { force: true });
|
|
936
|
-
} },
|
|
937
|
-
sessions: { liveCwd: () => void 0, persistedMeta: async () => void 0 },
|
|
938
|
-
rootCache
|
|
939
|
-
};
|
|
940
|
-
}
|
|
1115
|
+
const run = subprocess === void 0 ? { run: async () => {
|
|
1116
|
+
throw new Error("subprocess service unavailable");
|
|
1117
|
+
} } : createGitRunner(subprocess, config.timeoutMs, config.maxBytes);
|
|
941
1118
|
const sessions = get("sessions");
|
|
942
1119
|
const persistence = get("sessionPersistence");
|
|
943
1120
|
return {
|
|
944
|
-
run
|
|
945
|
-
fs
|
|
946
|
-
await rm(p, { force: true });
|
|
947
|
-
} },
|
|
1121
|
+
run,
|
|
1122
|
+
fs,
|
|
948
1123
|
sessions: {
|
|
949
1124
|
liveCwd: (id) => sessions?.get(id)?.header?.cwd,
|
|
950
1125
|
persistedMeta: async (id) => {
|
|
@@ -957,7 +1132,8 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
|
|
|
957
1132
|
}
|
|
958
1133
|
}
|
|
959
1134
|
},
|
|
960
|
-
rootCache
|
|
1135
|
+
rootCache,
|
|
1136
|
+
rootNegCache
|
|
961
1137
|
};
|
|
962
1138
|
}
|
|
963
1139
|
async snapshot(request, signal) {
|
|
@@ -970,7 +1146,11 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
|
|
|
970
1146
|
return runQuery(this.withSignal(signal), this.liveConfig(), request);
|
|
971
1147
|
}
|
|
972
1148
|
async version(request) {
|
|
973
|
-
|
|
1149
|
+
try {
|
|
1150
|
+
return request.check === true ? await checkLatestVersion() : await readVersionInfo();
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
return { current: "0.0.0", updateAvailable: false, checkedRemote: false, error: error instanceof Error ? error.message : "version unavailable" };
|
|
1153
|
+
}
|
|
974
1154
|
}
|
|
975
1155
|
/** Re-read config so a live-edited volatile field (showInputPill) is current. */
|
|
976
1156
|
liveConfig() {
|