зеркало из https://github.com/golang/build.git
269 строки
7.6 KiB
Go
269 строки
7.6 KiB
Go
// Copyright 2017 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 (
|
||
"compress/gzip"
|
||
"context"
|
||
"fmt"
|
||
"html/template"
|
||
"log"
|
||
"math/rand"
|
||
"net/http"
|
||
"path"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"golang.org/x/build/devapp/owners"
|
||
"golang.org/x/build/maintner"
|
||
"golang.org/x/build/maintner/godata"
|
||
)
|
||
|
||
// A server is an http.Handler that serves content within staticDir at root and
|
||
// the dynamically-generated dashboards at their respective endpoints.
|
||
type server struct {
|
||
mux *http.ServeMux
|
||
staticDir string
|
||
templateDir string
|
||
reloadTmpls bool
|
||
|
||
cMu sync.RWMutex // Used to protect the fields below.
|
||
corpus *maintner.Corpus
|
||
repo *maintner.GitHubRepo // The golang/go repo.
|
||
proj *maintner.GerritProject // The go.googlesource.com/go project.
|
||
helpWantedIssues []issueData
|
||
data pageData
|
||
|
||
// GopherCon-specific fields. Must still hold cMu when reading/writing these.
|
||
userMapping map[int]*maintner.GitHubUser // Gerrit Owner ID => GitHub user
|
||
activities []activity // All contribution activities
|
||
totalPoints int
|
||
}
|
||
|
||
type issueData struct {
|
||
id int32
|
||
titlePrefix string
|
||
}
|
||
|
||
type pageData struct {
|
||
release releaseData
|
||
reviews reviewsData
|
||
stats statsData
|
||
}
|
||
|
||
func newServer(mux *http.ServeMux, staticDir, templateDir string, reloadTmpls bool) *server {
|
||
s := &server{
|
||
mux: mux,
|
||
staticDir: staticDir,
|
||
templateDir: templateDir,
|
||
reloadTmpls: reloadTmpls,
|
||
userMapping: map[int]*maintner.GitHubUser{},
|
||
}
|
||
s.mux.Handle("/", http.FileServer(http.Dir(s.staticDir)))
|
||
s.mux.HandleFunc("/favicon.ico", s.handleFavicon)
|
||
s.mux.HandleFunc("/release", s.withTemplate("/release.tmpl", s.handleRelease))
|
||
s.mux.HandleFunc("/reviews", s.withTemplate("/reviews.tmpl", s.handleReviews))
|
||
s.mux.HandleFunc("/stats", s.withTemplate("/stats.tmpl", s.handleStats))
|
||
s.mux.HandleFunc("/dir/", handleDirRedirect)
|
||
s.mux.HandleFunc("/owners", owners.Handler)
|
||
s.mux.Handle("/owners/", http.RedirectHandler("/owners", http.StatusPermanentRedirect)) // TODO: remove after clients updated to use URL without trailing slash
|
||
for _, p := range []string{"/imfeelinghelpful", "/imfeelinglucky"} {
|
||
s.mux.HandleFunc(p, s.handleRandomHelpWantedIssue)
|
||
}
|
||
s.mux.HandleFunc("/_/activities", s.handleActivities)
|
||
return s
|
||
}
|
||
|
||
func (s *server) withTemplate(tmpl string, fn func(*template.Template, http.ResponseWriter, *http.Request)) http.HandlerFunc {
|
||
t := template.Must(template.ParseFiles(path.Join(s.templateDir, tmpl)))
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
if s.reloadTmpls {
|
||
t = template.Must(template.ParseFiles(path.Join(s.templateDir, tmpl)))
|
||
}
|
||
fn(t, w, r)
|
||
}
|
||
}
|
||
|
||
// initCorpus fetches a full maintner corpus, overwriting any existing data.
|
||
func (s *server) initCorpus(ctx context.Context) error {
|
||
s.cMu.Lock()
|
||
defer s.cMu.Unlock()
|
||
corpus, err := godata.Get(ctx)
|
||
if err != nil {
|
||
return fmt.Errorf("godata.Get: %v", err)
|
||
}
|
||
s.corpus = corpus
|
||
s.repo = s.corpus.GitHub().Repo("golang", "go")
|
||
if s.repo == nil {
|
||
return fmt.Errorf(`s.corpus.GitHub().Repo("golang", "go") = nil`)
|
||
}
|
||
s.proj = s.corpus.Gerrit().Project("go.googlesource.com", "go")
|
||
if s.proj == nil {
|
||
return fmt.Errorf(`s.corpus.Gerrit().Project("go.googlesource.com", "go") = nil`)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// corpusUpdateLoop continuously updates the server’s corpus until ctx’s Done
|
||
// channel is closed.
|
||
func (s *server) corpusUpdateLoop(ctx context.Context) {
|
||
log.Println("Starting corpus update loop ...")
|
||
for {
|
||
log.Println("Updating help wanted issues ...")
|
||
s.updateHelpWantedIssues()
|
||
log.Println("Updating activities ...")
|
||
s.updateActivities()
|
||
s.cMu.Lock()
|
||
s.data.release.dirty = true
|
||
s.data.reviews.dirty = true
|
||
s.data.stats.dirty = true
|
||
s.cMu.Unlock()
|
||
err := s.corpus.UpdateWithLocker(ctx, &s.cMu)
|
||
if err != nil {
|
||
if err == maintner.ErrSplit {
|
||
log.Println("Corpus out of sync. Re-fetching corpus.")
|
||
s.initCorpus(ctx)
|
||
} else {
|
||
log.Printf("corpus.Update: %v; sleeping 15s", err)
|
||
time.Sleep(15 * time.Second)
|
||
continue
|
||
}
|
||
}
|
||
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
default:
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
|
||
const (
|
||
issuesURLBase = "https://golang.org/issue/"
|
||
|
||
labelHelpWantedID = 150880243
|
||
)
|
||
|
||
func (s *server) updateHelpWantedIssues() {
|
||
s.cMu.Lock()
|
||
defer s.cMu.Unlock()
|
||
|
||
var issues []issueData
|
||
s.repo.ForeachIssue(func(i *maintner.GitHubIssue) error {
|
||
if i.Closed {
|
||
return nil
|
||
}
|
||
if i.HasLabelID(labelHelpWantedID) {
|
||
prefix := strings.SplitN(i.Title, ":", 2)[0]
|
||
issues = append(issues, issueData{id: i.Number, titlePrefix: prefix})
|
||
}
|
||
return nil
|
||
})
|
||
s.helpWantedIssues = issues
|
||
}
|
||
|
||
func (s *server) handleRandomHelpWantedIssue(w http.ResponseWriter, r *http.Request) {
|
||
s.cMu.RLock()
|
||
defer s.cMu.RUnlock()
|
||
if len(s.helpWantedIssues) == 0 {
|
||
http.Redirect(w, r, issuesURLBase, http.StatusSeeOther)
|
||
return
|
||
}
|
||
pkgs := r.URL.Query().Get("pkg")
|
||
var rid int32
|
||
if pkgs == "" {
|
||
rid = s.helpWantedIssues[rand.Intn(len(s.helpWantedIssues))].id
|
||
} else {
|
||
filtered := s.filteredHelpWantedIssues(strings.Split(pkgs, ",")...)
|
||
if len(filtered) == 0 {
|
||
http.Redirect(w, r, issuesURLBase, http.StatusSeeOther)
|
||
return
|
||
}
|
||
rid = filtered[rand.Intn(len(filtered))].id
|
||
}
|
||
http.Redirect(w, r, issuesURLBase+strconv.Itoa(int(rid)), http.StatusSeeOther)
|
||
}
|
||
|
||
func (s *server) filteredHelpWantedIssues(pkgs ...string) []issueData {
|
||
var issues []issueData
|
||
for _, i := range s.helpWantedIssues {
|
||
for _, p := range pkgs {
|
||
if strings.HasPrefix(i.titlePrefix, p) {
|
||
issues = append(issues, i)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
return issues
|
||
}
|
||
|
||
func (s *server) handleFavicon(w http.ResponseWriter, r *http.Request) {
|
||
// Need to specify content type for consistent tests, without this it's
|
||
// determined from mime.types on the box the test is running on
|
||
w.Header().Set("Content-Type", "image/x-icon")
|
||
http.ServeFile(w, r, path.Join(s.staticDir, "/favicon.ico"))
|
||
}
|
||
|
||
// ServeHTTP satisfies the http.Handler interface.
|
||
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
if r.TLS != nil {
|
||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload")
|
||
}
|
||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||
w.Header().Set("Content-Encoding", "gzip")
|
||
gz := gzip.NewWriter(w)
|
||
defer gz.Close()
|
||
gzw := &gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||
s.mux.ServeHTTP(gzw, r)
|
||
return
|
||
}
|
||
s.mux.ServeHTTP(w, r)
|
||
}
|
||
|
||
// handleDirRedirect accepts requests of the form:
|
||
//
|
||
// /dir/REPO/some/dir/
|
||
//
|
||
// And redirects them to either:
|
||
//
|
||
// https://github.com/golang/REPO/tree/master/some/dir/
|
||
//
|
||
// or:
|
||
//
|
||
// https://go.googlesource.com/REPO/+/master/some/dir/
|
||
//
|
||
// ... depending on the Referer. This is so we can make links
|
||
// in Markdown docs that are clickable on both GitHub and
|
||
// in the go.googlesource.com viewer. If detection fails, we
|
||
// default to GitHub.
|
||
func handleDirRedirect(w http.ResponseWriter, r *http.Request) {
|
||
useGoog := strings.Contains(r.Referer(), "googlesource.com")
|
||
path := r.URL.Path
|
||
if !strings.HasPrefix(path, "/dir/") {
|
||
http.Error(w, "bad mux", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
path = strings.TrimPrefix(path, "/dir/")
|
||
// path is now "REPO/some/dir/"
|
||
var repo string
|
||
slash := strings.IndexByte(path, '/')
|
||
if slash == -1 {
|
||
repo, path = path, ""
|
||
} else {
|
||
repo, path = path[:slash], path[slash+1:]
|
||
}
|
||
path = strings.TrimSuffix(path, "/")
|
||
var target string
|
||
if useGoog {
|
||
target = fmt.Sprintf("https://go.googlesource.com/%s/+/master/%s", repo, path)
|
||
} else {
|
||
target = fmt.Sprintf("https://github.com/golang/%s/tree/master/%s", repo, path)
|
||
}
|
||
http.Redirect(w, r, target, http.StatusFound)
|
||
}
|