-
Notifications
You must be signed in to change notification settings - Fork 156
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
maintner/cmd/maintserve: add maintserve command
maintserve is a program that serves Go issues over HTTP, so they can be viewed in a browser. It uses x/build/maintner/godata as its backing source of data. Note that 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. This is an iteration of an existing command named servegoissues, with import path github.com/bradfitz/go-issue-mirror/cmd/servegoissues. That program served the same purpose, but was located in another repository. It used a "previous generation" approach to having a corpus of GitHub issues data for Go projects, namely the github.com/bradfitz/go-issue-mirror/issues package, which used github.com/bradfitz/issuemirror technology. The intent of maintserve is to replace servegoissues. It uses the golang.org/x/build/maintner/godata package as its source of data, which is built on top of the golang.org/x/build/maintner technology. Currently, maintserve has 2 types of pages: - Index page, which lists repositories. - Issues pages, which display a read-only version of issues. This functionality is implemented in external issuesapp package. By default, maintserve starts as an HTTP server at on port 8080, so you should visit http://localhost:8080/ in a browser after running the command. Note that the first run may take a while, since godata.Get will need to download the entire corprus: The initial call to Get will download approximately 350-400 MB of data into a directory "golang-maintner" under your operating system's user cache directory. Subsequent calls will only download what's changed since the previous call. Helps bradfitz/go-issue-mirror#7. Depends on shurcooL/issues#5. Change-Id: I421147df08c6f664afff0e70abb5d6aa6a42b2d5 Reviewed-on: https://go-review.googlesource.com/52932 Reviewed-by: Brad Fitzpatrick <[email protected]>
- Loading branch information
Showing
2 changed files
with
274 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
[![GoDoc](https://godoc.org/golang.org/x/build/maintner/cmd/maintserve?status.svg)](https://godoc.org/golang.org/x/build/maintner/cmd/maintserve) | ||
|
||
# golang.org/x/build/maintner/cmd/maintserve | ||
|
||
maintserve is a program that serves Go issues 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
// 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 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. | ||
package main | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"fmt" | ||
"html/template" | ||
"log" | ||
"mime" | ||
"net/http" | ||
"net/url" | ||
"sort" | ||
"strings" | ||
"time" | ||
|
||
"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", "application/font-woff"); 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, | ||
}) | ||
|
||
// TODO: Implement background updates for corpus while the appliation is running. | ||
// Right now, it only updates at startup. | ||
// It's likely just a matter of calling RLock/RUnlock before all read operations, | ||
// and launching a background goroutine that occasionally calls corpus.Update() | ||
// or corpus.Sync() or something. | ||
|
||
printServingAt(*httpFlag) | ||
err = http.ListenAndServe(*httpFlag, &handler{ | ||
c: corpus, | ||
fontsHandler: httpgzip.FileServer(gofontwoff.Assets, httpgzip.FileServerOptions{}), | ||
issuesHandler: issuesApp, | ||
}) | ||
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 | ||
} | ||
|
||
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 | ||
} | ||
|
||
// Handle "/owner/repo/..." URLs. | ||
elems := strings.SplitN(req.URL.Path[1:], "/", 3) | ||
if len(elems) < 2 { | ||
http.Error(w, "404 Not Found", http.StatusNotFound) | ||
return | ||
} | ||
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}) | ||
} | ||
|
||
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> | ||
<h3>Repos</h3> | ||
<ul>{{range .}} | ||
<li><a href="/{{.RepoID}}">{{.RepoID}}</a> ({{.Count}} issues)</li> | ||
{{- end}} | ||
</ul> | ||
</div> | ||
<body> | ||
</html>`)) | ||
|
||
// serveIndex serves the index page, which lists all available repositories. | ||
func (h *handler) serveIndex(w http.ResponseWriter, req *http.Request) { | ||
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() | ||
}) | ||
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8") | ||
err = indexHTML.Execute(w, repos) | ||
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 { | ||
return nil | ||
} | ||
issues++ | ||
return nil | ||
}) | ||
return issues, err | ||
} | ||
|
||
// serveIssues serves issues for 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\nrepository %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) | ||
} | ||
|
||
// 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); | ||
}` |