Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ git gtr list
# Remove when done
git gtr rm my-feature

# Or remove all worktrees with merged PRs/MRs (requires gh or glab CLI)
git gtr clean --merged
# Or remove all worktrees with merged or closed PRs/MRs (requires gh or glab CLI)
git gtr clean --merged --closed
```

## Why gtr?
Expand Down Expand Up @@ -321,27 +321,29 @@ git gtr config list # List all gtr config

### `git gtr clean [options]`

Remove worktrees: clean up empty directories, or remove those with merged PRs/MRs.
Remove worktrees: clean up empty directories, or remove those with merged or closed PRs/MRs.

```bash
git gtr clean # Remove empty worktree directories and prune
git gtr clean --merged # Remove worktrees for merged PRs/MRs
git gtr clean --merged --to main # Only remove worktrees merged to main
git gtr clean --closed # Remove worktrees for closed PRs/MRs
git gtr clean --merged --closed --to main # Remove worktrees for merged or closed PRs/MRs targeting main
git gtr clean --merged --dry-run # Preview which worktrees would be removed
git gtr clean --merged --yes # Remove without confirmation prompts
git gtr clean --merged --force # Force-clean merged, ignoring local changes
git gtr clean --merged --force # Force-clean PR cleanup, ignoring local changes
git gtr clean --merged --force --yes # Force-clean and auto-confirm
```

**Options:**

- `--merged`: Remove worktrees whose branches have merged PRs/MRs (also deletes the branch)
- `--to <ref>`: Limit `--merged` cleanup to PRs/MRs merged into the given base ref
- `--closed`: Remove worktrees whose branches have closed PRs/MRs (also deletes the branch)
- `--to <ref>`: Limit PR/MR cleanup to PRs/MRs targeting the given base ref
- `--dry-run`, `-n`: Preview changes without removing
- `--yes`, `-y`: Non-interactive mode (skip confirmation prompts)
- `--force`, `-f`: Force removal even if worktree has uncommitted changes or untracked files

**Note:** The `--merged` mode auto-detects your hosting provider (GitHub or GitLab) from the `origin` remote URL and requires the corresponding CLI tool (`gh` or `glab`) to be installed and authenticated. For self-hosted instances, set the provider explicitly: `git gtr config set gtr.provider gitlab`.
**Note:** The `--merged`/`--closed` mode auto-detects your hosting provider (GitHub or GitLab) from the `origin` remote URL and requires the corresponding CLI tool (`gh` or `glab`) to be installed and authenticated. For self-hosted instances, set the provider explicitly: `git gtr config set gtr.provider gitlab`.

**Note:** `clean` also detects registry entries that are locked but whose directories no longer exist (for example, a crashed agent session that deleted its worktree directory). `git worktree prune` skips locked entries by design, so these linger and keep their branches checked out. `clean` offers to unlock and prune them; `--force` or `--yes` confirms automatically.

Expand Down
3 changes: 2 additions & 1 deletion completions/_git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ _git-gtr() {
if (( CURRENT >= 4 )) && [[ $words[3] == clean ]]; then
_arguments \
'--merged[Remove worktrees with merged PRs/MRs]' \
'--to[Only remove worktrees for PRs/MRs merged into this ref]:ref:' \
'--closed[Remove worktrees with closed PRs/MRs]' \
'--to[Only remove worktrees for PRs/MRs targeting this ref]:ref:' \
'--yes[Skip confirmation prompts]' \
'-y[Skip confirmation prompts]' \
'--dry-run[Show what would be removed]' \
Expand Down
3 changes: 2 additions & 1 deletion completions/git-gtr.fish
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ complete -c git -n '__fish_git_gtr_using_command ai' -l ai -d 'AI tool to use' -

# Clean command options
complete -c git -n '__fish_git_gtr_using_command clean' -l merged -d 'Remove worktrees with merged PRs/MRs'
complete -c git -n '__fish_git_gtr_using_command clean' -l to -d 'Only remove worktrees for PRs/MRs merged into this ref' -r
complete -c git -n '__fish_git_gtr_using_command clean' -l closed -d 'Remove worktrees with closed PRs/MRs'
complete -c git -n '__fish_git_gtr_using_command clean' -l to -d 'Only remove worktrees for PRs/MRs targeting this ref' -r
complete -c git -n '__fish_git_gtr_using_command clean' -l yes -d 'Skip confirmation prompts'
complete -c git -n '__fish_git_gtr_using_command clean' -s y -d 'Skip confirmation prompts'
complete -c git -n '__fish_git_gtr_using_command clean' -l dry-run -d 'Show what would be removed'
Expand Down
2 changes: 1 addition & 1 deletion completions/gtr.bash
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ _git_gtr() {
;;
clean)
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--merged --to --yes -y --dry-run -n --force -f" -- "$cur"))
COMPREPLY=($(compgen -W "--merged --closed --to --yes -y --dry-run -n --force -f" -- "$cur"))
fi
;;
copy)
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ echo "/.worktrees/" >> .gitignore

## Provider Settings

The `clean --merged` command auto-detects your hosting provider from the `origin` remote URL (`github.com` → GitHub, `gitlab.com` → GitLab). For self-hosted instances, set the provider explicitly:
The `clean --merged` and `clean --closed` commands auto-detect your hosting provider from the `origin` remote URL (`github.com` → GitHub, `gitlab.com` → GitLab). For self-hosted instances, set the provider explicitly:

```bash
# Override auto-detected hosting provider (github or gitlab)
Expand Down
66 changes: 47 additions & 19 deletions lib/commands/clean.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ _clean_detect_provider() {
return 1
}

