@xbzbing/dsh-git-panel 0.1.1 → 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 +17 -16
- package/README_EN.md +17 -16
- package/cordis.patch.yml +1 -0
- package/lib/client.js +105 -44
- package/lib/host/actions.d.ts +2 -3
- package/lib/host/core.d.ts +47 -2
- package/lib/host/git.d.ts +6 -1
- package/lib/host/index.d.ts +7 -4
- package/lib/host/index.js +469 -151
- package/lib/host/parser.d.ts +8 -1
- package/lib/host/types.d.ts +60 -4
- package/lib/host/validate.d.ts +10 -0
- package/lib/host/version.d.ts +6 -4
- package/package.json +4 -7
package/lib/host/index.js
CHANGED
|
@@ -50,7 +50,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
|
|
|
50
50
|
// src/host/index.ts
|
|
51
51
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
52
52
|
import Schema from "@deepseek-ai/schemastery";
|
|
53
|
-
import { readFile as readFile3, realpath, rm, stat } from "node:fs/promises";
|
|
53
|
+
import { readFile as readFile3, readdir, realpath, rm, stat } from "node:fs/promises";
|
|
54
54
|
|
|
55
55
|
// src/host/git.ts
|
|
56
56
|
import { readFile } from "node:fs/promises";
|
|
@@ -66,7 +66,7 @@ function createGitRunner(subprocess, timeoutMs, maxBytes) {
|
|
|
66
66
|
argv,
|
|
67
67
|
cwd: opts.cwd,
|
|
68
68
|
stdio: {
|
|
69
|
-
stdin: "ignore",
|
|
69
|
+
stdin: opts.stdinData === void 0 ? "ignore" : { data: opts.stdinData },
|
|
70
70
|
stdout: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } },
|
|
71
71
|
stderr: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } }
|
|
72
72
|
},
|
|
@@ -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,29 +280,71 @@ 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,
|
|
267
305
|
maxBytes: 4 * 1024 * 1024,
|
|
268
306
|
maxChanges: 1e3,
|
|
269
307
|
refreshIntervalMs: 3e4,
|
|
270
|
-
showInputPill: true
|
|
308
|
+
showInputPill: true,
|
|
309
|
+
defaultDiffView: "unified"
|
|
271
310
|
};
|
|
272
311
|
function normalizeConfig(raw) {
|
|
273
312
|
const c = raw ?? {};
|
|
274
|
-
const num = (v, d) => typeof v === "number" && Number.isFinite(v) && v >=
|
|
313
|
+
const num = (v, d) => typeof v === "number" && Number.isFinite(v) && v >= 1 ? Math.floor(v) : d;
|
|
275
314
|
return {
|
|
276
315
|
timeoutMs: num(c.timeoutMs, DEFAULT_CONFIG.timeoutMs),
|
|
277
316
|
maxBytes: num(c.maxBytes, DEFAULT_CONFIG.maxBytes),
|
|
278
317
|
maxChanges: num(c.maxChanges, DEFAULT_CONFIG.maxChanges),
|
|
279
318
|
refreshIntervalMs: num(c.refreshIntervalMs, DEFAULT_CONFIG.refreshIntervalMs),
|
|
280
|
-
showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill)
|
|
319
|
+
showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill),
|
|
320
|
+
defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView)
|
|
281
321
|
};
|
|
282
322
|
}
|
|
323
|
+
function unwrapVolatile(value) {
|
|
324
|
+
return value !== null && typeof value === "object" && "get" in value && typeof value.get === "function" ? value.get() : value;
|
|
325
|
+
}
|
|
326
|
+
function readDiffView(value, fallback) {
|
|
327
|
+
const raw = unwrapVolatile(value);
|
|
328
|
+
return raw === "unified" || raw === "split" ? raw : fallback;
|
|
329
|
+
}
|
|
283
330
|
function readBool(value, fallback) {
|
|
284
|
-
const raw = value
|
|
331
|
+
const raw = unwrapVolatile(value);
|
|
285
332
|
return typeof raw === "boolean" ? raw : fallback;
|
|
286
333
|
}
|
|
334
|
+
var NEG_CACHE_MS = 15e3;
|
|
335
|
+
async function resolveBrowseRoot(deps, sessionId) {
|
|
336
|
+
const ws = await resolveWorkspace(deps, sessionId);
|
|
337
|
+
if (ws.ok) return { ok: true, root: ws.root, isGitRepo: true };
|
|
338
|
+
const err = ws.failure.error;
|
|
339
|
+
if (err.code === "not-a-git-repo" && err.cwd !== void 0 && err.cwd !== "") {
|
|
340
|
+
try {
|
|
341
|
+
return { ok: true, root: await deps.fs.realpath(err.cwd), isGitRepo: false };
|
|
342
|
+
} catch {
|
|
343
|
+
return { ok: false, error: { code: "git-error" } };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { ok: false, error: mapWorkspaceFailure(ws.failure) };
|
|
347
|
+
}
|
|
287
348
|
async function resolveWorkspace(deps, sessionId) {
|
|
288
349
|
let cwd = deps.sessions.liveCwd(sessionId);
|
|
289
350
|
if (cwd === void 0 || cwd === "") {
|
|
@@ -295,16 +356,26 @@ async function resolveWorkspace(deps, sessionId) {
|
|
|
295
356
|
}
|
|
296
357
|
const cached = deps.rootCache?.get(cwd);
|
|
297
358
|
if (cached !== void 0) return { ok: true, root: cached };
|
|
359
|
+
const negAt = deps.rootNegCache?.get(cwd);
|
|
360
|
+
if (negAt !== void 0) {
|
|
361
|
+
if (Date.now() < negAt) return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
|
|
362
|
+
deps.rootNegCache?.delete(cwd);
|
|
363
|
+
}
|
|
298
364
|
const top = await runCommand(deps.run, ["git", "rev-parse", "--show-toplevel"], cwd, "toplevel", deps.signal);
|
|
299
365
|
if ("failure" in top) {
|
|
300
366
|
return { ok: false, failure: { ok: false, error: mapRunFailure(top.failure) } };
|
|
301
367
|
}
|
|
368
|
+
if (top.run.cancelled) return { ok: false, failure: { ok: false, error: { code: "cancelled" } } };
|
|
302
369
|
if (top.run.timedOut) return { ok: false, failure: { ok: false, error: { code: "timeout" } } };
|
|
303
370
|
if (top.run.exitCode !== 0) {
|
|
371
|
+
deps.rootNegCache?.set(cwd, Date.now() + NEG_CACHE_MS);
|
|
304
372
|
return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
|
|
305
373
|
}
|
|
306
374
|
const raw = top.run.stdout.trim();
|
|
307
|
-
if (raw === "")
|
|
375
|
+
if (raw === "") {
|
|
376
|
+
deps.rootNegCache?.set(cwd, Date.now() + NEG_CACHE_MS);
|
|
377
|
+
return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
|
|
378
|
+
}
|
|
308
379
|
let root = raw;
|
|
309
380
|
try {
|
|
310
381
|
root = await deps.fs.realpath(raw);
|
|
@@ -314,9 +385,9 @@ async function resolveWorkspace(deps, sessionId) {
|
|
|
314
385
|
deps.rootCache?.set(cwd, root);
|
|
315
386
|
return { ok: true, root };
|
|
316
387
|
}
|
|
317
|
-
async function runCommand(runner, argv, cwd, _label, signal) {
|
|
388
|
+
async function runCommand(runner, argv, cwd, _label, signal, stdinData) {
|
|
318
389
|
try {
|
|
319
|
-
const run = await runner.run(argv, { cwd, ...signal ? { signal } : {} });
|
|
390
|
+
const run = await runner.run(argv, { cwd, ...signal ? { signal } : {}, ...stdinData !== void 0 ? { stdinData } : {} });
|
|
320
391
|
return { run };
|
|
321
392
|
} catch (error) {
|
|
322
393
|
return { failure: error };
|
|
@@ -326,6 +397,11 @@ function mapRunFailure(failure) {
|
|
|
326
397
|
const message = failure instanceof Error ? failure.message : String(failure);
|
|
327
398
|
return { code: "git-unavailable", detail: message };
|
|
328
399
|
}
|
|
400
|
+
function mapWorkspaceFailure(failure) {
|
|
401
|
+
const error = failure.error;
|
|
402
|
+
const message = "detail" in error ? error.detail : void 0;
|
|
403
|
+
return { code: error.code, ...message !== void 0 ? { message } : {} };
|
|
404
|
+
}
|
|
329
405
|
async function snapshotForSession(deps, config, sessionId) {
|
|
330
406
|
const workspace = await resolveWorkspace(deps, sessionId);
|
|
331
407
|
if (!workspace.ok) {
|
|
@@ -336,26 +412,30 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
336
412
|
return failure;
|
|
337
413
|
}
|
|
338
414
|
const root = workspace.root;
|
|
339
|
-
const [branchRes, headRes, statusRes, aheadBehindRes, lastCommitRes] = await Promise.all([
|
|
415
|
+
const [branchRes, headRes, statusRes, aheadBehindRes, lastCommitRes, worktreeNumRes, stagedNumRes] = await Promise.all([
|
|
340
416
|
runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "HEAD"], root, "branch", deps.signal),
|
|
341
417
|
runCommand(deps.run, ["git", "rev-parse", "--short", "HEAD"], root, "head", deps.signal),
|
|
342
418
|
runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z"], root, "status", deps.signal),
|
|
343
419
|
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)
|
|
420
|
+
runCommand(deps.run, ["git", "log", "-1", "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI"], root, "lastCommit", deps.signal),
|
|
421
|
+
runCommand(deps.run, ["git", "diff", "--numstat"], root, "numstat-worktree", deps.signal),
|
|
422
|
+
runCommand(deps.run, ["git", "diff", "--numstat", "--cached"], root, "numstat-staged", deps.signal)
|
|
345
423
|
]);
|
|
346
424
|
const branch = "run" in branchRes && branchRes.run.exitCode === 0 ? branchRes.run.stdout.trim() || null : null;
|
|
347
425
|
const unborn = "run" in headRes && headRes.run.exitCode !== 0;
|
|
348
426
|
const head = unborn ? null : "run" in headRes ? headRes.run.stdout.trim() || null : null;
|
|
349
427
|
let allChanges = [];
|
|
428
|
+
let statusLossy = false;
|
|
350
429
|
if ("run" in statusRes && !statusRes.run.timedOut && statusRes.run.exitCode === 0) {
|
|
351
430
|
allChanges = parseStatus(statusRes.run.stdout);
|
|
431
|
+
statusLossy = statusRes.run.stdoutLossy;
|
|
352
432
|
}
|
|
353
|
-
const truncated = allChanges.length > config.maxChanges;
|
|
354
|
-
const changes =
|
|
433
|
+
const truncated = allChanges.length > config.maxChanges || statusLossy;
|
|
434
|
+
const changes = allChanges.length > config.maxChanges ? allChanges.slice(0, config.maxChanges) : allChanges;
|
|
355
435
|
let staged = 0;
|
|
356
436
|
let modified = 0;
|
|
357
437
|
let untracked = 0;
|
|
358
|
-
for (const c of
|
|
438
|
+
for (const c of allChanges) {
|
|
359
439
|
if (c.status === "untracked") untracked++;
|
|
360
440
|
else if (c.staged) staged++;
|
|
361
441
|
else modified++;
|
|
@@ -380,12 +460,35 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
380
460
|
};
|
|
381
461
|
}
|
|
382
462
|
}
|
|
463
|
+
const wt = "run" in worktreeNumRes && worktreeNumRes.run.exitCode === 0 ? sumNumstat(worktreeNumRes.run.stdout) : { insertions: 0, deletions: 0 };
|
|
464
|
+
const stg = "run" in stagedNumRes && stagedNumRes.run.exitCode === 0 ? sumNumstat(stagedNumRes.run.stdout) : { insertions: 0, deletions: 0 };
|
|
465
|
+
let untrackedInsertions = 0;
|
|
466
|
+
const untrackedPaths = allChanges.filter((c) => c.status === "untracked" && !c.isDirectory).map((c) => c.path);
|
|
467
|
+
const safeUntracked = untrackedPaths.filter(isSafePath).slice(0, 200);
|
|
468
|
+
if (safeUntracked.length > 0) {
|
|
469
|
+
const perFile = await Promise.all(
|
|
470
|
+
safeUntracked.map((p) => runCommand(deps.run, ["git", "diff", "--numstat", "--no-index", "--", "/dev/null", p], root, "numstat-untracked", deps.signal))
|
|
471
|
+
);
|
|
472
|
+
for (const r of perFile) if ("run" in r) untrackedInsertions += sumNumstat(r.run.stdout).insertions;
|
|
473
|
+
}
|
|
474
|
+
const lastChangeAt = await maxChangeMtime(deps, root, allChanges);
|
|
475
|
+
const distinct = new Set(allChanges.map((c) => c.path));
|
|
476
|
+
const stats = {
|
|
477
|
+
fileCount: distinct.size,
|
|
478
|
+
staged,
|
|
479
|
+
modified,
|
|
480
|
+
untracked,
|
|
481
|
+
insertions: wt.insertions + stg.insertions + untrackedInsertions,
|
|
482
|
+
deletions: wt.deletions + stg.deletions,
|
|
483
|
+
lastChangeAt,
|
|
484
|
+
headCommittedAt: lastCommit?.dateIso ?? null
|
|
485
|
+
};
|
|
383
486
|
const snapshot = {
|
|
384
487
|
root,
|
|
385
488
|
branch,
|
|
386
489
|
head,
|
|
387
490
|
unborn,
|
|
388
|
-
dirty:
|
|
491
|
+
dirty: allChanges.length > 0,
|
|
389
492
|
staged,
|
|
390
493
|
modified,
|
|
391
494
|
untracked,
|
|
@@ -393,9 +496,11 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
393
496
|
behind,
|
|
394
497
|
lastCommit,
|
|
395
498
|
changes,
|
|
499
|
+
stats,
|
|
396
500
|
truncated,
|
|
397
501
|
refreshIntervalMs: config.refreshIntervalMs,
|
|
398
502
|
showInputPill: config.showInputPill,
|
|
503
|
+
defaultDiffView: config.defaultDiffView,
|
|
399
504
|
checkedAt: Date.now()
|
|
400
505
|
};
|
|
401
506
|
return { ok: true, value: snapshot };
|
|
@@ -417,13 +522,7 @@ async function maxChangeMtime(deps, root, changes, cap = 200) {
|
|
|
417
522
|
}
|
|
418
523
|
|
|
419
524
|
// 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
|
-
}
|
|
525
|
+
import { join as join2, sep } from "node:path";
|
|
427
526
|
function withPaths(prefixes, paths) {
|
|
428
527
|
if (paths.length === 0) return { error: "invalid-path", message: "no paths given" };
|
|
429
528
|
for (const path of paths) {
|
|
@@ -445,55 +544,66 @@ function planAction(action, unborn) {
|
|
|
445
544
|
return withPaths([["git", "restore", "--"]], action.paths);
|
|
446
545
|
case "commit": {
|
|
447
546
|
const message = action.message.trim();
|
|
448
|
-
const
|
|
449
|
-
if (message === "" &&
|
|
450
|
-
const
|
|
547
|
+
const amend = action.amend === true;
|
|
548
|
+
if (message === "" && !amend) return { error: "empty-message" };
|
|
549
|
+
const amendFlag = amend ? ["--amend"] : [];
|
|
550
|
+
const msgArgs = message === "" ? ["--no-edit"] : ["-m", message];
|
|
451
551
|
if (action.paths === void 0 || action.paths.length === 0) {
|
|
452
552
|
return { argv: [["git", "commit", ...amendFlag, ...msgArgs]] };
|
|
453
553
|
}
|
|
454
554
|
const staged = withPaths([["git", "add", "--"]], action.paths);
|
|
455
555
|
if ("error" in staged) return staged;
|
|
456
556
|
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
557
|
return { argv: [...staged.argv, commitCmd] };
|
|
461
558
|
}
|
|
462
559
|
case "branch-checkout":
|
|
463
|
-
return {
|
|
560
|
+
if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe branch name: ${action.name}` };
|
|
561
|
+
return { argv: [["git", "checkout", "--end-of-options", action.name]] };
|
|
464
562
|
case "fetch":
|
|
465
563
|
return { argv: [["git", "fetch", "--all", "--prune"]] };
|
|
466
564
|
}
|
|
467
565
|
}
|
|
468
566
|
async function runAction(deps, config, request) {
|
|
469
567
|
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
|
-
}
|
|
568
|
+
if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
|
|
474
569
|
const root = workspace.root;
|
|
475
570
|
const headProbe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "head-probe", deps.signal);
|
|
476
571
|
const unborn = !("run" in headProbe) || headProbe.run.exitCode !== 0;
|
|
572
|
+
if (request.action.kind === "discard") {
|
|
573
|
+
const removed = await discardUntracked(deps, config, root, request.sessionId, request.action.paths);
|
|
574
|
+
if (removed !== null) {
|
|
575
|
+
if (!removed.ok) return removed.result;
|
|
576
|
+
if (removed.remainingTracked.length === 0) {
|
|
577
|
+
const snapshot2 = await snapshotForSession(deps, config, request.sessionId);
|
|
578
|
+
if (!snapshot2.ok) return { ok: false, error: { code: "git-error", message: "snapshot after action failed" } };
|
|
579
|
+
return { ok: true, snapshot: snapshot2.value, output: "" };
|
|
580
|
+
}
|
|
581
|
+
request = { ...request, action: { kind: "discard", paths: removed.remainingTracked } };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
477
584
|
const plan = planAction(request.action, unborn);
|
|
478
585
|
if ("error" in plan) return { ok: false, error: { code: plan.error, ...plan.message ? { message: plan.message } : {} } };
|
|
479
586
|
let lastOutput = "";
|
|
480
|
-
for (
|
|
587
|
+
for (let step = 0; step < plan.argv.length; step += 1) {
|
|
588
|
+
const argv = plan.argv[step];
|
|
481
589
|
const outcome = await runCommand(deps.run, argv, root, "action", deps.signal);
|
|
590
|
+
const where = plan.argv.length > 1 ? ` (step ${step + 1}/${plan.argv.length}: ${argv.join(" ")})` : "";
|
|
482
591
|
if ("failure" in outcome) {
|
|
483
592
|
const message = outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure);
|
|
484
|
-
return { ok: false, error: { code: "git-unavailable", message } };
|
|
593
|
+
return { ok: false, error: { code: "git-unavailable", message: message + where } };
|
|
485
594
|
}
|
|
595
|
+
if (outcome.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
486
596
|
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
487
597
|
lastOutput = outcome.run.stdout || outcome.run.stderr;
|
|
488
598
|
if (outcome.run.exitCode !== 0) {
|
|
489
599
|
const stderr = outcome.run.stderr;
|
|
490
600
|
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" } };
|
|
601
|
+
return { ok: false, error: { code: "git-error", message: (stderr.trim() || "nothing to commit") + where } };
|
|
492
602
|
}
|
|
493
603
|
if (/would be overwritten by checkout|local changes/i.test(stderr)) {
|
|
494
|
-
return { ok: false, error: { code: "local-changes-block", message: stderr.trim() } };
|
|
604
|
+
return { ok: false, error: { code: "local-changes-block", message: stderr.trim() + where } };
|
|
495
605
|
}
|
|
496
|
-
return { ok: false, error: { code: "git-error", message: stderr.trim() || `git exited ${outcome.run.exitCode}` } };
|
|
606
|
+
return { ok: false, error: { code: "git-error", message: (stderr.trim() || `git exited ${outcome.run.exitCode}`) + where } };
|
|
497
607
|
}
|
|
498
608
|
}
|
|
499
609
|
const snapshot = await snapshotForSession(deps, config, request.sessionId);
|
|
@@ -502,9 +612,39 @@ async function runAction(deps, config, request) {
|
|
|
502
612
|
}
|
|
503
613
|
return { ok: true, snapshot: snapshot.value, output: lastOutput.trim() };
|
|
504
614
|
}
|
|
615
|
+
async function discardUntracked(deps, config, root, sessionId, paths) {
|
|
616
|
+
const snap = await snapshotForSession(deps, config, sessionId);
|
|
617
|
+
if (!snap.ok) return null;
|
|
618
|
+
const untrackedSet = new Set(snap.value.changes.filter((c) => c.status === "untracked").map((c) => c.path));
|
|
619
|
+
const untracked = paths.filter((p) => untrackedSet.has(p));
|
|
620
|
+
if (untracked.length === 0) return null;
|
|
621
|
+
const tracked = paths.filter((p) => !untrackedSet.has(p));
|
|
622
|
+
let rootReal;
|
|
623
|
+
try {
|
|
624
|
+
rootReal = await deps.fs.realpath(root);
|
|
625
|
+
} catch {
|
|
626
|
+
return { ok: false, result: { ok: false, error: { code: "git-error", message: "repository root unavailable" } } };
|
|
627
|
+
}
|
|
628
|
+
for (const path of untracked) {
|
|
629
|
+
if (!isSafePath(path)) return { ok: false, result: { ok: false, error: { code: "invalid-path", message: `unsafe path: ${path}` } } };
|
|
630
|
+
const target = join2(root, path);
|
|
631
|
+
let targetReal;
|
|
632
|
+
try {
|
|
633
|
+
targetReal = await deps.fs.realpath(target);
|
|
634
|
+
} catch {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (targetReal !== rootReal && !targetReal.startsWith(rootReal + sep)) {
|
|
638
|
+
return { ok: false, result: { ok: false, error: { code: "invalid-path", message: `path escapes repository: ${path}` } } };
|
|
639
|
+
}
|
|
640
|
+
await deps.fs.remove(target).catch(() => {
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return { ok: true, remainingTracked: tracked };
|
|
644
|
+
}
|
|
505
645
|
|
|
506
646
|
// src/host/queries.ts
|
|
507
|
-
import { join as
|
|
647
|
+
import { join as join3, sep as sep2 } from "node:path";
|
|
508
648
|
|
|
509
649
|
// src/host/types.ts
|
|
510
650
|
var IMAGE_MIME = {
|
|
@@ -532,25 +672,27 @@ function isHexLike(text) {
|
|
|
532
672
|
return /^[0-9a-fA-F]{7,40}$/.test(text.trim());
|
|
533
673
|
}
|
|
534
674
|
async function runQuery(deps, config, request) {
|
|
535
|
-
const
|
|
536
|
-
if (
|
|
537
|
-
const
|
|
538
|
-
return {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
};
|
|
675
|
+
const q = request.query;
|
|
676
|
+
if (q.kind === "dir-list" || q.kind === "file-content") {
|
|
677
|
+
const browse = await resolveBrowseRoot(deps, request.sessionId);
|
|
678
|
+
if (!browse.ok) return { ok: false, error: browse.error };
|
|
679
|
+
try {
|
|
680
|
+
return q.kind === "dir-list" ? await queryDirList(deps, browse.root, q, browse.isGitRepo) : await queryFileContent(deps, config, browse.root, q);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : String(error) } };
|
|
683
|
+
}
|
|
545
684
|
}
|
|
685
|
+
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
686
|
+
if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
|
|
546
687
|
const root = workspace.root;
|
|
547
|
-
const q = request.query;
|
|
548
688
|
try {
|
|
549
689
|
switch (q.kind) {
|
|
550
690
|
case "history":
|
|
551
691
|
return await queryHistory(deps, root, q);
|
|
552
692
|
case "diff":
|
|
553
693
|
return await queryDiff(deps, root, q);
|
|
694
|
+
case "file-lines":
|
|
695
|
+
return await queryFileLines(deps, root, q);
|
|
554
696
|
case "image-diff":
|
|
555
697
|
return await queryImageDiff(deps, config, root, q);
|
|
556
698
|
case "show":
|
|
@@ -564,7 +706,7 @@ async function runQuery(deps, config, request) {
|
|
|
564
706
|
case "last-commit-message":
|
|
565
707
|
return await queryLastCommitMessage(deps, root);
|
|
566
708
|
case "worktree-stats":
|
|
567
|
-
return await queryWorktreeStats(deps, config,
|
|
709
|
+
return await queryWorktreeStats(deps, config, request.sessionId);
|
|
568
710
|
}
|
|
569
711
|
} catch (error) {
|
|
570
712
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -572,18 +714,13 @@ async function runQuery(deps, config, request) {
|
|
|
572
714
|
}
|
|
573
715
|
}
|
|
574
716
|
async function queryHistory(deps, root, q) {
|
|
575
|
-
const
|
|
717
|
+
const limit = Number.isFinite(q.limit) ? Math.min(500, Math.max(1, Math.trunc(q.limit))) : 100;
|
|
718
|
+
const skip = Number.isFinite(q.skip) ? Math.max(0, Math.trunc(q.skip)) : 0;
|
|
719
|
+
const args = ["git", "log", GRAPH_FORMAT, `--max-count=${limit}`, `--skip=${skip}`];
|
|
576
720
|
const search = q.search?.trim() ?? "";
|
|
577
721
|
const hexJump = search !== "" && isHexLike(search);
|
|
578
722
|
const countArgs = ["git", "rev-list", "--count"];
|
|
579
723
|
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
724
|
if (search !== "") {
|
|
588
725
|
args.push("-i", "-E", `--grep=${search}`);
|
|
589
726
|
countArgs.push("-i", "-E", `--grep=${search}`);
|
|
@@ -596,22 +733,35 @@ async function queryHistory(deps, root, q) {
|
|
|
596
733
|
args.push(`--since=${q.since}`);
|
|
597
734
|
countArgs.push(`--since=${q.since}`);
|
|
598
735
|
}
|
|
736
|
+
if (q.ref !== void 0 && q.ref !== "") {
|
|
737
|
+
if (!isSafeRev(q.ref)) return { ok: false, error: { code: "invalid-name", message: `unsafe ref: ${q.ref}` } };
|
|
738
|
+
args.push("--end-of-options", q.ref);
|
|
739
|
+
countArgs.push("--end-of-options", q.ref);
|
|
740
|
+
} else {
|
|
741
|
+
args.push("--all");
|
|
742
|
+
countArgs.push("--all");
|
|
743
|
+
}
|
|
599
744
|
} else {
|
|
600
|
-
args.push(search);
|
|
745
|
+
args.push("--end-of-options", search);
|
|
601
746
|
}
|
|
602
747
|
const [res, countRes] = await Promise.all([
|
|
603
748
|
runCommand(deps.run, args, root, "history", deps.signal),
|
|
604
749
|
hexJump ? Promise.resolve(null) : runCommand(deps.run, countArgs, root, "history-count", deps.signal)
|
|
605
750
|
]);
|
|
606
751
|
if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
|
|
752
|
+
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
607
753
|
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
608
754
|
if (res.run.exitCode !== 0) {
|
|
609
|
-
|
|
755
|
+
const stderr = res.run.stderr.trim();
|
|
756
|
+
if (hexJump || /unknown revision|bad revision|does not have any commits|ambiguous argument/i.test(stderr)) {
|
|
757
|
+
return { ok: true, value: { kind: "history", commits: [], total: 0 } };
|
|
758
|
+
}
|
|
759
|
+
return { ok: false, error: { code: "git-error", message: stderr || `git exited ${res.run.exitCode}` } };
|
|
610
760
|
}
|
|
611
761
|
const commits = parseGraphLog(res.run.stdout);
|
|
612
762
|
let total = -1;
|
|
613
763
|
if (hexJump) {
|
|
614
|
-
total =
|
|
764
|
+
total = -1;
|
|
615
765
|
} else if (countRes !== null && "run" in countRes && countRes.run.exitCode === 0) {
|
|
616
766
|
const n = Number(countRes.run.stdout.trim());
|
|
617
767
|
if (Number.isFinite(n)) total = n;
|
|
@@ -626,22 +776,75 @@ async function queryDiff(deps, root, q) {
|
|
|
626
776
|
if (q.base === "staged") {
|
|
627
777
|
args = ["git", "diff", unified, "--cached", "--", q.path];
|
|
628
778
|
} else if (q.base === "commit") {
|
|
629
|
-
|
|
779
|
+
if (!isSafeRev(q.commit)) return { ok: false, error: { code: "invalid-name", message: `unsafe commit: ${q.commit}` } };
|
|
780
|
+
args = ["git", "show", unified, "--end-of-options", q.commit, "--", q.path];
|
|
630
781
|
} else {
|
|
631
782
|
args = ["git", "diff", unified, "--", q.path];
|
|
632
783
|
}
|
|
633
784
|
const res = await runCommand(deps.run, args, root, "diff", deps.signal);
|
|
634
785
|
if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
|
|
786
|
+
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
635
787
|
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
636
788
|
let text = res.run.stdout;
|
|
637
789
|
if (q.base === "worktree" && text.trim() === "") {
|
|
638
|
-
const
|
|
639
|
-
|
|
790
|
+
const tracked = await runCommand(deps.run, ["git", "ls-files", "--error-unmatch", "--", q.path], root, "diff-tracked-probe", deps.signal);
|
|
791
|
+
const isUntracked = !("run" in tracked) || tracked.run.exitCode !== 0;
|
|
792
|
+
if (isUntracked) {
|
|
793
|
+
const noIndex = await runCommand(deps.run, ["git", "diff", unified, "--no-index", "--", "/dev/null", q.path], root, "diff-untracked", deps.signal);
|
|
794
|
+
if ("run" in noIndex) text = noIndex.run.stdout;
|
|
795
|
+
}
|
|
640
796
|
}
|
|
641
797
|
return { ok: true, value: { kind: "diff", path: q.path, text } };
|
|
642
798
|
}
|
|
799
|
+
async function queryFileLines(deps, root, q) {
|
|
800
|
+
if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
|
|
801
|
+
const start = Number.isFinite(q.start) ? Math.max(1, Math.floor(q.start)) : 1;
|
|
802
|
+
const end = Number.isFinite(q.end) ? Math.max(start, Math.floor(q.end)) : start;
|
|
803
|
+
let args;
|
|
804
|
+
if (q.base === "commit") {
|
|
805
|
+
if (!isSafeRev(q.commit)) return { ok: false, error: { code: "invalid-name", message: `unsafe commit: ${q.commit}` } };
|
|
806
|
+
args = ["git", "show", "--end-of-options", `${q.commit}:${q.path}`];
|
|
807
|
+
} else {
|
|
808
|
+
args = ["git", "show", `--end-of-options`, `:${q.path}`];
|
|
809
|
+
if (q.base === "worktree") args = ["git", "cat-file", "-p", `:${q.path}`];
|
|
810
|
+
}
|
|
811
|
+
const res = await runCommand(deps.run, args, root, "file-lines", deps.signal);
|
|
812
|
+
if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
|
|
813
|
+
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
814
|
+
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
815
|
+
if (res.run.exitCode !== 0) {
|
|
816
|
+
if (q.base === "worktree") {
|
|
817
|
+
const wt = await readWorktreeText(deps, root, q.path);
|
|
818
|
+
if (wt !== null) return sliceLines(q.path, wt, start, end);
|
|
819
|
+
}
|
|
820
|
+
return { ok: false, error: { code: "git-error", message: res.run.stderr.trim() || "no such blob" } };
|
|
821
|
+
}
|
|
822
|
+
return sliceLines(q.path, res.run.stdout, start, end);
|
|
823
|
+
}
|
|
824
|
+
async function readWorktreeText(deps, root, path) {
|
|
825
|
+
try {
|
|
826
|
+
const file = join3(root, path);
|
|
827
|
+
const real = await deps.fs.realpath(file);
|
|
828
|
+
const rootReal = await deps.fs.realpath(root);
|
|
829
|
+
if (real !== rootReal && !real.startsWith(rootReal + sep2)) return null;
|
|
830
|
+
const buf = await deps.fs.readFile(file);
|
|
831
|
+
return buf.toString("utf8");
|
|
832
|
+
} catch {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function sliceLines(path, content, start, end) {
|
|
837
|
+
const all = content.split("\n");
|
|
838
|
+
if (all.length > 0 && all[all.length - 1] === "") all.pop();
|
|
839
|
+
const from = Math.min(start, all.length + 1);
|
|
840
|
+
const to = Math.min(end, all.length);
|
|
841
|
+
const lines = from <= to ? all.slice(from - 1, to) : [];
|
|
842
|
+
const eof = to >= all.length;
|
|
843
|
+
return { ok: true, value: { kind: "file-lines", path, start: from, lines, eof } };
|
|
844
|
+
}
|
|
643
845
|
async function queryImageDiff(deps, config, root, q) {
|
|
644
846
|
if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
|
|
847
|
+
if (q.base === "commit" && !isSafeRev(q.commit)) return { ok: false, error: { code: "invalid-name", message: `unsafe commit: ${q.commit}` } };
|
|
645
848
|
const mime = imageMimeFor(q.path);
|
|
646
849
|
if (mime === null) return { ok: true, value: { kind: "image-diff", path: q.path, mime } };
|
|
647
850
|
const oldSpec = q.base === "staged" ? `HEAD:${q.path}` : q.base === "commit" ? `${q.commit}^1:${q.path}` : `:${q.path}`;
|
|
@@ -694,7 +897,7 @@ async function blobSide(deps, gitDir, oid, cap) {
|
|
|
694
897
|
if (!("run" in nameRes) || nameRes.run.exitCode !== 0) return void 0;
|
|
695
898
|
const name = nameRes.run.stdout.trim();
|
|
696
899
|
if (!/^[A-Za-z0-9._-]+$/.test(name)) return void 0;
|
|
697
|
-
const file =
|
|
900
|
+
const file = join3(gitDir, name);
|
|
698
901
|
try {
|
|
699
902
|
const buf = await deps.fs.readFile(file);
|
|
700
903
|
if (buf.length > cap) return { tooLarge: true };
|
|
@@ -708,7 +911,10 @@ async function blobSide(deps, gitDir, oid, cap) {
|
|
|
708
911
|
}
|
|
709
912
|
async function worktreeSide(deps, root, path, cap) {
|
|
710
913
|
try {
|
|
711
|
-
const file =
|
|
914
|
+
const file = join3(root, path);
|
|
915
|
+
const real = await deps.fs.realpath(file);
|
|
916
|
+
const rootReal = await deps.fs.realpath(root);
|
|
917
|
+
if (real !== rootReal && !real.startsWith(rootReal + sep2)) return void 0;
|
|
712
918
|
const info = await deps.fs.stat(file);
|
|
713
919
|
if (info.size > cap) return { tooLarge: true };
|
|
714
920
|
const buf = await deps.fs.readFile(file);
|
|
@@ -718,13 +924,115 @@ async function worktreeSide(deps, root, path, cap) {
|
|
|
718
924
|
return void 0;
|
|
719
925
|
}
|
|
720
926
|
}
|
|
927
|
+
var DIR_ENTRY_CAP = 2e3;
|
|
928
|
+
async function queryDirList(deps, root, q, isGitRepo) {
|
|
929
|
+
const rel = q.path;
|
|
930
|
+
if (rel !== "" && !isSafePath(rel) || hasGitSegment(rel)) return { ok: false, error: { code: "invalid-path", message: rel } };
|
|
931
|
+
const dir = rel === "" ? root : join3(root, rel);
|
|
932
|
+
const inside = await isInsideRoot(deps, root, dir, true);
|
|
933
|
+
if (!inside) return { ok: false, error: { code: "invalid-path", message: rel } };
|
|
934
|
+
let raw;
|
|
935
|
+
try {
|
|
936
|
+
raw = await deps.fs.readdir(dir);
|
|
937
|
+
} catch (error) {
|
|
938
|
+
return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "readdir failed" } };
|
|
939
|
+
}
|
|
940
|
+
const filtered = raw.filter((e) => e.name !== ".git");
|
|
941
|
+
filtered.sort((a, b) => a.isDirectory !== b.isDirectory ? a.isDirectory ? -1 : 1 : a.name.localeCompare(b.name));
|
|
942
|
+
const truncated = filtered.length > DIR_ENTRY_CAP;
|
|
943
|
+
const slice = truncated ? filtered.slice(0, DIR_ENTRY_CAP) : filtered;
|
|
944
|
+
const ignored = isGitRepo ? await ignoredEntries(deps, root, rel, slice.map((e) => e.name)) : /* @__PURE__ */ new Set();
|
|
945
|
+
const entries = await Promise.all(slice.map(async (e) => {
|
|
946
|
+
const ignoreFlag = ignored.has(e.name) ? { ignored: true } : {};
|
|
947
|
+
if (e.isDirectory) return { name: e.name, dir: true, ...ignoreFlag };
|
|
948
|
+
let size;
|
|
949
|
+
try {
|
|
950
|
+
size = (await deps.fs.stat(join3(dir, e.name))).size;
|
|
951
|
+
} catch {
|
|
952
|
+
size = void 0;
|
|
953
|
+
}
|
|
954
|
+
return { name: e.name, dir: false, ...size !== void 0 ? { size } : {}, ...ignoreFlag };
|
|
955
|
+
}));
|
|
956
|
+
const path = rel === "" ? "" : rel.replace(/\/+$/, "");
|
|
957
|
+
return { ok: true, value: { kind: "dir-list", path, entries, truncated } };
|
|
958
|
+
}
|
|
959
|
+
async function ignoredEntries(deps, root, parent, names) {
|
|
960
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
961
|
+
const paths = names.map((name) => parent === "" ? name : `${parent}/${name}`);
|
|
962
|
+
for (let index = 0; index < paths.length; ) {
|
|
963
|
+
const batch = [];
|
|
964
|
+
let bytes = 0;
|
|
965
|
+
while (index < paths.length && batch.length < 128) {
|
|
966
|
+
const candidate = paths[index];
|
|
967
|
+
const size = Buffer.byteLength(candidate) + 1;
|
|
968
|
+
if (batch.length > 0 && bytes + size > 32 * 1024) break;
|
|
969
|
+
batch.push(candidate);
|
|
970
|
+
bytes += size;
|
|
971
|
+
index++;
|
|
972
|
+
}
|
|
973
|
+
const result = await runCommand(deps.run, ["git", "check-ignore", "-z", "--stdin"], root, "dir-ignore", deps.signal, `${batch.join("\0")}\0`);
|
|
974
|
+
if ("failure" in result || result.run.timedOut || result.run.cancelled || result.run.stdoutLossy || ![0, 1].includes(result.run.exitCode ?? -1)) continue;
|
|
975
|
+
const matches = new Set(result.run.stdout.split("\0"));
|
|
976
|
+
for (const path of batch) if (matches.has(path)) ignored.add(parent === "" ? path : path.slice(parent.length + 1));
|
|
977
|
+
}
|
|
978
|
+
return ignored;
|
|
979
|
+
}
|
|
980
|
+
async function queryFileContent(deps, config, root, q) {
|
|
981
|
+
if (!isSafePath(q.path) || hasGitSegment(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
|
|
982
|
+
const file = join3(root, q.path);
|
|
983
|
+
const inside = await isInsideRoot(deps, root, file, true);
|
|
984
|
+
if (!inside) return { ok: false, error: { code: "invalid-path", message: q.path } };
|
|
985
|
+
const cap = config.maxBytes;
|
|
986
|
+
let info;
|
|
987
|
+
try {
|
|
988
|
+
info = await deps.fs.stat(file);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "stat failed" } };
|
|
991
|
+
}
|
|
992
|
+
const mime = imageMimeFor(q.path);
|
|
993
|
+
if (info.size > cap) return { ok: true, value: { kind: "file-content", path: q.path, variant: mime !== null ? "image" : "text", tooLarge: true } };
|
|
994
|
+
let buf;
|
|
995
|
+
try {
|
|
996
|
+
buf = await deps.fs.readFile(file);
|
|
997
|
+
} catch (error) {
|
|
998
|
+
return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "read failed" } };
|
|
999
|
+
}
|
|
1000
|
+
if (buf.length > cap) return { ok: true, value: { kind: "file-content", path: q.path, variant: mime !== null ? "image" : "text", tooLarge: true } };
|
|
1001
|
+
if (mime !== null) {
|
|
1002
|
+
return { ok: true, value: { kind: "file-content", path: q.path, variant: "image", dataUrl: `data:${mime};base64,${buf.toString("base64")}` } };
|
|
1003
|
+
}
|
|
1004
|
+
if (isBinaryBuffer(buf)) return { ok: true, value: { kind: "file-content", path: q.path, variant: "binary" } };
|
|
1005
|
+
const content = buf.toString("utf8");
|
|
1006
|
+
const lines = content === "" ? 0 : content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
|
|
1007
|
+
return { ok: true, value: { kind: "file-content", path: q.path, variant: "text", content, lines } };
|
|
1008
|
+
}
|
|
1009
|
+
function hasGitSegment(path) {
|
|
1010
|
+
return path.split(/[\\/]/).includes(".git");
|
|
1011
|
+
}
|
|
1012
|
+
async function isInsideRoot(deps, root, path, hideGit = false) {
|
|
1013
|
+
try {
|
|
1014
|
+
const real = await deps.fs.realpath(path);
|
|
1015
|
+
const rootReal = await deps.fs.realpath(root);
|
|
1016
|
+
if (real !== rootReal && !real.startsWith(rootReal + sep2)) return false;
|
|
1017
|
+
return !hideGit || !hasGitSegment(real.slice(rootReal.length + 1));
|
|
1018
|
+
} catch {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function isBinaryBuffer(buf) {
|
|
1023
|
+
const n = Math.min(buf.length, 8192);
|
|
1024
|
+
for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
721
1027
|
async function queryShow(deps, root, ref) {
|
|
1028
|
+
if (!isSafeRev(ref)) return { ok: false, error: { code: "invalid-name", message: `unsafe ref: ${ref}` } };
|
|
722
1029
|
const metaFormat = "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%b";
|
|
723
1030
|
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)
|
|
1031
|
+
runCommand(deps.run, ["git", "show", "-s", metaFormat, "--end-of-options", ref], root, "show-meta", deps.signal),
|
|
1032
|
+
runCommand(deps.run, ["git", "show", "--name-status", "-z", "--format=", "--end-of-options", ref], root, "show-stat", deps.signal)
|
|
726
1033
|
]);
|
|
727
1034
|
if (!("run" in metaRes)) return { ok: false, error: { code: "git-unavailable" } };
|
|
1035
|
+
if (metaRes.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
728
1036
|
if (metaRes.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
729
1037
|
if (metaRes.run.exitCode !== 0) {
|
|
730
1038
|
return { ok: false, error: { code: "git-error", message: metaRes.run.stderr.trim() || "unknown ref" } };
|
|
@@ -740,7 +1048,7 @@ async function queryShow(deps, root, ref) {
|
|
|
740
1048
|
author: parts[3] ?? "",
|
|
741
1049
|
dateIso: parts[4] ?? ""
|
|
742
1050
|
};
|
|
743
|
-
body = (
|
|
1051
|
+
body = parts.slice(5).join("").trim();
|
|
744
1052
|
}
|
|
745
1053
|
const stats = "run" in statRes && statRes.run.exitCode === 0 ? parseNameStatus(statRes.run.stdout) : [];
|
|
746
1054
|
return { ok: true, value: { kind: "show", ref, commit, body, stats } };
|
|
@@ -765,11 +1073,7 @@ async function queryBranches(deps, root) {
|
|
|
765
1073
|
}
|
|
766
1074
|
async function queryTags(deps, root) {
|
|
767
1075
|
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
|
-
}) : [];
|
|
1076
|
+
const tags = "run" in res && res.run.exitCode === 0 ? parseTags(res.run.stdout) : [];
|
|
773
1077
|
return { ok: true, value: { kind: "tags", tags } };
|
|
774
1078
|
}
|
|
775
1079
|
async function queryAuthors(deps, root) {
|
|
@@ -782,48 +1086,18 @@ async function queryLastCommitMessage(deps, root) {
|
|
|
782
1086
|
const message = "run" in res && res.run.exitCode === 0 ? res.run.stdout.replace(/\n+$/, "") : "";
|
|
783
1087
|
return { ok: true, value: { kind: "last-commit-message", message } };
|
|
784
1088
|
}
|
|
785
|
-
async function queryWorktreeStats(deps, config,
|
|
1089
|
+
async function queryWorktreeStats(deps, config, sessionId) {
|
|
786
1090
|
const snapshot = await snapshotForSession(deps, config, sessionId);
|
|
787
1091
|
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 } };
|
|
1092
|
+
return { ok: true, value: { kind: "worktree-stats", stats: snapshot.value.stats } };
|
|
819
1093
|
}
|
|
820
1094
|
|
|
821
1095
|
// src/host/version.ts
|
|
822
1096
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
823
|
-
import { dirname, join as
|
|
1097
|
+
import { dirname, join as join4 } from "node:path";
|
|
824
1098
|
import { fileURLToPath } from "node:url";
|
|
825
1099
|
function manifestPath() {
|
|
826
|
-
return
|
|
1100
|
+
return join4(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
827
1101
|
}
|
|
828
1102
|
function stringField(value) {
|
|
829
1103
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
@@ -841,9 +1115,10 @@ function compareVersions(a, b) {
|
|
|
841
1115
|
const split = (value) => {
|
|
842
1116
|
const trimmed = value.trim().replace(/^v/i, "");
|
|
843
1117
|
const [core, ...rest] = trimmed.split("-");
|
|
1118
|
+
const preRaw = rest.join("-");
|
|
844
1119
|
return {
|
|
845
1120
|
core: core.split(".").map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part)),
|
|
846
|
-
|
|
1121
|
+
pre: preRaw === "" ? [] : preRaw.split(".")
|
|
847
1122
|
};
|
|
848
1123
|
};
|
|
849
1124
|
const left = split(a);
|
|
@@ -855,17 +1130,39 @@ function compareVersions(a, b) {
|
|
|
855
1130
|
if (l > r) return 1;
|
|
856
1131
|
if (l < r) return -1;
|
|
857
1132
|
}
|
|
858
|
-
if (left.
|
|
859
|
-
|
|
1133
|
+
if (left.pre.length === 0 && right.pre.length === 0) return 0;
|
|
1134
|
+
if (left.pre.length === 0) return 1;
|
|
1135
|
+
if (right.pre.length === 0) return -1;
|
|
1136
|
+
const preLen = Math.max(left.pre.length, right.pre.length);
|
|
1137
|
+
for (let i = 0; i < preLen; i += 1) {
|
|
1138
|
+
const lp = left.pre[i];
|
|
1139
|
+
const rp = right.pre[i];
|
|
1140
|
+
if (lp === void 0) return -1;
|
|
1141
|
+
if (rp === void 0) return 1;
|
|
1142
|
+
const ln = Number.parseInt(lp, 10);
|
|
1143
|
+
const rn = Number.parseInt(rp, 10);
|
|
1144
|
+
const lNum = Number.isFinite(ln) && String(ln) === lp;
|
|
1145
|
+
const rNum = Number.isFinite(rn) && String(rn) === rp;
|
|
1146
|
+
if (lNum && rNum) {
|
|
1147
|
+
if (ln !== rn) return ln > rn ? 1 : -1;
|
|
1148
|
+
} else if (lp !== rp) return lp > rp ? 1 : -1;
|
|
1149
|
+
}
|
|
1150
|
+
return 0;
|
|
860
1151
|
}
|
|
1152
|
+
var manifestCache;
|
|
861
1153
|
async function readManifest(path) {
|
|
862
1154
|
const raw = await readFile2(path, "utf8");
|
|
863
1155
|
const parsed = JSON.parse(raw);
|
|
864
1156
|
if (typeof parsed !== "object" || parsed === null) throw new Error("package.json is not an object");
|
|
865
1157
|
return parsed;
|
|
866
1158
|
}
|
|
1159
|
+
async function loadManifest(path) {
|
|
1160
|
+
if (path !== void 0) return readManifest(path);
|
|
1161
|
+
if (manifestCache === void 0) manifestCache = await readManifest(manifestPath());
|
|
1162
|
+
return manifestCache;
|
|
1163
|
+
}
|
|
867
1164
|
async function readVersionInfo(options = {}) {
|
|
868
|
-
const manifest = await
|
|
1165
|
+
const manifest = await loadManifest(options.manifestPath);
|
|
869
1166
|
const current = stringField(manifest.version) ?? "0.0.0";
|
|
870
1167
|
const repo = parseRepository(manifest.repository);
|
|
871
1168
|
const repositoryUrl = repo === void 0 ? void 0 : `https://github.com/${repo.owner}/${repo.repo}`;
|
|
@@ -876,16 +1173,26 @@ async function readVersionInfo(options = {}) {
|
|
|
876
1173
|
checkedRemote: false
|
|
877
1174
|
};
|
|
878
1175
|
}
|
|
1176
|
+
function safeReleaseUrl(url) {
|
|
1177
|
+
if (url === void 0) return void 0;
|
|
1178
|
+
return /^https:\/\/github\.com\//i.test(url) ? url : void 0;
|
|
1179
|
+
}
|
|
1180
|
+
var REMOTE_CACHE_MS = 10 * 60 * 1e3;
|
|
1181
|
+
var remoteCache;
|
|
879
1182
|
async function checkLatestVersion(options = {}) {
|
|
1183
|
+
if (options.manifestPath === void 0 && options.fetchFn === void 0 && remoteCache !== void 0 && Date.now() - remoteCache.at < REMOTE_CACHE_MS) {
|
|
1184
|
+
return remoteCache.info;
|
|
1185
|
+
}
|
|
880
1186
|
const base = await readVersionInfo({ manifestPath: options.manifestPath });
|
|
881
|
-
const repo = parseRepository((await
|
|
1187
|
+
const repo = parseRepository((await loadManifest(options.manifestPath)).repository);
|
|
882
1188
|
if (repo === void 0) {
|
|
883
1189
|
return { ...base, checkedRemote: false, error: "repository is not configured" };
|
|
884
1190
|
}
|
|
885
1191
|
const fetchFn = options.fetchFn ?? fetch;
|
|
886
1192
|
try {
|
|
887
1193
|
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" }
|
|
1194
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "dsh-git-panel" },
|
|
1195
|
+
signal: AbortSignal.timeout(5e3)
|
|
889
1196
|
});
|
|
890
1197
|
if (!response.ok) {
|
|
891
1198
|
return { ...base, checkedRemote: true, error: `GitHub responded with ${response.status}` };
|
|
@@ -897,13 +1204,15 @@ async function checkLatestVersion(options = {}) {
|
|
|
897
1204
|
return { ...base, checkedRemote: true, error: "GitHub response did not include a release tag" };
|
|
898
1205
|
}
|
|
899
1206
|
const latest = tag.replace(/^v/i, "");
|
|
900
|
-
|
|
1207
|
+
const info = {
|
|
901
1208
|
...base,
|
|
902
1209
|
checkedRemote: true,
|
|
903
1210
|
latest,
|
|
904
1211
|
updateAvailable: compareVersions(latest, base.current) > 0,
|
|
905
|
-
releaseUrl: htmlUrl ?? `${base.repositoryUrl
|
|
1212
|
+
releaseUrl: safeReleaseUrl(htmlUrl) ?? (base.repositoryUrl !== void 0 ? `${base.repositoryUrl}/releases/latest` : void 0)
|
|
906
1213
|
};
|
|
1214
|
+
if (options.manifestPath === void 0 && options.fetchFn === void 0) remoteCache = { at: Date.now(), info };
|
|
1215
|
+
return info;
|
|
907
1216
|
} catch (error) {
|
|
908
1217
|
return { ...base, checkedRemote: true, error: error instanceof Error ? error.message : "Unable to reach GitHub" };
|
|
909
1218
|
}
|
|
@@ -924,27 +1233,26 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
|
|
|
924
1233
|
}
|
|
925
1234
|
buildDeps(ctx, config) {
|
|
926
1235
|
const rootCache = /* @__PURE__ */ new Map();
|
|
1236
|
+
const rootNegCache = /* @__PURE__ */ new Map();
|
|
927
1237
|
const get = (key) => ctx.get(key);
|
|
1238
|
+
const fs = {
|
|
1239
|
+
realpath,
|
|
1240
|
+
stat: async (p) => stat(p),
|
|
1241
|
+
readFile: readFile3,
|
|
1242
|
+
readdir: async (p) => (await readdir(p, { withFileTypes: true })).map((e) => ({ name: e.name, isDirectory: e.isDirectory() })),
|
|
1243
|
+
remove: async (p) => {
|
|
1244
|
+
await rm(p, { force: true });
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
928
1247
|
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
|
-
}
|
|
1248
|
+
const run = subprocess === void 0 ? { run: async () => {
|
|
1249
|
+
throw new Error("subprocess service unavailable");
|
|
1250
|
+
} } : createGitRunner(subprocess, config.timeoutMs, config.maxBytes);
|
|
941
1251
|
const sessions = get("sessions");
|
|
942
1252
|
const persistence = get("sessionPersistence");
|
|
943
1253
|
return {
|
|
944
|
-
run
|
|
945
|
-
fs
|
|
946
|
-
await rm(p, { force: true });
|
|
947
|
-
} },
|
|
1254
|
+
run,
|
|
1255
|
+
fs,
|
|
948
1256
|
sessions: {
|
|
949
1257
|
liveCwd: (id) => sessions?.get(id)?.header?.cwd,
|
|
950
1258
|
persistedMeta: async (id) => {
|
|
@@ -957,7 +1265,8 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
|
|
|
957
1265
|
}
|
|
958
1266
|
}
|
|
959
1267
|
},
|
|
960
|
-
rootCache
|
|
1268
|
+
rootCache,
|
|
1269
|
+
rootNegCache
|
|
961
1270
|
};
|
|
962
1271
|
}
|
|
963
1272
|
async snapshot(request, signal) {
|
|
@@ -970,7 +1279,11 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
|
|
|
970
1279
|
return runQuery(this.withSignal(signal), this.liveConfig(), request);
|
|
971
1280
|
}
|
|
972
1281
|
async version(request) {
|
|
973
|
-
|
|
1282
|
+
try {
|
|
1283
|
+
return request.check === true ? await checkLatestVersion() : await readVersionInfo();
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
return { current: "0.0.0", updateAvailable: false, checkedRemote: false, error: error instanceof Error ? error.message : "version unavailable" };
|
|
1286
|
+
}
|
|
974
1287
|
}
|
|
975
1288
|
/** Re-read config so a live-edited volatile field (showInputPill) is current. */
|
|
976
1289
|
liveConfig() {
|
|
@@ -990,12 +1303,17 @@ __decorateElement(_init, 1, "version", _version_dec, GitPanelService);
|
|
|
990
1303
|
__decoratorMetadata(_init, GitPanelService);
|
|
991
1304
|
__publicField(GitPanelService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
|
|
992
1305
|
/**
|
|
993
|
-
* Config schema surfaced on the plugin detail page.
|
|
994
|
-
* `.volatile()`, so the settings host renders
|
|
995
|
-
* the operational limits stay profile-only and out
|
|
1306
|
+
* Config schema surfaced on the plugin detail page. `showInputPill` and
|
|
1307
|
+
* `defaultDiffView` are `.volatile()`, so the settings host renders them as
|
|
1308
|
+
* live-editable controls; the operational limits stay profile-only and out
|
|
1309
|
+
* of the UI form.
|
|
996
1310
|
*/
|
|
997
1311
|
__publicField(GitPanelService, "Config", Schema.object({
|
|
998
|
-
showInputPill: Schema.boolean().default(true).volatile().description("\u663E\u793A\u8F93\u5165\u6846\u7684 Git \u5206\u652F\u6807\u8BB0")
|
|
1312
|
+
showInputPill: Schema.boolean().default(true).volatile().description("\u663E\u793A\u8F93\u5165\u6846\u7684 Git \u5206\u652F\u6807\u8BB0"),
|
|
1313
|
+
defaultDiffView: Schema.union([
|
|
1314
|
+
Schema.const("unified").description("\u7EDF\u4E00\u89C6\u56FE\uFF08\u5355\u680F\u884C\u5185\u5BF9\u6BD4\uFF09"),
|
|
1315
|
+
Schema.const("split").description("\u5E76\u6392\u89C6\u56FE\uFF08\u5DE6\u53F3\u5206\u680F\u5BF9\u6BD4\uFF09")
|
|
1316
|
+
]).default("unified").volatile().description("\u5DEE\u5F02\u5BF9\u6BD4\u9ED8\u8BA4\u89C6\u56FE")
|
|
999
1317
|
}));
|
|
1000
1318
|
var index_default = GitPanelService;
|
|
1001
1319
|
export {
|