mirror of
https://github.com/golang/go
synced 2024-11-19 04:14:45 -07:00
8d0ad68ce2
Change-Id: I90be865aaab01701d10af57bdf1e0bee5533d3e5 Reviewed-on: https://go-review.googlesource.com/1562 Reviewed-by: Andrew Gerrand <adg@golang.org>
493 lines
11 KiB
Go
493 lines
11 KiB
Go
// Copyright 2011 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.
|
|
|
|
// TODO(adg): packages at weekly/release
|
|
// TODO(adg): some means to register new packages
|
|
|
|
// +build appengine
|
|
|
|
package build
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"cache"
|
|
|
|
"appengine"
|
|
"appengine/datastore"
|
|
)
|
|
|
|
func init() {
|
|
handleFunc("/", uiHandler)
|
|
}
|
|
|
|
// uiHandler draws the build status page.
|
|
func uiHandler(w http.ResponseWriter, r *http.Request) {
|
|
d := dashboardForRequest(r)
|
|
c := d.Context(appengine.NewContext(r))
|
|
now := cache.Now(c)
|
|
key := "build-ui"
|
|
|
|
page, _ := strconv.Atoi(r.FormValue("page"))
|
|
if page < 0 {
|
|
page = 0
|
|
}
|
|
key += fmt.Sprintf("-page%v", page)
|
|
|
|
branch := r.FormValue("branch")
|
|
if branch != "" {
|
|
key += "-branch-" + branch
|
|
}
|
|
|
|
repo := r.FormValue("repo")
|
|
if repo != "" {
|
|
key += "-repo-" + repo
|
|
}
|
|
|
|
var b []byte
|
|
if cache.Get(r, now, key, &b) {
|
|
w.Write(b)
|
|
return
|
|
}
|
|
|
|
pkg := &Package{} // empty package is the main repository
|
|
if repo != "" {
|
|
var err error
|
|
pkg, err = GetPackage(c, repo)
|
|
if err != nil {
|
|
logErr(w, r, err)
|
|
return
|
|
}
|
|
}
|
|
commits, err := dashCommits(c, pkg, page, branch)
|
|
if err != nil {
|
|
logErr(w, r, err)
|
|
return
|
|
}
|
|
builders := commitBuilders(commits)
|
|
|
|
var tipState *TagState
|
|
if pkg.Kind == "" && page == 0 && (branch == "" || branch == "default") {
|
|
// only show sub-repo state on first page of normal repo view
|
|
tipState, err = TagStateByName(c, "tip")
|
|
if err != nil {
|
|
logErr(w, r, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
p := &Pagination{}
|
|
if len(commits) == commitsPerPage {
|
|
p.Next = page + 1
|
|
}
|
|
if page > 0 {
|
|
p.Prev = page - 1
|
|
p.HasPrev = true
|
|
}
|
|
data := &uiTemplateData{d, pkg, commits, builders, tipState, p, branch}
|
|
|
|
if r.FormValue("mode") == "failures" {
|
|
failuresHandler(w, r, data)
|
|
return
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := uiTemplate.Execute(&buf, data); err != nil {
|
|
logErr(w, r, err)
|
|
return
|
|
}
|
|
|
|
cache.Set(r, now, key, buf.Bytes())
|
|
|
|
buf.WriteTo(w)
|
|
}
|
|
|
|
// failuresHandler is https://build.golang.org/?mode=failures , where it outputs
|
|
// one line per failure on the front page, in the form:
|
|
// hash builder failure-url
|
|
func failuresHandler(w http.ResponseWriter, r *http.Request, data *uiTemplateData) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
d := dashboardForRequest(r)
|
|
for _, c := range data.Commits {
|
|
for _, b := range data.Builders {
|
|
res := c.Result(b, "")
|
|
if res == nil || res.OK || res.LogHash == "" {
|
|
continue
|
|
}
|
|
url := fmt.Sprintf("https://%v%v/log/%v", r.Host, d.Prefix, res.LogHash)
|
|
fmt.Fprintln(w, c.Hash, b, url)
|
|
}
|
|
}
|
|
}
|
|
|
|
type Pagination struct {
|
|
Next, Prev int
|
|
HasPrev bool
|
|
}
|
|
|
|
// dashCommits gets a slice of the latest Commits to the current dashboard.
|
|
// If page > 0 it paginates by commitsPerPage.
|
|
func dashCommits(c appengine.Context, pkg *Package, page int, branch string) ([]*Commit, error) {
|
|
offset := page * commitsPerPage
|
|
q := datastore.NewQuery("Commit").
|
|
Ancestor(pkg.Key(c)).
|
|
Order("-Num")
|
|
|
|
var commits []*Commit
|
|
if branch == "" {
|
|
_, err := q.Limit(commitsPerPage).Offset(offset).
|
|
GetAll(c, &commits)
|
|
return commits, err
|
|
}
|
|
|
|
// Look for commits on a specific branch.
|
|
for t, n := q.Run(c), 0; len(commits) < commitsPerPage && n < 1000; {
|
|
var c Commit
|
|
_, err := t.Next(&c)
|
|
if err == datastore.Done {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isBranchCommit(&c, branch) {
|
|
continue
|
|
}
|
|
if n >= offset {
|
|
commits = append(commits, &c)
|
|
}
|
|
n++
|
|
}
|
|
return commits, nil
|
|
}
|
|
|
|
// isBranchCommit reports whether the given commit is on the specified branch.
|
|
// It does so by examining the commit description, so there will be some bad
|
|
// matches where the branch commits do not begin with the "[branch]" prefix.
|
|
func isBranchCommit(c *Commit, b string) bool {
|
|
d := strings.TrimSpace(c.Desc)
|
|
if b == "default" {
|
|
return !strings.HasPrefix(d, "[")
|
|
}
|
|
return strings.HasPrefix(d, "["+b+"]")
|
|
}
|
|
|
|
// commitBuilders returns the names of the builders that provided
|
|
// Results for the provided commits.
|
|
func commitBuilders(commits []*Commit) []string {
|
|
builders := make(map[string]bool)
|
|
for _, commit := range commits {
|
|
for _, r := range commit.Results() {
|
|
builders[r.Builder] = true
|
|
}
|
|
}
|
|
k := keys(builders)
|
|
sort.Sort(builderOrder(k))
|
|
return k
|
|
}
|
|
|
|
func keys(m map[string]bool) (s []string) {
|
|
for k := range m {
|
|
s = append(s, k)
|
|
}
|
|
sort.Strings(s)
|
|
return
|
|
}
|
|
|
|
// builderOrder implements sort.Interface, sorting builder names
|
|
// ("darwin-amd64", etc) first by builderPriority and then alphabetically.
|
|
type builderOrder []string
|
|
|
|
func (s builderOrder) Len() int { return len(s) }
|
|
func (s builderOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
func (s builderOrder) Less(i, j int) bool {
|
|
pi, pj := builderPriority(s[i]), builderPriority(s[j])
|
|
if pi == pj {
|
|
return s[i] < s[j]
|
|
}
|
|
return pi < pj
|
|
}
|
|
|
|
func builderPriority(builder string) (p int) {
|
|
// Put -temp builders at the end, always.
|
|
if strings.HasSuffix(builder, "-temp") {
|
|
defer func() { p += 20 }()
|
|
}
|
|
// Group race builders together.
|
|
if isRace(builder) {
|
|
return 1
|
|
}
|
|
// If the OS has a specified priority, use it.
|
|
if p, ok := osPriority[builderOS(builder)]; ok {
|
|
return p
|
|
}
|
|
// The rest.
|
|
return 10
|
|
}
|
|
|
|
func isRace(s string) bool {
|
|
return strings.Contains(s, "-race-") || strings.HasSuffix(s, "-race")
|
|
}
|
|
|
|
func unsupported(builder string) bool {
|
|
if strings.HasSuffix(builder, "-temp") {
|
|
return true
|
|
}
|
|
return unsupportedOS(builderOS(builder))
|
|
}
|
|
|
|
func unsupportedOS(os string) bool {
|
|
if os == "race" {
|
|
return false
|
|
}
|
|
p, ok := osPriority[os]
|
|
return !ok || p > 0
|
|
}
|
|
|
|
// Priorities for specific operating systems.
|
|
var osPriority = map[string]int{
|
|
"darwin": 0,
|
|
"freebsd": 0,
|
|
"linux": 0,
|
|
"windows": 0,
|
|
// race == 1
|
|
"openbsd": 2,
|
|
"netbsd": 3,
|
|
"dragonfly": 4,
|
|
}
|
|
|
|
// TagState represents the state of all Packages at a Tag.
|
|
type TagState struct {
|
|
Tag *Commit
|
|
Packages []*PackageState
|
|
}
|
|
|
|
// PackageState represents the state of a Package at a Tag.
|
|
type PackageState struct {
|
|
Package *Package
|
|
Commit *Commit
|
|
}
|
|
|
|
// TagStateByName fetches the results for all Go subrepos at the specified Tag.
|
|
func TagStateByName(c appengine.Context, name string) (*TagState, error) {
|
|
tag, err := GetTag(c, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkgs, err := Packages(c, "subrepo")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var st TagState
|
|
for _, pkg := range pkgs {
|
|
com, err := pkg.LastCommit(c)
|
|
if err != nil {
|
|
c.Warningf("%v: no Commit found: %v", pkg, err)
|
|
continue
|
|
}
|
|
st.Packages = append(st.Packages, &PackageState{pkg, com})
|
|
}
|
|
st.Tag, err = tag.Commit(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &st, nil
|
|
}
|
|
|
|
type uiTemplateData struct {
|
|
Dashboard *Dashboard
|
|
Package *Package
|
|
Commits []*Commit
|
|
Builders []string
|
|
TipState *TagState
|
|
Pagination *Pagination
|
|
Branch string
|
|
}
|
|
|
|
var uiTemplate = template.Must(
|
|
template.New("ui.html").Funcs(tmplFuncs).ParseFiles("build/ui.html"),
|
|
)
|
|
|
|
var tmplFuncs = template.FuncMap{
|
|
"buildDashboards": buildDashboards,
|
|
"builderOS": builderOS,
|
|
"builderSpans": builderSpans,
|
|
"builderSubheading": builderSubheading,
|
|
"builderTitle": builderTitle,
|
|
"repoURL": repoURL,
|
|
"shortDesc": shortDesc,
|
|
"shortHash": shortHash,
|
|
"shortUser": shortUser,
|
|
"tail": tail,
|
|
"unsupported": unsupported,
|
|
}
|
|
|
|
func splitDash(s string) (string, string) {
|
|
i := strings.Index(s, "-")
|
|
if i >= 0 {
|
|
return s[:i], s[i+1:]
|
|
}
|
|
return s, ""
|
|
}
|
|
|
|
// builderOS returns the os tag for a builder string
|
|
func builderOS(s string) string {
|
|
os, _ := splitDash(s)
|
|
return os
|
|
}
|
|
|
|
// builderOSOrRace returns the builder OS or, if it is a race builder, "race".
|
|
func builderOSOrRace(s string) string {
|
|
if isRace(s) {
|
|
return "race"
|
|
}
|
|
return builderOS(s)
|
|
}
|
|
|
|
// builderArch returns the arch tag for a builder string
|
|
func builderArch(s string) string {
|
|
_, arch := splitDash(s)
|
|
arch, _ = splitDash(arch) // chop third part
|
|
return arch
|
|
}
|
|
|
|
// builderSubheading returns a short arch tag for a builder string
|
|
// or, if it is a race builder, the builder OS.
|
|
func builderSubheading(s string) string {
|
|
if isRace(s) {
|
|
return builderOS(s)
|
|
}
|
|
arch := builderArch(s)
|
|
switch arch {
|
|
case "amd64":
|
|
return "x64"
|
|
}
|
|
return arch
|
|
}
|
|
|
|
// builderArchChar returns the architecture letter for a builder string
|
|
func builderArchChar(s string) string {
|
|
arch := builderArch(s)
|
|
switch arch {
|
|
case "386":
|
|
return "8"
|
|
case "amd64":
|
|
return "6"
|
|
case "arm":
|
|
return "5"
|
|
}
|
|
return arch
|
|
}
|
|
|
|
type builderSpan struct {
|
|
N int
|
|
OS string
|
|
Unsupported bool
|
|
}
|
|
|
|
// builderSpans creates a list of tags showing
|
|
// the builder's operating system names, spanning
|
|
// the appropriate number of columns.
|
|
func builderSpans(s []string) []builderSpan {
|
|
var sp []builderSpan
|
|
for len(s) > 0 {
|
|
i := 1
|
|
os := builderOSOrRace(s[0])
|
|
u := unsupportedOS(os) || strings.HasSuffix(s[0], "-temp")
|
|
for i < len(s) && builderOSOrRace(s[i]) == os {
|
|
i++
|
|
}
|
|
sp = append(sp, builderSpan{i, os, u})
|
|
s = s[i:]
|
|
}
|
|
return sp
|
|
}
|
|
|
|
// builderTitle formats "linux-amd64-foo" as "linux amd64 foo".
|
|
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 {
|
|
desc = desc[:i]
|
|
}
|
|
return limitStringLength(desc, 100)
|
|
}
|
|
|
|
// shortHash returns a short version of a hash.
|
|
func shortHash(hash string) string {
|
|
if len(hash) > 12 {
|
|
hash = hash[:12]
|
|
}
|
|
return hash
|
|
}
|
|
|
|
// shortUser returns a shortened version of a user string.
|
|
func shortUser(user string) string {
|
|
if i, j := strings.Index(user, "<"), strings.Index(user, ">"); 0 <= i && i < j {
|
|
user = user[i+1 : j]
|
|
}
|
|
if i := strings.Index(user, "@"); i >= 0 {
|
|
return user[:i]
|
|
}
|
|
return user
|
|
}
|
|
|
|
// repoRe matches Google Code repositories and subrepositories (without paths).
|
|
var repoRe = regexp.MustCompile(`^code\.google\.com/p/([a-z0-9\-]+)(\.[a-z0-9\-]+)?$`)
|
|
|
|
// repoURL returns the URL of a change at a Google Code repository or subrepo.
|
|
func repoURL(dashboard, hash, packagePath string) (string, error) {
|
|
if packagePath == "" {
|
|
if dashboard == "Gccgo" {
|
|
return "https://code.google.com/p/gofrontend/source/detail?r=" + hash, nil
|
|
}
|
|
if dashboard == "Mercurial" {
|
|
return "https://golang.org/change/" + hash, nil
|
|
}
|
|
// TODO(adg): use the above once /change/ points to git hashes
|
|
return "https://go.googlesource.com/go/+/" + hash, nil
|
|
}
|
|
|
|
// TODO(adg): remove this old hg stuff, one day.
|
|
if dashboard == "Mercurial" {
|
|
m := repoRe.FindStringSubmatch(packagePath)
|
|
if m == nil {
|
|
return "", errors.New("unrecognized package: " + packagePath)
|
|
}
|
|
url := "https://code.google.com/p/" + m[1] + "/source/detail?r=" + hash
|
|
if len(m) > 2 {
|
|
url += "&repo=" + m[2][1:]
|
|
}
|
|
return url, nil
|
|
}
|
|
|
|
repo := strings.TrimPrefix(packagePath, "golang.org/x/")
|
|
return "https://go.googlesource.com/" + repo + "/+/" + hash, nil
|
|
}
|
|
|
|
// tail returns the trailing n lines of s.
|
|
func tail(n int, s string) string {
|
|
lines := strings.Split(s, "\n")
|
|
if len(lines) < n {
|
|
return s
|
|
}
|
|
return strings.Join(lines[len(lines)-n:], "\n")
|
|
}
|