Skip to content

Add checkout_branch command to switch to existing branches#19

Merged
dev-ankit merged 1 commit into
mainfrom
claude/worktree-checkout-branch-udd9U
Feb 19, 2026
Merged

Add checkout_branch command to switch to existing branches#19
dev-ankit merged 1 commit into
mainfrom
claude/worktree-checkout-branch-udd9U

Conversation

@dev-ankit

Copy link
Copy Markdown
Owner

Summary

Add a new checkout_branch() method to create worktrees from existing branches without creating new branches. This enables users to check out branches created by others or on remote repositories directly into worktrees.

Key Changes

  • New checkout_branch() method in WorktreeManager:

    • Checks out an existing branch into a new worktree (no branch creation)
    • Supports optional custom worktree names via name parameter
    • Supports fetching from remote before checkout via fetch parameter
    • Validates that branch exists locally or on remote
    • Prevents duplicate worktrees for the same branch
    • Stores worktree name for later retrieval by derived name
  • New _derive_name_from_branch() static method:

    • Extracts worktree name from branch name (e.g., fix/login-buglogin-bug)
    • Handles remote-tracking branches (strips origin/ prefix)
    • Takes the last path component of hierarchical branch names
  • Updated list_worktrees() method:

    • Now checks for stored worktree names first (set by checkout_branch())
    • Falls back to extracting name from branch if no stored name exists
    • Ensures worktrees created via checkout_branch() are findable by derived name
  • New fetch_branch() git helper:

    • Fetches a specific branch from remote
    • Used by checkout_branch() when --fetch flag is provided
  • CLI integration (wt switch -c -B):

    • Added -B/--branch option to switch command for checking out existing branches
    • Added -f/--fetch option to fetch before checkout
    • Validates flag combinations (e.g., -B requires -c, cannot be used with -d or -b)
    • Comprehensive test coverage for all flag combinations and edge cases

Implementation Details

  • Worktree names are stored in git config (worktree.<path>.name) to persist the derived name
  • The checkout_branch() method uses git worktree add with create_branch=False to avoid creating new branches
  • Upstream tracking is configured automatically if the remote branch exists
  • Full backward compatibility maintained with existing worktree creation methods

https://claude.ai/code/session_0189JpNR1U5W4vguS2XTetXg

Adds the ability to checkout an existing branch (by its full name) into
a new worktree without prefix mangling. This is useful for pulling in
changes locally to validate a branch without switching the current branch.

Usage:
  wt switch -c -B fix/login-bug          # auto-derives name "login-bug"
  wt switch -c review -B fix/login-bug   # custom worktree name
  wt switch -c -B feature/pr-123 --fetch # fetch from remote first

Changes:
- Add -B/--branch and -f/--fetch options to `wt switch` command
- Add checkout_branch() and _derive_name_from_branch() to WorktreeManager
- Add fetch_branch() helper to git module
- Update list_worktrees() to check stored name for branched worktrees
- Add 18 new tests covering checkout, name derivation, and integration

https://claude.ai/code/session_0189JpNR1U5W4vguS2XTetXg
Copilot AI review requested due to automatic review settings February 19, 2026 04:21
@dev-ankit dev-ankit merged commit 52ac350 into main Feb 19, 2026
7 checks passed
@dev-ankit dev-ankit deleted the claude/worktree-checkout-branch-udd9U branch February 19, 2026 04:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds a new checkout_branch() method to the worktree manager, enabling users to check out existing branches (created by others or on remote repositories) into new worktrees without creating new branches. This complements the existing create_worktree() method which always creates a new branch with a configured prefix.

