ai-git-tools 2.0.78 → 2.0.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/cli.js +39 -61
- package/package.json +1 -1
- package/src/ai/prompts/commit-message.js +42 -0
- package/src/ai/prompts/pr-content.js +151 -0
- package/src/commands/commit-all.js +23 -12
- package/src/commands/commit.js +92 -103
- package/src/commands/pr.js +22 -31
- package/src/commands/usage.js +557 -0
- package/src/core/ai-client.js +33 -6
- package/src/core/config-loader.js +151 -113
- package/src/core/git-operations.js +122 -39
- package/src/pr-modules/ai/code-analyzer.js +19 -141
- package/src/pr-modules/ai/label-analyzer.js +1 -1
- package/src/pr-modules/core/github-api.js +56 -5
- package/src/pr-modules/core/workflow.js +25 -5
- package/src/pr-modules/reviewers/reviewer-selector.js +2 -2
- package/src/pr-modules/ui/interactive-select.js +1 -1
- package/src/utils/cli-helpers.js +51 -0
- package/src/utils/constants.js +49 -0
- package/src/utils/helpers.js +74 -3
- package/src/utils/logger.js +13 -11
- package/src/{pr-modules/utils/constants.js → utils/project-skills.js} +26 -49
- package/src/pr-modules/core/config-loader.js +0 -132
- package/src/pr-modules/core/git-operations.js +0 -248
- package/src/pr-modules/ui/logger.js +0 -40
- package/src/pr-modules/utils/helpers.js +0 -75
package/src/core/ai-client.js
CHANGED
|
@@ -6,6 +6,26 @@
|
|
|
6
6
|
import { CopilotClient, approveAll } from '@github/copilot-sdk';
|
|
7
7
|
|
|
8
8
|
export class AIClient {
|
|
9
|
+
/**
|
|
10
|
+
* 檢測是否為 Copilot 授權相關的錯誤
|
|
11
|
+
*/
|
|
12
|
+
static isCopilotAuthError(error) {
|
|
13
|
+
const message = error?.message || error?.toString() || '';
|
|
14
|
+
const lowerMessage = message.toLowerCase();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
lowerMessage.includes('permission') ||
|
|
18
|
+
lowerMessage.includes('unauthorized') ||
|
|
19
|
+
lowerMessage.includes('forbidden') ||
|
|
20
|
+
lowerMessage.includes('not authorized') ||
|
|
21
|
+
lowerMessage.includes('authentication failed') ||
|
|
22
|
+
lowerMessage.includes('access denied') ||
|
|
23
|
+
lowerMessage.includes('you do not have access') ||
|
|
24
|
+
lowerMessage.includes('copilot') ||
|
|
25
|
+
message.includes('EACCES')
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
/**
|
|
10
30
|
* 發送 prompt 並等待回應(帶重試機制和超時保護)
|
|
11
31
|
*/
|
|
@@ -15,11 +35,11 @@ export class AIClient {
|
|
|
15
35
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
16
36
|
const client = new CopilotClient();
|
|
17
37
|
try {
|
|
18
|
-
const session = await client.createSession({
|
|
38
|
+
const session = await client.createSession({
|
|
19
39
|
model,
|
|
20
|
-
onPermissionRequest: approveAll
|
|
40
|
+
onPermissionRequest: approveAll,
|
|
21
41
|
});
|
|
22
|
-
|
|
42
|
+
|
|
23
43
|
// 使用 Promise.race 實現超時控制
|
|
24
44
|
const responsePromise = session.sendAndWait({ prompt });
|
|
25
45
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -34,7 +54,7 @@ export class AIClient {
|
|
|
34
54
|
lastError = error;
|
|
35
55
|
if (attempt < maxRetries) {
|
|
36
56
|
console.log(`⚠️ AI 請求失敗,重試第 ${attempt}/${maxRetries} 次...`);
|
|
37
|
-
await new Promise(
|
|
57
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
38
58
|
}
|
|
39
59
|
} finally {
|
|
40
60
|
// 確保每次都關閉 client,無論成功或失敗
|
|
@@ -46,7 +66,11 @@ export class AIClient {
|
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
|
|
69
|
+
// 區分不同類型的錯誤
|
|
70
|
+
const errorObj = new Error(`AI 請求失敗: ${lastError?.message || '未知錯誤'}`);
|
|
71
|
+
errorObj.originalError = lastError;
|
|
72
|
+
errorObj.isCopilotAuth = AIClient.isCopilotAuthError(lastError);
|
|
73
|
+
throw errorObj;
|
|
50
74
|
}
|
|
51
75
|
|
|
52
76
|
/**
|
|
@@ -54,7 +78,10 @@ export class AIClient {
|
|
|
54
78
|
*/
|
|
55
79
|
static parseJSON(content) {
|
|
56
80
|
// 移除可能的 markdown code block 標記
|
|
57
|
-
const jsonContent = content
|
|
81
|
+
const jsonContent = content
|
|
82
|
+
.replace(/```json\n?/g, '')
|
|
83
|
+
.replace(/```\n?/g, '')
|
|
84
|
+
.trim();
|
|
58
85
|
|
|
59
86
|
try {
|
|
60
87
|
return JSON.parse(jsonContent);
|
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 配置載入器
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 統一 commit 與 PR 命令的配置載入
|
|
4
|
+
* 支援從目前工作目錄下的 .ai-git-config.mjs 載入配置
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync } from 'fs';
|
|
8
8
|
import { resolve } from 'path';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* 內建預設配置
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CONFIG = {
|
|
14
|
+
ai: {
|
|
15
|
+
model: 'gpt-4.1',
|
|
16
|
+
maxDiffLength: 8000,
|
|
17
|
+
maxRetries: 3,
|
|
18
|
+
},
|
|
19
|
+
github: {
|
|
20
|
+
defaultBase: 'release',
|
|
21
|
+
autoLabels: true,
|
|
22
|
+
includeImpactAnalysis: false,
|
|
23
|
+
},
|
|
24
|
+
reviewers: {
|
|
25
|
+
interactiveReviewers: true,
|
|
26
|
+
maxSuggested: 5,
|
|
27
|
+
gitHistoryDepth: 20,
|
|
28
|
+
excludeAuthors: [],
|
|
29
|
+
},
|
|
30
|
+
output: {
|
|
31
|
+
verbose: false,
|
|
32
|
+
saveHistory: false,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
10
36
|
/**
|
|
11
37
|
* 解析命令行參數
|
|
12
38
|
*/
|
|
@@ -17,6 +43,13 @@ export function parseCliArgs() {
|
|
|
17
43
|
verbose: false,
|
|
18
44
|
maxDiffLength: null,
|
|
19
45
|
maxRetries: null,
|
|
46
|
+
baseBranch: null,
|
|
47
|
+
headBranch: null,
|
|
48
|
+
preview: false,
|
|
49
|
+
noConfirm: false,
|
|
50
|
+
interactiveReviewers: undefined,
|
|
51
|
+
autoLabels: null,
|
|
52
|
+
includeImpactAnalysis: null,
|
|
20
53
|
};
|
|
21
54
|
|
|
22
55
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -34,91 +67,11 @@ export function parseCliArgs() {
|
|
|
34
67
|
case '--max-retries':
|
|
35
68
|
config.maxRetries = parseInt(args[++i], 10);
|
|
36
69
|
break;
|
|
37
|
-
default:
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return config;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 載入配置(commit 工具使用)
|
|
47
|
-
* 支援從目前工作目錄或使用者專案目錄載入 .ai-git-config.mjs
|
|
48
|
-
*/
|
|
49
|
-
export async function loadCommitConfig() {
|
|
50
|
-
// 內建預設值
|
|
51
|
-
const defaults = {
|
|
52
|
-
ai: {
|
|
53
|
-
model: 'gpt-4.1',
|
|
54
|
-
maxDiffLength: 8000,
|
|
55
|
-
maxRetries: 3,
|
|
56
|
-
},
|
|
57
|
-
output: {
|
|
58
|
-
verbose: false,
|
|
59
|
-
saveHistory: false,
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// 嘗試從目前工作目錄載入配置檔案
|
|
64
|
-
const configPath = resolve(process.cwd(), '.ai-git-config.mjs');
|
|
65
|
-
let userConfig = {};
|
|
66
|
-
|
|
67
|
-
if (existsSync(configPath)) {
|
|
68
|
-
try {
|
|
69
|
-
const imported = await import(`file://${configPath}`);
|
|
70
|
-
userConfig = imported.default || {};
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.warn(`⚠️ 載入配置檔案失敗: ${error.message}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 合併配置
|
|
77
|
-
const config = {
|
|
78
|
-
ai: {
|
|
79
|
-
model: userConfig.ai?.model ?? defaults.ai.model,
|
|
80
|
-
maxDiffLength: userConfig.ai?.maxDiffLength ?? defaults.ai.maxDiffLength,
|
|
81
|
-
maxRetries: userConfig.ai?.maxRetries ?? defaults.ai.maxRetries,
|
|
82
|
-
},
|
|
83
|
-
output: {
|
|
84
|
-
verbose: userConfig.output?.verbose ?? defaults.output.verbose,
|
|
85
|
-
saveHistory: userConfig.output?.saveHistory ?? defaults.output.saveHistory,
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// 命令行參數優先
|
|
90
|
-
const cliConfig = parseCliArgs();
|
|
91
|
-
if (cliConfig.model) config.ai.model = cliConfig.model;
|
|
92
|
-
if (cliConfig.verbose) config.output.verbose = cliConfig.verbose;
|
|
93
|
-
if (cliConfig.maxDiffLength) config.ai.maxDiffLength = cliConfig.maxDiffLength;
|
|
94
|
-
if (cliConfig.maxRetries) config.ai.maxRetries = cliConfig.maxRetries;
|
|
95
|
-
|
|
96
|
-
return config;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 解析 PR 命令行參數
|
|
101
|
-
*/
|
|
102
|
-
export function parsePRCliArgs() {
|
|
103
|
-
const args = process.argv.slice(2);
|
|
104
|
-
const config = {
|
|
105
|
-
baseBranch: null,
|
|
106
|
-
headBranch: null,
|
|
107
|
-
model: null,
|
|
108
|
-
draft: false,
|
|
109
|
-
preview: false,
|
|
110
|
-
noConfirm: false,
|
|
111
|
-
autoReviewers: false,
|
|
112
|
-
autoLabels: null,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
for (let i = 0; i < args.length; i++) {
|
|
116
|
-
switch (args[i]) {
|
|
117
70
|
case '--base':
|
|
118
71
|
config.baseBranch = args[++i];
|
|
119
72
|
break;
|
|
120
|
-
case '--
|
|
121
|
-
config.
|
|
73
|
+
case '--head':
|
|
74
|
+
config.headBranch = args[++i];
|
|
122
75
|
break;
|
|
123
76
|
case '--preview':
|
|
124
77
|
config.preview = true;
|
|
@@ -126,9 +79,19 @@ export function parsePRCliArgs() {
|
|
|
126
79
|
case '--no-confirm':
|
|
127
80
|
config.noConfirm = true;
|
|
128
81
|
break;
|
|
82
|
+
case '--interactive-reviewers':
|
|
83
|
+
config.interactiveReviewers = true;
|
|
84
|
+
break;
|
|
129
85
|
case '--auto-labels':
|
|
130
86
|
config.autoLabels = true;
|
|
131
87
|
break;
|
|
88
|
+
case '--include-impact':
|
|
89
|
+
config.includeImpactAnalysis = true;
|
|
90
|
+
break;
|
|
91
|
+
case '--help':
|
|
92
|
+
showHelp();
|
|
93
|
+
process.exit(0);
|
|
94
|
+
break;
|
|
132
95
|
default:
|
|
133
96
|
break;
|
|
134
97
|
}
|
|
@@ -138,44 +101,119 @@ export function parsePRCliArgs() {
|
|
|
138
101
|
}
|
|
139
102
|
|
|
140
103
|
/**
|
|
141
|
-
*
|
|
104
|
+
* 深度合併物件(簡易版)
|
|
142
105
|
*/
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const userConfig = await import(`file://${configPath}`);
|
|
151
|
-
config = userConfig.default;
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.warn(`⚠️ 載入配置檔案失敗: ${error.message}`);
|
|
106
|
+
function mergeDeep(target, source) {
|
|
107
|
+
const result = { ...target };
|
|
108
|
+
for (const key of Object.keys(source)) {
|
|
109
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
110
|
+
result[key] = mergeDeep(result[key] || {}, source[key]);
|
|
111
|
+
} else {
|
|
112
|
+
result[key] = source[key];
|
|
154
113
|
}
|
|
155
114
|
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 載入使用者配置檔案
|
|
120
|
+
*/
|
|
121
|
+
export async function loadUserConfig() {
|
|
122
|
+
const configPath = resolve(process.cwd(), '.ai-git-config.mjs');
|
|
156
123
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
config = {
|
|
160
|
-
ai: { model: 'gpt-4.1', maxDiffLength: 8000, maxRetries: 3 },
|
|
161
|
-
github: { defaultBase: 'release', autoLabels: true },
|
|
162
|
-
reviewers: { autoSelect: true, maxSuggested: 5, gitHistoryDepth: 20, excludeAuthors: [] },
|
|
163
|
-
output: { verbose: false, saveHistory: false },
|
|
164
|
-
};
|
|
124
|
+
if (!existsSync(configPath)) {
|
|
125
|
+
return DEFAULT_CONFIG;
|
|
165
126
|
}
|
|
166
127
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
128
|
+
try {
|
|
129
|
+
const imported = await import(`file://${configPath}`);
|
|
130
|
+
const userConfig = imported.default || {};
|
|
131
|
+
return mergeDeep(DEFAULT_CONFIG, userConfig);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.warn(`⚠️ 載入配置檔案失敗: ${error.message}`);
|
|
134
|
+
return DEFAULT_CONFIG;
|
|
170
135
|
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 載入統一配置(commit 與 PR 共用)
|
|
140
|
+
* 已廢棄:保留給舊 import 路徑向後相容,請改用 loadConfig
|
|
141
|
+
*/
|
|
142
|
+
export async function loadCommitConfig() {
|
|
143
|
+
return loadConfig();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 載入統一配置(PR 使用)
|
|
148
|
+
*/
|
|
149
|
+
export async function loadPRConfig() {
|
|
150
|
+
return loadConfig();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 載入統一配置
|
|
155
|
+
*/
|
|
156
|
+
export async function loadConfig() {
|
|
157
|
+
const config = await loadUserConfig();
|
|
158
|
+
const cliConfig = parseCliArgs();
|
|
159
|
+
|
|
160
|
+
// 確保各區塊存在
|
|
161
|
+
config.ai = config.ai || {};
|
|
162
|
+
config.github = config.github || {};
|
|
163
|
+
config.reviewers = config.reviewers || {};
|
|
164
|
+
config.output = config.output || {};
|
|
171
165
|
|
|
172
|
-
//
|
|
173
|
-
const cliConfig = parsePRCliArgs();
|
|
166
|
+
// 合併命令行參數(CLI 優先)
|
|
174
167
|
if (cliConfig.model) config.ai.model = cliConfig.model;
|
|
175
|
-
config.
|
|
176
|
-
config.
|
|
177
|
-
config.
|
|
178
|
-
config.
|
|
168
|
+
if (cliConfig.verbose) config.output.verbose = cliConfig.verbose;
|
|
169
|
+
if (cliConfig.maxDiffLength) config.ai.maxDiffLength = cliConfig.maxDiffLength;
|
|
170
|
+
if (cliConfig.maxRetries) config.ai.maxRetries = cliConfig.maxRetries;
|
|
171
|
+
if (cliConfig.baseBranch) config.baseBranch = cliConfig.baseBranch;
|
|
172
|
+
if (cliConfig.headBranch) config.headBranch = cliConfig.headBranch;
|
|
173
|
+
if (cliConfig.preview) config.preview = cliConfig.preview;
|
|
174
|
+
if (cliConfig.noConfirm) config.noConfirm = cliConfig.noConfirm;
|
|
175
|
+
if (cliConfig.interactiveReviewers !== undefined) {
|
|
176
|
+
config.reviewers.interactiveReviewers = cliConfig.interactiveReviewers;
|
|
177
|
+
}
|
|
178
|
+
if (cliConfig.autoLabels !== null) config.github.autoLabels = cliConfig.autoLabels;
|
|
179
|
+
if (cliConfig.includeImpactAnalysis !== null) {
|
|
180
|
+
config.github.includeImpactAnalysis = cliConfig.includeImpactAnalysis;
|
|
181
|
+
}
|
|
179
182
|
|
|
180
183
|
return config;
|
|
181
184
|
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 顯示 PR 幫助訊息
|
|
188
|
+
*/
|
|
189
|
+
function showHelp() {
|
|
190
|
+
console.log(`
|
|
191
|
+
使用方式:
|
|
192
|
+
npx ai-git-tools pr [選項]
|
|
193
|
+
|
|
194
|
+
選項:
|
|
195
|
+
--base <branch> 指定目標分支 (預設: 使用配置檔的 defaultBase 或自動偵測)
|
|
196
|
+
--model <model> 指定 AI 模型 (預設: gpt-4.1)
|
|
197
|
+
--preview 僅預覽 PR 內容,不實際創建
|
|
198
|
+
--no-confirm 跳過確認直接創建
|
|
199
|
+
--auto-labels 自動添加 Labels (預設啟用)
|
|
200
|
+
--help 顯示此說明
|
|
201
|
+
|
|
202
|
+
範例:
|
|
203
|
+
npx ai-git-tools pr
|
|
204
|
+
npx ai-git-tools pr --base release-2025-m12.1
|
|
205
|
+
npx ai-git-tools pr --preview
|
|
206
|
+
npx ai-git-tools pr --no-confirm
|
|
207
|
+
|
|
208
|
+
配置檔範例 (.ai-git-config.mjs):
|
|
209
|
+
export default {
|
|
210
|
+
github: {
|
|
211
|
+
defaultBase: 'release-2025-m12.1',
|
|
212
|
+
autoLabels: true,
|
|
213
|
+
},
|
|
214
|
+
reviewers: {
|
|
215
|
+
interactiveReviewers: true,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
`);
|
|
219
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Git 操作封裝
|
|
3
|
-
*
|
|
3
|
+
* 統一 commit 與 PR 命令的 Git 操作
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
+
import { CONSTANTS } from '../utils/constants.js';
|
|
8
|
+
import { PRError } from '../utils/helpers.js';
|
|
7
9
|
|
|
8
|
-
const MAX_BUFFER_SIZE =
|
|
9
|
-
const DIFF_CONTEXT_LINES =
|
|
10
|
+
const MAX_BUFFER_SIZE = CONSTANTS.MAX_BUFFER_SIZE;
|
|
11
|
+
const DIFF_CONTEXT_LINES = CONSTANTS.DIFF_CONTEXT_LINES;
|
|
10
12
|
|
|
11
13
|
export class GitOperations {
|
|
12
14
|
/**
|
|
@@ -60,7 +62,11 @@ export class GitOperations {
|
|
|
60
62
|
*/
|
|
61
63
|
static detectReleaseBranches() {
|
|
62
64
|
try {
|
|
63
|
-
|
|
65
|
+
try {
|
|
66
|
+
execSync('git fetch --prune origin', { stdio: 'ignore', timeout: 15000 });
|
|
67
|
+
} catch (_) {
|
|
68
|
+
// fetch 失敗,繼續使用已經 cache 的遠端分支
|
|
69
|
+
}
|
|
64
70
|
const branches = execSync('git branch -r', { encoding: 'utf-8' })
|
|
65
71
|
.toString()
|
|
66
72
|
.split('\n')
|
|
@@ -82,18 +88,24 @@ export class GitOperations {
|
|
|
82
88
|
|
|
83
89
|
const monthlyBranches = branches.filter((b) => b.includes('-m'));
|
|
84
90
|
const weeklyBranches = branches.filter((b) => b.includes('-w'));
|
|
85
|
-
const priorityBranches =
|
|
91
|
+
const priorityBranches =
|
|
92
|
+
monthlyBranches.length > 0
|
|
93
|
+
? monthlyBranches
|
|
94
|
+
: weeklyBranches.length > 0
|
|
95
|
+
? weeklyBranches
|
|
96
|
+
: branches;
|
|
86
97
|
|
|
87
98
|
priorityBranches.sort().reverse();
|
|
88
|
-
return priorityBranches[0];
|
|
99
|
+
return priorityBranches[0] || null;
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
/**
|
|
92
103
|
* 獲取變更的檔案列表
|
|
93
104
|
*/
|
|
94
|
-
static getChangedFiles(baseBranch, headBranch) {
|
|
105
|
+
static getChangedFiles(baseBranch, headBranch, useRemoteHead = false) {
|
|
95
106
|
try {
|
|
96
|
-
const
|
|
107
|
+
const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
|
|
108
|
+
const files = execSync(`git diff --name-only origin/${baseBranch}...${headRef}`, {
|
|
97
109
|
encoding: 'utf-8',
|
|
98
110
|
})
|
|
99
111
|
.split('\n')
|
|
@@ -107,35 +119,41 @@ export class GitOperations {
|
|
|
107
119
|
/**
|
|
108
120
|
* 獲取 commit 列表
|
|
109
121
|
*/
|
|
110
|
-
static getCommits(baseBranch, headBranch) {
|
|
122
|
+
static getCommits(baseBranch, headBranch, options = {}) {
|
|
123
|
+
const { oneline = true, noDecorate = true, useRemoteHead = false } = options;
|
|
111
124
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
|
|
126
|
+
let cmd = `git log origin/${baseBranch}..${headRef}`;
|
|
127
|
+
if (oneline) cmd += ' --oneline';
|
|
128
|
+
if (noDecorate) cmd += ' --no-decorate';
|
|
129
|
+
|
|
130
|
+
return execSync(cmd, { encoding: 'utf-8' });
|
|
115
131
|
} catch (error) {
|
|
116
|
-
throw new
|
|
132
|
+
throw new PRError(
|
|
133
|
+
'無法比較分支差異',
|
|
134
|
+
'GIT_COMPARE_FAILED',
|
|
135
|
+
['檢查遠端分支是否存在: git branch -r', '執行診斷: npm run diagnose:pr'],
|
|
136
|
+
`git log origin/${baseBranch}..${headBranch}`
|
|
137
|
+
);
|
|
117
138
|
}
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
/**
|
|
121
142
|
* 獲取 diff
|
|
122
143
|
*/
|
|
123
|
-
static getDiff(baseBranch, headBranch) {
|
|
144
|
+
static getDiff(baseBranch, headBranch, useRemoteHead = false, maxBuffer = MAX_BUFFER_SIZE) {
|
|
124
145
|
try {
|
|
125
|
-
|
|
146
|
+
const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
|
|
147
|
+
return execSync(`git diff origin/${baseBranch}...${headRef}`, {
|
|
126
148
|
encoding: 'utf-8',
|
|
127
|
-
maxBuffer
|
|
149
|
+
maxBuffer,
|
|
128
150
|
});
|
|
129
151
|
} catch (error) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
} catch (fallbackError) {
|
|
137
|
-
throw new Error(`無法獲取分支差異: ${error.message}`);
|
|
138
|
-
}
|
|
152
|
+
const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
|
|
153
|
+
return execSync(`git diff origin/${baseBranch}..${headRef}`, {
|
|
154
|
+
encoding: 'utf-8',
|
|
155
|
+
maxBuffer,
|
|
156
|
+
});
|
|
139
157
|
}
|
|
140
158
|
}
|
|
141
159
|
|
|
@@ -143,39 +161,60 @@ export class GitOperations {
|
|
|
143
161
|
* 智能截斷 diff
|
|
144
162
|
* 保留前後各 50 行,中間用省略標記
|
|
145
163
|
*/
|
|
146
|
-
static truncateDiff(diff, maxLength =
|
|
164
|
+
static truncateDiff(diff, maxLength = CONSTANTS.MAX_DIFF_LENGTH) {
|
|
147
165
|
if (diff.length <= maxLength) return diff;
|
|
148
166
|
|
|
149
167
|
const lines = diff.split('\n');
|
|
150
|
-
const contextLines = DIFF_CONTEXT_LINES;
|
|
151
168
|
|
|
152
|
-
if (lines.length <=
|
|
169
|
+
if (lines.length <= DIFF_CONTEXT_LINES * 2) {
|
|
153
170
|
return diff;
|
|
154
171
|
}
|
|
155
172
|
|
|
156
|
-
const header = lines.slice(0,
|
|
157
|
-
const footer = lines.slice(-
|
|
158
|
-
const omittedLines = lines.length - contextLines * 2;
|
|
173
|
+
const header = lines.slice(0, DIFF_CONTEXT_LINES).join('\n');
|
|
174
|
+
const footer = lines.slice(-DIFF_CONTEXT_LINES).join('\n');
|
|
159
175
|
|
|
160
|
-
return `${header}\n\n... [已省略 ${
|
|
176
|
+
return `${header}\n\n... [已省略 ${lines.length - DIFF_CONTEXT_LINES * 2} 行變更] ...\n\n${footer}`;
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
/**
|
|
164
180
|
* 推送到遠端
|
|
165
181
|
*/
|
|
166
|
-
static push(branch) {
|
|
182
|
+
static async push(branch) {
|
|
167
183
|
try {
|
|
168
|
-
execSync(`git push -u origin ${branch}`, {
|
|
184
|
+
execSync(`git push -u origin ${branch}`, {
|
|
185
|
+
stdio: ['ignore', 'inherit', 'pipe'],
|
|
186
|
+
encoding: 'utf-8',
|
|
187
|
+
});
|
|
169
188
|
return true;
|
|
170
189
|
} catch (error) {
|
|
171
|
-
|
|
190
|
+
const errMsg = (error.stderr || error.message || '').toString();
|
|
191
|
+
const is403 = errMsg.includes('403') || errMsg.includes('Write access') || errMsg.includes('write access');
|
|
192
|
+
if (is403) {
|
|
193
|
+
throw new PRError(
|
|
194
|
+
'推送失敗:git 沒有寫入權限',
|
|
195
|
+
'GIT_PUSH_FORBIDDEN',
|
|
196
|
+
[
|
|
197
|
+
'建議執行以下指令讓 git 使用 gh 的認證:',
|
|
198
|
+
' gh auth setup-git',
|
|
199
|
+
'或者改用 SSH 權限:',
|
|
200
|
+
' git remote set-url origin git@github.com:<org>/<repo>.git',
|
|
201
|
+
],
|
|
202
|
+
`git push -u origin ${branch}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
throw new PRError(
|
|
206
|
+
'推送失敗',
|
|
207
|
+
'GIT_PUSH_FAILED',
|
|
208
|
+
['檢查是否有推送權限', '檢查遠端分支是否有衝突', '檢查網路連接'],
|
|
209
|
+
`git push -u origin ${branch}`
|
|
210
|
+
);
|
|
172
211
|
}
|
|
173
212
|
}
|
|
174
213
|
|
|
175
214
|
/**
|
|
176
215
|
* 同步遠端資訊
|
|
177
216
|
*/
|
|
178
|
-
static fetch() {
|
|
217
|
+
static async fetch() {
|
|
179
218
|
try {
|
|
180
219
|
execSync('git fetch origin', { stdio: 'ignore' });
|
|
181
220
|
return true;
|
|
@@ -187,14 +226,15 @@ export class GitOperations {
|
|
|
187
226
|
/**
|
|
188
227
|
* 獲取變更統計
|
|
189
228
|
*/
|
|
190
|
-
static getChangeStats(baseBranch, headBranch) {
|
|
229
|
+
static getChangeStats(baseBranch, headBranch, useRemoteHead = false) {
|
|
191
230
|
try {
|
|
192
|
-
const
|
|
231
|
+
const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
|
|
232
|
+
const stats = execSync(`git diff --shortstat origin/${baseBranch}...${headRef}`, {
|
|
193
233
|
encoding: 'utf-8',
|
|
194
234
|
}).trim();
|
|
195
235
|
|
|
196
236
|
const filesChanged = execSync(
|
|
197
|
-
`git diff --name-only origin/${baseBranch}...${
|
|
237
|
+
`git diff --name-only origin/${baseBranch}...${headRef} | wc -l`,
|
|
198
238
|
{ encoding: 'utf-8' }
|
|
199
239
|
).trim();
|
|
200
240
|
|
|
@@ -203,4 +243,47 @@ export class GitOperations {
|
|
|
203
243
|
return { stats: '無法獲取統計', filesChanged: 0 };
|
|
204
244
|
}
|
|
205
245
|
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 獲取當前用戶資訊
|
|
249
|
+
*/
|
|
250
|
+
static getCurrentUser() {
|
|
251
|
+
try {
|
|
252
|
+
const email = execSync('git config user.email', { encoding: 'utf-8' }).trim();
|
|
253
|
+
const name = execSync('git config user.name', { encoding: 'utf-8' }).trim();
|
|
254
|
+
let githubUser = null;
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
githubUser = execSync('gh api user --jq .login', {
|
|
258
|
+
encoding: 'utf-8',
|
|
259
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
260
|
+
}).trim();
|
|
261
|
+
} catch {
|
|
262
|
+
// GitHub CLI 未認證或未安裝
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { email, name, githubUser };
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 獲取 repository owner
|
|
273
|
+
*/
|
|
274
|
+
static getRepoOwner() {
|
|
275
|
+
try {
|
|
276
|
+
const remoteUrl = execSync('git config --get remote.origin.url', {
|
|
277
|
+
encoding: 'utf-8',
|
|
278
|
+
}).trim();
|
|
279
|
+
|
|
280
|
+
const httpsMatch = remoteUrl.match(/github\.com[/:]([^/]+)\//);
|
|
281
|
+
const sshMatch = remoteUrl.match(/github\.com:([^/]+)\//);
|
|
282
|
+
|
|
283
|
+
const owner = httpsMatch?.[1] || sshMatch?.[1];
|
|
284
|
+
return owner || null;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
206
289
|
}
|