zen-gitsync 2.1.11 → 2.1.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 +1 -1
- package/src/ui/public/assets/index-B2PuJ0eQ.css +1 -0
- package/src/ui/public/assets/index-Bp-pxa_s.js +22 -0
- package/src/ui/public/assets/{vendor-lzd7uK7e.js → vendor-7KY24w-Q.js} +45 -45
- package/src/ui/public/assets/vendor-Dp0FkvMe.css +1 -1
- package/src/ui/public/index.html +16 -16
- package/src/ui/server/index.js +1733 -1669
- package/src/ui/public/assets/index-2zWLRJg9.css +0 -1
- package/src/ui/public/assets/index-BKaSsEOq.js +0 -22
package/src/ui/server/index.js
CHANGED
|
@@ -1,1670 +1,1734 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import { createServer } from 'http';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { execGitCommand } from '../../utils/index.js';
|
|
6
|
-
import open from 'open';
|
|
7
|
-
import config from '../../config.js';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
|
-
import fs from 'fs/promises';
|
|
10
|
-
import os from 'os';
|
|
11
|
-
import { Server } from 'socket.io';
|
|
12
|
-
import chokidar from 'chokidar';
|
|
13
|
-
// import { exec } from 'child_process';
|
|
14
|
-
|
|
15
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
-
const __dirname = path.dirname(__filename);
|
|
17
|
-
const configManager = config; // 确保 configManager 可用
|
|
18
|
-
|
|
19
|
-
// 文件系统变动监控器
|
|
20
|
-
let watcher = null;
|
|
21
|
-
// 防抖计时器
|
|
22
|
-
let debounceTimer = null;
|
|
23
|
-
// 防抖延迟时间 (毫秒)
|
|
24
|
-
const DEBOUNCE_DELAY = 1000;
|
|
25
|
-
|
|
26
|
-
async function startUIServer() {
|
|
27
|
-
const app = express();
|
|
28
|
-
const httpServer = createServer(app);
|
|
29
|
-
const io = new Server(httpServer);
|
|
30
|
-
|
|
31
|
-
// 添加全局中间件来解析JSON请求体
|
|
32
|
-
app.use(express.json());
|
|
33
|
-
|
|
34
|
-
// 添加请求日志中间件
|
|
35
|
-
app.use((req, res, next) => {
|
|
36
|
-
const now = new Date().toLocaleString();
|
|
37
|
-
console.log(`[${now}] ${req.method} ${req.url}`);
|
|
38
|
-
next();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// 静态文件服务
|
|
42
|
-
app.use(express.static(path.join(__dirname, '../public')));
|
|
43
|
-
|
|
44
|
-
// API路由
|
|
45
|
-
app.get('/api/status', async (req, res) => {
|
|
46
|
-
try {
|
|
47
|
-
const { stdout } = await execGitCommand('git status');
|
|
48
|
-
res.json({ status: stdout });
|
|
49
|
-
} catch (error) {
|
|
50
|
-
res.status(500).json({ error: error.message });
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
app.get('/api/status_porcelain', async (req, res) => {
|
|
54
|
-
try {
|
|
55
|
-
const { stdout } = await execGitCommand('git status --porcelain --untracked-files=all');
|
|
56
|
-
res.json({ status: stdout });
|
|
57
|
-
} catch (error) {
|
|
58
|
-
res.status(500).json({ error: error.message });
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// 获取当前分支
|
|
63
|
-
app.get('/api/branch', async (req, res) => {
|
|
64
|
-
try {
|
|
65
|
-
const { stdout } = await execGitCommand('git rev-parse --abbrev-ref HEAD');
|
|
66
|
-
res.json({ branch: stdout.trim() });
|
|
67
|
-
} catch (error) {
|
|
68
|
-
res.status(500).json({ error: error.message });
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// 获取分支与远程的差异状态(领先/落后提交数)
|
|
73
|
-
app.get('/api/branch-status', async (req, res) => {
|
|
74
|
-
try {
|
|
75
|
-
// 检查当前目录是否是Git仓库
|
|
76
|
-
if (!isGitRepo) {
|
|
77
|
-
return res.json({ hasUpstream: false, ahead: 0, behind: 0 });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 获取当前分支
|
|
81
|
-
const { stdout: branchOutput } = await execGitCommand('git symbolic-ref --short HEAD');
|
|
82
|
-
const currentBranch = branchOutput.trim();
|
|
83
|
-
|
|
84
|
-
// 获取上游分支
|
|
85
|
-
const { stdout: upstreamOutput } = await execGitCommand('git rev-parse --abbrev-ref --symbolic-full-name @{u}', { ignoreError: true });
|
|
86
|
-
|
|
87
|
-
if (!upstreamOutput.trim()) {
|
|
88
|
-
// 没有上游分支
|
|
89
|
-
return res.json({ hasUpstream: false, ahead: 0, behind: 0 });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const upstreamBranch = upstreamOutput.trim();
|
|
93
|
-
|
|
94
|
-
// 获取领先/落后提交数
|
|
95
|
-
const { stdout: aheadBehindOutput } = await execGitCommand(`git rev-list --left-right --count ${currentBranch}...${upstreamBranch}`);
|
|
96
|
-
const [ahead, behind] = aheadBehindOutput.trim().split('\t').map(Number);
|
|
97
|
-
|
|
98
|
-
res.json({
|
|
99
|
-
hasUpstream: true,
|
|
100
|
-
upstreamBranch,
|
|
101
|
-
ahead,
|
|
102
|
-
behind
|
|
103
|
-
});
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error('获取分支状态失败:', error);
|
|
106
|
-
res.status(500).json({ error: error.message });
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// 获取所有分支
|
|
111
|
-
app.get('/api/branches', async (req, res) => {
|
|
112
|
-
try {
|
|
113
|
-
// 获取本地分支
|
|
114
|
-
const { stdout: localBranches } = await execGitCommand('git branch --format="%(refname:short)"');
|
|
115
|
-
// 获取远程分支(过滤掉 origin/HEAD 和 origin)
|
|
116
|
-
const { stdout: remoteBranches } = await execGitCommand('git branch -r --format="%(refname:short)"');
|
|
117
|
-
|
|
118
|
-
// 合并并去重
|
|
119
|
-
const allBranches = [...new Set([
|
|
120
|
-
...localBranches.split('\n')
|
|
121
|
-
.filter(Boolean)
|
|
122
|
-
.filter(b => !b.startsWith('* ')), // 过滤掉HEAD指针
|
|
123
|
-
...remoteBranches.split('\n')
|
|
124
|
-
.filter(Boolean)
|
|
125
|
-
.filter(b => b.includes('/')) // 过滤掉单纯的 origin
|
|
126
|
-
.map(b => b.split('/')[1]) // 提取真正的分支名称
|
|
127
|
-
])];
|
|
128
|
-
|
|
129
|
-
res.json({ branches: allBranches });
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error('获取分支列表失败:', error);
|
|
132
|
-
res.status(500).json({ error: error.message });
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// 创建新分支
|
|
137
|
-
app.post('/api/create-branch', express.json(), async (req, res) => {
|
|
138
|
-
try {
|
|
139
|
-
const { newBranchName, baseBranch } = req.body;
|
|
140
|
-
|
|
141
|
-
if (!newBranchName) {
|
|
142
|
-
return res.status(400).json({ success: false, error: '分支名称不能为空' });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 构建创建分支的命令
|
|
146
|
-
let command = `git branch ${newBranchName}`;
|
|
147
|
-
|
|
148
|
-
// 如果指定了基础分支,则基于该分支创建
|
|
149
|
-
if (baseBranch) {
|
|
150
|
-
command = `git branch ${newBranchName} ${baseBranch}`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 执行创建分支命令
|
|
154
|
-
await execGitCommand(command);
|
|
155
|
-
|
|
156
|
-
// 切换到新创建的分支
|
|
157
|
-
await execGitCommand(`git checkout ${newBranchName}`);
|
|
158
|
-
|
|
159
|
-
res.json({ success: true, branch: newBranchName });
|
|
160
|
-
} catch (error) {
|
|
161
|
-
console.error('创建分支失败:', error);
|
|
162
|
-
res.status(500).json({ success: false, error: error.message });
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// 切换分支
|
|
167
|
-
app.post('/api/checkout', async (req, res) => {
|
|
168
|
-
try {
|
|
169
|
-
const { branch } = req.body;
|
|
170
|
-
if (!branch) {
|
|
171
|
-
return res.status(400).json({ success: false, error: '分支名称不能为空' });
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// 执行分支切换
|
|
175
|
-
await execGitCommand(`git checkout ${branch}`);
|
|
176
|
-
|
|
177
|
-
res.json({ success: true });
|
|
178
|
-
} catch (error) {
|
|
179
|
-
console.error('切换分支失败:', error);
|
|
180
|
-
res.status(500).json({ success: false, error: error.message });
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
app.
|
|
186
|
-
try {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
await configManager.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
config
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
//
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
//
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
//
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
)
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
//
|
|
913
|
-
if (
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
//
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
//
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
console.
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
res.status(500).json({ error: error.message });
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
res.status(500).json({
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
//
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
console.log(
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
console.
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
//
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
//
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { execGitCommand } from '../../utils/index.js';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
import config from '../../config.js';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { Server } from 'socket.io';
|
|
12
|
+
import chokidar from 'chokidar';
|
|
13
|
+
// import { exec } from 'child_process';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
const configManager = config; // 确保 configManager 可用
|
|
18
|
+
|
|
19
|
+
// 文件系统变动监控器
|
|
20
|
+
let watcher = null;
|
|
21
|
+
// 防抖计时器
|
|
22
|
+
let debounceTimer = null;
|
|
23
|
+
// 防抖延迟时间 (毫秒)
|
|
24
|
+
const DEBOUNCE_DELAY = 1000;
|
|
25
|
+
|
|
26
|
+
async function startUIServer() {
|
|
27
|
+
const app = express();
|
|
28
|
+
const httpServer = createServer(app);
|
|
29
|
+
const io = new Server(httpServer);
|
|
30
|
+
|
|
31
|
+
// 添加全局中间件来解析JSON请求体
|
|
32
|
+
app.use(express.json());
|
|
33
|
+
|
|
34
|
+
// 添加请求日志中间件
|
|
35
|
+
app.use((req, res, next) => {
|
|
36
|
+
const now = new Date().toLocaleString();
|
|
37
|
+
console.log(`[${now}] ${req.method} ${req.url}`);
|
|
38
|
+
next();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// 静态文件服务
|
|
42
|
+
app.use(express.static(path.join(__dirname, '../public')));
|
|
43
|
+
|
|
44
|
+
// API路由
|
|
45
|
+
app.get('/api/status', async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = await execGitCommand('git status');
|
|
48
|
+
res.json({ status: stdout });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
res.status(500).json({ error: error.message });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
app.get('/api/status_porcelain', async (req, res) => {
|
|
54
|
+
try {
|
|
55
|
+
const { stdout } = await execGitCommand('git status --porcelain --untracked-files=all');
|
|
56
|
+
res.json({ status: stdout });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
res.status(500).json({ error: error.message });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// 获取当前分支
|
|
63
|
+
app.get('/api/branch', async (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const { stdout } = await execGitCommand('git rev-parse --abbrev-ref HEAD');
|
|
66
|
+
res.json({ branch: stdout.trim() });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
res.status(500).json({ error: error.message });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 获取分支与远程的差异状态(领先/落后提交数)
|
|
73
|
+
app.get('/api/branch-status', async (req, res) => {
|
|
74
|
+
try {
|
|
75
|
+
// 检查当前目录是否是Git仓库
|
|
76
|
+
if (!isGitRepo) {
|
|
77
|
+
return res.json({ hasUpstream: false, ahead: 0, behind: 0 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 获取当前分支
|
|
81
|
+
const { stdout: branchOutput } = await execGitCommand('git symbolic-ref --short HEAD');
|
|
82
|
+
const currentBranch = branchOutput.trim();
|
|
83
|
+
|
|
84
|
+
// 获取上游分支
|
|
85
|
+
const { stdout: upstreamOutput } = await execGitCommand('git rev-parse --abbrev-ref --symbolic-full-name @{u}', { ignoreError: true });
|
|
86
|
+
|
|
87
|
+
if (!upstreamOutput.trim()) {
|
|
88
|
+
// 没有上游分支
|
|
89
|
+
return res.json({ hasUpstream: false, ahead: 0, behind: 0 });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const upstreamBranch = upstreamOutput.trim();
|
|
93
|
+
|
|
94
|
+
// 获取领先/落后提交数
|
|
95
|
+
const { stdout: aheadBehindOutput } = await execGitCommand(`git rev-list --left-right --count ${currentBranch}...${upstreamBranch}`);
|
|
96
|
+
const [ahead, behind] = aheadBehindOutput.trim().split('\t').map(Number);
|
|
97
|
+
|
|
98
|
+
res.json({
|
|
99
|
+
hasUpstream: true,
|
|
100
|
+
upstreamBranch,
|
|
101
|
+
ahead,
|
|
102
|
+
behind
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('获取分支状态失败:', error);
|
|
106
|
+
res.status(500).json({ error: error.message });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 获取所有分支
|
|
111
|
+
app.get('/api/branches', async (req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
// 获取本地分支
|
|
114
|
+
const { stdout: localBranches } = await execGitCommand('git branch --format="%(refname:short)"');
|
|
115
|
+
// 获取远程分支(过滤掉 origin/HEAD 和 origin)
|
|
116
|
+
const { stdout: remoteBranches } = await execGitCommand('git branch -r --format="%(refname:short)"');
|
|
117
|
+
|
|
118
|
+
// 合并并去重
|
|
119
|
+
const allBranches = [...new Set([
|
|
120
|
+
...localBranches.split('\n')
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.filter(b => !b.startsWith('* ')), // 过滤掉HEAD指针
|
|
123
|
+
...remoteBranches.split('\n')
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.filter(b => b.includes('/')) // 过滤掉单纯的 origin
|
|
126
|
+
.map(b => b.split('/')[1]) // 提取真正的分支名称
|
|
127
|
+
])];
|
|
128
|
+
|
|
129
|
+
res.json({ branches: allBranches });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('获取分支列表失败:', error);
|
|
132
|
+
res.status(500).json({ error: error.message });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 创建新分支
|
|
137
|
+
app.post('/api/create-branch', express.json(), async (req, res) => {
|
|
138
|
+
try {
|
|
139
|
+
const { newBranchName, baseBranch } = req.body;
|
|
140
|
+
|
|
141
|
+
if (!newBranchName) {
|
|
142
|
+
return res.status(400).json({ success: false, error: '分支名称不能为空' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 构建创建分支的命令
|
|
146
|
+
let command = `git branch ${newBranchName}`;
|
|
147
|
+
|
|
148
|
+
// 如果指定了基础分支,则基于该分支创建
|
|
149
|
+
if (baseBranch) {
|
|
150
|
+
command = `git branch ${newBranchName} ${baseBranch}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 执行创建分支命令
|
|
154
|
+
await execGitCommand(command);
|
|
155
|
+
|
|
156
|
+
// 切换到新创建的分支
|
|
157
|
+
await execGitCommand(`git checkout ${newBranchName}`);
|
|
158
|
+
|
|
159
|
+
res.json({ success: true, branch: newBranchName });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('创建分支失败:', error);
|
|
162
|
+
res.status(500).json({ success: false, error: error.message });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 切换分支
|
|
167
|
+
app.post('/api/checkout', async (req, res) => {
|
|
168
|
+
try {
|
|
169
|
+
const { branch } = req.body;
|
|
170
|
+
if (!branch) {
|
|
171
|
+
return res.status(400).json({ success: false, error: '分支名称不能为空' });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 执行分支切换
|
|
175
|
+
await execGitCommand(`git checkout ${branch}`);
|
|
176
|
+
|
|
177
|
+
res.json({ success: true });
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('切换分支失败:', error);
|
|
180
|
+
res.status(500).json({ success: false, error: error.message });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// 合并分支
|
|
185
|
+
app.post('/api/merge', async (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
const { branch, noCommit, noFf, squash, message } = req.body;
|
|
188
|
+
|
|
189
|
+
if (!branch) {
|
|
190
|
+
return res.status(400).json({ success: false, error: '分支名称不能为空' });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 构建Git合并命令
|
|
194
|
+
let command = `git merge ${branch}`;
|
|
195
|
+
|
|
196
|
+
// 添加可选参数
|
|
197
|
+
if (noCommit) {
|
|
198
|
+
command += ' --no-commit';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (noFf) {
|
|
202
|
+
command += ' --no-ff';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (squash) {
|
|
206
|
+
command += ' --squash';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (message) {
|
|
210
|
+
command += ` -m "${message}"`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
// 执行合并命令
|
|
215
|
+
const { stdout } = await execGitCommand(command);
|
|
216
|
+
|
|
217
|
+
res.json({
|
|
218
|
+
success: true,
|
|
219
|
+
message: '分支合并成功',
|
|
220
|
+
output: stdout
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
// 检查是否有合并冲突
|
|
224
|
+
const errorMsg = error.message || '';
|
|
225
|
+
const hasConflicts = errorMsg.includes('CONFLICT') ||
|
|
226
|
+
errorMsg.includes('Automatic merge failed');
|
|
227
|
+
|
|
228
|
+
if (hasConflicts) {
|
|
229
|
+
res.status(409).json({
|
|
230
|
+
success: false,
|
|
231
|
+
hasConflicts: true,
|
|
232
|
+
error: '合并过程中发生冲突,需要手动解决',
|
|
233
|
+
details: errorMsg
|
|
234
|
+
});
|
|
235
|
+
} else {
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error('合并分支失败:', error);
|
|
241
|
+
res.status(500).json({
|
|
242
|
+
success: false,
|
|
243
|
+
error: `合并分支失败: ${error.message}`
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// 获取Git用户配置信息
|
|
249
|
+
app.get('/api/user-info', async (req, res) => {
|
|
250
|
+
try {
|
|
251
|
+
// 获取全局用户名
|
|
252
|
+
const { stdout: userName } = await execGitCommand('git config --global user.name');
|
|
253
|
+
// 获取全局用户邮箱
|
|
254
|
+
const { stdout: userEmail } = await execGitCommand('git config --global user.email');
|
|
255
|
+
|
|
256
|
+
res.json({
|
|
257
|
+
name: userName.trim(),
|
|
258
|
+
email: userEmail.trim()
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
res.status(500).json({ error: error.message });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// 新增获取当前工作目录接口
|
|
266
|
+
app.get('/api/current_directory', async (req, res) => {
|
|
267
|
+
try {
|
|
268
|
+
const directory = process.cwd();
|
|
269
|
+
|
|
270
|
+
// 检查当前目录是否是Git仓库
|
|
271
|
+
try {
|
|
272
|
+
await execGitCommand('git rev-parse --is-inside-work-tree');
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return res.status(400).json({
|
|
275
|
+
error: '当前目录不是一个Git仓库',
|
|
276
|
+
directory,
|
|
277
|
+
isGitRepo: false
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
res.json({
|
|
282
|
+
directory,
|
|
283
|
+
isGitRepo: true
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
res.status(500).json({ error: error.message });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// 新增切换工作目录接口
|
|
291
|
+
app.post('/api/change_directory', async (req, res) => {
|
|
292
|
+
try {
|
|
293
|
+
const { path } = req.body;
|
|
294
|
+
|
|
295
|
+
if (!path) {
|
|
296
|
+
return res.status(400).json({ success: false, error: '目录路径不能为空' });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
process.chdir(path);
|
|
301
|
+
const newDirectory = process.cwd();
|
|
302
|
+
|
|
303
|
+
// 检查新目录是否是Git仓库
|
|
304
|
+
try {
|
|
305
|
+
await execGitCommand('git rev-parse --is-inside-work-tree');
|
|
306
|
+
|
|
307
|
+
// 初始化文件监控
|
|
308
|
+
initFileSystemWatcher();
|
|
309
|
+
|
|
310
|
+
res.json({
|
|
311
|
+
success: true,
|
|
312
|
+
directory: newDirectory,
|
|
313
|
+
isGitRepo: true
|
|
314
|
+
});
|
|
315
|
+
} catch (error) {
|
|
316
|
+
// 不是Git仓库,停止监控
|
|
317
|
+
if (watcher) {
|
|
318
|
+
watcher.close().catch(err => console.error('关闭监控器失败:', err));
|
|
319
|
+
watcher = null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
res.json({
|
|
323
|
+
success: true,
|
|
324
|
+
directory: newDirectory,
|
|
325
|
+
isGitRepo: false,
|
|
326
|
+
warning: '新目录不是一个Git仓库'
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
res.status(400).json({
|
|
331
|
+
success: false,
|
|
332
|
+
error: `切换到目录 "${path}" 失败: ${error.message}`
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
res.status(500).json({ error: error.message });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// 获取目录内容(用于浏览目录)
|
|
341
|
+
app.get('/api/browse_directory', async (req, res) => {
|
|
342
|
+
try {
|
|
343
|
+
|
|
344
|
+
// 获取要浏览的目录路径,如果没有提供,则使用当前目录
|
|
345
|
+
const directoryPath = req.query.path || process.cwd();
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
// 读取目录内容
|
|
349
|
+
const items = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
350
|
+
|
|
351
|
+
// 分离文件夹和文件
|
|
352
|
+
const directories = [];
|
|
353
|
+
const files = [];
|
|
354
|
+
|
|
355
|
+
for (const item of items) {
|
|
356
|
+
const fullPath = path.join(directoryPath, item.name);
|
|
357
|
+
if (item.isDirectory()) {
|
|
358
|
+
directories.push({
|
|
359
|
+
name: item.name,
|
|
360
|
+
path: fullPath,
|
|
361
|
+
type: 'directory'
|
|
362
|
+
});
|
|
363
|
+
} else if (item.isFile()) {
|
|
364
|
+
files.push({
|
|
365
|
+
name: item.name,
|
|
366
|
+
path: fullPath,
|
|
367
|
+
type: 'file'
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 优先显示目录,然后是文件,都按字母排序
|
|
373
|
+
directories.sort((a, b) => a.name.localeCompare(b.name));
|
|
374
|
+
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
375
|
+
|
|
376
|
+
// 获取父目录路径
|
|
377
|
+
const parentPath = path.dirname(directoryPath);
|
|
378
|
+
|
|
379
|
+
res.json({
|
|
380
|
+
success: true,
|
|
381
|
+
currentPath: directoryPath,
|
|
382
|
+
parentPath: parentPath !== directoryPath ? parentPath : null,
|
|
383
|
+
items: [...directories, ...files]
|
|
384
|
+
});
|
|
385
|
+
} catch (error) {
|
|
386
|
+
res.status(400).json({
|
|
387
|
+
success: false,
|
|
388
|
+
error: `无法读取目录 "${directoryPath}": ${error.message}`
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
} catch (error) {
|
|
392
|
+
res.status(500).json({ error: error.message });
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// POST接口版本的浏览目录功能
|
|
397
|
+
app.post('/api/browse_directory', async (req, res) => {
|
|
398
|
+
try {
|
|
399
|
+
// 获取要浏览的目录路径,如果没有提供,则使用当前目录
|
|
400
|
+
const directoryPath = req.body.currentPath || process.cwd();
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
// 读取目录内容
|
|
404
|
+
const items = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
405
|
+
|
|
406
|
+
// 分离文件夹和文件
|
|
407
|
+
const directories = [];
|
|
408
|
+
const files = [];
|
|
409
|
+
|
|
410
|
+
for (const item of items) {
|
|
411
|
+
const fullPath = path.join(directoryPath, item.name);
|
|
412
|
+
if (item.isDirectory()) {
|
|
413
|
+
directories.push({
|
|
414
|
+
name: item.name,
|
|
415
|
+
path: fullPath,
|
|
416
|
+
type: 'directory'
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 只返回目录,不返回文件
|
|
422
|
+
directories.sort((a, b) => a.name.localeCompare(b.name));
|
|
423
|
+
|
|
424
|
+
// 获取父目录路径
|
|
425
|
+
const parentPath = path.dirname(directoryPath);
|
|
426
|
+
|
|
427
|
+
// 返回选择的目录路径
|
|
428
|
+
res.json({
|
|
429
|
+
success: true,
|
|
430
|
+
path: directoryPath,
|
|
431
|
+
parentPath: parentPath !== directoryPath ? parentPath : null,
|
|
432
|
+
items: directories
|
|
433
|
+
});
|
|
434
|
+
} catch (error) {
|
|
435
|
+
res.status(400).json({
|
|
436
|
+
success: false,
|
|
437
|
+
error: `无法读取目录 "${directoryPath}": ${error.message}`
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
res.status(500).json({
|
|
442
|
+
success: false,
|
|
443
|
+
error: error.message
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// 获取最近访问的目录列表
|
|
449
|
+
app.get('/api/recent_directories', async (req, res) => {
|
|
450
|
+
try {
|
|
451
|
+
// 尝试从配置中获取最近的目录
|
|
452
|
+
const recentDirs = await configManager.getRecentDirectories();
|
|
453
|
+
res.json({
|
|
454
|
+
success: true,
|
|
455
|
+
directories: recentDirs || []
|
|
456
|
+
});
|
|
457
|
+
} catch (error) {
|
|
458
|
+
res.status(500).json({
|
|
459
|
+
success: false,
|
|
460
|
+
error: error.message
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// 在资源管理器/访达中打开当前目录
|
|
466
|
+
app.post('/api/open_directory', async (req, res) => {
|
|
467
|
+
try {
|
|
468
|
+
// 获取要打开的目录路径,如果没有提供,则使用当前目录
|
|
469
|
+
const directoryPath = req.body.path || process.cwd();
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
// 检查目录是否存在
|
|
473
|
+
await fs.access(directoryPath);
|
|
474
|
+
|
|
475
|
+
// 使用open模块打开目录,自动处理不同操作系统
|
|
476
|
+
await open(directoryPath, { wait: false });
|
|
477
|
+
|
|
478
|
+
res.json({
|
|
479
|
+
success: true,
|
|
480
|
+
message: '已在文件管理器中打开目录'
|
|
481
|
+
});
|
|
482
|
+
} catch (error) {
|
|
483
|
+
res.status(400).json({
|
|
484
|
+
success: false,
|
|
485
|
+
error: `无法打开目录 "${directoryPath}": ${error.message}`
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
res.status(500).json({
|
|
490
|
+
success: false,
|
|
491
|
+
error: error.message
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// 保存最近访问的目录
|
|
497
|
+
app.post('/api/save_recent_directory', async (req, res) => {
|
|
498
|
+
try {
|
|
499
|
+
const { path } = req.body;
|
|
500
|
+
|
|
501
|
+
if (!path) {
|
|
502
|
+
return res.status(400).json({
|
|
503
|
+
success: false,
|
|
504
|
+
error: '目录路径不能为空'
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 保存到配置
|
|
509
|
+
await configManager.saveRecentDirectory(path);
|
|
510
|
+
|
|
511
|
+
res.json({
|
|
512
|
+
success: true
|
|
513
|
+
});
|
|
514
|
+
} catch (error) {
|
|
515
|
+
res.status(500).json({
|
|
516
|
+
success: false,
|
|
517
|
+
error: error.message
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// 获取配置
|
|
523
|
+
app.get('/api/config/getConfig', async (req, res) => {
|
|
524
|
+
try {
|
|
525
|
+
const config = await configManager.loadConfig()
|
|
526
|
+
res.json(config)
|
|
527
|
+
} catch (error) {
|
|
528
|
+
res.status(500).json({ error: error.message })
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
// 保存默认提交信息
|
|
533
|
+
app.post('/api/config/saveDefaultMessage', express.json(), async (req, res) => {
|
|
534
|
+
try {
|
|
535
|
+
const { defaultCommitMessage } = req.body
|
|
536
|
+
|
|
537
|
+
if (!defaultCommitMessage) {
|
|
538
|
+
return res.status(400).json({ success: false, error: '缺少必要参数' })
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const config = await configManager.loadConfig()
|
|
542
|
+
|
|
543
|
+
// 更新默认提交信息
|
|
544
|
+
config.defaultCommitMessage = defaultCommitMessage
|
|
545
|
+
await configManager.saveConfig(config)
|
|
546
|
+
|
|
547
|
+
res.json({ success: true })
|
|
548
|
+
} catch (error) {
|
|
549
|
+
res.status(500).json({ success: false, error: error.message })
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
// 保存模板
|
|
554
|
+
app.post('/api/config/save-template', express.json(), async (req, res) => {
|
|
555
|
+
try {
|
|
556
|
+
const { template, type } = req.body
|
|
557
|
+
|
|
558
|
+
if (!template || !type) {
|
|
559
|
+
return res.status(400).json({ success: false, error: '缺少必要参数' })
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const config = await configManager.loadConfig()
|
|
563
|
+
|
|
564
|
+
if (type === 'description') {
|
|
565
|
+
// 确保描述模板数组存在
|
|
566
|
+
if (!config.descriptionTemplates) {
|
|
567
|
+
config.descriptionTemplates = []
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 检查是否已存在相同模板
|
|
571
|
+
if (!config.descriptionTemplates.includes(template)) {
|
|
572
|
+
config.descriptionTemplates.push(template)
|
|
573
|
+
await configManager.saveConfig(config)
|
|
574
|
+
}
|
|
575
|
+
} else if (type === 'scope') {
|
|
576
|
+
// 确保作用域模板数组存在
|
|
577
|
+
if (!config.scopeTemplates) {
|
|
578
|
+
config.scopeTemplates = []
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// 检查是否已存在相同模板
|
|
582
|
+
if (!config.scopeTemplates.includes(template)) {
|
|
583
|
+
config.scopeTemplates.push(template)
|
|
584
|
+
await configManager.saveConfig(config)
|
|
585
|
+
}
|
|
586
|
+
} else if (type === 'message') {
|
|
587
|
+
// 确保提交信息模板数组存在
|
|
588
|
+
if (!config.messageTemplates) {
|
|
589
|
+
config.messageTemplates = []
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// 检查是否已存在相同模板
|
|
593
|
+
if (!config.messageTemplates.includes(template)) {
|
|
594
|
+
config.messageTemplates.push(template)
|
|
595
|
+
await configManager.saveConfig(config)
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
return res.status(400).json({ success: false, error: '不支持的模板类型' })
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
res.json({ success: true })
|
|
602
|
+
} catch (error) {
|
|
603
|
+
res.status(500).json({ success: false, error: error.message })
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
// 删除模板
|
|
608
|
+
app.post('/api/config/delete-template', express.json(), async (req, res) => {
|
|
609
|
+
try {
|
|
610
|
+
const { template, type } = req.body
|
|
611
|
+
|
|
612
|
+
if (!template || !type) {
|
|
613
|
+
return res.status(400).json({ success: false, error: '缺少必要参数' })
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const config = await configManager.loadConfig()
|
|
617
|
+
|
|
618
|
+
if (type === 'description') {
|
|
619
|
+
// 确保描述模板数组存在
|
|
620
|
+
if (config.descriptionTemplates) {
|
|
621
|
+
const index = config.descriptionTemplates.indexOf(template)
|
|
622
|
+
if (index !== -1) {
|
|
623
|
+
config.descriptionTemplates.splice(index, 1)
|
|
624
|
+
await configManager.saveConfig(config)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} else if (type === 'scope') {
|
|
628
|
+
// 确保作用域模板数组存在
|
|
629
|
+
if (config.scopeTemplates) {
|
|
630
|
+
const index = config.scopeTemplates.indexOf(template)
|
|
631
|
+
if (index !== -1) {
|
|
632
|
+
config.scopeTemplates.splice(index, 1)
|
|
633
|
+
await configManager.saveConfig(config)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} else if (type === 'message') {
|
|
637
|
+
// 确保提交信息模板数组存在
|
|
638
|
+
if (config.messageTemplates) {
|
|
639
|
+
const index = config.messageTemplates.indexOf(template)
|
|
640
|
+
if (index !== -1) {
|
|
641
|
+
config.messageTemplates.splice(index, 1)
|
|
642
|
+
await configManager.saveConfig(config)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
return res.status(400).json({ success: false, error: '不支持的模板类型' })
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
res.json({ success: true })
|
|
650
|
+
} catch (error) {
|
|
651
|
+
res.status(500).json({ success: false, error: error.message })
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
// 更新模板
|
|
656
|
+
app.post('/api/config/update-template', express.json(), async (req, res) => {
|
|
657
|
+
try {
|
|
658
|
+
const { oldTemplate, newTemplate, type } = req.body
|
|
659
|
+
|
|
660
|
+
if (!oldTemplate || !newTemplate || !type) {
|
|
661
|
+
return res.status(400).json({ success: false, error: '缺少必要参数' })
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const config = await configManager.loadConfig()
|
|
665
|
+
|
|
666
|
+
if (type === 'description') {
|
|
667
|
+
// 确保描述模板数组存在
|
|
668
|
+
if (config.descriptionTemplates) {
|
|
669
|
+
const index = config.descriptionTemplates.indexOf(oldTemplate)
|
|
670
|
+
if (index !== -1) {
|
|
671
|
+
config.descriptionTemplates[index] = newTemplate
|
|
672
|
+
await configManager.saveConfig(config)
|
|
673
|
+
} else {
|
|
674
|
+
return res.status(404).json({ success: false, error: '未找到原模板' })
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
return res.status(404).json({ success: false, error: '模板列表不存在' })
|
|
678
|
+
}
|
|
679
|
+
} else if (type === 'scope') {
|
|
680
|
+
// 确保作用域模板数组存在
|
|
681
|
+
if (config.scopeTemplates) {
|
|
682
|
+
const index = config.scopeTemplates.indexOf(oldTemplate)
|
|
683
|
+
if (index !== -1) {
|
|
684
|
+
config.scopeTemplates[index] = newTemplate
|
|
685
|
+
await configManager.saveConfig(config)
|
|
686
|
+
} else {
|
|
687
|
+
return res.status(404).json({ success: false, error: '未找到原模板' })
|
|
688
|
+
}
|
|
689
|
+
} else {
|
|
690
|
+
return res.status(404).json({ success: false, error: '模板列表不存在' })
|
|
691
|
+
}
|
|
692
|
+
} else if (type === 'message') {
|
|
693
|
+
// 确保提交信息模板数组存在
|
|
694
|
+
if (config.messageTemplates) {
|
|
695
|
+
const index = config.messageTemplates.indexOf(oldTemplate)
|
|
696
|
+
if (index !== -1) {
|
|
697
|
+
config.messageTemplates[index] = newTemplate
|
|
698
|
+
await configManager.saveConfig(config)
|
|
699
|
+
} else {
|
|
700
|
+
return res.status(404).json({ success: false, error: '未找到原模板' })
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
return res.status(404).json({ success: false, error: '模板列表不存在' })
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
return res.status(400).json({ success: false, error: '不支持的模板类型' })
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
res.json({ success: true })
|
|
710
|
+
} catch (error) {
|
|
711
|
+
res.status(500).json({ success: false, error: error.message })
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
// 提交更改
|
|
716
|
+
app.post('/api/commit', express.json(), async (req, res) => {
|
|
717
|
+
try {
|
|
718
|
+
const { message, hasNewlines, noVerify } = req.body;
|
|
719
|
+
|
|
720
|
+
// 构建 git commit 命令
|
|
721
|
+
let commitCommand = 'git commit';
|
|
722
|
+
|
|
723
|
+
// 如果消息包含换行符,使用文件方式提交
|
|
724
|
+
if (hasNewlines) {
|
|
725
|
+
// 创建临时文件存储提交信息
|
|
726
|
+
const tempFile = path.join(os.tmpdir(), `commit-msg-${Date.now()}.txt`);
|
|
727
|
+
await fs.writeFile(tempFile, message);
|
|
728
|
+
commitCommand += ` -F "${tempFile}"`;
|
|
729
|
+
} else {
|
|
730
|
+
// 否则直接在命令行中提供消息
|
|
731
|
+
commitCommand += ` -m "${message}"`;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// 添加 --no-verify 参数
|
|
735
|
+
if (noVerify) {
|
|
736
|
+
commitCommand += ' --no-verify';
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
console.log(`commitCommand ==>`, commitCommand);
|
|
740
|
+
// 执行提交命令
|
|
741
|
+
await execGitCommand(commitCommand);
|
|
742
|
+
|
|
743
|
+
// 如果使用了临时文件,删除它
|
|
744
|
+
if (hasNewlines) {
|
|
745
|
+
const tempFile = path.join(os.tmpdir(), `commit-msg-${Date.now()}.txt`);
|
|
746
|
+
await fs.unlink(tempFile).catch(() => {});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
res.json({ success: true });
|
|
750
|
+
} catch (error) {
|
|
751
|
+
res.status(500).json({ success: false, error: error.message });
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// 添加 add 接口
|
|
756
|
+
app.post('/api/add', async (req, res) => {
|
|
757
|
+
try {
|
|
758
|
+
// 执行 git add . 命令添加所有更改
|
|
759
|
+
await execGitCommand('git add .');
|
|
760
|
+
res.json({ success: true });
|
|
761
|
+
} catch (error) {
|
|
762
|
+
res.status(500).json({ success: false, error: error.message });
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// 添加单个文件到暂存区
|
|
767
|
+
app.post('/api/add-file', async (req, res) => {
|
|
768
|
+
try {
|
|
769
|
+
const { filePath } = req.body;
|
|
770
|
+
|
|
771
|
+
if (!filePath) {
|
|
772
|
+
return res.status(400).json({
|
|
773
|
+
success: false,
|
|
774
|
+
error: '缺少文件路径参数'
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// 执行 git add 命令添加特定文件
|
|
779
|
+
await execGitCommand(`git add "${filePath}"`);
|
|
780
|
+
res.json({ success: true });
|
|
781
|
+
} catch (error) {
|
|
782
|
+
res.status(500).json({ success: false, error: error.message });
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// 从暂存区移除单个文件
|
|
787
|
+
app.post('/api/unstage-file', async (req, res) => {
|
|
788
|
+
try {
|
|
789
|
+
const { filePath } = req.body;
|
|
790
|
+
|
|
791
|
+
if (!filePath) {
|
|
792
|
+
return res.status(400).json({
|
|
793
|
+
success: false,
|
|
794
|
+
error: '缺少文件路径参数'
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// 执行 git reset HEAD 命令移除特定文件的暂存
|
|
799
|
+
await execGitCommand(`git reset HEAD -- "${filePath}"`);
|
|
800
|
+
res.json({ success: true });
|
|
801
|
+
} catch (error) {
|
|
802
|
+
res.status(500).json({ success: false, error: error.message });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// 推送更改
|
|
807
|
+
app.post('/api/push', async (req, res) => {
|
|
808
|
+
try {
|
|
809
|
+
const { stdout } = await execGitCommand('git push');
|
|
810
|
+
res.json({ success: true, message: stdout });
|
|
811
|
+
} catch (error) {
|
|
812
|
+
res.status(500).json({ success: false, error: error.message });
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// 添加git pull API端点
|
|
817
|
+
app.post('/api/pull', async (req, res) => {
|
|
818
|
+
try {
|
|
819
|
+
const { stdout } = await execGitCommand('git pull');
|
|
820
|
+
res.json({ success: true, message: stdout });
|
|
821
|
+
} catch (error) {
|
|
822
|
+
// 改进错误处理,检查是否需要合并
|
|
823
|
+
const errorMsg = error.message || '';
|
|
824
|
+
const needsMerge = errorMsg.includes('merge') ||
|
|
825
|
+
errorMsg.includes('需要合并') ||
|
|
826
|
+
errorMsg.includes('CONFLICT') ||
|
|
827
|
+
errorMsg.includes('冲突');
|
|
828
|
+
|
|
829
|
+
// 返回更详细的错误信息和标记
|
|
830
|
+
res.status(500).json({
|
|
831
|
+
success: false,
|
|
832
|
+
error: error.message,
|
|
833
|
+
needsMerge: needsMerge,
|
|
834
|
+
// 包含完整的错误输出
|
|
835
|
+
fullError: error.stderr || error.message,
|
|
836
|
+
pullOutput: error.stdout || ''
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
// 添加git fetch --all API端点
|
|
842
|
+
app.post('/api/fetch-all', async (req, res) => {
|
|
843
|
+
try {
|
|
844
|
+
const { stdout } = await execGitCommand('git fetch --all');
|
|
845
|
+
res.json({ success: true, message: stdout });
|
|
846
|
+
} catch (error) {
|
|
847
|
+
res.status(500).json({ success: false, error: error.message });
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// 获取日志
|
|
852
|
+
app.get('/api/log', async (req, res) => {
|
|
853
|
+
try {
|
|
854
|
+
// 获取分页参数
|
|
855
|
+
const page = parseInt(req.query.page) || 1;
|
|
856
|
+
const limit = parseInt(req.query.limit) || 100;
|
|
857
|
+
const skip = (page - 1) * limit;
|
|
858
|
+
|
|
859
|
+
// 获取筛选参数
|
|
860
|
+
const author = req.query.author ? req.query.author.split(',') : [];
|
|
861
|
+
const message = req.query.message || '';
|
|
862
|
+
const dateFrom = req.query.dateFrom || '';
|
|
863
|
+
const dateTo = req.query.dateTo || '';
|
|
864
|
+
const branch = req.query.branch ? req.query.branch.split(',') : [];
|
|
865
|
+
const withParents = req.query.with_parents === 'true';
|
|
866
|
+
|
|
867
|
+
// 构建Git命令选项
|
|
868
|
+
let commandOptions = [];
|
|
869
|
+
|
|
870
|
+
// 修改分支筛选处理 - 使用正确的引用格式
|
|
871
|
+
if (branch.length > 0) {
|
|
872
|
+
// 不再简单拼接分支名,而是将它们作为引用路径处理
|
|
873
|
+
// 如果指定了分支,不再使用--all参数,而是直接用分支名
|
|
874
|
+
commandOptions = commandOptions.filter(opt => opt !== '--all');
|
|
875
|
+
|
|
876
|
+
// 将分支名格式化为Git可理解的引用格式
|
|
877
|
+
const branchRefs = branch.map(b => b.trim()).join(' ');
|
|
878
|
+
|
|
879
|
+
// 直接将分支名作为命令参数,并确保后面添加 -- 分隔符防止歧义
|
|
880
|
+
return executeGitLogCommand(res, branchRefs, author, message, dateFrom, dateTo, limit, skip, req.query.all === 'true', withParents);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// 如果没有指定分支,则使用--all参数
|
|
884
|
+
// 作者筛选(支持多作者,使用正则表达式OR操作)
|
|
885
|
+
if (author.length > 0) {
|
|
886
|
+
// 过滤掉空作者
|
|
887
|
+
const validAuthors = author.filter(a => a.trim() !== '');
|
|
888
|
+
|
|
889
|
+
if (validAuthors.length === 1) {
|
|
890
|
+
// 单个作者,直接使用--author
|
|
891
|
+
commandOptions.push(`--author="${validAuthors[0].trim()}"`);
|
|
892
|
+
} else if (validAuthors.length > 1) {
|
|
893
|
+
// 多个作者,使用正则表达式OR条件
|
|
894
|
+
// 只转义OR运算符,保持其他内容不变
|
|
895
|
+
const authorPattern = validAuthors
|
|
896
|
+
.map(a => a.trim())
|
|
897
|
+
.join('\\|'); // 在JavaScript字符串中\\|会变成\|,在shell中会成为|
|
|
898
|
+
|
|
899
|
+
commandOptions.push(`--author="${authorPattern}"`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// 日期范围筛选
|
|
904
|
+
if (dateFrom && dateTo) {
|
|
905
|
+
commandOptions.push(`--after="${dateFrom}" --before="${dateTo} 23:59:59"`);
|
|
906
|
+
} else if (dateFrom) {
|
|
907
|
+
commandOptions.push(`--after="${dateFrom}"`);
|
|
908
|
+
} else if (dateTo) {
|
|
909
|
+
commandOptions.push(`--before="${dateTo} 23:59:59"`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// 提交信息筛选
|
|
913
|
+
if (message) {
|
|
914
|
+
commandOptions.push(`--grep="${message}"`);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// 如果all=true,则不使用限制,否则按页码和limit精确获取
|
|
918
|
+
// 修复:只获取当前页的数据,而不是累计所有之前页的数据
|
|
919
|
+
const limitOption = req.query.all === 'true' ? '' : `-n ${limit} --skip=${skip}`;
|
|
920
|
+
|
|
921
|
+
// 合并所有命令选项
|
|
922
|
+
const options = [...commandOptions, limitOption].filter(Boolean).join(' ');
|
|
923
|
+
|
|
924
|
+
// 添加父提交信息的格式
|
|
925
|
+
let formatString = '%H|%an|%ae|%ad|%B|%D';
|
|
926
|
+
if (withParents) {
|
|
927
|
+
formatString = '%H|%an|%ae|%ad|%B|%D|%P';
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
console.log(`执行Git命令: git log --all --pretty=format:"${formatString}" --date=short ${options}`);
|
|
931
|
+
|
|
932
|
+
// 使用 git log 命令获取提交历史
|
|
933
|
+
let { stdout: logOutput } = await execGitCommand(
|
|
934
|
+
`git log --all --pretty=format:"${formatString}" --date=short ${options}`
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
// 获取总提交数量(考虑筛选条件)
|
|
938
|
+
let totalCommits = 0;
|
|
939
|
+
try {
|
|
940
|
+
// 构建计数命令,包含相同的筛选条件
|
|
941
|
+
// 对于作者筛选,使用 rev-list 并手动计数,避免 --count 与复杂作者筛选结合可能的问题
|
|
942
|
+
if (author.length > 1) {
|
|
943
|
+
// 多作者情况,使用 wc -l 手动计数
|
|
944
|
+
const countCommand = `git rev-list --all ${commandOptions.join(' ')}`;
|
|
945
|
+
console.log(`执行计数命令(多作者): ${countCommand}`);
|
|
946
|
+
|
|
947
|
+
const { stdout: countOutput } = await execGitCommand(countCommand);
|
|
948
|
+
// 计算行数作为提交数
|
|
949
|
+
totalCommits = countOutput.trim().split('\n').filter(line => line.trim() !== '').length;
|
|
950
|
+
} else {
|
|
951
|
+
// 单作者或无作者筛选,可以直接使用 --count
|
|
952
|
+
const countCommand = `git rev-list --all --count ${commandOptions.join(' ')}`;
|
|
953
|
+
console.log(`执行计数命令(单作者): ${countCommand}`);
|
|
954
|
+
|
|
955
|
+
const { stdout: countOutput } = await execGitCommand(countCommand);
|
|
956
|
+
totalCommits = parseInt(countOutput.trim());
|
|
957
|
+
}
|
|
958
|
+
} catch (error) {
|
|
959
|
+
console.error('获取提交总数失败:', error);
|
|
960
|
+
totalCommits = 0;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
processAndSendLogOutput(res, logOutput, totalCommits, page, limit, withParents);
|
|
964
|
+
} catch (error) {
|
|
965
|
+
console.error('获取Git日志失败:', error);
|
|
966
|
+
res.status(500).json({ error: '获取日志失败: ' + error.message });
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// 抽取执行Git日志命令的函数
|
|
971
|
+
async function executeGitLogCommand(res, branchRefs, author, message, dateFrom, dateTo, limit, skip, isAll, withParents = false) {
|
|
972
|
+
try {
|
|
973
|
+
// 构建命令选项
|
|
974
|
+
const commandOptions = [];
|
|
975
|
+
|
|
976
|
+
// 作者筛选
|
|
977
|
+
if (author.length > 0) {
|
|
978
|
+
const validAuthors = author.filter(a => a.trim() !== '');
|
|
979
|
+
|
|
980
|
+
if (validAuthors.length === 1) {
|
|
981
|
+
commandOptions.push(`--author="${validAuthors[0].trim()}"`);
|
|
982
|
+
} else if (validAuthors.length > 1) {
|
|
983
|
+
const authorPattern = validAuthors.map(a => a.trim()).join('\\|');
|
|
984
|
+
commandOptions.push(`--author="${authorPattern}"`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// 日期范围筛选
|
|
989
|
+
if (dateFrom && dateTo) {
|
|
990
|
+
commandOptions.push(`--after="${dateFrom}" --before="${dateTo} 23:59:59"`);
|
|
991
|
+
} else if (dateFrom) {
|
|
992
|
+
commandOptions.push(`--after="${dateFrom}"`);
|
|
993
|
+
} else if (dateTo) {
|
|
994
|
+
commandOptions.push(`--before="${dateTo} 23:59:59"`);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// 提交信息筛选
|
|
998
|
+
if (message) {
|
|
999
|
+
commandOptions.push(`--grep="${message}"`);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// 限制选项
|
|
1003
|
+
const limitOption = isAll ? '' : `-n ${limit} --skip=${skip}`;
|
|
1004
|
+
|
|
1005
|
+
// 合并所有选项
|
|
1006
|
+
const options = [...commandOptions, limitOption].filter(Boolean).join(' ');
|
|
1007
|
+
|
|
1008
|
+
// 准备分支引用,确保它们被正确识别为分支而不是文件名
|
|
1009
|
+
// 使用 refs/heads/ 前缀明确指示这是分支
|
|
1010
|
+
const formattedBranchRefs = branchRefs.split(' ')
|
|
1011
|
+
.map(branch => {
|
|
1012
|
+
// 检查是否已经是完整引用
|
|
1013
|
+
if (branch.startsWith('refs/') || branch.includes('/')) {
|
|
1014
|
+
return branch;
|
|
1015
|
+
}
|
|
1016
|
+
// 添加refs/heads/前缀
|
|
1017
|
+
return `refs/heads/${branch}`;
|
|
1018
|
+
})
|
|
1019
|
+
.join(' ');
|
|
1020
|
+
|
|
1021
|
+
// 添加父提交信息的格式
|
|
1022
|
+
let formatString = '%H|%an|%ae|%ad|%B|%D';
|
|
1023
|
+
if (withParents) {
|
|
1024
|
+
formatString = '%H|%an|%ae|%ad|%B|%D|%P';
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// 构建执行的命令
|
|
1028
|
+
const command = `git log ${formattedBranchRefs} --pretty=format:"${formatString}" --date=short ${options}`;
|
|
1029
|
+
console.log(`执行Git命令(带分支引用): ${command}`);
|
|
1030
|
+
|
|
1031
|
+
// 执行命令
|
|
1032
|
+
const { stdout: logOutput } = await execGitCommand(command);
|
|
1033
|
+
|
|
1034
|
+
// 获取总提交数
|
|
1035
|
+
let totalCommits = 0;
|
|
1036
|
+
try {
|
|
1037
|
+
// 构建计数命令
|
|
1038
|
+
const countCommand = `git rev-list ${formattedBranchRefs} --count ${commandOptions.join(' ')}`;
|
|
1039
|
+
console.log(`执行计数命令(分支): ${countCommand}`);
|
|
1040
|
+
|
|
1041
|
+
const { stdout: countOutput } = await execGitCommand(countCommand);
|
|
1042
|
+
totalCommits = parseInt(countOutput.trim());
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
console.error('获取提交总数失败:', error);
|
|
1045
|
+
totalCommits = 0;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
processAndSendLogOutput(res, logOutput, totalCommits, skip / limit + 1, limit, withParents);
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
console.error('执行Git日志命令失败:', error);
|
|
1051
|
+
res.status(500).json({ error: '获取日志失败: ' + error.message });
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// 抽取处理输出并发送响应的函数
|
|
1056
|
+
function processAndSendLogOutput(res, logOutput, totalCommits, page, limit, withParents = false) {
|
|
1057
|
+
// 替换提交记录之间的换行符
|
|
1058
|
+
logOutput = logOutput.replace(/\n(?=[a-f0-9]{40}\|)/g, "<<<RECORD_SEPARATOR>>>");
|
|
1059
|
+
|
|
1060
|
+
// 按分隔符拆分日志条目
|
|
1061
|
+
const logEntries = logOutput.split("<<<RECORD_SEPARATOR>>>");
|
|
1062
|
+
|
|
1063
|
+
// 处理每个日志条目
|
|
1064
|
+
const data = logEntries.map(entry => {
|
|
1065
|
+
const parts = entry.split('|');
|
|
1066
|
+
if (parts.length >= 5) {
|
|
1067
|
+
const result = {
|
|
1068
|
+
hash: parts[0],
|
|
1069
|
+
author: parts[1],
|
|
1070
|
+
email: parts[2],
|
|
1071
|
+
date: parts[3],
|
|
1072
|
+
message: parts[4],
|
|
1073
|
+
branch: parts[5] || ''
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// 如果请求了父提交信息,添加到结果中
|
|
1077
|
+
if (withParents && parts[6]) {
|
|
1078
|
+
result.parents = parts[6].split(' ');
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1083
|
+
return null;
|
|
1084
|
+
}).filter(item => item !== null);
|
|
1085
|
+
|
|
1086
|
+
// 计算是否有更多数据
|
|
1087
|
+
const hasMore = page * limit < totalCommits;
|
|
1088
|
+
|
|
1089
|
+
console.log(`分页查询 - 页码: ${page}, 每页数量: ${limit}, 总数: ${totalCommits}, 返回数量: ${data.length}, 是否有更多: ${hasMore}`);
|
|
1090
|
+
|
|
1091
|
+
// 返回提交历史数据,包括是否有更多数据的标志
|
|
1092
|
+
res.json({
|
|
1093
|
+
data: data,
|
|
1094
|
+
total: totalCommits,
|
|
1095
|
+
page: page,
|
|
1096
|
+
limit: limit,
|
|
1097
|
+
hasMore: hasMore
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// 获取文件差异
|
|
1102
|
+
app.get('/api/diff', async (req, res) => {
|
|
1103
|
+
try {
|
|
1104
|
+
const filePath = req.query.file;
|
|
1105
|
+
|
|
1106
|
+
if (!filePath) {
|
|
1107
|
+
return res.status(400).json({ error: '缺少文件路径参数' });
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// 执行git diff命令获取文件差异
|
|
1111
|
+
const { stdout } = await execGitCommand(`git diff -- "${filePath}"`);
|
|
1112
|
+
|
|
1113
|
+
res.json({ diff: stdout });
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
res.status(500).json({ error: error.message });
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
// 获取已暂存文件差异
|
|
1119
|
+
app.get('/api/diff-cached', async (req, res) => {
|
|
1120
|
+
try {
|
|
1121
|
+
const filePath = req.query.file;
|
|
1122
|
+
|
|
1123
|
+
if (!filePath) {
|
|
1124
|
+
return res.status(400).json({ error: '缺少文件路径参数' });
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// 执行git diff --cached命令获取已暂存文件差异
|
|
1128
|
+
const { stdout } = await execGitCommand(`git diff --cached -- "${filePath}"`);
|
|
1129
|
+
|
|
1130
|
+
res.json({ diff: stdout });
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
res.status(500).json({ error: error.message });
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
// 获取文件内容 (用于未跟踪文件)
|
|
1137
|
+
app.get('/api/file-content', async (req, res) => {
|
|
1138
|
+
try {
|
|
1139
|
+
const filePath = req.query.file;
|
|
1140
|
+
|
|
1141
|
+
if (!filePath) {
|
|
1142
|
+
return res.status(400).json({ error: '缺少文件路径参数' });
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
try {
|
|
1146
|
+
// 读取文件内容
|
|
1147
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1148
|
+
res.json({ success: true, content });
|
|
1149
|
+
} catch (readError) {
|
|
1150
|
+
res.status(500).json({ success: false, error: `无法读取文件: ${readError.message}` });
|
|
1151
|
+
}
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
res.status(500).json({ error: error.message });
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
// 撤回文件修改
|
|
1158
|
+
app.post('/api/revert_file', async (req, res) => {
|
|
1159
|
+
try {
|
|
1160
|
+
const { filePath } = req.body;
|
|
1161
|
+
|
|
1162
|
+
if (!filePath) {
|
|
1163
|
+
return res.status(400).json({
|
|
1164
|
+
success: false,
|
|
1165
|
+
error: '缺少文件路径参数'
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// 检查文件状态:未跟踪文件需要删除,修改文件需要恢复
|
|
1170
|
+
const { stdout: statusOutput } = await execGitCommand(`git status --porcelain -- "${filePath}"`);
|
|
1171
|
+
|
|
1172
|
+
// 未跟踪的文件 (??), 需要删除它
|
|
1173
|
+
if (statusOutput.startsWith('??')) {
|
|
1174
|
+
try {
|
|
1175
|
+
await fs.unlink(filePath);
|
|
1176
|
+
return res.json({ success: true, message: '未跟踪的文件已删除' });
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
return res.status(500).json({
|
|
1179
|
+
success: false,
|
|
1180
|
+
error: `删除文件失败: ${error.message}`
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// 已暂存的文件,先取消暂存
|
|
1185
|
+
else if (statusOutput.startsWith('A ') || statusOutput.startsWith('M ') || statusOutput.startsWith('D ')) {
|
|
1186
|
+
// 先取消暂存
|
|
1187
|
+
await execGitCommand(`git reset HEAD -- "${filePath}"`);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// 已修改文件,取消所有本地修改
|
|
1191
|
+
if (statusOutput) {
|
|
1192
|
+
await execGitCommand(`git checkout -- "${filePath}"`);
|
|
1193
|
+
return res.json({ success: true, message: '文件修改已撤回' });
|
|
1194
|
+
} else {
|
|
1195
|
+
return res.status(400).json({
|
|
1196
|
+
success: false,
|
|
1197
|
+
error: '文件没有修改或不存在'
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
console.error('撤回文件修改失败:', error);
|
|
1202
|
+
res.status(500).json({
|
|
1203
|
+
success: false,
|
|
1204
|
+
error: `撤回文件修改失败: ${error.message}`
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
// 重置暂存区 (git reset HEAD)
|
|
1210
|
+
app.post('/api/reset-head', async (req, res) => {
|
|
1211
|
+
try {
|
|
1212
|
+
// 执行 git reset HEAD 命令
|
|
1213
|
+
await execGitCommand('git reset HEAD');
|
|
1214
|
+
res.json({ success: true });
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
console.error('重置暂存区失败:', error);
|
|
1217
|
+
res.status(500).json({
|
|
1218
|
+
success: false,
|
|
1219
|
+
error: `重置暂存区失败: ${error.message}`
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// 重置到远程分支 (git reset --hard origin/branch)
|
|
1225
|
+
app.post('/api/reset-to-remote', async (req, res) => {
|
|
1226
|
+
try {
|
|
1227
|
+
const { branch } = req.body;
|
|
1228
|
+
|
|
1229
|
+
if (!branch) {
|
|
1230
|
+
return res.status(400).json({
|
|
1231
|
+
success: false,
|
|
1232
|
+
error: '缺少分支名称参数'
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// 执行 git reset --hard origin/branch 命令
|
|
1237
|
+
await execGitCommand(`git reset --hard origin/${branch}`);
|
|
1238
|
+
res.json({ success: true });
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
console.error('重置到远程分支失败:', error);
|
|
1241
|
+
res.status(500).json({
|
|
1242
|
+
success: false,
|
|
1243
|
+
error: `重置到远程分支失败: ${error.message}`
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// 获取提交的文件列表
|
|
1249
|
+
app.get('/api/commit-files', async (req, res) => {
|
|
1250
|
+
try {
|
|
1251
|
+
const hash = req.query.hash;
|
|
1252
|
+
|
|
1253
|
+
if (!hash) {
|
|
1254
|
+
return res.status(400).json({
|
|
1255
|
+
success: false,
|
|
1256
|
+
error: '缺少提交哈希参数'
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
console.log(`获取提交文件列表: hash=${hash}`);
|
|
1261
|
+
|
|
1262
|
+
// 执行命令获取提交中修改的文件列表
|
|
1263
|
+
const { stdout } = await execGitCommand(`git show --name-only --format="" ${hash}`);
|
|
1264
|
+
|
|
1265
|
+
// 将输出按行分割,并过滤掉空行
|
|
1266
|
+
const files = stdout.split('\n').filter(line => line.trim());
|
|
1267
|
+
console.log(`找到${files.length}个文件:`, files);
|
|
1268
|
+
|
|
1269
|
+
res.json({
|
|
1270
|
+
success: true,
|
|
1271
|
+
files
|
|
1272
|
+
});
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
console.error('获取提交文件列表失败:', error);
|
|
1275
|
+
res.status(500).json({
|
|
1276
|
+
success: false,
|
|
1277
|
+
error: `获取提交文件列表失败: ${error.message}`
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
// 获取提交中特定文件的差异
|
|
1283
|
+
app.get('/api/commit-file-diff', async (req, res) => {
|
|
1284
|
+
try {
|
|
1285
|
+
const hash = req.query.hash;
|
|
1286
|
+
const filePath = req.query.file;
|
|
1287
|
+
|
|
1288
|
+
if (!hash || !filePath) {
|
|
1289
|
+
return res.status(400).json({
|
|
1290
|
+
success: false,
|
|
1291
|
+
error: '缺少必要参数'
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
console.log(`获取提交文件差异: hash=${hash}, file=${filePath}`);
|
|
1296
|
+
|
|
1297
|
+
// 执行命令获取文件差异,-p显示补丁,限定文件路径
|
|
1298
|
+
const { stdout } = await execGitCommand(`git show ${hash} -- "${filePath}"`);
|
|
1299
|
+
|
|
1300
|
+
console.log(`获取到差异内容,长度: ${stdout.length}`);
|
|
1301
|
+
// 如果差异内容太长,只打印前100个字符
|
|
1302
|
+
if (stdout.length > 100) {
|
|
1303
|
+
console.log(`差异内容预览: ${stdout.substring(0, 100)}...`);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
res.json({
|
|
1307
|
+
success: true,
|
|
1308
|
+
diff: stdout
|
|
1309
|
+
});
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
console.error('获取提交文件差异失败:', error);
|
|
1312
|
+
res.status(500).json({
|
|
1313
|
+
success: false,
|
|
1314
|
+
error: `获取提交文件差异失败: ${error.message}`
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
// 撤销某个提交 (revert)
|
|
1320
|
+
app.post('/api/revert-commit', async (req, res) => {
|
|
1321
|
+
try {
|
|
1322
|
+
const { hash } = req.body;
|
|
1323
|
+
|
|
1324
|
+
if (!hash) {
|
|
1325
|
+
return res.status(400).json({
|
|
1326
|
+
success: false,
|
|
1327
|
+
error: '缺少提交哈希参数'
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
console.log(`执行撤销提交操作: hash=${hash}`);
|
|
1332
|
+
|
|
1333
|
+
// 执行git revert命令
|
|
1334
|
+
await execGitCommand(`git revert --no-edit ${hash}`);
|
|
1335
|
+
|
|
1336
|
+
res.json({
|
|
1337
|
+
success: true,
|
|
1338
|
+
message: `已成功撤销提交 ${hash}`
|
|
1339
|
+
});
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
console.error('撤销提交失败:', error);
|
|
1342
|
+
res.status(500).json({
|
|
1343
|
+
success: false,
|
|
1344
|
+
error: `撤销提交失败: ${error.message}`
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// Cherry-pick某个提交
|
|
1350
|
+
app.post('/api/cherry-pick-commit', async (req, res) => {
|
|
1351
|
+
try {
|
|
1352
|
+
const { hash } = req.body;
|
|
1353
|
+
|
|
1354
|
+
if (!hash) {
|
|
1355
|
+
return res.status(400).json({
|
|
1356
|
+
success: false,
|
|
1357
|
+
error: '缺少提交哈希参数'
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
console.log(`执行Cherry-pick操作: hash=${hash}`);
|
|
1362
|
+
|
|
1363
|
+
// 执行git cherry-pick命令
|
|
1364
|
+
await execGitCommand(`git cherry-pick ${hash}`);
|
|
1365
|
+
|
|
1366
|
+
res.json({
|
|
1367
|
+
success: true,
|
|
1368
|
+
message: `已成功Cherry-pick提交 ${hash}`
|
|
1369
|
+
});
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
console.error('Cherry-pick提交失败:', error);
|
|
1372
|
+
res.status(500).json({
|
|
1373
|
+
success: false,
|
|
1374
|
+
error: `Cherry-pick提交失败: ${error.message}`
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// 重置到指定提交(hard)
|
|
1380
|
+
app.post('/api/reset-to-commit', async (req, res) => {
|
|
1381
|
+
try {
|
|
1382
|
+
const { hash } = req.body;
|
|
1383
|
+
|
|
1384
|
+
if (!hash) {
|
|
1385
|
+
return res.status(400).json({
|
|
1386
|
+
success: false,
|
|
1387
|
+
error: '缺少提交哈希参数'
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
console.log(`执行重置到指定提交操作: hash=${hash}`);
|
|
1392
|
+
|
|
1393
|
+
// 执行git reset --hard命令
|
|
1394
|
+
await execGitCommand(`git reset --hard ${hash}`);
|
|
1395
|
+
|
|
1396
|
+
res.json({
|
|
1397
|
+
success: true,
|
|
1398
|
+
message: `已成功重置到提交 ${hash}`
|
|
1399
|
+
});
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
console.error('重置到指定提交失败:', error);
|
|
1402
|
+
res.status(500).json({
|
|
1403
|
+
success: false,
|
|
1404
|
+
error: `重置到指定提交失败: ${error.message}`
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
// 添加清理Git锁定文件的接口
|
|
1410
|
+
app.post('/api/remove-lock', async (req, res) => {
|
|
1411
|
+
try {
|
|
1412
|
+
const gitDir = path.join(process.cwd(), '.git')
|
|
1413
|
+
const indexLockFile = path.join(gitDir, 'index.lock')
|
|
1414
|
+
|
|
1415
|
+
// 检查文件是否存在
|
|
1416
|
+
try {
|
|
1417
|
+
await fs.access(indexLockFile)
|
|
1418
|
+
// 如果文件存在,尝试删除它
|
|
1419
|
+
await fs.unlink(indexLockFile)
|
|
1420
|
+
res.json({ success: true, message: '已清理锁定文件' })
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
// 如果文件不存在,也返回成功
|
|
1423
|
+
if (error.code === 'ENOENT') {
|
|
1424
|
+
res.json({ success: true, message: '没有发现锁定文件' })
|
|
1425
|
+
} else {
|
|
1426
|
+
throw error
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
console.error('清理锁定文件失败:', error)
|
|
1431
|
+
res.status(500).json({
|
|
1432
|
+
success: false,
|
|
1433
|
+
error: `清理锁定文件失败: ${error.message}`
|
|
1434
|
+
})
|
|
1435
|
+
}
|
|
1436
|
+
})
|
|
1437
|
+
|
|
1438
|
+
// 清除Git用户配置
|
|
1439
|
+
app.post('/api/clear-user-config', async (req, res) => {
|
|
1440
|
+
try {
|
|
1441
|
+
// 检查全局配置是否存在,如果存在才删除
|
|
1442
|
+
try {
|
|
1443
|
+
const { stdout: userName } = await execGitCommand('git config --global user.name');
|
|
1444
|
+
if (userName.trim()) {
|
|
1445
|
+
await execGitCommand('git config --global --unset user.name');
|
|
1446
|
+
}
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
console.log('全局用户名配置检查失败,可能不存在:', error.message);
|
|
1449
|
+
// 忽略错误继续执行
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
try {
|
|
1453
|
+
const { stdout: userEmail } = await execGitCommand('git config --global user.email');
|
|
1454
|
+
if (userEmail.trim()) {
|
|
1455
|
+
await execGitCommand('git config --global --unset user.email');
|
|
1456
|
+
}
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
console.log('全局邮箱配置检查失败,可能不存在:', error.message);
|
|
1459
|
+
// 忽略错误继续执行
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
res.json({ success: true, message: '已清除全局Git用户配置' });
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
res.status(500).json({
|
|
1465
|
+
success: false,
|
|
1466
|
+
error: `清除全局Git用户配置失败: ${error.message}`
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
// 恢复Git用户配置
|
|
1472
|
+
app.post('/api/restore-user-config', async (req, res) => {
|
|
1473
|
+
try {
|
|
1474
|
+
const { name, email } = req.body;
|
|
1475
|
+
if (!name || !email) {
|
|
1476
|
+
return res.status(400).json({
|
|
1477
|
+
success: false,
|
|
1478
|
+
error: '需要提供用户名和邮箱'
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
await execGitCommand(`git config --global user.name "${name}"`);
|
|
1483
|
+
await execGitCommand(`git config --global user.email "${email}"`);
|
|
1484
|
+
res.json({ success: true, message: '已更新全局Git用户配置' });
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
res.status(500).json({
|
|
1487
|
+
success: false,
|
|
1488
|
+
error: `更新全局Git用户配置失败: ${error.message}`
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
// 获取远程仓库URL的API
|
|
1494
|
+
app.get('/api/remote-url', async (req, res) => {
|
|
1495
|
+
try {
|
|
1496
|
+
// 检查当前目录是否是Git仓库
|
|
1497
|
+
if (!isGitRepo) {
|
|
1498
|
+
return res.json({ success: false, error: '当前目录不是Git仓库' });
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// 执行git命令获取远程仓库URL
|
|
1502
|
+
const { stdout } = await execGitCommand('git config --get remote.origin.url');
|
|
1503
|
+
|
|
1504
|
+
// 返回远程仓库URL
|
|
1505
|
+
res.json({
|
|
1506
|
+
success: true,
|
|
1507
|
+
url: stdout.trim()
|
|
1508
|
+
});
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
console.error('获取远程仓库URL失败:', error);
|
|
1511
|
+
res.json({
|
|
1512
|
+
success: false,
|
|
1513
|
+
error: error.message || '获取远程仓库URL失败'
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
// 获取所有作者列表
|
|
1519
|
+
app.get('/api/authors', async (req, res) => {
|
|
1520
|
+
try {
|
|
1521
|
+
// 使用git命令获取所有提交者,不依赖Unix命令
|
|
1522
|
+
const { stdout } = await execGitCommand('git log --format="%an"');
|
|
1523
|
+
|
|
1524
|
+
// 将结果按行分割并过滤空行
|
|
1525
|
+
const lines = stdout.split('\n').filter(author => author.trim() !== '');
|
|
1526
|
+
|
|
1527
|
+
// 手动去重,不依赖Unix的uniq命令
|
|
1528
|
+
const uniqueAuthors = Array.from(new Set(lines)).sort();
|
|
1529
|
+
|
|
1530
|
+
// 控制台输出一下搜索示例,方便调试
|
|
1531
|
+
if (uniqueAuthors.length > 1) {
|
|
1532
|
+
const searchExample = uniqueAuthors.slice(0, 2).join('|');
|
|
1533
|
+
console.log(`多作者搜索示例: git log --author="${searchExample}"`);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
res.json({
|
|
1537
|
+
success: true,
|
|
1538
|
+
authors: uniqueAuthors
|
|
1539
|
+
});
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
console.error('获取作者列表失败:', error);
|
|
1542
|
+
res.status(500).json({
|
|
1543
|
+
success: false,
|
|
1544
|
+
error: '获取作者列表失败: ' + error.message
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
// Socket.io 实时更新
|
|
1550
|
+
io.on('connection', (socket) => {
|
|
1551
|
+
console.log('客户端已连接:', socket.id);
|
|
1552
|
+
|
|
1553
|
+
// 当客户端连接时,立即发送一次Git状态
|
|
1554
|
+
getAndBroadcastStatus();
|
|
1555
|
+
|
|
1556
|
+
// 客户端可以请求开始/停止监控
|
|
1557
|
+
socket.on('start_monitoring', () => {
|
|
1558
|
+
if (!watcher) {
|
|
1559
|
+
initFileSystemWatcher();
|
|
1560
|
+
socket.emit('monitoring_status', { active: true });
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
socket.on('stop_monitoring', () => {
|
|
1565
|
+
if (watcher) {
|
|
1566
|
+
watcher.close().catch(err => console.error('关闭监控器失败:', err));
|
|
1567
|
+
watcher = null;
|
|
1568
|
+
socket.emit('monitoring_status', { active: false });
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
// 客户端断开连接
|
|
1573
|
+
socket.on('disconnect', () => {
|
|
1574
|
+
console.log('客户端已断开连接:', socket.id);
|
|
1575
|
+
});
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
// 初始化文件系统监控
|
|
1579
|
+
function initFileSystemWatcher() {
|
|
1580
|
+
// 停止已有的监控器
|
|
1581
|
+
if (watcher) {
|
|
1582
|
+
watcher.close().catch(err => console.error('关闭旧监控器失败:', err));
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
try {
|
|
1586
|
+
// 获取当前工作目录
|
|
1587
|
+
const currentDir = process.cwd();
|
|
1588
|
+
|
|
1589
|
+
console.log(`初始化文件系统监控器,路径: ${currentDir}`);
|
|
1590
|
+
|
|
1591
|
+
// 检查是否是Git仓库
|
|
1592
|
+
if (!isGitRepo) {
|
|
1593
|
+
console.log('当前目录不是Git仓库,不启动监控');
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// 使用chokidar监控文件变动
|
|
1598
|
+
watcher = chokidar.watch(currentDir, {
|
|
1599
|
+
ignored: [
|
|
1600
|
+
/(^|[\/\\])\../, // 忽略.开头的文件和目录
|
|
1601
|
+
'**/node_modules/**', // 忽略node_modules
|
|
1602
|
+
'**/.git/**', // 忽略.git目录
|
|
1603
|
+
],
|
|
1604
|
+
persistent: true,
|
|
1605
|
+
ignoreInitial: true, // 忽略初始扫描时的文件
|
|
1606
|
+
awaitWriteFinish: {
|
|
1607
|
+
stabilityThreshold: 300, // 等待文件写入完成的时间
|
|
1608
|
+
pollInterval: 100 // 轮询间隔
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// 合并所有变动事件到一个处理程序
|
|
1613
|
+
const events = ['add', 'change', 'unlink'];
|
|
1614
|
+
events.forEach(event => {
|
|
1615
|
+
watcher.on(event, path => {
|
|
1616
|
+
console.log(`检测到文件变动 [${event}]: ${path}`);
|
|
1617
|
+
debouncedNotifyChanges();
|
|
1618
|
+
});
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
watcher.on('error', error => {
|
|
1622
|
+
console.error('文件监控错误:', error);
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
console.log('文件系统监控器已启动');
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
console.error('启动文件监控失败:', error);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// 获取并广播Git状态
|
|
1632
|
+
async function getAndBroadcastStatus() {
|
|
1633
|
+
try {
|
|
1634
|
+
// 如果不是Git仓库,发送特殊状态
|
|
1635
|
+
if (!isGitRepo) {
|
|
1636
|
+
io.emit('git_status_update', {
|
|
1637
|
+
isGitRepo: false,
|
|
1638
|
+
status: '当前目录不是Git仓库',
|
|
1639
|
+
porcelain: '',
|
|
1640
|
+
timestamp: new Date().toISOString()
|
|
1641
|
+
});
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// 获取常规状态
|
|
1646
|
+
const { stdout: statusOutput } = await execGitCommand('git status');
|
|
1647
|
+
|
|
1648
|
+
// 获取porcelain格式状态
|
|
1649
|
+
const { stdout: porcelainOutput } = await execGitCommand('git status --porcelain --untracked-files=all');
|
|
1650
|
+
|
|
1651
|
+
// 广播到所有连接的客户端
|
|
1652
|
+
io.emit('git_status_update', {
|
|
1653
|
+
isGitRepo: true,
|
|
1654
|
+
status: statusOutput,
|
|
1655
|
+
porcelain: porcelainOutput,
|
|
1656
|
+
timestamp: new Date().toISOString()
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
console.log('已广播Git状态更新');
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
console.error('获取或广播Git状态失败:', error);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// 防抖处理函数
|
|
1666
|
+
function debouncedNotifyChanges() {
|
|
1667
|
+
if (debounceTimer) {
|
|
1668
|
+
clearTimeout(debounceTimer);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
debounceTimer = setTimeout(() => {
|
|
1672
|
+
getAndBroadcastStatus();
|
|
1673
|
+
}, DEBOUNCE_DELAY);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// 检查当前目录是否是Git仓库
|
|
1677
|
+
let isGitRepo = false;
|
|
1678
|
+
try {
|
|
1679
|
+
const { stdout } = await execGitCommand('git rev-parse --is-inside-work-tree', { log: false });
|
|
1680
|
+
isGitRepo = stdout.trim() === 'true';
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
isGitRepo = false;
|
|
1683
|
+
console.log(chalk.yellow('======================================'));
|
|
1684
|
+
console.log(chalk.yellow(` 提示: 当前目录不是Git仓库`));
|
|
1685
|
+
console.log(chalk.yellow(` 目录: ${process.cwd()}`));
|
|
1686
|
+
console.log(chalk.yellow('======================================'));
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// 启动服务器
|
|
1690
|
+
const PORT = 3000;
|
|
1691
|
+
httpServer.listen(PORT, () => {
|
|
1692
|
+
console.log(chalk.green('======================================'));
|
|
1693
|
+
console.log(chalk.green(` Zen GitSync 服务器已启动`));
|
|
1694
|
+
console.log(chalk.green(` 访问地址: http://localhost:${PORT}`));
|
|
1695
|
+
console.log(chalk.green(` 启动时间: ${new Date().toLocaleString()}`));
|
|
1696
|
+
if (isGitRepo) {
|
|
1697
|
+
console.log(chalk.green(` 当前目录是Git仓库,文件监控已启动`));
|
|
1698
|
+
// 启动文件监控
|
|
1699
|
+
initFileSystemWatcher();
|
|
1700
|
+
} else {
|
|
1701
|
+
console.log(chalk.yellow(` 当前目录不是Git仓库,文件监控未启动`));
|
|
1702
|
+
}
|
|
1703
|
+
console.log(chalk.green('======================================'));
|
|
1704
|
+
|
|
1705
|
+
open(`http://localhost:${PORT}`);
|
|
1706
|
+
}).on('error', async (err) => {
|
|
1707
|
+
if (err.code === 'EADDRINUSE') {
|
|
1708
|
+
console.log(`端口 ${PORT} 被占用,尝试其他端口...`);
|
|
1709
|
+
let newPort = PORT + 1;
|
|
1710
|
+
while (newPort < PORT + 100) {
|
|
1711
|
+
try {
|
|
1712
|
+
await new Promise((resolve, reject) => {
|
|
1713
|
+
httpServer.listen(newPort, () => {
|
|
1714
|
+
console.log(chalk.green('======================================'));
|
|
1715
|
+
console.log(chalk.green(` Zen GitSync 服务器已启动`));
|
|
1716
|
+
console.log(chalk.green(` 访问地址: http://localhost:${newPort}`));
|
|
1717
|
+
console.log(chalk.green(` 启动时间: ${new Date().toLocaleString()}`));
|
|
1718
|
+
console.log(chalk.green('======================================'));
|
|
1719
|
+
resolve();
|
|
1720
|
+
});
|
|
1721
|
+
});
|
|
1722
|
+
break;
|
|
1723
|
+
} catch (error) {
|
|
1724
|
+
newPort++;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
} else {
|
|
1728
|
+
console.error('启动服务器失败:', err);
|
|
1729
|
+
process.exit(1);
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1670
1734
|
export default startUIServer;
|