Skip to content
Merged
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
74 changes: 29 additions & 45 deletions bin/wt-context
Original file line number Diff line number Diff line change
Expand Up @@ -175,32 +175,13 @@ remove_context() {
git -C "$repo_root" config --local --remove-section wt 2>/dev/null || true
fi

# 2. Remove adoption markers from worktrees
info "Removing adoption markers from worktrees..."
wt_source wt-adopt
local wt_path=""
while IFS= read -r line; do
if [[ "$line" == "worktree "* ]]; then
wt_path="${line#worktree }"
elif [[ -z "$line" && -n "$wt_path" ]]; then
# End of worktree block — check and remove marker
if wt_is_adopted "$wt_path" 2>/dev/null; then
local marker_ctx
marker_ctx="$(wt_read_adopted_context "$wt_path" 2>/dev/null)" || true
# Remove if context matches, or if it's an old empty marker
if [[ -z "$marker_ctx" || "$marker_ctx" == "$context_name" ]]; then
wt_unmark_adopted "$wt_path" 2>/dev/null || true
fi
fi
wt_path=""
fi
done < <(git -C "$repo_root" worktree list --porcelain 2>/dev/null; echo)

# 3. Delete .conf file
# 2. Delete .conf file
info "Removing config file..."
rm -f "$conf_file"

# 4. Reverse repo migration: move main repo back to symlink location
# 3. Reverse repo migration: move main repo back to symlink location
# Done BEFORE removing adoption markers so pointer update can check adoption state
wt_source wt-adopt
if [[ -L "$active_worktree" ]]; then
local link_target
link_target="$(readlink "$active_worktree")"
Expand All @@ -215,28 +196,11 @@ remove_context() {
mv "$repo_root" "$active_worktree"

# Fix worktree .git pointers after repo move
# Each worktree has a .git FILE containing "gitdir: <old>/.git/worktrees/<name>"
# After moving the repo, these need to point to the new location
if [[ -d "$active_worktree/.git/worktrees" ]]; then
info "Updating worktree references..."
local new_git_dir="$active_worktree/.git"

for wt_meta_dir in "$new_git_dir"/worktrees/*/; do
[[ -d "$wt_meta_dir" ]] || continue
# Read the gitdir file to find the worktree's .git file location
local gitdir_file="$wt_meta_dir/gitdir"
[[ -f "$gitdir_file" ]] || continue
local wt_dot_git
wt_dot_git="$(cat "$gitdir_file" | tr -d '[:space:]')"

# Update the .git file in the worktree to point to the new location
if [[ -f "$wt_dot_git" ]]; then
local wt_name
wt_name="$(basename "$wt_meta_dir")"
echo "gitdir: ${new_git_dir}/worktrees/${wt_name}" > "$wt_dot_git"
fi
done
fi
# Only update worktrees that were adopted by this context
_wt_update_worktree_pointers "$active_worktree" "adopted" "$context_name"

# Update repo_root for subsequent marker removal
repo_root="$active_worktree"
else
warn "Symlink at $active_worktree points to $link_target (expected $repo_root)"
warn "Removing symlink but not moving repository"
Expand All @@ -246,6 +210,26 @@ remove_context() {
warn "$active_worktree exists but is not a symlink — leaving it unchanged"
fi

# 4. Remove adoption markers from worktrees (after repo migration so pointers are updated first)
info "Removing adoption markers from worktrees..."
local wt_path=""
while IFS= read -r line; do
if [[ "$line" == "worktree "* ]]; then
wt_path="${line#worktree }"
elif [[ -z "$line" && -n "$wt_path" ]]; then
# End of worktree block — check and remove marker
if wt_is_adopted "$wt_path" 2>/dev/null; then
local marker_ctx
marker_ctx="$(wt_read_adopted_context "$wt_path" 2>/dev/null)" || true
# Remove if context matches, or if it's an old empty marker
if [[ -z "$marker_ctx" || "$marker_ctx" == "$context_name" ]]; then
wt_unmark_adopted "$wt_path" 2>/dev/null || true
fi
fi
wt_path=""
fi
done < <(git -C "$repo_root" worktree list --porcelain 2>/dev/null; echo)

# 5. Update ~/.wt/current
local current_context
current_context="$(wt_get_current_context)"
Expand Down
75 changes: 75 additions & 0 deletions lib/wt-common
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,81 @@ wt_read_config || true
# Context name (for display purposes)
: "${WT_CONTEXT_NAME:=""}"

# ─────────────────────────────────────────────────────────────────────────────
# Worktree pointer maintenance
# ─────────────────────────────────────────────────────────────────────────────

# After moving a repo's .git directory to a new location, update
# worktrees' .git files to point to the new .git path.
#
# Each git worktree has a .git FILE (not directory) containing:
# gitdir: /old/path/.git/worktrees/<name>
#
# After the repo is moved, these pointers become stale. This function
# reads the main repo's .git/worktrees/*/gitdir to find each worktree's
# .git file, then rewrites it to point to the new location.
#
# Args: $1 = repo directory (the new location containing .git/)
# $2 = mode (optional): "unadopted" (default) - only unadopted worktrees
# "adopted" - only worktrees adopted by context
# "all" - all worktrees unconditionally
# $3 = context name (optional, used with "adopted" mode)
_wt_update_worktree_pointers() {
local repo_dir="$1"
local mode="${2:-unadopted}"
local context_name="${3:-}"
local git_dir="$repo_dir/.git"

[[ -d "$git_dir/worktrees" ]] || return 0

info "Updating worktree references..."
for wt_meta_dir in "$git_dir"/worktrees/*/; do
[[ -d "$wt_meta_dir" ]] || continue

# Read gitdir to find the worktree path
local gitdir_file="$wt_meta_dir/gitdir"
[[ -f "$gitdir_file" ]] || continue
local wt_dot_git
wt_dot_git="$(cat "$gitdir_file" | tr -d '[:space:]')"
[[ -f "$wt_dot_git" ]] || continue

# Check adoption state based on mode.
# We check the marker directly in the worktree metadata dir rather than
# using wt_is_adopted (which runs git rev-parse) because after a repo
# move the .git pointers are stale and git operations would fail.
# The marker lives at <git_dir>/worktrees/<name>/wt/adopted.
local adopted_marker="$wt_meta_dir/wt/adopted"
case "$mode" in
unadopted)
# Skip adopted worktrees — they already have correct physical paths
if [[ -f "$adopted_marker" ]]; then
continue
fi
;;
adopted)
# Skip unadopted worktrees and worktrees adopted by other contexts
if [[ ! -f "$adopted_marker" ]]; then
continue
fi
if [[ -n "$context_name" ]]; then
local adopted_ctx
adopted_ctx="$(head -1 "$adopted_marker" 2>/dev/null | tr -d '[:space:]')"
if [[ -n "$adopted_ctx" && "$adopted_ctx" != "$context_name" ]]; then
continue
fi
fi
;;
all)
# Update everything
;;
esac

local wt_name="${wt_meta_dir%/}"
wt_name="${wt_name##*/}"
echo "gitdir: ${git_dir}/worktrees/${wt_name}" > "$wt_dot_git"
done
}

# ─────────────────────────────────────────────────────────────────────────────
# Expand ~ to $HOME in a path (bash doesn't expand ~ in variable assignments
# or interactive read input)
Expand Down
6 changes: 6 additions & 0 deletions lib/wt-context-setup
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,12 @@ _wt_migrate_repo() {
echo "Creating symlink $WT_ACTIVE_WORKTREE -> $WT_MAIN_REPO_ROOT ..."
ln -s "$WT_MAIN_REPO_ROOT" "$WT_ACTIVE_WORKTREE"

# Update pre-existing worktree .git pointers to use the physical base path.
# Before migration, worktrees pointed to the original location (now the symlink).
# After wt switch, the symlink moves away from base, breaking those pointers.
# Only update unadopted worktrees — adopted ones already have correct physical paths.
_wt_update_worktree_pointers "$WT_MAIN_REPO_ROOT" "unadopted"

echo " ✓ Migration complete!"

elif [[ -L "$WT_ACTIVE_WORKTREE" ]]; then
Expand Down
5 changes: 5 additions & 0 deletions test/integration/wt-context-remove.bats
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ teardown() {
# Normalize worktree path
wt_path="$(cd "$wt_path" && pwd -P)"

# Mark the worktree as adopted by this context — remove only updates
# worktrees adopted by the context being removed
source "$TEST_HOME/.wt/lib/wt-adopt"
WT_CONTEXT_NAME="ptr-test" wt_mark_adopted "$wt_path"

# Verify the worktree's .git file points to the OLD location
local dot_git_content
dot_git_content="$(cat "$wt_path/.git")"
Expand Down
184 changes: 184 additions & 0 deletions test/unit/wt-context-setup.bats
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ setup() {

# Source the libraries under test
source "$TEST_HOME/.wt/lib/wt-common"
source "$TEST_HOME/.wt/lib/wt-adopt"
source "$TEST_HOME/.wt/lib/wt-context"
source "$TEST_HOME/.wt/lib/wt-context-setup"
}
Expand Down Expand Up @@ -229,3 +230,186 @@ teardown() {
assert_success
}

# =============================================================================
# Tests for _wt_update_worktree_pointers()
# =============================================================================

@test "_wt_update_worktree_pointers updates .git files after repo move" {
# Create a repo with a worktree
local repo
repo=$(create_mock_repo "$BATS_TEST_TMPDIR/original-repo")
create_branch "$repo" "feat-branch"
local wt_path="$BATS_TEST_TMPDIR/wt-feat"
create_worktree "$repo" "$wt_path" "feat-branch"
wt_path="$(cd "$wt_path" && pwd -P)"

# Verify initial .git pointer uses old path
local dot_git_content
dot_git_content="$(cat "$wt_path/.git")"
assert_equal "$dot_git_content" "gitdir: ${repo}/.git/worktrees/wt-feat"

# Simulate migration: move repo to new location
local new_repo="$BATS_TEST_TMPDIR/new-location"
mv "$repo" "$new_repo"

# Run the pointer update
run _wt_update_worktree_pointers "$new_repo"
assert_success

# .git file should now point to the new location
dot_git_content="$(cat "$wt_path/.git")"
assert_equal "$dot_git_content" "gitdir: ${new_repo}/.git/worktrees/wt-feat"

# Verify git operations work
run git -C "$wt_path" status
assert_success
}

@test "_wt_update_worktree_pointers is no-op without worktrees" {
local repo
repo=$(create_mock_repo "$BATS_TEST_TMPDIR/no-wt-repo")

# Should succeed silently when there are no worktrees
run _wt_update_worktree_pointers "$repo"
assert_success
}

@test "_wt_update_worktree_pointers handles multiple worktrees" {
local repo
repo=$(create_mock_repo "$BATS_TEST_TMPDIR/multi-wt-repo")

create_branch "$repo" "branch-a"
create_branch "$repo" "branch-b"
local wt_a="$BATS_TEST_TMPDIR/wt-a"
local wt_b="$BATS_TEST_TMPDIR/wt-b"
create_worktree "$repo" "$wt_a" "branch-a"
create_worktree "$repo" "$wt_b" "branch-b"
wt_a="$(cd "$wt_a" && pwd -P)"
wt_b="$(cd "$wt_b" && pwd -P)"

# Move repo
local new_repo="$BATS_TEST_TMPDIR/moved-multi"
mv "$repo" "$new_repo"

run _wt_update_worktree_pointers "$new_repo"
assert_success

# Both worktrees should be updated
local content_a content_b
content_a="$(cat "$wt_a/.git")"
content_b="$(cat "$wt_b/.git")"
assert_equal "$content_a" "gitdir: ${new_repo}/.git/worktrees/wt-a"
assert_equal "$content_b" "gitdir: ${new_repo}/.git/worktrees/wt-b"
}

@test "_wt_update_worktree_pointers unadopted mode skips adopted worktrees" {
local repo
repo=$(create_mock_repo "$BATS_TEST_TMPDIR/adopt-skip-repo")

create_branch "$repo" "branch-adopted"
create_branch "$repo" "branch-unadopted"
local wt_adopted="$BATS_TEST_TMPDIR/wt-adopted"
local wt_unadopted="$BATS_TEST_TMPDIR/wt-unadopted"
create_worktree "$repo" "$wt_adopted" "branch-adopted"
create_worktree "$repo" "$wt_unadopted" "branch-unadopted"
wt_adopted="$(cd "$wt_adopted" && pwd -P)"
wt_unadopted="$(cd "$wt_unadopted" && pwd -P)"

# Mark one worktree as adopted
wt_mark_adopted "$wt_adopted"

# Move repo
local new_repo="$BATS_TEST_TMPDIR/moved-adopt-skip"
mv "$repo" "$new_repo"

run _wt_update_worktree_pointers "$new_repo" "unadopted"
assert_success

# Unadopted worktree should be updated
local content_unadopted
content_unadopted="$(cat "$wt_unadopted/.git")"
assert_equal "$content_unadopted" "gitdir: ${new_repo}/.git/worktrees/wt-unadopted"

# Adopted worktree should NOT be updated (still has old path)
local content_adopted
content_adopted="$(cat "$wt_adopted/.git")"
assert_equal "$content_adopted" "gitdir: ${repo}/.git/worktrees/wt-adopted"
}

@test "_wt_update_worktree_pointers adopted mode only updates worktrees adopted by context" {
local repo
repo=$(create_mock_repo "$BATS_TEST_TMPDIR/adopt-ctx-repo")

create_branch "$repo" "branch-ctx-a"
create_branch "$repo" "branch-ctx-b"
create_branch "$repo" "branch-unadopted"
local wt_ctx_a="$BATS_TEST_TMPDIR/wt-ctx-a"
local wt_ctx_b="$BATS_TEST_TMPDIR/wt-ctx-b"
local wt_unadopted="$BATS_TEST_TMPDIR/wt-ctx-unadopted"
create_worktree "$repo" "$wt_ctx_a" "branch-ctx-a"
create_worktree "$repo" "$wt_ctx_b" "branch-ctx-b"
create_worktree "$repo" "$wt_unadopted" "branch-unadopted"
wt_ctx_a="$(cd "$wt_ctx_a" && pwd -P)"
wt_ctx_b="$(cd "$wt_ctx_b" && pwd -P)"
wt_unadopted="$(cd "$wt_unadopted" && pwd -P)"

# Mark worktrees as adopted by different contexts
export WT_CONTEXT_NAME="ctx-a"
wt_mark_adopted "$wt_ctx_a"
export WT_CONTEXT_NAME="ctx-b"
wt_mark_adopted "$wt_ctx_b"
unset WT_CONTEXT_NAME

# Move repo
local new_repo="$BATS_TEST_TMPDIR/moved-adopt-ctx"
mv "$repo" "$new_repo"

run _wt_update_worktree_pointers "$new_repo" "adopted" "ctx-a"
assert_success

# Only the worktree adopted by ctx-a should be updated
local content_a
content_a="$(cat "$wt_ctx_a/.git")"
assert_equal "$content_a" "gitdir: ${new_repo}/.git/worktrees/wt-ctx-a"

# Worktree adopted by ctx-b should NOT be updated
local content_b
content_b="$(cat "$wt_ctx_b/.git")"
assert_equal "$content_b" "gitdir: ${repo}/.git/worktrees/wt-ctx-b"

# Unadopted worktree should NOT be updated
local content_unadopted
content_unadopted="$(cat "$wt_unadopted/.git")"
assert_equal "$content_unadopted" "gitdir: ${repo}/.git/worktrees/wt-ctx-unadopted"
}

@test "_wt_update_worktree_pointers all mode updates everything" {
local repo
repo=$(create_mock_repo "$BATS_TEST_TMPDIR/all-mode-repo")

create_branch "$repo" "branch-adopted"
create_branch "$repo" "branch-unadopted"
local wt_adopted="$BATS_TEST_TMPDIR/wt-all-adopted"
local wt_unadopted="$BATS_TEST_TMPDIR/wt-all-unadopted"
create_worktree "$repo" "$wt_adopted" "branch-adopted"
create_worktree "$repo" "$wt_unadopted" "branch-unadopted"
wt_adopted="$(cd "$wt_adopted" && pwd -P)"
wt_unadopted="$(cd "$wt_unadopted" && pwd -P)"

wt_mark_adopted "$wt_adopted"

# Move repo
local new_repo="$BATS_TEST_TMPDIR/moved-all-mode"
mv "$repo" "$new_repo"

run _wt_update_worktree_pointers "$new_repo" "all"
assert_success

# Both should be updated
local content_adopted content_unadopted
content_adopted="$(cat "$wt_adopted/.git")"
content_unadopted="$(cat "$wt_unadopted/.git")"
assert_equal "$content_adopted" "gitdir: ${new_repo}/.git/worktrees/wt-all-adopted"
assert_equal "$content_unadopted" "gitdir: ${new_repo}/.git/worktrees/wt-all-unadopted"
}

Loading
Loading