blob: 3ee148ad1a74164940a7283a7796f3ef2c8ca0fb [file] [log] [blame]
// 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/mod/module"
"golang.org/x/mod/semver"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/internal/version"
)
// 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 given v1 path.
func fetchPackageVersionsDetails(ctx context.Context, ds internal.DataSource, pkgPath, v1Path, modulePath string) (*VersionsDetails, error) {
versions, err := ds.GetTaggedVersionsForPackageSeries(ctx, pkgPath)
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, pkgPath)
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(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 = pkgPath
} else {
versionPath = pathInVersion(v1Path, mi)
}
return constructPackageURL(versionPath, mi.ModulePath, linkVersion(mi.Version, mi.ModulePath))
}
return buildVersionDetails(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
}