cmd/relnote: use Gerrit API to find RELNOTE markers in inline comments
Also filter out CLs on development or release branches.
Fixes golang/go#41849.
Change-Id: Ie6da4356d5f323dc15bd31b396889bfbf550d30b
Reviewed-on: https://go-review.googlesource.com/c/build/+/272907
Trust: Dmitri Shuralyov <dmitshur@golang.org>
Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Alexander Rakoczy <alex@golang.org>
diff --git a/cmd/relnote/relnote.go b/cmd/relnote/relnote.go
index e6d4064..b751699 100644
--- a/cmd/relnote/relnote.go
+++ b/cmd/relnote/relnote.go
@@ -14,13 +14,16 @@
"html"
"io/ioutil"
"log"
+ "path"
"regexp"
"sort"
"strings"
"time"
+ "golang.org/x/build/gerrit"
"golang.org/x/build/maintner"
"golang.org/x/build/maintner/godata"
+ "golang.org/x/build/repos"
)
var (
@@ -55,6 +58,15 @@
// Previous release was 6 months earlier.
cutoff = cutoff.AddDate(0, -6, 0)
+ // The maintner corpus doesn't track inline comments. See golang.org/issue/24863.
+ // So we need to use a Gerrit API client to fetch them instead. If maintner starts
+ // tracking inline comments in the future, this extra complexity can be dropped.
+ gerritClient := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
+ matchedCLs, err := findCLsWithRelNote(gerritClient, cutoff)
+ if err != nil {
+ log.Fatal(err)
+ }
+
var existingHTML []byte
if *exclFile != "" {
var err error
@@ -68,9 +80,8 @@
if err != nil {
log.Fatal(err)
}
- ger := corpus.Gerrit()
changes := map[string][]change{} // keyed by pkg
- ger.ForeachProjectUnsorted(func(gp *maintner.GerritProject) error {
+ corpus.Gerrit().ForeachProjectUnsorted(func(gp *maintner.GerritProject) error {
if gp.Server() != "go.googlesource.com" {
return nil
}
@@ -78,11 +89,25 @@
if cl.Status != "merged" {
return nil
}
+ if cl.Branch() != "master" {
+ // Ignore CLs sent to development or release branches.
+ return nil
+ }
if cl.Commit.CommitTime.Before(cutoff) {
// Was in a previous release; not for this one.
return nil
}
- relnote := clRelNote(cl)
+ _, ok := matchedCLs[int(cl.Number)]
+ if !ok {
+ // Wasn't matched by the Gerrit API search query.
+ // Return before making further Gerrit API calls.
+ return nil
+ }
+ comments, err := gerritClient.ListChangeComments(context.Background(), fmt.Sprint(cl.Number))
+ if err != nil {
+ return err
+ }
+ relnote := clRelNote(cl, comments)
if relnote == "" ||
bytes.Contains(existingHTML, []byte(fmt.Sprintf("CL %d", cl.Number))) {
return nil
@@ -119,7 +144,7 @@
if strings.HasPrefix(pkg, "cmd/") {
continue
}
- fmt.Printf("<dl id=%q><dt><a href=%q>%s</a></dt>\n <dd>",
+ fmt.Printf("\n<dl id=%q><dt><a href=%q>%s</a></dt>\n <dd>",
pkg, "/pkg/"+pkg+"/", pkg)
for _, change := range changes[pkg] {
changeURL := fmt.Sprintf("https://golang.org/cl/%d", change.CL.Number)
@@ -128,9 +153,8 @@
fmt.Printf("\n <p><!-- CL %d -->\n TODO: <a href=%q>%s</a>: %s\n </p>\n",
change.CL.Number, changeURL, changeURL, html.EscapeString(subj))
}
- fmt.Printf(" </dd>\n</dl><!-- %s -->\n\n", pkg)
+ fmt.Printf(" </dd>\n</dl><!-- %s -->\n", pkg)
}
-
} else {
for _, pkg := range pkgs {
fmt.Printf("%s\n", pkg)
@@ -141,34 +165,71 @@
}
}
-// clPackage returns the package name from the CL's commit message,
+// findCLsWithRelNote finds CLs that contain a RELNOTE marker by
+// using a Gerrit API client. Returned map is keyed by CL number.
+func findCLsWithRelNote(client *gerrit.Client, since time.Time) (map[int]*gerrit.ChangeInfo, error) {
+ // Gerrit search operators are documented at
+ // https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators.
+ query := fmt.Sprintf(`status:merged branch:master since:%s (comment:"RELNOTE" OR comment:"RELNOTES")`,
+ since.Format("2006-01-02"))
+ cs, err := client.QueryChanges(context.Background(), query)
+ if err != nil {
+ return nil, err
+ }
+ m := make(map[int]*gerrit.ChangeInfo) // CL Number → CL.
+ for _, c := range cs {
+ m[c.ChangeNumber] = c
+ }
+ return m, nil
+}
+
+// clPackage returns the package import path from the CL's commit message,
// or "??" if it's formatted unconventionally.
func clPackage(cl *maintner.GerritCL) string {
- subj := cl.Subject()
- if i := strings.Index(subj, ":"); i != -1 {
- return subj[:i]
+ var pkg string
+ if i := strings.Index(cl.Subject(), ":"); i == -1 {
+ return "??"
+ } else {
+ pkg = cl.Subject()[:i]
}
- return "??"
+ if r := repos.ByGerritProject[cl.Project.Project()]; r == nil {
+ return "??"
+ } else {
+ pkg = path.Join(r.ImportPath, pkg)
+ }
+ return pkg
}
-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 {
+// clRelNote extracts a RELNOTE note from a Gerrit CL commit
+// message and any inline comments. If there isn't a RELNOTE
+// note, it returns the empty string.
+func clRelNote(cl *maintner.GerritCL, comments map[string][]gerrit.CommentInfo) 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)
+ // Since July 2020, Gerrit UI has replaced top-level comments
+ // with patchset-level inline comments, so don't bother looking
+ // for RELNOTE= in cl.Messages—there won't be any. Instead, do
+ // look through all inline comments that we got via Gerrit API.
+ for _, cs := range comments {
+ for _, c := range cs {
+ if strings.Contains(c.Message, "RELNOTE") {
+ return parseRelNote(c.Message)
+ }
}
}
return ""
}
+
+// parseRelNote parses a RELNOTE annotation from the string s.
+// It returns the empty string if no such annotation exists.
+func parseRelNote(s string) string {
+ m := relNoteRx.FindStringSubmatch(s)
+ if m == nil {
+ return ""
+ }
+ return m[1]
+}
+
+var relNoteRx = regexp.MustCompile(`RELNOTES?=(.+)`)