Skip to content

Commit 037bfd7

Browse files
committed
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 <[email protected]>
1 parent 31ed75f commit 037bfd7

File tree

1 file changed

+135
-33
lines changed

1 file changed

+135
-33
lines changed

maintner/cmd/maintserve/maintserve.go

Lines changed: 135 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
// maintserve is a program that serves Go issues over HTTP, so they
6-
// can be viewed in a browser. It uses x/build/maintner/godata as
7-
// its backing source of data.
5+
// maintserve is a program that serves Go issues and CLs over HTTP,
6+
// so they can be viewed in a browser. It uses x/build/maintner/godata
7+
// as its backing source of data.
88
//
99
// It statically embeds all the resources it uses, so it's possible to use
1010
// it when offline. During that time, the corpus will not be able to update,
1111
// and GitHub user profile pictures won't load.
12+
//
13+
// maintserve displays partial Gerrit CL data that is available within the
14+
// maintner corpus. Code diffs and inline review comments are not included.
1215
package main
1316

1417
import (
@@ -24,6 +27,8 @@ import (
2427
"strings"
2528
"time"
2629

30+
"dmitri.shuralyov.com/app/changes"
31+
maintnerchange "dmitri.shuralyov.com/service/change/maintner"
2732
"github.com/shurcooL/gofontwoff"
2833
"github.com/shurcooL/httpgzip"
2934
"github.com/shurcooL/issues"
@@ -53,6 +58,7 @@ func run() error {
5358
if err != nil {
5459
return err
5560
}
61+
5662
issuesService := maintnerissues.NewService(corpus)
5763
issuesApp := issuesapp.New(issuesService, nil, issuesapp.Options{
5864
HeadPre: `<meta name="viewport" content="width=device-width">
@@ -71,17 +77,29 @@ func run() error {
7177
DisableReactions: true,
7278
})
7379

74-
// TODO: Implement background updates for corpus while the appliation is running.
80+
changeService := maintnerchange.NewService(corpus)
81+
changesApp := changes.New(changeService, nil, changes.Options{
82+
HeadPre: `<meta name="viewport" content="width=device-width">
83+
<link href="/assets/fonts/fonts.css" rel="stylesheet" type="text/css">
84+
<link href="/assets/style.css" rel="stylesheet" type="text/css">`,
85+
HeadPost: `<style type="text/css">
86+
.markdown-body { font-family: Go; }
87+
tt, code, pre { font-family: "Go Mono"; }
88+
</style>`,
89+
BodyPre: `<div style="max-width: 800px; margin: 0 auto 100px auto;">`,
90+
DisableReactions: true,
91+
})
92+
93+
// TODO: Implement background updates for corpus while the application is running.
7594
// Right now, it only updates at startup.
76-
// It's likely just a matter of calling RLock/RUnlock before all read operations,
77-
// and launching a background goroutine that occasionally calls corpus.Update()
78-
// or corpus.Sync() or something.
95+
// See gido source code for an example of how to do this.
7996

8097
printServingAt(*httpFlag)
8198
err = http.ListenAndServe(*httpFlag, &handler{
82-
c: corpus,
83-
fontsHandler: httpgzip.FileServer(gofontwoff.Assets, httpgzip.FileServerOptions{}),
84-
issuesHandler: issuesApp,
99+
c: corpus,
100+
fontsHandler: httpgzip.FileServer(gofontwoff.Assets, httpgzip.FileServerOptions{}),
101+
issuesHandler: issuesApp,
102+
changesHandler: changesApp,
85103
})
86104
return err
87105
}
@@ -97,9 +115,10 @@ func printServingAt(addr string) {
97115
// handler handles all requests to maintserve. It acts like a request multiplexer,
98116
// choosing from various endpoints and parsing the repository ID from URL.
99117
type handler struct {
100-
c *maintner.Corpus
101-
fontsHandler http.Handler
102-
issuesHandler http.Handler
118+
c *maintner.Corpus
119+
fontsHandler http.Handler
120+
issuesHandler http.Handler
121+
changesHandler http.Handler
103122
}
104123

105124
func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@@ -122,24 +141,42 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
122141
return
123142
}
124143

125-
// Handle "/owner/repo/..." URLs.
126144
elems := strings.SplitN(req.URL.Path[1:], "/", 3)
127145
if len(elems) < 2 {
128146
http.Error(w, "404 Not Found", http.StatusNotFound)
129147
return
130148
}
131-
owner, repo := elems[0], elems[1]
132-
baseURLLen := 1 + len(owner) + 1 + len(repo) // Base URL is "/owner/repo".
133-
if baseURL := req.URL.Path[:baseURLLen]; req.URL.Path == baseURL+"/" {
134-
// Redirect "/owner/repo/" to "/owner/repo".
135-
if req.URL.RawQuery != "" {
136-
baseURL += "?" + req.URL.RawQuery
149+
switch strings.HasSuffix(elems[0], ".googlesource.com") {
150+
case false:
151+
// Handle "/owner/repo/..." GitHub repository URLs.
152+
owner, repo := elems[0], elems[1]
153+
baseURLLen := 1 + len(owner) + 1 + len(repo) // Base URL is "/owner/repo".
154+
if baseURL := req.URL.Path[:baseURLLen]; req.URL.Path == baseURL+"/" {
155+
// Redirect "/owner/repo/" to "/owner/repo".
156+
if req.URL.RawQuery != "" {
157+
baseURL += "?" + req.URL.RawQuery
158+
}
159+
http.Redirect(w, req, baseURL, http.StatusFound)
160+
return
137161
}
138-
http.Redirect(w, req, baseURL, http.StatusFound)
139-
return
162+
req = stripPrefix(req, baseURLLen)
163+
h.serveIssues(w, req, maintner.GitHubRepoID{Owner: owner, Repo: repo})
164+
165+
case true:
166+
// Handle "/server/project/..." Gerrit project URLs.
167+
server, project := elems[0], elems[1]
168+
baseURLLen := 1 + len(server) + 1 + len(project) // Base URL is "/server/project".
169+
if baseURL := req.URL.Path[:baseURLLen]; req.URL.Path == baseURL+"/" {
170+
// Redirect "/server/project/" to "/server/project".
171+
if req.URL.RawQuery != "" {
172+
baseURL += "?" + req.URL.RawQuery
173+
}
174+
http.Redirect(w, req, baseURL, http.StatusFound)
175+
return
176+
}
177+
req = stripPrefix(req, baseURLLen)
178+
h.serveChanges(w, req, server, project)
140179
}
141-
req = stripPrefix(req, baseURLLen)
142-
h.serveIssues(w, req, maintner.GitHubRepoID{Owner: owner, Repo: repo})
143180
}
144181

145182
var indexHTML = template.Must(template.New("").Parse(`<html>
@@ -152,17 +189,27 @@ var indexHTML = template.Must(template.New("").Parse(`<html>
152189
<body>
153190
<div style="max-width: 800px; margin: 0 auto 100px auto;">
154191
<h2>maintserve</h2>
155-
<h3>Repos</h3>
156-
<ul>{{range .}}
157-
<li><a href="/{{.RepoID}}">{{.RepoID}}</a> ({{.Count}} issues)</li>
158-
{{- end}}
159-
</ul>
192+
<div style="display: inline-block; width: 50%;">
193+
<h3>GitHub Repos</h3>
194+
<ul>{{range .Repos}}
195+
<li><a href="/{{.RepoID}}">{{.RepoID}}</a> ({{.Count}} issues)</li>
196+
{{- end}}
197+
</ul>
198+
</div><div style="display: inline-block; width: 50%; vertical-align: top;">
199+
<h3>Gerrit Projects</h3>
200+
<ul>{{range .Projects}}
201+
<li><a href="/{{.ServProj}}">{{.ServProj}}</a> ({{.Count}} changes)</li>
202+
{{- end}}
203+
</ul>
204+
</div>
160205
</div>
161206
<body>
162207
</html>`))
163208

164-
// serveIndex serves the index page, which lists all available repositories.
209+
// serveIndex serves the index page, which lists all available
210+
// GitHub repositories and Gerrit projects.
165211
func (h *handler) serveIndex(w http.ResponseWriter, req *http.Request) {
212+
// Enumerate all GitHub repositories.
166213
type repo struct {
167214
RepoID maintner.GitHubRepoID
168215
Count uint64 // Issues count.
@@ -187,8 +234,36 @@ func (h *handler) serveIndex(w http.ResponseWriter, req *http.Request) {
187234
return repos[i].RepoID.String() < repos[j].RepoID.String()
188235
})
189236

237+
// Enumerate all Gerrit projects.
238+
type project struct {
239+
ServProj string
240+
Count uint64 // Changes count.
241+
}
242+
var projects []project
243+
err = h.c.Gerrit().ForeachProjectUnsorted(func(r *maintner.GerritProject) error {
244+
changes, err := countChanges(r)
245+
if err != nil {
246+
return err
247+
}
248+
projects = append(projects, project{
249+
ServProj: r.ServerSlashProject(),
250+
Count: changes,
251+
})
252+
return nil
253+
})
254+
if err != nil {
255+
http.Error(w, err.Error(), http.StatusInternalServerError)
256+
return
257+
}
258+
sort.Slice(projects, func(i, j int) bool {
259+
return projects[i].ServProj < projects[j].ServProj
260+
})
261+
190262
w.Header().Set("Content-Type", "text/html; charset=utf-8")
191-
err = indexHTML.Execute(w, repos)
263+
err = indexHTML.Execute(w, map[string]interface{}{
264+
"Repos": repos,
265+
"Projects": projects,
266+
})
192267
if err != nil {
193268
log.Println(err)
194269
}
@@ -207,10 +282,23 @@ func countIssues(r *maintner.GitHubRepo) (uint64, error) {
207282
return issues, err
208283
}
209284

210-
// serveIssues serves issues for repository id.
285+
// countChanges reports the number of changes in a GerritProject p.
286+
func countChanges(p *maintner.GerritProject) (uint64, error) {
287+
var changes uint64
288+
err := p.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
289+
if cl.Private {
290+
return nil
291+
}
292+
changes++
293+
return nil
294+
})
295+
return changes, err
296+
}
297+
298+
// serveIssues serves issues for GitHub repository id.
211299
func (h *handler) serveIssues(w http.ResponseWriter, req *http.Request, id maintner.GitHubRepoID) {
212300
if h.c.GitHub().Repo(id.Owner, id.Repo) == nil {
213-
http.Error(w, fmt.Sprintf("404 Not Found\n\nrepository %q not found", id), http.StatusNotFound)
301+
http.Error(w, fmt.Sprintf("404 Not Found\n\nGitHub repository %q not found", id), http.StatusNotFound)
214302
return
215303
}
216304

@@ -221,6 +309,20 @@ func (h *handler) serveIssues(w http.ResponseWriter, req *http.Request, id maint
221309
h.issuesHandler.ServeHTTP(w, req)
222310
}
223311

312+
// serveChanges serves changes for Gerrit project server/project.
313+
func (h *handler) serveChanges(w http.ResponseWriter, req *http.Request, server, project string) {
314+
if h.c.Gerrit().Project(server, project) == nil {
315+
http.Error(w, fmt.Sprintf("404 Not Found\n\nGerrit project %s/%s not found", server, project), http.StatusNotFound)
316+
return
317+
}
318+
319+
req = req.WithContext(context.WithValue(req.Context(),
320+
changes.RepoSpecContextKey, fmt.Sprintf("%s/%s", server, project)))
321+
req = req.WithContext(context.WithValue(req.Context(),
322+
changes.BaseURIContextKey, fmt.Sprintf("/%s/%s", server, project)))
323+
h.changesHandler.ServeHTTP(w, req)
324+
}
325+
224326
// stripPrefix returns request r with prefix of length prefixLen stripped from r.URL.Path.
225327
// prefixLen must not be longer than len(r.URL.Path), otherwise stripPrefix panics.
226328
// If r.URL.Path is empty after the prefix is stripped, the path is changed to "/".

0 commit comments

Comments
 (0)