Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ Remove worktrees: clean up empty directories, or remove those with merged PRs/MR
```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 --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
Expand All @@ -334,6 +335,7 @@ 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
- `--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
Expand Down
1 change: 1 addition & 0 deletions completions/_git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ _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:' \
'--yes[Skip confirmation prompts]' \
Comment thread
helizaga marked this conversation as resolved.
'-y[Skip confirmation prompts]' \
'--dry-run[Show what would be removed]' \
Expand Down
1 change: 1 addition & 0 deletions completions/git-gtr.fish
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ 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 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 --yes -y --dry-run -n --force -f" -- "$cur"))
COMPREPLY=($(compgen -W "--merged --to --yes -y --dry-run -n --force -f" -- "$cur"))
fi
;;
copy)
Expand Down
10 changes: 6 additions & 4 deletions lib/commands/clean.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ _clean_should_skip() {
}

# 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]
# 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:-}"
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:-}"

log_step "Checking for worktrees with merged PRs/MRs..."

Expand Down Expand Up @@ -100,7 +100,7 @@ _clean_merged() {
fi

# Check if branch has a merged PR/MR
if check_branch_merged "$provider" "$branch"; then
if check_branch_merged "$provider" "$branch" "$target_ref"; then
if [ "$dry_run" -eq 1 ]; then
log_info "[dry-run] Would remove: $branch ($dir)"
removed=$((removed + 1))
Expand Down Expand Up @@ -146,12 +146,14 @@ _clean_merged() {
cmd_clean() {
local _spec
_spec="--merged
--to: value
--yes|-y
--dry-run|-n
--force|-f"
parse_args "$_spec" "$@"

local merged_mode="${_arg_merged:-0}"
local target_ref="${_arg_to:-}"
local yes_mode="${_arg_yes:-0}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
local dry_run="${_arg_dry_run:-0}"
local force="${_arg_force:-0}"
Expand Down Expand Up @@ -204,6 +206,6 @@ EOF

# --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"
_clean_merged "$repo_root" "$base_dir" "$prefix" "$yes_mode" "$dry_run" "$force" "$active_worktree_path" "$target_ref"
fi
}
3 changes: 3 additions & 0 deletions lib/commands/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,15 @@ the remote URL.

Options:
--merged Also remove worktrees with merged PRs/MRs
--to <ref> Only remove worktrees for PRs/MRs merged into <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 --merged --yes # Auto-confirm everything
git gtr clean --merged --force # Force-clean merged, ignoring local changes
Expand Down Expand Up @@ -566,6 +568,7 @@ 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>
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
15 changes: 12 additions & 3 deletions lib/provider.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,30 @@ ensure_provider_cli() {
}

# Check if a branch has a merged PR/MR on the detected provider
# Usage: check_branch_merged <provider> <branch>
# Usage: check_branch_merged <provider> <branch> [target_ref]
# Returns 0 if merged, 1 if not
check_branch_merged() {
local provider="$1"
local branch="$2"
local target_ref="${3:-}"

case "$provider" in
github)
local pr_state
pr_state=$(gh pr list --head "$branch" --state merged --json state --jq '.[0].state' 2>/dev/null || true)
if [ -n "$target_ref" ]; then
pr_state=$(gh pr list --head "$branch" --base "$target_ref" --state merged --json state --jq '.[0].state' 2>/dev/null || true)
else
pr_state=$(gh pr list --head "$branch" --state merged --json state --jq '.[0].state' 2>/dev/null || true)
fi
[ "$pr_state" = "MERGED" ]
;;
gitlab)
local mr_result
mr_result=$(glab mr list --source-branch "$branch" --merged --per-page 1 --output json 2>/dev/null || true)
if [ -n "$target_ref" ]; then
mr_result=$(glab mr list --source-branch "$branch" --target-branch "$target_ref" --merged --per-page 1 --output json 2>/dev/null || true)
else
mr_result=$(glab mr list --source-branch "$branch" --merged --per-page 1 --output json 2>/dev/null || true)
fi
[ -n "$mr_result" ] && [ "$mr_result" != "[]" ] && [ "$mr_result" != "null" ]
;;
*)
Expand Down
23 changes: 21 additions & 2 deletions tests/cmd_clean.bats
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,19 @@ teardown() {
[ "$status" -eq 0 ]
}

