Skip to content

Commit e9020e8

Browse files
committed
cmd/golangorg: generate release history page from structured data
Previously, the release history page was a raw HTML file that was manually edited whenever new Go releases were made. This change converts release history entries into a structured format in the new internal/history package, and generates release history entries from that format. For now, only Go 1.9 and newer releases are converted, but the structured format is flexible enough to represent all releases going back to the original Go 1 release. Various English grammar rules and special cases are preserved, so that the release history entries appear in a consistent way. New release history entries need only to be added to the internal/ history package, making it so that English grammar rules and HTML tags don't need to go through human code review for each release. Future work may involve constructing that list from data already available in the Go issue tracker. This change makes minimal contributions to reducing the dependence of x/website on the x/tools/godoc rendering engine for displaying pages other than Go package documentation. The x/tools/godoc code is in another module and does not provide flexibility desired for the general purpose website needs of x/website. Fixes golang/go#38488. For golang/go#37090. For golang/go#29206. Change-Id: I80864e4f218782e6e3b5fcd5a1d63f3699314c81 Reviewed-on: https://go-review.googlesource.com/c/website/+/229081 Run-TryBot: Dmitri Shuralyov <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Alexander Rakoczy <[email protected]>
1 parent 2f57061 commit e9020e8

File tree

8 files changed

+1713
-469
lines changed

8 files changed

+1713
-469
lines changed

cmd/golangorg/godoc.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"bytes"
9+
"encoding/json"
10+
"net/http"
11+
"strings"
12+
13+
"golang.org/x/tools/godoc"
14+
"golang.org/x/website/internal/env"
15+
)
16+
17+
// This file holds common code from the x/tools/godoc serving engine.
18+
// It's being used during the transition. See golang.org/issue/29206.
19+
20+
// extractMetadata extracts the godoc.Metadata from a byte slice.
21+
// It returns the godoc.Metadata value and the remaining data.
22+
// If no metadata is present the original byte slice is returned.
23+
//
24+
func extractMetadata(b []byte) (meta godoc.Metadata, tail []byte, _ error) {
25+
tail = b
26+
if !bytes.HasPrefix(b, jsonStart) {
27+
return godoc.Metadata{}, tail, nil
28+
}
29+
end := bytes.Index(b, jsonEnd)
30+
if end < 0 {
31+
return godoc.Metadata{}, tail, nil
32+
}
33+
b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
34+
if err := json.Unmarshal(b, &meta); err != nil {
35+
return godoc.Metadata{}, nil, err
36+
}
37+
tail = tail[end+len(jsonEnd):]
38+
return meta, tail, nil
39+
}
40+
41+
var (
42+
jsonStart = []byte("<!--{")
43+
jsonEnd = []byte("}-->")
44+
)
45+
46+
// googleCN reports whether request r is considered
47+
// to be served from golang.google.cn.
48+
// TODO: This is duplicated within internal/proxy. Move to a common location.
49+
func googleCN(r *http.Request) bool {
50+
if r.FormValue("googlecn") != "" {
51+
return true
52+
}
53+
if strings.HasSuffix(r.Host, ".cn") {
54+
return true
55+
}
56+
if !env.CheckCountry() {
57+
return false
58+
}
59+
switch r.Header.Get("X-Appengine-Country") {
60+
case "", "ZZ", "CN":
61+
return true
62+
}
63+
return false
64+
}

cmd/golangorg/handlers.go

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"golang.org/x/tools/godoc"
2424
"golang.org/x/tools/godoc/vfs"
2525
"golang.org/x/website/internal/env"
26+
"golang.org/x/website/internal/history"
2627
"golang.org/x/website/internal/redirect"
2728
)
2829

@@ -85,6 +86,7 @@ func registerHandlers(pres *godoc.Presentation) *http.ServeMux {
8586
mux.Handle("/", pres)
8687
mux.Handle("/pkg/C/", redirect.Handler("/cmd/cgo/"))
8788
mux.HandleFunc("/fmt", fmtHandler)
89+
mux.Handle("/doc/devel/release.html", releaseHandler{ReleaseHistory: sortReleases(history.Releases)})
8890
redirect.Register(mux)
8991

9092
http.Handle("/", hostEnforcerHandler{mux})

cmd/golangorg/regtest_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ func TestLiveServer(t *testing.T) {
123123
Substring: `<meta name="go-import" content="golang.org/x/net git https://go.googlesource.com/net">`,
124124
NoAnalytics: true,
125125
},
126+
{
127+
Message: "release history page has an entry for Go 1.14.2",
128+
Path: "/doc/devel/release.html",
129+
Regexp: `go1\.14\.2\s+\(released 2020/04/08\)\s+includes\s+fixes to cgo, the go command, the runtime,`,
130+
},
126131
}
127132

