ai-git-tools 1.0.4 → 2.0.0
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/bin/cli.js +24 -45
- package/package.json +1 -1
- package/src/commands/commit-all.js +250 -179
- package/src/commands/commit.js +92 -76
- package/src/commands/init.js +33 -131
- package/src/commands/pr.js +122 -238
- package/src/core/ai-client.js +19 -84
- package/src/core/config-loader.js +165 -133
- package/src/core/git-operations.js +103 -183
- package/src/utils/helpers.js +43 -54
- package/src/utils/logger.js +28 -100
- package/src/commands/workflow.js +0 -36
- package/src/core/github-api.js +0 -139
- package/src/index.js +0 -16
package/bin/cli.js
CHANGED
|
@@ -4,14 +4,13 @@
|
|
|
4
4
|
* AI Git Tools CLI
|
|
5
5
|
*
|
|
6
6
|
* AI-powered Git automation for commit messages and PR generation
|
|
7
|
+
* 完全重写版本基于 scripts/ 原始实现
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { Command } from 'commander';
|
|
10
|
-
import chalk from 'chalk';
|
|
11
11
|
import { commitCommand } from '../src/commands/commit.js';
|
|
12
12
|
import { commitAllCommand } from '../src/commands/commit-all.js';
|
|
13
13
|
import { prCommand } from '../src/commands/pr.js';
|
|
14
|
-
import { workflowCommand } from '../src/commands/workflow.js';
|
|
15
14
|
import { initCommand } from '../src/commands/init.js';
|
|
16
15
|
|
|
17
16
|
const program = new Command();
|
|
@@ -19,68 +18,48 @@ const program = new Command();
|
|
|
19
18
|
program
|
|
20
19
|
.name('ai-git-tools')
|
|
21
20
|
.description('AI-powered Git automation tools')
|
|
22
|
-
.version('
|
|
21
|
+
.version('2.0.0');
|
|
23
22
|
|
|
24
23
|
// Init 命令
|
|
25
24
|
program
|
|
26
25
|
.command('init')
|
|
27
|
-
.description('
|
|
26
|
+
.description('初始化配置文件 (.ai-git-config.mjs)')
|
|
28
27
|
.action(initCommand);
|
|
29
28
|
|
|
30
29
|
// Commit 命令
|
|
31
30
|
program
|
|
32
31
|
.command('commit')
|
|
33
|
-
.description('
|
|
34
|
-
.option('
|
|
35
|
-
.option('-v, --verbose', '
|
|
36
|
-
.option('--max-diff <number>', '最大 diff
|
|
37
|
-
.option('--max-retries <number>', '
|
|
32
|
+
.description('AI 自动生成 commit message 并提交')
|
|
33
|
+
.option('--model <model>', '指定 AI 模型')
|
|
34
|
+
.option('-v, --verbose', '显示详细输出')
|
|
35
|
+
.option('--max-diff <number>', '最大 diff 长度')
|
|
36
|
+
.option('--max-retries <number>', '最大重试次数')
|
|
38
37
|
.action(commitCommand);
|
|
39
38
|
|
|
40
39
|
// Commit All 命令
|
|
41
40
|
program
|
|
42
41
|
.command('commit-all')
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
-
.option('-
|
|
46
|
-
.option('-
|
|
47
|
-
.option('--max-
|
|
48
|
-
.option('--max-retries <number>', '最大重試次數', parseInt)
|
|
42
|
+
.description('智能分析所有变更并自动分组提交')
|
|
43
|
+
.option('--model <model>', '指定 AI 模型')
|
|
44
|
+
.option('-v, --verbose', '显示详细输出')
|
|
45
|
+
.option('--max-diff <number>', '最大 diff 长度')
|
|
46
|
+
.option('--max-retries <number>', '最大重试次数')
|
|
49
47
|
.action(commitAllCommand);
|
|
50
48
|
|
|
51
49
|
// PR 命令
|
|
52
50
|
program
|
|
53
51
|
.command('pr')
|
|
54
|
-
.description('
|
|
55
|
-
.option('
|
|
56
|
-
.option('
|
|
57
|
-
.option('
|
|
58
|
-
.option('--
|
|
59
|
-
.option('--
|
|
60
|
-
.option('--
|
|
61
|
-
.option('--
|
|
62
|
-
.option('--auto-
|
|
52
|
+
.description('AI 自动生成 PR 并创建 Pull Request')
|
|
53
|
+
.option('--base <branch>', '指定目标分支')
|
|
54
|
+
.option('--head <branch>', '指定来源分支')
|
|
55
|
+
.option('--model <model>', '指定 AI 模型')
|
|
56
|
+
.option('--org <org-name>', '指定 GitHub 组织名称')
|
|
57
|
+
.option('--draft', '创建草稿 PR')
|
|
58
|
+
.option('--preview', '仅预览 PR 内容,不实际创建')
|
|
59
|
+
.option('--no-confirm', '跳过确认直接创建')
|
|
60
|
+
.option('--auto-reviewers', '自动选择 reviewers')
|
|
61
|
+
.option('--auto-labels', '自动添加 Labels')
|
|
63
62
|
.option('--no-labels', '不添加 Labels')
|
|
64
|
-
.option('--org <name>', 'GitHub 組織名稱')
|
|
65
63
|
.action(prCommand);
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
program
|
|
69
|
-
.command('workflow')
|
|
70
|
-
.alias('wf')
|
|
71
|
-
.description('完整工作流程:commit-all + pr')
|
|
72
|
-
.option('-m, --model <model>', '指定 AI 模型')
|
|
73
|
-
.option('-v, --verbose', '顯示詳細輸出')
|
|
74
|
-
.option('-b, --base <branch>', 'PR 目標分支')
|
|
75
|
-
.option('--draft', '創建草稿 PR')
|
|
76
|
-
.option('--auto-reviewers', '自動選擇 reviewers')
|
|
77
|
-
.option('--auto-labels', '自動添加 Labels')
|
|
78
|
-
.action(workflowCommand);
|
|
79
|
-
|
|
80
|
-
// 解析命令列參數
|
|
81
|
-
program.parse(process.argv);
|
|
82
|
-
|
|
83
|
-
// 如果沒有提供命令,顯示幫助
|
|
84
|
-
if (!process.argv.slice(2).length) {
|
|
85
|
-
program.outputHelp();
|
|
86
|
-
}
|
|
65
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,306 +1,377 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Commit All 命令
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 基于 scripts/ai-auto-commit-all.mjs
|
|
4
|
+
* 智能分析所有变更并自动分类提交
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
9
|
+
import { loadCommitConfig } from '../core/config-loader.js';
|
|
9
10
|
import { AIClient } from '../core/ai-client.js';
|
|
10
|
-
import { GitOperations } from '../core/git-operations.js';
|
|
11
11
|
import { Logger } from '../utils/logger.js';
|
|
12
|
-
import {
|
|
13
|
-
handleError,
|
|
14
|
-
validateCommitMessage,
|
|
15
|
-
formatFileList,
|
|
16
|
-
getProjectTypePrompt,
|
|
17
|
-
getConventionalCommitsRules,
|
|
18
|
-
} from '../utils/helpers.js';
|
|
12
|
+
import { handleError } from '../utils/helpers.js';
|
|
19
13
|
|
|
20
14
|
/**
|
|
21
|
-
*
|
|
15
|
+
* 获取文件的变更内容
|
|
22
16
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
function getFileDiff(filePath, isNew) {
|
|
18
|
+
try {
|
|
19
|
+
if (isNew) {
|
|
20
|
+
// 新文件:读取完整内容(前 100 行)
|
|
21
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
22
|
+
const lines = content.split('\n').slice(0, 100);
|
|
23
|
+
return `[新文件]\n${lines.join('\n')}${lines.length >= 100 ? '\n...' : ''}`;
|
|
24
|
+
}
|
|
25
|
+
// 已存在文件:获取 diff
|
|
26
|
+
const diff = execSync(`git diff HEAD -- "${filePath}"`, {
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
}).toString();
|
|
29
|
+
return diff || '[无变更]';
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return `[读取错误: ${error.message}]`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 获取所有未提交的变更
|
|
37
|
+
*/
|
|
38
|
+
function getAllChanges() {
|
|
39
|
+
try {
|
|
40
|
+
const status = execSync('git status --porcelain', {
|
|
41
|
+
encoding: 'utf-8',
|
|
42
|
+
}).toString();
|
|
43
|
+
|
|
44
|
+
if (!status.trim()) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const changes = [];
|
|
49
|
+
const lines = status.split('\n').filter((line) => line.trim());
|
|
50
|
+
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const statusCode = line.substring(0, 2);
|
|
53
|
+
const filePath = line.substring(3).trim();
|
|
54
|
+
|
|
55
|
+
// 跳过已删除的文件
|
|
56
|
+
if (statusCode.includes('D')) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 跳过某些不需要提交的文件
|
|
61
|
+
if (
|
|
62
|
+
filePath.includes('node_modules/') ||
|
|
63
|
+
filePath.includes('.next/') ||
|
|
64
|
+
filePath.includes('dist/') ||
|
|
65
|
+
filePath.includes('.DS_Store')
|
|
66
|
+
) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
25
69
|
|
|
26
|
-
|
|
70
|
+
const isNew = statusCode.includes('?') || statusCode.includes('A');
|
|
71
|
+
const isStaged = statusCode[0] !== ' ' && statusCode[0] !== '?';
|
|
72
|
+
|
|
73
|
+
changes.push({
|
|
74
|
+
filePath,
|
|
75
|
+
isNew,
|
|
76
|
+
isStaged,
|
|
77
|
+
statusCode,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return changes;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('获取变更列表失败:', error.message);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 使用 AI 分析并分组变更
|
|
90
|
+
*/
|
|
91
|
+
async function analyzeAndGroupChanges(changes, config) {
|
|
92
|
+
console.log('🤖 正在使用 AI 分析变更并分组...\n');
|
|
93
|
+
|
|
94
|
+
// 准备变更摘要
|
|
27
95
|
const maxDiffPerFile = Math.floor(config.ai.maxDiffLength / Math.max(changes.length, 1));
|
|
28
96
|
const changeSummary = changes
|
|
29
97
|
.map((change, index) => {
|
|
30
|
-
const diff =
|
|
98
|
+
const diff = getFileDiff(change.filePath, change.isNew);
|
|
31
99
|
const lines = diff.split('\n');
|
|
32
100
|
const truncatedDiff = lines.slice(0, Math.min(50, maxDiffPerFile / 100)).join('\n');
|
|
33
|
-
return `[
|
|
34
|
-
change.isNew ? '
|
|
101
|
+
return `[文件 ${index}] ${change.filePath}\n${
|
|
102
|
+
change.isNew ? '(新文件)' : '(已修改)'
|
|
35
103
|
}\n${truncatedDiff}\n`;
|
|
36
104
|
})
|
|
37
105
|
.join('\n---\n\n');
|
|
38
106
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
107
|
+
const prompt = `你是一个资深前端工程师,熟悉 Next.js 专案的开发规范。请分析以下的文件变更,并将它们按照功能/目的分组。
|
|
108
|
+
|
|
109
|
+
**专案背景**:
|
|
110
|
+
- Next.js 12+ (Pages Router)
|
|
111
|
+
- TypeScript + JavaScript 混合
|
|
112
|
+
- Tailwind CSS + Styled Components
|
|
113
|
+
- Zustand (客户端状态) + SWR (伺服器资料获取)
|
|
114
|
+
- React Hook Form + Zod (表单处理)
|
|
115
|
+
- 架构:Modified Atomic Design(UI / Page / Feature 三层)
|
|
116
|
+
|
|
117
|
+
**专案目录结构参考**:
|
|
118
|
+
- pages/ → 页面路由
|
|
119
|
+
- components/Page/ → 页面级元件
|
|
120
|
+
- components/UI/ 或 components/Common/ → 共用 UI 元件
|
|
121
|
+
- components/[Feature]/ → 功能模块元件
|
|
122
|
+
- store/ → Zustand 状态管理
|
|
123
|
+
- api/ → API 呼叫
|
|
124
|
+
- utils/ → 工具函式
|
|
125
|
+
- styles/ → 全域样式
|
|
126
|
+
|
|
127
|
+
规则:
|
|
128
|
+
1. 将相关功能的变更归类在同一组(例如:同一个功能开发、同一个 bug 修复、相关的重构等)
|
|
129
|
+
2. 每组应该要有明确的主题
|
|
130
|
+
3. 同一个功能的元件、API、store、样式应归为同一组
|
|
131
|
+
4. 设定档(config)和文件(docs)变更可以独立成一组
|
|
132
|
+
5. 输出格式为 JSON 数组,每个元素包含:
|
|
133
|
+
- group_name: 群组名称(简短描述,繁体中文)
|
|
134
|
+
- commit_type: commit 类型(feat/fix/docs/style/refactor/test/chore/perf)
|
|
135
|
+
- commit_scope: commit 影响范围(如 member、report、auth、api、ui、config)
|
|
136
|
+
- file_indices: 属于这组的文件索引数组(对应上面的 [文件 X])
|
|
137
|
+
- description: 这组变更的详细说明(繁体中文)
|
|
138
|
+
|
|
139
|
+
范例输出:
|
|
58
140
|
[
|
|
59
141
|
{
|
|
60
142
|
"group_name": "新增使用者登入功能",
|
|
61
143
|
"commit_type": "feat",
|
|
62
144
|
"commit_scope": "auth",
|
|
63
145
|
"file_indices": [0, 1, 2],
|
|
64
|
-
"description": "
|
|
146
|
+
"description": "实作使用者登入 API 和前端页面"
|
|
65
147
|
},
|
|
66
148
|
{
|
|
67
|
-
"group_name": "
|
|
149
|
+
"group_name": "修正导航列手机版显示",
|
|
68
150
|
"commit_type": "fix",
|
|
69
151
|
"commit_scope": "ui",
|
|
70
152
|
"file_indices": [3, 4],
|
|
71
|
-
"description": "
|
|
153
|
+
"description": "修正导航列在手机版的显示问题"
|
|
72
154
|
}
|
|
73
155
|
]
|
|
74
156
|
|
|
75
|
-
|
|
157
|
+
文件变更内容:
|
|
76
158
|
${changeSummary}
|
|
77
159
|
|
|
78
|
-
|
|
160
|
+
请只输出 JSON,不要其他文字。`;
|
|
79
161
|
|
|
80
|
-
const response = await
|
|
81
|
-
await aiClient.stop();
|
|
162
|
+
const response = await AIClient.sendAndWait(prompt, config.ai.model);
|
|
82
163
|
|
|
83
164
|
try {
|
|
84
|
-
|
|
85
|
-
logger.succeedSpinner(`AI 分析完成,共分為 ${groups.length} 個群組`);
|
|
86
|
-
return groups;
|
|
165
|
+
return AIClient.parseJSON(response);
|
|
87
166
|
} catch (error) {
|
|
88
|
-
|
|
89
|
-
|
|
167
|
+
console.error('❌ 无法解析 AI 回应:', error.message);
|
|
168
|
+
console.log('原始回应:', response);
|
|
169
|
+
return null;
|
|
90
170
|
}
|
|
91
171
|
}
|
|
92
172
|
|
|
93
173
|
/**
|
|
94
|
-
*
|
|
174
|
+
* 为特定群组生成 commit message
|
|
95
175
|
*/
|
|
96
|
-
async function generateCommitMessage(group, files, config
|
|
176
|
+
async function generateCommitMessage(group, files, config) {
|
|
97
177
|
const filesList = files
|
|
98
|
-
.map(file => {
|
|
99
|
-
const diff =
|
|
100
|
-
return
|
|
178
|
+
.map((file) => {
|
|
179
|
+
const diff = getFileDiff(file.filePath, file.isNew);
|
|
180
|
+
return `文件: ${file.filePath}\n${diff}`;
|
|
101
181
|
})
|
|
102
182
|
.join('\n\n---\n\n');
|
|
103
183
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const prompt = `請根據以下資訊生成一則 commit message:
|
|
184
|
+
const prompt = `请根据以下资讯生成一则 commit message:
|
|
107
185
|
|
|
108
|
-
|
|
109
|
-
Commit
|
|
110
|
-
Commit
|
|
111
|
-
|
|
186
|
+
群组名称: ${group.group_name}
|
|
187
|
+
Commit 类型: ${group.commit_type}
|
|
188
|
+
Commit 范围: ${group.commit_scope || '未指定'}
|
|
189
|
+
说明: ${group.description}
|
|
112
190
|
|
|
113
|
-
|
|
191
|
+
文件变更:
|
|
114
192
|
${filesList}
|
|
115
193
|
|
|
116
|
-
|
|
194
|
+
规则:
|
|
117
195
|
- 使用 Conventional Commits 格式:${group.commit_type}${
|
|
118
196
|
group.commit_scope ? `(${group.commit_scope})` : ''
|
|
119
197
|
}: <subject>
|
|
120
|
-
- subject 限制在 50
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
- 不要包含 markdown code block
|
|
124
|
-
-
|
|
198
|
+
- subject 限制在 50 字内,使用繁体中文
|
|
199
|
+
- 如果变更复杂,可以加上 body(用空行分隔),body 使用 bullet points
|
|
200
|
+
- 只输出 commit message 本身,不要其他说明
|
|
201
|
+
- 不要包含 markdown code block 标记(不要 \`\`\`)
|
|
202
|
+
- 不要加上任何引导语句
|
|
125
203
|
|
|
126
|
-
|
|
204
|
+
输出格式范例:
|
|
127
205
|
feat(auth): 新增使用者登入功能
|
|
128
206
|
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
- 整合 JWT
|
|
207
|
+
- 实作登入 API endpoint
|
|
208
|
+
- 新增登入页面 UI
|
|
209
|
+
- 整合 JWT 认证机制`;
|
|
132
210
|
|
|
133
|
-
const response = await
|
|
134
|
-
await aiClient.stop();
|
|
211
|
+
const response = await AIClient.sendAndWait(prompt, config.ai.model);
|
|
135
212
|
|
|
136
|
-
|
|
213
|
+
// 清理可能的 markdown code block 标记
|
|
214
|
+
let commitMessage = response.trim();
|
|
215
|
+
commitMessage = commitMessage
|
|
216
|
+
.replace(/^```[\s\S]*?\n/, '')
|
|
217
|
+
.replace(/\n```$/, '')
|
|
218
|
+
.trim();
|
|
219
|
+
|
|
220
|
+
return commitMessage;
|
|
137
221
|
}
|
|
138
222
|
|
|
139
223
|
/**
|
|
140
|
-
*
|
|
224
|
+
* 执行分组提交
|
|
141
225
|
*/
|
|
142
|
-
async function commitGroup(group, files, config
|
|
226
|
+
async function commitGroup(group, files, config) {
|
|
143
227
|
try {
|
|
144
|
-
console.log(
|
|
145
|
-
console.log(
|
|
146
|
-
|
|
228
|
+
console.log(`\n📦 处理群组: ${group.group_name}`);
|
|
229
|
+
console.log(
|
|
230
|
+
` 类型: ${group.commit_type}${group.commit_scope ? `(${group.commit_scope})` : ''}`
|
|
231
|
+
);
|
|
232
|
+
console.log(` 文件数量: ${files.length}`);
|
|
147
233
|
|
|
148
|
-
//
|
|
149
|
-
|
|
234
|
+
// 先 reset 所有已 staged 的文件
|
|
235
|
+
try {
|
|
236
|
+
execSync('git reset HEAD -- .', { stdio: 'ignore' });
|
|
237
|
+
} catch (e) {
|
|
238
|
+
// 忽略错误(可能没有 staged 的文件)
|
|
239
|
+
}
|
|
150
240
|
|
|
151
|
-
// Add
|
|
152
|
-
const addedFiles = [];
|
|
241
|
+
// Add 这组的文件
|
|
153
242
|
for (const file of files) {
|
|
154
243
|
console.log(` ├─ ${file.filePath}`);
|
|
155
244
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// 繼續處理其他檔案,不要中斷
|
|
161
|
-
continue;
|
|
245
|
+
execSync(`git add "${file.filePath}"`, { encoding: 'utf-8' });
|
|
246
|
+
} catch (addError) {
|
|
247
|
+
console.error(` ⚠️ 无法加入文件: ${file.filePath}`, addError.message);
|
|
248
|
+
throw addError;
|
|
162
249
|
}
|
|
163
250
|
}
|
|
164
251
|
|
|
165
|
-
// 如果沒有成功 add 任何檔案,跳過這個群組
|
|
166
|
-
if (addedFiles.length === 0) {
|
|
167
|
-
logger.warn('沒有檔案被成功 add,跳過此群組');
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
252
|
// 生成 commit message
|
|
172
253
|
console.log(` └─ 生成 commit message...`);
|
|
173
|
-
const commitMessage = await generateCommitMessage(group, files, config
|
|
254
|
+
const commitMessage = await generateCommitMessage(group, files, config);
|
|
174
255
|
|
|
175
256
|
if (!commitMessage) {
|
|
176
|
-
|
|
257
|
+
console.log(` ❌ 无法生成 commit message,跳过此群组`);
|
|
177
258
|
return false;
|
|
178
259
|
}
|
|
179
260
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
console.log(chalk.cyan('\n 📝 Commit Message:'));
|
|
188
|
-
logger.code(commitMessage.split('\n').map(line => ` ${line}`).join('\n'));
|
|
261
|
+
console.log(`\n 📝 Commit Message:`);
|
|
262
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
263
|
+
commitMessage.split('\n').forEach((line) => {
|
|
264
|
+
console.log(` ${line}`);
|
|
265
|
+
});
|
|
266
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
189
267
|
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
268
|
+
// 执行 commit
|
|
269
|
+
// 使用临时文件避免 commit message 中的特殊字符问题
|
|
270
|
+
const tmpFile = '.git/COMMIT_EDITMSG_TMP';
|
|
271
|
+
try {
|
|
272
|
+
writeFileSync(tmpFile, commitMessage, 'utf-8');
|
|
273
|
+
execSync(`git commit -F ${tmpFile}`, {
|
|
274
|
+
stdio: 'inherit',
|
|
275
|
+
});
|
|
276
|
+
unlinkSync(tmpFile);
|
|
277
|
+
} catch (commitError) {
|
|
278
|
+
try {
|
|
279
|
+
unlinkSync(tmpFile);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
// 忽略删除临时文件的错误
|
|
282
|
+
}
|
|
283
|
+
throw commitError;
|
|
284
|
+
}
|
|
193
285
|
|
|
286
|
+
console.log(` ✅ Commit 完成!`);
|
|
194
287
|
return true;
|
|
195
288
|
} catch (error) {
|
|
196
|
-
|
|
289
|
+
console.error(` ❌ Commit 失败:`, error.message);
|
|
197
290
|
return false;
|
|
198
291
|
}
|
|
199
292
|
}
|
|
200
293
|
|
|
201
294
|
/**
|
|
202
|
-
* Commit All
|
|
295
|
+
* Commit All 命令主函数
|
|
203
296
|
*/
|
|
204
|
-
export async function commitAllCommand(
|
|
205
|
-
const logger = new Logger(
|
|
297
|
+
export async function commitAllCommand() {
|
|
298
|
+
const logger = new Logger();
|
|
206
299
|
|
|
207
300
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// 檢查是否在 Git 倉庫中
|
|
211
|
-
if (!GitOperations.isGitRepository()) {
|
|
212
|
-
logger.error('當前目錄不是 Git 倉庫');
|
|
213
|
-
process.exit(1);
|
|
214
|
-
}
|
|
301
|
+
// 载入配置
|
|
302
|
+
const config = await loadCommitConfig();
|
|
215
303
|
|
|
216
|
-
|
|
217
|
-
const config = await loadConfig(options);
|
|
304
|
+
logger.header('智能分析所有变更并自动提交');
|
|
218
305
|
|
|
219
306
|
if (config.output.verbose) {
|
|
220
|
-
|
|
221
|
-
|
|
307
|
+
console.log('📋 使用配置:');
|
|
308
|
+
console.log(` AI Model: ${config.ai.model}`);
|
|
309
|
+
console.log(` Max Diff Length: ${config.ai.maxDiffLength}`);
|
|
310
|
+
console.log(` Max Retries: ${config.ai.maxRetries}`);
|
|
311
|
+
console.log('');
|
|
222
312
|
}
|
|
223
313
|
|
|
224
|
-
//
|
|
225
|
-
logger.
|
|
226
|
-
const changes =
|
|
227
|
-
logger.succeedSpinner(`找到 ${changes.length} 個變更的檔案`);
|
|
314
|
+
// 1. 获取所有变更
|
|
315
|
+
logger.step('扫描变更中...');
|
|
316
|
+
const changes = getAllChanges();
|
|
228
317
|
|
|
229
318
|
if (changes.length === 0) {
|
|
230
|
-
logger.info('
|
|
319
|
+
logger.info('没有需要提交的变更');
|
|
231
320
|
process.exit(0);
|
|
232
321
|
}
|
|
233
322
|
|
|
234
|
-
console.log(
|
|
235
|
-
|
|
323
|
+
console.log(`📊 找到 ${changes.length} 个变更的文件:\n`);
|
|
324
|
+
changes.forEach((change, index) => {
|
|
325
|
+
const status = change.isNew ? '新增' : '修改';
|
|
326
|
+
console.log(` [${index}] ${status} - ${change.filePath}`);
|
|
327
|
+
});
|
|
328
|
+
console.log();
|
|
236
329
|
|
|
237
|
-
// 使用 AI
|
|
238
|
-
const groups = await analyzeAndGroupChanges(changes, config
|
|
330
|
+
// 2. 使用 AI 分析并分组
|
|
331
|
+
const groups = await analyzeAndGroupChanges(changes, config);
|
|
239
332
|
|
|
240
333
|
if (!groups || groups.length === 0) {
|
|
241
|
-
logger.error('AI
|
|
334
|
+
logger.error('AI 分析失败或没有产生分组');
|
|
242
335
|
process.exit(1);
|
|
243
336
|
}
|
|
244
337
|
|
|
245
|
-
|
|
338
|
+
logger.success(`AI 分析完成,共分为 ${groups.length} 个群组:\n`);
|
|
246
339
|
groups.forEach((group, index) => {
|
|
247
|
-
console.log(`
|
|
248
|
-
console.log(` └─ 包含 ${group.file_indices.length}
|
|
340
|
+
console.log(` 群组 ${index + 1}: ${group.group_name} (${group.commit_type})`);
|
|
341
|
+
console.log(` └─ 包含 ${group.file_indices.length} 个文件`);
|
|
249
342
|
});
|
|
250
343
|
|
|
251
|
-
//
|
|
252
|
-
logger.
|
|
344
|
+
// 3. 依序提交每个群组
|
|
345
|
+
logger.separator('=', 60);
|
|
346
|
+
console.log('开始执行提交...');
|
|
347
|
+
logger.separator('=', 60);
|
|
253
348
|
|
|
254
349
|
let successCount = 0;
|
|
255
350
|
for (let i = 0; i < groups.length; i++) {
|
|
256
351
|
const group = groups[i];
|
|
257
|
-
|
|
258
|
-
// 驗證並過濾有效的檔案索引
|
|
259
|
-
const validFiles = group.file_indices
|
|
260
|
-
.filter(index => {
|
|
261
|
-
if (index < 0 || index >= changes.length) {
|
|
262
|
-
logger.warn(`無效的檔案索引: ${index}`);
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
return true;
|
|
266
|
-
})
|
|
267
|
-
.map(index => changes[index])
|
|
268
|
-
.filter(file => {
|
|
269
|
-
if (!file || !file.filePath) {
|
|
270
|
-
logger.warn('檔案資訊不完整,已跳過');
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
return true;
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
if (validFiles.length === 0) {
|
|
277
|
-
logger.warn(`群組 ${i + 1} 沒有有效的檔案,跳過`);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
352
|
+
const groupFiles = group.file_indices.map((index) => changes[index]);
|
|
280
353
|
|
|
281
|
-
const success = await commitGroup(group,
|
|
354
|
+
const success = await commitGroup(group, groupFiles, config);
|
|
282
355
|
if (success) {
|
|
283
356
|
successCount++;
|
|
284
357
|
}
|
|
285
358
|
}
|
|
286
359
|
|
|
287
|
-
//
|
|
288
|
-
logger.
|
|
360
|
+
// 4. 显示摘要
|
|
361
|
+
logger.separator('=', 60);
|
|
362
|
+
logger.success(`完成!成功提交 ${successCount}/${groups.length} 个群组`);
|
|
363
|
+
logger.separator('=', 60);
|
|
364
|
+
|
|
365
|
+
// 显示最近的几个 commits
|
|
366
|
+
console.log('\n📋 最近的 commits:');
|
|
367
|
+
execSync(`git log -${successCount} --oneline`, { stdio: 'inherit' });
|
|
289
368
|
|
|
290
|
-
//
|
|
291
|
-
console.log(chalk.cyan('\n📋 最近的 commits:'));
|
|
369
|
+
// Reset 任何剩余的 staged 文件
|
|
292
370
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
} catch (error) {
|
|
298
|
-
// 忽略顯示 commit 的錯誤
|
|
371
|
+
execSync('git reset HEAD -- .', { stdio: 'ignore' });
|
|
372
|
+
} catch (e) {
|
|
373
|
+
// 忽略
|
|
299
374
|
}
|
|
300
|
-
|
|
301
|
-
// Reset 任何剩餘的 staged 檔案
|
|
302
|
-
GitOperations.resetStaged();
|
|
303
|
-
|
|
304
375
|
} catch (error) {
|
|
305
376
|
handleError(error);
|
|
306
377
|
process.exit(1);
|