Add Branch Pruning Command to GitLab CLI
## Summary Add a native command to `glab` that automatically identifies and deletes local Git branches that have been merged to GitLab, replacing the need for the standalone Ruby script currently used in the main GitLab project. ## Motivation The main GitLab project includes a Ruby script ([`scripts/prune_merged_branches`](https://gitlab.com/gitlab-org/gitlab/-/raw/master/scripts/prune_merged_branches)) that helps developers clean up local branches that have already been merged. This is a common workflow need across all GitLab projects, not just the main repo. **Current workflow pain points:** - Users must maintain external scripts for branch cleanup - Scripts require dependencies (Ruby, rainbow gem) - Manual tracking of which branches have merged MRs - Risk of accidentally deleting unmerged work **Value of native CLI support:** - Built into `glab` - no external dependencies - Consistent UX with other `glab` commands - Better safety guardrails and confirmations - Discoverable through `glab --help` - Works across all GitLab projects ### Why Not Just Use `git branch --merged`? A common Git-only approach for pruning branches is: ```bash git branch --merged | grep -v "\*" | grep -v "master\|main" | xargs git branch -d ``` However, this has a **critical limitation** with modern GitLab workflows: **`git branch --merged` only detects fast-forward merges.** It checks if a branch's commits exist in the current branch's history. This fails for: 1. **Squash merges**: GitLab MR shows "merged" but all commits were squashed into one, so Git doesn't recognize the original commits as merged 2. **Rebase merges**: GitLab MR shows "merged" but commits were rewritten with new SHAs, so Git sees them as different commits **In modern GitLab projects, squash and rebase merges are extremely common** (often enforced by project settings). This means `git branch --merged` will miss most merged branches. **`glab mr prune` solves this** by checking the **GitLab merge request status** (the platform source of truth) rather than trying to infer from local Git history. If GitLab says the MR is merged, it's safe to delete the local branch regardless of merge strategy. ## Proposed Command **`glab mr prune [flags]`** **Rationale:** - The command's primary logic is checking merge request status on GitLab - Existing MR commands already perform local Git operations (`mr checkout`, `mr merge`) - Shorter, more memorable than `repo prune-branches` - Natural semantic fit: "prune branches associated with merged MRs" - Documentation will clarify this is local-only (following precedent from `mr checkout`) **Alternative considered:** `glab repo prune-branches` - groups with repo operations but risks confusion about whether it affects remote repository. Mentioned here for completeness but not recommended. ## Detailed Behavior ### Core Algorithm 1. **Discovery**: List all local Git branches (excluding default branch and current branch) 2. **Validation**: For each branch, query GitLab API for merged merge requests with that source branch 3. **Confirmation**: Show list of branches with merged MRs and prompt for confirmation 4. **Deletion**: Delete confirmed branches using `git branch -D` 5. **Summary**: Report how many branches were deleted ### Safety Features - **Confirmation prompt required** by default (unless `--yes` flag used) - **Never deletes:** - Default branch (main/master) - Current checked-out branch (unless `--include-current`) - Explicitly excluded branches (via `--exclude`) - **Dry-run mode** to preview without deleting - **Clear output** showing exactly what will be deleted - **Graceful error handling** for API failures ## Proposed Flags | Flag | Short | Description | Default | |------|-------|-------------|---------| | `--dry-run` | | Preview branches that would be deleted without deleting | false | | `--yes` | `-y` | Skip confirmation prompt | false (interactive prompt) | | `--exclude` | `-e` | Comma-separated list of branch patterns to exclude (repeatable) | none | | `--include-current` | | Allow pruning current branch | false | | `--target-branch` | `-t` | Target branch to check MRs against | repo default | | `--output` | `-F` | Output format: text or json | text | ## Examples ### Preview what would be deleted ```bash $ glab mr prune --dry-run ``` **Output:** ``` 🔍 Checking local branches for merged merge requests... The following branches have merged MRs and would be deleted: ✓ feature/add-user-auth (MR !123 merged to main) ✓ bugfix/fix-typo (MR !124 merged to main) ✓ refactor/cleanup-code (MR !125 merged to main) 3 branches would be deleted. Run without --dry-run to delete these branches. ``` ### Delete merged branches with confirmation ```bash $ glab mr prune ``` **Output:** ``` 🔍 Checking local branches for merged merge requests... The following branches have merged MRs: ✓ feature/add-user-auth (MR !123 merged to main) ✓ bugfix/fix-typo (MR !124 merged to main) ✓ refactor/cleanup-code (MR !125 merged to main) ⚠️ This will permanently delete 3 local branches. This cannot be undone. ? Are you sure you want to delete these branches? (y/N) y ✓ Deleted feature/add-user-auth ✓ Deleted bugfix/fix-typo ✓ Deleted refactor/cleanup-code 3 branches deleted successfully. ``` ### Delete without confirmation (for scripts/automation) ```bash $ glab mr prune --yes ``` ### Exclude specific branches ```bash $ glab mr prune --exclude feature-important,wip-* ``` ### No branches to prune ```bash $ glab mr prune ``` **Output:** ``` 🔍 Checking local branches for merged merge requests... ✓ No branches found with merged merge requests. ``` ## Technical Implementation Notes ### Key Files to Create/Modify 1. **New command file**: `/internal/commands/mr/prune/prune.go` (or `/internal/commands/repo/prune-branches/prune_branches.go`) 2. **Register command**: Modify `/internal/commands/mr/mr.go` (or `/internal/commands/project/repo.go`) 3. **Git utilities**: May need to add `ListLocalBranches()` to `/internal/git/git_runner.go` ### Leveraging Existing Code - **MR API calls**: Use existing `api.ListMRs()` from `/internal/api/merge_request.go` - **Git operations**: Use `git.DeleteLocalBranch()` from `/internal/git/git.go` - **Bulk deletion pattern**: Follow pattern from `/internal/commands/ci/delete/delete.go` - **Confirmation pattern**: Follow pattern from `/internal/commands/project/delete/delete.go` - **Factory pattern**: Use `cmdutils.Factory` for dependency injection ### API Usage For each local branch: ```go // Check for merged MRs with this source branch mrs, err := api.ListMRs(client, repo.FullName(), &gitlab.ListProjectMergeRequestsOptions{ State: gitlab.Ptr("merged"), SourceBranch: gitlab.Ptr(branchName), }) // If len(mrs) > 0, branch has merged MRs ``` ## Edge Cases & Considerations 1. **Rate Limiting**: With many branches, this makes one API call per branch. Consider: - Progress indicator for large branch counts - Potential rate limit handling - Maybe warn if >50 branches 2. **Multiple MRs from Same Branch**: If a branch has multiple MRs and any are merged, it's safe to delete 3. **Deleted/Force-Pushed MRs**: If MR was deleted or force-pushed, API may return no results. These branches are NOT deleted (conservative approach) 4. **Protected Branches**: Even if merged, branches like `develop`, `staging`, `production` shouldn't be deleted. Consider default exclude list or warning. 5. **Branch Naming**: Handle branches with special characters, spaces, slashes properly 6. **API Failures**: If API calls fail for some branches, skip them and continue with others 7. **Non-Interactive Mode**: Require `--yes` flag when not running in TTY (for CI/CD use) ## Testing Strategy ### Unit Tests - Branch filtering logic (exclude defaults, current, excluded patterns) - MR validation with mocked API responses - Confirmation prompt behavior - Error handling (API failures, git failures) - Output formatting (text and JSON) ### Integration Tests - Test with actual Git repository fixtures - Test with mock GitLab API server - Test interactive prompts ### Manual Testing Checklist - [ ] Run in repo with no branches to prune - [ ] Run in repo with merged branches - [ ] Test `--dry-run` mode - [ ] Test `--yes` flag - [ ] Test `--exclude` with various patterns - [ ] Test with current branch that has merged MR - [ ] Test API error handling - [ ] Test JSON output format - [ ] Test in non-interactive mode ## User Documentation ### Help Text ``` Prune local Git branches that have merged merge requests. This command identifies local branches where the associated merge request has been merged to the target branch, and deletes those local branches. This helps keep your local repository clean without manually tracking which branches have been merged. ⚠️ This only affects your local Git repository. Remote branches on GitLab are not touched. USAGE glab mr prune [flags] FLAGS --dry-run Preview branches that would be deleted -y, --yes Skip confirmation prompt -e, --exclude string Exclude branches matching pattern (repeatable) --include-current Allow pruning current branch -t, --target-branch Target branch to check MRs against -F, --output string Output format: text or json (default "text") EXAMPLES # Preview what would be deleted $ glab mr prune --dry-run # Delete merged branches with confirmation $ glab mr prune # Delete without confirmation (for scripts) $ glab mr prune --yes # Exclude specific branches $ glab mr prune --exclude feature-important,wip-* LEARN MORE Use 'glab mr <command> --help' for more information about a command. ``` ## Security & Safety - Command marked with `mcpannotations.Destructive: "true"` - Always requires confirmation in interactive mode unless `--yes` - Non-interactive mode (no TTY) requires explicit `--yes` flag - Clear warnings about permanent deletion - Conservative defaults (never delete default/current branch) ## Future Enhancements (Out of Scope) These could be added later based on user feedback: 1. **Remote branch pruning**: `--remote` flag to also delete remote branches (requires more safety checks) 2. **Age-based filtering**: `--older-than` flag to filter by branch age 3. **Interactive selection**: TUI to select specific branches to delete 4. **Undo support**: Keep backup refs for recently deleted branches 5. **Batch mode**: Read branch list from file ## Open Questions for Maintainers 1. **Default exclude list**: Should we have a built-in list of protected branch names (main, master, develop, staging, production)? 2. **Progress indicators**: For repos with many branches, should we show a progress bar during API calls? 3. **Parallel API calls**: Should we make API calls concurrently (with rate limit consideration)? --- ## Implementation Checklist When this issue is accepted and implementation begins: - [ ] Decide on final command location (`mr prune` vs `repo prune-branches`) - [ ] Create command structure following existing patterns - [ ] Implement core branch discovery and filtering logic - [ ] Implement MR validation via API - [ ] Add confirmation prompt with proper safety checks - [ ] Implement branch deletion with error handling - [ ] Add comprehensive unit tests - [ ] Add integration tests - [ ] Write user documentation and help text - [ ] Test with real-world repositories - [ ] Update main CLI documentation/changelog
issue