dsh-git-ui 0.0.1 → 0.1.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 +41 -16
- package/README.zh.md +42 -17
- package/cordis.patch.yml +1 -1
- package/lib/client.js +36 -35
- package/lib/client.js.map +4 -4
- package/lib/host/actions.d.ts +34 -0
- package/lib/host/core.d.ts +31 -1
- package/lib/host/index.d.ts +15 -8
- package/lib/host/index.js +464 -23
- 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 +174 -0
- package/package.json +1 -1
- package/src/client/GitCenter.tsx +1524 -0
- package/src/client/GitPill.tsx +303 -54
- package/src/client/changes-diff.ts +63 -0
- package/src/client/controller.ts +83 -1
- 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 +4 -2
- package/src/client/locales.ts +162 -0
- package/src/client/popup-close.ts +19 -0
- package/src/client/remote.ts +140 -0
- package/src/client/select-menu.tsx +113 -0
- package/src/client/side-by-side.ts +150 -0
- package/src/client/styles.ts +1488 -46
- package/src/client/time-format.ts +32 -0
- package/src/host/actions.ts +178 -0
- package/src/host/core.ts +39 -13
- package/src/host/index.ts +45 -17
- package/src/host/parser.ts +155 -12
- package/src/host/queries.ts +289 -0
- package/src/host/types.ts +143 -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;
|
|
@@ -257,7 +356,7 @@ async function runCommand(runner, argv, cwd, label, signal) {
|
|
|
257
356
|
return { failure: { code: "git-unavailable", detail: `${label}: ${error instanceof Error ? error.message : String(error)}` } };
|
|
258
357
|
}
|
|
259
358
|
}
|
|
260
|
-
async function
|
|
359
|
+
async function resolveWorkspace(deps, sessionId) {
|
|
261
360
|
const resolved = await resolveCwd(deps.sessions, sessionId);
|
|
262
361
|
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
263
362
|
let realCwd;
|
|
@@ -282,6 +381,12 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
282
381
|
}
|
|
283
382
|
const root = toplevel.run.stdout.trim();
|
|
284
383
|
if (root === "") return { ok: false, error: { code: "not-a-git-repo" } };
|
|
384
|
+
return { ok: true, cwd: realCwd, root };
|
|
385
|
+
}
|
|
386
|
+
async function snapshotForSession(deps, config, sessionId) {
|
|
387
|
+
const workspace = await resolveWorkspace(deps, sessionId);
|
|
388
|
+
if (!workspace.ok) return { ok: false, error: workspace.error };
|
|
389
|
+
const root = workspace.root;
|
|
285
390
|
const branchRun = await runCommand(deps.run, ["git", "branch", "--show-current"], root, "branch", deps.signal);
|
|
286
391
|
if ("failure" in branchRun) return { ok: false, error: branchRun.failure };
|
|
287
392
|
if (branchRun.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
@@ -290,7 +395,7 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
290
395
|
if ("failure" in headRun) return { ok: false, error: headRun.failure };
|
|
291
396
|
if (headRun.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
292
397
|
const head = headRun.run.exitCode === 0 ? headRun.run.stdout.trim() || null : null;
|
|
293
|
-
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);
|
|
294
399
|
if ("failure" in status) return { ok: false, error: status.failure };
|
|
295
400
|
if (status.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
296
401
|
if (status.run.exitCode !== 0) {
|
|
@@ -323,25 +428,337 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
323
428
|
return { ok: true, value: snapshot };
|
|
324
429
|
}
|
|
325
430
|
|
|
431
|
+
// src/host/actions.ts
|
|
432
|
+
import { resolve, sep } from "node:path";
|
|
433
|
+
function buildArgv(action, root) {
|
|
434
|
+
switch (action.kind) {
|
|
435
|
+
case "stage":
|
|
436
|
+
return withPaths([["git", "add", "--"]], action.paths, root);
|
|
437
|
+
case "stage-all":
|
|
438
|
+
return { argv: [["git", "add", "-A"]] };
|
|
439
|
+
case "unstage":
|
|
440
|
+
return withPaths([["git", "restore", "--staged", "--"]], action.paths, root);
|
|
441
|
+
case "unstage-all":
|
|
442
|
+
return { argv: [["git", "restore", "--staged", "--", "."]] };
|
|
443
|
+
case "discard":
|
|
444
|
+
return withPaths([["git", "restore", "--"]], action.paths, root);
|
|
445
|
+
case "discard-all":
|
|
446
|
+
return { argv: [["git", "restore", "--staged", "--", "."], ["git", "restore", "--", "."]] };
|
|
447
|
+
case "commit": {
|
|
448
|
+
const message = action.message.trim();
|
|
449
|
+
if (action.paths === void 0 || action.paths.length === 0) {
|
|
450
|
+
return { argv: [["git", "commit", "-m", message]] };
|
|
451
|
+
}
|
|
452
|
+
return withPaths([["git", "add", "--"], ["git", "commit", "-m", message, "--"]], action.paths, root);
|
|
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"]] };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
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) {
|
|
471
|
+
if (paths.length === 0) return { error: "no paths given" };
|
|
472
|
+
for (const path of paths) {
|
|
473
|
+
if (!isSafePath(path, root)) return { error: `unsafe path: ${path}` };
|
|
474
|
+
}
|
|
475
|
+
return { argv: prefixes.map((prefix) => [...prefix, ...paths]) };
|
|
476
|
+
}
|
|
477
|
+
function isSafePath(path, root) {
|
|
478
|
+
if (path === "") return false;
|
|
479
|
+
if (path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:/.test(path)) return false;
|
|
480
|
+
const resolved = resolve(root, path);
|
|
481
|
+
const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
482
|
+
return resolved === root || resolved.startsWith(prefix);
|
|
483
|
+
}
|
|
484
|
+
function operationError(failure) {
|
|
485
|
+
if (failure.code === "git-unavailable") {
|
|
486
|
+
return { ok: false, error: { code: "git-error", message: failure.detail } };
|
|
487
|
+
}
|
|
488
|
+
return { ok: false, error: failure };
|
|
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
|
+
}
|
|
496
|
+
async function runAction(deps, config, request) {
|
|
497
|
+
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
498
|
+
if (!workspace.ok) return operationError(workspace.error);
|
|
499
|
+
const root = workspace.root;
|
|
500
|
+
if (request.action.kind === "commit" && request.action.message.trim() === "") {
|
|
501
|
+
return { ok: false, error: { code: "git-error", message: "commit message is empty" } };
|
|
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
|
+
}
|
|
510
|
+
const built = buildArgv(request.action, root);
|
|
511
|
+
if ("error" in built) return { ok: false, error: { code: "invalid-path", message: built.error } };
|
|
512
|
+
let lastStdout = "";
|
|
513
|
+
for (const argv of built.argv) {
|
|
514
|
+
const outcome = await runCommand(deps.run, argv, root, `action ${request.action.kind}`, deps.signal);
|
|
515
|
+
if ("failure" in outcome) return operationError(outcome.failure);
|
|
516
|
+
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
517
|
+
if (outcome.run.exitCode !== 0) {
|
|
518
|
+
const message = outcome.run.stderr.trim() || outcome.run.stdout.trim();
|
|
519
|
+
const code = classifyOperationError(request.action.kind, message);
|
|
520
|
+
return {
|
|
521
|
+
ok: false,
|
|
522
|
+
error: {
|
|
523
|
+
code,
|
|
524
|
+
message: message !== "" ? message : `git ${request.action.kind} exited ${String(outcome.run.exitCode)}`
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
lastStdout = outcome.run.stdout.trim();
|
|
529
|
+
}
|
|
530
|
+
const snapshot = await snapshotForSession(deps, config, request.sessionId);
|
|
531
|
+
if (!snapshot.ok) return operationError(snapshot.error);
|
|
532
|
+
return { ok: true, snapshot: snapshot.value, ...lastStdout === "" ? {} : { output: lastStdout } };
|
|
533
|
+
}
|
|
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
|
+
|
|
326
742
|
// src/host/index.ts
|
|
327
|
-
var _snapshot_dec, _a, _init;
|
|
328
|
-
var GitStatusService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _a) {
|
|
743
|
+
var _query_dec, _run_dec, _snapshot_dec, _a, _init;
|
|
744
|
+
var GitStatusService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _a) {
|
|
329
745
|
constructor(ctx, config) {
|
|
330
746
|
super(ctx, "gitInfo");
|
|
331
747
|
__runInitializers(_init, 5, this);
|
|
332
748
|
__publicField(this, "config");
|
|
333
749
|
this.config = normalizeConfig(config);
|
|
334
750
|
}
|
|
335
|
-
|
|
751
|
+
/** Adapter face shared by both endpoints (injected services + runner). */
|
|
752
|
+
deps(signal) {
|
|
336
753
|
const subprocess = this.ctx.get("subprocess");
|
|
337
754
|
if (subprocess === void 0) {
|
|
338
|
-
return {
|
|
755
|
+
return { failure: { code: "git-unavailable", detail: "subprocess service unavailable" } };
|
|
339
756
|
}
|
|
340
757
|
const sessions = this.ctx.get("sessions");
|
|
341
758
|
const persistence = this.ctx.get("sessionPersistence");
|
|
342
759
|
const runner = createGitRunner(subprocess, this.config.timeoutMs, this.config.maxStatusBytes);
|
|
343
|
-
return
|
|
344
|
-
{
|
|
760
|
+
return {
|
|
761
|
+
deps: {
|
|
345
762
|
run: runner,
|
|
346
763
|
fs: { realpath, stat },
|
|
347
764
|
sessions: {
|
|
@@ -357,14 +774,33 @@ var GitStatusService = class extends (_a = TypertRemoteService, _snapshot_dec =
|
|
|
357
774
|
}
|
|
358
775
|
},
|
|
359
776
|
signal
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
async snapshot(request, signal) {
|
|
781
|
+
const adapted = this.deps(signal);
|
|
782
|
+
if ("failure" in adapted) return { ok: false, error: adapted.failure };
|
|
783
|
+
return snapshotForSession(adapted.deps, this.config, request.sessionId);
|
|
784
|
+
}
|
|
785
|
+
async run(request, signal) {
|
|
786
|
+
const adapted = this.deps(signal);
|
|
787
|
+
if ("failure" in adapted) {
|
|
788
|
+
return { ok: false, error: { code: "git-error", message: adapted.failure.detail } };
|
|
789
|
+
}
|
|
790
|
+
return runAction(adapted.deps, this.config, request);
|
|
791
|
+
}
|
|
792
|
+
async query(request, signal) {
|
|
793
|
+
const adapted = this.deps(signal);
|
|
794
|
+
if ("failure" in adapted) {
|
|
795
|
+
return { ok: false, error: { code: "git-error", message: adapted.failure.detail } };
|
|
796
|
+
}
|
|
797
|
+
return runQuery(adapted.deps, this.config, request);
|
|
364
798
|
}
|
|
365
799
|
};
|
|
366
800
|
_init = __decoratorStart(_a);
|
|
367
801
|
__decorateElement(_init, 1, "snapshot", _snapshot_dec, GitStatusService);
|
|
802
|
+
__decorateElement(_init, 1, "run", _run_dec, GitStatusService);
|
|
803
|
+
__decorateElement(_init, 1, "query", _query_dec, GitStatusService);
|
|
368
804
|
__decoratorMetadata(_init, GitStatusService);
|
|
369
805
|
__publicField(GitStatusService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
|
|
370
806
|
var index_default = GitStatusService;
|
|
@@ -372,9 +808,14 @@ export {
|
|
|
372
808
|
DEFAULT_CONFIG,
|
|
373
809
|
GitStatusService,
|
|
374
810
|
index_default as default,
|
|
811
|
+
isSafePath,
|
|
812
|
+
isValidBranchName,
|
|
375
813
|
normalizeConfig,
|
|
376
814
|
parseBranchOutput,
|
|
377
815
|
parseLogOutput,
|
|
378
|
-
|
|
816
|
+
parseNameStatusOutput,
|
|
817
|
+
parseStatusOutput,
|
|
818
|
+
runAction,
|
|
819
|
+
runQuery
|
|
379
820
|
};
|
|
380
821
|
//# sourceMappingURL=index.js.map
|