1
2
3
4
5
6
7 package codehost
8
9 import (
10 "bytes"
11 "crypto/sha256"
12 "fmt"
13 exec "internal/execabs"
14 "io"
15 "io/fs"
16 "os"
17 "path/filepath"
18 "strings"
19 "sync"
20 "time"
21
22 "cmd/go/internal/cfg"
23 "cmd/go/internal/lockedfile"
24 "cmd/go/internal/str"
25 )
26
27
28 const (
29 MaxGoMod = 16 << 20
30 MaxLICENSE = 16 << 20
31 MaxZipFile = 500 << 20
32 )
33
34
35
36
37
38 type Repo interface {
39
40 Tags(prefix string) (tags []string, err error)
41
42
43
44
45 Stat(rev string) (*RevInfo, error)
46
47
48
49 Latest() (*RevInfo, error)
50
51
52
53
54
55
56 ReadFile(rev, file string, maxSize int64) (data []byte, err error)
57
58
59
60
61
62
63
64 ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error)
65
66
67
68 RecentTag(rev, prefix string, allowed func(string) bool) (tag string, err error)
69
70
71
72
73
74 DescendsFrom(rev, tag string) (bool, error)
75 }
76
77
78 type RevInfo struct {
79 Name string
80 Short string
81 Version string
82 Time time.Time
83 Tags []string
84 }
85
86
87 type FileRev struct {
88 Rev string
89 Data []byte
90 Err error
91 }
92
93
94
95 type UnknownRevisionError struct {
96 Rev string
97 }
98
99 func (e *UnknownRevisionError) Error() string {
100 return "unknown revision " + e.Rev
101 }
102 func (UnknownRevisionError) Is(err error) bool {
103 return err == fs.ErrNotExist
104 }
105
106
107
108 var ErrNoCommits error = noCommitsError{}
109
110 type noCommitsError struct{}
111
112 func (noCommitsError) Error() string {
113 return "no commits"
114 }
115 func (noCommitsError) Is(err error) bool {
116 return err == fs.ErrNotExist
117 }
118
119
120 func AllHex(rev string) bool {
121 for i := 0; i < len(rev); i++ {
122 c := rev[i]
123 if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' {
124 continue
125 }
126 return false
127 }
128 return true
129 }
130
131
132
133 func ShortenSHA1(rev string) string {
134 if AllHex(rev) && len(rev) == 40 {
135 return rev[:12]
136 }
137 return rev
138 }
139
140
141
142 func WorkDir(typ, name string) (dir, lockfile string, err error) {
143 if cfg.GOMODCACHE == "" {
144 return "", "", fmt.Errorf("neither GOPATH nor GOMODCACHE are set")
145 }
146
147
148
149
150
151
152 if strings.Contains(typ, ":") {
153 return "", "", fmt.Errorf("codehost.WorkDir: type cannot contain colon")
154 }
155 key := typ + ":" + name
156 dir = filepath.Join(cfg.GOMODCACHE, "cache/vcs", fmt.Sprintf("%x", sha256.Sum256([]byte(key))))
157
158 if cfg.BuildX {
159 fmt.Fprintf(os.Stderr, "mkdir -p %s # %s %s\n", filepath.Dir(dir), typ, name)
160 }
161 if err := os.MkdirAll(filepath.Dir(dir), 0777); err != nil {
162 return "", "", err
163 }
164
165 lockfile = dir + ".lock"
166 if cfg.BuildX {
167 fmt.Fprintf(os.Stderr, "# lock %s", lockfile)
168 }
169
170 unlock, err := lockedfile.MutexAt(lockfile).Lock()
171 if err != nil {
172 return "", "", fmt.Errorf("codehost.WorkDir: can't find or create lock file: %v", err)
173 }
174 defer unlock()
175
176 data, err := os.ReadFile(dir + ".info")
177 info, err2 := os.Stat(dir)
178 if err == nil && err2 == nil && info.IsDir() {
179
180 have := strings.TrimSuffix(string(data), "\n")
181 if have != key {
182 return "", "", fmt.Errorf("%s exists with wrong content (have %q want %q)", dir+".info", have, key)
183 }
184 if cfg.BuildX {
185 fmt.Fprintf(os.Stderr, "# %s for %s %s\n", dir, typ, name)
186 }
187 return dir, lockfile, nil
188 }
189
190
191 if cfg.BuildX {
192 fmt.Fprintf(os.Stderr, "mkdir -p %s # %s %s\n", dir, typ, name)
193 }
194 os.RemoveAll(dir)
195 if err := os.MkdirAll(dir, 0777); err != nil {
196 return "", "", err
197 }
198 if err := os.WriteFile(dir+".info", []byte(key), 0666); err != nil {
199 os.RemoveAll(dir)
200 return "", "", err
201 }
202 return dir, lockfile, nil
203 }
204
205 type RunError struct {
206 Cmd string
207 Err error
208 Stderr []byte
209 HelpText string
210 }
211
212 func (e *RunError) Error() string {
213 text := e.Cmd + ": " + e.Err.Error()
214 stderr := bytes.TrimRight(e.Stderr, "\n")
215 if len(stderr) > 0 {
216 text += ":\n\t" + strings.ReplaceAll(string(stderr), "\n", "\n\t")
217 }
218 if len(e.HelpText) > 0 {
219 text += "\n" + e.HelpText
220 }
221 return text
222 }
223
224 var dirLock sync.Map
225
226
227
228
229
230
231 func Run(dir string, cmdline ...any) ([]byte, error) {
232 return RunWithStdin(dir, nil, cmdline...)
233 }
234
235
236
237 var bashQuoter = strings.NewReplacer(`"`, `\"`, `$`, `\$`, "`", "\\`", `\`, `\\`)
238
239 func RunWithStdin(dir string, stdin io.Reader, cmdline ...any) ([]byte, error) {
240 if dir != "" {
241 muIface, ok := dirLock.Load(dir)
242 if !ok {
243 muIface, _ = dirLock.LoadOrStore(dir, new(sync.Mutex))
244 }
245 mu := muIface.(*sync.Mutex)
246 mu.Lock()
247 defer mu.Unlock()
248 }
249
250 cmd := str.StringList(cmdline...)
251 if os.Getenv("TESTGOVCS") == "panic" {
252 panic(fmt.Sprintf("use of vcs: %v", cmd))
253 }
254 if cfg.BuildX {
255 text := new(strings.Builder)
256 if dir != "" {
257 text.WriteString("cd ")
258 text.WriteString(dir)
259 text.WriteString("; ")
260 }
261 for i, arg := range cmd {
262 if i > 0 {
263 text.WriteByte(' ')
264 }
265 switch {
266 case strings.ContainsAny(arg, "'"):
267
268 text.WriteByte('"')
269 text.WriteString(bashQuoter.Replace(arg))
270 text.WriteByte('"')
271 case strings.ContainsAny(arg, "$`\\*?[\"\t\n\v\f\r \u0085\u00a0"):
272
273 text.WriteByte('\'')
274 text.WriteString(arg)
275 text.WriteByte('\'')
276 default:
277 text.WriteString(arg)
278 }
279 }
280 fmt.Fprintf(os.Stderr, "%s\n", text)
281 start := time.Now()
282 defer func() {
283 fmt.Fprintf(os.Stderr, "%.3fs # %s\n", time.Since(start).Seconds(), text)
284 }()
285 }
286
287
288 var stderr bytes.Buffer
289 var stdout bytes.Buffer
290 c := exec.Command(cmd[0], cmd[1:]...)
291 c.Dir = dir
292 c.Stdin = stdin
293 c.Stderr = &stderr
294 c.Stdout = &stdout
295 err := c.Run()
296 if err != nil {
297 err = &RunError{Cmd: strings.Join(cmd, " ") + " in " + dir, Stderr: stderr.Bytes(), Err: err}
298 }
299 return stdout.Bytes(), err
300 }
301
View as plain text