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 RepoSum string `json:",omitempty"`
123 }
124
125
126 type Tags struct {
127 Origin *Origin
128 List []Tag
129 }
130
131
132 type Tag struct {
133 Name string
134 Hash string
135 }
136
137
138
139
140
141
142
143 func isOriginTag(tag string) bool {
144
145
146
147
148
149
150
151 c := semver.Canonical(tag)
152 return c != "" && strings.HasPrefix(tag, c) && !module.IsPseudoVersion(tag)
153 }
154
155
156 type RevInfo struct {
157 Origin *Origin
158 Name string
159 Short string
160 Version string
161 Time time.Time
162 Tags []string
163 }
164
165
166
167 type UnknownRevisionError struct {
168 Rev string
169 }
170
171 func (e *UnknownRevisionError) Error() string {
172 return "unknown revision " + e.Rev
173 }
174 func (UnknownRevisionError) Is(err error) bool {
175 return err == fs.ErrNotExist
176 }
177
178
179
180 var ErrNoCommits error = noCommitsError{}
181
182 type noCommitsError struct{}
183
184 func (noCommitsError) Error() string {
185 return "no commits"
186 }
187 func (noCommitsError) Is(err error) bool {
188 return err == fs.ErrNotExist
189 }
190
191
192 func AllHex(rev string) bool {
193 for i := 0; i < len(rev); i++ {
194 c := rev[i]
195 if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' {
196 continue
197 }
198 return false
199 }
200 return true
201 }
202
203
204
205 func ShortenSHA1(rev string) string {
206 if AllHex(rev) && len(rev) == 40 {
207 return rev[:12]
208 }
209 return rev
210 }
211
212
213
214 func WorkDir(ctx context.Context, typ, name string) (dir, lockfile string, err error) {
215 if cfg.GOMODCACHE == "" {
216 return "", "", fmt.Errorf("neither GOPATH nor GOMODCACHE are set")
217 }
218
219
220
221
222
223
224 if strings.Contains(typ, ":") {
225 return "", "", fmt.Errorf("codehost.WorkDir: type cannot contain colon")
226 }
227 key := typ + ":" + name
228 dir = filepath.Join(cfg.GOMODCACHE, "cache/vcs", fmt.Sprintf("%x", sha256.Sum256([]byte(key))))
229
230 xLog, buildX := cfg.BuildXWriter(ctx)
231 if buildX {
232 fmt.Fprintf(xLog, "mkdir -p %s # %s %s\n", filepath.Dir(dir), typ, name)
233 }
234 if err := os.MkdirAll(filepath.Dir(dir), 0777); err != nil {
235 return "", "", err
236 }
237
238 lockfile = dir + ".lock"
239 if buildX {
240 fmt.Fprintf(xLog, "# lock %s\n", lockfile)
241 }
242
243 unlock, err := lockedfile.MutexAt(lockfile).Lock()
244 if err != nil {
245 return "", "", fmt.Errorf("codehost.WorkDir: can't find or create lock file: %v", err)
246 }
247 defer unlock()
248
249 data, err := os.ReadFile(dir + ".info")
250 info, err2 := os.Stat(dir)
251 if err == nil && err2 == nil && info.IsDir() {
252
253 have := strings.TrimSuffix(string(data), "\n")
254 if have != key {
255 return "", "", fmt.Errorf("%s exists with wrong content (have %q want %q)", dir+".info", have, key)
256 }
257 if buildX {
258 fmt.Fprintf(xLog, "# %s for %s %s\n", dir, typ, name)
259 }
260 return dir, lockfile, nil
261 }
262
263
264 if xLog != nil {
265 fmt.Fprintf(xLog, "mkdir -p %s # %s %s\n", dir, typ, name)
266 }
267 os.RemoveAll(dir)
268 if err := os.MkdirAll(dir, 0777); err != nil {
269 return "", "", err
270 }
271 if err := os.WriteFile(dir+".info", []byte(key), 0666); err != nil {
272 os.RemoveAll(dir)
273 return "", "", err
274 }
275 return dir, lockfile, nil
276 }
277
278 type RunError struct {
279 Cmd string
280 Err error
281 Stderr []byte
282 HelpText string
283 }
284
285 func (e *RunError) Error() string {
286 text := e.Cmd + ": " + e.Err.Error()
287 stderr := bytes.TrimRight(e.Stderr, "\n")
288 if len(stderr) > 0 {
289 text += ":\n\t" + strings.ReplaceAll(string(stderr), "\n", "\n\t")
290 }
291 if len(e.HelpText) > 0 {
292 text += "\n" + e.HelpText
293 }
294 return text
295 }
296
297 var dirLock sync.Map
298
299 type RunArgs struct {
300 cmdline []any
301 dir string
302 local bool
303 env []string
304 stdin io.Reader
305 }
306
307
308
309
310
311
312 func Run(ctx context.Context, dir string, cmdline ...any) ([]byte, error) {
313 return run(ctx, RunArgs{cmdline: cmdline, dir: dir})
314 }
315
316
317 func RunWithArgs(ctx context.Context, args RunArgs) ([]byte, error) {
318 return run(ctx, args)
319 }
320
321
322
323 var bashQuoter = strings.NewReplacer(`"`, `\"`, `$`, `\$`, "`", "\\`", `\`, `\\`)
324
325 func run(ctx context.Context, args RunArgs) ([]byte, error) {
326 if args.dir != "" {
327 muIface, ok := dirLock.Load(args.dir)
328 if !ok {
329 muIface, _ = dirLock.LoadOrStore(args.dir, new(sync.Mutex))
330 }
331 mu := muIface.(*sync.Mutex)
332 mu.Lock()
333 defer mu.Unlock()
334 }
335
336 cmd := str.StringList(args.cmdline...)
337 if os.Getenv("TESTGOVCSREMOTE") == "panic" && !args.local {
338 panic(fmt.Sprintf("use of remote vcs: %v", cmd))
339 }
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