-
Notifications
You must be signed in to change notification settings - Fork 247
/
repos.go
232 lines (221 loc) · 6.79 KB
/
repos.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
package main
import (
"context"
"fmt"
"log"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/brigadecore/brigade-foundations/retries"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/storage/filesystem"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors"
)
const (
retryTimeLimit = time.Minute
retryMaxBackoff = 5 * time.Second
)
// cloneAndCheckoutCommit clones the remote repo specified by cloneURL and
// fetches and checks out only the tree identified by the provided SHA.
func cloneAndCheckoutCommit(cloneURL, sha string) error {
// We have some repo configuration we want to handle before we attempt
// cloning, so we start by initializing an empty repository.
repo, err := git.Init(
filesystem.NewStorage(
osfs.New(filepath.Join(workspace, ".git")),
cache.NewObjectLRUDefault(),
),
osfs.New(workspace),
)
if err != nil {
return errors.Wrapf(err, "error initializing git repo at %q", workspace)
}
// Set the credential helper to "store" in case there's a username and
// password in play. If there isn't this configuration causes no harm.
// We fall back on the CLI to accomplish this because we'll also be falling
// back on the CLI for the fetch.
cmd := exec.Command(
"git",
"config",
"credential.helper",
"store",
)
cmd.Dir = workspace
if err = execCommand(cmd); err != nil {
return errors.Wrap(err, "error configuring credential helper")
}
// Define the remote
if _, err = repo.CreateRemote(
&config.RemoteConfig{
Name: "origin",
URLs: []string{cloneURL},
},
); err != nil {
return errors.Wrapf(err, "error creating remote for %q", cloneURL)
}
// Fetch using the CLI. We use the CLI for this because the go-git library we
// use for interacting with repositories programmatically does not implement
// certain newer parts of the git spec that some providers -- namely Azure
// DevOps -- require.
ctx, cancel := context.WithTimeout(context.Background(), retryTimeLimit)
defer cancel()
if err = retries.ManageRetries(
ctx, // Try until this context expires
fmt.Sprintf("fetching %q", sha),
0, // Infinite retries (until the context expires)
retryMaxBackoff,
func() (bool, error) {
cmd = exec.Command(
"git",
"fetch",
"origin",
sha,
"--no-tags",
)
cmd.Dir = workspace
if err = execCommand(cmd); err != nil {
err = errors.Wrapf(err, "error fetching %q", sha)
// We only want to retry on DNS lookups because this is the error (on
// Windows) that's indicative of the container's networking not being
// ready yet.
if strings.Contains(err.Error(), "Could not resolve host") {
log.Println("Retrying...")
return true, err
}
return false, err // Any other error, we'll surface right away
}
return false, nil // Success
},
); err != nil {
return err
}
// The previous command should have pointed FETCH_HEAD at what we just
// fetched, so check that out into the working tree.
worktree, err := repo.Worktree()
if err != nil {
return errors.Wrap(err, "error getting repository's working tree")
}
return errors.Wrap(
worktree.Checkout(
&git.CheckoutOptions{
Branch: "FETCH_HEAD",
},
),
"error checking out FETCH_HEAD to working tree",
)
}
// cloneAndCheckoutRef clones the remote repo specified by cloneURL and
// fetches and checks out only the tree identified by the symbolic reference.
func cloneAndCheckoutRef(cloneURL, ref string) error {
ctx, cancel := context.WithTimeout(context.Background(), retryTimeLimit)
defer cancel()
// Here, a single branch clone executed via CLI works very nicely.
return retries.ManageRetries(
ctx, // Try until this context expires
fmt.Sprintf("fetching %q", ref),
0, // Infinite retries (until the context expires)
retryMaxBackoff,
func() (bool, error) {
if err := execCommand(
exec.Command(
"git",
"clone",
"--branch",
ref,
"--single-branch",
"--no-tags",
"--config",
"credential.helper=store",
cloneURL,
workspace,
),
); err != nil {
err = errors.Wrapf(err, "error fetching %q", ref)
// We only want to retry on DNS lookups because this is the error (on
// Windows) that's indicative of the container's networking not being
// ready yet.
if strings.Contains(err.Error(), "Could not resolve host") {
log.Println("Retrying...")
return true, err
}
return false, err // Any other error, we'll surface right away
}
return false, nil // Success
},
)
}
// getDefaultBranch can find the default branch of a remote repository by
// listing all its references and looking for the one named HEAD. This is useful
// when we want to clone/checkout ONLY the default branch, but we don't already
// know its name.
func getDefaultBranch(
cloneURL string,
auth transport.AuthMethod,
) (string, error) {
remote := git.NewRemote(
memory.NewStorage(),
&config.RemoteConfig{
URLs: []string{cloneURL},
},
)
var refs []*plumbing.Reference
var err error
ctx, cancel := context.WithTimeout(context.Background(), retryTimeLimit)
defer cancel()
if err = retries.ManageRetries(
ctx, // Try until this context expires
"listing refs",
0, // Infinite retries (until the context expires)
retryMaxBackoff,
func() (bool, error) {
if refs, err = remote.List(
&git.ListOptions{
Auth: auth,
},
); err != nil {
err = errors.Wrap(err, "error listing refs")
// We only want to retry on DNS lookups because this is the error (on
// Windows) that's indicative of the container's networking not being
// ready yet.
if strings.Contains(err.Error(), "Could not resolve host") {
log.Println("Retrying...")
return true, err
}
return false, err // Any other error, we'll surface right away
}
return false, nil // Success
},
); err != nil {
return "", err
}
// Now look through the refs...
for _, ref := range refs {
if ref.Name() == plumbing.HEAD {
return ref.Target().Short(), nil
}
}
return "", errors.New("failed to find default branch")
}
// getShortRef recognizes refs of the form refs/tags/* and refs/heads/* and
// returns only the "short" name of the ref -- which is the input expected by
// the `--branch`` flag on a `git clone`.
func getShortRef(ref string) string {
if strings.HasPrefix(ref, "refs/tags/") ||
strings.HasPrefix(ref, "refs/heads/") {
return strings.SplitAfterN(ref, "/", 3)[2]
}
return ref
}
// initSubmodules initializes and updates submodules.
func initSubmodules() error {
cmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
cmd.Dir = workspace
return errors.Wrap(execCommand(cmd), "error initializing submodules")
}