From e98d48cbf23afd8581f0a60b11c078311c7a9c63 Mon Sep 17 00:00:00 2001 From: Louis FRADIN Date: Mon, 12 Jan 2026 11:17:59 -0500 Subject: [PATCH 1/2] fix(worktree): handle branch checkout when branch doesn't exist on remote Fix branch checkout failure when creating worktrees for branches that don't exist on the remote repository. Previously, the code would attempt to create a branch from origin/branch without checking if that remote branch exists, resulting in errors like: fatal: 'origin/deploy-v0-0-67' is not a commit and a branch 'deploy-v0-0-67' cannot be created from it Changes: - Modified CheckoutBranch to check if origin/branch exists before attempting to create from it - Added fallback logic: fetch from origin, then check again - Final fallback: create branch from HEAD if remote branch doesn't exist - Added integration tests for various checkout scenarios - Added E2E tests to reproduce and verify fix for the original bug scenario Fixes the issue where worktree creation fails for branches like deploy-v0-0-67 that don't exist on the remote but need to be created locally. --- pkg/git/checkout_branch.go | 75 ++++++++++++++++-- pkg/git/checkout_branch_test.go | 102 ++++++++++++++++++++++++- test/worktree_create_test.go | 131 ++++++++++++++++++++++++++++++++ 3 files changed, 296 insertions(+), 12 deletions(-) diff --git a/pkg/git/checkout_branch.go b/pkg/git/checkout_branch.go index 439c926..f2e0587 100644 --- a/pkg/git/checkout_branch.go +++ b/pkg/git/checkout_branch.go @@ -7,6 +7,7 @@ import ( // CheckoutBranch checks out a branch in the specified worktree. // If the branch doesn't exist locally, it will try to checkout from origin/branch. +// If origin/branch doesn't exist, it will create the branch from HEAD. func (g *realGit) CheckoutBranch(worktreePath, branch string) error { // First try to checkout the branch directly cmd := exec.Command("git", "checkout", branch) @@ -16,15 +17,73 @@ func (g *realGit) CheckoutBranch(worktreePath, branch string) error { return nil } - // If that fails, try to checkout with tracking from origin - cmd = exec.Command("git", "checkout", "-b", branch, "origin/"+branch) + // If that fails, check if the branch exists locally + branchExists, branchErr := g.BranchExists(worktreePath, branch) + if branchErr == nil && branchExists { + // Branch exists locally, try checkout again (might need ref refresh) + cmd = exec.Command("git", "checkout", branch) + cmd.Dir = worktreePath + _, err2 := cmd.CombinedOutput() + if err2 == nil { + return nil + } + // If it still fails, continue to fallback logic + } + + // Check if origin/branch exists on remote + remoteBranchExists, remoteErr := g.BranchExistsOnRemote(BranchExistsOnRemoteParams{ + RepoPath: worktreePath, + RemoteName: "origin", + Branch: branch, + }) + + var originOutput []byte + if remoteErr == nil && remoteBranchExists { + // origin/branch exists, create local branch tracking it + cmd = exec.Command("git", "checkout", "-b", branch, "origin/"+branch) + cmd.Dir = worktreePath + var err2 error + originOutput, err2 = cmd.CombinedOutput() + if err2 == nil { + return nil + } + // If that fails, continue to final fallback + } + + // origin/branch doesn't exist or checking failed, fetch to ensure we have latest refs + if fetchErr := g.FetchRemote(worktreePath, "origin"); fetchErr == nil { + // After fetch, check again if origin/branch exists + remoteBranchExists, remoteErr = g.BranchExistsOnRemote(BranchExistsOnRemoteParams{ + RepoPath: worktreePath, + RemoteName: "origin", + Branch: branch, + }) + if remoteErr == nil && remoteBranchExists { + // origin/branch exists after fetch, create local branch tracking it + cmd = exec.Command("git", "checkout", "-b", branch, "origin/"+branch) + cmd.Dir = worktreePath + var err2 error + originOutput, err2 = cmd.CombinedOutput() + if err2 == nil { + return nil + } + } + } + + // Final fallback: create branch from HEAD + cmd = exec.Command("git", "checkout", "-b", branch, "HEAD") cmd.Dir = worktreePath - output2, err2 := cmd.CombinedOutput() - if err2 != nil { - // Return the original error if both attempts fail - return fmt.Errorf( - "git checkout failed: %w (command: git checkout %s, output: %s; fallback: git checkout -b %s origin/%s, output: %s)", - err, branch, string(output), branch, branch, string(output2)) + headOutput, err3 := cmd.CombinedOutput() + if err3 != nil { + // Return comprehensive error with all attempted operations + errorMsg := fmt.Sprintf( + "git checkout failed: %v (command: git checkout %s, output: %s", + err, branch, string(output)) + if len(originOutput) > 0 { + errorMsg += fmt.Sprintf("; fallback: git checkout -b %s origin/%s, output: %s", branch, branch, string(originOutput)) + } + errorMsg += fmt.Sprintf("; final fallback: git checkout -b %s HEAD, output: %s)", branch, string(headOutput)) + return fmt.Errorf("%s", errorMsg) } return nil } diff --git a/pkg/git/checkout_branch_test.go b/pkg/git/checkout_branch_test.go index cb32281..62cecc2 100644 --- a/pkg/git/checkout_branch_test.go +++ b/pkg/git/checkout_branch_test.go @@ -35,10 +35,20 @@ func TestGit_CheckoutBranch(t *testing.T) { t.Fatalf("Expected no error checking out branch: %v", err) } - // Test checking out non-existent branch - err = git.CheckoutBranch(testWorktreePath, "non-existent-branch") - if err == nil { - t.Error("Expected error when checking out non-existent branch") + // Test checking out non-existent branch (should create from HEAD) + nonExistentBranch := "non-existent-branch-" + strings.ReplaceAll(t.Name(), "/", "-") + err = git.CheckoutBranch(testWorktreePath, nonExistentBranch) + if err != nil { + t.Errorf("Expected no error when checking out non-existent branch (should create from HEAD): %v", err) + } + + // Verify the branch was created + exists, err := git.BranchExists(testWorktreePath, nonExistentBranch) + if err != nil { + t.Errorf("Expected no error checking created branch existence: %v", err) + } + if !exists { + t.Errorf("Expected branch %s to exist after checkout", nonExistentBranch) } // Test in non-existent directory @@ -47,3 +57,87 @@ func TestGit_CheckoutBranch(t *testing.T) { t.Error("Expected error for non-existent directory") } } + +// TestGit_CheckoutBranch_FromHead tests checkout when branch doesn't exist locally or on remote +func TestGit_CheckoutBranch_FromHead(t *testing.T) { + git := NewGit() + _, cleanup := SetupTestRepo(t) + defer cleanup() + + // Create a unique branch for worktree creation to avoid conflicts + worktreeBaseBranch := "worktree-base-" + strings.ReplaceAll(t.Name(), "/", "-") + err := git.CreateBranch(".", worktreeBaseBranch) + if err != nil { + t.Fatalf("Expected no error creating base branch: %v", err) + } + + // Create a worktree without checkout + testWorktreePath := "test-checkout-head-worktree-" + strings.ReplaceAll(t.Name(), "/", "-") + testBranchName := "test-branch-from-head-" + strings.ReplaceAll(t.Name(), "/", "-") + + err = git.CreateWorktreeWithNoCheckout(".", testWorktreePath, worktreeBaseBranch) + if err != nil { + t.Fatalf("Expected no error creating worktree without checkout: %v", err) + } + defer os.RemoveAll(testWorktreePath) + + // Checkout a branch that doesn't exist (should create from HEAD) + err = git.CheckoutBranch(testWorktreePath, testBranchName) + if err != nil { + t.Fatalf("Expected no error checking out non-existent branch (should create from HEAD): %v", err) + } + + // Verify the branch was created + exists, err := git.BranchExists(testWorktreePath, testBranchName) + if err != nil { + t.Errorf("Expected no error checking created branch existence: %v", err) + } + if !exists { + t.Errorf("Expected branch %s to exist after checkout", testBranchName) + } +} + +// TestGit_CheckoutBranch_LocalBranchExists tests checkout when branch exists locally +func TestGit_CheckoutBranch_LocalBranchExists(t *testing.T) { + git := NewGit() + _, cleanup := SetupTestRepo(t) + defer cleanup() + + // Create a unique branch for worktree creation to avoid conflicts + worktreeBaseBranch := "worktree-base-local-" + strings.ReplaceAll(t.Name(), "/", "-") + err := git.CreateBranch(".", worktreeBaseBranch) + if err != nil { + t.Fatalf("Expected no error creating base branch: %v", err) + } + + // Create a test branch in main repo + testBranchName := "test-local-branch-" + strings.ReplaceAll(t.Name(), "/", "-") + err = git.CreateBranch(".", testBranchName) + if err != nil { + t.Fatalf("Expected no error creating branch: %v", err) + } + + // Create a worktree without checkout + testWorktreePath := "test-checkout-local-worktree-" + strings.ReplaceAll(t.Name(), "/", "-") + + err = git.CreateWorktreeWithNoCheckout(".", testWorktreePath, worktreeBaseBranch) + if err != nil { + t.Fatalf("Expected no error creating worktree without checkout: %v", err) + } + defer os.RemoveAll(testWorktreePath) + + // Checkout the branch that exists locally + err = git.CheckoutBranch(testWorktreePath, testBranchName) + if err != nil { + t.Fatalf("Expected no error checking out existing local branch: %v", err) + } + + // Verify we're on the correct branch + currentWorktreeBranch, err := git.GetCurrentBranch(testWorktreePath) + if err != nil { + t.Errorf("Expected no error getting current branch in worktree: %v", err) + } + if currentWorktreeBranch != testBranchName { + t.Errorf("Expected to be on branch %s, got %s", testBranchName, currentWorktreeBranch) + } +} diff --git a/test/worktree_create_test.go b/test/worktree_create_test.go index f0cc26e..69aaee7 100644 --- a/test/worktree_create_test.go +++ b/test/worktree_create_test.go @@ -1027,3 +1027,134 @@ func TestCreateWorktreeWorkspaceMode_RepositoryNames(t *testing.T) { assert.Contains(t, workspaceContentStr, `"name": "Hello-World"`, "Workspace file should contain Hello-World name") assert.Contains(t, workspaceContentStr, `"name": "Spoon-Knife"`, "Workspace file should contain Spoon-Knife name") } + +// TestCreateWorktreeBranchNotOnRemote_RepoMode tests creating a worktree for a branch that doesn't exist on the remote. +// This test reproduces and verifies the fix for the bug where checkout fails when trying to create from origin/branch +// that doesn't exist. The fix should create the branch from HEAD instead. +func TestCreateWorktreeBranchNotOnRemote_RepoMode(t *testing.T) { + setup := setupTestEnvironment(t) + defer cleanupTestEnvironment(t, setup) + + // Create a test Git repository + createTestGitRepo(t, setup.RepoPath) + + // Create a worktree for a branch that doesn't exist on the remote (like deploy-v0-0-67 from the bug report) + branchName := "deploy-v0-0-67" + err := createWorktree(t, setup, branchName) + require.NoError(t, err, "Command should succeed even though branch doesn't exist on remote") + + // Verify the status.yaml file was created and updated + status := readStatusFile(t, setup.StatusPath) + require.NotNil(t, status.Repositories, "Status file should have repositories section") + require.Len(t, status.Repositories, 1, "Should have one repository entry") + + // Get the first repository from the map + var repoURL string + var repo Repository + for url, r := range status.Repositories { + repoURL = url + repo = r + break + } + + assert.NotEmpty(t, repoURL, "Repository URL should be set") + assert.NotEmpty(t, repo.Path, "Repository path should be set") + + // Check that the repository has the worktree + require.True(t, len(repo.Worktrees) > 0, "Repository should have at least one worktree") + + // Check that the worktree for our branch exists + var foundWorktree bool + for _, worktree := range repo.Worktrees { + if worktree.Branch == branchName { + foundWorktree = true + break + } + } + assert.True(t, foundWorktree, "Should have worktree for %s", branchName) + + // Verify the worktree exists in the .cm directory + assertWorktreeExists(t, setup, branchName) + + // Verify the worktree is properly linked in the original repository + assertWorktreeInRepo(t, setup, branchName) + + // Verify the worktree is on the correct branch + worktreePath := findWorktreePath(t, setup, branchName) + require.NotEmpty(t, worktreePath, "Worktree path should be found") + + cmd := exec.Command("git", "branch", "--show-current") + cmd.Dir = worktreePath + output, err := cmd.Output() + require.NoError(t, err) + assert.Equal(t, branchName, strings.TrimSpace(string(output)), "Worktree should be on the correct branch") +} + +// TestCreateWorktreeBranchNotOnRemote_WorkspaceMode tests creating worktrees from workspace for branches that don't exist on the remote. +// This test verifies the fix works in workspace mode as well. +func TestCreateWorktreeBranchNotOnRemote_WorkspaceMode(t *testing.T) { + setup := setupTestEnvironment(t) + defer cleanupTestEnvironment(t, setup) + + // Create test repositories + repo1Path := filepath.Join(setup.CmPath, "Hello-World") + repo2Path := filepath.Join(setup.CmPath, "Spoon-Knife") + + err := os.MkdirAll(repo1Path, 0755) + require.NoError(t, err) + err = os.MkdirAll(repo2Path, 0755) + require.NoError(t, err) + + // Initialize Git repositories + createTestGitRepo(t, repo1Path) + createTestGitRepo(t, repo2Path) + + // Create CM instance + cmInstance, err := codemanager.NewCodeManager(codemanager.NewCodeManagerParams{ + Dependencies: createE2EDependencies(setup.ConfigPath). + WithConfig(config.NewManager(setup.ConfigPath)), + }) + require.NoError(t, err) + + // Create workspace + workspaceName := "test-workspace-branch-not-on-remote" + workspaceParams := codemanager.CreateWorkspaceParams{ + WorkspaceName: workspaceName, + Repositories: []string{repo1Path, repo2Path}, + } + err = cmInstance.CreateWorkspace(workspaceParams) + require.NoError(t, err) + + // Create worktrees from workspace for a branch that doesn't exist on remote + branchName := "deploy-v0-0-67" + err = cmInstance.CreateWorkTree(branchName, codemanager.CreateWorkTreeOpts{ + WorkspaceName: workspaceName, + }) + require.NoError(t, err, "Should succeed even though branch doesn't exist on remote") + + // Verify workspace file was created + workspaceFilePath := filepath.Join(setup.CmPath, "workspaces", workspaceName, branchName+".code-workspace") + assert.FileExists(t, workspaceFilePath, "Workspace file should be created") + + // Verify workspace file content + content, err := os.ReadFile(workspaceFilePath) + require.NoError(t, err) + assert.Contains(t, string(content), "github.com/octocat/Hello-World") + assert.Contains(t, string(content), "github.com/octocat/Spoon-Knife") + assert.Contains(t, string(content), "folders") + + // Verify worktrees were created for both repositories + status := readStatusFile(t, setup.StatusPath) + require.NotNil(t, status.Repositories, "Status file should have repositories section") + + // Count worktrees for the branch + worktreeCount := 0 + for _, repo := range status.Repositories { + for _, worktree := range repo.Worktrees { + if worktree.Branch == branchName { + worktreeCount++ + } + } + } + assert.Equal(t, 2, worktreeCount, "Should have worktrees for both repositories") +} From 92534ac98e529468092f7fd98eed4cb07217d0f1 Mon Sep 17 00:00:00 2001 From: Louis FRADIN Date: Mon, 12 Jan 2026 11:22:46 -0500 Subject: [PATCH 2/2] refactor(git): reduce cyclomatic complexity in CheckoutBranch Extract helper functions to reduce complexity from 14 to below 10: - tryCheckoutBranch: attempts direct branch checkout - tryCheckoutFromOrigin: attempts checkout from origin/branch - createBranchFromHead: creates branch from HEAD This addresses the golangci-lint cyclop linter requirement. --- pkg/git/checkout_branch.go | 103 ++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 48 deletions(-) diff --git a/pkg/git/checkout_branch.go b/pkg/git/checkout_branch.go index f2e0587..4825162 100644 --- a/pkg/git/checkout_branch.go +++ b/pkg/git/checkout_branch.go @@ -10,70 +10,34 @@ import ( // If origin/branch doesn't exist, it will create the branch from HEAD. func (g *realGit) CheckoutBranch(worktreePath, branch string) error { // First try to checkout the branch directly - cmd := exec.Command("git", "checkout", branch) - cmd.Dir = worktreePath - output, err := cmd.CombinedOutput() + output, err := g.tryCheckoutBranch(worktreePath, branch) if err == nil { return nil } // If that fails, check if the branch exists locally - branchExists, branchErr := g.BranchExists(worktreePath, branch) - if branchErr == nil && branchExists { + if branchExists, branchErr := g.BranchExists(worktreePath, branch); branchErr == nil && branchExists { // Branch exists locally, try checkout again (might need ref refresh) - cmd = exec.Command("git", "checkout", branch) - cmd.Dir = worktreePath - _, err2 := cmd.CombinedOutput() - if err2 == nil { + if _, err2 := g.tryCheckoutBranch(worktreePath, branch); err2 == nil { return nil } - // If it still fails, continue to fallback logic } - // Check if origin/branch exists on remote - remoteBranchExists, remoteErr := g.BranchExistsOnRemote(BranchExistsOnRemoteParams{ - RepoPath: worktreePath, - RemoteName: "origin", - Branch: branch, - }) - - var originOutput []byte - if remoteErr == nil && remoteBranchExists { - // origin/branch exists, create local branch tracking it - cmd = exec.Command("git", "checkout", "-b", branch, "origin/"+branch) - cmd.Dir = worktreePath - var err2 error - originOutput, err2 = cmd.CombinedOutput() - if err2 == nil { - return nil - } - // If that fails, continue to final fallback + // Try to checkout from origin/branch + originOutput, err := g.tryCheckoutFromOrigin(worktreePath, branch) + if err == nil { + return nil } - // origin/branch doesn't exist or checking failed, fetch to ensure we have latest refs - if fetchErr := g.FetchRemote(worktreePath, "origin"); fetchErr == nil { - // After fetch, check again if origin/branch exists - remoteBranchExists, remoteErr = g.BranchExistsOnRemote(BranchExistsOnRemoteParams{ - RepoPath: worktreePath, - RemoteName: "origin", - Branch: branch, - }) - if remoteErr == nil && remoteBranchExists { - // origin/branch exists after fetch, create local branch tracking it - cmd = exec.Command("git", "checkout", "-b", branch, "origin/"+branch) - cmd.Dir = worktreePath - var err2 error - originOutput, err2 = cmd.CombinedOutput() - if err2 == nil { - return nil - } + // Fetch and try again + if g.FetchRemote(worktreePath, "origin") == nil { + if originOutput, err = g.tryCheckoutFromOrigin(worktreePath, branch); err == nil { + return nil } } // Final fallback: create branch from HEAD - cmd = exec.Command("git", "checkout", "-b", branch, "HEAD") - cmd.Dir = worktreePath - headOutput, err3 := cmd.CombinedOutput() + headOutput, err3 := g.createBranchFromHead(worktreePath, branch) if err3 != nil { // Return comprehensive error with all attempted operations errorMsg := fmt.Sprintf( @@ -87,3 +51,46 @@ func (g *realGit) CheckoutBranch(worktreePath, branch string) error { } return nil } + +// tryCheckoutBranch attempts to checkout a branch directly. +func (g *realGit) tryCheckoutBranch(worktreePath, branch string) ([]byte, error) { + cmd := exec.Command("git", "checkout", branch) + cmd.Dir = worktreePath + output, err := cmd.CombinedOutput() + if err != nil { + return output, err + } + return output, nil +} + +// tryCheckoutFromOrigin attempts to checkout a branch from origin/branch if it exists. +func (g *realGit) tryCheckoutFromOrigin(worktreePath, branch string) ([]byte, error) { + remoteBranchExists, remoteErr := g.BranchExistsOnRemote(BranchExistsOnRemoteParams{ + RepoPath: worktreePath, + RemoteName: "origin", + Branch: branch, + }) + if remoteErr != nil || !remoteBranchExists { + return nil, fmt.Errorf("remote branch origin/%s does not exist", branch) + } + + // origin/branch exists, create local branch tracking it + cmd := exec.Command("git", "checkout", "-b", branch, "origin/"+branch) + cmd.Dir = worktreePath + output, err := cmd.CombinedOutput() + if err != nil { + return output, err + } + return output, nil +} + +// createBranchFromHead creates a new branch from HEAD. +func (g *realGit) createBranchFromHead(worktreePath, branch string) ([]byte, error) { + cmd := exec.Command("git", "checkout", "-b", branch, "HEAD") + cmd.Dir = worktreePath + output, err := cmd.CombinedOutput() + if err != nil { + return output, err + } + return output, nil +}