fix(client): resolve default branch from the remote advertisement

A cached bare clone's HEAD points at refs/heads/<branch>, which is written
once at clone time and never updated again — fetch only touches
refs/remotes/*. Syncs without --ref therefore resolved the frozen
clone-time revision forever: the literal "cached version of the repo"
symptom.

Resolve the default branch via remote.List instead: the advertised HEAD
(plain hash or symref target) is the only source of truth. Verified
against the production Forgejo instance; e2e test fails on the pre-fix
binary and passes now.
This commit is contained in:
m3ta-chiron committed 2026-10-02 12:34:41 +02:00
1 parent addca3dd74
commit 2017329015
3 files changed
+92 -2

No files matched your search

+46 -1
View File
@@ -31,7 +31,15 @@ func Pull(cacheDir, url, ref, token, tokenUser string) (*PullResult, error) {
if err != nil {
return nil, err
}
hash, err := resolveRemoteRef(repo, ref)
var hash plumbing.Hash
if ref == "" {
// The cached bare clone's HEAD is frozen at clone time (fetch never
// updates refs/heads/*), so the default branch must be asked from the
// remote directly — never resolved through local refs.
hash, err = resolveDefaultBranch(repo, auth)
} else {
hash, err = resolveRemoteRef(repo, ref)
}
if err != nil {
return nil, fmt.Errorf("resolving %q in %s: %w", refLabel(ref), gitsource.RedactURL(url), err)
}
@@ -86,6 +94,43 @@ func openOrCreate(cacheDir, url, ref string, auth gittransport.AuthMethod) (*git
return repo, nil
}
// resolveDefaultBranch asks the remote for its advertised HEAD — the only
// source of truth for the default branch tip. HEAD may be advertised as a
// plain hash or as a symbolic reference to one of the advertised refs; both
// are handled. Local refs are never consulted: the cached bare clone's HEAD
// is frozen at clone time because fetch updates only refs/remotes/*.
func resolveDefaultBranch(repo *git.Repository, auth gittransport.AuthMethod) (plumbing.Hash, error) {
remote, err := repo.Remote(git.DefaultRemoteName)
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("reading remote: %w", err)
}
refs, err := remote.List(&git.ListOptions{Auth: auth})
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("listing remote refs: %w", err)
}
hashes := map[string]plumbing.Hash{}
var head *plumbing.Reference
for _, ref := range refs {
if ref.Name() == plumbing.HEAD {
head = ref
continue
}
if ref.Type() == plumbing.HashReference {
hashes[ref.Name().String()] = ref.Hash()
}
}
if head == nil {
return plumbing.ZeroHash, errors.New("remote advertises no HEAD")
}
if head.Type() == plumbing.HashReference {
return head.Hash(), nil
}
if h, ok := hashes[head.Target().String()]; ok {
return h, nil
}
return plumbing.ZeroHash, fmt.Errorf("advertised HEAD points at %s, which was not advertised", head.Target())
}
// repointRemote aligns the cached origin with the configured URL so a fleet
// re-point takes effect on the next sync.
func repointRemote(repo *git.Repository, url string) error {
+42
View File
@@ -0,0 +1,42 @@
package e2e
import (
"os"
"path/filepath"
"testing"
)
// TestSyncDefaultBranchStaysFresh pins the fix for the stale-cache bug: with
// no ref configured, sync must resolve the default branch from the remote
// advertisement, never from the cached bare clone's frozen refs/heads ref.
func TestSyncDefaultBranchStaysFresh(t *testing.T) {
h := newHarness(t)
env := newSyncEnv(t, h)
buildWorkRepo(t, h, env.workRepo)
// config without "ref": the default-branch resolution path
env.config = filepath.Join(env.configDir, "config.json")
os.WriteFile(env.config, []byte(`{"repo_url": "`+env.workRepo+`"}`), 0o600)
if _, err := env.run(t); err != nil {
t.Fatal(err)
}
// upstream change on the default branch
p := filepath.Join(env.workRepo, "commands/deploy.md")
os.WriteFile(p, []byte("---\nname: Deploy\ndescription: updated\n---\nfresh body\n"), 0o644)
h.git(t, env.workRepo, "add", "-A")
h.git(t, env.workRepo, "commit", "-m", "update command on default branch")
out, err := env.run(t)
if err != nil {
t.Fatalf("second sync (default branch) failed: %v\n%s", err, out)
}
if !contains(out, "updated: 1") {
t.Errorf("default-branch sync must pick up the upstream change:\n%s", out)
}
deployed := readFile(t, filepath.Join(env.home, ".config/opencode/commands/deploy.md"))
if !contains(deployed, "fresh body") {
t.Error("default-branch sync must deploy the fresh content, not the clone-time revision")
}
}