glab-tool 1.0.4

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.
@@ -0,0 +1,11 @@
1
+ # GitLab configuration file
2
+ # Place this file in your current working directory as .glabrc
3
+
4
+ # GitLab API URL
5
+ GITLAB_URL=https://gitlab.example.com/api/v4
6
+
7
+ # GitLab Personal Access Token
8
+ GITLAB_TOKEN=your_personal_access_token_here
9
+
10
+ # Project ID or path
11
+ PROJECT_ID=my-group/my-project
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # glab-tool - GitLab Extended CLI Tool
2
+
3
+ 一个多功能的 GitLab 命令行工具集。
4
+
5
+ ## 功能特性
6
+
7
+ - **列出分支** - 显示项目所有分支及其详细信息(创建日期、作者、保护状态)
8
+ - **智能清理** - 清理已合并或已关闭的合并请求对应的源分支
9
+ - **交互式选择** - 使用 inquirer 进行交互式分支选择
10
+ - **按状态分组** - 分支按 "已合并" 和 "已关闭" 分组展示
11
+ - **干运行模式** - 预览将删除的分支而不实际执行
12
+ - **强制删除** - 支持强制删除受保护分支
13
+ - **通配符支持** - 目标分支支持通配符匹配(如 `release/*`)
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ npm install -g glab-tool
19
+ ```
20
+
21
+ ## 快速开始
22
+
23
+ 1. 创建配置文件 `.glabrc`:
24
+ ```ini
25
+ gitlab_url=https://gitlab.example.com/api/v4
26
+ gitlab_token=your_access_token
27
+ project_id=your_project_id
28
+ ```
29
+
30
+ 2. 列出所有分支:
31
+ ```bash
32
+ glabx list
33
+ ```
34
+
35
+ 3. 清理分支(交互式选择):
36
+ ```bash
37
+ glabx clean
38
+ ```
39
+
40
+ ## 使用说明
41
+
42
+ ### 全局选项
43
+
44
+ - `--config <file>`: 配置文件路径(默认: `.glabrc`)
45
+
46
+ ### 命令
47
+
48
+ #### `glabx list`
49
+
50
+ 列出 GitLab 项目的所有分支。
51
+
52
+ ```bash
53
+ glabx list
54
+ ```
55
+
56
+ 输出示例:
57
+ ```
58
+ Found 15 branches:
59
+ +-------------------+--------------+------------------+-----------+
60
+ | Branch | Created Date | Author | Protected |
61
+ +-------------------+--------------+------------------+-----------+
62
+ | main | 2024-01-15 | John Doe | Yes |
63
+ | feature/user-auth | 2024-02-20 | Jane Smith | No |
64
+ | bugfix/login | 2024-02-22 | Bob Wilson | No |
65
+ +-------------------+--------------+------------------+-----------+
66
+ ```
67
+
68
+ #### `glabx clean`
69
+
70
+ 清理目标分支的已合并/已关闭合并请求对应的源分支。
71
+
72
+ ```bash
73
+ glabx clean [options]
74
+ ```
75
+
76
+ **选项**:
77
+ - `--target <branch>`: 目标分支(默认: `main`),支持通配符如 `release/*`
78
+ - `--force`: 强制删除受保护分支
79
+ - `-d, --dry-run`: 预览模式,显示将删除的分支但不实际删除
80
+
81
+ **使用示例**:
82
+
83
+ ```bash
84
+ # 清理 targeting main 分支的分支
85
+ glabx clean --target main
86
+
87
+ # 预览将删除的分支
88
+ glabx clean --target main --dry-run
89
+
90
+ # 强制删除(包括受保护分支)
91
+ glabx clean --target main --force
92
+
93
+ # 使用通配符匹配多个目标分支
94
+ glabx clean --target release/*
95
+
96
+ # 交互式选择(无参数)
97
+ glabx clean
98
+ ```
99
+
100
+ ## 配置
101
+
102
+ 支持三种配置方式(优先级从高到低):
103
+
104
+ ### 1. 配置文件 `.glabrc`
105
+
106
+ ```ini
107
+ # GitLab API 地址
108
+ gitlab_url=https://gitlab.example.com/api/v4
109
+
110
+ # GitLab 访问令牌
111
+ gitlab_token=glpat-xxxxxxxxxxxx
112
+
113
+ # 项目 ID 或路径(如 group/project)
114
+ project_id=123
115
+ ```
116
+
117
+ ### 2. 环境变量
118
+
119
+ ```bash
120
+ export GITLAB_URL=https://gitlab.example.com/api/v4
121
+ export GITLAB_TOKEN=glpat-xxxxxxxxxxxx
122
+ export PROJECT_ID=123
123
+ ```
124
+
125
+ ### 3. 命令行参数
126
+
127
+ (当前版本通过配置文件或环境变量配置)
128
+
129
+ ## 技术栈
130
+
131
+ - **Node.js** >= 18.0.0
132
+ - **Commander** ^13.1.0 - CLI 命令解析
133
+ - **Axios** ^1.6.0 - GitLab API 客户端
134
+ - **Inquirer** ^9.2.12 - 交互式提示
135
+
136
+ ## 项目结构
137
+
138
+ ```
139
+ git-clean/
140
+ ├── index.js # 主入口,CLI 命令定义
141
+ ├── package.json
142
+ ├── src/
143
+ │ ├── list.js # 列出分支功能
144
+ │ └── cleanup.js # 清理分支功能
145
+ └── README.md
146
+ ```
147
+
148
+ ## 架构说明
149
+
150
+ ```
151
+ CLI 层 (index.js)
152
+
153
+ 业务层 (list.js / cleanup.js)
154
+
155
+ 工具层 (配置读取、API 调用)
156
+
157
+ 外部层 (GitLab API、配置文件)
158
+ ```
159
+
160
+ ## 数据流向
161
+
162
+ ### 列出分支流程
163
+ 1. 读取配置(文件 → 环境变量)
164
+ 2. 创建 GitLab API 客户端
165
+ 3. 分页获取所有分支
166
+ 4. 对每个分支获取最后提交信息
167
+ 5. 格式化输出表格
168
+
169
+ ### 清理分支流程
170
+ 1. 读取配置
171
+ 2. 获取目标分支的已合并/已关闭 MR
172
+ 3. 获取当前存在的分支
173
+ 4. 过滤出可清理的分支
174
+ 5. 交互式选择 → 确认 → 删除
175
+
176
+ ## 开发
177
+
178
+ ```bash
179
+ # 克隆项目
180
+ git clone <repo>
181
+ cd git-clean
182
+
183
+ # 安装依赖
184
+ npm install
185
+
186
+ # 本地运行
187
+ node index.js list
188
+ node index.js clean
189
+ ```
190
+
191
+ ## 许可证
192
+
193
+ MIT
package/index.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { cleanup } from './src/cleanup.js';
5
+ import { list } from './src/list.js';
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name('glabx')
11
+ .description('A CLI tool to clean up invalid GitLab branches')
12
+ .version('1.0.0');
13
+
14
+ // 添加 clean 命令
15
+ program
16
+ .command('clean')
17
+ .description('Clean up GitLab branches from merge requests targeting main branch')
18
+ .option('--target <branch>', 'Target branch to filter merge requests (default: main)', 'main')
19
+ .option('--force', 'Force delete protected branches')
20
+ .option('-d, --dry-run', 'Show what would be deleted without actually deleting')
21
+ .action(async (options) => {
22
+ await cleanup(options);
23
+ });
24
+
25
+ // 添加 list 命令
26
+ program
27
+ .command('list')
28
+ .description('List all GitLab branches')
29
+ .action(async (options) => {
30
+ await list(options);
31
+ });
32
+
33
+ // 解析命令行参数
34
+ program.parse();
35
+
36
+ // 如果没有提供任何命令,则显示帮助
37
+ if (!process.argv.slice(2).length) {
38
+ program.help();
39
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "glab-tool",
3
+ "version": "1.0.4",
4
+ "description": "GitLab Extended CLI Tool - A multi-purpose GitLab utility CLI",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "glabx": "./index.js"
9
+ },
10
+ "files": [
11
+ "src/*",
12
+ ".glabrc.example",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node index.js",
17
+ "test": "echo \"Error: no test specified\" && exit 1"
18
+ },
19
+ "keywords": [
20
+ "gitlab",
21
+ "branch",
22
+ "cleanup",
23
+ "cli"
24
+ ],
25
+ "author": "GitLab Branch Cleaner",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "axios": "^1.6.0",
29
+ "commander": "^13.1.0",
30
+ "inquirer": "^9.2.12"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ }
35
+ }
package/src/cleanup.js ADDED
@@ -0,0 +1,445 @@
1
+ import axios from 'axios';
2
+ import fs from 'fs';
3
+ import inquirer from 'inquirer';
4
+
5
+ // 从配置文件读取配置
6
+ function readConfig(configPath) {
7
+ const config = {};
8
+
9
+ if (fs.existsSync(configPath)) {
10
+ try {
11
+ const configContent = fs.readFileSync(configPath, 'utf8');
12
+ const lines = configContent.split('\n');
13
+
14
+ for (const line of lines) {
15
+ const trimmedLine = line.trim();
16
+ // 跳过空行和注释
17
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
18
+ continue;
19
+ }
20
+
21
+ // 解析键值对格式 (key=value)
22
+ const [key, value] = trimmedLine.split('=').map(part => part.trim());
23
+ if (key && value) {
24
+ config[key.toLowerCase()] = value;
25
+ }
26
+ }
27
+ } catch (error) {
28
+ console.warn('Warning: Could not read config file:', error.message);
29
+ }
30
+ }
31
+
32
+ return config;
33
+ }
34
+
35
+ // 获取配置值
36
+ function getConfigValue(config, key) {
37
+ // 优先级:配置文件 > 环境变量 > 默认值
38
+ return config[key] ||
39
+ process.env[key.toUpperCase()] ||
40
+ null;
41
+ }
42
+
43
+ // 获取指定目标分支的合并请求(获取已合并和已关闭的)
44
+ async function getMergeRequestsByTargetBranch(apiClient, projectId, targetBranch) {
45
+ const mergeRequests = [];
46
+ let page = 1;
47
+ let hasNextPage = true;
48
+
49
+ try {
50
+ // 先获取已合并的
51
+ while (hasNextPage) {
52
+ const response = await apiClient.get(`/projects/${projectId}/merge_requests`, {
53
+ params: {
54
+ page: page,
55
+ per_page: 100,
56
+ state: 'merged',
57
+ target_branch: targetBranch
58
+ }
59
+ });
60
+
61
+ mergeRequests.push(...response.data);
62
+
63
+ // 检查是否有下一页
64
+ const linkHeader = response.headers.link;
65
+ hasNextPage = linkHeader && linkHeader.includes('rel="next"');
66
+ page++;
67
+ }
68
+ } catch (error) {
69
+ console.error('Error fetching merged merge requests:', error);
70
+ throw error;
71
+ }
72
+
73
+ // 重置页码获取已关闭的
74
+ page = 1;
75
+ hasNextPage = true;
76
+
77
+ try {
78
+ while (hasNextPage) {
79
+ const response = await apiClient.get(`/projects/${projectId}/merge_requests`, {
80
+ params: {
81
+ page: page,
82
+ per_page: 100,
83
+ state: 'closed',
84
+ target_branch: targetBranch
85
+ }
86
+ });
87
+
88
+ mergeRequests.push(...response.data);
89
+
90
+ // 检查是否有下一页
91
+ const linkHeader = response.headers.link;
92
+ hasNextPage = linkHeader && linkHeader.includes('rel="next"');
93
+ page++;
94
+ }
95
+ } catch (error) {
96
+ console.error('Error fetching closed merge requests:', error);
97
+ throw error;
98
+ }
99
+
100
+ return mergeRequests;
101
+ }
102
+
103
+ // 检查分支名称是否匹配模式(支持通配符 *)
104
+ function matchesPattern(branchName, pattern) {
105
+ if (!pattern.includes('*')) {
106
+ // 不包含通配符,直接比较
107
+ return branchName === pattern;
108
+ }
109
+
110
+ // 将通配符转换为正则表达式
111
+ const escapedPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
112
+ const regex = new RegExp(`^${escapedPattern}$`);
113
+ return regex.test(branchName);
114
+ }
115
+
116
+ // 删除分支
117
+ async function deleteBranch(apiClient, projectId, branchName, force = false) {
118
+ try {
119
+ const deleteUrl = `/projects/${projectId}/repository/branches/${encodeURIComponent(branchName)}`;
120
+
121
+ // 如果是强制删除,需要使用不同的API端点
122
+ if (force) {
123
+ // 使用强制删除的API端点
124
+ await apiClient.delete(deleteUrl, {
125
+ params: {
126
+ force: true
127
+ }
128
+ });
129
+ } else {
130
+ // 正常删除
131
+ await apiClient.delete(deleteUrl);
132
+ }
133
+
134
+ console.log(`Successfully deleted branch: ${branchName}`);
135
+ return true;
136
+ } catch (error) {
137
+ if (error.response?.status === 403) {
138
+ console.error(`Error: Cannot delete protected branch "${branchName}". Use --force to override.`);
139
+ } else if (error.response?.status === 404) {
140
+ console.error(`Error: Branch "${branchName}" not found.`);
141
+ } else {
142
+ console.error(`Error deleting branch "${branchName}":`, error.message);
143
+ }
144
+ return false;
145
+ }
146
+ }
147
+
148
+ // 获取项目中的所有分支名称
149
+ async function getAllBranches(apiClient, projectId) {
150
+ const branches = [];
151
+ let page = 1;
152
+ let hasNextPage = true;
153
+
154
+ try {
155
+ while (hasNextPage) {
156
+ const response = await apiClient.get(`/projects/${projectId}/repository/branches`, {
157
+ params: {
158
+ page: page,
159
+ per_page: 100
160
+ }
161
+ });
162
+
163
+ branches.push(...response.data.map(branch => branch.name));
164
+
165
+ // 检查是否有下一页
166
+ const linkHeader = response.headers.link;
167
+ hasNextPage = linkHeader && linkHeader.includes('rel="next"');
168
+ page++;
169
+ }
170
+
171
+ return branches;
172
+ } catch (error) {
173
+ console.error('Error fetching branches:', error);
174
+ return [];
175
+ }
176
+ }
177
+
178
+ // 清理分支
179
+ async function cleanup(options) {
180
+ // 读取配置
181
+ const configPath = options.config || '.glabrc';
182
+ const config = readConfig(configPath);
183
+
184
+ // 获取配置值
185
+ const gitlabUrl = getConfigValue(config, 'gitlab_url');
186
+ const token = getConfigValue(config, 'gitlab_token');
187
+ const projectId = getConfigValue(config, 'project_id');
188
+
189
+ // 验证必需选项
190
+ if (!gitlabUrl) {
191
+ console.error('Error: GitLab API URL is required');
192
+ process.exit(1);
193
+ }
194
+
195
+ if (!token) {
196
+ console.error('Error: GitLab token is required');
197
+ process.exit(1);
198
+ }
199
+
200
+ if (!projectId) {
201
+ console.error('Error: Project ID or path is required');
202
+ process.exit(1);
203
+ }
204
+
205
+ // 构建 GitLab API 基础 URL
206
+ const gitlabBaseUrl = gitlabUrl.replace(/\/$/, ''); // 移除末尾的斜杠
207
+
208
+ // 创建 axios 实例
209
+ const apiClient = axios.create({
210
+ baseURL: gitlabBaseUrl,
211
+ headers: {
212
+ 'PRIVATE-TOKEN': token,
213
+ 'Content-Type': 'application/json'
214
+ }
215
+ });
216
+
217
+ // 对项目ID进行URL编码(如果包含特殊字符)
218
+ const encodedProjectId = encodeURIComponent(projectId);
219
+
220
+ // 获取指定目标分支的合并请求
221
+ try {
222
+ const targetBranch = options.target || 'main';
223
+ let targetMergeRequests = [];
224
+
225
+ // 如果目标分支包含通配符,则获取所有合并请求并在客户端过滤
226
+ if (targetBranch.includes('*')) {
227
+ // 获取所有合并请求,不指定目标分支(包括已合并和已关闭的)
228
+ const allMergeRequests = [];
229
+ let page = 1;
230
+ let hasNextPage = true;
231
+
232
+ // 先获取已合并的
233
+ while (hasNextPage) {
234
+ const response = await apiClient.get(`/projects/${encodedProjectId}/merge_requests`, {
235
+ params: {
236
+ page: page,
237
+ per_page: 100,
238
+ state: 'merged'
239
+ }
240
+ });
241
+
242
+ allMergeRequests.push(...response.data);
243
+
244
+ // 检查是否有下一页
245
+ const linkHeader = response.headers.link;
246
+ hasNextPage = linkHeader && linkHeader.includes('rel="next"');
247
+ page++;
248
+ }
249
+
250
+ // 重置页码获取已关闭的
251
+ page = 1;
252
+ hasNextPage = true;
253
+
254
+ while (hasNextPage) {
255
+ const response = await apiClient.get(`/projects/${encodedProjectId}/merge_requests`, {
256
+ params: {
257
+ page: page,
258
+ per_page: 100,
259
+ state: 'closed'
260
+ }
261
+ });
262
+
263
+ allMergeRequests.push(...response.data);
264
+
265
+ // 检查是否有下一页
266
+ const linkHeader = response.headers.link;
267
+ hasNextPage = linkHeader && linkHeader.includes('rel="next"');
268
+ page++;
269
+ }
270
+
271
+ // 根据目标分支模式过滤合并请求
272
+ targetMergeRequests = allMergeRequests.filter(mr => matchesPattern(mr.target_branch, targetBranch));
273
+ } else {
274
+ // 如果不包含通配符,使用原来的 API 查询方式
275
+ targetMergeRequests = await getMergeRequestsByTargetBranch(apiClient, encodedProjectId, targetBranch);
276
+ }
277
+
278
+ if (targetMergeRequests.length === 0) {
279
+ console.log(`No merge requests targeting the "${targetBranch}" branch found.`);
280
+ return;
281
+ }
282
+
283
+ // 获取项目中所有实际存在的分支
284
+ const allBranches = await getAllBranches(apiClient, encodedProjectId);
285
+
286
+ // 过滤出实际存在的分支,并按状态分组
287
+ const mergedBranches = [];
288
+ const closedBranches = [];
289
+
290
+ for (const mr of targetMergeRequests) {
291
+ if (allBranches.includes(mr.source_branch)) {
292
+ if (mr.state === 'merged') {
293
+ mergedBranches.push(mr);
294
+ } else if (mr.state === 'closed') {
295
+ closedBranches.push(mr);
296
+ }
297
+ }
298
+ }
299
+
300
+ // 如果两个分组都为空
301
+ if (mergedBranches.length === 0 && closedBranches.length === 0) {
302
+ console.log(`No existing merge requests targeting the "${targetBranch}" branch found.`);
303
+ return;
304
+ }
305
+
306
+ // 询问用户是否确认删除
307
+ if (options.dryRun) {
308
+ console.log('Dry run mode: No branches will be deleted.');
309
+ // 显示分组统计信息
310
+ if (mergedBranches.length > 0) {
311
+ console.log(`\nMerged branches (${mergedBranches.length}):`);
312
+ mergedBranches.forEach(mr => console.log(` - MR !${mr.iid}: ${mr.source_branch}`));
313
+ }
314
+ if (closedBranches.length > 0) {
315
+ console.log(`\nClosed branches (${closedBranches.length}):`);
316
+ closedBranches.forEach(mr => console.log(` - MR !${mr.iid}: ${mr.source_branch}`));
317
+ }
318
+ return;
319
+ }
320
+
321
+ // 如果有 --force 参数,直接删除所有分支
322
+ if (options.force) {
323
+ console.log('Force mode enabled: Deleting all branches without confirmation...');
324
+ const allBranchesToDelete = [...mergedBranches, ...closedBranches];
325
+ for (const mr of allBranchesToDelete) {
326
+ await deleteBranch(apiClient, encodedProjectId, mr.source_branch, true);
327
+ }
328
+ return;
329
+ }
330
+
331
+ // 创建选择项数组,按状态分组显示
332
+ const choices = [];
333
+
334
+ // 添加已合并分组
335
+ if (mergedBranches.length > 0) {
336
+ choices.push(new inquirer.Separator(' 📦 Merged branches (' + mergedBranches.length + ')'));
337
+ mergedBranches.forEach((mr, index) => {
338
+ choices.push({
339
+ name: `${index + 1}. MR !${mr.iid}: ${mr.title} (${mr.source_branch})`,
340
+ value: {
341
+ id: index + 1,
342
+ mrId: mr.iid,
343
+ sourceBranch: mr.source_branch,
344
+ title: mr.title,
345
+ state: 'merged'
346
+ }
347
+ });
348
+ });
349
+ }
350
+
351
+ // 添加已关闭分组
352
+ if (closedBranches.length > 0) {
353
+ choices.push(new inquirer.Separator(' ❌ Closed branches (' + closedBranches.length + ')'));
354
+ closedBranches.forEach((mr, index) => {
355
+ choices.push({
356
+ name: `${mergedBranches.length + index + 1}. MR !${mr.iid}: ${mr.title} (${mr.source_branch})`,
357
+ value: {
358
+ id: mergedBranches.length + index + 1,
359
+ mrId: mr.iid,
360
+ sourceBranch: mr.source_branch,
361
+ title: mr.title,
362
+ state: 'closed'
363
+ }
364
+ });
365
+ });
366
+ }
367
+
368
+ // 使用 inquirer 进行交互式选择
369
+ const questions = [
370
+ {
371
+ type: 'checkbox',
372
+ name: 'selectedBranches',
373
+ message: 'Select branches to delete:',
374
+ choices: choices,
375
+ validate: function (answer) {
376
+ if (answer.length < 1) {
377
+ return 'You must select at least one branch.';
378
+ }
379
+ return true;
380
+ },
381
+ pageSize: 100, // 设置较大的值以显示更多选项
382
+ loop: false, // 禁止循环选择,防止从最后一个回到第一个
383
+ // 添加额外的安全配置
384
+ searchable: false
385
+ }
386
+ ];
387
+
388
+ const answers = await inquirer.prompt(questions);
389
+
390
+ // 获取用户选择的分支信息
391
+ const selectedBranches = answers.selectedBranches;
392
+
393
+ if (selectedBranches.length === 0) {
394
+ console.log('No branches selected for deletion.');
395
+ return;
396
+ }
397
+
398
+ // 显示用户选择的分支(按状态分组显示)
399
+ console.log('\nSelected branches to delete:');
400
+
401
+ const selectedMerged = selectedBranches.filter(b => b.state === 'merged');
402
+ const selectedClosed = selectedBranches.filter(b => b.state === 'closed');
403
+
404
+ if (selectedMerged.length > 0) {
405
+ console.log(`\n📦 Merged branches (${selectedMerged.length}):`);
406
+ selectedMerged.forEach(branch => {
407
+ console.log(` - MR !${branch.mrId}: ${branch.sourceBranch}`);
408
+ });
409
+ }
410
+
411
+ if (selectedClosed.length > 0) {
412
+ console.log(`\n❌ Closed branches (${selectedClosed.length}):`);
413
+ selectedClosed.forEach(branch => {
414
+ console.log(` - MR !${branch.mrId}: ${branch.sourceBranch}`);
415
+ });
416
+ }
417
+
418
+ // 确认删除
419
+ const confirmQuestions = [
420
+ {
421
+ type: 'confirm',
422
+ name: 'confirmDelete',
423
+ message: '\nDo you want to delete these branches?',
424
+ default: false
425
+ }
426
+ ];
427
+
428
+ const confirmAnswers = await inquirer.prompt(confirmQuestions);
429
+
430
+ if (confirmAnswers.confirmDelete) {
431
+ console.log('\nDeleting branches...');
432
+ for (const branch of selectedBranches) {
433
+ await deleteBranch(apiClient, encodedProjectId, branch.sourceBranch, options.force);
434
+ }
435
+ } else {
436
+ console.log('\nOperation cancelled by user.');
437
+ }
438
+
439
+ } catch (error) {
440
+ console.error('Error cleaning up branches:', error.message);
441
+ process.exit(1);
442
+ }
443
+ }
444
+
445
+ export { cleanup };
package/src/index.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { list } from "./list.js";
5
+
6
+ const program = new Command();
7
+
8
+ program
9
+ .name("glbc")
10
+ .description("A CLI tool to clean up invalid GitLab branches")
11
+ .version("1.0.0");
12
+
13
+ // 添加 list 命令
14
+ program
15
+ .command("list")
16
+ .description("List all GitLab branches")
17
+ .action(async (options) => {
18
+ await list(options);
19
+ });
20
+
21
+ // 解析命令行参数
22
+ program.parse();
23
+
24
+ // 如果没有提供任何命令,则显示帮助
25
+ if (!process.argv.slice(2).length) {
26
+ program.help();
27
+ }
package/src/list.js ADDED
@@ -0,0 +1,208 @@
1
+ import axios from 'axios';
2
+ import fs from 'fs';
3
+
4
+ // 从配置文件读取配置
5
+ function readConfig(configPath) {
6
+ const config = {};
7
+
8
+ if (fs.existsSync(configPath)) {
9
+ try {
10
+ const configContent = fs.readFileSync(configPath, 'utf8');
11
+ const lines = configContent.split('\n');
12
+
13
+ for (const line of lines) {
14
+ const trimmedLine = line.trim();
15
+ // 跳过空行和注释
16
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
17
+ continue;
18
+ }
19
+
20
+ // 解析键值对格式 (key=value)
21
+ const [key, value] = trimmedLine.split('=').map(part => part.trim());
22
+ if (key && value) {
23
+ config[key.toLowerCase()] = value;
24
+ }
25
+ }
26
+ } catch (error) {
27
+ console.warn('Warning: Could not read config file:', error.message);
28
+ }
29
+ }
30
+
31
+ return config;
32
+ }
33
+
34
+ // 获取配置值
35
+ function getConfigValue(config, key) {
36
+ // 优先级:配置文件 > 环境变量 > 默认值
37
+ return config[key] ||
38
+ process.env[key.toUpperCase()] ||
39
+ null;
40
+ }
41
+
42
+ // 获取所有分支
43
+ async function getAllBranches(apiClient, projectId) {
44
+ const branches = [];
45
+ let page = 1;
46
+ let hasNextPage = true;
47
+
48
+ try {
49
+ while (hasNextPage) {
50
+ const response = await apiClient.get(`/projects/${projectId}/repository/branches`, {
51
+ params: {
52
+ page: page,
53
+ per_page: 100
54
+ }
55
+ });
56
+
57
+ branches.push(...response.data);
58
+
59
+ // 检查是否有下一页
60
+ const linkHeader = response.headers.link;
61
+ hasNextPage = linkHeader && linkHeader.includes('rel="next"');
62
+ page++;
63
+ }
64
+
65
+ return branches;
66
+ } catch (error) {
67
+ console.error('Error fetching branches:', error.message);
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ // 获取分支最后提交信息(包括提交者信息)
73
+ async function getLastCommitInfo(apiClient, projectId, branchName) {
74
+ try {
75
+ const response = await apiClient.get(`/projects/${projectId}/repository/commits`, {
76
+ params: {
77
+ ref_name: branchName,
78
+ per_page: 1
79
+ }
80
+ });
81
+
82
+ if (response.data.length > 0) {
83
+ const commit = response.data[0];
84
+ return {
85
+ date: new Date(commit.committed_date),
86
+ author: commit.author_name || 'Unknown'
87
+ };
88
+ }
89
+
90
+ return null;
91
+ } catch (error) {
92
+ console.error(`Error fetching commit info for branch ${branchName}:`, error.message);
93
+ return null;
94
+ }
95
+ }
96
+
97
+ // 列出所有分支
98
+ async function list(options) {
99
+ // 读取配置
100
+ const configPath = options.config || '.glabrc';
101
+ const config = readConfig(configPath);
102
+
103
+ // 获取配置值
104
+ const gitlabUrl = getConfigValue(config, 'gitlab_url');
105
+ const token = getConfigValue(config, 'gitlab_token');
106
+ const projectId = getConfigValue(config, 'project_id');
107
+
108
+ // 验证必需选项
109
+ if (!gitlabUrl) {
110
+ console.error('Error: GitLab API URL is required');
111
+ process.exit(1);
112
+ }
113
+
114
+ if (!token) {
115
+ console.error('Error: GitLab token is required');
116
+ process.exit(1);
117
+ }
118
+
119
+ if (!projectId) {
120
+ console.error('Error: Project ID or path is required');
121
+ process.exit(1);
122
+ }
123
+
124
+ // 构建 GitLab API 基础 URL
125
+ const gitlabBaseUrl = gitlabUrl.replace(/\/$/, ''); // 移除末尾的斜杠
126
+
127
+ // 创建 axios 实例
128
+ const apiClient = axios.create({
129
+ baseURL: gitlabBaseUrl,
130
+ headers: {
131
+ 'PRIVATE-TOKEN': token,
132
+ 'Content-Type': 'application/json'
133
+ }
134
+ });
135
+
136
+ // 对项目ID进行URL编码(如果包含特殊字符)
137
+ const encodedProjectId = encodeURIComponent(projectId);
138
+
139
+ // 列出所有分支
140
+ try {
141
+ const branches = await getAllBranches(apiClient, encodedProjectId);
142
+
143
+ // 计算每列的最大宽度
144
+ let maxBranchWidth = 6; // "Branch" header length
145
+ let maxDateWidth = 12; // "Created Date" header length
146
+ let maxAuthorWidth = 6; // "Author" header length
147
+ let maxProtectedWidth = 8; // "Protected" header length
148
+
149
+ // 遍历所有分支计算最大宽度
150
+ for (const branch of branches) {
151
+ maxBranchWidth = Math.max(maxBranchWidth, branch.name.length);
152
+ // 日期格式通常是 YYYY-MM-DD,固定长度为 10
153
+ maxDateWidth = Math.max(maxDateWidth, 10);
154
+ // 作者名称可能较长,但通常不会太长
155
+ maxAuthorWidth = Math.max(maxAuthorWidth, 16);
156
+ maxProtectedWidth = Math.max(maxProtectedWidth, 8);
157
+ }
158
+
159
+ // 确保最小宽度
160
+ maxBranchWidth = Math.max(maxBranchWidth, 6);
161
+ maxDateWidth = Math.max(maxDateWidth, 12);
162
+ maxAuthorWidth = Math.max(maxAuthorWidth, 6);
163
+ maxProtectedWidth = Math.max(maxProtectedWidth, 8);
164
+
165
+ // 添加一些额外空间
166
+ maxBranchWidth += 2;
167
+ maxDateWidth += 2;
168
+ maxAuthorWidth += 2;
169
+ maxProtectedWidth += 2;
170
+
171
+ // 构建分隔线
172
+ const separator = '+' + '-'.repeat(maxBranchWidth) + '+' + '-'.repeat(maxDateWidth) + '+' + '-'.repeat(maxAuthorWidth) + '+' + '-'.repeat(maxProtectedWidth) + '+';
173
+
174
+ // 输出表格头部
175
+ console.log(`\nFound ${branches.length} branches:`);
176
+ console.log(separator);
177
+ console.log(`| ${'Branch'.padEnd(maxBranchWidth - 2)} | ${'Created Date'.padEnd(maxDateWidth - 2)} | ${'Author'.padEnd(maxAuthorWidth - 2)} | ${'Protected'.padEnd(maxProtectedWidth - 2)} |`);
178
+ console.log(separator);
179
+
180
+ // 输出每个分支的信息
181
+ for (const branch of branches) {
182
+ // 获取最后提交信息
183
+ const commitInfo = await getLastCommitInfo(apiClient, encodedProjectId, branch.name);
184
+
185
+ // 格式化分支名称(不截断)
186
+ const branchName = branch.name;
187
+
188
+ // 格式化日期
189
+ const commitDate = commitInfo ? commitInfo.date.toISOString().split('T')[0] : 'Unknown';
190
+
191
+ // 格式化作者名称
192
+ const author = commitInfo ? (commitInfo.author.length > maxAuthorWidth - 2 ? commitInfo.author.substring(0, maxAuthorWidth - 5) + '...' : commitInfo.author) : 'Unknown';
193
+
194
+ // 格式化保护状态
195
+ const protectedStatus = branch.protected ? 'Yes' : 'No';
196
+
197
+ // 输出表格行
198
+ console.log(`| ${branchName.padEnd(maxBranchWidth - 2)} | ${commitDate.padEnd(maxDateWidth - 2)} | ${author.padEnd(maxAuthorWidth - 2)} | ${protectedStatus.padEnd(maxProtectedWidth - 2)} |`);
199
+ }
200
+
201
+ console.log(separator);
202
+ } catch (error) {
203
+ console.error('Error listing branches:', error.message);
204
+ process.exit(1);
205
+ }
206
+ }
207
+
208
+ export { list };