[build] devapp: remove godash code in favor of maintner

9 views
Skip to first unread message

Andrew Bonventre (Gerrit)

unread,
Jul 21, 2017, 5:07:46 PM7/21/17
to Brad Fitzpatrick, Ian Lance Taylor, golang-co...@googlegroups.com

Andrew Bonventre would like Brad Fitzpatrick to review this change.

View Change

devapp: remove godash code in favor of maintner

Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
---
M devapp/devapp.go
D devapp/github.go
M devapp/server.go
A devapp/templates/release.tmpl
4 files changed, 28 insertions(+), 195 deletions(-)

diff --git a/devapp/devapp.go b/devapp/devapp.go
index 468bf8d..bbf80e8 100644
--- a/devapp/devapp.go
+++ b/devapp/devapp.go
@@ -7,7 +7,6 @@
package main

import (
- "bytes"
"context"
"crypto/tls"
"errors"
@@ -21,13 +20,10 @@
"os"
"strconv"
"strings"
- "sync/atomic"
"time"

"cloud.google.com/go/storage"
"golang.org/x/build/autocertcache"
- "golang.org/x/build/gerrit"
- "golang.org/x/build/godash"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/net/http2"
)
@@ -44,15 +40,13 @@
listen = flag.String("listen", "localhost:6343", "listen address")
devTLSPort = flag.Int("dev-tls-port", 0, "if non-zero, port number to run localhost self-signed TLS server")
autocertBucket = flag.String("autocert-bucket", "", "if non-empty, listen on port 443 and serve a LetsEncrypt TLS cert using this Google Cloud Storage bucket as a cache")
- updateInterval = flag.Duration("update-interval", 5*time.Minute, "how often to update the dashboard data")
staticDir = flag.String("static-dir", "./static/", "location of static directory relative to binary location")
+ templateDir = flag.String("template-dir", "./templates/", "location of templates directory relative to binary location")
)
flag.Parse()
rand.Seed(time.Now().UnixNano())

- go updateLoop(*updateInterval)
-
- s := newServer(http.NewServeMux(), *staticDir)
+ s := newServer(http.NewServeMux(), *staticDir, *templateDir)
ctx := context.Background()
s.initCorpus(ctx)
go s.corpusUpdateLoop(ctx)
@@ -83,16 +77,6 @@
log.Fatal(<-errc)
}

