From 037bfd7a21c5d9ed4a029292dcb7e351c4d69be2 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Mon, 22 Oct 2018 15:35:28 -0400 Subject: [PATCH] maintner/cmd/maintserve: display Gerrit projects and CLs This change expands the scope of cmd/maintserve to visualize Gerrit CL maintner data, in addition to the GitHub repository issue tracker data. I've needed this recently when investigating golang/go#28318 to check maintner.GerritHashtags values of various CLs. They are shown as of https://dmitri.shuralyov.com/service/change/...$commit/e712a6949fbe7fe04b2f49fc22810f827b17f3f8. maintner doesn't have sufficient information to present Gerrit CLs in full detail, so this does a best effort and displays the available information. Inline review comments and diffs are not included. The downside of this change is that it adds new dependencies. However, they are actively maintained by me. Updates golang/go#28318 Change-Id: Ie6fe14f95f107e95371ea820af88563e03a6bb2a Reviewed-on: https://go-review.googlesource.com/c/145258 Reviewed-by: Brad Fitzpatrick --- maintner/cmd/maintserve/maintserve.go | 168 +++++++++++++++++++++----- 1 file changed, 135 insertions(+), 33 deletions(-) diff --git a/maintner/cmd/maintserve/maintserve.go b/maintner/cmd/maintserve/maintserve.go index ef4e2862..e33d5eb7 100644 --- a/maintner/cmd/maintserve/maintserve.go +++ b/maintner/cmd/maintserve/maintserve.go @@ -2,13 +2,16 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// maintserve is a program that serves Go issues over HTTP, so they -// can be viewed in a browser. It uses x/build/maintner/godata as -// its backing source of data. +// maintserve is a program that serves Go issues and CLs over HTTP, +// so they can be viewed in a browser. It uses x/build/maintner/godata +// as its backing source of data. // // It statically embeds all the resources it uses, so it's possible to use // it when offline. During that time, the corpus will not be able to update, // and GitHub user profile pictures won't load. +// +// maintserve displays partial Gerrit CL data that is available within the +// maintner corpus. Code diffs and inline review comments are not included. package main import ( @@ -24,6 +27,8 @@ import ( "strings" "time" + "dmitri.shuralyov.com/app/changes" + maintnerchange "dmitri.shuralyov.com/service/change/maintner" "github.com/shurcooL/gofontwoff" "github.com/shurcooL/httpgzip" "github.com/shurcooL/issues" @@ -53,6 +58,7 @@ func run() error { if err != nil { return err } + issuesService := maintnerissues.NewService(corpus) issuesApp := issuesapp.New(issuesService, nil, issuesapp.Options{ HeadPre: ` @@ -71,17 +77,29 @@ func run() error { DisableReactions: true, }) - // TODO: Implement background updates for corpus while the appliation is running. + changeService := maintnerchange.NewService(corpus) + changesApp := changes.New(changeService, nil, changes.Options{ + HeadPre: ` + +`, + HeadPost: ``, + BodyPre: `
`, + DisableReactions: true, + }) + + // TODO: Implement background updates for corpus while the application is running. // Right now, it only updates at startup. - // It's likely just a matter of calling RLock/RUnlock before all read operations, - // and launching a background goroutine that occasionally calls corpus.Update() - // or corpus.Sync() or something. + // See gido source code for an example of how to do this. printServingAt(*httpFlag) err = http.ListenAndServe(*httpFlag, &handler{ - c: corpus, - fontsHandler: httpgzip.FileServer(gofontwoff.Assets, httpgzip.FileServerOptions{}), - issuesHandler: issuesApp, + c: corpus, + fontsHandler: httpgzip.FileServer(gofontwoff.Assets, httpgzip.FileServerOptions{}), + issuesHandler: issuesApp, + changesHandler: changesApp, }) return err } @@ -97,9 +115,10 @@ func printServingAt(addr string) { // handler handles all requests to maintserve. It acts like a request multiplexer, // choosing from various endpoints and parsing the repository ID from URL. type handler struct { - c *maintner.Corpus - fontsHandler http.Handler - issuesHandler http.Handler + c *maintner.Corpus + fontsHandler http.Handler + issuesHandler http.Handler + changesHandler http.Handler } func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { @@ -122,24 +141,42 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - // Handle "/owner/repo/..." URLs. elems := strings.SplitN(req.URL.Path[1:], "/", 3) if len(elems) < 2 { http.Error(w, "404 Not Found", http.StatusNotFound) return } - owner, repo := elems[0], elems[1] - baseURLLen := 1 + len(owner) + 1 + len(repo) // Base URL is "/owner/repo". - if baseURL := req.URL.Path[:baseURLLen]; req.URL.Path == baseURL+"/" { - // Redirect "/owner/repo/" to "/owner/repo". - if req.URL.RawQuery != "" { - baseURL += "?" + req.URL.RawQuery + switch strings.HasSuffix(elems[0], ".googlesource.com") { + case false: + // Handle "/owner/repo/..." GitHub repository URLs. + owner, repo := elems[0], elems[1] + baseURLLen := 1 + len(owner) + 1 + len(repo) // Base URL is "/owner/repo". + if baseURL := req.URL.Path[:baseURLLen]; req.URL.Path == baseURL+"/" { + // Redirect "/owner/repo/" to "/owner/repo". + if req.URL.RawQuery != "" { + baseURL += "?" + req.URL.RawQuery + } + http.Redirect(w, req, baseURL, http.StatusFound) + return } - http.Redirect(w, req, baseURL, http.StatusFound) - return + req = stripPrefix(req, baseURLLen) + h.serveIssues(w, req, maintner.GitHubRepoID{Owner: owner, Repo: repo}) + + case true: + // Handle "/server/project/..." Gerrit project URLs. + server, project := elems[0], elems[1] + baseURLLen := 1 + len(server) + 1 + len(project) // Base URL is "/server/project". + if baseURL := req.URL.Path[:baseURLLen]; req.URL.Path == baseURL+"/" { + // Redirect "/server/project/" to "/server/project". + if req.URL.RawQuery != "" { + baseURL += "?" + req.URL.RawQuery + } + http.Redirect(w, req, baseURL, http.StatusFound) + return + } + req = stripPrefix(req, baseURLLen) + h.serveChanges(w, req, server, project) } - req = stripPrefix(req, baseURLLen) - h.serveIssues(w, req, maintner.GitHubRepoID{Owner: owner, Repo: repo}) } var indexHTML = template.Must(template.New("").Parse(` @@ -152,17 +189,27 @@ var indexHTML = template.Must(template.New("").Parse(`

maintserve

-

Repos

-
    {{range .}} -
  • {{.RepoID}} ({{.Count}} issues)
  • - {{- end}} -
+
+

GitHub Repos

+
    {{range .Repos}} +
  • {{.RepoID}} ({{.Count}} issues)
  • + {{- end}} +
+
+

Gerrit Projects

+
    {{range .Projects}} +
  • {{.ServProj}} ({{.Count}} changes)
  • + {{- end}} +
+
`)) -// serveIndex serves the index page, which lists all available repositories. +// serveIndex serves the index page, which lists all available +// GitHub repositories and Gerrit projects. func (h *handler) serveIndex(w http.ResponseWriter, req *http.Request) { + // Enumerate all GitHub repositories. type repo struct { RepoID maintner.GitHubRepoID Count uint64 // Issues count. @@ -187,8 +234,36 @@ func (h *handler) serveIndex(w http.ResponseWriter, req *http.Request) { return repos[i].RepoID.String() < repos[j].RepoID.String() }) + // Enumerate all Gerrit projects. + type project struct { + ServProj string + Count uint64 // Changes count. + } + var projects []project + err = h.c.Gerrit().ForeachProjectUnsorted(func(r *maintner.GerritProject) error { + changes, err := countChanges(r) + if err != nil { + return err + } + projects = append(projects, project{ + ServProj: r.ServerSlashProject(), + Count: changes, + }) + return nil + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + sort.Slice(projects, func(i, j int) bool { + return projects[i].ServProj < projects[j].ServProj + }) + w.Header().Set("Content-Type", "text/html; charset=utf-8") - err = indexHTML.Execute(w, repos) + err = indexHTML.Execute(w, map[string]interface{}{ + "Repos": repos, + "Projects": projects, + }) if err != nil { log.Println(err) } @@ -207,10 +282,23 @@ func countIssues(r *maintner.GitHubRepo) (uint64, error) { return issues, err } -// serveIssues serves issues for repository id. +// countChanges reports the number of changes in a GerritProject p. +func countChanges(p *maintner.GerritProject) (uint64, error) { + var changes uint64 + err := p.ForeachCLUnsorted(func(cl *maintner.GerritCL) error { + if cl.Private { + return nil + } + changes++ + return nil + }) + return changes, err +} + +// serveIssues serves issues for GitHub repository id. func (h *handler) serveIssues(w http.ResponseWriter, req *http.Request, id maintner.GitHubRepoID) { if h.c.GitHub().Repo(id.Owner, id.Repo) == nil { - http.Error(w, fmt.Sprintf("404 Not Found\n\nrepository %q not found", id), http.StatusNotFound) + http.Error(w, fmt.Sprintf("404 Not Found\n\nGitHub repository %q not found", id), http.StatusNotFound) return } @@ -221,6 +309,20 @@ func (h *handler) serveIssues(w http.ResponseWriter, req *http.Request, id maint h.issuesHandler.ServeHTTP(w, req) } +// serveChanges serves changes for Gerrit project server/project. +func (h *handler) serveChanges(w http.ResponseWriter, req *http.Request, server, project string) { + if h.c.Gerrit().Project(server, project) == nil { + http.Error(w, fmt.Sprintf("404 Not Found\n\nGerrit project %s/%s not found", server, project), http.StatusNotFound) + return + } + + req = req.WithContext(context.WithValue(req.Context(), + changes.RepoSpecContextKey, fmt.Sprintf("%s/%s", server, project))) + req = req.WithContext(context.WithValue(req.Context(), + changes.BaseURIContextKey, fmt.Sprintf("/%s/%s", server, project))) + h.changesHandler.ServeHTTP(w, req) +} + // stripPrefix returns request r with prefix of length prefixLen stripped from r.URL.Path. // prefixLen must not be longer than len(r.URL.Path), otherwise stripPrefix panics. // If r.URL.Path is empty after the prefix is stripped, the path is changed to "/".