Add checkout_branch command to switch to existing branches#19
Conversation
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
There was a problem hiding this comment.
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/--branchand-f/--fetchflags in theswitchcommand 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") |
There was a problem hiding this comment.
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.
| 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 |
| Raises: | ||
| GitError: If fetch fails | ||
| """ | ||
| run_git(["fetch", remote, branch], cwd=path) |
There was a problem hiding this comment.
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.
| 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) |
| 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." | ||
| ) |
There was a problem hiding this comment.
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.
| 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}") |
| 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." |
There was a problem hiding this comment.
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.
| f"Please remove it or use --name to choose a different name." | |
| f"Please remove it or choose a different name." |
| # 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"]) |
There was a problem hiding this comment.
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.
| # 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"] |
| except git.GitError: | ||
| pass |
There was a problem hiding this comment.
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.
| except git.GitError: | |
| pass | |
| except git.GitError as e: | |
| warning(f"Failed to fetch branch '{branch}' from 'origin': {e}") |
| 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) |
There was a problem hiding this comment.
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.
| 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) |
| 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 | ||
|
|
There was a problem hiding this comment.
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.
| 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 |
| # Create worktree using existing branch | ||
| try: | ||
| git.add_worktree(wt_path, branch, create_branch=False, |
There was a problem hiding this comment.
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.
| # 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, |
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 inWorktreeManager:nameparameterfetchparameterNew
_derive_name_from_branch()static method:fix/login-bug→login-bug)origin/prefix)Updated
list_worktrees()method:checkout_branch())checkout_branch()are findable by derived nameNew
fetch_branch()git helper:checkout_branch()when--fetchflag is providedCLI integration (
wt switch -c -B):-B/--branchoption toswitchcommand for checking out existing branches-f/--fetchoption to fetch before checkout-Brequires-c, cannot be used with-dor-b)Implementation Details
worktree.<path>.name) to persist the derived namecheckout_branch()method usesgit worktree addwithcreate_branch=Falseto avoid creating new brancheshttps://claude.ai/code/session_0189JpNR1U5W4vguS2XTetXg