@@ -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
4546var 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.
4867type 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+
210245func (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