cmd/relnote: find cutoff date automatically
Look for the date of CL that opened the tree to find the
cutoff for TODOs.
Add a flag for the date in case that doesn't work.
For golang/go#64169.
Change-Id: I756e5622339f5e1963c39b8e0bbd7eeb3fc23d85
Reviewed-on: https://go-review.googlesource.com/c/build/+/584401
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/cmd/relnote/relnote.go b/cmd/relnote/relnote.go
index e24d6c8..4103949 100644
--- a/cmd/relnote/relnote.go
+++ b/cmd/relnote/relnote.go
@@ -19,8 +19,9 @@
)
var (
- verbose = flag.Bool("v", false, "print verbose logging")
- goroot = flag.String("goroot", runtime.GOROOT(), "root of Go repo containing docs")
+ verbose = flag.Bool("v", false, "print verbose logging")
+ goroot = flag.String("goroot", runtime.GOROOT(), "root of Go repo containing docs")
+ todosSince = flag.String("since", "", "earliest to look for TODOs, in YYYY-MM-DD format")
)
func usage() {
@@ -28,8 +29,8 @@
fmt.Fprintf(out, "usage:\n")
fmt.Fprintf(out, " relnote generate\n")
fmt.Fprintf(out, " generate release notes from doc/next\n")
- fmt.Fprintf(out, " relnote todo PREVIOUS_RELEASE_DATE\n")
- fmt.Fprintf(out, " report which release notes need to be written; use YYYY-MM-DD format for date of last release\n")
+ fmt.Fprintf(out, " relnote todo\n")
+ fmt.Fprintf(out, " report which release notes need to be written\n")
flag.PrintDefaults()
}
@@ -54,18 +55,16 @@
if cmd := flag.Arg(0); cmd != "" {
switch cmd {
case "generate":
- err = generate(version, flag.Arg(1))
+ err = generate(version, *goroot)
case "todo":
- prevDate := flag.Arg(1)
- if prevDate == "" {
- log.Fatal("need previous release date")
+ var sinceDate time.Time
+ if *todosSince != "" {
+ sinceDate, err = time.Parse(time.DateOnly, *todosSince)
+ if err != nil {
+ log.Fatalf("-since flag: %v", err)
+ }
}
- prevDateTime, err := time.Parse("2006-01-02", prevDate)
- if err != nil {
- log.Fatalf("previous release date: %s", err)
- }
- nextDir := filepath.Join(*goroot, "doc", "next")
- err = todo(os.Stdout, os.DirFS(nextDir), prevDateTime)
+ err = todo(os.Stdout, *goroot, sinceDate)
default:
err = fmt.Errorf("unknown command %q", cmd)
}
diff --git a/cmd/relnote/todo.go b/cmd/relnote/todo.go
index bf34651..1faec09 100644
--- a/cmd/relnote/todo.go
+++ b/cmd/relnote/todo.go
@@ -10,6 +10,10 @@
"fmt"
"io"
"io/fs"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
"regexp"
"slices"
"strconv"
@@ -29,24 +33,59 @@
// todo prints a report to w on which release notes need to be written.
// It takes the doc/next directory of the repo and the date of the last release.
-func todo(w io.Writer, fsys fs.FS, prevRelDate time.Time) error {
+func todo(w io.Writer, goroot string, treeOpenDate time.Time) error {
+ // If not provided, determine when the tree was opened by looking
+ // at when the version file was updated.
+ if treeOpenDate.IsZero() {
+ var err error
+ treeOpenDate, err = findTreeOpenDate(goroot)
+ if err != nil {
+ return err
+ }
+ }
+ log.Printf("collecting TODOs from %s since %s", goroot, treeOpenDate.Format(time.DateOnly))
+
var todos []ToDo
addToDo := func(td ToDo) { todos = append(todos, td) }
mentionedIssues := map[int]bool{} // issues mentioned in the existing relnotes
addIssue := func(num int) { mentionedIssues[num] = true }
- if err := infoFromDocFiles(fsys, addToDo, addIssue); err != nil {
+ nextDir := filepath.Join(goroot, "doc", "next")
+ if err := infoFromDocFiles(os.DirFS(nextDir), addToDo, addIssue); err != nil {
return err
}
- if !prevRelDate.IsZero() {
- if err := todosFromCLs(prevRelDate, mentionedIssues, addToDo); err != nil {
- return err
- }
+ if err := todosFromCLs(treeOpenDate, mentionedIssues, addToDo); err != nil {
+ return err
}
return writeToDos(w, todos)
}
+// findTreeOpenDate returns the time of the most recent commit to the file that
+// determines the version of Go under development.
+func findTreeOpenDate(goroot string) (time.Time, error) {
+ versionFilePath := filepath.FromSlash("src/internal/goversion/goversion.go")
+ if _, err := exec.LookPath("git"); err != nil {
+ return time.Time{}, fmt.Errorf("looking for git binary: %v", err)
+ }
+ // List the most recent commit to versionFilePath, displaying the date and subject.
+ outb, err := exec.Command("git", "-C", goroot, "log", "-n", "1",
+ "--format=%cs %s", "--", versionFilePath).Output()
+ if err != nil {
+ return time.Time{}, err
+ }
+ out := string(outb)
+ // The commit messages follow a standard form. Check for the right words to avoid mistakenly
+ // choosing the wrong commit.
+ const updateString = "update version to"
+ if !strings.Contains(strings.ToLower(out), updateString) {
+ return time.Time{}, fmt.Errorf("cannot determine tree-open date: most recent commit for %s does not contain %q",
+ versionFilePath, updateString)
+ }
+ dateString, _, _ := strings.Cut(out, " ")
+ return time.Parse(time.DateOnly, dateString)
+}
+
// Collect TODOs and issue numbers from the markdown files in the main repo.
func infoFromDocFiles(fsys fs.FS, addToDo func(ToDo), addIssue func(int)) error {
// This is essentially a grep.
diff --git a/cmd/relnote/todo_test.go b/cmd/relnote/todo_test.go
index cfa1323..95f2df0 100644
--- a/cmd/relnote/todo_test.go
+++ b/cmd/relnote/todo_test.go
@@ -5,13 +5,12 @@
package main
import (
- "bytes"
+ "slices"
"testing"
"testing/fstest"
- "time"
)
-func TestToDo(t *testing.T) {
+func TestInfoFromDocFiles(t *testing.T) {
files := map[string]string{
"a.md": "TODO: write something",
"b.md": "nothing to do",
@@ -22,14 +21,20 @@
for name, contents := range files {
dir[name] = &fstest.MapFile{Data: []byte(contents)}
}
- var buf bytes.Buffer
- if err := todo(&buf, dir, time.Time{}); err != nil {
+ var got []ToDo
+ addToDo := func(td ToDo) { got = append(got, td) }
+ addIssue := func(int) {}
+ if err := infoFromDocFiles(dir, addToDo, addIssue); err != nil {
t.Fatal(err)
}
- got := buf.String()
- want := `TODO: write something (from a.md:1)
-`
- if got != want {
- t.Errorf("\ngot:\n%s\nwant:\n%s", got, want)
+ want := []ToDo{
+ {
+ message: "TODO: write something",
+ provenance: "a.md:1",
+ },
+ }
+
+ if !slices.Equal(got, want) {
+ t.Errorf("\ngot:\n%+v\nwant:\n%+v", got, want)
}
}