dmitri.shuralyov.com/website/gido/...

Initial commit.
dmitshur committed 4 years ago commit 799f57b8fdf59d8e51d29b7ac6e66ddae8433ed0
LICENSE
@@ -0,0 +1,27 @@
Copyright (c) 2018 The Go Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

   * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
   * Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
_data/style.css
@@ -0,0 +1,128 @@
body {
	font-family: Go;
	font-size: 14px;
	color: rgb(35, 35, 35);
}
a {
	color: #4183c4;
	text-decoration: none;
}
a:hover {
	text-decoration: underline;
}
a.black {
	color: black;
}
a.black:hover {
	color: #4183c4;
	text-decoration: none;
}

code {
	font-family: "Go Mono";
}
kbd {
	display: inline-block;
	padding: 3px 5px 2px 5px;
	font-family: "Go Mono";
	font-size: 12px;
	line-height: 10px;
	background-color: #fafbfc;
	border: solid 1px #c6cbd1;
	border-bottom-width: 2px;
	border-bottom-color: #959da5;
	border-radius: 3px;
}

.table {
	font-size: 14px;
	border-collapse: collapse;
	background-color: transparent;
	width: 100%;
	max-width: 100%;
	margin-bottom: 20px;
}
.table th, .table td {
	padding: 8px;
	line-height: 1.42857143;
	vertical-align: top;
}
.table td {
	border-top: 1px solid #dddddd;
}
.table thead th {
	text-align: left;
	vertical-align: bottom;
	border-bottom: 2px solid #dddddd;
}
.table .table {
	background-color: #fff;
}
.table-sm th, .table-sm td {
	padding: 5px;
}

.gray {
	color: #888;
}
.lightgray {
	color: #ddd;
}
.tiny {
	font-size: 12px;
}

div.list-entry {
	margin-top: 12px;
}
div.list-entry-container {
	width: 100%;
	box-sizing: border-box;
}
div.list-entry-border {
	border: 1px solid #ddd;
	border-radius: 4px;
}
div.list-entry-header {
	font-size: 13px;
	background-color: #f8f8f8;
	padding: 10px;
	border-radius: 4px 4px 0 0;
	border-bottom: 1px solid #eee;
}
.hash-selected div.list-entry-border {
	border: 1px solid #8ca2d9;
}
.hash-selected div.list-entry-header {
	background-color: #dbe5ff;
	border-bottom: 1px solid #8ca2d9;
}
div.list-entry-header.tabs {
	padding: 16px 10px 6px 10px;
}
div.list-entry-header.tabs-title {
	padding-bottom: 6px;
	background-color: #fff;
}
div.list-entry-body {
	padding: 10px;
}

div.multilist-entry:not(:nth-child(0n+2)) {
	border: 0px solid #ddd;
	border-top-width: 1px;
}

div.list-entry-header nav a {
	color: #767676;
	text-decoration: none;
}
div.list-entry-header nav a:hover,
div.list-entry-header nav a:active,
div.list-entry-header nav a:focus {
	color: #000;
}
div.list-entry-header nav .selected {
	color: #000;
	font-weight: bold;
}
assets/assets.go
@@ -0,0 +1,24 @@
// +build dev

package assets

import (
	"go/build"
	"log"
	"net/http"

	"github.com/shurcooL/httpfs/union"
)

// Assets contains assets for issuesapp.
var Assets = union.New(map[string]http.FileSystem{
	"/assets": http.Dir(importPathToDir("dmitri.shuralyov.com/website/gido/_data")),
})

func importPathToDir(importPath string) string {
	p, err := build.Import(importPath, "", build.FindOnly)
	if err != nil {
		log.Fatalln(err)
	}
	return p.Dir
}
assets/assets_vfsdata.go
@@ -0,0 +1,193 @@
// Code generated by vfsgen; DO NOT EDIT.

// +build !dev

package assets

import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	pathpkg "path"
	"time"
)

// Assets contains assets for issuesapp.
var Assets = func() http.FileSystem {
	fs := vfsgen۰FS{
		"/": &vfsgen۰DirInfo{
			name:    "/",
			modTime: time.Time{},
		},
		"/assets": &vfsgen۰DirInfo{
			name:    "assets",
			modTime: time.Date(2018, 3, 10, 2, 38, 16, 913855967, time.UTC),
		},
		"/assets/style.css": &vfsgen۰CompressedFileInfo{
			name:             "style.css",
			modTime:          time.Date(2018, 3, 12, 21, 9, 27, 876382302, time.UTC),
			uncompressedSize: 2137,

			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x55\xeb\x6e\xa3\x3a\x10\xfe\x1d\x9e\xc2\x6a\x75\xa4\x56\xa7\x44\x90\x5b\xa9\x79\x80\xfe\x3a\x0f\x61\x6c\x13\xac\x3a\x36\x32\x93\x94\x9c\xd5\xbe\xfb\xca\x17\x6e\x09\x61\x77\x83\x50\x80\x19\x7f\x33\xf3\x7d\x33\x76\xa1\xd9\x15\xfd\x88\x56\xa5\x56\x10\x97\xe4\x24\xe4\x15\xa3\x4f\x9d\x87\x2f\x8d\xf8\x9f\x63\x94\xee\xea\x36\x8f\x56\x54\x4b\x6d\x30\x32\xc7\xe2\x65\xbb\x7f\x43\xfe\x7e\xcd\xa3\x9f\x11\xb1\x10\xc1\xfc\xbc\x4b\xb3\x2d\xdd\xe5\xd1\x0a\x78\x0b\x31\xe3\x54\x1b\x02\x42\x2b\x8c\x94\x56\xdc\xb9\xe3\x4a\x5f\xb8\xb1\x8b\xee\x7c\xce\x8a\x71\x23\x45\x70\x5c\x17\x92\xd0\xaf\x11\xba\x7b\x1f\x99\x06\xa4\x3f\x0f\x1f\x51\xcd\xf8\x5d\xd1\x4f\x9f\x1a\xfd\xa7\x95\x7e\xb2\x2e\x5f\x05\xb3\x0e\x4c\x34\xb5\x24\x57\x8c\x84\xb2\x29\xc5\x85\xd4\x36\xfa\xaa\x26\x8c\x09\x75\xc4\x68\x5b\xb7\x68\x5f\xb7\x68\xe3\xff\xf3\xc7\x98\x13\x3e\x37\xce\xd5\x41\x56\x5c\x1c\x2b\xc0\x28\x4d\xdc\xb7\x82\xd0\xaf\xa3\xd1\x67\xc5\xe2\xae\xa0\x92\x94\x45\x49\xad\x4d\x1b\xc6\x0d\x46\x8d\x96\x82\xa1\xb4\x6e\xd1\x33\x3d\xd0\x82\xa5\xbd\x2d\x2e\x34\x80\x3e\xc5\xdf\x82\x41\x85\x91\x0f\x33\x35\x75\xa8\x1f\xfb\x0f\x46\xf6\x83\xd9\x10\x26\xce\x8d\xab\xc8\x71\xb4\x06\x52\xc8\x81\xa5\x49\x23\x84\x25\x54\x4b\x49\xea\x86\x63\xd4\x3d\xcd\x16\x00\x86\xa8\xa6\x26\x86\x2b\xc8\xa3\x55\x48\x2d\x4d\x92\x7f\xf2\x68\x75\x22\x6d\x7c\xfb\xc5\x1c\x85\x0a\xd9\x62\xb4\x49\x7c\x3e\x21\x1d\xa8\xde\x50\xf7\xe8\x14\xea\x95\xc8\x66\x28\x5d\xef\x36\xd9\xfe\x3d\xdd\x6d\xf3\x68\x75\xe1\x06\x04\x25\x32\x26\x52\x1c\x15\x46\xa0\xeb\x31\xae\x03\x0b\x75\x81\xae\xb1\xa3\xd7\x13\xfd\xcc\xdc\x6f\x92\x05\x27\x0c\x41\xd5\x37\x70\xc0\x94\xbc\x84\x99\x50\xbe\x96\x5b\x25\x9c\x3c\x0f\x43\x0c\xf4\xcf\x75\x44\x59\x0e\xae\x71\x73\x1a\xd1\xe2\xde\xa6\xcc\xec\x3b\x45\x8f\x86\x5c\xc7\x93\x92\x65\x99\x83\x91\x96\xae\x5b\x63\x9f\x8e\x50\xd7\xdb\x2e\xd8\x04\x44\x26\x2e\x6b\x29\x1a\x88\xb9\x02\xe3\xbc\x82\x7a\x9e\xc1\xe0\x36\xf5\x8a\xa9\x56\x40\x84\xf2\x43\x3b\xd5\xbe\xd0\xad\x0d\xe1\xb2\xee\xb9\x9a\xc3\xf0\xc6\x41\xb2\x5b\xb9\xee\xdb\x7a\x37\x9b\x8b\xd5\xd1\xe3\x8c\xcb\xdb\x3e\x9c\xc4\xcc\x5e\xe3\xf9\xef\xa6\xf6\x2e\x98\xbb\x13\x94\xdc\xab\x3e\xca\x94\x73\xb7\x1f\xad\x2b\xd2\x54\x71\xc3\x25\xa7\xc0\x19\xfa\x9b\x5a\x33\x4a\x36\xec\xe3\xf7\x20\x43\xa1\x33\x65\xb1\x82\xef\x6d\x47\x2d\x64\x3a\xc4\x99\x05\xb6\xbd\xd7\x4c\x9a\x2e\x3d\xd4\xad\x63\x07\x75\x0f\xcb\x8b\x63\x10\xe0\xfb\x3d\x40\xf4\x59\x1c\x1e\xaa\xe1\xa7\xe0\x8e\x2e\x7f\xa0\xdd\x48\x14\xba\xf5\x74\x96\x20\x06\x6f\xac\x34\xbc\x60\x05\x55\x4c\x2b\x21\xd9\x4b\xa2\xfe\xdd\xbc\xbe\x8e\xb9\x4e\x1e\xf4\x15\xe8\xba\xdf\xb8\x66\xa7\xa1\xa3\x5c\x91\x0b\x9a\x1c\x8f\xef\x07\x7b\x2d\x9d\x4f\x0b\x40\xfe\xb8\x7b\x5b\x74\x21\x14\xc4\x85\x2f\xfb\x94\x9a\x9e\x9b\x71\x56\x49\x92\x2c\x87\x5e\xf7\xad\x75\xbb\xca\xcf\xce\x77\xd8\x74\x0b\x2d\xdd\xc6\xf1\x2b\x00\x00\xff\xff\xda\x4e\x50\x1d\x59\x08\x00\x00"),
		},
	}
	fs["/"].(*vfsgen۰DirInfo).entries = []os.FileInfo{
		fs["/assets"].(os.FileInfo),
	}
	fs["/assets"].(*vfsgen۰DirInfo).entries = []os.FileInfo{
		fs["/assets/style.css"].(os.FileInfo),
	}

	return fs
}()

