blob: 0083a3c63cd5ed6fe2c4bffe4abef99b007504e3 [file] [log] [blame]
// 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.
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"cloud.google.com/go/datastore"
"golang.org/x/build/dashboard"
"golang.org/x/build/maintner/maintnerd/apipb"
"golang.org/x/build/repos"
"golang.org/x/build/types"
"golang.org/x/sync/errgroup"
"grpc.go4.org"
"grpc.go4.org/codes"
)
// uiHandler is the HTTP handler for the https://build.golang.org/.
func uiHandler(w http.ResponseWriter, r *http.Request) {
view, err := viewForRequest(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
dashReq, err := dashboardRequest(view, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx := r.Context()
tb := &uiTemplateDataBuilder{
view: view,
req: dashReq,
}
var rpcs errgroup.Group
rpcs.Go(func() error {
var err error
tb.res, err = maintnerClient.GetDashboard(ctx, dashReq)
return err
})
if view.ShowsActiveBuilds() {
rpcs.Go(func() error {
tb.activeBuilds = getActiveBuilds(ctx)
return nil
})
}
if err := rpcs.Wait(); err != nil {
http.Error(w, "maintner.GetDashboard: "+err.Error(), httpStatusOfErr(err))
return
}
data, err := tb.buildTemplateData(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
view.ServeDashboard(w, r, data)
}
// dashboardView is something that can render uiTemplateData.
// See viewForRequest.
type dashboardView interface {
ServeDashboard(w http.ResponseWriter, r *http.Request, data *uiTemplateData)
// ShowsActiveBuilds reports whether this view uses
// information about the currently active builds.
ShowsActiveBuilds() bool
}
// viewForRequest selects the dashboardView based on the HTTP
// request's "mode" parameter. Any error should be considered
// an HTTP 400 Bad Request.
func viewForRequest(r *http.Request) (dashboardView, error) {
if r.Method != "GET" && r.Method != "HEAD" {
return nil, errors.New("unsupported method")
}
switch r.FormValue("mode") {
case "failures":
return failuresView{}, nil
case "json":
return jsonView{}, nil
case "":
return htmlView{}, nil
}
return nil, errors.New("unsupported mode argument")
}
type commitInPackage struct {
packagePath string // "" for Go, else package import path
commit string // git commit hash
}
// uiTemplateDataBuilder builds the uiTemplateData used by the various
// dashboardViews. That is, it maps the maintner protobuf response to
// the data structure needed by the dashboardView/template.
type uiTemplateDataBuilder struct {
view dashboardView
req *apipb.DashboardRequest
res *apipb.DashboardResponse
activeBuilds []types.ActivePostSubmitBuild // optional; for blue gopher links
// testCommitData, if non-nil, provides an alternate data
// source to use for testing instead of making real datastore
// calls. The keys are stringified datastore.Keys.
testCommitData map[string]*Commit
}
// getCommitsToLoad returns a set (all values are true) of which commits to load from
// the datastore.
func (tb *uiTemplateDataBuilder) getCommitsToLoad() map[commitInPackage]bool {
if *fakeResults {
return nil
}
m := make(map[commitInPackage]bool)
add := func(packagePath, commit string) {
m[commitInPackage{packagePath: packagePath, commit: commit}] = true
}
for _, dc := range tb.res.Commits {
add(tb.req.Repo, dc.Commit)
}
// We also want to load the Commits for the x/repo heads.
if tb.showXRepoSection() {
for _, rh := range tb.res.RepoHeads {
if path := repoImportPath(rh); path != "" {
add(path, rh.Commit.Commit)
}
}
}
return m
}
// loadDatastoreCommits loads the commits given in the keys of the
// want map. The returned map is keyed by the git hash and may not
// contain items that didn't exist in the datastore. (It is not an
// error if 1 or all don't exist.)
func (tb *uiTemplateDataBuilder) loadDatastoreCommits(ctx context.Context, want map[commitInPackage]bool) (map[string]*Commit, error) {
ret := map[string]*Commit{}
// Allow tests to fake what the datastore would've loaded, and
// thus also allow tests to be run without a real (or
// dev_appserver-based fake) datastore.
if m := tb.testCommitData; m != nil {
for k := range want {
if c, ok := m[k.commit]; ok {
ret[k.commit] = c
}
}
return ret, nil
}
var keys []*datastore.Key
for k := range want {
key := (&Commit{
PackagePath: k.packagePath,
Hash: k.commit,
}).Key()
keys = append(keys, key)
}
commits, err := fetchCommits(ctx, keys)
if err != nil {
return nil, fmt.Errorf("fetchCommits: %v", err)
}
for _, c := range commits {
ret[c.Hash] = c
}
return ret, nil
}
// formatGitAuthor formats the git author name and email (as split by
// maintner) back into the unified string how they're stored in a git
// commit, so the shortUser func (used by the HTML template) can parse
// back out the email part's username later. Maybe we could plumb down
// the parsed proto into the template later.
func formatGitAuthor(name, email string) string {
name = strings.TrimSpace(name)
email = strings.TrimSpace(email)
if name != "" && email != "" {
return fmt.Sprintf("%s <%s>", name, email)
}
if name != "" {
return name
}
return "<" + email + ">"
}
// newCommitInfo returns a new CommitInfo populated for the template
// data given a repo name and a dashboard commit from that repo, using
// previously loaded datastore commit info in tb.
func (tb *uiTemplateDataBuilder) newCommitInfo(dsCommits map[string]*Commit, repo string, dc *apipb.DashCommit) *CommitInfo {
branch := dc.Branch
if branch == "" {
branch = "master"
}
ci := &CommitInfo{
Hash: dc.Commit,
PackagePath: repo,
User: formatGitAuthor(dc.AuthorName, dc.AuthorEmail),
Desc: cleanTitle(dc.Title, tb.req.Branch),
Time: time.Unix(dc.CommitTimeSec, 0),
Branch: branch,
}
if dsc, ok := dsCommits[dc.Commit]; ok {
ci.ResultData = dsc.ResultData
}
// For non-go repos, add the rows for the Go commits that were
// at HEAD overlapping in time with dc.Commit.
if !tb.isGoRepo() {
if dc.GoCommitAtTime != "" {
ci.addEmptyResultGoHash(dc.GoCommitAtTime)
}
if dc.GoCommitLatest != "" && dc.GoCommitLatest != dc.GoCommitAtTime {
ci.addEmptyResultGoHash(dc.GoCommitLatest)
}
}
return ci
}
// showXRepoSection reports whether the dashboard should show the state of the x/foo repos at the bottom of
// the page in the three branches (master, latest release branch, two releases ago).
func (tb *uiTemplateDataBuilder) showXRepoSection() bool {
return tb.req.Page == 0 &&
(tb.branch() == "master" || tb.req.Branch == "mixed") &&
tb.isGoRepo()
}
func (tb *uiTemplateDataBuilder) isGoRepo() bool { return tb.req.Repo == "" || tb.req.Repo == "go" }
// repoGerritProj returns the Gerrit project name on go.googlesource.com for
// the repo requested, or empty if unknown.
func (tb *uiTemplateDataBuilder) repoGerritProj() string {
if tb.isGoRepo() {
return "go"
}
if r, ok := repos.ByImportPath[tb.req.Repo]; ok {
return r.GoGerritProject
}
return ""
}
// branch returns the request branch, or "master" if empty.
func (tb *uiTemplateDataBuilder) branch() string {
if tb.req.Branch == "" {
return "master"
}
return tb.req.Branch
}
// repoImportPath returns the import path for rh, unless rh is the
// main "go" repo or is configured to be hidden from the dashboard, in
// which case it returns the empty string.
func repoImportPath(rh *apipb.DashRepoHead) string {
if rh.GerritProject == "go" {
return ""
}
ri, ok := repos.ByGerritProject[rh.GerritProject]
if !ok || !ri.ShowOnDashboard() {
return ""
}
return ri.ImportPath
}
func (tb *uiTemplateDataBuilder) buildTemplateData(ctx context.Context) (*uiTemplateData, error) {
dsCommits, err := tb.loadDatastoreCommits(ctx, tb.getCommitsToLoad())
if err != nil {
return nil, err
}
var commits []*CommitInfo
for _, dc := range tb.res.Commits {
ci := tb.newCommitInfo(dsCommits, tb.req.Repo, dc)
commits = append(commits, ci)
}
// x/ repo sections at bottom (each is a "TagState", for historical reasons)
var xRepoSections []*TagState
if tb.showXRepoSection() {
for _, gorel := range tb.res.Releases {
ts := &TagState{
Name: gorel.BranchName,
Tag: &CommitInfo{ // only a minimally populated version is needed by the template
Hash: gorel.BranchCommit,
},
}
for _, rh := range tb.res.RepoHeads {
path := repoImportPath(rh)
if path == "" {
continue
}
ts.Packages = append(ts.Packages, &PackageState{
Package: &Package{
Name: rh.GerritProject,
Path: path,
},
Commit: tb.newCommitInfo(dsCommits, path, rh.Commit),
})
}
builders := map[string]bool{}
for _, pkg := range ts.Packages {
addBuilders(builders, pkg.Package.Name, ts.Branch())
}
ts.Builders = builderKeys(builders)
sort.Slice(ts.Packages, func(i, j int) bool {
return ts.Packages[i].Package.Name < ts.Packages[j].Package.Name
})
xRepoSections = append(xRepoSections, ts)
}
}
// Release Branches
var releaseBranches []string
for _, gr := range tb.res.Releases {
if gr.BranchName != "master" {
releaseBranches = append(releaseBranches, gr.BranchName)
}
}
gerritProject := "go"
if repo := repos.ByImportPath[tb.req.Repo]; repo != nil {
gerritProject = repo.GoGerritProject
}
data := &uiTemplateData{
Dashboard: goDash,
Package: goDash.packageWithPath(tb.req.Repo),
Commits: commits,
TagState: xRepoSections,
Pagination: &Pagination{},
Branches: tb.res.Branches,
Branch: tb.req.Branch,
Repo: gerritProject,
}
builders := buildersOfCommits(commits)
if tb.branch() == "mixed" {
for _, gr := range tb.res.Releases {
addBuilders(builders, tb.repoGerritProj(), gr.BranchName)
}
} else {
addBuilders(builders, tb.repoGerritProj(), tb.branch())
}
data.Builders = builderKeys(builders)
if tb.res.CommitsTruncated {
data.Pagination.Next = int(tb.req.Page) + 1
}
if tb.req.Page > 0 {
data.Pagination.Prev = int(tb.req.Page) - 1
data.Pagination.HasPrev = true
}
if tb.view.ShowsActiveBuilds() {
// Populate building URLs for the HTML UI only.
data.populateBuildingURLs(ctx, tb.activeBuilds)
}
return data, nil
}
// htmlView renders the HTML (default) form of https://build.golang.org/ with no mode parameter.
type htmlView struct{}
func (htmlView) ShowsActiveBuilds() bool { return true }
func (htmlView) ServeDashboard(w http.ResponseWriter, r *http.Request, data *uiTemplateData) {
var buf bytes.Buffer
if err := uiTemplate.Execute(&buf, data); err != nil {
logErr(w, r, err)
return
}
buf.WriteTo(w)
}
// dashboardRequest is a pure function that maps the provided HTTP
// request to a maintner DashboardRequest and lightly validates the
// HTTP request for the root dashboard handler. (It does not validate
// that, say, branches or repos are valid.)
// Any returned error is an HTTP 400 Bad Request.
func dashboardRequest(view dashboardView, r *http.Request) (*apipb.DashboardRequest, error) {
page := 0
if s := r.FormValue("page"); s != "" {
var err error
page, err = strconv.Atoi(r.FormValue("page"))
if err != nil {
return nil, fmt.Errorf("invalid page value %q", s)
}
if page < 0 {
return nil, errors.New("negative page")
}
}
repo := r.FormValue("repo") // empty for main go repo, else e.g. "golang.org/x/net"
branch := r.FormValue("branch")
if branch == "" {
branch = "master"
}
return &apipb.DashboardRequest{
Page: int32(page),
Branch: branch,
Repo: repo,
MaxCommits: commitsPerPage,
}, nil
}
// cleanTitle returns a cleaned version of the provided title for
// users viewing the provided viewBranch.
func cleanTitle(title, viewBranch string) string {
// Don't rewrite anything for master and mixed.
if viewBranch == "master" || viewBranch == "mixed" {
return title
}
// Strip the "[release-branch.go1.n]" prefixes from commit messages
// when looking at a branch.
if strings.HasPrefix(title, "[") {
if i := strings.IndexByte(title, ']'); i != -1 {
return strings.TrimSpace(title[i+1:])
}
}
return title
}
// failuresView renders https://build.golang.org/?mode=failures, where it outputs
// one line per failure on the front page, in the form:
// hash builder failure-url
type failuresView struct{}
func (failuresView) ShowsActiveBuilds() bool { return false }
func (failuresView) ServeDashboard(w http.ResponseWriter, r *http.Request, data *uiTemplateData) {
w.Header().Set("Content-Type", "text/plain")
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/log/%v", r.Host, res.LogHash)
fmt.Fprintln(w, c.Hash, b, url)
}
}
// TODO: this doesn't include the TagState commit. It would be
// needed if we want to do golang.org/issue/36131, to permit
// the retrybuilds command to wipe flaky non-go builds.
}
// jsonView renders https://build.golang.org/?mode=json.
// The output is a types.BuildStatus JSON object.
type jsonView struct{}
func (jsonView) ShowsActiveBuilds() bool { return false }
func (jsonView) ServeDashboard(w http.ResponseWriter, r *http.Request, data *uiTemplateData) {
res := toBuildStatus(r.Host, data)
v, _ := json.MarshalIndent(res, "", "\t")
w.Header().Set("Content-Type", "text/json; charset=utf-8")
w.Write(v)
}
func toBuildStatus(host string, data *uiTemplateData) types.BuildStatus {
// cell returns one of "" (no data), "ok", or a failure URL.
cell := func(res *Result) string {
switch {
case res == nil:
return ""
case res.OK:
return "ok"
}
return fmt.Sprintf("https://%v/log/%v", host, res.LogHash)
}
builders := data.allBuilders()
var res types.BuildStatus
res.Builders = builders
// First the commits from the main section (the requested repo)
for _, c := range data.Commits {
// The logic below works for both the go repo and other subrepos: if c is
// in the main go repo, ResultGoHashes returns a slice of length 1
// containing the empty string.
for _, h := range c.ResultGoHashes() {
rev := types.BuildRevision{
Repo: data.Repo,
Results: make([]string, len(res.Builders)),
GoRevision: h,
}
commitToBuildRevision(c, &rev)
for i, b := range res.Builders {
rev.Results[i] = cell(c.Result(b, h))
}
res.Revisions = append(res.Revisions, rev)
}
}
// Then the one commit each for the subrepos for each of the tracked tags.
// (tip, Go 1.4, etc)
for _, ts := range data.TagState {
for _, pkgState := range ts.Packages {
goRev := ts.Tag.Hash
goBranch := ts.Name
if goBranch == "tip" {
// Normalize old hg terminology into
// our git branch name.
goBranch = "master"
}
rev := types.BuildRevision{
Repo: pkgState.Package.Name,
GoRevision: goRev,
Results: make([]string, len(res.Builders)),
GoBranch: goBranch,
}
commitToBuildRevision(pkgState.Commit, &rev)
for i, b := range res.Builders {
rev.Results[i] = cell(pkgState.Commit.Result(b, goRev))
}
res.Revisions = append(res.Revisions, rev)
}
}
return res
}
// commitToBuildRevision fills in the fields of BuildRevision rev that
// are derived from Commit c.
func commitToBuildRevision(c *CommitInfo, rev *types.BuildRevision) {
rev.Revision = c.Hash
rev.Date = c.Time.Format(time.RFC3339)
rev.Author = c.User
rev.Desc = c.Desc
rev.Branch = c.Branch
}
type Pagination struct {
Next, Prev int
HasPrev bool
}
// fetchCommits loads any commits that exist given by keys.
// It is not an error if a commit doesn't exist.
// Only commits that were found in datastore are returned,
// in an unspecified order.
func fetchCommits(ctx context.Context, keys []*datastore.Key) ([]*Commit, error) {
if len(keys) == 0 {
return nil, nil
}
out := make([]*Commit, len(keys))
for i := range keys {
out[i] = new(Commit)
}
err := datastoreClient.GetMulti(ctx, keys, out)
err = filterDatastoreError(err)
err = filterNoSuchEntity(err)
if err != nil {
return nil, err
}
filtered := out[:0]
for _, c := range out {
if c.Valid() { // that is, successfully loaded
filtered = append(filtered, c)
}
}
return filtered, nil
}
// buildersOfCommits returns the set of builders that provided
// Results for the provided commits.
func buildersOfCommits(commits []*CommitInfo) map[string]bool {
m := make(map[string]bool)
for _, commit := range commits {
for _, r := range commit.Results() {
if r.Builder != "" {
m[r.Builder] = true
}
}
}
return m
}
// addBuilders adds builders to the provide map that should be active for
// the named Gerrit project & branch. (Issue 19930)
func addBuilders(builders map[string]bool, gerritProj, branch string) {
for name, bc := range dashboard.Builders {
if bc.BuildsRepoPostSubmit(gerritProj, branch, branch) {
builders[name] = true
}
}
}
func builderKeys(m map[string]bool) (s []string) {
s = make([]string, 0, len(m))
for k := range m {
s = append(s, k)
}
sort.Sort(builderOrder(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) {
// Group race builders together.
if isRace(builder) {
return 2
}
// 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 {
return unsupportedOS(builderOS(builder))
}
func unsupportedOS(os string) bool {
if os == "race" || os == "android" || os == "all" {
return false
}
p, ok := osPriority[os]
return !ok || p > 1
}
// Priorities for specific operating systems.
var osPriority = map[string]int{
"all": 0,
"darwin": 1,
"freebsd": 1,
"linux": 1,
"windows": 1,
// race == 2
"android": 3,
"openbsd": 4,
"netbsd": 5,
"dragonfly": 6,
}
// TagState represents the state of all Packages at a branch.
type TagState struct {
Name string // Go branch name: "master", "release-branch.go1.4", etc
Tag *CommitInfo // current Go commit on the Name branch
Packages []*PackageState
Builders []string
}
// Branch returns the git branch name, converting from the old
// terminology we used from Go's hg days into git terminology.
func (ts *TagState) Branch() string {
if ts.Name == "tip" {
return "master"
}
return ts.Name
}
// PackageState represents the state of a Package (x/foo repo) for given Go branch.
type PackageState struct {
Package *Package
Commit *CommitInfo
}
// A CommitInfo is a struct for use by html/template package.
// It is not stored in the datastore.
type CommitInfo struct {
Hash string
// ResultData is a copy of the Commit.ResultData field from datastore.
ResultData []string
// BuildingURLs contains the status URL values for builds that
// are currently in progress for this commit.
BuildingURLs map[builderAndGoHash]string
PackagePath string // (empty for main repo commits)
User string // "Foo Bar <foo@bar.com>"
Desc string // git commit title
Time time.Time // commit time
Branch string // "master", "release-branch.go1.14"
}
// addEmptyResultGoHash adds an empty result containing goHash to
// ci.ResultData, unless ci already contains a result for that hash.
// This is used for non-go repos to show the go commits (both earliest
// and latest) that correspond to this repo's commit time. We add an
// empty result so it shows up on the dashboard (both for humans, and
// in JSON form for the coordinator to pick up as work). Once the
// coordinator does that work and posts its result, then ResultData
// will be populate and this turns into a no-op.
func (ci *CommitInfo) addEmptyResultGoHash(goHash string) {
for _, exist := range ci.ResultData {
if strings.Contains(exist, goHash) {
return
}
}
ci.ResultData = append(ci.ResultData, (&Result{GoHash: goHash}).Data())
}
type uiTemplateData struct {
Dashboard *Dashboard
Package *Package
Commits []*CommitInfo
Builders []string // builders for just the main section; not the "TagState" sections
TagState []*TagState // x/foo repo overviews at master + last two releases
Pagination *Pagination
Branches []string
Branch string
Repo string // the repo gerrit project name. "go" if unspecified in the request.
}
// getActiveBuilds returns the builds that coordinator is currently doing.
// This isn't critical functionality so errors are logged but otherwise ignored for now.
// Once this is merged into the coordinator we won't need to make an RPC to get
// this info. See https://github.com/golang/go/issues/34744#issuecomment-563398753.
func getActiveBuilds(ctx context.Context) (builds []types.ActivePostSubmitBuild) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, _ := http.NewRequest("GET", "https://farmer.golang.org/status/post-submit-active.json", nil)
req = req.WithContext(ctx)
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("getActiveBuilds: Do: %v", err)
return
}
defer res.Body.Close()
if res.StatusCode != 200 {
log.Printf("getActiveBuilds: %v", res.Status)
return
}
if err := json.NewDecoder(res.Body).Decode(&builds); err != nil {
log.Printf("getActiveBuilds: JSON decode: %v", err)
}
return builds
}
// populateBuildingURLs populates each commit in Commits' buildingURLs map with the
// URLs of builds which are currently in progress.
func (td *uiTemplateData) populateBuildingURLs(ctx context.Context, activeBuilds []types.ActivePostSubmitBuild) {
// active maps from a build record with its status URL zeroed
// out to to the actual value of that status URL.
active := map[types.ActivePostSubmitBuild]string{}
for _, rec := range activeBuilds {
statusURL := rec.StatusURL
rec.StatusURL = ""
active[rec] = statusURL
}
condAdd := func(c *CommitInfo, rec types.ActivePostSubmitBuild) {
su, ok := active[rec]
if !ok {
return
}
if c.BuildingURLs == nil {
c.BuildingURLs = make(map[builderAndGoHash]string)
}
c.BuildingURLs[builderAndGoHash{rec.Builder, rec.GoCommit}] = su
}
for _, b := range td.Builders {
for _, c := range td.Commits {
condAdd(c, types.ActivePostSubmitBuild{Builder: b, Commit: c.Hash})
}
}
// Gather pending commits for sub-repos.
for _, ts := range td.TagState {
goHash := ts.Tag.Hash
for _, b := range td.Builders {
for _, pkg := range ts.Packages {
c := pkg.Commit
condAdd(c, types.ActivePostSubmitBuild{
Builder: b,
Commit: c.Hash,
GoCommit: goHash,
})
}
}
}
}
// allBuilders returns the list of builders, unified over the main
// section and any x/foo branch overview (TagState) sections.
func (td *uiTemplateData) allBuilders() []string {
m := map[string]bool{}
for _, b := range td.Builders {
m[b] = true
}
for _, ts := range td.TagState {
for _, b := range ts.Builders {
m[b] = true
}
}
return builderKeys(m)
}
var uiTemplate = template.Must(
template.New("ui.html").Funcs(tmplFuncs).ParseFiles(templateFile("ui.html")),
)
var tmplFuncs = template.FuncMap{
"builderSpans": builderSpans,
"builderSubheading": builderSubheading,
"builderSubheading2": builderSubheading2,
"shortDesc": shortDesc,
"shortHash": shortHash,
"shortUser": shortUser,
"unsupported": unsupported,
"isUntested": isUntested,
"knownIssue": knownIssue,
"formatTime": formatTime,
}
func formatTime(t time.Time) string {
if t.Year() != time.Now().Year() {
return t.Format("02 Jan 06")
}
return t.Format("02 Jan 15:04")
}
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)
}
return builderArch(s)
}
// builderSubheading2 returns any third part of a hyphenated builder name.
// For instance, for "linux-amd64-nocgo", it returns "nocgo".
// For race builders it returns the empty string.
func builderSubheading2(s string) string {
if isRace(s) {
return ""
}
_, secondThird := splitDash(s)
_, third := splitDash(secondThird)
return third
}
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)
for i < len(s) && builderOSOrRace(s[i]) == os {
i++
}
sp = append(sp, builderSpan{i, os, u})
s = s[i:]
}
return sp
}
// 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) > 7 {
hash = hash[:7]
}
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
}
// templateFile returns the path to the provided HTML template file,
// conditionally prepending a relative path depending on the
// environment.
func templateFile(base string) string {
// In tests the current directory is ".", but in prod it's up
// two levels. So just look to see if it's in . first.
if _, err := os.Stat(base); err == nil {
return base
}
return filepath.Join("app/appengine", base)
}
func httpStatusOfErr(err error) int {
fmt.Fprintf(os.Stderr, "Got error: %#v, code %v\n", err, grpc.Code(err))
switch grpc.Code(err) {
case codes.NotFound:
return http.StatusNotFound
case codes.InvalidArgument:
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}