128133
for _, tc := range substringTests {

cmd/golangorg/release.go

+269
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
"html"
11+
"html/template"
12+
"log"
13+
"net/http"
14+
"sort"
15+
"strings"
16+
17+
"golang.org/x/tools/godoc"
18+
"golang.org/x/tools/godoc/vfs"
19+
"golang.org/x/website/internal/history"
20+
)
21+
22+
// releaseHandler serves the Release History page.
23+
type releaseHandler struct {
24+
ReleaseHistory []Major // Pre-computed release history to display.
25+
}
26+
27+
func (h releaseHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
28+
const relPath = "doc/devel/release.html"
29+
30+
src, err := vfs.ReadFile(fs, "/doc/devel/release.html")
31+
if err != nil {
32+
log.Printf("reading template %s: %v", relPath, err)
33+
pres.ServeError(w, req, relPath, err)
34+
return
35+
}
36+
37+
meta, src, err := extractMetadata(src)
38+
if err != nil {
39+
log.Printf("decoding metadata %s: %v", relPath, err)
40+
pres.ServeError(w, req, relPath, err)
41+
return
42+
}
43+
if !meta.Template {
44+
err := fmt.Errorf("got non-template, want template")
45+
log.Printf("unexpected metadata %s: %v", relPath, err)
46+
pres.ServeError(w, req, relPath, err)
47+
return
48+
}
49+
50+
page := godoc.Page{
51+
Title: meta.Title,
52+
Subtitle: meta.Subtitle,
53+
GoogleCN: googleCN(req),
54+
}
55+
data := releaseTemplateData{
56+
Major: h.ReleaseHistory,
57+
}
58+
59+
// Evaluate as HTML template.
60+
tmpl, err := template.New("").Parse(string(src))
61+
if err != nil {
62+
log.Printf("parsing template %s: %v", relPath, err)
63+
pres.ServeError(w, req, relPath, err)
64+
return
65+
}
66+
var buf bytes.Buffer
67+
if err := tmpl.Execute(&buf, data); err != nil {
68+
log.Printf("executing template %s: %v", relPath, err)
69+
pres.ServeError(w, req, relPath, err)
70+
return
71+
}
72+
src = buf.Bytes()
73+
74+
page.Body = src
75+
pres.ServePage(w, page)
76+
}
77+
78+
// sortReleases returns a sorted list of Go releases, suitable to be
79+
// displayed on the Release History page. Releases are arranged into
80+
// major releases, each with minor revisions.
81+
func sortReleases(rs map[history.Version]history.Release) []Major {
82+
var major []Major
83+
byMajorVersion := make(map[history.Version]Major)
84+
for v, r := range rs {
85+
switch {
86+
case v.IsMajor():
87+
m := byMajorVersion[v]
88+
m.Release = Release{ver: v, rel: r}
89+
byMajorVersion[v] = m
90+
case v.IsMinor():
91+
m := byMajorVersion[majorOf(v)]
92+
m.Minor = append(m.Minor, Release{ver: v, rel: r})
93+
byMajorVersion[majorOf(v)] = m
94+
}
95+
}
96+
for _, m := range byMajorVersion {
97+
sort.Slice(m.Minor, func(i, j int) bool { return m.Minor[i].ver.Z < m.Minor[j].ver.Z })
98+
major = append(major, m)
99+
}
100+
sort.Slice(major, func(i, j int) bool {
101+
if major[i].ver.X != major[j].ver.X {
102+
return major[i].ver.X > major[j].ver.X
103+
}
104+
return major[i].ver.Y > major[j].ver.Y
105+
})
106+
return major
107+
}
108+
109+
// majorOf takes a Go version like 1.5, 1.5.1, 1.5.2, etc.,
110+
// and returns the corresponding major version like 1.5.
111+
func majorOf(v history.Version) history.Version {
112+
return history.Version{X: v.X, Y: v.Y, Z: 0}
113+
}
114+
115+
type releaseTemplateData struct {
116+
Major []Major
117+
}
118+
119+
// Major represents a major Go release and its minor revisions
120+
// as displayed on the release history page.
121+
type Major struct {
122+
Release
123+
Minor []Release
124+
}
125+
126+
// Release represents a Go release entry as displayed on the release history page.
127+
type Release struct {
128+
ver history.Version
129+
rel history.Release
130+
}
131+
132+
// V returns the Go release version string, like "1.14", "1.14.1", "1.14.2", etc.
133+
func (r Release) V() string {
134+
switch {
135+
case r.ver.Z != 0:
136+
return fmt.Sprintf("%d.%d.%d", r.ver.X, r.ver.Y, r.ver.Z)
137+
case r.ver.Y != 0:
138+
return fmt.Sprintf("%d.%d", r.ver.X, r.ver.Y)
139+
default:
140+
return fmt.Sprintf("%d", r.ver.X)
141+
}
142+
}
143+
144+
// Date returns the date of the release, formatted for display on the release history page.
145+
func (r Release) Date() string {
146+
d := r.rel.Date
147+
return fmt.Sprintf("%04d/%02d/%02d", d.Year, d.Month, d.Day)
148+
}
149+
150+
// Released reports whether release r has been released.
151+
func (r Release) Released() bool {
152+
return !r.rel.Future
153+
}
154+
155+
func (r Release) Summary() (template.HTML, error) {
156+
var buf bytes.Buffer
157+
err := releaseSummaryHTML.Execute(&buf, releaseSummaryTemplateData{
158+
V: r.V(),
159+
Security: r.rel.Security,
160+
Released: r.Released(),
161+
Quantifier: r.rel.Quantifier,
162+
ComponentsAndPackages: joinComponentsAndPackages(r.rel),
163+
More: r.rel.More,
164+
CustomSummary: r.rel.CustomSummary,
165+
})
166+
return template.HTML(buf.String()), err
167+
}
168+
169+
type releaseSummaryTemplateData struct {
170+
V string // Go release version string, like "1.14", "1.14.1", "1.14.2", etc.
171+
Security bool // Security release.
172+
Released bool // Whether release has been released.
173+
Quantifier string // Optional quantifier. Empty string for unspecified amount of fixes (typical), "a" for a single fix, "two", "three" for multiple fixes, etc.
174+
ComponentsAndPackages template.HTML // Components and packages involved.
175+
More template.HTML // Additional release content.
176+
CustomSummary template.HTML // CustomSummary, if non-empty, replaces the entire release content summary with custom HTML.
177+
}
178+
179+
var releaseSummaryHTML = template.Must(template.New("").Parse(`
180+
{{if not .CustomSummary}}
181+
{{if .Released}}includes{{else}}will include{{end}}
182+
{{.Quantifier}}
183+
{{if .Security}}security{{end}}
184+
{{if eq .Quantifier "a"}}fix{{else}}fixes{{end -}}
185+
{{with .ComponentsAndPackages}} to {{.}}{{end}}.
186+
{{.More}}
187+
188+
See the
189+
<a href="https://github.com/golang/go/issues?q=milestone%3AGo{{.V}}+label%3ACherryPickApproved">Go
190+
{{.V}} milestone</a> on our issue tracker for details.
191+
{{else}}
192+
{{.CustomSummary}}
193+
{{end}}
194+
`))
195+
196+
// joinComponentsAndPackages joins components and packages involved
197+
// in a Go release for the purposes of being displayed on the
198+
// release history page, keeping English grammar rules in mind.
199+
//
200+
// The different special cases are:
201+
//
202+
// c1
203+
// c1 and c2
204+
// c1, c2, and c3
205+
//
206+
// the p1 package
207+
// the p1 and p2 packages
208+
// the p1, p2, and p3 packages
209+
//
210+
// c1 and [1 package]
211+
// c1, and [2 or more packages]
212+
// c1, c2, and [1 or more packages]
213+
//
214+
func joinComponentsAndPackages(r history.Release) template.HTML {
215+
var buf strings.Builder
216+
217+
// List components, if any.
218+
for i, comp := range r.Components {
219+
if len(r.Packages) == 0 {
220+
// No packages, so components are joined with more rules.
221+
switch {
222+
case i != 0 && len(r.Components) == 2:
223+
buf.WriteString(" and ")
224+
case i != 0 && len(r.Components) >= 3 && i != len(r.Components)-1:
225+
buf.WriteString(", ")
226+
case i != 0 && len(r.Components) >= 3 && i == len(r.Components)-1:
227+
buf.WriteString(", and ")
228+
}
229+
} else {
230+
// When there are packages, all components are comma-separated.
231+
if i != 0 {
232+
buf.WriteString(", ")
233+
}
234+
}
235+
buf.WriteString(string(comp))
236+
}
237+
238+
// Join components and packages using a comma and/or "and" as needed.
239+
if len(r.Components) > 0 && len(r.Packages) > 0 {
240+
if len(r.Components)+len(r.Packages) >= 3 {
241+
buf.WriteString(",")
242+
}
243+
buf.WriteString(" and ")
244+
}
245+
246+
// List packages, if any.
247+
if len(r.Packages) > 0 {
248+
buf.WriteString("the ")
249+
}
250+
for i, pkg := range r.Packages {
251+
switch {
252+
case i != 0 && len(r.Packages) == 2:
253+
buf.WriteString(" and ")
254+
case i != 0 && len(r.Packages) >= 3 && i != len(r.Packages)-1:
255+
buf.WriteString(", ")
256+
case i != 0 && len(r.Packages) >= 3 && i == len(r.Packages)-1:
257+
buf.WriteString(", and ")
258+
}
259+
buf.WriteString("<code>" + html.EscapeString(pkg) + "</code>")
260+
}
261+
switch {
262+
case len(r.Packages) == 1:
263+
buf.WriteString(" package")
264+
case len(r.Packages) >= 2:
265+
buf.WriteString(" packages")
266+
}
267+
268+
return template.HTML(buf.String())
269+
}

0 commit comments

Comments
 (0)