create-branch-cli 1.0.1 → 1.0.2
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 +105 -12
- package/index.js +1240 -396
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -1,396 +1,1240 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { execSync } = require('child_process');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const readline = require('readline');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
config
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
return
|
|
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
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
log(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const http = require('http');
|
|
9
|
+
|
|
10
|
+
// 配置文件路径(存储在用户主目录)
|
|
11
|
+
const CONFIG_DIR = path.join(require('os').homedir(), '.create-branch');
|
|
12
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
13
|
+
|
|
14
|
+
// 工具函数
|
|
15
|
+
function log(message) {
|
|
16
|
+
console.log(`[${new Date().toLocaleTimeString()}] ${message}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function error(message) {
|
|
20
|
+
console.error(`[错误] ${message}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function success(message) {
|
|
24
|
+
console.log(`✅ ${message}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 验证分支名是否合法(防止命令注入)
|
|
28
|
+
function validateBranchName(branchName) {
|
|
29
|
+
if (!branchName || !branchName.trim()) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
// Git 分支名规则:不能包含空格、不能以 . 开头、不能包含 ..、不能包含 ~^:?*[]\、不能以 / 结尾
|
|
33
|
+
// 允许:字母、数字、下划线、连字符、斜杠(用于分隔)
|
|
34
|
+
const branchNameRegex = /^[a-zA-Z0-9._/-]+$/;
|
|
35
|
+
const trimmed = branchName.trim();
|
|
36
|
+
|
|
37
|
+
// 基本规则检查
|
|
38
|
+
if (trimmed.startsWith('.') ||
|
|
39
|
+
trimmed.includes('..') ||
|
|
40
|
+
trimmed.endsWith('/') ||
|
|
41
|
+
trimmed.endsWith('.') ||
|
|
42
|
+
trimmed.includes('//')) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 检查是否包含危险字符
|
|
47
|
+
if (!branchNameRegex.test(trimmed)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 检查长度(Git 限制分支名长度)
|
|
52
|
+
if (trimmed.length > 250) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 转义 shell 命令中的特殊字符(用于安全执行 git 命令)
|
|
60
|
+
function escapeShellArg(arg) {
|
|
61
|
+
// 对于简单的分支名,使用引号包裹即可
|
|
62
|
+
// 但为了更安全,我们验证分支名合法性,然后直接使用
|
|
63
|
+
// 因为我们已经验证了分支名,所以这里主要是添加引号防止空格问题
|
|
64
|
+
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 读取配置文件
|
|
68
|
+
function readConfig() {
|
|
69
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
74
|
+
return JSON.parse(content);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
error(`读取配置文件失败: ${err.message}`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 保存配置文件
|
|
82
|
+
function saveConfig(config) {
|
|
83
|
+
try {
|
|
84
|
+
// 确保配置目录存在
|
|
85
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
86
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
89
|
+
return true;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
error(`保存配置文件失败: ${err.message}`);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 交互式输入
|
|
97
|
+
function question(query) {
|
|
98
|
+
const rl = readline.createInterface({
|
|
99
|
+
input: process.stdin,
|
|
100
|
+
output: process.stdout
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return new Promise(resolve => {
|
|
104
|
+
rl.question(query, answer => {
|
|
105
|
+
rl.close();
|
|
106
|
+
resolve(answer);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 首次使用,录入用户信息
|
|
112
|
+
async function initUserInfo() {
|
|
113
|
+
console.log('\n=== 首次使用,需要录入您的信息 ===\n');
|
|
114
|
+
|
|
115
|
+
const workId = await question('请输入工号: ');
|
|
116
|
+
if (!workId || !workId.trim()) {
|
|
117
|
+
error('工号不能为空');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const name = await question('请输入姓名: ');
|
|
122
|
+
if (!name || !name.trim()) {
|
|
123
|
+
error('姓名不能为空');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const autoSyncAnswer = await question('是否启用合并后自动拉取主分支?(y/n,默认 n): ');
|
|
128
|
+
const autoSyncMaster = autoSyncAnswer.toLowerCase() === 'y' || autoSyncAnswer.toLowerCase() === 'yes';
|
|
129
|
+
|
|
130
|
+
const config = {
|
|
131
|
+
workId: workId.trim(),
|
|
132
|
+
name: name.trim(),
|
|
133
|
+
autoSyncMaster: autoSyncMaster
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// 如果启用了自动同步,需要配置 GitLab 信息
|
|
137
|
+
if (autoSyncMaster) {
|
|
138
|
+
console.log('\n需要配置 GitLab 信息以支持自动同步功能');
|
|
139
|
+
const gitlabUrl = await question('请输入 GitLab 地址(例如:https://gitlab.com): ');
|
|
140
|
+
const gitlabToken = await question('请输入 GitLab Personal Access Token: ');
|
|
141
|
+
|
|
142
|
+
if (gitlabUrl && gitlabUrl.trim() && gitlabToken && gitlabToken.trim()) {
|
|
143
|
+
config.gitlab = {
|
|
144
|
+
url: gitlabUrl.trim().replace(/\/$/, ''), // 移除末尾的斜杠
|
|
145
|
+
token: gitlabToken.trim()
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (saveConfig(config)) {
|
|
151
|
+
success('用户信息已保存!');
|
|
152
|
+
return config;
|
|
153
|
+
} else {
|
|
154
|
+
error('保存用户信息失败');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 获取当前日期(格式:YYYYMMDD)
|
|
160
|
+
function getCurrentDate() {
|
|
161
|
+
const now = new Date();
|
|
162
|
+
const year = now.getFullYear();
|
|
163
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
164
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
165
|
+
return `${year}${month}${day}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 生成分支名
|
|
169
|
+
function generateBranchName(workId, name, title, isBug) {
|
|
170
|
+
const prefix = isBug ? 'hotfix' : 'feature';
|
|
171
|
+
const date = getCurrentDate();
|
|
172
|
+
// 将需求标题中的空格替换为下划线,并移除特殊字符
|
|
173
|
+
const cleanTitle = title.trim().replace(/\s+/g, '_').replace(/[^\w\u4e00-\u9fa5_-]/g, '');
|
|
174
|
+
return `${prefix}/${workId}_${name}_${cleanTitle}_${date}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 检查是否在 Git 仓库中
|
|
178
|
+
function isGitRepo() {
|
|
179
|
+
try {
|
|
180
|
+
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
181
|
+
return true;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 检查分支是否已存在
|
|
188
|
+
function branchExists(branchName) {
|
|
189
|
+
try {
|
|
190
|
+
const branches = execSync('git branch -a', { encoding: 'utf-8' });
|
|
191
|
+
const branchList = branches.split('\n').map(b => b.trim().replace(/^\*\s*/, '').replace(/^remotes\/origin\//, ''));
|
|
192
|
+
return branchList.includes(branchName);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 获取当前分支名
|
|
199
|
+
function getCurrentBranch() {
|
|
200
|
+
try {
|
|
201
|
+
const branch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
|
|
202
|
+
return branch;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
error('无法获取当前分支');
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 获取当前项目的唯一标识(用于区分不同项目的需求信息)
|
|
210
|
+
function getProjectId() {
|
|
211
|
+
try {
|
|
212
|
+
// 使用 Git 远程仓库 URL 作为项目标识
|
|
213
|
+
const remoteUrl = getRemoteUrl();
|
|
214
|
+
if (remoteUrl) {
|
|
215
|
+
// 提取项目路径部分作为标识
|
|
216
|
+
const projectPath = parseGitLabProjectPath(remoteUrl);
|
|
217
|
+
if (projectPath) {
|
|
218
|
+
return projectPath.projectPath.replace(/\//g, '_');
|
|
219
|
+
}
|
|
220
|
+
// 如果没有 GitLab 格式,使用整个 URL(去除协议和 .git)
|
|
221
|
+
return remoteUrl.replace(/^https?:\/\//, '').replace(/\.git$/, '').replace(/[\/:]/g, '_');
|
|
222
|
+
}
|
|
223
|
+
// 如果没有远程仓库,使用当前工作目录的绝对路径
|
|
224
|
+
return require('crypto').createHash('md5').update(process.cwd()).digest('hex').substring(0, 8);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// 如果出错,使用当前目录的哈希值
|
|
227
|
+
return require('crypto').createHash('md5').update(process.cwd()).digest('hex').substring(0, 8);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 获取项目的目标分支名
|
|
232
|
+
function getProjectTargetBranch(config) {
|
|
233
|
+
if (!config) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const projectId = getProjectId();
|
|
237
|
+
if (config.projects && config.projects[projectId] && config.projects[projectId].targetBranch) {
|
|
238
|
+
return config.projects[projectId].targetBranch;
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 保存项目的目标分支名
|
|
244
|
+
function saveProjectTargetBranch(config, targetBranch) {
|
|
245
|
+
if (!config) {
|
|
246
|
+
error('配置对象不能为空');
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (!validateBranchName(targetBranch)) {
|
|
250
|
+
error('目标分支名不合法,请使用字母、数字、下划线、连字符和斜杠');
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const projectId = getProjectId();
|
|
254
|
+
if (!config.projects) {
|
|
255
|
+
config.projects = {};
|
|
256
|
+
}
|
|
257
|
+
if (!config.projects[projectId]) {
|
|
258
|
+
config.projects[projectId] = {};
|
|
259
|
+
}
|
|
260
|
+
config.projects[projectId].targetBranch = targetBranch.trim();
|
|
261
|
+
return saveConfig(config);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 保存需求信息到配置
|
|
265
|
+
function saveRequirementInfo(config, branchName, requirementId, title) {
|
|
266
|
+
if (!config) {
|
|
267
|
+
error('配置对象不能为空');
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
if (!validateBranchName(branchName)) {
|
|
271
|
+
error(`分支名不合法: ${branchName}`);
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const projectId = getProjectId();
|
|
275
|
+
if (!config.requirements) {
|
|
276
|
+
config.requirements = {};
|
|
277
|
+
}
|
|
278
|
+
if (!config.requirements[projectId]) {
|
|
279
|
+
config.requirements[projectId] = {};
|
|
280
|
+
}
|
|
281
|
+
config.requirements[projectId][branchName] = {
|
|
282
|
+
id: requirementId,
|
|
283
|
+
title: title,
|
|
284
|
+
branchName: branchName
|
|
285
|
+
};
|
|
286
|
+
return saveConfig(config);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 获取需求信息
|
|
290
|
+
function getRequirementInfo(config, branchName) {
|
|
291
|
+
if (!config || !config.requirements) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const projectId = getProjectId();
|
|
295
|
+
if (config.requirements[projectId] && config.requirements[projectId][branchName]) {
|
|
296
|
+
return config.requirements[projectId][branchName];
|
|
297
|
+
}
|
|
298
|
+
// 兼容旧格式(全局存储的需求信息)
|
|
299
|
+
if (config.requirements[branchName]) {
|
|
300
|
+
return config.requirements[branchName];
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 创建并切换到新分支
|
|
306
|
+
function createBranch(branchName) {
|
|
307
|
+
try {
|
|
308
|
+
// 检查分支是否已存在
|
|
309
|
+
if (branchExists(branchName)) {
|
|
310
|
+
error(`分支 ${branchName} 已存在!`);
|
|
311
|
+
const rl = readline.createInterface({
|
|
312
|
+
input: process.stdin,
|
|
313
|
+
output: process.stdout
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return new Promise((resolve, reject) => {
|
|
317
|
+
rl.question('是否切换到已存在的分支?(y/n): ', answer => {
|
|
318
|
+
rl.close();
|
|
319
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
320
|
+
try {
|
|
321
|
+
// 验证分支名合法性
|
|
322
|
+
if (!validateBranchName(branchName)) {
|
|
323
|
+
error(`分支名不合法: ${branchName}`);
|
|
324
|
+
reject(new Error('分支名不合法'));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
log(`切换到分支: ${branchName}`);
|
|
328
|
+
execSync(`git checkout ${escapeShellArg(branchName)}`, { stdio: 'inherit' });
|
|
329
|
+
success(`已切换到分支: ${branchName}`);
|
|
330
|
+
resolve(true);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
error(`切换分支失败: ${err.message}`);
|
|
333
|
+
reject(err);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
log('已取消操作');
|
|
337
|
+
reject(new Error('用户取消'));
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 验证分支名合法性
|
|
344
|
+
if (!validateBranchName(branchName)) {
|
|
345
|
+
error(`分支名不合法: ${branchName}`);
|
|
346
|
+
throw new Error('分支名不合法');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 创建新分支(使用引号包裹分支名,防止空格问题)
|
|
350
|
+
log(`创建新分支: ${branchName}`);
|
|
351
|
+
execSync(`git checkout -b ${escapeShellArg(branchName)}`, { stdio: 'inherit' });
|
|
352
|
+
success(`已创建并切换到分支: ${branchName}`);
|
|
353
|
+
return Promise.resolve(true);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
error(`创建分支失败: ${err.message}`);
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 设置项目的目标分支名
|
|
361
|
+
async function setProjectTargetBranch() {
|
|
362
|
+
try {
|
|
363
|
+
// 检查是否在 Git 仓库中
|
|
364
|
+
if (!isGitRepo()) {
|
|
365
|
+
error('当前目录不是 Git 仓库!');
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const config = readConfig();
|
|
370
|
+
if (!config) {
|
|
371
|
+
error('配置不存在,请先运行 cb config 进行初始化');
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const currentTargetBranch = getProjectTargetBranch(config);
|
|
376
|
+
console.log('\n=== 设置项目的目标分支名 ===\n');
|
|
377
|
+
|
|
378
|
+
if (currentTargetBranch) {
|
|
379
|
+
console.log(`当前目标分支: ${currentTargetBranch}`);
|
|
380
|
+
} else {
|
|
381
|
+
console.log('当前未设置目标分支');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const targetBranch = await question('请输入新的目标分支名(例如:master、main): ');
|
|
385
|
+
if (!targetBranch || !targetBranch.trim()) {
|
|
386
|
+
error('目标分支名不能为空');
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!validateBranchName(targetBranch)) {
|
|
391
|
+
error('目标分支名不合法,请使用字母、数字、下划线、连字符和斜杠');
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (saveProjectTargetBranch(config, targetBranch.trim())) {
|
|
396
|
+
success(`已设置目标分支: ${targetBranch.trim()}`);
|
|
397
|
+
return true;
|
|
398
|
+
} else {
|
|
399
|
+
error('保存目标分支失败');
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
} catch (err) {
|
|
403
|
+
error(`设置目标分支失败: ${err.message}`);
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 配置用户信息
|
|
409
|
+
async function configUserInfo() {
|
|
410
|
+
try {
|
|
411
|
+
console.log('\n=== 配置用户信息 ===\n');
|
|
412
|
+
|
|
413
|
+
const workId = await question('请输入新的工号: ');
|
|
414
|
+
if (!workId || !workId.trim()) {
|
|
415
|
+
error('工号不能为空');
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const name = await question('请输入新的姓名: ');
|
|
420
|
+
if (!name || !name.trim()) {
|
|
421
|
+
error('姓名不能为空');
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 读取现有配置(保留需求信息和现有设置)
|
|
426
|
+
let config = readConfig() || {};
|
|
427
|
+
|
|
428
|
+
// 询问是否启用自动同步主分支
|
|
429
|
+
const currentSyncSetting = config.autoSyncMaster ? '已启用' : '未启用';
|
|
430
|
+
const autoSyncAnswer = await question(`是否启用合并后自动拉取主分支?(当前: ${currentSyncSetting}, y/n): `);
|
|
431
|
+
const autoSyncMaster = autoSyncAnswer.toLowerCase() === 'y' || autoSyncAnswer.toLowerCase() === 'yes';
|
|
432
|
+
|
|
433
|
+
// 更新用户信息
|
|
434
|
+
config.workId = workId.trim();
|
|
435
|
+
config.name = name.trim();
|
|
436
|
+
config.autoSyncMaster = autoSyncMaster;
|
|
437
|
+
|
|
438
|
+
// 移除旧的 autoSwitchToMaster 配置(如果存在)
|
|
439
|
+
if (config.autoSwitchToMaster !== undefined) {
|
|
440
|
+
delete config.autoSwitchToMaster;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 如果启用了自动同步,需要配置或更新 GitLab 信息
|
|
444
|
+
if (autoSyncMaster) {
|
|
445
|
+
if (!config.gitlab || !config.gitlab.url || !config.gitlab.token) {
|
|
446
|
+
console.log('\n需要配置 GitLab 信息');
|
|
447
|
+
const gitlabUrl = await question('请输入 GitLab 地址(例如:https://gitlab.com): ');
|
|
448
|
+
const gitlabToken = await question('请输入 GitLab Personal Access Token: ');
|
|
449
|
+
|
|
450
|
+
if (gitlabUrl && gitlabUrl.trim() && gitlabToken && gitlabToken.trim()) {
|
|
451
|
+
config.gitlab = {
|
|
452
|
+
url: gitlabUrl.trim().replace(/\/$/, ''),
|
|
453
|
+
token: gitlabToken.trim()
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
const updateGitlab = await question('是否更新 GitLab 配置?(y/n,默认 n): ');
|
|
458
|
+
if (updateGitlab.toLowerCase() === 'y' || updateGitlab.toLowerCase() === 'yes') {
|
|
459
|
+
const gitlabUrl = await question(`请输入 GitLab 地址(当前: ${config.gitlab.url}): `);
|
|
460
|
+
const gitlabToken = await question('请输入 GitLab Personal Access Token(留空保持原值): ');
|
|
461
|
+
|
|
462
|
+
if (gitlabUrl && gitlabUrl.trim()) {
|
|
463
|
+
config.gitlab.url = gitlabUrl.trim().replace(/\/$/, '');
|
|
464
|
+
}
|
|
465
|
+
if (gitlabToken && gitlabToken.trim()) {
|
|
466
|
+
config.gitlab.token = gitlabToken.trim();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (saveConfig(config)) {
|
|
473
|
+
success('用户信息已更新!');
|
|
474
|
+
console.log(`工号: ${config.workId}`);
|
|
475
|
+
console.log(`姓名: ${config.name}`);
|
|
476
|
+
console.log(`自动同步主分支: ${autoSyncMaster ? '已启用' : '未启用'}`);
|
|
477
|
+
return true;
|
|
478
|
+
} else {
|
|
479
|
+
error('保存用户信息失败');
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
} catch (err) {
|
|
483
|
+
error(`配置用户信息失败: ${err.message}`);
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 获取 Git 远程仓库 URL
|
|
489
|
+
function getRemoteUrl() {
|
|
490
|
+
try {
|
|
491
|
+
const url = execSync('git config --get remote.origin.url', { encoding: 'utf-8' }).trim();
|
|
492
|
+
return url;
|
|
493
|
+
} catch (err) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 从 Git URL 解析 GitLab 项目路径
|
|
499
|
+
function parseGitLabProjectPath(gitUrl) {
|
|
500
|
+
// 支持多种格式:
|
|
501
|
+
// https://gitlab.com/group/project.git
|
|
502
|
+
// git@gitlab.com:group/project.git
|
|
503
|
+
// https://gitlab.example.com/group/project.git
|
|
504
|
+
|
|
505
|
+
let match;
|
|
506
|
+
if (gitUrl.startsWith('http://') || gitUrl.startsWith('https://')) {
|
|
507
|
+
match = gitUrl.match(/https?:\/\/([^\/]+)\/(.+?)(?:\.git)?$/);
|
|
508
|
+
if (match) {
|
|
509
|
+
return {
|
|
510
|
+
host: match[1],
|
|
511
|
+
projectPath: match[2]
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
} else if (gitUrl.startsWith('git@')) {
|
|
515
|
+
match = gitUrl.match(/git@([^:]+):(.+?)(?:\.git)?$/);
|
|
516
|
+
if (match) {
|
|
517
|
+
return {
|
|
518
|
+
host: match[1],
|
|
519
|
+
projectPath: match[2]
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 调用 GitLab API
|
|
527
|
+
function gitlabApiRequest(config, endpoint, method = 'GET', data = null) {
|
|
528
|
+
return new Promise((resolve, reject) => {
|
|
529
|
+
if (!config.gitlab || !config.gitlab.url || !config.gitlab.token) {
|
|
530
|
+
reject(new Error('GitLab 配置不完整,请运行 cb config 进行配置'));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const projectPath = parseGitLabProjectPath(getRemoteUrl());
|
|
535
|
+
if (!projectPath) {
|
|
536
|
+
reject(new Error('无法解析 GitLab 项目路径'));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// 构建完整的 API URL
|
|
541
|
+
const apiUrl = `${config.gitlab.url}/api/v4/projects/${encodeURIComponent(projectPath.projectPath)}${endpoint}`;
|
|
542
|
+
const url = new URL(apiUrl);
|
|
543
|
+
|
|
544
|
+
const options = {
|
|
545
|
+
hostname: url.hostname,
|
|
546
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
547
|
+
path: url.pathname + url.search,
|
|
548
|
+
method: method,
|
|
549
|
+
headers: {
|
|
550
|
+
'PRIVATE-TOKEN': config.gitlab.token,
|
|
551
|
+
'Content-Type': 'application/json'
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
556
|
+
|
|
557
|
+
const req = protocol.request(options, (res) => {
|
|
558
|
+
let responseData = '';
|
|
559
|
+
|
|
560
|
+
res.on('data', (chunk) => {
|
|
561
|
+
responseData += chunk;
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
res.on('end', () => {
|
|
565
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
566
|
+
try {
|
|
567
|
+
resolve(JSON.parse(responseData));
|
|
568
|
+
} catch (err) {
|
|
569
|
+
resolve(responseData);
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
reject(new Error(`GitLab API 错误: ${res.statusCode} - ${responseData}`));
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
req.on('error', (err) => {
|
|
578
|
+
reject(err);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// 如果有请求体数据,发送数据
|
|
582
|
+
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
|
583
|
+
req.write(JSON.stringify(data));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
req.end();
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// 创建合并请求
|
|
591
|
+
async function createMergeRequest(sourceBranch, targetBranch, title, description = '') {
|
|
592
|
+
try {
|
|
593
|
+
const config = readConfig();
|
|
594
|
+
if (!config || !config.gitlab || !config.gitlab.url || !config.gitlab.token) {
|
|
595
|
+
throw new Error('GitLab 配置不完整,请运行 cb config 进行配置');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// 检查是否已存在合并请求
|
|
599
|
+
const existingMRs = await gitlabApiRequest(config, `/merge_requests?source_branch=${encodeURIComponent(sourceBranch)}&state=opened`);
|
|
600
|
+
if (existingMRs && existingMRs.length > 0) {
|
|
601
|
+
log(`合并请求已存在: MR #${existingMRs[0].iid}`);
|
|
602
|
+
return existingMRs[0];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// 创建新的合并请求
|
|
606
|
+
const mrData = {
|
|
607
|
+
source_branch: sourceBranch,
|
|
608
|
+
target_branch: targetBranch,
|
|
609
|
+
title: title,
|
|
610
|
+
description: description,
|
|
611
|
+
remove_source_branch: true, // 自动删除源分支
|
|
612
|
+
squash: true // 合并时压缩提交
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
log('创建合并请求...');
|
|
616
|
+
const mergeRequest = await gitlabApiRequest(config, '/merge_requests', 'POST', mrData);
|
|
617
|
+
success(`合并请求已创建: MR #${mergeRequest.iid}`);
|
|
618
|
+
return mergeRequest;
|
|
619
|
+
} catch (err) {
|
|
620
|
+
error(`创建合并请求失败: ${err.message}`);
|
|
621
|
+
throw err;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// 打开浏览器
|
|
626
|
+
function openBrowser(url) {
|
|
627
|
+
try {
|
|
628
|
+
// 验证 URL 格式
|
|
629
|
+
if (!url || typeof url !== 'string') {
|
|
630
|
+
error('URL 无效');
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// 基本 URL 验证
|
|
635
|
+
try {
|
|
636
|
+
new URL(url);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
error(`URL 格式无效: ${url}`);
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const platform = process.platform;
|
|
643
|
+
let command;
|
|
644
|
+
|
|
645
|
+
if (platform === 'win32') {
|
|
646
|
+
// Windows: 转义 URL 中的特殊字符
|
|
647
|
+
const escapedUrl = url.replace(/"/g, '\\"');
|
|
648
|
+
command = `start "" "${escapedUrl}"`;
|
|
649
|
+
} else if (platform === 'darwin') {
|
|
650
|
+
// macOS: 转义 URL 中的特殊字符
|
|
651
|
+
const escapedUrl = url.replace(/"/g, '\\"');
|
|
652
|
+
command = `open "${escapedUrl}"`;
|
|
653
|
+
} else {
|
|
654
|
+
// Linux: 转义 URL 中的特殊字符
|
|
655
|
+
const escapedUrl = url.replace(/"/g, '\\"');
|
|
656
|
+
command = `xdg-open "${escapedUrl}"`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
execSync(command, { stdio: 'ignore' });
|
|
660
|
+
return true;
|
|
661
|
+
} catch (err) {
|
|
662
|
+
log(`无法自动打开浏览器: ${err.message}`);
|
|
663
|
+
log(`请手动访问: ${url}`);
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// 检查合并请求状态
|
|
669
|
+
async function checkMergeRequestStatus(branchName, config) {
|
|
670
|
+
try {
|
|
671
|
+
// 先查询所有状态的合并请求(包括已合并的)
|
|
672
|
+
const allMRs = await gitlabApiRequest(config, `/merge_requests?source_branch=${encodeURIComponent(branchName)}&per_page=10`);
|
|
673
|
+
|
|
674
|
+
if (allMRs && allMRs.length > 0) {
|
|
675
|
+
// 优先查找已合并的合并请求
|
|
676
|
+
const mergedMR = allMRs.find(mr => mr.state === 'merged');
|
|
677
|
+
if (mergedMR) {
|
|
678
|
+
return {
|
|
679
|
+
merged: true,
|
|
680
|
+
mergeRequest: mergedMR
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// 查找打开的合并请求
|
|
685
|
+
const openMR = allMRs.find(mr => mr.state === 'opened');
|
|
686
|
+
if (openMR) {
|
|
687
|
+
return {
|
|
688
|
+
merged: false,
|
|
689
|
+
mergeRequest: openMR,
|
|
690
|
+
hasOpenMR: true
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// 如果有其他状态的合并请求(如 closed),也返回
|
|
695
|
+
return {
|
|
696
|
+
merged: false,
|
|
697
|
+
mergeRequest: allMRs[0],
|
|
698
|
+
hasOpenMR: false
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
merged: false,
|
|
704
|
+
hasOpenMR: false
|
|
705
|
+
};
|
|
706
|
+
} catch (err) {
|
|
707
|
+
error(`检查合并请求状态失败: ${err.message}`);
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// 拉取最新的目标分支
|
|
713
|
+
function pullTargetBranch(targetBranch) {
|
|
714
|
+
try {
|
|
715
|
+
const currentBranch = getCurrentBranch();
|
|
716
|
+
|
|
717
|
+
// 验证目标分支名合法性
|
|
718
|
+
if (!validateBranchName(targetBranch)) {
|
|
719
|
+
error(`目标分支名不合法: ${targetBranch}`);
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 如果不在目标分支,先切换
|
|
724
|
+
if (currentBranch !== targetBranch) {
|
|
725
|
+
log(`切换到 ${targetBranch} 分支...`);
|
|
726
|
+
execSync(`git checkout ${escapeShellArg(targetBranch)}`, { stdio: 'inherit' });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// 先 fetch 获取最新信息
|
|
730
|
+
log('获取远程最新信息...');
|
|
731
|
+
execSync(`git fetch origin ${escapeShellArg(targetBranch)}`, { stdio: 'inherit' });
|
|
732
|
+
|
|
733
|
+
// 使用 rebase 方式拉取,避免产生 merge commit
|
|
734
|
+
log(`使用 rebase 方式拉取最新的 ${targetBranch} 代码...`);
|
|
735
|
+
try {
|
|
736
|
+
execSync(`git pull --rebase origin ${escapeShellArg(targetBranch)}`, { stdio: 'inherit' });
|
|
737
|
+
} catch (err) {
|
|
738
|
+
// 如果 rebase 失败,可能是本地有未提交的更改
|
|
739
|
+
// 尝试先检查状态
|
|
740
|
+
const status = execSync('git status --porcelain', { encoding: 'utf-8' });
|
|
741
|
+
if (status.trim()) {
|
|
742
|
+
error('本地有未提交的更改,无法拉取代码');
|
|
743
|
+
error('请先提交或暂存本地更改');
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
// 如果没有未提交的更改,可能是 rebase 冲突
|
|
747
|
+
error(`拉取失败: ${err.message}`);
|
|
748
|
+
log('提示:如果遇到冲突,请手动解决后运行 "git rebase --continue"');
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
success(`已拉取最新的 ${targetBranch} 代码`);
|
|
753
|
+
return true;
|
|
754
|
+
} catch (err) {
|
|
755
|
+
error(`拉取目标分支失败: ${err.message}`);
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// 检查并同步主分支(如果合并请求已合并)
|
|
761
|
+
async function checkAndSyncMaster(branchName = null, silent = false) {
|
|
762
|
+
try {
|
|
763
|
+
// 检查是否在 Git 仓库中
|
|
764
|
+
if (!isGitRepo()) {
|
|
765
|
+
error('当前目录不是 Git 仓库!');
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const config = readConfig();
|
|
770
|
+
if (!config || !config.autoSyncMaster) {
|
|
771
|
+
if (!silent) {
|
|
772
|
+
log('自动同步功能未启用');
|
|
773
|
+
}
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (!config.gitlab || !config.gitlab.url || !config.gitlab.token) {
|
|
778
|
+
error('GitLab 配置不完整,请运行 cb config 进行配置');
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// 如果没有指定分支名,使用当前分支
|
|
783
|
+
const sourceBranch = branchName || getCurrentBranch();
|
|
784
|
+
if (!silent) {
|
|
785
|
+
log(`检查分支 ${sourceBranch} 的合并请求状态...`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const status = await checkMergeRequestStatus(sourceBranch, config);
|
|
789
|
+
|
|
790
|
+
if (!status) {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (status.merged) {
|
|
795
|
+
success(`合并请求已合并!MR #${status.mergeRequest.iid}`);
|
|
796
|
+
// 获取目标分支名
|
|
797
|
+
const projectTargetBranch = getProjectTargetBranch(config);
|
|
798
|
+
if (!projectTargetBranch) {
|
|
799
|
+
error('未配置项目的目标分支,请先运行 cb push 或 cb set-target');
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
return pullTargetBranch(projectTargetBranch);
|
|
803
|
+
} else if (status.hasOpenMR) {
|
|
804
|
+
if (!silent) {
|
|
805
|
+
log(`合并请求尚未合并,状态: ${status.mergeRequest.state}`);
|
|
806
|
+
log(`MR #${status.mergeRequest.iid}: ${status.mergeRequest.web_url}`);
|
|
807
|
+
}
|
|
808
|
+
return false;
|
|
809
|
+
} else {
|
|
810
|
+
if (!silent) {
|
|
811
|
+
log('未找到相关的合并请求');
|
|
812
|
+
}
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
} catch (err) {
|
|
816
|
+
if (!silent) {
|
|
817
|
+
error(`检查合并状态失败: ${err.message}`);
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// 轮询检查合并状态
|
|
824
|
+
async function watchMergeStatus(branchName = null, intervalSeconds = 30, maxAttempts = null) {
|
|
825
|
+
try {
|
|
826
|
+
// 检查是否在 Git 仓库中
|
|
827
|
+
if (!isGitRepo()) {
|
|
828
|
+
error('当前目录不是 Git 仓库!');
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const config = readConfig();
|
|
833
|
+
if (!config || !config.autoSyncMaster) {
|
|
834
|
+
error('自动同步功能未启用,请运行 cb config 启用');
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (!config.gitlab || !config.gitlab.url || !config.gitlab.token) {
|
|
839
|
+
error('GitLab 配置不完整,请运行 cb config 进行配置');
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const targetBranch = branchName || getCurrentBranch();
|
|
844
|
+
log(`开始轮询检查分支 ${targetBranch} 的合并状态...`);
|
|
845
|
+
log(`检查间隔: ${intervalSeconds} 秒`);
|
|
846
|
+
if (maxAttempts) {
|
|
847
|
+
log(`最大检查次数: ${maxAttempts}`);
|
|
848
|
+
} else {
|
|
849
|
+
log('将一直检查直到合并或手动停止(Ctrl+C)');
|
|
850
|
+
}
|
|
851
|
+
log('');
|
|
852
|
+
|
|
853
|
+
let attempts = 0;
|
|
854
|
+
let isCompleted = false;
|
|
855
|
+
let checkInterval = null;
|
|
856
|
+
|
|
857
|
+
// 执行检查的函数
|
|
858
|
+
const performCheck = async () => {
|
|
859
|
+
attempts++;
|
|
860
|
+
|
|
861
|
+
if (maxAttempts && attempts > maxAttempts) {
|
|
862
|
+
if (checkInterval) {
|
|
863
|
+
clearInterval(checkInterval);
|
|
864
|
+
}
|
|
865
|
+
log(`已达到最大检查次数 (${maxAttempts}),停止轮询`);
|
|
866
|
+
isCompleted = true;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// 检查当前分支是否还是目标分支
|
|
871
|
+
try {
|
|
872
|
+
const currentBranch = getCurrentBranch();
|
|
873
|
+
if (currentBranch !== targetBranch) {
|
|
874
|
+
if (checkInterval) {
|
|
875
|
+
clearInterval(checkInterval);
|
|
876
|
+
}
|
|
877
|
+
log(`检测到分支已切换:当前分支 ${currentBranch},目标分支 ${targetBranch}`);
|
|
878
|
+
log('停止轮询');
|
|
879
|
+
isCompleted = true;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
} catch (err) {
|
|
883
|
+
// 如果获取当前分支失败,继续检查(可能是临时问题)
|
|
884
|
+
log(`警告:无法获取当前分支,继续检查...`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
888
|
+
log(`[${timestamp}] 第 ${attempts} 次检查...`);
|
|
889
|
+
|
|
890
|
+
try {
|
|
891
|
+
const result = await checkAndSyncMaster(targetBranch, true);
|
|
892
|
+
|
|
893
|
+
if (result) {
|
|
894
|
+
// 已合并并同步成功
|
|
895
|
+
if (checkInterval) {
|
|
896
|
+
clearInterval(checkInterval);
|
|
897
|
+
}
|
|
898
|
+
success('轮询完成:合并请求已合并,并已同步主分支');
|
|
899
|
+
isCompleted = true;
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// 继续等待下一次检查
|
|
904
|
+
if (!isCompleted) {
|
|
905
|
+
log(`等待 ${intervalSeconds} 秒后再次检查...\n`);
|
|
906
|
+
}
|
|
907
|
+
} catch (err) {
|
|
908
|
+
error(`检查失败: ${err.message}`);
|
|
909
|
+
if (!isCompleted) {
|
|
910
|
+
log(`等待 ${intervalSeconds} 秒后再次检查...\n`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// 立即执行第一次检查
|
|
916
|
+
await performCheck();
|
|
917
|
+
|
|
918
|
+
// 如果第一次检查就完成了,直接返回
|
|
919
|
+
if (isCompleted) {
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// 设置定时器继续检查
|
|
924
|
+
checkInterval = setInterval(async () => {
|
|
925
|
+
if (!isCompleted) {
|
|
926
|
+
await performCheck();
|
|
927
|
+
} else {
|
|
928
|
+
if (checkInterval) {
|
|
929
|
+
clearInterval(checkInterval);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}, intervalSeconds * 1000);
|
|
933
|
+
|
|
934
|
+
// 返回一个 Promise,但实际上会一直运行直到完成或用户中断
|
|
935
|
+
return new Promise((resolve) => {
|
|
936
|
+
// 处理进程退出信号,清理资源
|
|
937
|
+
const cleanup = () => {
|
|
938
|
+
if (checkInterval) {
|
|
939
|
+
clearInterval(checkInterval);
|
|
940
|
+
}
|
|
941
|
+
log('\n轮询已停止');
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// 注册清理函数
|
|
945
|
+
const sigintHandler = () => {
|
|
946
|
+
cleanup();
|
|
947
|
+
process.exit(0);
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
process.on('SIGINT', sigintHandler);
|
|
951
|
+
process.on('SIGTERM', cleanup);
|
|
952
|
+
});
|
|
953
|
+
} catch (err) {
|
|
954
|
+
error(`轮询检查失败: ${err.message}`);
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// 提交并推送代码
|
|
960
|
+
async function commitAndPush() {
|
|
961
|
+
try {
|
|
962
|
+
// 检查是否在 Git 仓库中
|
|
963
|
+
if (!isGitRepo()) {
|
|
964
|
+
error('当前目录不是 Git 仓库!');
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// 获取当前分支
|
|
969
|
+
const currentBranch = getCurrentBranch();
|
|
970
|
+
log(`当前分支: ${currentBranch}`);
|
|
971
|
+
|
|
972
|
+
// 读取配置,获取需求信息
|
|
973
|
+
const config = readConfig();
|
|
974
|
+
const requirement = getRequirementInfo(config, currentBranch);
|
|
975
|
+
if (!requirement) {
|
|
976
|
+
error('当前分支没有保存需求信息!');
|
|
977
|
+
error('请使用以下格式创建分支:cb <需求标题> <需求唯一标识>');
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
const commitMessage = `${requirement.id}:${requirement.title}`;
|
|
981
|
+
|
|
982
|
+
// 检查是否有变更
|
|
983
|
+
try {
|
|
984
|
+
const status = execSync('git status --porcelain', { encoding: 'utf-8' });
|
|
985
|
+
if (!status.trim()) {
|
|
986
|
+
log('没有文件变更,无需提交');
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
} catch (err) {
|
|
990
|
+
error('无法获取 Git 状态');
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// 添加所有更改
|
|
995
|
+
log('添加所有更改到暂存区...');
|
|
996
|
+
execSync('git add .', { stdio: 'inherit' });
|
|
997
|
+
|
|
998
|
+
// 验证分支名合法性
|
|
999
|
+
if (!validateBranchName(currentBranch)) {
|
|
1000
|
+
error(`当前分支名不合法: ${currentBranch}`);
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// 提交(转义提交信息中的引号)
|
|
1005
|
+
const escapedCommitMessage = commitMessage.replace(/"/g, '\\"');
|
|
1006
|
+
log(`提交更改: ${commitMessage}`);
|
|
1007
|
+
execSync(`git commit -m "${escapedCommitMessage}"`, { stdio: 'inherit' });
|
|
1008
|
+
|
|
1009
|
+
// 推送
|
|
1010
|
+
log('推送到远程仓库...');
|
|
1011
|
+
execSync(`git push origin ${escapeShellArg(currentBranch)}`, { stdio: 'inherit' });
|
|
1012
|
+
|
|
1013
|
+
success(`已提交并推送到分支: ${currentBranch}`);
|
|
1014
|
+
|
|
1015
|
+
// 获取或设置项目的目标分支名
|
|
1016
|
+
let targetBranch = getProjectTargetBranch(config);
|
|
1017
|
+
if (!targetBranch) {
|
|
1018
|
+
// 第一次使用,需要询问目标分支名
|
|
1019
|
+
log('\n首次使用,需要配置合并目标分支');
|
|
1020
|
+
targetBranch = await question('请输入合并目标分支名(例如:master、main): ');
|
|
1021
|
+
if (!targetBranch || !targetBranch.trim()) {
|
|
1022
|
+
error('目标分支名不能为空');
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
if (!validateBranchName(targetBranch)) {
|
|
1026
|
+
error('目标分支名不合法,请使用字母、数字、下划线、连字符和斜杠');
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
targetBranch = targetBranch.trim();
|
|
1030
|
+
if (!saveProjectTargetBranch(config, targetBranch)) {
|
|
1031
|
+
error('保存目标分支失败');
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
success(`已保存目标分支: ${targetBranch}`);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// 如果配置了 GitLab,自动创建合并请求
|
|
1038
|
+
if (config.gitlab && config.gitlab.url && config.gitlab.token) {
|
|
1039
|
+
try {
|
|
1040
|
+
const mergeRequest = await createMergeRequest(
|
|
1041
|
+
currentBranch,
|
|
1042
|
+
targetBranch,
|
|
1043
|
+
`${requirement.id}: ${requirement.title}`,
|
|
1044
|
+
`需求标识: ${requirement.id}\n需求标题: ${requirement.title}`
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
// 打开浏览器到合并请求页面
|
|
1048
|
+
const mrUrl = mergeRequest.web_url;
|
|
1049
|
+
log(`打开合并请求页面: ${mrUrl}`);
|
|
1050
|
+
openBrowser(mrUrl);
|
|
1051
|
+
log('💡 提示:请在浏览器中确认合并请求,确认无误后点击 Merge 按钮');
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
log(`创建合并请求失败: ${err.message}`);
|
|
1054
|
+
log('提示:可以手动在 GitLab 上创建合并请求');
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// 如果启用了自动同步,自动开始轮询检查合并状态
|
|
1059
|
+
if (config.autoSyncMaster) {
|
|
1060
|
+
log('\n开始自动轮询检查合并状态...');
|
|
1061
|
+
log('提示:可以使用 Ctrl+C 停止轮询,或使用 "cb sync" 手动检查一次');
|
|
1062
|
+
log('');
|
|
1063
|
+
|
|
1064
|
+
// 在后台启动轮询(不阻塞)
|
|
1065
|
+
watchMergeStatus(currentBranch, 30, null).catch(err => {
|
|
1066
|
+
log(`轮询检查失败: ${err.message}`);
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return true;
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
error(`提交或推送失败: ${err.message}`);
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// 主函数
|
|
1078
|
+
async function main() {
|
|
1079
|
+
try {
|
|
1080
|
+
// 解析命令行参数
|
|
1081
|
+
const args = process.argv.slice(2);
|
|
1082
|
+
|
|
1083
|
+
// 检查是否是 push 命令
|
|
1084
|
+
if (args.length > 0 && args[0].toLowerCase() === 'push') {
|
|
1085
|
+
const success = await commitAndPush();
|
|
1086
|
+
process.exit(success ? 0 : 1);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// 检查是否是 config 命令
|
|
1091
|
+
if (args.length > 0 && args[0].toLowerCase() === 'config') {
|
|
1092
|
+
const success = await configUserInfo();
|
|
1093
|
+
process.exit(success ? 0 : 1);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// 检查是否是 set-target 命令(设置项目的目标分支名)
|
|
1098
|
+
if (args.length > 0 && args[0].toLowerCase() === 'set-target') {
|
|
1099
|
+
const success = await setProjectTargetBranch();
|
|
1100
|
+
process.exit(success ? 0 : 1);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// 检查是否是 sync 命令(检查合并状态并同步主分支)
|
|
1105
|
+
if (args.length > 0 && (args[0].toLowerCase() === 'sync' || args[0].toLowerCase() === 'check')) {
|
|
1106
|
+
const success = await checkAndSyncMaster();
|
|
1107
|
+
process.exit(success ? 0 : 1);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// 检查是否是 watch 命令(轮询检查合并状态)
|
|
1112
|
+
if (args.length > 0 && (args[0].toLowerCase() === 'watch' || args[0].toLowerCase() === 'poll')) {
|
|
1113
|
+
// 解析参数:cb watch [分支名] [间隔秒数] [最大次数]
|
|
1114
|
+
let branchName = null;
|
|
1115
|
+
let intervalSeconds = 30;
|
|
1116
|
+
let maxAttempts = null;
|
|
1117
|
+
|
|
1118
|
+
if (args.length > 1) {
|
|
1119
|
+
// 第二个参数可能是分支名或间隔秒数
|
|
1120
|
+
const arg2 = args[1];
|
|
1121
|
+
if (!isNaN(parseInt(arg2))) {
|
|
1122
|
+
intervalSeconds = parseInt(arg2);
|
|
1123
|
+
} else {
|
|
1124
|
+
branchName = arg2;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (args.length > 2) {
|
|
1129
|
+
const arg3 = args[2];
|
|
1130
|
+
if (!isNaN(parseInt(arg3))) {
|
|
1131
|
+
if (branchName) {
|
|
1132
|
+
intervalSeconds = parseInt(arg3);
|
|
1133
|
+
} else {
|
|
1134
|
+
maxAttempts = parseInt(arg3);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (args.length > 3) {
|
|
1140
|
+
maxAttempts = parseInt(args[3]);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
await watchMergeStatus(branchName, intervalSeconds, maxAttempts);
|
|
1144
|
+
// watch 函数会持续运行,直到合并或用户中断
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// 检查是否是 bug 类型
|
|
1149
|
+
const isBug = args.includes('-b') || args.includes('--bug');
|
|
1150
|
+
|
|
1151
|
+
// 移除标志参数,获取需求标题和需求唯一标识
|
|
1152
|
+
const titleArgs = args.filter(arg => arg !== '-b' && arg !== '--bug');
|
|
1153
|
+
|
|
1154
|
+
if (titleArgs.length === 0) {
|
|
1155
|
+
console.log('\n用法:');
|
|
1156
|
+
console.log(' create-branch <需求标题> # 创建 feature 分支');
|
|
1157
|
+
console.log(' create-branch <需求标题> <需求唯一标识> # 创建 feature 分支并保存需求信息');
|
|
1158
|
+
console.log(' create-branch <需求标题> -b # 创建 hotfix 分支');
|
|
1159
|
+
console.log(' create-branch <需求标题> <需求唯一标识> -b # 创建 hotfix 分支并保存需求信息');
|
|
1160
|
+
console.log(' create-branch push # 提交并推送代码(使用保存的需求信息)');
|
|
1161
|
+
console.log(' create-branch config # 配置用户信息和选项');
|
|
1162
|
+
console.log(' create-branch set-target # 设置项目的目标分支名');
|
|
1163
|
+
console.log(' create-branch sync # 检查合并请求状态并同步主分支');
|
|
1164
|
+
console.log(' create-branch watch [间隔秒数] [最大次数] # 轮询检查合并状态并自动同步');
|
|
1165
|
+
console.log('\n示例:');
|
|
1166
|
+
console.log(' create-branch 用户登录功能');
|
|
1167
|
+
console.log(' create-branch 用户登录功能 PROJ-123');
|
|
1168
|
+
console.log(' create-branch 修复登录bug -b');
|
|
1169
|
+
console.log(' create-branch 修复登录bug PROJ-124 -b');
|
|
1170
|
+
console.log(' create-branch push');
|
|
1171
|
+
console.log(' create-branch config');
|
|
1172
|
+
console.log(' create-branch set-target');
|
|
1173
|
+
console.log(' create-branch sync');
|
|
1174
|
+
console.log(' create-branch watch 30 60 # 每30秒检查一次,最多60次');
|
|
1175
|
+
console.log('\n别名:');
|
|
1176
|
+
console.log(' cb <需求标题> [需求唯一标识] [-b|--bug]');
|
|
1177
|
+
console.log(' cb push');
|
|
1178
|
+
console.log(' cb config');
|
|
1179
|
+
console.log(' cb set-target # 设置项目的目标分支名');
|
|
1180
|
+
console.log(' cb sync 或 cb check');
|
|
1181
|
+
console.log(' cb watch 或 cb poll # 轮询检查合并状态');
|
|
1182
|
+
process.exit(0);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// 检查是否在 Git 仓库中
|
|
1186
|
+
if (!isGitRepo()) {
|
|
1187
|
+
error('当前目录不是 Git 仓库!');
|
|
1188
|
+
process.exit(1);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// 读取或初始化用户配置
|
|
1192
|
+
let config = readConfig();
|
|
1193
|
+
if (!config || !config.workId || !config.name) {
|
|
1194
|
+
config = await initUserInfo();
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// 解析需求标题和需求唯一标识
|
|
1198
|
+
// titleArgs 已经过滤掉了 -b 和 --bug
|
|
1199
|
+
// 如果参数数量 >= 2,则最后一个参数是需求唯一标识
|
|
1200
|
+
let title, requirementId;
|
|
1201
|
+
if (titleArgs.length >= 2) {
|
|
1202
|
+
// 最后一个参数是需求唯一标识
|
|
1203
|
+
requirementId = titleArgs[titleArgs.length - 1];
|
|
1204
|
+
title = titleArgs.slice(0, -1).join(' ');
|
|
1205
|
+
} else {
|
|
1206
|
+
// 只有需求标题
|
|
1207
|
+
title = titleArgs.join(' ');
|
|
1208
|
+
requirementId = null;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// 生成分支名
|
|
1212
|
+
const branchName = generateBranchName(config.workId, config.name, title, isBug);
|
|
1213
|
+
log(`生成的分支名: ${branchName}`);
|
|
1214
|
+
|
|
1215
|
+
// 创建分支
|
|
1216
|
+
await createBranch(branchName);
|
|
1217
|
+
|
|
1218
|
+
// 如果提供了需求唯一标识,保存到配置
|
|
1219
|
+
if (requirementId) {
|
|
1220
|
+
// 确保 config 存在
|
|
1221
|
+
if (!config) {
|
|
1222
|
+
config = readConfig() || {};
|
|
1223
|
+
}
|
|
1224
|
+
if (saveRequirementInfo(config, branchName, requirementId.trim(), title)) {
|
|
1225
|
+
success(`已保存需求信息: ${requirementId}:${title}`);
|
|
1226
|
+
log('可以使用 "cb push" 快速提交和推送代码');
|
|
1227
|
+
} else {
|
|
1228
|
+
error('保存需求信息失败');
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
} catch (err) {
|
|
1233
|
+
error(err.message);
|
|
1234
|
+
process.exit(1);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// 运行主函数
|
|
1239
|
+
main();
|
|
1240
|
+
|