2
2
// Use of this source code is governed by a BSD-style
3
3
// license that can be found in the LICENSE file.
4
4
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.
8
8
//
9
9
// It statically embeds all the resources it uses, so it's possible to use
10
10
// it when offline. During that time, the corpus will not be able to update,
11
11
// 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.
12
15
package main
13
16
14
17
import (
@@ -24,6 +27,8 @@ import (
24
27
"strings"
25
28
"time"
26
29
30
+ "dmitri.shuralyov.com/app/changes"
31
+ maintnerchange "dmitri.shuralyov.com/service/change/maintner"
27
32
"github.com/shurcooL/gofontwoff"
28
33
"github.com/shurcooL/httpgzip"
29
34
"github.com/shurcooL/issues"
@@ -53,6 +58,7 @@ func run() error {
53
58
if err != nil {
54
59
return err
55
60
}
61
+
56
62
issuesService := maintnerissues .NewService (corpus )
57
63
issuesApp := issuesapp .New (issuesService , nil , issuesapp.Options {
58
64
HeadPre : `<meta name="viewport" content="width=device-width">
@@ -71,17 +77,29 @@ func run() error {
71
77
DisableReactions : true ,
72
78
})
73
79
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.
75
94
// 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.
79
96
80
97
printServingAt (* httpFlag )
81
98
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 ,
85
103
})
86
104
return err
87
105
}
@@ -97,9 +115,10 @@ func printServingAt(addr string) {
97
115
// handler handles all requests to maintserve. It acts like a request multiplexer,
98
116
// choosing from various endpoints and parsing the repository ID from URL.
99
117
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
103
122
}
104
123
105
124
func (h * handler ) ServeHTTP (w http.ResponseWriter , req * http.Request ) {
@@ -122,24 +141,42 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
122
141
return
123
142
}
124
143
125
- // Handle "/owner/repo/..." URLs.
126
144
elems := strings .SplitN (req .URL .Path [1 :], "/" , 3 )
127
145
if len (elems ) < 2 {
128
146
http .Error (w , "404 Not Found" , http .StatusNotFound )
129
147
return
130
148
}
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
137
161
}
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 )
140
179
}
141
- req = stripPrefix (req , baseURLLen )
142
- h .serveIssues (w , req , maintner.GitHubRepoID {Owner : owner , Repo : repo })
143
180
}
144
181
145
182
var indexHTML = template .Must (template .New ("" ).Parse (`<html>
@@ -152,17 +189,27 @@ var indexHTML = template.Must(template.New("").Parse(`<html>
152
189
<body>
153
190
<div style="max-width: 800px; margin: 0 auto 100px auto;">
154
191
<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>
160
205
</div>
161
206
<body>
162
207
</html>` ))
163
208
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.
165
211
func (h * handler ) serveIndex (w http.ResponseWriter , req * http.Request ) {
212
+ // Enumerate all GitHub repositories.
166
213
type repo struct {
167
214
RepoID maintner.GitHubRepoID
168
215
Count uint64 // Issues count.
@@ -187,8 +234,36 @@ func (h *handler) serveIndex(w http.ResponseWriter, req *http.Request) {
187
234
return repos [i ].RepoID .String () < repos [j ].RepoID .String ()
188
235
})
189
236
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
+
190
262
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
+ })
192
267
if err != nil {
193
268
log .Println (err )
194
269
}
@@ -207,10 +282,23 @@ func countIssues(r *maintner.GitHubRepo) (uint64, error) {
207
282
return issues , err
208
283
}
209
284
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.
211
299
func (h * handler ) serveIssues (w http.ResponseWriter , req * http.Request , id maintner.GitHubRepoID ) {
212
300
if h .c .GitHub ().Repo (id .Owner , id .Repo ) == nil {
213
- http .Error (w , fmt .Sprintf ("404 Not Found\n \n repository %q not found" , id ), http .StatusNotFound )
301
+ http .Error (w , fmt .Sprintf ("404 Not Found\n \n GitHub repository %q not found" , id ), http .StatusNotFound )
214
302
return
215
303
}
216
304
@@ -221,6 +309,20 @@ func (h *handler) serveIssues(w http.ResponseWriter, req *http.Request, id maint
221
309
h .issuesHandler .ServeHTTP (w , req )
222
310
}
223
311
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 \n Gerrit 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
+
224
326
// stripPrefix returns request r with prefix of length prefixLen stripped from r.URL.Path.
225
327
// prefixLen must not be longer than len(r.URL.Path), otherwise stripPrefix panics.
226
328
// If r.URL.Path is empty after the prefix is stripped, the path is changed to "/".
0 commit comments