Skip to content
Open
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
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`: 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
5 changes: 3 additions & 2 deletions completions/_git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ _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]'
return
fi

Expand Down Expand Up @@ -133,7 +134,7 @@ _git-gtr() {
rm)
_arguments \
'--delete-branch[Delete branch]' \
'--force[Force removal even if dirty]' \
'--force[Force removal even if worktree has uncommitted changes or untracked files]' \
'--yes[Non-interactive mode]'
;;
mv|rename)
Expand Down
3 changes: 2 additions & 1 deletion completions/git-gtr.fish
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ complete -c git -n '__fish_git_gtr_using_command new' -s a -l ai -d 'Start AI to

# Remove command options
complete -c git -n '__fish_git_gtr_using_command rm' -l delete-branch -d 'Delete branch'
complete -c git -n '__fish_git_gtr_using_command rm' -l force -d 'Force removal even if dirty'
complete -c git -n '__fish_git_gtr_using_command rm' -l force -d 'Force removal even if worktree has uncommitted changes or untracked files'
complete -c git -n '__fish_git_gtr_using_command rm' -l yes -d 'Non-interactive mode'

# Rename command options
Expand Down Expand Up @@ -103,6 +103,7 @@ 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'

# 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" -- "$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"
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 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: force removal even if worktree has uncommitted changes or untracked files

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 ]
}