zen-gitsync 2.15.11 → 2.15.12
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/package.json +3 -3
- package/src/gitCommit.js +475 -475
- package/src/ui/public/assets/AppVersionBadge-BaXEI9Aq.js +2 -0
- package/src/ui/public/assets/BranchSelector-DJhEF0y9.js +1 -0
- package/src/ui/public/assets/CommitForm-BfpcK-bz.js +6 -0
- package/src/ui/public/assets/ConsoleView-D838O0O6.js +17 -0
- package/src/ui/public/assets/CustomCommandManager-BcCK46df.js +1 -0
- package/src/ui/public/assets/EditorView-DnnqYNGF.js +0 -0
- package/src/ui/public/assets/{FlowExecutionViewer-CJgoxffE.js → FlowExecutionViewer-CSlMmcDS.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-UgHpZ3FR.js +21 -0
- package/src/ui/public/assets/LogList-uUgskwwp.js +7 -0
- package/src/ui/public/assets/MindmapView-CCmFMiHR.js +1 -0
- package/src/ui/public/assets/MonitorView-BxoqeX70.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-C3AwivIB.js +1 -0
- package/src/ui/public/assets/{RemoteRepoCard-CKQguRWi.js → RemoteRepoCard-BbxeVpje.js} +1 -1
- package/src/ui/public/assets/SourceMapView-DKv-Y6t_.js +3 -0
- package/src/ui/public/assets/{SvgIcon-DOkE0Qie.js → SvgIcon-sMM8XFgm.js} +1 -1
- package/src/ui/public/assets/TemplateManager-Dzj8rXVw.js +1 -0
- package/src/ui/public/assets/UserInputNode-BU7Trzq-.js +1 -0
- package/src/ui/public/assets/WorkbenchView-DTgRoOkv.js +12 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-DvV-QrsM.js → _plugin-vue_export-helper-8ryIYVvs.js} +1 -1
- package/src/ui/public/assets/{configStore-BRR4SloR.js → configStore-D77uNk8X.js} +1 -1
- package/src/ui/public/assets/element-plus-D8SQiesr.js +17 -0
- package/src/ui/public/assets/element-plus-DB_sOyH_.css +1 -0
- package/src/ui/public/assets/{flow-mindmap-BMvnvRl8.js → flow-mindmap-BLa-kSA7.js} +1 -1
- package/src/ui/public/assets/index-CpFreGqX.js +23 -0
- package/src/ui/public/assets/{socket-io-C2w3MxRu.js → socket-io-B1foIbJS.js} +1 -1
- package/src/ui/public/assets/{vendor--LaaJk9m.js → vendor-B9orL8iU.js} +37 -37
- package/src/ui/public/assets/vendor-Dm_yd-By.css +1 -0
- package/src/ui/public/assets/{vue-flow-MtbKS8yD.js → vue-flow-BY7LODit.js} +1 -1
- package/src/ui/public/favicon.svg +75 -75
- package/src/ui/public/index.html +27 -27
- package/src/ui/public/logo.svg +74 -74
- package/src/ui/server/middleware/requestLogger.js +53 -53
- package/src/ui/server/routes/branchStatus.js +105 -105
- package/src/ui/server/routes/fileOpen.js +309 -309
- package/src/ui/server/routes/fs.js +791 -791
- package/src/ui/server/routes/git/diff.js +426 -426
- package/src/ui/server/routes/git/diffUtils.js +149 -149
- package/src/ui/server/routes/git/stash.js +550 -550
- package/src/ui/server/routes/git/tags.js +176 -176
- package/src/ui/server/routes/git.js +181 -181
- package/src/ui/server/routes/gitOps.js +1211 -1211
- package/src/ui/server/routes/process.js +86 -86
- package/src/ui/server/routes/status.js +67 -67
- package/src/ui/server/utils/createSavePortToFile.js +48 -48
- package/src/ui/server/utils/instanceRegistry.js +282 -282
- package/src/ui/server/utils/startServerOnAvailablePort.js +116 -116
- package/src/utils/index.js +1168 -1168
- package/src/ui/public/assets/AppVersionBadge-CEhrATQo.js +0 -2
- package/src/ui/public/assets/BranchSelector-xLl3O-4I.js +0 -1
- package/src/ui/public/assets/CommitForm--izGtr_x.js +0 -6
- package/src/ui/public/assets/ConsoleView-BLYfnXAy.js +0 -17
- package/src/ui/public/assets/CustomCommandManager-D8Qd3b9m.js +0 -1
- package/src/ui/public/assets/EditorView-DG8tqp3L.js +0 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BAIqeASp.js +0 -21
- package/src/ui/public/assets/LogList-DjuL8nWB.js +0 -7
- package/src/ui/public/assets/MindmapView-DPomtQsu.js +0 -1
- package/src/ui/public/assets/MonitorView-gZhz528u.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-C8nDEd8N.js +0 -1
- package/src/ui/public/assets/SourceMapView-DfSRy6lc.js +0 -3
- package/src/ui/public/assets/TemplateManager-BJZloJIO.js +0 -1
- package/src/ui/public/assets/UserInputNode-DpRNQzrz.js +0 -1
- package/src/ui/public/assets/WorkbenchView-vaZsec5Z.js +0 -12
- package/src/ui/public/assets/element-plus-CL7fBdM_.js +0 -24
- package/src/ui/public/assets/element-plus-Cu9Y2raX.css +0 -1
- package/src/ui/public/assets/index-BAFCDhj2.js +0 -23
- package/src/ui/public/assets/vendor-CCbwTgrE.css +0 -1
- package/src/ui/server/.claude/codediff.txt +0 -8
|
@@ -1,1211 +1,1211 @@
|
|
|
1
|
-
// Copyright 2026 xz333221
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
//
|
|
15
|
-
import express from 'express';
|
|
16
|
-
import logger from '../utils/logger.js'
|
|
17
|
-
import fs from 'fs/promises';
|
|
18
|
-
import os from 'os';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
import { spawn } from 'child_process';
|
|
21
|
-
|
|
22
|
-
import { registerGitDiffRoutes } from './git/diff.js';
|
|
23
|
-
import { createDiffHelpers } from './git/diffUtils.js';
|
|
24
|
-
import { registerGitStashRoutes } from './git/stash.js';
|
|
25
|
-
import { registerGitTagRoutes } from './git/tags.js';
|
|
26
|
-
|
|
27
|
-
export function registerGitOpsRoutes({
|
|
28
|
-
app,
|
|
29
|
-
execGitCommand,
|
|
30
|
-
configManager,
|
|
31
|
-
execGitAddWithLockFilter,
|
|
32
|
-
addCommandToHistory,
|
|
33
|
-
clearCommandHistory,
|
|
34
|
-
checkAndClearGitLock,
|
|
35
|
-
getIsGitRepo,
|
|
36
|
-
setRecentPushStatus
|
|
37
|
-
}) {
|
|
38
|
-
// 提交更改
|
|
39
|
-
app.post('/api/commit', express.json(), async (req, res) => {
|
|
40
|
-
try {
|
|
41
|
-
const { message, hasNewlines, noVerify } = req.body;
|
|
42
|
-
|
|
43
|
-
// 构建 git commit 命令
|
|
44
|
-
const commitArgs = ['commit'];
|
|
45
|
-
|
|
46
|
-
// 如果消息包含换行符,使用文件方式提交
|
|
47
|
-
let tempFile
|
|
48
|
-
if (hasNewlines) {
|
|
49
|
-
// 创建临时文件存储提交信息
|
|
50
|
-
tempFile = path.join(os.tmpdir(), `commit-msg-${Date.now()}.txt`);
|
|
51
|
-
await fs.writeFile(tempFile, message);
|
|
52
|
-
commitArgs.push('-F', tempFile);
|
|
53
|
-
} else {
|
|
54
|
-
// 否则直接在命令行中提供消息
|
|
55
|
-
commitArgs.push('-m', message);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 添加 --no-verify 参数
|
|
59
|
-
if (noVerify) {
|
|
60
|
-
commitArgs.push('--no-verify');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
logger.info(`commitArgs ==>`, commitArgs);
|
|
64
|
-
// 执行提交命令
|
|
65
|
-
await execGitCommand(commitArgs);
|
|
66
|
-
|
|
67
|
-
// 如果使用了临时文件,删除它
|
|
68
|
-
if (hasNewlines && tempFile) {
|
|
69
|
-
await fs.unlink(tempFile).catch(() => {});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
res.json({ success: true });
|
|
73
|
-
} catch (error) {
|
|
74
|
-
res.status(500).json({ success: false, error: error.message });
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// 添加 add 接口
|
|
79
|
-
app.post('/api/add', async (req, res) => {
|
|
80
|
-
try {
|
|
81
|
-
// 直接执行 git add . - 前端已经做了锁定文件过滤判断
|
|
82
|
-
await execGitCommand(['add', '.']);
|
|
83
|
-
res.json({ success: true });
|
|
84
|
-
} catch (error) {
|
|
85
|
-
res.status(500).json({ success: false, error: error.message });
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// 添加带锁定文件过滤的 add 接口
|
|
90
|
-
app.post('/api/add-filtered', async (req, res) => {
|
|
91
|
-
try {
|
|
92
|
-
// 使用带锁定文件过滤的 git add
|
|
93
|
-
await execGitAddWithLockFilter();
|
|
94
|
-
res.json({ success: true });
|
|
95
|
-
} catch (error) {
|
|
96
|
-
res.status(500).json({ success: false, error: error.message });
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// 添加 add-all 接口(直接执行 git add . 不考虑锁定文件)
|
|
101
|
-
app.post('/api/add-all', async (req, res) => {
|
|
102
|
-
try {
|
|
103
|
-
// 直接执行 git add . 不考虑锁定文件
|
|
104
|
-
await execGitCommand(['add', '.']);
|
|
105
|
-
res.json({ success: true });
|
|
106
|
-
} catch (error) {
|
|
107
|
-
res.status(500).json({ success: false, error: error.message });
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// 添加单个文件到暂存区
|
|
112
|
-
app.post('/api/add-file', async (req, res) => {
|
|
113
|
-
try {
|
|
114
|
-
const { filePath } = req.body;
|
|
115
|
-
|
|
116
|
-
if (!filePath) {
|
|
117
|
-
return res.status(400).json({
|
|
118
|
-
success: false,
|
|
119
|
-
error: '缺少文件路径参数'
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 执行 git add 命令添加特定文件
|
|
124
|
-
await execGitCommand(['add', '--', filePath]);
|
|
125
|
-
res.json({ success: true });
|
|
126
|
-
} catch (error) {
|
|
127
|
-
res.status(500).json({ success: false, error: error.message });
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// 批量添加文件到暂存区(一次 git add 命令,避免 index.lock 竞争)
|
|
132
|
-
app.post('/api/add-files', async (req, res) => {
|
|
133
|
-
try {
|
|
134
|
-
const { filePaths } = req.body || {};
|
|
135
|
-
|
|
136
|
-
if (!Array.isArray(filePaths) || filePaths.length === 0) {
|
|
137
|
-
return res.status(400).json({
|
|
138
|
-
success: false,
|
|
139
|
-
error: '缺少文件路径参数',
|
|
140
|
-
successCount: 0,
|
|
141
|
-
failedCount: 0
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 过滤掉空路径与重复路径
|
|
146
|
-
const uniquePaths = [...new Set(filePaths.filter(p => typeof p === 'string' && p.length > 0))];
|
|
147
|
-
if (uniquePaths.length === 0) {
|
|
148
|
-
return res.status(400).json({
|
|
149
|
-
success: false,
|
|
150
|
-
error: '没有有效的文件路径',
|
|
151
|
-
successCount: 0,
|
|
152
|
-
failedCount: 0
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 一次 git add 命令暂存所有文件(不再需要 shell 转义,execFile argv 数组天然支持任意路径)
|
|
157
|
-
await execGitCommand(['add', ...uniquePaths]);
|
|
158
|
-
|
|
159
|
-
res.json({
|
|
160
|
-
success: true,
|
|
161
|
-
successCount: uniquePaths.length,
|
|
162
|
-
failedCount: 0,
|
|
163
|
-
filePaths: uniquePaths
|
|
164
|
-
});
|
|
165
|
-
} catch (error) {
|
|
166
|
-
res.status(500).json({ success: false, error: error.message, successCount: 0, failedCount: 0 });
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// 从暂存区移除单个文件
|
|
171
|
-
app.post('/api/unstage-file', async (req, res) => {
|
|
172
|
-
try {
|
|
173
|
-
const { filePath } = req.body;
|
|
174
|
-
|
|
175
|
-
if (!filePath) {
|
|
176
|
-
return res.status(400).json({
|
|
177
|
-
success: false,
|
|
178
|
-
error: '缺少文件路径参数'
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// 执行 git reset HEAD 命令移除特定文件的暂存
|
|
183
|
-
await execGitCommand(['reset', 'HEAD', '--', filePath]);
|
|
184
|
-
res.json({ success: true });
|
|
185
|
-
} catch (error) {
|
|
186
|
-
res.status(500).json({ success: false, error: error.message });
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// 推送更改
|
|
191
|
-
app.post('/api/push', async (req, res) => {
|
|
192
|
-
try {
|
|
193
|
-
const { stdout } = await execGitCommand(['push']);
|
|
194
|
-
|
|
195
|
-
// 推送成功后,设置推送状态标记
|
|
196
|
-
setRecentPushStatus({
|
|
197
|
-
justPushed: true,
|
|
198
|
-
pushTime: Date.now(),
|
|
199
|
-
validDuration: 10000 // 10秒内认为分支状态是同步的
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
logger.info('推送成功,已设置推送状态标记');
|
|
203
|
-
res.json({ success: true, message: stdout });
|
|
204
|
-
} catch (error) {
|
|
205
|
-
res.status(500).json({ success: false, error: error.message });
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// 推送并设置上游分支(git push -u origin <branch>)
|
|
210
|
-
app.post('/api/git/push-with-upstream', async (req, res) => {
|
|
211
|
-
try {
|
|
212
|
-
const { branch } = req.body || {};
|
|
213
|
-
if (!branch || typeof branch !== 'string') {
|
|
214
|
-
return res.status(400).json({ success: false, error: '缺少 branch 参数' });
|
|
215
|
-
}
|
|
216
|
-
const { stdout } = await execGitCommand(['push', '-u', 'origin', branch]);
|
|
217
|
-
setRecentPushStatus({
|
|
218
|
-
justPushed: true,
|
|
219
|
-
pushTime: Date.now(),
|
|
220
|
-
validDuration: 10000
|
|
221
|
-
});
|
|
222
|
-
logger.info(`推送并设置上游成功: origin/${branch}`);
|
|
223
|
-
res.json({ success: true, message: stdout });
|
|
224
|
-
} catch (error) {
|
|
225
|
-
res.status(500).json({ success: false, error: error.message });
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// 读取单个 git 全局配置
|
|
230
|
-
// GET /api/git/global-config?key=<name>
|
|
231
|
-
// key 不存在时返回空串(与 git --get 行为一致;前端 getGlobalConfig 容错为空)
|
|
232
|
-
app.get('/api/git/global-config', async (req, res) => {
|
|
233
|
-
try {
|
|
234
|
-
const key = typeof req.query.key === 'string' ? req.query.key.trim() : '';
|
|
235
|
-
if (!key) {
|
|
236
|
-
return res.status(400).json({ success: false, error: '缺少 key 参数' });
|
|
237
|
-
}
|
|
238
|
-
try {
|
|
239
|
-
const { stdout } = await execGitCommand(['config', '--global', '--get', key], { log: false });
|
|
240
|
-
return res.json({ success: true, value: String(stdout || '').trim() });
|
|
241
|
-
} catch (err) {
|
|
242
|
-
// git --get 在 key 不存在时返回非零退出码 → 视为"未设置",返回空串
|
|
243
|
-
return res.json({ success: true, value: '' });
|
|
244
|
-
}
|
|
245
|
-
} catch (error) {
|
|
246
|
-
res.status(500).json({ success: false, error: error.message });
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// 写入单个 git 全局配置
|
|
251
|
-
// POST /api/git/global-config body: { key, value }
|
|
252
|
-
// value 为空字符串时执行 --unset,与 git 语义一致(允许前端"清空某项")
|
|
253
|
-
app.post('/api/git/global-config', async (req, res) => {
|
|
254
|
-
try {
|
|
255
|
-
const { key, value } = req.body || {};
|
|
256
|
-
if (!key || typeof key !== 'string') {
|
|
257
|
-
return res.status(400).json({ success: false, error: '缺少 key 参数' });
|
|
258
|
-
}
|
|
259
|
-
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
260
|
-
if (!trimmed) {
|
|
261
|
-
await execGitCommand(['config', '--global', '--unset', key]);
|
|
262
|
-
} else {
|
|
263
|
-
await execGitCommand(['config', '--global', key, trimmed]);
|
|
264
|
-
}
|
|
265
|
-
res.json({ success: true });
|
|
266
|
-
} catch (error) {
|
|
267
|
-
res.status(500).json({ success: false, error: error.message });
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// 带进度的推送更改 (SSE)
|
|
272
|
-
app.post('/api/push-with-progress', async (req, res) => {
|
|
273
|
-
// 设置SSE响应头
|
|
274
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
275
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
276
|
-
res.setHeader('Connection', 'keep-alive');
|
|
277
|
-
res.flushHeaders();
|
|
278
|
-
|
|
279
|
-
const sendProgress = (data) => {
|
|
280
|
-
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
// 获取当前工作目录 - 与execGitCommand保持一致
|
|
285
|
-
const cwdArg = process.argv.find(arg => arg.startsWith('--path')) || process.argv.find(arg => arg.startsWith('--cwd'));
|
|
286
|
-
let workDir = process.cwd();
|
|
287
|
-
if (cwdArg) {
|
|
288
|
-
const [, value] = cwdArg.split('=');
|
|
289
|
-
workDir = value || process.cwd();
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
logger.info('开始推送,工作目录:', workDir);
|
|
293
|
-
|
|
294
|
-
// 记录开始时间
|
|
295
|
-
const startTime = Date.now();
|
|
296
|
-
|
|
297
|
-
// 发送开始消息
|
|
298
|
-
sendProgress({
|
|
299
|
-
type: 'progress',
|
|
300
|
-
message: 'Starting to push to the remote repository...'
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// 使用spawn执行git push --progress
|
|
304
|
-
const gitPush = spawn('git', ['push', '--progress'], {
|
|
305
|
-
cwd: workDir,
|
|
306
|
-
env: {
|
|
307
|
-
...process.env,
|
|
308
|
-
GIT_CONFIG_PARAMETERS: "'core.quotepath=false'" // 关闭路径转义
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
let errorOutput = '';
|
|
313
|
-
let standardOutput = '';
|
|
314
|
-
|
|
315
|
-
// Git的进度信息在stderr中
|
|
316
|
-
gitPush.stderr.on('data', (data) => {
|
|
317
|
-
const output = data.toString();
|
|
318
|
-
errorOutput += output;
|
|
319
|
-
|
|
320
|
-
// 解析进度信息
|
|
321
|
-
const lines = output.split('\n');
|
|
322
|
-
for (const line of lines) {
|
|
323
|
-
if (line.trim()) {
|
|
324
|
-
// 发送原始行
|
|
325
|
-
sendProgress({
|
|
326
|
-
type: 'progress',
|
|
327
|
-
message: line.trim()
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// 识别不同阶段并解析百分比
|
|
331
|
-
const percentMatch = line.match(/(\d+)%/);
|
|
332
|
-
if (percentMatch) {
|
|
333
|
-
const percent = parseInt(percentMatch[1]);
|
|
334
|
-
let stage = 'unknown';
|
|
335
|
-
|
|
336
|
-
if (line.includes('Enumerating objects')) {
|
|
337
|
-
stage = 'enumerating';
|
|
338
|
-
} else if (line.includes('Counting objects')) {
|
|
339
|
-
stage = 'counting';
|
|
340
|
-
} else if (line.includes('Compressing objects')) {
|
|
341
|
-
stage = 'compressing';
|
|
342
|
-
} else if (line.includes('Writing objects')) {
|
|
343
|
-
stage = 'writing';
|
|
344
|
-
} else if (line.includes('Resolving deltas')) {
|
|
345
|
-
stage = 'resolving';
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
sendProgress({
|
|
349
|
-
type: 'stage-progress',
|
|
350
|
-
stage: stage,
|
|
351
|
-
percent: percent,
|
|
352
|
-
message: line.trim()
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
gitPush.stdout.on('data', (data) => {
|
|
360
|
-
standardOutput += data.toString();
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
gitPush.on('close', (code) => {
|
|
364
|
-
logger.info(`Git push 进程结束,退出码: ${code}`);
|
|
365
|
-
logger.info('标准输出:', standardOutput);
|
|
366
|
-
logger.info('错误输出:', errorOutput);
|
|
367
|
-
|
|
368
|
-
// 计算执行时间
|
|
369
|
-
const executionTime = Date.now() - startTime;
|
|
370
|
-
|
|
371
|
-
if (code === 0) {
|
|
372
|
-
// 推送成功
|
|
373
|
-
setRecentPushStatus({
|
|
374
|
-
justPushed: true,
|
|
375
|
-
pushTime: Date.now(),
|
|
376
|
-
validDuration: 10000
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// 添加到命令历史
|
|
380
|
-
addCommandToHistory(
|
|
381
|
-
'git push --progress',
|
|
382
|
-
standardOutput,
|
|
383
|
-
errorOutput,
|
|
384
|
-
null,
|
|
385
|
-
executionTime
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
sendProgress({
|
|
389
|
-
type: 'complete',
|
|
390
|
-
success: true,
|
|
391
|
-
message: standardOutput || errorOutput || 'Push successful'
|
|
392
|
-
});
|
|
393
|
-
} else {
|
|
394
|
-
// 推送失败
|
|
395
|
-
logger.error('推送失败:', errorOutput || standardOutput);
|
|
396
|
-
|
|
397
|
-
// 添加到命令历史(失败情况)
|
|
398
|
-
addCommandToHistory(
|
|
399
|
-
'git push --progress',
|
|
400
|
-
standardOutput,
|
|
401
|
-
errorOutput,
|
|
402
|
-
errorOutput || standardOutput || `Push failed with code ${code}`,
|
|
403
|
-
executionTime
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
sendProgress({
|
|
407
|
-
type: 'complete',
|
|
408
|
-
success: false,
|
|
409
|
-
error: errorOutput || standardOutput || `Push failed with code ${code}`
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
res.end();
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
gitPush.on('error', (error) => {
|
|
416
|
-
logger.error('Git push 进程错误:', error);
|
|
417
|
-
|
|
418
|
-
// 计算执行时间
|
|
419
|
-
const executionTime = Date.now() - startTime;
|
|
420
|
-
|
|
421
|
-
// 添加到命令历史(错误情况)
|
|
422
|
-
addCommandToHistory(
|
|
423
|
-
'git push --progress',
|
|
424
|
-
'',
|
|
425
|
-
'',
|
|
426
|
-
error.message,
|
|
427
|
-
executionTime
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
sendProgress({
|
|
431
|
-
type: 'complete',
|
|
432
|
-
success: false,
|
|
433
|
-
error: error.message
|
|
434
|
-
});
|
|
435
|
-
res.end();
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
} catch (error) {
|
|
439
|
-
// 添加到命令历史(异常情况)
|
|
440
|
-
addCommandToHistory(
|
|
441
|
-
'git push --progress',
|
|
442
|
-
'',
|
|
443
|
-
'',
|
|
444
|
-
error.message,
|
|
445
|
-
0
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
sendProgress({
|
|
449
|
-
type: 'complete',
|
|
450
|
-
success: false,
|
|
451
|
-
error: error.message
|
|
452
|
-
});
|
|
453
|
-
res.end();
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
// 添加git pull API端点
|
|
458
|
-
app.post('/api/pull', async (req, res) => {
|
|
459
|
-
try {
|
|
460
|
-
const { stdout } = await execGitCommand(['pull']);
|
|
461
|
-
res.json({ success: true, message: stdout });
|
|
462
|
-
} catch (error) {
|
|
463
|
-
// 改进错误处理,检查是否需要合并
|
|
464
|
-
// 合并冲突信息可能在 message 或 stderr 中
|
|
465
|
-
const errorMsg = (error.message || '').toLowerCase();
|
|
466
|
-
const stderrMsg = (error.stderr || '').toLowerCase();
|
|
467
|
-
const stdoutMsg = (error.stdout || '').toLowerCase();
|
|
468
|
-
const fullOutput = errorMsg + ' ' + stderrMsg + ' ' + stdoutMsg;
|
|
469
|
-
|
|
470
|
-
const needsMerge = fullOutput.includes('merge') ||
|
|
471
|
-
fullOutput.includes('需要合并') ||
|
|
472
|
-
fullOutput.includes('conflict') ||
|
|
473
|
-
fullOutput.includes('冲突') ||
|
|
474
|
-
fullOutput.includes('automatic merge failed');
|
|
475
|
-
|
|
476
|
-
// 返回更详细的错误信息和标记
|
|
477
|
-
res.status(500).json({
|
|
478
|
-
success: false,
|
|
479
|
-
error: error.message,
|
|
480
|
-
needsMerge: needsMerge,
|
|
481
|
-
// 包含完整的错误输出
|
|
482
|
-
fullError: error.stderr || error.message,
|
|
483
|
-
pullOutput: error.stdout || ''
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
// 添加git fetch --all API端点
|
|
489
|
-
app.post('/api/fetch-all', async (req, res) => {
|
|
490
|
-
try {
|
|
491
|
-
const { stdout } = await execGitCommand(['fetch', '--all']);
|
|
492
|
-
res.json({ success: true, message: stdout });
|
|
493
|
-
} catch (error) {
|
|
494
|
-
res.status(500).json({ success: false, error: error.message });
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
// 获取日志
|
|
499
|
-
app.get('/api/log', async (req, res) => {
|
|
500
|
-
try {
|
|
501
|
-
// 获取分页参数
|
|
502
|
-
const page = parseInt(req.query.page) || 1;
|
|
503
|
-
const limit = parseInt(req.query.limit) || 50;
|
|
504
|
-
const skip = (page - 1) * limit;
|
|
505
|
-
|
|
506
|
-
// 获取筛选参数
|
|
507
|
-
const author = req.query.author ? req.query.author.split(',') : [];
|
|
508
|
-
const message = req.query.message || '';
|
|
509
|
-
const dateFrom = req.query.dateFrom || '';
|
|
510
|
-
const dateTo = req.query.dateTo || '';
|
|
511
|
-
const branch = req.query.branch ? req.query.branch.split(',') : [];
|
|
512
|
-
const withParents = req.query.with_parents === 'true';
|
|
513
|
-
|
|
514
|
-
// 如果指定了分支,直接走 executeGitLogCommand 处理 refs/ 前缀
|
|
515
|
-
if (branch.length > 0) {
|
|
516
|
-
const branchRefs = branch.map(b => b.trim()).join(' ');
|
|
517
|
-
return executeGitLogCommand(res, branchRefs, author, message, dateFrom, dateTo, limit, skip, req.query.all === 'true', withParents);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// 构建 git log 的 argv 数组
|
|
521
|
-
const logArgs = ['log', '--all'];
|
|
522
|
-
|
|
523
|
-
// 作者筛选(支持多作者用正则 OR)
|
|
524
|
-
if (author.length > 0) {
|
|
525
|
-
const validAuthors = author.filter(a => a.trim() !== '');
|
|
526
|
-
if (validAuthors.length === 1) {
|
|
527
|
-
logArgs.push(`--author=${validAuthors[0].trim()}`);
|
|
528
|
-
} else if (validAuthors.length > 1) {
|
|
529
|
-
const authorPattern = validAuthors.map(a => a.trim()).join('\\|');
|
|
530
|
-
logArgs.push(`--author=${authorPattern}`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// 日期范围筛选
|
|
535
|
-
if (dateFrom && dateTo) {
|
|
536
|
-
logArgs.push(`--after=${dateFrom}`, `--before=${dateTo} 23:59:59`);
|
|
537
|
-
} else if (dateFrom) {
|
|
538
|
-
logArgs.push(`--after=${dateFrom}`);
|
|
539
|
-
} else if (dateTo) {
|
|
540
|
-
logArgs.push(`--before=${dateTo} 23:59:59`);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// 提交信息筛选
|
|
544
|
-
if (message) {
|
|
545
|
-
logArgs.push(`--grep=${message}`);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// 分页
|
|
549
|
-
if (req.query.all !== 'true') {
|
|
550
|
-
logArgs.push('-n', String(limit), `--skip=${skip}`);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// format 字符串直接作为 argv 单元素(不再需要外层 shell 双引号)
|
|
554
|
-
let formatString = '%H%x1E%an%x1E%ae%x1E%ad%x1E%B%x1E%D';
|
|
555
|
-
if (withParents) {
|
|
556
|
-
formatString = '%H%x1E%an%x1E%ae%x1E%ad%x1E%B%x1E%D%x1E%P';
|
|
557
|
-
}
|
|
558
|
-
logArgs.push(`--pretty=format:${formatString}`);
|
|
559
|
-
logArgs.push('--date=format-local:%Y-%m-%d %H:%M');
|
|
560
|
-
|
|
561
|
-
let { stdout: logOutput } = await execGitCommand(logArgs);
|
|
562
|
-
processAndSendLogOutput(res, logOutput, page, limit, withParents);
|
|
563
|
-
} catch (error) {
|
|
564
|
-
logger.error('获取Git日志失败:', error);
|
|
565
|
-
res.status(500).json({ error: '获取日志失败: ' + error.message });
|
|
566
|
-
}
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
// 抽取执行Git日志命令的函数
|
|
570
|
-
async function executeGitLogCommand(res, branchRefs, author, message, dateFrom, dateTo, limit, skip, isAll, withParents = false) {
|
|
571
|
-
try {
|
|
572
|
-
// 构建 git log argv 数组
|
|
573
|
-
const logArgs = ['log'];
|
|
574
|
-
|
|
575
|
-
// 准备分支引用,确保它们被正确识别为分支而不是文件名
|
|
576
|
-
// 使用 refs/heads/ 前缀明确指示这是分支
|
|
577
|
-
const formattedBranchRefs = branchRefs.split(' ')
|
|
578
|
-
.map(branch => {
|
|
579
|
-
if (branch.startsWith('refs/') || branch.includes('/')) {
|
|
580
|
-
return branch;
|
|
581
|
-
}
|
|
582
|
-
return `refs/heads/${branch}`;
|
|
583
|
-
});
|
|
584
|
-
logArgs.push(...formattedBranchRefs);
|
|
585
|
-
|
|
586
|
-
// 作者筛选
|
|
587
|
-
if (author.length > 0) {
|
|
588
|
-
const validAuthors = author.filter(a => a.trim() !== '');
|
|
589
|
-
if (validAuthors.length === 1) {
|
|
590
|
-
logArgs.push(`--author=${validAuthors[0].trim()}`);
|
|
591
|
-
} else if (validAuthors.length > 1) {
|
|
592
|
-
const authorPattern = validAuthors.map(a => a.trim()).join('\\|');
|
|
593
|
-
logArgs.push(`--author=${authorPattern}`);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// 日期范围筛选
|
|
598
|
-
if (dateFrom && dateTo) {
|
|
599
|
-
logArgs.push(`--after=${dateFrom}`, `--before=${dateTo} 23:59:59`);
|
|
600
|
-
} else if (dateFrom) {
|
|
601
|
-
logArgs.push(`--after=${dateFrom}`);
|
|
602
|
-
} else if (dateTo) {
|
|
603
|
-
logArgs.push(`--before=${dateTo} 23:59:59`);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// 提交信息筛选
|
|
607
|
-
if (message) {
|
|
608
|
-
logArgs.push(`--grep=${message}`);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// 限制选项
|
|
612
|
-
if (!isAll) {
|
|
613
|
-
logArgs.push('-n', String(limit), `--skip=${skip}`);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
let formatString = '%H%x1E%an%x1E%ae%x1E%ad%x1E%B%x1E%D';
|
|
617
|
-
if (withParents) {
|
|
618
|
-
formatString = '%H%x1E%an%x1E%ae%x1E%ad%x1E%B%x1E%D%x1E%P';
|
|
619
|
-
}
|
|
620
|
-
logArgs.push(`--pretty=format:${formatString}`);
|
|
621
|
-
logArgs.push('--date=format-local:%Y-%m-%d %H:%M');
|
|
622
|
-
|
|
623
|
-
logger.info(`执行Git log 命令 argv:`, logArgs);
|
|
624
|
-
|
|
625
|
-
const { stdout: logOutput } = await execGitCommand(logArgs);
|
|
626
|
-
processAndSendLogOutput(res, logOutput, skip / limit + 1, limit, withParents);
|
|
627
|
-
} catch (error) {
|
|
628
|
-
logger.error('执行Git日志命令失败:', error);
|
|
629
|
-
res.status(500).json({ error: '获取日志失败: ' + error.message });
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// 抽取处理输出并发送响应的函数(优化版本:不再依赖总数计算)
|
|
634
|
-
function processAndSendLogOutput(res, logOutput, page, limit, withParents = false) {
|
|
635
|
-
// 替换提交记录之间的换行符 - 使用 ASCII 控制字符 \x1E 匹配
|
|
636
|
-
logOutput = logOutput.replace(/\n(?=[a-f0-9]{40}\x1E)/g, "<<<RECORD_SEPARATOR>>>");
|
|
637
|
-
|
|
638
|
-
// 按分隔符拆分日志条目
|
|
639
|
-
const logEntries = logOutput.split("<<<RECORD_SEPARATOR>>>");
|
|
640
|
-
|
|
641
|
-
// 处理每个日志条目
|
|
642
|
-
const data = logEntries.map(entry => {
|
|
643
|
-
// 使用 ASCII 控制字符 \x1E 拆分字段
|
|
644
|
-
const parts = entry.split('\x1E');
|
|
645
|
-
if (parts.length >= 5) {
|
|
646
|
-
const result = {
|
|
647
|
-
hash: parts[0],
|
|
648
|
-
author: parts[1],
|
|
649
|
-
email: parts[2],
|
|
650
|
-
date: parts[3],
|
|
651
|
-
message: parts[4],
|
|
652
|
-
branch: parts[5] || ''
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
// 如果请求了父提交信息,添加到结果中
|
|
656
|
-
if (withParents && parts[6]) {
|
|
657
|
-
result.parents = parts[6].split(' ');
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
return result;
|
|
661
|
-
}
|
|
662
|
-
return null;
|
|
663
|
-
}).filter(item => item !== null);
|
|
664
|
-
|
|
665
|
-
// 优化:通过返回的数据量判断是否有更多数据,而不依赖总数计算
|
|
666
|
-
// 如果返回的数据量等于请求的limit,说明可能还有更多数据
|
|
667
|
-
// 如果返回的数据量小于limit,说明已经到底了
|
|
668
|
-
const hasMore = data.length === limit;
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
// 返回提交历史数据,包括是否有更多数据的标志
|
|
672
|
-
res.json({
|
|
673
|
-
data: data,
|
|
674
|
-
total: -1, // 不再提供总数,前端不应依赖此字段
|
|
675
|
-
page: page,
|
|
676
|
-
limit: limit,
|
|
677
|
-
hasMore: hasMore
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// ========== Diff 大文件检测和过滤工具函数 ==========
|
|
682
|
-
|
|
683
|
-
const { checkShouldSkipDiff, checkDiffSize, getDiffStats } = createDiffHelpers({ execGitCommand });
|
|
684
|
-
|
|
685
|
-
registerGitDiffRoutes({
|
|
686
|
-
app,
|
|
687
|
-
execGitCommand
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
registerGitStashRoutes({
|
|
691
|
-
app,
|
|
692
|
-
execGitCommand,
|
|
693
|
-
configManager
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
registerGitTagRoutes({
|
|
697
|
-
app,
|
|
698
|
-
execGitCommand,
|
|
699
|
-
clearCommandHistory
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
// 重置暂存区 (git reset HEAD)
|
|
703
|
-
app.post('/api/reset-head', async (req, res) => {
|
|
704
|
-
try {
|
|
705
|
-
// 执行 git reset HEAD 命令
|
|
706
|
-
await execGitCommand(['reset', 'HEAD']);
|
|
707
|
-
res.json({ success: true });
|
|
708
|
-
} catch (error) {
|
|
709
|
-
logger.error('重置暂存区失败:', error);
|
|
710
|
-
res.status(500).json({
|
|
711
|
-
success: false,
|
|
712
|
-
error: `重置暂存区失败: ${error.message}`
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
// 重置到远程分支 (git reset --hard origin/branch)
|
|
718
|
-
app.post('/api/reset-to-remote', async (req, res) => {
|
|
719
|
-
try {
|
|
720
|
-
const { branch } = req.body;
|
|
721
|
-
|
|
722
|
-
if (!branch) {
|
|
723
|
-
return res.status(400).json({
|
|
724
|
-
success: false,
|
|
725
|
-
error: '缺少分支名称参数'
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// 尝试清理 Git 锁文件(破坏性入口:force=true 绕过 mtime 判断,
|
|
730
|
-
// 仍走 PID liveness 校验——用户主动触发的 reset --hard 不该被
|
|
731
|
-
// 一个刚崩出来的新孤儿锁挡住)
|
|
732
|
-
await checkAndClearGitLock({ force: true });
|
|
733
|
-
|
|
734
|
-
// 执行 git reset --hard origin/branch 命令
|
|
735
|
-
await execGitCommand(['reset', '--hard', `origin/${branch}`]);
|
|
736
|
-
res.json({ success: true });
|
|
737
|
-
} catch (error) {
|
|
738
|
-
logger.error('重置到远程分支失败:', error);
|
|
739
|
-
res.status(500).json({
|
|
740
|
-
success: false,
|
|
741
|
-
error: `重置到远程分支失败: ${error.message}`
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
// 清除本地所有更改,包括未跟踪文件 (git reset --hard && git clean -fd)
|
|
747
|
-
app.post('/api/discard-all-changes', async (req, res) => {
|
|
748
|
-
try {
|
|
749
|
-
// 尝试清理 Git 锁文件(破坏性入口:force=true 绕过 mtime 判断,
|
|
750
|
-
// 仍走 PID liveness 校验——用户主动触发的 discard-all 不该被
|
|
751
|
-
// 一个刚崩出来的新孤儿锁挡住)
|
|
752
|
-
await checkAndClearGitLock({ force: true });
|
|
753
|
-
|
|
754
|
-
// 1. 执行 git reset --hard 丢弃已跟踪文件的更改
|
|
755
|
-
await execGitCommand(['reset', '--hard']);
|
|
756
|
-
|
|
757
|
-
// 2. 执行 git clean -fd 移除未跟踪的文件和目录
|
|
758
|
-
await execGitCommand(['clean', '-fd']);
|
|
759
|
-
|
|
760
|
-
res.json({ success: true });
|
|
761
|
-
} catch (error) {
|
|
762
|
-
logger.error('清除所有更改失败:', error);
|
|
763
|
-
res.status(500).json({
|
|
764
|
-
success: false,
|
|
765
|
-
error: `清除所有更改失败: ${error.message}`
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
// 获取提交的文件列表
|
|
771
|
-
app.get('/api/commit-files', async (req, res) => {
|
|
772
|
-
try {
|
|
773
|
-
const hash = req.query.hash;
|
|
774
|
-
|
|
775
|
-
if (!hash) {
|
|
776
|
-
return res.status(400).json({
|
|
777
|
-
success: false,
|
|
778
|
-
error: '缺少提交哈希参数'
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
logger.info(`获取提交文件列表: hash=${hash}`);
|
|
783
|
-
|
|
784
|
-
// 执行命令获取提交中修改的文件列表
|
|
785
|
-
const { stdout } = await execGitCommand(['show', '--name-only', '--format=', hash]);
|
|
786
|
-
|
|
787
|
-
// 将输出按行分割,并过滤掉空行
|
|
788
|
-
const files = stdout.split('\n').filter(line => line.trim());
|
|
789
|
-
logger.info(`找到${files.length}个文件:`, files);
|
|
790
|
-
|
|
791
|
-
res.json({
|
|
792
|
-
success: true,
|
|
793
|
-
files
|
|
794
|
-
});
|
|
795
|
-
} catch (error) {
|
|
796
|
-
logger.error('获取提交文件列表失败:', error);
|
|
797
|
-
res.status(500).json({
|
|
798
|
-
success: false,
|
|
799
|
-
error: `获取提交文件列表失败: ${error.message}`
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
// 获取提交中特定文件的差异
|
|
805
|
-
app.get('/api/commit-file-diff', async (req, res) => {
|
|
806
|
-
try {
|
|
807
|
-
const hash = req.query.hash;
|
|
808
|
-
const filePath = req.query.file;
|
|
809
|
-
|
|
810
|
-
if (!hash || !filePath) {
|
|
811
|
-
return res.status(400).json({
|
|
812
|
-
success: false,
|
|
813
|
-
error: '缺少必要参数'
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
logger.info(`获取提交文件差异: hash=${hash}, file=${filePath}`);
|
|
818
|
-
|
|
819
|
-
const diffCommandArgs = ['show', hash, '--', filePath];
|
|
820
|
-
|
|
821
|
-
// 使用优化的检查函数
|
|
822
|
-
// checkShouldSkipDiff 接受字符串命令,这里只用于大小判断,先按字面占位;
|
|
823
|
-
// 走 execGitCommand 时仍用 argv 数组保证转义正确。
|
|
824
|
-
const skipCheck = await checkShouldSkipDiff(filePath, `git show ${hash} -- "${filePath}"`);
|
|
825
|
-
if (skipCheck.shouldSkip) {
|
|
826
|
-
return res.json({
|
|
827
|
-
success: true,
|
|
828
|
-
diff: skipCheck.reason,
|
|
829
|
-
isLargeFile: true,
|
|
830
|
-
stats: skipCheck.stats
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// 执行命令获取文件差异
|
|
835
|
-
const { stdout } = await execGitCommand(diffCommandArgs);
|
|
836
|
-
|
|
837
|
-
logger.info(`获取到差异内容,长度: ${stdout.length}`);
|
|
838
|
-
|
|
839
|
-
// 检查实际diff大小
|
|
840
|
-
const sizeCheck = checkDiffSize(stdout, 500);
|
|
841
|
-
if (sizeCheck) {
|
|
842
|
-
return res.json({
|
|
843
|
-
success: true,
|
|
844
|
-
...sizeCheck
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// 统计增加和删除行数
|
|
849
|
-
const stats = getDiffStats(stdout);
|
|
850
|
-
|
|
851
|
-
res.json({
|
|
852
|
-
success: true,
|
|
853
|
-
diff: stdout,
|
|
854
|
-
stats
|
|
855
|
-
});
|
|
856
|
-
} catch (error) {
|
|
857
|
-
logger.error('获取提交文件差异失败:', error);
|
|
858
|
-
res.status(500).json({
|
|
859
|
-
success: false,
|
|
860
|
-
error: `获取提交文件差异失败: ${error.message}`
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
app.get('/api/commit-file-content', async (req, res) => {
|
|
866
|
-
try {
|
|
867
|
-
const hash = req.query.hash;
|
|
868
|
-
const filePath = req.query.file;
|
|
869
|
-
const version = req.query.version;
|
|
870
|
-
|
|
871
|
-
if (!hash || !filePath) {
|
|
872
|
-
return res.status(400).json({
|
|
873
|
-
success: false,
|
|
874
|
-
error: '缺少必要参数'
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
const isOld = String(version || 'new') === 'old';
|
|
879
|
-
const targetHash = isOld ? `${hash}^` : `${hash}`;
|
|
880
|
-
|
|
881
|
-
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;
|
|
882
|
-
if (skipExtensions.test(String(filePath))) {
|
|
883
|
-
return res.json({
|
|
884
|
-
success: true,
|
|
885
|
-
isBinary: true,
|
|
886
|
-
content: '⚠️ 检测到二进制/编译产物文件,不支持以文本形式显示完整内容。\n\n提示:建议使用专业工具或命令行查看。'
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
const spec = `${targetHash}:${filePath}`;
|
|
891
|
-
|
|
892
|
-
let sizeBytes = 0;
|
|
893
|
-
try {
|
|
894
|
-
const { stdout: sizeOut } = await execGitCommand(['cat-file', '-s', spec], { log: false });
|
|
895
|
-
sizeBytes = parseInt(String(sizeOut).trim(), 10) || 0;
|
|
896
|
-
} catch (e) {
|
|
897
|
-
return res.json({
|
|
898
|
-
success: true,
|
|
899
|
-
notFound: true,
|
|
900
|
-
content: isOld
|
|
901
|
-
? '该文件在该提交的上一个版本中不存在(可能是新增文件)。'
|
|
902
|
-
: '该文件在该提交版本中不存在(可能是删除文件或重命名)。'
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const maxBytes = 1024 * 1024;
|
|
907
|
-
if (sizeBytes > maxBytes) {
|
|
908
|
-
return res.json({
|
|
909
|
-
success: true,
|
|
910
|
-
isLargeFile: true,
|
|
911
|
-
size: sizeBytes,
|
|
912
|
-
content: `⚠️ 文件内容过大 (${(sizeBytes / 1024).toFixed(1)} KB),已跳过显示以避免浏览器卡顿。\n\n提示:建议使用命令行或编辑器打开查看。`
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
try {
|
|
917
|
-
const { stdout } = await execGitCommand(['show', spec], { log: false });
|
|
918
|
-
res.json({
|
|
919
|
-
success: true,
|
|
920
|
-
content: stdout ?? ''
|
|
921
|
-
});
|
|
922
|
-
} catch (error) {
|
|
923
|
-
res.status(500).json({
|
|
924
|
-
success: false,
|
|
925
|
-
error: `获取文件内容失败: ${error.message}`
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
} catch (error) {
|
|
929
|
-
res.status(500).json({
|
|
930
|
-
success: false,
|
|
931
|
-
error: error.message
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
// 撤销某个提交 (revert)
|
|
937
|
-
app.post('/api/revert-commit', async (req, res) => {
|
|
938
|
-
try {
|
|
939
|
-
const { hash } = req.body;
|
|
940
|
-
|
|
941
|
-
if (!hash) {
|
|
942
|
-
return res.status(400).json({
|
|
943
|
-
success: false,
|
|
944
|
-
error: '缺少提交哈希参数'
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
logger.info(`执行撤销提交操作: hash=${hash}`);
|
|
949
|
-
|
|
950
|
-
// 执行git revert命令
|
|
951
|
-
await execGitCommand(['revert', '--no-edit', hash]);
|
|
952
|
-
|
|
953
|
-
res.json({
|
|
954
|
-
success: true,
|
|
955
|
-
message: `已成功撤销提交 ${hash}`
|
|
956
|
-
});
|
|
957
|
-
} catch (error) {
|
|
958
|
-
logger.error('撤销提交失败:', error);
|
|
959
|
-
res.status(500).json({
|
|
960
|
-
success: false,
|
|
961
|
-
error: `撤销提交失败: ${error.message}`
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
// Cherry-pick某个提交
|
|
967
|
-
app.post('/api/cherry-pick-commit', async (req, res) => {
|
|
968
|
-
try {
|
|
969
|
-
const { hash } = req.body;
|
|
970
|
-
|
|
971
|
-
if (!hash) {
|
|
972
|
-
return res.status(400).json({
|
|
973
|
-
success: false,
|
|
974
|
-
error: '缺少提交哈希参数'
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
logger.info(`执行Cherry-pick操作: hash=${hash}`);
|
|
979
|
-
|
|
980
|
-
// 执行git cherry-pick命令
|
|
981
|
-
await execGitCommand(['cherry-pick', hash]);
|
|
982
|
-
|
|
983
|
-
res.json({
|
|
984
|
-
success: true,
|
|
985
|
-
message: `已成功Cherry-pick提交 ${hash}`
|
|
986
|
-
});
|
|
987
|
-
} catch (error) {
|
|
988
|
-
logger.error('Cherry-pick提交失败:', error);
|
|
989
|
-
res.status(500).json({
|
|
990
|
-
success: false,
|
|
991
|
-
error: `Cherry-pick提交失败: ${error.message}`
|
|
992
|
-
});
|
|
993
|
-
}
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
// 重置到指定提交(hard)
|
|
997
|
-
app.post('/api/reset-to-commit', async (req, res) => {
|
|
998
|
-
try {
|
|
999
|
-
const { hash } = req.body;
|
|
1000
|
-
|
|
1001
|
-
if (!hash) {
|
|
1002
|
-
return res.status(400).json({
|
|
1003
|
-
success: false,
|
|
1004
|
-
error: '缺少提交哈希参数'
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
logger.info(`执行重置到指定提交操作: hash=${hash}`);
|
|
1009
|
-
|
|
1010
|
-
// 执行git reset --hard命令
|
|
1011
|
-
await execGitCommand(['reset', '--hard', hash]);
|
|
1012
|
-
|
|
1013
|
-
res.json({
|
|
1014
|
-
success: true,
|
|
1015
|
-
message: `已成功重置到提交 ${hash}`
|
|
1016
|
-
});
|
|
1017
|
-
} catch (error) {
|
|
1018
|
-
logger.error('重置到指定提交失败:', error);
|
|
1019
|
-
res.status(500).json({
|
|
1020
|
-
success: false,
|
|
1021
|
-
error: `重置到指定提交失败: ${error.message}`
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
// 获取提交完整 diff(用于复制提交内容)
|
|
1027
|
-
app.get('/api/commit-diff-full', async (req, res) => {
|
|
1028
|
-
try {
|
|
1029
|
-
const hash = req.query.hash;
|
|
1030
|
-
if (!hash) {
|
|
1031
|
-
return res.status(400).json({ success: false, error: '缺少提交哈希参数' });
|
|
1032
|
-
}
|
|
1033
|
-
const { stdout } = await execGitCommand(['show', hash]);
|
|
1034
|
-
// 限制最大 200KB 防止内容过大
|
|
1035
|
-
const MAX = 200 * 1024;
|
|
1036
|
-
const content = stdout.length > MAX ? stdout.slice(0, MAX) + '\n\n[内容过大,已截断]' : stdout;
|
|
1037
|
-
res.json({ success: true, content });
|
|
1038
|
-
} catch (error) {
|
|
1039
|
-
res.status(500).json({ success: false, error: `获取提交内容失败: ${error.message}` });
|
|
1040
|
-
}
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
// 添加清理Git锁定文件的接口
|
|
1044
|
-
app.post('/api/remove-lock', async (req, res) => {
|
|
1045
|
-
try {
|
|
1046
|
-
const gitDir = path.join(process.cwd(), '.git')
|
|
1047
|
-
const indexLockFile = path.join(gitDir, 'index.lock')
|
|
1048
|
-
|
|
1049
|
-
// 检查文件是否存在
|
|
1050
|
-
try {
|
|
1051
|
-
await fs.access(indexLockFile)
|
|
1052
|
-
// 如果文件存在,尝试删除它
|
|
1053
|
-
await fs.unlink(indexLockFile)
|
|
1054
|
-
res.json({ success: true, message: '已清理锁定文件' })
|
|
1055
|
-
} catch (error) {
|
|
1056
|
-
// 如果文件不存在,也返回成功
|
|
1057
|
-
if (error.code === 'ENOENT') {
|
|
1058
|
-
res.json({ success: true, message: '没有发现锁定文件' })
|
|
1059
|
-
} else {
|
|
1060
|
-
throw error
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
logger.error('清理锁定文件失败:', error)
|
|
1065
|
-
res.status(500).json({
|
|
1066
|
-
success: false,
|
|
1067
|
-
error: `清理锁定文件失败: ${error.message}`
|
|
1068
|
-
})
|
|
1069
|
-
}
|
|
1070
|
-
})
|
|
1071
|
-
|
|
1072
|
-
// 清除Git用户配置
|
|
1073
|
-
app.post('/api/clear-user-config', async (req, res) => {
|
|
1074
|
-
try {
|
|
1075
|
-
// 检查全局配置是否存在,如果存在才删除
|
|
1076
|
-
try {
|
|
1077
|
-
const { stdout: userName } = await execGitCommand(['config', '--global', 'user.name']);
|
|
1078
|
-
if (userName.trim()) {
|
|
1079
|
-
await execGitCommand(['config', '--global', '--unset', 'user.name']);
|
|
1080
|
-
}
|
|
1081
|
-
} catch (error) {
|
|
1082
|
-
logger.info('全局用户名配置检查失败,可能不存在:', error.message);
|
|
1083
|
-
// 忽略错误继续执行
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
try {
|
|
1087
|
-
const { stdout: userEmail } = await execGitCommand(['config', '--global', 'user.email']);
|
|
1088
|
-
if (userEmail.trim()) {
|
|
1089
|
-
await execGitCommand(['config', '--global', '--unset', 'user.email']);
|
|
1090
|
-
}
|
|
1091
|
-
} catch (error) {
|
|
1092
|
-
logger.info('全局邮箱配置检查失败,可能不存在:', error.message);
|
|
1093
|
-
// 忽略错误继续执行
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
res.json({ success: true, message: '已清除全局Git用户配置' });
|
|
1097
|
-
} catch (error) {
|
|
1098
|
-
res.status(500).json({
|
|
1099
|
-
success: false,
|
|
1100
|
-
error: `清除全局Git用户配置失败: ${error.message}`
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
// 恢复Git用户配置
|
|
1106
|
-
app.post('/api/restore-user-config', async (req, res) => {
|
|
1107
|
-
try {
|
|
1108
|
-
const { name, email } = req.body;
|
|
1109
|
-
if (!name || !email) {
|
|
1110
|
-
return res.status(400).json({
|
|
1111
|
-
success: false,
|
|
1112
|
-
error: '需要提供用户名和邮箱'
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
await execGitCommand(['config', '--global', 'user.name', name]);
|
|
1117
|
-
await execGitCommand(['config', '--global', 'user.email', email]);
|
|
1118
|
-
res.json({ success: true, message: '已更新全局Git用户配置' });
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
res.status(500).json({
|
|
1121
|
-
success: false,
|
|
1122
|
-
error: `更新全局Git用户配置失败: ${error.message}`
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
// 初始化Git仓库
|
|
1128
|
-
app.post('/api/git-init', async (req, res) => {
|
|
1129
|
-
try {
|
|
1130
|
-
const { stdout } = await execGitCommand(['init']);
|
|
1131
|
-
res.json({ success: true, output: stdout.trim() });
|
|
1132
|
-
} catch (error) {
|
|
1133
|
-
logger.error('git init 失败:', error);
|
|
1134
|
-
res.json({ success: false, error: error.message || 'git init 失败' });
|
|
1135
|
-
}
|
|
1136
|
-
});
|
|
1137
|
-
|
|
1138
|
-
// 添加远程仓库的API
|
|
1139
|
-
app.post('/api/add-remote', express.json(), async (req, res) => {
|
|
1140
|
-
try {
|
|
1141
|
-
if (!getIsGitRepo()) {
|
|
1142
|
-
return res.json({ success: false, error: '当前目录不是Git仓库' });
|
|
1143
|
-
}
|
|
1144
|
-
const { name = 'origin', url } = req.body;
|
|
1145
|
-
if (!url || !url.trim()) {
|
|
1146
|
-
return res.json({ success: false, error: '远程仓库地址不能为空' });
|
|
1147
|
-
}
|
|
1148
|
-
await execGitCommand(['remote', 'add', name, url.trim()]);
|
|
1149
|
-
res.json({ success: true });
|
|
1150
|
-
} catch (error) {
|
|
1151
|
-
logger.error('添加远程仓库失败:', error);
|
|
1152
|
-
res.json({ success: false, error: error.message || '添加远程仓库失败' });
|
|
1153
|
-
}
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
// 获取远程仓库URL的API
|
|
1157
|
-
app.get('/api/remote-url', async (req, res) => {
|
|
1158
|
-
try {
|
|
1159
|
-
// 检查当前目录是否是Git仓库
|
|
1160
|
-
if (!getIsGitRepo()) {
|
|
1161
|
-
return res.json({ success: false, error: '当前目录不是Git仓库' });
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// 执行git命令获取远程仓库URL
|
|
1165
|
-
const { stdout } = await execGitCommand(['config', '--get', 'remote.origin.url']);
|
|
1166
|
-
|
|
1167
|
-
// 返回远程仓库URL
|
|
1168
|
-
res.json({
|
|
1169
|
-
success: true,
|
|
1170
|
-
url: stdout.trim()
|
|
1171
|
-
});
|
|
1172
|
-
} catch (error) {
|
|
1173
|
-
logger.error('获取远程仓库URL失败:', error);
|
|
1174
|
-
res.json({
|
|
1175
|
-
success: false,
|
|
1176
|
-
error: error.message || '获取远程仓库URL失败'
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
});
|
|
1180
|
-
|
|
1181
|
-
// 获取所有作者列表
|
|
1182
|
-
app.get('/api/authors', async (req, res) => {
|
|
1183
|
-
try {
|
|
1184
|
-
// 使用git命令获取所有提交者,不依赖Unix命令
|
|
1185
|
-
const { stdout } = await execGitCommand(['log', '--format=%an']);
|
|
1186
|
-
|
|
1187
|
-
// 将结果按行分割并过滤空行
|
|
1188
|
-
const lines = stdout.split('\n').filter(author => author.trim() !== '');
|
|
1189
|
-
|
|
1190
|
-
// 手动去重,不依赖Unix的uniq命令
|
|
1191
|
-
const uniqueAuthors = Array.from(new Set(lines)).sort();
|
|
1192
|
-
|
|
1193
|
-
// 控制台输出一下搜索示例,方便调试
|
|
1194
|
-
if (uniqueAuthors.length > 1) {
|
|
1195
|
-
const searchExample = uniqueAuthors.slice(0, 2).join('|');
|
|
1196
|
-
logger.info(`多作者搜索示例: git log --author="${searchExample}"`);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
res.json({
|
|
1200
|
-
success: true,
|
|
1201
|
-
authors: uniqueAuthors
|
|
1202
|
-
});
|
|
1203
|
-
} catch (error) {
|
|
1204
|
-
logger.error('获取作者列表失败:', error);
|
|
1205
|
-
res.status(500).json({
|
|
1206
|
-
success: false,
|
|
1207
|
-
error: '获取作者列表失败: ' + error.message
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
import express from 'express';
|
|
16
|
+
import logger from '../utils/logger.js'
|
|
17
|
+
import fs from 'fs/promises';
|
|
18
|
+
import os from 'os';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { spawn } from 'child_process';
|
|
21
|
+
|
|
22
|
+
import { registerGitDiffRoutes } from './git/diff.js';
|
|
23
|
+
import { createDiffHelpers } from './git/diffUtils.js';
|
|
24
|
+
import { registerGitStashRoutes } from './git/stash.js';
|
|
25
|
+
import { registerGitTagRoutes } from './git/tags.js';
|
|
26
|
+
|
|
27
|
+
export function registerGitOpsRoutes({
|
|
28
|
+
app,
|
|
29
|
+
execGitCommand,
|
|
30
|
+
configManager,
|
|
31
|
+
execGitAddWithLockFilter,
|
|
32
|
+
addCommandToHistory,
|
|
33
|
+
clearCommandHistory,
|
|
34
|
+
checkAndClearGitLock,
|
|
35
|
+
getIsGitRepo,
|
|
36
|
+
setRecentPushStatus
|
|
37
|
+
}) {
|
|
38
|
+
// 提交更改
|
|
39
|
+
app.post('/api/commit', express.json(), async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const { message, hasNewlines, noVerify } = req.body;
|
|
42
|
+
|
|
43
|
+
// 构建 git commit 命令
|
|
44
|
+
const commitArgs = ['commit'];
|
|
45
|
+
|
|
46
|
+
// 如果消息包含换行符,使用文件方式提交
|
|
47
|
+
let tempFile
|
|
48
|
+
if (hasNewlines) {
|
|
49
|
+
// 创建临时文件存储提交信息
|
|
50
|
+
tempFile = path.join(os.tmpdir(), `commit-msg-${Date.now()}.txt`);
|
|
51
|
+
await fs.writeFile(tempFile, message);
|
|
52
|
+
commitArgs.push('-F', tempFile);
|
|
53
|
+
} else {
|
|
54
|
+
// 否则直接在命令行中提供消息
|
|
55
|
+
commitArgs.push('-m', message);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 添加 --no-verify 参数
|
|
59
|
+
if (noVerify) {
|
|
60
|
+
commitArgs.push('--no-verify');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.info(`commitArgs ==>`, commitArgs);
|
|
64
|
+
// 执行提交命令
|
|
65
|
+
await execGitCommand(commitArgs);
|
|
66
|
+
|
|
67
|
+
// 如果使用了临时文件,删除它
|
|
68
|
+
if (hasNewlines && tempFile) {
|
|
69
|
+
await fs.unlink(tempFile).catch(() => {});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
res.json({ success: true });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
res.status(500).json({ success: false, error: error.message });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// 添加 add 接口
|
|
79
|
+
app.post('/api/add', async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
// 直接执行 git add . - 前端已经做了锁定文件过滤判断
|
|
82
|
+
await execGitCommand(['add', '.']);
|
|
83
|
+
res.json({ success: true });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
res.status(500).json({ success: false, error: error.message });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 添加带锁定文件过滤的 add 接口
|
|
90
|
+
app.post('/api/add-filtered', async (req, res) => {
|
|
91
|
+
try {
|
|
92
|
+
// 使用带锁定文件过滤的 git add
|
|
93
|
+
await execGitAddWithLockFilter();
|
|
94
|
+
res.json({ success: true });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
res.status(500).json({ success: false, error: error.message });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 添加 add-all 接口(直接执行 git add . 不考虑锁定文件)
|
|
101
|
+
app.post('/api/add-all', async (req, res) => {
|
|
102
|
+
try {
|
|
103
|
+
// 直接执行 git add . 不考虑锁定文件
|
|
104
|
+
await execGitCommand(['add', '.']);
|
|
105
|
+
res.json({ success: true });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
res.status(500).json({ success: false, error: error.message });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 添加单个文件到暂存区
|
|
112
|
+
app.post('/api/add-file', async (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
const { filePath } = req.body;
|
|
115
|
+
|
|
116
|
+
if (!filePath) {
|
|
117
|
+
return res.status(400).json({
|
|
118
|
+
success: false,
|
|
119
|
+
error: '缺少文件路径参数'
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 执行 git add 命令添加特定文件
|
|
124
|
+
await execGitCommand(['add', '--', filePath]);
|
|
125
|
+
res.json({ success: true });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
res.status(500).json({ success: false, error: error.message });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 批量添加文件到暂存区(一次 git add 命令,避免 index.lock 竞争)
|
|
132
|
+
app.post('/api/add-files', async (req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
const { filePaths } = req.body || {};
|
|
135
|
+
|
|
136
|
+
if (!Array.isArray(filePaths) || filePaths.length === 0) {
|
|
137
|
+
return res.status(400).json({
|
|
138
|
+
success: false,
|
|
139
|
+
error: '缺少文件路径参数',
|
|
140
|
+
successCount: 0,
|
|
141
|
+
failedCount: 0
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 过滤掉空路径与重复路径
|
|
146
|
+
const uniquePaths = [...new Set(filePaths.filter(p => typeof p === 'string' && p.length > 0))];
|
|
147
|
+
if (uniquePaths.length === 0) {
|
|
148
|
+
return res.status(400).json({
|
|
149
|
+
success: false,
|
|
150
|
+
error: '没有有效的文件路径',
|
|
151
|
+
successCount: 0,
|
|
152
|
+
failedCount: 0
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 一次 git add 命令暂存所有文件(不再需要 shell 转义,execFile argv 数组天然支持任意路径)
|
|
157
|
+
await execGitCommand(['add', ...uniquePaths]);
|
|
158
|
+
|
|
159
|
+
res.json({
|
|
160
|
+
success: true,
|
|
161
|
+
successCount: uniquePaths.length,
|
|
162
|
+
failedCount: 0,
|
|
163
|
+
filePaths: uniquePaths
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
res.status(500).json({ success: false, error: error.message, successCount: 0, failedCount: 0 });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// 从暂存区移除单个文件
|
|
171
|
+
app.post('/api/unstage-file', async (req, res) => {
|
|
172
|
+
try {
|
|
173
|
+
const { filePath } = req.body;
|
|
174
|
+
|
|
175
|
+
if (!filePath) {
|
|
176
|
+
return res.status(400).json({
|
|
177
|
+
success: false,
|
|
178
|
+
error: '缺少文件路径参数'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 执行 git reset HEAD 命令移除特定文件的暂存
|
|
183
|
+
await execGitCommand(['reset', 'HEAD', '--', filePath]);
|
|
184
|
+
res.json({ success: true });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
res.status(500).json({ success: false, error: error.message });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// 推送更改
|
|
191
|
+
app.post('/api/push', async (req, res) => {
|
|
192
|
+
try {
|
|
193
|
+
const { stdout } = await execGitCommand(['push']);
|
|
194
|
+
|
|
195
|
+
// 推送成功后,设置推送状态标记
|
|
196
|
+
setRecentPushStatus({
|
|
197
|
+
justPushed: true,
|
|
198
|
+
pushTime: Date.now(),
|
|
199
|
+
validDuration: 10000 // 10秒内认为分支状态是同步的
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
logger.info('推送成功,已设置推送状态标记');
|
|
203
|
+
res.json({ success: true, message: stdout });
|
|
204
|
+
} catch (error) {
|
|
205
|
+
res.status(500).json({ success: false, error: error.message });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// 推送并设置上游分支(git push -u origin <branch>)
|
|
210
|
+
app.post('/api/git/push-with-upstream', async (req, res) => {
|
|
211
|
+
try {
|
|
212
|
+
const { branch } = req.body || {};
|
|
213
|
+
if (!branch || typeof branch !== 'string') {
|
|
214
|
+
return res.status(400).json({ success: false, error: '缺少 branch 参数' });
|
|
215
|
+
}
|
|
216
|
+
const { stdout } = await execGitCommand(['push', '-u', 'origin', branch]);
|
|
217
|
+
setRecentPushStatus({
|
|
218
|
+
justPushed: true,
|
|
219
|
+
pushTime: Date.now(),
|
|
220
|
+
validDuration: 10000
|
|
221
|
+
});
|
|
222
|
+
logger.info(`推送并设置上游成功: origin/${branch}`);
|
|
223
|
+
res.json({ success: true, message: stdout });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
res.status(500).json({ success: false, error: error.message });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// 读取单个 git 全局配置
|
|
230
|
+
// GET /api/git/global-config?key=<name>
|
|
231
|
+
// key 不存在时返回空串(与 git --get 行为一致;前端 getGlobalConfig 容错为空)
|
|
232
|
+
app.get('/api/git/global-config', async (req, res) => {
|
|
233
|
+
try {
|
|
234
|
+
const key = typeof req.query.key === 'string' ? req.query.key.trim() : '';
|
|
235
|
+
if (!key) {
|
|
236
|
+
return res.status(400).json({ success: false, error: '缺少 key 参数' });
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const { stdout } = await execGitCommand(['config', '--global', '--get', key], { log: false });
|
|
240
|
+
return res.json({ success: true, value: String(stdout || '').trim() });
|
|
241
|
+
} catch (err) {
|
|
242
|
+
// git --get 在 key 不存在时返回非零退出码 → 视为"未设置",返回空串
|
|
243
|
+
return res.json({ success: true, value: '' });
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
res.status(500).json({ success: false, error: error.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// 写入单个 git 全局配置
|
|
251
|
+
// POST /api/git/global-config body: { key, value }
|
|
252
|
+
// value 为空字符串时执行 --unset,与 git 语义一致(允许前端"清空某项")
|
|
253
|
+
app.post('/api/git/global-config', async (req, res) => {
|
|
254
|
+
try {
|
|
255
|
+
const { key, value } = req.body || {};
|
|
256
|
+
if (!key || typeof key !== 'string') {
|
|
257
|
+
return res.status(400).json({ success: false, error: '缺少 key 参数' });
|
|
258
|
+
}
|
|
259
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
260
|
+
if (!trimmed) {
|
|
261
|
+
await execGitCommand(['config', '--global', '--unset', key]);
|
|
262
|
+
} else {
|
|
263
|
+
await execGitCommand(['config', '--global', key, trimmed]);
|
|
264
|
+
}
|
|
265
|
+
res.json({ success: true });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
res.status(500).json({ success: false, error: error.message });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// 带进度的推送更改 (SSE)
|
|
272
|
+
app.post('/api/push-with-progress', async (req, res) => {
|
|
273
|
+
// 设置SSE响应头
|
|
274
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
275
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
276
|
+
res.setHeader('Connection', 'keep-alive');
|
|
277
|
+
res.flushHeaders();
|
|
278
|
+
|
|
279
|
+
const sendProgress = (data) => {
|
|
280
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
// 获取当前工作目录 - 与execGitCommand保持一致
|
|
285
|
+
const cwdArg = process.argv.find(arg => arg.startsWith('--path')) || process.argv.find(arg => arg.startsWith('--cwd'));
|
|
286
|
+
let workDir = process.cwd();
|
|
287
|
+
if (cwdArg) {
|
|
288
|
+
const [, value] = cwdArg.split('=');
|
|
289
|
+
workDir = value || process.cwd();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
logger.info('开始推送,工作目录:', workDir);
|
|
293
|
+
|
|
294
|
+
// 记录开始时间
|
|
295
|
+
const startTime = Date.now();
|
|
296
|
+
|
|
297
|
+
// 发送开始消息
|
|
298
|
+
sendProgress({
|
|
299
|
+
type: 'progress',
|
|
300
|
+
message: 'Starting to push to the remote repository...'
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// 使用spawn执行git push --progress
|
|
304
|
+
const gitPush = spawn('git', ['push', '--progress'], {
|
|
305
|
+
cwd: workDir,
|
|
306
|
+
env: {
|
|
307
|
+
...process.env,
|
|
308
|
+
GIT_CONFIG_PARAMETERS: "'core.quotepath=false'" // 关闭路径转义
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
let errorOutput = '';
|
|
313
|
+
let standardOutput = '';
|
|
314
|
+
|
|
315
|
+
// Git的进度信息在stderr中
|
|
316
|
+
gitPush.stderr.on('data', (data) => {
|
|
317
|
+
const output = data.toString();
|
|
318
|
+
errorOutput += output;
|
|
319
|
+
|
|
320
|
+
// 解析进度信息
|
|
321
|
+
const lines = output.split('\n');
|
|
322
|
+
for (const line of lines) {
|
|
323
|
+
if (line.trim()) {
|
|
324
|
+
// 发送原始行
|
|
325
|
+
sendProgress({
|
|
326
|
+
type: 'progress',
|
|
327
|
+
message: line.trim()
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// 识别不同阶段并解析百分比
|
|
331
|
+
const percentMatch = line.match(/(\d+)%/);
|
|
332
|
+
if (percentMatch) {
|
|
333
|
+
const percent = parseInt(percentMatch[1]);
|
|
334
|
+
let stage = 'unknown';
|
|
335
|
+
|
|
336
|
+
if (line.includes('Enumerating objects')) {
|
|
337
|
+
stage = 'enumerating';
|
|
338
|
+
} else if (line.includes('Counting objects')) {
|
|
339
|
+
stage = 'counting';
|
|
340
|
+
} else if (line.includes('Compressing objects')) {
|
|
341
|
+
stage = 'compressing';
|
|
342
|
+
} else if (line.includes('Writing objects')) {
|
|
343
|
+
stage = 'writing';
|
|
344
|
+
} else if (line.includes('Resolving deltas')) {
|
|
345
|
+
stage = 'resolving';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
sendProgress({
|
|
349
|
+
type: 'stage-progress',
|
|
350
|
+
stage: stage,
|
|
351
|
+
percent: percent,
|
|
352
|
+
message: line.trim()
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
gitPush.stdout.on('data', (data) => {
|
|
360
|
+
standardOutput += data.toString();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
gitPush.on('close', (code) => {
|
|
364
|
+
logger.info(`Git push 进程结束,退出码: ${code}`);
|
|
365
|
+
logger.info('标准输出:', standardOutput);
|
|
366
|
+
logger.info('错误输出:', errorOutput);
|
|
367
|
+
|
|
368
|
+
// 计算执行时间
|
|
369
|
+
const executionTime = Date.now() - startTime;
|
|
370
|
+
|
|
371
|
+
if (code === 0) {
|
|
372
|
+
// 推送成功
|
|
373
|
+
setRecentPushStatus({
|
|
374
|
+
justPushed: true,
|
|
375
|
+
pushTime: Date.now(),
|
|
376
|
+
validDuration: 10000
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// 添加到命令历史
|
|
380
|
+
addCommandToHistory(
|
|
381
|
+
'git push --progress',
|
|
382
|
+
standardOutput,
|
|
383
|
+
errorOutput,
|
|
384
|
+
null,
|
|
385
|
+
executionTime
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
sendProgress({
|
|
389
|
+
type: 'complete',
|
|
390
|
+
success: true,
|
|
391
|
+
message: standardOutput || errorOutput || 'Push successful'
|
|
392
|
+
});
|
|
393
|
+
} else {
|
|
394
|
+
// 推送失败
|
|
395
|
+
logger.error('推送失败:', errorOutput || standardOutput);
|
|
396
|
+
|
|
397
|
+
// 添加到命令历史(失败情况)
|
|
398
|
+
addCommandToHistory(
|
|
399
|
+
'git push --progress',
|
|
400
|
+
standardOutput,
|
|
401
|
+
errorOutput,
|
|
402
|
+
errorOutput || standardOutput || `Push failed with code ${code}`,
|
|
403
|
+
executionTime
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
sendProgress({
|
|
407
|
+
type: 'complete',
|
|
408
|
+
success: false,
|
|
409
|
+
error: errorOutput || standardOutput || `Push failed with code ${code}`
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
res.end();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
gitPush.on('error', (error) => {
|
|
416
|
+
logger.error('Git push 进程错误:', error);
|
|
417
|
+
|
|
418
|
+
// 计算执行时间
|
|
419
|
+
const executionTime = Date.now() - startTime;
|
|
420
|
+
|
|
421
|
+
// 添加到命令历史(错误情况)
|
|
422
|
+
addCommandToHistory(
|
|
423
|
+
'git push --progress',
|
|
424
|
+
'',
|
|
425
|
+
'',
|
|
426
|
+
error.message,
|
|
427
|
+
executionTime
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
sendProgress({
|
|
431
|
+
type: 'complete',
|
|
432
|
+
success: false,
|
|
433
|
+
error: error.message
|
|
434
|
+
});
|
|
435
|
+
res.end();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
} catch (error) {
|
|
439
|
+
// 添加到命令历史(异常情况)
|
|
440
|
+
addCommandToHistory(
|
|
441
|
+
'git push --progress',
|
|
442
|
+
'',
|
|
443
|
+
'',
|
|
444
|
+
error.message,
|
|
445
|
+
0
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
sendProgress({
|
|
449
|
+
type: 'complete',
|
|
450
|
+
success: false,
|
|
451
|
+
error: error.message
|
|
452
|
+
});
|
|
453
|
+
res.end();
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// 添加git pull API端点
|
|
458
|
+
app.post('/api/pull', async (req, res) => {
|
|
459
|
+
try {
|
|
460
|
+
const { stdout } = await execGitCommand(['pull']);
|
|
461
|
+
res.json({ success: true, message: stdout });
|
|
462
|
+
} catch (error) {
|
|
463
|
+
// 改进错误处理,检查是否需要合并
|
|
464
|
+
// 合并冲突信息可能在 message 或 stderr 中
|
|
465
|
+
const errorMsg = (error.message || '').toLowerCase();
|
|
466
|
+
const stderrMsg = (error.stderr || '').toLowerCase();
|
|
467
|
+
const stdoutMsg = (error.stdout || '').toLowerCase();
|
|
468
|
+
const fullOutput = errorMsg + ' ' + stderrMsg + ' ' + stdoutMsg;
|
|
469
|
+
|
|
470
|
+
const needsMerge = fullOutput.includes('merge') ||
|
|
471
|
+
fullOutput.includes('需要合并') ||
|
|
472
|
+
fullOutput.includes('conflict') ||
|
|
473
|
+
fullOutput.includes('冲突') ||
|
|
474
|
+
fullOutput.includes('automatic merge failed');
|
|
475
|
+
|
|
476
|
+
// 返回更详细的错误信息和标记
|
|
477
|
+
res.status(500).json({
|
|
478
|
+
success: false,
|
|
479
|
+
error: error.message,
|
|
480
|
+
needsMerge: needsMerge,
|
|
481
|
+
// 包含完整的错误输出
|
|
482
|
+
fullError: error.stderr || error.message,
|
|
483
|
+
pullOutput: error.stdout || ''
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// 添加git fetch --all API端点
|
|
489
|
+
app.post('/api/fetch-all', async (req, res) => {
|
|
490
|
+
try {
|
|
491
|
+
const { stdout } = await execGitCommand(['fetch', '--all']);
|
|
492
|
+
res.json({ success: true, message: stdout });
|
|
493
|
+
} catch (error) {
|
|
494
|
+
res.status(500).json({ success: false, error: error.message });
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// 获取日志
|
|
499
|
+
app.get('/api/log', async (req, res) => {
|
|
500
|
+
try {
|
|
501
|
+
// 获取分页参数
|
|
502
|
+
const page = parseInt(req.query.page) || 1;
|
|
503
|
+
const limit = parseInt(req.query.limit) || 50;
|
|
504
|
+
const skip = (page - 1) * limit;
|
|
505
|
+
|
|
506
|
+
// 获取筛选参数
|
|
507
|
+
const author = req.query.author ? req.query.author.split(',') : [];
|
|
508
|
+
const message = req.query.message || '';
|
|
509
|
+
const dateFrom = req.query.dateFrom || '';
|
|
510
|
+
const dateTo = req.query.dateTo || '';
|
|
511
|
+
const branch = req.query.branch ? req.query.branch.split(',') : [];
|
|
512
|
+
const withParents = req.query.with_parents === 'true';
|
|
513
|
+
|
|
514
|
+
// 如果指定了分支,直接走 executeGitLogCommand 处理 refs/ 前缀
|
|
515
|
+
if (branch.length > 0) {
|
|
516
|
+
const branchRefs = branch.map(b => b.trim()).join(' ');
|
|
517
|
+
return executeGitLogCommand(res, branchRefs, author, message, dateFrom, dateTo, limit, skip, req.query.all === 'true', withParents);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 构建 git log 的 argv 数组
|
|
521
|
+
const logArgs = ['log', '--all'];
|
|
522
|
+
|
|
523
|
+
// 作者筛选(支持多作者用正则 OR)
|
|
524
|
+
if (author.length > 0) {
|
|
525
|
+
const validAuthors = author.filter(a => a.trim() !== '');
|
|
526
|
+
if (validAuthors.length === 1) {
|
|
527
|
+
logArgs.push(`--author=${validAuthors[0].trim()}`);
|
|
528
|
+
} else if (validAuthors.length > 1) {
|
|
529
|
+
const authorPattern = validAuthors.map(a => a.trim()).join('\\|');
|
|
530
|
+
logArgs.push(`--author=${authorPattern}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 日期范围筛选
|
|
535
|
+
if (dateFrom && dateTo) {
|
|
536
|
+
logArgs.push(`--after=${dateFrom}`, `--before=${dateTo} 23:59:59`);
|
|
537
|
+
} else if (dateFrom) {
|
|
538
|
+
logArgs.push(`--after=${dateFrom}`);
|
|
539
|
+
} else if (dateTo) {
|
|
540
|
+
logArgs.push(`--before=${dateTo} 23:59:59`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 提交信息筛选
|
|
544
|
+
if (message) {
|
|
545
|
+
logArgs.push(`--grep=${message}`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// 分页
|
|
549
|
+
if (req.query.all !== 'true') {
|
|
550
|
+
logArgs.push('-n', String(limit), `--skip=${skip}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// format 字符串直接作为 argv 单元素(不再需要外层 shell 双引号)
|
|
554
|
+
let formatString = '%H%x1E%an%x1E%ae%x1E%ad%x1E%B%x1E%D';
|
|
555
|
+
if (withParents) {
|
|
556
|
+
formatString = '%H%x1E%an%x1E%ae%x1E%ad%x1E%B%x1E%D%x1E%P';
|
|
557
|
+
}
|
|
558
|
+
logArgs.push(`--pretty=format:${formatString}`);
|
|
559
|
+
logArgs.push('--date=format-local:%Y-%m-%d %H:%M');
|
|
560
|
+
|
|
561
|
+
let { stdout: logOutput } = await execGitCommand(logArgs);
|
|
562
|
+
processAndSendLogOutput(res, logOutput, page, limit, withParents);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
logger.error('获取Git日志失败:', error);
|
|
565
|
+
res.status(500).json({ error: '获取日志失败: ' + error.message });
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// 抽取执行Git日志命令的函数
|
|
570
|
+
async function executeGitLogCommand(res, branchRefs, author, message, dateFrom, dateTo, limit, skip, isAll, withParents = false) {
|
|
571
|
+
try {
|
|
572
|
+
// 构建 git log argv 数组
|
|
573
|
+
const logArgs = ['log'];
|
|
574
|
+
|
|
575
|
+
// 准备分支引用,确保它们被正确识别为分支而不是文件名
|
|
576
|
+
// 使用 refs/heads/ 前缀明确指示这是分支
|
|
577
|
+
const formattedBranchRefs = branchRefs.split(' ')
|
|
578
|
+
.map(branch => {
|
|
579
|
+
if (branch.startsWith('refs/') || branch.includes('/')) {
|
|
580
|
+
return branch;
|
|
581
|
+
}
|
|
582
|
+
return `refs/heads/${branch}`;
|
|
583
|
+
});
|
|
584
|
+
logArgs.push(...formattedBranchRefs);
|
|
585
|
+
|
|
586
|
+
// 作者筛选
|
|
587
|
+
if (author.length > 0) {
|
|
588
|
+
const validAuthors = author.filter(a => a.trim() !== '');
|
|
589
|
+
if (validAuthors.length === 1) {
|
|
590
|
+
logArgs.push(`--author=${validAuthors[0].trim()}`);
|
|
591
|
+
} else if (validAuthors.length > 1) {
|
|
592
|
+
const authorPattern = validAuthors.map(a => a.trim()).join('\\|');
|
|
593
|
+
logArgs.push(`--author=${authorPattern}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// 日期范围筛选
|
|
598
|
+
if (dateFrom && dateTo) {
|
|
599
|
+
logArgs.push(`--after=${dateFrom}`, `--before=${dateTo} 23:59:59`);
|
|
600
|
+
} else if (dateFrom) {
|
|
601
|
+
logArgs.push(`--after=${dateFrom}`);
|
|
602
|
+
} else if (dateTo) {
|
|
603
|
+
logArgs.push(`--before=${dateTo} 23:59:59`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// 提交信息筛选
|
|
607
|
+
if (message) {
|
|
608
|
+
logArgs.push(`--grep=${message}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 限制选项
|
|
612
|
+
if (!isAll) {
|
|
613
|
+
logArgs.push('-n', String(limit), `--skip=${skip}`);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
let formatString = '%H%x1E%an%x1E%ae%x1E%ad%x1E%B%x1E%D';
|
|
617
|
+
if (withParents) {
|
|
618
|
+
formatString = '%H%x1E%an%x1E%ae%x1E%ad%x1E%B%x1E%D%x1E%P';
|
|
619
|
+
}
|
|
620
|
+
logArgs.push(`--pretty=format:${formatString}`);
|
|
621
|
+
logArgs.push('--date=format-local:%Y-%m-%d %H:%M');
|
|
622
|
+
|
|
623
|
+
logger.info(`执行Git log 命令 argv:`, logArgs);
|
|
624
|
+
|
|
625
|
+
const { stdout: logOutput } = await execGitCommand(logArgs);
|
|
626
|
+
processAndSendLogOutput(res, logOutput, skip / limit + 1, limit, withParents);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
logger.error('执行Git日志命令失败:', error);
|
|
629
|
+
res.status(500).json({ error: '获取日志失败: ' + error.message });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// 抽取处理输出并发送响应的函数(优化版本:不再依赖总数计算)
|
|
634
|
+
function processAndSendLogOutput(res, logOutput, page, limit, withParents = false) {
|
|
635
|
+
// 替换提交记录之间的换行符 - 使用 ASCII 控制字符 \x1E 匹配
|
|
636
|
+
logOutput = logOutput.replace(/\n(?=[a-f0-9]{40}\x1E)/g, "<<<RECORD_SEPARATOR>>>");
|
|
637
|
+
|
|
638
|
+
// 按分隔符拆分日志条目
|
|
639
|
+
const logEntries = logOutput.split("<<<RECORD_SEPARATOR>>>");
|
|
640
|
+
|
|
641
|
+
// 处理每个日志条目
|
|
642
|
+
const data = logEntries.map(entry => {
|
|
643
|
+
// 使用 ASCII 控制字符 \x1E 拆分字段
|
|
644
|
+
const parts = entry.split('\x1E');
|
|
645
|
+
if (parts.length >= 5) {
|
|
646
|
+
const result = {
|
|
647
|
+
hash: parts[0],
|
|
648
|
+
author: parts[1],
|
|
649
|
+
email: parts[2],
|
|
650
|
+
date: parts[3],
|
|
651
|
+
message: parts[4],
|
|
652
|
+
branch: parts[5] || ''
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// 如果请求了父提交信息,添加到结果中
|
|
656
|
+
if (withParents && parts[6]) {
|
|
657
|
+
result.parents = parts[6].split(' ');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
662
|
+
return null;
|
|
663
|
+
}).filter(item => item !== null);
|
|
664
|
+
|
|
665
|
+
// 优化:通过返回的数据量判断是否有更多数据,而不依赖总数计算
|
|
666
|
+
// 如果返回的数据量等于请求的limit,说明可能还有更多数据
|
|
667
|
+
// 如果返回的数据量小于limit,说明已经到底了
|
|
668
|
+
const hasMore = data.length === limit;
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
// 返回提交历史数据,包括是否有更多数据的标志
|
|
672
|
+
res.json({
|
|
673
|
+
data: data,
|
|
674
|
+
total: -1, // 不再提供总数,前端不应依赖此字段
|
|
675
|
+
page: page,
|
|
676
|
+
limit: limit,
|
|
677
|
+
hasMore: hasMore
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ========== Diff 大文件检测和过滤工具函数 ==========
|
|
682
|
+
|
|
683
|
+
const { checkShouldSkipDiff, checkDiffSize, getDiffStats } = createDiffHelpers({ execGitCommand });
|
|
684
|
+
|
|
685
|
+
registerGitDiffRoutes({
|
|
686
|
+
app,
|
|
687
|
+
execGitCommand
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
registerGitStashRoutes({
|
|
691
|
+
app,
|
|
692
|
+
execGitCommand,
|
|
693
|
+
configManager
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
registerGitTagRoutes({
|
|
697
|
+
app,
|
|
698
|
+
execGitCommand,
|
|
699
|
+
clearCommandHistory
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// 重置暂存区 (git reset HEAD)
|
|
703
|
+
app.post('/api/reset-head', async (req, res) => {
|
|
704
|
+
try {
|
|
705
|
+
// 执行 git reset HEAD 命令
|
|
706
|
+
await execGitCommand(['reset', 'HEAD']);
|
|
707
|
+
res.json({ success: true });
|
|
708
|
+
} catch (error) {
|
|
709
|
+
logger.error('重置暂存区失败:', error);
|
|
710
|
+
res.status(500).json({
|
|
711
|
+
success: false,
|
|
712
|
+
error: `重置暂存区失败: ${error.message}`
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// 重置到远程分支 (git reset --hard origin/branch)
|
|
718
|
+
app.post('/api/reset-to-remote', async (req, res) => {
|
|
719
|
+
try {
|
|
720
|
+
const { branch } = req.body;
|
|
721
|
+
|
|
722
|
+
if (!branch) {
|
|
723
|
+
return res.status(400).json({
|
|
724
|
+
success: false,
|
|
725
|
+
error: '缺少分支名称参数'
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// 尝试清理 Git 锁文件(破坏性入口:force=true 绕过 mtime 判断,
|
|
730
|
+
// 仍走 PID liveness 校验——用户主动触发的 reset --hard 不该被
|
|
731
|
+
// 一个刚崩出来的新孤儿锁挡住)
|
|
732
|
+
await checkAndClearGitLock({ force: true });
|
|
733
|
+
|
|
734
|
+
// 执行 git reset --hard origin/branch 命令
|
|
735
|
+
await execGitCommand(['reset', '--hard', `origin/${branch}`]);
|
|
736
|
+
res.json({ success: true });
|
|
737
|
+
} catch (error) {
|
|
738
|
+
logger.error('重置到远程分支失败:', error);
|
|
739
|
+
res.status(500).json({
|
|
740
|
+
success: false,
|
|
741
|
+
error: `重置到远程分支失败: ${error.message}`
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// 清除本地所有更改,包括未跟踪文件 (git reset --hard && git clean -fd)
|
|
747
|
+
app.post('/api/discard-all-changes', async (req, res) => {
|
|
748
|
+
try {
|
|
749
|
+
// 尝试清理 Git 锁文件(破坏性入口:force=true 绕过 mtime 判断,
|
|
750
|
+
// 仍走 PID liveness 校验——用户主动触发的 discard-all 不该被
|
|
751
|
+
// 一个刚崩出来的新孤儿锁挡住)
|
|
752
|
+
await checkAndClearGitLock({ force: true });
|
|
753
|
+
|
|
754
|
+
// 1. 执行 git reset --hard 丢弃已跟踪文件的更改
|
|
755
|
+
await execGitCommand(['reset', '--hard']);
|
|
756
|
+
|
|
757
|
+
// 2. 执行 git clean -fd 移除未跟踪的文件和目录
|
|
758
|
+
await execGitCommand(['clean', '-fd']);
|
|
759
|
+
|
|
760
|
+
res.json({ success: true });
|
|
761
|
+
} catch (error) {
|
|
762
|
+
logger.error('清除所有更改失败:', error);
|
|
763
|
+
res.status(500).json({
|
|
764
|
+
success: false,
|
|
765
|
+
error: `清除所有更改失败: ${error.message}`
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// 获取提交的文件列表
|
|
771
|
+
app.get('/api/commit-files', async (req, res) => {
|
|
772
|
+
try {
|
|
773
|
+
const hash = req.query.hash;
|
|
774
|
+
|
|
775
|
+
if (!hash) {
|
|
776
|
+
return res.status(400).json({
|
|
777
|
+
success: false,
|
|
778
|
+
error: '缺少提交哈希参数'
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
logger.info(`获取提交文件列表: hash=${hash}`);
|
|
783
|
+
|
|
784
|
+
// 执行命令获取提交中修改的文件列表
|
|
785
|
+
const { stdout } = await execGitCommand(['show', '--name-only', '--format=', hash]);
|
|
786
|
+
|
|
787
|
+
// 将输出按行分割,并过滤掉空行
|
|
788
|
+
const files = stdout.split('\n').filter(line => line.trim());
|
|
789
|
+
logger.info(`找到${files.length}个文件:`, files);
|
|
790
|
+
|
|
791
|
+
res.json({
|
|
792
|
+
success: true,
|
|
793
|
+
files
|
|
794
|
+
});
|
|
795
|
+
} catch (error) {
|
|
796
|
+
logger.error('获取提交文件列表失败:', error);
|
|
797
|
+
res.status(500).json({
|
|
798
|
+
success: false,
|
|
799
|
+
error: `获取提交文件列表失败: ${error.message}`
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// 获取提交中特定文件的差异
|
|
805
|
+
app.get('/api/commit-file-diff', async (req, res) => {
|
|
806
|
+
try {
|
|
807
|
+
const hash = req.query.hash;
|
|
808
|
+
const filePath = req.query.file;
|
|
809
|
+
|
|
810
|
+
if (!hash || !filePath) {
|
|
811
|
+
return res.status(400).json({
|
|
812
|
+
success: false,
|
|
813
|
+
error: '缺少必要参数'
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
logger.info(`获取提交文件差异: hash=${hash}, file=${filePath}`);
|
|
818
|
+
|
|
819
|
+
const diffCommandArgs = ['show', hash, '--', filePath];
|
|
820
|
+
|
|
821
|
+
// 使用优化的检查函数
|
|
822
|
+
// checkShouldSkipDiff 接受字符串命令,这里只用于大小判断,先按字面占位;
|
|
823
|
+
// 走 execGitCommand 时仍用 argv 数组保证转义正确。
|
|
824
|
+
const skipCheck = await checkShouldSkipDiff(filePath, `git show ${hash} -- "${filePath}"`);
|
|
825
|
+
if (skipCheck.shouldSkip) {
|
|
826
|
+
return res.json({
|
|
827
|
+
success: true,
|
|
828
|
+
diff: skipCheck.reason,
|
|
829
|
+
isLargeFile: true,
|
|
830
|
+
stats: skipCheck.stats
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// 执行命令获取文件差异
|
|
835
|
+
const { stdout } = await execGitCommand(diffCommandArgs);
|
|
836
|
+
|
|
837
|
+
logger.info(`获取到差异内容,长度: ${stdout.length}`);
|
|
838
|
+
|
|
839
|
+
// 检查实际diff大小
|
|
840
|
+
const sizeCheck = checkDiffSize(stdout, 500);
|
|
841
|
+
if (sizeCheck) {
|
|
842
|
+
return res.json({
|
|
843
|
+
success: true,
|
|
844
|
+
...sizeCheck
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// 统计增加和删除行数
|
|
849
|
+
const stats = getDiffStats(stdout);
|
|
850
|
+
|
|
851
|
+
res.json({
|
|
852
|
+
success: true,
|
|
853
|
+
diff: stdout,
|
|
854
|
+
stats
|
|
855
|
+
});
|
|
856
|
+
} catch (error) {
|
|
857
|
+
logger.error('获取提交文件差异失败:', error);
|
|
858
|
+
res.status(500).json({
|
|
859
|
+
success: false,
|
|
860
|
+
error: `获取提交文件差异失败: ${error.message}`
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
app.get('/api/commit-file-content', async (req, res) => {
|
|
866
|
+
try {
|
|
867
|
+
const hash = req.query.hash;
|
|
868
|
+
const filePath = req.query.file;
|
|
869
|
+
const version = req.query.version;
|
|
870
|
+
|
|
871
|
+
if (!hash || !filePath) {
|
|
872
|
+
return res.status(400).json({
|
|
873
|
+
success: false,
|
|
874
|
+
error: '缺少必要参数'
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const isOld = String(version || 'new') === 'old';
|
|
879
|
+
const targetHash = isOld ? `${hash}^` : `${hash}`;
|
|
880
|
+
|
|
881
|
+
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;
|
|
882
|
+
if (skipExtensions.test(String(filePath))) {
|
|
883
|
+
return res.json({
|
|
884
|
+
success: true,
|
|
885
|
+
isBinary: true,
|
|
886
|
+
content: '⚠️ 检测到二进制/编译产物文件,不支持以文本形式显示完整内容。\n\n提示:建议使用专业工具或命令行查看。'
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const spec = `${targetHash}:${filePath}`;
|
|
891
|
+
|
|
892
|
+
let sizeBytes = 0;
|
|
893
|
+
try {
|
|
894
|
+
const { stdout: sizeOut } = await execGitCommand(['cat-file', '-s', spec], { log: false });
|
|
895
|
+
sizeBytes = parseInt(String(sizeOut).trim(), 10) || 0;
|
|
896
|
+
} catch (e) {
|
|
897
|
+
return res.json({
|
|
898
|
+
success: true,
|
|
899
|
+
notFound: true,
|
|
900
|
+
content: isOld
|
|
901
|
+
? '该文件在该提交的上一个版本中不存在(可能是新增文件)。'
|
|
902
|
+
: '该文件在该提交版本中不存在(可能是删除文件或重命名)。'
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const maxBytes = 1024 * 1024;
|
|
907
|
+
if (sizeBytes > maxBytes) {
|
|
908
|
+
return res.json({
|
|
909
|
+
success: true,
|
|
910
|
+
isLargeFile: true,
|
|
911
|
+
size: sizeBytes,
|
|
912
|
+
content: `⚠️ 文件内容过大 (${(sizeBytes / 1024).toFixed(1)} KB),已跳过显示以避免浏览器卡顿。\n\n提示:建议使用命令行或编辑器打开查看。`
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
const { stdout } = await execGitCommand(['show', spec], { log: false });
|
|
918
|
+
res.json({
|
|
919
|
+
success: true,
|
|
920
|
+
content: stdout ?? ''
|
|
921
|
+
});
|
|
922
|
+
} catch (error) {
|
|
923
|
+
res.status(500).json({
|
|
924
|
+
success: false,
|
|
925
|
+
error: `获取文件内容失败: ${error.message}`
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
} catch (error) {
|
|
929
|
+
res.status(500).json({
|
|
930
|
+
success: false,
|
|
931
|
+
error: error.message
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// 撤销某个提交 (revert)
|
|
937
|
+
app.post('/api/revert-commit', async (req, res) => {
|
|
938
|
+
try {
|
|
939
|
+
const { hash } = req.body;
|
|
940
|
+
|
|
941
|
+
if (!hash) {
|
|
942
|
+
return res.status(400).json({
|
|
943
|
+
success: false,
|
|
944
|
+
error: '缺少提交哈希参数'
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
logger.info(`执行撤销提交操作: hash=${hash}`);
|
|
949
|
+
|
|
950
|
+
// 执行git revert命令
|
|
951
|
+
await execGitCommand(['revert', '--no-edit', hash]);
|
|
952
|
+
|
|
953
|
+
res.json({
|
|
954
|
+
success: true,
|
|
955
|
+
message: `已成功撤销提交 ${hash}`
|
|
956
|
+
});
|
|
957
|
+
} catch (error) {
|
|
958
|
+
logger.error('撤销提交失败:', error);
|
|
959
|
+
res.status(500).json({
|
|
960
|
+
success: false,
|
|
961
|
+
error: `撤销提交失败: ${error.message}`
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// Cherry-pick某个提交
|
|
967
|
+
app.post('/api/cherry-pick-commit', async (req, res) => {
|
|
968
|
+
try {
|
|
969
|
+
const { hash } = req.body;
|
|
970
|
+
|
|
971
|
+
if (!hash) {
|
|
972
|
+
return res.status(400).json({
|
|
973
|
+
success: false,
|
|
974
|
+
error: '缺少提交哈希参数'
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
logger.info(`执行Cherry-pick操作: hash=${hash}`);
|
|
979
|
+
|
|
980
|
+
// 执行git cherry-pick命令
|
|
981
|
+
await execGitCommand(['cherry-pick', hash]);
|
|
982
|
+
|
|
983
|
+
res.json({
|
|
984
|
+
success: true,
|
|
985
|
+
message: `已成功Cherry-pick提交 ${hash}`
|
|
986
|
+
});
|
|
987
|
+
} catch (error) {
|
|
988
|
+
logger.error('Cherry-pick提交失败:', error);
|
|
989
|
+
res.status(500).json({
|
|
990
|
+
success: false,
|
|
991
|
+
error: `Cherry-pick提交失败: ${error.message}`
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// 重置到指定提交(hard)
|
|
997
|
+
app.post('/api/reset-to-commit', async (req, res) => {
|
|
998
|
+
try {
|
|
999
|
+
const { hash } = req.body;
|
|
1000
|
+
|
|
1001
|
+
if (!hash) {
|
|
1002
|
+
return res.status(400).json({
|
|
1003
|
+
success: false,
|
|
1004
|
+
error: '缺少提交哈希参数'
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
logger.info(`执行重置到指定提交操作: hash=${hash}`);
|
|
1009
|
+
|
|
1010
|
+
// 执行git reset --hard命令
|
|
1011
|
+
await execGitCommand(['reset', '--hard', hash]);
|
|
1012
|
+
|
|
1013
|
+
res.json({
|
|
1014
|
+
success: true,
|
|
1015
|
+
message: `已成功重置到提交 ${hash}`
|
|
1016
|
+
});
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
logger.error('重置到指定提交失败:', error);
|
|
1019
|
+
res.status(500).json({
|
|
1020
|
+
success: false,
|
|
1021
|
+
error: `重置到指定提交失败: ${error.message}`
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
// 获取提交完整 diff(用于复制提交内容)
|
|
1027
|
+
app.get('/api/commit-diff-full', async (req, res) => {
|
|
1028
|
+
try {
|
|
1029
|
+
const hash = req.query.hash;
|
|
1030
|
+
if (!hash) {
|
|
1031
|
+
return res.status(400).json({ success: false, error: '缺少提交哈希参数' });
|
|
1032
|
+
}
|
|
1033
|
+
const { stdout } = await execGitCommand(['show', hash]);
|
|
1034
|
+
// 限制最大 200KB 防止内容过大
|
|
1035
|
+
const MAX = 200 * 1024;
|
|
1036
|
+
const content = stdout.length > MAX ? stdout.slice(0, MAX) + '\n\n[内容过大,已截断]' : stdout;
|
|
1037
|
+
res.json({ success: true, content });
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
res.status(500).json({ success: false, error: `获取提交内容失败: ${error.message}` });
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// 添加清理Git锁定文件的接口
|
|
1044
|
+
app.post('/api/remove-lock', async (req, res) => {
|
|
1045
|
+
try {
|
|
1046
|
+
const gitDir = path.join(process.cwd(), '.git')
|
|
1047
|
+
const indexLockFile = path.join(gitDir, 'index.lock')
|
|
1048
|
+
|
|
1049
|
+
// 检查文件是否存在
|
|
1050
|
+
try {
|
|
1051
|
+
await fs.access(indexLockFile)
|
|
1052
|
+
// 如果文件存在,尝试删除它
|
|
1053
|
+
await fs.unlink(indexLockFile)
|
|
1054
|
+
res.json({ success: true, message: '已清理锁定文件' })
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
// 如果文件不存在,也返回成功
|
|
1057
|
+
if (error.code === 'ENOENT') {
|
|
1058
|
+
res.json({ success: true, message: '没有发现锁定文件' })
|
|
1059
|
+
} else {
|
|
1060
|
+
throw error
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
logger.error('清理锁定文件失败:', error)
|
|
1065
|
+
res.status(500).json({
|
|
1066
|
+
success: false,
|
|
1067
|
+
error: `清理锁定文件失败: ${error.message}`
|
|
1068
|
+
})
|
|
1069
|
+
}
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
// 清除Git用户配置
|
|
1073
|
+
app.post('/api/clear-user-config', async (req, res) => {
|
|
1074
|
+
try {
|
|
1075
|
+
// 检查全局配置是否存在,如果存在才删除
|
|
1076
|
+
try {
|
|
1077
|
+
const { stdout: userName } = await execGitCommand(['config', '--global', 'user.name']);
|
|
1078
|
+
if (userName.trim()) {
|
|
1079
|
+
await execGitCommand(['config', '--global', '--unset', 'user.name']);
|
|
1080
|
+
}
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
logger.info('全局用户名配置检查失败,可能不存在:', error.message);
|
|
1083
|
+
// 忽略错误继续执行
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
try {
|
|
1087
|
+
const { stdout: userEmail } = await execGitCommand(['config', '--global', 'user.email']);
|
|
1088
|
+
if (userEmail.trim()) {
|
|
1089
|
+
await execGitCommand(['config', '--global', '--unset', 'user.email']);
|
|
1090
|
+
}
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
logger.info('全局邮箱配置检查失败,可能不存在:', error.message);
|
|
1093
|
+
// 忽略错误继续执行
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
res.json({ success: true, message: '已清除全局Git用户配置' });
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
res.status(500).json({
|
|
1099
|
+
success: false,
|
|
1100
|
+
error: `清除全局Git用户配置失败: ${error.message}`
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
// 恢复Git用户配置
|
|
1106
|
+
app.post('/api/restore-user-config', async (req, res) => {
|
|
1107
|
+
try {
|
|
1108
|
+
const { name, email } = req.body;
|
|
1109
|
+
if (!name || !email) {
|
|
1110
|
+
return res.status(400).json({
|
|
1111
|
+
success: false,
|
|
1112
|
+
error: '需要提供用户名和邮箱'
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
await execGitCommand(['config', '--global', 'user.name', name]);
|
|
1117
|
+
await execGitCommand(['config', '--global', 'user.email', email]);
|
|
1118
|
+
res.json({ success: true, message: '已更新全局Git用户配置' });
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
res.status(500).json({
|
|
1121
|
+
success: false,
|
|
1122
|
+
error: `更新全局Git用户配置失败: ${error.message}`
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
// 初始化Git仓库
|
|
1128
|
+
app.post('/api/git-init', async (req, res) => {
|
|
1129
|
+
try {
|
|
1130
|
+
const { stdout } = await execGitCommand(['init']);
|
|
1131
|
+
res.json({ success: true, output: stdout.trim() });
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
logger.error('git init 失败:', error);
|
|
1134
|
+
res.json({ success: false, error: error.message || 'git init 失败' });
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// 添加远程仓库的API
|
|
1139
|
+
app.post('/api/add-remote', express.json(), async (req, res) => {
|
|
1140
|
+
try {
|
|
1141
|
+
if (!getIsGitRepo()) {
|
|
1142
|
+
return res.json({ success: false, error: '当前目录不是Git仓库' });
|
|
1143
|
+
}
|
|
1144
|
+
const { name = 'origin', url } = req.body;
|
|
1145
|
+
if (!url || !url.trim()) {
|
|
1146
|
+
return res.json({ success: false, error: '远程仓库地址不能为空' });
|
|
1147
|
+
}
|
|
1148
|
+
await execGitCommand(['remote', 'add', name, url.trim()]);
|
|
1149
|
+
res.json({ success: true });
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
logger.error('添加远程仓库失败:', error);
|
|
1152
|
+
res.json({ success: false, error: error.message || '添加远程仓库失败' });
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
// 获取远程仓库URL的API
|
|
1157
|
+
app.get('/api/remote-url', async (req, res) => {
|
|
1158
|
+
try {
|
|
1159
|
+
// 检查当前目录是否是Git仓库
|
|
1160
|
+
if (!getIsGitRepo()) {
|
|
1161
|
+
return res.json({ success: false, error: '当前目录不是Git仓库' });
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// 执行git命令获取远程仓库URL
|
|
1165
|
+
const { stdout } = await execGitCommand(['config', '--get', 'remote.origin.url']);
|
|
1166
|
+
|
|
1167
|
+
// 返回远程仓库URL
|
|
1168
|
+
res.json({
|
|
1169
|
+
success: true,
|
|
1170
|
+
url: stdout.trim()
|
|
1171
|
+
});
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
logger.error('获取远程仓库URL失败:', error);
|
|
1174
|
+
res.json({
|
|
1175
|
+
success: false,
|
|
1176
|
+
error: error.message || '获取远程仓库URL失败'
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
// 获取所有作者列表
|
|
1182
|
+
app.get('/api/authors', async (req, res) => {
|
|
1183
|
+
try {
|
|
1184
|
+
// 使用git命令获取所有提交者,不依赖Unix命令
|
|
1185
|
+
const { stdout } = await execGitCommand(['log', '--format=%an']);
|
|
1186
|
+
|
|
1187
|
+
// 将结果按行分割并过滤空行
|
|
1188
|
+
const lines = stdout.split('\n').filter(author => author.trim() !== '');
|
|
1189
|
+
|
|
1190
|
+
// 手动去重,不依赖Unix的uniq命令
|
|
1191
|
+
const uniqueAuthors = Array.from(new Set(lines)).sort();
|
|
1192
|
+
|
|
1193
|
+
// 控制台输出一下搜索示例,方便调试
|
|
1194
|
+
if (uniqueAuthors.length > 1) {
|
|
1195
|
+
const searchExample = uniqueAuthors.slice(0, 2).join('|');
|
|
1196
|
+
logger.info(`多作者搜索示例: git log --author="${searchExample}"`);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
res.json({
|
|
1200
|
+
success: true,
|
|
1201
|
+
authors: uniqueAuthors
|
|
1202
|
+
});
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
logger.error('获取作者列表失败:', error);
|
|
1205
|
+
res.status(500).json({
|
|
1206
|
+
success: false,
|
|
1207
|
+
error: '获取作者列表失败: ' + error.message
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
}
|