Skip to content

fix: Bug switching to trunk with worktrees enabled#91

Merged
dansimau merged 2 commits intomainfrom
dan.simmons/fix-worktree-switch
Dec 22, 2025
Merged

fix: Bug switching to trunk with worktrees enabled#91
dansimau merged 2 commits intomainfrom
dan.simmons/fix-worktree-switch

Conversation

@dansimau
Copy link
Owner

@dansimau dansimau commented Dec 22, 2025

No description provided.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR adds important safeguards for trunk branch and primary worktree handling. The changes are generally well-structured, but there are a few areas that need attention:

Key Findings

Critical Issues:

  1. Primary worktree deletion logic (pkg/yas/branch.go:87): When a branch is in the primary worktree, the code falls through to deleteNonWorktreeBranch(), which may fail if the branch is currently checked out. This needs explicit handling.

  2. Potential duplicate worktree creation (pkg/yas/worktrees.go:53): The conditional logic may allow creating a second worktree if a branch is already checked out in a different linked worktree (not primary). Git will reject this.

Improvements Needed:

  • Missing test coverage for the new BranchHasWorktree() function
  • Comment accuracy issues in worktrees.go that could mislead future maintainers
  • Complex conditional logic that would benefit from using the new BranchHasWorktree() helper

Good Changes:

  • Trunk branch deletion protection is excellent defensive programming
  • Early return for trunk in EnsureLinkedWorktreeForBranch() prevents "already used" errors
  • Primary worktree detection logic is sound

Recommended Actions

  1. Add test coverage for edge cases, especially:

    • Deleting a branch checked out in primary worktree
    • Ensuring worktree when branch exists in another linked worktree
  2. Fix the primary worktree deletion flow to handle the checked-out case

  3. Simplify the worktree existence check using the new BranchHasWorktree() helper

The core improvements are valuable, but the edge cases need handling before merge to avoid runtime errors.

}
}

return false, nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test for this function

The new BranchHasWorktree function is straightforward, but would benefit from unit tests covering:

  • Branch exists in primary worktree
  • Branch exists in linked worktree
  • Branch doesn't exist in any worktree
  • Error handling when Worktrees() fails

This will help ensure the function behaves correctly across different scenarios.

