From cacbca932c8107c2a0b64c461fa25e9224218678 Mon Sep 17 00:00:00 2001 From: maro114510 Date: Mon, 16 Mar 2026 10:44:29 +0900 Subject: [PATCH 01/11] feat(clean): add --force/-f flag to skip dirty-worktree safety checks Extends _clean_should_skip() with an optional third argument `force`. When force=1, uncommitted-changes and untracked-files checks are bypassed, allowing removal of worktrees that contain build artifacts or other disposable local state. Detached HEAD and empty-branch protections are retained regardless of --force, as those states are indeterminate. Propagates force through _clean_merged() (new 6th arg) and cmd_clean() (new --force|-f flag spec entry). All new arguments default to 0, preserving full backward compatibility. --- lib/commands/clean.sh | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/commands/clean.sh b/lib/commands/clean.sh index 929f078..d5f0dec 100644 --- a/lib/commands/clean.sh +++ b/lib/commands/clean.sh @@ -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 +# Usage: _clean_should_skip [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..." @@ -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 @@ -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..." @@ -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 } \ No newline at end of file From 7d944f6d1dc5513d986480886f1fb7d0d1b1e993 Mon Sep 17 00:00:00 2001 From: maro114510 Date: Mon, 16 Mar 2026 10:44:34 +0900 Subject: [PATCH 02/11] test(clean): add BATS tests for --force flag behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers four cases for _clean_should_skip with force=1: - dirty worktree (staged changes) → not skipped - worktree with untracked files → not skipped - detached HEAD → still skipped (protection retained) - empty branch name → still skipped (protection retained) Also adds a smoke test confirming cmd_clean accepts --force without error. --- tests/cmd_clean.bats | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/cmd_clean.bats b/tests/cmd_clean.bats index fa8031d..dcfa149 100644 --- a/tests/cmd_clean.bats +++ b/tests/cmd_clean.bats @@ -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 ] +} From 0047c783c8a3bbc7325d3f90ea20e5156f83dae4 Mon Sep 17 00:00:00 2001 From: maro114510 Date: Mon, 16 Mar 2026 10:44:39 +0900 Subject: [PATCH 03/11] feat(completions): add --force/-f to clean command completions Updates all three shell completion files (Bash, Zsh, Fish) so that --force and -f appear as tab-completion candidates for `git gtr clean`. --- completions/_git-gtr | 4 +++- completions/git-gtr.fish | 2 ++ completions/gtr.bash | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/completions/_git-gtr b/completions/_git-gtr index 17c5c5b..834d76e 100644 --- a/completions/_git-gtr +++ b/completions/_git-gtr @@ -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 diff --git a/completions/git-gtr.fish b/completions/git-gtr.fish index 2a3b351..58bd9f3 100644 --- a/completions/git-gtr.fish +++ b/completions/git-gtr.fish @@ -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' diff --git a/completions/gtr.bash b/completions/gtr.bash index 0983376..13314cf 100644 --- a/completions/gtr.bash +++ b/completions/gtr.bash @@ -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) From 041c448d5a0e46d9971d745f960e13efe221538e Mon Sep 17 00:00:00 2001 From: maro114510 Date: Mon, 16 Mar 2026 10:44:43 +0900 Subject: [PATCH 04/11] docs(clean): document --force/-f flag in built-in help and README Adds --force, -f to the Options section of `git gtr help clean` and the full help page. Adds two usage examples demonstrating --force alone and combined with --yes. Updates README.md with the same option entry and examples. --- README.md | 3 +++ lib/commands/help.sh | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index ee0e5e0..d1a7ff4 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,8 @@ 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:** @@ -334,6 +336,7 @@ git gtr clean --merged --yes # Remove without confirmation pro - `--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`. diff --git a/lib/commands/help.sh b/lib/commands/help.sh index bbb3e60..d17aca6 100644 --- a/lib/commands/help.sh +++ b/lib/commands/help.sh @@ -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 } @@ -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 Generate shell completions (bash, zsh, fish) From 25436c68060c9304ed0e77089c819fcfa13a1987 Mon Sep 17 00:00:00 2001 From: maro114510 Date: Mon, 16 Mar 2026 15:22:37 +0900 Subject: [PATCH 05/11] docs(clean): remove -f alias and fix full-help description --- lib/commands/help.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commands/help.sh b/lib/commands/help.sh index d17aca6..ca81dca 100644 --- a/lib/commands/help.sh +++ b/lib/commands/help.sh @@ -305,7 +305,7 @@ 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 + --force Force removal even if worktree has uncommitted changes or untracked files Examples: git gtr clean # Clean empty directories @@ -570,7 +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 + --force: force removal even if worktree has uncommitted changes or untracked files completion Generate shell completions (bash, zsh, fish) From 08b1701b70c1aa50917175e2c52083b49dff93e7 Mon Sep 17 00:00:00 2001 From: maro114510 Date: Mon, 16 Mar 2026 15:25:54 +0900 Subject: [PATCH 06/11] refactor(clean): remove -f alias from --force flag --- README.md | 2 +- completions/_git-gtr | 3 +-- completions/git-gtr.fish | 1 - completions/gtr.bash | 2 +- lib/commands/clean.sh | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d1a7ff4..500cd66 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ git gtr clean --merged --force --yes # Force-clean and auto-confirm - `--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 +- `--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`. diff --git a/completions/_git-gtr b/completions/_git-gtr index 834d76e..28d5b51 100644 --- a/completions/_git-gtr +++ b/completions/_git-gtr @@ -84,8 +84,7 @@ _git-gtr() { '-y[Skip confirmation prompts]' \ '--dry-run[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]' + '--force[Force removal even if worktree has uncommitted changes]' return fi diff --git a/completions/git-gtr.fish b/completions/git-gtr.fish index 58bd9f3..d3b56f3 100644 --- a/completions/git-gtr.fish +++ b/completions/git-gtr.fish @@ -104,7 +104,6 @@ complete -c git -n '__fish_git_gtr_using_command clean' -s y -d 'Skip confirmati 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' diff --git a/completions/gtr.bash b/completions/gtr.bash index 13314cf..bbfaaca 100644 --- a/completions/gtr.bash +++ b/completions/gtr.bash @@ -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 --yes -y --dry-run -n --force" -- "$cur")) fi ;; copy) diff --git a/lib/commands/clean.sh b/lib/commands/clean.sh index d5f0dec..c5c0c77 100644 --- a/lib/commands/clean.sh +++ b/lib/commands/clean.sh @@ -136,7 +136,7 @@ cmd_clean() { _spec="--merged --yes|-y --dry-run|-n ---force|-f" +--force" parse_args "$_spec" "$@" local merged_mode="${_arg_merged:-0}" From ab65ca0eef300ae88a8d330348c94facee87351c Mon Sep 17 00:00:00 2001 From: maro114510 Date: Mon, 16 Mar 2026 15:56:06 +0900 Subject: [PATCH 07/11] docs(completion): clarify --force covers untracked files --- completions/_git-gtr | 2 +- completions/git-gtr.fish | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/completions/_git-gtr b/completions/_git-gtr index 28d5b51..992b6ba 100644 --- a/completions/_git-gtr +++ b/completions/_git-gtr @@ -134,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) diff --git a/completions/git-gtr.fish b/completions/git-gtr.fish index d3b56f3..bc361eb 100644 --- a/completions/git-gtr.fish +++ b/completions/git-gtr.fish @@ -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 From a9f52354ad8225d307a45c8f51f40d3844378188 Mon Sep 17 00:00:00 2001 From: Tom Elizaga Date: Wed, 18 Mar 2026 13:59:35 -0700 Subject: [PATCH 08/11] fix(clean): make --force actually remove dirty merged worktrees --- README.md | 2 +- completions/_git-gtr | 3 ++- completions/git-gtr.fish | 2 +- completions/gtr.bash | 2 +- lib/commands/clean.sh | 29 ++++++++++++++++++-------- lib/commands/help.sh | 4 ++-- tests/cmd_clean.bats | 44 +++++++++++++++++++++++++++++++++++++++- 7 files changed, 70 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 500cd66..d1a7ff4 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ git gtr clean --merged --force --yes # Force-clean and auto-confirm - `--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 +- `--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`. diff --git a/completions/_git-gtr b/completions/_git-gtr index 992b6ba..e0a5ba3 100644 --- a/completions/_git-gtr +++ b/completions/_git-gtr @@ -84,7 +84,8 @@ _git-gtr() { '-y[Skip confirmation prompts]' \ '--dry-run[Show what would be removed]' \ '-n[Show what would be removed]' \ - '--force[Force removal even if worktree has uncommitted changes]' + '--force[Force removal even if worktree has uncommitted changes or untracked files]' \ + '-f[Force removal even if worktree has uncommitted changes or untracked files]' return fi diff --git a/completions/git-gtr.fish b/completions/git-gtr.fish index bc361eb..7fdc786 100644 --- a/completions/git-gtr.fish +++ b/completions/git-gtr.fish @@ -103,7 +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' +complete -c git -n '__fish_git_gtr_using_command clean' -s f -l force -d 'Force removal even if worktree has uncommitted changes or untracked files' # Config command complete -f -c git -n '__fish_git_gtr_using_command config' -a 'list get set add unset' diff --git a/completions/gtr.bash b/completions/gtr.bash index bbfaaca..13314cf 100644 --- a/completions/gtr.bash +++ b/completions/gtr.bash @@ -82,7 +82,7 @@ _git_gtr() { ;; clean) if [[ "$cur" == -* ]]; then - COMPREPLY=($(compgen -W "--merged --yes -y --dry-run -n --force" -- "$cur")) + COMPREPLY=($(compgen -W "--merged --yes -y --dry-run -n --force -f" -- "$cur")) fi ;; copy) diff --git a/lib/commands/clean.sh b/lib/commands/clean.sh index c5c0c77..b5288d9 100644 --- a/lib/commands/clean.sh +++ b/lib/commands/clean.sh @@ -30,9 +30,14 @@ _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 [force] +# Usage: _clean_should_skip [force] [active_worktree_path] _clean_should_skip() { - local dir="$1" branch="$2" force="${3:-0}" + local dir="$1" branch="$2" force="${3:-0}" active_worktree_path="${4:-}" + + if [ -n "$active_worktree_path" ] && [ "$dir" = "$active_worktree_path" ]; then + log_warn "Skipping $branch (current active worktree)" + return 0 + fi if [ -z "$branch" ] || [ "$branch" = "(detached)" ]; then log_warn "Skipping $dir (detached HEAD)" @@ -56,9 +61,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] +# Usage: _clean_merged repo_root base_dir prefix yes_mode dry_run [force] [active_worktree_path] _clean_merged() { - local repo_root="$1" base_dir="$2" prefix="$3" yes_mode="$4" dry_run="$5" force="${6:-0}" + local repo_root="$1" base_dir="$2" prefix="$3" yes_mode="$4" dry_run="$5" force="${6:-0}" active_worktree_path="${7:-}" log_step "Checking for worktrees with merged PRs/MRs..." @@ -82,7 +87,7 @@ _clean_merged() { # Skip main repo branch silently (not counted) [ "$branch" = "$main_branch" ] && continue - if _clean_should_skip "$dir" "$branch" "$force"; then + if _clean_should_skip "$dir" "$branch" "$force" "$active_worktree_path"; then skipped=$((skipped + 1)) continue fi @@ -104,7 +109,7 @@ _clean_merged() { continue fi - if remove_worktree "$dir" 0; then + if remove_worktree "$dir" "$force"; then git branch -d "$branch" 2>/dev/null || git branch -D "$branch" 2>/dev/null || true removed=$((removed + 1)) @@ -136,13 +141,14 @@ cmd_clean() { _spec="--merged --yes|-y --dry-run|-n ---force" +--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}" + local active_worktree_path="" log_step "Cleaning up stale worktrees..." @@ -155,6 +161,11 @@ cmd_clean() { local repo_root="$_ctx_repo_root" base_dir="$_ctx_base_dir" prefix="$_ctx_prefix" + active_worktree_path=$(git rev-parse --show-toplevel 2>/dev/null || true) + if [ -n "$active_worktree_path" ]; then + active_worktree_path=$(canonicalize_path "$active_worktree_path" || true) + fi + if [ ! -d "$base_dir" ]; then log_info "No worktrees directory to clean" return 0 @@ -186,6 +197,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" + _clean_merged "$repo_root" "$base_dir" "$prefix" "$yes_mode" "$dry_run" "$force" "$active_worktree_path" fi -} \ No newline at end of file +} diff --git a/lib/commands/help.sh b/lib/commands/help.sh index ca81dca..c680987 100644 --- a/lib/commands/help.sh +++ b/lib/commands/help.sh @@ -305,7 +305,7 @@ 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 + --force, -f Force removal even if worktree has uncommitted changes or untracked files Examples: git gtr clean # Clean empty directories @@ -570,7 +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 + --force, -f: force removal even if worktree has uncommitted changes or untracked files completion Generate shell completions (bash, zsh, fish) diff --git a/tests/cmd_clean.bats b/tests/cmd_clean.bats index dcfa149..a076937 100644 --- a/tests/cmd_clean.bats +++ b/tests/cmd_clean.bats @@ -102,7 +102,49 @@ teardown() { [ "$status" -eq 0 ] # 0 = skip (protection maintained) } -@test "cmd_clean accepts --force flag without error" { +@test "_clean_should_skip with force=1 still skips current active worktree" { + create_test_worktree "active-force" + run _clean_should_skip "$TEST_WORKTREES_DIR/active-force" "active-force" 1 "$TEST_WORKTREES_DIR/active-force" + [ "$status" -eq 0 ] # 0 = skip (protection maintained) +} + +@test "cmd_clean accepts --force and -f flags without error" { run cmd_clean --force [ "$status" -eq 0 ] + + run cmd_clean -f + [ "$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" ]; } + run_hooks_in() { return 0; } + run_hooks() { return 0; } + + run cmd_clean --merged --force --yes + [ "$status" -eq 0 ] + [ ! -d "$TEST_WORKTREES_DIR/merged-force" ] +} + +@test "cmd_clean --merged --force skips the current active worktree" { + create_test_worktree "active-merged" + cd "$TEST_WORKTREES_DIR/active-merged" || false + echo "dirty" > dirty.txt + git add dirty.txt + + _clean_detect_provider() { printf "github"; } + ensure_provider_cli() { return 0; } + check_branch_merged() { [ "$2" = "active-merged" ]; } + run_hooks_in() { return 0; } + run_hooks() { return 0; } + + run cmd_clean --merged --force --yes + [ "$status" -eq 0 ] + [ -d "$TEST_WORKTREES_DIR/active-merged" ] } From 13a9707fa349bec8c7ded9876ad4f7bff2bb9f6a Mon Sep 17 00:00:00 2001 From: Tom Elizaga Date: Wed, 18 Mar 2026 14:05:56 -0700 Subject: [PATCH 09/11] Address CodeRabbit review: canonicalize active worktree comparison --- lib/commands/clean.sh | 9 ++++++++- tests/cmd_clean.bats | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/commands/clean.sh b/lib/commands/clean.sh index b5288d9..8e08bb5 100644 --- a/lib/commands/clean.sh +++ b/lib/commands/clean.sh @@ -33,8 +33,15 @@ _clean_detect_provider() { # Usage: _clean_should_skip [force] [active_worktree_path] _clean_should_skip() { local dir="$1" branch="$2" force="${3:-0}" active_worktree_path="${4:-}" + local dir_canonical="$dir" + local active_worktree_canonical="$active_worktree_path" - if [ -n "$active_worktree_path" ] && [ "$dir" = "$active_worktree_path" ]; then + if [ -n "$active_worktree_path" ]; then + dir_canonical=$(canonicalize_path "$dir" || printf "%s" "$dir") + active_worktree_canonical=$(canonicalize_path "$active_worktree_path" || printf "%s" "$active_worktree_path") + fi + + if [ -n "$active_worktree_path" ] && [ "$dir_canonical" = "$active_worktree_canonical" ]; then log_warn "Skipping $branch (current active worktree)" return 0 fi diff --git a/tests/cmd_clean.bats b/tests/cmd_clean.bats index a076937..59160c2 100644 --- a/tests/cmd_clean.bats +++ b/tests/cmd_clean.bats @@ -108,6 +108,13 @@ teardown() { [ "$status" -eq 0 ] # 0 = skip (protection maintained) } +@test "_clean_should_skip with force=1 skips current active worktree via symlink path" { + create_test_worktree "active-force-symlink" + ln -s "$TEST_WORKTREES_DIR/active-force-symlink" "$TEST_REPO/active-force-link" + run _clean_should_skip "$TEST_REPO/active-force-link" "active-force-symlink" 1 "$TEST_WORKTREES_DIR/active-force-symlink" + [ "$status" -eq 0 ] # 0 = skip (protection maintained) +} + @test "cmd_clean accepts --force and -f flags without error" { run cmd_clean --force [ "$status" -eq 0 ] From 28b0861b59066f7c3b48df3fc8326c964bb3f35c Mon Sep 17 00:00:00 2001 From: Tom Elizaga Date: Wed, 18 Mar 2026 14:39:14 -0700 Subject: [PATCH 10/11] Address CodeRabbit review: preserve active worktree fallback path --- lib/commands/clean.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/clean.sh b/lib/commands/clean.sh index 8e08bb5..477d07d 100644 --- a/lib/commands/clean.sh +++ b/lib/commands/clean.sh @@ -170,7 +170,7 @@ cmd_clean() { active_worktree_path=$(git rev-parse --show-toplevel 2>/dev/null || true) if [ -n "$active_worktree_path" ]; then - active_worktree_path=$(canonicalize_path "$active_worktree_path" || true) + active_worktree_path=$(canonicalize_path "$active_worktree_path" || printf "%s" "$active_worktree_path") fi if [ ! -d "$base_dir" ]; then From c9b8a0ce17c9e7064e0c4bdd13d3437e2eb1427e Mon Sep 17 00:00:00 2001 From: Tom Elizaga Date: Wed, 18 Mar 2026 16:41:22 -0700 Subject: [PATCH 11/11] fix(completions): generate clean force flags --- scripts/generate-completions.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/generate-completions.sh b/scripts/generate-completions.sh index 9f483c6..1072645 100755 --- a/scripts/generate-completions.sh +++ b/scripts/generate-completions.sh @@ -176,7 +176,7 @@ MIDDLE1 ;; 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) @@ -342,7 +342,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 or untracked files]' \ + '-f[Force removal even if worktree has uncommitted changes or untracked files]' return fi @@ -396,7 +398,7 @@ MIDDLE1 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) @@ -546,7 +548,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 @@ -580,6 +582,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' -s f -l force -d 'Force removal even if worktree has uncommitted changes or untracked files' # Config command complete -f -c git -n '__fish_git_gtr_using_command config' -a 'list get set add unset'