blob: 8c3d340c6b9452ec915f54436f6081d459072939 [file] [log] [blame]
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -04001// Copyright 2019 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 source constructs public URLs that link to the source files in a module. It
6// can be used to build references to Go source code, or to any other files in a
7// module.
8//
9// Of course, the module zip file contains all the files in the module. This
10// package attempts to find the origin of the zip file, in a repository that is
11// publicly readable, and constructs links to that repo. While a module zip file
12// could in theory come from anywhere, including a non-public location, this
13// package recognizes standard module path patterns and construct repository
14// URLs from them, like the go command does.
15package source
16
17//
18// Much of this code was adapted from
19// https://go.googlesource.com/gddo/+/refs/heads/master/gosrc
20// and
21// https://go.googlesource.com/go/+/refs/heads/master/src/cmd/go/internal/get
22
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -040023import (
Jonathan Amsterdam081e3bd2019-09-28 15:43:15 -040024 "context"
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -040025 "encoding/json"
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -040026 "fmt"
Jonathan Amsterdam081e3bd2019-09-28 15:43:15 -040027 "net/http"
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -040028 "path"
Michael Matloobf36927e2023-07-10 13:26:46 -040029 "path/filepath"
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -040030 "regexp"
31 "strconv"
32 "strings"
33
Jonathan Amsterdam860515b2019-09-30 13:36:59 -040034 "golang.org/x/net/context/ctxhttp"
Julie Qiu19794c82020-04-21 16:51:29 -040035 "golang.org/x/pkgsite/internal/derrors"
36 "golang.org/x/pkgsite/internal/log"
37 "golang.org/x/pkgsite/internal/stdlib"
Michael Matlooba2112592023-08-28 16:04:20 -040038 "golang.org/x/pkgsite/internal/trace"
Julie Qiu19794c82020-04-21 16:51:29 -040039 "golang.org/x/pkgsite/internal/version"
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -040040)
41
42// Info holds source information about a module, used to generate URLs referring
43// to directories, files and lines.
44type Info struct {
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -040045 repoURL string // URL of repo containing module; exported for DB schema compatibility
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -040046 moduleDir string // directory of module relative to repo root
47 commit string // tag or ID of commit corresponding to version
48 templates urlTemplates // for building URLs
49}
50
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -040051// RepoURL returns a URL for the home page of the repository.
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -040052func (i *Info) RepoURL() string {
53 if i == nil {
54 return ""
55 }
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -040056 if i.templates.Repo == "" {
57 // The default repo template is just "{repo}".
58 return i.repoURL
59 }
60 return expand(i.templates.Repo, map[string]string{
61 "repo": i.repoURL,
62 })
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -040063}
64
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -040065// ModuleURL returns a URL for the home page of the module.
66func (i *Info) ModuleURL() string {
67 return i.DirectoryURL("")
68}
69
70// DirectoryURL returns a URL for a directory relative to the module's home directory.
71func (i *Info) DirectoryURL(dir string) string {
Jonathan Amsterdamdfd2bbc2019-10-17 11:20:02 -040072 if i == nil {
73 return ""
74 }
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -040075 return strings.TrimSuffix(expand(i.templates.Directory, map[string]string{
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -040076 "repo": i.repoURL,
77 "importPath": path.Join(strings.TrimPrefix(i.repoURL, "https://"), dir),
78 "commit": i.commit,
79 "dir": path.Join(i.moduleDir, dir),
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -040080 }), "/")
81}
82
83// FileURL returns a URL for a file whose pathname is relative to the module's home directory.
84func (i *Info) FileURL(pathname string) string {
Jonathan Amsterdamdfd2bbc2019-10-17 11:20:02 -040085 if i == nil {
86 return ""
87 }
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -040088 dir, base := path.Split(pathname)
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -040089 return expand(i.templates.File, map[string]string{
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -040090 "repo": i.repoURL,
91 "importPath": path.Join(strings.TrimPrefix(i.repoURL, "https://"), dir),
92 "commit": i.commit,
Jonathan Amsterdam7d0f1a12021-03-22 10:08:42 -040093 "dir": dir,
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -040094 "file": path.Join(i.moduleDir, pathname),
95 "base": base,
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -040096 })
97}
98
99// LineURL returns a URL referring to a line in a file relative to the module's home directory.
100func (i *Info) LineURL(pathname string, line int) string {
Jonathan Amsterdamdfd2bbc2019-10-17 11:20:02 -0400101 if i == nil {
102 return ""
103 }
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -0400104 dir, base := path.Split(pathname)
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400105 return expand(i.templates.Line, map[string]string{
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -0400106 "repo": i.repoURL,
107 "importPath": path.Join(strings.TrimPrefix(i.repoURL, "https://"), dir),
108 "commit": i.commit,
109 "file": path.Join(i.moduleDir, pathname),
Jonathan Amsterdam7d0f1a12021-03-22 10:08:42 -0400110 "dir": dir,
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -0400111 "base": base,
112 "line": strconv.Itoa(line),
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400113 })
114}
115
Jonathan Amsterdama2fc41b2019-10-14 19:40:13 -0400116// RawURL returns a URL referring to the raw contents of a file relative to the
Jonathan Amsterdam186838c2020-07-16 18:21:49 -0400117// module's home directory.
Jonathan Amsterdama2fc41b2019-10-14 19:40:13 -0400118func (i *Info) RawURL(pathname string) string {
Jonathan Amsterdam77e252b2019-10-16 14:43:06 -0400119 if i == nil {
120 return ""
121 }
Jonathan Amsterdama2fc41b2019-10-14 19:40:13 -0400122 // Some templates don't support raw content serving.
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400123 if i.templates.Raw == "" {
Jonathan Amsterdama2fc41b2019-10-14 19:40:13 -0400124 return ""
125 }
Jonathan Amsterdam07471c32019-10-25 18:39:20 -0400126 moduleDir := i.moduleDir
127 // Special case: the standard library's source module path is set to "src",
128 // which is correct for source file links. But the README is at the repo
129 // root, not in the src directory. In other words,
Julie Qiu677d5082020-09-29 19:41:52 -0400130 // Module.Units[0].Readme.FilePath is not relative to
131 // Module.Units[0].SourceInfo.moduleDir, as it is for every other module.
Jonathan Amsterdam07471c32019-10-25 18:39:20 -0400132 // Correct for that here.
133 if i.repoURL == stdlib.GoSourceRepoURL {
134 moduleDir = ""
135 }
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400136 return expand(i.templates.Raw, map[string]string{
Jonathan Amsterdam186838c2020-07-16 18:21:49 -0400137 "repo": i.repoURL,
138 "commit": i.commit,
139 "file": path.Join(moduleDir, pathname),
Jonathan Amsterdama2fc41b2019-10-14 19:40:13 -0400140 })
141}
142
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400143// map of common urlTemplates
144var urlTemplatesByKind = map[string]urlTemplates{
145 "github": githubURLTemplates,
Jonathan Amsterdam4d7057b2022-01-05 11:57:08 -0500146 "gitlab": gitlabURLTemplates,
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400147 "bitbucket": bitbucketURLTemplates,
148}
149
150// jsonInfo is a Go struct describing the JSON structure of an INFO.
151type jsonInfo struct {
152 RepoURL string
153 ModuleDir string
154 Commit string
155 // Store common templates efficiently by setting this to a short string
156 // we look up in a map. If Kind != "", then Templates == nil.
157 Kind string `json:",omitempty"`
158 Templates *urlTemplates `json:",omitempty"`
159}
160
cui fliter81f6f8d2023-02-07 22:22:14 +0800161// MarshalJSON returns the Info encoded for storage in the database.
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400162func (i *Info) MarshalJSON() (_ []byte, err error) {
163 defer derrors.Wrap(&err, "MarshalJSON")
164
165 ji := &jsonInfo{
166 RepoURL: i.repoURL,
167 ModuleDir: i.moduleDir,
168 Commit: i.commit,
169 }
170 // Store common templates efficiently, by name.
171 for kind, templs := range urlTemplatesByKind {
172 if i.templates == templs {
173 ji.Kind = kind
174 break
175 }
176 }
177 if ji.Kind == "" && i.templates != (urlTemplates{}) {
178 ji.Templates = &i.templates
179 }
180 return json.Marshal(ji)
181}
182
183func (i *Info) UnmarshalJSON(data []byte) (err error) {
184 defer derrors.Wrap(&err, "UnmarshalJSON(data)")
185
186 var ji jsonInfo
187 if err := json.Unmarshal(data, &ji); err != nil {
188 return err
189 }
Jonathan Amsterdamff8cbf22021-02-01 17:41:51 -0500190 i.repoURL = trimVCSSuffix(ji.RepoURL)
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400191 i.moduleDir = ji.ModuleDir
192 i.commit = ji.Commit
193 if ji.Kind != "" {
194 i.templates = urlTemplatesByKind[ji.Kind]
195 } else if ji.Templates != nil {
196 i.templates = *ji.Templates
197 }
198 return nil
199}
200
Julie Qiubdcbfe02020-04-03 13:15:18 -0400201type Client struct {
202 // client used for HTTP requests. It is mutable for testing purposes.
Jonathan Amsterdam3b058032020-12-22 18:13:46 -0500203 // If nil, then moduleInfoDynamic will return nil, nil; also for testing.
Julie Qiubdcbfe02020-04-03 13:15:18 -0400204 httpClient *http.Client
205}
206
Michael Matloobebe617b2023-08-28 13:39:03 -0400207// New constructs a *Client using the provided *http.Client.
208func NewClient(httpClient *http.Client) *Client {
209 return &Client{httpClient: httpClient}
Julie Qiubdcbfe02020-04-03 13:15:18 -0400210}
211
Jonathan Amsterdam3b058032020-12-22 18:13:46 -0500212// NewClientForTesting returns a Client suitable for testing. It returns the
213// same results as an ordinary client for statically recognizable paths, but
214// always returns a nil *Info for dynamic paths (those requiring HTTP requests).
215func NewClientForTesting() *Client {
216 return &Client{}
217}
218
Julie Qiubdcbfe02020-04-03 13:15:18 -0400219// doURL makes an HTTP request using the given url and method. It returns an
220// error if the request returns an error. If only200 is true, it also returns an
221// error if any status code other than 200 is returned.
222func (c *Client) doURL(ctx context.Context, method, url string, only200 bool) (_ *http.Response, err error) {
223 defer derrors.Wrap(&err, "doURL(ctx, client, %q, %q)", method, url)
224
Julie Qiua0c5a482020-04-07 11:33:25 -0400225 if c == nil || c.httpClient == nil {
226 return nil, fmt.Errorf("c.httpClient cannot be nil")
227 }
Julie Qiubdcbfe02020-04-03 13:15:18 -0400228 req, err := http.NewRequest(method, url, nil)
229 if err != nil {
230 return nil, err
231 }
232 resp, err := ctxhttp.Do(ctx, c.httpClient, req)
233 if err != nil {
234 return nil, err
235 }
236 if only200 && resp.StatusCode != 200 {
237 resp.Body.Close()
238 return nil, fmt.Errorf("status %s", resp.Status)
239 }
240 return resp, nil
241}
242
Julie Qiu677d5082020-09-29 19:41:52 -0400243// ModuleInfo determines the repository corresponding to the module path. It
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400244// returns a URL to that repo, as well as the directory of the module relative
245// to the repo root.
246//
Julie Qiu677d5082020-09-29 19:41:52 -0400247// ModuleInfo may fetch from arbitrary URLs, so it can be slow.
Julie Qiue04fa6a2021-06-08 20:32:12 -0400248func ModuleInfo(ctx context.Context, client *Client, modulePath, v string) (info *Info, err error) {
249 defer derrors.Wrap(&err, "source.ModuleInfo(ctx, %q, %q)", modulePath, v)
Julie Qiu677d5082020-09-29 19:41:52 -0400250 ctx, span := trace.StartSpan(ctx, "source.ModuleInfo")
Julie Qiu7868b342020-04-26 23:48:55 -0400251 defer span.End()
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400252
Jonathan Amsterdam169a49f2021-02-10 16:43:34 -0500253 // The example.com domain can never be real; it is reserved for testing
254 // (https://en.wikipedia.org/wiki/Example.com). Treat it as if it used
255 // GitHub templates.
256 if strings.HasPrefix(modulePath, "example.com/") {
Julie Qiue04fa6a2021-06-08 20:32:12 -0400257 return NewGitHubInfo("https://"+modulePath, "", v), nil
Jonathan Amsterdam169a49f2021-02-10 16:43:34 -0500258 }
259
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400260 repo, relativeModulePath, templates, transformCommit, err := matchStatic(modulePath)
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400261 if err != nil {
Julie Qiue04fa6a2021-06-08 20:32:12 -0400262 info, err = moduleInfoDynamic(ctx, client, modulePath, v)
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400263 if err != nil {
264 return nil, err
265 }
266 } else {
Julie Qiue04fa6a2021-06-08 20:32:12 -0400267 commit, isHash := commitFromVersion(v, relativeModulePath)
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400268 if transformCommit != nil {
269 commit = transformCommit(commit, isHash)
270 }
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400271 info = &Info{
Jonathan Amsterdamff8cbf22021-02-01 17:41:51 -0500272 repoURL: trimVCSSuffix("https://" + repo),
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400273 moduleDir: relativeModulePath,
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400274 commit: commit,
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400275 templates: templates,
276 }
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400277 }
Jonathan Amsterdam3b058032020-12-22 18:13:46 -0500278 if info != nil {
279 adjustVersionedModuleDirectory(ctx, client, info)
280 }
Julie Qiue04fa6a2021-06-08 20:32:12 -0400281 if strings.HasPrefix(modulePath, "golang.org/") {
282 adjustGoRepoInfo(info, modulePath, version.IsPseudo(v))
283 }
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400284 return info, nil
Julie Qiu4ea5ad92020-06-16 22:51:46 -0400285 // TODO(golang/go#39627): support launchpad.net, including the special case
286 // in cmd/go/internal/get/vcs.go.
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400287}
288
Michael Matloobf159e612023-07-05 14:02:46 -0400289func NewStdlibInfo(version string) (_ *Info, err error) {
290 defer derrors.Wrap(&err, "NewStdlibInfo(%q)", version)
Jonathan Amsterdam8406a912020-12-14 10:30:57 -0500291
292 commit, err := stdlib.TagForVersion(version)
293 if err != nil {
294 return nil, err
295 }
296
Julie Qiu080753e2021-05-11 15:13:29 -0400297 templates := csopensourceTemplates
Jonathan Amsterdam8406a912020-12-14 10:30:57 -0500298 templates.Raw = "https://github.com/golang/go/raw/{commit}/{file}"
299 return &Info{
300 repoURL: stdlib.GoSourceRepoURL,
301 moduleDir: stdlib.Directory(version),
302 commit: commit,
303 templates: templates,
304 }, nil
305}
306
Julie Qiue04fa6a2021-06-08 20:32:12 -0400307// csNonXRepos is a set of repos hosted at https://cs.opensource.google/go,
308// that are not an x/repo.
309var csNonXRepos = map[string]bool{
310 "dl": true,
311 "proposal": true,
312 "vscode-go": true,
313}
314
315// csXRepos is the set of repos hosted at https://cs.opensource.google/go,
316// that have a x/ prefix.
317//
318// x/scratch is not included.
319var csXRepos = map[string]bool{
320 "x/arch": true,
321 "x/benchmarks": true,
322 "x/blog": true,
323 "x/build": true,
324 "x/crypto": true,
325 "x/debug": true,
326 "x/example": true,
327 "x/exp": true,
328 "x/image": true,
329 "x/mobile": true,
330 "x/mod": true,
331 "x/net": true,
332 "x/oauth2": true,
333 "x/perf": true,
334 "x/pkgsite": true,
335 "x/playground": true,
336 "x/review": true,
337 "x/sync": true,
338 "x/sys": true,
339 "x/talks": true,
340 "x/term": true,
341 "x/text": true,
342 "x/time": true,
343 "x/tools": true,
344 "x/tour": true,
345 "x/vgo": true,
346 "x/website": true,
347 "x/xerrors": true,
348}
349
350func adjustGoRepoInfo(info *Info, modulePath string, isHash bool) {
351 suffix := strings.TrimPrefix(modulePath, "golang.org/")
352
353 // Validate that this is a repo that exists on
354 // https://cs.opensource.google/go. Otherwise, default to the existing
355 // info.
356 parts := strings.Split(suffix, "/")
357 if len(parts) >= 2 {
358 suffix = parts[0] + "/" + parts[1]
359 }
360 if strings.HasPrefix(suffix, "x/") {
361 if !csXRepos[suffix] {
362 return
363 }
364 } else if !csNonXRepos[suffix] {
365 return
366 }
367
368 // rawURL needs to be set before info.templates is changed.
369 rawURL := fmt.Sprintf(
370 "https://github.com/golang/%s/raw/{commit}/{file}", strings.TrimPrefix(suffix, "x/"))
371
372 info.repoURL = fmt.Sprintf("https://cs.opensource.google/go/%s", suffix)
373 info.templates = csopensourceTemplates
374 info.templates.Raw = rawURL
375
376 if isHash {
377 // When we have a pseudoversion, info.commit will be an actual commit
378 // instead of a tag.
379 //
380 // https://cs.opensource.google/go/* has short commits hardcoded to 8
381 // chars. Commits shorter or longer will not work, unless it is the full
382 // commit hash.
383 info.commit = info.commit[0:8]
384 }
385}
386
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400387// matchStatic matches the given module or repo path against a list of known
388// patterns. It returns the repo name, the module path relative to the repo
389// root, and URL templates if there is a match.
390//
391// The relative module path may not be correct in all cases: it is wrong if it
392// ends in a version that is not part of the repo directory structure, because
393// the repo follows the "major branch" convention for versions 2 and above.
394// E.g. this function could return "foo/v2", but the module files live under "foo"; the
395// "/v2" is part of the module path (and the import paths of its packages) but
396// is not a subdirectory. This mistake is corrected in adjustVersionedModuleDirectory,
397// once we have all the information we need to fix it.
398//
399// repo + "/" + relativeModulePath is often, but not always, equal to
400// moduleOrRepoPath. It is not when the argument is a module path that uses the
401// go command's general syntax, which ends in a ".vcs" (e.g. ".git", ".hg") that
402// is neither part of the repo nor the suffix. For example, if the argument is
Russ Cox8ea409f2022-04-11 13:12:03 -0400403//
404// github.com/a/b/c
405//
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400406// then repo="github.com/a/b" and relativeModulePath="c"; together they make up the module path.
407// But if the argument is
Russ Cox8ea409f2022-04-11 13:12:03 -0400408//
409// example.com/a/b.git/c
410//
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400411// then repo="example.com/a/b" and relativeModulePath="c"; the ".git" is omitted, since it is neither
412// part of the repo nor part of the relative path to the module within the repo.
Jonathan Amsterdama9ff35d2021-01-21 11:02:52 -0500413func matchStatic(moduleOrRepoPath string) (repo, relativeModulePath string, _ urlTemplates, transformCommit transformCommitFunc, _ error) {
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400414 for _, pat := range patterns {
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400415 matches := pat.re.FindStringSubmatch(moduleOrRepoPath)
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400416 if matches == nil {
417 continue
418 }
419 var repo string
420 for i, n := range pat.re.SubexpNames() {
421 if n == "repo" {
422 repo = matches[i]
423 break
424 }
425 }
Jonathan Amsterdam2696ea72019-10-14 17:18:52 -0400426 // Special case: git.apache.org has a go-import tag that points to
427 // github.com/apache, but it's not quite right (the repo prefix is
428 // missing a ".git"), so handle it here.
429 const apacheDomain = "git.apache.org/"
430 if strings.HasPrefix(repo, apacheDomain) {
431 repo = strings.Replace(repo, apacheDomain, "github.com/apache/", 1)
432 }
Jonathan Amsterdam7d0f1a12021-03-22 10:08:42 -0400433 // Special case: module paths are blitiri.com.ar/go/..., but repos are blitiri.com.ar/git/r/...
434 if strings.HasPrefix(repo, "blitiri.com.ar/") {
435 repo = strings.Replace(repo, "/go/", "/git/r/", 1)
436 }
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400437 relativeModulePath = strings.TrimPrefix(moduleOrRepoPath, matches[0])
438 relativeModulePath = strings.TrimPrefix(relativeModulePath, "/")
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400439 return repo, relativeModulePath, pat.templates, pat.transformCommit, nil
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400440 }
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400441 return "", "", urlTemplates{}, nil, derrors.NotFound
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400442}
443
444// moduleInfoDynamic uses the go-import and go-source meta tags to construct an Info.
Julie Qiubdcbfe02020-04-03 13:15:18 -0400445func moduleInfoDynamic(ctx context.Context, client *Client, modulePath, version string) (_ *Info, err error) {
Julie Qiue04fa6a2021-06-08 20:32:12 -0400446 defer derrors.Wrap(&err, "moduleInfoDynamic(ctx, client, %q, %q)", modulePath, version)
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400447
Jonathan Amsterdam3b058032020-12-22 18:13:46 -0500448 if client.httpClient == nil {
449 return nil, nil // for testing
450 }
451
Jonathan Amsterdam081e3bd2019-09-28 15:43:15 -0400452 sourceMeta, err := fetchMeta(ctx, client, modulePath)
453 if err != nil {
454 return nil, err
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400455 }
Jonathan Amsterdam081e3bd2019-09-28 15:43:15 -0400456 // Don't check that the tag information at the repo root prefix is the same
457 // as in the module path. It was done for us by the proxy and/or go command.
458 // (This lets us merge information from the go-import and go-source tags.)
459
460 // sourceMeta contains some information about where the module's source lives. But there
461 // are some problems:
462 // - We may only have a go-import tag, not a go-source tag, so we don't have URL templates for
463 // building URLs to files and directories.
464 // - Even if we do have a go-source tag, its URL template format predates
465 // versioning, so the URL templates won't provide a way to specify a
466 // version or commit.
467 //
468 // We resolve these problems as follows:
469 // 1. First look at the repo URL from the tag. If that matches a known hosting site, use the
470 // URL templates corresponding to that site and ignore whatever's in the tag.
Jonathan Amsterdam778864e2019-10-02 07:35:31 -0400471 // 2. Then look at the URL templates to see if they match a known pattern, and use the templates
472 // from that pattern. For example, the meta tags for gopkg.in/yaml.v2 only mention github
473 // in the URL templates, like "https://github.com/go-yaml/yaml/tree/v2.2.3{/dir}". We can observe
474 // that that template begins with a known pattern--a GitHub repo, ignore the rest of it, and use the
475 // GitHub URL templates that we know.
Jonathan Amsterdam778864e2019-10-02 07:35:31 -0400476 repoURL := sourceMeta.repoURL
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400477 _, _, templates, transformCommit, _ := matchStatic(removeHTTPScheme(repoURL))
Jonathan Amsterdam8acea932020-06-17 20:22:39 -0400478 // If err != nil, templates will be the zero value, so we can ignore it (same just below).
Jonathan Amsterdam081e3bd2019-09-28 15:43:15 -0400479 if templates == (urlTemplates{}) {
Jonathan Amsterdam778864e2019-10-02 07:35:31 -0400480 var repo string
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400481 repo, _, templates, transformCommit, _ = matchStatic(removeHTTPScheme(sourceMeta.dirTemplate))
Jonathan Amsterdam778864e2019-10-02 07:35:31 -0400482 if templates == (urlTemplates{}) {
Jonathan Amsterdama9ff35d2021-01-21 11:02:52 -0500483 if err == nil {
484 templates, transformCommit = matchLegacyTemplates(ctx, sourceMeta)
485 repoURL = strings.TrimSuffix(repoURL, ".git")
486 } else {
487 log.Infof(ctx, "no templates for repo URL %q from meta tag: err=%v", sourceMeta.repoURL, err)
488 }
Jonathan Amsterdam778864e2019-10-02 07:35:31 -0400489 } else {
490 // Use the repo from the template, not the original one.
491 repoURL = "https://" + repo
492 }
Jonathan Amsterdam081e3bd2019-09-28 15:43:15 -0400493 }
494 dir := strings.TrimPrefix(strings.TrimPrefix(modulePath, sourceMeta.repoRootPrefix), "/")
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400495 commit, isHash := commitFromVersion(version, dir)
496 if transformCommit != nil {
497 commit = transformCommit(commit, isHash)
498 }
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400499 return &Info{
Jonathan Amsterdam4aff7c42020-07-15 21:39:34 -0400500 repoURL: strings.TrimSuffix(repoURL, "/"),
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400501 moduleDir: dir,
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400502 commit: commit,
Jonathan Amsterdam081e3bd2019-09-28 15:43:15 -0400503 templates: templates,
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400504 }, nil
505}
506
Jonathan Amsterdama9ff35d2021-01-21 11:02:52 -0500507// List of template regexps and their corresponding likely templates,
508// used by matchLegacyTemplates below.
509var legacyTemplateMatches = []struct {
510 fileRegexp *regexp.Regexp
511 templates urlTemplates
512 transformCommit transformCommitFunc
513}{
514 {
515 regexp.MustCompile(`/src/branch/\w+\{/dir\}/\{file\}#L\{line\}$`),
516 giteaURLTemplates, giteaTransformCommit,
517 },
518 {
519 regexp.MustCompile(`/src/\w+\{/dir\}/\{file\}#L\{line\}$`),
520 giteaURLTemplates, nil,
521 },
522 {
523 regexp.MustCompile(`/-/blob/\w+\{/dir\}/\{file\}#L\{line\}$`),
Jonathan Amsterdam4d7057b2022-01-05 11:57:08 -0500524 gitlabURLTemplates, nil,
Jonathan Amsterdama9ff35d2021-01-21 11:02:52 -0500525 },
526 {
527 regexp.MustCompile(`/tree\{/dir\}/\{file\}#n\{line\}$`),
528 fdioURLTemplates, fdioTransformCommit,
529 },
530}
531
532// matchLegacyTemplates matches the templates from the go-source meta tag
533// against some known patterns to guess the version-aware URL templates. If it
534// can't find a match, it falls back using the go-source templates with some
535// small replacements. These will not be version-aware but will still serve
536// source at a fixed commit, which is better than nothing.
537func matchLegacyTemplates(ctx context.Context, sm *sourceMeta) (_ urlTemplates, transformCommit transformCommitFunc) {
538 if sm.fileTemplate == "" {
539 return urlTemplates{}, nil
540 }
541 for _, ltm := range legacyTemplateMatches {
542 if ltm.fileRegexp.MatchString(sm.fileTemplate) {
543 return ltm.templates, ltm.transformCommit
544 }
545 }
546 log.Infof(ctx, "matchLegacyTemplates: no matches for repo URL %q; replacing", sm.repoURL)
547 rep := strings.NewReplacer(
548 "{/dir}/{file}", "/{file}",
549 "{dir}/{file}", "{file}",
550 "{/dir}", "/{dir}")
551 line := rep.Replace(sm.fileTemplate)
552 file := line
553 if i := strings.LastIndexByte(line, '#'); i > 0 {
554 file = line[:i]
555 }
556 return urlTemplates{
557 Repo: sm.repoURL,
558 Directory: rep.Replace(sm.dirTemplate),
559 File: file,
560 Line: line,
561 }, nil
562}
563
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400564// adjustVersionedModuleDirectory changes info.moduleDir if necessary to
565// correctly reflect the repo structure. info.moduleDir will be wrong if it has
566// a suffix "/vN" for N > 1, and the repo uses the "major branch" convention,
567// where modules at version 2 and higher live on branches rather than
568// subdirectories. See https://research.swtch.com/vgo-module for a discussion of
569// the "major branch" vs. "major subdirectory" conventions for organizing a
570// repo.
Julie Qiubdcbfe02020-04-03 13:15:18 -0400571func adjustVersionedModuleDirectory(ctx context.Context, client *Client, info *Info) {
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400572 dirWithoutVersion := removeVersionSuffix(info.moduleDir)
573 if info.moduleDir == dirWithoutVersion {
574 return
575 }
576 // moduleDir does have a "/vN" for N > 1. To see if that is the actual directory,
577 // fetch the go.mod file from it.
Julie Qiubdcbfe02020-04-03 13:15:18 -0400578 res, err := client.doURL(ctx, "HEAD", info.FileURL("go.mod"), true)
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400579 // On any failure, assume that the right directory is the one without the version.
580 if err != nil {
581 info.moduleDir = dirWithoutVersion
582 } else {
583 res.Body.Close()
584 }
585}
586
Jonathan Amsterdam778864e2019-10-02 07:35:31 -0400587// removeHTTPScheme removes an initial "http://" or "https://" from url.
588// The result can be used to match against our static patterns.
589// If the URL uses a different scheme, it won't be removed and it won't
590// match any patterns, as intended.
591func removeHTTPScheme(url string) string {
592 for _, prefix := range []string{"https://", "http://"} {
593 if strings.HasPrefix(url, prefix) {
594 return url[len(prefix):]
595 }
596 }
597 return url
598}
599
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400600// removeVersionSuffix returns s with "/vN" removed if N is an integer > 1.
601// Otherwise it returns s.
602func removeVersionSuffix(s string) string {
603 dir, base := path.Split(s)
604 if !strings.HasPrefix(base, "v") {
605 return s
606 }
607 if n, err := strconv.Atoi(base[1:]); err != nil || n < 2 {
608 return s
609 }
610 return strings.TrimSuffix(dir, "/")
611}
612
Jonathan Amsterdama9ff35d2021-01-21 11:02:52 -0500613type transformCommitFunc func(commit string, isHash bool) string
614
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400615// Patterns for determining repo and URL templates from module paths or repo
616// URLs. Each regexp must match a prefix of the target string, and must have a
617// group named "repo".
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400618var patterns = []struct {
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400619 pattern string // uncompiled regexp
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400620 templates urlTemplates
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400621 re *regexp.Regexp
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400622 // transformCommit may alter the commit before substitution
Jonathan Amsterdama9ff35d2021-01-21 11:02:52 -0500623 transformCommit transformCommitFunc
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400624}{
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400625 {
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400626 pattern: `^(?P<repo>github\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`,
627 templates: githubURLTemplates,
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400628 },
629 {
Jonathan Amsterdam22dfb1f2021-09-22 15:00:57 -0400630 // Assume that any site beginning with "github." works like github.com.
631 pattern: `^(?P<repo>github\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
632 templates: githubURLTemplates,
633 },
634 {
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400635 pattern: `^(?P<repo>bitbucket\.org/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`,
636 templates: bitbucketURLTemplates,
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400637 },
Jonathan Amsterdam1fa0ab82019-09-30 11:14:34 -0400638 {
Jonathan Amsterdam4d7057b2022-01-05 11:57:08 -0500639 // Gitlab repos can have multiple path components.
640 pattern: `^(?P<repo>gitlab\.com/[^.]+)(\.git|$)`,
641 templates: gitlabURLTemplates,
Jonathan Amsterdam1fa0ab82019-09-30 11:14:34 -0400642 },
Jonathan Amsterdam39fa46f2019-10-12 12:44:45 -0400643 {
Jonathan Amsterdam91725502020-07-29 09:32:06 -0400644 // Assume that any site beginning with "gitlab." works like gitlab.com.
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400645 pattern: `^(?P<repo>gitlab\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
Jonathan Amsterdam4d7057b2022-01-05 11:57:08 -0500646 templates: gitlabURLTemplates,
Jonathan Amsterdam39fa46f2019-10-12 12:44:45 -0400647 },
648 {
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400649 pattern: `^(?P<repo>gitee\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
650 templates: githubURLTemplates,
Jonathan Amsterdam39fa46f2019-10-12 12:44:45 -0400651 },
Jonathan Amsterdamd76e0252020-07-27 18:17:24 -0400652 {
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400653 pattern: `^(?P<repo>git\.sr\.ht/~[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`,
654 templates: urlTemplates{
Jonathan Amsterdamd76e0252020-07-27 18:17:24 -0400655 Directory: "{repo}/tree/{commit}/{dir}",
656 File: "{repo}/tree/{commit}/{file}",
657 Line: "{repo}/tree/{commit}/{file}#L{line}",
658 Raw: "{repo}/blob/{commit}/{file}",
659 },
660 },
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400661 {
Jonathan Amsterdama9ff35d2021-01-21 11:02:52 -0500662 pattern: `^(?P<repo>git\.fd\.io/[a-z0-9A-Z_.\-]+)`,
663 templates: fdioURLTemplates,
664 transformCommit: fdioTransformCommit,
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400665 },
666 {
Jonathan Amsterdama9ff35d2021-01-21 11:02:52 -0500667 pattern: `^(?P<repo>git\.pirl\.io/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`,
Jonathan Amsterdam4d7057b2022-01-05 11:57:08 -0500668 templates: gitlabURLTemplates,
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400669 },
670 {
Grégoire Détrezd90bbc52024-08-29 13:25:42 +0200671 pattern: `^(?P<repo>git\.glasklar\.is/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`,
672 templates: gitlabURLTemplates,
673 },
674 {
Jonathan Amsterdamb7cb5a22020-07-29 06:03:33 -0400675 pattern: `^(?P<repo>gitea\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
676 templates: giteaURLTemplates,
677 transformCommit: giteaTransformCommit,
678 },
679 {
Jonathan Amsterdam91725502020-07-29 09:32:06 -0400680 // Assume that any site beginning with "gitea." works like gitea.com.
Jonathan Amsterdamb7cb5a22020-07-29 06:03:33 -0400681 pattern: `^(?P<repo>gitea\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
682 templates: giteaURLTemplates,
683 transformCommit: giteaTransformCommit,
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400684 },
Jonathan Amsterdam9635aae2020-07-29 09:11:28 -0400685 {
686 pattern: `^(?P<repo>go\.isomorphicgo\.org/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
687 templates: giteaURLTemplates,
688 transformCommit: giteaTransformCommit,
689 },
690 {
691 pattern: `^(?P<repo>git\.openprivacy\.ca/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
692 templates: giteaURLTemplates,
693 transformCommit: giteaTransformCommit,
694 },
695 {
Daniel Jakotsd116adb2022-04-22 20:29:32 +0000696 pattern: `^(?P<repo>codeberg\.org/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
697 templates: giteaURLTemplates,
698 transformCommit: giteaTransformCommit,
699 },
700 {
Jonathan Amsterdam9635aae2020-07-29 09:11:28 -0400701 pattern: `^(?P<repo>gogs\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
702 // Gogs uses the same basic structure as Gitea, but omits the type of
703 // commit ("tag" or "commit"), so we don't need a transformCommit
704 // function. Gogs does not support short hashes, but we create those
705 // URLs anyway. See gogs/gogs#6242.
706 templates: giteaURLTemplates,
707 },
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -0400708 {
709 pattern: `^(?P<repo>dmitri\.shuralyov\.com\/.+)$`,
710 templates: urlTemplates{
711 Repo: "{repo}/...",
712 Directory: "https://gotools.org/{importPath}?rev={commit}",
713 File: "https://gotools.org/{importPath}?rev={commit}#{base}",
714 Line: "https://gotools.org/{importPath}?rev={commit}#{base}-L{line}",
715 },
716 },
Jonathan Amsterdam7d0f1a12021-03-22 10:08:42 -0400717 {
718 pattern: `^(?P<repo>blitiri\.com\.ar/go/.+)$`,
719 templates: urlTemplates{
720 Repo: "{repo}",
721 Directory: "{repo}/b/master/t/{dir}",
722 File: "{repo}/b/master/t/{dir}f={file}.html",
723 Line: "{repo}/b/master/t/{dir}f={file}.html#line-{line}",
724 },
725 },
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -0400726
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400727 // Patterns that match the general go command pattern, where they must have
728 // a ".git" repo suffix in an import path. If matching a repo URL from a meta tag,
729 // there is no ".git".
730 {
Jonathan Amsterdam8406a912020-12-14 10:30:57 -0500731 pattern: `^(?P<repo>[^.]+\.googlesource\.com/[^.]+)(\.git|$)`,
732 templates: googlesourceURLTemplates,
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400733 },
Jonathan Amsterdam2696ea72019-10-14 17:18:52 -0400734 {
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400735 pattern: `^(?P<repo>git\.apache\.org/[^.]+)(\.git|$)`,
736 templates: githubURLTemplates,
Jonathan Amsterdam2696ea72019-10-14 17:18:52 -0400737 },
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400738 // General syntax for the go command. We can extract the repo and directory, but
739 // we don't know the URL templates.
740 // Must be last in this list.
741 {
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400742 pattern: `(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?(/~?[A-Za-z0-9_.\-]+)+?)\.(bzr|fossil|git|hg|svn)`,
743 templates: urlTemplates{},
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400744 },
745}
746
747func init() {
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400748 for i := range patterns {
749 re := regexp.MustCompile(patterns[i].pattern)
Jonathan Amsterdamb7cb5a22020-07-29 06:03:33 -0400750 // The pattern regexp must contain a group named "repo".
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400751 found := false
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400752 for _, n := range re.SubexpNames() {
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400753 if n == "repo" {
754 found = true
755 break
756 }
757 }
758 if !found {
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400759 panic(fmt.Sprintf("pattern %s missing <repo> group", patterns[i].pattern))
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400760 }
Jonathan Amsterdam3a6b0012020-07-28 08:22:38 -0400761 patterns[i].re = re
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400762 }
763}
764
Jonathan Amsterdamb7cb5a22020-07-29 06:03:33 -0400765// giteaTransformCommit transforms commits for the Gitea code hosting system.
766func giteaTransformCommit(commit string, isHash bool) string {
767 // Hashes use "commit", tags use "tag".
Jonathan Amsterdam50e17862020-12-03 06:12:01 -0500768 // Short hashes are supported as of v1.14.0.
Jonathan Amsterdamb7cb5a22020-07-29 06:03:33 -0400769 if isHash {
770 return "commit/" + commit
771 }
772 return "tag/" + commit
773}
774
Jonathan Amsterdama9ff35d2021-01-21 11:02:52 -0500775func fdioTransformCommit(commit string, isHash bool) string {
776 // hashes use "?id=", tags use "?h="
777 p := "h"
778 if isHash {
779 p = "id"
780 }
781 return fmt.Sprintf("%s=%s", p, commit)
782}
783
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400784// urlTemplates describes how to build URLs from bits of source information.
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400785// The fields are exported for JSON encoding.
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -0400786//
787// The template variables are:
788//
Russ Cox8ea409f2022-04-11 13:12:03 -0400789// - {repo} - Repository URL with "https://" prefix ("https://example.com/myrepo").
790// - {importPath} - Package import path ("example.com/myrepo/mypkg").
791// - {commit} - Tag name or commit hash corresponding to version ("v0.1.0" or "1234567890ab").
792// - {dir} - Path to directory of the package, relative to repo root ("mypkg").
793// - {file} - Path to file containing the identifier, relative to repo root ("mypkg/file.go").
794// - {base} - Base name of file containing the identifier, including file extension ("file.go").
795// - {line} - Line number for the identifier ("41").
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400796type urlTemplates struct {
Dmitri Shuralyov21b6d172020-08-03 01:17:46 -0400797 Repo string `json:",omitempty"` // Optional URL template for the repository home page, with {repo}. If left empty, a default template "{repo}" is used.
798 Directory string // URL template for a directory, with {repo}, {importPath}, {commit}, {dir}.
799 File string // URL template for a file, with {repo}, {importPath}, {commit}, {file}, {base}.
800 Line string // URL template for a line, with {repo}, {importPath}, {commit}, {file}, {base}, {line}.
801 Raw string // Optional URL template for the raw contents of a file, with {repo}, {commit}, {file}.
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400802}
803
Jonathan Amsterdama2fc41b2019-10-14 19:40:13 -0400804var (
805 githubURLTemplates = urlTemplates{
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400806 Directory: "{repo}/tree/{commit}/{dir}",
807 File: "{repo}/blob/{commit}/{file}",
808 Line: "{repo}/blob/{commit}/{file}#L{line}",
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400809 Raw: "{repo}/raw/{commit}/{file}",
810 }
811
812 bitbucketURLTemplates = urlTemplates{
813 Directory: "{repo}/src/{commit}/{dir}",
814 File: "{repo}/src/{commit}/{file}",
815 Line: "{repo}/src/{commit}/{file}#lines-{line}",
816 Raw: "{repo}/raw/{commit}/{file}",
Jonathan Amsterdama2fc41b2019-10-14 19:40:13 -0400817 }
Jonathan Amsterdamb7cb5a22020-07-29 06:03:33 -0400818 giteaURLTemplates = urlTemplates{
819 Directory: "{repo}/src/{commit}/{dir}",
820 File: "{repo}/src/{commit}/{file}",
821 Line: "{repo}/src/{commit}/{file}#L{line}",
822 Raw: "{repo}/raw/{commit}/{file}",
823 }
Jonathan Amsterdam8406a912020-12-14 10:30:57 -0500824 googlesourceURLTemplates = urlTemplates{
825 Directory: "{repo}/+/{commit}/{dir}",
826 File: "{repo}/+/{commit}/{file}",
827 Line: "{repo}/+/{commit}/{file}#{line}",
828 // Gitiles has no support for serving raw content at this time.
829 }
Jonathan Amsterdam4d7057b2022-01-05 11:57:08 -0500830 gitlabURLTemplates = urlTemplates{
Jonathan Amsterdama9ff35d2021-01-21 11:02:52 -0500831 Directory: "{repo}/-/tree/{commit}/{dir}",
832 File: "{repo}/-/blob/{commit}/{file}",
833 Line: "{repo}/-/blob/{commit}/{file}#L{line}",
834 Raw: "{repo}/-/raw/{commit}/{file}",
835 }
836 fdioURLTemplates = urlTemplates{
837 Directory: "{repo}/tree/{dir}?{commit}",
838 File: "{repo}/tree/{file}?{commit}",
839 Line: "{repo}/tree/{file}?{commit}#n{line}",
840 Raw: "{repo}/plain/{file}?{commit}",
841 }
Julie Qiu080753e2021-05-11 15:13:29 -0400842 csopensourceTemplates = urlTemplates{
843 Directory: "{repo}/+/{commit}:{dir}",
844 File: "{repo}/+/{commit}:{file}",
845 Line: "{repo}/+/{commit}:{file};l={line}",
846 // Gitiles has no support for serving raw content at this time.
847 }
Jonathan Amsterdama2fc41b2019-10-14 19:40:13 -0400848)
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400849
850// commitFromVersion returns a string that refers to a commit corresponding to version.
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400851// It also reports whether the returned value is a commit hash.
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400852// The string may be a tag, or it may be the hash or similar unique identifier of a commit.
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400853// The second argument is the module path relative to the repo root.
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400854func commitFromVersion(vers, relativeModulePath string) (commit string, isHash bool) {
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400855 // Commit for the module: either a sha for pseudoversions, or a tag.
Jonathan Amsterdam8c2136a2019-10-15 09:02:41 -0400856 v := strings.TrimSuffix(vers, "+incompatible")
857 if version.IsPseudo(v) {
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400858 // Use the commit hash at the end.
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400859 return v[strings.LastIndex(v, "-")+1:], true
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400860 } else {
Jonathan Amsterdam860515b2019-09-30 13:36:59 -0400861 // The tags for a nested module begin with the relative module path of the module,
862 // removing a "/vN" suffix if N > 1.
863 prefix := removeVersionSuffix(relativeModulePath)
864 if prefix != "" {
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400865 return prefix + "/" + v, false
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400866 }
Jonathan Amsterdam43480f22020-07-28 12:22:52 -0400867 return v, false
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400868 }
869}
870
Jonathan Amsterdamff8cbf22021-02-01 17:41:51 -0500871// trimVCSSuffix removes a VCS suffix from a repo URL in selected cases.
872//
873// The Go command allows a VCS suffix on a repo, like github.com/foo/bar.git. But
874// some code hosting sites don't support all paths constructed from such URLs.
875// For example, GitHub will redirect github.com/foo/bar.git to github.com/foo/bar,
876// but will 404 on github.com/goo/bar.git/tree/master and any other URL with a
877// non-empty path.
878//
879// To be conservative, we remove the suffix only in cases where we know it's
880// wrong.
881func trimVCSSuffix(repoURL string) string {
882 if !strings.HasSuffix(repoURL, ".git") {
883 return repoURL
884 }
885 if strings.HasPrefix(repoURL, "https://github.com/") || strings.HasPrefix(repoURL, "https://gitlab.com/") {
886 return strings.TrimSuffix(repoURL, ".git")
887 }
888 return repoURL
889}
890
Jonathan Amsterdam00ed4e92019-09-27 16:26:07 -0400891// The following code copied from cmd/go/internal/get:
892
893// expand rewrites s to replace {k} with match[k] for each key k in match.
894func expand(s string, match map[string]string) string {
895 // We want to replace each match exactly once, and the result of expansion
896 // must not depend on the iteration order through the map.
897 // A strings.Replacer has exactly the properties we're looking for.
898 oldNew := make([]string, 0, 2*len(match))
899 for k, v := range match {
900 oldNew = append(oldNew, "{"+k+"}", v)
901 }
902 return strings.NewReplacer(oldNew...).Replace(s)
903}
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400904
Jonathan Amsterdam77e252b2019-10-16 14:43:06 -0400905// NewGitHubInfo creates a source.Info with GitHub URL templates.
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400906// It is for testing only.
907func NewGitHubInfo(repoURL, moduleDir, commit string) *Info {
908 return &Info{
Jonathan Amsterdamff8cbf22021-02-01 17:41:51 -0500909 repoURL: trimVCSSuffix(repoURL),
Jonathan Amsterdame2f17ff2019-10-15 06:00:58 -0400910 moduleDir: moduleDir,
911 commit: commit,
912 templates: githubURLTemplates,
913 }
914}
Jonathan Amsterdam8406a912020-12-14 10:30:57 -0500915
Michael Matloobf159e612023-07-05 14:02:46 -0400916// NewStdlibInfoForTest returns a source.Info for the standard library at the given
Jonathan Amsterdam8406a912020-12-14 10:30:57 -0500917// semantic version. It panics if the version does not correspond to a Go release
918// tag. It is for testing only.
Michael Matloobf159e612023-07-05 14:02:46 -0400919func NewStdlibInfoForTest(version string) *Info {
920 info, err := NewStdlibInfo(version)
Jonathan Amsterdam8406a912020-12-14 10:30:57 -0500921 if err != nil {
922 panic(err)
923 }
924 return info
925}
Jonathan Amsterdamf3da34a2021-09-05 18:21:11 -0400926
927// FilesInfo returns an Info that links to a path in the server's /files
928// namespace. The same path needs to be installed via frontend.Server.InstallFS.
929func FilesInfo(dir string) *Info {
930 // The repo and directory patterns need a final slash. Without it,
931 // http.FileServer redirects instead of serving the directory contents, with
932 // confusing results.
933 return &Info{
Michael Matloobf36927e2023-07-10 13:26:46 -0400934 repoURL: path.Join("/files", filepath.ToSlash(dir)),
Jonathan Amsterdamf3da34a2021-09-05 18:21:11 -0400935 templates: urlTemplates{
936 Repo: "{repo}/",
937 Directory: "{repo}/{dir}/",
938 File: "{repo}/{file}",
939 Line: "{repo}/{file}#L{line}", // not supported now, but maybe someday
940 Raw: "{repo}/{file}",
941 },
942 }
943}