@test "cmd_clean accepts --to with a value" {
run cmd_clean --to main
[ "$status" -eq 0 ]
}

@test "cmd_clean --merged --force removes dirty merged worktrees" {
create_test_worktree "merged-force"
echo "dirty" > "$TEST_WORKTREES_DIR/merged-force/dirty.txt"
git -C "$TEST_WORKTREES_DIR/merged-force" add dirty.txt

_clean_detect_provider() { printf "github"; }
ensure_provider_cli() { return 0; }
check_branch_merged() { [ "$2" = "merged-force" ]; }
check_branch_merged() { [ "$2" = "merged-force" ] && [ -z "$3" ]; }
run_hooks_in() { return 0; }
run_hooks() { return 0; }

Expand All @@ -139,6 +144,20 @@ teardown() {
[ ! -d "$TEST_WORKTREES_DIR/merged-force" ]
}

@test "cmd_clean --merged --to filters by target ref" {
create_test_worktree "merged-to-main"

_clean_detect_provider() { printf "github"; }
ensure_provider_cli() { return 0; }
check_branch_merged() { [ "$2" = "merged-to-main" ] && [ "$3" = "main" ]; }
run_hooks_in() { return 0; }
run_hooks() { return 0; }

run cmd_clean --merged --to main --yes
[ "$status" -eq 0 ]
[ ! -d "$TEST_WORKTREES_DIR/merged-to-main" ]
}

@test "cmd_clean --merged --force skips the current active worktree" {
create_test_worktree "active-merged"
cd "$TEST_WORKTREES_DIR/active-merged" || false
Expand All @@ -147,7 +166,7 @@ teardown() {

_clean_detect_provider() { printf "github"; }
ensure_provider_cli() { return 0; }
check_branch_merged() { [ "$2" = "active-merged" ]; }
check_branch_merged() { [ "$2" = "active-merged" ] && [ -z "$3" ]; }
run_hooks_in() { return 0; }
run_hooks() { return 0; }

Expand Down
1 change: 1 addition & 0 deletions tests/cmd_help.bats
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ teardown() {
[ "$status" -eq 0 ]
[[ "$output" == *"git gtr clean"* ]]
[[ "$output" == *"--merged"* ]]
[[ "$output" == *"--to <ref>"* ]]
}

@test "cmd_help copy shows copy help" {
Expand Down
43 changes: 43 additions & 0 deletions tests/provider.bats
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,46 @@ setup() {
run extract_hostname ""
[ "$status" -ne 0 ]
}

# ── check_branch_merged ───────────────────────────────────────────────────────

@test "check_branch_merged passes base ref to gh" {
gh() {
[ "$1" = "pr" ] || return 1
[ "$2" = "list" ] || return 1
[ "$3" = "--head" ] || return 1
[ "$4" = "feature/test" ] || return 1
[ "$5" = "--base" ] || return 1
[ "$6" = "main" ] || return 1
[ "$7" = "--state" ] || return 1
[ "$8" = "merged" ] || return 1
[ "$9" = "--json" ] || return 1
[ "${10}" = "state" ] || return 1
[ "${11}" = "--jq" ] || return 1
[ "${12}" = ".[0].state" ] || return 1
printf "MERGED"
}

run check_branch_merged github feature/test main
[ "$status" -eq 0 ]
}

@test "check_branch_merged passes target branch to glab" {
glab() {
[ "$1" = "mr" ] || return 1
[ "$2" = "list" ] || return 1
[ "$3" = "--source-branch" ] || return 1
[ "$4" = "feature/test" ] || return 1
[ "$5" = "--target-branch" ] || return 1
[ "$6" = "main" ] || return 1
[ "$7" = "--merged" ] || return 1
[ "$8" = "--per-page" ] || return 1
[ "$9" = "1" ] || return 1
[ "${10}" = "--output" ] || return 1
[ "${11}" = "json" ] || return 1
printf '[{"iid":1}]'
}

run check_branch_merged gitlab feature/test main
[ "$status" -eq 0 ]
}
Loading