zen-gitsync 2.13.22 → 2.13.24

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/gitCommit.js +2 -2
  3. package/src/ui/public/assets/{AppVersionBadge-CS59-kD5.js → AppVersionBadge-CaVphp8E.js} +2 -2
  4. package/src/ui/public/assets/{BranchSelector-DulCErgN.js → BranchSelector-B91Ylbas.js} +1 -1
  5. package/src/ui/public/assets/{CommandConsole-H1j4BsHx.js → CommandConsole-bK4iwZvI.js} +2 -2
  6. package/src/ui/public/assets/{CommitForm-DIdLEAZ_.js → CommitForm-zOCxmZ84.js} +1 -1
  7. package/src/ui/public/assets/{CustomCommandManager-Cm5yU0TM.js → CustomCommandManager-C1lRrp5z.js} +1 -1
  8. package/src/ui/public/assets/{EditorView-CTvDExIi.js → EditorView-DYo2oYtk.js} +0 -0
  9. package/src/ui/public/assets/{FileDiffViewer-C7Cosx8a.js → FileDiffViewer-Ce17WOZ-.js} +1 -1
  10. package/src/ui/public/assets/{FlowExecutionViewer-BeGTXBVV.js → FlowExecutionViewer-CRK6lIul.js} +1 -1
  11. package/src/ui/public/assets/{FlowOrchestrationWorkspace-CuqAZCaM.js → FlowOrchestrationWorkspace-BDw7ThWp.js} +1 -1
  12. package/src/ui/public/assets/{GitStatus-DBDjyLJD.js → GitStatus-DN4_m6sO.js} +1 -1
  13. package/src/ui/public/assets/{ImagePreview-C8eIs5yL.js → ImagePreview-D8klNa6F.js} +1 -1
  14. package/src/ui/public/assets/{LogList-YCJ_gVWr.js → LogList-BshRTCrE.js} +1 -1
  15. package/src/ui/public/assets/{ProjectStartupButton-gWSO3HCJ.js → ProjectStartupButton-C-dXxxMO.js} +1 -1
  16. package/src/ui/public/assets/{RemoteRepoCard-9iWOAx2g.js → RemoteRepoCard-DDZcP5-V.js} +1 -1
  17. package/src/ui/public/assets/{ResetToRemoteButton-BckRJ_lM.js → ResetToRemoteButton-TXJzvBxE.js} +1 -1
  18. package/src/ui/public/assets/{SourceMapView-DWBowIx1.js → SourceMapView-BUkB6h41.js} +1 -1
  19. package/src/ui/public/assets/{TemplateManager-DiKpGMhi.js → TemplateManager-DFVzDiu0.js} +1 -1
  20. package/src/ui/public/assets/{UserInputNode-CU11vBBn.js → UserInputNode-CXyV1xG4.js} +1 -1
  21. package/src/ui/public/assets/WorkbenchView-CEYhsa4O.js +12 -0
  22. package/src/ui/public/assets/WorkbenchView-DvML7yIv.css +1 -0
  23. package/src/ui/public/assets/{_plugin-vue_export-helper-vLPduyLG.js → _plugin-vue_export-helper-BfbhkIv4.js} +2 -2
  24. package/src/ui/public/assets/{index-BxxqloTb.js → index-BGmxuKoN.js} +3 -3
  25. package/src/ui/public/index.html +2 -2
  26. package/src/ui/server/index.js +3 -3
  27. package/src/ui/server/routes/branchStatus.js +4 -4
  28. package/src/ui/server/routes/config.js +8 -8
  29. package/src/ui/server/routes/exec.js +7 -1
  30. package/src/ui/server/routes/fs.js +2 -2
  31. package/src/ui/server/routes/git/diff.js +17 -16
  32. package/src/ui/server/routes/git/diffUtils.js +21 -2
  33. package/src/ui/server/routes/git/stash.js +38 -42
  34. package/src/ui/server/routes/git/tags.js +10 -10
  35. package/src/ui/server/routes/git.js +15 -26
  36. package/src/ui/server/routes/gitOps.js +91 -125
  37. package/src/ui/server/routes/status.js +3 -3
  38. package/src/ui/server/routes/workbench.js +160 -23
  39. package/src/utils/index.js +32 -29
  40. package/src/ui/public/assets/WorkbenchView-BsVMwobR.js +0 -12
  41. package/src/ui/public/assets/WorkbenchView-DslIMLgz.css +0 -1
@@ -10,12 +10,12 @@
10
10
  <link rel="preconnect" href="https://fonts.googleapis.com" />
11
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
12
  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
13
- <script type="module" crossorigin src="/assets/index-BxxqloTb.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-BGmxuKoN.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CMxvf4Kt.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/monaco-DC7mqInI.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/element-plus-CJjFCgLP.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/vendor-0YbzMuxw.js">
18
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-vLPduyLG.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-BfbhkIv4.js">
19
19
  <link rel="modulepreload" crossorigin href="/assets/socket-io-z4n873W1.js">
