blob: ab976a25d200ab10d441eea05802a1a533f85ca8 [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
Future bool // if true, the release hasn't happened yet
// Release content summary.
Security *FixSummary // Security fixes, if any.
Bug *FixSummary // Bug fixes, if any.
More template.HTML // Additional release content.
CustomSummary template.HTML // CustomSummary, if non-empty, replaces the entire release content summary with custom HTML.
}
// FixSummary summarizes fixes in a Go release, listing components and packages involved.
type FixSummary struct {
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 compiler", "runtime", "the <code>go</code> command", etc.
Packages []string // Packages involved. For example, "crypto/x509", "net/http", etc.
}
// A Version is a Go release version.
//
// In contrast to Semantic Versioning 2.0.0,
// a version like Go 1.21.0 is considered a major Go release and
// a version like Go 1.21.1 is considered a minor Go release.
// Prior to 1.21.0, trailing zero components were omitted. (See proposal 57631 that changed this.)
//
// See proposal go.dev/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.21.0", "1.21.1", "1.21.2", and so on.
func (v Version) String() string {
switch {
// Starting with 1.21.0, trailing 0 components are no longer
// left out out of the Go version string. See proposal 57631.
// So it only needs to be done for prior historical versions.
case v.X == 1 && v.Y < 21 && v.Z == 0:
return fmt.Sprintf("%d.%d", v.X, v.Y)
case v.X == 1 && v.Y == 0 && v.Z == 0:
return fmt.Sprintf("%d", v.X)
default:
return fmt.Sprintf("%d.%d.%d", v.X, v.Y, v.Z)
}
}
// MajorPrefix returns the major version prefix of a Go release version,
// like "1", "1.20", "1.21", and so on.
//
// This prefix can be used when referring to the entire series of releases,
// including the major Go release and all of its subsequent minor releases,
// that this version belongs to.
func (v Version) MajorPrefix() string {
switch {
case v.Y != 0:
return fmt.Sprintf("%d.%d", v.X, v.Y)
default:
return fmt.Sprintf("%d", v.X)
}
}
// Before reports whether version v comes before version u.
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
}
// 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 (f *FixSummary) ComponentsAndPackages() template.HTML {
var buf strings.Builder
// List components, if any.
for i, comp := range f.Components {
if len(f.Packages) == 0 {
// No packages, so components are joined with more rules.
switch {
case i != 0 && len(f.Components) == 2:
buf.WriteString(" and ")
case i != 0 && len(f.Components) >= 3 && i != len(f.Components)-1:
buf.WriteString(", ")
case i != 0 && len(f.Components) >= 3 && i == len(f.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(f.Components) > 0 && len(f.Packages) > 0 {
if len(f.Components)+len(f.Packages) >= 3 {
buf.WriteString(",")
}
buf.WriteString(" and ")
}
// List packages, if any.
if len(f.Packages) > 0 {
buf.WriteString("the ")
}
for i, pkg := range f.Packages {
switch {
case i != 0 && len(f.Packages) == 2:
buf.WriteString(" and ")
case i != 0 && len(f.Packages) >= 3 && i != len(f.Packages)-1:
buf.WriteString(", ")
case i != 0 && len(f.Packages) >= 3 && i == len(f.Packages)-1:
buf.WriteString(", and ")
}
buf.WriteString("<code>" + html.EscapeString(pkg) + "</code>")
}
switch {
case len(f.Packages) == 1:
buf.WriteString(" package")
case len(f.Packages) >= 2:
buf.WriteString(" packages")
}
return template.HTML(buf.String())
}