type vfsgen۰FS map[string]interface{}

func (fs vfsgen۰FS) Open(path string) (http.File, error) {
	path = pathpkg.Clean("/" + path)
	f, ok := fs[path]
	if !ok {
		return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
	}

	switch f := f.(type) {
	case *vfsgen۰CompressedFileInfo:
		gr, err := gzip.NewReader(bytes.NewReader(f.compressedContent))
		if err != nil {
			// This should never happen because we generate the gzip bytes such that they are always valid.
			panic("unexpected error reading own gzip compressed bytes: " + err.Error())
		}
		return &vfsgen۰CompressedFile{
			vfsgen۰CompressedFileInfo: f,
			gr: gr,
		}, nil
	case *vfsgen۰DirInfo:
		return &vfsgen۰Dir{
			vfsgen۰DirInfo: f,
		}, nil
	default:
		// This should never happen because we generate only the above types.
		panic(fmt.Sprintf("unexpected type %T", f))
	}
}

// vfsgen۰CompressedFileInfo is a static definition of a gzip compressed file.
type vfsgen۰CompressedFileInfo struct {
	name              string
	modTime           time.Time
	compressedContent []byte
	uncompressedSize  int64
}

func (f *vfsgen۰CompressedFileInfo) Readdir(count int) ([]os.FileInfo, error) {
	return nil, fmt.Errorf("cannot Readdir from file %s", f.name)
}
func (f *vfsgen۰CompressedFileInfo) Stat() (os.FileInfo, error) { return f, nil }

func (f *vfsgen۰CompressedFileInfo) GzipBytes() []byte {
	return f.compressedContent
}

func (f *vfsgen۰CompressedFileInfo) Name() string       { return f.name }
func (f *vfsgen۰CompressedFileInfo) Size() int64        { return f.uncompressedSize }
func (f *vfsgen۰CompressedFileInfo) Mode() os.FileMode  { return 0444 }
func (f *vfsgen۰CompressedFileInfo) ModTime() time.Time { return f.modTime }
func (f *vfsgen۰CompressedFileInfo) IsDir() bool        { return false }
func (f *vfsgen۰CompressedFileInfo) Sys() interface{}   { return nil }

// vfsgen۰CompressedFile is an opened compressedFile instance.
type vfsgen۰CompressedFile struct {
	*vfsgen۰CompressedFileInfo
	gr      *gzip.Reader
	grPos   int64 // Actual gr uncompressed position.
	seekPos int64 // Seek uncompressed position.
}

func (f *vfsgen۰CompressedFile) Read(p []byte) (n int, err error) {
	if f.grPos > f.seekPos {
		// Rewind to beginning.
		err = f.gr.Reset(bytes.NewReader(f.compressedContent))
		if err != nil {
			return 0, err
		}
		f.grPos = 0
	}
	if f.grPos < f.seekPos {
		// Fast-forward.
		_, err = io.CopyN(ioutil.Discard, f.gr, f.seekPos-f.grPos)
		if err != nil {
			return 0, err
		}
		f.grPos = f.seekPos
	}
	n, err = f.gr.Read(p)
	f.grPos += int64(n)
	f.seekPos = f.grPos
	return n, err
}
func (f *vfsgen۰CompressedFile) Seek(offset int64, whence int) (int64, error) {
	switch whence {
	case io.SeekStart:
		f.seekPos = 0 + offset
	case io.SeekCurrent:
		f.seekPos += offset
	case io.SeekEnd:
		f.seekPos = f.uncompressedSize + offset
	default:
		panic(fmt.Errorf("invalid whence value: %v", whence))
	}
	return f.seekPos, nil
}
func (f *vfsgen۰CompressedFile) Close() error {
	return f.gr.Close()
}

// vfsgen۰DirInfo is a static definition of a directory.
type vfsgen۰DirInfo struct {
	name    string
	modTime time.Time
	entries []os.FileInfo
}

func (d *vfsgen۰DirInfo) Read([]byte) (int, error) {
	return 0, fmt.Errorf("cannot Read from directory %s", d.name)
}
func (d *vfsgen۰DirInfo) Close() error               { return nil }
func (d *vfsgen۰DirInfo) Stat() (os.FileInfo, error) { return d, nil }

func (d *vfsgen۰DirInfo) Name() string       { return d.name }
func (d *vfsgen۰DirInfo) Size() int64        { return 0 }
func (d *vfsgen۰DirInfo) Mode() os.FileMode  { return 0755 | os.ModeDir }
func (d *vfsgen۰DirInfo) ModTime() time.Time { return d.modTime }
func (d *vfsgen۰DirInfo) IsDir() bool        { return true }
func (d *vfsgen۰DirInfo) Sys() interface{}   { return nil }

// vfsgen۰Dir is an opened dir instance.
type vfsgen۰Dir struct {
	*vfsgen۰DirInfo
	pos int // Position within entries for Seek and Readdir.
}

func (d *vfsgen۰Dir) Seek(offset int64, whence int) (int64, error) {
	if offset == 0 && whence == io.SeekStart {
		d.pos = 0
		return 0, nil
	}
	return 0, fmt.Errorf("unsupported Seek in directory %s", d.name)
}

func (d *vfsgen۰Dir) Readdir(count int) ([]os.FileInfo, error) {
	if d.pos >= len(d.entries) && count > 0 {
		return nil, io.EOF
	}
	if count <= 0 || count > len(d.entries)-d.pos {
		count = len(d.entries) - d.pos
	}
	e := d.entries[d.pos : d.pos+count]
	d.pos += count
	return e, nil
}
assets/doc.go
@@ -0,0 +1,4 @@
//go:generate vfsgendev -source="dmitri.shuralyov.com/website/gido/assets".Assets

// Package assets contains assets for gido.
package assets
assets/external.go
@@ -0,0 +1,8 @@
package assets

import "github.com/shurcooL/gofontwoff"

var (
	// Fonts contains the Go font family in Web Open Font Format.
	Fonts = gofontwoff.Assets
)
main.go
@@ -0,0 +1,359 @@
// gido is the command that powers the https://goissues.org website.
package main

import (
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"mime"
	"net/http"
	"net/url"
	"os"
	"path"
	"sort"
	"strings"

	"dmitri.shuralyov.com/website/gido/assets"
	"github.com/shurcooL/htmlg"
	"github.com/shurcooL/httperror"
	"github.com/shurcooL/httpgzip"
	"github.com/shurcooL/issues"
	"github.com/shurcooL/issuesapp/component"
	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
)

var (
	httpFlag          = flag.String("http", ":8080", "Listen for HTTP connections on this address.")
	analyticsFileFlag = flag.String("analytics-file", "", "Optional path to file containing analytics HTML to insert at the beginning of <head>.")
)

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
	}

	var analyticsHTML []byte
	if *analyticsFileFlag != "" {
		var err error
		analyticsHTML, err = ioutil.ReadFile(*analyticsFileFlag)
		if err != nil {
			return err
		}
	}

	printServingAt(*httpFlag)
	err := http.ListenAndServe(*httpFlag, top{&errorHandler{handler: (&handler{
		analyticsHTML: analyticsHTML,
		fontsHandler:  httpgzip.FileServer(assets.Fonts, httpgzip.FileServerOptions{ServeError: httpgzip.Detailed}),
		assetsHandler: httpgzip.FileServer(assets.Assets, httpgzip.FileServerOptions{ServeError: httpgzip.Detailed}),
		s:             newService(),
	}).ServeHTTP}})
	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 goissues requests. It acts like a request multiplexer,
// choosing from various endpoints and parsing the import path from URL.
type handler struct {
	analyticsHTML []byte
	fontsHandler  http.Handler
	assetsHandler http.Handler
	s             *service
}

func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) error {
	// Handle "/".
	if req.URL.Path == "/" {
		return h.ServeIndex(w, req)
	}

	// Handle "/assets/fonts/...".
	if req.URL.Path == "/assets/fonts" || strings.HasPrefix(req.URL.Path, "/assets/fonts/") {
		req = stripPrefix(req, len("/assets/fonts"))
		h.fontsHandler.ServeHTTP(w, req)
		return nil
	}

	// Handle (the rest of) "/assets/...".
	if req.URL.Path == "/assets" || strings.HasPrefix(req.URL.Path, "/assets/") {
		h.assetsHandler.ServeHTTP(w, req)
		return nil
	}

	// Handle "/..." URLs.
	if canonicalPath := path.Clean(req.URL.Path); req.URL.Path != canonicalPath {
		// Redirect to canonical path (no trailing slash, etc.).
		if req.URL.RawQuery != "" {
			canonicalPath += "?" + req.URL.RawQuery
		}
		http.Redirect(w, req, canonicalPath, http.StatusFound)
		return nil
	}
	pkg := req.URL.Path[1:]
	return h.ServeIssues(w, req, pkg)
}

const htmlPart1, htmlPart2, htmlPart3 = `<html>
	<head>
`, `		<title>Go Issues</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 style="margin: 0; position: relative;">
		<header style="background-color: hsl(209, 51%, 92%);">
			<div style="max-width: 800px; margin: 0 auto 0 auto; padding: 0 15px 0 15px;">
				<a class="black" href="/"><strong style="padding: 15px 0 15px 0; display: inline-block;">Go Issues</strong></a>
			</div>
		</header>

		<main style="max-width: 800px; margin: 0 auto 0 auto; padding: 0 15px 120px 15px;">`, `</main>

		<footer style="background-color: hsl(209, 51%, 92%); position: absolute; bottom: 0; left: 0; right: 0;">
			<div style="max-width: 800px; margin: 0 auto 0 auto; padding: 0 15px 0 15px; text-align: right;">
				<span style="padding: 15px 0 15px 0; display: inline-block;"><a href="https://dmitri.shuralyov.com/website/gido/...$issues">Website Issues</a></span>
			</div>
		</footer>
	</body>
</html>
`

