@xbzbing/dsh-git-panel 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/LICENSE +21 -0
- package/README.md +90 -0
- package/README_EN.md +90 -0
- package/cordis.patch.yml +12 -0
- package/icon.svg +15 -0
- package/lib/client.js +249 -0
- package/lib/host/actions.d.ts +16 -0
- package/lib/host/core.d.ts +69 -0
- package/lib/host/git.d.ts +62 -0
- package/lib/host/index.d.ts +45 -0
- package/lib/host/index.js +1022 -0
- package/lib/host/parser.d.ts +28 -0
- package/lib/host/queries.d.ts +3 -0
- package/lib/host/types.d.ts +289 -0
- package/lib/host/version.d.ts +40 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +91 -0
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
5
|
+
var __typeError = (msg) => {
|
|
6
|
+
throw TypeError(msg);
|
|
7
|
+
};
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
10
|
+
var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
|
|
11
|
+
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
|
|
12
|
+
var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
|
|
13
|
+
var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
|
|
14
|
+
var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
|
|
15
|
+
var __runInitializers = (array, flags, self, value) => {
|
|
16
|
+
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
20
|
+
var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
|
|
21
|
+
var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
|
|
22
|
+
var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
|
|
23
|
+
var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
|
|
24
|
+
return __privateGet(this, extra);
|
|
25
|
+
}, set [name](x) {
|
|
26
|
+
return __privateSet(this, extra, x);
|
|
27
|
+
} }, name));
|
|
28
|
+
k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
|
|
29
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
30
|
+
ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
|
|
31
|
+
if (k) {
|
|
32
|
+
ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
|
|
33
|
+
if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
|
|
34
|
+
if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
|
|
35
|
+
}
|
|
36
|
+
it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
|
|
37
|
+
if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
|
|
38
|
+
else if (typeof it !== "object" || it === null) __typeError("Object expected");
|
|
39
|
+
else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
|
|
40
|
+
}
|
|
41
|
+
return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
|
|
42
|
+
};
|
|
43
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
44
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
45
|
+
var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
|
|
46
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
47
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
48
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
49
|
+
|
|
50
|
+
// src/host/index.ts
|
|
51
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
52
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
53
|
+
import { readFile as readFile3, realpath, rm, stat } from "node:fs/promises";
|
|
54
|
+
|
|
55
|
+
// src/host/git.ts
|
|
56
|
+
import { readFile } from "node:fs/promises";
|
|
57
|
+
function createGitRunner(subprocess, timeoutMs, maxBytes) {
|
|
58
|
+
const spillMaxBytes = maxBytes * 16;
|
|
59
|
+
return {
|
|
60
|
+
async run(argv, opts) {
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
63
|
+
try {
|
|
64
|
+
const signal = opts.signal === void 0 ? controller.signal : AbortSignal.any([controller.signal, opts.signal]);
|
|
65
|
+
const handle = subprocess.spawn({
|
|
66
|
+
argv,
|
|
67
|
+
cwd: opts.cwd,
|
|
68
|
+
stdio: {
|
|
69
|
+
stdin: "ignore",
|
|
70
|
+
stdout: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } },
|
|
71
|
+
stderr: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } }
|
|
72
|
+
},
|
|
73
|
+
graceMs: 200,
|
|
74
|
+
signal
|
|
75
|
+
});
|
|
76
|
+
let outcome;
|
|
77
|
+
try {
|
|
78
|
+
outcome = await handle.done;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (controller.signal.aborted || opts.signal?.aborted === true) {
|
|
81
|
+
return { exitCode: null, stdout: "", stderr: "", timedOut: true, stdoutLossy: false };
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
const stdout = handle.collected.stdout?.readFrom(0);
|
|
86
|
+
const stderr = handle.collected.stderr?.readFrom(0);
|
|
87
|
+
const resolved = await resolveStdout(stdout);
|
|
88
|
+
return {
|
|
89
|
+
exitCode: outcome.exitCode,
|
|
90
|
+
stdout: resolved.text,
|
|
91
|
+
stderr: stderr?.text ?? "",
|
|
92
|
+
timedOut: controller.signal.aborted || opts.signal?.aborted === true,
|
|
93
|
+
stdoutLossy: resolved.lossy
|
|
94
|
+
};
|
|
95
|
+
} finally {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async function resolveStdout(read) {
|
|
102
|
+
if (read === void 0) return { text: "", lossy: false };
|
|
103
|
+
if (!read.lossy || read.spillPath === void 0) return { text: read.text, lossy: read.lossy };
|
|
104
|
+
try {
|
|
105
|
+
return { text: await readFile(read.spillPath, "utf8"), lossy: false };
|
|
106
|
+
} catch {
|
|
107
|
+
return { text: read.text, lossy: true };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/host/core.ts
|
|
112
|
+
import { join } from "node:path";
|
|
113
|
+
|
|
114
|
+
// src/host/parser.ts
|
|
115
|
+
function statusOf(code) {
|
|
116
|
+
switch (code) {
|
|
117
|
+
case "A":
|
|
118
|
+
return "added";
|
|
119
|
+
case "M":
|
|
120
|
+
return "modified";
|
|
121
|
+
case "D":
|
|
122
|
+
return "deleted";
|
|
123
|
+
case "R":
|
|
124
|
+
return "renamed";
|
|
125
|
+
case "C":
|
|
126
|
+
return "added";
|
|
127
|
+
case "T":
|
|
128
|
+
return "typechange";
|
|
129
|
+
case "U":
|
|
130
|
+
return "conflicted";
|
|
131
|
+
case "?":
|
|
132
|
+
return "untracked";
|
|
133
|
+
default:
|
|
134
|
+
return "modified";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function parseStatus(stdout) {
|
|
138
|
+
const out = [];
|
|
139
|
+
const records = stdout.split("\0");
|
|
140
|
+
for (let i = 0; i < records.length; i++) {
|
|
141
|
+
const rec = records[i];
|
|
142
|
+
if (rec === void 0 || rec.length < 3) continue;
|
|
143
|
+
const x = rec[0];
|
|
144
|
+
const y = rec[1];
|
|
145
|
+
let path = rec.slice(3);
|
|
146
|
+
if (x === "R" || x === "C" || y === "R" || y === "C") {
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
const isDirectory = path.endsWith("/");
|
|
150
|
+
if (isDirectory) path = path.replace(/\/+$/, "");
|
|
151
|
+
if (x === "?" && y === "?") {
|
|
152
|
+
out.push({ path, status: "untracked", staged: false, isDirectory });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (x === "U" || y === "U" || x === "A" && y === "A" || x === "D" && y === "D") {
|
|
156
|
+
out.push({ path, status: "conflicted", staged: false, isDirectory });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (x !== " " && x !== "?") {
|
|
160
|
+
out.push({ path, status: statusOf(x), staged: true, isDirectory });
|
|
161
|
+
}
|
|
162
|
+
if (y !== " " && y !== "?") {
|
|
163
|
+
out.push({ path, status: statusOf(y), staged: false, isDirectory });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
function parseGraphLog(stdout) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const record of stdout.split("")) {
|
|
171
|
+
const rec = record.replace(/^\n+/, "");
|
|
172
|
+
if (rec.trim() === "") continue;
|
|
173
|
+
const parts = rec.split("");
|
|
174
|
+
if (parts.length < 7) continue;
|
|
175
|
+
const [hash, shortHash, parentsRaw, author, dateIso, decoration, subject] = parts;
|
|
176
|
+
const parents = (parentsRaw ?? "").trim() === "" ? [] : parentsRaw.trim().split(/\s+/);
|
|
177
|
+
out.push({
|
|
178
|
+
hash: hash ?? "",
|
|
179
|
+
shortHash: shortHash ?? "",
|
|
180
|
+
subject: subject ?? "",
|
|
181
|
+
author: author ?? "",
|
|
182
|
+
dateIso: dateIso ?? "",
|
|
183
|
+
parents,
|
|
184
|
+
refs: parseRefs(decoration ?? "")
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
function parseRefs(decoration) {
|
|
190
|
+
const refs = [];
|
|
191
|
+
for (const raw of decoration.split(",")) {
|
|
192
|
+
let token = raw.trim();
|
|
193
|
+
if (token === "") continue;
|
|
194
|
+
let head = false;
|
|
195
|
+
if (token.startsWith("HEAD -> ")) {
|
|
196
|
+
head = true;
|
|
197
|
+
token = token.slice("HEAD -> ".length).trim();
|
|
198
|
+
} else if (token === "HEAD") {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (token.startsWith("tag: ")) {
|
|
202
|
+
refs.push({ kind: "tag", name: token.slice("tag: ".length).trim(), head: false });
|
|
203
|
+
} else if (token.startsWith("origin/") || token.includes("/")) {
|
|
204
|
+
refs.push({ kind: "remote", name: token, head });
|
|
205
|
+
} else {
|
|
206
|
+
refs.push({ kind: "branch", name: token, head });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return refs;
|
|
210
|
+
}
|
|
211
|
+
function parseBranches(stdout) {
|
|
212
|
+
const out = [];
|
|
213
|
+
for (const line of stdout.split("\n")) {
|
|
214
|
+
if (line.trim() === "") continue;
|
|
215
|
+
const [name, shortHash = "", track = ""] = line.split("\0");
|
|
216
|
+
if (name === void 0 || name === "") continue;
|
|
217
|
+
const branch = {
|
|
218
|
+
name,
|
|
219
|
+
shortHash: shortHash === "" ? null : shortHash
|
|
220
|
+
};
|
|
221
|
+
const ahead = /ahead (\d+)/.exec(track);
|
|
222
|
+
const behind = /behind (\d+)/.exec(track);
|
|
223
|
+
if (ahead) branch.ahead = Number(ahead[1]);
|
|
224
|
+
if (behind) branch.behind = Number(behind[1]);
|
|
225
|
+
out.push(branch);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
function parseNameStatus(stdout) {
|
|
230
|
+
const out = [];
|
|
231
|
+
const tokens = stdout.split("\0");
|
|
232
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
233
|
+
const tok = tokens[i];
|
|
234
|
+
if (tok === void 0 || tok === "") continue;
|
|
235
|
+
const code = tok[0];
|
|
236
|
+
if (code === void 0 || !/[AMDRCTU]/.test(code)) continue;
|
|
237
|
+
if (code === "R" || code === "C") {
|
|
238
|
+
const newPath = tokens[i + 2];
|
|
239
|
+
i += 2;
|
|
240
|
+
if (newPath) out.push({ path: newPath, status: statusOf(code) });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const path = tokens[i + 1];
|
|
244
|
+
i += 1;
|
|
245
|
+
if (path) out.push({ path, status: statusOf(code) });
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
function sumNumstat(stdout) {
|
|
250
|
+
let insertions = 0;
|
|
251
|
+
let deletions = 0;
|
|
252
|
+
for (const line of stdout.split("\n")) {
|
|
253
|
+
if (line.trim() === "") continue;
|
|
254
|
+
const parts = line.split(" ");
|
|
255
|
+
if (parts.length < 2) continue;
|
|
256
|
+
const add = Number(parts[0]);
|
|
257
|
+
const del = Number(parts[1]);
|
|
258
|
+
if (Number.isFinite(add)) insertions += add;
|
|
259
|
+
if (Number.isFinite(del)) deletions += del;
|
|
260
|
+
}
|
|
261
|
+
return { insertions, deletions };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/host/core.ts
|
|
265
|
+
var DEFAULT_CONFIG = {
|
|
266
|
+
timeoutMs: 8e3,
|
|
267
|
+
maxBytes: 4 * 1024 * 1024,
|
|
268
|
+
maxChanges: 1e3,
|
|
269
|
+
refreshIntervalMs: 3e4,
|
|
270
|
+
showInputPill: true
|
|
271
|
+
};
|
|
272
|
+
function normalizeConfig(raw) {
|
|
273
|
+
const c = raw ?? {};
|
|
274
|
+
const num = (v, d) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : d;
|
|
275
|
+
return {
|
|
276
|
+
timeoutMs: num(c.timeoutMs, DEFAULT_CONFIG.timeoutMs),
|
|
277
|
+
maxBytes: num(c.maxBytes, DEFAULT_CONFIG.maxBytes),
|
|
278
|
+
maxChanges: num(c.maxChanges, DEFAULT_CONFIG.maxChanges),
|
|
279
|
+
refreshIntervalMs: num(c.refreshIntervalMs, DEFAULT_CONFIG.refreshIntervalMs),
|
|
280
|
+
showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function readBool(value, fallback) {
|
|
284
|
+
const raw = value !== null && typeof value === "object" && "get" in value && typeof value.get === "function" ? value.get() : value;
|
|
285
|
+
return typeof raw === "boolean" ? raw : fallback;
|
|
286
|
+
}
|
|
287
|
+
async function resolveWorkspace(deps, sessionId) {
|
|
288
|
+
let cwd = deps.sessions.liveCwd(sessionId);
|
|
289
|
+
if (cwd === void 0 || cwd === "") {
|
|
290
|
+
const meta = await deps.sessions.persistedMeta(sessionId);
|
|
291
|
+
cwd = meta?.cwd;
|
|
292
|
+
}
|
|
293
|
+
if (cwd === void 0 || cwd === "") {
|
|
294
|
+
return { ok: false, failure: { ok: false, error: { code: "cwd-unavailable", sessionId } } };
|
|
295
|
+
}
|
|
296
|
+
const cached = deps.rootCache?.get(cwd);
|
|
297
|
+
if (cached !== void 0) return { ok: true, root: cached };
|
|
298
|
+
const top = await runCommand(deps.run, ["git", "rev-parse", "--show-toplevel"], cwd, "toplevel", deps.signal);
|
|
299
|
+
if ("failure" in top) {
|
|
300
|
+
return { ok: false, failure: { ok: false, error: mapRunFailure(top.failure) } };
|
|
301
|
+
}
|
|
302
|
+
if (top.run.timedOut) return { ok: false, failure: { ok: false, error: { code: "timeout" } } };
|
|
303
|
+
if (top.run.exitCode !== 0) {
|
|
304
|
+
return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
|
|
305
|
+
}
|
|
306
|
+
const raw = top.run.stdout.trim();
|
|
307
|
+
if (raw === "") return { ok: false, failure: { ok: false, error: { code: "not-a-git-repo", cwd } } };
|
|
308
|
+
let root = raw;
|
|
309
|
+
try {
|
|
310
|
+
root = await deps.fs.realpath(raw);
|
|
311
|
+
} catch {
|
|
312
|
+
root = raw;
|
|
313
|
+
}
|
|
314
|
+
deps.rootCache?.set(cwd, root);
|
|
315
|
+
return { ok: true, root };
|
|
316
|
+
}
|
|
317
|
+
async function runCommand(runner, argv, cwd, _label, signal) {
|
|
318
|
+
try {
|
|
319
|
+
const run = await runner.run(argv, { cwd, ...signal ? { signal } : {} });
|
|
320
|
+
return { run };
|
|
321
|
+
} catch (error) {
|
|
322
|
+
return { failure: error };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function mapRunFailure(failure) {
|
|
326
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
327
|
+
return { code: "git-unavailable", detail: message };
|
|
328
|
+
}
|
|
329
|
+
async function snapshotForSession(deps, config, sessionId) {
|
|
330
|
+
const workspace = await resolveWorkspace(deps, sessionId);
|
|
331
|
+
if (!workspace.ok) {
|
|
332
|
+
const failure = workspace.failure;
|
|
333
|
+
if (failure.error.code === "not-a-git-repo") {
|
|
334
|
+
return { ok: false, error: { ...failure.error, showInputPill: config.showInputPill } };
|
|
335
|
+
}
|
|
336
|
+
return failure;
|
|
337
|
+
}
|
|
338
|
+
const root = workspace.root;
|
|
339
|
+
const [branchRes, headRes, statusRes, aheadBehindRes, lastCommitRes] = await Promise.all([
|
|
340
|
+
runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "HEAD"], root, "branch", deps.signal),
|
|
341
|
+
runCommand(deps.run, ["git", "rev-parse", "--short", "HEAD"], root, "head", deps.signal),
|
|
342
|
+
runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z"], root, "status", deps.signal),
|
|
343
|
+
runCommand(deps.run, ["git", "rev-list", "--count", "--left-right", "@{upstream}...HEAD"], root, "aheadBehind", deps.signal),
|
|
344
|
+
runCommand(deps.run, ["git", "log", "-1", "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI"], root, "lastCommit", deps.signal)
|
|
345
|
+
]);
|
|
346
|
+
const branch = "run" in branchRes && branchRes.run.exitCode === 0 ? branchRes.run.stdout.trim() || null : null;
|
|
347
|
+
const unborn = "run" in headRes && headRes.run.exitCode !== 0;
|
|
348
|
+
const head = unborn ? null : "run" in headRes ? headRes.run.stdout.trim() || null : null;
|
|
349
|
+
let allChanges = [];
|
|
350
|
+
if ("run" in statusRes && !statusRes.run.timedOut && statusRes.run.exitCode === 0) {
|
|
351
|
+
allChanges = parseStatus(statusRes.run.stdout);
|
|
352
|
+
}
|
|
353
|
+
const truncated = allChanges.length > config.maxChanges;
|
|
354
|
+
const changes = truncated ? allChanges.slice(0, config.maxChanges) : allChanges;
|
|
355
|
+
let staged = 0;
|
|
356
|
+
let modified = 0;
|
|
357
|
+
let untracked = 0;
|
|
358
|
+
for (const c of changes) {
|
|
359
|
+
if (c.status === "untracked") untracked++;
|
|
360
|
+
else if (c.staged) staged++;
|
|
361
|
+
else modified++;
|
|
362
|
+
}
|
|
363
|
+
let ahead = 0;
|
|
364
|
+
let behind = 0;
|
|
365
|
+
if ("run" in aheadBehindRes && aheadBehindRes.run.exitCode === 0) {
|
|
366
|
+
const parts = aheadBehindRes.run.stdout.trim().split(/\s+/);
|
|
367
|
+
behind = Number(parts[0]) || 0;
|
|
368
|
+
ahead = Number(parts[1]) || 0;
|
|
369
|
+
}
|
|
370
|
+
let lastCommit = null;
|
|
371
|
+
if ("run" in lastCommitRes && lastCommitRes.run.exitCode === 0) {
|
|
372
|
+
const parts = lastCommitRes.run.stdout.trim().split("");
|
|
373
|
+
if (parts.length >= 5 && parts[0]) {
|
|
374
|
+
lastCommit = {
|
|
375
|
+
hash: parts[0],
|
|
376
|
+
shortHash: parts[1] ?? "",
|
|
377
|
+
subject: parts[2] ?? "",
|
|
378
|
+
author: parts[3] ?? "",
|
|
379
|
+
dateIso: parts[4] ?? ""
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const snapshot = {
|
|
384
|
+
root,
|
|
385
|
+
branch,
|
|
386
|
+
head,
|
|
387
|
+
unborn,
|
|
388
|
+
dirty: changes.length > 0,
|
|
389
|
+
staged,
|
|
390
|
+
modified,
|
|
391
|
+
untracked,
|
|
392
|
+
ahead,
|
|
393
|
+
behind,
|
|
394
|
+
lastCommit,
|
|
395
|
+
changes,
|
|
396
|
+
truncated,
|
|
397
|
+
refreshIntervalMs: config.refreshIntervalMs,
|
|
398
|
+
showInputPill: config.showInputPill,
|
|
399
|
+
checkedAt: Date.now()
|
|
400
|
+
};
|
|
401
|
+
return { ok: true, value: snapshot };
|
|
402
|
+
}
|
|
403
|
+
async function maxChangeMtime(deps, root, changes, cap = 200) {
|
|
404
|
+
let max = null;
|
|
405
|
+
const slice = changes.slice(0, cap);
|
|
406
|
+
await Promise.all(slice.map(async (c) => {
|
|
407
|
+
if (c.isDirectory) return;
|
|
408
|
+
try {
|
|
409
|
+
const info = await deps.fs.stat(join(root, c.path));
|
|
410
|
+
if (typeof info.mtimeMs === "number" && Number.isFinite(info.mtimeMs)) {
|
|
411
|
+
max = max === null ? info.mtimeMs : Math.max(max, info.mtimeMs);
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
}));
|
|
416
|
+
return max;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/host/actions.ts
|
|
420
|
+
import { isAbsolute, normalize } from "node:path";
|
|
421
|
+
function isSafePath(path) {
|
|
422
|
+
if (path === "" || isAbsolute(path)) return false;
|
|
423
|
+
const norm = normalize(path);
|
|
424
|
+
if (norm === ".." || norm.startsWith("../") || norm.startsWith("..\\")) return false;
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
function withPaths(prefixes, paths) {
|
|
428
|
+
if (paths.length === 0) return { error: "invalid-path", message: "no paths given" };
|
|
429
|
+
for (const path of paths) {
|
|
430
|
+
if (!isSafePath(path)) return { error: "invalid-path", message: `unsafe path: ${path}` };
|
|
431
|
+
}
|
|
432
|
+
return { argv: prefixes.map((prefix) => [...prefix, ...paths]) };
|
|
433
|
+
}
|
|
434
|
+
function planAction(action, unborn) {
|
|
435
|
+
switch (action.kind) {
|
|
436
|
+
case "stage":
|
|
437
|
+
return withPaths([["git", "add", "--"]], action.paths);
|
|
438
|
+
case "stage-all":
|
|
439
|
+
return { argv: [["git", "add", "-A"]] };
|
|
440
|
+
case "unstage":
|
|
441
|
+
return unborn ? withPaths([["git", "rm", "--cached", "-r", "--"]], action.paths) : withPaths([["git", "restore", "--staged", "--"]], action.paths);
|
|
442
|
+
case "unstage-all":
|
|
443
|
+
return unborn ? { argv: [["git", "rm", "--cached", "-r", "--", "."]] } : { argv: [["git", "restore", "--staged", "--", "."]] };
|
|
444
|
+
case "discard":
|
|
445
|
+
return withPaths([["git", "restore", "--"]], action.paths);
|
|
446
|
+
case "commit": {
|
|
447
|
+
const message = action.message.trim();
|
|
448
|
+
const amendFlag = action.amend === true ? ["--amend"] : [];
|
|
449
|
+
if (message === "" && action.amend !== true) return { error: "empty-message" };
|
|
450
|
+
const msgArgs = message === "" ? [] : ["-m", message];
|
|
451
|
+
if (action.paths === void 0 || action.paths.length === 0) {
|
|
452
|
+
return { argv: [["git", "commit", ...amendFlag, ...msgArgs]] };
|
|
453
|
+
}
|
|
454
|
+
const staged = withPaths([["git", "add", "--"]], action.paths);
|
|
455
|
+
if ("error" in staged) return staged;
|
|
456
|
+
const commitCmd = ["git", "commit", ...amendFlag, ...msgArgs, "--", ...action.paths];
|
|
457
|
+
for (const path of action.paths) {
|
|
458
|
+
if (!isSafePath(path)) return { error: "invalid-path", message: `unsafe path: ${path}` };
|
|
459
|
+
}
|
|
460
|
+
return { argv: [...staged.argv, commitCmd] };
|
|
461
|
+
}
|
|
462
|
+
case "branch-checkout":
|
|
463
|
+
return { argv: [["git", "checkout", action.name]] };
|
|
464
|
+
case "fetch":
|
|
465
|
+
return { argv: [["git", "fetch", "--all", "--prune"]] };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function runAction(deps, config, request) {
|
|
469
|
+
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
470
|
+
if (!workspace.ok) {
|
|
471
|
+
const error = workspace.failure.error;
|
|
472
|
+
return { ok: false, error: { code: error.code, message: "detail" in error ? error.detail : void 0 } };
|
|
473
|
+
}
|
|
474
|
+
const root = workspace.root;
|
|
475
|
+
const headProbe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "head-probe", deps.signal);
|
|
476
|
+
const unborn = !("run" in headProbe) || headProbe.run.exitCode !== 0;
|
|
477
|
+
const plan = planAction(request.action, unborn);
|
|
478
|
+
if ("error" in plan) return { ok: false, error: { code: plan.error, ...plan.message ? { message: plan.message } : {} } };
|
|
479
|
+
let lastOutput = "";
|
|
480
|
+
for (const argv of plan.argv) {
|
|
481
|
+
const outcome = await runCommand(deps.run, argv, root, "action", deps.signal);
|
|
482
|
+
if ("failure" in outcome) {
|
|
483
|
+
const message = outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure);
|
|
484
|
+
return { ok: false, error: { code: "git-unavailable", message } };
|
|
485
|
+
}
|
|
486
|
+
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
487
|
+
lastOutput = outcome.run.stdout || outcome.run.stderr;
|
|
488
|
+
if (outcome.run.exitCode !== 0) {
|
|
489
|
+
const stderr = outcome.run.stderr;
|
|
490
|
+
if (/nothing to commit|no changes added/i.test(stderr + lastOutput)) {
|
|
491
|
+
return { ok: false, error: { code: "git-error", message: stderr.trim() || "nothing to commit" } };
|
|
492
|
+
}
|
|
493
|
+
if (/would be overwritten by checkout|local changes/i.test(stderr)) {
|
|
494
|
+
return { ok: false, error: { code: "local-changes-block", message: stderr.trim() } };
|
|
495
|
+
}
|
|
496
|
+
return { ok: false, error: { code: "git-error", message: stderr.trim() || `git exited ${outcome.run.exitCode}` } };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const snapshot = await snapshotForSession(deps, config, request.sessionId);
|
|
500
|
+
if (!snapshot.ok) {
|
|
501
|
+
return { ok: false, error: { code: "git-error", message: "snapshot after action failed" } };
|
|
502
|
+
}
|
|
503
|
+
return { ok: true, snapshot: snapshot.value, output: lastOutput.trim() };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/host/queries.ts
|
|
507
|
+
import { join as join2 } from "node:path";
|
|
508
|
+
|
|
509
|
+
// src/host/types.ts
|
|
510
|
+
var IMAGE_MIME = {
|
|
511
|
+
png: "image/png",
|
|
512
|
+
jpg: "image/jpeg",
|
|
513
|
+
jpeg: "image/jpeg",
|
|
514
|
+
gif: "image/gif",
|
|
515
|
+
webp: "image/webp",
|
|
516
|
+
avif: "image/avif",
|
|
517
|
+
bmp: "image/bmp",
|
|
518
|
+
svg: "image/svg+xml",
|
|
519
|
+
ico: "image/x-icon",
|
|
520
|
+
tif: "image/tiff",
|
|
521
|
+
tiff: "image/tiff"
|
|
522
|
+
};
|
|
523
|
+
function imageMimeFor(path) {
|
|
524
|
+
const dot = path.lastIndexOf(".");
|
|
525
|
+
if (dot < 0) return null;
|
|
526
|
+
return IMAGE_MIME[path.slice(dot + 1).toLowerCase()] ?? null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/host/queries.ts
|
|
530
|
+
var GRAPH_FORMAT = "--format=%H%x1f%h%x1f%P%x1f%an%x1f%aI%x1f%D%x1f%s%x1e";
|
|
531
|
+
function isHexLike(text) {
|
|
532
|
+
return /^[0-9a-fA-F]{7,40}$/.test(text.trim());
|
|
533
|
+
}
|
|
534
|
+
async function runQuery(deps, config, request) {
|
|
535
|
+
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
536
|
+
if (!workspace.ok) {
|
|
537
|
+
const error = workspace.failure.error;
|
|
538
|
+
return {
|
|
539
|
+
ok: false,
|
|
540
|
+
error: {
|
|
541
|
+
code: error.code === "not-a-git-repo" || error.code === "cwd-unavailable" || error.code === "session-not-found" || error.code === "timeout" || error.code === "git-unavailable" ? error.code : "git-error",
|
|
542
|
+
..."detail" in error ? { message: error.detail } : {}
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const root = workspace.root;
|
|
547
|
+
const q = request.query;
|
|
548
|
+
try {
|
|
549
|
+
switch (q.kind) {
|
|
550
|
+
case "history":
|
|
551
|
+
return await queryHistory(deps, root, q);
|
|
552
|
+
case "diff":
|
|
553
|
+
return await queryDiff(deps, root, q);
|
|
554
|
+
case "image-diff":
|
|
555
|
+
return await queryImageDiff(deps, config, root, q);
|
|
556
|
+
case "show":
|
|
557
|
+
return await queryShow(deps, root, q.ref);
|
|
558
|
+
case "branches":
|
|
559
|
+
return await queryBranches(deps, root);
|
|
560
|
+
case "tags":
|
|
561
|
+
return await queryTags(deps, root);
|
|
562
|
+
case "authors":
|
|
563
|
+
return await queryAuthors(deps, root);
|
|
564
|
+
case "last-commit-message":
|
|
565
|
+
return await queryLastCommitMessage(deps, root);
|
|
566
|
+
case "worktree-stats":
|
|
567
|
+
return await queryWorktreeStats(deps, config, root, request.sessionId);
|
|
568
|
+
}
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
571
|
+
return { ok: false, error: { code: "git-error", message } };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async function queryHistory(deps, root, q) {
|
|
575
|
+
const args = ["git", "log", GRAPH_FORMAT, `--max-count=${q.limit}`, `--skip=${q.skip}`];
|
|
576
|
+
const search = q.search?.trim() ?? "";
|
|
577
|
+
const hexJump = search !== "" && isHexLike(search);
|
|
578
|
+
const countArgs = ["git", "rev-list", "--count"];
|
|
579
|
+
if (!hexJump) {
|
|
580
|
+
if (q.ref !== void 0 && q.ref !== "") {
|
|
581
|
+
args.push(q.ref);
|
|
582
|
+
countArgs.push(q.ref);
|
|
583
|
+
} else {
|
|
584
|
+
args.push("--all");
|
|
585
|
+
countArgs.push("--all");
|
|
586
|
+
}
|
|
587
|
+
if (search !== "") {
|
|
588
|
+
args.push("-i", "-E", `--grep=${search}`);
|
|
589
|
+
countArgs.push("-i", "-E", `--grep=${search}`);
|
|
590
|
+
}
|
|
591
|
+
if (q.author !== void 0 && q.author !== "") {
|
|
592
|
+
args.push(`--author=${q.author}`);
|
|
593
|
+
countArgs.push(`--author=${q.author}`);
|
|
594
|
+
}
|
|
595
|
+
if (q.since !== void 0 && q.since !== "") {
|
|
596
|
+
args.push(`--since=${q.since}`);
|
|
597
|
+
countArgs.push(`--since=${q.since}`);
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
args.push(search);
|
|
601
|
+
}
|
|
602
|
+
const [res, countRes] = await Promise.all([
|
|
603
|
+
runCommand(deps.run, args, root, "history", deps.signal),
|
|
604
|
+
hexJump ? Promise.resolve(null) : runCommand(deps.run, countArgs, root, "history-count", deps.signal)
|
|
605
|
+
]);
|
|
606
|
+
if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
|
|
607
|
+
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
608
|
+
if (res.run.exitCode !== 0) {
|
|
609
|
+
return { ok: true, value: { kind: "history", commits: [], total: 0 } };
|
|
610
|
+
}
|
|
611
|
+
const commits = parseGraphLog(res.run.stdout);
|
|
612
|
+
let total = -1;
|
|
613
|
+
if (hexJump) {
|
|
614
|
+
total = commits.length;
|
|
615
|
+
} else if (countRes !== null && "run" in countRes && countRes.run.exitCode === 0) {
|
|
616
|
+
const n = Number(countRes.run.stdout.trim());
|
|
617
|
+
if (Number.isFinite(n)) total = n;
|
|
618
|
+
}
|
|
619
|
+
return { ok: true, value: { kind: "history", commits, total } };
|
|
620
|
+
}
|
|
621
|
+
async function queryDiff(deps, root, q) {
|
|
622
|
+
if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
|
|
623
|
+
const ctx = q.context !== void 0 && Number.isFinite(q.context) ? Math.max(0, Math.min(1e5, Math.floor(q.context))) : 3;
|
|
624
|
+
const unified = `-U${ctx}`;
|
|
625
|
+
let args;
|
|
626
|
+
if (q.base === "staged") {
|
|
627
|
+
args = ["git", "diff", unified, "--cached", "--", q.path];
|
|
628
|
+
} else if (q.base === "commit") {
|
|
629
|
+
args = ["git", "show", unified, `${q.commit}`, "--", q.path];
|
|
630
|
+
} else {
|
|
631
|
+
args = ["git", "diff", unified, "--", q.path];
|
|
632
|
+
}
|
|
633
|
+
const res = await runCommand(deps.run, args, root, "diff", deps.signal);
|
|
634
|
+
if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
|
|
635
|
+
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
636
|
+
let text = res.run.stdout;
|
|
637
|
+
if (q.base === "worktree" && text.trim() === "") {
|
|
638
|
+
const noIndex = await runCommand(deps.run, ["git", "diff", unified, "--no-index", "--", "/dev/null", q.path], root, "diff-untracked", deps.signal);
|
|
639
|
+
if ("run" in noIndex) text = noIndex.run.stdout;
|
|
640
|
+
}
|
|
641
|
+
return { ok: true, value: { kind: "diff", path: q.path, text } };
|
|
642
|
+
}
|
|
643
|
+
async function queryImageDiff(deps, config, root, q) {
|
|
644
|
+
if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
|
|
645
|
+
const mime = imageMimeFor(q.path);
|
|
646
|
+
if (mime === null) return { ok: true, value: { kind: "image-diff", path: q.path, mime } };
|
|
647
|
+
const oldSpec = q.base === "staged" ? `HEAD:${q.path}` : q.base === "commit" ? `${q.commit}^1:${q.path}` : `:${q.path}`;
|
|
648
|
+
const newSpec = q.base === "staged" ? `:${q.path}` : q.base === "commit" ? `${q.commit}:${q.path}` : null;
|
|
649
|
+
const [oldOid, newOid] = await Promise.all([
|
|
650
|
+
resolveOid(deps, root, oldSpec),
|
|
651
|
+
newSpec === null ? Promise.resolve(void 0) : resolveOid(deps, root, newSpec)
|
|
652
|
+
]);
|
|
653
|
+
const cap = config.maxBytes;
|
|
654
|
+
const gitDir = oldOid !== void 0 || newOid !== void 0 ? await absoluteGitDir(deps, root) : "";
|
|
655
|
+
const [oldSide, newSide] = await Promise.all([
|
|
656
|
+
oldOid === void 0 ? Promise.resolve(void 0) : blobSide(deps, gitDir, oldOid, cap),
|
|
657
|
+
newOid !== void 0 ? blobSide(deps, gitDir, newOid, cap) : q.base === "worktree" ? worktreeSide(deps, root, q.path, cap) : Promise.resolve(void 0)
|
|
658
|
+
]);
|
|
659
|
+
const sides = [oldSide, newSide];
|
|
660
|
+
if (sides.some((s) => s !== void 0 && "tooLarge" in s)) {
|
|
661
|
+
return { ok: true, value: { kind: "image-diff", path: q.path, mime, tooLarge: true } };
|
|
662
|
+
}
|
|
663
|
+
const old64 = sides[0] !== void 0 && !("tooLarge" in sides[0]) ? sides[0].data : void 0;
|
|
664
|
+
const new64 = sides[1] !== void 0 && !("tooLarge" in sides[1]) ? sides[1].data : void 0;
|
|
665
|
+
return {
|
|
666
|
+
ok: true,
|
|
667
|
+
value: {
|
|
668
|
+
kind: "image-diff",
|
|
669
|
+
path: q.path,
|
|
670
|
+
mime,
|
|
671
|
+
...old64 !== void 0 ? { old: `data:${mime};base64,${old64}` } : {},
|
|
672
|
+
...new64 !== void 0 ? { new: `data:${mime};base64,${new64}` } : {}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async function resolveOid(deps, root, spec) {
|
|
677
|
+
const res = await runCommand(deps.run, ["git", "rev-parse", "--verify", "--quiet", spec], root, "image-oid", deps.signal);
|
|
678
|
+
if (!("run" in res) || res.run.exitCode !== 0) return void 0;
|
|
679
|
+
const oid = res.run.stdout.trim();
|
|
680
|
+
return /^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(oid) ? oid : void 0;
|
|
681
|
+
}
|
|
682
|
+
async function absoluteGitDir(deps, root) {
|
|
683
|
+
const res = await runCommand(deps.run, ["git", "rev-parse", "--absolute-git-dir"], root, "git-dir", deps.signal);
|
|
684
|
+
if (!("run" in res) || res.run.exitCode !== 0) throw new Error("git dir unavailable");
|
|
685
|
+
return res.run.stdout.trim();
|
|
686
|
+
}
|
|
687
|
+
async function blobSide(deps, gitDir, oid, cap) {
|
|
688
|
+
const sizeRes = await runCommand(deps.run, ["git", "cat-file", "-s", oid], gitDir, "image-size", deps.signal);
|
|
689
|
+
if (!("run" in sizeRes) || sizeRes.run.exitCode !== 0) return void 0;
|
|
690
|
+
const size = Number(sizeRes.run.stdout.trim());
|
|
691
|
+
if (!Number.isFinite(size)) return void 0;
|
|
692
|
+
if (size > cap) return { tooLarge: true };
|
|
693
|
+
const nameRes = await runCommand(deps.run, ["git", "unpack-file", oid], gitDir, "image-unpack", deps.signal);
|
|
694
|
+
if (!("run" in nameRes) || nameRes.run.exitCode !== 0) return void 0;
|
|
695
|
+
const name = nameRes.run.stdout.trim();
|
|
696
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name)) return void 0;
|
|
697
|
+
const file = join2(gitDir, name);
|
|
698
|
+
try {
|
|
699
|
+
const buf = await deps.fs.readFile(file);
|
|
700
|
+
if (buf.length > cap) return { tooLarge: true };
|
|
701
|
+
return { data: buf.toString("base64") };
|
|
702
|
+
} catch {
|
|
703
|
+
return void 0;
|
|
704
|
+
} finally {
|
|
705
|
+
await deps.fs.remove(file).catch(() => {
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async function worktreeSide(deps, root, path, cap) {
|
|
710
|
+
try {
|
|
711
|
+
const file = join2(root, path);
|
|
712
|
+
const info = await deps.fs.stat(file);
|
|
713
|
+
if (info.size > cap) return { tooLarge: true };
|
|
714
|
+
const buf = await deps.fs.readFile(file);
|
|
715
|
+
if (buf.length > cap) return { tooLarge: true };
|
|
716
|
+
return { data: buf.toString("base64") };
|
|
717
|
+
} catch {
|
|
718
|
+
return void 0;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async function queryShow(deps, root, ref) {
|
|
722
|
+
const metaFormat = "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%b";
|
|
723
|
+
const [metaRes, statRes] = await Promise.all([
|
|
724
|
+
runCommand(deps.run, ["git", "show", "-s", metaFormat, ref], root, "show-meta", deps.signal),
|
|
725
|
+
runCommand(deps.run, ["git", "show", "--name-status", "-z", "--format=", ref], root, "show-stat", deps.signal)
|
|
726
|
+
]);
|
|
727
|
+
if (!("run" in metaRes)) return { ok: false, error: { code: "git-unavailable" } };
|
|
728
|
+
if (metaRes.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
729
|
+
if (metaRes.run.exitCode !== 0) {
|
|
730
|
+
return { ok: false, error: { code: "git-error", message: metaRes.run.stderr.trim() || "unknown ref" } };
|
|
731
|
+
}
|
|
732
|
+
const parts = metaRes.run.stdout.split("");
|
|
733
|
+
let commit = null;
|
|
734
|
+
let body = "";
|
|
735
|
+
if (parts.length >= 5 && parts[0]) {
|
|
736
|
+
commit = {
|
|
737
|
+
hash: parts[0],
|
|
738
|
+
shortHash: parts[1] ?? "",
|
|
739
|
+
subject: parts[2] ?? "",
|
|
740
|
+
author: parts[3] ?? "",
|
|
741
|
+
dateIso: parts[4] ?? ""
|
|
742
|
+
};
|
|
743
|
+
body = (parts[5] ?? "").trim();
|
|
744
|
+
}
|
|
745
|
+
const stats = "run" in statRes && statRes.run.exitCode === 0 ? parseNameStatus(statRes.run.stdout) : [];
|
|
746
|
+
return { ok: true, value: { kind: "show", ref, commit, body, stats } };
|
|
747
|
+
}
|
|
748
|
+
async function queryBranches(deps, root) {
|
|
749
|
+
const fmt = "--format=%(refname:short)%00%(objectname:short)%00%(upstream:track)";
|
|
750
|
+
const [localRes, remoteRes, currentRes, defaultRes] = await Promise.all([
|
|
751
|
+
runCommand(deps.run, ["git", "for-each-ref", "--sort=-committerdate", fmt, "refs/heads"], root, "branches-local", deps.signal),
|
|
752
|
+
runCommand(deps.run, ["git", "for-each-ref", "--sort=-committerdate", fmt, "refs/remotes"], root, "branches-remote", deps.signal),
|
|
753
|
+
runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "HEAD"], root, "branch-current", deps.signal),
|
|
754
|
+
runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], root, "branch-default", deps.signal)
|
|
755
|
+
]);
|
|
756
|
+
const local = "run" in localRes && localRes.run.exitCode === 0 ? parseBranches(localRes.run.stdout) : [];
|
|
757
|
+
const remote = "run" in remoteRes && remoteRes.run.exitCode === 0 ? parseBranches(remoteRes.run.stdout).filter((b) => !b.name.endsWith("/HEAD")) : [];
|
|
758
|
+
const current = "run" in currentRes && currentRes.run.exitCode === 0 ? currentRes.run.stdout.trim() || null : null;
|
|
759
|
+
let defaultBranch = null;
|
|
760
|
+
if ("run" in defaultRes && defaultRes.run.exitCode === 0) {
|
|
761
|
+
const raw = defaultRes.run.stdout.trim();
|
|
762
|
+
defaultBranch = raw.replace(/^origin\//, "") || null;
|
|
763
|
+
}
|
|
764
|
+
return { ok: true, value: { kind: "branches", current, defaultBranch, local, remote } };
|
|
765
|
+
}
|
|
766
|
+
async function queryTags(deps, root) {
|
|
767
|
+
const res = await runCommand(deps.run, ["git", "for-each-ref", "--sort=-creatordate", "--format=%(refname:short)%00%(objectname:short)", "refs/tags"], root, "tags", deps.signal);
|
|
768
|
+
const tags = "run" in res && res.run.exitCode === 0 ? res.run.stdout.split("\n").flatMap((line) => {
|
|
769
|
+
if (line.trim() === "") return [];
|
|
770
|
+
const [name, shortHash = ""] = line.split("\0");
|
|
771
|
+
return name ? [{ name, shortHash: shortHash === "" ? null : shortHash }] : [];
|
|
772
|
+
}) : [];
|
|
773
|
+
return { ok: true, value: { kind: "tags", tags } };
|
|
774
|
+
}
|
|
775
|
+
async function queryAuthors(deps, root) {
|
|
776
|
+
const res = await runCommand(deps.run, ["git", "log", "--all", "--format=%an", "--max-count=2000"], root, "authors", deps.signal);
|
|
777
|
+
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)) : [];
|
|
778
|
+
return { ok: true, value: { kind: "authors", authors } };
|
|
779
|
+
}
|
|
780
|
+
async function queryLastCommitMessage(deps, root) {
|
|
781
|
+
const res = await runCommand(deps.run, ["git", "log", "-1", "--format=%B"], root, "last-message", deps.signal);
|
|
782
|
+
const message = "run" in res && res.run.exitCode === 0 ? res.run.stdout.replace(/\n+$/, "") : "";
|
|
783
|
+
return { ok: true, value: { kind: "last-commit-message", message } };
|
|
784
|
+
}
|
|
785
|
+
async function queryWorktreeStats(deps, config, root, sessionId) {
|
|
786
|
+
const snapshot = await snapshotForSession(deps, config, sessionId);
|
|
787
|
+
if (!snapshot.ok) return { ok: false, error: { code: "git-error", message: "snapshot failed" } };
|
|
788
|
+
const snap = snapshot.value;
|
|
789
|
+
const distinct = new Set(snap.changes.map((c) => c.path));
|
|
790
|
+
const [worktreeNum, stagedNum, headTime] = await Promise.all([
|
|
791
|
+
runCommand(deps.run, ["git", "diff", "--numstat"], root, "numstat-worktree", deps.signal),
|
|
792
|
+
runCommand(deps.run, ["git", "diff", "--numstat", "--cached"], root, "numstat-staged", deps.signal),
|
|
793
|
+
runCommand(deps.run, ["git", "log", "-1", "--format=%aI"], root, "head-time", deps.signal)
|
|
794
|
+
]);
|
|
795
|
+
const a = "run" in worktreeNum && worktreeNum.run.exitCode === 0 ? sumNumstat(worktreeNum.run.stdout) : { insertions: 0, deletions: 0 };
|
|
796
|
+
const b = "run" in stagedNum && stagedNum.run.exitCode === 0 ? sumNumstat(stagedNum.run.stdout) : { insertions: 0, deletions: 0 };
|
|
797
|
+
let untrackedInsertions = 0;
|
|
798
|
+
const untrackedPaths = snap.changes.filter((c) => c.status === "untracked" && !c.isDirectory).map((c) => c.path);
|
|
799
|
+
if (untrackedPaths.length > 0) {
|
|
800
|
+
const safe = untrackedPaths.filter(isSafePath).slice(0, 200);
|
|
801
|
+
if (safe.length > 0) {
|
|
802
|
+
const noIndex = await runCommand(deps.run, ["git", "diff", "--numstat", "--no-index", "--", "/dev/null", ...safe], root, "numstat-untracked", deps.signal);
|
|
803
|
+
if ("run" in noIndex) untrackedInsertions = sumNumstat(noIndex.run.stdout).insertions;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const lastChangeAt = await maxChangeMtime(deps, root, snap.changes);
|
|
807
|
+
const headCommittedAt = "run" in headTime && headTime.run.exitCode === 0 ? headTime.run.stdout.trim() || null : null;
|
|
808
|
+
const stats = {
|
|
809
|
+
fileCount: distinct.size,
|
|
810
|
+
staged: snap.staged,
|
|
811
|
+
modified: snap.modified,
|
|
812
|
+
untracked: snap.untracked,
|
|
813
|
+
insertions: a.insertions + b.insertions + untrackedInsertions,
|
|
814
|
+
deletions: a.deletions + b.deletions,
|
|
815
|
+
lastChangeAt,
|
|
816
|
+
headCommittedAt
|
|
817
|
+
};
|
|
818
|
+
return { ok: true, value: { kind: "worktree-stats", stats } };
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/host/version.ts
|
|
822
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
823
|
+
import { dirname, join as join3 } from "node:path";
|
|
824
|
+
import { fileURLToPath } from "node:url";
|
|
825
|
+
function manifestPath() {
|
|
826
|
+
return join3(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
827
|
+
}
|
|
828
|
+
function stringField(value) {
|
|
829
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
830
|
+
}
|
|
831
|
+
function parseRepository(repository) {
|
|
832
|
+
const raw = typeof repository === "string" ? repository : typeof repository === "object" && repository !== null ? stringField(repository.url) : void 0;
|
|
833
|
+
if (raw === void 0) return void 0;
|
|
834
|
+
const fromHost = raw.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?(?:$|[/#?])/i);
|
|
835
|
+
if (fromHost !== null) return { owner: fromHost[1], repo: fromHost[2] };
|
|
836
|
+
const shorthand = raw.match(/^(?:github:)?([\w.-]+)\/([\w.-]+?)(?:\.git)?$/i);
|
|
837
|
+
if (shorthand !== null) return { owner: shorthand[1], repo: shorthand[2] };
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
function compareVersions(a, b) {
|
|
841
|
+
const split = (value) => {
|
|
842
|
+
const trimmed = value.trim().replace(/^v/i, "");
|
|
843
|
+
const [core, ...rest] = trimmed.split("-");
|
|
844
|
+
return {
|
|
845
|
+
core: core.split(".").map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part)),
|
|
846
|
+
prerelease: rest.length > 0 && rest.join("-") !== ""
|
|
847
|
+
};
|
|
848
|
+
};
|
|
849
|
+
const left = split(a);
|
|
850
|
+
const right = split(b);
|
|
851
|
+
const length = Math.max(left.core.length, right.core.length);
|
|
852
|
+
for (let index = 0; index < length; index += 1) {
|
|
853
|
+
const l = left.core[index] ?? 0;
|
|
854
|
+
const r = right.core[index] ?? 0;
|
|
855
|
+
if (l > r) return 1;
|
|
856
|
+
if (l < r) return -1;
|
|
857
|
+
}
|
|
858
|
+
if (left.prerelease === right.prerelease) return 0;
|
|
859
|
+
return left.prerelease ? -1 : 1;
|
|
860
|
+
}
|
|
861
|
+
async function readManifest(path) {
|
|
862
|
+
const raw = await readFile2(path, "utf8");
|
|
863
|
+
const parsed = JSON.parse(raw);
|
|
864
|
+
if (typeof parsed !== "object" || parsed === null) throw new Error("package.json is not an object");
|
|
865
|
+
return parsed;
|
|
866
|
+
}
|
|
867
|
+
async function readVersionInfo(options = {}) {
|
|
868
|
+
const manifest = await readManifest(options.manifestPath ?? manifestPath());
|
|
869
|
+
const current = stringField(manifest.version) ?? "0.0.0";
|
|
870
|
+
const repo = parseRepository(manifest.repository);
|
|
871
|
+
const repositoryUrl = repo === void 0 ? void 0 : `https://github.com/${repo.owner}/${repo.repo}`;
|
|
872
|
+
return {
|
|
873
|
+
current,
|
|
874
|
+
...repositoryUrl ? { repositoryUrl } : {},
|
|
875
|
+
updateAvailable: false,
|
|
876
|
+
checkedRemote: false
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
async function checkLatestVersion(options = {}) {
|
|
880
|
+
const base = await readVersionInfo({ manifestPath: options.manifestPath });
|
|
881
|
+
const repo = parseRepository((await readManifest(options.manifestPath ?? manifestPath())).repository);
|
|
882
|
+
if (repo === void 0) {
|
|
883
|
+
return { ...base, checkedRemote: false, error: "repository is not configured" };
|
|
884
|
+
}
|
|
885
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
886
|
+
try {
|
|
887
|
+
const response = await fetchFn(`https://api.github.com/repos/${repo.owner}/${repo.repo}/releases/latest`, {
|
|
888
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "dsh-git-panel" }
|
|
889
|
+
});
|
|
890
|
+
if (!response.ok) {
|
|
891
|
+
return { ...base, checkedRemote: true, error: `GitHub responded with ${response.status}` };
|
|
892
|
+
}
|
|
893
|
+
const body = await response.json();
|
|
894
|
+
const tag = typeof body === "object" && body !== null ? stringField(body.tag_name) : void 0;
|
|
895
|
+
const htmlUrl = typeof body === "object" && body !== null ? stringField(body.html_url) : void 0;
|
|
896
|
+
if (tag === void 0) {
|
|
897
|
+
return { ...base, checkedRemote: true, error: "GitHub response did not include a release tag" };
|
|
898
|
+
}
|
|
899
|
+
const latest = tag.replace(/^v/i, "");
|
|
900
|
+
return {
|
|
901
|
+
...base,
|
|
902
|
+
checkedRemote: true,
|
|
903
|
+
latest,
|
|
904
|
+
updateAvailable: compareVersions(latest, base.current) > 0,
|
|
905
|
+
releaseUrl: htmlUrl ?? `${base.repositoryUrl ?? ""}/releases/latest`
|
|
906
|
+
};
|
|
907
|
+
} catch (error) {
|
|
908
|
+
return { ...base, checkedRemote: true, error: error instanceof Error ? error.message : "Unable to reach GitHub" };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/host/index.ts
|
|
913
|
+
var _version_dec, _query_dec, _run_dec, _snapshot_dec, _a, _init;
|
|
914
|
+
var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _version_dec = [Remote("version")], _a) {
|
|
915
|
+
constructor(ctx, config) {
|
|
916
|
+
super(ctx, "gitPanel");
|
|
917
|
+
__runInitializers(_init, 5, this);
|
|
918
|
+
__publicField(this, "deps");
|
|
919
|
+
__publicField(this, "config");
|
|
920
|
+
__publicField(this, "rawConfig");
|
|
921
|
+
this.rawConfig = config;
|
|
922
|
+
this.config = normalizeConfig(config);
|
|
923
|
+
this.deps = this.buildDeps(ctx, this.config);
|
|
924
|
+
}
|
|
925
|
+
buildDeps(ctx, config) {
|
|
926
|
+
const rootCache = /* @__PURE__ */ new Map();
|
|
927
|
+
const get = (key) => ctx.get(key);
|
|
928
|
+
const subprocess = get("subprocess");
|
|
929
|
+
if (subprocess === void 0) {
|
|
930
|
+
return {
|
|
931
|
+
run: { run: async () => {
|
|
932
|
+
throw new Error("subprocess service unavailable");
|
|
933
|
+
} },
|
|
934
|
+
fs: { realpath, stat: async (p) => stat(p), readFile: readFile3, remove: async (p) => {
|
|
935
|
+
await rm(p, { force: true });
|
|
936
|
+
} },
|
|
937
|
+
sessions: { liveCwd: () => void 0, persistedMeta: async () => void 0 },
|
|
938
|
+
rootCache
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
const sessions = get("sessions");
|
|
942
|
+
const persistence = get("sessionPersistence");
|
|
943
|
+
return {
|
|
944
|
+
run: createGitRunner(subprocess, config.timeoutMs, config.maxBytes),
|
|
945
|
+
fs: { realpath, stat: async (p) => stat(p), readFile: readFile3, remove: async (p) => {
|
|
946
|
+
await rm(p, { force: true });
|
|
947
|
+
} },
|
|
948
|
+
sessions: {
|
|
949
|
+
liveCwd: (id) => sessions?.get(id)?.header?.cwd,
|
|
950
|
+
persistedMeta: async (id) => {
|
|
951
|
+
if (persistence === void 0) return void 0;
|
|
952
|
+
try {
|
|
953
|
+
const snap = await persistence.stat(id);
|
|
954
|
+
return snap?.header?.cwd === void 0 ? void 0 : { cwd: snap.header.cwd };
|
|
955
|
+
} catch {
|
|
956
|
+
return void 0;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
rootCache
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
async snapshot(request, signal) {
|
|
964
|
+
return snapshotForSession(this.withSignal(signal), this.liveConfig(), request.sessionId);
|
|
965
|
+
}
|
|
966
|
+
async run(request, signal) {
|
|
967
|
+
return runAction(this.withSignal(signal), this.liveConfig(), request);
|
|
968
|
+
}
|
|
969
|
+
async query(request, signal) {
|
|
970
|
+
return runQuery(this.withSignal(signal), this.liveConfig(), request);
|
|
971
|
+
}
|
|
972
|
+
async version(request) {
|
|
973
|
+
return request.check === true ? checkLatestVersion() : readVersionInfo();
|
|
974
|
+
}
|
|
975
|
+
/** Re-read config so a live-edited volatile field (showInputPill) is current. */
|
|
976
|
+
liveConfig() {
|
|
977
|
+
this.config = normalizeConfig(this.rawConfig);
|
|
978
|
+
return this.config;
|
|
979
|
+
}
|
|
980
|
+
withSignal(signal) {
|
|
981
|
+
if (signal === void 0) return this.deps;
|
|
982
|
+
return { ...this.deps, signal };
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
_init = __decoratorStart(_a);
|
|
986
|
+
__decorateElement(_init, 1, "snapshot", _snapshot_dec, GitPanelService);
|
|
987
|
+
__decorateElement(_init, 1, "run", _run_dec, GitPanelService);
|
|
988
|
+
__decorateElement(_init, 1, "query", _query_dec, GitPanelService);
|
|
989
|
+
__decorateElement(_init, 1, "version", _version_dec, GitPanelService);
|
|
990
|
+
__decoratorMetadata(_init, GitPanelService);
|
|
991
|
+
__publicField(GitPanelService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
|
|
992
|
+
/**
|
|
993
|
+
* Config schema surfaced on the plugin detail page. Only `showInputPill` is
|
|
994
|
+
* `.volatile()`, so the settings host renders it as a live-editable toggle;
|
|
995
|
+
* the operational limits stay profile-only and out of the UI form.
|
|
996
|
+
*/
|
|
997
|
+
__publicField(GitPanelService, "Config", Schema.object({
|
|
998
|
+
showInputPill: Schema.boolean().default(true).volatile().description("\u663E\u793A\u8F93\u5165\u6846\u7684 Git \u5206\u652F\u6807\u8BB0")
|
|
999
|
+
}));
|
|
1000
|
+
var index_default = GitPanelService;
|
|
1001
|
+
export {
|
|
1002
|
+
DEFAULT_CONFIG,
|
|
1003
|
+
GitPanelService,
|
|
1004
|
+
checkLatestVersion,
|
|
1005
|
+
compareVersions,
|
|
1006
|
+
createGitRunner,
|
|
1007
|
+
index_default as default,
|
|
1008
|
+
isSafePath,
|
|
1009
|
+
normalizeConfig,
|
|
1010
|
+
parseBranches,
|
|
1011
|
+
parseGraphLog,
|
|
1012
|
+
parseNameStatus,
|
|
1013
|
+
parseRepository,
|
|
1014
|
+
parseStatus,
|
|
1015
|
+
planAction,
|
|
1016
|
+
readVersionInfo,
|
|
1017
|
+
resolveWorkspace,
|
|
1018
|
+
runAction,
|
|
1019
|
+
runQuery,
|
|
1020
|
+
snapshotForSession,
|
|
1021
|
+
sumNumstat
|
|
1022
|
+
};
|