@dotobokuri/fleet-console 1.23.0 → 1.25.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/AGENTS.md +43 -257
- package/README.md +8 -0
- package/dist/cli.d.ts +11 -1
- package/dist/cli.mjs +4464 -3241
- package/dist/client/assets/{_baseUniq-BjOe5yYF.js → _baseUniq-CftO_qma.js} +1 -1
- package/dist/client/assets/{arc-bFUuO3jf.js → arc-CIUNumHO.js} +1 -1
- package/dist/client/assets/{architectureDiagram-Q4EWVU46-Ba7nimQq.js → architectureDiagram-Q4EWVU46-TpVISJv_.js} +1 -1
- package/dist/client/assets/{blockDiagram-DXYQGD6D-CQGf47hl.js → blockDiagram-DXYQGD6D-iHi0B0P6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AHTNJAMY-DZF_cAK4.js → c4Diagram-AHTNJAMY-CYAcFDrF.js} +1 -1
- package/dist/client/assets/channel-B-2UBgQ_.js +1 -0
- package/dist/client/assets/{chunk-4BX2VUAB-DjUUHSQ5.js → chunk-4BX2VUAB-B3Nh-xBT.js} +1 -1
- package/dist/client/assets/{chunk-4TB4RGXK-Dvqti8bd.js → chunk-4TB4RGXK-CDbmqVYQ.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-2BxiBvkp.js → chunk-55IACEB6-DRXIVd03.js} +1 -1
- package/dist/client/assets/{chunk-EDXVE4YY-CqCjol5O.js → chunk-EDXVE4YY-b05C0dVN.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-eoCfIbly.js → chunk-FMBD7UC4-ByrEd9CG.js} +1 -1
- package/dist/client/assets/{chunk-OYMX7WX6-CoC86V_1.js → chunk-OYMX7WX6-BpEVT-V-.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-CJQA5mlQ.js → chunk-QZHKN3VN-CfdhAAA9.js} +1 -1
- package/dist/client/assets/{chunk-YZCP3GAM-C5C4kbLu.js → chunk-YZCP3GAM-Fdx8QRTs.js} +1 -1
- package/dist/client/assets/classDiagram-6PBFFD2Q-DiaCSZq-.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-DiaCSZq-.js +1 -0
- package/dist/client/assets/clone-Dq4F-r1q.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-CahuunpL.js → cose-bilkent-S5V4N54A-D8w9KovE.js} +1 -1
- package/dist/client/assets/{dagre-KV5264BT-Cbv5kSi5.js → dagre-KV5264BT-D4fw7rTF.js} +1 -1
- package/dist/client/assets/{diagram-5BDNPKRD-CNm40ZGB.js → diagram-5BDNPKRD-BRFcBwDM.js} +1 -1
- package/dist/client/assets/{diagram-G4DWMVQ6-DrlGNT3i.js → diagram-G4DWMVQ6-BKzuEau8.js} +1 -1
- package/dist/client/assets/{diagram-MMDJMWI5-r8Y2oqmw.js → diagram-MMDJMWI5-DkqP7aZ4.js} +1 -1
- package/dist/client/assets/{diagram-TYMM5635-CMzEhXup.js → diagram-TYMM5635-NelXGAN3.js} +1 -1
- package/dist/client/assets/{erDiagram-SMLLAGMA-UFjUUMPz.js → erDiagram-SMLLAGMA-DHH5btR9.js} +1 -1
- package/dist/client/assets/{flowDiagram-DWJPFMVM-Dd1STmYc.js → flowDiagram-DWJPFMVM-BmqHUJoi.js} +1 -1
- package/dist/client/assets/{ganttDiagram-T4ZO3ILL-CHHU0hVd.js → ganttDiagram-T4ZO3ILL-BtmWEvNP.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-vj2HukAF.js → gitGraphDiagram-UUTBAWPF-B64kk9Nb.js} +1 -1
- package/dist/client/assets/{graph-DhjfEbEb.js → graph-CEjP0hvY.js} +1 -1
- package/dist/client/assets/index-B3gs6ohi.js +380 -0
- package/dist/client/assets/index-DDfl_vPx.css +1 -0
- package/dist/client/assets/{infoDiagram-42DDH7IO-CC7AEgDm.js → infoDiagram-42DDH7IO-CaTXrw5R.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-DlRw2B9G.js → ishikawaDiagram-UXIWVN3A-D9-Ds_71.js} +1 -1
- package/dist/client/assets/{journeyDiagram-VCZTEJTY-BbRXANKQ.js → journeyDiagram-VCZTEJTY-Cv4qWOhs.js} +1 -1
- package/dist/client/assets/{kanban-definition-6JOO6SKY-DtvdCxGN.js → kanban-definition-6JOO6SKY-CWBLcIpD.js} +1 -1
- package/dist/client/assets/{layout-Dg5cvw5v.js → layout-D8eNRDnf.js} +1 -1
- package/dist/client/assets/{linear-BvVlQLqF.js → linear-lDifS_HV.js} +1 -1
- package/dist/client/assets/{mermaid.core-DzrV8sC-.js → mermaid.core-DcGkHMSR.js} +4 -4
- package/dist/client/assets/{min-DNGQ1Ciz.js → min-Dz_jFvYt.js} +1 -1
- package/dist/client/assets/{mindmap-definition-QFDTVHPH-DxJCNZbg.js → mindmap-definition-QFDTVHPH-F3RmDOG-.js} +1 -1
- package/dist/client/assets/{pieDiagram-DEJITSTG-B75XHG-s.js → pieDiagram-DEJITSTG-B1fDKBcl.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-34T5L4WZ-CL4yoF0m.js → quadrantDiagram-34T5L4WZ-D93hlO3O.js} +1 -1
- package/dist/client/assets/{requirementDiagram-MS252O5E-kGV4vcr1.js → requirementDiagram-MS252O5E-B4wj4AQL.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-XADWPNL6-DoA4u7wI.js → sankeyDiagram-XADWPNL6-ZRwx77Xd.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-FGHM5R23-ZpNDf1bS.js → sequenceDiagram-FGHM5R23-COMMRNyM.js} +1 -1
- package/dist/client/assets/{stateDiagram-FHFEXIEX-BzgKcVAG.js → stateDiagram-FHFEXIEX-CpBccMyi.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-UCYrbaKP.js +1 -0
- package/dist/client/assets/{timeline-definition-GMOUNBTQ-B_DkUDsD.js → timeline-definition-GMOUNBTQ-C44EI_-o.js} +1 -1
- package/dist/client/assets/{vennDiagram-DHZGUBPP-MCUnnsKb.js → vennDiagram-DHZGUBPP-BiBJbCdD.js} +1 -1
- package/dist/client/assets/{wardley-RL74JXVD-D_JAMSxj.js → wardley-RL74JXVD-Dm5-kWNd.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-NUSXRM2D-BHCwZGLq.js → wardleyDiagram-NUSXRM2D-BjbesihY.js} +1 -1
- package/dist/client/assets/{xychartDiagram-5P7HB3ND-FbGwbD3V.js → xychartDiagram-5P7HB3ND-5P6xhAMr.js} +1 -1
- package/dist/client/index.html +3 -3
- package/dist/desktop-protocol.d.ts +50 -0
- package/dist/desktop-protocol.mjs +107 -0
- package/dist/fleet-plugins/diff/routes.mjs +233 -226
- package/dist/fleet-plugins/skills/routes.mjs +218 -82
- package/dist/fleet-plugins/terminal/routes.mjs +3541 -3499
- package/package.json +7 -2
- package/dist/client/assets/channel-D2BahVp1.js +0 -1
- package/dist/client/assets/classDiagram-6PBFFD2Q-DsDAVLhS.js +0 -1
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-DsDAVLhS.js +0 -1
- package/dist/client/assets/clone-CdZy2Xzf.js +0 -1
- package/dist/client/assets/index-Dq9EKm2G.css +0 -1
- package/dist/client/assets/index-DtzbOdd7.js +0 -380
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-yRK5hzZ4.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import 'fs';
|
|
4
|
-
import
|
|
4
|
+
import path2 from 'path';
|
|
5
5
|
import fs from 'fs/promises';
|
|
6
6
|
|
|
7
7
|
// sdk/plugin/node.ts
|
|
@@ -100,17 +100,22 @@ function runGit(args, opts) {
|
|
|
100
100
|
function isPlainObject(value) {
|
|
101
101
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
102
102
|
}
|
|
103
|
+
function literalPathspec(relativePath) {
|
|
104
|
+
return `:(literal)${relativePath}`;
|
|
105
|
+
}
|
|
103
106
|
function normalizeNumstatPath(p) {
|
|
104
107
|
const match = /^(.*?)\{[^}]* => ([^}]*)\}(.*)$/.exec(p);
|
|
105
|
-
if (
|
|
106
|
-
|
|
108
|
+
if (match) return (match[1] ?? "") + (match[2] ?? "") + (match[3] ?? "");
|
|
109
|
+
const plainRename = /^.+ => (.+)$/.exec(p);
|
|
110
|
+
return plainRename?.[1] ?? p;
|
|
107
111
|
}
|
|
108
112
|
function parseDiffFileList(nameStatusOutput, numstatOutput) {
|
|
109
113
|
const numstatMap = /* @__PURE__ */ new Map();
|
|
110
114
|
for (const line of numstatOutput.split("\n")) {
|
|
111
115
|
const parts = line.split(" ");
|
|
112
116
|
if (parts.length < 3) continue;
|
|
113
|
-
const [adds, dels
|
|
117
|
+
const [adds, dels] = parts;
|
|
118
|
+
const filePath = parts.length > 3 ? parts[parts.length - 1] : parts[2];
|
|
114
119
|
if (!filePath) continue;
|
|
115
120
|
numstatMap.set(normalizeNumstatPath(filePath), {
|
|
116
121
|
additions: parseInt(adds ?? "0", 10) || 0,
|
|
@@ -123,16 +128,17 @@ function parseDiffFileList(nameStatusOutput, numstatOutput) {
|
|
|
123
128
|
const [rawStatus, ...pathParts] = line.split(" ");
|
|
124
129
|
if (!rawStatus || pathParts.length === 0) continue;
|
|
125
130
|
const statusChar = rawStatus.charAt(0).toUpperCase();
|
|
126
|
-
if (statusChar !== "M" && statusChar !== "A" && statusChar !== "D" && statusChar !== "R") continue;
|
|
131
|
+
if (statusChar !== "M" && statusChar !== "A" && statusChar !== "D" && statusChar !== "R" && statusChar !== "T") continue;
|
|
132
|
+
const oldPath = statusChar === "R" ? pathParts[0] : void 0;
|
|
127
133
|
const filePath = statusChar === "R" ? pathParts[1] ?? pathParts[0] ?? "" : pathParts[0] ?? "";
|
|
128
134
|
if (!filePath) continue;
|
|
129
135
|
const nums = numstatMap.get(filePath) ?? { additions: 0, deletions: 0 };
|
|
130
|
-
files.push({ path: filePath, status: statusChar, ...nums });
|
|
136
|
+
files.push({ path: filePath, ...oldPath ? { oldPath } : {}, status: statusChar, ...nums });
|
|
131
137
|
}
|
|
132
138
|
return files;
|
|
133
139
|
}
|
|
134
140
|
async function fetchUntrackedFiles(cwd) {
|
|
135
|
-
const result = await runGit(["ls-files", "--others", "--exclude-standard"], { cwd });
|
|
141
|
+
const result = await runGit(["ls-files", "--others", "--exclude-standard", "--", "."], { cwd });
|
|
136
142
|
return result.stdout.split("\n").filter((p) => p.trim()).map((p) => ({
|
|
137
143
|
path: p,
|
|
138
144
|
status: "U",
|
|
@@ -140,6 +146,9 @@ async function fetchUntrackedFiles(cwd) {
|
|
|
140
146
|
deletions: 0
|
|
141
147
|
}));
|
|
142
148
|
}
|
|
149
|
+
async function ensureGitRepository(cwd) {
|
|
150
|
+
await runGit(["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
151
|
+
}
|
|
143
152
|
function isNoHeadError(error) {
|
|
144
153
|
if (!(error instanceof GitExecutorError)) return false;
|
|
145
154
|
if (error.code !== "non_zero_exit") return false;
|
|
@@ -147,9 +156,9 @@ function isNoHeadError(error) {
|
|
|
147
156
|
}
|
|
148
157
|
async function resolveGitCwd(theaterPath, rawSubPath) {
|
|
149
158
|
if (rawSubPath === "") return { gitCwd: theaterPath };
|
|
150
|
-
const resolvedSub =
|
|
151
|
-
if (!resolvedSub.startsWith(theaterPath +
|
|
152
|
-
if (
|
|
159
|
+
const resolvedSub = path2.resolve(theaterPath, rawSubPath);
|
|
160
|
+
if (!resolvedSub.startsWith(theaterPath + path2.sep) && resolvedSub !== theaterPath) return null;
|
|
161
|
+
if (path2.normalize(rawSubPath).startsWith("..")) return null;
|
|
153
162
|
let realTheater;
|
|
154
163
|
let realSub;
|
|
155
164
|
try {
|
|
@@ -160,7 +169,7 @@ async function resolveGitCwd(theaterPath, rawSubPath) {
|
|
|
160
169
|
} catch {
|
|
161
170
|
return null;
|
|
162
171
|
}
|
|
163
|
-
const norm = realTheater.endsWith(
|
|
172
|
+
const norm = realTheater.endsWith(path2.sep) ? realTheater : realTheater + path2.sep;
|
|
164
173
|
if (realSub !== realTheater && !realSub.startsWith(norm)) return null;
|
|
165
174
|
return { gitCwd: resolvedSub };
|
|
166
175
|
}
|
|
@@ -196,20 +205,21 @@ async function handleDiffChanged(req, res, ctx) {
|
|
|
196
205
|
}
|
|
197
206
|
const { gitCwd } = cwdResult;
|
|
198
207
|
try {
|
|
208
|
+
await ensureGitRepository(gitCwd);
|
|
199
209
|
let files;
|
|
200
210
|
let truncated = false;
|
|
201
211
|
try {
|
|
202
212
|
const [nameStatusResult, numstatResult] = await Promise.all([
|
|
203
|
-
runGit(["diff", "HEAD", "--name-status", "--diff-filter=
|
|
204
|
-
runGit(["diff", "HEAD", "--numstat", "--diff-filter=
|
|
213
|
+
runGit(["diff", "HEAD", "--relative", "--name-status", "--diff-filter=MADRT", "--", "."], { cwd: gitCwd }),
|
|
214
|
+
runGit(["diff", "HEAD", "--relative", "--numstat", "--diff-filter=MADRT", "--", "."], { cwd: gitCwd })
|
|
205
215
|
]);
|
|
206
216
|
files = parseDiffFileList(nameStatusResult.stdout, numstatResult.stdout);
|
|
207
217
|
truncated = nameStatusResult.truncated || numstatResult.truncated;
|
|
208
218
|
} catch (err) {
|
|
209
219
|
if (!isNoHeadError(err)) throw err;
|
|
210
220
|
const [nsResult, nsNumstat] = await Promise.all([
|
|
211
|
-
runGit(["diff", "--cached", "--name-status", "--diff-filter=
|
|
212
|
-
runGit(["diff", "--cached", "--numstat", "--diff-filter=
|
|
221
|
+
runGit(["diff", "--cached", "--relative", "--name-status", "--diff-filter=MADRT", "--", "."], { cwd: gitCwd }),
|
|
222
|
+
runGit(["diff", "--cached", "--relative", "--numstat", "--diff-filter=MADRT", "--", "."], { cwd: gitCwd })
|
|
213
223
|
]);
|
|
214
224
|
files = parseDiffFileList(nsResult.stdout, nsNumstat.stdout);
|
|
215
225
|
truncated = nsResult.truncated || nsNumstat.truncated;
|
|
@@ -269,12 +279,12 @@ async function handleDiffFile(req, res, ctx) {
|
|
|
269
279
|
}
|
|
270
280
|
const { gitCwd } = cwdResult;
|
|
271
281
|
const rawFilePath = body.filePath;
|
|
272
|
-
const resolvedFilePath =
|
|
273
|
-
if (!resolvedFilePath.startsWith(gitCwd +
|
|
282
|
+
const resolvedFilePath = path2.resolve(gitCwd, rawFilePath);
|
|
283
|
+
if (!resolvedFilePath.startsWith(gitCwd + path2.sep) && resolvedFilePath !== gitCwd) {
|
|
274
284
|
ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
|
|
275
285
|
return;
|
|
276
286
|
}
|
|
277
|
-
const relativePath =
|
|
287
|
+
const relativePath = path2.normalize(rawFilePath);
|
|
278
288
|
if (relativePath.startsWith("..")) {
|
|
279
289
|
ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
|
|
280
290
|
return;
|
|
@@ -292,12 +302,12 @@ async function handleDiffFile(req, res, ctx) {
|
|
|
292
302
|
ctx.host.http.writeJson(res, 404, { error: "file_not_found" });
|
|
293
303
|
return;
|
|
294
304
|
}
|
|
295
|
-
if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd +
|
|
305
|
+
if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path2.sep)) {
|
|
296
306
|
ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
|
|
297
307
|
return;
|
|
298
308
|
}
|
|
299
309
|
const result2 = await runGit(
|
|
300
|
-
["diff", "--no-index", "--unified=3", "--", "/dev/null", relativePath],
|
|
310
|
+
["diff", "--no-index", "--relative", "--unified=3", "--", "/dev/null", relativePath],
|
|
301
311
|
{ cwd: gitCwd, allowExitCodes: [1] }
|
|
302
312
|
);
|
|
303
313
|
ctx.host.http.writeJson(res, 200, { content: result2.stdout, truncated: result2.truncated });
|
|
@@ -308,7 +318,7 @@ async function handleDiffFile(req, res, ctx) {
|
|
|
308
318
|
fs.realpath(gitCwd),
|
|
309
319
|
fs.realpath(resolvedFilePath)
|
|
310
320
|
]);
|
|
311
|
-
if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd +
|
|
321
|
+
if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path2.sep)) {
|
|
312
322
|
ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
|
|
313
323
|
return;
|
|
314
324
|
}
|
|
@@ -316,10 +326,10 @@ async function handleDiffFile(req, res, ctx) {
|
|
|
316
326
|
}
|
|
317
327
|
let result;
|
|
318
328
|
try {
|
|
319
|
-
result = await runGit(["diff", "HEAD", "--unified=3", "--", relativePath], { cwd: gitCwd });
|
|
329
|
+
result = await runGit(["diff", "HEAD", "--relative", "--unified=3", "--", literalPathspec(relativePath)], { cwd: gitCwd });
|
|
320
330
|
} catch (err) {
|
|
321
331
|
if (!isNoHeadError(err)) throw err;
|
|
322
|
-
result = await runGit(["diff", "--cached", "--unified=3", "--", relativePath], { cwd: gitCwd });
|
|
332
|
+
result = await runGit(["diff", "--cached", "--relative", "--unified=3", "--", literalPathspec(relativePath)], { cwd: gitCwd });
|
|
323
333
|
}
|
|
324
334
|
ctx.host.http.writeJson(res, 200, { content: result.stdout, truncated: result.truncated });
|
|
325
335
|
} catch (error) {
|
|
@@ -340,6 +350,22 @@ var REF_RE = /^[0-9a-f]{7,40}$/i;
|
|
|
340
350
|
function isPlainObject2(value) {
|
|
341
351
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
342
352
|
}
|
|
353
|
+
function parseCommitMeta(output) {
|
|
354
|
+
const fields = output.split("\0");
|
|
355
|
+
if (fields.length < 6) return null;
|
|
356
|
+
const [authorName, authorEmail, rawAuthorAt, subject, rawParents, ...bodyParts] = fields;
|
|
357
|
+
if (authorName === void 0 || authorEmail === void 0 || rawAuthorAt === void 0 || subject === void 0 || rawParents === void 0) return null;
|
|
358
|
+
const authorAt = Number.parseInt(rawAuthorAt, 10);
|
|
359
|
+
if (!Number.isFinite(authorAt)) return null;
|
|
360
|
+
return {
|
|
361
|
+
authorName,
|
|
362
|
+
authorEmail,
|
|
363
|
+
authorAt,
|
|
364
|
+
subject,
|
|
365
|
+
body: bodyParts.join("\0").trimEnd(),
|
|
366
|
+
parents: rawParents.split(" ").filter(Boolean).map((full) => ({ full, short: full.slice(0, 9) }))
|
|
367
|
+
};
|
|
368
|
+
}
|
|
343
369
|
async function handleDiffCommit(req, res, ctx) {
|
|
344
370
|
if (req.method !== "POST") {
|
|
345
371
|
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
@@ -377,8 +403,21 @@ async function handleDiffCommit(req, res, ctx) {
|
|
|
377
403
|
}
|
|
378
404
|
const { gitCwd } = cwdResult;
|
|
379
405
|
try {
|
|
380
|
-
const
|
|
381
|
-
|
|
406
|
+
const [metaResult, nameStatusResult, numstatResult] = await Promise.all([
|
|
407
|
+
runGit(["show", "-s", "--format=%an%x00%ae%x00%at%x00%s%x00%P%x00%b", ref], { cwd: gitCwd }),
|
|
408
|
+
runGit(["show", "--first-parent", "--format=", "--relative", "--name-status", "--diff-filter=MADRT", ref, "--", "."], { cwd: gitCwd }),
|
|
409
|
+
runGit(["show", "--first-parent", "--format=", "--relative", "--numstat", "--diff-filter=MADRT", ref, "--", "."], { cwd: gitCwd })
|
|
410
|
+
]);
|
|
411
|
+
const meta = parseCommitMeta(metaResult.stdout);
|
|
412
|
+
if (!meta) {
|
|
413
|
+
ctx.host.http.writeJson(res, 500, { error: "git_failed" });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
ctx.host.http.writeJson(res, 200, {
|
|
417
|
+
meta,
|
|
418
|
+
files: parseDiffFileList(nameStatusResult.stdout, numstatResult.stdout),
|
|
419
|
+
...metaResult.truncated || nameStatusResult.truncated || numstatResult.truncated ? { truncated: true } : {}
|
|
420
|
+
});
|
|
382
421
|
} catch (error) {
|
|
383
422
|
if (error instanceof GitExecutorError) {
|
|
384
423
|
if (error.code === "no_git_repo" || error.code === "git_unavailable") {
|
|
@@ -391,51 +430,19 @@ async function handleDiffCommit(req, res, ctx) {
|
|
|
391
430
|
throw error;
|
|
392
431
|
}
|
|
393
432
|
}
|
|
394
|
-
|
|
395
|
-
// ../fleet-plugins/diff/server/log.ts
|
|
396
433
|
function isPlainObject3(value) {
|
|
397
434
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
398
435
|
}
|
|
399
|
-
function
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (!trimmed) continue;
|
|
405
|
-
const lines = trimmed.split("\n");
|
|
406
|
-
const firstLine = lines[0] ?? "";
|
|
407
|
-
const fields = firstLine.split("\0");
|
|
408
|
-
const fullHash = fields[0] ?? "";
|
|
409
|
-
const shortHash = fields[1] ?? "";
|
|
410
|
-
const subject = fields[2] ?? "";
|
|
411
|
-
const authorName = fields[3] ?? "";
|
|
412
|
-
const relTime = fields[4] ?? "";
|
|
413
|
-
const refsRaw = fields[5] ?? "";
|
|
414
|
-
const parentsRaw = fields[6] ?? "";
|
|
415
|
-
if (!fullHash) continue;
|
|
416
|
-
const refs = refsRaw.split(",").map((r) => r.trim()).filter(Boolean);
|
|
417
|
-
const parents = parentsRaw.split(" ").map((p) => p.trim()).filter(Boolean);
|
|
418
|
-
let additions = 0;
|
|
419
|
-
let deletions = 0;
|
|
420
|
-
for (const line of lines.slice(1)) {
|
|
421
|
-
if (!line.trim()) continue;
|
|
422
|
-
const parts = line.split(" ");
|
|
423
|
-
if (parts.length < 2) continue;
|
|
424
|
-
const a = parseInt(parts[0] ?? "0", 10);
|
|
425
|
-
const d = parseInt(parts[1] ?? "0", 10);
|
|
426
|
-
if (!isNaN(a)) additions += a;
|
|
427
|
-
if (!isNaN(d)) deletions += d;
|
|
428
|
-
}
|
|
429
|
-
commits.push({ shortHash, fullHash, subject, authorName, relTime, refs, parents, additions, deletions });
|
|
430
|
-
}
|
|
431
|
-
return commits;
|
|
436
|
+
function resolveLiteralFilePath(gitCwd, rawPath) {
|
|
437
|
+
const relativePath = path2.normalize(rawPath);
|
|
438
|
+
const resolved = path2.resolve(gitCwd, relativePath);
|
|
439
|
+
if (relativePath === ".." || relativePath.startsWith(".." + path2.sep) || !resolved.startsWith(gitCwd + path2.sep)) return null;
|
|
440
|
+
return relativePath;
|
|
432
441
|
}
|
|
433
|
-
function
|
|
434
|
-
|
|
435
|
-
if (error.code !== "non_zero_exit") return false;
|
|
436
|
-
return error.stderr.includes("unknown revision") || error.stderr.includes("bad revision");
|
|
442
|
+
function literalPathspec2(relativePath) {
|
|
443
|
+
return `:(literal)${relativePath}`;
|
|
437
444
|
}
|
|
438
|
-
async function
|
|
445
|
+
async function handleDiffCommitFile(req, res, ctx) {
|
|
439
446
|
if (req.method !== "POST") {
|
|
440
447
|
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
441
448
|
return;
|
|
@@ -445,186 +452,163 @@ async function handleDiffLog(req, res, ctx) {
|
|
|
445
452
|
return;
|
|
446
453
|
}
|
|
447
454
|
const body = await ctx.host.http.readJsonBody(req);
|
|
448
|
-
if (!isPlainObject3(body)) {
|
|
455
|
+
if (!isPlainObject3(body) || typeof body.theaterId !== "string" || typeof body.ref !== "string" || typeof body.filePath !== "string") {
|
|
449
456
|
ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
|
|
450
457
|
return;
|
|
451
458
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
|
|
459
|
+
if (!REF_RE.test(body.ref)) {
|
|
460
|
+
ctx.host.http.writeJson(res, 400, { error: "invalid_ref" });
|
|
455
461
|
return;
|
|
456
462
|
}
|
|
457
|
-
|
|
463
|
+
if (!body.filePath) {
|
|
464
|
+
ctx.host.http.writeJson(res, 400, { error: "invalid_file_path" });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (body.oldPath !== void 0 && (typeof body.oldPath !== "string" || !body.oldPath)) {
|
|
468
|
+
ctx.host.http.writeJson(res, 400, { error: "invalid_file_path" });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
|
|
458
472
|
if (!theaterPath) {
|
|
459
473
|
ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
|
|
460
474
|
return;
|
|
461
475
|
}
|
|
462
|
-
const
|
|
463
|
-
const cwdResult = await resolveGitCwd(theaterPath, rawSubPath);
|
|
476
|
+
const cwdResult = await resolveGitCwd(theaterPath, typeof body.subPath === "string" ? body.subPath : "");
|
|
464
477
|
if (!cwdResult) {
|
|
465
478
|
ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
|
|
466
479
|
return;
|
|
467
480
|
}
|
|
468
|
-
const
|
|
481
|
+
const relativePath = resolveLiteralFilePath(cwdResult.gitCwd, body.filePath);
|
|
482
|
+
const oldPath = typeof body.oldPath === "string" ? resolveLiteralFilePath(cwdResult.gitCwd, body.oldPath) : void 0;
|
|
483
|
+
if (!relativePath || body.oldPath !== void 0 && !oldPath) {
|
|
484
|
+
ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
469
487
|
try {
|
|
470
|
-
const result = await runGit(
|
|
471
|
-
|
|
472
|
-
{ cwd: gitCwd }
|
|
473
|
-
);
|
|
474
|
-
const commits = parseLogOutput(result.stdout);
|
|
475
|
-
ctx.host.http.writeJson(res, 200, { commits, ...result.truncated ? { truncated: true } : {} });
|
|
488
|
+
const result = await runGit(oldPath ? ["show", "--first-parent", body.ref, "-M", "--format=", "--unified=3", "--", literalPathspec2(oldPath), literalPathspec2(relativePath)] : ["show", "--first-parent", body.ref, "--format=", "--unified=3", "--", literalPathspec2(relativePath)], { cwd: cwdResult.gitCwd });
|
|
489
|
+
ctx.host.http.writeJson(res, 200, { content: result.stdout, ...result.truncated ? { truncated: true } : {} });
|
|
476
490
|
} catch (error) {
|
|
477
491
|
if (error instanceof GitExecutorError) {
|
|
478
|
-
if (error.code === "no_git_repo") {
|
|
479
|
-
ctx.host.http.writeJson(res, 200, { commits: [] });
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
if (error.code === "git_unavailable") {
|
|
492
|
+
if (error.code === "no_git_repo" || error.code === "git_unavailable") {
|
|
483
493
|
ctx.host.http.writeJson(res, 422, { error: error.code });
|
|
484
494
|
return;
|
|
485
495
|
}
|
|
486
|
-
if (isNoHeadError2(error)) {
|
|
487
|
-
ctx.host.http.writeJson(res, 200, { commits: [] });
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
496
|
ctx.host.http.writeJson(res, 500, { error: "git_failed" });
|
|
491
497
|
return;
|
|
492
498
|
}
|
|
493
499
|
throw error;
|
|
494
500
|
}
|
|
495
501
|
}
|
|
496
|
-
var
|
|
497
|
-
var HARD_CAP_DEPTH = 8;
|
|
498
|
-
var DEFAULT_DEPTH = 3;
|
|
502
|
+
var WORKTREE_SHA_RE = /^[0-9a-f]{40}$/;
|
|
499
503
|
function isPlainObject4(value) {
|
|
500
504
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
501
505
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
506
|
+
function parseLogOutput(stdout) {
|
|
507
|
+
if (!stdout.trim()) return [];
|
|
508
|
+
const commits = [];
|
|
509
|
+
for (const chunk of stdout.split("")) {
|
|
510
|
+
const trimmed = chunk.trimStart();
|
|
511
|
+
if (!trimmed) continue;
|
|
512
|
+
const lines = trimmed.split("\n");
|
|
513
|
+
const firstLine = lines[0] ?? "";
|
|
514
|
+
const fields = firstLine.split("\0");
|
|
515
|
+
const fullHash = fields[0] ?? "";
|
|
516
|
+
const shortHash = fields[1] ?? "";
|
|
517
|
+
const subject = fields[2] ?? "";
|
|
518
|
+
const authorName = fields[3] ?? "";
|
|
519
|
+
const relTime = fields[4] ?? "";
|
|
520
|
+
const authorAt = Number.parseInt(fields[5] ?? "0", 10);
|
|
521
|
+
const refsRaw = fields[6] ?? "";
|
|
522
|
+
const parentsRaw = fields[7] ?? "";
|
|
523
|
+
if (!fullHash) continue;
|
|
524
|
+
const refs = refsRaw.split(",").map((r) => r.trim()).filter(Boolean);
|
|
525
|
+
const parents = parentsRaw.split(" ").map((p) => p.trim()).filter(Boolean);
|
|
526
|
+
commits.push({
|
|
527
|
+
shortHash,
|
|
528
|
+
fullHash,
|
|
529
|
+
subject,
|
|
530
|
+
authorName,
|
|
531
|
+
relTime,
|
|
532
|
+
authorAt: Number.isFinite(authorAt) ? authorAt : 0,
|
|
533
|
+
refs,
|
|
534
|
+
parents,
|
|
535
|
+
onHead: true
|
|
536
|
+
});
|
|
513
537
|
}
|
|
538
|
+
return commits;
|
|
514
539
|
}
|
|
515
|
-
async function
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
540
|
+
async function parseWorktreePorcelain(stdout, currentWorktreePath) {
|
|
541
|
+
const worktrees = [];
|
|
542
|
+
let worktreePath = null;
|
|
543
|
+
let sha = "";
|
|
544
|
+
let branch = null;
|
|
545
|
+
let prunable = false;
|
|
546
|
+
const pushCurrent = () => {
|
|
547
|
+
if (!worktreePath || !sha || prunable || /^0+$/.test(sha)) return;
|
|
548
|
+
worktrees.push({
|
|
549
|
+
sha,
|
|
550
|
+
branch,
|
|
551
|
+
worktreePath
|
|
552
|
+
});
|
|
553
|
+
};
|
|
554
|
+
for (const line of stdout.split("\n")) {
|
|
555
|
+
if (line.startsWith("worktree ")) {
|
|
556
|
+
pushCurrent();
|
|
557
|
+
worktreePath = line.slice(9);
|
|
558
|
+
sha = "";
|
|
559
|
+
branch = null;
|
|
560
|
+
prunable = false;
|
|
561
|
+
} else if (line.startsWith("HEAD ")) {
|
|
562
|
+
sha = line.slice(5);
|
|
563
|
+
} else if (line.startsWith("branch ")) {
|
|
564
|
+
const ref = line.slice(7);
|
|
565
|
+
branch = ref.startsWith("refs/heads/") ? ref.slice(11) : ref;
|
|
566
|
+
} else if (line === "prunable" || line.startsWith("prunable ")) {
|
|
567
|
+
prunable = true;
|
|
568
|
+
}
|
|
529
569
|
}
|
|
530
|
-
|
|
531
|
-
|
|
570
|
+
pushCurrent();
|
|
571
|
+
const normalizedCurrentWorktreePath = await normalizeWorktreePath(currentWorktreePath);
|
|
572
|
+
return Promise.all(worktrees.map(async ({ worktreePath: worktreePath2, ...checkout }) => ({
|
|
573
|
+
...checkout,
|
|
574
|
+
isCurrent: normalizedCurrentWorktreePath !== "" && await normalizeWorktreePath(worktreePath2) === normalizedCurrentWorktreePath
|
|
575
|
+
})));
|
|
576
|
+
}
|
|
577
|
+
function annotateHeadReachability(commits, revListStdout) {
|
|
578
|
+
const reachable = new Set(revListStdout.split("\n").map((line) => line.trim()).filter(Boolean));
|
|
579
|
+
if (reachable.size === 0) return commits;
|
|
580
|
+
return commits.map((commit) => ({ ...commit, onHead: reachable.has(commit.fullHash) }));
|
|
581
|
+
}
|
|
582
|
+
function isNoHeadError2(error) {
|
|
583
|
+
if (!(error instanceof GitExecutorError)) return false;
|
|
584
|
+
if (error.code !== "non_zero_exit") return false;
|
|
585
|
+
return error.stderr.includes("unknown revision") || error.stderr.includes("bad revision") || error.stderr.includes("does not have any commits");
|
|
586
|
+
}
|
|
587
|
+
async function normalizeWorktreePath(worktreePath) {
|
|
588
|
+
if (!worktreePath) return "";
|
|
532
589
|
try {
|
|
533
|
-
|
|
590
|
+
return await fs.realpath(worktreePath);
|
|
534
591
|
} catch {
|
|
535
|
-
|
|
592
|
+
return worktreePath;
|
|
536
593
|
}
|
|
537
|
-
return { isWorktree: true, parentAbs };
|
|
538
594
|
}
|
|
539
|
-
async function
|
|
540
|
-
if (repos.length >= cap) return true;
|
|
541
|
-
const gitPath = path3.join(dir, ".git");
|
|
595
|
+
async function readHeadRevList(gitCwd) {
|
|
542
596
|
try {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (gitStat.isFile()) {
|
|
548
|
-
const content = await fs.readFile(gitPath, "utf8");
|
|
549
|
-
const worktreeResult = await detectWorktreeParent(dir, content);
|
|
550
|
-
if (worktreeResult) {
|
|
551
|
-
repos.push({ relPath, name, branch, isWorktree: true, _wtParentAbs: worktreeResult.parentAbs });
|
|
552
|
-
} else {
|
|
553
|
-
repos.push({ relPath, name, branch });
|
|
554
|
-
}
|
|
555
|
-
} else {
|
|
556
|
-
repos.push({ relPath, name, branch });
|
|
557
|
-
}
|
|
558
|
-
if (repos.length >= cap) return true;
|
|
559
|
-
} catch {
|
|
597
|
+
return (await runGit(["rev-list", "-n", "1000", "HEAD"], { cwd: gitCwd })).stdout;
|
|
598
|
+
} catch (error) {
|
|
599
|
+
if (error instanceof GitExecutorError) return "";
|
|
600
|
+
throw error;
|
|
560
601
|
}
|
|
561
|
-
|
|
562
|
-
|
|
602
|
+
}
|
|
603
|
+
async function readCurrentWorktreePath(gitCwd) {
|
|
563
604
|
try {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (repos.length >= cap) return true;
|
|
569
|
-
if (dirent.isDirectory()) {
|
|
570
|
-
const childName = dirent.name;
|
|
571
|
-
if (childName === "node_modules" || childName === ".git") {
|
|
572
|
-
dirent = await dirHandle.read();
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
const childPath = path3.join(dir, childName);
|
|
576
|
-
let realChildPath;
|
|
577
|
-
try {
|
|
578
|
-
realChildPath = await fs.realpath(childPath);
|
|
579
|
-
} catch {
|
|
580
|
-
dirent = await dirHandle.read();
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
const normalizedReal = realTheaterPath.endsWith(path3.sep) ? realTheaterPath : realTheaterPath + path3.sep;
|
|
584
|
-
if (realChildPath !== realTheaterPath && !realChildPath.startsWith(normalizedReal)) {
|
|
585
|
-
dirent = await dirHandle.read();
|
|
586
|
-
continue;
|
|
587
|
-
}
|
|
588
|
-
const truncated = await scanRepos(theaterPath, realTheaterPath, childPath, currentDepth + 1, maxDepth, repos, cap);
|
|
589
|
-
if (truncated) return true;
|
|
590
|
-
}
|
|
591
|
-
dirent = await dirHandle.read();
|
|
592
|
-
}
|
|
593
|
-
return false;
|
|
594
|
-
} finally {
|
|
595
|
-
await dirHandle.close();
|
|
596
|
-
}
|
|
597
|
-
} catch {
|
|
598
|
-
if (dirHandle) {
|
|
599
|
-
try {
|
|
600
|
-
await dirHandle.close();
|
|
601
|
-
} catch {
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
return false;
|
|
605
|
+
return (await runGit(["rev-parse", "--show-toplevel"], { cwd: gitCwd })).stdout.trim();
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (error instanceof GitExecutorError) return "";
|
|
608
|
+
throw error;
|
|
605
609
|
}
|
|
606
610
|
}
|
|
607
|
-
function
|
|
608
|
-
const absToRelPath = /* @__PURE__ */ new Map();
|
|
609
|
-
for (const entry of raw) {
|
|
610
|
-
const absDir = path3.normalize(
|
|
611
|
-
entry.relPath === "" ? realTheaterPath : path3.join(realTheaterPath, entry.relPath)
|
|
612
|
-
);
|
|
613
|
-
absToRelPath.set(absDir, entry.relPath);
|
|
614
|
-
}
|
|
615
|
-
return raw.map((entry) => {
|
|
616
|
-
const { _wtParentAbs, ...rest } = entry;
|
|
617
|
-
if (_wtParentAbs && entry.isWorktree) {
|
|
618
|
-
const normalizedParent = path3.normalize(_wtParentAbs);
|
|
619
|
-
const parentRelPath = absToRelPath.get(normalizedParent);
|
|
620
|
-
if (parentRelPath !== void 0) {
|
|
621
|
-
return { ...rest, worktreeOf: parentRelPath };
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
return rest;
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
async function handleDiffRepos(req, res, ctx) {
|
|
611
|
+
async function handleDiffLog(req, res, ctx) {
|
|
628
612
|
if (req.method !== "POST") {
|
|
629
613
|
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
630
614
|
return;
|
|
@@ -648,35 +632,54 @@ async function handleDiffRepos(req, res, ctx) {
|
|
|
648
632
|
ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
|
|
649
633
|
return;
|
|
650
634
|
}
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
realTheaterPath = await fs.realpath(theaterPath);
|
|
656
|
-
} catch {
|
|
657
|
-
ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
|
|
635
|
+
const rawSubPath = typeof body.subPath === "string" ? body.subPath : "";
|
|
636
|
+
const cwdResult = await resolveGitCwd(theaterPath, rawSubPath);
|
|
637
|
+
if (!cwdResult) {
|
|
638
|
+
ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
|
|
658
639
|
return;
|
|
659
640
|
}
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
641
|
+
const { gitCwd } = cwdResult;
|
|
642
|
+
try {
|
|
643
|
+
const [worktrees, currentWorktreePath, headRevList] = await Promise.all([
|
|
644
|
+
runGit(["worktree", "list", "--porcelain"], { cwd: gitCwd }),
|
|
645
|
+
readCurrentWorktreePath(gitCwd),
|
|
646
|
+
readHeadRevList(gitCwd)
|
|
647
|
+
]);
|
|
648
|
+
const checkouts = await parseWorktreePorcelain(worktrees.stdout, currentWorktreePath);
|
|
649
|
+
const worktreeRevs = [...new Set(checkouts.map((checkout) => checkout.sha))].filter((sha) => WORKTREE_SHA_RE.test(sha) && !/^0+$/.test(sha));
|
|
650
|
+
const headRevs = headRevList ? ["HEAD"] : [];
|
|
651
|
+
const result = await runGit(
|
|
652
|
+
// --all은 refs/stash·refs/notes까지 그래프에 유입시키므로 브랜치/태그/원격 + 현재 HEAD + 워크트리 HEAD로 한정한다
|
|
653
|
+
["log", "--branches", "--tags", "--remotes", "--date-order", "-n", "200", "--relative", "--decorate=full", "--pretty=format:%x1e%H%x00%h%x00%s%x00%an%x00%ar%x00%at%x00%D%x00%P", ...headRevs, ...worktreeRevs, "--", "."],
|
|
654
|
+
{ cwd: gitCwd }
|
|
655
|
+
);
|
|
656
|
+
const commits = annotateHeadReachability(parseLogOutput(result.stdout), headRevList);
|
|
657
|
+
ctx.host.http.writeJson(res, 200, { commits, checkouts, ...result.truncated ? { truncated: true } : {} });
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (error instanceof GitExecutorError) {
|
|
660
|
+
if (error.code === "no_git_repo") {
|
|
661
|
+
ctx.host.http.writeJson(res, 200, { commits: [], checkouts: [] });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (error.code === "git_unavailable") {
|
|
665
|
+
ctx.host.http.writeJson(res, 422, { error: error.code });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (isNoHeadError2(error)) {
|
|
669
|
+
ctx.host.http.writeJson(res, 200, { commits: [], checkouts: [] });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
ctx.host.http.writeJson(res, 500, { error: "git_failed" });
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
670
677
|
}
|
|
671
678
|
|
|
672
679
|
// ../fleet-plugins/diff/routes.ts
|
|
673
680
|
var routes_default = definePlugin({
|
|
674
681
|
id: "diff",
|
|
675
682
|
register(ctx) {
|
|
676
|
-
registerRouter(ctx, "repos", async ({ req, res }) => {
|
|
677
|
-
await handleDiffRepos(req, res, ctx);
|
|
678
|
-
return true;
|
|
679
|
-
});
|
|
680
683
|
registerRouter(ctx, "changed", async ({ req, res }) => {
|
|
681
684
|
await handleDiffChanged(req, res, ctx);
|
|
682
685
|
return true;
|
|
@@ -693,6 +696,10 @@ var routes_default = definePlugin({
|
|
|
693
696
|
await handleDiffCommit(req, res, ctx);
|
|
694
697
|
return true;
|
|
695
698
|
});
|
|
699
|
+
registerRouter(ctx, "commit-file", async ({ req, res }) => {
|
|
700
|
+
await handleDiffCommitFile(req, res, ctx);
|
|
701
|
+
return true;
|
|
702
|
+
});
|
|
696
703
|
}
|
|
697
704
|
});
|
|
698
705
|
|