func (h *handler) ServeIndex(w http.ResponseWriter, req *http.Request) error {
	if req.Method != http.MethodGet {
		return httperror.Method{Allowed: []string{http.MethodGet}}
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, err := io.WriteString(w, htmlPart1)
	if err != nil {
		return err
	}
	_, err = w.Write(h.analyticsHTML)
	if err != nil {
		return err
	}
	_, err = io.WriteString(w, htmlPart2)
	if err != nil {
		return err
	}

	// Write the About section.
	_, err = io.WriteString(w, `<h3 style="margin-top: 30px;">About</h3>

			<p>Go Issues shows issues for Go packages.
			It's just like <a href="https://godoc.org">godoc.org</a>, but for issues.</p>

			<p>To view issues of a Go package with a given import path, navigate to <code>goissues.org/import/path</code>
			using your browser's address bar (<kbd>⌘Cmd</kbd>+<kbd>L</kbd> or <kbd>Ctrl</kbd>+<kbd>L</kbd>).</p>

			<p>Supported import paths include:</p>

			<ul>
			<li><a href="https://golang.org/pkg/#stdlib">Standard library</a> (e.g., <code>io</code>, <code>net/http</code>, etc.),</li>
			<li><a href="https://golang.org/pkg/#subrepo">Sub-repositories</a> (i.e., <code>golang.org/x/...</code>).</li>
			</ul>

			<p>Import paths of 3rd party packages (e.g., <code>github.com/...</code>) are not supported.</p>

			<p>It's a simple website with a narrow scope. Enjoy. ʕ◔ϖ◔ʔ</p>
		`)
	if err != nil {
		return err
	}

	_, err = io.WriteString(w, `<h3 style="margin-top: 30px;">Popular Packages</h3>`)
	if err != nil {
		return err
	}

	// Find some popular packages to display.
	h.s.PackageIssuesMu.RLock()
	pis := h.s.PackageIssues
	h.s.PackageIssuesMu.RUnlock()
	type pkg struct {
		Path string
		Open int
	}
	var popular []pkg
	for _, p := range h.s.Packages {
		popular = append(popular, pkg{
			Path: p,
			Open: len(pis[p].Open),
		})
	}
	sort.SliceStable(popular, func(i, j int) bool { return popular[i].Open > popular[j].Open })
	popular = popular[:15]

	// Render the table.
	_, err = io.WriteString(w, `<table class="table table-sm">
		<thead>
			<tr>
				<th>Path</th>
				<th>Open Issues</th>
			</tr>
		</thead>
		<tbody>`)
	if err != nil {
		return err
	}
	for _, p := range popular {
		err := html.Render(w, htmlg.TR(
			htmlg.TD(htmlg.A(p.Path, "/"+p.Path)),
			htmlg.TD(htmlg.Text(fmt.Sprint(p.Open))),
		))
		if err != nil {
			return err
		}
	}
	_, err = io.WriteString(w, `</tbody></table>`)
	if err != nil {
		return err
	}

	_, err = io.WriteString(w, htmlPart3)
	return err
}

// ServeIssues serves a list of issues for the package with import path pkg.
func (h *handler) ServeIssues(w http.ResponseWriter, req *http.Request, pkg string) error {
	if req.Method != http.MethodGet {
		return httperror.Method{Allowed: []string{http.MethodGet}}
	}
	filter, err := stateFilter(req.URL.Query())
	if err != nil {
		return httperror.BadRequest{Err: err}
	}

	h.s.PackageIssuesMu.RLock()
	pi, ok := h.s.PackageIssues[pkg]
	h.s.PackageIssuesMu.RUnlock()
	if !ok {
		return os.ErrNotExist
	}
	var is []issues.Issue
	switch {
	case filter == issues.StateFilter(issues.OpenState):
		is = pi.Open
	case filter == issues.StateFilter(issues.ClosedState):
		is = pi.Closed
	case filter == issues.AllStates:
		is = append(pi.Open, pi.Closed...) // TODO: Measure if slow, optimize if needed.
		sort.Slice(is, func(i, j int) bool { return is[i].ID > is[j].ID })
	}
	openCount := uint64(len(pi.Open))
	closedCount := uint64(len(pi.Closed))

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, err = io.WriteString(w, htmlPart1)
	if err != nil {
		return err
	}
	_, err = w.Write(h.analyticsHTML)
	if err != nil {
		return err
	}
	_, err = io.WriteString(w, htmlPart2)
	if err != nil {
		return err
	}
	heading := htmlg.NodeComponent{
		Type: html.ElementNode, Data: atom.H2.String(),
		FirstChild: htmlg.Text(pkg),
	}
	title := pkg + ": "
	if pkg == otherPackages {
		title = ""
	}
	newIssue := htmlg.NodeComponent{
		Type: html.ElementNode, Data: atom.Div.String(),
		Attr:       []html.Attribute{{Key: atom.Style.String(), Val: "text-align: right;"}},
		FirstChild: htmlg.A("New Issue", "https://golang.org/issue/new?title="+url.QueryEscape(title)),
	}
	var es []component.IssueEntry
	for _, i := range is {
		es = append(es, component.IssueEntry{Issue: i, BaseURI: "https://golang.org/issue"})
	}
	issues := component.Issues{
		IssuesNav: component.IssuesNav{
			OpenCount:     openCount,
			ClosedCount:   closedCount,
			Path:          req.URL.Path,
			Query:         req.URL.Query(),
			StateQueryKey: stateQueryKey,
		},
		Filter:  filter,
		Entries: es,
	}
	err = htmlg.RenderComponents(w, heading, subheading{pkg}, newIssue, issues)
	if err != nil {
		return err
	}
	_, err = io.WriteString(w, htmlPart3)
	return err
}

type subheading struct{ Pkg string }

func (s subheading) Render() []*html.Node {
	switch s.Pkg {
	case otherPackages:
		return []*html.Node{htmlg.P(htmlg.Text("Issues that don't fit into any existing Go package."))}
	default:
		return nil
	}
}

const (
	// stateQueryKey is name of query key for controlling issue state filter.
	stateQueryKey = "state"
)

// stateFilter parses the issue state filter from query,
// returning an error if the value is unsupported.
func stateFilter(query url.Values) (issues.StateFilter, error) {
	selectedTabName := query.Get(stateQueryKey)
	switch selectedTabName {
	case "":
		return issues.StateFilter(issues.OpenState), nil
	case "closed":
		return issues.StateFilter(issues.ClosedState), nil
	case "all":
		return issues.AllStates, nil
	default:
		return "", fmt.Errorf("unsupported state filter value: %q", selectedTabName)
	}
}

// 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
}
packages.go
@@ -0,0 +1,820 @@
package main

// TODO: I was missing some golang.org/x/... packages in my GOPATH when generating
//       this list, so it's incomplete.

