1
2
3
4
5
6
7 package codehost
8
9 import (
10 "bytes"
11 "context"
12 "crypto/sha256"
13 "fmt"
14 "io"
15 "io/fs"
16 "os"
17 "os/exec"
18 "path/filepath"
19 "strings"
20 "sync"
21 "time"
22
23 "cmd/go/internal/cfg"
24 "cmd/go/internal/lockedfile"
25 "cmd/go/internal/str"
26
27 "golang.org/x/mod/module"
28 "golang.org/x/mod/semver"
29 )
30
31
32 const (
33 MaxGoMod = 16 << 20
34 MaxLICENSE = 16 << 20
35 MaxZipFile = 500 << 20
36 )
37
38
39
40
41
42
43
44 type Repo interface {
45
46
47
48
49
50 CheckReuse(ctx context.Context, old *Origin, subdir string) error
51
52
53 Tags(ctx context.Context, prefix string) (*Tags, error)
54
55
56
57
58 Stat(ctx context.Context, rev string) (*RevInfo, error)
59
60
61
62 Latest(ctx context.Context) (*RevInfo, error)
63
64
65
66
67
68
69 ReadFile(ctx context.Context, rev, file string, maxSize int64) (data []byte, err error)
70
71
72
73
74
75
76
77 ReadZip(ctx context.Context, rev, subdir string, maxSize int64) (zip io.ReadCloser, err error)
78
79
80
81 RecentTag(ctx context.Context, rev, prefix string, allowed func(tag string) bool) (tag string, err error)
82
83
84
85
86
87 DescendsFrom(ctx context.Context, rev, tag string) (bool, error)
88 }
89
90
91
92
93 type Origin struct {
94 VCS string `json:",omitempty"`
95 URL string `json:",omitempty"`
96 Subdir string `json:",omitempty"`
97
98 Hash string `json:",omitempty"`
99
100
101
102
103
104
105
106 TagPrefix string `json:",omitempty"`
107 TagSum string `json:",omitempty"`
108
109
110
111
112
113
114
115
116 Ref string `json:",omitempty"`
117
118
119
120
121
122
123
124
125 RepoSum string `json:",omitempty"`
126 }
127
128
129 type Tags struct {
130 Origin *Origin
131 List []Tag
132 }
133
134
135 type Tag struct {
136 Name string
137 Hash string
138 }
139
140
141
142
143
144
145
146 func isOriginTag(tag string) bool {
147
148
149
150
151
152
153
154 c := semver.Canonical(tag)
155 return c != "" && strings.HasPrefix(tag, c) && !module.IsPseudoVersion(tag)
156 }
157
158
159 type RevInfo struct {
160 Origin *Origin
161 Name string
162 Short string
163 Version string
164 Time time.Time
165 Tags []string
166 }
167
168
169
170 type UnknownRevisionError struct {
171 Rev string
172 }
173
174 func (e *UnknownRevisionError) Error() string {
175 return "unknown revision " + e.Rev
176 }
177 func (UnknownRevisionError) Is(err error) bool {
178 return err == fs.ErrNotExist
179 }
180
181
182
183 var ErrNoCommits error = noCommitsError{}
184
185 type noCommitsError struct{}
186
187 func (noCommitsError) Error() string {
188 return "no commits"
189 }
190 func (noCommitsError) Is(err error) bool {
191 return err == fs.ErrNotExist
192 }
193
194
195 func AllHex(rev string) bool {
196 for i := 0; i < len(rev); i++ {
197 c := rev[i]
198 if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' {
199 continue
200 }
201 return false
202 }
203 return true
204 }
205
206
207
208 func ShortenSHA1(rev string) string {
209 if AllHex(rev) && len(rev) == 40 {
210 return rev[:12]
211 }
212 return rev
213 }
214
215
216
217 func WorkDir(ctx context.Context, typ, name string) (dir, lockfile string, err error) {
218 if cfg.GOMODCACHE == "" {
219 return "", "", fmt.Errorf("neither GOPATH nor GOMODCACHE are set")
220 }
221
222
223
224
225
226
227 if strings.Contains(typ, ":") {
228 return "", "", fmt.Errorf("codehost.WorkDir: type cannot contain colon")
229 }
230 key := typ + ":" + name
231 dir = filepath.Join(cfg.GOMODCACHE, "cache/vcs", fmt.Sprintf("%x", sha256.Sum256([]byte(key))))
232
233 xLog, buildX := cfg.BuildXWriter(ctx)
234 if buildX {
235 fmt.Fprintf(xLog, "mkdir -p %s # %s %s\n", filepath.Dir(dir), typ, name)
236 }
237 if err := os.MkdirAll(filepath.Dir(dir), 0777); err != nil {
238 return "", "", err
239 }
240
241 lockfile = dir + ".lock"
242 if buildX {
243 fmt.Fprintf(xLog, "# lock %s\n", lockfile)
244 }
245
246 unlock, err := lockedfile.MutexAt(lockfile).Lock()
247 if err != nil {
248 return "", "", fmt.Errorf("codehost.WorkDir: can't find or create lock file: %v", err)
249 }
250 defer unlock()
251
252 data, err := os.ReadFile(dir + ".info")
253 info, err2 := os.Stat(dir)
254 if err == nil && err2 == nil && info.IsDir() {
255
256 have := strings.TrimSuffix(string(data), "\n")
257 if have != key {
258 return "", "", fmt.Errorf("%s exists with wrong content (have %q want %q)", dir+".info", have, key)
259 }
260 if buildX {
261 fmt.Fprintf(xLog, "# %s for %s %s\n", dir, typ, name)
262 }
263 return dir, lockfile, nil
264 }
265
266
267 if xLog != nil {
268 fmt.Fprintf(xLog, "mkdir -p %s # %s %s\n", dir, typ, name)
269 }
270 os.RemoveAll(dir)
271 if err := os.MkdirAll(dir, 0777); err != nil {
272 return "", "", err
273 }
274 if err := os.WriteFile(dir+".info", []byte(key), 0666); err != nil {
275 os.RemoveAll(dir)
276 return "", "", err
277 }
278 return dir, lockfile, nil
279 }
280
281 type RunError struct {
282 Cmd string
283 Err error
284 Stderr []byte
285 HelpText string
286 }
287
288 func (e *RunError) Error() string {
289 text := e.Cmd + ": " + e.Err.Error()
290 stderr := bytes.TrimRight(e.Stderr, "\n")
291 if len(stderr) > 0 {
292 text += ":\n\t" + strings.ReplaceAll(string(stderr), "\n", "\n\t")
293 }
294 if len(e.HelpText) > 0 {
295 text += "\n" + e.HelpText
296 }
297 return text
298 }
299
300 var dirLock sync.Map
301
302 type RunArgs struct {
303 cmdline []any
304 dir string
305 local bool
306 env []string
307 stdin io.Reader
308 }
309
310
311
312
313
314
315 func Run(ctx context.Context, dir string, cmdline ...any) ([]byte, error) {
316 return run(ctx, RunArgs{cmdline: cmdline, dir: dir})
317 }
318
319
320 func RunWithArgs(ctx context.Context, args RunArgs) ([]byte, error) {
321 return run(ctx, args)
322 }
323
324
325
326 var bashQuoter = strings.NewReplacer(`"`, `\"`, `$`, `\$`, "`", "\\`", `\`, `\\`)
327
328 func run(ctx context.Context, args RunArgs) ([]byte, error) {
329 if args.dir != "" {
330 muIface, ok := dirLock.Load(args.dir)
331 if !ok {
332 muIface, _ = dirLock.LoadOrStore(args.dir, new(sync.Mutex))
333 }
334 mu := muIface.(*sync.Mutex)
335 mu.Lock()
336 defer mu.Unlock()
337 }
338
339 cmd := str.StringList(args.cmdline...)
340 if os.Getenv("TESTGOVCSREMOTE") == "panic" && !args.local {
341 panic(fmt.Sprintf("use of remote vcs: %v", cmd))
342 }
343 if xLog, ok := cfg.BuildXWriter(ctx); ok {
344 text := new(strings.Builder)
345 if args.dir != "" {
346 text.WriteString("cd ")
347 text.WriteString(args.dir)
348 text.WriteString("; ")
349 }
350 for i, arg := range cmd {
351 if i > 0 {
352 text.WriteByte(' ')
353 }
354 switch {
355 case strings.ContainsAny(arg, "'"):
356
357 text.WriteByte('"')
358 text.WriteString(bashQuoter.Replace(arg))
359 text.WriteByte('"')
360 case strings.ContainsAny(arg, "$`\\*?[\"\t\n\v\f\r \u0085\u00a0"):
361
362 text.WriteByte('\'')
363 text.WriteString(arg)
364 text.WriteByte('\'')
365 default:
366 text.WriteString(arg)
367 }
368 }
369 fmt.Fprintf(xLog, "%s\n", text)
370 start := time.Now()
371 defer func() {
372 fmt.Fprintf(xLog, "%.3fs # %s\n", time.Since(start).Seconds(), text)
373 }()
374 }
375
376
377 var stderr bytes.Buffer
378 var stdout bytes.Buffer
379 c := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
380 c.Cancel = func() error { return c.Process.Signal(os.Interrupt) }
381 c.Dir = args.dir
382 c.Stdin = args.stdin
383 c.Stderr = &stderr
384 c.Stdout = &stdout
385 c.Env = append(c.Environ(), args.env...)
386 err := c.Run()
387 if err != nil {
388 err = &RunError{Cmd: strings.Join(cmd, " ") + " in " + args.dir, Stderr: stderr.Bytes(), Err: err}
389 }
390 return stdout.Bytes(), err
391 }
392
View as plain text