66 "strings"
77
88 "github.com/go-git/go-git/v5"
9+ "github.com/go-git/go-git/v5/config"
910 "github.com/go-git/go-git/v5/plumbing"
1011 "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
1112 "github.com/go-git/go-git/v5/plumbing/transport"
@@ -23,6 +24,7 @@ const defaultBranch = "master"
2324
2425var (
2526 plainClone = git .PlainClone
27+ cloneCommit = cloneCommitShallow
2628 updateSubmodules = submodule .UpdateSubmodules
2729 readFile = os .ReadFile
2830 fileStat = os .Stat
@@ -80,39 +82,117 @@ func (c *Cloner) CloneRepo(opts *GitCloner) error {
8082}
8183
8284func cloneBranch (opts * GitCloner , auth transport.AuthMethod , caBundle []byte ) error {
83- r , err := plainClone (opts .Path , false , & git.CloneOptions {
85+ r , err := shallowCloneRef (opts , auth , caBundle , plumbing .ReferenceName (opts .Branch ))
86+ if err != nil {
87+ return fmt .Errorf ("failed to clone main repo from branch %s: %w, skipping submodule clone" , repo (opts ), err )
88+ }
89+
90+ return updateSubmodulesShallow (r , auth )
91+ }
92+
93+ func cloneRevision (opts * GitCloner , auth transport.AuthMethod , caBundle []byte ) error {
94+ // A bare 40-hex revision resolves to the commit object (git ignores any
95+ // ref with that name), so skip the tag/branch attempts and fetch the exact
96+ // commit shallowly. The full-clone fallback covers the rare case where the
97+ // 40-hex value is actually a ref pointing at some other commit.
98+ if plumbing .IsHash (opts .Revision ) {
99+ if err := cloneCommit (opts , auth , caBundle ); err == nil {
100+ return nil
101+ }
102+ if err := resetDir (opts .Path ); err != nil {
103+ return fmt .Errorf ("failed to reset clone dir for %s: %w" , repo (opts ), err )
104+ }
105+ return fullCloneRevision (opts , auth , caBundle )
106+ }
107+
108+ if r , err := shallowCloneRef (opts , auth , caBundle , plumbing .NewTagReferenceName (opts .Revision )); err == nil {
109+ return updateSubmodulesShallow (r , auth )
110+ }
111+ if err := resetDir (opts .Path ); err != nil {
112+ return fmt .Errorf ("failed to reset clone dir for %s: %w" , repo (opts ), err )
113+ }
114+
115+ if r , err := shallowCloneRef (opts , auth , caBundle , plumbing .NewBranchReferenceName (opts .Revision )); err == nil {
116+ return updateSubmodulesShallow (r , auth )
117+ }
118+ if err := resetDir (opts .Path ); err != nil {
119+ return fmt .Errorf ("failed to reset clone dir for %s: %w" , repo (opts ), err )
120+ }
121+
122+ return fullCloneRevision (opts , auth , caBundle )
123+ }
124+
125+ // shallowCloneRef performs a depth-1, single-branch clone pinned to ref. It
126+ // only owns the clone and leaves submodule handling to the caller, so the
127+ // revision path can fall through to the next strategy on error while the
128+ // branch path can wrap the error and update submodules on success.
129+ func shallowCloneRef (opts * GitCloner , auth transport.AuthMethod , caBundle []byte , ref plumbing.ReferenceName ) (* git.Repository , error ) {
130+ return plainClone (opts .Path , false , & git.CloneOptions {
84131 URL : opts .Repo ,
85132 Depth : 1 ,
86133 Auth : auth ,
87134 InsecureSkipTLS : opts .InsecureSkipTLS ,
88135 CABundle : caBundle ,
89136 SingleBranch : true ,
90- ReferenceName : plumbing . ReferenceName ( opts . Branch ) ,
137+ ReferenceName : ref ,
91138 RecurseSubmodules : git .NoRecurseSubmodules ,
92139 Tags : git .NoTags ,
93140 ProxyOptions : fleetgit .ProxyOptsFromEnvironment (opts .Repo ),
94141 })
142+ }
143+
144+ // cloneCommitShallow fetches a single commit by SHA with depth 1, avoiding the
145+ // full-history clone that resolving an arbitrary commit would otherwise need.
146+ // It only succeeds when the server allows fetching an exact SHA
147+ // (uploadpack.allowReachableSHA1InWant / allowAnySHA1InWant); callers must fall
148+ // back to a full clone on error.
149+ func cloneCommitShallow (opts * GitCloner , auth transport.AuthMethod , caBundle []byte ) error {
150+ r , err := git .PlainInit (opts .Path , false )
95151 if err != nil {
96- return fmt . Errorf ( "failed to clone main repo from branch %s: %w, skipping submodule clone" , repo ( opts ), err )
152+ return err
97153 }
98-
99- return updateSubmodules (r , & git.SubmoduleUpdateOptions {
100- Init : true ,
101- RecurseSubmodules : git .DefaultSubmoduleRecursionDepth ,
102- Depth : 1 ,
103- Auth : auth ,
104- })
154+ if _ , err := r .CreateRemote (& config.RemoteConfig {
155+ Name : git .DefaultRemoteName ,
156+ URLs : []string {opts .Repo },
157+ }); err != nil {
158+ return err
159+ }
160+ // Store the fetched commit under a normal local ref, matching the SHA-fetch
161+ // strategies in submodule/strategy. The checkout below reads the object by
162+ // hash, so the destination ref name itself is irrelevant.
163+ refSpec := config .RefSpec (opts .Revision + ":refs/heads/temp" )
164+ if err := r .Fetch (& git.FetchOptions {
165+ RemoteName : git .DefaultRemoteName ,
166+ Depth : 1 ,
167+ RefSpecs : []config.RefSpec {refSpec },
168+ Auth : auth ,
169+ InsecureSkipTLS : opts .InsecureSkipTLS ,
170+ CABundle : caBundle ,
171+ Tags : git .NoTags ,
172+ ProxyOptions : fleetgit .ProxyOptsFromEnvironment (opts .Repo ),
173+ }); err != nil {
174+ return err
175+ }
176+ w , err := r .Worktree ()
177+ if err != nil {
178+ return err
179+ }
180+ if err := w .Checkout (& git.CheckoutOptions {Hash : plumbing .NewHash (opts .Revision )}); err != nil {
181+ return err
182+ }
183+ return updateSubmodulesShallow (r , auth )
105184}
106185
107- func cloneRevision (opts * GitCloner , auth transport.AuthMethod , caBundle []byte ) error {
186+ // fullCloneRevision clones the whole repository (all history and tags) and
187+ // resolves opts.Revision locally. This is the slowest path and is only used
188+ // when no cheaper strategy can satisfy the revision.
189+ func fullCloneRevision (opts * GitCloner , auth transport.AuthMethod , caBundle []byte ) error {
108190 r , err := plainClone (opts .Path , false , & git.CloneOptions {
109191 URL : opts .Repo ,
110- Depth : 1 ,
111192 Auth : auth ,
112193 InsecureSkipTLS : opts .InsecureSkipTLS ,
113194 CABundle : caBundle ,
114195 RecurseSubmodules : git .NoRecurseSubmodules ,
115- Tags : git .NoTags ,
116196 ProxyOptions : fleetgit .ProxyOptsFromEnvironment (opts .Repo ),
117197 })
118198 if err != nil {
@@ -129,7 +209,12 @@ func cloneRevision(opts *GitCloner, auth transport.AuthMethod, caBundle []byte)
129209 if err := w .Checkout (& git.CheckoutOptions {Hash : * h }); err != nil {
130210 return fmt .Errorf ("failed to checkout in worktree %s: %w" , repo (opts ), err )
131211 }
212+ return updateSubmodulesShallow (r , auth )
213+ }
132214
215+ // updateSubmodulesShallow recursively initializes and shallowly updates the
216+ // repository's submodules. It is shared by every clone path.
217+ func updateSubmodulesShallow (r * git.Repository , auth transport.AuthMethod ) error {
133218 return updateSubmodules (r , & git.SubmoduleUpdateOptions {
134219 Init : true ,
135220 RecurseSubmodules : git .DefaultSubmoduleRecursionDepth ,
@@ -138,6 +223,15 @@ func cloneRevision(opts *GitCloner, auth transport.AuthMethod, caBundle []byte)
138223 })
139224}
140225
226+ // resetDir clears the clone destination so a subsequent clone attempt starts
227+ // from a clean slate. go-git refuses to clone into a non-empty directory.
228+ func resetDir (path string ) error {
229+ if err := os .RemoveAll (path ); err != nil {
230+ return err
231+ }
232+ return os .MkdirAll (path , 0750 )
233+ }
234+
141235func getCABundleFromFile (path string ) ([]byte , error ) {
142236 var caBundle []byte
143237 if path != "" {
0 commit comments