dsh-git-ui 0.0.2 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -20
- package/README.zh.md +55 -21
- package/cordis.patch.yml +1 -1
- package/lib/client.js +36 -35
- package/lib/client.js.map +4 -4
- package/lib/contracts/host-endpoints.d.ts +27 -0
- package/lib/host/actions.d.ts +22 -2
- package/lib/host/core.d.ts +7 -1
- package/lib/host/index.d.ts +25 -15
- package/lib/host/index.js +419 -53
- package/lib/host/index.js.map +4 -4
- package/lib/host/parser.d.ts +37 -1
- package/lib/host/queries.d.ts +17 -0
- package/lib/host/types.d.ts +131 -1
- package/package.json +1 -1
- package/src/adapters/dsh/client-adapter.ts +120 -0
- package/src/adapters/dsh/types/cordis.d.ts +48 -0
- package/src/adapters/dsh/types/typert-protocol.d.ts +81 -0
- package/src/adapters/dsh/types/ui-primitives.d.ts +45 -0
- package/src/adapters/dsh/ui-primitives.ts +16 -0
- package/src/client/GitCenter.tsx +1414 -149
- package/src/client/GitPill.tsx +282 -67
- package/src/client/changes-diff.ts +63 -0
- package/src/client/controller.ts +34 -31
- package/src/client/error-text.ts +21 -0
- package/src/client/file-tree.ts +101 -0
- package/src/client/git-graph.ts +188 -0
- package/src/client/icons.tsx +292 -0
- package/src/client/index.ts +38 -134
- package/src/client/locales.ts +124 -0
- package/src/client/popup-close.ts +19 -0
- package/src/client/remote.ts +85 -3
- package/src/client/select-menu.tsx +113 -0
- package/src/client/side-by-side.ts +150 -0
- package/src/client/styles.ts +1375 -86
- package/src/client/time-format.ts +32 -0
- package/src/contracts/client-platform.ts +147 -0
- package/src/contracts/host-endpoints.ts +58 -0
- package/src/contracts/plugin-activation.ts +129 -0
- package/src/contracts/ui-context.tsx +28 -0
- package/src/contracts/ui-primitives.ts +48 -0
- package/src/host/actions.ts +66 -15
- package/src/host/core.ts +9 -2
- package/src/host/index.ts +60 -54
- package/src/host/parser.ts +155 -12
- package/src/host/queries.ts +289 -0
- package/src/host/types.ts +104 -0
package/lib/host/index.js
CHANGED
|
@@ -136,10 +136,8 @@ function parseStatusHeader(line) {
|
|
|
136
136
|
const branch = core.split("...", 1)[0] ?? core;
|
|
137
137
|
return { branch: branch === "" ? null : branch, unborn: false, ahead, behind };
|
|
138
138
|
}
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
if (x === "U" || y === "U" || x !== " " && y !== " ") return "conflicted";
|
|
142
|
-
switch (x) {
|
|
139
|
+
function singleStatus(code) {
|
|
140
|
+
switch (code) {
|
|
143
141
|
case "A":
|
|
144
142
|
return "added";
|
|
145
143
|
case "M":
|
|
@@ -156,6 +154,9 @@ function changeStatus(x, y) {
|
|
|
156
154
|
return "modified";
|
|
157
155
|
}
|
|
158
156
|
}
|
|
157
|
+
function isConflicted(x, y) {
|
|
158
|
+
return x === "U" || y === "U" || x === "A" && y === "A" || x === "D" && y === "D";
|
|
159
|
+
}
|
|
159
160
|
function parseStatusOutput(output, maxChanges) {
|
|
160
161
|
const raw = output.split(NUL);
|
|
161
162
|
const segments = raw[raw.length - 1] === "" ? raw.slice(0, -1) : raw;
|
|
@@ -165,6 +166,13 @@ function parseStatusOutput(output, maxChanges) {
|
|
|
165
166
|
let untracked = 0;
|
|
166
167
|
const changes = [];
|
|
167
168
|
let truncated = false;
|
|
169
|
+
const pushChange = (path, status, isStaged) => {
|
|
170
|
+
if (changes.length < maxChanges) {
|
|
171
|
+
changes.push({ path, status, staged: isStaged, isDirectory: path.endsWith("/") });
|
|
172
|
+
} else {
|
|
173
|
+
truncated = true;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
168
176
|
for (let index = 1; index < segments.length; index += 1) {
|
|
169
177
|
const entry = segments[index] ?? "";
|
|
170
178
|
const x = entry[0] ?? " ";
|
|
@@ -176,14 +184,19 @@ function parseStatusOutput(output, maxChanges) {
|
|
|
176
184
|
}
|
|
177
185
|
if (x === "?" && y === "?") {
|
|
178
186
|
untracked += 1;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (y !== " " && y !== "?") modified += 1;
|
|
187
|
+
pushChange(path, "untracked", false);
|
|
188
|
+
continue;
|
|
182
189
|
}
|
|
183
|
-
if (
|
|
184
|
-
|
|
190
|
+
if (x !== " " && x !== "?") staged += 1;
|
|
191
|
+
if (y !== " " && y !== "?") modified += 1;
|
|
192
|
+
if (isConflicted(x, y)) {
|
|
193
|
+
pushChange(path, "conflicted", true);
|
|
194
|
+
} else if (x !== " " && y !== " ") {
|
|
195
|
+
pushChange(path, singleStatus(x), true);
|
|
196
|
+
pushChange(path, singleStatus(y), false);
|
|
185
197
|
} else {
|
|
186
|
-
|
|
198
|
+
const isStaged = x !== " ";
|
|
199
|
+
pushChange(path, singleStatus(isStaged ? x : y), isStaged);
|
|
187
200
|
}
|
|
188
201
|
}
|
|
189
202
|
return {
|
|
@@ -214,6 +227,92 @@ function parseLogOutput(output) {
|
|
|
214
227
|
}
|
|
215
228
|
return commits;
|
|
216
229
|
}
|
|
230
|
+
function parseGraphLogOutput(output, remotes = []) {
|
|
231
|
+
const commits = [];
|
|
232
|
+
for (const line of output.split("\n")) {
|
|
233
|
+
if (line === "") continue;
|
|
234
|
+
const [hash, shortHash, subject, author, dateIso, parentField, decoField] = line.split(LOG_SEP);
|
|
235
|
+
if (hash === void 0 || hash === "") continue;
|
|
236
|
+
const parents = (parentField ?? "").split(" ").filter((p) => p !== "");
|
|
237
|
+
commits.push({
|
|
238
|
+
hash,
|
|
239
|
+
shortHash: shortHash ?? "",
|
|
240
|
+
subject: subject ?? "",
|
|
241
|
+
author: author ?? "",
|
|
242
|
+
dateIso: dateIso ?? "",
|
|
243
|
+
parents,
|
|
244
|
+
refs: parseDecorations(decoField ?? "", remotes)
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return commits;
|
|
248
|
+
}
|
|
249
|
+
function parseDecorations(decorations, remotes) {
|
|
250
|
+
const trimmed = decorations.trim();
|
|
251
|
+
if (trimmed === "") return [];
|
|
252
|
+
const refs = [];
|
|
253
|
+
for (const token of trimmed.split(", ")) {
|
|
254
|
+
if (token.startsWith("HEAD -> ")) {
|
|
255
|
+
refs.push({ kind: "branch", name: token.slice(8), head: true });
|
|
256
|
+
} else if (token.startsWith("tag: ")) {
|
|
257
|
+
refs.push({ kind: "tag", name: token.slice(5), head: false });
|
|
258
|
+
} else if (remotes.some((remote) => token === remote || token.startsWith(`${remote}/`))) {
|
|
259
|
+
refs.push({ kind: "remote", name: token, head: false });
|
|
260
|
+
} else {
|
|
261
|
+
refs.push({ kind: "branch", name: token, head: false });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return refs;
|
|
265
|
+
}
|
|
266
|
+
function parseShowMeta(output) {
|
|
267
|
+
const trimmed = output.trimEnd();
|
|
268
|
+
if (trimmed === "") return null;
|
|
269
|
+
const [hash, shortHash, subject, author, dateIso, ...bodyParts] = trimmed.split(LOG_SEP);
|
|
270
|
+
if (hash === void 0 || hash === "") return null;
|
|
271
|
+
return {
|
|
272
|
+
commit: {
|
|
273
|
+
hash,
|
|
274
|
+
shortHash: shortHash ?? "",
|
|
275
|
+
subject: subject ?? "",
|
|
276
|
+
author: author ?? "",
|
|
277
|
+
dateIso: dateIso ?? ""
|
|
278
|
+
},
|
|
279
|
+
body: bodyParts.join(LOG_SEP).trimEnd()
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function nameStatusCode(code) {
|
|
283
|
+
switch (code) {
|
|
284
|
+
case "A":
|
|
285
|
+
return "added";
|
|
286
|
+
case "D":
|
|
287
|
+
return "deleted";
|
|
288
|
+
case "R":
|
|
289
|
+
return "renamed";
|
|
290
|
+
case "T":
|
|
291
|
+
return "typechange";
|
|
292
|
+
case "U":
|
|
293
|
+
return "conflicted";
|
|
294
|
+
default:
|
|
295
|
+
return "modified";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function parseNameStatusOutput(output) {
|
|
299
|
+
const raw = output.split(NUL);
|
|
300
|
+
const segments = raw[raw.length - 1] === "" ? raw.slice(0, -1) : raw;
|
|
301
|
+
const rows = [];
|
|
302
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
303
|
+
const entry = segments[i] ?? "";
|
|
304
|
+
if (entry === "") continue;
|
|
305
|
+
const code = entry[0] ?? " ";
|
|
306
|
+
if (code === "R" || code === "C") {
|
|
307
|
+
rows.push({ path: segments[i + 2] ?? "", status: nameStatusCode(code) });
|
|
308
|
+
i += 2;
|
|
309
|
+
} else {
|
|
310
|
+
rows.push({ path: segments[i + 1] ?? "", status: nameStatusCode(code) });
|
|
311
|
+
i += 1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return rows;
|
|
315
|
+
}
|
|
217
316
|
function parseBranchOutput(output) {
|
|
218
317
|
const trimmed = output.trim();
|
|
219
318
|
return trimmed === "" ? null : trimmed;
|
|
@@ -296,7 +395,7 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
296
395
|
if ("failure" in headRun) return { ok: false, error: headRun.failure };
|
|
297
396
|
if (headRun.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
298
397
|
const head = headRun.run.exitCode === 0 ? headRun.run.stdout.trim() || null : null;
|
|
299
|
-
const status = await runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z", "--branch"], root, "status", deps.signal);
|
|
398
|
+
const status = await runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z", "--branch", "--untracked-files=all"], root, "status", deps.signal);
|
|
300
399
|
if ("failure" in status) return { ok: false, error: status.failure };
|
|
301
400
|
if (status.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
302
401
|
if (status.run.exitCode !== 0) {
|
|
@@ -334,15 +433,15 @@ import { resolve, sep } from "node:path";
|
|
|
334
433
|
function buildArgv(action, root) {
|
|
335
434
|
switch (action.kind) {
|
|
336
435
|
case "stage":
|
|
337
|
-
return withPaths(["git", "add", "--"], action.paths, root);
|
|
436
|
+
return withPaths([["git", "add", "--"]], action.paths, root);
|
|
338
437
|
case "stage-all":
|
|
339
438
|
return { argv: [["git", "add", "-A"]] };
|
|
340
439
|
case "unstage":
|
|
341
|
-
return withPaths(["git", "restore", "--staged", "--"], action.paths, root);
|
|
440
|
+
return withPaths([["git", "restore", "--staged", "--"]], action.paths, root);
|
|
342
441
|
case "unstage-all":
|
|
343
442
|
return { argv: [["git", "restore", "--staged", "--", "."]] };
|
|
344
443
|
case "discard":
|
|
345
|
-
return withPaths(["git", "restore", "--"], action.paths, root);
|
|
444
|
+
return withPaths([["git", "restore", "--"]], action.paths, root);
|
|
346
445
|
case "discard-all":
|
|
347
446
|
return { argv: [["git", "restore", "--staged", "--", "."], ["git", "restore", "--", "."]] };
|
|
348
447
|
case "commit": {
|
|
@@ -350,16 +449,30 @@ function buildArgv(action, root) {
|
|
|
350
449
|
if (action.paths === void 0 || action.paths.length === 0) {
|
|
351
450
|
return { argv: [["git", "commit", "-m", message]] };
|
|
352
451
|
}
|
|
353
|
-
return withPaths(["git", "commit", "-m", message, "--"], action.paths, root);
|
|
452
|
+
return withPaths([["git", "add", "--"], ["git", "commit", "-m", message, "--"]], action.paths, root);
|
|
354
453
|
}
|
|
454
|
+
case "branch-create": {
|
|
455
|
+
const from = action.from === void 0 || action.from === "" ? [] : [action.from];
|
|
456
|
+
return { argv: [["git", "branch", action.name, ...from]] };
|
|
457
|
+
}
|
|
458
|
+
case "branch-checkout":
|
|
459
|
+
return { argv: [["git", "checkout", action.name]] };
|
|
460
|
+
case "branch-delete":
|
|
461
|
+
return { argv: [["git", "branch", action.force === true ? "-D" : "-d", action.name]] };
|
|
462
|
+
case "fetch":
|
|
463
|
+
return { argv: [["git", "fetch", "--all", "--prune"]] };
|
|
355
464
|
}
|
|
356
465
|
}
|
|
357
|
-
function
|
|
466
|
+
function isValidBranchName(name) {
|
|
467
|
+
if (name === "" || name.startsWith("-") || name.includes("..") || name.endsWith("/") || name.includes("//")) return false;
|
|
468
|
+
return /^[A-Za-z0-9._/-]+$/.test(name);
|
|
469
|
+
}
|
|
470
|
+
function withPaths(prefixes, paths, root) {
|
|
358
471
|
if (paths.length === 0) return { error: "no paths given" };
|
|
359
472
|
for (const path of paths) {
|
|
360
473
|
if (!isSafePath(path, root)) return { error: `unsafe path: ${path}` };
|
|
361
474
|
}
|
|
362
|
-
return { argv: [
|
|
475
|
+
return { argv: prefixes.map((prefix) => [...prefix, ...paths]) };
|
|
363
476
|
}
|
|
364
477
|
function isSafePath(path, root) {
|
|
365
478
|
if (path === "") return false;
|
|
@@ -374,6 +487,12 @@ function operationError(failure) {
|
|
|
374
487
|
}
|
|
375
488
|
return { ok: false, error: failure };
|
|
376
489
|
}
|
|
490
|
+
function classifyOperationError(kind, message) {
|
|
491
|
+
if (kind === "branch-checkout" && /would be overwritten by checkout|将被 checkout 覆盖|有未跟踪工作区文件将会被 checkout 覆盖/i.test(message)) {
|
|
492
|
+
return "local-changes-block";
|
|
493
|
+
}
|
|
494
|
+
return "git-error";
|
|
495
|
+
}
|
|
377
496
|
async function runAction(deps, config, request) {
|
|
378
497
|
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
379
498
|
if (!workspace.ok) return operationError(workspace.error);
|
|
@@ -381,6 +500,13 @@ async function runAction(deps, config, request) {
|
|
|
381
500
|
if (request.action.kind === "commit" && request.action.message.trim() === "") {
|
|
382
501
|
return { ok: false, error: { code: "git-error", message: "commit message is empty" } };
|
|
383
502
|
}
|
|
503
|
+
const kind = request.action.kind;
|
|
504
|
+
if (kind === "branch-create" || kind === "branch-checkout" || kind === "branch-delete") {
|
|
505
|
+
const name = request.action.name;
|
|
506
|
+
if (!isValidBranchName(name)) {
|
|
507
|
+
return { ok: false, error: { code: "invalid-name", message: `invalid branch name: ${name}` } };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
384
510
|
const built = buildArgv(request.action, root);
|
|
385
511
|
if ("error" in built) return { ok: false, error: { code: "invalid-path", message: built.error } };
|
|
386
512
|
let lastStdout = "";
|
|
@@ -390,10 +516,11 @@ async function runAction(deps, config, request) {
|
|
|
390
516
|
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
391
517
|
if (outcome.run.exitCode !== 0) {
|
|
392
518
|
const message = outcome.run.stderr.trim() || outcome.run.stdout.trim();
|
|
519
|
+
const code = classifyOperationError(request.action.kind, message);
|
|
393
520
|
return {
|
|
394
521
|
ok: false,
|
|
395
522
|
error: {
|
|
396
|
-
code
|
|
523
|
+
code,
|
|
397
524
|
message: message !== "" ? message : `git ${request.action.kind} exited ${String(outcome.run.exitCode)}`
|
|
398
525
|
}
|
|
399
526
|
};
|
|
@@ -405,72 +532,311 @@ async function runAction(deps, config, request) {
|
|
|
405
532
|
return { ok: true, snapshot: snapshot.value, ...lastStdout === "" ? {} : { output: lastStdout } };
|
|
406
533
|
}
|
|
407
534
|
|
|
535
|
+
// src/host/queries.ts
|
|
536
|
+
var LOG_FORMAT = "%H%x1f%h%x1f%s%x1f%an%x1f%aI";
|
|
537
|
+
var GRAPH_FORMAT = "%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%P%x1f%D";
|
|
538
|
+
var MAX_HISTORY_LIMIT = 1e3;
|
|
539
|
+
function isValidRef(ref) {
|
|
540
|
+
return ref !== "" && !/\s/.test(ref);
|
|
541
|
+
}
|
|
542
|
+
async function runQuery(deps, config, request) {
|
|
543
|
+
void config;
|
|
544
|
+
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
545
|
+
if (!workspace.ok) return { ok: false, error: operationError(workspace.error).error };
|
|
546
|
+
const root = workspace.root;
|
|
547
|
+
const query = request.query;
|
|
548
|
+
switch (query.kind) {
|
|
549
|
+
case "history":
|
|
550
|
+
return historyQuery(deps, root, query);
|
|
551
|
+
case "diff":
|
|
552
|
+
return diffQuery(deps, root, query.path, query.base);
|
|
553
|
+
case "show":
|
|
554
|
+
return showQuery(deps, root, query.ref);
|
|
555
|
+
case "branches":
|
|
556
|
+
return branchesQuery(deps, root);
|
|
557
|
+
case "tags":
|
|
558
|
+
return tagsQuery(deps, root);
|
|
559
|
+
case "authors":
|
|
560
|
+
return authorsQuery(deps, root);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function historyQuery(deps, root, query) {
|
|
564
|
+
const safeLimit = Math.min(Math.max(Math.floor(query.limit), 0), MAX_HISTORY_LIMIT);
|
|
565
|
+
const safeSkip = Math.max(Math.floor(query.skip), 0);
|
|
566
|
+
if (query.ref !== void 0 && !isValidRef(query.ref)) {
|
|
567
|
+
return { ok: false, error: { code: "invalid-name", message: `invalid ref: ${query.ref}` } };
|
|
568
|
+
}
|
|
569
|
+
const search = query.search?.trim() ?? "";
|
|
570
|
+
const hexLike = /^[0-9a-f]{7,40}$/i.test(search);
|
|
571
|
+
const scope = hexLike ? [] : query.ref === void 0 ? ["--all"] : [query.ref];
|
|
572
|
+
const noWalk = hexLike ? ["--no-walk", search] : [];
|
|
573
|
+
const filters = [];
|
|
574
|
+
if (search !== "" && !hexLike) filters.push("--regexp-ignore-case", "--extended-regexp", `--grep=${search}`);
|
|
575
|
+
const author = query.author?.trim() ?? "";
|
|
576
|
+
if (author !== "") filters.push(`--author=${author}`);
|
|
577
|
+
const since = query.since?.trim() ?? "";
|
|
578
|
+
if (since !== "") filters.push(`--since=${since}`);
|
|
579
|
+
const log = await runCommand(
|
|
580
|
+
deps.run,
|
|
581
|
+
// -n/--skip 前置:git 的 `-n N` 出现在 `--no-walk` 之后会重置 no-walk
|
|
582
|
+
// (hexLike 会错误列出全部祖先),前置则 `-n 1000 --no-walk x` 恒返回单条。
|
|
583
|
+
["git", "log", ...filters, `--skip=${String(safeSkip)}`, "-n", String(safeLimit), ...noWalk, ...scope, `--format=${GRAPH_FORMAT}`],
|
|
584
|
+
root,
|
|
585
|
+
"log",
|
|
586
|
+
deps.signal
|
|
587
|
+
);
|
|
588
|
+
if ("failure" in log) return { ok: false, error: operationError(log.failure).error };
|
|
589
|
+
if (log.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
590
|
+
if (log.run.exitCode !== 0) {
|
|
591
|
+
if (log.run.stderr.includes("does not have any commits")) {
|
|
592
|
+
return { ok: true, value: { kind: "history", commits: [], total: 0 } };
|
|
593
|
+
}
|
|
594
|
+
if (hexLike && /unknown revision|bad revision|ambiguous/i.test(log.run.stderr)) {
|
|
595
|
+
return { ok: true, value: { kind: "history", commits: [], total: 0 } };
|
|
596
|
+
}
|
|
597
|
+
return gitError("log", log.run.stderr, log.run.stdout);
|
|
598
|
+
}
|
|
599
|
+
let total = 0;
|
|
600
|
+
const count = await runCommand(deps.run, ["git", "rev-list", "--count", ...noWalk, ...scope, ...filters], root, "rev-list", deps.signal);
|
|
601
|
+
if ("run" in count && count.run.exitCode === 0) {
|
|
602
|
+
const parsed = Number(count.run.stdout.trim());
|
|
603
|
+
if (Number.isFinite(parsed) && parsed >= 0) total = parsed;
|
|
604
|
+
}
|
|
605
|
+
let remotes = [];
|
|
606
|
+
const remoteRun = await runCommand(deps.run, ["git", "remote"], root, "remote", deps.signal);
|
|
607
|
+
if ("run" in remoteRun && remoteRun.run.exitCode === 0) {
|
|
608
|
+
remotes = remoteRun.run.stdout.split("\n").map((s) => s.trim()).filter((s) => s !== "");
|
|
609
|
+
}
|
|
610
|
+
return { ok: true, value: { kind: "history", commits: parseGraphLogOutput(log.run.stdout, remotes), total } };
|
|
611
|
+
}
|
|
612
|
+
async function diffQuery(deps, root, path, base) {
|
|
613
|
+
if (!isSafePath(path, root)) return { ok: false, error: { code: "invalid-path", message: `unsafe path: ${path}` } };
|
|
614
|
+
const argv = base === "staged" ? ["git", "diff", "--cached", "-U999999", "--", path] : ["git", "diff", "-U999999", "--", path];
|
|
615
|
+
const run = await runCommand(deps.run, argv, root, "diff", deps.signal);
|
|
616
|
+
if ("failure" in run) return { ok: false, error: operationError(run.failure).error };
|
|
617
|
+
if (run.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
618
|
+
if (run.run.exitCode !== 0) return gitError("diff", run.run.stderr, run.run.stdout);
|
|
619
|
+
if (run.run.stdout !== "" || base === "staged") {
|
|
620
|
+
return { ok: true, value: { kind: "diff", path, text: run.run.stdout } };
|
|
621
|
+
}
|
|
622
|
+
const ni = await runCommand(deps.run, ["git", "diff", "--no-index", "-U999999", "--", "/dev/null", path], root, "diff --no-index", deps.signal);
|
|
623
|
+
if ("failure" in ni) return { ok: false, error: operationError(ni.failure).error };
|
|
624
|
+
if (ni.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
625
|
+
if (ni.run.exitCode !== 0 && ni.run.exitCode !== 1) return gitError("diff", ni.run.stderr, ni.run.stdout);
|
|
626
|
+
return { ok: true, value: { kind: "diff", path, text: ni.run.stdout } };
|
|
627
|
+
}
|
|
628
|
+
async function showQuery(deps, root, ref) {
|
|
629
|
+
if (!isValidRef(ref)) return { ok: false, error: { code: "invalid-name", message: `invalid ref: ${ref}` } };
|
|
630
|
+
const meta = await runCommand(
|
|
631
|
+
deps.run,
|
|
632
|
+
["git", "show", "-s", `--format=${LOG_FORMAT}%x1f%b`, ref],
|
|
633
|
+
root,
|
|
634
|
+
"show",
|
|
635
|
+
deps.signal
|
|
636
|
+
);
|
|
637
|
+
if ("failure" in meta) return { ok: false, error: operationError(meta.failure).error };
|
|
638
|
+
if (meta.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
639
|
+
if (meta.run.exitCode !== 0) return gitError("show", meta.run.stderr, meta.run.stdout);
|
|
640
|
+
const stat2 = await runCommand(
|
|
641
|
+
deps.run,
|
|
642
|
+
["git", "-c", "core.quotePath=false", "show", "--format=", "--name-status", "-z", ref],
|
|
643
|
+
root,
|
|
644
|
+
"show --name-status",
|
|
645
|
+
deps.signal
|
|
646
|
+
);
|
|
647
|
+
if ("failure" in stat2) return { ok: false, error: operationError(stat2.failure).error };
|
|
648
|
+
if (stat2.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
649
|
+
if (stat2.run.exitCode !== 0) return gitError("show", stat2.run.stderr, stat2.run.stdout);
|
|
650
|
+
const parsed = parseShowMeta(meta.run.stdout);
|
|
651
|
+
return {
|
|
652
|
+
ok: true,
|
|
653
|
+
value: {
|
|
654
|
+
kind: "show",
|
|
655
|
+
ref,
|
|
656
|
+
commit: parsed?.commit ?? null,
|
|
657
|
+
body: parsed?.body ?? "",
|
|
658
|
+
stats: parseNameStatusOutput(stat2.run.stdout)
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async function authorsQuery(deps, root) {
|
|
663
|
+
const run = await runCommand(deps.run, ["git", "log", "--all", "-n", "1000", "--format=%an"], root, "log authors", deps.signal);
|
|
664
|
+
if ("failure" in run) return { ok: false, error: operationError(run.failure).error };
|
|
665
|
+
if (run.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
666
|
+
if (run.run.exitCode !== 0) return { ok: true, value: { kind: "authors", authors: [] } };
|
|
667
|
+
const authors = [...new Set(run.run.stdout.split("\n").map((s) => s.trim()).filter((s) => s !== ""))].sort().slice(0, 100);
|
|
668
|
+
return { ok: true, value: { kind: "authors", authors } };
|
|
669
|
+
}
|
|
670
|
+
async function tagsQuery(deps, root) {
|
|
671
|
+
const FORMAT = "--format=%(refname:short)%09%(objectname:short)";
|
|
672
|
+
const run = await runCommand(deps.run, ["git", "tag", FORMAT], root, "tag", deps.signal);
|
|
673
|
+
if ("failure" in run) return { ok: false, error: operationError(run.failure).error };
|
|
674
|
+
if (run.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
675
|
+
if (run.run.exitCode !== 0) return gitError("tag", run.run.stderr, run.run.stdout);
|
|
676
|
+
return { ok: true, value: { kind: "tags", tags: parseBranchList(run.run.stdout) } };
|
|
677
|
+
}
|
|
678
|
+
async function branchesQuery(deps, root) {
|
|
679
|
+
const LOCAL_FORMAT = "--format=%(refname:short)%09%(objectname:short)%09%(upstream:short)%09%(upstream:track)";
|
|
680
|
+
const REMOTE_FORMAT = "--format=%(refname:short)%09%(objectname:short)";
|
|
681
|
+
const local = await runCommand(deps.run, ["git", "branch", LOCAL_FORMAT], root, "branch", deps.signal);
|
|
682
|
+
if ("failure" in local) return { ok: false, error: operationError(local.failure).error };
|
|
683
|
+
if (local.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
684
|
+
if (local.run.exitCode !== 0) return gitError("branch", local.run.stderr, local.run.stdout);
|
|
685
|
+
const remote = await runCommand(deps.run, ["git", "branch", "-r", REMOTE_FORMAT], root, "branch -r", deps.signal);
|
|
686
|
+
if ("failure" in remote) return { ok: false, error: operationError(remote.failure).error };
|
|
687
|
+
if (remote.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
688
|
+
if (remote.run.exitCode !== 0) return gitError("branch -r", remote.run.stderr, remote.run.stdout);
|
|
689
|
+
const current = await runCommand(deps.run, ["git", "branch", "--show-current"], root, "branch", deps.signal);
|
|
690
|
+
const currentName = "run" in current && current.run.exitCode === 0 ? parseBranchOutput(current.run.stdout) : null;
|
|
691
|
+
let defaultBranch = null;
|
|
692
|
+
const def = await runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], root, "symbolic-ref", deps.signal);
|
|
693
|
+
if ("run" in def && def.run.exitCode === 0) {
|
|
694
|
+
const value = def.run.stdout.trim();
|
|
695
|
+
const slash = value.indexOf("/");
|
|
696
|
+
defaultBranch = value === "" ? null : slash === -1 ? value : value.slice(slash + 1);
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
ok: true,
|
|
700
|
+
value: {
|
|
701
|
+
kind: "branches",
|
|
702
|
+
current: currentName,
|
|
703
|
+
defaultBranch,
|
|
704
|
+
local: parseBranchList(local.run.stdout),
|
|
705
|
+
remote: parseBranchList(remote.run.stdout).filter((branch) => !branch.name.endsWith("/HEAD"))
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function parseBranchList(output) {
|
|
710
|
+
const branches = [];
|
|
711
|
+
for (const line of output.split("\n")) {
|
|
712
|
+
if (line === "") continue;
|
|
713
|
+
const parts = line.split(" ");
|
|
714
|
+
const name = parts[0];
|
|
715
|
+
const hash = parts[1];
|
|
716
|
+
if (name === void 0 || name === "") continue;
|
|
717
|
+
const track = parts[3] ?? "";
|
|
718
|
+
const aheadMatch = /ahead (\d+)/.exec(track);
|
|
719
|
+
const behindMatch = /behind (\d+)/.exec(track);
|
|
720
|
+
const ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
|
|
721
|
+
const behind = behindMatch ? Number(behindMatch[1]) : 0;
|
|
722
|
+
branches.push({
|
|
723
|
+
name,
|
|
724
|
+
shortHash: hash === void 0 || hash === "" ? null : hash,
|
|
725
|
+
...ahead > 0 ? { ahead } : {},
|
|
726
|
+
...behind > 0 ? { behind } : {}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
return branches;
|
|
730
|
+
}
|
|
731
|
+
function gitError(label, stderr, stdout) {
|
|
732
|
+
const message = stderr.trim() || stdout.trim();
|
|
733
|
+
return {
|
|
734
|
+
ok: false,
|
|
735
|
+
error: {
|
|
736
|
+
code: "git-error",
|
|
737
|
+
message: message !== "" ? message : `git ${label} failed`
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/contracts/host-endpoints.ts
|
|
743
|
+
function createHostEndpoints(deps, config) {
|
|
744
|
+
return {
|
|
745
|
+
snapshot(request, signal) {
|
|
746
|
+
const merged = mergeSignals(deps.signal, signal);
|
|
747
|
+
const effectiveDeps = merged === deps.signal ? deps : { ...deps, signal: merged };
|
|
748
|
+
return snapshotForSession(effectiveDeps, config, request.sessionId);
|
|
749
|
+
},
|
|
750
|
+
run(request, signal) {
|
|
751
|
+
const merged = mergeSignals(deps.signal, signal);
|
|
752
|
+
const effectiveDeps = merged === deps.signal ? deps : { ...deps, signal: merged };
|
|
753
|
+
return runAction(effectiveDeps, config, request);
|
|
754
|
+
},
|
|
755
|
+
query(request, signal) {
|
|
756
|
+
const merged = mergeSignals(deps.signal, signal);
|
|
757
|
+
const effectiveDeps = merged === deps.signal ? deps : { ...deps, signal: merged };
|
|
758
|
+
return runQuery(effectiveDeps, config, request);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function mergeSignals(a, b) {
|
|
763
|
+
if (a === void 0) return b;
|
|
764
|
+
if (b === void 0) return a;
|
|
765
|
+
return AbortSignal.any([a, b]);
|
|
766
|
+
}
|
|
767
|
+
|
|
408
768
|
// src/host/index.ts
|
|
409
|
-
var _run_dec, _snapshot_dec, _a, _init;
|
|
410
|
-
var GitStatusService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _a) {
|
|
769
|
+
var _query_dec, _run_dec, _snapshot_dec, _a, _init;
|
|
770
|
+
var GitStatusService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _a) {
|
|
411
771
|
constructor(ctx, config) {
|
|
412
772
|
super(ctx, "gitInfo");
|
|
413
773
|
__runInitializers(_init, 5, this);
|
|
414
|
-
__publicField(this, "
|
|
415
|
-
|
|
774
|
+
__publicField(this, "endpoints");
|
|
775
|
+
const normalizedConfig = normalizeConfig(config);
|
|
776
|
+
const deps = this.buildDeps(ctx, normalizedConfig);
|
|
777
|
+
this.endpoints = createHostEndpoints(deps, normalizedConfig);
|
|
416
778
|
}
|
|
417
|
-
/**
|
|
418
|
-
|
|
419
|
-
const subprocess =
|
|
779
|
+
/** 将 Cordis 服务适配为结构化 SnapshotDeps。 */
|
|
780
|
+
buildDeps(ctx, config) {
|
|
781
|
+
const subprocess = ctx.get("subprocess");
|
|
420
782
|
if (subprocess === void 0) {
|
|
421
|
-
return {
|
|
783
|
+
return {
|
|
784
|
+
run: { run: async () => {
|
|
785
|
+
throw new Error("subprocess service unavailable");
|
|
786
|
+
} },
|
|
787
|
+
fs: { realpath, stat },
|
|
788
|
+
sessions: { liveCwd: () => void 0, persistedMeta: async () => void 0 }
|
|
789
|
+
};
|
|
422
790
|
}
|
|
423
|
-
const sessions =
|
|
424
|
-
const persistence =
|
|
425
|
-
const runner = createGitRunner(subprocess, this.config.timeoutMs, this.config.maxStatusBytes);
|
|
791
|
+
const sessions = ctx.get("sessions");
|
|
792
|
+
const persistence = ctx.get("sessionPersistence");
|
|
426
793
|
return {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
return void 0;
|
|
439
|
-
}
|
|
794
|
+
run: createGitRunner(subprocess, config.timeoutMs, config.maxStatusBytes),
|
|
795
|
+
fs: { realpath, stat },
|
|
796
|
+
sessions: {
|
|
797
|
+
liveCwd: (id) => sessions?.get(id)?.header?.cwd,
|
|
798
|
+
persistedMeta: async (id) => {
|
|
799
|
+
if (persistence === void 0) return void 0;
|
|
800
|
+
try {
|
|
801
|
+
const inspection = await persistence.inspect(id);
|
|
802
|
+
return { cwd: inspection.meta.cwd };
|
|
803
|
+
} catch {
|
|
804
|
+
return void 0;
|
|
440
805
|
}
|
|
441
|
-
}
|
|
442
|
-
signal
|
|
806
|
+
}
|
|
443
807
|
}
|
|
444
808
|
};
|
|
445
809
|
}
|
|
446
810
|
async snapshot(request, signal) {
|
|
447
|
-
|
|
448
|
-
if ("failure" in adapted) return { ok: false, error: adapted.failure };
|
|
449
|
-
return snapshotForSession(adapted.deps, this.config, request.sessionId);
|
|
811
|
+
return this.endpoints.snapshot(request, signal);
|
|
450
812
|
}
|
|
451
813
|
async run(request, signal) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return runAction(adapted.deps, this.config, request);
|
|
814
|
+
return this.endpoints.run(request, signal);
|
|
815
|
+
}
|
|
816
|
+
async query(request, signal) {
|
|
817
|
+
return this.endpoints.query(request, signal);
|
|
457
818
|
}
|
|
458
819
|
};
|
|
459
820
|
_init = __decoratorStart(_a);
|
|
460
821
|
__decorateElement(_init, 1, "snapshot", _snapshot_dec, GitStatusService);
|
|
461
822
|
__decorateElement(_init, 1, "run", _run_dec, GitStatusService);
|
|
823
|
+
__decorateElement(_init, 1, "query", _query_dec, GitStatusService);
|
|
462
824
|
__decoratorMetadata(_init, GitStatusService);
|
|
463
825
|
__publicField(GitStatusService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
|
|
464
826
|
var index_default = GitStatusService;
|
|
465
827
|
export {
|
|
466
828
|
DEFAULT_CONFIG,
|
|
467
829
|
GitStatusService,
|
|
830
|
+
createHostEndpoints,
|
|
468
831
|
index_default as default,
|
|
469
832
|
isSafePath,
|
|
833
|
+
isValidBranchName,
|
|
470
834
|
normalizeConfig,
|
|
471
835
|
parseBranchOutput,
|
|
472
836
|
parseLogOutput,
|
|
837
|
+
parseNameStatusOutput,
|
|
473
838
|
parseStatusOutput,
|
|
474
|
-
runAction
|
|
839
|
+
runAction,
|
|
840
|
+
runQuery
|
|
475
841
|
};
|
|
476
842
|
//# sourceMappingURL=index.js.map
|