-func updateLoop(interval time.Duration) {
- ticker := time.NewTicker(interval)
- for {
- if err := update(); err != nil {
- log.Printf("update: %v", err)
- }
- <-ticker.C
- }
-}
-
func redirectHTTP(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil || r.Host == "" {
http.NotFound(w, r)
@@ -171,70 +155,3 @@
tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}
-
-type countTransport struct {
- http.RoundTripper
- count int64
-}
-
-func (ct *countTransport) RoundTrip(req *http.Request) (*http.Response, error) {
- atomic.AddInt64(&ct.count, 1)
- return ct.RoundTripper.RoundTrip(req)
-}
-
-func (ct *countTransport) Count() int64 {
- return atomic.LoadInt64(&ct.count)
-}
-
-func update() error {
- log.Printf("Updating dashboard data...")
- token, err := getToken()
- if err != nil {
- return err
- }
- gzdata, _ := getCache("gzdata")
-
- ctx := context.Background()
- ct := &countTransport{newTransport(ctx), 0}
- gh := godash.NewGitHubClient("golang/go", token, ct)
- defer func() {
- log.Printf("Sent %d requests to GitHub", ct.Count())
- }()
-
- data, err := parseData(gzdata)
- if err != nil {
- return err
- }
-
- if err := data.Reviewers.LoadGithub(ctx, gh); err != nil {
- return fmt.Errorf("failed to load reviewers: %v", err)
- }
- ger := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
-
- if err := data.FetchData(ctx, gh, ger, log.Printf, 7, false, false); err != nil {
- return fmt.Errorf("failed to fetch data: %v", err)
- }
-
- var output bytes.Buffer
- const kind = "release"
- fmt.Fprintf(&output, "Go %s dashboard\n", kind)
- fmt.Fprintf(&output, "%v\n\n", time.Now().UTC().Format(time.UnixDate))
- fmt.Fprintf(&output, "HOWTO\n\n")
- data.PrintIssues(&output)
- var html bytes.Buffer
- godash.PrintHTML(&html, output.String())
-
- if err := writePage(kind, html.Bytes()); err != nil {
- return err
- }
- return writeCache("gzdata", &data)
-}
-
-func newTransport(ctx context.Context) http.RoundTripper {
- dline, ok := ctx.Deadline()
- t := &http.Transport{}
- if ok {
- t.ResponseHeaderTimeout = time.Until(dline)
- }
- return t
-}
diff --git a/devapp/github.go b/devapp/github.go
deleted file mode 100644
index 196be42..0000000
--- a/devapp/github.go
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright 2017 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package main
-
-import (
- "flag"
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
- "strings"
- "sync"
-
- "cloud.google.com/go/compute/metadata"
-)
-
-var (
- tokenFile = flag.String("token", "", "read GitHub token personal access token from `file` (default $HOME/.github-issue-token)")
-
- githubToken string
- githubOnceErr error
- githubOnce sync.Once
-)
-
-func getTokenFromFile() (string, error) {
- githubOnce.Do(func() {
- const short = ".github-issue-token"
- filename := filepath.Clean(os.Getenv("HOME") + "/" + short)
- shortFilename := filepath.Clean("$HOME/" + short)
- if *tokenFile != "" {
- filename = *tokenFile
- shortFilename = *tokenFile
- }
- data, err := ioutil.ReadFile(filename)
- if err != nil {
- const fmtStr = `reading token: %v
-
-Please create a personal access token at https://github.com/settings/tokens/new
-and write it to %s or store it in GCE metadata using the
-key 'maintner-github-token' to use this program.
-The token only needs the repo scope, or private_repo if you want to view or edit
-issues for private repositories. The benefit of using a personal access token
-over using your GitHub password directly is that you can limit its use and revoke
-it at any time.`
- githubOnceErr = fmt.Errorf(fmtStr, err, shortFilename)
- return
- }
- fi, err := os.Stat(filename)
- if err != nil {
- githubOnceErr = err
- return
- }
- if fi.Mode()&0077 != 0 {
- githubOnceErr = fmt.Errorf("reading token: %s mode is %#o, want %#o", shortFilename, fi.Mode()&0777, fi.Mode()&0700)
- return
- }
- githubToken = strings.TrimSpace(string(data))
- })
- return githubToken, githubOnceErr
-}
-
-func getToken() (string, error) {
- if metadata.OnGCE() {
- // Re-use maintner-github-token until this is migrated to using the maintner API.
- token, err := metadata.ProjectAttributeValue("maintner-github-token")
- if len(token) > 0 && err == nil {
- return token, nil
- }
- }
- return getTokenFromFile()
-}
diff --git a/devapp/server.go b/devapp/server.go
index 29d911d..77761e3 100644
--- a/devapp/server.go
+++ b/devapp/server.go
@@ -8,6 +8,7 @@
"context"
"encoding/json"
"fmt"
+ "html/template"
"log"
"math/rand"
"net/http"
@@ -24,8 +25,9 @@
// A server is an http.Handler that serves content within staticDir at root and
// the dynamically-generated dashboards at their respective endpoints.
type server struct {
- mux *http.ServeMux
- staticDir string
+ mux *http.ServeMux
+ staticDir string
+ templateDir string

cMu sync.RWMutex // Used to protect the fields below.
corpus *maintner.Corpus
@@ -35,15 +37,16 @@
totalPoints int
}

-func newServer(mux *http.ServeMux, staticDir string) *server {
+func newServer(mux *http.ServeMux, staticDir, templateDir string) *server {
s := &server{
mux: mux,
staticDir: staticDir,
+ templateDir: templateDir,
userMapping: map[int]*maintner.GitHubUser{},
}
s.mux.Handle("/", http.FileServer(http.Dir(s.staticDir)))
s.mux.HandleFunc("/favicon.ico", s.handleFavicon)
- s.mux.HandleFunc("/release", handleRelease)
+ s.mux.HandleFunc("/release", s.handleRelease)
for _, p := range []string{"/imfeelinghelpful", "/imfeelinglucky"} {
s.mux.HandleFunc(p, s.handleRandomHelpWantedIssue)
}
@@ -308,39 +311,11 @@
s.mux.ServeHTTP(w, r)
}

-var (
- pageStoreMu sync.Mutex
- pageStore = map[string][]byte{}
-)
-
-func getPage(name string) ([]byte, error) {
- pageStoreMu.Lock()
- defer pageStoreMu.Unlock()
- p, ok := pageStore[name]
- if ok {
- return p, nil
- }
- return nil, fmt.Errorf("page key %s not found", name)
-}
-
-func writePage(key string, content []byte) error {
- pageStoreMu.Lock()
- defer pageStoreMu.Unlock()
- pageStore[key] = content
- return nil
-}
-
-func servePage(w http.ResponseWriter, r *http.Request, key string) {
- b, err := getPage(key)
- if err != nil {
- log.Printf("getPage(%q) = %v", key, err)
- http.NotFound(w, r)
+func (s *server) handleRelease(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ t := template.Must(template.ParseFiles(path.Join(s.templateDir, "/release.tmpl")))
+ if err := t.Execute(w, nil); err != nil {
+ log.Printf("t.Execute(w, nil) = %v", err)
return
}
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- w.Write(b)
-}
-
-func handleRelease(w http.ResponseWriter, r *http.Request) {
- servePage(w, r, "release")
}
diff --git a/devapp/templates/release.tmpl b/devapp/templates/release.tmpl
new file mode 100644
index 0000000..143e85e
--- /dev/null
+++ b/devapp/templates/release.tmpl
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Release Dashboard</title>
+<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1">
+<style>
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+</style>
+<pre>
+ Release dashboard
+</pre>
\ No newline at end of file

To view, visit change 50652. To unsubscribe, visit settings.

Gerrit-Project: build
Gerrit-Branch: master
Gerrit-MessageType: newchange
Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
Gerrit-Change-Number: 50652
Gerrit-PatchSet: 1
Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>

Andrew Bonventre (Gerrit)

unread,
Jul 21, 2017, 5:20:38 PM7/21/17
to Brad Fitzpatrick, Kevin Burke, golang-co...@googlegroups.com

Andrew Bonventre uploaded patch set #2 to this change.

View Change

devapp: remove godash code in favor of maintner

Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
---
D devapp/cache.go

M devapp/devapp.go
D devapp/github.go
M devapp/server.go
A devapp/templates/release.tmpl
5 files changed, 28 insertions(+), 275 deletions(-)

To view, visit change 50652. To unsubscribe, visit settings.

Gerrit-Project: build
Gerrit-Branch: master
Gerrit-MessageType: newpatchset
Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
Gerrit-Change-Number: 50652
Gerrit-PatchSet: 2
Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
Gerrit-Reviewer: Andrew Bonventre <andy...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Kevin Burke <k...@inburke.com>

Andrew Bonventre (Gerrit)

unread,
Jul 21, 2017, 7:38:46 PM7/21/17
to Brad Fitzpatrick, Kevin Burke, golang-co...@googlegroups.com

Andrew Bonventre uploaded patch set #4 to this change.

View Change

devapp: remove godash code in favor of maintner

Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
---
D devapp/cache.go

M devapp/devapp.go
D devapp/github.go
M devapp/server.go
4 files changed, 4 insertions(+), 275 deletions(-)

To view, visit change 50652. To unsubscribe, visit settings.

Gerrit-Project: build
Gerrit-Branch: master
Gerrit-MessageType: newpatchset
Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
Gerrit-Change-Number: 50652
Gerrit-PatchSet: 4
Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
Gerrit-Reviewer: Andrew Bonventre <andy...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Kevin Burke <k...@inburke.com>

Andrew Bonventre (Gerrit)

unread,
Jul 21, 2017, 8:17:28 PM7/21/17
to Brad Fitzpatrick, Kevin Burke, golang-co...@googlegroups.com

Andrew Bonventre uploaded patch set #5 to this change.

View Change

devapp: remove godash code in favor of maintner

Adds two new functions to maintner:
+ (*GerritCL).Subject() returns the first line of the latest
commit message.
+ (*GitHubRepo).ForeachMilestone calls a passed function for each
milestone in the repo.

Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
---
M devapp/Dockerfile

D devapp/cache.go
M devapp/devapp.go
D devapp/github.go
A devapp/gophercon.go
A devapp/gophercon_test.go
A devapp/release.go
A devapp/release_test.go
M devapp/server.go
M devapp/server_test.go
A devapp/templates/release.tmpl
M maintner/gerrit.go
M maintner/gerrit_test.go
M maintner/github.go
14 files changed, 703 insertions(+), 483 deletions(-)

To view, visit change 50652. To unsubscribe, visit settings.

Gerrit-Project: build
Gerrit-Branch: master
Gerrit-MessageType: newpatchset
Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
Gerrit-Change-Number: 50652
Gerrit-PatchSet: 5
Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
Gerrit-Reviewer: Andrew Bonventre <andy...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Kevin Burke <k...@inburke.com>

Andrew Bonventre (Gerrit)

unread,
Jul 21, 2017, 8:19:02 PM7/21/17
to Kevin Burke, Brad Fitzpatrick, golang-co...@googlegroups.com

Andrew Bonventre posted comments on this change.

View Change

Patch set 5:

OK. This is now the canonical patch.

    To view, visit change 50652. To unsubscribe, visit settings.

    Gerrit-Project: build
    Gerrit-Branch: master
    Gerrit-MessageType: comment
    Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
    Gerrit-Change-Number: 50652
    Gerrit-PatchSet: 5
    Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Reviewer: Kevin Burke <k...@inburke.com>
    Gerrit-Comment-Date: Sat, 22 Jul 2017 00:19:00 +0000
    Gerrit-HasComments: No
    Gerrit-HasLabels: No

    Brad Fitzpatrick (Gerrit)

    unread,
    Jul 21, 2017, 8:32:26 PM7/21/17
    to Andrew Bonventre, Brad Fitzpatrick, Kevin Burke, golang-co...@googlegroups.com

    Brad Fitzpatrick posted comments on this change.

    View Change

    Patch set 5:

    (5 comments)

    • File devapp/gophercon.go:

      • Patch Set #5, Line 5: package main

        Add a file-level comment above here with some context. Be sure to have blank line so it's not a package comment.

    • File devapp/release.go:

      • Patch Set #5, Line 118: func (s *server) updateReleaseData() {

        So, you're doing all this work on every maintner corpus update. I suppose that works, but my guess is that there's a lot more maintner corpus updates than there are page loads of dev.golang.org/release.

        I think all this work could be done on demand when needed, not even caching s.data.

        But if it's important to cache s.data (say, dev.golang.org/release is posted to reddit or hacker news or something), then you could just have a "dirty bool" on server and then after corpus.Update you can just set dirty = true, and in handleRelease only re-build the s.data structure if dirty.

      • Patch Set #5, Line 231: func (s *server) appendPendingProposals(issueToCLs map[int32][]*maintner.GerritCL) {

        I'd either name these helper methods with "Locked" at the end of the method names or document the preconditions above them in comments:

        // requires s.cMu be locked.

      • Patch Set #5, Line 299: func (s *server) handleRelease(t *template.Template, w http.ResponseWriter, r *http.Request) {

        comment above this:

        // handleRelease serves dev.golang.org/release.

      • Patch Set #5, Line 301: s.data

        I don't see any locking protecting s.data.

        This needs to be RLocked using the same mutex protecting the corpus and used with UpdateWithLocker

    To view, visit change 50652. To unsubscribe, visit settings.

    Gerrit-Project: build
    Gerrit-Branch: master
    Gerrit-MessageType: comment
    Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
    Gerrit-Change-Number: 50652
    Gerrit-PatchSet: 5
    Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Reviewer: Kevin Burke <k...@inburke.com>
    Gerrit-Comment-Date: Sat, 22 Jul 2017 00:32:24 +0000
    Gerrit-HasComments: Yes
    Gerrit-HasLabels: No

    Andrew Bonventre (Gerrit)

    unread,
    Jul 21, 2017, 11:20:13 PM7/21/17
    to Brad Fitzpatrick, Kevin Burke, golang-co...@googlegroups.com

    Andrew Bonventre uploaded patch set #6 to this change.

    View Change

    devapp: remove godash code in favor of maintner

    Adds two new functions to maintner:
    + (*GerritCL).Subject() returns the first line of the latest
    commit message.
    + (*GitHubRepo).ForeachMilestone calls a passed function for each
    milestone in the repo.

    Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
    ---
    M devapp/Dockerfile
    D devapp/cache.go
    M devapp/devapp.go
    D devapp/github.go
    A devapp/gophercon.go
    A devapp/gophercon_test.go
    A devapp/release.go
    A devapp/release_test.go
    M devapp/server.go
    M devapp/server_test.go
    A devapp/templates/release.tmpl
    M maintner/gerrit.go
    M maintner/gerrit_test.go
    M maintner/github.go
    14 files changed, 730 insertions(+), 483 deletions(-)

    To view, visit change 50652. To unsubscribe, visit settings.

    Gerrit-Project: build
    Gerrit-Branch: master
    Gerrit-MessageType: newpatchset
    Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
    Gerrit-Change-Number: 50652
    Gerrit-PatchSet: 6
    Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Reviewer: Kevin Burke <k...@inburke.com>

    Andrew Bonventre (Gerrit)

    unread,
    Jul 21, 2017, 11:20:29 PM7/21/17
    to Brad Fitzpatrick, Kevin Burke, golang-co...@googlegroups.com

    Andrew Bonventre posted comments on this change.

    View Change

    Patch set 6:

    (5 comments)

      • Add a file-level comment above here with some context. Be sure to have blan

      • So, you're doing all this work on every maintner corpus update. I suppose t

      • Done. Now that I’m getting the hang of maintner things I’ll likely follow up with another change applying the same type of recalculate-on-dirty for the other endpoints.

      • Patch Set #5, Line 231: s.data.Sections = append(s.data.Sections, section{

      • I'd either name these helper methods with "Locked" at the end of the method

      • I don't see any locking protecting s.data.

        Done

    To view, visit change 50652. To unsubscribe, visit settings.

    Gerrit-Project: build
    Gerrit-Branch: master
    Gerrit-MessageType: comment
    Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
    Gerrit-Change-Number: 50652
    Gerrit-PatchSet: 6
    Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Reviewer: Kevin Burke <k...@inburke.com>
    Gerrit-Comment-Date: Sat, 22 Jul 2017 03:20:27 +0000
    Gerrit-HasComments: Yes
    Gerrit-HasLabels: No

    Brad Fitzpatrick (Gerrit)

    unread,
    Jul 22, 2017, 1:16:59 PM7/22/17
    to Andrew Bonventre, Brad Fitzpatrick, Kevin Burke, golang-co...@googlegroups.com

    Brad Fitzpatrick posted comments on this change.

    View Change

    Patch set 6:Code-Review +2

    (4 comments)

    • File devapp/release.go:

      • Patch Set #6, Line 28: func titleDirs(title string) []string {

        docs on this? the line 35 case surprised me, that it returns an 1-lengthed slice with "" as the only element.

      • Patch Set #6, Line 106: type milestones []milestone

        milestonesByGoVersion

           => sort.Sort(milestonesByGoVersion(milestones))

        (the foobyBar convention makes the sort site more readable)

      • Patch Set #6, Line 291: sm := milestoneRE.FindStringSubmatch(m.Title)

        this is fine because there aren't many milestones, but keep in mind that Go's regexps are kinda slow. don't use them when looping over lots of things. I used to use them in maintner for parsing some git/gerrit stuff and when I removed them in favor of explicit parsing the speedup was amazing.

    • File devapp/server.go:

    To view, visit change 50652. To unsubscribe, visit settings.

    Gerrit-Project: build
    Gerrit-Branch: master
    Gerrit-MessageType: comment
    Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
    Gerrit-Change-Number: 50652
    Gerrit-PatchSet: 6
    Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Reviewer: Kevin Burke <k...@inburke.com>
    Gerrit-Comment-Date: Sat, 22 Jul 2017 17:16:56 +0000
    Gerrit-HasComments: Yes
    Gerrit-HasLabels: Yes

    Andrew Bonventre (Gerrit)

    unread,
    Jul 22, 2017, 5:34:29 PM7/22/17
    to Brad Fitzpatrick, Kevin Burke, golang-co...@googlegroups.com

    Andrew Bonventre posted comments on this change.

    View Change

    Patch set 6:

    (4 comments)

      • Patch Set #6, Line 28: func titleDirs(title string) []string {

        docs on this? the line 35 case surprised me, that it returns an 1-lengthed

      • Done. Changed so that it returns nil in that case and handled that case at the call sites.

      • Done

      • Patch Set #6, Line 291: sm := milestoneRE.FindStringSubmatch(m.Title)

        this is fine because there aren't many milestones, but keep in mind that Go

      • Done

    To view, visit change 50652. To unsubscribe, visit settings.

    Gerrit-Project: build
    Gerrit-Branch: master
    Gerrit-MessageType: comment
    Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
    Gerrit-Change-Number: 50652
    Gerrit-PatchSet: 6
    Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Reviewer: Kevin Burke <k...@inburke.com>
    Gerrit-Comment-Date: Sat, 22 Jul 2017 21:34:26 +0000
    Gerrit-HasComments: Yes
    Gerrit-HasLabels: No

    Andrew Bonventre (Gerrit)

    unread,
    Jul 22, 2017, 5:35:19 PM7/22/17
    to golang-...@googlegroups.com, Brad Fitzpatrick, Kevin Burke, golang-co...@googlegroups.com

    Andrew Bonventre merged this change.

    View Change

    Approvals: Brad Fitzpatrick: Looks good to me, approved
    devapp: remove godash code in favor of maintner

    Adds two new functions to maintner:
    + (*GerritCL).Subject() returns the first line of the latest
    commit message.
    + (*GitHubRepo).ForeachMilestone calls a passed function for each
    milestone in the repo.

    Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
    Reviewed-on: https://go-review.googlesource.com/50652
    Reviewed-by: Brad Fitzpatrick <brad...@golang.org>

    ---
    M devapp/Dockerfile
    D devapp/cache.go
    M devapp/devapp.go
    D devapp/github.go
    A devapp/gophercon.go
    A devapp/gophercon_test.go
    A devapp/release.go
    A devapp/release_test.go
    M devapp/server.go
    M devapp/server_test.go
    A devapp/templates/release.tmpl
    M maintner/gerrit.go
    M maintner/gerrit_test.go
    M maintner/github.go
    14 files changed, 730 insertions(+), 483 deletions(-)

    diff --git a/devapp/Dockerfile b/devapp/Dockerfile
    index e52e65e..3488031 100644
    --- a/devapp/Dockerfile
    +++ b/devapp/Dockerfile
    @@ -6,4 +6,5 @@
    COPY ca-certificates.crt /etc/ssl/certs/
    COPY devapp /
    COPY static /static
    +COPY templates /templates
    ENTRYPOINT ["/devapp"]
    diff --git a/devapp/cache.go b/devapp/cache.go
    deleted file mode 100644
    index 6768af4..0000000
    --- a/devapp/cache.go
    +++ /dev/null
    @@ -1,80 +0,0 @@
    -// Copyright 2016 The Go Authors. All rights reserved.

    -// Use of this source code is governed by a BSD-style
    -// license that can be found in the LICENSE file.
    -
    -package main
    -
    -import (
    -	"bytes"
    - "compress/gzip"
    - "encoding/gob"
    - "fmt"
    - "log"
    - "sync"
    -
    - "golang.org/x/build/godash"
    -)
    -
    -// A Cache contains serialized data for dashboards.
    -type Cache struct {
    - // Value contains a gzipped gob'd serialization of the object
    - // to be cached.
    - Value []byte
    -}
    -
    -var (
    - dstore = map[string]*Cache{}
    - dstoreMu sync.Mutex
    -)
    -
    -func parseData(cache *Cache) (*godash.Data, error) {
    - data := &godash.Data{Reviewers: &godash.Reviewers{}}
    - return data, unpackCache(cache, &data)
    -}
    -
    -func unpackCache(cache *Cache, data interface{}) error {
    - if len(cache.Value) > 0 {
    - gzr, err := gzip.NewReader(bytes.NewReader(cache.Value))

    - if err != nil {
    - return err
    - }
    -		defer gzr.Close()
    - if err := gob.NewDecoder(gzr).Decode(data); err != nil {

    - return err
    - }
    - }
    -	return nil
    -}
    -
    -func writeCache(name string, data interface{}) error {
    - var cache Cache
    - var cacheout bytes.Buffer
    - cachegz := gzip.NewWriter(&cacheout)
    - e := gob.NewEncoder(cachegz)
    - if err := e.Encode(data); err != nil {
    - return err
    - }
    - if err := cachegz.Close(); err != nil {
    - return err
    - }
    - cache.Value = cacheout.Bytes()
    - log.Printf("Cache %q update finished; writing %d bytes", name, cacheout.Len())
    - return putCache(name, &cache)
    -}
    -
    -func putCache(name string, c *Cache) error {
    - dstoreMu.Lock()
    - defer dstoreMu.Unlock()
    - dstore[name] = c

    - return nil
    -}
    -
    -func getCache(name string) (*Cache, error) {
    - dstoreMu.Lock()
    - defer dstoreMu.Unlock()
    - cache, ok := dstore[name]
    - if ok {
    - return cache, nil
    - }
    - return &Cache{}, fmt.Errorf("cache key %s not found", name)
    -}
    diff --git a/devapp/gophercon.go b/devapp/gophercon.go
    new file mode 100644
    index 0000000..fc2614a
    --- /dev/null
    +++ b/devapp/gophercon.go
    @@ -0,0 +1,187 @@
    +// Copyright 2017 The Go Authors. All rights reserved.
    +// Use of this source code is governed by a BSD-style
    +// license that can be found in the LICENSE file.
    +
    +// Logic for the /gophercon endpoint which shows a semi-realtime dashboard of
    +// contribution activity. Users add their Gerrit user IDs to a hard-coded GitHub
    +// issue to provide a mapping of Gerrit ID to GitHub user, which allows any
    +// activity from the user across GitHub and Gerrit to be associated with a
    +// single GitHub user object. Points are awarded depending on the type of
    +// activity performed and an aggregated total for all participants is displayed.
    +
    +package main
    +
    +import (
    + "encoding/json"
    + "fmt"
    + "log"
    + "net/http"
    + "sort"
    + "strconv"
    + "time"
    +
    + "golang.org/x/build/maintner"
    +)
    +
    +const issueNumGerritUserMapping = 21017 // Special sign-up issue.
    +
    +// intFromStr returns the first integer within s, allowing for non-numeric
    +// characters to be present.
    +func intFromStr(s string) (int, bool) {
    + var (
    + foundNum bool
    + r int
    + )
    + for i := 0; i < len(s); i++ {
    + if s[i] >= '0' && s[i] <= '9' {
    + foundNum = true
    + r = r*10 + int(s[i]-'0')
    + } else if foundNum {
    + return r, true
    + }
    + }
    + if foundNum {
    + return r, true
    + }
    + return 0, false
    +}
    +
    +// Keep these in sync with the frontend JS.
    +const (
    + activityTypeRegister = "REGISTER"
    + activityTypeCreateChange = "CREATE_CHANGE"
    + activityTypeAmendChange = "AMEND_CHANGE"
    + activityTypeMergeChange = "MERGE_CHANGE"
    +)
    +
    +var pointsPerActivity = map[string]int{
    + activityTypeRegister: 1,
    + activityTypeCreateChange: 2,
    + activityTypeAmendChange: 2,
    + activityTypeMergeChange: 3,
    +}
    +
    +// An activity represents something a contributor has done. e.g. register on
    +// the GitHub issue, create a change, amend a change, etc.
    +type activity struct {
    + Type string `json:"type"`
    + Created time.Time `json:"created"`
    + User string `json:"gitHubUser"`
    + Points int `json:"points"`
    +}
    +
    +func (s *server) updateActivities() {
    + s.cMu.Lock()
    + defer s.cMu.Unlock()
    + repo := s.corpus.GitHub().Repo("golang", "go")
    + if repo == nil {
    + log.Println(`s.corpus.GitHub().Repo("golang", "go") = nil`)
    + return
    + }
    + issue := repo.Issue(issueNumGerritUserMapping)
    + if issue == nil {
    + log.Printf("repo.Issue(%d) = nil", issueNumGerritUserMapping)
    + return
    + }
    + latest := issue.Created
    + if len(s.activities) > 0 {
    + latest = s.activities[len(s.activities)-1].Created
    + }
    +
    + var newActivities []activity
    + issue.ForeachComment(func(c *maintner.GitHubComment) error {
    + if !c.Created.After(latest) {
    + return nil
    + }
    + id, ok := intFromStr(c.Body)
    + if !ok {
    + return fmt.Errorf("intFromStr(%q) = %v", c.Body, ok)
    + }
    + s.userMapping[id] = c.User
    +
    + newActivities = append(newActivities, activity{
    + Type: activityTypeRegister,
    + Created: c.Created,
    + User: c.User.Login,
    + Points: pointsPerActivity[activityTypeRegister],
    + })
    + s.totalPoints += pointsPerActivity[activityTypeRegister]
    + return nil
    + })
    +
    + s.corpus.Gerrit().ForeachProjectUnsorted(func(p *maintner.GerritProject) error {
    + p.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
    + if !cl.Commit.CommitTime.After(latest) {
    + return nil
    + }
    + user := s.userMapping[cl.OwnerID()]
    + if user == nil {
    + return nil
    + }
    +
    + newActivities = append(newActivities, activity{
    + Type: activityTypeCreateChange,
    + Created: cl.Created,
    + User: user.Login,
    + Points: pointsPerActivity[activityTypeCreateChange],
    + })
    + s.totalPoints += pointsPerActivity[activityTypeCreateChange]
    + if cl.Version > 1 {
    + newActivities = append(newActivities, activity{
    + Type: activityTypeAmendChange,
    + Created: cl.Commit.CommitTime,
    + User: user.Login,
    + Points: pointsPerActivity[activityTypeAmendChange],
    + })
    + s.totalPoints += pointsPerActivity[activityTypeAmendChange]
    + }
    + if cl.Status == "merged" {
    + newActivities = append(newActivities, activity{
    + Type: activityTypeMergeChange,
    + Created: cl.Commit.CommitTime,
    + User: user.Login,
    + Points: pointsPerActivity[activityTypeMergeChange],
    + })
    + s.totalPoints += pointsPerActivity[activityTypeMergeChange]
    + }
    + return nil
    + })
    + return nil
    + })
    +
    + sort.Sort(byCreated(newActivities))
    + s.activities = append(s.activities, newActivities...)
    +}
    +
    +type byCreated []activity
    +
    +func (a byCreated) Len() int { return len(a) }
    +func (a byCreated) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
    +func (a byCreated) Less(i, j int) bool { return a[i].Created.Before(a[j].Created) }
    +
    +func (s *server) handleActivities(w http.ResponseWriter, r *http.Request) {
    + i, _ := strconv.Atoi(r.FormValue("since"))
    + since := time.Unix(int64(i)/1000, 0)
    +
    + recentActivity := []activity{}
    + for _, a := range s.activities {
    + if a.Created.After(since) {
    + recentActivity = append(recentActivity, a)
    + }
    + }
    +
    + s.cMu.RLock()
    + defer s.cMu.RUnlock()
    + w.Header().Set("Content-Type", "application/json")
    + result := struct {
    + Activities []activity `json:"activities"`
    + TotalPoints int `json:"totalPoints"`
    + }{
    + Activities: recentActivity,
    + TotalPoints: s.totalPoints,
    + }
    + if err := json.NewEncoder(w).Encode(result); err != nil {
    + log.Printf("Encode(%+v) = %v", result, err)
    + return
    + }
    +}
    diff --git a/devapp/gophercon_test.go b/devapp/gophercon_test.go
    new file mode 100644
    index 0000000..331abac
    --- /dev/null
    +++ b/devapp/gophercon_test.go
    @@ -0,0 +1,32 @@
    +// Copyright 2017 The Go Authors. All rights reserved.
    +// Use of this source code is governed by a BSD-style
    +// license that can be found in the LICENSE file.
    +package main
    +
    +import "testing"
    +
    +func TestIntFromStr(t *testing.T) {
    + testcases := []struct {
    + s string
    + i int
    + }{
    + {"123", 123},
    + {"User ID: 98403", 98403},
    + {"1234 User 5431 ID", 1234},
    + {"Stardate 153.2415", 153},
    + }
    + for _, tc := range testcases {
    + r, ok := intFromStr(tc.s)
    + if !ok {
    + t.Errorf("intFromStr(%q) = %v", tc.s, ok)
    + }
    + if r != tc.i {
    + t.Errorf("intFromStr(%q) = %d; want %d", tc.s, r, tc.i)
    + }
    + }
    + noInt := "hello there"
    + _, ok := intFromStr(noInt)
    + if ok {
    + t.Errorf("intFromStr(%q) = %v; want false", noInt, ok)
    + }
    +}
    diff --git a/devapp/release.go b/devapp/release.go
    new file mode 100644
    index 0000000..0084524
    --- /dev/null
    +++ b/devapp/release.go
    @@ -0,0 +1,324 @@
    +// Copyright 2017 The Go Authors. All rights reserved.
    +// Use of this source code is governed by a BSD-style
    +// license that can be found in the LICENSE file.
    +
    +package main
    +
    +import (
    + "bytes"
    + "html/template"
    + "log"
    + "net/http"
    + "regexp"
    + "sort"
    + "strconv"
    + "strings"
    + "time"
    +
    + "golang.org/x/build/maintner"
    +)
    +
    +const (
    + labelProposal = "Proposal"
    +
    + prefixProposal = "proposal:"
    + prefixDev = "[dev."
    +)
    +
    +func titleDirs(title string) []string {
    + if i := strings.Index(title, "\n"); i >= 0 {
    + title = title[:i]
    + }
    + title = strings.TrimSpace(title)
    + i := strings.Index(title, ":")
    + if i < 0 {
    + return []string{""}
    + }
    + var (
    + b bytes.Buffer
    + r []string
    + )
    + for j := 0; j < i; j++ {
    + switch title[j] {
    + case ' ':
    + continue
    + case ',':
    + r = append(r, b.String())
    + b.Reset()
    + continue
    + default:
    + b.WriteByte(title[j])
    + }
    + }
    + if b.Len() > 0 {
    + r = append(r, b.String())
    + }
    + return r
    +}
    +
    +type releaseData struct {
    + LastUpdated string
    + Sections []section
    +
    + // dirty is set if this data needs to be updated due to a corpus change.
    + dirty bool
    +}
    +
    +type section struct {
    + Title string
    + Count int
    + Groups []group
    +}
    +
    +type group struct {
    + Dir string
    + Items []item
    +}
    +
    +type item struct {
    + Issue *maintner.GitHubIssue
    + CLs []*maintner.GerritCL
    +}
    +
    +type itemsBySummary []item
    +
    +func (x itemsBySummary) Len() int { return len(x) }
    +func (x itemsBySummary) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
    +func (x itemsBySummary) Less(i, j int) bool { return itemSummary(x[i]) < itemSummary(x[j]) }
    +
    +func itemSummary(it item) string {
    + if it.Issue != nil {
    + return it.Issue.Title
    + }
    + for _, cl := range it.CLs {
    + return cl.Subject()
    + }
    + return ""
    +}
    +
    +var milestoneRE = regexp.MustCompile(`^Go1\.(\d+)(|\.(\d+))(|[A-Z].*)$`)
    +
    +type milestone struct {
    + title string
    + major, minor int
    +}
    +
    +type milestones []milestone
    +
    +func (x milestones) Len() int { return len(x) }
    +func (x milestones) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
    +func (x milestones) Less(i, j int) bool {
    + a, b := x[i], x[j]
    + if a.major != b.major {
    + return a.major < b.major
    + }
    + if a.minor != b.minor {
    + return a.minor < b.minor
    + }
    + return a.title < b.title
    +}
    +
    +func (s *server) updateReleaseData() {
    + log.Println("Updating release data ...")
    + s.cMu.Lock()
    + defer s.cMu.Unlock()
    +
    + dirToCLs := map[string][]*maintner.GerritCL{}
    + issueToCLs := map[int32][]*maintner.GerritCL{}
    + s.corpus.Gerrit().ForeachProjectUnsorted(func(p *maintner.GerritProject) error {
    + p.ForeachOpenCL(func(cl *maintner.GerritCL) error {
    + if strings.HasPrefix(cl.Subject(), prefixDev) {
    + return nil
    + }
    + for _, r := range cl.GitHubIssueRefs {
    + issueToCLs[r.Number] = append(issueToCLs[r.Number], cl)
    + }
    + for _, d := range titleDirs(cl.Subject()) {
    + dirToCLs[d] = append(dirToCLs[d], cl)
    + }
    + return nil
    + })
    + return nil
    + })
    +
    + dirToIssues := map[string][]*maintner.GitHubIssue{}
    + s.repo.ForeachIssue(func(issue *maintner.GitHubIssue) error {
    + // Issues in active milestones.
    + if !issue.Closed && issue.Milestone != nil && !issue.Milestone.Closed {
    + for _, d := range titleDirs(issue.Title) {
    + dirToIssues[d] = append(dirToIssues[d], issue)
    + }
    + }
    + return nil
    + })
    +
    + s.data.Sections = nil
    + s.appendOpenIssues(dirToIssues, issueToCLs)
    + s.appendPendingCLs(dirToCLs)
    + s.appendPendingProposals(issueToCLs)
    + s.appendClosedIssues()
    + s.data.LastUpdated = time.Now().UTC().Format(time.UnixDate)
    + s.data.dirty = false
    +}
    +
    +// requires s.cMu be locked.
    +func (s *server) appendOpenIssues(dirToIssues map[string][]*maintner.GitHubIssue, issueToCLs map[int32][]*maintner.GerritCL) {
    + var issueDirs []string
    + for d := range dirToIssues {
    + issueDirs = append(issueDirs, d)
    + }
    + sort.Strings(issueDirs)
    + ms := s.allMilestones()
    + for _, m := range ms {
    + var (
    + issueGroups []group
    + issueCount int
    + )
    + for _, d := range issueDirs {
    + issues, ok := dirToIssues[d]
    + if !ok {
    + continue
    + }
    + var items []item
    + for _, i := range issues {
    + if i.Milestone.Title != m.title {
    + continue
    + }
    +
    + items = append(items, item{
    + Issue: i,
    + CLs: issueToCLs[i.Number],
    + })
    + issueCount++
    + }
    + if len(items) == 0 {
    + continue
    + }
    + sort.Sort(itemsBySummary(items))
    + issueGroups = append(issueGroups, group{
    + Dir: d,
    + Items: items,
    + })
    + }
    + s.data.Sections = append(s.data.Sections, section{
    + Title: m.title,
    + Count: issueCount,
    + Groups: issueGroups,
    + })
    + }
    +}
    +
    +// requires s.cMu be locked.
    +func (s *server) appendPendingCLs(dirToCLs map[string][]*maintner.GerritCL) {
    + var clDirs []string
    + for d := range dirToCLs {
    + clDirs = append(clDirs, d)
    + }
    + sort.Strings(clDirs)
    + var (
    + clGroups []group
    + clCount int
    + )
    + for _, d := range clDirs {
    + if cls, ok := dirToCLs[d]; ok {
    + clCount += len(cls)
    + g := group{Dir: d}
    + g.Items = append(g.Items, item{CLs: cls})
    + sort.Sort(itemsBySummary(g.Items))
    + clGroups = append(clGroups, g)
    + }
    + }
    + s.data.Sections = append(s.data.Sections, section{
    + Title: "Pending CLs",
    + Count: clCount,
    + Groups: clGroups,
    + })
    +}
    +
    +// requires s.cMu be locked.
    +func (s *server) appendPendingProposals(issueToCLs map[int32][]*maintner.GerritCL) {
    + var proposals group
    + s.repo.ForeachIssue(func(issue *maintner.GitHubIssue) error {
    + if issue.Closed {
    + return nil
    + }
    + if issue.HasLabel(labelProposal) || strings.HasPrefix(issue.Title, prefixProposal) {
    + proposals.Items = append(proposals.Items, item{
    + Issue: issue,
    + CLs: issueToCLs[issue.Number],
    + })
    + }
    + return nil
    + })
    + sort.Sort(itemsBySummary(proposals.Items))
    + s.data.Sections = append(s.data.Sections, section{
    + Title: "Pending Proposals",
    + Count: len(proposals.Items),
    + Groups: []group{proposals},
    + })
    +}
    +
    +// requires s.cMu be locked.
    +func (s *server) appendClosedIssues() {
    + var (
    + closed group
    + lastWeek = time.Now().Add(-(7*24 + 12) * time.Hour)
    + )
    + s.repo.ForeachIssue(func(issue *maintner.GitHubIssue) error {
    + if !issue.Closed {
    + return nil
    + }
    + if issue.Updated.After(lastWeek) {
    + closed.Items = append(closed.Items, item{Issue: issue})
    + }
    + return nil
    + })
    + sort.Sort(itemsBySummary(closed.Items))
    + s.data.Sections = append(s.data.Sections, section{
    + Title: "Closed Last Week",
    + Count: len(closed.Items),
    + Groups: []group{closed},
    + })
    +}
    +
    +// requires s.cMu be read locked.
    +func (s *server) allMilestones() []milestone {
    + var ms []milestone
    + s.repo.ForeachMilestone(func(m *maintner.GitHubMilestone) error {
    + if m.Closed {
    + return nil
    + }
    + sm := milestoneRE.FindStringSubmatch(m.Title)
    + if sm == nil {
    + return nil
    + }
    + major, _ := strconv.Atoi(sm[1])
    + minor, _ := strconv.Atoi(sm[3])
    + ms = append(ms, milestone{
    + title: m.Title,
    + major: major,
    + minor: minor,
    + })
    + return nil
    + })
    + sort.Sort(milestones(ms))
    + return ms
    +}
    +
    +// handleRelease serves dev.golang.org/release.
    +func (s *server) handleRelease(t *template.Template, w http.ResponseWriter, r *http.Request) {

    + w.Header().Set("Content-Type", "text/html; charset=utf-8")
    +	s.cMu.RLock()
    + dirty := s.data.dirty
    + s.cMu.RUnlock()
    + if dirty {
    + s.updateReleaseData()
    + }
    +
    + s.cMu.RLock()
    + defer s.cMu.RUnlock()
    + if err := t.Execute(w, s.data); err != nil {

    + log.Printf("t.Execute(w, nil) = %v", err)
    +		return
    + }
    +}
    diff --git a/devapp/release_test.go b/devapp/release_test.go
    new file mode 100644
    index 0000000..6fe1fdb
    --- /dev/null
    +++ b/devapp/release_test.go
    @@ -0,0 +1,30 @@
    +// Copyright 2017 The Go Authors. All rights reserved.
    +// Use of this source code is governed by a BSD-style
    +// license that can be found in the LICENSE file.
    +
    +package main
    +
    +import "testing"
    +
    +func TestTitleDir(t *testing.T) {
    + testcases := []struct {
    + title string
    + dirs []string
    + }{
    + {" cmd/compile , cmd/go: do awesome things", []string{"cmd/compile", "cmd/go"}},
    + {"cmd/compile: cleanup MOVaddr code generation", []string{"cmd/compile"}},
    + {`cmd/asm, cmd/internal/obj/s390x, math: add "test under mask" instructions`,
    + []string{"cmd/asm", "cmd/internal/obj/s390x", "math"}},
    + }
    + for _, tc := range testcases {
    + r := titleDirs(tc.title)
    + if len(r) != len(tc.dirs) {
    + t.Fatalf("titleDirs(%q) = %v (%d); want %d length", tc.title, r, len(r), len(tc.dirs))
    + }
    + for i := range tc.dirs {
    + if r[i] != tc.dirs[i] {
    + t.Errorf("titleDirs[%d](%v) != tc.dirs[%d](%q)", i, r[i], i, tc.dirs[i])
    + }
    + }
    + }
    +}
    diff --git a/devapp/server.go b/devapp/server.go
    index 29d911d..0f11c37 100644
    --- a/devapp/server.go
    +++ b/devapp/server.go
    @@ -6,13 +6,12 @@

    import (
    "context"
    - "encoding/json"

    "fmt"
    + "html/template"
    "log"
    "math/rand"
    "net/http"
     	"path"
    - "sort"
    "strconv"
    "sync"
    "time"
    @@ -24,26 +23,32 @@

    // A server is an http.Handler that serves content within staticDir at root and
    // the dynamically-generated dashboards at their respective endpoints.
    type server struct {
    - mux *http.ServeMux
    - staticDir string
    + mux *http.ServeMux
    + staticDir string
    + templateDir string

    cMu sync.RWMutex // Used to protect the fields below.
    corpus *maintner.Corpus
    +	repo             *maintner.GitHubRepo
    helpWantedIssues []int32
    - userMapping map[int]*maintner.GitHubUser // Gerrit Owner ID => GitHub user
    - activities []activity // All contribution activities
    - totalPoints int
    + data releaseData
    +
    + // GopherCon-specific fields. Must still hold cMu when reading/writing these.
    + userMapping map[int]*maintner.GitHubUser // Gerrit Owner ID => GitHub user
    + activities []activity // All contribution activities
    + totalPoints int

    }

    -func newServer(mux *http.ServeMux, staticDir string) *server {
    +func newServer(mux *http.ServeMux, staticDir, templateDir string) *server {
    s := &server{
    mux: mux,
    staticDir: staticDir,
    + templateDir: templateDir,
    userMapping: map[int]*maintner.GitHubUser{},
    }
    s.mux.Handle("/", http.FileServer(http.Dir(s.staticDir)))
    s.mux.HandleFunc("/favicon.ico", s.handleFavicon)
    - s.mux.HandleFunc("/release", handleRelease)
    +	s.mux.HandleFunc("/release", s.withTemplate("/release.tmpl", s.handleRelease))

    for _, p := range []string{"/imfeelinghelpful", "/imfeelinglucky"} {
    s.mux.HandleFunc(p, s.handleRandomHelpWantedIssue)
    }
    @@ -51,6 +56,11 @@
    return s
    }

    +func (s *server) withTemplate(tmpl string, fn func(*template.Template, http.ResponseWriter, *http.Request)) http.HandlerFunc {
    + t := template.Must(template.ParseFiles(path.Join(s.templateDir, tmpl)))
    + return func(w http.ResponseWriter, r *http.Request) { fn(t, w, r) }
    +}
    +
    // initCorpus fetches a full maintner corpus, overwriting any existing data.
    func (s *server) initCorpus(ctx context.Context) error {
    s.cMu.Lock()
    @@ -60,6 +70,10 @@
    return fmt.Errorf("godata.Get: %v", err)
    }
    s.corpus = corpus
    + s.repo = s.corpus.GitHub().Repo("golang", "go")
    + if s.repo == nil {
    + return fmt.Errorf(`s.corpus.GitHub().Repo("golang", "go") = nil`)
    + }
    return nil
    }

    @@ -72,6 +86,9 @@
    s.updateHelpWantedIssues()
    log.Println("Updating activities ...")
    s.updateActivities()
    + s.cMu.Lock()
    + s.data.dirty = true
    + s.cMu.Unlock()
    err := s.corpus.UpdateWithLocker(ctx, &s.cMu)
    if err != nil {
    if err == maintner.ErrSplit {
    @@ -94,26 +111,21 @@
    }

    const (
    - labelIDHelpWanted = 150880243
    - issuesURLBase = "https://github.com/golang/go/issues/"
    - issueNumGerritUserMapping = 21017 // Special sign-up issue.
    + issuesURLBase = "https://golang.org/issue/"
    +
    + labelHelpWanted = "HelpWanted"
    )

    func (s *server) updateHelpWantedIssues() {
    s.cMu.Lock()
    defer s.cMu.Unlock()
    - repo := s.corpus.GitHub().Repo("golang", "go")
    - if repo == nil {
    - log.Println(`s.corpus.GitHub().Repo("golang", "go") = nil`)
    - return
    - }

    - ids := []int32{}
    - repo.ForeachIssue(func(i *maintner.GitHubIssue) error {
    + var ids []int32
    + s.repo.ForeachIssue(func(i *maintner.GitHubIssue) error {
    if i.Closed {
    return nil
    }
    - if _, ok := i.Labels[labelIDHelpWanted]; ok {
    + if i.HasLabel(labelHelpWanted) {
    ids = append(ids, i.Number)
    }
    return nil
    @@ -121,167 +133,6 @@
    s.helpWantedIssues = ids
    }

    -// intFromStr returns the first integer within s, allowing for non-numeric
    -// characters to be present.
    -func intFromStr(s string) (int, bool) {
    - var (
    - foundNum bool
    - r int
    - )
    - for i := 0; i < len(s); i++ {
    - if s[i] >= '0' && s[i] <= '9' {
    - foundNum = true
    - r = r*10 + int(s[i]-'0')
    - } else if foundNum {
    - return r, true
    - }
    - }
    - if foundNum {
    - return r, true
    - }
    - return 0, false
    -}
    -
    -// Keep these in sync with the frontend JS.
    -const (
    - activityTypeRegister = "REGISTER"
    - activityTypeCreateChange = "CREATE_CHANGE"
    - activityTypeAmendChange = "AMEND_CHANGE"
    - activityTypeMergeChange = "MERGE_CHANGE"
    -)
    -
    -var pointsPerActivity = map[string]int{
    - activityTypeRegister: 1,
    - activityTypeCreateChange: 2,
    - activityTypeAmendChange: 2,
    - activityTypeMergeChange: 3,
    -}
    -
    -// An activity represents something a contributor has done. e.g. register on
    -// the GitHub issue, create a change, amend a change, etc.
    -type activity struct {
    - Type string `json:"type"`
    - Created time.Time `json:"created"`
    - User string `json:"gitHubUser"`
    - Points int `json:"points"`
    -}
    -
    -func (s *server) updateActivities() {
    - s.cMu.Lock()
    - defer s.cMu.Unlock()
    - repo := s.corpus.GitHub().Repo("golang", "go")
    - if repo == nil {
    - log.Println(`s.corpus.GitHub().Repo("golang", "go") = nil`)
    - return
    - }
    - issue := repo.Issue(issueNumGerritUserMapping)
    - if issue == nil {
    - log.Printf("repo.Issue(%d) = nil", issueNumGerritUserMapping)
    - return
    - }
    - latest := issue.Created
    - if len(s.activities) > 0 {
    - latest = s.activities[len(s.activities)-1].Created
    - }
    -
    - var newActivities []activity
    - issue.ForeachComment(func(c *maintner.GitHubComment) error {
    - if !c.Created.After(latest) {
    - return nil
    - }
    - id, ok := intFromStr(c.Body)
    - if !ok {
    - return fmt.Errorf("intFromStr(%q) = %v", c.Body, ok)
    - }
    - s.userMapping[id] = c.User
    -
    - newActivities = append(newActivities, activity{
    - Type: activityTypeRegister,
    - Created: c.Created,
    - User: c.User.Login,
    - Points: pointsPerActivity[activityTypeRegister],
    - })
    - s.totalPoints += pointsPerActivity[activityTypeRegister]
    - return nil
    - })
    -
    - s.corpus.Gerrit().ForeachProjectUnsorted(func(p *maintner.GerritProject) error {
    - p.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
    - if !cl.Commit.CommitTime.After(latest) {
    - return nil
    - }
    - user := s.userMapping[cl.OwnerID()]
    - if user == nil {

    - return nil
    - }
    -
    -			newActivities = append(newActivities, activity{
    - Type: activityTypeCreateChange,
    - Created: cl.Created,
    - User: user.Login,
    - Points: pointsPerActivity[activityTypeCreateChange],
    - })
    - s.totalPoints += pointsPerActivity[activityTypeCreateChange]
    - if cl.Version > 1 {
    - newActivities = append(newActivities, activity{
    - Type: activityTypeAmendChange,
    - Created: cl.Commit.CommitTime,
    - User: user.Login,
    - Points: pointsPerActivity[activityTypeAmendChange],
    - })
    - s.totalPoints += pointsPerActivity[activityTypeAmendChange]
    - }
    - if cl.Status == "merged" {
    - newActivities = append(newActivities, activity{
    - Type: activityTypeMergeChange,
    - Created: cl.Commit.CommitTime,
    - User: user.Login,
    - Points: pointsPerActivity[activityTypeMergeChange],
    - })
    - s.totalPoints += pointsPerActivity[activityTypeMergeChange]
    - }
    - return nil
    - })
    - return nil
    - })
    -
    - sort.Sort(byCreated(newActivities))
    - s.activities = append(s.activities, newActivities...)
    -}
    -
    -type byCreated []activity
    -
    -func (a byCreated) Len() int { return len(a) }
    -func (a byCreated) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
    -func (a byCreated) Less(i, j int) bool { return a[i].Created.Before(a[j].Created) }
    -
    -func (s *server) handleActivities(w http.ResponseWriter, r *http.Request) {
    - i, _ := strconv.Atoi(r.FormValue("since"))
    - since := time.Unix(int64(i)/1000, 0)
    -
    - recentActivity := []activity{}
    - for _, a := range s.activities {
    - if a.Created.After(since) {
    - recentActivity = append(recentActivity, a)
    - }
    - }
    -
    - s.cMu.RLock()
    - defer s.cMu.RUnlock()
    - w.Header().Set("Content-Type", "application/json")
    - result := struct {
    - Activities []activity `json:"activities"`
    - TotalPoints int `json:"totalPoints"`
    - }{
    - Activities: recentActivity,
    - TotalPoints: s.totalPoints,
    - }
    - if err := json.NewEncoder(w).Encode(result); err != nil {
    - log.Printf("Encode(%+v) = %v", result, err)
    - return
    - }
    -}
    -
    func (s *server) handleRandomHelpWantedIssue(w http.ResponseWriter, r *http.Request) {
    s.cMu.RLock()
    defer s.cMu.RUnlock()
    @@ -307,40 +158,3 @@
    }
    s.mux.ServeHTTP(w, r)
    }
    -
    -		return
    - }

    - w.Header().Set("Content-Type", "text/html; charset=utf-8")
    - w.Write(b)
    -}
    -
    -func handleRelease(w http.ResponseWriter, r *http.Request) {
    - servePage(w, r, "release")
    -}
    diff --git a/devapp/server_test.go b/devapp/server_test.go
    index 948065d..2e16bbe 100644
    --- a/devapp/server_test.go
    +++ b/devapp/server_test.go
    @@ -11,7 +11,7 @@
    "testing"
    )

    -var testServer = newServer(http.DefaultServeMux, "./static/")
    +var testServer = newServer(http.DefaultServeMux, "./static/", "./templates/")

    func TestStaticAssetsFound(t *testing.T) {
    req := httptest.NewRequest("GET", "/", nil)
    @@ -70,29 +70,3 @@
    t.Errorf("Location header = %q; want %q", g, w)
    }
    }
    -
    -func TestIntFromStr(t *testing.T) {
    - testcases := []struct {
    - s string
    - i int
    - }{
    - {"123", 123},
    - {"User ID: 98403", 98403},
    - {"1234 User 5431 ID", 1234},
    - {"Stardate 153.2415", 153},
    - }
    - for _, tc := range testcases {
    - r, ok := intFromStr(tc.s)
    - if !ok {
    - t.Errorf("intFromStr(%q) = %v", tc.s, ok)
    - }
    - if r != tc.i {
    - t.Errorf("intFromStr(%q) = %d; want %d", tc.s, r, tc.i)
    - }
    - }
    - noInt := "hello there"
    - _, ok := intFromStr(noInt)
    - if ok {
    - t.Errorf("intFromStr(%q) = %v; want false", noInt, ok)
    - }
    -}

    diff --git a/devapp/templates/release.tmpl b/devapp/templates/release.tmpl
    new file mode 100644
    index 0000000..fa9b197
    --- /dev/null
    +++ b/devapp/templates/release.tmpl
    @@ -0,0 +1,80 @@

    +<!DOCTYPE html>
    +<meta charset="utf-8">
    +<title>Go Release Dashboard</title>

    +<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1">
    +<style>
    +* {
    + box-sizing: border-box;
    + margin: 0;
    + padding: 0;
    +}
    +body {
    + font: 13px monospace;
    + padding: 1rem;
    +}
    +a:link,
    +a:visited {
    + color: #00c;
    +}
    +.CountSummary {
    + font-weight: bold;
    + list-style: none;
    + margin: .5em 0 1em;
    +}
    +.Header {
    + font-weight: bold;
    +}
    +.Section {
    + border-top: 1px solid #aaa;
    + padding-bottom: 2em;
    +}
    +.Section-title {
    + margin: .5em 0;
    +}
    +.Item {
    + display: flex;
    +}
    +.Item-num {
    + margin-left: 4ch;
    + min-width: 12ch;
    +}
    +.DirTitle {
    + margin: 1em 0 .25em;
    +}
    +</style>
    +<header class="Header">
    + <div>Release dashboard</div>
    + <div>{{.LastUpdated}}</div>
    +</header>
    +<main>
    +<ul class="CountSummary">
    +{{range .Sections}}
    + <li><a href="#{{.Title}}">{{.Count}} {{.Title}}</a></li>
    +{{end}}
    +</ul>
    +{{range .Sections}}
    + <section class="Section">
    + <h3 class="Section-title" id="{{.Title}}">{{.Title}}</h3>
    + {{range .Groups}}
    + {{if .Dir}}<h4 class="DirTitle">{{.Dir}}</h4>{{end}}
    + {{range .Items}}
    + {{$i := .Issue}}
    + {{if $i}}
    + <div class="Item">
    + <a class="Item-num" href="https://golang.org/issue/{{.Issue.Number}}" target="_blank">#{{.Issue.Number}}</a>
    + <span class="Item-title">{{.Issue.Title}}</span>
    + </div>
    + {{end}}
    + {{range .CLs}}
    + <div class="Item">
    + <span class="Item-num">
    + {{if $i}}⤷{{end}} <a href="https://golang.org/cl/{{.Number}}" target="_blank">CL {{.Number}}</a>
    + </span>
    + <span class="Item-title">{{if $i}}⤷{{end}} {{.Subject}}</span>
    + </div>
    + {{end}}
    + {{end}}
    + {{end}}
    + </section>
    +{{end}}
    +</main>

    \ No newline at end of file
    diff --git a/maintner/gerrit.go b/maintner/gerrit.go
    index 11fcf4d..2abfafd 100644
    --- a/maintner/gerrit.go
    +++ b/maintner/gerrit.go
    @@ -302,6 +302,17 @@
    return id
    }

    +// Subject returns the first line of the latest commit message.
    +func (cl *GerritCL) Subject() string {
    + if cl.Commit == nil {
    + return ""
    + }
    + if i := strings.Index(cl.Commit.Msg, "\n"); i >= 0 {
    + return cl.Commit.Msg[:i]
    + }
    + return cl.Commit.Msg
    +}
    +
    func (cl *GerritCL) firstMetaCommit() *GitCommit {
    m := cl.Meta
    for {
    diff --git a/maintner/gerrit_test.go b/maintner/gerrit_test.go
    index 3c76fd5..6eb8218 100644
    --- a/maintner/gerrit_test.go
    +++ b/maintner/gerrit_test.go
    @@ -147,3 +147,21 @@
    t.Errorf("cl.OwnerID() = %d; want %d", cl.OwnerID(), 137)
    }
    }
    +
    +func TestSubject(t *testing.T) {
    + cl := &GerritCL{}
    + if w, e := cl.Subject(), ""; w != e {
    + t.Errorf("cl.Subject() = %q; want %q", w, e)
    + }
    +
    + testcases := []struct{ msg, subject string }{
    + {"maintner: slurp up all the things", "maintner: slurp up all the things"},
    + {"cmd/go: build stuff\n\nand do other stuff, too.", "cmd/go: build stuff"},
    + }
    + for _, tc := range testcases {
    + cl = &GerritCL{Commit: &GitCommit{Msg: tc.msg}}
    + if cl.Subject() != tc.subject {
    + t.Errorf("cl.Subject() = %q; want %q", cl.Subject(), tc.subject)
    + }
    + }
    +}
    diff --git a/maintner/github.go b/maintner/github.go
    index c16c6a1..8d45ed4 100644
    --- a/maintner/github.go
    +++ b/maintner/github.go
    @@ -129,6 +129,18 @@
    return nil
    }

    +// ForeachMilestone calls fn for each milestone in the repo, in unsorted order.
    +//
    +// Iteration ends if fn returns an error, with that error.
    +func (gr *GitHubRepo) ForeachMilestone(fn func(*GitHubMilestone) error) error {
    + for _, m := range gr.milestones {
    + if err := fn(m); err != nil {
    + return err
    + }
    + }
    + return nil
    +}
    +
    // ForeachIssue calls fn for each issue in the repo.
    //
    // If fn returns an error, iteration ends and ForeachIssue returns

    To view, visit change 50652. To unsubscribe, visit settings.

    Gerrit-Project: build
    Gerrit-Branch: master
    Gerrit-MessageType: merged
    Gerrit-Change-Id: I08c50ea53b781064c8ab2ddaff2177f51ebb091d
    Gerrit-Change-Number: 50652
    Gerrit-PatchSet: 7
    Gerrit-Owner: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Andrew Bonventre <andy...@golang.org>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Reviewer: Kevin Burke <k...@inburke.com>
    Reply all
    Reply to author
    Forward
    0 new messages