Skip to content

Commit 7f0cfee

Browse files
authored
reworked ref resolution so it works especially for tags/releases/branches (#423)
1 parent 5ea29c5 commit 7f0cfee

2 files changed

Lines changed: 297 additions & 36 deletions

File tree

providers/gitops/gitops.go

Lines changed: 159 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/go-git/go-git/v6/plumbing"
2020
"github.com/go-git/go-git/v6/plumbing/object"
2121
"github.com/go-git/go-git/v6/plumbing/protocol/packp"
22+
"github.com/go-git/go-git/v6/plumbing/storer"
2223
"github.com/go-git/go-git/v6/plumbing/transport"
2324
gogithttp "github.com/go-git/go-git/v6/plumbing/transport/http"
2425
"github.com/go-git/go-git/v6/storage/memory"
@@ -43,6 +44,24 @@ func init() {
4344
}
4445

4546
var ErrRepoNotReachable = errors.New("repo or ref not reachable")
47+
var ErrRemoteRefNotFound = errors.New("remote ref not found")
48+
49+
type resolvedRefKind int
50+
51+
const (
52+
resolvedRefHead resolvedRefKind = iota
53+
resolvedRefBranch
54+
resolvedRefTag
55+
resolvedRefCommit
56+
)
57+
58+
type resolvedRef struct {
59+
kind resolvedRefKind
60+
input string
61+
fullRef string
62+
localRef plumbing.ReferenceName
63+
commitHash plumbing.Hash
64+
}
4665

4766
// inMemRepo holds an in-memory git repository.
4867
type inMemRepo struct {
@@ -105,6 +124,10 @@ func (g *GitClient) Clone(ctx context.Context, clonePath string, url string, tok
105124
// Build refspec and fetch. For HEAD, try common defaults first to avoid
106125
// an extra ls-remote round-trip.
107126
var defaultBranch string
127+
resolved := &resolvedRef{
128+
kind: resolvedRefHead,
129+
input: ref,
130+
}
108131
fetchOpts := &gogit.FetchOptions{
109132
RemoteName: "origin",
110133
Depth: 1,
@@ -123,6 +146,7 @@ func (g *GitClient) Clone(ctx context.Context, clonePath string, url string, tok
123146
err = repo.FetchContext(ctx, fetchOpts)
124147
if err == nil {
125148
defaultBranch = branch
149+
resolved.localRef = plumbing.ReferenceName("refs/remotes/origin/" + branch)
126150
break
127151
}
128152
if classifyFetchError(err) != nil && !strings.Contains(err.Error(), "couldn't find remote ref") {
@@ -136,6 +160,7 @@ func (g *GitClient) Clone(ctx context.Context, clonePath string, url string, tok
136160
fetchOpts.RefSpecs = []config.RefSpec{
137161
config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/origin/%s", discovered, discovered)),
138162
}
163+
resolved.localRef = plumbing.ReferenceName("refs/remotes/origin/" + discovered)
139164
} else {
140165
fetchOpts.RefSpecs = []config.RefSpec{config.RefSpec("+refs/heads/*:refs/remotes/origin/*")}
141166
}
@@ -145,31 +170,20 @@ func (g *GitClient) Clone(ctx context.Context, clonePath string, url string, tok
145170
}
146171
defaultBranch = discovered
147172
}
148-
case looksLikeSHA(ref):
149-
fetchOpts.RefSpecs = []config.RefSpec{
150-
config.RefSpec(fmt.Sprintf("+%s:refs/remotes/origin/target", ref)),
151-
}
152-
err = repo.FetchContext(ctx, fetchOpts)
153-
if err := classifyFetchError(err); err != nil {
154-
return err
155-
}
156173
default:
157-
fullRef := ref
158-
if !strings.HasPrefix(ref, "refs/") {
159-
fullRef = "refs/heads/" + ref
174+
resolved, err = resolveRemoteRef(repo, url, token, ref)
175+
if err != nil {
176+
return err
160177
}
161-
defaultBranch = strings.TrimPrefix(fullRef, "refs/heads/")
162-
localRef := "refs/remotes/origin/" + defaultBranch
163-
fetchOpts.RefSpecs = []config.RefSpec{
164-
config.RefSpec(fmt.Sprintf("+%s:%s", fullRef, localRef)),
178+
if resolved.kind == resolvedRefBranch {
179+
defaultBranch = strings.TrimPrefix(resolved.fullRef, "refs/heads/")
165180
}
166-
err = repo.FetchContext(ctx, fetchOpts)
167-
if err := classifyFetchError(err); err != nil {
181+
if err := fetchResolvedRef(ctx, repo, fetchOpts, resolved); err != nil {
168182
return err
169183
}
170184
}
171185

172-
headHash, err := resolveHead(repo, ref, token)
186+
headHash, err := resolveFetchedTargetToCommit(store, repo, resolved, token)
173187
if err != nil {
174188
return fmt.Errorf("failed to resolve head after clone: %w", err)
175189
}
@@ -207,6 +221,27 @@ func (g *GitClient) Clone(ctx context.Context, clonePath string, url string, tok
207221
return nil
208222
}
209223

224+
func fetchResolvedRef(ctx context.Context, repo *gogit.Repository, fetchOpts *gogit.FetchOptions, resolved *resolvedRef) error {
225+
switch resolved.kind {
226+
case resolvedRefBranch, resolvedRefTag:
227+
fetchOpts.RefSpecs = []config.RefSpec{
228+
config.RefSpec(fmt.Sprintf("+%s:%s", resolved.fullRef, resolved.localRef)),
229+
}
230+
case resolvedRefCommit:
231+
fetchOpts.RefSpecs = []config.RefSpec{
232+
config.RefSpec(fmt.Sprintf("+%s:%s", resolved.commitHash.String(), resolved.localRef)),
233+
}
234+
default:
235+
return fmt.Errorf("unsupported resolved ref kind for fetch: %q", resolved.input)
236+
}
237+
238+
err := repo.FetchContext(ctx, fetchOpts)
239+
if err := classifyFetchError(err); err != nil {
240+
return err
241+
}
242+
return nil
243+
}
244+
210245
func (g *GitClient) FetchCone(ctx context.Context, clonePath, url, token, ref string, cone string) error {
211246
store := memory.NewStorage()
212247
repo, err := gogit.Init(store, nil)
@@ -582,22 +617,84 @@ func (g *GitClient) Cleanup(clonePath string) {
582617
g.mu.Unlock()
583618
}
584619

585-
// resolveHead finds the commit hash for the fetched ref.
586-
// It resolves entirely from local refs (no network calls) for performance.
587-
func resolveHead(repo *gogit.Repository, ref string, token string) (plumbing.Hash, error) {
588-
// If a specific branch or SHA was requested, look it up directly
589-
if ref != "HEAD" && ref != "" {
590-
if looksLikeSHA(ref) {
591-
return plumbing.NewHash(ref), nil
620+
func resolveRemoteRef(repo *gogit.Repository, url string, token string, ref string) (*resolvedRef, error) {
621+
switch {
622+
case ref == "" || ref == "HEAD":
623+
return &resolvedRef{kind: resolvedRefHead, input: ref}, nil
624+
case looksLikeSHA(ref):
625+
return &resolvedRef{
626+
kind: resolvedRefCommit,
627+
input: ref,
628+
localRef: plumbing.ReferenceName("refs/poutine/target"),
629+
commitHash: plumbing.NewHash(ref),
630+
}, nil
631+
case strings.HasPrefix(ref, "refs/heads/"):
632+
return &resolvedRef{
633+
kind: resolvedRefBranch,
634+
input: ref,
635+
fullRef: ref,
636+
localRef: plumbing.ReferenceName("refs/remotes/origin/" + strings.TrimPrefix(ref, "refs/heads/")),
637+
}, nil
638+
case strings.HasPrefix(ref, "refs/tags/"):
639+
return &resolvedRef{
640+
kind: resolvedRefTag,
641+
input: ref,
642+
fullRef: ref,
643+
localRef: plumbing.ReferenceName("refs/poutine/target"),
644+
}, nil
645+
}
646+
647+
remote, err := repo.Remote("origin")
648+
if err != nil {
649+
return nil, fmt.Errorf("failed to get origin remote for ref resolution: %w", err)
650+
}
651+
652+
remoteRefs, err := remote.List(&gogit.ListOptions{
653+
Auth: authForToken(token),
654+
})
655+
if err != nil {
656+
if err := classifyFetchError(err); err != nil {
657+
return nil, err
592658
}
593-
branch := strings.TrimPrefix(ref, "refs/heads/")
594-
refName := plumbing.ReferenceName("refs/remotes/origin/" + branch)
595-
r, err := repo.Reference(refName, true)
596-
if err == nil {
597-
return r.Hash(), nil
659+
return nil, fmt.Errorf("failed to list remote refs for %s: %w", url, err)
660+
}
661+
662+
tagRef := plumbing.ReferenceName("refs/tags/" + ref)
663+
for _, remoteRef := range remoteRefs {
664+
if remoteRef.Name() == tagRef {
665+
return &resolvedRef{
666+
kind: resolvedRefTag,
667+
input: ref,
668+
fullRef: tagRef.String(),
669+
localRef: plumbing.ReferenceName("refs/poutine/target"),
670+
}, nil
598671
}
599672
}
600673

674+
branchRef := plumbing.ReferenceName("refs/heads/" + ref)
675+
for _, remoteRef := range remoteRefs {
676+
if remoteRef.Name() == branchRef {
677+
return &resolvedRef{
678+
kind: resolvedRefBranch,
679+
input: ref,
680+
fullRef: branchRef.String(),
681+
localRef: plumbing.ReferenceName("refs/remotes/origin/" + ref),
682+
}, nil
683+
}
684+
}
685+
686+
return nil, fmt.Errorf("%w: %s", ErrRemoteRefNotFound, ref)
687+
}
688+
689+
func resolveFetchedTargetToCommit(store storer.EncodedObjectStorer, repo *gogit.Repository, resolved *resolvedRef, token string) (plumbing.Hash, error) {
690+
if resolved != nil && resolved.kind != resolvedRefHead {
691+
return resolveFetchedRefToCommit(store, repo, resolved.localRef)
692+
}
693+
694+
if resolved != nil && resolved.localRef != "" {
695+
return resolveFetchedRefToCommit(store, repo, resolved.localRef)
696+
}
697+
601698
// For HEAD: try common default branch names from local refs (no network call)
602699
for _, name := range []string{"main", "master"} {
603700
localRef := plumbing.ReferenceName("refs/remotes/origin/" + name)
@@ -618,7 +715,7 @@ func resolveHead(repo *gogit.Repository, ref string, token string) (plumbing.Has
618715
name := string(r.Name())
619716
if strings.HasPrefix(name, "refs/remotes/origin/") && !strings.HasSuffix(name, "/HEAD") {
620717
headHash = r.Hash()
621-
return errors.New("found") // break
718+
return errors.New("found")
622719
}
623720
return nil
624721
})
@@ -637,16 +734,42 @@ func resolveHead(repo *gogit.Repository, ref string, token string) (plumbing.Has
637734
if r.Name() == plumbing.HEAD && r.Type() == plumbing.SymbolicReference {
638735
defaultBranch := strings.TrimPrefix(string(r.Target()), "refs/heads/")
639736
localRef := plumbing.ReferenceName("refs/remotes/origin/" + defaultBranch)
640-
r, err := repo.Reference(localRef, true)
641-
if err == nil {
642-
return r.Hash(), nil
643-
}
737+
return resolveFetchedRefToCommit(store, repo, localRef)
644738
}
645739
}
646740
}
647741
}
648742

649-
return plumbing.ZeroHash, fmt.Errorf("could not resolve head for ref %s", ref)
743+
return plumbing.ZeroHash, fmt.Errorf("could not resolve head for ref %s", resolved.input)
744+
}
745+
746+
func resolveFetchedRefToCommit(store storer.EncodedObjectStorer, repo *gogit.Repository, refName plumbing.ReferenceName) (plumbing.Hash, error) {
747+
r, err := repo.Reference(refName, true)
748+
if err != nil {
749+
return plumbing.ZeroHash, fmt.Errorf("failed to resolve fetched ref %s: %w", refName, err)
750+
}
751+
752+
return peelToCommit(store, r.Hash())
753+
}
754+
755+
func peelToCommit(store storer.EncodedObjectStorer, hash plumbing.Hash) (plumbing.Hash, error) {
756+
obj, err := store.EncodedObject(plumbing.AnyObject, hash)
757+
if err != nil {
758+
return plumbing.ZeroHash, fmt.Errorf("failed to load object %s: %w", hash, err)
759+
}
760+
761+
switch obj.Type() {
762+
case plumbing.CommitObject:
763+
return hash, nil
764+
case plumbing.TagObject:
765+
tag, err := object.GetTag(store, hash)
766+
if err != nil {
767+
return plumbing.ZeroHash, fmt.Errorf("failed to load tag %s: %w", hash, err)
768+
}
769+
return peelToCommit(store, tag.Target)
770+
default:
771+
return plumbing.ZeroHash, fmt.Errorf("object %s is %s, expected commit or tag", hash, obj.Type())
772+
}
650773
}
651774

652775
// looksLikeSHA returns true if s looks like a full-length git commit SHA.

0 commit comments

Comments
 (0)