// existingPackages is a set of import paths of Go packages that are known to exist.
// It includes packages in Go standard library and sub-repositories.
//
// The list is generated with:
//
// 	go list std cmd golang.org/x/...
//
var existingPackages = map[string]struct{}{
	"archive/tar":                                             {},
	"archive/zip":                                             {},
	"bufio":                                                   {},
	"bytes":                                                   {},
	"cmd/addr2line":                                           {},
	"cmd/api":                                                 {},
	"cmd/asm":                                                 {},
	"cmd/asm/internal/arch":                                   {},
	"cmd/asm/internal/asm":                                    {},
	"cmd/asm/internal/flags":                                  {},
	"cmd/asm/internal/lex":                                    {},
	"cmd/buildid":                                             {},
	"cmd/cgo":                                                 {},
	"cmd/compile":                                             {},
	"cmd/compile/internal/amd64":                              {},
	"cmd/compile/internal/arm":                                {},
	"cmd/compile/internal/arm64":                              {},
	"cmd/compile/internal/gc":                                 {},
	"cmd/compile/internal/mips":                               {},
	"cmd/compile/internal/mips64":                             {},
	"cmd/compile/internal/ppc64":                              {},
	"cmd/compile/internal/s390x":                              {},
	"cmd/compile/internal/ssa":                                {},
	"cmd/compile/internal/syntax":                             {},
	"cmd/compile/internal/test":                               {},
	"cmd/compile/internal/types":                              {},
	"cmd/compile/internal/x86":                                {},
	"cmd/cover":                                               {},
	"cmd/dist":                                                {},
	"cmd/doc":                                                 {},
	"cmd/fix":                                                 {},
	"cmd/go":                                                  {},
	"cmd/go/internal/base":                                    {},
	"cmd/go/internal/bug":                                     {},
	"cmd/go/internal/cache":                                   {},
	"cmd/go/internal/cfg":                                     {},
	"cmd/go/internal/clean":                                   {},
	"cmd/go/internal/cmdflag":                                 {},
	"cmd/go/internal/doc":                                     {},
	"cmd/go/internal/envcmd":                                  {},
	"cmd/go/internal/fix":                                     {},
	"cmd/go/internal/fmtcmd":                                  {},
	"cmd/go/internal/generate":                                {},
	"cmd/go/internal/get":                                     {},
	"cmd/go/internal/help":                                    {},
	"cmd/go/internal/list":                                    {},
	"cmd/go/internal/load":                                    {},
	"cmd/go/internal/run":                                     {},
	"cmd/go/internal/str":                                     {},
	"cmd/go/internal/test":                                    {},
	"cmd/go/internal/tool":                                    {},
	"cmd/go/internal/version":                                 {},
	"cmd/go/internal/vet":                                     {},
	"cmd/go/internal/web":                                     {},
	"cmd/go/internal/work":                                    {},
	"cmd/gofmt":                                               {},
	"cmd/internal/bio":                                        {},
	"cmd/internal/browser":                                    {},
	"cmd/internal/buildid":                                    {},
	"cmd/internal/dwarf":                                      {},
	"cmd/internal/edit":                                       {},
	"cmd/internal/gcprog":                                     {},
	"cmd/internal/goobj":                                      {},
	"cmd/internal/obj":                                        {},
	"cmd/internal/obj/arm":                                    {},
	"cmd/internal/obj/arm64":                                  {},
	"cmd/internal/obj/mips":                                   {},
	"cmd/internal/obj/ppc64":                                  {},
	"cmd/internal/obj/s390x":                                  {},
	"cmd/internal/obj/x86":                                    {},
	"cmd/internal/objabi":                                     {},
	"cmd/internal/objfile":                                    {},
	"cmd/internal/src":                                        {},
	"cmd/internal/sys":                                        {},
	"cmd/internal/test2json":                                  {},
	"cmd/link":                                                {},
	"cmd/link/internal/amd64":                                 {},
	"cmd/link/internal/arm":                                   {},
	"cmd/link/internal/arm64":                                 {},
	"cmd/link/internal/ld":                                    {},
	"cmd/link/internal/loadelf":                               {},
	"cmd/link/internal/loadmacho":                             {},
	"cmd/link/internal/loadpe":                                {},
	"cmd/link/internal/mips":                                  {},
	"cmd/link/internal/mips64":                                {},
	"cmd/link/internal/objfile":                               {},
	"cmd/link/internal/ppc64":                                 {},
	"cmd/link/internal/s390x":                                 {},
	"cmd/link/internal/sym":                                   {},
	"cmd/link/internal/x86":                                   {},
	"cmd/nm":                                                  {},
	"cmd/objdump":                                             {},
	"cmd/pack":                                                {},
	"cmd/pprof":                                               {},
	"cmd/test2json":                                           {},
	"cmd/trace":                                               {},
	"cmd/vendor/github.com/google/pprof/driver":               {},
	"cmd/vendor/github.com/google/pprof/internal/binutils":    {},
	"cmd/vendor/github.com/google/pprof/internal/driver":      {},
	"cmd/vendor/github.com/google/pprof/internal/elfexec":     {},
	"cmd/vendor/github.com/google/pprof/internal/graph":       {},
	"cmd/vendor/github.com/google/pprof/internal/measurement": {},
	"cmd/vendor/github.com/google/pprof/internal/plugin":      {},
	"cmd/vendor/github.com/google/pprof/internal/proftest":    {},
	"cmd/vendor/github.com/google/pprof/internal/report":      {},
	"cmd/vendor/github.com/google/pprof/internal/symbolizer":  {},
	"cmd/vendor/github.com/google/pprof/internal/symbolz":     {},
	"cmd/vendor/github.com/google/pprof/profile":              {},
	"cmd/vendor/github.com/google/pprof/third_party/svg":      {},
	"cmd/vendor/github.com/ianlancetaylor/demangle":           {},
	"cmd/vendor/golang.org/x/arch/arm/armasm":                 {},
	"cmd/vendor/golang.org/x/arch/arm64/arm64asm":             {},
	"cmd/vendor/golang.org/x/arch/ppc64/ppc64asm":             {},
	"cmd/vendor/golang.org/x/arch/x86/x86asm":                 {},
	"cmd/vet":                           {},
	"cmd/vet/internal/cfg":              {},
	"cmd/vet/internal/whitelist":        {},
	"compress/bzip2":                    {},
	"compress/flate":                    {},
	"compress/gzip":                     {},
	"compress/lzw":                      {},
	"compress/zlib":                     {},
	"container/heap":                    {},
	"container/list":                    {},
	"container/ring":                    {},
	"context":                           {},
	"crypto":                            {},
	"crypto/aes":                        {},
	"crypto/cipher":                     {},
	"crypto/des":                        {},
	"crypto/dsa":                        {},
	"crypto/ecdsa":                      {},
	"crypto/elliptic":                   {},
	"crypto/hmac":                       {},
	"crypto/internal/cipherhw":          {},
	"crypto/md5":                        {},
	"crypto/rand":                       {},
	"crypto/rc4":                        {},
	"crypto/rsa":                        {},
	"crypto/sha1":                       {},
	"crypto/sha256":                     {},
	"crypto/sha512":                     {},
	"crypto/subtle":                     {},
	"crypto/tls":                        {},
	"crypto/x509":                       {},
	"crypto/x509/pkix":                  {},
	"database/sql":                      {},
	"database/sql/driver":               {},
	"debug/dwarf":                       {},
	"debug/elf":                         {},
	"debug/gosym":                       {},
	"debug/macho":                       {},
	"debug/pe":                          {},
	"debug/plan9obj":                    {},
	"encoding":                          {},
	"encoding/ascii85":                  {},
	"encoding/asn1":                     {},
	"encoding/base32":                   {},
	"encoding/base64":                   {},
	"encoding/binary":                   {},
	"encoding/csv":                      {},
	"encoding/gob":                      {},
	"encoding/hex":                      {},
	"encoding/json":                     {},
	"encoding/pem":                      {},
	"encoding/xml":                      {},
	"errors":                            {},
	"expvar":                            {},
	"flag":                              {},
	"fmt":                               {},
	"go/ast":                            {},
	"go/build":                          {},
	"go/constant":                       {},
	"go/doc":                            {},
	"go/format":                         {},
	"go/importer":                       {},
	"go/internal/gccgoimporter":         {},
	"go/internal/gcimporter":            {},
	"go/internal/srcimporter":           {},
	"go/parser":                         {},
	"go/printer":                        {},
	"go/scanner":                        {},
	"go/token":                          {},
	"go/types":                          {},
	"hash":                              {},
	"hash/adler32":                      {},
	"hash/crc32":                        {},
	"hash/crc64":                        {},
	"hash/fnv":                          {},
	"html":                              {},
	"html/template":                     {},
	"image":                             {},
	"image/color":                       {},
	"image/color/palette":               {},
	"image/draw":                        {},
	"image/gif":                         {},
	"image/internal/imageutil":          {},
	"image/jpeg":                        {},
	"image/png":                         {},
	"index/suffixarray":                 {},
	"internal/cpu":                      {},
	"internal/nettrace":                 {},
	"internal/poll":                     {},
	"internal/race":                     {},
	"internal/singleflight":             {},
	"internal/syscall/windows":          {},
	"internal/syscall/windows/registry": {},
	"internal/syscall/windows/sysdll":   {},
	"internal/testenv":                  {},
	"internal/testlog":                  {},
	"internal/trace":                    {},
	"io":                                {},
	"io/ioutil":                         {},
	"log":                               {},
	"log/syslog":                        {},
	"math":                              {},
	"math/big":                          {},
	"math/bits":                         {},
	"math/cmplx":                        {},
	"math/rand":                         {},
	"mime":                              {},
	"mime/multipart":                    {},
	"mime/quotedprintable":              {},
	"net":                            {},
	"net/http":                       {},
	"net/http/cgi":                   {},
	"net/http/cookiejar":             {},
	"net/http/fcgi":                  {},
	"net/http/httptest":              {},
	"net/http/httptrace":             {},
	"net/http/httputil":              {},
	"net/http/internal":              {},
	"net/http/pprof":                 {},
	"net/internal/socktest":          {},
	"net/mail":                       {},
	"net/rpc":                        {},
	"net/rpc/jsonrpc":                {},
	"net/smtp":                       {},
	"net/textproto":                  {},
	"net/url":                        {},
	"os":                             {},
	"os/exec":                        {},
	"os/signal":                      {},
	"os/signal/internal/pty":         {},
	"os/user":                        {},
	"path":                           {},
	"path/filepath":                  {},
	"plugin":                         {},
	"reflect":                        {},
	"regexp":                         {},
	"regexp/syntax":                  {},
	"runtime":                        {},
	"runtime/cgo":                    {},
	"runtime/debug":                  {},
	"runtime/internal/atomic":        {},
	"runtime/internal/sys":           {},
	"runtime/pprof":                  {},
	"runtime/pprof/internal/profile": {},
	"runtime/race":                   {},
	"runtime/trace":                  {},
	"sort":                           {},
	"strconv":                        {},
	"strings":                        {},
	"sync":                           {},
	"sync/atomic":                    {},
	"syscall":                        {},
	"testing":                        {},
	"testing/internal/testdeps":      {},
	"testing/iotest":                 {},
	"testing/quick":                  {},
	"text/scanner":                   {},
	"text/tabwriter":                 {},
	"text/template":                  {},
	"text/template/parse":            {},
	"time":                           {},
	"unicode":                        {},
	"unicode/utf16":                  {},
	"unicode/utf8":                   {},
	"unsafe":                         {},
	"vendor/golang_org/x/crypto/chacha20poly1305":                   {},
	"vendor/golang_org/x/crypto/chacha20poly1305/internal/chacha20": {},
	"vendor/golang_org/x/crypto/cryptobyte":                         {},
	"vendor/golang_org/x/crypto/cryptobyte/asn1":                    {},
	"vendor/golang_org/x/crypto/curve25519":                         {},
	"vendor/golang_org/x/crypto/poly1305":                           {},
	"vendor/golang_org/x/net/http2/hpack":                           {},
	"vendor/golang_org/x/net/idna":                                  {},
	"vendor/golang_org/x/net/internal/nettest":                      {},
	"vendor/golang_org/x/net/lex/httplex":                           {},
	"vendor/golang_org/x/net/nettest":                               {},
	"vendor/golang_org/x/net/proxy":                                 {},
	"vendor/golang_org/x/net/route":                                 {},
	"vendor/golang_org/x/text/secure":                               {},
	"vendor/golang_org/x/text/secure/bidirule":                      {},
	"vendor/golang_org/x/text/transform":                            {},
	"vendor/golang_org/x/text/unicode":                              {},
	"vendor/golang_org/x/text/unicode/bidi":                         {},
	"vendor/golang_org/x/text/unicode/norm":                         {},

	"golang.org/x/build":                                     {},
	"golang.org/x/build/auth":                                {},
	"golang.org/x/build/autocertcache":                       {},
	"golang.org/x/build/buildenv":                            {},
	"golang.org/x/build/buildlet":                            {},
	"golang.org/x/build/cmd/builder":                         {},
	"golang.org/x/build/cmd/buildlet":                        {},
	"golang.org/x/build/cmd/buildlet/stage0":                 {},
	"golang.org/x/build/cmd/buildstats":                      {},
	"golang.org/x/build/cmd/cl":                              {},
	"golang.org/x/build/cmd/coordinator":                     {},
	"golang.org/x/build/cmd/coordinator/buildongce":          {},
	"golang.org/x/build/cmd/coordinator/metrics":             {},
	"golang.org/x/build/cmd/coordinator/spanlog":             {},
	"golang.org/x/build/cmd/debugnewvm":                      {},
	"golang.org/x/build/cmd/docker2boot":                     {},
	"golang.org/x/build/cmd/fetchlogs":                       {},
	"golang.org/x/build/cmd/genbootstrap":                    {},
	"golang.org/x/build/cmd/gerritbot":                       {},
	"golang.org/x/build/cmd/gitlock":                         {},
	"golang.org/x/build/cmd/gitmirror":                       {},
	"golang.org/x/build/cmd/gomote":                          {},
	"golang.org/x/build/cmd/gopherbot":                       {},
	"golang.org/x/build/cmd/gopherstats":                     {},
	"golang.org/x/build/cmd/makemac":                         {},
	"golang.org/x/build/cmd/perfrun":                         {},
	"golang.org/x/build/cmd/pubsubhelper":                    {},
	"golang.org/x/build/cmd/pubsubhelper/pubsubtypes":        {},
	"golang.org/x/build/cmd/pushback":                        {},
	"golang.org/x/build/cmd/racebuild":                       {},
	"golang.org/x/build/cmd/release":                         {},
	"golang.org/x/build/cmd/releasebot":                      {},
	"golang.org/x/build/cmd/relnote":                         {},
	"golang.org/x/build/cmd/retrybuilds":                     {},
	"golang.org/x/build/cmd/rundockerbuildlet":               {},
	"golang.org/x/build/cmd/scaleway":                        {},
	"golang.org/x/build/cmd/upload":                          {},
	"golang.org/x/build/dashboard":                           {},
	"golang.org/x/build/devapp":                              {},
	"golang.org/x/build/envutil":                             {},
	"golang.org/x/build/gerrit":                              {},
	"golang.org/x/build/internal/buildgo":                    {},
	"golang.org/x/build/internal/buildstats":                 {},
	"golang.org/x/build/internal/gitauth":                    {},
	"golang.org/x/build/internal/gophers":                    {},
	"golang.org/x/build/internal/httpdl":                     {},
	"golang.org/x/build/internal/https":                      {},
	"golang.org/x/build/internal/loghash":                    {},
	"golang.org/x/build/internal/lru":                        {},
	"golang.org/x/build/internal/singleflight":               {},
	"golang.org/x/build/internal/sourcecache":                {},
	"golang.org/x/build/internal/untar":                      {},
	"golang.org/x/build/kubernetes":                          {},
	"golang.org/x/build/kubernetes/api":                      {},
	"golang.org/x/build/kubernetes/gke":                      {},
	"golang.org/x/build/livelog":                             {},
	"golang.org/x/build/maintner":                            {},
	"golang.org/x/build/maintner/cmd/maintserve":             {},
	"golang.org/x/build/maintner/godata":                     {},
	"golang.org/x/build/maintner/gostats":                    {},
	"golang.org/x/build/maintner/maintnerd":                  {},
	"golang.org/x/build/maintner/maintnerd/apipb":            {},
	"golang.org/x/build/maintner/maintpb":                    {},
	"golang.org/x/build/maintner/maintq":                     {},
	"golang.org/x/build/maintner/reclog":                     {},
	"golang.org/x/build/pargzip":                             {},
	"golang.org/x/build/revdial":                             {},
	"golang.org/x/build/status":                              {},
	"golang.org/x/build/status/statusserver":                 {},
	"golang.org/x/build/tarutil":                             {},
	"golang.org/x/build/types":                               {},
	"golang.org/x/build/vcs-test/vcweb":                      {},
	"golang.org/x/build/version":                             {},
	"golang.org/x/build/version/go1.10":                      {},
	"golang.org/x/build/version/go1.10beta1":                 {},
	"golang.org/x/build/version/go1.10beta2":                 {},
	"golang.org/x/build/version/go1.10rc1":                   {},
	"golang.org/x/build/version/go1.10rc2":                   {},
	"golang.org/x/build/version/go1.8":                       {},
	"golang.org/x/build/version/go1.8.1":                     {},
	"golang.org/x/build/version/go1.8.2":                     {},
	"golang.org/x/build/version/go1.8.3":                     {},
	"golang.org/x/build/version/go1.8.4":                     {},
	"golang.org/x/build/version/go1.8.5":                     {},
	"golang.org/x/build/version/go1.8.6":                     {},
	"golang.org/x/build/version/go1.8.7":                     {},
	"golang.org/x/build/version/go1.8beta1":                  {},
	"golang.org/x/build/version/go1.8beta2":                  {},
	"golang.org/x/build/version/go1.8rc1":                    {},
	"golang.org/x/build/version/go1.8rc2":                    {},
	"golang.org/x/build/version/go1.8rc3":                    {},
	"golang.org/x/build/version/go1.9":                       {},
	"golang.org/x/build/version/go1.9.1":                     {},
	"golang.org/x/build/version/go1.9.2":                     {},
	"golang.org/x/build/version/go1.9.3":                     {},
	"golang.org/x/build/version/go1.9.4":                     {},
	"golang.org/x/build/version/go1.9beta1":                  {},
	"golang.org/x/build/version/go1.9beta2":                  {},
	"golang.org/x/build/version/go1.9rc1":                    {},
	"golang.org/x/build/version/go1.9rc2":                    {},
	"golang.org/x/build/version/internal/genv":               {},
	"golang.org/x/crypto/acme":                               {},
	"golang.org/x/crypto/acme/autocert":                      {},
	"golang.org/x/crypto/argon2":                             {},
	"golang.org/x/crypto/bcrypt":                             {},
	"golang.org/x/crypto/blake2b":                            {},
	"golang.org/x/crypto/blake2s":                            {},
	"golang.org/x/crypto/blowfish":                           {},
	"golang.org/x/crypto/bn256":                              {},
	"golang.org/x/crypto/cast5":                              {},
	"golang.org/x/crypto/chacha20poly1305":                   {},
	"golang.org/x/crypto/cryptobyte":                         {},
	"golang.org/x/crypto/cryptobyte/asn1":                    {},
	"golang.org/x/crypto/curve25519":                         {},
	"golang.org/x/crypto/ed25519":                            {},
	"golang.org/x/crypto/ed25519/internal/edwards25519":      {},
	"golang.org/x/crypto/hkdf":                               {},
	"golang.org/x/crypto/internal/chacha20":                  {},
	"golang.org/x/crypto/md4":                                {},
	"golang.org/x/crypto/nacl/auth":                          {},
	"golang.org/x/crypto/nacl/box":                           {},
	"golang.org/x/crypto/nacl/secretbox":                     {},
	"golang.org/x/crypto/ocsp":                               {},
	"golang.org/x/crypto/openpgp":                            {},
	"golang.org/x/crypto/openpgp/armor":                      {},
	"golang.org/x/crypto/openpgp/clearsign":                  {},
	"golang.org/x/crypto/openpgp/elgamal":                    {},
	"golang.org/x/crypto/openpgp/errors":                     {},
	"golang.org/x/crypto/openpgp/packet":                     {},
	"golang.org/x/crypto/openpgp/s2k":                        {},
	"golang.org/x/crypto/otr":                                {},
	"golang.org/x/crypto/pbkdf2":                             {},
	"golang.org/x/crypto/pkcs12":                             {},
	"golang.org/x/crypto/pkcs12/internal/rc2":                {},
	"golang.org/x/crypto/poly1305":                           {},
	"golang.org/x/crypto/ripemd160":                          {},
	"golang.org/x/crypto/salsa20":                            {},
	"golang.org/x/crypto/salsa20/salsa":                      {},
	"golang.org/x/crypto/scrypt":                             {},
	"golang.org/x/crypto/sha3":                               {},
	"golang.org/x/crypto/ssh":                                {},
	"golang.org/x/crypto/ssh/agent":                          {},
	"golang.org/x/crypto/ssh/knownhosts":                     {},
	"golang.org/x/crypto/ssh/terminal":                       {},
	"golang.org/x/crypto/ssh/test":                           {},
	"golang.org/x/crypto/tea":                                {},
	"golang.org/x/crypto/twofish":                            {},
	"golang.org/x/crypto/xtea":                               {},
	"golang.org/x/crypto/xts":                                {},
	"golang.org/x/exp/ebnf":                                  {},
	"golang.org/x/exp/ebnflint":                              {},
	"golang.org/x/exp/io/i2c":                                {},
	"golang.org/x/exp/io/i2c/driver":                         {},
	"golang.org/x/exp/io/i2c/example/displayip":              {},
	"golang.org/x/exp/io/spi":                                {},
	"golang.org/x/exp/io/spi/driver":                         {},
	"golang.org/x/exp/mmap":                                  {},
	"golang.org/x/exp/old/netchan":                           {},
	"golang.org/x/exp/rand":                                  {},
	"golang.org/x/exp/shiny/driver":                          {},
	"golang.org/x/exp/shiny/driver/gldriver":                 {},
	"golang.org/x/exp/shiny/driver/internal/drawer":          {},
	"golang.org/x/exp/shiny/driver/internal/errscreen":       {},
	"golang.org/x/exp/shiny/driver/internal/event":           {},
	"golang.org/x/exp/shiny/driver/internal/lifecycler":      {},
	"golang.org/x/exp/shiny/driver/internal/swizzle":         {},
	"golang.org/x/exp/shiny/driver/internal/win32":           {},
	"golang.org/x/exp/shiny/driver/internal/x11key":          {},
	"golang.org/x/exp/shiny/driver/windriver":                {},
	"golang.org/x/exp/shiny/driver/x11driver":                {},
	"golang.org/x/exp/shiny/gesture":                         {},
	"golang.org/x/exp/shiny/iconvg":                          {},
	"golang.org/x/exp/shiny/iconvg/internal/gradient":        {},
	"golang.org/x/exp/shiny/imageutil":                       {},
	"golang.org/x/exp/shiny/materialdesign/colornames":       {},
	"golang.org/x/exp/shiny/materialdesign/icons":            {},
	"golang.org/x/exp/shiny/screen":                          {},
	"golang.org/x/exp/shiny/text":                            {},
	"golang.org/x/exp/shiny/unit":                            {},
	"golang.org/x/exp/shiny/widget":                          {},
	"golang.org/x/exp/shiny/widget/flex":                     {},
	"golang.org/x/exp/shiny/widget/glwidget":                 {},
	"golang.org/x/exp/shiny/widget/node":                     {},
	"golang.org/x/exp/shiny/widget/theme":                    {},
	"golang.org/x/exp/utf8string":                            {},
	"golang.org/x/image/bmp":                                 {},
	"golang.org/x/image/colornames":                          {},
	"golang.org/x/image/draw":                                {},
	"golang.org/x/image/font":                                {},
	"golang.org/x/image/font/basicfont":                      {},
	"golang.org/x/image/font/gofont/gobold":                  {},
	"golang.org/x/image/font/gofont/gobolditalic":            {},
	"golang.org/x/image/font/gofont/goitalic":                {},
	"golang.org/x/image/font/gofont/gomedium":                {},
	"golang.org/x/image/font/gofont/gomediumitalic":          {},
	"golang.org/x/image/font/gofont/gomono":                  {},
	"golang.org/x/image/font/gofont/gomonobold":              {},
	"golang.org/x/image/font/gofont/gomonobolditalic":        {},
	"golang.org/x/image/font/gofont/gomonoitalic":            {},
	"golang.org/x/image/font/gofont/goregular":               {},
	"golang.org/x/image/font/gofont/gosmallcaps":             {},
	"golang.org/x/image/font/gofont/gosmallcapsitalic":       {},
	"golang.org/x/image/font/inconsolata":                    {},
	"golang.org/x/image/font/opentype":                       {},
	"golang.org/x/image/font/plan9font":                      {},
	"golang.org/x/image/font/sfnt":                           {},
	"golang.org/x/image/math/f32":                            {},
	"golang.org/x/image/math/f64":                            {},
	"golang.org/x/image/math/fixed":                          {},
	"golang.org/x/image/riff":                                {},
	"golang.org/x/image/tiff":                                {},
	"golang.org/x/image/tiff/lzw":                            {},
	"golang.org/x/image/vector":                              {},
	"golang.org/x/image/vp8":                                 {},
	"golang.org/x/image/vp8l":                                {},
	"golang.org/x/image/webp":                                {},
	"golang.org/x/image/webp/nycbcra":                        {},
	"golang.org/x/lint":                                      {},
	"golang.org/x/lint/golint":                               {},
	"golang.org/x/mobile/app":                                {},
	"golang.org/x/mobile/app/internal/apptest":               {},
	"golang.org/x/mobile/app/internal/testapp":               {},
	"golang.org/x/mobile/asset":                              {},
	"golang.org/x/mobile/bind":                               {},
	"golang.org/x/mobile/bind/benchmark":                     {},
	"golang.org/x/mobile/bind/java":                          {},
	"golang.org/x/mobile/bind/objc":                          {},
	"golang.org/x/mobile/bind/seq":                           {},
	"golang.org/x/mobile/bind/testpkg":                       {},
	"golang.org/x/mobile/bind/testpkg/secondpkg":             {},
	"golang.org/x/mobile/bind/testpkg/simplepkg":             {},
	"golang.org/x/mobile/bind/testpkg/unboundpkg":            {},
	"golang.org/x/mobile/cmd/gobind":                         {},
	"golang.org/x/mobile/cmd/gomobile":                       {},
	"golang.org/x/mobile/event/key":                          {},
	"golang.org/x/mobile/event/lifecycle":                    {},
	"golang.org/x/mobile/event/mouse":                        {},
	"golang.org/x/mobile/event/paint":                        {},
	"golang.org/x/mobile/event/size":                         {},
	"golang.org/x/mobile/event/touch":                        {},
	"golang.org/x/mobile/example/basic":                      {},
	"golang.org/x/mobile/example/bind/hello":                 {},
	"golang.org/x/mobile/example/flappy":                     {},
	"golang.org/x/mobile/example/network":                    {},
	"golang.org/x/mobile/exp/app/debug":                      {},
	"golang.org/x/mobile/exp/audio/al":                       {},
	"golang.org/x/mobile/exp/f32":                            {},
	"golang.org/x/mobile/exp/font":                           {},
	"golang.org/x/mobile/exp/gl/glutil":                      {},
	"golang.org/x/mobile/exp/sensor":                         {},
	"golang.org/x/mobile/exp/sprite":                         {},
	"golang.org/x/mobile/exp/sprite/clock":                   {},
	"golang.org/x/mobile/exp/sprite/glsprite":                {},
	"golang.org/x/mobile/exp/sprite/portable":                {},
	"golang.org/x/mobile/geom":                               {},
	"golang.org/x/mobile/gl":                                 {},
	"golang.org/x/mobile/internal/binres":                    {},
	"golang.org/x/mobile/internal/importers":                 {},
	"golang.org/x/mobile/internal/importers/java":            {},
	"golang.org/x/mobile/internal/importers/objc":            {},
	"golang.org/x/mobile/internal/mobileinit":                {},
	"golang.org/x/net/bpf":                                   {},
	"golang.org/x/net/context":                               {},
	"golang.org/x/net/context/ctxhttp":                       {},
	"golang.org/x/net/dict":                                  {},
	"golang.org/x/net/dns/dnsmessage":                        {},
	"golang.org/x/net/html":                                  {},
	"golang.org/x/net/html/atom":                             {},
	"golang.org/x/net/html/charset":                          {},
	"golang.org/x/net/http/httpproxy":                        {},
	"golang.org/x/net/http2":                                 {},
	"golang.org/x/net/http2/h2i":                             {},
	"golang.org/x/net/http2/hpack":                           {},
	"golang.org/x/net/icmp":                                  {},
	"golang.org/x/net/idna":                                  {},
	"golang.org/x/net/internal/iana":                         {},
	"golang.org/x/net/internal/nettest":                      {},
	"golang.org/x/net/internal/socket":                       {},
	"golang.org/x/net/internal/timeseries":                   {},
	"golang.org/x/net/ipv4":                                  {},
	"golang.org/x/net/ipv6":                                  {},
	"golang.org/x/net/lex/httplex":                           {},
	"golang.org/x/net/nettest":                               {},
	"golang.org/x/net/netutil":                               {},
	"golang.org/x/net/proxy":                                 {},
	"golang.org/x/net/publicsuffix":                          {},
	"golang.org/x/net/route":                                 {},
	"golang.org/x/net/trace":                                 {},
	"golang.org/x/net/webdav":                                {},
	"golang.org/x/net/webdav/internal/xml":                   {},
	"golang.org/x/net/websocket":                             {},
	"golang.org/x/net/xsrftoken":                             {},
	"golang.org/x/oauth2":                                    {},
	"golang.org/x/oauth2/amazon":                             {},
	"golang.org/x/oauth2/bitbucket":                          {},
	"golang.org/x/oauth2/clientcredentials":                  {},
	"golang.org/x/oauth2/facebook":                           {},
	"golang.org/x/oauth2/fitbit":                             {},
	"golang.org/x/oauth2/foursquare":                         {},
	"golang.org/x/oauth2/github":                             {},
	"golang.org/x/oauth2/google":                             {},
	"golang.org/x/oauth2/heroku":                             {},
	"golang.org/x/oauth2/hipchat":                            {},
	"golang.org/x/oauth2/internal":                           {},
	"golang.org/x/oauth2/jws":                                {},
	"golang.org/x/oauth2/jwt":                                {},
	"golang.org/x/oauth2/linkedin":                           {},
	"golang.org/x/oauth2/mailchimp":                          {},
	"golang.org/x/oauth2/mailru":                             {},
	"golang.org/x/oauth2/mediamath":                          {},
	"golang.org/x/oauth2/microsoft":                          {},
	"golang.org/x/oauth2/odnoklassniki":                      {},
	"golang.org/x/oauth2/paypal":                             {},
	"golang.org/x/oauth2/slack":                              {},
	"golang.org/x/oauth2/spotify":                            {},
	"golang.org/x/oauth2/twitch":                             {},
	"golang.org/x/oauth2/uber":                               {},
	"golang.org/x/oauth2/vk":                                 {},
	"golang.org/x/oauth2/yahoo":                              {},
	"golang.org/x/oauth2/yandex":                             {},
	"golang.org/x/perf/analysis/app":                         {},
	"golang.org/x/perf/analysis/localperf":                   {},
	"golang.org/x/perf/benchstat":                            {},
	"golang.org/x/perf/cmd/benchsave":                        {},
	"golang.org/x/perf/cmd/benchstat":                        {},
	"golang.org/x/perf/internal/basedir":                     {},
	"golang.org/x/perf/internal/diff":                        {},
	"golang.org/x/perf/internal/stats":                       {},
	"golang.org/x/perf/storage":                              {},
	"golang.org/x/perf/storage/app":                          {},
	"golang.org/x/perf/storage/benchfmt":                     {},
	"golang.org/x/perf/storage/db":                           {},
	"golang.org/x/perf/storage/db/dbtest":                    {},
	"golang.org/x/perf/storage/db/sqlite3":                   {},
	"golang.org/x/perf/storage/fs":                           {},
	"golang.org/x/perf/storage/fs/gcs":                       {},
	"golang.org/x/perf/storage/fs/local":                     {},
	"golang.org/x/perf/storage/localperfdata":                {},
	"golang.org/x/perf/storage/query":                        {},
	"golang.org/x/review/git-codereview":                     {},
	"golang.org/x/sync/errgroup":                             {},
	"golang.org/x/sync/semaphore":                            {},
	"golang.org/x/sync/singleflight":                         {},
	"golang.org/x/sync/syncmap":                              {},
	"golang.org/x/sys/unix":                                  {},
	"golang.org/x/sys/windows":                               {},
	"golang.org/x/sys/windows/registry":                      {},
	"golang.org/x/talks/2014/go4java/runner":                 {},
	"golang.org/x/talks/2014/organizeio":                     {},
	"golang.org/x/talks/2014/research2":                      {},
	"golang.org/x/talks/2014/static-analysis/egtest":         {},
	"golang.org/x/talks/2014/taste":                          {},
	"golang.org/x/talks/2014/testing/subprocess":             {},
	"golang.org/x/talks/2014/testing/test1":                  {},
	"golang.org/x/talks/2014/testing/test2":                  {},
	"golang.org/x/talks/2015/keeping-up":                     {},
	"golang.org/x/talks/2015/tricks/broadcastwriter":         {},
	"golang.org/x/talks/2015/tricks/subprocess":              {},
	"golang.org/x/talks/2016/applicative":                    {},
	"golang.org/x/talks/2016/applicative/google":             {},
	"golang.org/x/talks/2016/asm":                            {},
	"golang.org/x/talks/2017/state-of-go-may/alias":          {},
	"golang.org/x/talks/2017/state-of-go-may/bits":           {},
	"golang.org/x/talks/2017/state-of-go-may/exec":           {},
	"golang.org/x/talks/2017/state-of-go-may/exec/getenv":    {},
	"golang.org/x/talks/2017/state-of-go-may/html":           {},
	"golang.org/x/talks/2017/state-of-go-may/syncmap":        {},
	"golang.org/x/talks/2017/state-of-go/stdlib/sort":        {},
	"golang.org/x/text":                                      {},
	"golang.org/x/text/cases":                                {},
	"golang.org/x/text/cmd/gotext":                           {},
	"golang.org/x/text/cmd/gotext/examples/extract":          {},
	"golang.org/x/text/cmd/gotext/examples/extract_http":     {},
	"golang.org/x/text/cmd/gotext/examples/extract_http/pkg": {},
	"golang.org/x/text/cmd/gotext/examples/rewrite":          {},
	"golang.org/x/text/collate":                              {},
	"golang.org/x/text/collate/build":                        {},
	"golang.org/x/text/collate/tools/colcmp":                 {},
	"golang.org/x/text/currency":                             {},
	"golang.org/x/text/date":                                 {},
	"golang.org/x/text/encoding":                             {},
	"golang.org/x/text/encoding/charmap":                     {},
	"golang.org/x/text/encoding/htmlindex":                   {},
	"golang.org/x/text/encoding/ianaindex":                   {},
	"golang.org/x/text/encoding/internal":                    {},
	"golang.org/x/text/encoding/internal/enctest":            {},
	"golang.org/x/text/encoding/internal/identifier":         {},
	"golang.org/x/text/encoding/japanese":                    {},
	"golang.org/x/text/encoding/korean":                      {},
	"golang.org/x/text/encoding/simplifiedchinese":           {},
	"golang.org/x/text/encoding/traditionalchinese":          {},
	"golang.org/x/text/encoding/unicode":                     {},
	"golang.org/x/text/encoding/unicode/utf32":               {},
	"golang.org/x/text/feature/plural":                       {},
	"golang.org/x/text/internal":                             {},
	"golang.org/x/text/internal/catmsg":                      {},
	"golang.org/x/text/internal/cldrtree":                    {},
	"golang.org/x/text/internal/colltab":                     {},
	"golang.org/x/text/internal/export/idna":                 {},
	"golang.org/x/text/internal/format":                      {},
	"golang.org/x/text/internal/gen":                         {},
	"golang.org/x/text/internal/gen/bitfield":                {},
	"golang.org/x/text/internal/language":                    {},
	"golang.org/x/text/internal/language/compact":            {},
	"golang.org/x/text/internal/number":                      {},
	"golang.org/x/text/internal/stringset":                   {},
	"golang.org/x/text/internal/tag":                         {},
	"golang.org/x/text/internal/testtext":                    {},
	"golang.org/x/text/internal/triegen":                     {},
	"golang.org/x/text/internal/ucd":                         {},
	"golang.org/x/text/internal/utf8internal":                {},
	"golang.org/x/text/language":                             {},
	"golang.org/x/text/language/display":                     {},
	"golang.org/x/text/message":                              {},
	"golang.org/x/text/message/catalog":                      {},
	"golang.org/x/text/message/pipeline":                     {},
	"golang.org/x/text/number":                               {},
	"golang.org/x/text/runes":                                {},
	"golang.org/x/text/search":                               {},
	"golang.org/x/text/secure":                               {},
	"golang.org/x/text/secure/bidirule":                      {},
	"golang.org/x/text/secure/precis":                        {},
	"golang.org/x/text/transform":                            {},
	"golang.org/x/text/unicode":                              {},
	"golang.org/x/text/unicode/bidi":                         {},
	"golang.org/x/text/unicode/cldr":                         {},
	"golang.org/x/text/unicode/norm":                         {},
	"golang.org/x/text/unicode/rangetable":                   {},
	"golang.org/x/text/unicode/runenames":                    {},
	"golang.org/x/text/width":                                {},
	"golang.org/x/tools/benchmark/parse":                     {},
	"golang.org/x/tools/blog":                                {},
	"golang.org/x/tools/blog/atom":                           {},
	"golang.org/x/tools/cmd/benchcmp":                        {},
	"golang.org/x/tools/cmd/bundle":                          {},
	"golang.org/x/tools/cmd/callgraph":                       {},
	"golang.org/x/tools/cmd/compilebench":                    {},
	"golang.org/x/tools/cmd/cover":                           {},
	"golang.org/x/tools/cmd/digraph":                         {},
	"golang.org/x/tools/cmd/eg":                              {},
	"golang.org/x/tools/cmd/fiximports":                      {},
	"golang.org/x/tools/cmd/getgo":                           {},
	"golang.org/x/tools/cmd/getgo/server":                    {},
	"golang.org/x/tools/cmd/go-contrib-init":                 {},
	"golang.org/x/tools/cmd/godex":                           {},
	"golang.org/x/tools/cmd/godoc":                           {},
	"golang.org/x/tools/cmd/goimports":                       {},
	"golang.org/x/tools/cmd/gomvpkg":                         {},
	"golang.org/x/tools/cmd/gorename":                        {},
	"golang.org/x/tools/cmd/gotype":                          {},
	"golang.org/x/tools/cmd/goyacc":                          {},
	"golang.org/x/tools/cmd/guru":                            {},
	"golang.org/x/tools/cmd/guru/serial":                     {},
	"golang.org/x/tools/cmd/heapview":                        {},
	"golang.org/x/tools/cmd/heapview/internal/core":          {},
	"golang.org/x/tools/cmd/html2article":                    {},
	"golang.org/x/tools/cmd/present":                         {},
	"golang.org/x/tools/cmd/ssadump":                         {},
	"golang.org/x/tools/cmd/stress":                          {},
	"golang.org/x/tools/cmd/stringer":                        {},
	"golang.org/x/tools/cmd/tip":                             {},
	"golang.org/x/tools/cmd/toolstash":                       {},
	"golang.org/x/tools/container/intsets":                   {},
	"golang.org/x/tools/cover":                               {},
	"golang.org/x/tools/go/ast/astutil":                      {},
	"golang.org/x/tools/go/buildutil":                        {},
	"golang.org/x/tools/go/callgraph":                        {},
	"golang.org/x/tools/go/callgraph/cha":                    {},
	"golang.org/x/tools/go/callgraph/rta":                    {},
	"golang.org/x/tools/go/callgraph/static":                 {},
	"golang.org/x/tools/go/gccgoexportdata":                  {},
	"golang.org/x/tools/go/gcexportdata":                     {},
	"golang.org/x/tools/go/gcimporter15":                     {},
	"golang.org/x/tools/go/internal/gccgoimporter":           {},
	"golang.org/x/tools/go/loader":                           {},
	"golang.org/x/tools/go/pointer":                          {},
	"golang.org/x/tools/go/ssa":                              {},
	"golang.org/x/tools/go/ssa/interp":                       {},
	"golang.org/x/tools/go/ssa/ssautil":                      {},
	"golang.org/x/tools/go/types/typeutil":                   {},
	"golang.org/x/tools/go/vcs":                              {},
	"golang.org/x/tools/godoc":                               {},
	"golang.org/x/tools/godoc/analysis":                      {},
	"golang.org/x/tools/godoc/redirect":                      {},
	"golang.org/x/tools/godoc/static":                        {},
	"golang.org/x/tools/godoc/util":                          {},
	"golang.org/x/tools/godoc/vfs":                           {},
	"golang.org/x/tools/godoc/vfs/gatefs":                    {},
	"golang.org/x/tools/godoc/vfs/httpfs":                    {},
	"golang.org/x/tools/godoc/vfs/mapfs":                     {},
	"golang.org/x/tools/godoc/vfs/zipfs":                     {},
	"golang.org/x/tools/imports":                             {},
	"golang.org/x/tools/playground":                          {},
	"golang.org/x/tools/playground/socket":                   {},
	"golang.org/x/tools/present":                             {},
	"golang.org/x/tools/refactor/eg":                         {},
	"golang.org/x/tools/refactor/importgraph":                {},
	"golang.org/x/tools/refactor/rename":                     {},
	"golang.org/x/tools/refactor/satisfy":                    {},
	"golang.org/x/tour/content":                              {},
	"golang.org/x/tour/gotour":                               {},
	"golang.org/x/tour/pic":                                  {},
	"golang.org/x/tour/reader":                               {},
	"golang.org/x/tour/tree":                                 {},
	"golang.org/x/tour/wc":                                   {},

	"golang.org/x/vgo": {},
}
service.go
@@ -0,0 +1,280 @@
package main

