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 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
- - `--target <branch>`: 目标分支(默认: `main`),支持通配符如 `release/*`
78
- - `--force`: 强制删除受保护分支
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 --dry-run
89
+ # 同时删除远程和本地分支
90
+ glabx clean --target main --local
89
91
 
90
- # 强制删除(包括受保护分支)
91
- glabx clean --target main --force
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.4",
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": "./index.js"
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 => console.log(` - MR !${mr.iid}: ${mr.source_branch}`));
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 => console.log(` - MR !${mr.iid}: ${mr.source_branch}`));
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.');