Skip to content

Commit 4a4e21b

Browse files
authored
fix(core): resolve --from ref to SHA to prevent git DWIM overriding branch name (#147)
* 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. * 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.
1 parent 0403d74 commit 4a4e21b

File tree

2 files changed

+137
-2
lines changed

2 files changed

+137
-2
lines changed

lib/core.sh

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,14 @@ create_worktree() {
445445

446446
_check_branch_refs "$branch_name"
447447

448+
# Resolve from_ref to a commit SHA to prevent git's guess-remote logic
449+
# from overriding the -b flag when from_ref matches a remote branch name.
450+
# Try the ref as-is first, then with origin/ prefix for remote-only refs.
451+
local resolved_ref
452+
resolved_ref=$(git rev-parse --verify "${from_ref}^{commit}" 2>/dev/null) \
453+
|| resolved_ref=$(git rev-parse --verify "origin/${from_ref}^{commit}" 2>/dev/null) \
454+
|| resolved_ref="$from_ref"
455+
448456
case "$track_mode" in
449457
remote)
450458
if [ "$_wt_remote_exists" -eq 1 ]; then
@@ -475,7 +483,7 @@ create_worktree() {
475483
_try_worktree_add "$worktree_path" \
476484
"Creating new branch $branch_name from $from_ref" \
477485
"Worktree created with new branch $branch_name" \
478-
"${force_args[@]}" -b "$branch_name" "$from_ref" && return 0
486+
"${force_args[@]}" -b "$branch_name" "$resolved_ref" && return 0
479487
log_error "Failed to create worktree with new branch"
480488
return 1
481489
;;
@@ -492,7 +500,7 @@ create_worktree() {
492500
_try_worktree_add "$worktree_path" \
493501
"Creating new branch $branch_name from $from_ref" \
494502
"Worktree created with new branch $branch_name" \
495-
"${force_args[@]}" -b "$branch_name" "$from_ref" && return 0
503+
"${force_args[@]}" -b "$branch_name" "$resolved_ref" && return 0
496504
fi
497505
;;
498506
esac

tests/core_create_worktree.bats

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,130 @@ teardown() {
139139
wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "feature/deep/path" "HEAD" "none" "1")
140140
[ "$wt_path" = "$TEST_WORKTREES_DIR/feature-deep-path" ]
141141
}
142+
143+
# ── from_ref handling ──────────────────────────────────────────────────────
144+
145+
@test "create_worktree from local branch starts at that branch's commit" {
146+
git commit --allow-empty -m "second" --quiet
147+
local expected_sha
148+
expected_sha=$(git rev-parse HEAD)
149+
150+
git branch from-source HEAD
151+
git reset --hard HEAD~1 --quiet
152+
153+
local wt_path
154+
wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-local" "from-source" "none" "1")
155+
[ -d "$wt_path" ]
156+
157+
local actual_sha
158+
actual_sha=$(git -C "$wt_path" rev-parse HEAD)
159+
[ "$actual_sha" = "$expected_sha" ]
160+
}
161+
162+
@test "create_worktree from lightweight tag starts at the tagged commit" {
163+
git commit --allow-empty -m "tagged commit" --quiet
164+
local expected_sha
165+
expected_sha=$(git rev-parse HEAD)
166+
git tag v1.0.0
167+
168+
git commit --allow-empty -m "after tag" --quiet
169+
170+
local wt_path
171+
wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-light-tag" "v1.0.0" "none" "1")
172+
[ -d "$wt_path" ]
173+
174+
local actual_sha
175+
actual_sha=$(git -C "$wt_path" rev-parse HEAD)
176+
[ "$actual_sha" = "$expected_sha" ]
177+
}
178+
179+
@test "create_worktree from annotated tag starts at the tagged commit" {
180+
git commit --allow-empty -m "tagged commit" --quiet
181+
local expected_sha
182+
expected_sha=$(git rev-parse HEAD)
183+
git tag -a v2.0.0 -m "v2.0.0"
184+
185+
git commit --allow-empty -m "after tag" --quiet
186+
187+
local wt_path
188+
wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-ann-tag" "v2.0.0" "none" "1")
189+
[ -d "$wt_path" ]
190+
191+
local actual_sha
192+
actual_sha=$(git -C "$wt_path" rev-parse HEAD)
193+
[ "$actual_sha" = "$expected_sha" ]
194+
}
195+
196+
@test "create_worktree from commit SHA starts at that commit" {
197+
git commit --allow-empty -m "target commit" --quiet
198+
local expected_sha
199+
expected_sha=$(git rev-parse HEAD)
200+
201+
git commit --allow-empty -m "later commit" --quiet
202+
203+
local wt_path
204+
wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-sha" "$expected_sha" "none" "1")
205+
[ -d "$wt_path" ]
206+
207+
local actual_sha
208+
actual_sha=$(git -C "$wt_path" rev-parse HEAD)
209+
[ "$actual_sha" = "$expected_sha" ]
210+
}
211+
212+
@test "create_worktree from remote branch uses the requested branch name not the remote name" {
213+
# Set up a "remote" by using the test repo as its own remote
214+
git remote add origin "$TEST_REPO" 2>/dev/null || true
215+
git branch remote-feature HEAD
216+
git fetch origin --quiet
217+
218+
local wt_path
219+
wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "my-branch" "remote-feature" "none" "1")
220+
[ -d "$wt_path" ]
221+
222+
# The worktree branch must be our requested name, not the remote branch name
223+
local actual_branch
224+
actual_branch=$(git -C "$wt_path" rev-parse --abbrev-ref HEAD)
225+
[ "$actual_branch" = "my-branch" ]
226+
}
227+
228+
@test "create_worktree from remote branch starts at the correct commit" {
229+
git commit --allow-empty -m "remote target" --quiet
230+
local expected_sha
231+
expected_sha=$(git rev-parse HEAD)
232+
233+
git remote add origin "$TEST_REPO" 2>/dev/null || true
234+
git branch remote-source HEAD
235+
git fetch origin --quiet
236+
237+
git commit --allow-empty -m "moved on" --quiet
238+
239+
local wt_path
240+
wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "from-remote-ref" "remote-source" "none" "1")
241+
[ -d "$wt_path" ]
242+
243+
local actual_sha
244+
actual_sha=$(git -C "$wt_path" rev-parse HEAD)
245+
[ "$actual_sha" = "$expected_sha" ]
246+
}
247+
248+
@test "create_worktree auto mode from local branch starts at that commit" {
249+
git commit --allow-empty -m "auto target" --quiet
250+
local expected_sha
251+
expected_sha=$(git rev-parse HEAD)
252+
253+
git branch auto-source HEAD
254+
git reset --hard HEAD~1 --quiet
255+
256+
local wt_path
257+
wt_path=$(create_worktree "$TEST_WORKTREES_DIR" "" "auto-from-local" "auto-source" "auto" "1")
258+
[ -d "$wt_path" ]
259+
260+
local actual_sha
261+
actual_sha=$(git -C "$wt_path" rev-parse HEAD)
262+
[ "$actual_sha" = "$expected_sha" ]
263+
}
264+
265+
@test "create_worktree fails with invalid from_ref" {
266+
run create_worktree "$TEST_WORKTREES_DIR" "" "bad-ref" "nonexistent-ref-xyz" "none" "1"
267+
[ "$status" -eq 1 ]
268+
}

0 commit comments

Comments
 (0)