blob: b5ae7ce0d7e7aac6edaf746be3c0f77b6392ad68 [file] [log] [blame]
// Copyright 2018 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 owners
import (
"bytes"
"encoding/json"
"html/template"
"log"
"net/http"
)
type Owner struct {
// GitHubUsername is a GitHub user name or team name.
GitHubUsername string `json:"githubUsername"`
GerritEmail string `json:"gerritEmail"`
}
type Entry struct {
Primary []Owner `json:"primary"`
Secondary []Owner `json:"secondary,omitempty"`
}
type Request struct {
Payload struct {
Paths []string `json:"paths"`
} `json:"payload"`
Version int `json:"v"` // API version
}
type Response struct {
Payload struct {
Entries map[string]*Entry `json:"entries"` // paths in request -> Entry
} `json:"payload"`
Error string `json:"error,omitempty"`
}
// match takes a path consisting of the repo name and full path of a file or
// directory within that repo and returns the deepest Entry match in the file
// hierarchy for the given resource.
func match(path string) *Entry {
var deepestPath string
for p := range entries {
if hasPathPrefix(path, p) && len(p) > len(deepestPath) {
deepestPath = p
}
}
return entries[deepestPath]
}
// hasPathPrefix reports whether the slash-separated path s
// begins with the elements in prefix.
//
// Copied from go/src/cmd/go/internal/str.HasPathPrefix.
func hasPathPrefix(s, prefix string) bool {
if len(s) == len(prefix) {
return s == prefix
}
if prefix == "" {
return true
}
if len(s) > len(prefix) {
if prefix[len(prefix)-1] == '/' || s[len(prefix)] == '/' {
return s[:len(prefix)] == prefix
}
}
return false
}
// Handler takes one or more paths and returns a map of each to a matching
// Entry struct. If no Entry is matched for the path, the value for the key
// is nil.
func Handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case "GET":
serveIndex(w, r)
return
case "POST":
var req Request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, "unable to decode request", http.StatusBadRequest)
// TODO: increment expvar for monitoring.
log.Printf("unable to decode owners request: %v", err)
return
}
var resp Response
resp.Payload.Entries = make(map[string]*Entry)
for _, p := range req.Payload.Paths {
resp.Payload.Entries[p] = match(p)
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(resp); err != nil {
jsonError(w, "unable to encode response", http.StatusInternalServerError)
// TODO: increment expvar for monitoring.
log.Printf("unable to encode owners response: %v", err)
return
}
w.Write(buf.Bytes())
case "OPTIONS":
// Likely a CORS preflight request; leave resp.Payload empty.
default:
jsonError(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
}
func jsonError(w http.ResponseWriter, text string, code int) {
w.WriteHeader(code)
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(Response{Error: text}); err != nil {
// TODO: increment expvar for monitoring.
log.Printf("unable to encode error response: %v", err)
return
}
w.Write(buf.Bytes())
}
func serveIndex(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
var buf bytes.Buffer
if err := indexTmpl.Execute(&buf, entries); err != nil {
log.Printf("unable to execute index template: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Write(buf.Bytes())
}
var indexTmpl = template.Must(template.New("index").Parse(`<!DOCTYPE html>
<html lang="en">
<title>Go Code Owners</title>
<meta name=viewport content="width=device-width, initial-scale=1">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: sans-serif;
margin: 1rem 1.5rem;
}
.header {
color: #666;
font-size: 90%;
margin-bottom: 1rem;
}
.table-header {
font-weight: bold;
position: sticky;
top: 0;
}
.table-header,
.entry {
background-color: #fff;
border-bottom: 1px solid #ddd;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin: .15rem 0;
padding: .15rem 0;
}
.path,
.primary,
.secondary {
flex-basis: 33.3%;
}
</style>
<header class="header">
Alter these entries at
<a href="https://go.googlesource.com/build/+/master/devapp/owners"
target="_blank" rel="noopener">golang.org/x/build/devapp/owners</a>
</header>
<main>
<div class="table-header">
<span class="path">Path</span>
<span class="primary">Primaries</span>
<span class="secondary">Secondaries</span>
</div>
{{range $path, $entry := .}}
<div class="entry">
<span class="path">{{$path}}</span>
<span class="primary">
{{range .Primary}}
<a href="https://github.com/{{.GitHubUsername}}" target="_blank" rel="noopener">@{{.GitHubUsername}}</a>
{{end}}
</span>
<span class="secondary">
{{range .Secondary}}
<a href="https://github.com/{{.GitHubUsername}}" target="_blank" rel="noopener">@{{.GitHubUsername}}</a>
{{end}}
</span>
</div>
{{end}}
</main>
`))