From 22d14d9169f93dac6a142ee16a104d90bf465da0 Mon Sep 17 00:00:00 2001 From: Damian Lewis <7067514+damianlewis@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:57:29 +0000 Subject: [PATCH 1/2] fix(core): resolve --from ref to SHA to prevent git DWIM overriding branch name When from_ref matches a remote branch name, git's guess-remote logic overrides the -b flag and creates a tracking branch with the remote name instead of the requested name. Resolving from_ref to a commit SHA before passing it to git worktree add prevents this. --- lib/core.sh | 12 +++- tests/core_create_worktree.bats | 110 ++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/lib/core.sh b/lib/core.sh index ff26de1..d904842 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -445,6 +445,14 @@ create_worktree() { _check_branch_refs "$branch_name" + # Resolve from_ref to a commit SHA to prevent git's guess-remote logic + # from overriding the -b flag when from_ref matches a remote branch name. + # Try the ref as-is first, then with origin/ prefix for remote-only refs. + local resolved_ref + resolved_ref=$(git rev-parse --verify "$from_ref" 2>/dev/null) \ + || resolved_ref=$(git rev-parse --verify "origin/$from_ref" 2>/dev/null) \ + || resolved_ref="$from_ref" + case "$track_mode" in remote) if [ "$_wt_remote_exists" -eq 1 ]; then @@ -475,7 +483,7 @@ create_worktree() { _try_worktree_add "$worktree_path" \ "Creating new branch $branch_name from $from_ref" \ "Worktree created with new branch $branch_name" \ - "${force_args[@]}" -b "$branch_name" "$from_ref" && return 0 + "${force_args[@]}" -b "$branch_name" "$resolved_ref" && return 0 log_error "Failed to create worktree with new branch" return 1 ;; @@ -492,7 +500,7 @@ create_worktree() { _try_worktree_add "$worktree_path" \ "Creating new branch $branch_name from $from_ref" \ "Worktree created with new branch $branch_name" \ - "${force_args[@]}" -b "$branch_name" "$from_ref" && return 0 + "${force_args[@]}" -b "$branch_name" "$resolved_ref" && return 0 fi ;; esac diff --git a/tests/core_create_worktree.bats b/tests/core_create_worktree.bats index 13109cc..3884e60 100644 --- a/tests/core_create_worktree.bats +++ b/tests/core_create_worktree.bats @@ -139,3 +139,113 @@ teardown() { wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "feature/deep/path" "HEAD" "none" "1") [ "$wt_path" = "$TEST_WORKTREES_DIR/feature-deep-path" ] } + +# ── from_ref handling ────────────────────────────────────────────────────── + +@test "create_worktree from local branch starts at that branch's commit" { + git commit --allow-empty -m "second" --quiet + local expected_sha + expected_sha=$(git rev-parse HEAD) + + git branch from-source HEAD + git reset --hard HEAD~1 --quiet + + local wt_path + wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-local" "from-source" "none" "1") + [ -d "$wt_path" ] + + local actual_sha + actual_sha=$(git -C "$wt_path" rev-parse HEAD) + [ "$actual_sha" = "$expected_sha" ] +} + +@test "create_worktree from tag starts at the tagged commit" { + git commit --allow-empty -m "tagged commit" --quiet + local expected_sha + expected_sha=$(git rev-parse HEAD) + git tag v1.0.0 + + git commit --allow-empty -m "after tag" --quiet + + local wt_path + wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-tag" "v1.0.0" "none" "1") + [ -d "$wt_path" ] + + local actual_sha + actual_sha=$(git -C "$wt_path" rev-parse HEAD) + [ "$actual_sha" = "$expected_sha" ] +} + +@test "create_worktree from commit SHA starts at that commit" { + git commit --allow-empty -m "target commit" --quiet + local expected_sha + expected_sha=$(git rev-parse HEAD) + + git commit --allow-empty -m "later commit" --quiet + + local wt_path + wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-sha" "$expected_sha" "none" "1") + [ -d "$wt_path" ] + + local actual_sha + actual_sha=$(git -C "$wt_path" rev-parse HEAD) + [ "$actual_sha" = "$expected_sha" ] +} + +@test "create_worktree from remote branch uses the requested branch name not the remote name" { + # Set up a "remote" by using the test repo as its own remote + git remote add origin "$TEST_REPO" 2>/dev/null || true + git branch remote-feature HEAD + git fetch origin --quiet + + local wt_path + wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "my-branch" "remote-feature" "none" "1") + [ -d "$wt_path" ] + + # The worktree branch must be our requested name, not the remote branch name + local actual_branch + actual_branch=$(git -C "$wt_path" rev-parse --abbrev-ref HEAD) + [ "$actual_branch" = "my-branch" ] +} + +@test "create_worktree from remote branch starts at the correct commit" { + git commit --allow-empty -m "remote target" --quiet + local expected_sha + expected_sha=$(git rev-parse HEAD) + + git remote add origin "$TEST_REPO" 2>/dev/null || true + git branch remote-source HEAD + git fetch origin --quiet + + git commit --allow-empty -m "moved on" --quiet + + local wt_path + wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-remote-ref" "remote-source" "none" "1") + [ -d "$wt_path" ] + + local actual_sha + actual_sha=$(git -C "$wt_path" rev-parse HEAD) + [ "$actual_sha" = "$expected_sha" ] +} + +@test "create_worktree auto mode from local branch starts at that commit" { + git commit --allow-empty -m "auto target" --quiet + local expected_sha + expected_sha=$(git rev-parse HEAD) + + git branch auto-source HEAD + git reset --hard HEAD~1 --quiet + + local wt_path + wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "auto-from-local" "auto-source" "auto" "1") + [ -d "$wt_path" ] + + local actual_sha + actual_sha=$(git -C "$wt_path" rev-parse HEAD) + [ "$actual_sha" = "$expected_sha" ] +} + +@test "create_worktree fails with invalid from_ref" { + run create_worktree "$TEST_WORKTREES_DIR" "" "bad-ref" "nonexistent-ref-xyz" "none" "1" + [ "$status" -eq 1 ] +} From e674c884dfcc9d7106727109914c48abd259d854 Mon Sep 17 00:00:00 2001 From: Damian Lewis <7067514+damianlewis@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:37:51 +0000 Subject: [PATCH 2/2] fix(core): peel annotated tags to commits when resolving from_ref Use ^{commit} peeling syntax in rev-parse to ensure resolved_ref is always a commit SHA, not a tag object. Add annotated tag test case alongside the existing lightweight tag test. --- lib/core.sh | 4 ++-- tests/core_create_worktree.bats | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/core.sh b/lib/core.sh index d904842..37a53e4 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -449,8 +449,8 @@ create_worktree() { # from overriding the -b flag when from_ref matches a remote branch name. # Try the ref as-is first, then with origin/ prefix for remote-only refs. local resolved_ref - resolved_ref=$(git rev-parse --verify "$from_ref" 2>/dev/null) \ - || resolved_ref=$(git rev-parse --verify "origin/$from_ref" 2>/dev/null) \ + resolved_ref=$(git rev-parse --verify "${from_ref}^{commit}" 2>/dev/null) \ + || resolved_ref=$(git rev-parse --verify "origin/${from_ref}^{commit}" 2>/dev/null) \ || resolved_ref="$from_ref" case "$track_mode" in diff --git a/tests/core_create_worktree.bats b/tests/core_create_worktree.bats index 3884e60..0bca387 100644 --- a/tests/core_create_worktree.bats +++ b/tests/core_create_worktree.bats @@ -159,7 +159,7 @@ teardown() { [ "$actual_sha" = "$expected_sha" ] } -@test "create_worktree from tag starts at the tagged commit" { +@test "create_worktree from lightweight tag starts at the tagged commit" { git commit --allow-empty -m "tagged commit" --quiet local expected_sha expected_sha=$(git rev-parse HEAD) @@ -168,7 +168,24 @@ teardown() { git commit --allow-empty -m "after tag" --quiet local wt_path - wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-tag" "v1.0.0" "none" "1") + wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-light-tag" "v1.0.0" "none" "1") + [ -d "$wt_path" ] + + local actual_sha + actual_sha=$(git -C "$wt_path" rev-parse HEAD) + [ "$actual_sha" = "$expected_sha" ] +} + +@test "create_worktree from annotated tag starts at the tagged commit" { + git commit --allow-empty -m "tagged commit" --quiet + local expected_sha + expected_sha=$(git rev-parse HEAD) + git tag -a v2.0.0 -m "v2.0.0" + + git commit --allow-empty -m "after tag" --quiet + + local wt_path + wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-ann-tag" "v2.0.0" "none" "1") [ -d "$wt_path" ] local actual_sha