+++ /dev/null
-// Copyright 2012 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 dashboard
-
-// This file handles operations on the CL entity kind.
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "html/template"
- "io"
- "io/ioutil"
- "net/http"
- "net/url"
- "regexp"
- "sort"
- "strings"
- "time"
-
- "appengine"
- "appengine/datastore"
- "appengine/taskqueue"
- "appengine/urlfetch"
- "appengine/user"
-)
-
-func init() {
- http.HandleFunc("/assign", handleAssign)
- http.HandleFunc("/update-cl", handleUpdateCL)
-}
-
-const codereviewBase = "http://codereview.appspot.com"
-const gobotBase = "http://research.swtch.com/gobot_codereview"
-
-var clRegexp = regexp.MustCompile(`\d+`)
-
-// CL represents a code review.
-type CL struct {
- Number string // e.g. "5903061"
- Closed bool
- Owner string // email address
-
- Created, Modified time.Time
-
- Description []byte `datastore:",noindex"`
- FirstLine string `datastore:",noindex"`
- LGTMs []string
- NotLGTMs []string
- LastUpdateBy string // author of most recent review message
- LastUpdate string `datastore:",noindex"` // first line of most recent review message
-
- // Mail information.
- Subject string `datastore:",noindex"`
- Recipients []string `datastore:",noindex"`
- LastMessageID string `datastore:",noindex"`
-
- // These are person IDs (e.g. "rsc"); they may be empty
- Author string
- Reviewer string
-}
-
-// Reviewed reports whether the reviewer has replied to the CL.
-// The heuristic is that the CL has been replied to if it is LGTMed
-// or if the last CL message was from the reviewer.
-func (cl *CL) Reviewed() bool {
- if cl.LastUpdateBy == cl.Reviewer {
- return true
- }
- if person := emailToPerson[cl.LastUpdateBy]; person != "" && person == cl.Reviewer {
- return true
- }
- for _, who := range cl.LGTMs {
- if who == cl.Reviewer {
- return true
- }
- }
- return false
-}
-
-// DisplayOwner returns the CL's owner, either as their email address
-// or the person ID if it's a reviewer. It is for display only.
-func (cl *CL) DisplayOwner() string {
- if p, ok := emailToPerson[cl.Owner]; ok {
- return p
- }
- return cl.Owner
-}
-
-func (cl *CL) FirstLineHTML() template.HTML {
- s := template.HTMLEscapeString(cl.FirstLine)
- // Embolden the package name.
- if i := strings.Index(s, ":"); i >= 0 {
- s = "<b>" + s[:i] + "</b>" + s[i:]
- }
- return template.HTML(s)
-}
-
-func formatEmails(e []string) template.HTML {
- x := make([]string, len(e))
- for i, s := range e {
- s = template.HTMLEscapeString(s)
- if !strings.Contains(s, "@") {
- s = "<b>" + s + "</b>"
- }
- s = `<span class="email">` + s + "</span>"
- x[i] = s
- }
- return template.HTML(strings.Join(x, ", "))
-}
-
-func (cl *CL) LGTMHTML() template.HTML {
- return formatEmails(cl.LGTMs)
-}
-
-func (cl *CL) NotLGTMHTML() template.HTML {
- return formatEmails(cl.NotLGTMs)
-}
-
-func (cl *CL) ModifiedAgo() string {
- // Just the first non-zero unit.
- units := [...]struct {
- suffix string
- unit time.Duration
- }{
- {"d", 24 * time.Hour},
- {"h", time.Hour},
- {"m", time.Minute},
- {"s", time.Second},
- }
- d := time.Now().Sub(cl.Modified)
- for _, u := range units {
- if d > u.unit {
- return fmt.Sprintf("%d%s", d/u.unit, u.suffix)
- }
- }
- return "just now"
-}
-
-func handleAssign(w http.ResponseWriter, r *http.Request) {
- c := appengine.NewContext(r)
-
- if r.Method != "POST" {
- http.Error(w, "Bad method "+r.Method, 400)
- return
- }
-
- u := user.Current(c)
- person, ok := emailToPerson[u.Email]
- if !ok {
- http.Error(w, "Not allowed", http.StatusUnauthorized)
- return
- }
-
- n, rev := r.FormValue("cl"), r.FormValue("r")
- if !clRegexp.MatchString(n) {
- c.Errorf("Bad CL %q", n)
- http.Error(w, "Bad CL", 400)
- return
- }
- if _, ok := preferredEmail[rev]; !ok && rev != "" {
- c.Errorf("Unknown reviewer %q", rev)
- http.Error(w, "Unknown reviewer", 400)
- return
- }
-
- key := datastore.NewKey(c, "CL", n, 0, nil)
-
- if rev != "" {
- // Make sure the reviewer is listed in Rietveld as a reviewer.
- url := codereviewBase + "/" + n + "/fields"
- resp, err := urlfetch.Client(c).Get(url + "?field=reviewers")
- if err != nil {
- c.Errorf("Retrieving CL reviewer list failed: %v", err)
- http.Error(w, err.Error(), 500)
- return
- }
- defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- c.Errorf("Failed reading body: %v", err)
- http.Error(w, err.Error(), 500)
- return
- }
- if resp.StatusCode != 200 {
- c.Errorf("Retrieving CL reviewer list failed: got HTTP response %d\nBody: %s", resp.StatusCode, body)
- http.Error(w, "Failed contacting Rietveld", 500)
- return
- }
-
- var apiResp struct {
- Reviewers []string `json:"reviewers"`
- }
- if err := json.Unmarshal(body, &apiResp); err != nil {
- // probably can't be retried
- msg := fmt.Sprintf("Malformed JSON from %v: %v", url, err)
- c.Errorf("%s", msg)
- http.Error(w, msg, 500)
- return
- }
- found := false
- for _, r := range apiResp.Reviewers {
- if emailToPerson[r] == rev {
- found = true
- break
- }
- }
- if !found {
- c.Infof("Adding %v as a reviewer of CL %v", rev, n)
-
- url := fmt.Sprintf("%s?cl=%s&r=%s&obo=%s", gobotBase, n, rev, person)
- resp, err := urlfetch.Client(c).Get(url)
- if err != nil {
- c.Errorf("Gobot GET failed: %v", err)
- http.Error(w, err.Error(), 500)
- return
- }
- defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- c.Errorf("Failed reading Gobot body: %v", err)
- http.Error(w, err.Error(), 500)
- return
- }
- if resp.StatusCode != 200 {
- c.Errorf("Gobot GET failed: got HTTP response %d\nBody: %s", resp.StatusCode, body)
- http.Error(w, "Failed contacting Gobot", 500)
- return
- }
-
- c.Infof("Gobot said %q", resp.Status)
- }
- }
-
- // Update our own record.
- err := datastore.RunInTransaction(c, func(c appengine.Context) error {
- cl := new(CL)
- err := datastore.Get(c, key, cl)
- if err != nil {
- return err
- }
- cl.Reviewer = rev
- _, err = datastore.Put(c, key, cl)
- return err
- }, nil)
- if err != nil {
- msg := fmt.Sprintf("Assignment failed: %v", err)
- c.Errorf("%s", msg)
- http.Error(w, msg, 500)
- return
- }
- c.Infof("Assigned CL %v to %v", n, rev)
-}
-
-func UpdateCLLater(c appengine.Context, n string, delay time.Duration) {
- t := taskqueue.NewPOSTTask("/update-cl", url.Values{
- "cl": []string{n},
- })
- t.Delay = delay
- if _, err := taskqueue.Add(c, t, "update-cl"); err != nil {
- c.Errorf("Failed adding task: %v", err)
- }
-}
-
-func handleUpdateCL(w http.ResponseWriter, r *http.Request) {
- c := appengine.NewContext(r)
-
- n := r.FormValue("cl")
- if !clRegexp.MatchString(n) {
- c.Errorf("Bad CL %q", n)
- http.Error(w, "Bad CL", 400)
- return
- }
-
- if err := updateCL(c, n); err != nil {
- c.Errorf("Failed updating CL %v: %v", n, err)
- http.Error(w, "Failed update", 500)
- return
- }
-
- io.WriteString(w, "OK")
-}
-
-// apiMessage describes the JSON sent back by Rietveld in the CL messages list.
-type apiMessage struct {
- Date string `json:"date"`
- Text string `json:"text"`
- Sender string `json:"sender"`
- Recipients []string `json:"recipients"`
- Approval bool `json:"approval"`
-}
-
-// byDate implements sort.Interface to order the messages by date, earliest first.
-// The dates are sent in RFC 3339 format, so string comparison matches time value comparison.
-type byDate []*apiMessage
-
-func (x byDate) Len() int { return len(x) }
-func (x byDate) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
-func (x byDate) Less(i, j int) bool { return x[i].Date < x[j].Date }
-
-// updateCL updates a single CL. If a retryable failure occurs, an error is returned.
-func updateCL(c appengine.Context, n string) error {
- c.Debugf("Updating CL %v", n)
- key := datastore.NewKey(c, "CL", n, 0, nil)
-
- url := codereviewBase + "/api/" + n + "?messages=true"
- resp, err := urlfetch.Client(c).Get(url)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- raw, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return fmt.Errorf("Failed reading HTTP body: %v", err)
- }
-
- // Special case for abandoned CLs.
- if resp.StatusCode == 404 && bytes.Contains(raw, []byte("No issue exists with that id")) {
- // Don't bother checking for errors. The CL might never have been saved, for instance.
- datastore.Delete(c, key)
- c.Infof("Deleted abandoned CL %v", n)
- return nil
- }
-
- if resp.StatusCode != 200 {
- return fmt.Errorf("Update: got HTTP response %d", resp.StatusCode)
- }
-
- var apiResp struct {
- Description string `json:"description"`
- Reviewers []string `json:"reviewers"`
- Created string `json:"created"`
- OwnerEmail string `json:"owner_email"`
- Modified string `json:"modified"`
- Closed bool `json:"closed"`
- Subject string `json:"subject"`
- Messages []*apiMessage `json:"messages"`
- }
- if err := json.Unmarshal(raw, &apiResp); err != nil {
- // probably can't be retried
- c.Errorf("Malformed JSON from %v: %v", url, err)
- return nil
- }
- //c.Infof("RAW: %+v", apiResp)
- sort.Sort(byDate(apiResp.Messages))
-
- cl := &CL{
- Number: n,
- Closed: apiResp.Closed,
- Owner: apiResp.OwnerEmail,
- Description: []byte(apiResp.Description),
- FirstLine: apiResp.Description,
- Subject: apiResp.Subject,
- Author: emailToPerson[apiResp.OwnerEmail],
- }
- cl.Created, err = time.Parse("2006-01-02 15:04:05.000000", apiResp.Created)
- if err != nil {
- c.Errorf("Bad creation time %q: %v", apiResp.Created, err)
- }
- cl.Modified, err = time.Parse("2006-01-02 15:04:05.000000", apiResp.Modified)
- if err != nil {
- c.Errorf("Bad modification time %q: %v", apiResp.Modified, err)
- }
- if i := strings.Index(cl.FirstLine, "\n"); i >= 0 {
- cl.FirstLine = cl.FirstLine[:i]
- }
- // Treat zero reviewers as a signal that the CL is completed.
- // This could be after the CL has been submitted, but before the CL author has synced,
- // but it could also be a CL manually edited to remove reviewers.
- if len(apiResp.Reviewers) == 0 {
- cl.Closed = true
- }
-
- lgtm := make(map[string]bool)
- notLGTM := make(map[string]bool)
- rcpt := make(map[string]bool)
- for _, msg := range apiResp.Messages {
- s, rev := msg.Sender, false
- if p, ok := emailToPerson[s]; ok {
- s, rev = p, true
- }
-
- line := firstLine(msg.Text)
- if line != "" {
- cl.LastUpdateBy = msg.Sender
- cl.LastUpdate = line
- }
-
- // CLs submitted by someone other than the CL owner do not immediately
- // transition to "closed". Let's simulate the intention by treating
- // messages starting with "*** Submitted as " from a reviewer as a
- // signal that the CL is now closed.
- if rev && strings.HasPrefix(msg.Text, "*** Submitted as ") {
- cl.Closed = true
- }
-
- if msg.Approval {
- lgtm[s] = true
- delete(notLGTM, s) // "LGTM" overrules previous "NOT LGTM"
- }
- if strings.Contains(line, "NOT LGTM") {
- notLGTM[s] = true
- delete(lgtm, s) // "NOT LGTM" overrules previous "LGTM"
- }
-
- for _, r := range msg.Recipients {
- rcpt[r] = true
- }
- }
- for l := range lgtm {
- cl.LGTMs = append(cl.LGTMs, l)
- }
- for l := range notLGTM {
- cl.NotLGTMs = append(cl.NotLGTMs, l)
- }
- for r := range rcpt {
- cl.Recipients = append(cl.Recipients, r)
- }
- sort.Strings(cl.LGTMs)
- sort.Strings(cl.NotLGTMs)
- sort.Strings(cl.Recipients)
-
- err = datastore.RunInTransaction(c, func(c appengine.Context) error {
- ocl := new(CL)
- err := datastore.Get(c, key, ocl)
- if err != nil && err != datastore.ErrNoSuchEntity {
- return err
- } else if err == nil {
- // LastMessageID and Reviewer need preserving.
- cl.LastMessageID = ocl.LastMessageID
- cl.Reviewer = ocl.Reviewer
- }
- _, err = datastore.Put(c, key, cl)
- return err
- }, nil)
- if err != nil {
- return err
- }
- c.Infof("Updated CL %v", n)
- return nil
-}
-
-// trailingSpaceRE matches trailing spaces.
-var trailingSpaceRE = regexp.MustCompile(`(?m)[ \t\r]+$`)
-
-// removeRE is the list of patterns to skip over at the beginning of a
-// message when looking for message text.
-var removeRE = regexp.MustCompile(`(?m-s)\A(` +
- // Skip leading "Hello so-and-so," generated by codereview plugin.
- `(Hello(.|\n)*?\n\n)` +
-
- // Skip quoted text.
- `|((On.*|.* writes|.* wrote):\n)` +
- `|((>.*\n)+)` +
-
- // Skip lines with no letters.
- `|(([^A-Za-z]*\n)+)` +
-
- // Skip links to comments and file info.
- `|(http://codereview.*\n([^ ]+:[0-9]+:.*\n)?)` +
- `|(File .*:\n)` +
-
- `)`,
-)
-
-// firstLine returns the first interesting line of the message text.
-func firstLine(text string) string {
- // Cut trailing spaces.
- text = trailingSpaceRE.ReplaceAllString(text, "")
-
- // Skip uninteresting lines.
- for {
- text = strings.TrimSpace(text)
- m := removeRE.FindStringIndex(text)
- if m == nil || m[0] != 0 {
- break
- }
- text = text[m[1]:]
- }
-
- // Chop line at newline or else at 74 bytes.
- i := strings.Index(text, "\n")
- if i >= 0 {
- text = text[:i]
- }
- if len(text) > 74 {
- text = text[:70] + "..."
- }
- return text
-}
+++ /dev/null
-// Copyright 2012 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 dashboard
-
-// This file handles the front page.
-
-import (
- "bytes"
- "html/template"
- "io"
- "net/http"
- "strings"
- "sync"
- "time"
-
- "appengine"
- "appengine/datastore"
- "appengine/user"
-)
-
-func init() {
- http.HandleFunc("/", handleFront)
- http.HandleFunc("/favicon.ico", http.NotFound)
-}
-
-// maximum number of active CLs to show in person-specific tables.
-const maxCLs = 100
-
-func handleFront(w http.ResponseWriter, r *http.Request) {
- c := appengine.NewContext(r)
-
- data := &frontPageData{
- Reviewers: personList,
- User: user.Current(c).Email,
- IsAdmin: user.IsAdmin(c),
- }
- var currentPerson string
- u := data.User
- you := "you"
- if e := r.FormValue("user"); e != "" {
- u = e
- you = e
- }
- currentPerson, data.UserIsReviewer = emailToPerson[u]
- if !data.UserIsReviewer {
- currentPerson = u
- }
-
- var wg sync.WaitGroup
- errc := make(chan error, 10)
- activeCLs := datastore.NewQuery("CL").
- Filter("Closed =", false).
- Order("-Modified")
-
- tableFetch := func(index int, f func(tbl *clTable) error) {
- wg.Add(1)
- go func() {
- defer wg.Done()
- start := time.Now()
- if err := f(&data.Tables[index]); err != nil {
- errc <- err
- }
- data.Timing[index] = time.Now().Sub(start)
- }()
- }
-
- data.Tables[0].Title = "CLs assigned to " + you + " for review"
- if data.UserIsReviewer {
- tableFetch(0, func(tbl *clTable) error {
- q := activeCLs.Filter("Reviewer =", currentPerson).Limit(maxCLs)
- tbl.Assignable = true
- _, err := q.GetAll(c, &tbl.CLs)
- return err
- })
- }
-
- tableFetch(1, func(tbl *clTable) error {
- q := activeCLs
- if data.UserIsReviewer {
- q = q.Filter("Author =", currentPerson)
- } else {
- q = q.Filter("Owner =", currentPerson)
- }
- q = q.Limit(maxCLs)
- tbl.Title = "CLs sent by " + you
- tbl.Assignable = true
- _, err := q.GetAll(c, &tbl.CLs)
- return err
- })
-
- tableFetch(2, func(tbl *clTable) error {
- q := activeCLs.Limit(50)
- tbl.Title = "Other active CLs"
- tbl.Assignable = true
- if _, err := q.GetAll(c, &tbl.CLs); err != nil {
- return err
- }
- // filter
- for i := len(tbl.CLs) - 1; i >= 0; i-- {
- cl := tbl.CLs[i]
- if cl.Owner == currentPerson || cl.Author == currentPerson || cl.Reviewer == currentPerson {
- // Preserve order.
- copy(tbl.CLs[i:], tbl.CLs[i+1:])
- tbl.CLs = tbl.CLs[:len(tbl.CLs)-1]
- }
- }
- return nil
- })
-
- tableFetch(3, func(tbl *clTable) error {
- q := datastore.NewQuery("CL").
- Filter("Closed =", true).
- Order("-Modified").
- Limit(10)
- tbl.Title = "Recently closed CLs"
- tbl.Assignable = false
- _, err := q.GetAll(c, &tbl.CLs)
- return err
- })
-
- // Not really a table fetch.
- tableFetch(0, func(_ *clTable) error {
- var err error
- data.LogoutURL, err = user.LogoutURL(c, "/")
- return err
- })
-
- wg.Wait()
-
- select {
- case err := <-errc:
- c.Errorf("%v", err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- default:
- }
-
- var b bytes.Buffer
- if err := frontPage.ExecuteTemplate(&b, "front", &data); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- io.Copy(w, &b)
-}
-
-type frontPageData struct {
- Tables [4]clTable
- Timing [4]time.Duration
-
- Reviewers []string
- UserIsReviewer bool
-
- User, LogoutURL string // actual logged in user
- IsAdmin bool
-}
-
-type clTable struct {
- Title string
- Assignable bool
- CLs []*CL
-}
-
-var frontPage = template.Must(template.New("front").Funcs(template.FuncMap{
- "selected": func(a, b string) string {
- if a == b {
- return "selected"
- }
- return ""
- },
- "shortemail": func(s string) string {
- if i := strings.Index(s, "@"); i >= 0 {
- s = s[:i]
- }
- return s
- },
-}).Parse(`
-<!doctype html>
-<html>
- <head>
- <title>Go code reviews</title>
- <link rel="icon" type="image/png" href="/static/icon.png" />
- <style type="text/css">
- body {
- font-family: Helvetica, sans-serif;
- }
- img#gopherstamp {
- float: right;
- height: auto;
- width: 250px;
- }
- h1, h2, h3 {
- color: #777;
- margin-bottom: 0;
- }
- table {
- border-spacing: 0;
- }
- td {
- vertical-align: top;
- padding: 2px 5px;
- }
- tr.unreplied td.email {
- border-left: 2px solid blue;
- }
- tr.pending td {
- background: #fc8;
- }
- tr.failed td {
- background: #f88;
- }
- tr.saved td {
- background: #8f8;
- }
- .cls {
- margin-top: 0;
- }
- a {
- color: blue;
- text-decoration: none; /* no link underline */
- }
- address {
- font-size: 10px;
- text-align: right;
- }
- .email {
- font-family: monospace;
- }
- </style>
- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
- </head>
- <body>
-
-<img id="gopherstamp" src="/static/gopherstamp.jpg" />
-<h1>Go code reviews</h1>
-
-<table class="cls">
-{{range $i, $tbl := .Tables}}
-<tr><td colspan="5"><h3>{{$tbl.Title}}</h3></td></tr>
-{{if .CLs}}
-{{range $cl := .CLs}}
- <tr id="cl-{{$cl.Number}}" class="{{if not $i}}{{if not .Reviewed}}unreplied{{end}}{{end}}">
- <td class="email">{{$cl.DisplayOwner}}</td>
- <td>
- {{if $tbl.Assignable}}
- <select id="cl-rev-{{$cl.Number}}" {{if not $.UserIsReviewer}}disabled{{end}}>
- <option></option>
- {{range $.Reviewers}}
- <option {{selected . $cl.Reviewer}}>{{.}}</option>
- {{end}}
- </select>
- <script type="text/javascript">
- $(function() {
- $('#cl-rev-{{$cl.Number}}').change(function() {
- var r = $(this).val();
- var row = $('tr#cl-{{$cl.Number}}');
- row.addClass('pending');
- $.post('/assign', {
- 'cl': '{{$cl.Number}}',
- 'r': r
- }).success(function() {
- row.removeClass('pending');
- row.addClass('saved');
- }).error(function() {
- row.removeClass('pending');
- row.addClass('failed');
- });
- });
- });
- </script>
- {{end}}
- </td>
- <td>
- <a href="http://codereview.appspot.com/{{.Number}}/" title="{{ printf "%s" .Description}}">{{.Number}}: {{.FirstLineHTML}}</a>
- {{if and .LGTMs $tbl.Assignable}}<br /><span style="font-size: smaller;">LGTMs: {{.LGTMHTML}}</span>{{end}}
- {{if and .NotLGTMs $tbl.Assignable}}<br /><span style="font-size: smaller; color: #f74545;">NOT LGTMs: {{.NotLGTMHTML}}</span>{{end}}
- {{if .LastUpdateBy}}<br /><span style="font-size: smaller; color: #777777;">(<span title="{{.LastUpdateBy}}">{{.LastUpdateBy | shortemail}}</span>) {{.LastUpdate}}</span>{{end}}
- </td>
- <td title="Last modified">{{.ModifiedAgo}}</td>
- <td>{{if $.IsAdmin}}<a href="/update-cl?cl={{.Number}}" title="Update this CL">⟳</a>{{end}}</td>
- </tr>
-{{end}}
-{{else}}
-<tr><td colspan="5"><em>none</em></td></tr>
-{{end}}
-{{end}}
-</table>
-
-<hr />
-<address>
-You are <span class="email">{{.User}}</span> · <a href="{{.LogoutURL}}">logout</a><br />
-datastore timing: {{range .Timing}} {{.}}{{end}}
-</address>
-
- </body>
-</html>
-`))