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 <noreply@anthropic.com>
This commit is contained in:
jack
2026-01-08 23:27:57 -08:00
committed by Steve Yegge
parent 9b2f4a7652
commit a91e6cd643
2 changed files with 118 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ package doctor
import ( import (
"bufio" "bufio"
"bytes"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -1089,6 +1090,103 @@ func hasBeadsData(beadsDir string) bool {
return false 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. // RigChecks returns all rig-level health checks.
func RigChecks() []Check { func RigChecks() []Check {
return []Check{ return []Check{
@@ -1096,6 +1194,7 @@ func RigChecks() []Check {
NewGitExcludeConfiguredCheck(), NewGitExcludeConfiguredCheck(),
NewHooksPathConfiguredCheck(), NewHooksPathConfiguredCheck(),
NewSparseCheckoutCheck(), NewSparseCheckoutCheck(),
NewBareRepoRefspecCheck(),
NewWitnessExistsCheck(), NewWitnessExistsCheck(),
NewRefineryExistsCheck(), NewRefineryExistsCheck(),
NewMayorCloneExistsCheck(), NewMayorCloneExistsCheck(),

View File

@@ -138,7 +138,8 @@ func (g *Git) CloneBare(url, dest string) error {
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return g.wrapError(err, stderr.String(), []string{"clone", "--bare", url}) 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 // configureHooksPath sets core.hooksPath to use the repo's .githooks directory
@@ -160,6 +161,21 @@ func configureHooksPath(repoPath string) error {
return nil 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. // CloneBareWithReference clones a bare repository using a local repo as an object reference.
func (g *Git) CloneBareWithReference(url, dest, reference string) error { func (g *Git) CloneBareWithReference(url, dest, reference string) error {
cmd := exec.Command("git", "clone", "--bare", "--reference-if-able", reference, url, dest) 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 { if err := cmd.Run(); err != nil {
return g.wrapError(err, stderr.String(), []string{"clone", "--bare", "--reference-if-able", url}) 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. // Checkout checks out the given ref.