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.
- package/README.md +31 -0
- package/package.json +2 -2
- package/src/config.js +12 -2
- package/src/ui/public/assets/AgentView-D9y5maA3.js +4 -0
- package/src/ui/public/assets/AppVersionBadge-CsKq0HLG.js +2 -0
- package/src/ui/public/assets/BranchSelector-C09wdMBR.js +1 -0
- package/src/ui/public/assets/CommitForm-wTVAlj8K.js +6 -0
- package/src/ui/public/assets/CommonDialog-gF_3v5QY.js +1 -0
- package/src/ui/public/assets/EditorView-BYaXYY5H.js +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-DvSe3GqC.js +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-FYLZxzMC.js +21 -0
- package/src/ui/public/assets/LogList-BB56Ma_-.js +7 -0
- package/src/ui/public/assets/MindmapView-B57NJ-tK.js +2 -0
- package/src/ui/public/assets/MonitorView-DujOLv6G.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-B0vIxM8Y.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-Tdhzf-Jb.js +5 -0
- package/src/ui/public/assets/SourceMapView-Bf7__bEW.js +3 -0
- package/src/ui/public/assets/SvgIcon-DV0F5fm7.js +1 -0
- package/src/ui/public/assets/UserInputNode-ZatmWGxI.js +1 -0
- package/src/ui/public/assets/WorkbenchView-DVjvpQMi.js +28 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-DkdPQgM8.js +17 -0
- package/src/ui/public/assets/configStore-C2xoYf_U.js +1 -0
- package/src/ui/public/assets/element-plus-BrZIKMTV.js +47 -0
- package/src/ui/public/assets/{flow-mindmap-C0_IdibG.css → flow-mindmap-DnzM9SlK.css} +1 -1
- package/src/ui/public/assets/flow-mindmap-uF7lcnUF.js +139 -0
- package/src/ui/public/assets/index-DDa4fUq_.js +79 -0
- package/src/ui/public/assets/index-DkMUEB4_.css +1 -0
- package/src/ui/public/assets/office-docx-DZfJaVF6.js +32 -0
- package/src/ui/public/assets/office-excel-BhBLQBXN.js +170 -0
- package/src/ui/public/assets/office-pptx-C0yb5pVU.js +74 -0
- package/src/ui/public/assets/toolsStore-Dpb3rc-o.js +1 -0
- package/src/ui/public/assets/{vendor-Cx8PA1NP.js → vendor-BuJWEuf4.js} +43 -43
- package/src/ui/public/assets/vue-flow-CrEicpfB.js +39 -0
- package/src/ui/public/index.html +12 -12
- package/src/ui/server/index.js +9 -23
- package/src/ui/server/middleware/originGuard.js +138 -0
- package/src/ui/server/middleware/originGuard.test.js +125 -0
- package/src/ui/server/routes/fs.js +83 -3
- package/src/ui/server/routes/git/diff.js +12 -4
- package/src/ui/server/routes/git/stash.js +39 -29
- package/src/ui/server/routes/git/tags.js +23 -16
- package/src/ui/server/routes/git.js +17 -11
- package/src/ui/server/routes/gitOps.js +126 -47
- package/src/ui/server/routes/npm.js +36 -10
- package/src/ui/server/utils/gitArgs.js +165 -0
- package/src/ui/server/utils/gitArgs.test.js +215 -0
- package/src/ui/server/utils/officePreview.js +80 -0
- package/src/ui/server/utils/officePreview.test.js +21 -0
- package/src/ui/server/utils/startServerOnAvailablePort.js +20 -2
- package/src/ui/server/utils/startServerOnAvailablePort.test.js +181 -0
- package/src/ui/public/assets/AgentView-DPan52IZ.js +0 -4
- package/src/ui/public/assets/AppVersionBadge-CiorBQrl.js +0 -2
- package/src/ui/public/assets/BranchSelector-OQWGKJq8.js +0 -1
- package/src/ui/public/assets/CommitForm-Cq1UUNM4.js +0 -6
- package/src/ui/public/assets/CommonDialog-DJqOwkbm.js +0 -1
- package/src/ui/public/assets/EditorView-0HmbFAex.js +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-DjIAKDQt.js +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CCs7sMOO.js +0 -21
- package/src/ui/public/assets/LogList-De9E23D1.js +0 -7
- package/src/ui/public/assets/MindmapView-CpyaWr7b.js +0 -2
- package/src/ui/public/assets/MonitorView-DVwS51YV.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-DiaZ8xiI.js +0 -1
- package/src/ui/public/assets/RemoteRepoCard-B299cQhO.js +0 -5
- package/src/ui/public/assets/SourceMapView-C7vO_5Hz.js +0 -3
- package/src/ui/public/assets/SvgIcon-B9XhBoIQ.js +0 -1
- package/src/ui/public/assets/UserInputNode-CeNehn29.js +0 -1
- package/src/ui/public/assets/WorkbenchView-Be4GcjdO.js +0 -28
- package/src/ui/public/assets/_plugin-vue_export-helper-aENhMEEX.js +0 -17
- package/src/ui/public/assets/configStore-BvJZNumP.js +0 -1
- package/src/ui/public/assets/element-plus-B3pTS_5R.js +0 -32
- package/src/ui/public/assets/flow-mindmap-hUZCS6o1.js +0 -138
- package/src/ui/public/assets/index-CuD0oK0z.js +0 -79
- package/src/ui/public/assets/index-zmReBTKj.css +0 -1
- package/src/ui/public/assets/toolsStore-D9gXnqwO.js +0 -1
- package/src/ui/public/assets/vue-flow-COROsN-V.js +0 -39
|
@@ -24,6 +24,14 @@ import { registerAiDiffSummaryRoutes } from './git/aiDiffSummary.js';
|
|
|
24
24
|
import { createDiffHelpers } from './git/diffUtils.js';
|
|
25
25
|
import { registerGitStashRoutes } from './git/stash.js';
|
|
26
26
|
import { registerGitTagRoutes } from './git/tags.js';
|
|
27
|
+
import {
|
|
28
|
+
assertGitRef,
|
|
29
|
+
assertGitHash,
|
|
30
|
+
assertGitPath,
|
|
31
|
+
assertGitRemoteUrl,
|
|
32
|
+
assertGitConfigKey,
|
|
33
|
+
assertGitConfigValue
|
|
34
|
+
} from '../utils/gitArgs.js';
|
|
27
35
|
|
|
28
36
|
export function registerGitOpsRoutes({
|
|
29
37
|
app,
|
|
@@ -229,7 +237,10 @@ export function registerGitOpsRoutes({
|
|
|
229
237
|
await execGitCommand(['add', '--', filePath]);
|
|
230
238
|
res.json({ success: true });
|
|
231
239
|
} catch (error) {
|
|
232
|
-
res.status(500).json({
|
|
240
|
+
res.status(error?.statusCode || 500).json({
|
|
241
|
+
success: false,
|
|
242
|
+
error: error.message
|
|
243
|
+
});
|
|
233
244
|
}
|
|
234
245
|
});
|
|
235
246
|
|
|
@@ -259,7 +270,8 @@ export function registerGitOpsRoutes({
|
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
// 一次 git add 命令暂存所有文件(不再需要 shell 转义,execFile argv 数组天然支持任意路径)
|
|
262
|
-
|
|
273
|
+
// 前插 '--':文件名若以 - 开头会被 git 当成选项,-- 之后一律按路径处理
|
|
274
|
+
await execGitCommand(['add', '--', ...uniquePaths]);
|
|
263
275
|
|
|
264
276
|
res.json({
|
|
265
277
|
success: true,
|
|
@@ -331,6 +343,9 @@ export function registerGitOpsRoutes({
|
|
|
331
343
|
if (!branch || typeof branch !== 'string') {
|
|
332
344
|
return res.status(400).json({ success: false, error: '缺少 branch 参数' });
|
|
333
345
|
}
|
|
346
|
+
// branch 落在 git 的参数位置上,不做形状校验的话传 '--upload-pack=curl x|sh'
|
|
347
|
+
// 就能让 git 执行命令(execFile 无 shell 也拦不住)。
|
|
348
|
+
const safeBranch = assertGitRef(branch, '分支名');
|
|
334
349
|
// 防御性检查:空仓库(没有任何 commit)直接返回明确错误,
|
|
335
350
|
// 避免 git push -u 报 "src refspec master does not match any" 让用户摸不着头脑。
|
|
336
351
|
// git rev-parse --verify HEAD 在空仓库下返回非零退出 → execGitCommand 用 ignoreError
|
|
@@ -346,7 +361,7 @@ export function registerGitOpsRoutes({
|
|
|
346
361
|
errorCode: 'EMPTY_REPO'
|
|
347
362
|
});
|
|
348
363
|
}
|
|
349
|
-
const { stdout } = await execGitCommand(['push', '-u', 'origin',
|
|
364
|
+
const { stdout } = await execGitCommand(['push', '-u', 'origin', safeBranch]);
|
|
350
365
|
setRecentPushStatus({
|
|
351
366
|
justPushed: true,
|
|
352
367
|
pushTime: Date.now(),
|
|
@@ -355,7 +370,8 @@ export function registerGitOpsRoutes({
|
|
|
355
370
|
logger.info(`推送并设置上游成功: origin/${branch}`);
|
|
356
371
|
res.json({ success: true, message: stdout });
|
|
357
372
|
} catch (error) {
|
|
358
|
-
|
|
373
|
+
// assertGitRef 抛的是 HttpError(400),别吞成 500
|
|
374
|
+
res.status(error?.statusCode || 500).json({ success: false, error: error.message });
|
|
359
375
|
}
|
|
360
376
|
});
|
|
361
377
|
|
|
@@ -368,15 +384,16 @@ export function registerGitOpsRoutes({
|
|
|
368
384
|
if (!key) {
|
|
369
385
|
return res.status(400).json({ success: false, error: '缺少 key 参数' });
|
|
370
386
|
}
|
|
387
|
+
const safeKey = assertGitConfigKey(key);
|
|
371
388
|
try {
|
|
372
|
-
const { stdout } = await execGitCommand(['config', '--global', '--get',
|
|
389
|
+
const { stdout } = await execGitCommand(['config', '--global', '--get', safeKey], { log: false });
|
|
373
390
|
return res.json({ success: true, value: String(stdout || '').trim() });
|
|
374
391
|
} catch (err) {
|
|
375
392
|
// git --get 在 key 不存在时返回非零退出码 → 视为"未设置",返回空串
|
|
376
393
|
return res.json({ success: true, value: '' });
|
|
377
394
|
}
|
|
378
395
|
} catch (error) {
|
|
379
|
-
res.status(500).json({ success: false, error: error.message });
|
|
396
|
+
res.status(error?.statusCode || 500).json({ success: false, error: error.message });
|
|
380
397
|
}
|
|
381
398
|
});
|
|
382
399
|
|
|
@@ -389,15 +406,18 @@ export function registerGitOpsRoutes({
|
|
|
389
406
|
if (!key || typeof key !== 'string') {
|
|
390
407
|
return res.status(400).json({ success: false, error: '缺少 key 参数' });
|
|
391
408
|
}
|
|
392
|
-
|
|
409
|
+
// key 走白名单(挡住 core.editor / core.pager / http.*.extraheader / alias.*),
|
|
410
|
+
// value 挡换行 —— 否则值里的 \n 会在 .gitconfig 里凭空造出一行新 key
|
|
411
|
+
const safeKey = assertGitConfigKey(key);
|
|
412
|
+
const trimmed = assertGitConfigValue(value);
|
|
393
413
|
if (!trimmed) {
|
|
394
|
-
await execGitCommand(['config', '--global', '--unset',
|
|
414
|
+
await execGitCommand(['config', '--global', '--unset', safeKey]);
|
|
395
415
|
} else {
|
|
396
|
-
await execGitCommand(['config', '--global',
|
|
416
|
+
await execGitCommand(['config', '--global', safeKey, trimmed]);
|
|
397
417
|
}
|
|
398
418
|
res.json({ success: true });
|
|
399
419
|
} catch (error) {
|
|
400
|
-
res.status(500).json({ success: false, error: error.message });
|
|
420
|
+
res.status(error?.statusCode || 500).json({ success: false, error: error.message });
|
|
401
421
|
}
|
|
402
422
|
});
|
|
403
423
|
|
|
@@ -409,9 +429,18 @@ export function registerGitOpsRoutes({
|
|
|
409
429
|
res.setHeader('Connection', 'keep-alive');
|
|
410
430
|
res.flushHeaders();
|
|
411
431
|
|
|
432
|
+
// 客户端断开后 res.write 抛 EPIPE,若不吞掉,异常会从 gitPush.stderr 的
|
|
433
|
+
// 'data' 监听器冒泡成 uncaughtException → index.js 的 fatalExit → 整个服务退出。
|
|
434
|
+
let clientClosed = false;
|
|
412
435
|
const sendProgress = (data) => {
|
|
413
|
-
|
|
436
|
+
if (clientClosed) return;
|
|
437
|
+
try {
|
|
438
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
439
|
+
} catch {
|
|
440
|
+
// res 已关闭,忽略
|
|
441
|
+
}
|
|
414
442
|
};
|
|
443
|
+
const finish = () => { try { res.end(); } catch { /* ignore */ } };
|
|
415
444
|
|
|
416
445
|
try {
|
|
417
446
|
// 获取当前工作目录 - 与execGitCommand保持一致
|
|
@@ -435,7 +464,7 @@ export function registerGitOpsRoutes({
|
|
|
435
464
|
error: '当前仓库没有任何提交,请先在左侧暂存并提交至少一个文件后再推送。',
|
|
436
465
|
errorCode: 'EMPTY_REPO'
|
|
437
466
|
});
|
|
438
|
-
return
|
|
467
|
+
return finish();
|
|
439
468
|
}
|
|
440
469
|
|
|
441
470
|
logger.info('开始推送,工作目录:', workDir);
|
|
@@ -558,7 +587,7 @@ export function registerGitOpsRoutes({
|
|
|
558
587
|
error: errorOutput || standardOutput || `Push failed with code ${code}`
|
|
559
588
|
});
|
|
560
589
|
}
|
|
561
|
-
|
|
590
|
+
finish();
|
|
562
591
|
});
|
|
563
592
|
|
|
564
593
|
gitPush.on('error', (error) => {
|
|
@@ -581,7 +610,27 @@ export function registerGitOpsRoutes({
|
|
|
581
610
|
success: false,
|
|
582
611
|
error: error.message
|
|
583
612
|
});
|
|
584
|
-
|
|
613
|
+
finish();
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// 客户端断连 → kill 掉 git push。推送在远端是原子的(要么整体接受要么整体拒绝),
|
|
617
|
+
// 中断不会留下半截引用;不 kill 则会残留孤儿进程直到推送结束。
|
|
618
|
+
req.on('close', () => {
|
|
619
|
+
if (clientClosed) return;
|
|
620
|
+
clientClosed = true;
|
|
621
|
+
if (gitPush && !gitPush.killed) {
|
|
622
|
+
try {
|
|
623
|
+
gitPush.kill('SIGTERM');
|
|
624
|
+
setTimeout(() => {
|
|
625
|
+
if (!gitPush.killed) {
|
|
626
|
+
try { gitPush.kill('SIGKILL'); } catch { /* ignore */ }
|
|
627
|
+
}
|
|
628
|
+
}, 2000);
|
|
629
|
+
} catch (e) {
|
|
630
|
+
logger.warn('[push-with-progress] 客户端断连后 kill 子进程失败:', e?.message || e);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
finish();
|
|
585
634
|
});
|
|
586
635
|
|
|
587
636
|
} catch (error) {
|
|
@@ -599,7 +648,7 @@ export function registerGitOpsRoutes({
|
|
|
599
648
|
success: false,
|
|
600
649
|
error: error.message
|
|
601
650
|
});
|
|
602
|
-
|
|
651
|
+
finish();
|
|
603
652
|
}
|
|
604
653
|
});
|
|
605
654
|
|
|
@@ -657,7 +706,11 @@ export function registerGitOpsRoutes({
|
|
|
657
706
|
const message = req.query.message || '';
|
|
658
707
|
const dateFrom = req.query.dateFrom || '';
|
|
659
708
|
const dateTo = req.query.dateTo || '';
|
|
660
|
-
|
|
709
|
+
// branch 最终会原样进 git 的参数位(见 executeGitLogCommand:含 '/' 的不加
|
|
710
|
+
// refs/heads/ 前缀),所以 '--output=/path' 这类字符串能被 git 解析成选项。
|
|
711
|
+
const branch = req.query.branch
|
|
712
|
+
? req.query.branch.split(',').map(b => b.trim()).filter(Boolean).map(b => assertGitRef(b, '分支名'))
|
|
713
|
+
: [];
|
|
661
714
|
const withParents = req.query.with_parents === 'true';
|
|
662
715
|
|
|
663
716
|
// 如果指定了分支,直接走 executeGitLogCommand 处理 refs/ 前缀
|
|
@@ -716,7 +769,8 @@ export function registerGitOpsRoutes({
|
|
|
716
769
|
processAndSendLogOutput(res, logOutput, page, limit, withParents);
|
|
717
770
|
} catch (error) {
|
|
718
771
|
logger.error('获取Git日志失败:', error);
|
|
719
|
-
|
|
772
|
+
// 参数校验失败是 HttpError(400),不该被一律吞成 500
|
|
773
|
+
res.status(error?.statusCode || 500).json({ error: '获取日志失败: ' + error.message });
|
|
720
774
|
}
|
|
721
775
|
});
|
|
722
776
|
|
|
@@ -785,7 +839,7 @@ export function registerGitOpsRoutes({
|
|
|
785
839
|
processAndSendLogOutput(res, logOutput, skip / limit + 1, limit, withParents);
|
|
786
840
|
} catch (error) {
|
|
787
841
|
logger.error('执行Git日志命令失败:', error);
|
|
788
|
-
res.status(500).json({ error: '获取日志失败: ' + error.message });
|
|
842
|
+
res.status(error?.statusCode || 500).json({ error: '获取日志失败: ' + error.message });
|
|
789
843
|
}
|
|
790
844
|
}
|
|
791
845
|
|
|
@@ -891,6 +945,8 @@ export function registerGitOpsRoutes({
|
|
|
891
945
|
error: '缺少分支名称参数'
|
|
892
946
|
});
|
|
893
947
|
}
|
|
948
|
+
// 拼成 origin/<branch> 后进 git 参数位,同样要过 ref 校验
|
|
949
|
+
const safeBranch = assertGitRef(branch, '分支名');
|
|
894
950
|
|
|
895
951
|
// 尝试清理 Git 锁文件(破坏性入口:force=true 绕过 mtime 判断,
|
|
896
952
|
// 仍走 PID liveness 校验——用户主动触发的 reset --hard 不该被
|
|
@@ -898,11 +954,11 @@ export function registerGitOpsRoutes({
|
|
|
898
954
|
await checkAndClearGitLock({ force: true });
|
|
899
955
|
|
|
900
956
|
// 执行 git reset --hard origin/branch 命令
|
|
901
|
-
await execGitCommand(['reset', '--hard', `origin/${
|
|
957
|
+
await execGitCommand(['reset', '--hard', `origin/${safeBranch}`]);
|
|
902
958
|
res.json({ success: true });
|
|
903
959
|
} catch (error) {
|
|
904
960
|
logger.error('重置到远程分支失败:', error);
|
|
905
|
-
res.status(500).json({
|
|
961
|
+
res.status(error?.statusCode || 500).json({
|
|
906
962
|
success: false,
|
|
907
963
|
error: `重置到远程分支失败: ${error.message}`
|
|
908
964
|
});
|
|
@@ -944,11 +1000,12 @@ export function registerGitOpsRoutes({
|
|
|
944
1000
|
error: '缺少提交哈希参数'
|
|
945
1001
|
});
|
|
946
1002
|
}
|
|
1003
|
+
const safeHash = assertGitHash(hash);
|
|
947
1004
|
|
|
948
|
-
logger.info(`获取提交文件列表: hash=${
|
|
1005
|
+
logger.info(`获取提交文件列表: hash=${safeHash}`);
|
|
949
1006
|
|
|
950
1007
|
// 执行命令获取提交中修改的文件列表
|
|
951
|
-
const { stdout } = await execGitCommand(['show', '--name-only', '--format=',
|
|
1008
|
+
const { stdout } = await execGitCommand(['show', '--name-only', '--format=', safeHash]);
|
|
952
1009
|
|
|
953
1010
|
// 将输出按行分割,并过滤掉空行
|
|
954
1011
|
const files = stdout.split('\n').filter(line => line.trim());
|
|
@@ -960,7 +1017,7 @@ export function registerGitOpsRoutes({
|
|
|
960
1017
|
});
|
|
961
1018
|
} catch (error) {
|
|
962
1019
|
logger.error('获取提交文件列表失败:', error);
|
|
963
|
-
res.status(500).json({
|
|
1020
|
+
res.status(error?.statusCode || 500).json({
|
|
964
1021
|
success: false,
|
|
965
1022
|
error: `获取提交文件列表失败: ${error.message}`
|
|
966
1023
|
});
|
|
@@ -980,14 +1037,20 @@ export function registerGitOpsRoutes({
|
|
|
980
1037
|
});
|
|
981
1038
|
}
|
|
982
1039
|
|
|
983
|
-
|
|
1040
|
+
// hash 落在 `--` 之前,会被 git 当 rev 解析;以 - 开头就变成选项了
|
|
1041
|
+
// (git show 有 --output=,能写任意文件)。filePath 虽有 -- 保护,
|
|
1042
|
+
// 但仍要挡住绝对路径与 .. 上跳。
|
|
1043
|
+
const safeHash = assertGitHash(hash);
|
|
1044
|
+
const safePath = assertGitPath(String(filePath).replace(/^\/+/, ''));
|
|
1045
|
+
|
|
1046
|
+
logger.info(`获取提交文件差异: hash=${safeHash}, file=${safePath}`);
|
|
984
1047
|
|
|
985
|
-
const diffCommandArgs = ['show',
|
|
1048
|
+
const diffCommandArgs = ['show', safeHash, '--', safePath];
|
|
986
1049
|
|
|
987
1050
|
// 使用优化的检查函数
|
|
988
1051
|
// checkShouldSkipDiff 接受字符串命令,这里只用于大小判断,先按字面占位;
|
|
989
1052
|
// 走 execGitCommand 时仍用 argv 数组保证转义正确。
|
|
990
|
-
const skipCheck = await checkShouldSkipDiff(
|
|
1053
|
+
const skipCheck = await checkShouldSkipDiff(safePath, `git show ${safeHash} -- "${safePath}"`);
|
|
991
1054
|
if (skipCheck.shouldSkip) {
|
|
992
1055
|
return res.json({
|
|
993
1056
|
success: true,
|
|
@@ -1021,7 +1084,7 @@ export function registerGitOpsRoutes({
|
|
|
1021
1084
|
});
|
|
1022
1085
|
} catch (error) {
|
|
1023
1086
|
logger.error('获取提交文件差异失败:', error);
|
|
1024
|
-
res.status(500).json({
|
|
1087
|
+
res.status(error?.statusCode || 500).json({
|
|
1025
1088
|
success: false,
|
|
1026
1089
|
error: `获取提交文件差异失败: ${error.message}`
|
|
1027
1090
|
});
|
|
@@ -1041,8 +1104,16 @@ export function registerGitOpsRoutes({
|
|
|
1041
1104
|
});
|
|
1042
1105
|
}
|
|
1043
1106
|
|
|
1107
|
+
// hash 与 filePath 都要先过校验:spec 是 `<rev>:<path>` 复合形式,
|
|
1108
|
+
// 任一环节带 - 开头都会被 git 当选项。hash 只接受十六进制(^ 由下面自己拼,
|
|
1109
|
+
// 不交给外部传入),filePath 只接受仓库内的相对路径。
|
|
1110
|
+
const safeHash = assertGitHash(hash);
|
|
1111
|
+
// git 的 <rev>:<path> 里 /src/foo.ts 与 src/foo.ts 等价(都相对仓库根),
|
|
1112
|
+
// 去掉前导 / 再校验,避免正常路径被误拒
|
|
1113
|
+
const safePath = assertGitPath(String(filePath).replace(/^\/+/, ''));
|
|
1114
|
+
|
|
1044
1115
|
const isOld = String(version || 'new') === 'old';
|
|
1045
|
-
const targetHash = isOld ? `${
|
|
1116
|
+
const targetHash = isOld ? `${safeHash}^` : `${safeHash}`;
|
|
1046
1117
|
|
|
1047
1118
|
const skipExtensions = /\.(min\.js|umd\.cjs|bundle\.js|dist\.js|prod\.js|map|wasm|exe|dll|so|dylib|bin|zip|tar|gz|rar|7z|jar|war|ear|pdf|doc|docx|xls|xlsx|ppt|pptx|jpg|jpeg|png|gif|bmp|ico|mp3|mp4|avi|mov|wmv|flv|webm|mkv|ttf|woff|woff2|eot|otf)$/i;
|
|
1048
1119
|
if (skipExtensions.test(String(filePath))) {
|
|
@@ -1053,7 +1124,7 @@ export function registerGitOpsRoutes({
|
|
|
1053
1124
|
});
|
|
1054
1125
|
}
|
|
1055
1126
|
|
|
1056
|
-
const spec = `${targetHash}:${
|
|
1127
|
+
const spec = `${targetHash}:${safePath}`;
|
|
1057
1128
|
|
|
1058
1129
|
let sizeBytes = 0;
|
|
1059
1130
|
try {
|
|
@@ -1086,13 +1157,14 @@ export function registerGitOpsRoutes({
|
|
|
1086
1157
|
content: stdout ?? ''
|
|
1087
1158
|
});
|
|
1088
1159
|
} catch (error) {
|
|
1089
|
-
res.status(500).json({
|
|
1160
|
+
res.status(error?.statusCode || 500).json({
|
|
1090
1161
|
success: false,
|
|
1091
1162
|
error: `获取文件内容失败: ${error.message}`
|
|
1092
1163
|
});
|
|
1093
1164
|
}
|
|
1094
1165
|
} catch (error) {
|
|
1095
|
-
|
|
1166
|
+
// assertGitHash / assertGitPath 抛的是 HttpError(400),别吞成 500
|
|
1167
|
+
res.status(error?.statusCode || 500).json({
|
|
1096
1168
|
success: false,
|
|
1097
1169
|
error: error.message
|
|
1098
1170
|
});
|
|
@@ -1111,18 +1183,20 @@ export function registerGitOpsRoutes({
|
|
|
1111
1183
|
});
|
|
1112
1184
|
}
|
|
1113
1185
|
|
|
1114
|
-
|
|
1186
|
+
const safeHash = assertGitHash(hash);
|
|
1187
|
+
logger.info(`执行撤销提交操作: hash=${safeHash}`);
|
|
1115
1188
|
|
|
1116
1189
|
// 执行git revert命令
|
|
1117
|
-
await execGitCommand(['revert', '--no-edit',
|
|
1190
|
+
await execGitCommand(['revert', '--no-edit', safeHash]);
|
|
1118
1191
|
|
|
1119
1192
|
res.json({
|
|
1120
1193
|
success: true,
|
|
1121
|
-
message: `已成功撤销提交 ${
|
|
1194
|
+
message: `已成功撤销提交 ${safeHash}`
|
|
1122
1195
|
});
|
|
1123
1196
|
} catch (error) {
|
|
1124
1197
|
logger.error('撤销提交失败:', error);
|
|
1125
|
-
|
|
1198
|
+
// 校验失败是 HttpError(400),不该被一律吞成 500
|
|
1199
|
+
res.status(error?.statusCode || 500).json({
|
|
1126
1200
|
success: false,
|
|
1127
1201
|
error: `撤销提交失败: ${error.message}`
|
|
1128
1202
|
});
|
|
@@ -1141,18 +1215,19 @@ export function registerGitOpsRoutes({
|
|
|
1141
1215
|
});
|
|
1142
1216
|
}
|
|
1143
1217
|
|
|
1144
|
-
|
|
1218
|
+
const safeHash = assertGitHash(hash);
|
|
1219
|
+
logger.info(`执行Cherry-pick操作: hash=${safeHash}`);
|
|
1145
1220
|
|
|
1146
1221
|
// 执行git cherry-pick命令
|
|
1147
|
-
await execGitCommand(['cherry-pick',
|
|
1222
|
+
await execGitCommand(['cherry-pick', safeHash]);
|
|
1148
1223
|
|
|
1149
1224
|
res.json({
|
|
1150
1225
|
success: true,
|
|
1151
|
-
message: `已成功Cherry-pick提交 ${
|
|
1226
|
+
message: `已成功Cherry-pick提交 ${safeHash}`
|
|
1152
1227
|
});
|
|
1153
1228
|
} catch (error) {
|
|
1154
1229
|
logger.error('Cherry-pick提交失败:', error);
|
|
1155
|
-
res.status(500).json({
|
|
1230
|
+
res.status(error?.statusCode || 500).json({
|
|
1156
1231
|
success: false,
|
|
1157
1232
|
error: `Cherry-pick提交失败: ${error.message}`
|
|
1158
1233
|
});
|
|
@@ -1171,18 +1246,19 @@ export function registerGitOpsRoutes({
|
|
|
1171
1246
|
});
|
|
1172
1247
|
}
|
|
1173
1248
|
|
|
1174
|
-
|
|
1249
|
+
const safeHash = assertGitHash(hash);
|
|
1250
|
+
logger.info(`执行重置到指定提交操作: hash=${safeHash}`);
|
|
1175
1251
|
|
|
1176
1252
|
// 执行git reset --hard命令
|
|
1177
|
-
await execGitCommand(['reset', '--hard',
|
|
1253
|
+
await execGitCommand(['reset', '--hard', safeHash]);
|
|
1178
1254
|
|
|
1179
1255
|
res.json({
|
|
1180
1256
|
success: true,
|
|
1181
|
-
message: `已成功重置到提交 ${
|
|
1257
|
+
message: `已成功重置到提交 ${safeHash}`
|
|
1182
1258
|
});
|
|
1183
1259
|
} catch (error) {
|
|
1184
1260
|
logger.error('重置到指定提交失败:', error);
|
|
1185
|
-
res.status(500).json({
|
|
1261
|
+
res.status(error?.statusCode || 500).json({
|
|
1186
1262
|
success: false,
|
|
1187
1263
|
error: `重置到指定提交失败: ${error.message}`
|
|
1188
1264
|
});
|
|
@@ -1196,13 +1272,14 @@ export function registerGitOpsRoutes({
|
|
|
1196
1272
|
if (!hash) {
|
|
1197
1273
|
return res.status(400).json({ success: false, error: '缺少提交哈希参数' });
|
|
1198
1274
|
}
|
|
1199
|
-
const
|
|
1275
|
+
const safeHash = assertGitHash(hash);
|
|
1276
|
+
const { stdout } = await execGitCommand(['show', safeHash]);
|
|
1200
1277
|
// 限制最大 200KB 防止内容过大
|
|
1201
1278
|
const MAX = 200 * 1024;
|
|
1202
1279
|
const content = stdout.length > MAX ? stdout.slice(0, MAX) + '\n\n[内容过大,已截断]' : stdout;
|
|
1203
1280
|
res.json({ success: true, content });
|
|
1204
1281
|
} catch (error) {
|
|
1205
|
-
res.status(500).json({ success: false, error: `获取提交内容失败: ${error.message}` });
|
|
1282
|
+
res.status(error?.statusCode || 500).json({ success: false, error: `获取提交内容失败: ${error.message}` });
|
|
1206
1283
|
}
|
|
1207
1284
|
});
|
|
1208
1285
|
|
|
@@ -1311,11 +1388,13 @@ export function registerGitOpsRoutes({
|
|
|
1311
1388
|
if (!url || !url.trim()) {
|
|
1312
1389
|
return res.json({ success: false, error: '远程仓库地址不能为空' });
|
|
1313
1390
|
}
|
|
1314
|
-
|
|
1391
|
+
const safeName = assertGitRef(name, '远程仓库名');
|
|
1392
|
+
const safeUrl = assertGitRemoteUrl(url);
|
|
1393
|
+
await execGitCommand(['remote', 'add', safeName, safeUrl]);
|
|
1315
1394
|
res.json({ success: true });
|
|
1316
1395
|
} catch (error) {
|
|
1317
1396
|
logger.error('添加远程仓库失败:', error);
|
|
1318
|
-
res.json({ success: false, error: error.message || '添加远程仓库失败' });
|
|
1397
|
+
res.status(error?.statusCode || 200).json({ success: false, error: error.message || '添加远程仓库失败' });
|
|
1319
1398
|
}
|
|
1320
1399
|
});
|
|
1321
1400
|
|
|
@@ -162,9 +162,19 @@ export function registerNpmRoutes({
|
|
|
162
162
|
res.setHeader('Connection', 'keep-alive')
|
|
163
163
|
res.flushHeaders?.()
|
|
164
164
|
|
|
165
|
+
// 客户端断开后 res.write 会抛 EPIPE。这里必须吞掉:
|
|
166
|
+
// 异常若从 child.stdout 的 'data' 监听器冒泡出去,会变成 uncaughtException,
|
|
167
|
+
// 命中 index.js 的 fatalExit → 整个 GUI 服务退出,而不仅仅是这一个请求失败。
|
|
168
|
+
let clientClosed = false
|
|
165
169
|
const send = (obj) => {
|
|
166
|
-
|
|
170
|
+
if (clientClosed) return
|
|
171
|
+
try {
|
|
172
|
+
res.write(JSON.stringify(obj) + '\n')
|
|
173
|
+
} catch {
|
|
174
|
+
// res 已关闭,忽略
|
|
175
|
+
}
|
|
167
176
|
}
|
|
177
|
+
const finish = () => { try { res.end() } catch { /* ignore */ } }
|
|
168
178
|
|
|
169
179
|
// Windows 上 npm install -g 通常不需要管理员;*nix 需要 sudo。
|
|
170
180
|
// 用 sudo -n(非交互式)检测是否可免密,失败时让用户用 sudo 重启 GUI
|
|
@@ -188,7 +198,7 @@ export function registerNpmRoutes({
|
|
|
188
198
|
} catch (err) {
|
|
189
199
|
send({ type: 'error', message: `启动升级进程失败: ${err.message}` })
|
|
190
200
|
send({ type: 'done', code: -1 })
|
|
191
|
-
|
|
201
|
+
finish()
|
|
192
202
|
cleanup()
|
|
193
203
|
return
|
|
194
204
|
}
|
|
@@ -215,7 +225,28 @@ export function registerNpmRoutes({
|
|
|
215
225
|
send({ type: 'error', message: `\n升级失败,退出码 ${code}${hint ? ' ' + hint : ''}\n` })
|
|
216
226
|
}
|
|
217
227
|
send({ type: 'done', code })
|
|
218
|
-
|
|
228
|
+
finish()
|
|
229
|
+
cleanup()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// 客户端断连 → 主动 kill,防止孤儿 npm 进程;同时必须 cleanup(),
|
|
233
|
+
// 否则 activeUpgrade 一直非 null,后续升级请求会永久返回 409。
|
|
234
|
+
req.on('close', () => {
|
|
235
|
+
if (clientClosed) return
|
|
236
|
+
clientClosed = true
|
|
237
|
+
if (child && !child.killed) {
|
|
238
|
+
try {
|
|
239
|
+
child.kill('SIGTERM')
|
|
240
|
+
setTimeout(() => {
|
|
241
|
+
if (!child.killed) {
|
|
242
|
+
try { child.kill('SIGKILL') } catch { /* ignore */ }
|
|
243
|
+
}
|
|
244
|
+
}, 2000)
|
|
245
|
+
} catch (e) {
|
|
246
|
+
logger.warn('[app-upgrade] 客户端断连后 kill 子进程失败:', e?.message || e)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
finish()
|
|
219
250
|
cleanup()
|
|
220
251
|
})
|
|
221
252
|
})
|
|
@@ -1092,19 +1123,14 @@ export function registerNpmRoutes({
|
|
|
1092
1123
|
logger.error('打开终端失败:', err);
|
|
1093
1124
|
}
|
|
1094
1125
|
|
|
1126
|
+
// 只响应一次。原先这里连续调用了两次 res.json() → ERR_HTTP_HEADERS_SENT,
|
|
1127
|
+
// 且第二次还把未经校验的原始 packagePath 直接回给了前端。
|
|
1095
1128
|
res.json({
|
|
1096
1129
|
success: true,
|
|
1097
1130
|
message: `已在新终端中执行: ${scriptName}`,
|
|
1098
1131
|
command: npmCommand,
|
|
1099
1132
|
path: resolvedPkgPath
|
|
1100
1133
|
});
|
|
1101
|
-
|
|
1102
|
-
res.json({
|
|
1103
|
-
success: true,
|
|
1104
|
-
message: `已在新终端中执行: ${scriptName}`,
|
|
1105
|
-
command: npmCommand,
|
|
1106
|
-
path: packagePath
|
|
1107
|
-
});
|
|
1108
1134
|
} catch (error) {
|
|
1109
1135
|
logger.error('执行npm脚本失败:', error);
|
|
1110
1136
|
res.status(500).json({
|