diff --git a/pkg/git/checkout_branch.go b/pkg/git/checkout_branch.go index c6559e2..439c926 100644 --- a/pkg/git/checkout_branch.go +++ b/pkg/git/checkout_branch.go @@ -6,13 +6,25 @@ 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. 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() - if err != nil { - return fmt.Errorf("git checkout failed: %w (command: git checkout %s, output: %s)", - err, branch, string(output)) + if err == nil { + return nil + } + + // If that fails, try to checkout with tracking from origin + cmd = exec.Command("git", "checkout", "-b", branch, "origin/"+branch) + 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)) } return nil } diff --git a/pkg/worktree/create.go b/pkg/worktree/create.go index 7e9b20f..42948dc 100644 --- a/pkg/worktree/create.go +++ b/pkg/worktree/create.go @@ -4,6 +4,7 @@ package worktree import ( "fmt" + "github.com/lerenn/code-manager/pkg/git" "github.com/lerenn/code-manager/pkg/logger" ) @@ -26,11 +27,6 @@ func (w *realWorktree) Create(params CreateParams) error { return err } - // Ensure branch exists - if err := w.EnsureBranchExists(params.RepoPath, params.Branch); err != nil { - return err - } - // Create worktree directory if err := w.createWorktreeDirectory(params.WorktreePath); err != nil { return err @@ -40,22 +36,77 @@ func (w *realWorktree) Create(params CreateParams) error { if params.Detached { return w.createDetachedClone(params) } + + // Ensure branch exists (only for regular worktrees) + if err := w.EnsureBranchExists(params.RepoPath, params.Branch); err != nil { + return err + } + return w.createRegularWorktree(params) } // createDetachedClone creates a standalone clone for detached worktrees. func (w *realWorktree) createDetachedClone(params CreateParams) error { + // Check if branch exists locally - if so, clone from local path + // Otherwise, clone from remote URL + branchExists, err := w.git.BranchExists(params.RepoPath, params.Branch) + if err != nil { + return fmt.Errorf("failed to check if branch exists: %w", err) + } + + if branchExists { + return w.createDetachedCloneFromLocal(params) + } + return w.createDetachedCloneFromRemote(params) +} + +// createDetachedCloneFromLocal creates a detached clone from a local repository path. +func (w *realWorktree) createDetachedCloneFromLocal(params CreateParams) error { + w.logger.Logf("Branch exists locally, cloning from local path: %s", params.RepoPath) if err := w.git.CloneToPath(params.RepoPath, params.WorktreePath, params.Branch); err != nil { - // Clean up directory on failure - if cleanupErr := w.cleanupWorktreeDirectory(params.WorktreePath); cleanupErr != nil { - w.logger.Logf("Warning: failed to clean up worktree directory: %v", cleanupErr) - } + w.cleanupOnError(params.WorktreePath) return fmt.Errorf("failed to create detached clone: %w", err) } w.logger.Logf("✓ Detached clone created successfully for %s:%s", params.Remote, params.Branch) return nil } +// createDetachedCloneFromRemote creates a detached clone from a remote URL. +func (w *realWorktree) createDetachedCloneFromRemote(params CreateParams) error { + remoteURL, err := w.git.GetRemoteURL(params.RepoPath, params.Remote) + if err != nil { + return fmt.Errorf("failed to get remote URL for %s: %w", params.Remote, err) + } + + w.logger.Logf("Branch doesn't exist locally, cloning from remote URL: %s", remoteURL) + + // Clone from the remote URL (this will fetch all branches) + if err := w.git.Clone(git.CloneParams{ + RepoURL: remoteURL, + TargetPath: params.WorktreePath, + Recursive: true, + }); err != nil { + w.cleanupOnError(params.WorktreePath) + return fmt.Errorf("failed to clone from remote: %w", err) + } + + // Checkout the specific branch + if err := w.git.CheckoutBranch(params.WorktreePath, params.Branch); err != nil { + w.cleanupOnError(params.WorktreePath) + return fmt.Errorf("failed to checkout branch %s: %w", params.Branch, err) + } + + w.logger.Logf("✓ Detached clone created successfully for %s:%s", params.Remote, params.Branch) + return nil +} + +// cleanupOnError attempts to clean up the worktree directory on error. +func (w *realWorktree) cleanupOnError(worktreePath string) { + if cleanupErr := w.cleanupWorktreeDirectory(worktreePath); cleanupErr != nil { + w.logger.Logf("Warning: failed to clean up worktree directory: %v", cleanupErr) + } +} + // createRegularWorktree creates a regular Git worktree. func (w *realWorktree) createRegularWorktree(params CreateParams) error { if err := w.git.CreateWorktreeWithNoCheckout(params.RepoPath, params.WorktreePath, params.Branch); err != nil { diff --git a/pkg/worktree/create_test.go b/pkg/worktree/create_test.go index acd69fc..b6b61e5 100644 --- a/pkg/worktree/create_test.go +++ b/pkg/worktree/create_test.go @@ -206,11 +206,19 @@ func TestWorktree_Create_DetachedMode(t *testing.T) { mockFS.EXPECT().Exists(params.WorktreePath).Return(false, nil) mockStatus.EXPECT().GetWorktree(params.RepoURL, params.Branch).Return(nil, errors.New("not found")) mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil) - mockGit.EXPECT().CheckReferenceConflict(params.RepoPath, params.Branch).Return(nil) - mockGit.EXPECT().BranchExists(params.RepoPath, params.Branch).Return(true, nil) + // For detached mode, check if branch exists locally - if not, clone from remote + mockGit.EXPECT().BranchExists(params.RepoPath, params.Branch).Return(false, nil) mockFS.EXPECT().MkdirAll(params.WorktreePath, gomock.Any()).Return(nil) - // For detached mode, should call CloneToPath instead of CreateWorktreeWithNoCheckout - mockGit.EXPECT().CloneToPath(params.RepoPath, params.WorktreePath, params.Branch).Return(nil) + // For detached mode, should get remote URL, clone from remote, and checkout branch + remoteURL := "https://github.com/octocat/Hello-World.git" + mockGit.EXPECT().GetRemoteURL(params.RepoPath, params.Remote).Return(remoteURL, nil) + mockGit.EXPECT().Clone(gomock.Any()).DoAndReturn(func(cloneParams git.CloneParams) error { + assert.Equal(t, remoteURL, cloneParams.RepoURL) + assert.Equal(t, params.WorktreePath, cloneParams.TargetPath) + assert.True(t, cloneParams.Recursive) + return nil + }) + mockGit.EXPECT().CheckoutBranch(params.WorktreePath, params.Branch).Return(nil) err := worktree.Create(params) assert.NoError(t, err)