@xbzbing/dsh-git-panel 1.3.0 → 1.5.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 +109 -16
- package/lib/host/actions.d.ts +19 -0
- package/lib/host/core.d.ts +11 -1
- package/lib/host/git.d.ts +2 -0
- package/lib/host/index.d.ts +7 -4
- package/lib/host/index.js +259 -23
- package/lib/host/parser.d.ts +9 -1
- package/lib/host/remote.d.ts +14 -0
- package/lib/host/types.d.ts +95 -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, GitRemote, 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
|
|
@@ -81,6 +85,12 @@ export interface SnapshotDeps {
|
|
|
81
85
|
readonly getLlm?: () => LlmFace | undefined;
|
|
82
86
|
readonly getAgentDefaultModel?: () => AgentDefaultModelFace | undefined;
|
|
83
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Pick the primary remote from `git remote -v` output and parse its fetch URL.
|
|
90
|
+
* Prefers `origin`; otherwise the first remote listed. Lines look like
|
|
91
|
+
* `origin\thttps://…\t(fetch)` — only `(fetch)` rows are considered.
|
|
92
|
+
*/
|
|
93
|
+
export declare function parsePrimaryRemote(stdout: string): GitRemote | null;
|
|
84
94
|
export type WorkspaceResolution = {
|
|
85
95
|
readonly ok: true;
|
|
86
96
|
readonly root: string;
|
package/lib/host/git.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ interface SpawnSpec {
|
|
|
18
18
|
};
|
|
19
19
|
readonly graceMs: number;
|
|
20
20
|
readonly signal?: AbortSignal;
|
|
21
|
+
/** Explicit child env entries merged onto the service's scrubbed base. */
|
|
22
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
21
23
|
}
|
|
22
24
|
interface OutputRead {
|
|
23
25
|
readonly text: string;
|
package/lib/host/index.d.ts
CHANGED
|
@@ -10,11 +10,12 @@ 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';
|
|
14
|
-
export { normalizeConfig, DEFAULT_CONFIG, snapshotForSession, resolveWorkspace } from './core.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, GitRemote, RemoteHostKind, } from './types.ts';
|
|
14
|
+
export { normalizeConfig, DEFAULT_CONFIG, snapshotForSession, resolveWorkspace, parsePrimaryRemote } from './core.ts';
|
|
15
15
|
export { createGitRunner } from './git.ts';
|
|
16
|
-
export { parseStatus, parseGraphLog, parseBranches, parseNameStatus, sumNumstat } from './parser.ts';
|
|
17
|
-
export {
|
|
16
|
+
export { parseStatus, parseGraphLog, parseBranches, parseNameStatus, parseStashList, parseTags, sumNumstat } from './parser.ts';
|
|
17
|
+
export { parseRemote, classifyHost } from './remote.ts';
|
|
18
|
+
export { isSafePath, planAction, runAction, classifyActionFailure } from './actions.ts';
|
|
18
19
|
export { runQuery } from './queries.ts';
|
|
19
20
|
export { runSuggest } from './suggest.ts';
|
|
20
21
|
export { readVersionInfo, checkLatestVersion, compareVersions, parseRepository } from './version.ts';
|
|
@@ -29,10 +30,12 @@ export declare class GitPanelService extends TypertRemoteService {
|
|
|
29
30
|
static Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
30
31
|
showInputPill: Schema<boolean, boolean, "volatile-defined">;
|
|
31
32
|
defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
|
|
33
|
+
graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
|
|
32
34
|
suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
|
|
33
35
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
34
36
|
showInputPill: Schema<boolean, boolean, "volatile-defined">;
|
|
35
37
|
defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
|
|
38
|
+
graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
|
|
36
39
|
suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
|
|
37
40
|
}>>, "plain">;
|
|
38
41
|
private readonly deps;
|
package/lib/host/index.js
CHANGED
|
@@ -65,6 +65,11 @@ function createGitRunner(subprocess, timeoutMs, maxBytes) {
|
|
|
65
65
|
const handle = subprocess.spawn({
|
|
66
66
|
argv,
|
|
67
67
|
cwd: opts.cwd,
|
|
68
|
+
// Never let a network command (fetch/pull) block on an interactive
|
|
69
|
+
// credential or host-key prompt: with stdin ignored git would hang
|
|
70
|
+
// until the timeout. GIT_TERMINAL_PROMPT=0 makes it fail fast instead,
|
|
71
|
+
// and is harmless for the non-network commands that share this runner.
|
|
72
|
+
env: { GIT_TERMINAL_PROMPT: "0" },
|
|
68
73
|
stdio: {
|
|
69
74
|
stdin: opts.stdinData === void 0 ? "ignore" : { data: opts.stdinData },
|
|
70
75
|
stdout: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } },
|
|
@@ -214,7 +219,15 @@ function parseRefs(decoration) {
|
|
|
214
219
|
continue;
|
|
215
220
|
}
|
|
216
221
|
if (token.startsWith("tag: ")) {
|
|
217
|
-
refs.push({ kind: "tag", name: token.slice("tag: ".length).trim(), head: false });
|
|
222
|
+
refs.push({ kind: "tag", name: token.slice("tag: ".length).trim().replace(/^refs\/tags\//, ""), head: false });
|
|
223
|
+
} else if (token.startsWith("refs/tags/")) {
|
|
224
|
+
refs.push({ kind: "tag", name: token.slice("refs/tags/".length), head: false });
|
|
225
|
+
} else if (token.startsWith("refs/remotes/")) {
|
|
226
|
+
refs.push({ kind: "remote", name: token.slice("refs/remotes/".length), head });
|
|
227
|
+
} else if (token.startsWith("refs/heads/")) {
|
|
228
|
+
refs.push({ kind: "branch", name: token.slice("refs/heads/".length), head });
|
|
229
|
+
} else if (head) {
|
|
230
|
+
refs.push({ kind: "branch", name: token, head });
|
|
218
231
|
} else if (token.startsWith("origin/") || token.includes("/")) {
|
|
219
232
|
refs.push({ kind: "remote", name: token, head });
|
|
220
233
|
} else {
|
|
@@ -250,6 +263,25 @@ function parseTags(stdout) {
|
|
|
250
263
|
}
|
|
251
264
|
return out;
|
|
252
265
|
}
|
|
266
|
+
function parseStashList(stdout) {
|
|
267
|
+
const out = [];
|
|
268
|
+
for (const record of stdout.split("\0")) {
|
|
269
|
+
if (record.trim() === "") continue;
|
|
270
|
+
const [selector = "", sha = "", subject = "", relTime = ""] = record.split("");
|
|
271
|
+
const idxMatch = /stash@\{(\d+)\}/.exec(selector);
|
|
272
|
+
if (idxMatch === null) continue;
|
|
273
|
+
const index = Number(idxMatch[1]);
|
|
274
|
+
let branch = null;
|
|
275
|
+
let message = subject;
|
|
276
|
+
const m = /^(?:WIP on|On) (.+?): (.*)$/s.exec(subject);
|
|
277
|
+
if (m !== null) {
|
|
278
|
+
branch = m[1] === "(no branch)" ? null : m[1] ?? null;
|
|
279
|
+
message = m[2] ?? "";
|
|
280
|
+
}
|
|
281
|
+
out.push({ index, sha: sha.trim(), message: message.trim(), branch, relTime: relTime.trim() });
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
253
285
|
function parseNameStatus(stdout) {
|
|
254
286
|
const out = [];
|
|
255
287
|
const tokens = stdout.split("\0");
|
|
@@ -291,6 +323,52 @@ function sumNumstat(stdout) {
|
|
|
291
323
|
return { insertions, deletions };
|
|
292
324
|
}
|
|
293
325
|
|
|
326
|
+
// src/host/remote.ts
|
|
327
|
+
function classifyHost(host) {
|
|
328
|
+
const h = host.toLowerCase();
|
|
329
|
+
if (h === "github.com" || h.endsWith(".github.com")) return "github";
|
|
330
|
+
if (h === "gitlab.com" || h.endsWith(".gitlab.com")) return "gitlab";
|
|
331
|
+
if (h === "gitee.com" || h.endsWith(".gitee.com")) return "gitee";
|
|
332
|
+
if (h === "bitbucket.org" || h.endsWith(".bitbucket.org")) return "bitbucket";
|
|
333
|
+
return "other";
|
|
334
|
+
}
|
|
335
|
+
function normalizePath(path) {
|
|
336
|
+
return path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
337
|
+
}
|
|
338
|
+
function splitHostPath(raw) {
|
|
339
|
+
const url = raw.trim();
|
|
340
|
+
if (url === "") return null;
|
|
341
|
+
const scp = url.match(/^(?:[^@/]+@)?([^/:]+):(.+)$/);
|
|
342
|
+
if (scp !== null && !/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
|
|
343
|
+
const host = scp[1];
|
|
344
|
+
const path = normalizePath(scp[2]);
|
|
345
|
+
return host !== "" && path !== "" ? { host, path } : null;
|
|
346
|
+
}
|
|
347
|
+
const scheme = url.match(/^[a-z][a-z0-9+.-]*:\/\/([^/]+)\/(.+)$/i);
|
|
348
|
+
if (scheme !== null) {
|
|
349
|
+
let authority = scheme[1];
|
|
350
|
+
const at = authority.lastIndexOf("@");
|
|
351
|
+
if (at >= 0) authority = authority.slice(at + 1);
|
|
352
|
+
const host = authority.replace(/:\d+$/, "");
|
|
353
|
+
const path = normalizePath(scheme[2]);
|
|
354
|
+
return host !== "" && path !== "" ? { host, path } : null;
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
function parseRemote(name, url) {
|
|
359
|
+
const parsed = splitHostPath(url);
|
|
360
|
+
if (parsed === null) {
|
|
361
|
+
return { name, url, webUrl: null, host: null, hostKind: "other" };
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
name,
|
|
365
|
+
url,
|
|
366
|
+
webUrl: `https://${parsed.host}/${parsed.path}`,
|
|
367
|
+
host: parsed.host,
|
|
368
|
+
hostKind: classifyHost(parsed.host)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
294
372
|
// src/host/validate.ts
|
|
295
373
|
import { isAbsolute, normalize } from "node:path";
|
|
296
374
|
function isSafePath(path) {
|
|
@@ -318,6 +396,7 @@ var DEFAULT_CONFIG = {
|
|
|
318
396
|
refreshIntervalMs: 3e4,
|
|
319
397
|
showInputPill: true,
|
|
320
398
|
defaultDiffView: "unified",
|
|
399
|
+
graphStyle: "parallel",
|
|
321
400
|
suggestEnabled: true,
|
|
322
401
|
suggestMaxBytes: 64 * 1024,
|
|
323
402
|
suggestMaxOutputTokens: 200,
|
|
@@ -336,6 +415,7 @@ function normalizeConfig(raw) {
|
|
|
336
415
|
refreshIntervalMs: num(c.refreshIntervalMs, DEFAULT_CONFIG.refreshIntervalMs),
|
|
337
416
|
showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill),
|
|
338
417
|
defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView),
|
|
418
|
+
graphStyle: readGraphStyle(c.graphStyle, DEFAULT_CONFIG.graphStyle),
|
|
339
419
|
suggestEnabled: readBool(c.suggestEnabled, DEFAULT_CONFIG.suggestEnabled),
|
|
340
420
|
suggestMaxBytes: num(c.suggestMaxBytes, DEFAULT_CONFIG.suggestMaxBytes),
|
|
341
421
|
suggestMaxOutputTokens: num(c.suggestMaxOutputTokens, DEFAULT_CONFIG.suggestMaxOutputTokens),
|
|
@@ -350,11 +430,32 @@ function readDiffView(value, fallback) {
|
|
|
350
430
|
const raw = unwrapVolatile(value);
|
|
351
431
|
return raw === "unified" || raw === "split" ? raw : fallback;
|
|
352
432
|
}
|
|
433
|
+
function readGraphStyle(value, fallback) {
|
|
434
|
+
const raw = unwrapVolatile(value);
|
|
435
|
+
return raw === "parallel" || raw === "compact" ? raw : fallback;
|
|
436
|
+
}
|
|
353
437
|
function readBool(value, fallback) {
|
|
354
438
|
const raw = unwrapVolatile(value);
|
|
355
439
|
return typeof raw === "boolean" ? raw : fallback;
|
|
356
440
|
}
|
|
357
441
|
var NEG_CACHE_MS = 15e3;
|
|
442
|
+
function parsePrimaryRemote(stdout) {
|
|
443
|
+
const fetchUrls = /* @__PURE__ */ new Map();
|
|
444
|
+
const order = [];
|
|
445
|
+
for (const line of stdout.split("\n")) {
|
|
446
|
+
const m = line.match(/^(\S+)\t(\S+)\s+\(fetch\)$/);
|
|
447
|
+
if (m === null) continue;
|
|
448
|
+
const name2 = m[1];
|
|
449
|
+
if (!fetchUrls.has(name2)) {
|
|
450
|
+
fetchUrls.set(name2, m[2]);
|
|
451
|
+
order.push(name2);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (order.length === 0) return null;
|
|
455
|
+
const name = fetchUrls.has("origin") ? "origin" : order[0];
|
|
456
|
+
return parseRemote(name, fetchUrls.get(name));
|
|
457
|
+
}
|
|
458
|
+
var STATS_FANOUT_LIMIT = 200;
|
|
358
459
|
async function resolveBrowseRoot(deps, sessionId) {
|
|
359
460
|
const ws = await resolveWorkspace(deps, sessionId);
|
|
360
461
|
if (ws.ok) return { ok: true, root: ws.root, isGitRepo: true };
|
|
@@ -458,14 +559,17 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
458
559
|
return failure;
|
|
459
560
|
}
|
|
460
561
|
const root = workspace.root;
|
|
461
|
-
const [branchRes, headRes, statusRes, aheadBehindRes, lastCommitRes, worktreeNumRes, stagedNumRes] = await Promise.all([
|
|
562
|
+
const [branchRes, headRes, statusRes, aheadBehindRes, lastCommitRes, worktreeNumRes, stagedNumRes, remoteRes] = await Promise.all([
|
|
462
563
|
runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "HEAD"], root, "branch", deps.signal),
|
|
463
564
|
runCommand(deps.run, ["git", "rev-parse", "--short", "HEAD"], root, "head", deps.signal),
|
|
464
565
|
runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z"], root, "status", deps.signal),
|
|
465
566
|
runCommand(deps.run, ["git", "rev-list", "--count", "--left-right", "@{upstream}...HEAD"], root, "aheadBehind", deps.signal),
|
|
466
567
|
runCommand(deps.run, ["git", "log", "-1", "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI"], root, "lastCommit", deps.signal),
|
|
467
568
|
runCommand(deps.run, ["git", "diff", "--numstat"], root, "numstat-worktree", deps.signal),
|
|
468
|
-
runCommand(deps.run, ["git", "diff", "--numstat", "--cached"], root, "numstat-staged", deps.signal)
|
|
569
|
+
runCommand(deps.run, ["git", "diff", "--numstat", "--cached"], root, "numstat-staged", deps.signal),
|
|
570
|
+
// `git remote -v` lists every remote's fetch/push URL; parsed into the
|
|
571
|
+
// primary remote (origin, else the first) for the left-column status bar.
|
|
572
|
+
runCommand(deps.run, ["git", "remote", "-v"], root, "remote", deps.signal)
|
|
469
573
|
]);
|
|
470
574
|
const branch = "run" in branchRes && branchRes.run.exitCode === 0 ? branchRes.run.stdout.trim() || null : null;
|
|
471
575
|
const unborn = "run" in headRes && headRes.run.exitCode !== 0;
|
|
@@ -488,27 +592,32 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
488
592
|
}
|
|
489
593
|
let ahead = 0;
|
|
490
594
|
let behind = 0;
|
|
491
|
-
|
|
595
|
+
const hasUpstream = "run" in aheadBehindRes && aheadBehindRes.run.exitCode === 0;
|
|
596
|
+
if (hasUpstream && "run" in aheadBehindRes) {
|
|
492
597
|
const parts = aheadBehindRes.run.stdout.trim().split(/\s+/);
|
|
493
598
|
behind = Number(parts[0]) || 0;
|
|
494
599
|
ahead = Number(parts[1]) || 0;
|
|
495
600
|
}
|
|
601
|
+
const remote = "run" in remoteRes && remoteRes.run.exitCode === 0 ? parsePrimaryRemote(remoteRes.run.stdout) : null;
|
|
496
602
|
let lastCommit = null;
|
|
497
603
|
if ("run" in lastCommitRes && lastCommitRes.run.exitCode === 0) {
|
|
498
604
|
lastCommit = commitFromFields(lastCommitRes.run.stdout.trim().split(""));
|
|
499
605
|
}
|
|
500
606
|
const wt = "run" in worktreeNumRes && worktreeNumRes.run.exitCode === 0 ? sumNumstat(worktreeNumRes.run.stdout) : { insertions: 0, deletions: 0 };
|
|
501
607
|
const stg = "run" in stagedNumRes && stagedNumRes.run.exitCode === 0 ? sumNumstat(stagedNumRes.run.stdout) : { insertions: 0, deletions: 0 };
|
|
608
|
+
const statsDetailed = allChanges.length <= STATS_FANOUT_LIMIT;
|
|
502
609
|
let untrackedInsertions = 0;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
610
|
+
if (statsDetailed) {
|
|
611
|
+
const untrackedPaths = allChanges.filter((c) => c.status === "untracked" && !c.isDirectory).map((c) => c.path);
|
|
612
|
+
const safeUntracked = untrackedPaths.filter(isSafePath);
|
|
613
|
+
if (safeUntracked.length > 0) {
|
|
614
|
+
const perFile = await Promise.all(
|
|
615
|
+
safeUntracked.map((p) => runCommand(deps.run, ["git", "diff", "--numstat", "--no-index", "--", "/dev/null", p], root, "numstat-untracked", deps.signal))
|
|
616
|
+
);
|
|
617
|
+
for (const r of perFile) if ("run" in r) untrackedInsertions += sumNumstat(r.run.stdout).insertions;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const lastChangeAt = statsDetailed ? await maxChangeMtime(deps, root, allChanges) : null;
|
|
512
621
|
const distinct = new Set(allChanges.map((c) => c.path));
|
|
513
622
|
const stats = {
|
|
514
623
|
fileCount: distinct.size,
|
|
@@ -518,7 +627,8 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
518
627
|
insertions: wt.insertions + stg.insertions + untrackedInsertions,
|
|
519
628
|
deletions: wt.deletions + stg.deletions,
|
|
520
629
|
lastChangeAt,
|
|
521
|
-
headCommittedAt: lastCommit?.dateIso ?? null
|
|
630
|
+
headCommittedAt: lastCommit?.dateIso ?? null,
|
|
631
|
+
partial: !statsDetailed
|
|
522
632
|
};
|
|
523
633
|
const snapshot = {
|
|
524
634
|
root,
|
|
@@ -531,6 +641,8 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
531
641
|
untracked,
|
|
532
642
|
ahead,
|
|
533
643
|
behind,
|
|
644
|
+
hasUpstream,
|
|
645
|
+
remote,
|
|
534
646
|
lastCommit,
|
|
535
647
|
changes,
|
|
536
648
|
stats,
|
|
@@ -538,6 +650,7 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
538
650
|
refreshIntervalMs: config.refreshIntervalMs,
|
|
539
651
|
showInputPill: config.showInputPill,
|
|
540
652
|
defaultDiffView: config.defaultDiffView,
|
|
653
|
+
graphStyle: config.graphStyle,
|
|
541
654
|
suggestEnabled: config.suggestEnabled,
|
|
542
655
|
checkedAt: Date.now()
|
|
543
656
|
};
|
|
@@ -599,7 +712,73 @@ function planAction(action, unborn) {
|
|
|
599
712
|
return { argv: [["git", "checkout", "--end-of-options", action.name]] };
|
|
600
713
|
case "fetch":
|
|
601
714
|
return { argv: [["git", "fetch", "--all", "--prune"]] };
|
|
715
|
+
case "pull-ff":
|
|
716
|
+
return { argv: [["git", "pull", "--ff-only"]] };
|
|
717
|
+
case "tag-create": {
|
|
718
|
+
if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe tag name: ${action.name}` };
|
|
719
|
+
if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
|
|
720
|
+
const msg = action.message?.trim() ?? "";
|
|
721
|
+
const flags = msg === "" ? [] : ["-a", "-m", msg];
|
|
722
|
+
return { argv: [["git", "tag", ...flags, "--end-of-options", action.name, action.commit]] };
|
|
723
|
+
}
|
|
724
|
+
case "tag-delete":
|
|
725
|
+
if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe tag name: ${action.name}` };
|
|
726
|
+
return { argv: [["git", "tag", "-d", "--end-of-options", action.name]] };
|
|
727
|
+
case "stash-push": {
|
|
728
|
+
const msg = action.message?.trim() ?? "";
|
|
729
|
+
return { argv: [msg === "" ? ["git", "stash", "push"] : ["git", "stash", "push", "-m", msg]] };
|
|
730
|
+
}
|
|
731
|
+
case "stash-apply":
|
|
732
|
+
case "stash-pop":
|
|
733
|
+
case "stash-drop": {
|
|
734
|
+
if (!Number.isInteger(action.index) || action.index < 0) {
|
|
735
|
+
return { error: "invalid-index", message: `invalid stash index: ${action.index}` };
|
|
736
|
+
}
|
|
737
|
+
const verb = action.kind === "stash-apply" ? "apply" : action.kind === "stash-pop" ? "pop" : "drop";
|
|
738
|
+
return { argv: [["git", "stash", verb, "--end-of-options", `stash@{${action.index}}`]] };
|
|
739
|
+
}
|
|
740
|
+
case "revert": {
|
|
741
|
+
if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
|
|
742
|
+
return { argv: [["git", "revert", "--no-edit", "--end-of-options", action.commit]] };
|
|
743
|
+
}
|
|
744
|
+
case "reset": {
|
|
745
|
+
if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
|
|
746
|
+
if (action.mode !== "soft" && action.mode !== "mixed" && action.mode !== "hard") {
|
|
747
|
+
return { error: "invalid-name", message: `invalid reset mode: ${String(action.mode)}` };
|
|
748
|
+
}
|
|
749
|
+
return { argv: [["git", "reset", `--${action.mode}`, "--end-of-options", action.commit]] };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function isIndexBusy(outcome) {
|
|
754
|
+
if (!("run" in outcome) || outcome.run.exitCode === 0) return false;
|
|
755
|
+
return /index\.lock|Unable to create.*index|another git process/i.test(outcome.run.stderr + outcome.run.stdout);
|
|
756
|
+
}
|
|
757
|
+
function classifyActionFailure(stdout, stderr, exitCode) {
|
|
758
|
+
const combined = stderr + stdout;
|
|
759
|
+
const err = stderr.trim();
|
|
760
|
+
if (/CONFLICT|Merge conflict|needs merge|could not restore untracked/i.test(combined)) {
|
|
761
|
+
return { code: "conflict", message: err || "merge conflict" };
|
|
762
|
+
}
|
|
763
|
+
if (/index\.lock|Unable to create.*index|another git process/i.test(combined)) {
|
|
764
|
+
return { code: "index-busy", message: err };
|
|
602
765
|
}
|
|
766
|
+
if (/nothing to commit|no changes added/i.test(combined)) {
|
|
767
|
+
return { code: "git-error", message: err || "nothing to commit" };
|
|
768
|
+
}
|
|
769
|
+
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)) {
|
|
770
|
+
return { code: "not-found", message: err || "not found" };
|
|
771
|
+
}
|
|
772
|
+
if (/Not possible to fast-forward|cannot fast-forward|not a fast-forward|Need to specify how to reconcile divergent branches/i.test(combined)) {
|
|
773
|
+
return { code: "not-ff", message: err || "cannot fast-forward (branches diverged)" };
|
|
774
|
+
}
|
|
775
|
+
if (/would be overwritten by (checkout|merge)|local changes|overwritten by merge|Your local changes/i.test(stderr)) {
|
|
776
|
+
return { code: "local-changes-block", message: err };
|
|
777
|
+
}
|
|
778
|
+
if (/is a merge but no -m|mainline parent/i.test(combined)) {
|
|
779
|
+
return { code: "revert-merge", message: err || "cannot revert a merge commit from the panel" };
|
|
780
|
+
}
|
|
781
|
+
return { code: "git-error", message: err || `git exited ${exitCode}` };
|
|
603
782
|
}
|
|
604
783
|
async function runAction(deps, config, request) {
|
|
605
784
|
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
@@ -607,6 +786,17 @@ async function runAction(deps, config, request) {
|
|
|
607
786
|
const root = workspace.root;
|
|
608
787
|
const headProbe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "head-probe", deps.signal);
|
|
609
788
|
const unborn = !("run" in headProbe) || headProbe.run.exitCode !== 0;
|
|
789
|
+
const act = request.action;
|
|
790
|
+
if (act.kind === "stash-apply" || act.kind === "stash-pop" || act.kind === "stash-drop") {
|
|
791
|
+
if (!Number.isInteger(act.index) || act.index < 0) {
|
|
792
|
+
return { ok: false, error: { code: "invalid-index", message: `invalid stash index: ${act.index}` } };
|
|
793
|
+
}
|
|
794
|
+
const probe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "--quiet", "--end-of-options", `stash@{${act.index}}`], root, "stash-verify", deps.signal);
|
|
795
|
+
const sha = "run" in probe && probe.run.exitCode === 0 ? probe.run.stdout.trim() : "";
|
|
796
|
+
if (sha === "" || sha !== act.sha) {
|
|
797
|
+
return { ok: false, error: { code: "not-found", message: "stash entry changed; refresh and retry" } };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
610
800
|
if (request.action.kind === "discard") {
|
|
611
801
|
const removed = await discardUntracked(deps, config, root, request.sessionId, request.action.paths);
|
|
612
802
|
if (removed !== null) {
|
|
@@ -624,7 +814,11 @@ async function runAction(deps, config, request) {
|
|
|
624
814
|
let lastOutput = "";
|
|
625
815
|
for (let step = 0; step < plan.argv.length; step += 1) {
|
|
626
816
|
const argv = plan.argv[step];
|
|
627
|
-
|
|
817
|
+
let outcome = await runCommand(deps.run, argv, root, "action", deps.signal);
|
|
818
|
+
if (isIndexBusy(outcome)) {
|
|
819
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
820
|
+
outcome = await runCommand(deps.run, argv, root, "action-retry", deps.signal);
|
|
821
|
+
}
|
|
628
822
|
const where = plan.argv.length > 1 ? ` (step ${step + 1}/${plan.argv.length}: ${argv.join(" ")})` : "";
|
|
629
823
|
if ("failure" in outcome) {
|
|
630
824
|
const message = outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure);
|
|
@@ -634,14 +828,20 @@ async function runAction(deps, config, request) {
|
|
|
634
828
|
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
635
829
|
lastOutput = outcome.run.stdout || outcome.run.stderr;
|
|
636
830
|
if (outcome.run.exitCode !== 0) {
|
|
637
|
-
const
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
831
|
+
const failure = classifyActionFailure(outcome.run.stdout, outcome.run.stderr, outcome.run.exitCode ?? -1);
|
|
832
|
+
if (request.action.kind === "revert" && failure.code === "conflict") {
|
|
833
|
+
let abort = await runCommand(deps.run, ["git", "revert", "--abort"], root, "revert-abort", deps.signal);
|
|
834
|
+
if (isIndexBusy(abort)) {
|
|
835
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
836
|
+
abort = await runCommand(deps.run, ["git", "revert", "--abort"], root, "revert-abort-retry", deps.signal);
|
|
837
|
+
}
|
|
838
|
+
const aborted = "run" in abort && abort.run.exitCode === 0;
|
|
839
|
+
return aborted ? { ok: false, error: { code: "revert-conflict", message: failure.message + where } } : { ok: false, error: { code: "revert-stuck", message: failure.message + where } };
|
|
643
840
|
}
|
|
644
|
-
return { ok: false, error: { code:
|
|
841
|
+
return { ok: false, error: { code: failure.code, message: failure.message + where } };
|
|
842
|
+
}
|
|
843
|
+
if (request.action.kind === "stash-push" && /No local changes to save/i.test(outcome.run.stdout + outcome.run.stderr)) {
|
|
844
|
+
return { ok: false, error: { code: "git-error", message: (outcome.run.stdout.trim() || "No local changes to save") + where } };
|
|
645
845
|
}
|
|
646
846
|
}
|
|
647
847
|
const snapshot = await snapshotForSession(deps, config, request.sessionId);
|
|
@@ -740,12 +940,16 @@ async function runQuery(deps, config, request) {
|
|
|
740
940
|
return await queryBranches(deps, root);
|
|
741
941
|
case "tags":
|
|
742
942
|
return await queryTags(deps, root);
|
|
943
|
+
case "stash-list":
|
|
944
|
+
return await queryStashList(deps, root);
|
|
743
945
|
case "authors":
|
|
744
946
|
return await queryAuthors(deps, root);
|
|
745
947
|
case "last-commit-message":
|
|
746
948
|
return await queryLastCommitMessage(deps, root);
|
|
747
949
|
case "worktree-stats":
|
|
748
950
|
return await queryWorktreeStats(deps, config, request.sessionId);
|
|
951
|
+
case "pull-preview":
|
|
952
|
+
return await queryPullPreview(deps, root);
|
|
749
953
|
}
|
|
750
954
|
} catch (error) {
|
|
751
955
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -755,7 +959,7 @@ async function runQuery(deps, config, request) {
|
|
|
755
959
|
async function queryHistory(deps, root, q) {
|
|
756
960
|
const limit = Number.isFinite(q.limit) ? Math.min(500, Math.max(1, Math.trunc(q.limit))) : 100;
|
|
757
961
|
const skip = Number.isFinite(q.skip) ? Math.max(0, Math.trunc(q.skip)) : 0;
|
|
758
|
-
const args = ["git", "log", "--topo-order", GRAPH_FORMAT, `--max-count=${limit}`, `--skip=${skip}`];
|
|
962
|
+
const args = ["git", "log", "--topo-order", "--decorate=full", GRAPH_FORMAT, `--max-count=${limit}`, `--skip=${skip}`];
|
|
759
963
|
const search = q.search?.trim() ?? "";
|
|
760
964
|
const hexJump = search !== "" && isHexLike(search);
|
|
761
965
|
const countArgs = ["git", "rev-list", "--count"];
|
|
@@ -1096,6 +1300,11 @@ async function queryTags(deps, root) {
|
|
|
1096
1300
|
const tags = "run" in res && res.run.exitCode === 0 ? parseTags(res.run.stdout) : [];
|
|
1097
1301
|
return { ok: true, value: { kind: "tags", tags } };
|
|
1098
1302
|
}
|
|
1303
|
+
async function queryStashList(deps, root) {
|
|
1304
|
+
const res = await runCommand(deps.run, ["git", "stash", "list", "-z", "--format=%gd%x1f%H%x1f%gs%x1f%cr"], root, "stash-list", deps.signal);
|
|
1305
|
+
const entries = "run" in res && res.run.exitCode === 0 ? parseStashList(res.run.stdout) : [];
|
|
1306
|
+
return { ok: true, value: { kind: "stash-list", entries } };
|
|
1307
|
+
}
|
|
1099
1308
|
async function queryAuthors(deps, root) {
|
|
1100
1309
|
const res = await runCommand(deps.run, ["git", "log", ...VISIBLE_REFS, "--format=%an", "--max-count=2000"], root, "authors", deps.signal);
|
|
1101
1310
|
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)) : [];
|
|
@@ -1106,6 +1315,23 @@ async function queryLastCommitMessage(deps, root) {
|
|
|
1106
1315
|
const message = "run" in res && res.run.exitCode === 0 ? res.run.stdout.replace(/\n+$/, "") : "";
|
|
1107
1316
|
return { ok: true, value: { kind: "last-commit-message", message } };
|
|
1108
1317
|
}
|
|
1318
|
+
async function queryPullPreview(deps, root) {
|
|
1319
|
+
const empty = { kind: "pull-preview", hasUpstream: false, commits: 0, files: 0, insertions: 0, deletions: 0 };
|
|
1320
|
+
const commitsRes = await runCommand(deps.run, ["git", "rev-list", "--count", "--end-of-options", "HEAD..@{upstream}"], root, "pull-preview-count", deps.signal);
|
|
1321
|
+
if (!("run" in commitsRes) || commitsRes.run.exitCode !== 0) return { ok: true, value: empty };
|
|
1322
|
+
const commits = Number(commitsRes.run.stdout.trim()) || 0;
|
|
1323
|
+
const diffRes = await runCommand(deps.run, ["git", "diff", "--numstat", "--end-of-options", "HEAD", "@{upstream}"], root, "pull-preview-diff", deps.signal);
|
|
1324
|
+
let files = 0;
|
|
1325
|
+
let insertions = 0;
|
|
1326
|
+
let deletions = 0;
|
|
1327
|
+
if ("run" in diffRes && diffRes.run.exitCode === 0) {
|
|
1328
|
+
files = diffRes.run.stdout.split("\n").filter((l) => l.trim() !== "").length;
|
|
1329
|
+
const sum = sumNumstat(diffRes.run.stdout);
|
|
1330
|
+
insertions = sum.insertions;
|
|
1331
|
+
deletions = sum.deletions;
|
|
1332
|
+
}
|
|
1333
|
+
return { ok: true, value: { kind: "pull-preview", hasUpstream: true, commits, files, insertions, deletions } };
|
|
1334
|
+
}
|
|
1109
1335
|
async function queryWorktreeStats(deps, config, sessionId) {
|
|
1110
1336
|
const snapshot = await snapshotForSession(deps, config, sessionId);
|
|
1111
1337
|
if (!snapshot.ok) return { ok: false, error: { code: "git-error", message: "snapshot failed" } };
|
|
@@ -1557,6 +1783,10 @@ __publicField(GitPanelService, "Config", Schema.object({
|
|
|
1557
1783
|
Schema.const("unified").description("\u7EDF\u4E00\u89C6\u56FE\uFF08\u5355\u680F\u884C\u5185\u5BF9\u6BD4\uFF09"),
|
|
1558
1784
|
Schema.const("split").description("\u5E76\u6392\u89C6\u56FE\uFF08\u5DE6\u53F3\u5206\u680F\u5BF9\u6BD4\uFF09")
|
|
1559
1785
|
]).default("unified").volatile().description("\u5DEE\u5F02\u5BF9\u6BD4\u9ED8\u8BA4\u89C6\u56FE"),
|
|
1786
|
+
graphStyle: Schema.union([
|
|
1787
|
+
Schema.const("parallel").description("\u5E73\u884C\u7EBF\uFF08VSCode \u98CE\u683C\uFF0C\u5206\u652F\u5404\u5360\u4E00\u5217\uFF09"),
|
|
1788
|
+
Schema.const("compact").description("\u7D27\u51D1\uFF08git log \u98CE\u683C\uFF0C\u5171\u4EAB\u7956\u5148\u5408\u5E76\u4E3A\u4E00\u5217\uFF09")
|
|
1789
|
+
]).default("parallel").volatile().description("\u63D0\u4EA4\u5386\u53F2\u56FE\u7EBF\u6761\u6837\u5F0F"),
|
|
1560
1790
|
suggestEnabled: Schema.boolean().default(true).volatile().description("\u63D0\u4EA4\u6846\u663E\u793A AI \u751F\u6210\u6309\u94AE")
|
|
1561
1791
|
}));
|
|
1562
1792
|
var index_default = GitPanelService;
|
|
@@ -1564,6 +1794,8 @@ export {
|
|
|
1564
1794
|
DEFAULT_CONFIG,
|
|
1565
1795
|
GitPanelService,
|
|
1566
1796
|
checkLatestVersion,
|
|
1797
|
+
classifyActionFailure,
|
|
1798
|
+
classifyHost,
|
|
1567
1799
|
compareVersions,
|
|
1568
1800
|
createGitRunner,
|
|
1569
1801
|
index_default as default,
|
|
@@ -1572,8 +1804,12 @@ export {
|
|
|
1572
1804
|
parseBranches,
|
|
1573
1805
|
parseGraphLog,
|
|
1574
1806
|
parseNameStatus,
|
|
1807
|
+
parsePrimaryRemote,
|
|
1808
|
+
parseRemote,
|
|
1575
1809
|
parseRepository,
|
|
1810
|
+
parseStashList,
|
|
1576
1811
|
parseStatus,
|
|
1812
|
+
parseTags,
|
|
1577
1813
|
planAction,
|
|
1578
1814
|
readVersionInfo,
|
|
1579
1815
|
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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a git remote URL into a browsable https page URL + host classification.
|
|
3
|
+
*
|
|
4
|
+
* Git remotes come in several shapes — scp-like ssh (`git@host:owner/repo.git`),
|
|
5
|
+
* ssh:// , https:// , git:// — all of which must collapse to a single
|
|
6
|
+
* `https://<host>/<path>` page link and a known-host kind for the status-bar
|
|
7
|
+
* icon. Credentials in the URL (`https://user:token@host/…`) are stripped so a
|
|
8
|
+
* token can never leak into an href the browser renders.
|
|
9
|
+
*/
|
|
10
|
+
import type { GitRemote, RemoteHostKind } from './types.ts';
|
|
11
|
+
/** Map a hostname to a known-host kind (self-hosted / unknown → 'other'). */
|
|
12
|
+
export declare function classifyHost(host: string): RemoteHostKind;
|
|
13
|
+
/** Build a {@link GitRemote} from a remote name + its configured URL. */
|
|
14
|
+
export declare function parseRemote(name: string, url: string): GitRemote;
|