blob: f5b63e49997f2a3f8d35ed75c606802316ef5b62 [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/postgres"
"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 {
CommitTime string
// Link to this version, for use in the anchor href.
Link string
Version string
}
func fetchVersionsDetails(ctx context.Context, ds internal.DataSource, fullPath, modulePath string) (*VersionsDetails, error) {
db, ok := ds.(*postgres.DB)
if !ok {
// The proxydatasource does not support the imported by page.
return nil, proxydatasourceNotSupportedErr()
}
versions, err := db.GetVersionsForPath(ctx, fullPath)
if err != nil {
return nil, err
}
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 = fullPath
} else {
versionPath = pathInVersion(internal.V1Path(fullPath, modulePath), mi)
}
return constructUnitURL(versionPath, mi.ModulePath, linkVersion(mi.Version, mi.ModulePath))
}
return buildVersionDetails(modulePath, versions, 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 := internal.Suffix(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}
vs := &VersionSummary{
Link: linkify(mi),
CommitTime: absoluteTime(mi.CommitTime),
Version: linkVersion(mi.Version, mi.ModulePath),
}
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 pseudo versions, the version string will use a shorten commit hash of 7
// characters to identify the version, and hide timestamp using ellipses.
//
// For any version string longer than 25 characters, the pre-release string will be
// truncated, such that the string displayed is exactly 25 characters, including the ellipses.
//
// See TestFormatVersion for examples.
func formatVersion(v string) string {
const maxLen = 25
if len(v) <= maxLen {
return v
}
vType, err := version.ParseType(v)
if err != nil {
log.Errorf(context.TODO(), "formatVersion(%q): error parsing version: %v", v, err)
return v
}
if vType != version.TypePseudo {
// If the version is release or prerelease, return a version string of
// maxLen by truncating the end of the string. maxLen is inclusive of
// the "..." characters.
return v[:maxLen-3] + "..."
}
// The version string will have a max length of 25:
// base: "vX.Y.Z-prerelease.0" = up to 15
// ellipse: "..." = 3
// commit: "-abcdefa" = 7
commit := shorten(pseudoVersionRev(v), 7)
base := shorten(pseudoVersionBase(v), 15)
return fmt.Sprintf("%s...-%s", base, commit)
}
// shorten shortens the string s to maxLen by removing the trailing characters.
func shorten(s string, maxLen int) string {
if len(s) > maxLen {
return s[:maxLen]
}
return s
}
// pseudoVersionRev extracts the pseudo version base, excluding the timestamp.
// It assumes the pseudo version is correctly formatted.
//
// See TestPseudoVersionBase for examples.
func pseudoVersionBase(v string) string {
parts := strings.Split(v, "-")
if len(parts) != 3 {
mid := strings.Join(parts[1:len(parts)-1], "-")
parts = []string{parts[0], mid, parts[2]}
}
// The version string will always be split into one
// of these 3 parts:
// 1. [vX.0.0, yyyymmddhhmmss, abcdefabcdef]
// 2. [vX.Y.Z, pre.0.yyyymmddhhmmss, abcdefabcdef]
// 3. [vX.Y.Z, 0.yyyymmddhhmmss, abcdefabcdef]
p := strings.Split(parts[1], ".")
var suffix string
if len(p) > 0 {
// There is a "pre.0" or "0" prefix in the second element.
suffix = strings.Join(p[0:len(p)-1], ".")
}
return fmt.Sprintf("%s-%s", parts[0], suffix)
}
// pseudoVersionRev extracts the first 7 characters of 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.
// TODO(golang/go#41855): Clarify definition / use case for linkVersion and
// other version strings.
func linkVersion(v string, modulePath string) string {
if modulePath == stdlib.ModulePath {
if strings.HasPrefix(v, "go") {
return v
}
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
}