diff --git a/server/internal/daemon/repocache/cache.go b/server/internal/daemon/repocache/cache.go index d747fa060..ec3a91830 100644 --- a/server/internal/daemon/repocache/cache.go +++ b/server/internal/daemon/repocache/cache.go @@ -399,19 +399,23 @@ func (c *Cache) CreateWorktree(params WorktreeParams) (*WorktreeResult, error) { } // Determine the ref to base the worktree on. By default this is the remote's - // default branch. Callers may request a specific branch, tag, or commit so - // review/QA agents can inspect the exact revision without trying to mutate - // the daemon-owned worktree metadata themselves. + // default branch (resolved internally via getRemoteDefaultBranch, which walks + // origin/HEAD → origin/main, origin/master → bare-HEAD hint into origin/ + // → single-entry scan of origin/* → bare HEAD when origin/* is empty). + // Callers may request a specific branch, tag, or commit so review/QA agents + // can inspect the exact revision without trying to mutate the daemon-owned + // worktree metadata themselves. baseRef, err := resolveBaseRef(barePath, params.Ref) if err != nil { return nil, err } - // getRemoteDefaultBranch walks origin/HEAD → origin/main, origin/master → - // bare-HEAD hint into origin/ → single-entry scan of origin/* → bare - // HEAD (only if origin/* is empty). Reaching "" here means the cache is in a - // state we refuse to guess from (no origin/HEAD, no main/master, bare HEAD - // doesn't match any origin/* entry, and origin/* has multiple candidates). + // Empty here means params.Ref was unset and getRemoteDefaultBranch couldn't + // resolve a default — the cache is in a state we refuse to guess from (no + // origin/HEAD, no main/master, bare HEAD doesn't match any origin/* entry, + // and origin/* has multiple candidates). The requested-ref path returns an + // explicit error before reaching here, so this branch only fires for the + // default-branch case. if baseRef == "" { return nil, fmt.Errorf("cannot resolve default branch for %s: bare cache at %s has no usable refs (origin/* is empty or ambiguous and bare HEAD has no match). The cache may be corrupted; delete it and retry", params.RepoURL, barePath) } diff --git a/server/internal/daemon/repocache/cache_test.go b/server/internal/daemon/repocache/cache_test.go index a910a4fe7..971ef8b30 100644 --- a/server/internal/daemon/repocache/cache_test.go +++ b/server/internal/daemon/repocache/cache_test.go @@ -591,6 +591,38 @@ func TestCreateWorktreeWithRequestedCommitRef(t *testing.T) { } } +func TestCreateWorktreeWithRequestedTagRef(t *testing.T) { + t.Parallel() + sourceRepo := createTestRepo(t) + taggedCommit := gitHead(t, sourceRepo) + runGitAuthored(t, sourceRepo, "tag", "v1") + // Advance the default branch past the tag so worktree HEAD == taggedCommit + // can only be true if the tag was actually resolved (vs falling back to + // the default branch tip). + addEmptyCommit(t, sourceRepo, "post-tag commit") + + cache := New(t.TempDir(), testLogger()) + if err := cache.Sync("ws-1", []RepoInfo{{URL: sourceRepo}}); err != nil { + t.Fatalf("sync failed: %v", err) + } + + result, err := cache.CreateWorktree(WorktreeParams{ + WorkspaceID: "ws-1", + RepoURL: sourceRepo, + WorkDir: t.TempDir(), + Ref: "v1", + AgentName: "Reviewer", + TaskID: "tag-task-id", + }) + if err != nil { + t.Fatalf("CreateWorktree failed: %v", err) + } + + if got := gitHead(t, result.Path); got != taggedCommit { + t.Fatalf("worktree HEAD = %s, want tagged commit %s", got, taggedCommit) + } +} + func TestCreateWorktreeWithUnknownRequestedRef(t *testing.T) { t.Parallel() sourceRepo := createTestRepo(t)