import (
	"context"
	"fmt"
	"log"
	"path"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/shurcooL/issues"
	"github.com/shurcooL/users"
	"golang.org/x/build/maintner"
	"golang.org/x/build/maintner/godata"
)

type service struct {
	// PackageIssues contains issues for all packages. Map key is import path.
	// An additional entry with key otherPackages is for issues that don't fit
	// into any existing Go package.
	PackageIssuesMu sync.RWMutex
	PackageIssues   map[string]*pkgIssues

	// Packages is a list of all packages. Sorted by import path, standard library first.
	Packages []string
}

type pkgIssues struct {
	Open, Closed []issues.Issue
}

func newService() *service {
	packageIssues := emptyPackages()

	// Initialize list of packages sorted by import path, standard library first.
	var packages []string
	for p := range packageIssues {
		if p == otherPackages { // Don't include "other", it's not a real package.
			continue
		}
		packages = append(packages, p)
	}
	sort.Slice(packages, func(i, j int) bool {
		if a, b := category(packages[i]), category(packages[j]); a != b {
			return a < b
		}
		return packages[i] < packages[j]
	})

	s := &service{
		PackageIssues: packageIssues,
		Packages:      packages,
	}
	go s.poll()
	return s
}

func emptyPackages() map[string]*pkgIssues {
	// Initialize places for issues, using existing packages
	// and their parent directories.
	packageIssues := make(map[string]*pkgIssues)
	for p := range existingPackages {
		elems := strings.Split(p, "/")
		for i := len(elems); i >= 1; i-- { // Iterate in reverse order so we can break out early.
			p := path.Join(elems[:i]...)
			if _, ok := packageIssues[p]; ok {
				break
			}
			packageIssues[p] = new(pkgIssues)
		}
	}
	packageIssues[otherPackages] = new(pkgIssues)
	return packageIssues
}

