golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/relnote/todo.go (about)

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bufio"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"io/fs"
    13  	"strings"
    14  	"time"
    15  
    16  	"golang.org/x/build/gerrit"
    17  	"golang.org/x/build/maintner"
    18  	"golang.org/x/build/maintner/godata"
    19  )
    20  
    21  type ToDo struct {
    22  	message    string // what is to be done
    23  	provenance string // where the TODO came from
    24  }
    25  
    26  // todo prints a report to w on which release notes need to be written.
    27  // It takes the doc/next directory of the repo and the date of the last release.
    28  func todo(w io.Writer, fsys fs.FS, prevRelDate time.Time) error {
    29  	var todos []ToDo
    30  
    31  	add := func(td ToDo) { todos = append(todos, td) }
    32  
    33  	if err := todosFromDocFiles(fsys, add); err != nil {
    34  		return err
    35  	}
    36  	if !prevRelDate.IsZero() {
    37  		if err := todosFromRelnoteCLs(prevRelDate, add); err != nil {
    38  			return err
    39  		}
    40  	}
    41  	return writeToDos(w, todos)
    42  }
    43  
    44  // Collect TODOs from the markdown files in the main repo.
    45  func todosFromDocFiles(fsys fs.FS, add func(ToDo)) error {
    46  	// This is essentially a grep.
    47  	return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
    48  		if err != nil {
    49  			return err
    50  		}
    51  		if !d.IsDir() && strings.HasSuffix(path, ".md") {
    52  			if err := todosFromFile(fsys, path, add); err != nil {
    53  				return err
    54  			}
    55  		}
    56  		return nil
    57  	})
    58  }
    59  
    60  func todosFromFile(dir fs.FS, filename string, add func(ToDo)) error {
    61  	f, err := dir.Open(filename)
    62  	if err != nil {
    63  		return err
    64  	}
    65  	defer f.Close()
    66  	scan := bufio.NewScanner(f)
    67  	ln := 0
    68  	for scan.Scan() {
    69  		ln++
    70  		if line := scan.Text(); strings.Contains(line, "TODO") {
    71  			add(ToDo{
    72  				message:    line,
    73  				provenance: fmt.Sprintf("%s:%d", filename, ln),
    74  			})
    75  		}
    76  	}
    77  	return scan.Err()
    78  }
    79  
    80  func todosFromRelnoteCLs(cutoff time.Time, add func(ToDo)) error {
    81  	ctx := context.Background()
    82  	// The maintner corpus doesn't track inline comments. See go.dev/issue/24863.
    83  	// So we need to use a Gerrit API client to fetch them instead. If maintner starts
    84  	// tracking inline comments in the future, this extra complexity can be dropped.
    85  	gerritClient := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
    86  	matchedCLs, err := findCLsWithRelNote(gerritClient, cutoff)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	corpus, err := godata.Get(ctx)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	return corpus.Gerrit().ForeachProjectUnsorted(func(gp *maintner.GerritProject) error {
    95  		if gp.Server() != "go.googlesource.com" {
    96  			return nil
    97  		}
    98  		return gp.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
    99  			if cl.Status != "merged" {
   100  				return nil
   101  			}
   102  			if cl.Branch() != "master" {
   103  				// Ignore CLs sent to development or release branches.
   104  				return nil
   105  			}
   106  			if cl.Commit.CommitTime.Before(cutoff) {
   107  				// Was in a previous release; not for this one.
   108  				return nil
   109  			}
   110  			// TODO(jba): look for accepted proposals that don't have release notes.
   111  			if _, ok := matchedCLs[int(cl.Number)]; ok {
   112  				comments, err := gerritClient.ListChangeComments(context.Background(), fmt.Sprint(cl.Number))
   113  				if err != nil {
   114  					return err
   115  				}
   116  				if rn := clRelNote(cl, comments); rn != "" {
   117  					if rn == "yes" || rn == "y" {
   118  						rn = "UNKNOWN"
   119  					}
   120  					add(ToDo{
   121  						message:    "TODO:" + rn,
   122  						provenance: fmt.Sprintf("RELNOTE comment in https://go.dev/cl/%d", cl.Number),
   123  					})
   124  				}
   125  			}
   126  			return nil
   127  		})
   128  	})
   129  }
   130  
   131  func writeToDos(w io.Writer, todos []ToDo) error {
   132  	for _, td := range todos {
   133  		if _, err := fmt.Fprintf(w, "%s (from %s)\n", td.message, td.provenance); err != nil {
   134  			return err
   135  		}
   136  	}
   137  	return nil
   138  }