diff --git a/bin/_common.sh b/bin/_common.sh index dac914a14b..72ad3cca0c 100755 --- a/bin/_common.sh +++ b/bin/_common.sh @@ -31,6 +31,14 @@ validate_port() { fi } +# Sanitize a branch name for use as a directory name: feat/foo -> feat-foo. +# Canonical transform shared by worktree.sh (worktree dir naming) and env.sh +# (mount parsing + env-destroy branch resolution) so the rule can't drift +# between call sites. +sanitize_branch() { + echo "$1" | tr '/' '-' +} + # Logging helpers — mirror the colored output used by bin/site-setup.sh. NP_RED='\033[0;31m' NP_GREEN='\033[0;32m' diff --git a/bin/env.sh b/bin/env.sh index 6b71f1e6fb..435003e9aa 100755 --- a/bin/env.sh +++ b/bin/env.sh @@ -135,7 +135,7 @@ case $1 in validate_name "$wt_repo" "repo" validate_name "$wt_branch" "branch" # Sanitize branch for directory name (feat/foo -> feat-foo). - safe_branch=$(echo "$wt_branch" | tr '/' '-') + safe_branch=$(sanitize_branch "$wt_branch") # Create a monorepo worktree at this branch if it doesn't exist. if [[ ! -d "$NABSPATH/worktrees/$safe_branch" ]]; then echo "Creating worktree at branch $wt_branch..." @@ -549,23 +549,38 @@ MIGRATE fi # Remove compose file before worktrees so worktree.sh doesn't see them as env-bound. rm -f "$compose_file" - # Remove worktrees that were mounted by this environment. The branch - # here is the mount-derived (safe) form from parse_worktree_mount — - # the stable filesystem identifier, not the live git branch. This is - # deliberate: if the worktree was retargeted to a different branch - # via `git checkout` after env creation, we still want destroy to - # remove the worktree directory the env was bound to, not whatever - # branch is currently checked out there. + # Remove worktrees that were mounted by this environment. For monorepo + # mounts wt_branch is the mount-derived (safe) form from + # parse_worktree_mount — the stable filesystem identifier, not the live + # git branch. (Legacy repos-shape mounts carry the already-unsanitized + # branch instead; they have no slash-vs-dash mismatch, so they fall + # straight through the safe-form branch below and delete correctly.) # - # Known follow-up: for monorepo worktrees the safe form (e.g. feat-foo) - # won't match the real branch (feat/foo) in worktree.sh's final - # `git branch -D`, so the local branch ref is left dangling after the - # worktree dir is removed. Harmless (re-create reuses it) but accrues - # across create/destroy cycles. A proper fix removes the dir by safe - # name and deletes the branch by its resolved real name separately. + # worktree.sh remove re-sanitizes whatever branch it's given to locate + # the directory, but deletes the git ref by the raw argument. So to also + # clear the local branch (which the safe form can't match — feat-foo vs + # feat/foo), we pass the resolved real branch — but only when the dir → + # branch mapping is unambiguous: the live branch must sanitize back to + # the bound dir AND be the *only* local branch that does. This avoids + # force-deleting the wrong ref if the worktree was retargeted via + # `git checkout` to a different branch — including a colliding one that + # sanitizes to the same dir name (e.g. feat/foo-bar vs feat-foo/bar). + # In any ambiguous or retargeted case we fall back to the safe form, + # which still removes the bound directory (the real ref may then orphan + # — strictly better than deleting the wrong branch). The fully robust + # fix persists the original branch at env-creation time (see #154). for entry in "${worktree_entries[@]}"; do IFS='|' read -r wt_repo wt_branch <<< "$entry" - "$NABSPATH/bin/worktree.sh" remove --yes "$wt_repo" "$wt_branch" + real_branch=$(resolve_unsanitized_branch "$wt_branch") + sanitized_matches=0 + while IFS= read -r candidate; do + [[ "$(sanitize_branch "$candidate")" == "$wt_branch" ]] && sanitized_matches=$((sanitized_matches + 1)) + done < <(git -C "$NABSPATH" for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null) + if [[ "$(sanitize_branch "$real_branch")" == "$wt_branch" && "$sanitized_matches" -eq 1 ]]; then + "$NABSPATH/bin/worktree.sh" remove --yes "$wt_repo" "$real_branch" + else + "$NABSPATH/bin/worktree.sh" remove --yes "$wt_repo" "$wt_branch" + fi done echo "Destroyed environment '$env_name'" ;; diff --git a/bin/worktree.sh b/bin/worktree.sh index afe58afae6..713718dcd4 100755 --- a/bin/worktree.sh +++ b/bin/worktree.sh @@ -8,11 +8,6 @@ source "$(dirname "${BASH_SOURCE[0]}")/repos.sh" # contains the entire monorepo tree. The env system mounts specific # subdirectories (plugins/, themes/) into the container. -# Sanitize a branch name for use as a directory: feat/foo -> feat-foo. -sanitize_branch() { - echo "$1" | tr '/' '-' -} - case $1 in add) # Usage: worktree.sh add