blob: 1150d21d4f9abb01e8dc46f1aa082cdd1673e387 [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package frontend
import (
"encoding/hex"
"fmt"
"html/template"
"net/http"
"golang.org/x/net/context"
"github.com/luci/luci-go/common/clock"
"github.com/luci/luci-go/common/errors"
"github.com/luci/luci-go/common/logging"
"github.com/luci/luci-go/common/proto/google"
"github.com/luci/luci-go/server/router"
"github.com/luci/luci-go/server/templates"
"github.com/luci/luci-go/milo/api/resp"
"github.com/luci/luci-go/milo/buildsource"
"github.com/luci/luci-go/milo/common"
"github.com/luci/luci-go/milo/common/model"
"github.com/luci/luci-go/milo/git"
)
// getConsoleDef finds the console definition as defined by any project.
// If the user is not a reader of the project, this will return a 404.
// TODO(hinoka): If the user is not a reader of any of of the builders returned,
// that builder will be removed from list of results.
func getConsoleDef(c context.Context, project, name string) (*common.Console, error) {
cs, err := common.GetConsole(c, project, name)
if err != nil {
return nil, err
}
// TODO(hinoka): Remove builders that the user does not have access to.
return cs, nil
}
func console(c context.Context, project, name string) (*resp.Console, error) {
tStart := clock.Now(c)
def, err := getConsoleDef(c, project, name)
if err != nil {
return nil, err
}
commitInfo, err := git.GetHistory(c, def.RepoURL, def.Ref, 25)
if err != nil {
return nil, err
}
tGitiles := clock.Now(c)
logging.Debugf(c, "Loading commits took %s.", tGitiles.Sub(tStart))
builderNames := make([]string, len(def.Builders))
builders := make([]resp.BuilderRef, len(def.Builders))
for i, b := range def.Builders {
builderNames[i] = b
builders[i].Name = b
// TODO(hinoka): Add Categories and ShortNames back in.
}
commitNames := make([]string, len(commitInfo.Commits))
for i, commit := range commitInfo.Commits {
commitNames[i] = hex.EncodeToString(commit.Hash)
}
rows, err := buildsource.GetConsoleRows(c, project, def, commitNames, builderNames)
tConsole := clock.Now(c)
logging.Debugf(c, "Loading the console took a total of %s.", tConsole.Sub(tGitiles))
if err != nil {
return nil, err
}
ccb := make([]resp.CommitBuild, len(commitInfo.Commits))
for row, commit := range commitInfo.Commits {
ccb[row].Build = make([]*model.BuildSummary, len(builders))
ccb[row].Commit = resp.Commit{
AuthorName: commit.AuthorName,
AuthorEmail: commit.AuthorEmail,
CommitTime: google.TimeFromProto(commit.CommitTime),
Repo: def.RepoURL,
Branch: def.Ref, // TODO(hinoka): Actually this doesn't match, change branch to ref.
Description: commit.Msg,
Revision: resp.NewLink(commitNames[row], def.RepoURL+"/+/"+commitNames[row]),
}
for col, b := range builders {
name := buildsource.BuilderID(b.Name)
if summaries := rows[row].Builds[name]; len(summaries) > 0 {
ccb[row].Build[col] = summaries[0]
}
}
}
return &resp.Console{
Name: def.ID,
Commit: ccb,
BuilderRef: builders,
}, nil
}
// consoleRenderer is a wrapper around Console to provide additional methods.
type consoleRenderer struct {
*resp.Console
}
// Header generates the console header html.
func (c consoleRenderer) Header() template.HTML {
// First, split things into nice rows and find the max depth.
cat := make([][]string, len(c.BuilderRef))
depth := 0
for i, b := range c.BuilderRef {
cat[i] = b.Category
if len(cat[i]) > depth {
depth = len(cat[i])
}
}
result := ""
for row := 0; row < depth; row++ {
result += "<tr><th></th>"
// "" is the first node, " " is an empty node.
current := ""
colspan := 0
for _, br := range cat {
colspan++
var s string
if row >= len(br) {
s = " "
} else {
s = br[row]
}
if s != current || current == " " {
if current != "" || current == " " {
result += fmt.Sprintf(`<th colspan="%d">%s</th>`, colspan, current)
colspan = 0
}
current = s
}
}
if colspan != 0 {
result += fmt.Sprintf(`<th colspan="%d">%s</th>`, colspan, current)
}
result += "</tr>"
}
// Last row: The actual builder shortnames.
result += "<tr><th></th>"
for _, br := range c.BuilderRef {
result += fmt.Sprintf("<th>%s</th>", br.ShortName)
}
result += "</tr>"
return template.HTML(result)
}
func (c consoleRenderer) BuilderLink(bs *model.BuildSummary) (*resp.Link, error) {
_, _, builderName, err := buildsource.BuilderID(bs.BuilderID).Split()
if err != nil {
return nil, err
}
return resp.NewLink(builderName, "/"+bs.BuilderID), nil
}
// ConsoleHandler renders the console page.
func ConsoleHandler(c *router.Context) {
project := c.Params.ByName("project")
if project == "" {
ErrorHandler(c, errors.New("Missing Project", common.CodeParameterError))
return
}
name := c.Params.ByName("name")
result, err := console(c.Context, project, name)
if err != nil {
ErrorHandler(c, err)
return
}
templates.MustRender(c.Context, c.Writer, "pages/console.html", templates.Args{
"Console": consoleRenderer{result},
})
}
// ConsoleMainHandler is a redirect handler that redirects the user to the main
// console for a particular project.
func ConsoleMainHandler(ctx *router.Context) {
w, r, p := ctx.Writer, ctx.Request, ctx.Params
proj := p.ByName("project")
http.Redirect(w, r, fmt.Sprintf("/console/%s/main", proj), http.StatusMovedPermanently)
return
}