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:
@@ -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(),
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user