func category(importPath string) int {
	switch isStandard(importPath) {
	case true:
		return 0
	case false:
		return 1
	default:
		panic("unreachable")
	}
}

// isStandard reports whether import path p is in standard library.
// It's determined by whether the first '/'-separated element contains a dot.
func isStandard(p string) bool {
	if i := strings.IndexByte(p, '/'); i != -1 {
		p = p[:i]
	}
	return !strings.Contains(p, ".")
}

func (s *service) poll() {
	corpus, repo, err := initCorpus()
	if err != nil {
		log.Fatalln("poll: initial initCorpus failed:", err)
	}

	for {
		packageIssues := packageIssues(repo)
		s.PackageIssuesMu.Lock()
		s.PackageIssues = packageIssues
		s.PackageIssuesMu.Unlock()
		for {
			started := time.Now()
			updateError := corpus.Update(context.Background())
			if updateError == maintner.ErrSplit {
				log.Println("corpus.Update: Corpus out of sync. Re-fetching corpus.")
				corpus, repo, err = initCorpus()
				if err != nil {
					log.Fatalln("poll: post-ErrSplit initCorpus failed:", err)
				}
			} else if updateError != nil {
				log.Printf("corpus.Update: %v; sleeping 15s", updateError)
				time.Sleep(15 * time.Second)
				continue
			}
			log.Printf("got corpus update after %v", time.Since(started))
			break
		}
	}
}

