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 xLog, ok := cfg.BuildXWriter(ctx); ok {
341 text := new(strings.Builder)
342 if args.dir != "" {
343 text.WriteString("cd ")
344 text.WriteString(args.dir)
345 text.WriteString("; ")
346 }
347 for i, arg := range cmd {
348 if i > 0 {
349 text.WriteByte(' ')
350 }
351 switch {
352 case strings.ContainsAny(arg, "'"):
353
354 text.WriteByte('"')
355 text.WriteString(bashQuoter.Replace(arg))
356 text.WriteByte('"')
357 case strings.ContainsAny(arg, "$`\\*?[\"\t\n\v\f\r \u0085\u00a0"):
358
359 text.WriteByte('\'')
360 text.WriteString(arg)
361 text.WriteByte('\'')
362 default:
363 text.WriteString(arg)
364 }
365 }
366 fmt.Fprintf(xLog, "%s\n", text)
367 start := time.Now()
368 defer func() {
369 fmt.Fprintf(xLog, "%.3fs # %s\n", time.Since(start).Seconds(), text)
370 }()
371 }
372
373
374 var stderr bytes.Buffer
375 var stdout bytes.Buffer
376 c := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
377 c.Cancel = func() error { return c.Process.Signal(os.Interrupt) }
378 c.Dir = args.dir
379 c.Stdin = args.stdin
380 c.Stderr = &stderr
381 c.Stdout = &stdout
382 c.Env = append(c.Environ(), args.env...)
383 err := c.Run()
384 if err != nil {
385 err = &RunError{Cmd: strings.Join(cmd, " ") + " in " + args.dir, Stderr: stderr.Bytes(), Err: err}
386 }
387 return stdout.Bytes(), err
388 }
389
View as plain text