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
300
301
302
303
304 func Run(ctx context.Context, dir string, cmdline ...any) ([]byte, error) {
305 return RunWithStdin(ctx, dir, nil, cmdline...)
306 }
307
308
309
310 var bashQuoter = strings.NewReplacer(`"`, `\"`, `$`, `\$`, "`", "\\`", `\`, `\\`)
311
312 func RunWithStdin(ctx context.Context, dir string, stdin io.Reader, cmdline ...any) ([]byte, error) {
313 if dir != "" {
314 muIface, ok := dirLock.Load(dir)
315 if !ok {
316 muIface, _ = dirLock.LoadOrStore(dir, new(sync.Mutex))
317 }
318 mu := muIface.(*sync.Mutex)
319 mu.Lock()
320 defer mu.Unlock()
321 }
322
323 cmd := str.StringList(cmdline...)
324 if os.Getenv("TESTGOVCS") == "panic" {
325 panic(fmt.Sprintf("use of vcs: %v", cmd))
326 }
327 if xLog, ok := cfg.BuildXWriter(ctx); ok {
328 text := new(strings.Builder)
329 if dir != "" {
330 text.WriteString("cd ")
331 text.WriteString(dir)
332 text.WriteString("; ")
333 }
334 for i, arg := range cmd {
335 if i > 0 {
336 text.WriteByte(' ')
337 }
338 switch {
339 case strings.ContainsAny(arg, "'"):
340
341 text.WriteByte('"')
342 text.WriteString(bashQuoter.Replace(arg))
343 text.WriteByte('"')
344 case strings.ContainsAny(arg, "$`\\*?[\"\t\n\v\f\r \u0085\u00a0"):
345
346 text.WriteByte('\'')
347 text.WriteString(arg)
348 text.WriteByte('\'')
349 default:
350 text.WriteString(arg)
351 }
352 }
353 fmt.Fprintf(xLog, "%s\n", text)
354 start := time.Now()
355 defer func() {
356 fmt.Fprintf(xLog, "%.3fs # %s\n", time.Since(start).Seconds(), text)
357 }()
358 }
359
360
361 var stderr bytes.Buffer
362 var stdout bytes.Buffer
363 c := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
364 c.Cancel = func() error { return c.Process.Signal(os.Interrupt) }
365 c.Dir = dir
366 c.Stdin = stdin
367 c.Stderr = &stderr
368 c.Stdout = &stdout
369
370 c.Env = append(c.Environ(), "GIT_DIR="+dir)
371 err := c.Run()
372 if err != nil {
373 err = &RunError{Cmd: strings.Join(cmd, " ") + " in " + dir, Stderr: stderr.Bytes(), Err: err}
374 }
375 return stdout.Bytes(), err
376 }
377
View as plain text