blob: 67b106e45e24daa3878f6dfc9b457f561ef32a41 [file] [log] [blame]
// Copyright 2017 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.
// The relnote command summarizes the Go changes in Gerrit marked with
// RELNOTE annotations for the release notes.
package main
import (
"bytes"
"context"
"flag"
"fmt"
"html"
"io/ioutil"
"log"
"regexp"
"sort"
"strings"
"time"
"golang.org/x/build/maintner"
"golang.org/x/build/maintner/godata"
)
var (
htmlMode = flag.Bool("html", false, "write HTML output")
exclFile = flag.String("exclude-from", "", "optional path to release notes HTML file. If specified, any 'CL NNNN' occurence in the content will cause that CL to be excluded from this tool's output.")
)
// change is a change that was noted via a RELNOTE= comment.
type change struct {
CL *maintner.GerritCL
Note string // the part after RELNOTE=
}
func (c change) TextLine() string {
subj := clSubject(c.CL)
if c.Note != "yes" && c.Note != "y" {
subj = c.Note + ": " + subj
}
return fmt.Sprintf("https://golang.org/cl/%d: %s", c.CL.Number, subj)
}
func main() {
flag.Parse()
// Releases are every 6 months. Walk forward by 6 month increments to next release.
cutoff := time.Date(2016, time.August, 1, 00, 00, 00, 0, time.UTC)
now := time.Now()
for cutoff.Before(now) {
cutoff = cutoff.AddDate(0, 6, 0)
}
// Previous release was 6 months earlier.
cutoff = cutoff.AddDate(0, -6, 0)
var existingHTML []byte
if *exclFile != "" {
var err error
existingHTML, err = ioutil.ReadFile(*exclFile)
if err != nil {
log.Fatal(err)
}
}
corpus, err := godata.Get(context.Background())
if err != nil {
log.Fatal(err)
}
ger := corpus.Gerrit()
changes := map[string][]change{} // keyed by pkg
ger.ForeachProjectUnsorted(func(gp *maintner.GerritProject) error {
if gp.Server() != "go.googlesource.com" {
return nil
}
gp.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
if cl.Status != "merged" {
return nil
}
if cl.Commit.CommitTime.Before(cutoff) {
// Was in a previous release; not for this one.
return nil
}
relnote := clRelNote(cl)
if relnote == "" ||
bytes.Contains(existingHTML, []byte(fmt.Sprintf("CL %d", cl.Number))) {
return nil
}
pkg := clPackage(cl)
changes[pkg] = append(changes[pkg], change{
Note: relnote,
CL: cl,
})
return nil
})
return nil
})
var pkgs []string
for pkg, changes := range changes {
pkgs = append(pkgs, pkg)
sort.Slice(changes, func(i, j int) bool {
return changes[i].CL.Number < changes[j].CL.Number
})
}
sort.Strings(pkgs)
if *htmlMode {
for _, pkg := range pkgs {
if !strings.HasPrefix(pkg, "cmd/") {
continue
}
for _, change := range changes[pkg] {
fmt.Printf("<!-- CL %d: %s -->\n", change.CL.Number, change.TextLine())
}
}
for _, pkg := range pkgs {
if strings.HasPrefix(pkg, "cmd/") {
continue
}
fmt.Printf("<dl id=%q><dt><a href=%q>%s</a></dt>\n <dd>\n",
pkg, "/pkg/"+pkg+"/", pkg)
for _, change := range changes[pkg] {
changeURL := fmt.Sprintf("https://golang.org/cl/%d", change.CL.Number)
subj := clSubject(change.CL)
subj = strings.TrimPrefix(subj, pkg+": ")
fmt.Printf(" <p><!-- CL %d -->\n TODO: <a href=%q>%s</a>: %s\n </p>\n\n",
change.CL.Number, changeURL, changeURL, html.EscapeString(subj))
}
fmt.Printf("</dl><!-- %s -->\n\n", pkg)
}
} else {
for _, pkg := range pkgs {
fmt.Printf("%s\n", pkg)
for _, change := range changes[pkg] {
fmt.Printf(" %s\n", change.TextLine())
}
}
}
}
// clSubject returns the first line of the CL's commit message,
// without the trailing newline.
func clSubject(cl *maintner.GerritCL) string {
subj := cl.Commit.Msg
if i := strings.Index(subj, "\n"); i != -1 {
return subj[:i]
}
return subj
}
// clPackage returns the package name from the CL's commit message,
// or "??" if it's formatted unconventionally.
func clPackage(cl *maintner.GerritCL) string {
subj := clSubject(cl)
if i := strings.Index(subj, ":"); i != -1 {
return subj[:i]
}
return "??"
}
var relNoteRx = regexp.MustCompile(`RELNOTES?=(.+)`)
func parseRelNote(s string) string {
if m := relNoteRx.FindStringSubmatch(s); m != nil {
return m[1]
}
return ""
}
func clRelNote(cl *maintner.GerritCL) string {
msg := cl.Commit.Msg
if strings.Contains(msg, "RELNOTE") {
return parseRelNote(msg)
}
for _, comment := range cl.Messages {
if strings.Contains(comment.Message, "RELNOTE") {
return parseRelNote(comment.Message)
}
}
return ""
}