Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,13 +327,16 @@ git gtr clean # Remove empty worktree directori
git gtr clean --merged # Remove worktrees for merged PRs/MRs
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 --yes # Force-clean and auto-confirm
```

**Options:**

- `--merged`: Remove worktrees whose branches have merged PRs/MRs (also deletes the branch)
- `--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`.

Expand Down
4 changes: 3 additions & 1 deletion completions/_git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ _git-gtr() {
'--yes[Skip confirmation prompts]' \
'-y[Skip confirmation prompts]' \
'--dry-run[Show what would be removed]' \
'-n[Show what would be removed]'
'-n[Show what would be removed]' \
'--force[Force removal even if worktree has uncommitted changes]' \
'-f[Force removal even if worktree has uncommitted changes]'
return
fi

Expand Down
2 changes: 2 additions & 0 deletions completions/git-gtr.fish
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ complete -c git -n '__fish_git_gtr_using_command clean' -l yes -d 'Skip confirma
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'
complete -c git -n '__fish_git_gtr_using_command clean' -s n -d 'Show what would be removed'
complete -c git -n '__fish_git_gtr_using_command clean' -l force -d 'Force removal even if worktree has uncommitted changes'
complete -c git -n '__fish_git_gtr_using_command clean' -s f -d 'Force removal even if worktree has uncommitted changes'

# Config command
complete -f -c git -n '__fish_git_gtr_using_command config' -a 'list get set add unset'
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" -- "$cur"))
COMPREPLY=($(compgen -W "--merged --yes -y --dry-run -n --force -f" -- "$cur"))
fi
;;
copy)
Expand Down
34 changes: 19 additions & 15 deletions lib/commands/clean.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,35 @@ _clean_detect_provider() {

# Check if a worktree should be skipped during merged cleanup.
# Returns 0 if should skip, 1 if should process.
# Usage: _clean_should_skip <dir> <branch>
# Usage: _clean_should_skip <dir> <branch> [force]
_clean_should_skip() {
local dir="$1" branch="$2"
local dir="$1" branch="$2" force="${3:-0}"

if [ -z "$branch" ] || [ "$branch" = "(detached)" ]; then
log_warn "Skipping $dir (detached HEAD)"
return 0
fi

if ! git -C "$dir" diff --quiet 2>/dev/null || \
! git -C "$dir" diff --cached --quiet 2>/dev/null; then
log_warn "Skipping $branch (has uncommitted changes)"
return 0
fi
if [ "$force" -eq 0 ]; then
if ! git -C "$dir" diff --quiet 2>/dev/null || \
! git -C "$dir" diff --cached --quiet 2>/dev/null; then
log_warn "Skipping $branch (has uncommitted changes)"
return 0
fi

if [ -n "$(git -C "$dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then
log_warn "Skipping $branch (has untracked files)"
return 0
if [ -n "$(git -C "$dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then
log_warn "Skipping $branch (has untracked files)"
return 0
fi
fi

return 1
}

# Remove worktrees whose PRs/MRs are merged (handles squash merges)
# Usage: _clean_merged repo_root base_dir prefix yes_mode dry_run
# Usage: _clean_merged repo_root base_dir prefix yes_mode dry_run [force]
_clean_merged() {
local repo_root="$1" base_dir="$2" prefix="$3" yes_mode="$4" dry_run="$5"
local repo_root="$1" base_dir="$2" prefix="$3" yes_mode="$4" dry_run="$5" force="${6:-0}"

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

Expand All @@ -80,7 +82,7 @@ _clean_merged() {
# Skip main repo branch silently (not counted)
[ "$branch" = "$main_branch" ] && continue

if _clean_should_skip "$dir" "$branch"; then
if _clean_should_skip "$dir" "$branch" "$force"; then
skipped=$((skipped + 1))
continue
fi
Expand Down Expand Up @@ -133,12 +135,14 @@ cmd_clean() {
local _spec
_spec="--merged
--yes|-y
--dry-run|-n"
--dry-run|-n
--force|-f"
parse_args "$_spec" "$@"

local merged_mode="${_arg_merged:-0}"
local yes_mode="${_arg_yes:-0}"
local dry_run="${_arg_dry_run:-0}"
local force="${_arg_force:-0}"

log_step "Cleaning up stale worktrees..."

Expand Down Expand Up @@ -182,6 +186,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"
_clean_merged "$repo_root" "$base_dir" "$prefix" "$yes_mode" "$dry_run" "$force"
fi
}
4 changes: 4 additions & 0 deletions lib/commands/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -305,12 +305,15 @@ Options:
--merged Also remove worktrees with merged PRs/MRs
--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 --dry-run # Preview merged 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 --yes # Force-clean and auto-confirm
EOF
}

Expand Down Expand Up @@ -567,6 +570,7 @@ SETUP & MAINTENANCE:
Override: git gtr config set gtr.provider gitlab
--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

completion <shell>
Generate shell completions (bash, zsh, fish)
Expand Down
30 changes: 30 additions & 0 deletions tests/cmd_clean.bats
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,33 @@ teardown() {
run _clean_should_skip "$TEST_WORKTREES_DIR/clean-wt" "clean-wt"
[ "$status" -eq 1 ] # 1 = don't skip
}

@test "_clean_should_skip with force=1 does not skip dirty worktree" {
create_test_worktree "dirty-force"
echo "dirty" > "$TEST_WORKTREES_DIR/dirty-force/untracked.txt"
git -C "$TEST_WORKTREES_DIR/dirty-force" add untracked.txt
run _clean_should_skip "$TEST_WORKTREES_DIR/dirty-force" "dirty-force" 1
[ "$status" -eq 1 ] # 1 = don't skip
}

@test "_clean_should_skip with force=1 does not skip worktree with untracked files" {
create_test_worktree "untracked-force"
echo "new" > "$TEST_WORKTREES_DIR/untracked-force/newfile.txt"
run _clean_should_skip "$TEST_WORKTREES_DIR/untracked-force" "untracked-force" 1
[ "$status" -eq 1 ] # 1 = don't skip
}

@test "_clean_should_skip with force=1 still skips detached HEAD" {
run _clean_should_skip "/some/dir" "(detached)" 1
[ "$status" -eq 0 ] # 0 = skip (protection maintained)
}

@test "_clean_should_skip with force=1 still skips empty branch" {
run _clean_should_skip "/some/dir" "" 1
[ "$status" -eq 0 ] # 0 = skip (protection maintained)
}

@test "cmd_clean accepts --force flag without error" {
run cmd_clean --force
[ "$status" -eq 0 ]
}