ai-git-tools 2.0.25 → 2.0.26
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/commands/commit-all.js +59 -44
- package/src/commands/commit.js +3 -3
- package/src/commands/pr.js +1 -1
- package/src/core/ai-client.js +18 -6
- package/src/pr-modules/ai/code-analyzer.js +29 -10
- package/src/pr-modules/core/git-operations.js +20 -11
- package/src/pr-modules/core/workflow.js +30 -5
- package/src/pr-modules/ui/interactive-select.js +1 -0
package/package.json
CHANGED
|
@@ -247,56 +247,71 @@ async function commitGroup(group, files, config) {
|
|
|
247
247
|
// 忽略錯誤(可能沒有 staged 的檔案)
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
// Add
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
250
|
+
// Add 這組的檔案(使用 try-finally 確保失敗時清理)
|
|
251
|
+
const addedFiles = [];
|
|
252
|
+
try {
|
|
253
|
+
for (const file of files) {
|
|
254
|
+
const fileStatus = file.isNew ? '新增' : file.isDeleted ? '刪除' : '修改';
|
|
255
|
+
console.log(` ├─ [${fileStatus}] ${file.filePath}`);
|
|
256
|
+
try {
|
|
257
|
+
// 使用 JSON.stringify 來正確處理包含空格或特殊字符的檔案路徑
|
|
258
|
+
// git add 對於刪除的檔案也能正確處理
|
|
259
|
+
execSync(`git add ${JSON.stringify(file.filePath)}`, { encoding: 'utf-8' });
|
|
260
|
+
addedFiles.push(file.filePath);
|
|
261
|
+
} catch (addError) {
|
|
262
|
+
console.error(` ⚠️ 無法加入檔案: ${file.filePath}`, addError.message);
|
|
263
|
+
throw addError;
|
|
264
|
+
}
|
|
261
265
|
}
|
|
262
|
-
}
|
|
263
266
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
// 生成 commit message
|
|
268
|
+
console.log(` └─ 生成 commit message...`);
|
|
269
|
+
const commitMessage = await generateCommitMessage(group, files, config);
|
|
267
270
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
console.log(`\n 📝 Commit Message:`);
|
|
274
|
-
console.log(` ${'─'.repeat(50)}`);
|
|
275
|
-
commitMessage.split('\n').forEach((line) => {
|
|
276
|
-
console.log(` ${line}`);
|
|
277
|
-
});
|
|
278
|
-
console.log(` ${'─'.repeat(50)}`);
|
|
271
|
+
if (!commitMessage) {
|
|
272
|
+
console.log(` ❌ 無法生成 commit message,跳過此群組`);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
279
275
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
writeFileSync(tmpFile, commitMessage, 'utf-8');
|
|
285
|
-
execSync(`git commit -F ${tmpFile}`, {
|
|
286
|
-
stdio: 'inherit',
|
|
276
|
+
console.log(`\n 📝 Commit Message:`);
|
|
277
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
278
|
+
commitMessage.split('\n').forEach((line) => {
|
|
279
|
+
console.log(` ${line}`);
|
|
287
280
|
});
|
|
288
|
-
|
|
289
|
-
|
|
281
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
282
|
+
|
|
283
|
+
// 執行 commit
|
|
284
|
+
// 使用暫存檔案避免 commit message 中的特殊字元問題
|
|
285
|
+
const tmpFile = '.git/COMMIT_EDITMSG_TMP';
|
|
290
286
|
try {
|
|
287
|
+
writeFileSync(tmpFile, commitMessage, 'utf-8');
|
|
288
|
+
execSync(`git commit -F ${tmpFile}`, {
|
|
289
|
+
stdio: 'inherit',
|
|
290
|
+
});
|
|
291
291
|
unlinkSync(tmpFile);
|
|
292
|
-
} catch (
|
|
293
|
-
|
|
292
|
+
} catch (commitError) {
|
|
293
|
+
try {
|
|
294
|
+
unlinkSync(tmpFile);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
// 忽略刪除暫存檔案的錯誤
|
|
297
|
+
}
|
|
298
|
+
throw commitError;
|
|
294
299
|
}
|
|
295
|
-
throw commitError;
|
|
296
|
-
}
|
|
297
300
|
|
|
298
|
-
|
|
299
|
-
|
|
301
|
+
console.log(` ✅ Commit 完成!`);
|
|
302
|
+
return true;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
// 如果失敗,unstage 所有已經 add 的檔案
|
|
305
|
+
if (addedFiles.length > 0) {
|
|
306
|
+
console.log(` 🔄 清理已 staged 的檔案...`);
|
|
307
|
+
try {
|
|
308
|
+
execSync('git reset HEAD -- .', { stdio: 'ignore' });
|
|
309
|
+
} catch (e) {
|
|
310
|
+
// 忽略 reset 錯誤
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
300
315
|
} catch (error) {
|
|
301
316
|
console.error(` ❌ Commit 失敗:`, error.message);
|
|
302
317
|
return false;
|
|
@@ -329,7 +344,7 @@ export async function commitAllCommand() {
|
|
|
329
344
|
|
|
330
345
|
if (changes.length === 0) {
|
|
331
346
|
logger.info('沒有需要提交的變更');
|
|
332
|
-
|
|
347
|
+
return;
|
|
333
348
|
}
|
|
334
349
|
|
|
335
350
|
console.log(`📊 找到 ${changes.length} 個變更的檔案:\n`);
|
|
@@ -346,7 +361,7 @@ export async function commitAllCommand() {
|
|
|
346
361
|
|
|
347
362
|
if (!groups || groups.length === 0) {
|
|
348
363
|
logger.error('AI 分析失敗或沒有產生分組');
|
|
349
|
-
|
|
364
|
+
throw new Error('AI 分析失敗或沒有產生分組');
|
|
350
365
|
}
|
|
351
366
|
|
|
352
367
|
// 驗證所有檔案都被包含在分組中
|
|
@@ -441,6 +456,6 @@ export async function commitAllCommand() {
|
|
|
441
456
|
}
|
|
442
457
|
} catch (error) {
|
|
443
458
|
handleError(error);
|
|
444
|
-
|
|
459
|
+
throw error;
|
|
445
460
|
}
|
|
446
461
|
}
|
package/src/commands/commit.js
CHANGED
|
@@ -31,7 +31,7 @@ export async function commitCommand() {
|
|
|
31
31
|
if (!diff.trim()) {
|
|
32
32
|
logger.error('沒有 staged 的變更');
|
|
33
33
|
console.log('💡 請先使用 git add 來 stage 你的變更');
|
|
34
|
-
|
|
34
|
+
throw new Error('沒有 staged 的變更');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
logger.step('正在分析變更內容...\n');
|
|
@@ -115,7 +115,7 @@ ${truncatedDiff}`;
|
|
|
115
115
|
console.log(' 1. 檢查網路連線');
|
|
116
116
|
console.log(' 2. 嘗試更換 AI 模型(使用 --model 參數)');
|
|
117
117
|
console.log(' 3. 確認變更內容不會太複雜或太大');
|
|
118
|
-
|
|
118
|
+
throw new Error('無法產生有效的 commit message');
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
logger.success('產生的 Commit Message:');
|
|
@@ -132,6 +132,6 @@ ${truncatedDiff}`;
|
|
|
132
132
|
logger.success('Commit 完成!\n');
|
|
133
133
|
} catch (error) {
|
|
134
134
|
handleError(error);
|
|
135
|
-
|
|
135
|
+
throw error;
|
|
136
136
|
}
|
|
137
137
|
}
|
package/src/commands/pr.js
CHANGED
package/src/core/ai-client.js
CHANGED
|
@@ -7,17 +7,23 @@ import { CopilotClient } from '@github/copilot-sdk';
|
|
|
7
7
|
|
|
8
8
|
export class AIClient {
|
|
9
9
|
/**
|
|
10
|
-
* 發送 prompt
|
|
10
|
+
* 發送 prompt 並等待回應(帶重試機制和超時保護)
|
|
11
11
|
*/
|
|
12
|
-
static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3) {
|
|
13
|
-
const client = new CopilotClient();
|
|
12
|
+
static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3, timeout = 60000) {
|
|
14
13
|
let lastError = null;
|
|
15
14
|
|
|
16
15
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
16
|
+
const client = new CopilotClient();
|
|
17
17
|
try {
|
|
18
18
|
const session = await client.createSession({ model });
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
|
|
20
|
+
// 使用 Promise.race 實現超時控制
|
|
21
|
+
const responsePromise = session.sendAndWait({ prompt });
|
|
22
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
23
|
+
setTimeout(() => reject(new Error(`AI 請求超時 (${timeout}ms)`)), timeout);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
21
27
|
|
|
22
28
|
const content = response?.data?.content || '';
|
|
23
29
|
return content.trim();
|
|
@@ -26,7 +32,13 @@ export class AIClient {
|
|
|
26
32
|
if (attempt < maxRetries) {
|
|
27
33
|
console.log(`⚠️ AI 請求失敗,重試第 ${attempt}/${maxRetries} 次...`);
|
|
28
34
|
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
|
29
|
-
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
// 確保每次都關閉 client,無論成功或失敗
|
|
38
|
+
try {
|
|
39
|
+
await client.stop();
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// 忽略關閉錯誤
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
44
|
}
|
|
@@ -29,19 +29,27 @@ export class AIAnalyzer {
|
|
|
29
29
|
const { client, session } = await this.createClient();
|
|
30
30
|
|
|
31
31
|
try {
|
|
32
|
-
|
|
33
|
-
const
|
|
32
|
+
// 使用超時保護 (60 秒)
|
|
33
|
+
const responsePromise = session.sendAndWait({ prompt });
|
|
34
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
35
|
+
setTimeout(() => reject(new Error('AI 請求超時 (60 秒)')), 60000);
|
|
36
|
+
});
|
|
34
37
|
|
|
35
|
-
await
|
|
38
|
+
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
39
|
+
const prContent = response?.data.content?.trim() || '';
|
|
36
40
|
|
|
37
41
|
if (!prContent) {
|
|
38
42
|
throw new Error('AI 未能生成 PR 內容');
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
return this.parsePRContent(prContent);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
} finally {
|
|
47
|
+
// 確保 client 一定會被關閉
|
|
48
|
+
try {
|
|
49
|
+
await client.stop();
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// 忽略關閉錯誤
|
|
52
|
+
}
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
|
|
@@ -56,10 +64,15 @@ export class AIAnalyzer {
|
|
|
56
64
|
|
|
57
65
|
try {
|
|
58
66
|
log.info(' 正在使用 AI 深度分析程式碼變更...');
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
|
|
68
|
+
// 使用超時保護 (60 秒)
|
|
69
|
+
const responsePromise = session.sendAndWait({ prompt });
|
|
70
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
71
|
+
setTimeout(() => reject(new Error('AI 請求超時 (60 秒)')), 60000);
|
|
72
|
+
});
|
|
61
73
|
|
|
62
|
-
await
|
|
74
|
+
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
75
|
+
const content = response?.data.content?.trim() || '';
|
|
63
76
|
|
|
64
77
|
// 解析 JSON
|
|
65
78
|
try {
|
|
@@ -85,9 +98,15 @@ export class AIAnalyzer {
|
|
|
85
98
|
return this.getFallbackAnalysis(changedFiles);
|
|
86
99
|
}
|
|
87
100
|
} catch (error) {
|
|
88
|
-
await client.stop();
|
|
89
101
|
log.warning(` AI 分析失敗 (${error.message}),使用基礎分析...\n`);
|
|
90
102
|
return this.getFallbackAnalysis(changedFiles);
|
|
103
|
+
} finally {
|
|
104
|
+
// 確保 client 一定會被關閉
|
|
105
|
+
try {
|
|
106
|
+
await client.stop();
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// 忽略關閉錯誤
|
|
109
|
+
}
|
|
91
110
|
}
|
|
92
111
|
}
|
|
93
112
|
|
|
@@ -49,14 +49,16 @@ export class GitOperations {
|
|
|
49
49
|
/**
|
|
50
50
|
* 獲取變更統計
|
|
51
51
|
*/
|
|
52
|
-
getChangeStats(baseBranch, headBranch) {
|
|
52
|
+
getChangeStats(baseBranch, headBranch, useRemoteHead = false) {
|
|
53
53
|
try {
|
|
54
|
-
|
|
54
|
+
// 使用本地 headBranch 以確保能檢測到未推送的變更
|
|
55
|
+
const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
|
|
56
|
+
const stats = execSync(`git diff --shortstat origin/${baseBranch}...${headRef}`, {
|
|
55
57
|
encoding: 'utf-8',
|
|
56
58
|
}).trim();
|
|
57
59
|
|
|
58
60
|
const filesChanged = execSync(
|
|
59
|
-
`git diff --name-only origin/${baseBranch}...${
|
|
61
|
+
`git diff --name-only origin/${baseBranch}...${headRef} | wc -l`,
|
|
60
62
|
{ encoding: 'utf-8' }
|
|
61
63
|
).trim();
|
|
62
64
|
|
|
@@ -69,9 +71,11 @@ export class GitOperations {
|
|
|
69
71
|
/**
|
|
70
72
|
* 獲取變更的檔案列表
|
|
71
73
|
*/
|
|
72
|
-
getChangedFiles(baseBranch, headBranch) {
|
|
74
|
+
getChangedFiles(baseBranch, headBranch, useRemoteHead = false) {
|
|
73
75
|
try {
|
|
74
|
-
|
|
76
|
+
// 使用本地 headBranch 以確保能檢測到未推送的變更
|
|
77
|
+
const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
|
|
78
|
+
const files = execSync(`git diff --name-only origin/${baseBranch}...${headRef}`, {
|
|
75
79
|
encoding: 'utf-8',
|
|
76
80
|
})
|
|
77
81
|
.split('\n')
|
|
@@ -86,9 +90,11 @@ export class GitOperations {
|
|
|
86
90
|
* 獲取 commit 列表
|
|
87
91
|
*/
|
|
88
92
|
getCommits(baseBranch, headBranch, options = {}) {
|
|
89
|
-
const { oneline = true, noDecorate = true } = options;
|
|
93
|
+
const { oneline = true, noDecorate = true, useRemoteHead = false } = options;
|
|
90
94
|
try {
|
|
91
|
-
|
|
95
|
+
// 使用本地 headBranch 以確保能檢測到未推送的 commit
|
|
96
|
+
const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
|
|
97
|
+
let cmd = `git log origin/${baseBranch}..${headRef}`;
|
|
92
98
|
if (oneline) cmd += ' --oneline';
|
|
93
99
|
if (noDecorate) cmd += ' --no-decorate';
|
|
94
100
|
|
|
@@ -98,7 +104,7 @@ export class GitOperations {
|
|
|
98
104
|
'無法比較分支差異',
|
|
99
105
|
'GIT_COMPARE_FAILED',
|
|
100
106
|
['檢查遠端分支是否存在: git branch -r', '執行診斷: npm run diagnose:pr'],
|
|
101
|
-
`git log origin/${baseBranch}
|
|
107
|
+
`git log origin/${baseBranch}..${headBranch}`
|
|
102
108
|
);
|
|
103
109
|
}
|
|
104
110
|
}
|
|
@@ -106,15 +112,18 @@ export class GitOperations {
|
|
|
106
112
|
/**
|
|
107
113
|
* 獲取 diff
|
|
108
114
|
*/
|
|
109
|
-
getDiff(baseBranch, headBranch, maxBuffer = CONSTANTS.MAX_BUFFER_SIZE) {
|
|
115
|
+
getDiff(baseBranch, headBranch, useRemoteHead = false, maxBuffer = CONSTANTS.MAX_BUFFER_SIZE) {
|
|
110
116
|
try {
|
|
111
|
-
|
|
117
|
+
// 使用本地 headBranch 以確保能檢測到未推送的變更
|
|
118
|
+
const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
|
|
119
|
+
return execSync(`git diff origin/${baseBranch}...${headRef}`, {
|
|
112
120
|
encoding: 'utf-8',
|
|
113
121
|
maxBuffer,
|
|
114
122
|
});
|
|
115
123
|
} catch (error) {
|
|
116
124
|
// 嘗試替代方案
|
|
117
|
-
|
|
125
|
+
const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
|
|
126
|
+
return execSync(`git diff origin/${baseBranch}..${headRef}`, {
|
|
118
127
|
encoding: 'utf-8',
|
|
119
128
|
maxBuffer,
|
|
120
129
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createInterface } from 'readline';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
2
3
|
import { GitOperations } from './git-operations.js';
|
|
3
4
|
import { GitHubAPI } from './github-api.js';
|
|
4
5
|
import { AIAnalyzer } from '../ai/code-analyzer.js';
|
|
@@ -202,12 +203,35 @@ export class PRWorkflow {
|
|
|
202
203
|
await this.git.fetch();
|
|
203
204
|
log.success('同步完成\n');
|
|
204
205
|
|
|
205
|
-
//
|
|
206
|
+
// 顯示分支狀態診斷信息
|
|
207
|
+
if (this.config.output?.verbose) {
|
|
208
|
+
console.log(`${colors.cyan}🔍 分支診斷信息:${colors.reset}`);
|
|
209
|
+
try {
|
|
210
|
+
const currentBranch = this.git.getCurrentBranch();
|
|
211
|
+
console.log(` 當前分支: ${currentBranch}`);
|
|
212
|
+
console.log(` Base 分支: origin/${baseBranch}`);
|
|
213
|
+
console.log(` Head 分支: ${headBranch} (本地)`);
|
|
214
|
+
|
|
215
|
+
// 檢查遠端分支是否存在
|
|
216
|
+
try {
|
|
217
|
+
execSync(`git rev-parse --verify origin/${headBranch}`, { stdio: 'pipe' });
|
|
218
|
+
console.log(` 遠端 ${headBranch}: ✓ 存在`);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.log(` 遠端 ${headBranch}: ✗ 不存在(尚未推送)`);
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
// 忽略診斷錯誤
|
|
224
|
+
}
|
|
225
|
+
console.log('');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 檢查本地 commit 差異(使用本地 headBranch)
|
|
206
229
|
let localCommits;
|
|
207
230
|
try {
|
|
208
231
|
localCommits = this.git.getCommits(baseBranch, headBranch, {
|
|
209
232
|
oneline: true,
|
|
210
233
|
noDecorate: true,
|
|
234
|
+
useRemoteHead: false, // 使用本地分支以檢測未推送的commit
|
|
211
235
|
});
|
|
212
236
|
} catch (error) {
|
|
213
237
|
throw new PRError(
|
|
@@ -261,10 +285,11 @@ export class PRWorkflow {
|
|
|
261
285
|
* 收集變更資料
|
|
262
286
|
*/
|
|
263
287
|
collectChangeData(baseBranch, headBranch) {
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
const
|
|
288
|
+
// 此時已經推送完成,使用本地分支即可(本地和遠端應該已同步)
|
|
289
|
+
const stats = this.git.getChangeStats(baseBranch, headBranch, false);
|
|
290
|
+
const changedFiles = this.git.getChangedFiles(baseBranch, headBranch, false);
|
|
291
|
+
const commits = this.git.getCommits(baseBranch, headBranch, { useRemoteHead: false });
|
|
292
|
+
const diff = this.git.getDiff(baseBranch, headBranch, false);
|
|
268
293
|
const truncatedDiff = this.git.truncateDiff(diff);
|
|
269
294
|
|
|
270
295
|
console.log(`📈 變更統計: ${stats.stats}`);
|