|  | // 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 "" | 
|  | } |