@git-ai/cli 1.0.2 → 1.0.3

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.
@@ -1,388 +1,366 @@
1
- import { execSync } from "child_process";
2
- import { collectGitExecLog } from "../utils/Log.mjs";
3
- import Logger from "../utils/Logger.mjs";
4
-
5
- const sanitizeExecOptions = (options = {}) => {
6
- return Object.entries(options).reduce((acc, [key, value]) => {
7
- if (typeof value === "function") {
8
- acc[key] = `[Function ${value.name || "anonymous"}]`;
9
- return acc;
10
- }
11
- if (value instanceof Buffer) {
12
- acc[key] = `[Buffer length=${value.length}]`;
13
- return acc;
14
- }
15
- acc[key] = value;
16
- return acc;
17
- }, {});
18
- };
19
-
20
- /**
21
- * Git 操作服务类
22
- * 封装所有 Git 相关操作
23
- */
24
- export class GitService {
25
- constructor() {}
26
-
27
- /**
28
- * 执行 Git 命令
29
- */
30
- exec(command, options = {}) {
31
- const execOptions = {
32
- encoding: "utf8",
33
- stdio: ["pipe", "pipe", "ignore"],
34
- ...options,
35
- };
36
-
37
- try {
38
- const result = execSync(command, execOptions);
39
- collectGitExecLog({
40
- command,
41
- options: sanitizeExecOptions(execOptions),
42
- status: "success",
43
- output: typeof result === "string" ? result : "",
44
- });
45
- return result;
46
- } catch (error) {
47
- collectGitExecLog({
48
- command,
49
- options: sanitizeExecOptions(execOptions),
50
- status: "error",
51
- error: error && error.message ? error.message : String(error),
52
- });
53
- throw error;
54
- }
55
- }
56
-
57
- /**
58
- * 检查是否安装 Git
59
- */
60
- checkInstalled() {
61
- try {
62
- const hasGit = this.exec("git --version").toString().trim();
63
- if (!hasGit) {
64
- throw "当前没有安装 git,请先安装 git...";
65
- }
66
- } catch (error) {
67
- throw error && error.message ? error.message : error;
68
- }
69
- }
70
-
71
- /**
72
- * 检查是否为 Git 仓库
73
- */
74
- checkRepository() {
75
- try {
76
- this.exec("git rev-parse --is-inside-work-tree");
77
- } catch (error) {
78
- throw `当前目录不是 git 仓库,请先初始化 git 仓库...\n${
79
- error && error.message ? error.message : error
80
- }`;
81
- }
82
- }
83
-
84
- /**
85
- * 获取工作目录前缀
86
- */
87
- getWorkingPrefix() {
88
- try {
89
- const workingPrefix =
90
- this.exec("git rev-parse --show-prefix", {
91
- stdio: ["pipe", "pipe", "ignore"],
92
- }) || "";
93
- return workingPrefix.trim();
94
- } catch (error) {
95
- Logger.warn(`${error && error.message ? error.message : error}`);
96
- return "";
97
- }
98
- }
99
-
100
- /**
101
- * 执行 git add
102
- */
103
- add() {
104
- this.exec("git add .", { stdio: ["pipe", "pipe", "ignore"] });
105
- }
106
-
107
- /**
108
- * 获取用户名
109
- */
110
- getUserName() {
111
- return this.exec("git config user.name").toString().trim();
112
- }
113
-
114
- /**
115
- * 获取差异字符串
116
- */
117
- getDiff(maxToken) {
118
- try {
119
- let diffString = this.exec("git diff --staged -U0", {
120
- stdio: ["pipe", "pipe", "ignore"],
121
- }).toString();
122
-
123
- if (!diffString || diffString.length <= maxToken) {
124
- return diffString.trim();
125
- }
126
- return this.exec("git diff --staged --stat", {
127
- stdio: ["pipe", "pipe", "ignore"],
128
- })
129
- .toString()
130
- .trim();
131
- } catch (e) {
132
- return this.exec("git diff --staged --stat", {
133
- stdio: ["pipe", "pipe", "ignore"],
134
- })
135
- .toString()
136
- .trim();
137
- }
138
- }
139
-
140
- /**
141
- * 获取 Git 状态
142
- */
143
- getStatus() {
144
- return this.exec("git status --porcelain");
145
- }
146
-
147
- /**
148
- * 执行 commit
149
- */
150
- async commit(commitMessage) {
151
- return new Promise((resolve, reject) => {
152
- try {
153
- execSync(commitMessage, {
154
- encoding: "utf8",
155
- stdio: ["pipe", "pipe", "pipe"],
156
- });
157
- resolve();
158
- } catch (error) {
159
- const stdout = this.normalizeExecOutput(error && error.stdout);
160
- const stderr = this.normalizeExecOutput(error && error.stderr);
161
- const messageParts = [
162
- error && error.message ? error.message : "",
163
- stderr,
164
- stdout,
165
- ].filter(Boolean);
166
- error.stdout = stdout;
167
- error.stderr = stderr;
168
- error.message = messageParts.join("\n").trim() || "git commit 失败";
169
- reject(error);
170
- }
171
- });
172
- }
173
-
174
- normalizeExecOutput(output) {
175
- if (!output) {
176
- return "";
177
- }
178
- if (typeof output === "string") {
179
- return output.trim();
180
- }
181
- if (Buffer.isBuffer(output)) {
182
- return output.toString("utf8").trim();
183
- }
184
- return "";
185
- }
186
-
187
- /**
188
- * 重置暂存区
189
- */
190
- reset() {
191
- this.exec("git reset", { stdio: ["pipe", "pipe", "ignore"] });
192
- }
193
-
194
- /**
195
- * 获取当前分支名
196
- */
197
- getCurrentBranch() {
198
- return this.exec("git rev-parse --abbrev-ref HEAD").trim();
199
- }
200
-
201
- /**
202
- * 获取远程仓库名称
203
- */
204
- async getRemoteName(currentBranch) {
205
- return new Promise((resolve, reject) => {
206
- let remoteName = "";
207
-
208
- // 优先从当前分支配置中读取 remote
209
- try {
210
- remoteName = this.exec(
211
- `git config --get branch.${currentBranch}.remote`,
212
- {
213
- stdio: ["pipe", "pipe", "ignore"],
214
- }
215
- ).trim();
216
- } catch {}
217
-
218
- if (!remoteName) {
219
- try {
220
- const upstream = this.exec(
221
- "git rev-parse --abbrev-ref --symbolic-full-name @{u}",
222
- {
223
- stdio: ["pipe", "pipe", "ignore"],
224
- }
225
- ).trim();
226
- if (upstream && upstream.includes("/")) {
227
- remoteName = upstream.split("/")[0];
228
- }
229
- } catch {}
230
- }
231
-
232
- // 回退:选择已存在的 remote(优先 origin)
233
- if (!remoteName) {
234
- try {
235
- const remotesOutput = this.exec("git remote", {
236
- stdio: ["pipe", "pipe", "ignore"],
237
- }).trim();
238
- const remotes = remotesOutput
239
- ? remotesOutput.split("\n").filter(Boolean)
240
- : [];
241
- if (remotes.includes("origin")) {
242
- remoteName = "origin";
243
- } else if (remotes.length > 0) {
244
- remoteName = remotes[0];
245
- }
246
- } catch (error) {
247
- reject(error);
248
- }
249
- }
250
-
251
- if (!remoteName) {
252
- reject("未找到任何远程仓库配置");
253
- }
254
-
255
- try {
256
- // 验证 remote 是否存在
257
- this.exec(`git remote show ${remoteName}`, {
258
- stdio: ["pipe", "pipe", "ignore"],
259
- });
260
- // 返回 remote
261
- resolve(remoteName);
262
- } catch (error) {
263
- reject(error);
264
- }
265
- });
266
- }
267
-
268
- /**
269
- * 检查是否处于合并状态
270
- */
271
- isMerging() {
272
- try {
273
- this.exec("git rev-parse --verify --quiet MERGE_HEAD", {
274
- stdio: ["pipe", "pipe", "ignore"],
275
- });
276
- return true;
277
- } catch {
278
- return false;
279
- }
280
- }
281
-
282
- /**
283
- * 完成合并
284
- */
285
- finishMerge() {
286
- this.exec("git commit --no-edit", {
287
- stdio: ["pipe", "pipe", "ignore"],
288
- });
289
- }
290
-
291
- /**
292
- * Fetch 远程分支
293
- */
294
- fetch(remoteName) {
295
- this.exec(`git fetch ${remoteName}`, {
296
- stdio: ["pipe", "pipe", "ignore"],
297
- });
298
- }
299
-
300
- /**
301
- * 合并远程分支
302
- */
303
- merge(remoteName, currentBranch) {
304
- this.exec(`git merge ${remoteName}/${currentBranch}`, {
305
- stdio: ["pipe", "pipe", "ignore"],
306
- });
307
- }
308
-
309
- /**
310
- * 获取本地领先远程的提交数
311
- */
312
- getAheadCount(remoteName, currentBranch) {
313
- const aheadOutput = this.exec(
314
- `git log --oneline HEAD..${remoteName}/${currentBranch}`,
315
- {
316
- stdio: ["pipe", "pipe", "ignore"],
317
- }
318
- ).trim();
319
- return aheadOutput
320
- ? aheadOutput.split("\n").filter((line) => line.trim()).length
321
- : 0;
322
- }
323
-
324
- /**
325
- * 获取需要推送的提交数
326
- */
327
- getPushCount(remoteName, currentBranch) {
328
- // 先验证远程分支是否存在
329
- try {
330
- this.exec(
331
- `git rev-parse --verify --quiet ${remoteName}/${currentBranch}`,
332
- {
333
- stdio: ["pipe", "pipe", "ignore"],
334
- }
335
- );
336
- } catch {
337
- return -1;
338
- }
339
-
340
- try {
341
- const pushOutput = this.exec(
342
- `git log --oneline ${remoteName}/${currentBranch}..HEAD`,
343
- {
344
- encoding: "utf8",
345
- stdio: ["pipe", "pipe", "ignore"],
346
- }
347
- ).trim();
348
- return pushOutput
349
- ? pushOutput.split("\n").filter((line) => line.trim()).length
350
- : 0;
351
- } catch {
352
- return 0;
353
- }
354
- }
355
-
356
- /**
357
- * 推送到远程
358
- */
359
- push(remoteName, currentBranch) {
360
- return new Promise((resolve, reject) => {
361
- try {
362
- this.exec(`git push -u ${remoteName} ${currentBranch}`, {
363
- stdio: ["pipe", "pipe", "ignore"],
364
- });
365
- resolve();
366
- } catch (error) {
367
- reject(error);
368
- }
369
- });
370
- }
371
-
372
- /**
373
- * 检查远程分支是否存在
374
- */
375
- remoteHasBranch(remoteName, currentBranch) {
376
- try {
377
- this.exec(
378
- `git rev-parse --verify --quiet ${remoteName}/${currentBranch}`,
379
- {
380
- stdio: ["pipe", "pipe", "ignore"],
381
- }
382
- );
383
- return true;
384
- } catch {
385
- return false;
386
- }
387
- }
388
- }
1
+ import { execSync } from 'child_process';
2
+ import { collectGitExecLog } from '../utils/Log.mjs';
3
+ import Logger from '../utils/Logger.mjs';
4
+
5
+ const sanitizeExecOptions = (options = {}) => {
6
+ return Object.entries(options).reduce((acc, [key, value]) => {
7
+ if (typeof value === 'function') {
8
+ acc[key] = `[Function ${value.name || 'anonymous'}]`;
9
+ return acc;
10
+ }
11
+ if (value instanceof Buffer) {
12
+ acc[key] = `[Buffer length=${value.length}]`;
13
+ return acc;
14
+ }
15
+ acc[key] = value;
16
+ return acc;
17
+ }, {});
18
+ };
19
+
20
+ /**
21
+ * Git 操作服务类
22
+ * 封装所有 Git 相关操作
23
+ */
24
+ export class GitService {
25
+ constructor() {}
26
+
27
+ /**
28
+ * 执行 Git 命令
29
+ */
30
+ exec(command, options = {}) {
31
+ const execOptions = {
32
+ encoding: 'utf8',
33
+ stdio: ['pipe', 'pipe', 'ignore'],
34
+ ...options,
35
+ };
36
+
37
+ try {
38
+ const result = execSync(command, execOptions);
39
+ collectGitExecLog({
40
+ command,
41
+ options: sanitizeExecOptions(execOptions),
42
+ status: 'success',
43
+ output: typeof result === 'string' ? result : '',
44
+ });
45
+ return result;
46
+ } catch (error) {
47
+ collectGitExecLog({
48
+ command,
49
+ options: sanitizeExecOptions(execOptions),
50
+ status: 'error',
51
+ error: error && error.message ? error.message : String(error),
52
+ });
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 检查是否安装 Git
59
+ */
60
+ checkInstalled() {
61
+ try {
62
+ const hasGit = this.exec('git --version').toString().trim();
63
+ if (!hasGit) {
64
+ throw '当前没有安装 git,请先安装 git...';
65
+ }
66
+ } catch (error) {
67
+ throw error && error.message ? error.message : error;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 检查是否为 Git 仓库
73
+ */
74
+ checkRepository() {
75
+ try {
76
+ this.exec('git rev-parse --is-inside-work-tree');
77
+ } catch (error) {
78
+ throw `当前目录不是 git 仓库,请先初始化 git 仓库...\n${
79
+ error && error.message ? error.message : error
80
+ }`;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 获取工作目录前缀
86
+ */
87
+ getWorkingPrefix() {
88
+ try {
89
+ const workingPrefix =
90
+ this.exec('git rev-parse --show-prefix', {
91
+ stdio: ['pipe', 'pipe', 'ignore'],
92
+ }) || '';
93
+ return workingPrefix.trim();
94
+ } catch (error) {
95
+ Logger.warn(`${error && error.message ? error.message : error}`);
96
+ return '';
97
+ }
98
+ }
99
+
100
+ /**
101
+ * 执行 git add
102
+ */
103
+ add() {
104
+ this.exec('git add .', { stdio: ['pipe', 'pipe', 'ignore'] });
105
+ }
106
+
107
+ /**
108
+ * 获取用户名
109
+ */
110
+ getUserName() {
111
+ return this.exec('git config user.name').toString().trim();
112
+ }
113
+
114
+ /**
115
+ * 获取差异字符串
116
+ */
117
+ getDiff(maxToken) {
118
+ try {
119
+ let diffString = this.exec('git diff --staged -U0', {
120
+ stdio: ['pipe', 'pipe', 'ignore'],
121
+ }).toString();
122
+
123
+ if (!diffString || diffString.length <= maxToken) {
124
+ return diffString.trim();
125
+ }
126
+ return this.exec('git diff --staged --stat', {
127
+ stdio: ['pipe', 'pipe', 'ignore'],
128
+ })
129
+ .toString()
130
+ .trim();
131
+ } catch (e) {
132
+ return this.exec('git diff --staged --stat', {
133
+ stdio: ['pipe', 'pipe', 'ignore'],
134
+ })
135
+ .toString()
136
+ .trim();
137
+ }
138
+ }
139
+
140
+ /**
141
+ * 获取 Git 状态
142
+ */
143
+ getStatus() {
144
+ return this.exec('git status --porcelain');
145
+ }
146
+
147
+ /**
148
+ * 执行 commit
149
+ */
150
+ async commit(commitMessage) {
151
+ return new Promise((resolve, reject) => {
152
+ try {
153
+ execSync(commitMessage, {
154
+ encoding: 'utf8',
155
+ stdio: ['pipe', 'pipe', 'pipe'],
156
+ });
157
+ resolve();
158
+ } catch (error) {
159
+ const stdout = this.normalizeExecOutput(error && error.stdout);
160
+ const stderr = this.normalizeExecOutput(error && error.stderr);
161
+ const messageParts = [error && error.message ? error.message : '', stderr, stdout].filter(
162
+ Boolean
163
+ );
164
+ error.stdout = stdout;
165
+ error.stderr = stderr;
166
+ error.message = messageParts.join('\n').trim() || 'git commit 失败';
167
+ reject(error);
168
+ }
169
+ });
170
+ }
171
+
172
+ normalizeExecOutput(output) {
173
+ if (!output) {
174
+ return '';
175
+ }
176
+ if (typeof output === 'string') {
177
+ return output.trim();
178
+ }
179
+ if (Buffer.isBuffer(output)) {
180
+ return output.toString('utf8').trim();
181
+ }
182
+ return '';
183
+ }
184
+
185
+ /**
186
+ * 重置暂存区
187
+ */
188
+ reset() {
189
+ this.exec('git reset', { stdio: ['pipe', 'pipe', 'ignore'] });
190
+ }
191
+
192
+ /**
193
+ * 获取当前分支名
194
+ */
195
+ getCurrentBranch() {
196
+ return this.exec('git rev-parse --abbrev-ref HEAD').trim();
197
+ }
198
+
199
+ /**
200
+ * 获取远程仓库名称
201
+ */
202
+ async getRemoteName(currentBranch) {
203
+ return new Promise((resolve, reject) => {
204
+ let remoteName = '';
205
+
206
+ // 优先从当前分支配置中读取 remote
207
+ try {
208
+ remoteName = this.exec(`git config --get branch.${currentBranch}.remote`, {
209
+ stdio: ['pipe', 'pipe', 'ignore'],
210
+ }).trim();
211
+ } catch {
212
+ Logger.warn(`未找到当前分支 ${currentBranch} 的 remote 配置`);
213
+ }
214
+
215
+ if (!remoteName) {
216
+ try {
217
+ const upstream = this.exec('git rev-parse --abbrev-ref --symbolic-full-name @{u}', {
218
+ stdio: ['pipe', 'pipe', 'ignore'],
219
+ }).trim();
220
+ if (upstream && upstream.includes('/')) {
221
+ remoteName = upstream.split('/')[0];
222
+ }
223
+ } catch {
224
+ Logger.warn(`未找到当前分支 ${currentBranch} 的 upstream 配置`);
225
+ }
226
+ }
227
+
228
+ // 回退:选择已存在的 remote(优先 origin)
229
+ if (!remoteName) {
230
+ try {
231
+ const remotesOutput = this.exec('git remote', {
232
+ stdio: ['pipe', 'pipe', 'ignore'],
233
+ }).trim();
234
+ const remotes = remotesOutput ? remotesOutput.split('\n').filter(Boolean) : [];
235
+ if (remotes.includes('origin')) {
236
+ remoteName = 'origin';
237
+ } else if (remotes.length > 0) {
238
+ remoteName = remotes[0];
239
+ }
240
+ } catch (error) {
241
+ reject(error);
242
+ }
243
+ }
244
+
245
+ if (!remoteName) {
246
+ reject('未找到任何远程仓库配置');
247
+ }
248
+
249
+ try {
250
+ // 验证 remote 是否存在
251
+ this.exec(`git remote show ${remoteName}`, {
252
+ stdio: ['pipe', 'pipe', 'ignore'],
253
+ });
254
+ // 返回 remote
255
+ resolve(remoteName);
256
+ } catch (error) {
257
+ reject(error);
258
+ }
259
+ });
260
+ }
261
+
262
+ /**
263
+ * 检查是否处于合并状态
264
+ */
265
+ isMerging() {
266
+ try {
267
+ this.exec('git rev-parse --verify --quiet MERGE_HEAD', {
268
+ stdio: ['pipe', 'pipe', 'ignore'],
269
+ });
270
+ return true;
271
+ } catch {
272
+ return false;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * 完成合并
278
+ */
279
+ finishMerge() {
280
+ this.exec('git commit --no-edit', {
281
+ stdio: ['pipe', 'pipe', 'ignore'],
282
+ });
283
+ }
284
+
285
+ /**
286
+ * Fetch 远程分支
287
+ */
288
+ fetch(remoteName) {
289
+ this.exec(`git fetch ${remoteName}`, {
290
+ stdio: ['pipe', 'pipe', 'ignore'],
291
+ });
292
+ }
293
+
294
+ /**
295
+ * 合并远程分支
296
+ */
297
+ merge(remoteName, currentBranch) {
298
+ this.exec(`git merge ${remoteName}/${currentBranch}`, {
299
+ stdio: ['pipe', 'pipe', 'ignore'],
300
+ });
301
+ }
302
+
303
+ /**
304
+ * 获取本地领先远程的提交数
305
+ */
306
+ getAheadCount(remoteName, currentBranch) {
307
+ const aheadOutput = this.exec(`git log --oneline HEAD..${remoteName}/${currentBranch}`, {
308
+ stdio: ['pipe', 'pipe', 'ignore'],
309
+ }).trim();
310
+ return aheadOutput ? aheadOutput.split('\n').filter((line) => line.trim()).length : 0;
311
+ }
312
+
313
+ /**
314
+ * 获取需要推送的提交数
315
+ */
316
+ getPushCount(remoteName, currentBranch) {
317
+ // 先验证远程分支是否存在
318
+ try {
319
+ this.exec(`git rev-parse --verify --quiet ${remoteName}/${currentBranch}`, {
320
+ stdio: ['pipe', 'pipe', 'ignore'],
321
+ });
322
+ } catch {
323
+ return -1;
324
+ }
325
+
326
+ try {
327
+ const pushOutput = this.exec(`git log --oneline ${remoteName}/${currentBranch}..HEAD`, {
328
+ encoding: 'utf8',
329
+ stdio: ['pipe', 'pipe', 'ignore'],
330
+ }).trim();
331
+ return pushOutput ? pushOutput.split('\n').filter((line) => line.trim()).length : 0;
332
+ } catch {
333
+ return 0;
334
+ }
335
+ }
336
+
337
+ /**
338
+ * 推送到远程
339
+ */
340
+ push(remoteName, currentBranch) {
341
+ return new Promise((resolve, reject) => {
342
+ try {
343
+ this.exec(`git push -u ${remoteName} ${currentBranch}`, {
344
+ stdio: ['pipe', 'pipe', 'ignore'],
345
+ });
346
+ resolve();
347
+ } catch (error) {
348
+ reject(error);
349
+ }
350
+ });
351
+ }
352
+
353
+ /**
354
+ * 检查远程分支是否存在
355
+ */
356
+ remoteHasBranch(remoteName, currentBranch) {
357
+ try {
358
+ this.exec(`git rev-parse --verify --quiet ${remoteName}/${currentBranch}`, {
359
+ stdio: ['pipe', 'pipe', 'ignore'],
360
+ });
361
+ return true;
362
+ } catch {
363
+ return false;
364
+ }
365
+ }
366
+ }