20
20
  <link rel="stylesheet" crossorigin href="/assets/monaco-UXx6YYK1.css">
21
21
  <link rel="stylesheet" crossorigin href="/assets/element-plus-D36sOm0a.css">
@@ -110,7 +110,7 @@ async function startUIServer(noOpen = false, savePort = false) {
110
110
  let dirPath = process.cwd();
111
111
  try {
112
112
  if(showConsole) console.log(`记录最近打开目录`)
113
- const { stdout } = await execGitCommand('git rev-parse --show-toplevel');
113
+ const { stdout } = await execGitCommand(['rev-parse', '--show-toplevel']);
114
114
  const root = stdout?.trim();
115
115
  if (root) dirPath = root;
116
116
  } catch (_) {
@@ -290,7 +290,7 @@ async function startUIServer(noOpen = false, savePort = false) {
290
290
  }
291
291
 
292
292
  // 只获取porcelain格式状态,不再获取完整的git status
293
- const { stdout: porcelainOutput } = await execGitCommand('git status --porcelain --untracked-files=all');
293
+ const { stdout: porcelainOutput } = await execGitCommand(['status', '--porcelain', '--untracked-files=all']);
294
294
 
295
295
  // 广播到当前项目房间的所有客户端
296
296
  io.to(projectRoomId).emit('git_status_update', {
@@ -309,7 +309,7 @@ async function startUIServer(noOpen = false, savePort = false) {
309
309
  // 检查当前目录是否是Git仓库
310
310
  let isGitRepo = false;
311
311
  try {
312
- const { stdout } = await execGitCommand('git rev-parse --is-inside-work-tree', { log: false });
312
+ const { stdout } = await execGitCommand(['rev-parse', '--is-inside-work-tree'], { log: false });
313
313
  isGitRepo = stdout.trim() === 'true';
314
314
  } catch (error) {
315
315
  isGitRepo = false;
@@ -24,7 +24,7 @@ export function registerBranchStatusRoutes({
24
24
  // 获取当前分支 - 直接读取,不缓存(git symbolic-ref 极快,<5ms)
25
25
  app.get('/api/branch', async (req, res) => {
26
26
  try {
27
- const { stdout } = await execGitCommand('git symbolic-ref --short HEAD');
27
+ const { stdout } = await execGitCommand(['symbolic-ref', '--short', 'HEAD']);
28
28
  res.json({ branch: stdout.trim() });
29
29
  } catch (error) {
30
30
  res.status(500).json({ error: error.message });
@@ -70,11 +70,11 @@ export function registerBranchStatusRoutes({
70
70
  console.log(`使用5秒缓存的分支名: ${currentBranch} -> ${upstreamBranch}`);
71
71
  } else {
72
72
  // 直接读取,不再走5分钟长缓存
73
- const { stdout: branchOut } = await execGitCommand('git symbolic-ref --short HEAD');
73
+ const { stdout: branchOut } = await execGitCommand(['symbolic-ref', '--short', 'HEAD']);
74
74
  currentBranch = branchOut.trim();
75
75
 
76
76
  const { stdout: upstreamOut } = await execGitCommand(
77
- 'git rev-parse --abbrev-ref --symbolic-full-name @{u}',
77
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'],
78
78
  { ignoreError: true }
79
79
  );
80
80
  upstreamBranch = upstreamOut.trim() || null;
@@ -88,7 +88,7 @@ export function registerBranchStatusRoutes({
88
88
  }
89
89
 
90
90
  const { stdout: aheadBehindOutput } = await execGitCommand(
91
- `git rev-list --left-right --count ${currentBranch}...${upstreamBranch}`
91
+ ['rev-list', '--left-right', '--count', `${currentBranch}...${upstreamBranch}`]
92
92
  );
93
93
  const [ahead, behind] = aheadBehindOutput.trim().split('\t').map(Number);
94
94
 
@@ -101,7 +101,7 @@ async function collectDiffForAi({ execGitCommand, getCurrentProjectPath, selecte
101
101
  let fileList = []
102
102
  try {
103
103
  // 1. 拿到工作区状态,识别 untracked 文件
104
- const { stdout: statusOut } = await execGitCommand('git status --porcelain=1 --untracked-files=all', cwdOpts)
104
+ const { stdout: statusOut } = await execGitCommand(['status', '--porcelain=1', '--untracked-files=all'], cwdOpts)
105
105
  const untracked = []
106
106
  const trackedChanges = []
107
107
  for (const line of (statusOut || '').split('\n')) {
@@ -129,9 +129,8 @@ async function collectDiffForAi({ execGitCommand, getCurrentProjectPath, selecte
129
129
  const batchSize = 20
130
130
  for (let i = 0; i < untracked.length; i += batchSize) {
131
131
  const batch = untracked.slice(i, i + batchSize)
132
- const quoted = batch.map(p => `"${p.replace(/"/g, '\\"')}"`).join(' ')
133
132
  try {
134
- await execGitCommand(`git add -N --force ${quoted}`, cwdOpts)
133
+ await execGitCommand(['add', '-N', '--force', ...batch], cwdOpts)
135
134
  } catch (e) {
136
135
  // 单批失败不影响整体
137
136
  console.warn('[generate-commit] git add -N failed for batch:', e?.message)
@@ -142,12 +141,13 @@ async function collectDiffForAi({ execGitCommand, getCurrentProjectPath, selecte
142
141
  // 3. 合并 staged + unstaged diff
143
142
  // 用 --no-color 避免 ANSI 干扰, --no-ext-diff 避免外接 diff 工具
144
143
  // 选择模式下用 `-- <paths>` 限定,避免拉取无关 staged 改动
145
- const pathArgs = selectedSet
146
- ? ' -- ' + [...selectedSet].map(p => `"${p.replace(/"/g, '\\"')}"`).join(' ')
147
- : ''
144
+ const pathArgs = selectedSet ? [...selectedSet] : []
145
+ const diffBase = ['diff', '--no-color', '--no-ext-diff']
146
+ const stagedArgs = ['diff', '--staged', '--no-color', '--no-ext-diff', ...(pathArgs.length ? ['--', ...pathArgs] : [])]
147
+ const unstagedArgs = [...diffBase, ...(pathArgs.length ? ['--', ...pathArgs] : [])]
148
148
  const [stagedRes, unstagedRes] = await Promise.all([
149
- execGitCommand(`git diff --staged --no-color --no-ext-diff${pathArgs}`, cwdOpts).catch(() => ({ stdout: '' })),
150
- execGitCommand(`git diff --no-color --no-ext-diff${pathArgs}`, cwdOpts).catch(() => ({ stdout: '' }))
149
+ execGitCommand(stagedArgs, cwdOpts).catch(() => ({ stdout: '' })),
150
+ execGitCommand(unstagedArgs, cwdOpts).catch(() => ({ stdout: '' }))
151
151
  ])
152
152
  let combined = ''
153
153
  if (stagedRes?.stdout) combined += stagedRes.stdout.trim() + '\n'
@@ -16,6 +16,12 @@ import path from 'path';
16
16
  import iconv from 'iconv-lite';
17
17
  import { spawn } from 'child_process';
18
18
 
19
+ // 把 "git config --global key value" 这种空格分隔命令拆成 argv 数组,
20
+ // 注意:简单 split 不处理引号/转义;保持与之前 shell 模式一致的语义。
21
+ function splitCommandArgs(command) {
22
+ return command.trim().split(/\s+/).filter(Boolean);
23
+ }
24
+
19
25
  export function registerExecRoutes({
20
26
  app,
21
27
  execGitCommand,
@@ -33,7 +39,7 @@ export function registerExecRoutes({
33
39
  }
34
40
 
35
41
  try {
36
- const { stdout = '', stderr = '' } = await execGitCommand(command, { log: false });
42
+ const { stdout = '', stderr = '' } = await execGitCommand(splitCommandArgs(command), { log: false });
37
43
  return res.json({ success: true, stdout, stderr });
38
44
  } catch (err) {
39
45
  return res.status(400).json({ success: false, error: err?.message || String(err) });
@@ -45,7 +45,7 @@ export function registerFsRoutes({
45
45
 
46
46
  // 检查当前目录是否是Git仓库
47
47
  try {
48
- await execGitCommand('git rev-parse --is-inside-work-tree');
48
+ await execGitCommand(['rev-parse', '--is-inside-work-tree']);
49
49
  } catch (error) {
50
50
  return res.status(400).json({
51
51
  error: '当前目录不是一个Git仓库',
@@ -86,7 +86,7 @@ export function registerFsRoutes({
86
86
 
87
87
  // 检查新目录是否是Git仓库
88
88
  try {
89
- await execGitCommand('git rev-parse --is-inside-work-tree');
89
+ await execGitCommand(['rev-parse', '--is-inside-work-tree']);
90
90
 
91
91
  // 更新全局变量
92
92
  setCurrentProjectPath(newProjectPath);
@@ -34,10 +34,11 @@ export function registerGitDiffRoutes({
34
34
  return res.status(400).json({ error: '缺少文件路径参数' });
35
35
  }
36
36
 
37
- const diffCommand = `git diff -- "${filePath}"`;
37
+ const diffArgs = ['diff', '--', filePath];
38
38
 
39
- // 使用优化的检查函数
40
- const skipCheck = await checkShouldSkipDiff(filePath, diffCommand);
39
+ // 使用优化的检查函数(diffCommand 用于内部 numstat 检测的字符串转换,
40
+ // 实际走 git 时用 argv 数组,避免 Windows cmd.exe 拼引号问题)
41
+ const skipCheck = await checkShouldSkipDiff(filePath, `git diff -- "${filePath}"`);
41
42
  if (skipCheck.shouldSkip) {
42
43
  return res.json({
43
44
  diff: skipCheck.reason,
@@ -47,7 +48,7 @@ export function registerGitDiffRoutes({
47
48
  }
48
49
 
49
50
  // 执行git diff命令获取文件差异
50
- const { stdout } = await execGitCommand(diffCommand);
51
+ const { stdout } = await execGitCommand(diffArgs);
51
52
 
52
53
  // 检查实际diff大小
53
54
  const sizeCheck = checkDiffSize(stdout, 500);
@@ -72,10 +73,10 @@ export function registerGitDiffRoutes({
72
73
  return res.status(400).json({ error: '缺少文件路径参数' });
73
74
  }
74
75
 
75
- const diffCommand = `git diff --cached -- "${filePath}"`;
76
+ const diffArgs = ['diff', '--cached', '--', filePath];
76
77
 
77
78
  // 使用优化的检查函数
78
- const skipCheck = await checkShouldSkipDiff(filePath, diffCommand);
79
+ const skipCheck = await checkShouldSkipDiff(filePath, `git diff --cached -- "${filePath}"`);
79
80
  if (skipCheck.shouldSkip) {
80
81
  return res.json({
81
82
  diff: skipCheck.reason,
@@ -85,7 +86,7 @@ export function registerGitDiffRoutes({
85
86
  }
86
87
 
87
88
  // 执行git diff --cached命令获取已暂存文件差异
88
- const { stdout } = await execGitCommand(diffCommand);
89
+ const { stdout } = await execGitCommand(diffArgs);
89
90
 
90
91
  // 检查实际diff大小
91
92
  const sizeCheck = checkDiffSize(stdout, 500);
@@ -105,7 +106,7 @@ export function registerGitDiffRoutes({
105
106
  // 获取全量 diff(git diff HEAD,含已暂存与未暂存的所有变更)
106
107
  app.get('/api/diff-head', async (req, res) => {
107
108
  try {
108
- const { stdout } = await execGitCommand('git diff HEAD');
109
+ const { stdout } = await execGitCommand(['diff', 'HEAD']);
109
110
  const MAX = 500 * 1024;
110
111
  const content = stdout.length > MAX
111
112
  ? stdout.slice(0, MAX) + '\n\n[内容过大,已截断]'
@@ -172,7 +173,7 @@ export function registerGitDiffRoutes({
172
173
 
173
174
  let sizeBytes = 0;
174
175
  try {
175
- const { stdout: sizeOut } = await execGitCommand(`git cat-file -s "${spec}"`, { log: false });
176
+ const { stdout: sizeOut } = await execGitCommand(['cat-file', '-s', spec], { log: false });
176
177
  sizeBytes = parseInt(String(sizeOut).trim(), 10) || 0;
177
178
  } catch (e) {
178
179
  return res.json({ success: true, notFound: true, content: '' });
@@ -187,7 +188,7 @@ export function registerGitDiffRoutes({
187
188
  });
188
189
  }
189
190
 
190
- const { stdout } = await execGitCommand(`git show "${spec}"`, { log: false });
191
+ const { stdout } = await execGitCommand(['show', spec], { log: false });
191
192
  return res.json({ success: true, content: stdout ?? '' });
192
193
  } catch (error) {
193
194
  res.status(500).json({ success: false, error: error.message });
@@ -253,7 +254,7 @@ export function registerGitDiffRoutes({
253
254
  }
254
255
 
255
256
  // 检查文件状态:未跟踪文件需要删除,修改文件需要恢复
256
- const { stdout: statusOutput } = await execGitCommand(`git status --porcelain -- "${filePath}"`);
257
+ const { stdout: statusOutput } = await execGitCommand(['status', '--porcelain', '--', filePath]);
257
258
 
258
259
  // 未跟踪的文件 (??), 需要删除它
259
260
  if (statusOutput.startsWith('??')) {
@@ -270,12 +271,12 @@ export function registerGitDiffRoutes({
270
271
  // 已暂存的文件,先取消暂存
271
272
  else if (statusOutput.startsWith('A ') || statusOutput.startsWith('M ') || statusOutput.startsWith('D ')) {
272
273
  // 先取消暂存
273
- await execGitCommand(`git reset HEAD -- "${filePath}"`);
274
+ await execGitCommand(['reset', 'HEAD', '--', filePath]);
274
275
  }
275
276
 
276
277
  // 已修改文件,取消所有本地修改
277
278
  if (statusOutput) {
278
- await execGitCommand(`git checkout -- "${filePath}"`);
279
+ await execGitCommand(['checkout', '--', filePath]);
279
280
  return res.json({ success: true, message: '文件修改已撤回' });
280
281
  } else {
281
282
  return res.status(400).json({
@@ -307,7 +308,7 @@ export function registerGitDiffRoutes({
307
308
  for (const filePath of filePaths) {
308
309
  try {
309
310
  // 检查文件状态:未跟踪 ??、已暂存 A/M/D、已修改(空状态会返回空字符串)
310
- const { stdout: statusOutput } = await execGitCommand(`git status --porcelain -- "${filePath}"`)
311
+ const { stdout: statusOutput } = await execGitCommand(['status', '--porcelain', '--', filePath])
311
312
 
312
313
  // 未跟踪的文件 (??) → 直接删除
313
314
  if (statusOutput.startsWith('??')) {
@@ -324,12 +325,12 @@ export function registerGitDiffRoutes({
324
325
 
325
326
  // 已暂存的文件,先取消暂存(不影响工作区)
326
327
  if (statusOutput.startsWith('A ') || statusOutput.startsWith('M ') || statusOutput.startsWith('D ')) {
327
- await execGitCommand(`git reset HEAD -- "${filePath}"`)
328
+ await execGitCommand(['reset', 'HEAD', '--', filePath])
328
329
  }
329
330
 
330
331
  // 已修改文件:丢弃工作区修改
331
332
  if (statusOutput) {
332
- await execGitCommand(`git checkout -- "${filePath}"`)
333
+ await execGitCommand(['checkout', '--', filePath])
333
334
  results.push({ path: filePath, success: true, message: '文件修改已撤回' })
334
335
  successCount++
335
336
  } else {
@@ -31,8 +31,27 @@ export function createDiffHelpers({ execGitCommand }) {
31
31
 
32
32
  // 2. 使用 --numstat 快速检查变更量(不获取实际内容,速度快)
33
33
  try {
34
- const numstatCommand = diffCommand.replace(/git (diff|show)/, 'git $1 --numstat');
35
- const { stdout: numstat } = await execGitCommand(numstatCommand, { log: false });
34
+ // 把传入的 diffCommand 字符串改写成 git diff/show --numstat 的 argv 数组。
35
+ // 不再走 shell 模式,避免 Windows cmd.exe 下引号兼容问题( utils execGitCommand execFile )
36
+ const numstatCommand = diffCommand.replace(/git (diff|show)/, 'git $1 --numstat')
37
+ const argvMatch = numstatCommand.trim().match(/^git\s+(\S+)\s*(.*)$/)
38
+ let numstatArgs
39
+ if (argvMatch) {
40
+ const subCmd = argvMatch[1]
41
+ const rest = argvMatch[2]
42
+ // 简单按空白拆分,带引号段整体保留 -- "--" + 路径
43
+ // 这里最常见形态是 'git diff/show -- "--path"' 或 'git diff/show hash -- "--path"'
44
+ const tokens = []
45
+ const re = /"([^"]*)"|(\S+)/g
46
+ let m
47
+ while ((m = re.exec(rest)) !== null) tokens.push(m[1] !== undefined ? m[1] : m[2])
48
+ // 找到 '--numstat' 位置,把它加到 subCmd 后面作为第一个参数
49
+ numstatArgs = [subCmd, '--numstat', ...tokens]
50
+ } else {
51
+ // fallback: 走旧逻辑(字符串)
52
+ numstatArgs = numstatCommand
53
+ }
54
+ const { stdout: numstat } = await execGitCommand(numstatArgs, { log: false });
36
55
 
37
56
  if (numstat.trim()) {
38
57
  const lines = numstat.trim().split('\n');
@@ -20,7 +20,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
20
20
  // 获取stash列表
21
21
  app.get('/api/stash-list', async (req, res) => {
22
22
  try {
23
- const { stdout } = await execGitCommand('git stash list');
23
+ const { stdout } = await execGitCommand(['stash', 'list']);
24
24
 
25
25
  // 解析stash列表
26
26
  const stashList = stdout.split('\n')
@@ -53,7 +53,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
53
53
  if (excludeLocked) {
54
54
  const lockedFiles = await configManager.getLockedFiles();
55
55
  // 包含未跟踪文件,确保状态与 UI 一致
56
- const { stdout: statusStdout } = await execGitCommand('git status --porcelain --untracked-files=all', { log: false });
56
+ const { stdout: statusStdout } = await execGitCommand(['status', '--porcelain', '--untracked-files=all'], { log: false });
57
57
  const changedFiles = statusStdout
58
58
  .split('\n')
59
59
  .filter(line => line.trim())
@@ -100,7 +100,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
100
100
  if (includeUntracked) {
101
101
  try {
102
102
  // 使用 git 列出该目录下的未跟踪和已修改文件
103
- const { stdout: listStdout } = await execGitCommand(`git ls-files -mo --exclude-standard "${filename}"`, { log: false });
103
+ const { stdout: listStdout } = await execGitCommand(['ls-files', '-mo', '--exclude-standard', '--', filename], { log: false });
104
104
  const listed = listStdout
105
105
  .split('\n')
106
106
  .map(l => l.trim())
@@ -145,14 +145,13 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
145
145
  // 在执行 stash 前进行候选校验:
146
146
  // 1) 仍有跟踪差异的文件
147
147
  try {
148
- const args = filesToStash.map(f => `"${f}"`).join(' ');
149
- const { stdout: diffNames } = await execGitCommand(`git diff --name-only -- ${args}`, { log: false });
148
+ const { stdout: diffNames } = await execGitCommand(['diff', '--name-only', '--', ...filesToStash], { log: false });
150
149
  const trackedChanged = new Set(diffNames.split('\n').map(s => s.trim()).filter(Boolean));
151
150
 
152
151
  // 2) 仍为未跟踪的文件(当 includeUntracked 才检查)
153
152
  let untrackedExisting = new Set();
154
153
  if (includeUntracked) {
155
- const { stdout: others } = await execGitCommand(`git ls-files --others --exclude-standard -- ${args}`, { log: false });
154
+ const { stdout: others } = await execGitCommand(['ls-files', '--others', '--exclude-standard', '--', ...filesToStash], { log: false });
156
155
  untrackedExisting = new Set(others.split('\n').map(s => s.trim()).filter(Boolean));
157
156
  }
158
157
 
@@ -174,27 +173,22 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
174
173
  return res.json({ success: false, message: '没有可储藏的更改(可能刚刚已储藏,或被锁定过滤)' });
175
174
  }
176
175
 
177
- let command = 'git stash push';
178
- if (message) command += ` -m "${message}"`;
179
- if (includeUntracked) command += ' --include-untracked';
180
- const args = filesToStash.map(f => `"${f}"`).join(' ');
181
- command += ` -- ${args}`;
176
+ const stashArgs = ['stash', 'push'];
177
+ if (message) stashArgs.push('-m', message);
178
+ if (includeUntracked) stashArgs.push('--include-untracked');
179
+ if (filesToStash.length > 0) stashArgs.push('--', ...filesToStash);
182
180
 
183
- const { stdout } = await execGitCommand(command);
181
+ const { stdout } = await execGitCommand(stashArgs);
184
182
  if (stdout.includes('No local changes to save')) {
185
183
  return res.json({ success: false, message: '没有本地更改需要保存' });
186
184
  }
187
185
  return res.json({ success: true, message: '成功保存未锁定的工作区更改', output: stdout });
188
186
  }
189
187
 
190
- let command = 'git stash push';
191
- if (message) {
192
- command += ` -m "${message}"`;
193
- }
194
- if (includeUntracked) {
195
- command += ' --include-untracked';
196
- }
197
- const { stdout } = await execGitCommand(command);
188
+ const stashArgs = ['stash', 'push'];
189
+ if (message) stashArgs.push('-m', message);
190
+ if (includeUntracked) stashArgs.push('--include-untracked');
191
+ const { stdout } = await execGitCommand(stashArgs);
198
192
  if (stdout.includes('No local changes to save')) {
199
193
  return res.json({ success: false, message: '没有本地更改需要保存' });
200
194
  }
@@ -220,19 +214,18 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
220
214
  }
221
215
 
222
216
  // 构建 git stash push 命令
223
- let command = 'git stash push';
217
+ const stashArgs = ['stash', 'push'];
224
218
  if (message) {
225
- command += ` -m "${message}"`;
219
+ stashArgs.push('-m', message);
226
220
  }
227
221
  if (includeUntracked) {
228
- command += ' --include-untracked';
222
+ stashArgs.push('--include-untracked');
229
223
  }
230
224
 
231
225
  // 添加文件列表
232
- const args = files.map(f => `"${f}"`).join(' ');
233
- command += ` -- ${args}`;
226
+ stashArgs.push('--', ...files);
234
227
 
235
- const { stdout } = await execGitCommand(command);
228
+ const { stdout } = await execGitCommand(stashArgs);
236
229
 
237
230
  if (stdout.includes('No local changes to save')) {
238
231
  return res.json({ success: false, message: '没有本地更改需要保存' });
@@ -266,7 +259,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
266
259
  }
267
260
 
268
261
  // 决定是使用apply(保留stash)还是pop(应用后删除stash)
269
- const command = pop ? `git stash pop ${stashId}` : `git stash apply ${stashId}`;
262
+ const command = pop ? ['stash', 'pop', stashId] : ['stash', 'apply', stashId];
270
263
 
271
264
  try {
272
265
  const { stdout } = await execGitCommand(command);
@@ -306,7 +299,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
306
299
  });
307
300
  }
308
301
 
309
- const { stdout } = await execGitCommand(`git stash drop ${stashId}`);
302
+ const { stdout } = await execGitCommand(['stash', 'drop', stashId]);
310
303
 
311
304
  res.json({
312
305
  success: true,
@@ -322,7 +315,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
322
315
  // 清空所有stash
323
316
  app.post('/api/stash-clear', async (req, res) => {
324
317
  try {
325
- const { stdout } = await execGitCommand('git stash clear');
318
+ const { stdout } = await execGitCommand(['stash', 'clear']);
326
319
 
327
320
  res.json({
328
321
  success: true,
@@ -350,7 +343,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
350
343
  console.log(`获取stash文件列表: stashId=${stashId}`);
351
344
 
352
345
  // 0) 解析出当前 stash 提交及其父提交哈希,避免在 Windows 上使用 ^ 语法
353
- const { stdout: parentsLine } = await execGitCommand(`git rev-list --parents -n 1 ${stashId}`, { log: false });
346
+ const { stdout: parentsLine } = await execGitCommand(['rev-list', '--parents', '-n', '1', stashId], { log: false });
354
347
  const hashes = parentsLine.trim().split(/\s+/).filter(Boolean);
355
348
  const stashCommit = hashes[0] || '';
356
349
  const parent1 = hashes[1] || '';
@@ -359,14 +352,14 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
359
352
  // 1) 跟踪文件的变更列表:父1 与 stash 提交的差异(若无父1则为空)
360
353
  let trackedFiles = [];
361
354
  if (parent1) {
362
- const { stdout: trackedOut } = await execGitCommand(`git diff --name-only ${parent1} ${stashCommit}`, { log: false });
355
+ const { stdout: trackedOut } = await execGitCommand(['diff', '--name-only', parent1, stashCommit], { log: false });
363
356
  trackedFiles = trackedOut.split('\n').map(s => s.trim()).filter(Boolean);
364
357
  }
365
358
 
366
359
  // 2) 未跟踪文件:来自第三父(若存在)
367
360
  let untrackedFiles = [];
368
361
  if (parent3) {
369
- const { stdout: untrackedOut } = await execGitCommand(`git ls-tree -r --name-only ${parent3}`, { log: false });
362
+ const { stdout: untrackedOut } = await execGitCommand(['ls-tree', '-r', '--name-only', parent3], { log: false });
370
363
  untrackedFiles = untrackedOut.split('\n').map(s => s.trim()).filter(Boolean);
371
364
  }
372
365
 
@@ -403,7 +396,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
403
396
  console.log(`获取stash文件差异: stashId=${stashId}, file=${file}`);
404
397
 
405
398
  // 先解析父提交哈希,避免使用 ^ 语法
406
- const { stdout: parentsLine } = await execGitCommand(`git rev-list --parents -n 1 ${stashId}`, { log: false });
399
+ const { stdout: parentsLine } = await execGitCommand(['rev-list', '--parents', '-n', '1', stashId], { log: false });
407
400
  const hashes = parentsLine.trim().split(/\s+/).filter(Boolean);
408
401
  const stashCommit = hashes[0] || '';
409
402
  const parent1 = hashes[1] || '';
@@ -413,7 +406,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
413
406
  let isFromThirdParent = false;
414
407
  if (parent3) {
415
408
  try {
416
- await execGitCommand(`git cat-file -e ${parent3}:"${file}"`, { log: false });
409
+ await execGitCommand(['cat-file', '-e', `${parent3}:${file}`], { log: false });
417
410
  isFromThirdParent = true;
418
411
  } catch (_) {
419
412
  isFromThirdParent = false;
@@ -422,7 +415,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
422
415
 
423
416
  if (isFromThirdParent) {
424
417
  // 未跟踪文件:读取第三父中的内容,构造新增文件的统一diff
425
- const { stdout: blob } = await execGitCommand(`git show ${parent3}:"${file}"`, { log: false });
418
+ const { stdout: blob } = await execGitCommand(['show', `${parent3}:${file}`], { log: false });
426
419
 
427
420
  // 检查文件大小
428
421
  const sizeCheck = checkDiffSize(blob, 500);
@@ -457,10 +450,13 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
457
450
  }
458
451
 
459
452
  // 否则,使用原有方式获取与父1的变更
460
- const diffCommand = `git show ${stashCommit} -- "${file}"`;
453
+ // checkShouldSkipDiff 接受字符串命令用于日志展示,这里只用于大小判断;
454
+ // 走 execGitCommand 时用 argv 数组,避免 Windows 下 cmd.exe 拼引号被破坏。
455
+ const diffCommandForCheck = `git show ${stashCommit} -- "${file}"`;
456
+ const diffCommandArgs = ['show', stashCommit, '--', file];
461
457
 
462
458
  // 使用优化的检查函数
463
- const skipCheck = await checkShouldSkipDiff(file, diffCommand);
459
+ const skipCheck = await checkShouldSkipDiff(file, diffCommandForCheck);
464
460
  if (skipCheck.shouldSkip) {
465
461
  return res.json({
466
462
  success: true,
@@ -470,7 +466,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
470
466
  });
471
467
  }
472
468
 
473
- const { stdout } = await execGitCommand(diffCommand);
469
+ const { stdout } = await execGitCommand(diffCommandArgs);
474
470
 
475
471
  console.log(`获取到差异内容,长度: ${stdout.length}`);
476
472
 
@@ -503,7 +499,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
503
499
  }
504
500
 
505
501
  // 解析父提交哈希
506
- const { stdout: parentsLine } = await execGitCommand(`git rev-list --parents -n 1 ${stashId}`, { log: false });
502
+ const { stdout: parentsLine } = await execGitCommand(['rev-list', '--parents', '-n', '1', stashId], { log: false });
507
503
  const hashes = parentsLine.trim().split(/\s+/).filter(Boolean);
508
504
  const stashCommit = hashes[0] || '';
509
505
  const parent1 = hashes[1] || '';
@@ -513,7 +509,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
513
509
  let isFromThirdParent = false;
514
510
  if (parent3) {
515
511
  try {
516
- await execGitCommand(`git cat-file -e ${parent3}:"${file}"`, { log: false });
512
+ await execGitCommand(['cat-file', '-e', `${parent3}:${file}`], { log: false });
517
513
  isFromThirdParent = true;
518
514
  } catch (_) {
519
515
  isFromThirdParent = false;
@@ -524,7 +520,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
524
520
  let original = '';
525
521
  if (!isFromThirdParent && parent1) {
526
522
  try {
527
- const { stdout: origOut } = await execGitCommand(`git show ${parent1}:"${file}"`, { log: false });
523
+ const { stdout: origOut } = await execGitCommand(['show', `${parent1}:${file}`], { log: false });
528
524
  original = origOut ?? '';
529
525
  } catch (_) {
530
526
  original = ''; // 文件在储藏前不存在
@@ -536,7 +532,7 @@ export function registerGitStashRoutes({ app, execGitCommand, configManager }) {
536
532
  const modRef = isFromThirdParent ? parent3 : stashCommit;
537
533
  if (modRef) {
538
534
  try {
539
- const { stdout: modOut } = await execGitCommand(`git show ${modRef}:"${file}"`, { log: false });
535
+ const { stdout: modOut } = await execGitCommand(['show', `${modRef}:${file}`], { log: false });
540
536
  modified = modOut ?? '';
541
537
  } catch (_) {
542
538
  modified = ''; // 文件已被删除
@@ -24,25 +24,25 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
24
24
  return res.status(400).json({ success: false, error: '缺少标签名称' });
25
25
  }
26
26
 
27
- let command = 'git tag';
27
+ const tagArgs = ['tag'];
28
28
 
29
29
  if (type === 'annotated') {
30
30
  // 附注标签
31
31
  if (!message) {
32
32
  return res.status(400).json({ success: false, error: '附注标签需要提供说明信息' });
33
33
  }
34
- command += ` -a "${tagName}" -m "${message}"`;
34
+ tagArgs.push('-a', tagName, '-m', message);
35
35
  } else {
36
36
  // 轻量标签
37
- command += ` "${tagName}"`;
37
+ tagArgs.push(tagName);
38
38
  }
39
39
 
40
40
  // 如果指定了commit,添加到命令中
41
41
  if (commit && commit.trim()) {
42
- command += ` ${commit.trim()}`;
42
+ tagArgs.push(commit.trim());
43
43
  }
44
44
 
45
- const { stdout } = await execGitCommand(command);
45
+ const { stdout } = await execGitCommand(tagArgs);
46
46
 
47
47
  res.json({
48
48
  success: true,
@@ -60,7 +60,7 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
60
60
  try {
61
61
  // 使用 git tag -n --format 获取详细信息
62
62
  const { stdout } = await execGitCommand(
63
- 'git tag -n --format="%(refname:short)|%(objectname:short)|%(creatordate:iso8601)|%(subject)"'
63
+ ['tag', '-n', '--format=%(refname:short)|%(objectname:short)|%(creatordate:iso8601)|%(subject)']
64
64
  );
65
65
 
66
66
  if (!stdout.trim()) {
@@ -82,7 +82,7 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
82
82
  for (const tag of tags) {
83
83
  try {
84
84
  const { stdout: typeCheck } = await execGitCommand(
85
- `git cat-file -t ${tag.name}`,
85
+ ['cat-file', '-t', tag.name],
86
86
  { log: false }
87
87
  );
88
88
  if (typeCheck.trim() === 'tag') {
@@ -109,7 +109,7 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
109
109
  return res.status(400).json({ success: false, error: '缺少标签名称' });
110
110
  }
111
111
 
112
- const { stdout } = await execGitCommand(`git push origin ${tagName}`);
112
+ const { stdout } = await execGitCommand(['push', 'origin', tagName]);
113
113
 
114
114
  res.json({
115
115
  success: true,
@@ -125,7 +125,7 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
125
125
  // 推送所有标签到远程
126
126
  app.post('/api/push-all-tags', async (req, res) => {
127
127
  try {
128
- const { stdout } = await execGitCommand('git push origin --tags');
128
+ const { stdout } = await execGitCommand(['push', 'origin', '--tags']);
129
129
 
130
130
  res.json({
131
131
  success: true,
@@ -147,7 +147,7 @@ export function registerGitTagRoutes({ app, execGitCommand, clearCommandHistory
147
147
  return res.status(400).json({ success: false, error: '缺少标签名称' });
148
148
  }
149
149
 
150
- const { stdout } = await execGitCommand(`git tag -d ${tagName}`);
150
+ const { stdout } = await execGitCommand(['tag', '-d', tagName]);
151
151
 
152
152
  res.json({
153
153
  success: true,