@@ -15,6 +15,7 @@ import (
15
15
giturls "github.com/chainguard-dev/git-urls"
16
16
"github.com/go-git/go-billy/v5"
17
17
"github.com/go-git/go-git/v5"
18
+ gitconfig "github.com/go-git/go-git/v5/config"
18
19
"github.com/go-git/go-git/v5/plumbing"
19
20
"github.com/go-git/go-git/v5/plumbing/cache"
20
21
"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
@@ -99,8 +100,7 @@ func CloneRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOpt
99
100
return false , fmt .Errorf ("chroot .git: %w" , err )
100
101
}
101
102
gitStorage := filesystem .NewStorage (gitDir , cache .NewObjectLRU (cache .DefaultMaxSize * 10 ))
102
- fsStorage := filesystem .NewStorage (fs , cache .NewObjectLRU (cache .DefaultMaxSize * 10 ))
103
- repo , err := git .Open (fsStorage , gitDir )
103
+ repo , err := git .Open (gitStorage , fs )
104
104
if err == nil {
105
105
// Repo exists, so fast-forward it.
106
106
return fastForwardRepo (ctx , logf , opts , repo , reference )
@@ -129,44 +129,107 @@ func CloneRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOpt
129
129
if err != nil {
130
130
return false , fmt .Errorf ("clone %q: %w" , opts .RepoURL , err )
131
131
}
132
- if head , err := repo .Head (); err == nil && head != nil {
133
- logf ("cloned repo HEAD: %s" , head .Hash ().String ())
132
+ if head , err := repoHead (repo ); err != nil {
133
+ logf ("failed to get repo HEAD: %s" , err )
134
+ } else {
135
+ logf ("cloned repo at %s" , head )
134
136
}
135
137
return true , nil
136
138
}
137
139
138
140
func fastForwardRepo (ctx context.Context , logf func (string , ... any ), opts CloneRepoOptions , repo * git.Repository , referenceName string ) (bool , error ) {
139
- if head , err := repo . Head (); err == nil && head != nil {
140
- logf ( "existing repo HEAD: %s" , head . Hash (). String ())
141
+ if referenceName == "" {
142
+ referenceName = "refs/heads/main"
141
143
}
142
- wt , err := repo .Worktree ()
144
+ ref := plumbing .ReferenceName (referenceName )
145
+ headBefore , err := repoHead (repo )
143
146
if err != nil {
144
- return false , fmt . Errorf ( "worktree: %w" , err )
147
+ return false , err
145
148
}
146
- err = wt .PullContext (ctx , & git.PullOptions {
147
- RemoteName : "" , // use default remote
148
- ReferenceName : plumbing .ReferenceName (referenceName ),
149
- SingleBranch : opts .SingleBranch ,
150
- Depth : opts .Depth ,
149
+ logf ("existing repo HEAD: %s" , headBefore )
150
+
151
+ remote , err := repo .Remote ("origin" )
152
+ if err != nil {
153
+ return false , fmt .Errorf ("remote: %w" , err )
154
+ }
155
+
156
+ if len (remote .Config ().URLs ) == 0 {
157
+ logf ("remote %q has no URLs configured!" , remote .String ())
158
+ return false , nil
159
+ }
160
+
161
+ // If the remote URL differs, stop! We don't want to accidentally
162
+ // fetch from a different remote.
163
+ for _ , u := range remote .Config ().URLs {
164
+ if u != opts .RepoURL {
165
+ logf ("remote %q has URL %q, expected %q" , remote .String (), u , opts .RepoURL )
166
+ return false , nil
167
+ }
168
+ }
169
+
170
+ var refSpecs []gitconfig.RefSpec
171
+ if referenceName != "" {
172
+ // git checkout -b <branch> --track <remote>/<branch>
173
+ refSpecs = []gitconfig.RefSpec {
174
+ gitconfig .RefSpec (fmt .Sprintf ("%s:%s" , referenceName , referenceName )),
175
+ }
176
+ }
177
+
178
+ if err := remote .FetchContext (ctx , & git.FetchOptions {
151
179
Auth : opts .RepoAuth ,
152
- Progress : opts .Progress ,
180
+ CABundle : opts .CABundle ,
181
+ Depth : opts .Depth ,
153
182
Force : false ,
154
183
InsecureSkipTLS : opts .Insecure ,
155
- CABundle : opts .CABundle ,
184
+ Progress : opts .Progress ,
156
185
ProxyOptions : opts .ProxyOptions ,
157
- })
158
- if err == nil {
159
- if head , err := repo .Head (); err == nil && head != nil {
160
- logf ("fast-forwarded to %s" , head .Hash ().String ())
186
+ RefSpecs : refSpecs ,
187
+ RemoteName : "origin" ,
188
+ }); err != nil {
189
+ if err != git .NoErrAlreadyUpToDate {
190
+ return false , fmt .Errorf ("fetch changes from remote: %w" , err )
161
191
}
162
- return true , nil
192
+ logf ("repository is already up-to-date" )
193
+ }
194
+
195
+ // Now that we've fetched changes from the remote,
196
+ // attempt to check out the requested reference.
197
+ wt , err := repo .Worktree ()
198
+ if err != nil {
199
+ return false , fmt .Errorf ("worktree: %w" , err )
163
200
}
164
- if errors .Is (err , git .NoErrAlreadyUpToDate ) {
165
- logf ("existing repo already up-to-date" )
201
+ st , err := wt .Status ()
202
+ if err != nil {
203
+ return false , fmt .Errorf ("status: %w" , err )
204
+ }
205
+ // If the working tree is dirty, assume that the user wishes to
206
+ // test local changes. Skip the checkout.
207
+ if ! st .IsClean () {
208
+ logf ("working tree is dirty, skipping checkout" )
166
209
return false , nil
167
210
}
168
- logf ("failed to fast-forward: %s" , err .Error ())
169
- return false , err
211
+ if err := wt .Checkout (& git.CheckoutOptions {
212
+ Branch : ref ,
213
+ // Note: we already checked that the working tree is clean, but I'd rather
214
+ // err on the side of caution.
215
+ Keep : true ,
216
+ }); err != nil {
217
+ return false , fmt .Errorf ("checkout branch %s: %w" , ref .String (), err )
218
+ }
219
+ headAfter , err := repoHead (repo )
220
+ if err != nil {
221
+ return false , fmt .Errorf ("check repo HEAD: %w" , err )
222
+ }
223
+ logf ("checked out %s" , headAfter )
224
+ return headBefore != headAfter , nil
225
+ }
226
+
227
+ func repoHead (repo * git.Repository ) (string , error ) {
228
+ head , err := repo .Head ()
229
+ if err != nil {
230
+ return "" , err
231
+ }
232
+ return head .Hash ().String (), nil
170
233
}
171
234
172
235
// ShallowCloneRepo will clone the repository at the given URL into the given path
0 commit comments