github.com/knieriem/gointernal@v0.2.0-pre2/cmd/go/modfetch/codehost/codehost.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package codehost defines the interface implemented by a code hosting source, 6 // along with support code for use by implementations. 7 package codehost 8 9 import ( 10 "bytes" 11 "crypto/sha256" 12 "fmt" 13 exec "github.com/knieriem/gointernal/internal/execabs" 14 "io" 15 "io/fs" 16 "os" 17 "path/filepath" 18 "strings" 19 "sync" 20 "time" 21 22 "github.com/knieriem/gointernal/cmd/go/cfg" 23 "github.com/knieriem/gointernal/cmd/go/internal/lockedfile" 24 "github.com/knieriem/gointernal/cmd/go/internal/str" 25 ) 26 27 // Downloaded size limits. 28 const ( 29 MaxGoMod = 16 << 20 // maximum size of go.mod file 30 MaxLICENSE = 16 << 20 // maximum size of LICENSE file 31 MaxZipFile = 500 << 20 // maximum size of downloaded zip file 32 ) 33 34 // A Repo represents a code hosting source. 35 // Typical implementations include local version control repositories, 36 // remote version control servers, and code hosting sites. 37 // A Repo must be safe for simultaneous use by multiple goroutines. 38 type Repo interface { 39 // List lists all tags with the given prefix. 40 Tags(prefix string) (tags []string, err error) 41 42 // Stat returns information about the revision rev. 43 // A revision can be any identifier known to the underlying service: 44 // commit hash, branch, tag, and so on. 45 Stat(rev string) (*RevInfo, error) 46 47 // Latest returns the latest revision on the default branch, 48 // whatever that means in the underlying implementation. 49 Latest() (*RevInfo, error) 50 51 // ReadFile reads the given file in the file tree corresponding to revision rev. 52 // It should refuse to read more than maxSize bytes. 53 // 54 // If the requested file does not exist it should return an error for which 55 // os.IsNotExist(err) returns true. 56 ReadFile(rev, file string, maxSize int64) (data []byte, err error) 57 58 // ReadZip downloads a zip file for the subdir subdirectory 59 // of the given revision to a new file in a given temporary directory. 60 // It should refuse to read more than maxSize bytes. 61 // It returns a ReadCloser for a streamed copy of the zip file. 62 // All files in the zip file are expected to be 63 // nested in a single top-level directory, whose name is not specified. 64 ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) 65 66 // RecentTag returns the most recent tag on rev or one of its predecessors 67 // with the given prefix. allowed may be used to filter out unwanted versions. 68 RecentTag(rev, prefix string, allowed func(string) bool) (tag string, err error) 69 70 // DescendsFrom reports whether rev or any of its ancestors has the given tag. 71 // 72 // DescendsFrom must return true for any tag returned by RecentTag for the 73 // same revision. 74 DescendsFrom(rev, tag string) (bool, error) 75 } 76 77 // A Rev describes a single revision in a source code repository. 78 type RevInfo struct { 79 Name string // complete ID in underlying repository 80 Short string // shortened ID, for use in pseudo-version 81 Version string // version used in lookup 82 Time time.Time // commit time 83 Tags []string // known tags for commit 84 } 85 86 // A FileRev describes the result of reading a file at a given revision. 87 type FileRev struct { 88 Rev string // requested revision 89 Data []byte // file data 90 Err error // error if any; os.IsNotExist(Err)==true if rev exists but file does not exist in that rev 91 } 92 93 // UnknownRevisionError is an error equivalent to fs.ErrNotExist, but for a 94 // revision rather than a file. 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 // ErrNoCommits is an error equivalent to fs.ErrNotExist indicating that a given 107 // repository or module contains no commits. 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 // AllHex reports whether the revision rev is entirely lower-case hexadecimal digits. 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 // ShortenSHA1 shortens a SHA1 hash (40 hex digits) to the canonical length 132 // used in pseudo-versions (12 hex digits). 133 func ShortenSHA1(rev string) string { 134 if AllHex(rev) && len(rev) == 40 { 135 return rev[:12] 136 } 137 return rev 138 } 139 140 // WorkDir returns the name of the cached work directory to use for the 141 // given repository type and name. 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 // We name the work directory for the SHA256 hash of the type and name. 148 // We intentionally avoid the actual name both because of possible 149 // conflicts with valid file system paths and because we want to ensure 150 // that one checkout is never nested inside another. That nesting has 151 // led to security problems in the past. 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 // Info file and directory both already exist: reuse. 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 // Info file or directory missing. Start from scratch. 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 // Run runs the command line in the given directory 227 // (an empty dir means the current directory). 228 // It returns the standard output and, for a non-zero exit, 229 // a *RunError indicating the command, exit status, and standard error. 230 // Standard error is unavailable for commands that exit successfully. 231 func Run(dir string, cmdline ...interface{}) ([]byte, error) { 232 return RunWithStdin(dir, nil, cmdline...) 233 } 234 235 // bashQuoter escapes characters that have special meaning in double-quoted strings in the bash shell. 236 // See https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html. 237 var bashQuoter = strings.NewReplacer(`"`, `\"`, `$`, `\$`, "`", "\\`", `\`, `\\`) 238 239 func RunWithStdin(dir string, stdin io.Reader, cmdline ...interface{}) ([]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 // Quote args that could be mistaken for quoted args. 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 // Quote args that contain special characters, glob patterns, or spaces. 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 // TODO: Impose limits on command output size. 287 // TODO: Set environment to get English error messages. 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 }