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.
5 // Package codehost defines the interface implemented by a code hosting source,
6 // along with support code for use by implementations.
13 exec
"internal/execabs"
23 "cmd/go/internal/lockedfile"
27 // Downloaded size limits.
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
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.
39 // List lists all tags with the given prefix.
40 Tags(prefix
string) (tags
[]string, err error
)
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
)
47 // Latest returns the latest revision on the default branch,
48 // whatever that means in the underlying implementation.
49 Latest() (*RevInfo
, error
)
51 // ReadFile reads the given file in the file tree corresponding to revision rev.
52 // It should refuse to read more than maxSize bytes.
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
)
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
)
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
)
70 // DescendsFrom reports whether rev or any of its ancestors has the given tag.
72 // DescendsFrom must return true for any tag returned by RecentTag for the
74 DescendsFrom(rev
, tag
string) (bool, error
)
77 // A Rev describes a single revision in a source code repository.
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
86 // A FileRev describes the result of reading a file at a given revision.
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
93 // UnknownRevisionError is an error equivalent to fs.ErrNotExist, but for a
94 // revision rather than a file.
95 type UnknownRevisionError
struct {
99 func (e
*UnknownRevisionError
) Error() string {
100 return "unknown revision " + e
.Rev
102 func (UnknownRevisionError
) Is(err error
) bool {
103 return err
== fs
.ErrNotExist
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
{}
110 type noCommitsError
struct{}
112 func (noCommitsError
) Error() string {
115 func (noCommitsError
) Is(err error
) bool {
116 return err
== fs
.ErrNotExist
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
++ {
123 if '0' <= c
&& c
<= '9' ||
'a' <= c
&& c
<= 'f' {
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 {
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")
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")
155 key
:= typ
+ ":" + name
156 dir
= filepath
.Join(cfg
.GOMODCACHE
, "cache/vcs", fmt
.Sprintf("%x", sha256
.Sum256([]byte(key
))))
159 fmt
.Fprintf(os
.Stderr
, "mkdir -p %s # %s %s\n", filepath
.Dir(dir
), typ
, name
)
161 if err
:= os
.MkdirAll(filepath
.Dir(dir
), 0777); err
!= nil {
165 lockfile
= dir
+ ".lock"
167 fmt
.Fprintf(os
.Stderr
, "# lock %s", lockfile
)
170 unlock
, err
:= lockedfile
.MutexAt(lockfile
).Lock()
172 return "", "", fmt
.Errorf("codehost.WorkDir: can't find or create lock file: %v", err
)
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")
182 return "", "", fmt
.Errorf("%s exists with wrong content (have %q want %q)", dir
+".info", have
, key
)
185 fmt
.Fprintf(os
.Stderr
, "# %s for %s %s\n", dir
, typ
, name
)
187 return dir
, lockfile
, nil
190 // Info file or directory missing. Start from scratch.
192 fmt
.Fprintf(os
.Stderr
, "mkdir -p %s # %s %s\n", dir
, typ
, name
)
195 if err
:= os
.MkdirAll(dir
, 0777); err
!= nil {
198 if err
:= os
.WriteFile(dir
+".info", []byte(key
), 0666); err
!= nil {
202 return dir
, lockfile
, nil
205 type RunError
struct {
212 func (e
*RunError
) Error() string {
213 text
:= e
.Cmd
+ ": " + e
.Err
.Error()
214 stderr
:= bytes
.TrimRight(e
.Stderr
, "\n")
216 text
+= ":\n\t" + strings
.ReplaceAll(string(stderr
), "\n", "\n\t")
218 if len(e
.HelpText
) > 0 {
219 text
+= "\n" + e
.HelpText
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
...any
) ([]byte, error
) {
232 return RunWithStdin(dir
, nil, cmdline
...)
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(`"`, `\"`, `$`, `\$`, "`", "\\`", `\`, `\\`)
239 func RunWithStdin(dir
string, stdin io
.Reader
, cmdline
...any
) ([]byte, error
) {
241 muIface
, ok
:= dirLock
.Load(dir
)
243 muIface
, _
= dirLock
.LoadOrStore(dir
, new(sync
.Mutex
))
245 mu
:= muIface
.(*sync
.Mutex
)
250 cmd
:= str
.StringList(cmdline
...)
251 if os
.Getenv("TESTGOVCS") == "panic" {
252 panic(fmt
.Sprintf("use of vcs: %v", cmd
))
255 text
:= new(strings
.Builder
)
257 text
.WriteString("cd ")
258 text
.WriteString(dir
)
259 text
.WriteString("; ")
261 for i
, arg
:= range cmd
{
266 case strings
.ContainsAny(arg
, "'"):
267 // Quote args that could be mistaken for quoted args.
269 text
.WriteString(bashQuoter
.Replace(arg
))
271 case strings
.ContainsAny(arg
, "$`\\*?[\"\t\n\v\f\r \u0085\u00a0"):
272 // Quote args that contain special characters, glob patterns, or spaces.
274 text
.WriteString(arg
)
277 text
.WriteString(arg
)
280 fmt
.Fprintf(os
.Stderr
, "%s\n", text
)
283 fmt
.Fprintf(os
.Stderr
, "%.3fs # %s\n", time
.Since(start
).Seconds(), text
)
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:]...)
297 err
= &RunError
{Cmd
: strings
.Join(cmd
, " ") + " in " + dir
, Stderr
: stderr
.Bytes(), Err
: err
}
299 return stdout
.Bytes(), err