-
Notifications
You must be signed in to change notification settings - Fork 185
/
Copy pathsource.go
722 lines (673 loc) · 25.2 KB
/
source.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package source constructs public URLs that link to the source files in a module. It
// can be used to build references to Go source code, or to any other files in a
// module.
//
// Of course, the module zip file contains all the files in the module. This
// package attempts to find the origin of the zip file, in a repository that is
// publicly readable, and constructs links to that repo. While a module zip file
// could in theory come from anywhere, including a non-public location, this
// package recognizes standard module path patterns and construct repository
// URLs from them, like the go command does.
package source
//
// Much of this code was adapted from
// https://go.googlesource.com/gddo/+/refs/heads/master/gosrc
// and
// https://go.googlesource.com/go/+/refs/heads/master/src/cmd/go/internal/get
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"regexp"
"strconv"
"strings"
"time"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/trace"
"golang.org/x/net/context/ctxhttp"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/internal/version"
)
// Info holds source information about a module, used to generate URLs referring
// to directories, files and lines.
type Info struct {
repoURL string // URL of repo containing module; exported for DB schema compatibility
moduleDir string // directory of module relative to repo root
commit string // tag or ID of commit corresponding to version
templates urlTemplates // for building URLs
}
// RepoURL returns a URL for the home page of the repository.
func (i *Info) RepoURL() string {
if i == nil {
return ""
}
if i.templates.Repo == "" {
// The default repo template is just "{repo}".
return i.repoURL
}
return expand(i.templates.Repo, map[string]string{
"repo": i.repoURL,
})
}
// ModuleURL returns a URL for the home page of the module.
func (i *Info) ModuleURL() string {
return i.DirectoryURL("")
}
// DirectoryURL returns a URL for a directory relative to the module's home directory.
func (i *Info) DirectoryURL(dir string) string {
if i == nil {
return ""
}
return strings.TrimSuffix(expand(i.templates.Directory, map[string]string{
"repo": i.repoURL,
"importPath": path.Join(strings.TrimPrefix(i.repoURL, "https://"), dir),
"commit": i.commit,
"dir": path.Join(i.moduleDir, dir),
}), "/")
}
// FileURL returns a URL for a file whose pathname is relative to the module's home directory.
func (i *Info) FileURL(pathname string) string {
if i == nil {
return ""
}
dir, base := path.Split(pathname)
return expand(i.templates.File, map[string]string{
"repo": i.repoURL,
"importPath": path.Join(strings.TrimPrefix(i.repoURL, "https://"), dir),
"commit": i.commit,
"file": path.Join(i.moduleDir, pathname),
"base": base,
})
}
// LineURL returns a URL referring to a line in a file relative to the module's home directory.
func (i *Info) LineURL(pathname string, line int) string {
if i == nil {
return ""
}
dir, base := path.Split(pathname)
return expand(i.templates.Line, map[string]string{
"repo": i.repoURL,
"importPath": path.Join(strings.TrimPrefix(i.repoURL, "https://"), dir),
"commit": i.commit,
"file": path.Join(i.moduleDir, pathname),
"base": base,
"line": strconv.Itoa(line),
})
}
// UsesURL returns a URL redirecting to Sourcegraph site showing the usage for a particular component of the code.
func (i *Info) UsesURL(modulePath string, importPath string, defParts []string) string {
sourcegraphBaseURL := "https://sourcegraph.com/-/godoc/refs?"
var def string
switch len(defParts) {
case 1:
def = defParts[0]
case 2:
typeName, methodName := defParts[0], defParts[1]
typeName = strings.TrimPrefix(typeName, "*")
def = typeName + "/" + methodName
default:
panic(fmt.Errorf("%v defParts, want 1 or 2", len(defParts)))
}
repo := strings.TrimPrefix(modulePath, "https://")
pkg := strings.TrimPrefix(importPath, "https://")
q := url.Values{
"repo": []string{repo},
"pkg": []string{pkg},
"def": []string{def},
"source": []string{"pkgsite"},
}
return sourcegraphBaseURL + q.Encode()
}
// RawURL returns a URL referring to the raw contents of a file relative to the
// module's home directory.
func (i *Info) RawURL(pathname string) string {
if i == nil {
return ""
}
// Some templates don't support raw content serving.
if i.templates.Raw == "" {
return ""
}
moduleDir := i.moduleDir
// Special case: the standard library's source module path is set to "src",
// which is correct for source file links. But the README is at the repo
// root, not in the src directory. In other words,
// Module.Units[0].Readme.FilePath is not relative to
// Module.Units[0].SourceInfo.moduleDir, as it is for every other module.
// Correct for that here.
if i.repoURL == stdlib.GoSourceRepoURL {
moduleDir = ""
}
return expand(i.templates.Raw, map[string]string{
"repo": i.repoURL,
"commit": i.commit,
"file": path.Join(moduleDir, pathname),
})
}
// map of common urlTemplates
var urlTemplatesByKind = map[string]urlTemplates{
"github": githubURLTemplates,
"gitlab": githubURLTemplates, // preserved for backwards compatibility (DB still has source_info->Kind = "gitlab")
"bitbucket": bitbucketURLTemplates,
}
// jsonInfo is a Go struct describing the JSON structure of an INFO.
type jsonInfo struct {
RepoURL string
ModuleDir string
Commit string
// Store common templates efficiently by setting this to a short string
// we look up in a map. If Kind != "", then Templates == nil.
Kind string `json:",omitempty"`
Templates *urlTemplates `json:",omitempty"`
}
// ToJSONForDB returns the Info encoded for storage in the database.
func (i *Info) MarshalJSON() (_ []byte, err error) {
defer derrors.Wrap(&err, "MarshalJSON")
ji := &jsonInfo{
RepoURL: i.repoURL,
ModuleDir: i.moduleDir,
Commit: i.commit,
}
// Store common templates efficiently, by name.
for kind, templs := range urlTemplatesByKind {
if i.templates == templs {
ji.Kind = kind
break
}
}
// We used to use different templates for GitHub and GitLab. Now that
// they're the same, prefer "github" for consistency (map random iteration
// order means we could get either here).
if ji.Kind == "gitlab" {
ji.Kind = "github"
}
if ji.Kind == "" && i.templates != (urlTemplates{}) {
ji.Templates = &i.templates
}
return json.Marshal(ji)
}
func (i *Info) UnmarshalJSON(data []byte) (err error) {
defer derrors.Wrap(&err, "UnmarshalJSON(data)")
var ji jsonInfo
if err := json.Unmarshal(data, &ji); err != nil {
return err
}
i.repoURL = ji.RepoURL
i.moduleDir = ji.ModuleDir
i.commit = ji.Commit
if ji.Kind != "" {
i.templates = urlTemplatesByKind[ji.Kind]
} else if ji.Templates != nil {
i.templates = *ji.Templates
}
return nil
}
type Client struct {
// client used for HTTP requests. It is mutable for testing purposes.
httpClient *http.Client
}
// New constructs a *Client using the provided timeout.
func NewClient(timeout time.Duration) *Client {
return &Client{
httpClient: &http.Client{
Transport: &ochttp.Transport{},
Timeout: timeout,
},
}
}
// doURL makes an HTTP request using the given url and method. It returns an
// error if the request returns an error. If only200 is true, it also returns an
// error if any status code other than 200 is returned.
func (c *Client) doURL(ctx context.Context, method, url string, only200 bool) (_ *http.Response, err error) {
defer derrors.Wrap(&err, "doURL(ctx, client, %q, %q)", method, url)
if c == nil || c.httpClient == nil {
return nil, fmt.Errorf("c.httpClient cannot be nil")
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
resp, err := ctxhttp.Do(ctx, c.httpClient, req)
if err != nil {
return nil, err
}
if only200 && resp.StatusCode != 200 {
resp.Body.Close()
return nil, fmt.Errorf("status %s", resp.Status)
}
return resp, nil
}
// ModuleInfo determines the repository corresponding to the module path. It
// returns a URL to that repo, as well as the directory of the module relative
// to the repo root.
//
// ModuleInfo may fetch from arbitrary URLs, so it can be slow.
func ModuleInfo(ctx context.Context, client *Client, modulePath, version string) (info *Info, err error) {
defer derrors.Wrap(&err, "source.ModuleInfo(ctx, %q, %q)", modulePath, version)
ctx, span := trace.StartSpan(ctx, "source.ModuleInfo")
defer span.End()
if modulePath == stdlib.ModulePath {
commit, err := stdlib.TagForVersion(version)
if err != nil {
return nil, err
}
return &Info{
repoURL: stdlib.GoSourceRepoURL,
moduleDir: stdlib.Directory(version),
commit: commit,
templates: githubURLTemplates,
}, nil
}
repo, relativeModulePath, templates, transformCommit, err := matchStatic(modulePath)
if err != nil {
info, err = moduleInfoDynamic(ctx, client, modulePath, version)
if err != nil {
return nil, err
}
} else {
commit, isHash := commitFromVersion(version, relativeModulePath)
if transformCommit != nil {
commit = transformCommit(commit, isHash)
}
info = &Info{
repoURL: "https://" + repo,
moduleDir: relativeModulePath,
commit: commit,
templates: templates,
}
}
adjustVersionedModuleDirectory(ctx, client, info)
return info, nil
// TODO(golang/go#39627): support launchpad.net, including the special case
// in cmd/go/internal/get/vcs.go.
}
// matchStatic matches the given module or repo path against a list of known
// patterns. It returns the repo name, the module path relative to the repo
// root, and URL templates if there is a match.
//
// The relative module path may not be correct in all cases: it is wrong if it
// ends in a version that is not part of the repo directory structure, because
// the repo follows the "major branch" convention for versions 2 and above.
// E.g. this function could return "foo/v2", but the module files live under "foo"; the
// "/v2" is part of the module path (and the import paths of its packages) but
// is not a subdirectory. This mistake is corrected in adjustVersionedModuleDirectory,
// once we have all the information we need to fix it.
//
// repo + "/" + relativeModulePath is often, but not always, equal to
// moduleOrRepoPath. It is not when the argument is a module path that uses the
// go command's general syntax, which ends in a ".vcs" (e.g. ".git", ".hg") that
// is neither part of the repo nor the suffix. For example, if the argument is
// github.com/a/b/c
// then repo="github.com/a/b" and relativeModulePath="c"; together they make up the module path.
// But if the argument is
// example.com/a/b.git/c
// then repo="example.com/a/b" and relativeModulePath="c"; the ".git" is omitted, since it is neither
// part of the repo nor part of the relative path to the module within the repo.
func matchStatic(moduleOrRepoPath string) (repo, relativeModulePath string, _ urlTemplates, transformCommit func(string, bool) string, _ error) {
for _, pat := range patterns {
matches := pat.re.FindStringSubmatch(moduleOrRepoPath)
if matches == nil {
continue
}
var repo string
for i, n := range pat.re.SubexpNames() {
if n == "repo" {
repo = matches[i]
break
}
}
// Special case: git.apache.org has a go-import tag that points to
// github.com/apache, but it's not quite right (the repo prefix is
// missing a ".git"), so handle it here.
const apacheDomain = "git.apache.org/"
if strings.HasPrefix(repo, apacheDomain) {
repo = strings.Replace(repo, apacheDomain, "github.com/apache/", 1)
}
relativeModulePath = strings.TrimPrefix(moduleOrRepoPath, matches[0])
relativeModulePath = strings.TrimPrefix(relativeModulePath, "/")
return repo, relativeModulePath, pat.templates, pat.transformCommit, nil
}
return "", "", urlTemplates{}, nil, derrors.NotFound
}
// moduleInfoDynamic uses the go-import and go-source meta tags to construct an Info.
func moduleInfoDynamic(ctx context.Context, client *Client, modulePath, version string) (_ *Info, err error) {
defer derrors.Wrap(&err, "source.moduleInfoDynamic(ctx, client, %q, %q)", modulePath, version)
sourceMeta, err := fetchMeta(ctx, client, modulePath)
if err != nil {
return nil, err
}
// Don't check that the tag information at the repo root prefix is the same
// as in the module path. It was done for us by the proxy and/or go command.
// (This lets us merge information from the go-import and go-source tags.)
// sourceMeta contains some information about where the module's source lives. But there
// are some problems:
// - We may only have a go-import tag, not a go-source tag, so we don't have URL templates for
// building URLs to files and directories.
// - Even if we do have a go-source tag, its URL template format predates
// versioning, so the URL templates won't provide a way to specify a
// version or commit.
//
// We resolve these problems as follows:
// 1. First look at the repo URL from the tag. If that matches a known hosting site, use the
// URL templates corresponding to that site and ignore whatever's in the tag.
// 2. Then look at the URL templates to see if they match a known pattern, and use the templates
// from that pattern. For example, the meta tags for gopkg.in/yaml.v2 only mention github
// in the URL templates, like "https://github.com/go-yaml/yaml/tree/v2.2.3{/dir}". We can observe
// that that template begins with a known pattern--a GitHub repo, ignore the rest of it, and use the
// GitHub URL templates that we know.
repoURL := sourceMeta.repoURL
_, _, templates, transformCommit, _ := matchStatic(removeHTTPScheme(repoURL))
// If err != nil, templates will be the zero value, so we can ignore it (same just below).
if templates == (urlTemplates{}) {
var repo string
repo, _, templates, transformCommit, _ = matchStatic(removeHTTPScheme(sourceMeta.dirTemplate))
if templates == (urlTemplates{}) {
log.Infof(ctx, "no templates for repo URL %q from meta tag: err=%v", sourceMeta.repoURL, err)
} else {
// Use the repo from the template, not the original one.
repoURL = "https://" + repo
}
}
dir := strings.TrimPrefix(strings.TrimPrefix(modulePath, sourceMeta.repoRootPrefix), "/")
commit, isHash := commitFromVersion(version, dir)
if transformCommit != nil {
commit = transformCommit(commit, isHash)
}
return &Info{
repoURL: strings.TrimSuffix(repoURL, "/"),
moduleDir: dir,
commit: commit,
templates: templates,
}, nil
}
// adjustVersionedModuleDirectory changes info.moduleDir if necessary to
// correctly reflect the repo structure. info.moduleDir will be wrong if it has
// a suffix "/vN" for N > 1, and the repo uses the "major branch" convention,
// where modules at version 2 and higher live on branches rather than
// subdirectories. See https://research.swtch.com/vgo-module for a discussion of
// the "major branch" vs. "major subdirectory" conventions for organizing a
// repo.
func adjustVersionedModuleDirectory(ctx context.Context, client *Client, info *Info) {
dirWithoutVersion := removeVersionSuffix(info.moduleDir)
if info.moduleDir == dirWithoutVersion {
return
}
// moduleDir does have a "/vN" for N > 1. To see if that is the actual directory,
// fetch the go.mod file from it.
res, err := client.doURL(ctx, "HEAD", info.FileURL("go.mod"), true)
// On any failure, assume that the right directory is the one without the version.
if err != nil {
info.moduleDir = dirWithoutVersion
} else {
res.Body.Close()
}
}
// removeHTTPScheme removes an initial "http://" or "https://" from url.
// The result can be used to match against our static patterns.
// If the URL uses a different scheme, it won't be removed and it won't
// match any patterns, as intended.
func removeHTTPScheme(url string) string {
for _, prefix := range []string{"https://", "http://"} {
if strings.HasPrefix(url, prefix) {
return url[len(prefix):]
}
}
return url
}
// removeVersionSuffix returns s with "/vN" removed if N is an integer > 1.
// Otherwise it returns s.
func removeVersionSuffix(s string) string {
dir, base := path.Split(s)
if !strings.HasPrefix(base, "v") {
return s
}
if n, err := strconv.Atoi(base[1:]); err != nil || n < 2 {
return s
}
return strings.TrimSuffix(dir, "/")
}
// Patterns for determining repo and URL templates from module paths or repo
// URLs. Each regexp must match a prefix of the target string, and must have a
// group named "repo".
var patterns = []struct {
pattern string // uncompiled regexp
templates urlTemplates
re *regexp.Regexp
// transformCommit may alter the commit before substitution
transformCommit func(commit string, isHash bool) string
}{
{
pattern: `^(?P<repo>github\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`,
templates: githubURLTemplates,
},
{
pattern: `^(?P<repo>bitbucket\.org/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`,
templates: bitbucketURLTemplates,
},
{
pattern: `^(?P<repo>gitlab\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`,
templates: githubURLTemplates,
},
{
// Assume that any site beginning with "gitlab." works like gitlab.com.
pattern: `^(?P<repo>gitlab\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
templates: githubURLTemplates,
},
{
pattern: `^(?P<repo>gitee\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
templates: githubURLTemplates,
},
{
pattern: `^(?P<repo>git\.sr\.ht/~[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`,
templates: urlTemplates{
Directory: "{repo}/tree/{commit}/{dir}",
File: "{repo}/tree/{commit}/{file}",
Line: "{repo}/tree/{commit}/{file}#L{line}",
Raw: "{repo}/blob/{commit}/{file}",
},
},
{
pattern: `^(?P<repo>git\.fd\.io/[a-z0-9A-Z_.\-]+)`,
templates: urlTemplates{
Directory: "{repo}/tree/{dir}?{commit}",
File: "{repo}/tree/{file}?{commit}",
Line: "{repo}/tree/{file}?{commit}#n{line}",
Raw: "{repo}/plain/{file}?{commit}",
},
transformCommit: func(commit string, isHash bool) string {
// hashes use "?id=", tags use "?h="
p := "h"
if isHash {
p = "id"
}
return fmt.Sprintf("%s=%s", p, commit)
},
},
{
pattern: `^(?P<repo>git\.pirl\.io/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)`,
templates: urlTemplates{
Directory: "{repo}/-/tree/{commit}/{dir}",
File: "{repo}/-/blob/{commit}/{file}",
Line: "{repo}/-/blob/{commit}/{file}#L{line}",
Raw: "{repo}/-/raw/{commit}/{file}",
},
},
{
pattern: `^(?P<repo>gitea\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
templates: giteaURLTemplates,
transformCommit: giteaTransformCommit,
},
{
// Assume that any site beginning with "gitea." works like gitea.com.
pattern: `^(?P<repo>gitea\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
templates: giteaURLTemplates,
transformCommit: giteaTransformCommit,
},
{
pattern: `^(?P<repo>go\.isomorphicgo\.org/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
templates: giteaURLTemplates,
transformCommit: giteaTransformCommit,
},
{
pattern: `^(?P<repo>git\.openprivacy\.ca/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
templates: giteaURLTemplates,
transformCommit: giteaTransformCommit,
},
{
pattern: `^(?P<repo>gogs\.[a-z0-9A-Z.-]+/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(\.git|$)`,
// Gogs uses the same basic structure as Gitea, but omits the type of
// commit ("tag" or "commit"), so we don't need a transformCommit
// function. Gogs does not support short hashes, but we create those
// URLs anyway. See gogs/gogs#6242.
templates: giteaURLTemplates,
},
{
pattern: `^(?P<repo>dmitri\.shuralyov\.com\/.+)$`,
templates: urlTemplates{
Repo: "{repo}/...",
Directory: "https://gotools.org/{importPath}?rev={commit}",
File: "https://gotools.org/{importPath}?rev={commit}#{base}",
Line: "https://gotools.org/{importPath}?rev={commit}#{base}-L{line}",
},
},
// Patterns that match the general go command pattern, where they must have
// a ".git" repo suffix in an import path. If matching a repo URL from a meta tag,
// there is no ".git".
{
pattern: `^(?P<repo>[^.]+\.googlesource\.com/[^.]+)(\.git|$)`,
templates: urlTemplates{
Directory: "{repo}/+/{commit}/{dir}",
File: "{repo}/+/{commit}/{file}",
Line: "{repo}/+/{commit}/{file}#{line}",
// Gitiles has no support for serving raw content at this time.
},
},
{
pattern: `^(?P<repo>git\.apache\.org/[^.]+)(\.git|$)`,
templates: githubURLTemplates,
},
// General syntax for the go command. We can extract the repo and directory, but
// we don't know the URL templates.
// Must be last in this list.
{
pattern: `(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?(/~?[A-Za-z0-9_.\-]+)+?)\.(bzr|fossil|git|hg|svn)`,
templates: urlTemplates{},
},
}
func init() {
for i := range patterns {
re := regexp.MustCompile(patterns[i].pattern)
// The pattern regexp must contain a group named "repo".
found := false
for _, n := range re.SubexpNames() {
if n == "repo" {
found = true
break
}
}
if !found {
panic(fmt.Sprintf("pattern %s missing <repo> group", patterns[i].pattern))
}
patterns[i].re = re
}
}
// giteaTransformCommit transforms commits for the Gitea code hosting system.
func giteaTransformCommit(commit string, isHash bool) string {
// Hashes use "commit", tags use "tag".
// Short hashes aren't currently supported, but we build the URL
// anyway in the hope that someday they will be.
if isHash {
return "commit/" + commit
}
return "tag/" + commit
}
// urlTemplates describes how to build URLs from bits of source information.
// The fields are exported for JSON encoding.
//
// The template variables are:
//
// • {repo} - Repository URL with "https://" prefix ("https://example.com/myrepo").
// • {importPath} - Package import path ("example.com/myrepo/mypkg").
// • {commit} - Tag name or commit hash corresponding to version ("v0.1.0" or "1234567890ab").
// • {dir} - Path to directory of the package, relative to repo root ("mypkg").
// • {file} - Path to file containing the identifier, relative to repo root ("mypkg/file.go").
// • {base} - Base name of file containing the identifier, including file extension ("file.go").
// • {line} - Line number for the identifier ("41").
//
type urlTemplates struct {
Repo string `json:",omitempty"` // Optional URL template for the repository home page, with {repo}. If left empty, a default template "{repo}" is used.
Directory string // URL template for a directory, with {repo}, {importPath}, {commit}, {dir}.
File string // URL template for a file, with {repo}, {importPath}, {commit}, {file}, {base}.
Line string // URL template for a line, with {repo}, {importPath}, {commit}, {file}, {base}, {line}.
Raw string // Optional URL template for the raw contents of a file, with {repo}, {commit}, {file}.
}
var (
githubURLTemplates = urlTemplates{
Directory: "{repo}/tree/{commit}/{dir}",
File: "{repo}/blob/{commit}/{file}",
Line: "{repo}/blob/{commit}/{file}#L{line}",
Raw: "{repo}/raw/{commit}/{file}",
}
bitbucketURLTemplates = urlTemplates{
Directory: "{repo}/src/{commit}/{dir}",
File: "{repo}/src/{commit}/{file}",
Line: "{repo}/src/{commit}/{file}#lines-{line}",
Raw: "{repo}/raw/{commit}/{file}",
}
giteaURLTemplates = urlTemplates{
Directory: "{repo}/src/{commit}/{dir}",
File: "{repo}/src/{commit}/{file}",
Line: "{repo}/src/{commit}/{file}#L{line}",
Raw: "{repo}/raw/{commit}/{file}",
}
)
// commitFromVersion returns a string that refers to a commit corresponding to version.
// It also reports whether the returned value is a commit hash.
// The string may be a tag, or it may be the hash or similar unique identifier of a commit.
// The second argument is the module path relative to the repo root.
func commitFromVersion(vers, relativeModulePath string) (commit string, isHash bool) {
// Commit for the module: either a sha for pseudoversions, or a tag.
v := strings.TrimSuffix(vers, "+incompatible")
if version.IsPseudo(v) {
// Use the commit hash at the end.
return v[strings.LastIndex(v, "-")+1:], true
} else {
// The tags for a nested module begin with the relative module path of the module,
// removing a "/vN" suffix if N > 1.
prefix := removeVersionSuffix(relativeModulePath)
if prefix != "" {
return prefix + "/" + v, false
}
return v, false
}
}
// The following code copied from cmd/go/internal/get:
// expand rewrites s to replace {k} with match[k] for each key k in match.
func expand(s string, match map[string]string) string {
// We want to replace each match exactly once, and the result of expansion
// must not depend on the iteration order through the map.
// A strings.Replacer has exactly the properties we're looking for.
oldNew := make([]string, 0, 2*len(match))
for k, v := range match {
oldNew = append(oldNew, "{"+k+"}", v)
}
return strings.NewReplacer(oldNew...).Replace(s)
}
// NewGitHubInfo creates a source.Info with GitHub URL templates.
// It is for testing only.
func NewGitHubInfo(repoURL, moduleDir, commit string) *Info {
return &Info{
repoURL: repoURL,
moduleDir: moduleDir,
commit: commit,
templates: githubURLTemplates,
}
}