func (yas *YAS) DeleteBranch(branchName string, force bool) error {
// Refuse to delete trunk branch
if branchName == yas.cfg.TrunkBranch {
return errors.New("refusing to delete trunk branch")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good safety check!

Adding validation to prevent deleting the trunk branch is excellent defensive programming. This prevents accidental data loss.

Optional improvement: Consider returning a more specific error type (e.g., ErrCannotDeleteTrunk) that could be checked with errors.Is() for better error handling by callers.

}

if !isPrimary {
return yas.deleteWorktreeBranch(worktreePath, branchName, force)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic flow concern: Primary worktree branch deletion

When a branch is checked out in the primary worktree (isPrimary == true), the code falls through to deleteNonWorktreeBranch(). However, this seems incorrect because:

  1. The branch IS in a worktree (the primary one)
  2. deleteNonWorktreeBranch() assumes no worktree and tries to delete the branch directly

This could fail if the branch is currently checked out. Should this case either:

  • Return an error asking the user to switch to a different branch first?
  • Automatically switch to trunk before deletion?
  • Handle this case explicitly rather than falling through?

Test case needed: Try deleting a branch that's checked out in the primary worktree.

// This also handles the case where trunk is already checked out in primary worktree,
// which would otherwise cause "branch is already used by worktree" error.
if branchName == yas.cfg.TrunkBranch {
return nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good edge case handling

Preventing worktree creation for the trunk branch makes sense and avoids the "branch is already used by worktree" error. This is a clean solution.

Minor suggestion: The comment could be more specific about why trunk should stay in primary:

// Don't create a worktree for the trunk branch - it must remain in the primary 
// worktree as the stable base for the repository. Creating a linked worktree 
// would cause "branch is already used by worktree" errors.

}

// Check if the existing worktree is the primary repo (not a separate worktree)
// Check if the found worktree is different from where we're running
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment accuracy issue

The comment says "Check if the found worktree is different from where we're running" but the code actually checks if it's different from RepoDirectory, not necessarily "where we're running" (which would be the current working directory).

Suggested revision:

// Check if the existing worktree path differs from the repository root directory

This is more precise and matches what the code actually does.

// Worktree already exists for this branch in a DIFFERENT location than current directory.
// If the branch is just temporarily checked out in the current worktree (same as RepoDirectory),
// we still want to create a dedicated worktree for it.
if existingWorktreePath != "" && existingResolved != repoResolved {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complex conditional logic needs clarification

This condition is doing a lot and the multi-line comment indicates it's handling a subtle case. The logic seems to be:

"Return early (don't create worktree) if a worktree exists AND it's not the current directory"

However, this is confusing because:

  1. If existingWorktreePath == "", the condition short-circuits (correct)
  2. If existingResolved == repoResolved, we continue to create a new worktree

Potential issue: What happens if the branch is already checked out in a different linked worktree, not the primary? The code would try to create another worktree for the same branch, which git will reject.

Suggestion:

  • Add a test case for: branch checked out in linked worktree A, try to ensure worktree from linked worktree B
  • Consider using the new BranchHasWorktree() helper for clarity:
hasWorktree, err := yas.git.BranchHasWorktree(branchName)
if err != nil {
    return fmt.Errorf("failed to check if branch has worktree: %w", err)
}
if hasWorktree && existingResolved != repoResolved {
    return nil // Branch already has a worktree elsewhere
}

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR successfully fixes the bug where yas br <trunk> would fail with "branch already used by worktree" when worktree-branch mode is enabled. The fix is well-implemented with good defensive programming.

Key Changes

Core fix: EnsureLinkedWorktreeForBranch now skips worktree creation for trunk branch (pkg/yas/worktrees.go:28-29)
Defensive check: DeleteBranch now refuses to delete trunk (pkg/yas/branch.go:50-52)
Primary worktree handling: DeleteBranch correctly handles deletion when branch is in primary worktree (pkg/yas/branch.go:75-87)
Excellent test coverage: Integration test documents bug and prevents regression (test/worktree_test.go:677-746)
Improved comments: Better documentation of path comparison logic

Minor Notes

  • BranchHasWorktree function (pkg/gitexec/worktrees.go:135-149) is added but unused - consider removing or documenting its future purpose
  • Error message for trunk deletion could include the branch name for clarity

Verdict

The PR is well-structured, addresses the root cause correctly, and includes comprehensive testing. The code quality is high with clear comments explaining the intent. Ready to merge after considering the minor feedback above.

}
}

return false, nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new BranchHasWorktree function looks correct but isn't actually used in this PR. Consider:

  • If it's needed for future work, mention that in the PR description
  • If it's not needed, remove it to keep the PR focused

The function itself is well-implemented with proper error handling and clear logic.

func (yas *YAS) DeleteBranch(branchName string, force bool) error {
// Refuse to delete trunk branch
if branchName == yas.cfg.TrunkBranch {
return errors.New("refusing to delete trunk branch")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good defensive check! This prevents accidentally deleting the trunk branch.

Minor suggestion: Consider using fmt.Errorf for consistency with other errors in the codebase:

Suggested change
return errors.New("refusing to delete trunk branch")
return fmt.Errorf("refusing to delete trunk branch: %s", yas.cfg.TrunkBranch)

This provides more context about which branch is being protected.

}

if !isPrimary {
return yas.deleteWorktreeBranch(worktreePath, branchName, force)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic change is subtle and important. The old code always tried to delete as a worktree branch if worktreePath != "". The new code now checks if it's the primary worktree first.

This correctly handles the case where trunk is checked out in the primary worktree. However, the logic flow might be clearer with an early return:

Suggested change
return yas.deleteWorktreeBranch(worktreePath, branchName, force)
if isPrimary {
// Can't use git worktree remove on primary - fall through to regular deletion
} else {
return yas.deleteWorktreeBranch(worktreePath, branchName, force)
}

Actually, on second thought, your current implementation is fine and the comment helps clarify intent.

// This also handles the case where trunk is already checked out in primary worktree,
// which would otherwise cause "branch is already used by worktree" error.
if branchName == yas.cfg.TrunkBranch {
return nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent fix! This is the core of the issue - preventing worktree creation for the trunk branch.

The comment clearly explains both:

  1. Why we don't create a worktree for trunk (it stays in primary)
  2. What error this prevents

This is a clean solution that handles the edge case at the right layer of abstraction.

contentStr := string(content)
// Should switch to primary repo and run yas br main there
assert.Assert(t, cmp.Contains(contentStr, "yas br main"))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent integration test! This test:

  • ✅ Clearly documents the bug scenario in the function name and comments
  • ✅ Sets up realistic conditions (worktree-branch config enabled)
  • ✅ Tests the exact failure case (yas br main from a worktree)
  • ✅ Verifies both success and expected side effects (no worktree for main)

The test structure is clean and follows the existing patterns in the codebase. This will prevent regression of this bug.

}

// Check if the existing worktree is the primary repo (not a separate worktree)
// Check if the found worktree is different from where we're running
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These updated comments significantly improve clarity about what this check is actually doing. The old comment mentioned "primary repo" but that was misleading - this is about comparing paths regardless of which is primary.

The new wording "different from where we're running" is more accurate to the actual comparison being performed.

@dansimau dansimau merged commit 9c6887b into main Dec 22, 2025
9 checks passed
@dansimau dansimau deleted the dan.simmons/fix-worktree-switch branch December 22, 2025 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments