blob: 327b214c0b3e04e86440c8f293fa942016fca939 [file] [log] [blame]
// Copyright 2020 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 history holds the Go project release history.
package history
import (
"fmt"
"html"
"html/template"
"sort"
"strings"
"time"
)
// A Release describes a single Go release.
type Release struct {
Version Version
Date Date
Security bool // whether this is a security release
Future bool // if true, the release hasn't happened yet
// Release content summary.
Quantifier string // Optional quantifier. Empty string for unspecified amount of fixes (typical), "a" for a single fix, "two", "three" for multiple fixes, etc.
Components []template.HTML // Components involved. For example, "cgo", "the <code>go</code> command", "the runtime", etc.
Packages []string // Packages involved. For example, "net/http", "crypto/x509", etc.
More template.HTML // Additional release content.
CustomSummary template.HTML // CustomSummary, if non-empty, replaces the entire release content summary with custom HTML.
}
// A Version is a Go release version.
//
// In contrast to Semantic Versioning 2.0.0,
// trailing zero components are omitted,
// a version like Go 1.14 is considered a major Go release,
// a version like Go 1.14.1 is considered a minor Go release.
//
// See proposal golang.org/issue/32450 for background, details,
// and a discussion of the costs involved in making a change.
type Version struct {
X int // X is the 1st component of a Go X.Y.Z version. It must be 1 or higher.
Y int // Y is the 2nd component of a Go X.Y.Z version. It must be 0 or higher.
Z int // Z is the 3rd component of a Go X.Y.Z version. It must be 0 or higher.
}
// String returns the Go release version string,
// like "1.14", "1.14.1", "1.14.2", and so on.
func (v Version) String() string {
switch {
case v.Z != 0:
return fmt.Sprintf("%d.%d.%d", v.X, v.Y, v.Z)
case v.Y != 0:
return fmt.Sprintf("%d.%d", v.X, v.Y)
default:
return fmt.Sprintf("%d", v.X)
}
}
func (v Version) Before(u Version) bool {
if v.X != u.X {
return v.X < u.X
}
if v.Y != u.Y {
return v.Y < u.Y
}
return v.Z < u.Z
}
// IsMajor reports whether version v is considered to be a major Go release.
// For example, Go 1.14 and 1.13 are major Go releases.
func (v Version) IsMajor() bool { return v.Z == 0 }
// IsMinor reports whether version v is considered to be a minor Go release.
// For example, Go 1.14.1 and 1.13.9 are minor Go releases.
func (v Version) IsMinor() bool { return v.Z != 0 }
// A Date represents the date (year, month, day) of a Go release.
//
// This type does not include location information, and
// therefore does not describe a unique 24-hour timespan.
type Date struct {
Year int // Year (e.g., 2009).
Month time.Month // Month of the year (January = 1, ...).
Day int // Day of the month, starting at 1.
}
func (d Date) String() string {
return d.Format("2006-01-02")
}
func (d Date) Format(format string) string {
return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, time.UTC).Format(format)
}
// A Major describes a major Go release and its minor revisions.
type Major struct {
*Release
Minor []*Release // oldest first
}
var Majors []*Major = majors() // major versions, newest first
// majors returns a list of major versions, sorted newest first.
func majors() []*Major {
byVersion := make(map[Version]*Major)
for _, r := range Releases {
v := r.Version
v.Z = 0 // make major version
m := byVersion[v]
if m == nil {
m = new(Major)
byVersion[v] = m
}
if r.Version.Z == 0 {
m.Release = r
} else {
m.Minor = append(m.Minor, r)
}
}
var majors []*Major
for _, m := range byVersion {
majors = append(majors, m)
// minors oldest first
sort.Slice(m.Minor, func(i, j int) bool {
return m.Minor[i].Version.Before(m.Minor[j].Version)
})
}
// majors newest first
sort.Slice(majors, func(i, j int) bool {
return !majors[i].Version.Before(majors[j].Version)
})
return majors
}
// ComponentsAndPackages joins components and packages involved
// in a Go release for the purposes of being displayed on the
// release history page, keeping English grammar rules in mind.
//
// The different special cases are:
//
// c1
// c1 and c2
// c1, c2, and c3
//
// the p1 package
// the p1 and p2 packages
// the p1, p2, and p3 packages
//
// c1 and [1 package]
// c1, and [2 or more packages]
// c1, c2, and [1 or more packages]
//
func (r *Release) ComponentsAndPackages() template.HTML {
var buf strings.Builder
// List components, if any.
for i, comp := range r.Components {
if len(r.Packages) == 0 {
// No packages, so components are joined with more rules.
switch {
case i != 0 && len(r.Components) == 2:
buf.WriteString(" and ")
case i != 0 && len(r.Components) >= 3 && i != len(r.Components)-1:
buf.WriteString(", ")
case i != 0 && len(r.Components) >= 3 && i == len(r.Components)-1:
buf.WriteString(", and ")
}
} else {
// When there are packages, all components are comma-separated.
if i != 0 {
buf.WriteString(", ")
}
}
buf.WriteString(string(comp))
}
// Join components and packages using a comma and/or "and" as needed.
if len(r.Components) > 0 && len(r.Packages) > 0 {
if len(r.Components)+len(r.Packages) >= 3 {
buf.WriteString(",")
}
buf.WriteString(" and ")
}
// List packages, if any.
if len(r.Packages) > 0 {
buf.WriteString("the ")
}
for i, pkg := range r.Packages {
switch {
case i != 0 && len(r.Packages) == 2:
buf.WriteString(" and ")
case i != 0 && len(r.Packages) >= 3 && i != len(r.Packages)-1:
buf.WriteString(", ")
case i != 0 && len(r.Packages) >= 3 && i == len(r.Packages)-1:
buf.WriteString(", and ")
}
buf.WriteString("<code>" + html.EscapeString(pkg) + "</code>")
}
switch {
case len(r.Packages) == 1:
buf.WriteString(" package")
case len(r.Packages) >= 2:
buf.WriteString(" packages")
}
return template.HTML(buf.String())
}