@@ -5,10 +5,11 @@ import ( "html/template" "net/http" "net/url" "os" "sort" "strings" "dmitri.shuralyov.com/service/change" "github.com/shurcooL/htmlg" "github.com/shurcooL/httperror" ) @@ -36,14 +37,15 @@ var changesHTML = template.Must(template.New("").Parse(`{{define "Header"}}<html <p>Go Changes shows changes for Go packages. It's just like <a href="https://goissues.org">goissues.org</a>, but for changes (CLs, PRs, etc.).</p> <p>To view changes of a Go package with a given import path, navigate to <code>gochanges.org/import/path</code> using your browser's address bar (<kbd>⌘Cmd</kbd>+<kbd>L</kbd> or <kbd>Ctrl</kbd>+<kbd>L</kbd>). You may specify an <a href="https://golang.org/cmd/go/#hdr-Package_lists_and_patterns">import path pattern</a> You may specify comma-separated <a href="https://golang.org/cmd/go/#hdr-Package_lists_and_patterns">import path patterns</a> to view changes for all matching packages (e.g., <a href="/image/..."><code>gochanges.org/image/...</code></a>).</p> (such as <a href="/image/..."><code>image/...</code></a> or <a href="/cmd/compile/...,cmd/link/...,runtime"><code>cmd/compile/...</code>,<code>cmd/link/...</code>,<code>runtime</code></a>).</p> <p>Supported packages include:</p> <ul> <li><a href="/-/packages#stdlib">Standard library</a> (e.g., <code>io</code>, <code>net/http</code>, etc.),</li> @@ -113,38 +115,25 @@ func (h *handler) serveChangesPkg(w http.ResponseWriter, req *http.Request, pkg } err = h.executeTemplate(w, req, "Trailer", nil) return err } // serveChangesPattern serves a list of changes for packages matching import path pattern. func (h *handler) serveChangesPattern(w http.ResponseWriter, req *http.Request, pattern string) error { // serveChangesPatterns serves a list of changes for packages matching import path patterns. func (h *handler) serveChangesPatterns(w http.ResponseWriter, req *http.Request, patterns []string) error { if req.Method != http.MethodGet { return httperror.Method{Allowed: []string{http.MethodGet}} } filter, err := changeStateFilter(req.URL.Query()) if err != nil { return httperror.BadRequest{Err: err} } var pkgs []string switch pattern { case "all", "...": // "all" expands to all packages found in all the GOPATH trees. pkgs = h.s.AllPackages case "std": // "std" is like all but expands to just the packages in the standard Go library. pkgs = h.s.StdPackages case "cmd": // "cmd" expands to the Go repository's commands and their internal libraries. pkgs = h.s.CmdPackages default: pkgs = expandPattern(h.s.AllPackages, pattern) } pkgs := expandPatterns(h.s.AllPackages, h.s.StdPackages, h.s.CmdPackages, patterns) var cs []change.Change var openChanges, closedChanges, openIssues int match := matchPaths(pattern) match := matchPatterns(patterns) h.s.IssuesAndChangesMu.RLock() switch { case filter == change.FilterOpen: for _, c := range h.s.OpenChanges { if !match(c.Paths) { @@ -198,20 +187,20 @@ func (h *handler) serveChangesPattern(w http.ResponseWriter, req *http.Request, h.s.IssuesAndChangesMu.RUnlock() sort.Slice(cs, func(i, j int) bool { return cs[i].CreatedAt.After(cs[j].CreatedAt) }) w.Header().Set("Content-Type", "text/html; charset=utf-8") err = h.executeTemplate(w, req, "Header", map[string]interface{}{ "PageName": pattern, "PageName": strings.Join(patterns, " "), "AnalyticsHTML": h.analyticsHTML, }) if err != nil { return err } err = htmlg.RenderComponents(w, heading{PkgOrPattern: pattern}, subheadingPattern{Pattern: pattern, Pkgs: pkgs, pkgURL: h.rtr.ChangesURL}, renderTabnav(changesTab, openIssues, openChanges, pattern, h.rtr), heading{PkgOrPattern: strings.Join(patterns, " ")}, subheadingPattern{Pattern: strings.Join(patterns, " "), Pkgs: pkgs, pkgURL: h.rtr.ChangesURL}, renderTabnav(changesTab, openIssues, openChanges, strings.Join(patterns, ","), h.rtr), renderChanges(cs, openChanges, closedChanges, req.URL, filter), ) if err != nil { return err }
@@ -5,10 +5,11 @@ import ( "html/template" "net/http" "net/url" "os" "sort" "strings" "github.com/shurcooL/htmlg" "github.com/shurcooL/httperror" "github.com/shurcooL/issues" ) @@ -36,14 +37,15 @@ var issuesHTML = template.Must(template.New("").Parse(`{{define "Header"}}<html <p>Go Issues shows issues for Go packages. Documentation and other information for Go packages is available at <a href="https://pkg.go.dev">pkg.go.dev</a>.</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>). You may specify an <a href="https://golang.org/cmd/go/#hdr-Package_lists_and_patterns">import path pattern</a> You may specify comma-separated <a href="https://golang.org/cmd/go/#hdr-Package_lists_and_patterns">import path patterns</a> to view issues for all matching packages (e.g., <a href="/image/..."><code>goissues.org/image/...</code></a>).</p> (such as <a href="/image/..."><code>image/...</code></a> or <a href="/cmd/compile/...,cmd/link/...,runtime"><code>cmd/compile/...</code>,<code>cmd/link/...</code>,<code>runtime</code></a>).</p> <p>Supported packages include:</p> <ul> <li><a href="/-/packages#stdlib">Standard library</a> (e.g., <code>io</code>, <code>net/http</code>, etc.),</li> @@ -114,38 +116,25 @@ func (h *handler) serveIssuesPkg(w http.ResponseWriter, req *http.Request, pkg s } err = h.executeTemplate(w, req, "Trailer", nil) return err } // serveIssuesPattern serves a list of issues for packages matching import path pattern. func (h *handler) serveIssuesPattern(w http.ResponseWriter, req *http.Request, pattern string) error { // serveIssuesPatterns serves a list of issues for packages matching import path patterns. func (h *handler) serveIssuesPatterns(w http.ResponseWriter, req *http.Request, patterns []string) error { if req.Method != http.MethodGet { return httperror.Method{Allowed: []string{http.MethodGet}} } filter, err := issueStateFilter(req.URL.Query()) if err != nil { return httperror.BadRequest{Err: err} } var pkgs []string switch pattern { case "all", "...": // "all" expands to all packages found in all the GOPATH trees. pkgs = h.s.AllPackages case "std": // "std" is like all but expands to just the packages in the standard Go library. pkgs = h.s.StdPackages case "cmd": // "cmd" expands to the Go repository's commands and their internal libraries. pkgs = h.s.CmdPackages default: pkgs = expandPattern(h.s.AllPackages, pattern) } pkgs := expandPatterns(h.s.AllPackages, h.s.StdPackages, h.s.CmdPackages, patterns) var is []issues.Issue var openIssues, closedIssues, openChanges int match := matchPaths(pattern) match := matchPatterns(patterns) h.s.IssuesAndChangesMu.RLock() switch { case filter == issues.StateFilter(issues.OpenState): for _, i := range h.s.OpenIssues { if !match(i.Paths) { @@ -199,20 +188,20 @@ func (h *handler) serveIssuesPattern(w http.ResponseWriter, req *http.Request, p h.s.IssuesAndChangesMu.RUnlock() sort.Slice(is, func(i, j int) bool { return is[i].CreatedAt.After(is[j].CreatedAt) }) w.Header().Set("Content-Type", "text/html; charset=utf-8") err = h.executeTemplate(w, req, "Header", map[string]interface{}{ "PageName": pattern, "PageName": strings.Join(patterns, " "), "AnalyticsHTML": h.analyticsHTML, }) if err != nil { return err } err = htmlg.RenderComponents(w, heading{PkgOrPattern: pattern}, subheadingPattern{Pattern: pattern, Pkgs: pkgs, pkgURL: h.rtr.IssuesURL}, renderTabnav(issuesTab, openIssues, openChanges, pattern, h.rtr), heading{PkgOrPattern: strings.Join(patterns, " ")}, subheadingPattern{Pattern: strings.Join(patterns, " "), Pkgs: pkgs, pkgURL: h.rtr.IssuesURL}, renderTabnav(issuesTab, openIssues, openChanges, strings.Join(patterns, ","), h.rtr), renderIssues(is, openIssues, closedIssues, req.URL, filter), ) if err != nil { return err }
@@ -120,12 +120,11 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) error { // Redirect to canonical path (no trailing slash, etc.) if needed. if canonicalPath := path.Clean(req.URL.Path); req.URL.Path != canonicalPath { if req.URL.RawQuery != "" { canonicalPath += "?" + req.URL.RawQuery } http.Redirect(w, req, canonicalPath, http.StatusFound) return nil return httperror.Redirect{URL: canonicalPath} } // Handle "/". if req.URL.Path == "/" { return h.ServeIndex(w, req) @@ -147,18 +146,31 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) error { // Handle "/-/packages". if req.URL.Path == "/-/packages" { return h.ServePackages(w, req) } // Handle "/{pattern}[,{pattern}]" URLs. // Redirect to canonical path (no trailing slash, no spaces, etc.) if needed. patterns := strings.Split(req.URL.Path[1:], ",") for i := range patterns { patterns[i] = path.Clean("/" + strings.TrimSpace(patterns[i]))[1:] } if canonicalPath := "/" + strings.Join(patterns, ","); req.URL.Path != canonicalPath { if req.URL.RawQuery != "" { canonicalPath += "?" + req.URL.RawQuery } return httperror.Redirect{URL: canonicalPath} } // Handle "/..." URLs. switch isImportPathPattern(req.URL.Path[1:]) { case false: pkg := req.URL.Path[1:] switch { case len(patterns) == 1 && !isImportPathPattern(patterns[0]): pkg := patterns[0] return h.ServeIssuesOrChangesPkg(w, req, pkg) case true: pattern := req.URL.Path[1:] return h.ServeIssuesOrChangesPattern(w, req, pattern) return h.ServeIssuesOrChangesPatterns(w, req, patterns) default: panic("unreachable") } } @@ -346,17 +358,17 @@ func (h *handler) ServeIssuesOrChangesPkg(w http.ResponseWriter, req *http.Reque default: panic("unreachable") } } // ServeIssuesOrChangesPattern serves a list of issues or changes for packages matching import path pattern. func (h *handler) ServeIssuesOrChangesPattern(w http.ResponseWriter, req *http.Request, pattern string) error { // ServeIssuesOrChangesPatterns serves a list of issues or changes for packages matching import path patterns. func (h *handler) ServeIssuesOrChangesPatterns(w http.ResponseWriter, req *http.Request, patterns []string) error { switch changes := h.rtr.WantChanges(req); { case !changes: return h.serveIssuesPattern(w, req, pattern) return h.serveIssuesPatterns(w, req, patterns) case changes: return h.serveChangesPattern(w, req, pattern) return h.serveChangesPatterns(w, req, patterns) default: panic("unreachable") } }
@@ -127,10 +127,70 @@ func subTree(r *git.Repository, t *object.Tree, name string) (*object.Tree, erro return r.TreeObject(e.Hash) } return nil, os.ErrNotExist } // expandPatterns returns a list of Go packages matched by // the specified import path patterns. func expandPatterns(all, std, cmd []string, patterns []string) []string { switch len(patterns) { // Faster path for a single pattern. case 1: switch patterns[0] { case "all", "...": // "all" or "..." expands to all packages found in all the GOPATH trees. return all case "std": // "std" is like all but expands to just the packages in the standard Go library. return std case "cmd": // "cmd" expands to the Go repository's commands and their internal libraries. return cmd default: return expandPattern(all, patterns[0]) } // Multiple import path patterns. default: var hasStd, hasCmd bool for _, pattern := range patterns { switch pattern { case "all", "...": return all case "std": hasStd = true case "cmd": hasCmd = true } } var ms []func(path string) bool if hasStd { ms = append(ms, isStandard) } else if hasCmd { // cmd is a subset of std, so add iff hasCmd && !hasStd. ms = append(ms, isCommand) } for _, pattern := range patterns { if pattern == "std" || pattern == "cmd" { continue } ms = append(ms, matchPattern(pattern)) } var matched []string for _, match := range ms { for _, pkg := range all { if !match(pkg) { continue } matched = append(matched, pkg) } } return matched } } // expandPattern returns a list of Go packages matched by specified // import path pattern, which may have the following forms: // // example.org/single/package # a single package // example.org/dir/... # all packages beneath dir @@ -148,55 +208,95 @@ func expandPattern(allPackages []string, pattern string) []string { matched = append(matched, pkg) } return matched } // matchPaths(pattern)(paths) reports whether any of the import paths paths // match the import path pattern pattern. // It uses the same rules for pattern matching as matchPattern. func matchPaths(pattern string) func(paths []string) bool { switch pattern { case "all", "...": // "all" expands to all packages found in all the GOPATH trees. return func([]string) bool { return true } case "std": // "std" is like all but expands to just the packages in the standard Go library. return func(paths []string) bool { for _, p := range paths { if isStandard(p) { return true // matchPatterns(patterns)(paths) reports whether any of the import paths paths // match any of the import path patterns patterns. // It uses the same rules for pattern matching as matchPattern, // but also understands "all", "std", and "cmd" meta-patterns. func matchPatterns(patterns []string) func(paths []string) bool { switch len(patterns) { // Faster path for a single pattern. case 1: switch patterns[0] { case "all", "...": return func([]string) bool { return true } case "std": return func(paths []string) bool { for _, p := range paths { if isStandard(p) { return true } } return false } return false } case "cmd": // "cmd" expands to the Go repository's commands and their internal libraries. return func(paths []string) bool { for _, p := range paths { if p == "cmd" || strings.HasPrefix(p, "cmd/") { return true case "cmd": return func(paths []string) bool { for _, p := range paths { if isCommand(p) { return true } } return false } default: match := matchPattern(patterns[0]) return func(paths []string) bool { for _, p := range paths { if match(p) { return true } } return false } return false } // Multiple import path patterns. default: match := matchPattern(pattern) var std, cmd bool for _, pattern := range patterns { switch pattern { case "all", "...": return func([]string) bool { return true } case "std": std = true case "cmd": cmd = true } } var ms []func(path string) bool if std { ms = append(ms, isStandard) } else if cmd { // cmd is a subset of std, so add iff hasCmd && !hasStd. ms = append(ms, isCommand) } for _, pattern := range patterns { if pattern == "std" || pattern == "cmd" { continue } ms = append(ms, matchPattern(pattern)) } return func(paths []string) bool { for _, p := range paths { if match(p) { return true for _, match := range ms { for _, p := range paths { if match(p) { return true } } } return false } } } // matchPattern(pattern)(name) reports whether name matches pattern. // matchPattern(pattern)(path) reports whether path matches pattern. // Pattern is a limited glob pattern in which '...' means 'any string', // foo/... matches foo too, and there is no other special syntax. func matchPattern(pattern string) func(name string) bool { func matchPattern(pattern string) func(path string) bool { re := regexp.QuoteMeta(pattern) re = strings.Replace(re, `\.\.\.`, `.*`, -1) // Special case: foo/... matches foo too. if strings.HasSuffix(re, `/.*`) { re = re[:len(re)-len(`/.*`)] + `(/.*)?`
@@ -114,10 +114,17 @@ func isStandard(importPath string) bool { importPath = importPath[:i] } return !strings.Contains(importPath, ".") } // isCommand reports whether import path importPath is one of // Go repository's commands or their internal libraries. // It's determined by whether importPath equals "cmd" or has "cmd/" prefix. func isCommand(importPath string) bool { return importPath == "cmd" || strings.HasPrefix(importPath, "cmd/") } func (s *service) poll(ctx context.Context) { corpus, repo, err := initCorpus(ctx) if err != nil { log.Fatalln("poll: initial initCorpus failed:", err) }
@@ -41,11 +41,11 @@ func (h *errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { if err, ok := httperror.IsMethod(err); ok { httperror.HandleMethod(w, err) return } if err, ok := httperror.IsRedirect(err); ok { http.Redirect(w, req, err.URL, http.StatusSeeOther) http.Redirect(w, req, err.URL, http.StatusFound) return } if err, ok := httperror.IsBadRequest(err); ok { httperror.HandleBadRequest(w, err) return