# Check if a worktree should be skipped during merged cleanup.
# Check if a worktree should be skipped during PR/MR cleanup.
# Returns 0 if should skip, 1 if should process.
# Usage: _clean_should_skip <dir> <branch> [force] [active_worktree_path]
_clean_should_skip() {
Expand Down Expand Up @@ -122,16 +122,42 @@ EOF
fi
}

# Remove worktrees whose PRs/MRs are merged (handles squash merges)
# Usage: _clean_merged repo_root base_dir prefix yes_mode dry_run [force] [active_worktree_path] [target_ref]
_clean_merged() {
local repo_root="$1" base_dir="$2" prefix="$3" yes_mode="$4" dry_run="$5" force="${6:-0}" active_worktree_path="${7:-}" target_ref="${8:-}"
# Check if a branch has any requested PR/MR cleanup state.
# Returns 0 if matched, 1 if not.
# Usage: _clean_branch_matches_pr_state provider branch target_ref branch_tip merged_mode closed_mode
_clean_branch_matches_pr_state() {
local provider="$1" branch="$2" target_ref="$3" branch_tip="$4" merged_mode="$5" closed_mode="$6"

# base_dir and prefix are kept for the helper contract. Merged cleanup uses
if [ "$merged_mode" -eq 1 ] && check_branch_merged "$provider" "$branch" "$target_ref" "$branch_tip"; then
return 0
fi

if [ "$closed_mode" -eq 1 ] && check_branch_closed "$provider" "$branch" "$target_ref" "$branch_tip"; then
return 0
fi

return 1
}

# Remove worktrees whose PRs/MRs match requested cleanup states (handles squash merges)
# Usage: _clean_prs repo_root base_dir prefix yes_mode dry_run [force] [active_worktree_path] [target_ref] [merged_mode] [closed_mode]
_clean_prs() {
local repo_root="$1" base_dir="$2" prefix="$3" yes_mode="$4" dry_run="$5" force="${6:-0}" active_worktree_path="${7:-}" target_ref="${8:-}" merged_mode="${9:-0}" closed_mode="${10:-0}"

# base_dir and prefix are kept for the helper contract. PR/MR cleanup uses
# Git's registry so nested registered worktrees are processed directly.
: "$base_dir" "$prefix"

log_step "Checking for worktrees with merged PRs/MRs..."
local cleanup_label="matching"
if [ "$merged_mode" -eq 1 ] && [ "$closed_mode" -eq 1 ]; then
cleanup_label="merged or closed"
elif [ "$merged_mode" -eq 1 ]; then
cleanup_label="merged"
elif [ "$closed_mode" -eq 1 ]; then
cleanup_label="closed"
fi

log_step "Checking for worktrees with $cleanup_label PRs/MRs..."

local provider
provider=$(_clean_detect_provider) || exit 1
Expand All @@ -157,13 +183,13 @@ _clean_merged() {
# Skip main repo branch silently (not counted)
[ "$branch" = "$main_branch" ] && continue

# Check if branch has a merged PR/MR
if check_branch_merged "$provider" "$branch" "$target_ref" "$branch_tip"; then
if _clean_should_skip "$dir" "$branch" "$force" "$active_worktree_path"; then
skipped=$((skipped + 1))
continue
fi
if _clean_should_skip "$dir" "$branch" "$force" "$active_worktree_path"; then
skipped=$((skipped + 1))
continue
fi

# Check if branch has a PR/MR matching any requested state
if _clean_branch_matches_pr_state "$provider" "$branch" "$target_ref" "$branch_tip" "$merged_mode" "$closed_mode"; then
if [ "$dry_run" -eq 1 ]; then
log_info "[dry-run] Would remove: $branch ($dir)"
removed=$((removed + 1))
Expand Down Expand Up @@ -219,29 +245,31 @@ EOF
if [ "$dry_run" -eq 1 ]; then
log_info "Dry run complete. Would remove: $removed, Skipped: $skipped"
else
log_info "Merged cleanup complete. Removed: $removed, Skipped: $skipped"
log_info "PR cleanup complete. Removed: $removed, Skipped: $skipped"
fi
}

# shellcheck disable=SC2154 # _arg_* set by parse_args, _ctx_* set by resolve_*
cmd_clean() {
local _spec
_spec="--merged
--closed
--to: value
--yes|-y
--dry-run|-n
--force|-f"
parse_args "$_spec" "$@"

local merged_mode="${_arg_merged:-0}"
local closed_mode="${_arg_closed:-0}"
local target_ref="${_arg_to:-}"
local yes_mode="${_arg_yes:-0}"
local dry_run="${_arg_dry_run:-0}"
local force="${_arg_force:-0}"
local active_worktree_path=""

if [ -n "$target_ref" ] && [ "$merged_mode" -ne 1 ]; then
log_error "--to can only be used with --merged"
if [ -n "$target_ref" ] && [ "$merged_mode" -ne 1 ] && [ "$closed_mode" -ne 1 ]; then
log_error "--to can only be used with --merged or --closed"
return 1
fi

Expand Down Expand Up @@ -294,8 +322,8 @@ EOF
log_info "Cleanup complete (no empty directories found)"
fi

# --merged mode: remove worktrees with merged PRs/MRs (handles squash merges)
if [ "$merged_mode" -eq 1 ]; then
_clean_merged "$repo_root" "$base_dir" "$prefix" "$yes_mode" "$dry_run" "$force" "$active_worktree_path" "$target_ref"
# PR/MR cleanup mode: remove worktrees with matching PRs/MRs (handles squash merges)
if [ "$merged_mode" -eq 1 ] || [ "$closed_mode" -eq 1 ]; then
_clean_prs "$repo_root" "$base_dir" "$prefix" "$yes_mode" "$dry_run" "$force" "$active_worktree_path" "$target_ref" "$merged_mode" "$closed_mode"
fi
}
6 changes: 3 additions & 3 deletions lib/commands/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,19 @@ cmd_doctor() {
if command -v gh >/dev/null 2>&1; then
echo "[OK] GitHub CLI: $(gh --version 2>/dev/null | head -1)"
else
echo "[!] GitHub CLI: not found (needed for: clean --merged)"
echo "[!] GitHub CLI: not found (needed for: clean --merged/--closed)"
fi
;;
gitlab)
if command -v glab >/dev/null 2>&1; then
echo "[OK] GitLab CLI: $(glab --version 2>/dev/null | head -1)"
else
echo "[!] GitLab CLI: not found (needed for: clean --merged)"
echo "[!] GitLab CLI: not found (needed for: clean --merged/--closed)"
fi
;;
esac
else
echo "[i] Provider: unknown (set gtr.provider for clean --merged)"
echo "[i] Provider: unknown (set gtr.provider for clean --merged/--closed)"
fi
fi

Expand Down
17 changes: 10 additions & 7 deletions lib/commands/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -308,27 +308,29 @@ git gtr clean - Remove stale worktrees
Usage: git gtr clean [options]

Removes empty worktree directories and optionally removes worktrees whose
PRs/MRs have been merged. Auto-detects GitHub (gh) or GitLab (glab) from
the remote URL.
PRs/MRs have been merged or closed. Auto-detects GitHub (gh) or GitLab
(glab) from the remote URL.

Also detects registry entries that are locked but whose directories no
longer exist (git worktree prune skips locked entries) and offers to
unlock and prune them. Confirmed automatically with --force or --yes.

Options:
--merged Also remove worktrees with merged PRs/MRs
--to <ref> Only remove worktrees for PRs/MRs merged into <ref>
--closed Also remove worktrees with closed PRs/MRs
--to <ref> Only remove worktrees for PRs/MRs targeting <ref>
--yes, -y Skip confirmation prompts
--dry-run, -n Show what would be removed without removing
--force, -f Force removal even if worktree has uncommitted changes or untracked files

Examples:
git gtr clean # Clean empty directories
git gtr clean --merged # Also clean merged PRs
git gtr clean --merged --to main # Only clean PRs merged to main
git gtr clean --merged --dry-run # Preview merged cleanup
git gtr clean --closed # Also clean closed PRs
git gtr clean --merged --closed --to main # Clean merged or closed PRs targeting main
git gtr clean --merged --dry-run # Preview PR cleanup
git gtr clean --merged --yes # Auto-confirm everything
git gtr clean --merged --force # Force-clean merged, ignoring local changes
git gtr clean --merged --force # Force-clean, ignoring local changes
git gtr clean --merged --force --yes # Force-clean and auto-confirm
EOF
}
Expand Down Expand Up @@ -606,7 +608,8 @@ SETUP & MAINTENANCE:
clean [options]
Remove stale/prunable worktrees and empty directories
--merged: also remove worktrees with merged PRs/MRs
--to <ref>: limit merged cleanup to PRs/MRs merged into <ref>
--closed: also remove worktrees with closed PRs/MRs
--to <ref>: limit PR cleanup to PRs/MRs targeting <ref>
Auto-detects GitHub (gh) or GitLab (glab) from remote URL
Override: git gtr config set gtr.provider gitlab
--yes, -y: skip confirmation prompts
Expand Down
Loading