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.
@@ -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;AAuB5B,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"}
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 { createLogger } from '../utils/logger.js';
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
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/routes/git.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,eAAO,MAAM,SAAS,4CAAW,CAAC;AAWlC,wBAAsB,gCAAgC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe3F;AASD,KAAK,iBAAiB,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAEtE;;;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"}
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"}
@@ -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
- resolve({ stdout: '', stderr: error.message, exitCode: 1 });
130
+ finish({ stdout: '', stderr: error.message, exitCode: 1 });
96
131
  });
97
132
  child.on('close', (code) => {
98
- resolve({ stdout, stderr, exitCode: code ?? 1 });
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 statusMap = new Map();
473
- const nameStatusLines = nameStatusResult.stdout.split('\n').filter(line => line.trim());
474
- for (const line of nameStatusLines) {
475
- const parts = line.split('\t');
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 result = await execGitCommand(context.repositoryRootPath, ['diff', '--', repositoryRelativeFilePath]);
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,5 +1,2 @@
1
- /**
2
- * Git 路由单元测试
3
- */
4
1
  export {};
5
2
  //# sourceMappingURL=git.test.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"git.test.d.ts","sourceRoot":"","sources":["../../src/routes/git.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
1
+ {"version":3,"file":"git.test.d.ts","sourceRoot":"","sources":["../../src/routes/git.test.ts"],"names":[],"mappings":""}