From a8cea91f352a1bfe99b1466ae838ccdb726ca7db Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:41:51 +0530 Subject: [PATCH] fix(gh): handle stale worktree metadata during prune --- .../basectl/subcommands/gh_branch_worktree.sh | 13 ++++++ .../basectl/tests/gh-branch-worktree.bats | 42 +++++++++++++++++++ docs/command-reference.md | 2 +- docs/github-workflow.md | 10 +++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/cli/bash/commands/basectl/subcommands/gh_branch_worktree.sh b/cli/bash/commands/basectl/subcommands/gh_branch_worktree.sh index 2b56b64f..efb41dfb 100644 --- a/cli/bash/commands/basectl/subcommands/gh_branch_worktree.sh +++ b/cli/bash/commands/basectl/subcommands/gh_branch_worktree.sh @@ -829,6 +829,13 @@ base_gh_worktree_dirty() { [[ -n "$(git -C "$path" status --porcelain --ignore-submodules=none)" ]] } +base_gh_worktree_is_valid() { + local path="$1" + + [[ -f "$path/.git" ]] || return 1 + git -C "$path" rev-parse --is-inside-work-tree >/dev/null 2>&1 +} + base_gh_worktree_prune_delete_branch() { local branch="$1" local merge_source="$2" @@ -937,6 +944,12 @@ base_gh_worktree_prune() { skipped_current=$((skipped_current + 1)) continue fi + if ! base_gh_worktree_is_valid "$path"; then + printf 'FAIL %s (%s) invalid linked-worktree metadata; path and branch retained; inspect the path, then run git worktree prune for stale metadata\n' \ + "$path" "$branch" + failed=$((failed + 1)) + continue + fi if base_gh_worktree_dirty "$path"; then printf 'SKIP %s (%s) dirty worktree\n' "$path" "$branch" skipped_dirty=$((skipped_dirty + 1)) diff --git a/cli/bash/commands/basectl/tests/gh-branch-worktree.bats b/cli/bash/commands/basectl/tests/gh-branch-worktree.bats index 8fa828fe..18018950 100644 --- a/cli/bash/commands/basectl/tests/gh-branch-worktree.bats +++ b/cli/bash/commands/basectl/tests/gh-branch-worktree.bats @@ -863,6 +863,48 @@ EOF ! git -C "$repo" show-ref --verify --quiet refs/heads/merged-work } +@test "basectl gh worktree prune retains invalid linked worktree metadata without raw git fatal" { + local repo invalid_worktree + + repo="$TEST_TMPDIR/repo" + invalid_worktree="$TEST_TMPDIR/invalid-worktree" + init_git_repo "$repo" + printf 'hello\n' > "$repo/README.md" + commit_all "$repo" "Initial commit" + git -C "$repo" branch merged-work + mkdir -p "$invalid_worktree" + printf 'orphaned checkout\n' > "$invalid_worktree/README.md" + + run env \ + HOME="$TEST_HOME" \ + BASE_HOME="$BASE_REPO_ROOT" \ + INVALID_WORKTREE="$invalid_worktree" \ + bash -c ' + cd "$1" + source "$BASE_HOME/base_init.sh" + source "$BASE_HOME/cli/bash/commands/basectl/subcommands/gh.sh" + base_gh_list_worktree_branches() { + printf "%s\\t%s\\n" "$INVALID_WORKTREE" "merged-work" + } + base_gh_branch_cleanup_state() { + printf -v "$3" "%s" merged + printf -v "$4" "%s" github + printf -v "$5" "%s" "" + printf -v "$6" "%s" "" + printf -v "$7" "%s" 0 + return 0 + } + base_gh_subcommand_main worktree prune --yes + ' bash "$repo" + + [ "$status" -eq 1 ] + [[ "$output" == *"invalid linked-worktree metadata; path and branch retained"* ]] + [[ "$output" != *"fatal: not a git repository"* ]] + [[ "$output" == *"Summary: 0 removed, 0 skipped current/default, 0 skipped dirty, 0 skipped unmerged, 1 failed."* ]] + [ -d "$invalid_worktree" ] + git -C "$repo" show-ref --verify --quiet refs/heads/merged-work +} + @test "basectl gh worktree prune reports retained branches after deletion failure" { local repo worktree diff --git a/docs/command-reference.md b/docs/command-reference.md index e1b32783..79e3f34c 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -240,7 +240,7 @@ daily project loop commands from the local checkout. | `basectl gh pr create/status/checks/ready/merge` | Create and manage pull requests through Base's workflow wrapper. `pr create` rejects a noncanonical current branch before invoking GitHub, auto-injects `Fixes #` unless `--no-fixes` is passed, and uses `github.pr` from `base_manifest.yaml` when present. | passes through `gh` options; `pr create` also accepts `--no-fixes` | | `basectl gh branch stale` | Report stale local branches. Stable inspection JSON uses the shared v1 envelope. | `--days `, `--format ` | | `basectl gh branch prune` | Prune safe merged branches. Pass `--closed-unmerged` to include branches whose pull requests were closed without merging. | `--dry-run`, `--yes`, `--remote`, `--closed-unmerged` | -| `basectl gh worktree prune` | Prune stale merged worktrees. Pass `--closed-unmerged` to include clean worktrees tied to closed, unmerged pull requests. | `--dry-run`, `--yes`, `--closed-unmerged` | +| `basectl gh worktree prune` | Prune stale merged branch-attached worktrees. Pass `--closed-unmerged` to include clean worktrees tied to closed, unmerged pull requests; detached worktree metadata remains an explicit `git worktree prune` operation. | `--dry-run`, `--yes`, `--closed-unmerged` | | `basectl gh project doctor` | Inspect GitHub Project metadata against the Base Project schema. | `--project `, `--owner <login>`, `--schema base-project` | | `basectl gh project configure` | Create or repair Base-managed Project metadata. | `--project <title>`, `--owner <login>`, `--repo <owner/name>`, `--schema base-project`, `--config <path>`, `--copy-fields-from <title>`, `--replace-project`, `--initiative-option <name>`, `--dry-run` | | `basectl gh project issue set-fields <number>` | Add an issue to the Project if needed and update metadata fields. The target Project must be linked to the issue repository unless the intentional cross-repository exception is enabled. | `--project <title>`, `--repo <owner/name>`, `--config <path>`, `--allow-cross-repo`, field options | diff --git a/docs/github-workflow.md b/docs/github-workflow.md index 7eab1d79..faeb4844 100644 --- a/docs/github-workflow.md +++ b/docs/github-workflow.md @@ -489,6 +489,16 @@ worktree and local branch; the remote branch remains for GitHub verification failures and rerun the preview before applying it with `--yes`. +Worktree pruning only handles branch-attached worktrees with valid linked +worktree metadata. If a branch-attached path still exists but is no longer a +valid Git worktree, Base retains the path and local branch, reports a failure, +and does not delete the directory contents. Inspect or preserve anything needed +from that path, then use `git worktree prune --dry-run -v` followed by +`git worktree prune -v` to remove stale Git administrative records. Git's prune +only removes stale worktree metadata; it does not delete an orphaned directory. +Detached worktrees are intentionally outside Base's branch/PR cleanup boundary +and should be handled through the explicit Git command after review. + If a worktree is removed but its local branch cannot be deleted, the command retains the branch, reports the incomplete cleanup, and exits nonzero. Resolve the branch condition before treating the cleanup as complete.