zen-gitsync 2.9.8 → 2.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -148
- package/package.json +1 -1
- package/src/ui/public/assets/{index-C-ZzwQYX.css → index-CzUoE1WP.css} +1 -1
- package/src/ui/public/assets/index-GE6lIBHK.js +79 -0
- package/src/ui/public/index.html +2 -2
- package/src/ui/server/index.js +143 -5311
- package/src/ui/server/index_pro.js +5483 -0
- package/src/ui/server/middleware/requestLogger.js +37 -0
- package/src/ui/server/routes/branchStatus.js +168 -0
- package/src/ui/server/routes/config.js +586 -0
- package/src/ui/server/routes/exec.js +247 -0
- package/src/ui/server/routes/fileOpen.js +173 -0
- package/src/ui/server/routes/fs.js +443 -0
- package/src/ui/server/routes/git/diff.js +206 -0
- package/src/ui/server/routes/git/diffUtils.js +114 -0
- package/src/ui/server/routes/git/stash.js +481 -0
- package/src/ui/server/routes/git/tags.js +158 -0
- package/src/ui/server/routes/git.js +176 -0
- package/src/ui/server/routes/gitOps.js +974 -0
- package/src/ui/server/routes/npm.js +981 -0
- package/src/ui/server/routes/process.js +68 -0
- package/src/ui/server/routes/status.js +24 -0
- package/src/ui/server/routes/terminal.js +244 -0
- package/src/ui/server/socket/registerUiSocketHandlers.js +212 -0
- package/src/ui/server/utils/createSavePortToFile.js +32 -0
- package/src/ui/server/utils/startServerOnAvailablePort.js +87 -0
- package/src/ui/public/assets/index-DXmNo3gw.js +0 -79
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { spawn, exec } from 'child_process';
|
|
7
|
+
|
|
8
|
+
export function registerFsRoutes({
|
|
9
|
+
app,
|
|
10
|
+
execGitCommand,
|
|
11
|
+
configManager,
|
|
12
|
+
io,
|
|
13
|
+
getCurrentProjectPath,
|
|
14
|
+
setCurrentProjectPath,
|
|
15
|
+
getProjectRoomId,
|
|
16
|
+
setProjectRoomId,
|
|
17
|
+
setIsGitRepo
|
|
18
|
+
}) {
|
|
19
|
+
// 新增获取当前工作目录接口
|
|
20
|
+
app.get('/api/current_directory', async (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
const directory = process.cwd();
|
|
23
|
+
|
|
24
|
+
// 检查当前目录是否是Git仓库
|
|
25
|
+
try {
|
|
26
|
+
await execGitCommand('git rev-parse --is-inside-work-tree');
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return res.status(400).json({
|
|
29
|
+
error: '当前目录不是一个Git仓库',
|
|
30
|
+
directory,
|
|
31
|
+
isGitRepo: false
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
res.json({
|
|
36
|
+
directory,
|
|
37
|
+
isGitRepo: true
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
res.status(500).json({ error: error.message });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 新增切换工作目录接口
|
|
45
|
+
app.post('/api/change_directory', async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const { path: reqPath } = req.body;
|
|
48
|
+
|
|
49
|
+
if (!reqPath) {
|
|
50
|
+
return res.status(400).json({ success: false, error: '目录路径不能为空' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
process.chdir(reqPath);
|
|
55
|
+
const newDirectory = process.cwd();
|
|
56
|
+
|
|
57
|
+
// 更新当前项目路径和房间ID
|
|
58
|
+
const oldProjectPath = getCurrentProjectPath();
|
|
59
|
+
const newProjectPath = newDirectory;
|
|
60
|
+
const newProjectRoomId = `project:${newProjectPath.replace(/[\\/:*?"<>|\s]/g, '_')}`;
|
|
61
|
+
|
|
62
|
+
console.log(chalk.yellow(`项目路径切换: ${oldProjectPath} -> ${newProjectPath}`));
|
|
63
|
+
console.log(chalk.yellow(`房间ID更新: ${getProjectRoomId()} -> ${newProjectRoomId}`));
|
|
64
|
+
|
|
65
|
+
// 检查新目录是否是Git仓库
|
|
66
|
+
try {
|
|
67
|
+
await execGitCommand('git rev-parse --is-inside-work-tree');
|
|
68
|
+
|
|
69
|
+
// 更新全局变量
|
|
70
|
+
setCurrentProjectPath(newProjectPath);
|
|
71
|
+
setProjectRoomId(newProjectRoomId);
|
|
72
|
+
setIsGitRepo(true);
|
|
73
|
+
|
|
74
|
+
// 确保切换后立即初始化该项目的配置条目
|
|
75
|
+
try {
|
|
76
|
+
const currentCfg = await configManager.loadConfig();
|
|
77
|
+
await configManager.saveConfig(currentCfg);
|
|
78
|
+
// 将新目录加入最近目录
|
|
79
|
+
await configManager.saveRecentDirectory(newDirectory);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.warn('初始化项目配置失败:', e?.message || e);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 通知所有旧房间的客户端项目已切换
|
|
85
|
+
io.to(getProjectRoomId()).emit('project_changed', {
|
|
86
|
+
oldProjectPath: getCurrentProjectPath(),
|
|
87
|
+
newProjectPath: newProjectPath,
|
|
88
|
+
newProjectRoomId: newProjectRoomId
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
res.json({
|
|
92
|
+
success: true,
|
|
93
|
+
directory: newDirectory,
|
|
94
|
+
isGitRepo: true,
|
|
95
|
+
projectRoomId: newProjectRoomId
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// 不是Git仓库,停止监控
|
|
99
|
+
|
|
100
|
+
// 更新全局变量
|
|
101
|
+
setCurrentProjectPath(newProjectPath);
|
|
102
|
+
setProjectRoomId(newProjectRoomId);
|
|
103
|
+
setIsGitRepo(false);
|
|
104
|
+
|
|
105
|
+
// 通知所有旧房间的客户端项目已切换
|
|
106
|
+
io.to(getProjectRoomId()).emit('project_changed', {
|
|
107
|
+
oldProjectPath: getCurrentProjectPath(),
|
|
108
|
+
newProjectPath: newProjectPath,
|
|
109
|
+
newProjectRoomId: newProjectRoomId
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 即使不是Git仓库也初始化当前目录配置(使用CWD作为项目键)
|
|
113
|
+
try {
|
|
114
|
+
const currentCfg = await configManager.loadConfig();
|
|
115
|
+
await configManager.saveConfig(currentCfg);
|
|
116
|
+
// 将新目录加入最近目录
|
|
117
|
+
await configManager.saveRecentDirectory(newDirectory);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.warn('非Git目录初始化项目配置失败:', e?.message || e);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
res.json({
|
|
123
|
+
success: true,
|
|
124
|
+
directory: newDirectory,
|
|
125
|
+
isGitRepo: false,
|
|
126
|
+
warning: '新目录不是一个Git仓库',
|
|
127
|
+
projectRoomId: newProjectRoomId
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
res.status(400).json({
|
|
132
|
+
success: false,
|
|
133
|
+
error: `切换到目录 "${reqPath}" 失败: ${error.message}`
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
res.status(500).json({ error: error.message });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 获取目录内容(用于浏览目录)
|
|
142
|
+
app.get('/api/browse_directory', async (req, res) => {
|
|
143
|
+
try {
|
|
144
|
+
// 获取要浏览的目录路径,如果没有提供,则使用当前目录
|
|
145
|
+
const directoryPath = req.query.path || process.cwd();
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// 读取目录内容
|
|
149
|
+
const items = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
150
|
+
|
|
151
|
+
// 分离文件夹和文件
|
|
152
|
+
const directories = [];
|
|
153
|
+
const files = [];
|
|
154
|
+
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
const fullPath = path.join(directoryPath, item.name);
|
|
157
|
+
if (item.isDirectory()) {
|
|
158
|
+
directories.push({
|
|
159
|
+
name: item.name,
|
|
160
|
+
path: fullPath,
|
|
161
|
+
type: 'directory'
|
|
162
|
+
});
|
|
163
|
+
} else if (item.isFile()) {
|
|
164
|
+
files.push({
|
|
165
|
+
name: item.name,
|
|
166
|
+
path: fullPath,
|
|
167
|
+
type: 'file'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 优先显示目录,然后是文件,都按字母排序
|
|
173
|
+
directories.sort((a, b) => a.name.localeCompare(b.name));
|
|
174
|
+
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
175
|
+
|
|
176
|
+
// 获取父目录路径
|
|
177
|
+
const parentPath = path.dirname(directoryPath);
|
|
178
|
+
|
|
179
|
+
res.json({
|
|
180
|
+
success: true,
|
|
181
|
+
currentPath: directoryPath,
|
|
182
|
+
parentPath: parentPath !== directoryPath ? parentPath : null,
|
|
183
|
+
items: [...directories, ...files]
|
|
184
|
+
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
res.status(400).json({
|
|
187
|
+
success: false,
|
|
188
|
+
error: `无法读取目录 "${directoryPath}": ${error.message}`
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
res.status(500).json({ error: error.message });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// POST接口版本的浏览目录功能
|
|
197
|
+
app.post('/api/browse_directory', async (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
// 获取要浏览的目录路径,如果没有提供,则使用当前目录
|
|
200
|
+
const directoryPath = req.body.currentPath || process.cwd();
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// 读取目录内容
|
|
204
|
+
const items = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
205
|
+
|
|
206
|
+
// 分离文件夹和文件
|
|
207
|
+
const directories = [];
|
|
208
|
+
|
|
209
|
+
for (const item of items) {
|
|
210
|
+
const fullPath = path.join(directoryPath, item.name);
|
|
211
|
+
if (item.isDirectory()) {
|
|
212
|
+
directories.push({
|
|
213
|
+
name: item.name,
|
|
214
|
+
path: fullPath,
|
|
215
|
+
type: 'directory'
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 只返回目录,不返回文件
|
|
221
|
+
directories.sort((a, b) => a.name.localeCompare(b.name));
|
|
222
|
+
|
|
223
|
+
// 获取父目录路径
|
|
224
|
+
const parentPath = path.dirname(directoryPath);
|
|
225
|
+
|
|
226
|
+
// 返回选择的目录路径
|
|
227
|
+
res.json({
|
|
228
|
+
success: true,
|
|
229
|
+
path: directoryPath,
|
|
230
|
+
parentPath: parentPath !== directoryPath ? parentPath : null,
|
|
231
|
+
items: directories
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
res.status(400).json({
|
|
235
|
+
success: false,
|
|
236
|
+
error: `无法读取目录 "${directoryPath}": ${error.message}`
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
res.status(500).json({
|
|
241
|
+
success: false,
|
|
242
|
+
error: error.message
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 获取最近访问的目录列表
|
|
248
|
+
app.get('/api/recent_directories', async (req, res) => {
|
|
249
|
+
try {
|
|
250
|
+
// 尝试从配置中获取最近的目录
|
|
251
|
+
const recentDirs = await configManager.getRecentDirectories();
|
|
252
|
+
res.json({
|
|
253
|
+
success: true,
|
|
254
|
+
directories: recentDirs || []
|
|
255
|
+
});
|
|
256
|
+
} catch (error) {
|
|
257
|
+
res.status(500).json({
|
|
258
|
+
success: false,
|
|
259
|
+
error: error.message
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// 在资源管理器/访达中打开当前目录
|
|
265
|
+
app.post('/api/open_directory', async (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
// 获取要打开的目录路径,如果没有提供,则使用当前目录
|
|
268
|
+
const directoryPath = req.body.path || process.cwd();
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
// 检查目录是否存在
|
|
272
|
+
await fs.access(directoryPath);
|
|
273
|
+
|
|
274
|
+
// 使用open模块打开目录,自动处理不同操作系统
|
|
275
|
+
await open(directoryPath, { wait: false });
|
|
276
|
+
|
|
277
|
+
res.json({
|
|
278
|
+
success: true,
|
|
279
|
+
message: '已在文件管理器中打开目录'
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
res.status(400).json({
|
|
283
|
+
success: false,
|
|
284
|
+
error: `无法打开目录 "${directoryPath}": ${error.message}`
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
res.status(500).json({
|
|
289
|
+
success: false,
|
|
290
|
+
error: error.message
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// 在终端中打开当前目录
|
|
296
|
+
app.post('/api/open_terminal', async (req, res) => {
|
|
297
|
+
try {
|
|
298
|
+
// 获取要打开的目录路径,如果没有提供,则使用当前目录
|
|
299
|
+
const directoryPath = req.body.path || process.cwd();
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
// 检查目录是否存在
|
|
303
|
+
await fs.access(directoryPath);
|
|
304
|
+
|
|
305
|
+
// 根据不同操作系统打开终端
|
|
306
|
+
const platform = os.platform();
|
|
307
|
+
let command;
|
|
308
|
+
let args;
|
|
309
|
+
|
|
310
|
+
switch (platform) {
|
|
311
|
+
case 'win32':
|
|
312
|
+
// Windows: 将start命令的参数分开传递,避免引号转义问题
|
|
313
|
+
// 参数顺序:start [title] /D [path] [command]
|
|
314
|
+
command = 'cmd';
|
|
315
|
+
args = ['/c', 'start', '', '/D', directoryPath, 'cmd'];
|
|
316
|
+
break;
|
|
317
|
+
case 'darwin':
|
|
318
|
+
// macOS: 使用 Terminal.app
|
|
319
|
+
command = 'open';
|
|
320
|
+
args = ['-a', 'Terminal', directoryPath];
|
|
321
|
+
break;
|
|
322
|
+
case 'linux': {
|
|
323
|
+
// Linux: 尝试使用常见的终端模拟器
|
|
324
|
+
// 优先级: gnome-terminal, konsole, xterm
|
|
325
|
+
const terminals = [
|
|
326
|
+
{ cmd: 'gnome-terminal', args: ['--working-directory', directoryPath] },
|
|
327
|
+
{ cmd: 'konsole', args: ['--workdir', directoryPath] },
|
|
328
|
+
{ cmd: 'xterm', args: ['-e', `cd "${directoryPath}" && $SHELL`] }
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
// 尝试找到可用的终端
|
|
332
|
+
let terminalFound = false;
|
|
333
|
+
for (const terminal of terminals) {
|
|
334
|
+
try {
|
|
335
|
+
exec(`which ${terminal.cmd}`, (error) => {
|
|
336
|
+
if (!error) {
|
|
337
|
+
command = terminal.cmd;
|
|
338
|
+
args = terminal.args;
|
|
339
|
+
terminalFound = true;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
if (terminalFound) break;
|
|
343
|
+
} catch (e) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!terminalFound) {
|
|
349
|
+
return res.status(400).json({
|
|
350
|
+
success: false,
|
|
351
|
+
error: '未找到可用的终端模拟器'
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
default:
|
|
357
|
+
return res.status(400).json({
|
|
358
|
+
success: false,
|
|
359
|
+
error: `不支持的操作系统: ${platform}`
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 执行命令打开终端
|
|
364
|
+
spawn(command, args, {
|
|
365
|
+
detached: true,
|
|
366
|
+
stdio: 'ignore'
|
|
367
|
+
}).unref();
|
|
368
|
+
|
|
369
|
+
res.json({
|
|
370
|
+
success: true,
|
|
371
|
+
message: '已在终端中打开目录'
|
|
372
|
+
});
|
|
373
|
+
} catch (error) {
|
|
374
|
+
res.status(400).json({
|
|
375
|
+
success: false,
|
|
376
|
+
error: `无法打开终端 "${directoryPath}": ${error.message}`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
res.status(500).json({
|
|
381
|
+
success: false,
|
|
382
|
+
error: error.message
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// ========== 文件锁定相关 API ==========
|
|
388
|
+
|
|
389
|
+
// 获取锁定文件列表
|
|
390
|
+
app.get('/api/locked-files', async (req, res) => {
|
|
391
|
+
try {
|
|
392
|
+
const lockedFiles = await configManager.getLockedFiles();
|
|
393
|
+
res.json({ success: true, lockedFiles });
|
|
394
|
+
} catch (error) {
|
|
395
|
+
res.status(500).json({ success: false, error: error.message });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// 锁定文件
|
|
400
|
+
app.post('/api/lock-file', async (req, res) => {
|
|
401
|
+
try {
|
|
402
|
+
const { filePath } = req.body;
|
|
403
|
+
if (!filePath) {
|
|
404
|
+
return res.status(400).json({ success: false, error: '缺少文件路径参数' });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const result = await configManager.lockFile(filePath);
|
|
408
|
+
res.json({ success: true, locked: result });
|
|
409
|
+
} catch (error) {
|
|
410
|
+
res.status(500).json({ success: false, error: error.message });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// 解锁文件
|
|
415
|
+
app.post('/api/unlock-file', async (req, res) => {
|
|
416
|
+
try {
|
|
417
|
+
const { filePath } = req.body;
|
|
418
|
+
if (!filePath) {
|
|
419
|
+
return res.status(400).json({ success: false, error: '缺少文件路径参数' });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const result = await configManager.unlockFile(filePath);
|
|
423
|
+
res.json({ success: true, unlocked: result });
|
|
424
|
+
} catch (error) {
|
|
425
|
+
res.status(500).json({ success: false, error: error.message });
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// 检查文件是否锁定
|
|
430
|
+
app.post('/api/check-file-lock', async (req, res) => {
|
|
431
|
+
try {
|
|
432
|
+
const { filePath } = req.body;
|
|
433
|
+
if (!filePath) {
|
|
434
|
+
return res.status(400).json({ success: false, error: '缺少文件路径参数' });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const isLocked = await configManager.isFileLocked(filePath);
|
|
438
|
+
res.json({ success: true, isLocked });
|
|
439
|
+
} catch (error) {
|
|
440
|
+
res.status(500).json({ success: false, error: error.message });
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
import { createDiffHelpers } from './diffUtils.js';
|
|
4
|
+
|
|
5
|
+
export function registerGitDiffRoutes({
|
|
6
|
+
app,
|
|
7
|
+
execGitCommand
|
|
8
|
+
}) {
|
|
9
|
+
const { checkShouldSkipDiff, checkDiffSize, getDiffStats } = createDiffHelpers({ execGitCommand });
|
|
10
|
+
|
|
11
|
+
// 获取文件差异
|
|
12
|
+
app.get('/api/diff', async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const filePath = req.query.file;
|
|
15
|
+
|
|
16
|
+
if (!filePath) {
|
|
17
|
+
return res.status(400).json({ error: '缺少文件路径参数' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const diffCommand = `git diff -- "${filePath}"`;
|
|
21
|
+
|
|
22
|
+
// 使用优化的检查函数
|
|
23
|
+
const skipCheck = await checkShouldSkipDiff(filePath, diffCommand);
|
|
24
|
+
if (skipCheck.shouldSkip) {
|
|
25
|
+
return res.json({
|
|
26
|
+
diff: skipCheck.reason,
|
|
27
|
+
isLargeFile: true,
|
|
28
|
+
stats: skipCheck.stats
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 执行git diff命令获取文件差异
|
|
33
|
+
const { stdout } = await execGitCommand(diffCommand);
|
|
34
|
+
|
|
35
|
+
// 检查实际diff大小
|
|
36
|
+
const sizeCheck = checkDiffSize(stdout, 500);
|
|
37
|
+
if (sizeCheck) {
|
|
38
|
+
return res.json(sizeCheck);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 统计增加和删除行数
|
|
42
|
+
const stats = getDiffStats(stdout);
|
|
43
|
+
|
|
44
|
+
res.json({ diff: stdout, stats });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
res.status(500).json({ error: error.message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// 获取已暂存文件差异
|
|
50
|
+
app.get('/api/diff-cached', async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const filePath = req.query.file;
|
|
53
|
+
|
|
54
|
+
if (!filePath) {
|
|
55
|
+
return res.status(400).json({ error: '缺少文件路径参数' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const diffCommand = `git diff --cached -- "${filePath}"`;
|
|
59
|
+
|
|
60
|
+
// 使用优化的检查函数
|
|
61
|
+
const skipCheck = await checkShouldSkipDiff(filePath, diffCommand);
|
|
62
|
+
if (skipCheck.shouldSkip) {
|
|
63
|
+
return res.json({
|
|
64
|
+
diff: skipCheck.reason,
|
|
65
|
+
isLargeFile: true,
|
|
66
|
+
stats: skipCheck.stats
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 执行git diff --cached命令获取已暂存文件差异
|
|
71
|
+
const { stdout } = await execGitCommand(diffCommand);
|
|
72
|
+
|
|
73
|
+
// 检查实际diff大小
|
|
74
|
+
const sizeCheck = checkDiffSize(stdout, 500);
|
|
75
|
+
if (sizeCheck) {
|
|
76
|
+
return res.json(sizeCheck);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 统计增加和删除行数
|
|
80
|
+
const stats = getDiffStats(stdout);
|
|
81
|
+
|
|
82
|
+
res.json({ diff: stdout, stats });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
res.status(500).json({ error: error.message });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 获取文件内容 (用于未跟踪文件)
|
|
89
|
+
app.get('/api/file-content', async (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const filePath = req.query.file;
|
|
92
|
+
|
|
93
|
+
if (!filePath) {
|
|
94
|
+
return res.status(400).json({ error: '缺少文件路径参数' });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// 读取文件内容
|
|
99
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
100
|
+
res.json({ success: true, content });
|
|
101
|
+
} catch (readError) {
|
|
102
|
+
res.status(500).json({ success: false, error: `无法读取文件: ${readError.message}` });
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
res.status(500).json({ error: error.message });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 解决冲突:保存解决后的文件内容
|
|
110
|
+
app.post('/api/resolve-conflict', async (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const { filePath, content } = req.body;
|
|
113
|
+
|
|
114
|
+
if (!filePath) {
|
|
115
|
+
return res.status(400).json({
|
|
116
|
+
success: false,
|
|
117
|
+
error: '缺少文件路径参数'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (content === undefined) {
|
|
122
|
+
return res.status(400).json({
|
|
123
|
+
success: false,
|
|
124
|
+
error: '缺少文件内容参数'
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// 写入解决后的内容到文件
|
|
130
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
131
|
+
|
|
132
|
+
// 不自动添加到暂存区,让用户手动决定
|
|
133
|
+
// Git 会自动将冲突已解决的文件标记为"已修改"状态
|
|
134
|
+
// await execGitCommand(`git add "${filePath}"`);
|
|
135
|
+
|
|
136
|
+
res.json({
|
|
137
|
+
success: true,
|
|
138
|
+
message: '冲突已解决,文件已更新'
|
|
139
|
+
});
|
|
140
|
+
} catch (writeError) {
|
|
141
|
+
res.status(500).json({
|
|
142
|
+
success: false,
|
|
143
|
+
error: `保存文件失败: ${writeError.message}`
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('解决冲突失败:', error);
|
|
148
|
+
res.status(500).json({
|
|
149
|
+
success: false,
|
|
150
|
+
error: `解决冲突失败: ${error.message}`
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 撤回文件修改
|
|
156
|
+
app.post('/api/revert_file', async (req, res) => {
|
|
157
|
+
try {
|
|
158
|
+
const { filePath } = req.body;
|
|
159
|
+
|
|
160
|
+
if (!filePath) {
|
|
161
|
+
return res.status(400).json({
|
|
162
|
+
success: false,
|
|
163
|
+
error: '缺少文件路径参数'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 检查文件状态:未跟踪文件需要删除,修改文件需要恢复
|
|
168
|
+
const { stdout: statusOutput } = await execGitCommand(`git status --porcelain -- "${filePath}"`);
|
|
169
|
+
|
|
170
|
+
// 未跟踪的文件 (??), 需要删除它
|
|
171
|
+
if (statusOutput.startsWith('??')) {
|
|
172
|
+
try {
|
|
173
|
+
await fs.unlink(filePath);
|
|
174
|
+
return res.json({ success: true, message: '未跟踪的文件已删除' });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return res.status(500).json({
|
|
177
|
+
success: false,
|
|
178
|
+
error: `删除文件失败: ${error.message}`
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// 已暂存的文件,先取消暂存
|
|
183
|
+
else if (statusOutput.startsWith('A ') || statusOutput.startsWith('M ') || statusOutput.startsWith('D ')) {
|
|
184
|
+
// 先取消暂存
|
|
185
|
+
await execGitCommand(`git reset HEAD -- "${filePath}"`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 已修改文件,取消所有本地修改
|
|
189
|
+
if (statusOutput) {
|
|
190
|
+
await execGitCommand(`git checkout -- "${filePath}"`);
|
|
191
|
+
return res.json({ success: true, message: '文件修改已撤回' });
|
|
192
|
+
} else {
|
|
193
|
+
return res.status(400).json({
|
|
194
|
+
success: false,
|
|
195
|
+
error: '文件没有修改或不存在'
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('撤回文件修改失败:', error);
|
|
200
|
+
res.status(500).json({
|
|
201
|
+
success: false,
|
|
202
|
+
error: `撤回文件修改失败: ${error.message}`
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|