From 4b285768f2a2d5bc15e6d7d32ed6cab83f5b3530 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Fri, 10 May 2024 08:29:58 -0400 Subject: [PATCH] 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 Reviewed-by: Russ Cox --- cmd/relnote/relnote.go | 27 ++++++++++----------- cmd/relnote/todo.go | 51 +++++++++++++++++++++++++++++++++++----- cmd/relnote/todo_test.go | 25 ++++++++++++-------- 3 files changed, 73 insertions(+), 30 deletions(-) diff --git a/cmd/relnote/relnote.go b/cmd/relnote/relnote.go index e24d6c82e..4103949e9 100644 --- a/cmd/relnote/relnote.go +++ b/cmd/relnote/relnote.go @@ -19,8 +19,9 @@ import ( ) 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 @@ func usage() { 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 @@ func main() { 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 bf346515a..1faec0991 100644 --- a/cmd/relnote/todo.go +++ b/cmd/relnote/todo.go @@ -10,6 +10,10 @@ import ( "fmt" "io" "io/fs" + "log" + "os" + "os/exec" + "path/filepath" "regexp" "slices" "strconv" @@ -29,24 +33,59 @@ type ToDo struct { // 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 cfa1323fa..95f2df09c 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 @@ func TestToDo(t *testing.T) { 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) } }