@xbzbing/dsh-git-panel 1.3.0 → 1.4.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 +5 -1
- package/README_EN.md +5 -1
- package/lib/client.js +70 -10
- package/lib/host/actions.d.ts +19 -0
- package/lib/host/core.d.ts +5 -1
- package/lib/host/index.d.ts +5 -3
- package/lib/host/index.js +145 -18
- package/lib/host/parser.d.ts +9 -1
- package/lib/host/types.d.ts +65 -1
- package/package.json +1 -1
package/lib/host/actions.d.ts
CHANGED
|
@@ -11,5 +11,24 @@ type PlanResult = CommandPlan | {
|
|
|
11
11
|
};
|
|
12
12
|
/** Build the git command sequence for an action. */
|
|
13
13
|
export declare function planAction(action: GitAction, unborn: boolean): PlanResult;
|
|
14
|
+
/**
|
|
15
|
+
* Classify a non-zero git exit into a wire error code + message. Order is
|
|
16
|
+
* semantic and must not be reshuffled:
|
|
17
|
+
* - conflict first — a stash apply/pop conflict prints both "CONFLICT …" and
|
|
18
|
+
* "no changes added to commit", so it must win over the nothing-to-commit
|
|
19
|
+
* rule below (git keeps the stash entry on conflict → recoverable).
|
|
20
|
+
* - index.lock busy (another process holds the lock).
|
|
21
|
+
* - nothing-to-commit / no changes added (a real commit no-op).
|
|
22
|
+
* - not-found (missing ref / tag / stash).
|
|
23
|
+
* - local-changes-block (a dirty-worktree refusal; neutral wording covers both
|
|
24
|
+
* `git checkout` and `git stash apply`).
|
|
25
|
+
* - revert-merge (reverting a merge commit without a mainline parent — git
|
|
26
|
+
* refuses before touching the work tree, so it is a plain refusal).
|
|
27
|
+
* - git-error fallback (surface the repo's own stderr).
|
|
28
|
+
*/
|
|
29
|
+
export declare function classifyActionFailure(stdout: string, stderr: string, exitCode: number): {
|
|
30
|
+
code: GitErrorCode;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
14
33
|
/** Execute a management action, returning the fresh snapshot on success. */
|
|
15
34
|
export declare function runAction(deps: SnapshotDeps, config: GitPanelConfig, request: GitActionRequest): Promise<GitActionResult>;
|
package/lib/host/core.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GitRunner } from './git.ts';
|
|
2
2
|
import type { AgentDefaultModelFace, LlmFace } from './llm-face.ts';
|
|
3
|
-
import type { DiffViewMode, GitChange, GitErrorCode, GitSnapshotResult } from './types.ts';
|
|
3
|
+
import type { DiffViewMode, GraphStyle, GitChange, GitErrorCode, GitSnapshotResult } from './types.ts';
|
|
4
4
|
export interface GitPanelConfig {
|
|
5
5
|
readonly timeoutMs: number;
|
|
6
6
|
/** Per-command stdout cap; also the per-side image-diff payload cap. */
|
|
@@ -11,6 +11,8 @@ export interface GitPanelConfig {
|
|
|
11
11
|
readonly showInputPill: boolean;
|
|
12
12
|
/** Default diff layout the views open with (user can switch per-diff). */
|
|
13
13
|
readonly defaultDiffView: DiffViewMode;
|
|
14
|
+
/** Commit-graph line style (parallel = VSCode-like, compact = git log). */
|
|
15
|
+
readonly graphStyle: GraphStyle;
|
|
14
16
|
/** Whether the "AI suggest" button shows in the commit box. */
|
|
15
17
|
readonly suggestEnabled: boolean;
|
|
16
18
|
/** Max diff bytes sent to the model; beyond this the diff is truncated. */
|
|
@@ -25,6 +27,8 @@ export declare const DEFAULT_CONFIG: GitPanelConfig;
|
|
|
25
27
|
export declare function normalizeConfig(raw: unknown): GitPanelConfig;
|
|
26
28
|
/** Read the default-diff-view field, unwrapping a schemastery volatile ref. */
|
|
27
29
|
export declare function readDiffView(value: unknown, fallback: DiffViewMode): DiffViewMode;
|
|
30
|
+
/** Read the commit-graph style field, unwrapping a schemastery volatile ref. */
|
|
31
|
+
export declare function readGraphStyle(value: unknown, fallback: GraphStyle): GraphStyle;
|
|
28
32
|
/**
|
|
29
33
|
* Read a boolean config field, unwrapping a schemastery volatile reference
|
|
30
34
|
* (`{ get() }`) so a live-editable toggle reflects the latest value. Absent or
|
package/lib/host/index.d.ts
CHANGED
|
@@ -10,11 +10,11 @@ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
11
|
import Schema from '@deepseek-ai/schemastery';
|
|
12
12
|
import type { GitActionRequest, GitActionResult, GitQueryRequest, GitQueryResponse, GitSnapshotRequest, GitSnapshotResult, GitSuggestRequest, GitSuggestResult, GitVersionInfo, GitVersionRequest } from './types.ts';
|
|
13
|
-
export type { GitSnapshot, GitSnapshotResult, GitSnapshotRequest, GitFailure, GitCommit, GraphCommit, GitRef, GitChange, GitChangeStatus, GitAction, GitActionRequest, GitActionResult, GitErrorCode, GitQuery, GitQueryRequest, GitQueryResponse, GitQueryResult, GitBranch, GitFileStat, WorktreeStats, GitVersionRequest, GitVersionInfo, DiffViewMode, DirEntry, GitSuggestRequest, GitSuggestResult, } from './types.ts';
|
|
13
|
+
export type { GitSnapshot, GitSnapshotResult, GitSnapshotRequest, GitFailure, GitCommit, GraphCommit, GitRef, GitChange, GitChangeStatus, GitAction, GitActionRequest, GitActionResult, GitErrorCode, GitQuery, GitQueryRequest, GitQueryResponse, GitQueryResult, GitBranch, GitFileStat, WorktreeStats, StashEntry, GitVersionRequest, GitVersionInfo, DiffViewMode, GraphStyle, DirEntry, ResetMode, GitSuggestRequest, GitSuggestResult, } from './types.ts';
|
|
14
14
|
export { normalizeConfig, DEFAULT_CONFIG, snapshotForSession, resolveWorkspace } from './core.ts';
|
|
15
15
|
export { createGitRunner } from './git.ts';
|
|
16
|
-
export { parseStatus, parseGraphLog, parseBranches, parseNameStatus, sumNumstat } from './parser.ts';
|
|
17
|
-
export { isSafePath, planAction, runAction } from './actions.ts';
|
|
16
|
+
export { parseStatus, parseGraphLog, parseBranches, parseNameStatus, parseStashList, parseTags, sumNumstat } from './parser.ts';
|
|
17
|
+
export { isSafePath, planAction, runAction, classifyActionFailure } from './actions.ts';
|
|
18
18
|
export { runQuery } from './queries.ts';
|
|
19
19
|
export { runSuggest } from './suggest.ts';
|
|
20
20
|
export { readVersionInfo, checkLatestVersion, compareVersions, parseRepository } from './version.ts';
|
|
@@ -29,10 +29,12 @@ export declare class GitPanelService extends TypertRemoteService {
|
|
|
29
29
|
static Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
30
30
|
showInputPill: Schema<boolean, boolean, "volatile-defined">;
|
|
31
31
|
defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
|
|
32
|
+
graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
|
|
32
33
|
suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
|
|
33
34
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
34
35
|
showInputPill: Schema<boolean, boolean, "volatile-defined">;
|
|
35
36
|
defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
|
|
37
|
+
graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
|
|
36
38
|
suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
|
|
37
39
|
}>>, "plain">;
|
|
38
40
|
private readonly deps;
|
package/lib/host/index.js
CHANGED
|
@@ -250,6 +250,25 @@ function parseTags(stdout) {
|
|
|
250
250
|
}
|
|
251
251
|
return out;
|
|
252
252
|
}
|
|
253
|
+
function parseStashList(stdout) {
|
|
254
|
+
const out = [];
|
|
255
|
+
for (const record of stdout.split("\0")) {
|
|
256
|
+
if (record.trim() === "") continue;
|
|
257
|
+
const [selector = "", sha = "", subject = "", relTime = ""] = record.split("");
|
|
258
|
+
const idxMatch = /stash@\{(\d+)\}/.exec(selector);
|
|
259
|
+
if (idxMatch === null) continue;
|
|
260
|
+
const index = Number(idxMatch[1]);
|
|
261
|
+
let branch = null;
|
|
262
|
+
let message = subject;
|
|
263
|
+
const m = /^(?:WIP on|On) (.+?): (.*)$/s.exec(subject);
|
|
264
|
+
if (m !== null) {
|
|
265
|
+
branch = m[1] === "(no branch)" ? null : m[1] ?? null;
|
|
266
|
+
message = m[2] ?? "";
|
|
267
|
+
}
|
|
268
|
+
out.push({ index, sha: sha.trim(), message: message.trim(), branch, relTime: relTime.trim() });
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
253
272
|
function parseNameStatus(stdout) {
|
|
254
273
|
const out = [];
|
|
255
274
|
const tokens = stdout.split("\0");
|
|
@@ -318,6 +337,7 @@ var DEFAULT_CONFIG = {
|
|
|
318
337
|
refreshIntervalMs: 3e4,
|
|
319
338
|
showInputPill: true,
|
|
320
339
|
defaultDiffView: "unified",
|
|
340
|
+
graphStyle: "parallel",
|
|
321
341
|
suggestEnabled: true,
|
|
322
342
|
suggestMaxBytes: 64 * 1024,
|
|
323
343
|
suggestMaxOutputTokens: 200,
|
|
@@ -336,6 +356,7 @@ function normalizeConfig(raw) {
|
|
|
336
356
|
refreshIntervalMs: num(c.refreshIntervalMs, DEFAULT_CONFIG.refreshIntervalMs),
|
|
337
357
|
showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill),
|
|
338
358
|
defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView),
|
|
359
|
+
graphStyle: readGraphStyle(c.graphStyle, DEFAULT_CONFIG.graphStyle),
|
|
339
360
|
suggestEnabled: readBool(c.suggestEnabled, DEFAULT_CONFIG.suggestEnabled),
|
|
340
361
|
suggestMaxBytes: num(c.suggestMaxBytes, DEFAULT_CONFIG.suggestMaxBytes),
|
|
341
362
|
suggestMaxOutputTokens: num(c.suggestMaxOutputTokens, DEFAULT_CONFIG.suggestMaxOutputTokens),
|
|
@@ -350,11 +371,16 @@ function readDiffView(value, fallback) {
|
|
|
350
371
|
const raw = unwrapVolatile(value);
|
|
351
372
|
return raw === "unified" || raw === "split" ? raw : fallback;
|
|
352
373
|
}
|
|
374
|
+
function readGraphStyle(value, fallback) {
|
|
375
|
+
const raw = unwrapVolatile(value);
|
|
376
|
+
return raw === "parallel" || raw === "compact" ? raw : fallback;
|
|
377
|
+
}
|
|
353
378
|
function readBool(value, fallback) {
|
|
354
379
|
const raw = unwrapVolatile(value);
|
|
355
380
|
return typeof raw === "boolean" ? raw : fallback;
|
|
356
381
|
}
|
|
357
382
|
var NEG_CACHE_MS = 15e3;
|
|
383
|
+
var STATS_FANOUT_LIMIT = 200;
|
|
358
384
|
async function resolveBrowseRoot(deps, sessionId) {
|
|
359
385
|
const ws = await resolveWorkspace(deps, sessionId);
|
|
360
386
|
if (ws.ok) return { ok: true, root: ws.root, isGitRepo: true };
|
|
@@ -499,16 +525,19 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
499
525
|
}
|
|
500
526
|
const wt = "run" in worktreeNumRes && worktreeNumRes.run.exitCode === 0 ? sumNumstat(worktreeNumRes.run.stdout) : { insertions: 0, deletions: 0 };
|
|
501
527
|
const stg = "run" in stagedNumRes && stagedNumRes.run.exitCode === 0 ? sumNumstat(stagedNumRes.run.stdout) : { insertions: 0, deletions: 0 };
|
|
528
|
+
const statsDetailed = allChanges.length <= STATS_FANOUT_LIMIT;
|
|
502
529
|
let untrackedInsertions = 0;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
530
|
+
if (statsDetailed) {
|
|
531
|
+
const untrackedPaths = allChanges.filter((c) => c.status === "untracked" && !c.isDirectory).map((c) => c.path);
|
|
532
|
+
const safeUntracked = untrackedPaths.filter(isSafePath);
|
|
533
|
+
if (safeUntracked.length > 0) {
|
|
534
|
+
const perFile = await Promise.all(
|
|
535
|
+
safeUntracked.map((p) => runCommand(deps.run, ["git", "diff", "--numstat", "--no-index", "--", "/dev/null", p], root, "numstat-untracked", deps.signal))
|
|
536
|
+
);
|
|
537
|
+
for (const r of perFile) if ("run" in r) untrackedInsertions += sumNumstat(r.run.stdout).insertions;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const lastChangeAt = statsDetailed ? await maxChangeMtime(deps, root, allChanges) : null;
|
|
512
541
|
const distinct = new Set(allChanges.map((c) => c.path));
|
|
513
542
|
const stats = {
|
|
514
543
|
fileCount: distinct.size,
|
|
@@ -518,7 +547,8 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
518
547
|
insertions: wt.insertions + stg.insertions + untrackedInsertions,
|
|
519
548
|
deletions: wt.deletions + stg.deletions,
|
|
520
549
|
lastChangeAt,
|
|
521
|
-
headCommittedAt: lastCommit?.dateIso ?? null
|
|
550
|
+
headCommittedAt: lastCommit?.dateIso ?? null,
|
|
551
|
+
partial: !statsDetailed
|
|
522
552
|
};
|
|
523
553
|
const snapshot = {
|
|
524
554
|
root,
|
|
@@ -538,6 +568,7 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
538
568
|
refreshIntervalMs: config.refreshIntervalMs,
|
|
539
569
|
showInputPill: config.showInputPill,
|
|
540
570
|
defaultDiffView: config.defaultDiffView,
|
|
571
|
+
graphStyle: config.graphStyle,
|
|
541
572
|
suggestEnabled: config.suggestEnabled,
|
|
542
573
|
checkedAt: Date.now()
|
|
543
574
|
};
|
|
@@ -599,14 +630,86 @@ function planAction(action, unborn) {
|
|
|
599
630
|
return { argv: [["git", "checkout", "--end-of-options", action.name]] };
|
|
600
631
|
case "fetch":
|
|
601
632
|
return { argv: [["git", "fetch", "--all", "--prune"]] };
|
|
633
|
+
case "tag-create": {
|
|
634
|
+
if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe tag name: ${action.name}` };
|
|
635
|
+
if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
|
|
636
|
+
const msg = action.message?.trim() ?? "";
|
|
637
|
+
const flags = msg === "" ? [] : ["-a", "-m", msg];
|
|
638
|
+
return { argv: [["git", "tag", ...flags, "--end-of-options", action.name, action.commit]] };
|
|
639
|
+
}
|
|
640
|
+
case "tag-delete":
|
|
641
|
+
if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe tag name: ${action.name}` };
|
|
642
|
+
return { argv: [["git", "tag", "-d", "--end-of-options", action.name]] };
|
|
643
|
+
case "stash-push": {
|
|
644
|
+
const msg = action.message?.trim() ?? "";
|
|
645
|
+
return { argv: [msg === "" ? ["git", "stash", "push"] : ["git", "stash", "push", "-m", msg]] };
|
|
646
|
+
}
|
|
647
|
+
case "stash-apply":
|
|
648
|
+
case "stash-pop":
|
|
649
|
+
case "stash-drop": {
|
|
650
|
+
if (!Number.isInteger(action.index) || action.index < 0) {
|
|
651
|
+
return { error: "invalid-index", message: `invalid stash index: ${action.index}` };
|
|
652
|
+
}
|
|
653
|
+
const verb = action.kind === "stash-apply" ? "apply" : action.kind === "stash-pop" ? "pop" : "drop";
|
|
654
|
+
return { argv: [["git", "stash", verb, "--end-of-options", `stash@{${action.index}}`]] };
|
|
655
|
+
}
|
|
656
|
+
case "revert": {
|
|
657
|
+
if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
|
|
658
|
+
return { argv: [["git", "revert", "--no-edit", "--end-of-options", action.commit]] };
|
|
659
|
+
}
|
|
660
|
+
case "reset": {
|
|
661
|
+
if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
|
|
662
|
+
if (action.mode !== "soft" && action.mode !== "mixed" && action.mode !== "hard") {
|
|
663
|
+
return { error: "invalid-name", message: `invalid reset mode: ${String(action.mode)}` };
|
|
664
|
+
}
|
|
665
|
+
return { argv: [["git", "reset", `--${action.mode}`, "--end-of-options", action.commit]] };
|
|
666
|
+
}
|
|
602
667
|
}
|
|
603
668
|
}
|
|
669
|
+
function isIndexBusy(outcome) {
|
|
670
|
+
if (!("run" in outcome) || outcome.run.exitCode === 0) return false;
|
|
671
|
+
return /index\.lock|Unable to create.*index|another git process/i.test(outcome.run.stderr + outcome.run.stdout);
|
|
672
|
+
}
|
|
673
|
+
function classifyActionFailure(stdout, stderr, exitCode) {
|
|
674
|
+
const combined = stderr + stdout;
|
|
675
|
+
const err = stderr.trim();
|
|
676
|
+
if (/CONFLICT|Merge conflict|needs merge|could not restore untracked/i.test(combined)) {
|
|
677
|
+
return { code: "conflict", message: err || "merge conflict" };
|
|
678
|
+
}
|
|
679
|
+
if (/index\.lock|Unable to create.*index|another git process/i.test(combined)) {
|
|
680
|
+
return { code: "index-busy", message: err };
|
|
681
|
+
}
|
|
682
|
+
if (/nothing to commit|no changes added/i.test(combined)) {
|
|
683
|
+
return { code: "git-error", message: err || "nothing to commit" };
|
|
684
|
+
}
|
|
685
|
+
if (/No such ref|not a valid reference|is not a stash|no tag|tag .* not found|unknown revision|bad revision|Could not parse object|ambiguous argument/i.test(combined)) {
|
|
686
|
+
return { code: "not-found", message: err || "not found" };
|
|
687
|
+
}
|
|
688
|
+
if (/would be overwritten by (checkout|merge)|local changes|overwritten by merge|Your local changes/i.test(stderr)) {
|
|
689
|
+
return { code: "local-changes-block", message: err };
|
|
690
|
+
}
|
|
691
|
+
if (/is a merge but no -m|mainline parent/i.test(combined)) {
|
|
692
|
+
return { code: "revert-merge", message: err || "cannot revert a merge commit from the panel" };
|
|
693
|
+
}
|
|
694
|
+
return { code: "git-error", message: err || `git exited ${exitCode}` };
|
|
695
|
+
}
|
|
604
696
|
async function runAction(deps, config, request) {
|
|
605
697
|
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
606
698
|
if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
|
|
607
699
|
const root = workspace.root;
|
|
608
700
|
const headProbe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "head-probe", deps.signal);
|
|
609
701
|
const unborn = !("run" in headProbe) || headProbe.run.exitCode !== 0;
|
|
702
|
+
const act = request.action;
|
|
703
|
+
if (act.kind === "stash-apply" || act.kind === "stash-pop" || act.kind === "stash-drop") {
|
|
704
|
+
if (!Number.isInteger(act.index) || act.index < 0) {
|
|
705
|
+
return { ok: false, error: { code: "invalid-index", message: `invalid stash index: ${act.index}` } };
|
|
706
|
+
}
|
|
707
|
+
const probe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "--quiet", "--end-of-options", `stash@{${act.index}}`], root, "stash-verify", deps.signal);
|
|
708
|
+
const sha = "run" in probe && probe.run.exitCode === 0 ? probe.run.stdout.trim() : "";
|
|
709
|
+
if (sha === "" || sha !== act.sha) {
|
|
710
|
+
return { ok: false, error: { code: "not-found", message: "stash entry changed; refresh and retry" } };
|
|
711
|
+
}
|
|
712
|
+
}
|
|
610
713
|
if (request.action.kind === "discard") {
|
|
611
714
|
const removed = await discardUntracked(deps, config, root, request.sessionId, request.action.paths);
|
|
612
715
|
if (removed !== null) {
|
|
@@ -624,7 +727,11 @@ async function runAction(deps, config, request) {
|
|
|
624
727
|
let lastOutput = "";
|
|
625
728
|
for (let step = 0; step < plan.argv.length; step += 1) {
|
|
626
729
|
const argv = plan.argv[step];
|
|
627
|
-
|
|
730
|
+
let outcome = await runCommand(deps.run, argv, root, "action", deps.signal);
|
|
731
|
+
if (isIndexBusy(outcome)) {
|
|
732
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
733
|
+
outcome = await runCommand(deps.run, argv, root, "action-retry", deps.signal);
|
|
734
|
+
}
|
|
628
735
|
const where = plan.argv.length > 1 ? ` (step ${step + 1}/${plan.argv.length}: ${argv.join(" ")})` : "";
|
|
629
736
|
if ("failure" in outcome) {
|
|
630
737
|
const message = outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure);
|
|
@@ -634,14 +741,20 @@ async function runAction(deps, config, request) {
|
|
|
634
741
|
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
635
742
|
lastOutput = outcome.run.stdout || outcome.run.stderr;
|
|
636
743
|
if (outcome.run.exitCode !== 0) {
|
|
637
|
-
const
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
744
|
+
const failure = classifyActionFailure(outcome.run.stdout, outcome.run.stderr, outcome.run.exitCode ?? -1);
|
|
745
|
+
if (request.action.kind === "revert" && failure.code === "conflict") {
|
|
746
|
+
let abort = await runCommand(deps.run, ["git", "revert", "--abort"], root, "revert-abort", deps.signal);
|
|
747
|
+
if (isIndexBusy(abort)) {
|
|
748
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
749
|
+
abort = await runCommand(deps.run, ["git", "revert", "--abort"], root, "revert-abort-retry", deps.signal);
|
|
750
|
+
}
|
|
751
|
+
const aborted = "run" in abort && abort.run.exitCode === 0;
|
|
752
|
+
return aborted ? { ok: false, error: { code: "revert-conflict", message: failure.message + where } } : { ok: false, error: { code: "revert-stuck", message: failure.message + where } };
|
|
643
753
|
}
|
|
644
|
-
return { ok: false, error: { code:
|
|
754
|
+
return { ok: false, error: { code: failure.code, message: failure.message + where } };
|
|
755
|
+
}
|
|
756
|
+
if (request.action.kind === "stash-push" && /No local changes to save/i.test(outcome.run.stdout + outcome.run.stderr)) {
|
|
757
|
+
return { ok: false, error: { code: "git-error", message: (outcome.run.stdout.trim() || "No local changes to save") + where } };
|
|
645
758
|
}
|
|
646
759
|
}
|
|
647
760
|
const snapshot = await snapshotForSession(deps, config, request.sessionId);
|
|
@@ -740,6 +853,8 @@ async function runQuery(deps, config, request) {
|
|
|
740
853
|
return await queryBranches(deps, root);
|
|
741
854
|
case "tags":
|
|
742
855
|
return await queryTags(deps, root);
|
|
856
|
+
case "stash-list":
|
|
857
|
+
return await queryStashList(deps, root);
|
|
743
858
|
case "authors":
|
|
744
859
|
return await queryAuthors(deps, root);
|
|
745
860
|
case "last-commit-message":
|
|
@@ -1096,6 +1211,11 @@ async function queryTags(deps, root) {
|
|
|
1096
1211
|
const tags = "run" in res && res.run.exitCode === 0 ? parseTags(res.run.stdout) : [];
|
|
1097
1212
|
return { ok: true, value: { kind: "tags", tags } };
|
|
1098
1213
|
}
|
|
1214
|
+
async function queryStashList(deps, root) {
|
|
1215
|
+
const res = await runCommand(deps.run, ["git", "stash", "list", "-z", "--format=%gd%x1f%H%x1f%gs%x1f%cr"], root, "stash-list", deps.signal);
|
|
1216
|
+
const entries = "run" in res && res.run.exitCode === 0 ? parseStashList(res.run.stdout) : [];
|
|
1217
|
+
return { ok: true, value: { kind: "stash-list", entries } };
|
|
1218
|
+
}
|
|
1099
1219
|
async function queryAuthors(deps, root) {
|
|
1100
1220
|
const res = await runCommand(deps.run, ["git", "log", ...VISIBLE_REFS, "--format=%an", "--max-count=2000"], root, "authors", deps.signal);
|
|
1101
1221
|
const authors = "run" in res && res.run.exitCode === 0 ? [...new Set(res.run.stdout.split("\n").map((s) => s.trim()).filter((s) => s !== ""))].sort((a, b) => a.localeCompare(b)) : [];
|
|
@@ -1557,6 +1677,10 @@ __publicField(GitPanelService, "Config", Schema.object({
|
|
|
1557
1677
|
Schema.const("unified").description("\u7EDF\u4E00\u89C6\u56FE\uFF08\u5355\u680F\u884C\u5185\u5BF9\u6BD4\uFF09"),
|
|
1558
1678
|
Schema.const("split").description("\u5E76\u6392\u89C6\u56FE\uFF08\u5DE6\u53F3\u5206\u680F\u5BF9\u6BD4\uFF09")
|
|
1559
1679
|
]).default("unified").volatile().description("\u5DEE\u5F02\u5BF9\u6BD4\u9ED8\u8BA4\u89C6\u56FE"),
|
|
1680
|
+
graphStyle: Schema.union([
|
|
1681
|
+
Schema.const("parallel").description("\u5E73\u884C\u7EBF\uFF08VSCode \u98CE\u683C\uFF0C\u5206\u652F\u5404\u5360\u4E00\u5217\uFF09"),
|
|
1682
|
+
Schema.const("compact").description("\u7D27\u51D1\uFF08git log \u98CE\u683C\uFF0C\u5171\u4EAB\u7956\u5148\u5408\u5E76\u4E3A\u4E00\u5217\uFF09")
|
|
1683
|
+
]).default("parallel").volatile().description("\u63D0\u4EA4\u5386\u53F2\u56FE\u7EBF\u6761\u6837\u5F0F"),
|
|
1560
1684
|
suggestEnabled: Schema.boolean().default(true).volatile().description("\u63D0\u4EA4\u6846\u663E\u793A AI \u751F\u6210\u6309\u94AE")
|
|
1561
1685
|
}));
|
|
1562
1686
|
var index_default = GitPanelService;
|
|
@@ -1564,6 +1688,7 @@ export {
|
|
|
1564
1688
|
DEFAULT_CONFIG,
|
|
1565
1689
|
GitPanelService,
|
|
1566
1690
|
checkLatestVersion,
|
|
1691
|
+
classifyActionFailure,
|
|
1567
1692
|
compareVersions,
|
|
1568
1693
|
createGitRunner,
|
|
1569
1694
|
index_default as default,
|
|
@@ -1573,7 +1698,9 @@ export {
|
|
|
1573
1698
|
parseGraphLog,
|
|
1574
1699
|
parseNameStatus,
|
|
1575
1700
|
parseRepository,
|
|
1701
|
+
parseStashList,
|
|
1576
1702
|
parseStatus,
|
|
1703
|
+
parseTags,
|
|
1577
1704
|
planAction,
|
|
1578
1705
|
readVersionInfo,
|
|
1579
1706
|
resolveWorkspace,
|
package/lib/host/parser.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* git output parsers: porcelain status, log, branch, numstat, name-status.
|
|
3
3
|
* Pure functions over raw stdout, no I/O.
|
|
4
4
|
*/
|
|
5
|
-
import type { GitBranch, GitChange, GitCommit, GitFileStat, GraphCommit, GitRef } from './types.ts';
|
|
5
|
+
import type { GitBranch, GitChange, GitCommit, GitFileStat, GraphCommit, GitRef, StashEntry } from './types.ts';
|
|
6
6
|
/**
|
|
7
7
|
* Build a GitCommit from a `%H\x1f%h\x1f%s\x1f%an\x1f%aI`-ordered field array
|
|
8
8
|
* (the body, when present, is the caller's `parts.slice(5)`). Null when the
|
|
@@ -27,6 +27,14 @@ export declare function parseRefs(decoration: string): GitRef[];
|
|
|
27
27
|
export declare function parseBranches(stdout: string): GitBranch[];
|
|
28
28
|
/** Parse `git for-each-ref` tag lines: `name\0shortHash` per line. */
|
|
29
29
|
export declare function parseTags(stdout: string): GitBranch[];
|
|
30
|
+
/**
|
|
31
|
+
* Parse `git stash list -z --format=%gd%x1f%H%x1f%gs%x1f%cr` into stash entries.
|
|
32
|
+
* Each NUL-separated record is: selector (`stash@{N}`), the stash commit SHA, the
|
|
33
|
+
* reflog subject, and a relative time. The subject is either auto ("WIP on
|
|
34
|
+
* <branch>: <sha> <subj>") or custom ("On <branch>: <message>"); the branch is
|
|
35
|
+
* the text between "on " and the first ": ", and the message is after it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseStashList(stdout: string): StashEntry[];
|
|
30
38
|
/**
|
|
31
39
|
* Parse `git show --name-status -z` into stats with an explicit state machine:
|
|
32
40
|
* read a status token, then consume exactly the paths it owns (2 for R/C, 1
|
package/lib/host/types.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** Diff layout: `unified` = single inline column, `split` = side-by-side. */
|
|
7
7
|
export type DiffViewMode = 'unified' | 'split';
|
|
8
|
+
/** Commit-graph line style: `compact` = converge a shared ancestor into one
|
|
9
|
+
* lane (git log --graph); `parallel` = keep each merge's first parent in its
|
|
10
|
+
* own lane, converging only at the ancestor (VSCode / GUI tools). */
|
|
11
|
+
export type GraphStyle = 'compact' | 'parallel';
|
|
8
12
|
/** Wire request: the browser sends only session identity, never a path. */
|
|
9
13
|
export interface GitSnapshotRequest {
|
|
10
14
|
readonly sessionId: string;
|
|
@@ -62,6 +66,8 @@ export interface GitSnapshot {
|
|
|
62
66
|
readonly showInputPill: boolean;
|
|
63
67
|
/** Default diff layout the views open with (user preference). */
|
|
64
68
|
readonly defaultDiffView: DiffViewMode;
|
|
69
|
+
/** Commit-graph line style (user preference). */
|
|
70
|
+
readonly graphStyle: GraphStyle;
|
|
65
71
|
/** Whether the commit box shows the AI-suggest button (user preference). */
|
|
66
72
|
readonly suggestEnabled: boolean;
|
|
67
73
|
/** Epoch millis of the snapshot. */
|
|
@@ -122,8 +128,41 @@ export type GitAction = {
|
|
|
122
128
|
readonly name: string;
|
|
123
129
|
} | {
|
|
124
130
|
readonly kind: 'fetch';
|
|
131
|
+
} | {
|
|
132
|
+
readonly kind: 'tag-create';
|
|
133
|
+
readonly name: string;
|
|
134
|
+
readonly commit: string;
|
|
135
|
+
readonly message?: string;
|
|
136
|
+
} | {
|
|
137
|
+
readonly kind: 'tag-delete';
|
|
138
|
+
readonly name: string;
|
|
139
|
+
} | {
|
|
140
|
+
readonly kind: 'stash-push';
|
|
141
|
+
readonly message?: string;
|
|
142
|
+
} | {
|
|
143
|
+
readonly kind: 'stash-apply';
|
|
144
|
+
readonly index: number;
|
|
145
|
+
readonly sha: string;
|
|
146
|
+
} | {
|
|
147
|
+
readonly kind: 'stash-pop';
|
|
148
|
+
readonly index: number;
|
|
149
|
+
readonly sha: string;
|
|
150
|
+
} | {
|
|
151
|
+
readonly kind: 'stash-drop';
|
|
152
|
+
readonly index: number;
|
|
153
|
+
readonly sha: string;
|
|
154
|
+
} | {
|
|
155
|
+
readonly kind: 'revert';
|
|
156
|
+
readonly commit: string;
|
|
157
|
+
} | {
|
|
158
|
+
readonly kind: 'reset';
|
|
159
|
+
readonly commit: string;
|
|
160
|
+
readonly mode: ResetMode;
|
|
125
161
|
};
|
|
126
|
-
|
|
162
|
+
/** `git reset` modes, from least to most destructive. Single source so the
|
|
163
|
+
* client dialog and the command builder cannot drift. */
|
|
164
|
+
export type ResetMode = 'soft' | 'mixed' | 'hard';
|
|
165
|
+
export type GitErrorCode = 'cwd-unavailable' | 'not-a-git-repo' | 'git-unavailable' | 'invalid-path' | 'invalid-name' | 'invalid-index' | 'git-error' | 'timeout' | 'cancelled' | 'empty-message' | 'local-changes-block' | 'conflict' | 'revert-conflict' | 'revert-stuck' | 'revert-merge' | 'not-found' | 'index-busy' | 'empty-diff' | 'llm-unavailable' | 'llm-error' | 'llm-output' | 'suggest-disabled';
|
|
127
166
|
export type GitActionResult = {
|
|
128
167
|
readonly ok: true;
|
|
129
168
|
readonly snapshot: GitSnapshot;
|
|
@@ -197,6 +236,8 @@ export type GitQuery = {
|
|
|
197
236
|
readonly kind: 'branches';
|
|
198
237
|
} | {
|
|
199
238
|
readonly kind: 'tags';
|
|
239
|
+
} | {
|
|
240
|
+
readonly kind: 'stash-list';
|
|
200
241
|
} | {
|
|
201
242
|
readonly kind: 'authors';
|
|
202
243
|
} | {
|
|
@@ -224,6 +265,19 @@ export interface GitBranch {
|
|
|
224
265
|
readonly ahead?: number;
|
|
225
266
|
readonly behind?: number;
|
|
226
267
|
}
|
|
268
|
+
/** One entry in the stash stack (`git stash list`). */
|
|
269
|
+
export interface StashEntry {
|
|
270
|
+
/** Stack position (0 = most recent). */
|
|
271
|
+
readonly index: number;
|
|
272
|
+
/** The stash commit SHA — a stable id the index position is not (see run). */
|
|
273
|
+
readonly sha: string;
|
|
274
|
+
/** The stash message (custom `-m` text, or the auto "WIP on …" subject). */
|
|
275
|
+
readonly message: string;
|
|
276
|
+
/** Branch the stash was taken on; null when it could not be parsed. */
|
|
277
|
+
readonly branch: string | null;
|
|
278
|
+
/** Human relative time (e.g. "2 hours ago"). */
|
|
279
|
+
readonly relTime: string;
|
|
280
|
+
}
|
|
227
281
|
/** Working-tree statistics for the changes page header. */
|
|
228
282
|
export interface WorktreeStats {
|
|
229
283
|
/** Distinct changed-file paths (staged ∪ modified ∪ untracked). */
|
|
@@ -239,6 +293,13 @@ export interface WorktreeStats {
|
|
|
239
293
|
readonly lastChangeAt: number | null;
|
|
240
294
|
/** HEAD commit time ISO; null when unborn. */
|
|
241
295
|
readonly headCommittedAt: string | null;
|
|
296
|
+
/**
|
|
297
|
+
* True when the per-file fan-outs were skipped because the change set was too
|
|
298
|
+
* large (issue #16): `insertions` then omits untracked lines and
|
|
299
|
+
* `lastChangeAt` is null. Counts (fileCount/staged/modified/untracked) and
|
|
300
|
+
* tracked insertions/deletions stay exact.
|
|
301
|
+
*/
|
|
302
|
+
readonly partial: boolean;
|
|
242
303
|
}
|
|
243
304
|
export type GitQueryResult = {
|
|
244
305
|
readonly kind: 'history';
|
|
@@ -286,6 +347,9 @@ export type GitQueryResult = {
|
|
|
286
347
|
} | {
|
|
287
348
|
readonly kind: 'tags';
|
|
288
349
|
readonly tags: readonly GitBranch[];
|
|
350
|
+
} | {
|
|
351
|
+
readonly kind: 'stash-list';
|
|
352
|
+
readonly entries: readonly StashEntry[];
|
|
289
353
|
} | {
|
|
290
354
|
readonly kind: 'authors';
|
|
291
355
|
readonly authors: readonly string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbzbing/dsh-git-panel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "DeepSeek Harness (dsh) plugin: an IDE-style Git panel in the Web GUI — branch/history overview, uncommitted changes with commit & amend, diff stats, and a zsh-style branch marker in the input bar.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|