diff --git a/lib/core.sh b/lib/core.sh index ff26de1..37a53e4 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}^{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 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..0bca387 100644 --- a/tests/core_create_worktree.bats +++ b/tests/core_create_worktree.bats @@ -139,3 +139,130 @@ 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 lightweight 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-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 + 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 ] +}