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
8 changes: 8 additions & 0 deletions bin/_common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
45 changes: 30 additions & 15 deletions bin/env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down Expand Up @@ -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'"
;;
Expand Down
5 changes: 0 additions & 5 deletions bin/worktree.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ source "$(dirname "${BASH_SOURCE[0]}")/repos.sh"
# contains the entire monorepo tree. The env system mounts specific
# subdirectories (plugins/<name>, themes/<name>) 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 <branch>
Expand Down
Loading