Skip to content

Commit 741fd53

Browse files
authored
Fix cloner when using tags in revision. (#5292)
* Fix cloner when using tags in revision. This PR fixes an issue when using tags in the revision field of `GitRepo`. It also includes a small refactoring creating functions with names to make core more readable. Refers to: #5181 Signed-off-by: Xavi Garcia <xavi.garcia@suse.com>
1 parent 2451e2d commit 741fd53

2 files changed

Lines changed: 399 additions & 155 deletions

File tree

internal/cmd/cli/gitcloner/cloner.go

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
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

2425
var (
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

8284
func 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+
141235
func getCABundleFromFile(path string) ([]byte, error) {
142236
var caBundle []byte
143237
if path != "" {

0 commit comments

Comments
 (0)