From a91e6cd643349240d66a588423ff922b760aef6c Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 8 Jan 2026 23:27:57 -0800 Subject: [PATCH] fix(git): configure refspec on bare clones for worktree compatibility Bare clones don't have remote.origin.fetch set by default, which breaks worktrees that need to fetch and see origin/* refs. This caused refinery to fail because origin/main never appeared after fetch. - Add configureRefspec() to set standard refspec on bare repos - Call from CloneBare() and CloneBareWithReference() - Add BareRepoRefspecCheck to doctor for existing rigs Closes #286 Co-Authored-By: Claude Opus 4.5 --- internal/doctor/rig_check.go | 99 ++++++++++++++++++++++++++++++++++++ internal/git/git.go | 21 +++++++- 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/internal/doctor/rig_check.go b/internal/doctor/rig_check.go index 17baf01c..36ee865d 100644 --- a/internal/doctor/rig_check.go +++ b/internal/doctor/rig_check.go @@ -2,6 +2,7 @@ package doctor import ( "bufio" + "bytes" "fmt" "os" "os/exec" @@ -1089,6 +1090,103 @@ func hasBeadsData(beadsDir string) bool { return false } +// BareRepoRefspecCheck verifies that the shared bare repo has the correct refspec configured. +// Without this, worktrees created from the bare repo cannot fetch and see origin/* refs. +// See: https://github.com/anthropics/gastown/issues/286 +type BareRepoRefspecCheck struct { + FixableCheck +} + +// NewBareRepoRefspecCheck creates a new bare repo refspec check. +func NewBareRepoRefspecCheck() *BareRepoRefspecCheck { + return &BareRepoRefspecCheck{ + FixableCheck: FixableCheck{ + BaseCheck: BaseCheck{ + CheckName: "bare-repo-refspec", + CheckDescription: "Verify bare repo has correct refspec for worktrees", + }, + }, + } +} + +// Run checks if the bare repo has the correct remote.origin.fetch refspec. +func (c *BareRepoRefspecCheck) Run(ctx *CheckContext) *CheckResult { + if ctx.RigName == "" { + return &CheckResult{ + Name: c.Name(), + Status: StatusOK, + Message: "No rig specified, skipping bare repo check", + } + } + + bareRepoPath := filepath.Join(ctx.RigPath(), ".repo.git") + if _, err := os.Stat(bareRepoPath); os.IsNotExist(err) { + // No bare repo - might be using a different architecture + return &CheckResult{ + Name: c.Name(), + Status: StatusOK, + Message: "No shared bare repo found (using individual clones)", + } + } + + // Check the refspec + cmd := exec.Command("git", "-C", bareRepoPath, "config", "--get", "remote.origin.fetch") + out, err := cmd.Output() + if err != nil { + return &CheckResult{ + Name: c.Name(), + Status: StatusError, + Message: "Bare repo missing remote.origin.fetch refspec", + Details: []string{ + "Worktrees cannot fetch or see origin/* refs without this config", + "This breaks refinery merge operations and causes stale origin/main", + }, + FixHint: "Run 'gt doctor --fix' to configure the refspec", + } + } + + refspec := strings.TrimSpace(string(out)) + expectedRefspec := "+refs/heads/*:refs/remotes/origin/*" + if refspec != expectedRefspec { + return &CheckResult{ + Name: c.Name(), + Status: StatusWarning, + Message: "Bare repo has non-standard refspec", + Details: []string{ + fmt.Sprintf("Current: %s", refspec), + fmt.Sprintf("Expected: %s", expectedRefspec), + }, + FixHint: "Run 'gt doctor --fix' to update the refspec", + } + } + + return &CheckResult{ + Name: c.Name(), + Status: StatusOK, + Message: "Bare repo refspec configured correctly", + } +} + +// Fix sets the correct refspec on the bare repo. +func (c *BareRepoRefspecCheck) Fix(ctx *CheckContext) error { + if ctx.RigName == "" { + return nil + } + + bareRepoPath := filepath.Join(ctx.RigPath(), ".repo.git") + if _, err := os.Stat(bareRepoPath); os.IsNotExist(err) { + return nil // No bare repo to fix + } + + cmd := exec.Command("git", "-C", bareRepoPath, "config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*") + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("setting refspec: %s", strings.TrimSpace(stderr.String())) + } + return nil +} + // RigChecks returns all rig-level health checks. func RigChecks() []Check { return []Check{ @@ -1096,6 +1194,7 @@ func RigChecks() []Check { NewGitExcludeConfiguredCheck(), NewHooksPathConfiguredCheck(), NewSparseCheckoutCheck(), + NewBareRepoRefspecCheck(), NewWitnessExistsCheck(), NewRefineryExistsCheck(), NewMayorCloneExistsCheck(), diff --git a/internal/git/git.go b/internal/git/git.go index 544918e7..567d1466 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -138,7 +138,8 @@ func (g *Git) CloneBare(url, dest string) error { if err := cmd.Run(); err != nil { return g.wrapError(err, stderr.String(), []string{"clone", "--bare", url}) } - return nil + // Configure refspec so worktrees can fetch and see origin/* refs + return configureRefspec(dest) } // configureHooksPath sets core.hooksPath to use the repo's .githooks directory @@ -160,6 +161,21 @@ func configureHooksPath(repoPath string) error { return nil } +// configureRefspec sets remote.origin.fetch to the standard refspec for bare repos. +// Bare clones don't have this set by default, which breaks worktrees that need to +// fetch and see origin/* refs. Without this, `git fetch` only updates FETCH_HEAD +// and origin/main never appears in refs/remotes/origin/main. +// See: https://github.com/anthropics/gastown/issues/286 +func configureRefspec(repoPath string) error { + cmd := exec.Command("git", "-C", repoPath, "config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*") + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("configuring refspec: %s", strings.TrimSpace(stderr.String())) + } + return nil +} + // CloneBareWithReference clones a bare repository using a local repo as an object reference. func (g *Git) CloneBareWithReference(url, dest, reference string) error { cmd := exec.Command("git", "clone", "--bare", "--reference-if-able", reference, url, dest) @@ -168,7 +184,8 @@ func (g *Git) CloneBareWithReference(url, dest, reference string) error { if err := cmd.Run(); err != nil { return g.wrapError(err, stderr.String(), []string{"clone", "--bare", "--reference-if-able", url}) } - return nil + // Configure refspec so worktrees can fetch and see origin/* refs + return configureRefspec(dest) } // Checkout checks out the given ref.