Changes:

  • Added checkout_branch() method to check out existing branches into worktrees with automatic name derivation
  • Introduced _derive_name_from_branch() static method to extract worktree names from hierarchical branch names
  • Added fetch_branch() git helper to fetch specific branches from remotes
  • Integrated CLI support via -B/--branch and -f/--fetch flags in the switch command with comprehensive validation

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tools/wt-worktree/wt/worktree.py Added checkout_branch() method and _derive_name_from_branch() helper, updated list_worktrees() to use stored worktree names
tools/wt-worktree/wt/git.py Added fetch_branch() function to fetch specific branches from remote
tools/wt-worktree/wt/cli.py Added -B/--branch and -f/--fetch options to switch command with validation logic
tools/wt-worktree/tests/test_worktree.py Comprehensive test coverage for checkout_branch() functionality including edge cases
tools/wt-worktree/tests/test_cli.py Test coverage for CLI integration including flag validation and combinations

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

manager.checkout_branch("fix/bug")

with pytest.raises(git.GitError, match="already exists"):
manager.checkout_branch("hotfix/bug")

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

The test coverage for checkout_branch doesn't include tests with actual remote branches. While there is a git_repo_with_remote fixture available in conftest.py, the tests only verify local branch checkout behavior. Consider adding tests that verify the remote branch checkout scenario, especially testing the fetch functionality and the case where a branch exists only on the remote.

Suggested change
manager.checkout_branch("hotfix/bug")
manager.checkout_branch("hotfix/bug")
def test_checkout_remote_tracking_branch(git_repo_with_remote, tmp_path, monkeypatch):
"""Test checking out a remote-tracking branch (e.g. origin/main)."""
# Use temp directory for config
monkeypatch.setenv("WT_CONFIG", str(tmp_path))
# git_repo_with_remote provides a repo with a configured remote
repo_path = git_repo_with_remote
config = Config(repo_path)
# For repos with remotes, default base may be a remote-tracking branch
config.set("default_base", "origin/main")
config.save()
manager = WorktreeManager(config)
wt_path = manager.checkout_branch("origin/main")
assert wt_path.exists()
wt = manager.find_worktree_by_name("main")
assert wt is not None
# Branch may be stored as 'origin/main' or 'main' depending on implementation
assert wt["branch"] in ("origin/main", "main")
def test_checkout_remote_only_branch(git_repo_with_remote, tmp_path, monkeypatch):
"""Test checking out a branch that exists only on the remote."""
# Use temp directory for config
monkeypatch.setenv("WT_CONFIG", str(tmp_path))
# Assume git_repo_with_remote returns (local_repo, remote_repo)
local_repo, remote_repo = git_repo_with_remote
# Create a branch only on the remote repository
git.create_branch("feature/remote-only", "HEAD", remote_repo)
config = Config(local_repo)
config.set("default_base", "origin/main")
config.save()
manager = WorktreeManager(config)
# Checkout should fetch the remote-only branch and create a worktree
wt_path = manager.checkout_branch("origin/feature/remote-only")
assert wt_path.exists()
wt = manager.find_worktree_by_name("remote-only")
assert wt is not None
assert wt["path"] == wt_path

