Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions tools/wt-worktree/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,91 @@ def test_detached_worktree_backward_compatibility(runner, initialized_repo, no_p
# Should be able to find it by inferred name
found_wt = manager.find_worktree_by_name("legacy")
assert found_wt is not None


# --- checkout branch (switch -c -B) tests ---


def test_switch_checkout_branch(runner, initialized_repo, no_prompt):
"""Test wt switch -c -B with existing branch."""
git.create_branch("fix/login-bug", "HEAD", initialized_repo)

result = runner.invoke(cli, ["switch", "-c", "-B", "fix/login-bug"])
assert result.exit_code == 0
assert "fix/login-bug" in result.output
assert "login-bug" in result.output


def test_switch_checkout_branch_custom_name(runner, initialized_repo, no_prompt):
"""Test wt switch -c <name> -B <branch>."""
git.create_branch("fix/login-bug", "HEAD", initialized_repo)

result = runner.invoke(cli, ["switch", "-c", "review", "-B", "fix/login-bug"])
assert result.exit_code == 0
assert "review" in result.output


def test_switch_checkout_branch_nonexistent(runner, initialized_repo):
"""Test wt switch -c -B with nonexistent branch."""
result = runner.invoke(cli, ["switch", "-c", "-B", "nonexistent/branch"])
assert result.exit_code == 3 # EXIT_GIT_ERROR
assert "does not exist" in result.output


def test_switch_checkout_branch_requires_create(runner, initialized_repo):
"""Test that -B requires -c flag."""
result = runner.invoke(cli, ["switch", "-B", "fix/login-bug"])
assert result.exit_code == 2
assert "requires" in result.output


def test_switch_checkout_branch_no_detached(runner, initialized_repo):
"""Test that -B cannot be used with -d."""
result = runner.invoke(cli, ["switch", "-c", "-B", "fix/login-bug", "-d"])
assert result.exit_code == 2
assert "cannot be used" in result.output.lower()


def test_switch_checkout_branch_no_base(runner, initialized_repo):
"""Test that -B cannot be used with -b."""
result = runner.invoke(cli, ["switch", "-c", "-B", "fix/login-bug", "-b", "main"])
assert result.exit_code == 2
assert "cannot be used" in result.output.lower()


def test_switch_checkout_branch_shell_helper(runner, initialized_repo, no_prompt):
"""Test wt switch -c -B with --shell-helper."""
git.create_branch("fix/login-bug", "HEAD", initialized_repo)

result = runner.invoke(cli, ["switch", "-c", "-B", "fix/login-bug", "--shell-helper"])
assert result.exit_code == 0
# Output should be just the path
output = result.output.strip()
assert "/" in output


def test_switch_checkout_then_switch(runner, initialized_repo, no_prompt):
"""Test that wt switch works after checkout."""
git.create_branch("fix/login-bug", "HEAD", initialized_repo)
runner.invoke(cli, ["switch", "-c", "-B", "fix/login-bug"])

# Should be able to switch to it by derived name
result = runner.invoke(cli, ["switch", "login-bug"])
assert result.exit_code == 0


def test_switch_checkout_then_delete(runner, initialized_repo, no_prompt):
"""Test that wt delete works after checkout."""
git.create_branch("fix/login-bug", "HEAD", initialized_repo)
runner.invoke(cli, ["switch", "-c", "-B", "fix/login-bug"])

# Should be able to delete by derived name
result = runner.invoke(cli, ["delete", "login-bug", "--force", "--keep-branch"])
assert result.exit_code == 0


def test_switch_checkout_fetch_requires_branch(runner, initialized_repo):
"""Test that --fetch requires -B."""
result = runner.invoke(cli, ["switch", "-c", "feat", "--fetch"])
assert result.exit_code == 2
assert "requires" in result.output
90 changes: 90 additions & 0 deletions tools/wt-worktree/tests/test_worktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,93 @@ def test_find_by_full_branch_name(manager):
assert wt1 is not None
assert wt2 is not None
assert wt1["path"] == wt2["path"]


# --- checkout_branch tests ---


def test_derive_name_from_branch():
"""Test name derivation from branch names."""
derive = WorktreeManager._derive_name_from_branch
assert derive("fix/login-bug") == "login-bug"
assert derive("feature/add-auth") == "add-auth"
assert derive("origin/feature/pr-123") == "pr-123"
assert derive("main") == "main"
assert derive("claude/some-branch") == "some-branch"
assert derive("origin/main") == "main"
assert derive("a/b/c") == "c"


def test_checkout_branch(manager, git_repo):
"""Test checking out an existing branch into a new worktree."""
git.create_branch("fix/login-bug", "HEAD", git_repo)

wt_path = manager.checkout_branch("fix/login-bug")

assert wt_path.exists()
wt = manager.find_worktree_by_name("login-bug")
assert wt is not None
assert wt["branch"] == "fix/login-bug"


def test_checkout_branch_custom_name(manager, git_repo):
"""Test checkout with a custom worktree name."""
git.create_branch("fix/login-bug", "HEAD", git_repo)

wt_path = manager.checkout_branch("fix/login-bug", name="review-login")

assert wt_path.exists()
wt = manager.find_worktree_by_name("review-login")
assert wt is not None
assert wt["branch"] == "fix/login-bug"


def test_checkout_nonexistent_branch(manager, git_repo):
"""Test checkout of a branch that doesn't exist."""
with pytest.raises(git.GitError, match="does not exist"):
manager.checkout_branch("nonexistent/branch")


def test_checkout_branch_already_has_worktree(manager, git_repo):
"""Test checkout of a branch that already has a worktree."""
git.create_branch("fix/login-bug", "HEAD", git_repo)
manager.checkout_branch("fix/login-bug")

with pytest.raises(git.GitError, match="already has a worktree"):
manager.checkout_branch("fix/login-bug")


def test_checkout_branch_findable_by_derived_name(manager, git_repo):
"""Test that checked-out worktree is findable by its derived name."""
git.create_branch("claude/my-feature", "HEAD", git_repo)
manager.checkout_branch("claude/my-feature")

# Should be findable by derived name
wt = manager.find_worktree_by_name("my-feature")
assert wt is not None

# Should also be findable by full branch name
wt2 = manager.find_worktree_by_name("claude/my-feature")
assert wt2 is not None
assert wt["path"] == wt2["path"]


def test_checkout_branch_appears_in_list(manager, git_repo):
"""Test that checked-out worktree appears in list with correct name."""
git.create_branch("fix/login-bug", "HEAD", git_repo)
manager.checkout_branch("fix/login-bug")

worktrees = manager.list_worktrees()
names = [wt["name"] for wt in worktrees]
assert "login-bug" in names


def test_checkout_branch_name_conflict(manager, git_repo):
"""Test checkout when derived name conflicts with existing path."""
git.create_branch("fix/bug", "HEAD", git_repo)
git.create_branch("hotfix/bug", "HEAD", git_repo)

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.
55 changes: 53 additions & 2 deletions tools/wt-worktree/wt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,68 @@ def init(ctx: Context, prefix: str, path_pattern: str):
@click.argument("name", required=False)
@click.option("-c", "--create", is_flag=True, help="Create worktree if it doesn't exist")
@click.option("-b", "--base", help="Base branch for new worktree")
@click.option("-B", "--branch", "checkout_branch", default=None,
help="Checkout an existing branch into a new worktree (use with -c)")
@click.option("-f", "--fetch", is_flag=True, help="Fetch branch from remote before checkout")
@click.option("-d", "--detached", is_flag=True, help="Create in detached HEAD state")
@click.option("--shell-helper", is_flag=True, hidden=True,
help="Internal flag for shell integration")
@pass_context
def switch(ctx: Context, name: Optional[str], create: bool, base: Optional[str],
detached: bool, shell_helper: bool):
"""Switch to a worktree, optionally creating it."""
checkout_branch: Optional[str], fetch: bool, detached: bool, shell_helper: bool):
"""Switch to a worktree, optionally creating it.

Use -B/--branch to checkout an existing branch into a new worktree:

\b
wt switch -c -B fix/login-bug
wt switch -c review -B fix/login-bug
wt switch -c -B fix/login-bug --fetch
"""
if not ctx.repo_root or not ctx.manager:
error("Not in a git repository.", EXIT_GIT_ERROR)
return