func initCorpus() (*maintner.Corpus, *maintner.GitHubRepo, error) {
	corpus, err := godata.Get(context.Background())
	if err != nil {
		return nil, nil, fmt.Errorf("godata.Get: %v", err)
	}
	repo := corpus.GitHub().Repo("golang", "go")
	if repo == nil {
		return nil, nil, fmt.Errorf("golang/go repo not found")
	}
	return corpus, repo, nil
}

func packageIssues(repo *maintner.GitHubRepo) map[string]*pkgIssues {
	packageIssues := emptyPackages()
	err := repo.ForeachIssue(func(i *maintner.GitHubIssue) error {
		if i.NotExist || i.PullRequest {
			return nil
		}

		pkgs, title := ParsePrefixedTitle(i.Title)
		var labels []issues.Label
		for _, l := range i.Labels {
			labels = append(labels, issues.Label{
				Name: l.Name,
				// TODO: Can we use label ID to figure out its color?
				Color: issues.RGB{R: 0xed, G: 0xed, B: 0xed}, // maintner.Corpus doesn't support GitHub issue label colors, so fall back to a default light gray.
			})
		}
		sort.Slice(labels, func(i, j int) bool { return labels[i].Name < labels[j].Name })
		var replies int
		err := i.ForeachComment(func(*maintner.GitHubComment) error {
			replies++
			return nil
		})
		if err != nil {
			panic(fmt.Errorf("internal error: ForeachComment returned non-nil error: %v", err))
		}
		issue := issues.Issue{
			ID:     uint64(i.Number),
			State:  ghState(i),
			Title:  title,
			Labels: labels,
			Comment: issues.Comment{
				User:      ghUser(i.User),
				CreatedAt: i.Created,
			},
			Replies: replies,
		}

		var added bool
		for _, p := range pkgs {
			pi := packageIssues[p]
			if pi == nil {
				continue
			}
			switch issue.State {
			case issues.OpenState:
				pi.Open = append(pi.Open, issue)
			case issues.ClosedState:
				pi.Closed = append(pi.Closed, issue)
			}
			added = true
		}
		if !added {
			pi := packageIssues[otherPackages]
			issue.Title = i.Title
			switch issue.State {
			case issues.OpenState:
				pi.Open = append(pi.Open, issue)
			case issues.ClosedState:
				pi.Closed = append(pi.Closed, issue)
			}
		}

		return nil
	})
	if err != nil {
		panic(fmt.Errorf("internal error: ForeachIssue returned non-nil error: %v", err))
	}
	// Sort issues by ID (newest first).
	for _, p := range packageIssues {
		sort.Slice(p.Open, func(i, j int) bool { return p.Open[i].ID > p.Open[j].ID })
		sort.Slice(p.Closed, func(i, j int) bool { return p.Closed[i].ID > p.Closed[j].ID })
	}
	return packageIssues
}

