|  | // 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. | 
|  |  | 
|  | // maintserve is a program that serves Go issues and CLs over HTTP, | 
|  | // so they can be viewed in a browser. It uses x/build/maintner/godata | 
|  | // as its backing source of data. | 
|  | // | 
|  | // It statically embeds all the resources it uses, so it's possible to use | 
|  | // it when offline. During that time, the corpus will not be able to update, | 
|  | // and GitHub user profile pictures won't load. | 
|  | // | 
|  | // maintserve displays partial Gerrit CL data that is available within the | 
|  | // maintner corpus. Code diffs and inline review comments are not included. | 
|  | package main | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "flag" | 
|  | "fmt" | 
|  | "html/template" | 
|  | "log" | 
|  | "mime" | 
|  | "net/http" | 
|  | "net/url" | 
|  | "sort" | 
|  | "strings" | 
|  | "time" | 
|  |  | 
|  | "dmitri.shuralyov.com/app/changes" | 
|  | maintnerchange "dmitri.shuralyov.com/service/change/maintner" | 
|  | "github.com/shurcooL/gofontwoff" | 
|  | "github.com/shurcooL/httpgzip" | 
|  | "github.com/shurcooL/issues" | 
|  | maintnerissues "github.com/shurcooL/issues/maintner" | 
|  | "github.com/shurcooL/issuesapp" | 
|  | "golang.org/x/build/maintner" | 
|  | "golang.org/x/build/maintner/godata" | 
|  | ) | 
|  |  | 
|  | var httpFlag = flag.String("http", ":8080", "Listen for HTTP connections on this address.") | 
|  |  | 
|  | func main() { | 
|  | flag.Parse() | 
|  |  | 
|  | err := run() | 
|  | if err != nil { | 
|  | log.Fatalln(err) | 
|  | } | 
|  | } | 
|  |  | 
|  | func run() error { | 
|  | if err := mime.AddExtensionType(".woff2", "font/woff2"); err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | corpus, err := godata.Get(context.Background()) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | issuesService := maintnerissues.NewService(corpus) | 
|  | issuesApp := issuesapp.New(issuesService, nil, issuesapp.Options{ | 
|  | HeadPre: `<meta name="viewport" content="width=device-width"> | 
|  | <link href="/assets/fonts/fonts.css" rel="stylesheet" type="text/css"> | 
|  | <link href="/assets/style.css" rel="stylesheet" type="text/css">`, | 
|  | HeadPost: `<style type="text/css"> | 
|  | .markdown-body { font-family: Go; } | 
|  | tt, code, pre  { font-family: "Go Mono"; } | 
|  | </style>`, | 
|  | BodyPre: `<div style="max-width: 800px; margin: 0 auto 100px auto;"> | 
|  |  | 
|  | {{/* Override new comment component to link to original issue for leaving comments. */}} | 
|  | {{define "new-comment"}}<div class="event" style="margin-top: 20px; margin-bottom: 100px;"> | 
|  | View <a href="https://github.com/{{.RepoSpec}}/issues/{{.Issue.ID}}#new_comment_field">original issue</a> to comment. | 
|  | </div>{{end}}`, | 
|  | DisableReactions: true, | 
|  | }) | 
|  |  | 
|  | changeService := maintnerchange.NewService(corpus) | 
|  | changesApp := changes.New(changeService, nil, changes.Options{ | 
|  | HeadPre: `<meta name="viewport" content="width=device-width"> | 
|  | <link href="/assets/fonts/fonts.css" rel="stylesheet" type="text/css"> | 
|  | <link href="/assets/style.css" rel="stylesheet" type="text/css">`, | 
|  | HeadPost: `<style type="text/css"> | 
|  | .markdown-body { font-family: Go; } | 
|  | tt, code, pre  { font-family: "Go Mono"; } | 
|  | </style>`, | 
|  | BodyPre:          `<div style="max-width: 800px; margin: 0 auto 100px auto;">`, | 
|  | DisableReactions: true, | 
|  | }) | 
|  |  | 
|  | // TODO: Implement background updates for corpus while the application is running. | 
|  | //       Right now, it only updates at startup. | 
|  | //       See gido source code for an example of how to do this. | 
|  |  | 
|  | printServingAt(*httpFlag) | 
|  | err = http.ListenAndServe(*httpFlag, &handler{ | 
|  | c:              corpus, | 
|  | fontsHandler:   httpgzip.FileServer(gofontwoff.Assets, httpgzip.FileServerOptions{}), | 
|  | issuesHandler:  issuesApp, | 
|  | changesHandler: changesApp, | 
|  | }) | 
|  | return err | 
|  | } | 
|  |  | 
|  | func printServingAt(addr string) { | 
|  | hostPort := addr | 
|  | if strings.HasPrefix(hostPort, ":") { | 
|  | hostPort = "localhost" + hostPort | 
|  | } | 
|  | fmt.Printf("serving at http://%s/\n", hostPort) | 
|  | } | 
|  |  | 
|  | // handler handles all requests to maintserve. It acts like a request multiplexer, | 
|  | // choosing from various endpoints and parsing the repository ID from URL. | 
|  | type handler struct { | 
|  | c              *maintner.Corpus | 
|  | fontsHandler   http.Handler | 
|  | issuesHandler  http.Handler | 
|  | changesHandler http.Handler | 
|  | } | 
|  |  | 
|  | func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | 
|  | // Handle "/". | 
|  | if req.URL.Path == "/" { | 
|  | h.serveIndex(w, req) | 
|  | return | 
|  | } | 
|  |  | 
|  | // Handle "/assets/fonts/...". | 
|  | if strings.HasPrefix(req.URL.Path, "/assets/fonts") { | 
|  | req = stripPrefix(req, len("/assets/fonts")) | 
|  | h.fontsHandler.ServeHTTP(w, req) | 
|  | return | 
|  | } | 
|  |  | 
|  | // Handle "/assets/style.css". | 
|  | if req.URL.Path == "/assets/style.css" { | 
|  | http.ServeContent(w, req, "style.css", time.Time{}, strings.NewReader(styleCSS)) | 
|  | return | 
|  | } | 
|  |  | 
|  | elems := strings.SplitN(req.URL.Path[1:], "/", 3) | 
|  | if len(elems) < 2 { | 
|  | http.Error(w, "404 Not Found", http.StatusNotFound) | 
|  | return | 
|  | } | 
|  | switch strings.HasSuffix(elems[0], ".googlesource.com") { | 
|  | case false: | 
|  | // Handle "/owner/repo/..." GitHub repository URLs. | 
|  | owner, repo := elems[0], elems[1] | 
|  | baseURLLen := 1 + len(owner) + 1 + len(repo) // Base URL is "/owner/repo". | 
|  | if baseURL := req.URL.Path[:baseURLLen]; req.URL.Path == baseURL+"/" { | 
|  | // Redirect "/owner/repo/" to "/owner/repo". | 
|  | if req.URL.RawQuery != "" { | 
|  | baseURL += "?" + req.URL.RawQuery | 
|  | } | 
|  | http.Redirect(w, req, baseURL, http.StatusFound) | 
|  | return | 
|  | } | 
|  | req = stripPrefix(req, baseURLLen) | 
|  | h.serveIssues(w, req, maintner.GitHubRepoID{Owner: owner, Repo: repo}) | 
|  |  | 
|  | case true: | 
|  | // Handle "/server/project/..." Gerrit project URLs. | 
|  | server, project := elems[0], elems[1] | 
|  | baseURLLen := 1 + len(server) + 1 + len(project) // Base URL is "/server/project". | 
|  | if baseURL := req.URL.Path[:baseURLLen]; req.URL.Path == baseURL+"/" { | 
|  | // Redirect "/server/project/" to "/server/project". | 
|  | if req.URL.RawQuery != "" { | 
|  | baseURL += "?" + req.URL.RawQuery | 
|  | } | 
|  | http.Redirect(w, req, baseURL, http.StatusFound) | 
|  | return | 
|  | } | 
|  | req = stripPrefix(req, baseURLLen) | 
|  | h.serveChanges(w, req, server, project) | 
|  | } | 
|  | } | 
|  |  | 
|  | var indexHTML = template.Must(template.New("").Parse(`<html> | 
|  | <head> | 
|  | <title>maintserve</title> | 
|  | <meta name="viewport" content="width=device-width"> | 
|  | <link href="/assets/fonts/fonts.css" rel="stylesheet" type="text/css"> | 
|  | <link href="/assets/style.css" rel="stylesheet" type="text/css"> | 
|  | </head> | 
|  | <body> | 
|  | <div style="max-width: 800px; margin: 0 auto 100px auto;"> | 
|  | <h2>maintserve</h2> | 
|  | <div style="display: inline-block; width: 50%;"> | 
|  | <h3>GitHub Repos</h3> | 
|  | <ul>{{range .Repos}} | 
|  | <li><a href="/{{.RepoID}}">{{.RepoID}}</a> ({{.Count}} issues)</li> | 
|  | {{- end}} | 
|  | </ul> | 
|  | </div><div style="display: inline-block; width: 50%; vertical-align: top;"> | 
|  | <h3>Gerrit Projects</h3> | 
|  | <ul>{{range .Projects}} | 
|  | <li><a href="/{{.ServProj}}">{{.ServProj}}</a> ({{.Count}} changes)</li> | 
|  | {{- end}} | 
|  | </ul> | 
|  | </div> | 
|  | </div> | 
|  | <body> | 
|  | </html>`)) | 
|  |  | 
|  | // serveIndex serves the index page, which lists all available | 
|  | // GitHub repositories and Gerrit projects. | 
|  | func (h *handler) serveIndex(w http.ResponseWriter, req *http.Request) { | 
|  | // Enumerate all GitHub repositories. | 
|  | type repo struct { | 
|  | RepoID maintner.GitHubRepoID | 
|  | Count  uint64 // Issues count. | 
|  | } | 
|  | var repos []repo | 
|  | err := h.c.GitHub().ForeachRepo(func(r *maintner.GitHubRepo) error { | 
|  | issues, err := countIssues(r) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | repos = append(repos, repo{ | 
|  | RepoID: r.ID(), | 
|  | Count:  issues, | 
|  | }) | 
|  | return nil | 
|  | }) | 
|  | if err != nil { | 
|  | http.Error(w, err.Error(), http.StatusInternalServerError) | 
|  | return | 
|  | } | 
|  | sort.Slice(repos, func(i, j int) bool { | 
|  | return repos[i].RepoID.String() < repos[j].RepoID.String() | 
|  | }) | 
|  |  | 
|  | // Enumerate all Gerrit projects. | 
|  | type project struct { | 
|  | ServProj string | 
|  | Count    uint64 // Changes count. | 
|  | } | 
|  | var projects []project | 
|  | err = h.c.Gerrit().ForeachProjectUnsorted(func(r *maintner.GerritProject) error { | 
|  | changes, err := countChanges(r) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | projects = append(projects, project{ | 
|  | ServProj: r.ServerSlashProject(), | 
|  | Count:    changes, | 
|  | }) | 
|  | return nil | 
|  | }) | 
|  | if err != nil { | 
|  | http.Error(w, err.Error(), http.StatusInternalServerError) | 
|  | return | 
|  | } | 
|  | sort.Slice(projects, func(i, j int) bool { | 
|  | return projects[i].ServProj < projects[j].ServProj | 
|  | }) | 
|  |  | 
|  | w.Header().Set("Content-Type", "text/html; charset=utf-8") | 
|  | err = indexHTML.Execute(w, map[string]interface{}{ | 
|  | "Repos":    repos, | 
|  | "Projects": projects, | 
|  | }) | 
|  | if err != nil { | 
|  | log.Println(err) | 
|  | } | 
|  | } | 
|  |  | 
|  | // countIssues reports the number of issues in a GitHubRepo r. | 
|  | func countIssues(r *maintner.GitHubRepo) (uint64, error) { | 
|  | var issues uint64 | 
|  | err := r.ForeachIssue(func(i *maintner.GitHubIssue) error { | 
|  | if i.NotExist || i.PullRequest { | 
|  | return nil | 
|  | } | 
|  | issues++ | 
|  | return nil | 
|  | }) | 
|  | return issues, err | 
|  | } | 
|  |  | 
|  | // countChanges reports the number of changes in a GerritProject p. | 
|  | func countChanges(p *maintner.GerritProject) (uint64, error) { | 
|  | var changes uint64 | 
|  | err := p.ForeachCLUnsorted(func(cl *maintner.GerritCL) error { | 
|  | if cl.Private { | 
|  | return nil | 
|  | } | 
|  | changes++ | 
|  | return nil | 
|  | }) | 
|  | return changes, err | 
|  | } | 
|  |  | 
|  | // serveIssues serves issues for GitHub repository id. | 
|  | func (h *handler) serveIssues(w http.ResponseWriter, req *http.Request, id maintner.GitHubRepoID) { | 
|  | if h.c.GitHub().Repo(id.Owner, id.Repo) == nil { | 
|  | http.Error(w, fmt.Sprintf("404 Not Found\n\nGitHub repository %q not found", id), http.StatusNotFound) | 
|  | return | 
|  | } | 
|  |  | 
|  | req = req.WithContext(context.WithValue(req.Context(), | 
|  | issuesapp.RepoSpecContextKey, issues.RepoSpec{URI: fmt.Sprintf("%s/%s", id.Owner, id.Repo)})) | 
|  | req = req.WithContext(context.WithValue(req.Context(), | 
|  | issuesapp.BaseURIContextKey, fmt.Sprintf("/%s/%s", id.Owner, id.Repo))) | 
|  | h.issuesHandler.ServeHTTP(w, req) | 
|  | } | 
|  |  | 
|  | // serveChanges serves changes for Gerrit project server/project. | 
|  | func (h *handler) serveChanges(w http.ResponseWriter, req *http.Request, server, project string) { | 
|  | if h.c.Gerrit().Project(server, project) == nil { | 
|  | http.Error(w, fmt.Sprintf("404 Not Found\n\nGerrit project %s/%s not found", server, project), http.StatusNotFound) | 
|  | return | 
|  | } | 
|  |  | 
|  | req = req.WithContext(context.WithValue(req.Context(), | 
|  | changes.RepoSpecContextKey, fmt.Sprintf("%s/%s", server, project))) | 
|  | req = req.WithContext(context.WithValue(req.Context(), | 
|  | changes.BaseURIContextKey, fmt.Sprintf("/%s/%s", server, project))) | 
|  | h.changesHandler.ServeHTTP(w, req) | 
|  | } | 
|  |  | 
|  | // stripPrefix returns request r with prefix of length prefixLen stripped from r.URL.Path. | 
|  | // prefixLen must not be longer than len(r.URL.Path), otherwise stripPrefix panics. | 
|  | // If r.URL.Path is empty after the prefix is stripped, the path is changed to "/". | 
|  | func stripPrefix(r *http.Request, prefixLen int) *http.Request { | 
|  | r2 := new(http.Request) | 
|  | *r2 = *r | 
|  | r2.URL = new(url.URL) | 
|  | *r2.URL = *r.URL | 
|  | r2.URL.Path = r.URL.Path[prefixLen:] | 
|  | if r2.URL.Path == "" { | 
|  | r2.URL.Path = "/" | 
|  | } | 
|  | return r2 | 
|  | } | 
|  |  | 
|  | const styleCSS = `body { | 
|  | margin: 20px; | 
|  | font-family: Go; | 
|  | font-size: 14px; | 
|  | line-height: initial; | 
|  | color: #373a3c; | 
|  | } | 
|  | a { | 
|  | color: #0275d8; | 
|  | text-decoration: none; | 
|  | } | 
|  | a:focus, a:hover { | 
|  | color: #014c8c; | 
|  | text-decoration: underline; | 
|  | } | 
|  | .btn { | 
|  | font-family: inherit; | 
|  | font-size: 11px; | 
|  | line-height: 11px; | 
|  | height: 18px; | 
|  | border-radius: 4px; | 
|  | border: solid #d2d2d2 1px; | 
|  | background-color: #fff; | 
|  | box-shadow: 0 1px 1px rgba(0, 0, 0, .05); | 
|  | }` |