Copilot uses AI. Check for mistakes.
Raises:
GitError: If fetch fails
"""
run_git(["fetch", remote, branch], cwd=path)

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

The fetch_branch function may not work correctly when fetching remote branches. The command git fetch origin branch_name expects a refspec that includes the refs path. For remote branches that don't exist locally, you may need to use the full refspec format like git fetch origin branch_name:refs/remotes/origin/branch_name or just use the branch name if it's a local branch being fetched from remote. Consider testing this with actual remote branches to ensure it works as expected.

Suggested change
run_git(["fetch", remote, branch], cwd=path)
# Use an explicit refspec so remote-only branches are fetched into the
# expected remote-tracking reference (refs/remotes/<remote>/<branch>).
refspec = f"{branch}:refs/remotes/{remote}/{branch}"
run_git(["fetch", remote, refspec], cwd=path)

Copilot uses AI. Check for mistakes.
Comment on lines +275 to +289
if fetch:
try:
git.fetch_branch(branch, "origin", self.repo_root)
except git.GitError:
pass

# Validate branch exists (locally or as a remote tracking branch)
local_exists = git.branch_exists(branch, self.repo_root)
remote_exists = git.remote_branch_exists(branch, "origin", self.repo_root)

if not local_exists and not remote_exists:
raise git.GitError(
f"Branch '{branch}' does not exist locally or on remote.\n"
f"Use '--fetch' to fetch from remote first, or check the branch name."
)

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

The error message suggests using --fetch even when fetch was already requested and failed. If the user explicitly used --fetch and the fetch failed silently (caught on line 278-279), this message will incorrectly suggest they should use --fetch. Consider checking if fetch was already attempted and providing a more helpful message in that case.

Suggested change
if fetch:
try:
git.fetch_branch(branch, "origin", self.repo_root)
except git.GitError:
pass
# Validate branch exists (locally or as a remote tracking branch)
local_exists = git.branch_exists(branch, self.repo_root)
remote_exists = git.remote_branch_exists(branch, "origin", self.repo_root)
if not local_exists and not remote_exists:
raise git.GitError(
f"Branch '{branch}' does not exist locally or on remote.\n"
f"Use '--fetch' to fetch from remote first, or check the branch name."
)
fetch_failed = False
if fetch:
try:
git.fetch_branch(branch, "origin", self.repo_root)
except git.GitError:
# Record that the fetch failed so we can present a helpful error
fetch_failed = True
# Validate branch exists (locally or as a remote tracking branch)
local_exists = git.branch_exists(branch, self.repo_root)
remote_exists = git.remote_branch_exists(branch, "origin", self.repo_root)
if not local_exists and not remote_exists:
base_msg = f"Branch '{branch}' does not exist locally or on remote."
if not fetch:
detail = (
"Use '--fetch' to fetch from remote first, or check the branch name."
)
elif fetch_failed:
detail = (
"Attempted to fetch from remote, but the operation failed and the "
"branch was still not found. Please verify that the branch exists "
"on 'origin' and that your remote configuration is correct."
)
else:
detail = (
"Fetched from remote, but the branch was still not found. Please "
"verify that the branch exists on 'origin' or check the branch name."
)
raise git.GitError(f"{base_msg}\n{detail}")

Copilot uses AI. Check for mistakes.
if wt_path.exists():
raise git.GitError(
f"Path {wt_path} already exists. "
f"Please remove it or use --name to choose a different name."

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

The error message mentions using --name option, but the CLI flag is actually defined as the positional argument name, not --name. The correct way to use a custom name is wt switch -c <custom_name> -B <branch>, not with a --name flag. Update this error message to reflect the actual CLI syntax.

Suggested change
f"Please remove it or use --name to choose a different name."
f"Please remove it or choose a different name."

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +77
# Check for stored name first (used by checkout_branch)
stored_name = git.get_worktree_name(wt["path"])
if stored_name:
wt["name"] = stored_name
else:
wt["name"] = self.config.extract_worktree_name(wt["branch"])

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

The list_worktrees() method always checks for stored names first for branches (lines 73-75), but this may override names for worktrees created via create_worktree() if a stored name exists. The logic assumes only checkout_branch() stores names, but this could cause confusion. Consider only using stored names when the branch name doesn't match the expected prefix pattern, or document this behavior more clearly to avoid unexpected naming conflicts.

Suggested change
# Check for stored name first (used by checkout_branch)
stored_name = git.get_worktree_name(wt["path"])
if stored_name:
wt["name"] = stored_name
else:
wt["name"] = self.config.extract_worktree_name(wt["branch"])
# Prefer name derived from branch; fall back to stored name if needed
name_from_branch = self.config.extract_worktree_name(wt["branch"])
if name_from_branch:
wt["name"] = name_from_branch
else:
stored_name = git.get_worktree_name(wt["path"])
if stored_name:
wt["name"] = stored_name
else:
# Fallback to using the branch name directly
wt["name"] = wt["branch"]

Copilot uses AI. Check for mistakes.
Comment on lines +278 to +279
except git.GitError:
pass

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

Silent exception handling (pass) for fetch_branch could hide legitimate errors. If the fetch fails due to network issues, authentication problems, or invalid branch names, the user won't be informed. Consider logging the error or providing feedback to the user when the fetch fails, especially since the --fetch flag was explicitly requested.

Suggested change
except git.GitError:
pass
except git.GitError as e:
warning(f"Failed to fetch branch '{branch}' from 'origin': {e}")

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +135
from .worktree import WorktreeManager

try:
wt_path = ctx.manager.checkout_branch(checkout_branch, name, fetch)
display_name = name if name else WorktreeManager._derive_name_from_branch(checkout_branch)

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

Consider using type(ctx.manager)._derive_name_from_branch(checkout_branch) instead of importing WorktreeManager separately. This would avoid the redundant import and use the already-available manager instance's class. Alternatively, if the import is intentional for clarity, consider moving it to the top of the file with other imports.

Suggested change
from .worktree import WorktreeManager
try:
wt_path = ctx.manager.checkout_branch(checkout_branch, name, fetch)
display_name = name if name else WorktreeManager._derive_name_from_branch(checkout_branch)
try:
wt_path = ctx.manager.checkout_branch(checkout_branch, name, fetch)
display_name = name if name else type(ctx.manager)._derive_name_from_branch(checkout_branch)

Copilot uses AI. Check for mistakes.
Comment on lines +322 to +332
except git.GitError:
try:
git.configure_push_remote(branch, "origin", branch, wt_path)
except git.GitError:
pass
else:
try:
git.configure_push_remote(branch, "origin", branch, wt_path)
except git.GitError:
pass

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

The upstream tracking configuration logic appears complex and may have edge cases. When remote_exists is true, the code tries to set upstream tracking but on failure falls back to configure_push_remote. However, when remote_exists is false, it also tries to configure push remote. This means in both branches, configure_push_remote might be called, which seems redundant. Consider simplifying this logic or adding comments to explain the different scenarios and why both paths might call configure_push_remote.

Suggested change
except git.GitError:
try:
git.configure_push_remote(branch, "origin", branch, wt_path)
except git.GitError:
pass
else:
try:
git.configure_push_remote(branch, "origin", branch, wt_path)
except git.GitError:
pass
# Upstream configured successfully; no need for separate push-remote config.
return wt_path
except git.GitError:
# If upstream configuration fails, fall back to configuring a push-only remote
# below. This is a best-effort step and should not abort worktree creation.
pass
# Either no remote exists, or upstream configuration failed above.
# In both cases, attempt to configure a push-only remote, but do not fail
# the checkout if this configuration step fails.
try:
git.configure_push_remote(branch, "origin", branch, wt_path)
except git.GitError:
pass

Copilot uses AI. Check for mistakes.
Comment on lines +308 to +310
# Create worktree using existing branch
try:
git.add_worktree(wt_path, branch, create_branch=False,

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

When the branch exists only on the remote (not locally), calling git.add_worktree(wt_path, branch, create_branch=False, ...) with the plain branch name may fail. Git worktree add expects either a local branch reference or needs to create a new local branch. Consider creating a local tracking branch first when only the remote branch exists, or pass the full remote reference (e.g., origin/branch_name) to git worktree add.

Suggested change
# Create worktree using existing branch
try:
git.add_worktree(wt_path, branch, create_branch=False,
# Determine which ref to use when creating the worktree.
# If the branch only exists on the remote, use the full remote ref so
# `git worktree add` can operate on a valid reference.
worktree_target = branch
if not local_exists and remote_exists:
worktree_target = f"origin/{branch}"
# Create worktree using existing branch or remote ref
try:
git.add_worktree(wt_path, worktree_target, create_branch=False,

Copilot uses AI. Check for mistakes.
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.

3 participants