const otherPackages = "other"

// ParsePrefixedTitle parses a prefixed issue title.
// It returns a list of paths from the prefix, and the remaining issue title.
// It does not try to verify whether each path is an existing Go package.
//
// Supported forms include:
//
// 	"import/path: Issue title."    -> ["import/path"],       "Issue title."
// 	"proposal: path: Issue title." -> ["path"],              "Issue title."  # Proposal.
// 	"Proposal: path: Issue title." -> ["path"],              "Issue title."  # Proposal.
// 	"x/path: Issue title."         -> ["golang.org/x/path"], "Issue title."  # "x/..." refers to "golang.org/x/...".
// 	"path1, path2: Issue title."   -> ["path1", "path2"],    "Issue title."  # Multiple comma-separated paths.
//
// If there's no path prefix (preceded by ": "), title is returned unmodified
// with an empty paths list:
//
// 	"Issue title."                 -> [], "Issue title."
//
func ParsePrefixedTitle(prefixedTitle string) (paths []string, title string) {
	prefixedTitle = strings.TrimPrefix(prefixedTitle, "proposal: ") // TODO: Consider preserving the "proposal: " prefix?
	prefixedTitle = strings.TrimPrefix(prefixedTitle, "Proposal: ")
	idx := strings.Index(prefixedTitle, ": ")
	if idx == -1 {
		return nil, prefixedTitle
	}
	prefix, title := prefixedTitle[:idx], prefixedTitle[idx+len(": "):]
	if strings.ContainsAny(prefix, "{}") {
		// TODO: Parse "x/image/{tiff,bmp}" as ["x/image/tiff", "x/image/bmp"], maybe?
		return []string{prefix}, title
	}
	paths = strings.Split(prefix, ",")
	for i := range paths {
		paths[i] = strings.TrimSpace(paths[i])
		if strings.HasPrefix(paths[i], "x/") { // Map "x/..." to "golang.org/x/...".
			paths[i] = "golang.org/x/" + paths[i][len("x/"):]
		}
	}
	return paths, title
}

// ghState converts a GitHub issue state into a issues.State.
func ghState(issue *maintner.GitHubIssue) issues.State {
	switch issue.Closed {
	case false:
		return issues.OpenState
	case true:
		return issues.ClosedState
	default:
		panic("unreachable")
	}
}

// ghUser converts a GitHub user into a users.User.
func ghUser(user *maintner.GitHubUser) users.User {
	return users.User{
		UserSpec: users.UserSpec{
			ID:     uint64(user.ID),
			Domain: "github.com",
		},
		Login:     user.Login,
		AvatarURL: fmt.Sprintf("https://avatars.githubusercontent.com/u/%d?v=4&s=96", user.ID),
		HTMLURL:   fmt.Sprintf("https://github.com/%v", user.Login),
	}
}
service_test.go
@@ -0,0 +1,50 @@
package main_test

import (
	"reflect"
	"testing"

	gido "dmitri.shuralyov.com/website/gido"
)

func TestParsePrefixedTitle(t *testing.T) {
	tests := []struct {
		in        string
		wantPaths []string
		wantTitle string
	}{
		{
			in:        "import/path: Issue title.",
			wantPaths: []string{"import/path"}, wantTitle: "Issue title.",
		},
		{ // Proposal.
			in:        "proposal: path: Issue title.",
			wantPaths: []string{"path"}, wantTitle: "Issue title.",
		},
		{ // Proposal.
			in:        "Proposal: path: Issue title.",
			wantPaths: []string{"path"}, wantTitle: "Issue title.",
		},
		{ // "x/..." refers to "golang.org/x/...".
			in:        "x/path: Issue title.",
			wantPaths: []string{"golang.org/x/path"}, wantTitle: "Issue title.",
		},
		{ // Multiple comma-separated paths.
			in:        "path1, path2: Issue title.",
			wantPaths: []string{"path1", "path2"}, wantTitle: "Issue title.",
		},
		{ // No path prefix.
			in:        "Issue title.",
			wantPaths: nil, wantTitle: "Issue title.",
		},
	}
	for _, tc := range tests {
		gotPaths, gotTitle := gido.ParsePrefixedTitle(tc.in)
		if !reflect.DeepEqual(gotPaths, tc.wantPaths) {
			t.Errorf("got: %q, want: %q", gotPaths, tc.wantPaths)
		}
		if gotTitle != tc.wantTitle {
			t.Errorf("got: %q, want: %q", gotTitle, tc.wantTitle)
		}
	}
}
util.go
@@ -0,0 +1,148 @@
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/shurcooL/httperror"
	"github.com/shurcooL/users"
)

// errorHandler factors error handling out of the HTTP handler.
type errorHandler struct {
	handler func(w http.ResponseWriter, req *http.Request) error
	users   interface {
		GetAuthenticated(context.Context) (users.User, error)
	} // May be nil if there's no users service.
}

func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	rw := &responseWriterHeader{ResponseWriter: w}
	err := h.handler(rw, req)
	if err == nil {
		// Do nothing.
		return
	}
	if err != nil && rw.WroteHeader {
		// The header has already been written, so it's too late to send
		// a different status code. Just log the error and move on.
		log.Println(err)
		return
	}
	if err, ok := httperror.IsMethod(err); ok {
		httperror.HandleMethod(w, err)
		return
	}
	if err, ok := httperror.IsRedirect(err); ok {
		if req.Method == http.MethodGet { // Workaround for https://groups.google.com/forum/#!topic/golang-nuts/9AVyMP9C8Ac.
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
		}
		http.Redirect(w, req, err.URL, http.StatusSeeOther)
		return
	}
	if err, ok := httperror.IsBadRequest(err); ok {
		httperror.HandleBadRequest(w, err)
		return
	}
	if err, ok := httperror.IsHTTP(err); ok {
		code := err.Code
		error := fmt.Sprintf("%d %s", code, http.StatusText(code))
		if user, e := h.getAuthenticated(req.Context()); e == nil && user.SiteAdmin {
			error += "\n\n" + err.Error()
		}
		http.Error(w, error, code)
		return
	}
	if os.IsNotExist(err) {
		log.Println(err)
		error := "404 Not Found"
		if user, e := h.getAuthenticated(req.Context()); e == nil && user.SiteAdmin {
			error += "\n\n" + err.Error()
		}
		http.Error(w, error, http.StatusNotFound)
		return
	}
	if os.IsPermission(err) {
		log.Println(err)
		error := "403 Forbidden"
		if user, e := h.getAuthenticated(req.Context()); e == nil && user.SiteAdmin {
			error += "\n\n" + err.Error()
		}
		http.Error(w, error, http.StatusForbidden)
		return
	}

	log.Println(err)
	error := "500 Internal Server Error"
	if user, e := h.getAuthenticated(req.Context()); e == nil && user.SiteAdmin {
		error += "\n\n" + err.Error()
	}
	http.Error(w, error, http.StatusInternalServerError)
}

func (h *errorHandler) getAuthenticated(ctx context.Context) (users.User, error) {
	if h.users == nil {
		return users.User{}, errors.New("no users service")
	}
	return h.users.GetAuthenticated(ctx)
}

// responseWriterHeader wraps a real http.ResponseWriter and captures
// whether or not the header has been written.
type responseWriterHeader struct {
	http.ResponseWriter

	WroteHeader bool // Write or WriteHeader was called.
}

func (rw *responseWriterHeader) Write(p []byte) (n int, err error) {
	rw.WroteHeader = true
	return rw.ResponseWriter.Write(p)
}
func (rw *responseWriterHeader) WriteHeader(code int) {
	rw.WroteHeader = true
	rw.ResponseWriter.WriteHeader(code)
}

// top adds some instrumentation on top of Handler.
type top struct{ Handler http.Handler }

func (t top) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	path := req.URL.Path
	started := time.Now()
	rw := &responseWriterBytes{ResponseWriter: w}
	t.Handler.ServeHTTP(rw, req)
	fmt.Printf("TIMING: %s: %v\n", path, time.Since(started))
	if path != req.URL.Path {
		log.Printf("warning: req.URL.Path was modified from %v to %v\n", path, req.URL.Path)
	}
	if rw.WroteBytes && !haveType(w) {
		log.Printf("warning: Content-Type header not set for %v %q\n", req.Method, path)
	}
}

// haveType reports whether w has the Content-Type header set.
func haveType(w http.ResponseWriter) bool {
	_, ok := w.Header()["Content-Type"]
	return ok
}

// responseWriterBytes wraps a real http.ResponseWriter and captures
// whether any bytes were written.
type responseWriterBytes struct {
	http.ResponseWriter

	WroteBytes bool // Whether non-zero bytes have been written.
}

func (rw *responseWriterBytes) Write(p []byte) (n int, err error) {
	if len(p) > 0 {
		rw.WroteBytes = true
	}
	return rw.ResponseWriter.Write(p)
}