Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(932)

Delta Between Two Patch Sets: misc/dashboard/app/build/build.go

Issue 5461047: code review 5461047: misc/dashboard: user interface (Closed)
Left Patch Set: diff -r fafcd328da73 https://go.googlecode.com/hg Created 13 years, 3 months ago
Right Patch Set: diff -r f49ead115cf9 https://go.googlecode.com/hg Created 13 years, 3 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « misc/dashboard/app/app.yaml ('k') | misc/dashboard/app/build/test.go » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 // Copyright 2011 The Go Authors. All rights reserved. 1 // Copyright 2011 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style 2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file. 3 // license that can be found in the LICENSE file.
4 4
5 package build 5 package build
6 6
7 import ( 7 import (
8 "appengine" 8 "appengine"
9 "appengine/datastore" 9 "appengine/datastore"
10 "bytes" 10 "bytes"
11 "compress/gzip" 11 "compress/gzip"
12 "crypto/sha1" 12 "crypto/sha1"
13 "fmt" 13 "fmt"
14 "http" 14 "http"
15 "io" 15 "io"
16 "json" 16 "json"
17 "os" 17 "os"
18 "sort"
19 "strings" 18 "strings"
20 "template"
21 ) 19 )
22 20
23 const commitsPerPage = 20 21 const commitsPerPage = 20
24 22
25 // A Package describes a package that is listed on the dashboard. 23 // A Package describes a package that is listed on the dashboard.
26 type Package struct { 24 type Package struct {
27 Name string 25 Name string
28 Path string // (empty for the main Go tree) 26 Path string // (empty for the main Go tree)
29 NextNum int // Num of the next head Commit 27 NextNum int // Num of the next head Commit
30 } 28 }
31 29
30 func (p *Package) String() string {
31 return fmt.Sprintf("%s: %q", p.Path, p.Name)
32 }
33
32 func (p *Package) Key(c appengine.Context) *datastore.Key { 34 func (p *Package) Key(c appengine.Context) *datastore.Key {
33 key := p.Path 35 key := p.Path
34 if key == "" { 36 if key == "" {
35 key = "go" 37 key = "go"
36 } 38 }
37 return datastore.NewKey(c, "Package", key, 0, nil) 39 return datastore.NewKey(c, "Package", key, 0, nil)
38 } 40 }
39 41
42 // LastCommit returns the most recent Commit for this Package.
43 func (p *Package) LastCommit(c appengine.Context) (*Commit, os.Error) {
44 var commits []*Commit
45 _, err := datastore.NewQuery("Commit").
46 Ancestor(p.Key(c)).
47 Order("-Time").
48 Limit(1).
49 GetAll(c, &commits)
50 if err != nil {
51 return nil, err
52 }
53 if len(commits) != 1 {
54 return nil, datastore.ErrNoSuchEntity
55 }
56 return commits[0], nil
57 }
58
59 // GetPackage fetches a Package by path from the datastore.
40 func GetPackage(c appengine.Context, path string) (*Package, os.Error) { 60 func GetPackage(c appengine.Context, path string) (*Package, os.Error) {
41 p := &Package{Path: path} 61 p := &Package{Path: path}
42 err := datastore.Get(c, p.Key(c), p) 62 err := datastore.Get(c, p.Key(c), p)
43 if err == datastore.ErrNoSuchEntity { 63 if err == datastore.ErrNoSuchEntity {
44 return nil, fmt.Errorf("package %q not found", path) 64 return nil, fmt.Errorf("package %q not found", path)
45 } 65 }
46 return p, err 66 return p, err
47 } 67 }
48 68
49 // A Commit describes an individual commit in a package. 69 // A Commit describes an individual commit in a package.
50 // 70 //
51 // Each Commit entity is a descendant of its associated Package entity. 71 // Each Commit entity is a descendant of its associated Package entity.
52 // In other words, all Commits with the same PackagePath belong to the same 72 // In other words, all Commits with the same PackagePath belong to the same
53 // datastore entity group. 73 // datastore entity group.
54 type Commit struct { 74 type Commit struct {
55 PackagePath string // (empty for Go commits) 75 PackagePath string // (empty for Go commits)
56 Hash string 76 Hash string
57 ParentHash string 77 ParentHash string
58 Num int // Internal monotonic counter unique to this package. 78 Num int // Internal monotonic counter unique to this package.
59 79
60 User string 80 User string
61 Desc string `datastore:",noindex"` 81 Desc string `datastore:",noindex"`
62 Time datastore.Time 82 Time datastore.Time
63 83
64 » // Result is the Data string of each build Result for this Commit. 84 » // ResultData is the Data string of each build Result for this Commit.
65 // For non-Go commits, only the Results for the current Go tip, weekly, 85 // For non-Go commits, only the Results for the current Go tip, weekly,
66 // and release Tags are stored here. This is purely de-normalized data. 86 // and release Tags are stored here. This is purely de-normalized data.
67 // The complete data set is stored in Result entities. 87 // The complete data set is stored in Result entities.
68 » Result []string `datastore:",noindex"` 88 » ResultData []string `datastore:",noindex"`
69 } 89 }
70 90
71 func (com *Commit) Key(c appengine.Context) *datastore.Key { 91 func (com *Commit) Key(c appengine.Context) *datastore.Key {
72 if com.Hash == "" { 92 if com.Hash == "" {
73 panic("tried Key on Commit with empty Hash") 93 panic("tried Key on Commit with empty Hash")
74 } 94 }
75 p := Package{Path: com.PackagePath} 95 p := Package{Path: com.PackagePath}
76 key := com.PackagePath + "|" + com.Hash 96 key := com.PackagePath + "|" + com.Hash
77 return datastore.NewKey(c, "Commit", key, 0, p.Key(c)) 97 return datastore.NewKey(c, "Commit", key, 0, p.Key(c))
78 } 98 }
79 99
80 func (c *Commit) Valid() os.Error { 100 func (c *Commit) Valid() os.Error {
81 if !validHash(c.Hash) { 101 if !validHash(c.Hash) {
82 return os.NewError("invalid Hash") 102 return os.NewError("invalid Hash")
83 } 103 }
84 if c.ParentHash != "" && !validHash(c.ParentHash) { // empty is OK 104 if c.ParentHash != "" && !validHash(c.ParentHash) { // empty is OK
85 return os.NewError("invalid ParentHash") 105 return os.NewError("invalid ParentHash")
86 } 106 }
87 return nil 107 return nil
88 } 108 }
89 109
90 // AddResult adds the denormalized Reuslt data to the Commit's Result field. 110 // AddResult adds the denormalized Reuslt data to the Commit's Result field.
91 // It must be called from inside a datastore transaction. 111 // It must be called from inside a datastore transaction.
92 func (com *Commit) AddResult(c appengine.Context, r *Result) os.Error { 112 func (com *Commit) AddResult(c appengine.Context, r *Result) os.Error {
93 if err := datastore.Get(c, com.Key(c), com); err != nil { 113 if err := datastore.Get(c, com.Key(c), com); err != nil {
94 return err 114 return err
95 } 115 }
96 » com.Result = append(com.Result, r.Data()) 116 » com.ResultData = append(com.ResultData, r.Data())
97 _, err := datastore.Put(c, com.Key(c), com) 117 _, err := datastore.Put(c, com.Key(c), com)
98 return err 118 return err
99 } 119 }
100 120
101 func (c *Commit) GetResult(builder, goHash string) *Result { 121 // Result returns the build Result for this Commit for the given builder/goHash.
102 » for _, r := range c.Result { 122 func (c *Commit) Result(builder, goHash string) *Result {
123 » for _, r := range c.ResultData {
103 p := strings.SplitN(r, "|", 4) 124 p := strings.SplitN(r, "|", 4)
104 if len(p) != 4 || p[0] != builder || p[3] != goHash { 125 if len(p) != 4 || p[0] != builder || p[3] != goHash {
105 continue 126 continue
106 } 127 }
107 » » return &Result{ 128 » » return partsToHash(c, p)
108 » » » Builder: p[0],
109 » » » Hash: c.Hash,
110 » » » PackagePath: c.PackagePath,
111 » » » GoHash: p[3],
112 » » » OK: p[1] == "true",
113 » » » LogHash: p[2],
114 » » }
115 } 129 }
116 return nil 130 return nil
131 }
132
133 // Results returns the build Results for this Commit for the given goHash.
134 func (c *Commit) Results(goHash string) (results []*Result) {
135 for _, r := range c.ResultData {
136 p := strings.SplitN(r, "|", 4)
137 if len(p) != 4 || p[3] != goHash {
138 continue
139 }
140 results = append(results, partsToHash(c, p))
141 }
142 return
143 }
144
145 // partsToHash converts a Commit and ResultData substrings to a Result.
146 func partsToHash(c *Commit, p []string) *Result {
147 return &Result{
148 Builder: p[0],
149 Hash: c.Hash,
150 PackagePath: c.PackagePath,
151 GoHash: p[3],
152 OK: p[1] == "true",
153 LogHash: p[2],
154 }
117 } 155 }
118 156
119 // A Result describes a build result for a Commit on an OS/architecture. 157 // A Result describes a build result for a Commit on an OS/architecture.
120 // 158 //
121 // Each Result entity is a descendant of its associated Commit entity. 159 // Each Result entity is a descendant of its associated Commit entity.
122 type Result struct { 160 type Result struct {
123 Builder string // "arch-os[-note]" 161 Builder string // "arch-os[-note]"
124 Hash string 162 Hash string
125 PackagePath string // (empty for Go commits) 163 PackagePath string // (empty for Go commits)
126 164
127 // The Go Commit this was built against (empty for Go commits). 165 // The Go Commit this was built against (empty for Go commits).
128 GoHash string 166 GoHash string
129 167
130 OK bool 168 OK bool
131 Log []byte `datastore:"-"` // for JSON unmarshaling 169 Log []byte `datastore:"-"` // for JSON unmarshaling
132 LogHash string `datastore:",noindex"` // Key to the Log record. 170 LogHash string `datastore:",noindex"` // Key to the Log record.
133 } 171 }
134 172
135 func (r *Result) Key(c appengine.Context) *datastore.Key { 173 func (r *Result) Key(c appengine.Context) *datastore.Key {
136 p := Package{Path: r.PackagePath} 174 p := Package{Path: r.PackagePath}
137 key := r.Builder + "|" + r.PackagePath + "|" + r.Hash + "|" + r.GoHash 175 key := r.Builder + "|" + r.PackagePath + "|" + r.Hash + "|" + r.GoHash
138 return datastore.NewKey(c, "Result", key, 0, p.Key(c)) 176 return datastore.NewKey(c, "Result", key, 0, p.Key(c))
139 } 177 }
140 178
141 func (r *Result) Data() string {
142 return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash)
143 }
144
145 func (r *Result) Valid() os.Error { 179 func (r *Result) Valid() os.Error {
146 if !validHash(r.Hash) { 180 if !validHash(r.Hash) {
147 return os.NewError("invalid Hash") 181 return os.NewError("invalid Hash")
148 } 182 }
149 if r.PackagePath != "" && !validHash(r.GoHash) { 183 if r.PackagePath != "" && !validHash(r.GoHash) {
150 return os.NewError("invalid GoHash") 184 return os.NewError("invalid GoHash")
151 } 185 }
152 return nil 186 return nil
187 }
188
189 // Data returns the Result in string format
190 // to be stored in Commit's ResultData field.
191 func (r *Result) Data() string {
192 return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash)
153 } 193 }
154 194
155 // A Log is a gzip-compressed log file stored under the SHA1 hash of the 195 // A Log is a gzip-compressed log file stored under the SHA1 hash of the
156 // uncompressed log text. 196 // uncompressed log text.
157 type Log struct { 197 type Log struct {
158 CompressedLog []byte 198 CompressedLog []byte
159 } 199 }
160 200
161 func PutLog(c appengine.Context, text []byte) (hash string, err os.Error) { 201 func PutLog(c appengine.Context, text []byte) (hash string, err os.Error) {
162 h := sha1.New() 202 h := sha1.New()
(...skipping 15 matching lines...) Expand all
178 Name string // the tag itself (for example: "release.r60") 218 Name string // the tag itself (for example: "release.r60")
179 Hash string 219 Hash string
180 } 220 }
181 221
182 func (t *Tag) Key(c appengine.Context) *datastore.Key { 222 func (t *Tag) Key(c appengine.Context) *datastore.Key {
183 p := &Package{} 223 p := &Package{}
184 return datastore.NewKey(c, "Tag", t.Kind, 0, p.Key(c)) 224 return datastore.NewKey(c, "Tag", t.Kind, 0, p.Key(c))
185 } 225 }
186 226
187 func (t *Tag) Valid() os.Error { 227 func (t *Tag) Valid() os.Error {
188 » if t.Kind != "weekly" || t.Kind != "release" || t.Kind != "tip" { 228 » if t.Kind != "weekly" && t.Kind != "release" && t.Kind != "tip" {
189 return os.NewError("invalid Kind") 229 return os.NewError("invalid Kind")
190 } 230 }
191 if !validHash(t.Hash) { 231 if !validHash(t.Hash) {
192 return os.NewError("invalid Hash") 232 return os.NewError("invalid Hash")
193 } 233 }
194 return nil 234 return nil
235 }
236
237 // GetTag fetches a Tag by name from the datastore.
238 func GetTag(c appengine.Context, tag string) (*Tag, os.Error) {
239 t := &Tag{Kind: tag}
240 if err := datastore.Get(c, t.Key(c), t); err != nil {
241 if err == datastore.ErrNoSuchEntity {
242 return nil, os.NewError("tag not found: " + tag)
243 }
244 return nil, err
245 }
246 if err := t.Valid(); err != nil {
247 return nil, err
248 }
249 return t, nil
195 } 250 }
196 251
197 // commitHandler retrieves commit data or records a new commit. 252 // commitHandler retrieves commit data or records a new commit.
198 // 253 //
199 // For GET requests it returns a Commit value for the specified 254 // For GET requests it returns a Commit value for the specified
200 // packagePath and hash. 255 // packagePath and hash.
201 // 256 //
202 // For POST requests it reads a JSON-encoded Commit value from the request 257 // For POST requests it reads a JSON-encoded Commit value from the request
203 // body and creates a new Commit entity. It also updates the "tip" Tag for 258 // body and creates a new Commit entity. It also updates the "tip" Tag for
204 // each new commit at tip. 259 // each new commit at tip.
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after
326 Order("-Num"). 381 Order("-Num").
327 Run(c) 382 Run(c)
328 for { 383 for {
329 com := new(Commit) 384 com := new(Commit)
330 if _, err := t.Next(com); err != nil { 385 if _, err := t.Next(com); err != nil {
331 if err == datastore.Done { 386 if err == datastore.Done {
332 err = nil 387 err = nil
333 } 388 }
334 return nil, err 389 return nil, err
335 } 390 }
336 » » if com.GetResult(builder, goHash) == nil { 391 » » if com.Result(builder, goHash) == nil {
337 return com.Hash, nil 392 return com.Hash, nil
338 } 393 }
339 } 394 }
340 panic("unreachable") 395 panic("unreachable")
341 } 396 }
342 397
343 // packagesHandler returns a list of the non-Go Packages monitored 398 // packagesHandler returns a list of the non-Go Packages monitored
344 // by the dashboard. 399 // by the dashboard.
345 func packagesHandler(r *http.Request) (interface{}, os.Error) { 400 func packagesHandler(r *http.Request) (interface{}, os.Error) {
346 » c := appengine.NewContext(r) 401 » return Packages(appengine.NewContext(r))
402 }
403
404 // Packages returns all non-Go packages.
405 func Packages(c appengine.Context) ([]*Package, os.Error) {
347 var pkgs []*Package 406 var pkgs []*Package
348 for t := datastore.NewQuery("Package").Run(c); ; { 407 for t := datastore.NewQuery("Package").Run(c); ; {
349 pkg := new(Package) 408 pkg := new(Package)
350 if _, err := t.Next(pkg); err == datastore.Done { 409 if _, err := t.Next(pkg); err == datastore.Done {
351 break 410 break
352 } else if err != nil { 411 } else if err != nil {
353 return nil, err 412 return nil, err
354 } 413 }
355 if pkg.Path != "" { 414 if pkg.Path != "" {
356 pkgs = append(pkgs, pkg) 415 pkgs = append(pkgs, pkg)
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
395 if _, err := datastore.Put(c, res.Key(c), res); err != nil { 454 if _, err := datastore.Put(c, res.Key(c), res); err != nil {
396 return err 455 return err
397 } 456 }
398 // add Result to Commit 457 // add Result to Commit
399 com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash} 458 com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash}
400 return com.AddResult(c, res) 459 return com.AddResult(c, res)
401 } 460 }
402 return nil, datastore.RunInTransaction(c, tx, nil) 461 return nil, datastore.RunInTransaction(c, tx, nil)
403 } 462 }
404 463
464 // logHandler displays log text for a given hash.
465 // It handles paths like "/log/hash".
405 func logHandler(w http.ResponseWriter, r *http.Request) { 466 func logHandler(w http.ResponseWriter, r *http.Request) {
406 c := appengine.NewContext(r) 467 c := appengine.NewContext(r)
407 h := r.URL.Path[len("/log/"):] 468 h := r.URL.Path[len("/log/"):]
408 k := datastore.NewKey(c, "Log", h, 0, nil) 469 k := datastore.NewKey(c, "Log", h, 0, nil)
409 l := new(Log) 470 l := new(Log)
410 if err := datastore.Get(c, k, l); err != nil { 471 if err := datastore.Get(c, k, l); err != nil {
411 logErr(w, r, err) 472 logErr(w, r, err)
412 return 473 return
413 } 474 }
414 d, err := gzip.NewReader(bytes.NewBuffer(l.CompressedLog)) 475 d, err := gzip.NewReader(bytes.NewBuffer(l.CompressedLog))
415 if err != nil { 476 if err != nil {
416 logErr(w, r, err) 477 logErr(w, r, err)
417 return 478 return
418 } 479 }
419 if _, err := io.Copy(w, d); err != nil { 480 if _, err := io.Copy(w, d); err != nil {
420 logErr(w, r, err) 481 logErr(w, r, err)
421 } 482 }
422 } 483 }
423 484
424 var uiTemplate = template.Must(template.ParseFile("build/ui.html"))
425
426 func uiHandler(w http.ResponseWriter, r *http.Request) {
427 c := appengine.NewContext(r)
428
429 p := &Package{}
430 q := datastore.NewQuery("Commit").
431 Ancestor(p.Key(c)).
432 Order("-Time").
433 Limit(commitsPerPage)
434 var commits []*Commit
435 if _, err := q.GetAll(c, &commits); err != nil {
436 logErr(w, r, err)
437 return
438 }
439
440 builders := make(map[string]bool)
441 for _, commit := range commits {
442 for _, r := range commit.Result {
443 p := strings.SplitN(r, "|", 4)
444 if len(p) != 4 {
445 continue
446 }
447 builders[p[0]] = true
448 }
449 }
450
451 c.Infof("\ncommits: %v\nq: %v\nkey: %v", commits, q, p.Key(c))
452
453 err := uiTemplate.Execute(w, struct {
454 Commits []*Commit
455 Builders []string
456 }{
457 commits,
458 keys(builders),
459 })
460 if err != nil {
461 logErr(w, r, err)
462 }
463 }
464
465 func keys(m map[string]bool) (s []string) {
466 for k := range m {
467 s = append(s, k)
468 }
469 sort.Strings(s)
470 return
471 }
472
473 type errBadMethod string
474
475 func (e errBadMethod) String() string {
476 return "bad method: " + string(e)
477 }
478
479 type dashHandler func(*http.Request) (interface{}, os.Error) 485 type dashHandler func(*http.Request) (interface{}, os.Error)
480 486
481 type dashResponse struct { 487 type dashResponse struct {
482 Response interface{} 488 Response interface{}
483 Error string 489 Error string
490 }
491
492 // errBadMethod is returned by a dashHandler when
493 // the request has an unsuitable method.
494 type errBadMethod string
495
496 func (e errBadMethod) String() string {
497 return "bad method: " + string(e)
484 } 498 }
485 499
486 // AuthHandler wraps a http.HandlerFunc with a handler that validates the 500 // AuthHandler wraps a http.HandlerFunc with a handler that validates the
487 // supplied key and builder query parameters. 501 // supplied key and builder query parameters.
488 func AuthHandler(h dashHandler) http.HandlerFunc { 502 func AuthHandler(h dashHandler) http.HandlerFunc {
489 return func(w http.ResponseWriter, r *http.Request) { 503 return func(w http.ResponseWriter, r *http.Request) {
490 // Put the URL Query values into r.Form to avoid parsing the 504 // Put the URL Query values into r.Form to avoid parsing the
491 // request body when calling r.FormValue. 505 // request body when calling r.FormValue.
492 r.Form = r.URL.Query() 506 r.Form = r.URL.Query()
493 507
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
540 http.HandleFunc("/init", initHandler) 554 http.HandleFunc("/init", initHandler)
541 555
542 // authenticated handlers 556 // authenticated handlers
543 http.HandleFunc("/commit", AuthHandler(commitHandler)) 557 http.HandleFunc("/commit", AuthHandler(commitHandler))
544 http.HandleFunc("/packages", AuthHandler(packagesHandler)) 558 http.HandleFunc("/packages", AuthHandler(packagesHandler))
545 http.HandleFunc("/result", AuthHandler(resultHandler)) 559 http.HandleFunc("/result", AuthHandler(resultHandler))
546 http.HandleFunc("/tag", AuthHandler(tagHandler)) 560 http.HandleFunc("/tag", AuthHandler(tagHandler))
547 http.HandleFunc("/todo", AuthHandler(todoHandler)) 561 http.HandleFunc("/todo", AuthHandler(todoHandler))
548 562
549 // public handlers 563 // public handlers
550 http.HandleFunc("/", uiHandler)
551 http.HandleFunc("/log/", logHandler) 564 http.HandleFunc("/log/", logHandler)
552 } 565 }
553 566
554 func validHash(hash string) bool { 567 func validHash(hash string) bool {
555 // TODO(adg): correctly validate a hash 568 // TODO(adg): correctly validate a hash
556 return hash != "" 569 return hash != ""
557 } 570 }
558 571
559 func logErr(w http.ResponseWriter, r *http.Request, err os.Error) { 572 func logErr(w http.ResponseWriter, r *http.Request, err os.Error) {
560 appengine.NewContext(r).Errorf("Error: %v", err) 573 appengine.NewContext(r).Errorf("Error: %v", err)
561 w.WriteHeader(http.StatusInternalServerError) 574 w.WriteHeader(http.StatusInternalServerError)
562 fmt.Fprint(w, "Error: ", err) 575 fmt.Fprint(w, "Error: ", err)
563 } 576 }
LEFTRIGHT

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b