| // 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 frontend |
| |
| import ( |
| "context" |
| "fmt" |
| "path" |
| "strings" |
| |
| "golang.org/x/discovery/internal" |
| "golang.org/x/discovery/internal/log" |
| "golang.org/x/discovery/internal/stdlib" |
| "golang.org/x/discovery/internal/version" |
| "golang.org/x/mod/module" |
| "golang.org/x/mod/semver" |
| ) |
| |
| // VersionsDetails contains the hierarchy of version summary information used |
| // to populate the version tab. Version information is organized into separate |
| // lists, one for each (ModulePath, Major Version) pair. |
| type VersionsDetails struct { |
| // ThisModule is the slice of VersionLists with the same module path as the |
| // current package. |
| ThisModule []*VersionList |
| |
| // OtherModules is the slice of VersionLists with a different module path |
| // from the current package. |
| OtherModules []*VersionList |
| } |
| |
| // VersionListKey identifies a version list on the versions tab. We have a |
| // separate VersionList for each major version of a module series. Notably we |
| // have more version lists than module paths: v0 and v1 module versions are in |
| // separate version lists, despite having the same module path. |
| type VersionListKey struct { |
| // ModulePath is the module path of this major version. |
| ModulePath string |
| // Major is the major version string (e.g. v1, v2) |
| Major string |
| } |
| |
| // VersionList holds all versions corresponding to a unique (module path, |
| // major version) tuple in the version hierarchy. |
| type VersionList struct { |
| VersionListKey |
| // Versions holds the nested version summaries, organized in descending |
| // semver order. |
| Versions []*VersionSummary |
| } |
| |
| // VersionSummary holds data required to format the version link on the |
| // versions tab. |
| type VersionSummary struct { |
| TooltipVersion string |
| DisplayVersion string |
| CommitTime string |
| // Link to this version, for use in the anchor href. |
| Link string |
| } |
| |
| // fetchModuleVersionsDetails builds a version hierarchy for module versions |
| // with the same series path as the given version. |
| func fetchModuleVersionsDetails(ctx context.Context, ds internal.DataSource, mi *internal.ModuleInfo) (*VersionsDetails, error) { |
| versions, err := ds.GetTaggedVersionsForModule(ctx, mi.ModulePath) |
| if err != nil { |
| return nil, err |
| } |
| // If no tagged versions of the module are found, fetch pseudo-versions |
| // instead. |
| if len(versions) == 0 { |
| versions, err = ds.GetPseudoVersionsForModule(ctx, mi.ModulePath) |
| if err != nil { |
| return nil, err |
| } |
| } |
| linkify := func(m *internal.ModuleInfo) string { |
| return constructModuleURL(m.ModulePath, linkVersion(m.Version, m.ModulePath)) |
| } |
| return buildVersionDetails(mi.ModulePath, versions, linkify), nil |
| } |
| |
| // fetchPackageVersionsDetails builds a version hierarchy for all module |
| // versions containing a package path with v1 import path matching the v1 |
| // import path of pkg. |
| func fetchPackageVersionsDetails(ctx context.Context, ds internal.DataSource, pkg *internal.VersionedPackage) (*VersionsDetails, error) { |
| versions, err := ds.GetTaggedVersionsForPackageSeries(ctx, pkg.Path) |
| if err != nil { |
| return nil, err |
| } |
| // If no tagged versions for the package series are found, fetch the |
| // pseudo-versions instead. |
| if len(versions) == 0 { |
| versions, err = ds.GetPseudoVersionsForPackageSeries(ctx, pkg.Path) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| var filteredVersions []*internal.ModuleInfo |
| // TODO(rfindley): remove this filtering, as it should not be necessary and |
| // is probably a relic of earlier version query implementations. |
| for _, v := range versions { |
| if seriesPath := v.SeriesPath(); strings.HasPrefix(pkg.V1Path, seriesPath) || seriesPath == stdlib.ModulePath { |
| filteredVersions = append(filteredVersions, v) |
| } else { |
| log.Errorf(ctx, "got version with mismatching series: %q", seriesPath) |
| } |
| } |
| |
| linkify := func(mi *internal.ModuleInfo) string { |
| // Here we have only version information, but need to construct the full |
| // import path of the package corresponding to this version. |
| var versionPath string |
| if mi.ModulePath == stdlib.ModulePath { |
| versionPath = pkg.Path |
| } else { |
| versionPath = pathInVersion(pkg.V1Path, mi) |
| } |
| return constructPackageURL(versionPath, mi.ModulePath, linkVersion(mi.Version, mi.ModulePath)) |
| } |
| return buildVersionDetails(pkg.ModulePath, filteredVersions, linkify), nil |
| } |
| |
| // pathInVersion constructs the full import path of the package corresponding |
| // to mi, given its v1 path. To do this, we first compute the suffix of the |
| // package path in the given module series, and then append it to the real |
| // (versioned) module path. |
| // |
| // For example: if we're considering package foo.com/v3/bar/baz, and encounter |
| // module version foo.com/bar/v2, we do the following: |
| // 1) Start with the v1Path foo.com/bar/baz. |
| // 2) Trim off the version series path foo.com/bar to get 'baz'. |
| // 3) Join with the versioned module path foo.com/bar/v2 to get |
| // foo.com/bar/v2/baz. |
| // ...being careful about slashes along the way. |
| func pathInVersion(v1Path string, mi *internal.ModuleInfo) string { |
| suffix := strings.TrimPrefix(strings.TrimPrefix(v1Path, mi.SeriesPath()), "/") |
| if suffix == "" { |
| return mi.ModulePath |
| } |
| return path.Join(mi.ModulePath, suffix) |
| } |
| |
| // buildVersionDetails constructs the version hierarchy to be rendered on the |
| // versions tab, organizing major versions into those that have the same module |
| // path as the package version under consideration, and those that don't. The |
| // given versions MUST be sorted first by module path and then by semver. |
| func buildVersionDetails(currentModulePath string, modInfos []*internal.ModuleInfo, linkify func(v *internal.ModuleInfo) string) *VersionsDetails { |
| |
| // lists organizes versions by VersionListKey. Note that major version isn't |
| // sufficient as a key: there are packages contained in the same major |
| // version of different modules, for example github.com/hashicorp/vault/api, |
| // which exists in v1 of both of github.com/hashicorp/vault and |
| // github.com/hashicorp/vault/api. |
| lists := make(map[VersionListKey][]*VersionSummary) |
| // seenLists tracks the order in which we encounter entries of each version |
| // list. We want to preserve this order. |
| var seenLists []VersionListKey |
| for _, mi := range modInfos { |
| // Try to resolve the most appropriate major version for this version. If |
| // we detect a +incompatible version (when the path version does not match |
| // the sematic version), we prefer the path version. |
| major := semver.Major(mi.Version) |
| if mi.ModulePath == stdlib.ModulePath { |
| var err error |
| major, err = stdlib.MajorVersionForVersion(mi.Version) |
| if err != nil { |
| panic(err) |
| } |
| } |
| if _, pathMajor, ok := module.SplitPathVersion(mi.ModulePath); ok { |
| // We prefer the path major version except for v1 import paths where the |
| // semver major version is v0. In this case, we prefer the more specific |
| // semver version. |
| if pathMajor != "" { |
| // Trim both '/' and '.' from the path major version to account for |
| // standard and gopkg.in module paths. |
| major = strings.TrimLeft(pathMajor, "/.") |
| } else if major != "v0" && !strings.HasPrefix(major, "go") { |
| major = "v1" |
| } |
| } |
| key := VersionListKey{ModulePath: mi.ModulePath, Major: major} |
| ttversion := mi.Version |
| fmtVersion := displayVersion(mi.Version, mi.ModulePath) |
| if mi.ModulePath == stdlib.ModulePath { |
| ttversion = fmtVersion // tooltips will show the Go tag |
| } |
| vs := &VersionSummary{ |
| TooltipVersion: ttversion, |
| Link: linkify(mi), |
| CommitTime: elapsedTime(mi.CommitTime), |
| DisplayVersion: fmtVersion, |
| } |
| if _, ok := lists[key]; !ok { |
| seenLists = append(seenLists, key) |
| } |
| lists[key] = append(lists[key], vs) |
| } |
| |
| var details VersionsDetails |
| for _, key := range seenLists { |
| vl := &VersionList{ |
| VersionListKey: key, |
| Versions: lists[key], |
| } |
| if key.ModulePath == currentModulePath { |
| details.ThisModule = append(details.ThisModule, vl) |
| } else { |
| details.OtherModules = append(details.OtherModules, vl) |
| } |
| } |
| return &details |
| } |
| |
| // formatVersion formats a more readable representation of the given version |
| // string. On any parsing error, it simply returns the input unmodified. |
| // |
| // For prerelease versions, formatVersion separates the prerelease portion of |
| // the version string into a parenthetical. i.e. |
| // formatVersion("v1.2.3-alpha") = "v1.2.3 (alpha)" |
| // |
| // For pseudo versions, formatVersion uses a short commit hash to identify the |
| // version. i.e. |
| // formatVersion("v1.2.3-20190311183353-d8887717615a") = "v.1.2.3 (d888771)" |
| func formatVersion(v string) string { |
| vType, err := version.ParseType(v) |
| if err != nil { |
| log.Errorf(context.TODO(), "Error parsing version %q: %v", v, err) |
| return v |
| } |
| pre := semver.Prerelease(v) |
| base := strings.TrimSuffix(v, pre) |
| pre = strings.TrimPrefix(pre, "-") |
| switch vType { |
| case version.TypePrerelease: |
| return fmt.Sprintf("%s (%s)", base, pre) |
| case version.TypePseudo: |
| rev := pseudoVersionRev(v) |
| commitLen := 7 |
| if len(rev) < commitLen { |
| commitLen = len(rev) |
| } |
| return fmt.Sprintf("%s (%s)", base, rev[0:commitLen]) |
| default: |
| return v |
| } |
| } |
| |
| // pseudoVersionRev extracts the commit identifier from a pseudo version |
| // string. It assumes the pseudo version is correctly formatted. |
| func pseudoVersionRev(v string) string { |
| v = strings.TrimSuffix(v, "+incompatible") |
| j := strings.LastIndex(v, "-") |
| return v[j+1:] |
| } |
| |
| // displayVersion returns the version string, formatted for display. |
| func displayVersion(v string, modulePath string) string { |
| if modulePath == stdlib.ModulePath { |
| return goTagForVersion(v) |
| } |
| return formatVersion(v) |
| } |
| |
| // linkVersion returns the version string, suitable for use in |
| // a link to this site. |
| func linkVersion(v string, modulePath string) string { |
| if modulePath == stdlib.ModulePath { |
| return goTagForVersion(v) |
| } |
| return v |
| } |
| |
| // goTagForVersion returns the Go tag corresponding to a given semantic |
| // version. It should only be used if we are 100% sure the version will |
| // correspond to a Go tag, such as when we are fetching the version from the |
| // database. |
| func goTagForVersion(v string) string { |
| tag, err := stdlib.TagForVersion(v) |
| if err != nil { |
| log.Errorf(context.TODO(), "goTagForVersion(%q): %v", v, err) |
| return "unknown" |
| } |
| return tag |
| } |