go.tools/dashboard: add gccgo build dashboard.

This change adds a new build dashboard url to the existing appengine app: $dashurl/gccgo/ which will show the build status of gccgo.
* Added Dashboard struct with exported Name, Rel(ative)Path, and Packages fields.
* Added Dashboard Context method that returns an appengine context with a namespace corresponding to the dashboard's name.
* Modified HandlerFuncs to use Dashboard's Context method for all appengine requests.
* Modified ui template to show different title/header for separate dashboard and added dashboard tab.

R=adg
CC=golang-dev
https://golang.org/cl/13753043
This commit is contained in:
Chris Manghane 2013-09-23 17:06:49 -07:00
parent 0e6d095d11
commit 7bcc81e644
7 changed files with 178 additions and 71 deletions

View File

@ -13,8 +13,8 @@ handlers:
static_dir: static
- url: /log/.+
script: _go_app
- url: /(|commit|packages|result|tag|todo)
- url: /(|gccgo/)(|commit|packages|result|tag|todo)
script: _go_app
- url: /(init|buildtest|key|_ah/queue/go/delay)
- url: /(|gccgo/)(init|buildtest|key|_ah/queue/go/delay)
script: _go_app
login: admin

View File

@ -84,7 +84,7 @@ func GetPackage(c appengine.Context, path string) (*Package, error) {
// In other words, all Commits with the same PackagePath belong to the same
// datastore entity group.
type Commit struct {
PackagePath string // (empty for Go commits)
PackagePath string // (empty for main repo commits)
Hash string
ParentHash string
Num int // Internal monotonic counter unique to this package.

113
dashboard/app/build/dash.go Normal file
View File

@ -0,0 +1,113 @@
// Copyright 2013 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.
// +build appengine
package build
import (
"net/http"
"strings"
"appengine"
)
// Dashboard describes a unique build dashboard.
type Dashboard struct {
Name string // This dashboard's name and namespace
RelPath string // The relative url path
Packages []*Package // The project's packages to build
}
// dashboardForRequest returns the appropriate dashboard for a given URL path.
func dashboardForRequest(r *http.Request) *Dashboard {
if strings.HasPrefix(r.URL.Path, gccgoDash.RelPath) {
return gccgoDash
}
return goDash
}
// Context returns a namespaced context for this dashboard, or panics if it
// fails to create a new context.
func (d *Dashboard) Context(c appengine.Context) appengine.Context {
// No namespace needed for the original Go dashboard.
if d.Name == "Go" {
return c
}
n, err := appengine.Namespace(c, d.Name)
if err != nil {
panic(err)
}
return n
}
// the currently known dashboards.
var dashboards = []*Dashboard{goDash, gccgoDash}
// goDash is the dashboard for the main go repository.
var goDash = &Dashboard{
Name: "Go",
RelPath: "/",
Packages: goPackages,
}
// goPackages is a list of all of the packages built by the main go repository.
var goPackages = []*Package{
{
Kind: "go",
Name: "Go",
},
{
Kind: "subrepo",
Name: "go.blog",
Path: "code.google.com/p/go.blog",
},
{
Kind: "subrepo",
Name: "go.codereview",
Path: "code.google.com/p/go.codereview",
},
{
Kind: "subrepo",
Name: "go.crypto",
Path: "code.google.com/p/go.crypto",
},
{
Kind: "subrepo",
Name: "go.exp",
Path: "code.google.com/p/go.exp",
},
{
Kind: "subrepo",
Name: "go.image",
Path: "code.google.com/p/go.image",
},
{
Kind: "subrepo",
Name: "go.net",
Path: "code.google.com/p/go.net",
},
{
Kind: "subrepo",
Name: "go.talks",
Path: "code.google.com/p/go.talks",
},
{
Kind: "subrepo",
Name: "go.tools",
Path: "code.google.com/p/go.tools",
},
}
// gccgoDash is the dashboard for gccgo.
var gccgoDash = &Dashboard{
Name: "Gccgo",
RelPath: "/gccgo/",
Packages: []*Package{
{
Kind: "gccgo",
Name: "Gccgo",
},
},
}

View File

@ -33,7 +33,7 @@ const commitsPerPage = 30
//
// This handler is used by a gobuilder process in -commit mode.
func commitHandler(r *http.Request) (interface{}, error) {
c := appengine.NewContext(r)
c := contextForRequest(r)
com := new(Commit)
if r.Method == "GET" {
@ -132,7 +132,7 @@ func tagHandler(r *http.Request) (interface{}, error) {
if err := t.Valid(); err != nil {
return nil, err
}
c := appengine.NewContext(r)
c := contextForRequest(r)
defer cache.Tick(c)
_, err := datastore.Put(c, t.Key(c), t)
return nil, err
@ -148,7 +148,7 @@ type Todo struct {
// It expects "builder" and "kind" query parameters and returns a *Todo value.
// Multiple "kind" parameters may be specified.
func todoHandler(r *http.Request) (interface{}, error) {
c := appengine.NewContext(r)
c := contextForRequest(r)
now := cache.Now(c)
key := "build-todo-" + r.Form.Encode()
var todo *Todo
@ -257,7 +257,7 @@ func buildTodo(c appengine.Context, builder, packagePath, goHash string) (interf
// by the dashboard.
func packagesHandler(r *http.Request) (interface{}, error) {
kind := r.FormValue("kind")
c := appengine.NewContext(r)
c := contextForRequest(r)
now := cache.Now(c)
key := "build-packages-" + kind
var p []*Package
@ -282,7 +282,7 @@ func resultHandler(r *http.Request) (interface{}, error) {
return nil, errBadMethod(r.Method)
}
c := appengine.NewContext(r)
c := contextForRequest(r)
res := new(Result)
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(res); err != nil {
@ -326,7 +326,7 @@ func resultHandler(r *http.Request) (interface{}, error) {
// It handles paths like "/log/hash".
func logHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "text/plain; charset=utf-8")
c := appengine.NewContext(r)
c := contextForRequest(r)
hash := r.URL.Path[len("/log/"):]
key := datastore.NewKey(c, "Log", hash, 0, nil)
l := new(Log)
@ -361,7 +361,7 @@ func (e errBadMethod) Error() string {
// supplied key and builder query parameters.
func AuthHandler(h dashHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
c := contextForRequest(r)
// Put the URL Query values into r.Form to avoid parsing the
// request body when calling r.FormValue.
@ -401,24 +401,26 @@ func keyHandler(w http.ResponseWriter, r *http.Request) {
logErr(w, r, errors.New("must supply builder in query string"))
return
}
c := appengine.NewContext(r)
c := contextForRequest(r)
fmt.Fprint(w, builderKey(c, builder))
}
func init() {
// admin handlers
http.HandleFunc("/init", initHandler)
http.HandleFunc("/key", keyHandler)
for _, d := range dashboards {
// admin handlers
http.HandleFunc(d.RelPath+"init", initHandler)
http.HandleFunc(d.RelPath+"key", keyHandler)
// authenticated handlers
http.HandleFunc("/commit", AuthHandler(commitHandler))
http.HandleFunc("/packages", AuthHandler(packagesHandler))
http.HandleFunc("/result", AuthHandler(resultHandler))
http.HandleFunc("/tag", AuthHandler(tagHandler))
http.HandleFunc("/todo", AuthHandler(todoHandler))
// authenticated handlers
http.HandleFunc(d.RelPath+"commit", AuthHandler(commitHandler))
http.HandleFunc(d.RelPath+"packages", AuthHandler(packagesHandler))
http.HandleFunc(d.RelPath+"result", AuthHandler(resultHandler))
http.HandleFunc(d.RelPath+"tag", AuthHandler(tagHandler))
http.HandleFunc(d.RelPath+"todo", AuthHandler(todoHandler))
// public handlers
http.HandleFunc("/log/", logHandler)
// public handlers
http.HandleFunc(d.RelPath+"log/", logHandler)
}
}
func validHash(hash string) bool {
@ -443,7 +445,11 @@ func builderKey(c appengine.Context, builder string) string {
}
func logErr(w http.ResponseWriter, r *http.Request, err error) {
appengine.NewContext(r).Errorf("Error: %v", err)
contextForRequest(r).Errorf("Error: %v", err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Error: ", err)
}
func contextForRequest(r *http.Request) appengine.Context {
return dashboardForRequest(r).Context(appengine.NewContext(r))
}

View File

@ -15,39 +15,11 @@ import (
"cache"
)
// defaultPackages specifies the Package records to be created by initHandler.
var defaultPackages = []*Package{
{Name: "Go", Kind: "go"},
}
// subRepos specifies the Go project sub-repositories.
var subRepos = []string{
"blog",
"codereview",
"crypto",
"exp",
"image",
"net",
"talks",
"tools",
}
// Put subRepos into defaultPackages.
func init() {
for _, name := range subRepos {
p := &Package{
Kind: "subrepo",
Name: "go." + name,
Path: "code.google.com/p/go." + name,
}
defaultPackages = append(defaultPackages, p)
}
}
func initHandler(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
d := dashboardForRequest(r)
c := d.Context(appengine.NewContext(r))
defer cache.Tick(c)
for _, p := range defaultPackages {
for _, p := range d.Packages {
err := datastore.Get(c, p.Key(c), new(Package))
if _, ok := err.(*datastore.ErrFieldMismatch); ok {
// Some fields have been removed, so it's okay to ignore this error.

View File

@ -25,12 +25,15 @@ import (
)
func init() {
http.HandleFunc("/", uiHandler)
for _, d := range dashboards {
http.HandleFunc(d.RelPath, uiHandler)
}
}
// uiHandler draws the build status page.
func uiHandler(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
d := dashboardForRequest(r)
c := d.Context(appengine.NewContext(r))
now := cache.Now(c)
const key = "build-ui"
@ -48,7 +51,7 @@ func uiHandler(w http.ResponseWriter, r *http.Request) {
}
}
commits, err := goCommits(c, page)
commits, err := dashCommits(c, page)
if err != nil {
logErr(w, r, err)
return
@ -73,7 +76,7 @@ func uiHandler(w http.ResponseWriter, r *http.Request) {
p.Prev = page - 1
p.HasPrev = true
}
data := &uiTemplateData{commits, builders, tipState, p}
data := &uiTemplateData{d, commits, builders, tipState, p}
var buf bytes.Buffer
if err := uiTemplate.Execute(&buf, data); err != nil {
@ -94,9 +97,9 @@ type Pagination struct {
HasPrev bool
}
// goCommits gets a slice of the latest Commits to the Go repository.
// dashCommits gets a slice of the latest Commits to the current dashboard.
// If page > 0 it paginates by commitsPerPage.
func goCommits(c appengine.Context, page int) ([]*Commit, error) {
func dashCommits(c appengine.Context, page int) ([]*Commit, error) {
q := datastore.NewQuery("Commit").
Ancestor((&Package{}).Key(c)).
Order("-Num").
@ -166,6 +169,7 @@ func TagStateByName(c appengine.Context, name string) (*TagState, error) {
}
type uiTemplateData struct {
Dashboard *Dashboard
Commits []*Commit
Builders []string
TipState *TagState
@ -183,6 +187,7 @@ var tmplFuncs = template.FuncMap{
"builderArchChar": builderArchChar,
"builderTitle": builderTitle,
"builderSpans": builderSpans,
"buildDashboards": buildDashboards,
"repoURL": repoURL,
"shortDesc": shortDesc,
"shortHash": shortHash,
@ -265,6 +270,11 @@ func builderTitle(s string) string {
return strings.Replace(s, "-", " ", -1)
}
// buildDashboards returns the known public dashboards.
func buildDashboards() []*Dashboard {
return dashboards
}
// shortDesc returns the first line of a description.
func shortDesc(desc string) string {
if i := strings.Index(desc, "\n"); i != -1 {

View File

@ -1,7 +1,7 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Go Build Dashboard</title>
<title>{{$.Dashboard.Name}} Build Dashboard</title>
<style>
body {
font-family: sans-serif;
@ -53,10 +53,10 @@
.build .desc, .build .time, .build .user {
white-space: nowrap;
}
.paginate {
.dashboards, .paginate {
padding: 0.5em;
}
.paginate a {
.dashboards a, .paginate a {
padding: 0.5em;
background: #eee;
color: blue;
@ -70,8 +70,12 @@
</style>
</head>
<body>
<h1>Go Build Status</h1>
<h1>{{$.Dashboard.Name}} Build Status</h1>
<nav class="dashboards">
{{range buildDashboards}}
<a href="{{.RelPath}}">{{.Name}}</a>
{{end}}
</nav>
{{if $.Commits}}
@ -138,12 +142,13 @@
{{with $.TipState}}
{{$goHash := .Tag.Hash}}
<h2>
Sub-repositories at tip
<small>(<a href="{{repoURL .Tag.Hash ""}}">{{shortHash .Tag.Hash}}</a>)</small>
</h2>
{{if .Packages}}
<h2>
Sub-repositories at tip
<small>(<a href="{{repoURL .Tag.Hash ""}}">{{shortHash .Tag.Hash}}</a>)</small>
</h2>
<table class="build">
<table class="build">
<colgroup class="col-package"></colgroup>
<colgroup class="col-hash"></colgroup>
{{range $.Builders | builderSpans}}
@ -203,6 +208,7 @@
</tr>
{{end}}
</table>
{{end}}
{{end}}
</body>