# Validate flag combinations
if checkout_branch and not create:
error("--branch/-B requires --create/-c", EXIT_INVALID_ARGS)
return

if checkout_branch and detached:
error("--branch/-B cannot be used with --detached/-d", EXIT_INVALID_ARGS)
return

if checkout_branch and base:
error("--branch/-B cannot be used with --base/-b", EXIT_INVALID_ARGS)
return

if fetch and not checkout_branch:
error("--fetch/-f requires --branch/-B", EXIT_INVALID_ARGS)
return

# Handle checkout of existing branch
if checkout_branch:
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)
Comment on lines +131 to +135

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.

# Record current worktree as previous
current_wt = ctx.manager.get_current_worktree()
if current_wt:
ctx.previous_worktree_file.write_text(str(current_wt["path"]))

if shell_helper:
print(wt_path)
else:
success(f"Checked out '{checkout_branch}' into worktree '{display_name}'")
info(f"Run: cd {wt_path}")

except git.GitError as e:
error(str(e), EXIT_GIT_ERROR)
return

# Handle special names
if name == "-":
# Switch to previous worktree
Expand Down
15 changes: 15 additions & 0 deletions tools/wt-worktree/wt/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,21 @@ def fetch_remote(remote: str = "origin", path: Optional[Path] = None):
run_git(["fetch", remote], cwd=path)


def fetch_branch(branch: str, remote: str = "origin", path: Optional[Path] = None):
"""
Fetch a specific branch from remote.

Args:
branch: Branch name to fetch
remote: Remote name
path: Repository path

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.


def enable_worktree_config(path: Path):
"""
Enable worktree-specific config support.
Expand Down
Loading