blob: 7288a3f83c635dc1bb1f0477251d6920a5aaa3bb [file] [log] [blame]
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -04001// Copyright 2017 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
5package main
6
7import (
8 "bytes"
Andrew Bonventre2b0e3d62018-11-20 20:04:09 -05009 "encoding/json"
Andrew Bonventre3dc19412017-11-08 15:22:15 -050010 "fmt"
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -040011 "html/template"
12 "log"
13 "net/http"
14 "regexp"
15 "sort"
16 "strconv"
17 "strings"
18 "time"
19
20 "golang.org/x/build/maintner"
Dmitri Shuralyovf087b012021-08-20 20:38:44 -040021 "golang.org/x/build/maintner/maintnerd/maintapi/version"
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -040022)
23
24const (
25 labelProposal = "Proposal"
26
27 prefixProposal = "proposal:"
28 prefixDev = "[dev."
29)
30
Andrew Bonventre20901f72017-07-22 17:37:30 -040031// titleDirs returns a slice of prefix directories contained in a title. For
32// devapp,maintner: my cool new change, it will return ["devapp", "maintner"].
33// If there is no dir prefix, it will return nil.
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -040034func titleDirs(title string) []string {
35 if i := strings.Index(title, "\n"); i >= 0 {
36 title = title[:i]
37 }
38 title = strings.TrimSpace(title)
39 i := strings.Index(title, ":")
40 if i < 0 {
Andrew Bonventre20901f72017-07-22 17:37:30 -040041 return nil
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -040042 }
43 var (
44 b bytes.Buffer
45 r []string
46 )
47 for j := 0; j < i; j++ {
48 switch title[j] {
49 case ' ':
50 continue
51 case ',':
52 r = append(r, b.String())
53 b.Reset()
54 continue
55 default:
56 b.WriteByte(title[j])
57 }
58 }
59 if b.Len() > 0 {
60 r = append(r, b.String())
61 }
62 return r
63}
64
65type releaseData struct {
Andrew Bonventre2b0e3d62018-11-20 20:04:09 -050066 LastUpdated string
67 Sections []section
68 BurndownJSON template.JS
Dmitri Shuralyovf087b012021-08-20 20:38:44 -040069 CurMilestone string // The title of the current release milestone in GitHub. For example, "Go1.18".
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -040070
71 // dirty is set if this data needs to be updated due to a corpus change.
72 dirty bool
73}
74
75type section struct {
76 Title string
77 Count int
78 Groups []group
79}
80
81type group struct {
82 Dir string
83 Items []item
84}
85
86type item struct {
Keith Randallcda6dc72018-10-30 10:08:57 -070087 Issue *maintner.GitHubIssue
88 CLs []*gerritCL
89 FirstPerformance bool // set if this item is the first item which is labeled "performance"
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -040090}
91
Andrew Bonventrea9443262017-11-22 14:21:54 -050092func (i *item) ReleaseBlocker() bool {
93 if i.Issue == nil {
94 return false
95 }
96 return i.Issue.HasLabel("release-blocker")
97}
98
kawakami54405f22019-06-14 01:27:36 +090099func (i *item) EarlyInCycle() bool {
100 return !i.ReleaseBlocker() && i.Issue.HasLabel("early-in-cycle")
101}
102
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400103type itemsBySummary []item
104
Keith Randallcda6dc72018-10-30 10:08:57 -0700105func (x itemsBySummary) Len() int { return len(x) }
106func (x itemsBySummary) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
107func (x itemsBySummary) Less(i, j int) bool {
kawakami54405f22019-06-14 01:27:36 +0900108 // Sort release-blocker issues to the front
109 ri := x[i].Issue != nil && x[i].Issue.HasLabel("release-blocker")
110 rj := x[j].Issue != nil && x[j].Issue.HasLabel("release-blocker")
111 if ri != rj {
112 return ri
113 }
Keith Randallcda6dc72018-10-30 10:08:57 -0700114 // Sort performance issues to the end.
115 pi := x[i].Issue != nil && x[i].Issue.HasLabel("Performance")
116 pj := x[j].Issue != nil && x[j].Issue.HasLabel("Performance")
117 if pi != pj {
118 return !pi
119 }
120 // Otherwise sort by the item summary.
121 return itemSummary(x[i]) < itemSummary(x[j])
122}
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400123
124func itemSummary(it item) string {
125 if it.Issue != nil {
126 return it.Issue.Title
127 }
128 for _, cl := range it.CLs {
129 return cl.Subject()
130 }
131 return ""
132}
133
134var milestoneRE = regexp.MustCompile(`^Go1\.(\d+)(|\.(\d+))(|[A-Z].*)$`)
135
136type milestone struct {
137 title string
138 major, minor int
139}
140
Andrew Bonventre20901f72017-07-22 17:37:30 -0400141type milestonesByGoVersion []milestone
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400142
Andrew Bonventre20901f72017-07-22 17:37:30 -0400143func (x milestonesByGoVersion) Len() int { return len(x) }
144func (x milestonesByGoVersion) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
145func (x milestonesByGoVersion) Less(i, j int) bool {
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400146 a, b := x[i], x[j]
147 if a.major != b.major {
148 return a.major < b.major
149 }
150 if a.minor != b.minor {
151 return a.minor < b.minor
152 }
153 return a.title < b.title
154}
155
Andrew Bonventre67504a62017-11-08 12:58:56 -0500156var annotationRE = regexp.MustCompile(`(?m)^R=(.+)\b`)
157
158type gerritCL struct {
159 *maintner.GerritCL
Dmitri Shuralyovb5dfb972019-02-05 16:23:39 -0500160 NoPrefixTitle string // CL title without the directory prefix (e.g., "improve ListenAndServe" without leading "net/http: ").
161 Closed bool
162 Milestone string
Andrew Bonventre67504a62017-11-08 12:58:56 -0500163}
164
Andrew Bonventre3dc19412017-11-08 15:22:15 -0500165// ReviewURL returns the code review address of cl.
166func (cl *gerritCL) ReviewURL() string {
167 s := cl.Project.Server()
168 if s == "go.googlesource.com" {
169 return fmt.Sprintf("https://golang.org/cl/%d", cl.Number)
170 }
171 subd := strings.TrimSuffix(s, ".googlesource.com")
172 if subd == s {
173 return ""
174 }
175 return fmt.Sprintf("https://%s-review.googlesource.com/%d", subd, cl.Number)
176}
177
Andrew Bonventre2b0e3d62018-11-20 20:04:09 -0500178// burndownData is encoded to JSON and embedded in the page for use when
179// rendering a burndown chart using JavaScript.
180type burndownData struct {
181 Milestone string `json:"milestone"`
182 Entries []burndownEntry `json:"entries"`
183}
184
185type burndownEntry struct {
186 DateStr string `json:"dateStr"` // "12-25"
187 Open int `json:"open"`
188 Blockers int `json:"blockers"`
189}
190
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400191func (s *server) updateReleaseData() {
192 log.Println("Updating release data ...")
193 s.cMu.Lock()
194 defer s.cMu.Unlock()
195
Andrew Bonventre67504a62017-11-08 12:58:56 -0500196 dirToCLs := map[string][]*gerritCL{}
197 issueToCLs := map[int32][]*gerritCL{}
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400198 s.corpus.Gerrit().ForeachProjectUnsorted(func(p *maintner.GerritProject) error {
199 p.ForeachOpenCL(func(cl *maintner.GerritCL) error {
200 if strings.HasPrefix(cl.Subject(), prefixDev) {
201 return nil
202 }
Andrew Bonventre67504a62017-11-08 12:58:56 -0500203
204 var (
Dmitri Shuralyovb5dfb972019-02-05 16:23:39 -0500205 pkgs, title = ParsePrefixedChangeTitle(projectRoot(p), cl.Subject())
Andrew Bonventre67504a62017-11-08 12:58:56 -0500206 closed bool
207 closedVersion int32
208 milestone string
209 )
210 for _, m := range cl.Messages {
211 if closed && closedVersion < m.Version {
212 closed = false
213 }
214 sm := annotationRE.FindStringSubmatch(m.Message)
215 if sm == nil {
216 continue
217 }
218 val := sm[1]
219 if val == "close" || val == "closed" {
220 closedVersion = m.Version
221 closed = true
222 } else if milestoneRE.MatchString(val) {
223 milestone = val
224 }
225 }
226 gcl := &gerritCL{
Dmitri Shuralyovb5dfb972019-02-05 16:23:39 -0500227 GerritCL: cl,
228 NoPrefixTitle: title,
229 Closed: closed,
230 Milestone: milestone,
Andrew Bonventre67504a62017-11-08 12:58:56 -0500231 }
232
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400233 for _, r := range cl.GitHubIssueRefs {
Andrew Bonventre67504a62017-11-08 12:58:56 -0500234 issueToCLs[r.Number] = append(issueToCLs[r.Number], gcl)
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400235 }
Dmitri Shuralyovb5dfb972019-02-05 16:23:39 -0500236 if len(pkgs) == 0 {
Andrew Bonventre67504a62017-11-08 12:58:56 -0500237 dirToCLs[""] = append(dirToCLs[""], gcl)
Andrew Bonventre20901f72017-07-22 17:37:30 -0400238 } else {
Dmitri Shuralyovb5dfb972019-02-05 16:23:39 -0500239 for _, p := range pkgs {
240 dirToCLs[p] = append(dirToCLs[p], gcl)
Andrew Bonventre20901f72017-07-22 17:37:30 -0400241 }
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400242 }
243 return nil
244 })
245 return nil
246 })
247
Dmitri Shuralyovf087b012021-08-20 20:38:44 -0400248 // Determine current milestone based on the highest go1.X tag.
249 var highestGo1X int
250 s.proj.ForeachNonChangeRef(func(ref string, _ maintner.GitHash) error {
251 if !strings.HasPrefix(ref, "refs/tags/go1.") {
252 return nil
253 }
254 tagName := ref[len("refs/tags/"):]
255 if _, x, _, ok := version.ParseTag(tagName); ok && x > highestGo1X {
256 highestGo1X = x
257 }
258 return nil
259 })
260 // The title of the current release milestone in GitHub. For example, "Go1.18".
261 curMilestoneTitle := fmt.Sprintf("Go1.%d", highestGo1X+1)
262 // The start date of the current release milestone, approximated by taking the
263 // Go 1.17 release date, and adding 6 months for each successive major release.
264 var monthsSinceGo117Release = time.Month(6 * (highestGo1X - 17))
265 curMilestoneStart := time.Date(2021, time.August+monthsSinceGo117Release, 1, 0, 0, 0, 0, time.UTC)
266
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400267 dirToIssues := map[string][]*maintner.GitHubIssue{}
Andrew Bonventre2b0e3d62018-11-20 20:04:09 -0500268 var curMilestoneIssues []*maintner.GitHubIssue
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400269 s.repo.ForeachIssue(func(issue *maintner.GitHubIssue) error {
Andrew Bonventre2b0e3d62018-11-20 20:04:09 -0500270 // Only include issues in active milestones.
271 if issue.Milestone.IsUnknown() || issue.Milestone.Closed || issue.Milestone.IsNone() {
272 return nil
273 }
274
275 if issue.Milestone.Title == curMilestoneTitle {
276 curMilestoneIssues = append(curMilestoneIssues, issue)
277 }
278
279 // Only open issues are displayed on the page using dirToIssues.
280 if issue.Closed {
281 return nil
282 }
283
284 dirs := titleDirs(issue.Title)
285 if len(dirs) == 0 {
286 dirToIssues[""] = append(dirToIssues[""], issue)
287 } else {
288 for _, d := range dirs {
289 dirToIssues[d] = append(dirToIssues[d], issue)
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400290 }
291 }
292 return nil
293 })
294
Andrew Bonventre2b0e3d62018-11-20 20:04:09 -0500295 bd := burndownData{Milestone: curMilestoneTitle}
296 for t, now := curMilestoneStart, time.Now(); t.Before(now); t = t.Add(24 * time.Hour) {
297 var e burndownEntry
298 for _, issue := range curMilestoneIssues {
299 if issue.Created.After(t) || (issue.Closed && issue.ClosedAt.Before(t)) {
300 continue
301 }
302 if issue.HasLabel("release-blocker") {
303 e.Blockers++
304 }
305 e.Open++
306 }
307 e.DateStr = t.Format("01-02")
308 bd.Entries = append(bd.Entries, e)
309 }
310
311 var buf bytes.Buffer
312 if err := json.NewEncoder(&buf).Encode(bd); err != nil {
313 log.Printf("json.Encode: %v", err)
314 }
315 s.data.release.BurndownJSON = template.JS(buf.String())
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400316 s.data.release.Sections = nil
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400317 s.appendOpenIssues(dirToIssues, issueToCLs)
318 s.appendPendingCLs(dirToCLs)
319 s.appendPendingProposals(issueToCLs)
320 s.appendClosedIssues()
Dmitri Shuralyovf087b012021-08-20 20:38:44 -0400321 s.data.release.CurMilestone = curMilestoneTitle
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400322 s.data.release.LastUpdated = time.Now().UTC().Format(time.UnixDate)
323 s.data.release.dirty = false
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400324}
325
Dmitri Shuralyovb5dfb972019-02-05 16:23:39 -0500326// projectRoot returns the import path corresponding to the repo root
327// of the Gerrit project p. For golang.org/x subrepos, the golang.org
328// part is omitted for previty.
329func projectRoot(p *maintner.GerritProject) string {
330 switch p.Server() {
331 case "go.googlesource.com":
332 switch subrepo := p.Project(); subrepo {
333 case "go":
334 // Main Go repo.
335 return ""
336 case "dl":
337 // dl is a special subrepo, there's no /x/ in its import path.
338 return "golang.org/dl"
339 case "gddo":
340 // There is no golang.org/x/gddo vanity import path, and
341 // the canonical import path for gddo is on GitHub.
342 return "github.com/golang/gddo"
343 default:
344 // For brevity, use x/subrepo rather than golang.org/x/subrepo.
345 return "x/" + subrepo
346 }
347 case "code.googlesource.com":
348 switch p.Project() {
349 case "gocloud":
350 return "cloud.google.com/go"
351 case "google-api-go-client":
352 return "google.golang.org/api"
353 }
354 }
355 return p.ServerSlashProject()
356}
357
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400358// requires s.cMu be locked.
Andrew Bonventre67504a62017-11-08 12:58:56 -0500359func (s *server) appendOpenIssues(dirToIssues map[string][]*maintner.GitHubIssue, issueToCLs map[int32][]*gerritCL) {
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400360 var issueDirs []string
361 for d := range dirToIssues {
362 issueDirs = append(issueDirs, d)
363 }
364 sort.Strings(issueDirs)
365 ms := s.allMilestones()
366 for _, m := range ms {
367 var (
368 issueGroups []group
369 issueCount int
370 )
371 for _, d := range issueDirs {
372 issues, ok := dirToIssues[d]
373 if !ok {
374 continue
375 }
376 var items []item
377 for _, i := range issues {
378 if i.Milestone.Title != m.title {
379 continue
380 }
381
382 items = append(items, item{
383 Issue: i,
384 CLs: issueToCLs[i.Number],
385 })
386 issueCount++
387 }
388 if len(items) == 0 {
389 continue
390 }
391 sort.Sort(itemsBySummary(items))
Keith Randallcda6dc72018-10-30 10:08:57 -0700392 for idx := range items {
Keith Randall229a5942019-12-03 10:11:58 -0800393 if items[idx].Issue.HasLabel("Performance") && !items[idx].Issue.HasLabel("release-blocker") {
Keith Randallcda6dc72018-10-30 10:08:57 -0700394 items[idx].FirstPerformance = true
395 break
396 }
397 }
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400398 issueGroups = append(issueGroups, group{
399 Dir: d,
400 Items: items,
401 })
402 }
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400403 s.data.release.Sections = append(s.data.release.Sections, section{
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400404 Title: m.title,
405 Count: issueCount,
406 Groups: issueGroups,
407 })
408 }
409}
410
411// requires s.cMu be locked.
Andrew Bonventre67504a62017-11-08 12:58:56 -0500412func (s *server) appendPendingCLs(dirToCLs map[string][]*gerritCL) {
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400413 var clDirs []string
414 for d := range dirToCLs {
415 clDirs = append(clDirs, d)
416 }
417 sort.Strings(clDirs)
418 var (
419 clGroups []group
420 clCount int
421 )
422 for _, d := range clDirs {
423 if cls, ok := dirToCLs[d]; ok {
424 clCount += len(cls)
425 g := group{Dir: d}
426 g.Items = append(g.Items, item{CLs: cls})
427 sort.Sort(itemsBySummary(g.Items))
428 clGroups = append(clGroups, g)
429 }
430 }
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400431 s.data.release.Sections = append(s.data.release.Sections, section{
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400432 Title: "Pending CLs",
433 Count: clCount,
434 Groups: clGroups,
435 })
436}
437
438// requires s.cMu be locked.
Andrew Bonventre67504a62017-11-08 12:58:56 -0500439func (s *server) appendPendingProposals(issueToCLs map[int32][]*gerritCL) {
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400440 var proposals group
441 s.repo.ForeachIssue(func(issue *maintner.GitHubIssue) error {
442 if issue.Closed {
443 return nil
444 }
445 if issue.HasLabel(labelProposal) || strings.HasPrefix(issue.Title, prefixProposal) {
446 proposals.Items = append(proposals.Items, item{
447 Issue: issue,
448 CLs: issueToCLs[issue.Number],
449 })
450 }
451 return nil
452 })
453 sort.Sort(itemsBySummary(proposals.Items))
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400454 s.data.release.Sections = append(s.data.release.Sections, section{
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400455 Title: "Pending Proposals",
456 Count: len(proposals.Items),
457 Groups: []group{proposals},
458 })
459}
460
461// requires s.cMu be locked.
462func (s *server) appendClosedIssues() {
463 var (
464 closed group
465 lastWeek = time.Now().Add(-(7*24 + 12) * time.Hour)
466 )
467 s.repo.ForeachIssue(func(issue *maintner.GitHubIssue) error {
468 if !issue.Closed {
469 return nil
470 }
471 if issue.Updated.After(lastWeek) {
472 closed.Items = append(closed.Items, item{Issue: issue})
473 }
474 return nil
475 })
476 sort.Sort(itemsBySummary(closed.Items))
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400477 s.data.release.Sections = append(s.data.release.Sections, section{
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400478 Title: "Closed Last Week",
479 Count: len(closed.Items),
480 Groups: []group{closed},
481 })
482}
483
484// requires s.cMu be read locked.
485func (s *server) allMilestones() []milestone {
486 var ms []milestone
487 s.repo.ForeachMilestone(func(m *maintner.GitHubMilestone) error {
488 if m.Closed {
489 return nil
490 }
491 sm := milestoneRE.FindStringSubmatch(m.Title)
492 if sm == nil {
493 return nil
494 }
495 major, _ := strconv.Atoi(sm[1])
496 minor, _ := strconv.Atoi(sm[3])
497 ms = append(ms, milestone{
498 title: m.Title,
499 major: major,
500 minor: minor,
501 })
502 return nil
503 })
Andrew Bonventre20901f72017-07-22 17:37:30 -0400504 sort.Sort(milestonesByGoVersion(ms))
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400505 return ms
506}
507
508// handleRelease serves dev.golang.org/release.
509func (s *server) handleRelease(t *template.Template, w http.ResponseWriter, r *http.Request) {
510 w.Header().Set("Content-Type", "text/html; charset=utf-8")
511 s.cMu.RLock()
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400512 dirty := s.data.release.dirty
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400513 s.cMu.RUnlock()
514 if dirty {
515 s.updateReleaseData()
516 }
517
518 s.cMu.RLock()
519 defer s.cMu.RUnlock()
Andrew Bonventre99aa0722017-09-20 16:27:09 -0400520 if err := t.Execute(w, s.data.release); err != nil {
Andrew Bonventree3b7b1d2017-07-18 17:04:23 -0400521 log.Printf("t.Execute(w, nil) = %v", err)
522 return
523 }
524}