aws-runtime-bridge 1.7.30 → 1.7.32
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/dist/routes/file-browser.d.ts.map +1 -1
- package/dist/routes/file-browser.js +41 -3
- package/dist/routes/git.d.ts +20 -0
- package/dist/routes/git.d.ts.map +1 -1
- package/dist/routes/git.js +327 -35
- package/dist/routes/git.test.d.ts +0 -3
- package/dist/routes/git.test.d.ts.map +1 -1
- package/dist/routes/git.test.js +20 -153
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +126 -24
- package/dist/routes/instance.test.js +77 -0
- package/dist/services/workspace-files.d.ts +48 -0
- package/dist/services/workspace-files.d.ts.map +1 -1
- package/dist/services/workspace-files.js +194 -34
- package/dist/services/workspace-files.test.js +73 -2
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +5 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +30 -0
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-browser.d.ts","sourceRoot":"","sources":["../../src/routes/file-browser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,MAAM,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"file-browser.d.ts","sourceRoot":"","sources":["../../src/routes/file-browser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAyB5B,eAAO,MAAM,iBAAiB,4CAAW,CAAC;AAE1C,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAUhD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,mBAAmB;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B;AAKD,UAAU,iBAAkB,SAAQ,eAAe;IACjD,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,qBAAsB,SAAQ,mBAAmB;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,IAAI,EAAE,iBAAiB,EAAE,CAAC;CAC3B;AAED,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAiBD,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAUvI;AAED,wBAAgB,iCAAiC,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAgBzF;AAyFD;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,WAAW,SAAK,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAkB5F;AAoCD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CAuBpE;AAWD,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,OAAO,CAWhH"}
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 提供文件系统浏览功能
|
|
5
5
|
*/
|
|
6
|
-
import os from 'node:os';
|
|
7
6
|
import { createReadStream, promises as fs } from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { Router } from 'express';
|
|
10
10
|
import multer from 'multer';
|
|
11
11
|
import { allowHostFileBrowser } from '../config.js';
|
|
12
12
|
import { validateToken } from '../middleware/auth.js';
|
|
13
|
-
import {
|
|
14
|
-
import { createWorkspaceEntry, deleteWorkspaceEntry, extractWorkspaceArchive, listWorkspaceDirectory, moveWorkspaceEntry, previewWorkspaceDocument, readWorkspaceFile, renameWorkspaceEntry, resolveWorkspaceDownloadTarget, streamWorkspaceDirectoryZip, uploadWorkspaceFiles, writeWorkspaceFile, } from '../services/workspace-files.js';
|
|
13
|
+
import { chmodWorkspaceEntry, createWorkspaceEntry, deleteWorkspaceEntry, extractWorkspaceArchive, getWorkspaceEntryProperties, listWorkspaceDirectory, moveWorkspaceEntry, previewWorkspaceDocument, readWorkspaceFile, renameWorkspaceEntry, resolveWorkspaceDownloadTarget, streamWorkspaceDirectoryZip, uploadWorkspaceFiles, writeWorkspaceFile, } from '../services/workspace-files.js';
|
|
15
14
|
import { unwatchWorkspaceFile, watchWorkspaceFile } from '../services/workspace-watch.js';
|
|
15
|
+
import { createLogger } from '../utils/logger.js';
|
|
16
16
|
const log = createLogger('file-browser');
|
|
17
17
|
export const fileBrowserRouter = Router();
|
|
18
18
|
export const WORKSPACE_UPLOAD_FILE_LIMIT = 2000;
|
|
@@ -414,6 +414,44 @@ fileBrowserRouter.post('/workspace/read', validateToken, async (req, res) => {
|
|
|
414
414
|
res.status(400).json({ error: err.message });
|
|
415
415
|
}
|
|
416
416
|
});
|
|
417
|
+
/**
|
|
418
|
+
* 读取工作区文件或目录属性。
|
|
419
|
+
* POST /api/file-browser/workspace/properties
|
|
420
|
+
*/
|
|
421
|
+
fileBrowserRouter.post('/workspace/properties', validateToken, async (req, res) => {
|
|
422
|
+
try {
|
|
423
|
+
const { workspacePath, targetPath } = req.body || {};
|
|
424
|
+
res.json(await getWorkspaceEntryProperties({
|
|
425
|
+
workspacePath: String(workspacePath || ''),
|
|
426
|
+
targetPath: String(targetPath || '')
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
const err = error;
|
|
431
|
+
log.error('failed to get workspace entry properties:', err);
|
|
432
|
+
res.status(400).json({ error: err.message });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
/**
|
|
436
|
+
* 修改工作区文件或目录权限。
|
|
437
|
+
* POST /api/file-browser/workspace/chmod
|
|
438
|
+
*/
|
|
439
|
+
fileBrowserRouter.post('/workspace/chmod', validateToken, async (req, res) => {
|
|
440
|
+
try {
|
|
441
|
+
const { workspacePath, targetPath, mode, recursive } = req.body || {};
|
|
442
|
+
res.json(await chmodWorkspaceEntry({
|
|
443
|
+
workspacePath: String(workspacePath || ''),
|
|
444
|
+
targetPath: String(targetPath || ''),
|
|
445
|
+
mode: String(mode || ''),
|
|
446
|
+
recursive: Boolean(recursive)
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
const err = error;
|
|
451
|
+
log.error('failed to chmod workspace entry:', err);
|
|
452
|
+
res.status(400).json({ error: err.message });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
417
455
|
/**
|
|
418
456
|
* 在目录选择器当前位置创建文件夹。
|
|
419
457
|
* POST /api/file-browser/directory-picker/create
|
package/dist/routes/git.d.ts
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* 提供 Git Stash 快照的创建、列表、恢复和删除功能
|
|
5
5
|
*/
|
|
6
6
|
export declare const gitRouter: import("express-serve-static-core").Router;
|
|
7
|
+
/**
|
|
8
|
+
* 解析 Git 命令超时时间。
|
|
9
|
+
* 主流程:优先使用环境变量,非法或过小配置回落到默认值,避免 Git 检测请求长期挂起。
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveGitCommandTimeoutMs(): number;
|
|
12
|
+
interface GitRepositoryContext {
|
|
13
|
+
workspacePath: string;
|
|
14
|
+
repositoryRootPath: string | null;
|
|
15
|
+
hasRepository: boolean;
|
|
16
|
+
isGitRepo: boolean;
|
|
17
|
+
usingParentGitRepo: boolean;
|
|
18
|
+
relativeWorkspacePath: string;
|
|
19
|
+
}
|
|
7
20
|
export declare function assertGitWorkspacePathAccessible(workspacePath: string): Promise<void>;
|
|
8
21
|
type GitDiffFileStatus = 'modified' | 'added' | 'deleted' | 'renamed';
|
|
9
22
|
/**
|
|
@@ -31,5 +44,12 @@ interface LatestGitStash {
|
|
|
31
44
|
* 主流程:提取 stash 引用并保留后续描述,供无变更快照复用最近快照引用。
|
|
32
45
|
*/
|
|
33
46
|
export declare function parseLatestGitStash(output: string): LatestGitStash | null;
|
|
47
|
+
/**
|
|
48
|
+
* 解析 git diff 的 name-status 与 numstat 输出,生成前端文件树需要的汇总数据。
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseGitStatusStagedPaths(output: string): Set<string>;
|
|
51
|
+
export declare function normalizeGitCommitMessage(message: unknown): string;
|
|
52
|
+
export declare function createScopedGitPathspecArgs(context: Pick<GitRepositoryContext, 'relativeWorkspacePath'>): string[];
|
|
53
|
+
export declare function createScopedFilePathspecArgs(filePath: string, context: Pick<GitRepositoryContext, 'relativeWorkspacePath'>): string[];
|
|
34
54
|
export {};
|
|
35
55
|
//# sourceMappingURL=git.d.ts.map
|
package/dist/routes/git.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/routes/git.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/routes/git.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,eAAO,MAAM,SAAS,4CAAW,CAAC;AAIlC;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAOnD;AAED,UAAU,oBAAoB;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAsB,gCAAgC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe3F;AASD,KAAK,iBAAiB,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAkBtE;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,iBAAiB,EACzB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,OAAO,GACpB,OAAO,CAMT;AAED;;;GAGG;AACH,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK3E;AAED,wBAAgB,uCAAuC,IAAI,MAAM,CAEhE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAKjE;AAED,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAWzE;AA+KD;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAmBrE;AAkDD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAElE;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,uBAAuB,CAAC,GAAG,MAAM,EAAE,CAElH;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,uBAAuB,CAAC,GAAG,MAAM,EAAE,CAErI"}
|
package/dist/routes/git.js
CHANGED
|
@@ -8,7 +8,20 @@ import { spawn } from 'node:child_process';
|
|
|
8
8
|
import { promises as fs } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { validateToken } from '../middleware/auth.js';
|
|
11
|
+
import { logger } from '../utils/logger.js';
|
|
11
12
|
export const gitRouter = Router();
|
|
13
|
+
const DEFAULT_GIT_COMMAND_TIMEOUT_MS = 10_000;
|
|
14
|
+
/**
|
|
15
|
+
* 解析 Git 命令超时时间。
|
|
16
|
+
* 主流程:优先使用环境变量,非法或过小配置回落到默认值,避免 Git 检测请求长期挂起。
|
|
17
|
+
*/
|
|
18
|
+
export function resolveGitCommandTimeoutMs() {
|
|
19
|
+
const configured = Number(process.env.AWS_RUNTIME_GIT_COMMAND_TIMEOUT_MS);
|
|
20
|
+
if (Number.isFinite(configured) && configured >= 1_000) {
|
|
21
|
+
return configured;
|
|
22
|
+
}
|
|
23
|
+
return DEFAULT_GIT_COMMAND_TIMEOUT_MS;
|
|
24
|
+
}
|
|
12
25
|
export async function assertGitWorkspacePathAccessible(workspacePath) {
|
|
13
26
|
let stat;
|
|
14
27
|
try {
|
|
@@ -79,6 +92,18 @@ export function parseLatestGitStash(output) {
|
|
|
79
92
|
*/
|
|
80
93
|
async function execGitCommand(workspacePath, args) {
|
|
81
94
|
return new Promise((resolve) => {
|
|
95
|
+
let settled = false;
|
|
96
|
+
let timeout = null;
|
|
97
|
+
const finish = (result) => {
|
|
98
|
+
if (settled) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
settled = true;
|
|
102
|
+
if (timeout) {
|
|
103
|
+
clearTimeout(timeout);
|
|
104
|
+
}
|
|
105
|
+
resolve(result);
|
|
106
|
+
};
|
|
82
107
|
const child = spawn('git', args, {
|
|
83
108
|
cwd: workspacePath,
|
|
84
109
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -91,11 +116,21 @@ async function execGitCommand(workspacePath, args) {
|
|
|
91
116
|
child.stderr?.on('data', (chunk) => {
|
|
92
117
|
stderr += String(chunk || '');
|
|
93
118
|
});
|
|
119
|
+
timeout = setTimeout(() => {
|
|
120
|
+
const timeoutMs = resolveGitCommandTimeoutMs();
|
|
121
|
+
logger.warn(`[runtime-bridge] git command timed out after ${timeoutMs}ms: git ${args.join(' ')} (cwd=${workspacePath})`);
|
|
122
|
+
child.kill('SIGKILL');
|
|
123
|
+
finish({
|
|
124
|
+
stdout,
|
|
125
|
+
stderr: stderr || `git command timed out after ${timeoutMs}ms`,
|
|
126
|
+
exitCode: 124,
|
|
127
|
+
});
|
|
128
|
+
}, resolveGitCommandTimeoutMs());
|
|
94
129
|
child.on('error', (error) => {
|
|
95
|
-
|
|
130
|
+
finish({ stdout: '', stderr: error.message, exitCode: 1 });
|
|
96
131
|
});
|
|
97
132
|
child.on('close', (code) => {
|
|
98
|
-
|
|
133
|
+
finish({ stdout, stderr, exitCode: code ?? 1 });
|
|
99
134
|
});
|
|
100
135
|
});
|
|
101
136
|
}
|
|
@@ -197,6 +232,96 @@ function parseRenameDisplayPath(rawPath) {
|
|
|
197
232
|
}
|
|
198
233
|
return normalizedPath;
|
|
199
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* 解析 git diff 的 name-status 与 numstat 输出,生成前端文件树需要的汇总数据。
|
|
237
|
+
*/
|
|
238
|
+
export function parseGitStatusStagedPaths(output) {
|
|
239
|
+
const stagedPaths = new Set();
|
|
240
|
+
const lines = String(output || '').split('\n').filter(line => line.trim());
|
|
241
|
+
for (const line of lines) {
|
|
242
|
+
const indexStatus = line[0] || ' ';
|
|
243
|
+
if (indexStatus === ' ' || indexStatus === '?') {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const rawPath = line.slice(3).trim();
|
|
247
|
+
const pathParts = rawPath.split(' -> ');
|
|
248
|
+
const displayPath = parseRenameDisplayPath(pathParts[pathParts.length - 1] || rawPath);
|
|
249
|
+
if (displayPath) {
|
|
250
|
+
stagedPaths.add(displayPath);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return stagedPaths;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 解析 git diff 的 name-status 与 numstat 输出,生成前端文件树需要的汇总数据。
|
|
257
|
+
*/
|
|
258
|
+
function parseGitDiffSummary(nameStatusOutput, numstatOutput, stagedPaths = new Set()) {
|
|
259
|
+
const statusMap = new Map();
|
|
260
|
+
const nameStatusLines = nameStatusOutput.split('\n').filter(line => line.trim());
|
|
261
|
+
for (const line of nameStatusLines) {
|
|
262
|
+
const parts = line.split('\t');
|
|
263
|
+
if (parts.length < 2) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const status = resolveGitDiffStatus(parts[0]);
|
|
267
|
+
const pathIndex = status === 'renamed' && parts.length >= 3 ? 2 : 1;
|
|
268
|
+
const displayPath = parseRenameDisplayPath(parts[pathIndex]);
|
|
269
|
+
if (!displayPath) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
statusMap.set(displayPath, status);
|
|
273
|
+
}
|
|
274
|
+
const files = [];
|
|
275
|
+
const lines = numstatOutput.split('\n').filter(line => line.trim());
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
const parts = line.split('\t');
|
|
278
|
+
if (parts.length < 3)
|
|
279
|
+
continue;
|
|
280
|
+
const isBinaryDiff = parts[0] === '-' || parts[1] === '-';
|
|
281
|
+
const additions = parts[0] === '-' ? 0 : parseInt(parts[0], 10) || 0;
|
|
282
|
+
const deletions = parts[1] === '-' ? 0 : parseInt(parts[1], 10) || 0;
|
|
283
|
+
const filePath = parseRenameDisplayPath(parts.slice(2).join('\t').trim());
|
|
284
|
+
const status = statusMap.get(filePath) || 'modified';
|
|
285
|
+
if (!filePath || !shouldIncludeGitDiffSummaryFile(status, additions, deletions, isBinaryDiff)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
files.push({ path: filePath, status, additions, deletions, staged: stagedPaths.has(filePath) });
|
|
289
|
+
}
|
|
290
|
+
return files;
|
|
291
|
+
}
|
|
292
|
+
function normalizeCommitHash(commitHash) {
|
|
293
|
+
return String(commitHash || '').trim();
|
|
294
|
+
}
|
|
295
|
+
export function normalizeGitCommitMessage(message) {
|
|
296
|
+
return String(message || '').trim();
|
|
297
|
+
}
|
|
298
|
+
export function createScopedGitPathspecArgs(context) {
|
|
299
|
+
return ['--', context.relativeWorkspacePath || '.'];
|
|
300
|
+
}
|
|
301
|
+
export function createScopedFilePathspecArgs(filePath, context) {
|
|
302
|
+
return ['--', resolveRepositoryRelativeFilePath(filePath, context)];
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* 解析 git log 的 NUL 分隔输出,避免提交标题中的特殊字符破坏字段切分。
|
|
306
|
+
*/
|
|
307
|
+
function parseGitCommitLog(output) {
|
|
308
|
+
const fields = output.split('\0');
|
|
309
|
+
const commits = [];
|
|
310
|
+
for (let index = 0; index + 4 < fields.length; index += 5) {
|
|
311
|
+
const hash = fields[index]?.trim();
|
|
312
|
+
if (!hash) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
commits.push({
|
|
316
|
+
hash,
|
|
317
|
+
shortHash: fields[index + 1]?.trim() || hash.slice(0, 8),
|
|
318
|
+
authorName: fields[index + 2]?.trim() || 'unknown',
|
|
319
|
+
authorDate: fields[index + 3]?.trim() || '',
|
|
320
|
+
subject: fields[index + 4]?.trim() || hash,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return commits;
|
|
324
|
+
}
|
|
200
325
|
/**
|
|
201
326
|
* 检查路径是否为 git 仓库
|
|
202
327
|
* POST /runtime/git/check
|
|
@@ -469,38 +594,12 @@ gitRouter.post('/git/diff', validateToken, async (req, res) => {
|
|
|
469
594
|
res.status(400).json({ error: result.stderr || 'git diff failed' });
|
|
470
595
|
return;
|
|
471
596
|
}
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (parts.length < 2) {
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
const status = resolveGitDiffStatus(parts[0]);
|
|
480
|
-
const pathIndex = status === 'renamed' && parts.length >= 3 ? 2 : 1;
|
|
481
|
-
const displayPath = parseRenameDisplayPath(parts[pathIndex]);
|
|
482
|
-
if (!displayPath) {
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
statusMap.set(displayPath, status);
|
|
486
|
-
}
|
|
487
|
-
const files = [];
|
|
488
|
-
const lines = result.stdout.split('\n').filter(line => line.trim());
|
|
489
|
-
for (const line of lines) {
|
|
490
|
-
// 格式: "12\t5\tsrc/file.java"
|
|
491
|
-
const parts = line.split('\t');
|
|
492
|
-
if (parts.length < 3)
|
|
493
|
-
continue;
|
|
494
|
-
const isBinaryDiff = parts[0] === '-' || parts[1] === '-';
|
|
495
|
-
const additions = parts[0] === '-' ? 0 : parseInt(parts[0], 10) || 0;
|
|
496
|
-
const deletions = parts[1] === '-' ? 0 : parseInt(parts[1], 10) || 0;
|
|
497
|
-
const filePath = parseRenameDisplayPath(parts.slice(2).join('\t').trim());
|
|
498
|
-
const status = statusMap.get(filePath) || 'modified';
|
|
499
|
-
if (!filePath || !shouldIncludeGitDiffSummaryFile(status, additions, deletions, isBinaryDiff)) {
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
files.push({ path: filePath, status, additions, deletions });
|
|
597
|
+
const statusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['status', '--porcelain=v1'], context));
|
|
598
|
+
if (statusResult.exitCode !== 0) {
|
|
599
|
+
res.status(400).json({ error: statusResult.stderr || 'git status failed' });
|
|
600
|
+
return;
|
|
503
601
|
}
|
|
602
|
+
const files = parseGitDiffSummary(nameStatusResult.stdout, result.stdout, parseGitStatusStagedPaths(statusResult.stdout));
|
|
504
603
|
res.json({
|
|
505
604
|
ok: true,
|
|
506
605
|
isGitRepo: context.isGitRepo,
|
|
@@ -518,12 +617,200 @@ gitRouter.post('/git/diff', validateToken, async (req, res) => {
|
|
|
518
617
|
res.status(500).json({ error: err.message || 'git diff failed' });
|
|
519
618
|
}
|
|
520
619
|
});
|
|
620
|
+
/**
|
|
621
|
+
* 提交工作区未提交变更
|
|
622
|
+
* POST /runtime/git/commit
|
|
623
|
+
*/
|
|
624
|
+
gitRouter.post('/git/commit', validateToken, async (req, res) => {
|
|
625
|
+
const { workspacePath, repositoryPath, message } = req.body || {};
|
|
626
|
+
if (!workspacePath || !String(workspacePath).trim()) {
|
|
627
|
+
res.status(400).json({ error: 'workspacePath is required' });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const commitMessage = normalizeGitCommitMessage(message);
|
|
631
|
+
if (!commitMessage) {
|
|
632
|
+
res.status(400).json({ error: 'commit message is required' });
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
637
|
+
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
638
|
+
res.status(400).json({ error: 'workspace is not inside a git repository' });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const pathspecArgs = createScopedGitPathspecArgs(context);
|
|
642
|
+
const addResult = await execGitCommand(context.repositoryRootPath, ['add', '--all', ...pathspecArgs]);
|
|
643
|
+
if (addResult.exitCode !== 0) {
|
|
644
|
+
res.status(400).json({ error: addResult.stderr || 'git add failed' });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const stagedResult = await execGitCommand(context.repositoryRootPath, ['diff', '--cached', '--quiet', ...pathspecArgs]);
|
|
648
|
+
if (stagedResult.exitCode === 0) {
|
|
649
|
+
res.status(400).json({ error: '没有可提交的变更' });
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (stagedResult.exitCode !== 1) {
|
|
653
|
+
res.status(400).json({ error: stagedResult.stderr || 'git staged diff failed' });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const commitResult = await execGitCommand(context.repositoryRootPath, ['commit', '-m', commitMessage, ...pathspecArgs]);
|
|
657
|
+
if (commitResult.exitCode !== 0) {
|
|
658
|
+
res.status(400).json({ error: commitResult.stderr || commitResult.stdout || 'git commit failed' });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
res.json({
|
|
662
|
+
ok: true,
|
|
663
|
+
message: '提交完成',
|
|
664
|
+
output: commitResult.stdout,
|
|
665
|
+
workspacePath: context.workspacePath,
|
|
666
|
+
repositoryRootPath: context.repositoryRootPath,
|
|
667
|
+
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
const err = error;
|
|
672
|
+
res.status(500).json({ error: err.message || 'git commit failed' });
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
/**
|
|
676
|
+
* 将单个文件加入暂存区
|
|
677
|
+
* POST /runtime/git/add-file
|
|
678
|
+
*/
|
|
679
|
+
gitRouter.post('/git/add-file', validateToken, async (req, res) => {
|
|
680
|
+
const { workspacePath, repositoryPath, filePath } = req.body || {};
|
|
681
|
+
if (!workspacePath || !String(workspacePath).trim()) {
|
|
682
|
+
res.status(400).json({ error: 'workspacePath is required' });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const normalizedFilePath = normalizeGitPath(String(filePath || '').trim());
|
|
686
|
+
if (!normalizedFilePath) {
|
|
687
|
+
res.status(400).json({ error: 'filePath is required' });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
692
|
+
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
693
|
+
res.status(400).json({ error: 'workspace is not inside a git repository' });
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const addResult = await execGitCommand(context.repositoryRootPath, ['add', ...createScopedFilePathspecArgs(normalizedFilePath, context)]);
|
|
697
|
+
if (addResult.exitCode !== 0) {
|
|
698
|
+
res.status(400).json({ error: addResult.stderr || 'git add file failed' });
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
res.json({
|
|
702
|
+
ok: true,
|
|
703
|
+
filePath: normalizedFilePath,
|
|
704
|
+
workspacePath: context.workspacePath,
|
|
705
|
+
repositoryRootPath: context.repositoryRootPath,
|
|
706
|
+
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
const err = error;
|
|
711
|
+
res.status(500).json({ error: err.message || 'git add file failed' });
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
/**
|
|
715
|
+
* 获取工作区提交列表
|
|
716
|
+
* POST /runtime/git/commits
|
|
717
|
+
*/
|
|
718
|
+
gitRouter.post('/git/commits', validateToken, async (req, res) => {
|
|
719
|
+
const { workspacePath, repositoryPath, limit } = req.body || {};
|
|
720
|
+
if (!workspacePath || !String(workspacePath).trim()) {
|
|
721
|
+
res.status(400).json({ error: 'workspacePath is required' });
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
726
|
+
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
727
|
+
res.json({ ok: true, commits: [], workspacePath: context.workspacePath, repositoryRootPath: null });
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const parsedLimit = Number(limit);
|
|
731
|
+
const safeLimit = Number.isInteger(parsedLimit) && parsedLimit > 0 && parsedLimit <= 100 ? parsedLimit : 30;
|
|
732
|
+
const result = await execGitCommand(context.repositoryRootPath, [
|
|
733
|
+
'log',
|
|
734
|
+
`-${safeLimit}`,
|
|
735
|
+
'--date=iso-strict',
|
|
736
|
+
'--pretty=format:%H%x00%h%x00%an%x00%ad%x00%s%x00',
|
|
737
|
+
'--',
|
|
738
|
+
context.relativeWorkspacePath || '.',
|
|
739
|
+
]);
|
|
740
|
+
if (result.exitCode !== 0) {
|
|
741
|
+
res.status(400).json({ error: result.stderr || 'git log failed' });
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
res.json({
|
|
745
|
+
ok: true,
|
|
746
|
+
commits: parseGitCommitLog(result.stdout),
|
|
747
|
+
workspacePath: context.workspacePath,
|
|
748
|
+
repositoryRootPath: context.repositoryRootPath,
|
|
749
|
+
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
const err = error;
|
|
754
|
+
res.status(500).json({ error: err.message || 'git commits failed' });
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
/**
|
|
758
|
+
* 获取指定提交相对父提交的变更概览
|
|
759
|
+
* POST /runtime/git/commit-diff
|
|
760
|
+
*/
|
|
761
|
+
gitRouter.post('/git/commit-diff', validateToken, async (req, res) => {
|
|
762
|
+
const { workspacePath, repositoryPath, commitHash } = req.body || {};
|
|
763
|
+
if (!workspacePath || !String(workspacePath).trim()) {
|
|
764
|
+
res.status(400).json({ error: 'workspacePath is required' });
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const normalizedCommitHash = normalizeCommitHash(commitHash);
|
|
768
|
+
if (!normalizedCommitHash) {
|
|
769
|
+
res.status(400).json({ error: 'commitHash is required' });
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
774
|
+
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
775
|
+
res.status(400).json({ error: 'workspace is not inside a git repository' });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const diffBaseArgs = ['diff', `${normalizedCommitHash}^!`];
|
|
779
|
+
const nameStatusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs([...diffBaseArgs, '--name-status'], context));
|
|
780
|
+
if (nameStatusResult.exitCode !== 0) {
|
|
781
|
+
res.status(400).json({ error: nameStatusResult.stderr || 'git commit diff failed' });
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const numstatResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs([...diffBaseArgs, '--numstat'], context));
|
|
785
|
+
if (numstatResult.exitCode !== 0) {
|
|
786
|
+
res.status(400).json({ error: numstatResult.stderr || 'git commit diff failed' });
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const files = parseGitDiffSummary(nameStatusResult.stdout, numstatResult.stdout);
|
|
790
|
+
res.json({
|
|
791
|
+
ok: true,
|
|
792
|
+
commitHash: normalizedCommitHash,
|
|
793
|
+
isGitRepo: context.isGitRepo,
|
|
794
|
+
hasRepository: true,
|
|
795
|
+
usingParentGitRepo: context.usingParentGitRepo,
|
|
796
|
+
hasChanges: files.length > 0,
|
|
797
|
+
files,
|
|
798
|
+
workspacePath: context.workspacePath,
|
|
799
|
+
repositoryRootPath: context.repositoryRootPath,
|
|
800
|
+
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
const err = error;
|
|
805
|
+
res.status(500).json({ error: err.message || 'git commit diff failed' });
|
|
806
|
+
}
|
|
807
|
+
});
|
|
521
808
|
/**
|
|
522
809
|
* 获取单个文件的详细差异
|
|
523
810
|
* POST /runtime/git/diff-file
|
|
524
811
|
*/
|
|
525
812
|
gitRouter.post('/git/diff-file', validateToken, async (req, res) => {
|
|
526
|
-
const { workspacePath, filePath } = req.body || {};
|
|
813
|
+
const { workspacePath, filePath, commitHash } = req.body || {};
|
|
527
814
|
if (!workspacePath || !String(workspacePath).trim()) {
|
|
528
815
|
res.status(400).json({ error: 'workspacePath is required' });
|
|
529
816
|
return;
|
|
@@ -542,7 +829,11 @@ gitRouter.post('/git/diff-file', validateToken, async (req, res) => {
|
|
|
542
829
|
return;
|
|
543
830
|
}
|
|
544
831
|
const repositoryRelativeFilePath = resolveRepositoryRelativeFilePath(normalizedFilePath, context);
|
|
545
|
-
const
|
|
832
|
+
const normalizedCommitHash = normalizeCommitHash(commitHash);
|
|
833
|
+
const gitArgs = normalizedCommitHash
|
|
834
|
+
? ['show', '--format=', '--no-ext-diff', normalizedCommitHash, '--', repositoryRelativeFilePath]
|
|
835
|
+
: ['diff', '--', repositoryRelativeFilePath];
|
|
836
|
+
const result = await execGitCommand(context.repositoryRootPath, gitArgs);
|
|
546
837
|
if (result.exitCode !== 0) {
|
|
547
838
|
res.status(400).json({ error: result.stderr || 'git diff file failed' });
|
|
548
839
|
return;
|
|
@@ -551,6 +842,7 @@ gitRouter.post('/git/diff-file', validateToken, async (req, res) => {
|
|
|
551
842
|
ok: true,
|
|
552
843
|
filePath: normalizedFilePath,
|
|
553
844
|
repositoryRelativeFilePath,
|
|
845
|
+
commitHash: normalizedCommitHash || null,
|
|
554
846
|
diffContent: result.stdout,
|
|
555
847
|
workspacePath: context.workspacePath,
|
|
556
848
|
repositoryRootPath: context.repositoryRootPath,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.test.d.ts","sourceRoot":"","sources":["../../src/routes/git.test.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"git.test.d.ts","sourceRoot":"","sources":["../../src/routes/git.test.ts"],"names":[],"mappings":""}
|