@@ -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.
@@ -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; }
@@ -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 }
@@ -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 }
@@ -0,0 +1,4 @@ //go:generate vfsgendev -source="dmitri.shuralyov.com/website/gido/assets".Assets // Package assets contains assets for gido. package assets
@@ -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 )
@@ -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 }
@@ -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": {}, }
@@ -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), } }
@@ -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) } } }
@@ -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) }