glab-tool 1.0.4 → 1.0.5
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 +18 -7
- package/index.js +3 -2
- package/package.json +2 -2
- package/src/cleanup.js +96 -6
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- **按状态分组** - 分支按 "已合并" 和 "已关闭" 分组展示
|
|
11
11
|
- **干运行模式** - 预览将删除的分支而不实际执行
|
|
12
12
|
- **强制删除** - 支持强制删除受保护分支
|
|
13
|
+
- **本地分支清理** - 可选同时删除本地对应的 Git 分支
|
|
13
14
|
- **通配符支持** - 目标分支支持通配符匹配(如 `release/*`)
|
|
14
15
|
|
|
15
16
|
## 安装
|
|
@@ -74,21 +75,25 @@ glabx clean [options]
|
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
**选项**:
|
|
77
|
-
-
|
|
78
|
-
-
|
|
78
|
+
- `-t, --target <branch>`: 目标分支(默认: `main`),支持通配符如 `release/*`
|
|
79
|
+
- `-f, --force`: 强制删除受保护分支(远程)和未合并分支(本地)
|
|
79
80
|
- `-d, --dry-run`: 预览模式,显示将删除的分支但不实际删除
|
|
81
|
+
- `-l, --local`: 同时删除本地对应的 Git 分支
|
|
80
82
|
|
|
81
83
|
**使用示例**:
|
|
82
84
|
|
|
83
85
|
```bash
|
|
84
|
-
# 清理 targeting main
|
|
86
|
+
# 清理 targeting main 分支的分支(仅远程)
|
|
85
87
|
glabx clean --target main
|
|
86
88
|
|
|
87
|
-
#
|
|
88
|
-
glabx clean --target main --
|
|
89
|
+
# 同时删除远程和本地分支
|
|
90
|
+
glabx clean --target main --local
|
|
89
91
|
|
|
90
|
-
#
|
|
91
|
-
glabx clean --target main --
|
|
92
|
+
# 预览将删除的分支(包括本地分支检查)
|
|
93
|
+
glabx clean --target main --local --dry-run
|
|
94
|
+
|
|
95
|
+
# 强制删除(包括受保护的远程分支和未合并的本地分支)
|
|
96
|
+
glabx clean --target main --local --force
|
|
92
97
|
|
|
93
98
|
# 使用通配符匹配多个目标分支
|
|
94
99
|
glabx clean --target release/*
|
|
@@ -97,6 +102,12 @@ glabx clean --target release/*
|
|
|
97
102
|
glabx clean
|
|
98
103
|
```
|
|
99
104
|
|
|
105
|
+
**关于 --local 说明**:
|
|
106
|
+
- 仅在当前目录是 Git 仓库且分支存在时才会删除本地分支
|
|
107
|
+
- 默认使用 `git branch -d` 安全删除(仅删除已合并的分支)
|
|
108
|
+
- 配合 `--force` 使用 `git branch -D` 强制删除(即使分支未合并)
|
|
109
|
+
- dry-run 模式下会显示 `[local exists]` 标识表示本地有对应分支
|
|
110
|
+
|
|
100
111
|
## 配置
|
|
101
112
|
|
|
102
113
|
支持三种配置方式(优先级从高到低):
|
package/index.js
CHANGED
|
@@ -15,9 +15,10 @@ program
|
|
|
15
15
|
program
|
|
16
16
|
.command('clean')
|
|
17
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')
|
|
18
|
+
.option('-t, --target <branch>', 'Target branch to filter merge requests (default: main)', 'main')
|
|
19
|
+
.option('-f, --force', 'Force delete protected branches')
|
|
20
20
|
.option('-d, --dry-run', 'Show what would be deleted without actually deleting')
|
|
21
|
+
.option('-l, --local', 'Also delete local Git branches')
|
|
21
22
|
.action(async (options) => {
|
|
22
23
|
await cleanup(options);
|
|
23
24
|
});
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glab-tool",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "GitLab Extended CLI Tool - A multi-purpose GitLab utility CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"glabx": "
|
|
8
|
+
"glabx": "index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"src/*",
|
package/src/cleanup.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
4
8
|
|
|
5
9
|
// 从配置文件读取配置
|
|
6
10
|
function readConfig(configPath) {
|
|
@@ -113,11 +117,11 @@ function matchesPattern(branchName, pattern) {
|
|
|
113
117
|
return regex.test(branchName);
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
//
|
|
120
|
+
// 删除远程分支
|
|
117
121
|
async function deleteBranch(apiClient, projectId, branchName, force = false) {
|
|
118
122
|
try {
|
|
119
123
|
const deleteUrl = `/projects/${projectId}/repository/branches/${encodeURIComponent(branchName)}`;
|
|
120
|
-
|
|
124
|
+
|
|
121
125
|
// 如果是强制删除,需要使用不同的API端点
|
|
122
126
|
if (force) {
|
|
123
127
|
// 使用强制删除的API端点
|
|
@@ -130,8 +134,8 @@ async function deleteBranch(apiClient, projectId, branchName, force = false) {
|
|
|
130
134
|
// 正常删除
|
|
131
135
|
await apiClient.delete(deleteUrl);
|
|
132
136
|
}
|
|
133
|
-
|
|
134
|
-
console.log(`Successfully deleted branch: ${branchName}`);
|
|
137
|
+
|
|
138
|
+
console.log(`Successfully deleted remote branch: ${branchName}`);
|
|
135
139
|
return true;
|
|
136
140
|
} catch (error) {
|
|
137
141
|
if (error.response?.status === 403) {
|
|
@@ -145,6 +149,51 @@ async function deleteBranch(apiClient, projectId, branchName, force = false) {
|
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
// 检查本地分支是否存在
|
|
153
|
+
async function localBranchExists(branchName) {
|
|
154
|
+
try {
|
|
155
|
+
await execAsync(`git rev-parse --verify ${branchName} 2>/dev/null`);
|
|
156
|
+
return true;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 获取所有本地分支
|
|
163
|
+
async function getLocalBranches() {
|
|
164
|
+
try {
|
|
165
|
+
const { stdout } = await execAsync('git branch --format="%(refname:short)"');
|
|
166
|
+
return stdout.trim().split('\n').filter(Boolean);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn('Warning: Could not get local branches:', error.message);
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 删除本地分支
|
|
174
|
+
async function deleteLocalBranch(branchName, force = false) {
|
|
175
|
+
try {
|
|
176
|
+
// 先检查分支是否存在
|
|
177
|
+
const exists = await localBranchExists(branchName);
|
|
178
|
+
if (!exists) {
|
|
179
|
+
console.log(`Local branch "${branchName}" does not exist, skipping.`);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 根据 force 参数决定使用 -d 还是 -D
|
|
184
|
+
const deleteFlag = force ? '-D' : '-d';
|
|
185
|
+
await execAsync(`git branch ${deleteFlag} ${branchName}`);
|
|
186
|
+
console.log(`Successfully deleted local branch: ${branchName}`);
|
|
187
|
+
return true;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(`Error deleting local branch "${branchName}":`, error.message);
|
|
190
|
+
if (!force) {
|
|
191
|
+
console.error(` Try using --force to force delete unmerged local branches.`);
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
148
197
|
// 获取项目中的所有分支名称
|
|
149
198
|
async function getAllBranches(apiClient, projectId) {
|
|
150
199
|
const branches = [];
|
|
@@ -306,14 +355,31 @@ async function cleanup(options) {
|
|
|
306
355
|
// 询问用户是否确认删除
|
|
307
356
|
if (options.dryRun) {
|
|
308
357
|
console.log('Dry run mode: No branches will be deleted.');
|
|
358
|
+
|
|
359
|
+
// 如果启用了 --local,获取本地分支列表
|
|
360
|
+
let localBranches = [];
|
|
361
|
+
if (options.local) {
|
|
362
|
+
localBranches = await getLocalBranches();
|
|
363
|
+
}
|
|
364
|
+
|
|
309
365
|
// 显示分组统计信息
|
|
310
366
|
if (mergedBranches.length > 0) {
|
|
311
367
|
console.log(`\nMerged branches (${mergedBranches.length}):`);
|
|
312
|
-
mergedBranches.forEach(mr =>
|
|
368
|
+
mergedBranches.forEach(mr => {
|
|
369
|
+
const localExists = options.local && localBranches.includes(mr.source_branch) ? ' [local exists]' : '';
|
|
370
|
+
console.log(` - MR !${mr.iid}: ${mr.source_branch}${localExists}`);
|
|
371
|
+
});
|
|
313
372
|
}
|
|
314
373
|
if (closedBranches.length > 0) {
|
|
315
374
|
console.log(`\nClosed branches (${closedBranches.length}):`);
|
|
316
|
-
closedBranches.forEach(mr =>
|
|
375
|
+
closedBranches.forEach(mr => {
|
|
376
|
+
const localExists = options.local && localBranches.includes(mr.source_branch) ? ' [local exists]' : '';
|
|
377
|
+
console.log(` - MR !${mr.iid}: ${mr.source_branch}${localExists}`);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (options.local) {
|
|
382
|
+
console.log('\n--local flag enabled: Local branches will also be deleted.');
|
|
317
383
|
}
|
|
318
384
|
return;
|
|
319
385
|
}
|
|
@@ -322,8 +388,20 @@ async function cleanup(options) {
|
|
|
322
388
|
if (options.force) {
|
|
323
389
|
console.log('Force mode enabled: Deleting all branches without confirmation...');
|
|
324
390
|
const allBranchesToDelete = [...mergedBranches, ...closedBranches];
|
|
391
|
+
|
|
392
|
+
// 如果启用了 --local,先获取本地分支列表
|
|
393
|
+
let localBranches = [];
|
|
394
|
+
if (options.local) {
|
|
395
|
+
localBranches = await getLocalBranches();
|
|
396
|
+
}
|
|
397
|
+
|
|
325
398
|
for (const mr of allBranchesToDelete) {
|
|
399
|
+
// 删除远程分支
|
|
326
400
|
await deleteBranch(apiClient, encodedProjectId, mr.source_branch, true);
|
|
401
|
+
// 如果启用了 --local 且该分支存在于本地,也删除本地分支
|
|
402
|
+
if (options.local && localBranches.includes(mr.source_branch)) {
|
|
403
|
+
await deleteLocalBranch(mr.source_branch, true);
|
|
404
|
+
}
|
|
327
405
|
}
|
|
328
406
|
return;
|
|
329
407
|
}
|
|
@@ -429,8 +507,20 @@ async function cleanup(options) {
|
|
|
429
507
|
|
|
430
508
|
if (confirmAnswers.confirmDelete) {
|
|
431
509
|
console.log('\nDeleting branches...');
|
|
510
|
+
|
|
511
|
+
// 如果启用了 --local,先获取本地分支列表
|
|
512
|
+
let localBranches = [];
|
|
513
|
+
if (options.local) {
|
|
514
|
+
localBranches = await getLocalBranches();
|
|
515
|
+
}
|
|
516
|
+
|
|
432
517
|
for (const branch of selectedBranches) {
|
|
518
|
+
// 删除远程分支
|
|
433
519
|
await deleteBranch(apiClient, encodedProjectId, branch.sourceBranch, options.force);
|
|
520
|
+
// 如果启用了 --local 且该分支存在于本地,也删除本地分支
|
|
521
|
+
if (options.local && localBranches.includes(branch.sourceBranch)) {
|
|
522
|
+
await deleteLocalBranch(branch.sourceBranch, options.force);
|
|
523
|
+
}
|
|
434
524
|
}
|
|
435
525
|
} else {
|
|
436
526
|
console.log('\nOperation cancelled by user.');
|