##// END OF EJS Templates
hgweb: skip body creation of HEAD for most requests...
Joerg Sonnenberger -
r50741:fda5a4b8 default
parent child Browse files
Show More
@@ -1,515 +1,516 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9
9
10 import contextlib
10 import contextlib
11 import os
11 import os
12
12
13 from .common import (
13 from .common import (
14 ErrorResponse,
14 ErrorResponse,
15 HTTP_BAD_REQUEST,
15 HTTP_BAD_REQUEST,
16 cspvalues,
16 cspvalues,
17 permhooks,
17 permhooks,
18 statusmessage,
18 statusmessage,
19 )
19 )
20 from ..pycompat import getattr
20 from ..pycompat import getattr
21
21
22 from .. import (
22 from .. import (
23 encoding,
23 encoding,
24 error,
24 error,
25 extensions,
25 extensions,
26 formatter,
26 formatter,
27 hg,
27 hg,
28 hook,
28 hook,
29 profiling,
29 profiling,
30 pycompat,
30 pycompat,
31 registrar,
31 registrar,
32 repoview,
32 repoview,
33 templatefilters,
33 templatefilters,
34 templater,
34 templater,
35 templateutil,
35 templateutil,
36 ui as uimod,
36 ui as uimod,
37 util,
37 util,
38 wireprotoserver,
38 wireprotoserver,
39 )
39 )
40
40
41 from . import (
41 from . import (
42 request as requestmod,
42 request as requestmod,
43 webcommands,
43 webcommands,
44 webutil,
44 webutil,
45 wsgicgi,
45 wsgicgi,
46 )
46 )
47
47
48
48
49 def getstyle(req, configfn, templatepath):
49 def getstyle(req, configfn, templatepath):
50 styles = (
50 styles = (
51 req.qsparams.get(b'style', None),
51 req.qsparams.get(b'style', None),
52 configfn(b'web', b'style'),
52 configfn(b'web', b'style'),
53 b'paper',
53 b'paper',
54 )
54 )
55 return styles, _stylemap(styles, templatepath)
55 return styles, _stylemap(styles, templatepath)
56
56
57
57
58 def _stylemap(styles, path=None):
58 def _stylemap(styles, path=None):
59 """Return path to mapfile for a given style.
59 """Return path to mapfile for a given style.
60
60
61 Searches mapfile in the following locations:
61 Searches mapfile in the following locations:
62 1. templatepath/style/map
62 1. templatepath/style/map
63 2. templatepath/map-style
63 2. templatepath/map-style
64 3. templatepath/map
64 3. templatepath/map
65 """
65 """
66
66
67 for style in styles:
67 for style in styles:
68 # only plain name is allowed to honor template paths
68 # only plain name is allowed to honor template paths
69 if (
69 if (
70 not style
70 not style
71 or style in (pycompat.oscurdir, pycompat.ospardir)
71 or style in (pycompat.oscurdir, pycompat.ospardir)
72 or pycompat.ossep in style
72 or pycompat.ossep in style
73 or pycompat.osaltsep
73 or pycompat.osaltsep
74 and pycompat.osaltsep in style
74 and pycompat.osaltsep in style
75 ):
75 ):
76 continue
76 continue
77 locations = (os.path.join(style, b'map'), b'map-' + style, b'map')
77 locations = (os.path.join(style, b'map'), b'map-' + style, b'map')
78
78
79 for location in locations:
79 for location in locations:
80 mapfile, fp = templater.try_open_template(location, path)
80 mapfile, fp = templater.try_open_template(location, path)
81 if mapfile:
81 if mapfile:
82 return style, mapfile, fp
82 return style, mapfile, fp
83
83
84 raise RuntimeError(b"No hgweb templates found in %r" % path)
84 raise RuntimeError(b"No hgweb templates found in %r" % path)
85
85
86
86
87 def makebreadcrumb(url, prefix=b''):
87 def makebreadcrumb(url, prefix=b''):
88 """Return a 'URL breadcrumb' list
88 """Return a 'URL breadcrumb' list
89
89
90 A 'URL breadcrumb' is a list of URL-name pairs,
90 A 'URL breadcrumb' is a list of URL-name pairs,
91 corresponding to each of the path items on a URL.
91 corresponding to each of the path items on a URL.
92 This can be used to create path navigation entries.
92 This can be used to create path navigation entries.
93 """
93 """
94 if url.endswith(b'/'):
94 if url.endswith(b'/'):
95 url = url[:-1]
95 url = url[:-1]
96 if prefix:
96 if prefix:
97 url = b'/' + prefix + url
97 url = b'/' + prefix + url
98 relpath = url
98 relpath = url
99 if relpath.startswith(b'/'):
99 if relpath.startswith(b'/'):
100 relpath = relpath[1:]
100 relpath = relpath[1:]
101
101
102 breadcrumb = []
102 breadcrumb = []
103 urlel = url
103 urlel = url
104 pathitems = [b''] + relpath.split(b'/')
104 pathitems = [b''] + relpath.split(b'/')
105 for pathel in reversed(pathitems):
105 for pathel in reversed(pathitems):
106 if not pathel or not urlel:
106 if not pathel or not urlel:
107 break
107 break
108 breadcrumb.append({b'url': urlel, b'name': pathel})
108 breadcrumb.append({b'url': urlel, b'name': pathel})
109 urlel = os.path.dirname(urlel)
109 urlel = os.path.dirname(urlel)
110 return templateutil.mappinglist(reversed(breadcrumb))
110 return templateutil.mappinglist(reversed(breadcrumb))
111
111
112
112
113 class requestcontext:
113 class requestcontext:
114 """Holds state/context for an individual request.
114 """Holds state/context for an individual request.
115
115
116 Servers can be multi-threaded. Holding state on the WSGI application
116 Servers can be multi-threaded. Holding state on the WSGI application
117 is prone to race conditions. Instances of this class exist to hold
117 is prone to race conditions. Instances of this class exist to hold
118 mutable and race-free state for requests.
118 mutable and race-free state for requests.
119 """
119 """
120
120
121 def __init__(self, app, repo, req, res):
121 def __init__(self, app, repo, req, res):
122 self.repo = repo
122 self.repo = repo
123 self.reponame = app.reponame
123 self.reponame = app.reponame
124 self.req = req
124 self.req = req
125 self.res = res
125 self.res = res
126
126
127 self.maxchanges = self.configint(b'web', b'maxchanges')
127 self.maxchanges = self.configint(b'web', b'maxchanges')
128 self.stripecount = self.configint(b'web', b'stripes')
128 self.stripecount = self.configint(b'web', b'stripes')
129 self.maxshortchanges = self.configint(b'web', b'maxshortchanges')
129 self.maxshortchanges = self.configint(b'web', b'maxshortchanges')
130 self.maxfiles = self.configint(b'web', b'maxfiles')
130 self.maxfiles = self.configint(b'web', b'maxfiles')
131 self.allowpull = self.configbool(b'web', b'allow-pull')
131 self.allowpull = self.configbool(b'web', b'allow-pull')
132
132
133 # we use untrusted=False to prevent a repo owner from using
133 # we use untrusted=False to prevent a repo owner from using
134 # web.templates in .hg/hgrc to get access to any file readable
134 # web.templates in .hg/hgrc to get access to any file readable
135 # by the user running the CGI script
135 # by the user running the CGI script
136 self.templatepath = self.config(b'web', b'templates', untrusted=False)
136 self.templatepath = self.config(b'web', b'templates', untrusted=False)
137
137
138 # This object is more expensive to build than simple config values.
138 # This object is more expensive to build than simple config values.
139 # It is shared across requests. The app will replace the object
139 # It is shared across requests. The app will replace the object
140 # if it is updated. Since this is a reference and nothing should
140 # if it is updated. Since this is a reference and nothing should
141 # modify the underlying object, it should be constant for the lifetime
141 # modify the underlying object, it should be constant for the lifetime
142 # of the request.
142 # of the request.
143 self.websubtable = app.websubtable
143 self.websubtable = app.websubtable
144
144
145 self.csp, self.nonce = cspvalues(self.repo.ui)
145 self.csp, self.nonce = cspvalues(self.repo.ui)
146
146
147 # Trust the settings from the .hg/hgrc files by default.
147 # Trust the settings from the .hg/hgrc files by default.
148 def config(self, *args, **kwargs):
148 def config(self, *args, **kwargs):
149 kwargs.setdefault('untrusted', True)
149 kwargs.setdefault('untrusted', True)
150 return self.repo.ui.config(*args, **kwargs)
150 return self.repo.ui.config(*args, **kwargs)
151
151
152 def configbool(self, *args, **kwargs):
152 def configbool(self, *args, **kwargs):
153 kwargs.setdefault('untrusted', True)
153 kwargs.setdefault('untrusted', True)
154 return self.repo.ui.configbool(*args, **kwargs)
154 return self.repo.ui.configbool(*args, **kwargs)
155
155
156 def configint(self, *args, **kwargs):
156 def configint(self, *args, **kwargs):
157 kwargs.setdefault('untrusted', True)
157 kwargs.setdefault('untrusted', True)
158 return self.repo.ui.configint(*args, **kwargs)
158 return self.repo.ui.configint(*args, **kwargs)
159
159
160 def configlist(self, *args, **kwargs):
160 def configlist(self, *args, **kwargs):
161 kwargs.setdefault('untrusted', True)
161 kwargs.setdefault('untrusted', True)
162 return self.repo.ui.configlist(*args, **kwargs)
162 return self.repo.ui.configlist(*args, **kwargs)
163
163
164 def archivelist(self, nodeid):
164 def archivelist(self, nodeid):
165 return webutil.archivelist(self.repo.ui, nodeid)
165 return webutil.archivelist(self.repo.ui, nodeid)
166
166
167 def templater(self, req):
167 def templater(self, req):
168 # determine scheme, port and server name
168 # determine scheme, port and server name
169 # this is needed to create absolute urls
169 # this is needed to create absolute urls
170 logourl = self.config(b'web', b'logourl')
170 logourl = self.config(b'web', b'logourl')
171 logoimg = self.config(b'web', b'logoimg')
171 logoimg = self.config(b'web', b'logoimg')
172 staticurl = (
172 staticurl = (
173 self.config(b'web', b'staticurl')
173 self.config(b'web', b'staticurl')
174 or req.apppath.rstrip(b'/') + b'/static/'
174 or req.apppath.rstrip(b'/') + b'/static/'
175 )
175 )
176 if not staticurl.endswith(b'/'):
176 if not staticurl.endswith(b'/'):
177 staticurl += b'/'
177 staticurl += b'/'
178
178
179 # figure out which style to use
179 # figure out which style to use
180
180
181 vars = {}
181 vars = {}
182 styles, (style, mapfile, fp) = getstyle(
182 styles, (style, mapfile, fp) = getstyle(
183 req, self.config, self.templatepath
183 req, self.config, self.templatepath
184 )
184 )
185 if style == styles[0]:
185 if style == styles[0]:
186 vars[b'style'] = style
186 vars[b'style'] = style
187
187
188 sessionvars = webutil.sessionvars(vars, b'?')
188 sessionvars = webutil.sessionvars(vars, b'?')
189
189
190 if not self.reponame:
190 if not self.reponame:
191 self.reponame = (
191 self.reponame = (
192 self.config(b'web', b'name', b'')
192 self.config(b'web', b'name', b'')
193 or req.reponame
193 or req.reponame
194 or req.apppath
194 or req.apppath
195 or self.repo.root
195 or self.repo.root
196 )
196 )
197
197
198 filters = {}
198 filters = {}
199 templatefilter = registrar.templatefilter(filters)
199 templatefilter = registrar.templatefilter(filters)
200
200
201 @templatefilter(b'websub', intype=bytes)
201 @templatefilter(b'websub', intype=bytes)
202 def websubfilter(text):
202 def websubfilter(text):
203 return templatefilters.websub(text, self.websubtable)
203 return templatefilters.websub(text, self.websubtable)
204
204
205 # create the templater
205 # create the templater
206 # TODO: export all keywords: defaults = templatekw.keywords.copy()
206 # TODO: export all keywords: defaults = templatekw.keywords.copy()
207 defaults = {
207 defaults = {
208 b'url': req.apppath + b'/',
208 b'url': req.apppath + b'/',
209 b'logourl': logourl,
209 b'logourl': logourl,
210 b'logoimg': logoimg,
210 b'logoimg': logoimg,
211 b'staticurl': staticurl,
211 b'staticurl': staticurl,
212 b'urlbase': req.advertisedbaseurl,
212 b'urlbase': req.advertisedbaseurl,
213 b'repo': self.reponame,
213 b'repo': self.reponame,
214 b'encoding': encoding.encoding,
214 b'encoding': encoding.encoding,
215 b'sessionvars': sessionvars,
215 b'sessionvars': sessionvars,
216 b'pathdef': makebreadcrumb(req.apppath),
216 b'pathdef': makebreadcrumb(req.apppath),
217 b'style': style,
217 b'style': style,
218 b'nonce': self.nonce,
218 b'nonce': self.nonce,
219 }
219 }
220 templatekeyword = registrar.templatekeyword(defaults)
220 templatekeyword = registrar.templatekeyword(defaults)
221
221
222 @templatekeyword(b'motd', requires=())
222 @templatekeyword(b'motd', requires=())
223 def motd(context, mapping):
223 def motd(context, mapping):
224 yield self.config(b'web', b'motd')
224 yield self.config(b'web', b'motd')
225
225
226 tres = formatter.templateresources(self.repo.ui, self.repo)
226 tres = formatter.templateresources(self.repo.ui, self.repo)
227 return templater.templater.frommapfile(
227 return templater.templater.frommapfile(
228 mapfile, fp=fp, filters=filters, defaults=defaults, resources=tres
228 mapfile, fp=fp, filters=filters, defaults=defaults, resources=tres
229 )
229 )
230
230
231 def sendtemplate(self, name, **kwargs):
231 def sendtemplate(self, name, **kwargs):
232 """Helper function to send a response generated from a template."""
232 """Helper function to send a response generated from a template."""
233 kwargs = pycompat.byteskwargs(kwargs)
233 if self.req.method != b'HEAD':
234 self.res.setbodygen(self.tmpl.generate(name, kwargs))
234 kwargs = pycompat.byteskwargs(kwargs)
235 self.res.setbodygen(self.tmpl.generate(name, kwargs))
235 return self.res.sendresponse()
236 return self.res.sendresponse()
236
237
237
238
238 class hgweb:
239 class hgweb:
239 """HTTP server for individual repositories.
240 """HTTP server for individual repositories.
240
241
241 Instances of this class serve HTTP responses for a particular
242 Instances of this class serve HTTP responses for a particular
242 repository.
243 repository.
243
244
244 Instances are typically used as WSGI applications.
245 Instances are typically used as WSGI applications.
245
246
246 Some servers are multi-threaded. On these servers, there may
247 Some servers are multi-threaded. On these servers, there may
247 be multiple active threads inside __call__.
248 be multiple active threads inside __call__.
248 """
249 """
249
250
250 def __init__(self, repo, name=None, baseui=None):
251 def __init__(self, repo, name=None, baseui=None):
251 if isinstance(repo, bytes):
252 if isinstance(repo, bytes):
252 if baseui:
253 if baseui:
253 u = baseui.copy()
254 u = baseui.copy()
254 else:
255 else:
255 u = uimod.ui.load()
256 u = uimod.ui.load()
256 extensions.loadall(u)
257 extensions.loadall(u)
257 extensions.populateui(u)
258 extensions.populateui(u)
258 r = hg.repository(u, repo)
259 r = hg.repository(u, repo)
259 else:
260 else:
260 # we trust caller to give us a private copy
261 # we trust caller to give us a private copy
261 r = repo
262 r = repo
262
263
263 r.ui.setconfig(b'ui', b'report_untrusted', b'off', b'hgweb')
264 r.ui.setconfig(b'ui', b'report_untrusted', b'off', b'hgweb')
264 r.baseui.setconfig(b'ui', b'report_untrusted', b'off', b'hgweb')
265 r.baseui.setconfig(b'ui', b'report_untrusted', b'off', b'hgweb')
265 r.ui.setconfig(b'ui', b'nontty', b'true', b'hgweb')
266 r.ui.setconfig(b'ui', b'nontty', b'true', b'hgweb')
266 r.baseui.setconfig(b'ui', b'nontty', b'true', b'hgweb')
267 r.baseui.setconfig(b'ui', b'nontty', b'true', b'hgweb')
267 # resolve file patterns relative to repo root
268 # resolve file patterns relative to repo root
268 r.ui.setconfig(b'ui', b'forcecwd', r.root, b'hgweb')
269 r.ui.setconfig(b'ui', b'forcecwd', r.root, b'hgweb')
269 r.baseui.setconfig(b'ui', b'forcecwd', r.root, b'hgweb')
270 r.baseui.setconfig(b'ui', b'forcecwd', r.root, b'hgweb')
270 # it's unlikely that we can replace signal handlers in WSGI server,
271 # it's unlikely that we can replace signal handlers in WSGI server,
271 # and mod_wsgi issues a big warning. a plain hgweb process (with no
272 # and mod_wsgi issues a big warning. a plain hgweb process (with no
272 # threading) could replace signal handlers, but we don't bother
273 # threading) could replace signal handlers, but we don't bother
273 # conditionally enabling it.
274 # conditionally enabling it.
274 r.ui.setconfig(b'ui', b'signal-safe-lock', b'false', b'hgweb')
275 r.ui.setconfig(b'ui', b'signal-safe-lock', b'false', b'hgweb')
275 r.baseui.setconfig(b'ui', b'signal-safe-lock', b'false', b'hgweb')
276 r.baseui.setconfig(b'ui', b'signal-safe-lock', b'false', b'hgweb')
276 # displaying bundling progress bar while serving feel wrong and may
277 # displaying bundling progress bar while serving feel wrong and may
277 # break some wsgi implementation.
278 # break some wsgi implementation.
278 r.ui.setconfig(b'progress', b'disable', b'true', b'hgweb')
279 r.ui.setconfig(b'progress', b'disable', b'true', b'hgweb')
279 r.baseui.setconfig(b'progress', b'disable', b'true', b'hgweb')
280 r.baseui.setconfig(b'progress', b'disable', b'true', b'hgweb')
280 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
281 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
281 self._lastrepo = self._repos[0]
282 self._lastrepo = self._repos[0]
282 hook.redirect(True)
283 hook.redirect(True)
283 self.reponame = name
284 self.reponame = name
284
285
285 def _webifyrepo(self, repo):
286 def _webifyrepo(self, repo):
286 repo = getwebview(repo)
287 repo = getwebview(repo)
287 self.websubtable = webutil.getwebsubs(repo)
288 self.websubtable = webutil.getwebsubs(repo)
288 return repo
289 return repo
289
290
290 @contextlib.contextmanager
291 @contextlib.contextmanager
291 def _obtainrepo(self):
292 def _obtainrepo(self):
292 """Obtain a repo unique to the caller.
293 """Obtain a repo unique to the caller.
293
294
294 Internally we maintain a stack of cachedlocalrepo instances
295 Internally we maintain a stack of cachedlocalrepo instances
295 to be handed out. If one is available, we pop it and return it,
296 to be handed out. If one is available, we pop it and return it,
296 ensuring it is up to date in the process. If one is not available,
297 ensuring it is up to date in the process. If one is not available,
297 we clone the most recently used repo instance and return it.
298 we clone the most recently used repo instance and return it.
298
299
299 It is currently possible for the stack to grow without bounds
300 It is currently possible for the stack to grow without bounds
300 if the server allows infinite threads. However, servers should
301 if the server allows infinite threads. However, servers should
301 have a thread limit, thus establishing our limit.
302 have a thread limit, thus establishing our limit.
302 """
303 """
303 if self._repos:
304 if self._repos:
304 cached = self._repos.pop()
305 cached = self._repos.pop()
305 r, created = cached.fetch()
306 r, created = cached.fetch()
306 else:
307 else:
307 cached = self._lastrepo.copy()
308 cached = self._lastrepo.copy()
308 r, created = cached.fetch()
309 r, created = cached.fetch()
309 if created:
310 if created:
310 r = self._webifyrepo(r)
311 r = self._webifyrepo(r)
311
312
312 self._lastrepo = cached
313 self._lastrepo = cached
313 self.mtime = cached.mtime
314 self.mtime = cached.mtime
314 try:
315 try:
315 yield r
316 yield r
316 finally:
317 finally:
317 self._repos.append(cached)
318 self._repos.append(cached)
318
319
319 def run(self):
320 def run(self):
320 """Start a server from CGI environment.
321 """Start a server from CGI environment.
321
322
322 Modern servers should be using WSGI and should avoid this
323 Modern servers should be using WSGI and should avoid this
323 method, if possible.
324 method, if possible.
324 """
325 """
325 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
326 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
326 b"CGI/1."
327 b"CGI/1."
327 ):
328 ):
328 raise RuntimeError(
329 raise RuntimeError(
329 b"This function is only intended to be "
330 b"This function is only intended to be "
330 b"called while running as a CGI script."
331 b"called while running as a CGI script."
331 )
332 )
332 wsgicgi.launch(self)
333 wsgicgi.launch(self)
333
334
334 def __call__(self, env, respond):
335 def __call__(self, env, respond):
335 """Run the WSGI application.
336 """Run the WSGI application.
336
337
337 This may be called by multiple threads.
338 This may be called by multiple threads.
338 """
339 """
339 req = requestmod.parserequestfromenv(env)
340 req = requestmod.parserequestfromenv(env)
340 res = requestmod.wsgiresponse(req, respond)
341 res = requestmod.wsgiresponse(req, respond)
341
342
342 return self.run_wsgi(req, res)
343 return self.run_wsgi(req, res)
343
344
344 def run_wsgi(self, req, res):
345 def run_wsgi(self, req, res):
345 """Internal method to run the WSGI application.
346 """Internal method to run the WSGI application.
346
347
347 This is typically only called by Mercurial. External consumers
348 This is typically only called by Mercurial. External consumers
348 should be using instances of this class as the WSGI application.
349 should be using instances of this class as the WSGI application.
349 """
350 """
350 with self._obtainrepo() as repo:
351 with self._obtainrepo() as repo:
351 profile = repo.ui.configbool(b'profiling', b'enabled')
352 profile = repo.ui.configbool(b'profiling', b'enabled')
352 with profiling.profile(repo.ui, enabled=profile):
353 with profiling.profile(repo.ui, enabled=profile):
353 for r in self._runwsgi(req, res, repo):
354 for r in self._runwsgi(req, res, repo):
354 yield r
355 yield r
355
356
356 def _runwsgi(self, req, res, repo):
357 def _runwsgi(self, req, res, repo):
357 rctx = requestcontext(self, repo, req, res)
358 rctx = requestcontext(self, repo, req, res)
358
359
359 # This state is global across all threads.
360 # This state is global across all threads.
360 encoding.encoding = rctx.config(b'web', b'encoding')
361 encoding.encoding = rctx.config(b'web', b'encoding')
361 rctx.repo.ui.environ = req.rawenv
362 rctx.repo.ui.environ = req.rawenv
362
363
363 if rctx.csp:
364 if rctx.csp:
364 # hgwebdir may have added CSP header. Since we generate our own,
365 # hgwebdir may have added CSP header. Since we generate our own,
365 # replace it.
366 # replace it.
366 res.headers[b'Content-Security-Policy'] = rctx.csp
367 res.headers[b'Content-Security-Policy'] = rctx.csp
367
368
368 handled = wireprotoserver.handlewsgirequest(
369 handled = wireprotoserver.handlewsgirequest(
369 rctx, req, res, self.check_perm
370 rctx, req, res, self.check_perm
370 )
371 )
371 if handled:
372 if handled:
372 return res.sendresponse()
373 return res.sendresponse()
373
374
374 # Old implementations of hgweb supported dispatching the request via
375 # Old implementations of hgweb supported dispatching the request via
375 # the initial query string parameter instead of using PATH_INFO.
376 # the initial query string parameter instead of using PATH_INFO.
376 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
377 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
377 # a value), we use it. Otherwise fall back to the query string.
378 # a value), we use it. Otherwise fall back to the query string.
378 if req.dispatchpath is not None:
379 if req.dispatchpath is not None:
379 query = req.dispatchpath
380 query = req.dispatchpath
380 else:
381 else:
381 query = req.querystring.partition(b'&')[0].partition(b';')[0]
382 query = req.querystring.partition(b'&')[0].partition(b';')[0]
382
383
383 # translate user-visible url structure to internal structure
384 # translate user-visible url structure to internal structure
384
385
385 args = query.split(b'/', 2)
386 args = query.split(b'/', 2)
386 if b'cmd' not in req.qsparams and args and args[0]:
387 if b'cmd' not in req.qsparams and args and args[0]:
387 cmd = args.pop(0)
388 cmd = args.pop(0)
388 style = cmd.rfind(b'-')
389 style = cmd.rfind(b'-')
389 if style != -1:
390 if style != -1:
390 req.qsparams[b'style'] = cmd[:style]
391 req.qsparams[b'style'] = cmd[:style]
391 cmd = cmd[style + 1 :]
392 cmd = cmd[style + 1 :]
392
393
393 # avoid accepting e.g. style parameter as command
394 # avoid accepting e.g. style parameter as command
394 if util.safehasattr(webcommands, cmd):
395 if util.safehasattr(webcommands, cmd):
395 req.qsparams[b'cmd'] = cmd
396 req.qsparams[b'cmd'] = cmd
396
397
397 if cmd == b'static':
398 if cmd == b'static':
398 req.qsparams[b'file'] = b'/'.join(args)
399 req.qsparams[b'file'] = b'/'.join(args)
399 else:
400 else:
400 if args and args[0]:
401 if args and args[0]:
401 node = args.pop(0).replace(b'%2F', b'/')
402 node = args.pop(0).replace(b'%2F', b'/')
402 req.qsparams[b'node'] = node
403 req.qsparams[b'node'] = node
403 if args:
404 if args:
404 if b'file' in req.qsparams:
405 if b'file' in req.qsparams:
405 del req.qsparams[b'file']
406 del req.qsparams[b'file']
406 for a in args:
407 for a in args:
407 req.qsparams.add(b'file', a)
408 req.qsparams.add(b'file', a)
408
409
409 ua = req.headers.get(b'User-Agent', b'')
410 ua = req.headers.get(b'User-Agent', b'')
410 if cmd == b'rev' and b'mercurial' in ua:
411 if cmd == b'rev' and b'mercurial' in ua:
411 req.qsparams[b'style'] = b'raw'
412 req.qsparams[b'style'] = b'raw'
412
413
413 if cmd == b'archive':
414 if cmd == b'archive':
414 fn = req.qsparams[b'node']
415 fn = req.qsparams[b'node']
415 for type_, spec in webutil.archivespecs.items():
416 for type_, spec in webutil.archivespecs.items():
416 ext = spec[2]
417 ext = spec[2]
417 if fn.endswith(ext):
418 if fn.endswith(ext):
418 req.qsparams[b'node'] = fn[: -len(ext)]
419 req.qsparams[b'node'] = fn[: -len(ext)]
419 req.qsparams[b'type'] = type_
420 req.qsparams[b'type'] = type_
420 else:
421 else:
421 cmd = req.qsparams.get(b'cmd', b'')
422 cmd = req.qsparams.get(b'cmd', b'')
422
423
423 # process the web interface request
424 # process the web interface request
424
425
425 try:
426 try:
426 rctx.tmpl = rctx.templater(req)
427 rctx.tmpl = rctx.templater(req)
427 ctype = rctx.tmpl.render(
428 ctype = rctx.tmpl.render(
428 b'mimetype', {b'encoding': encoding.encoding}
429 b'mimetype', {b'encoding': encoding.encoding}
429 )
430 )
430
431
431 # check read permissions non-static content
432 # check read permissions non-static content
432 if cmd != b'static':
433 if cmd != b'static':
433 self.check_perm(rctx, req, None)
434 self.check_perm(rctx, req, None)
434
435
435 if cmd == b'':
436 if cmd == b'':
436 req.qsparams[b'cmd'] = rctx.tmpl.render(b'default', {})
437 req.qsparams[b'cmd'] = rctx.tmpl.render(b'default', {})
437 cmd = req.qsparams[b'cmd']
438 cmd = req.qsparams[b'cmd']
438
439
439 # Don't enable caching if using a CSP nonce because then it wouldn't
440 # Don't enable caching if using a CSP nonce because then it wouldn't
440 # be a nonce.
441 # be a nonce.
441 if rctx.configbool(b'web', b'cache') and not rctx.nonce:
442 if rctx.configbool(b'web', b'cache') and not rctx.nonce:
442 tag = b'W/"%d"' % self.mtime
443 tag = b'W/"%d"' % self.mtime
443 if req.headers.get(b'If-None-Match') == tag:
444 if req.headers.get(b'If-None-Match') == tag:
444 res.status = b'304 Not Modified'
445 res.status = b'304 Not Modified'
445 # Content-Type may be defined globally. It isn't valid on a
446 # Content-Type may be defined globally. It isn't valid on a
446 # 304, so discard it.
447 # 304, so discard it.
447 try:
448 try:
448 del res.headers[b'Content-Type']
449 del res.headers[b'Content-Type']
449 except KeyError:
450 except KeyError:
450 pass
451 pass
451 # Response body not allowed on 304.
452 # Response body not allowed on 304.
452 res.setbodybytes(b'')
453 res.setbodybytes(b'')
453 return res.sendresponse()
454 return res.sendresponse()
454
455
455 res.headers[b'ETag'] = tag
456 res.headers[b'ETag'] = tag
456
457
457 if cmd not in webcommands.__all__:
458 if cmd not in webcommands.__all__:
458 msg = b'no such method: %s' % cmd
459 msg = b'no such method: %s' % cmd
459 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
460 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
460 else:
461 else:
461 # Set some globals appropriate for web handlers. Commands can
462 # Set some globals appropriate for web handlers. Commands can
462 # override easily enough.
463 # override easily enough.
463 res.status = b'200 Script output follows'
464 res.status = b'200 Script output follows'
464 res.headers[b'Content-Type'] = ctype
465 res.headers[b'Content-Type'] = ctype
465 return getattr(webcommands, cmd)(rctx)
466 return getattr(webcommands, cmd)(rctx)
466
467
467 except (error.LookupError, error.RepoLookupError) as err:
468 except (error.LookupError, error.RepoLookupError) as err:
468 msg = pycompat.bytestr(err)
469 msg = pycompat.bytestr(err)
469 if util.safehasattr(err, b'name') and not isinstance(
470 if util.safehasattr(err, b'name') and not isinstance(
470 err, error.ManifestLookupError
471 err, error.ManifestLookupError
471 ):
472 ):
472 msg = b'revision not found: %s' % err.name
473 msg = b'revision not found: %s' % err.name
473
474
474 res.status = b'404 Not Found'
475 res.status = b'404 Not Found'
475 res.headers[b'Content-Type'] = ctype
476 res.headers[b'Content-Type'] = ctype
476 return rctx.sendtemplate(b'error', error=msg)
477 return rctx.sendtemplate(b'error', error=msg)
477 except (error.RepoError, error.StorageError) as e:
478 except (error.RepoError, error.StorageError) as e:
478 res.status = b'500 Internal Server Error'
479 res.status = b'500 Internal Server Error'
479 res.headers[b'Content-Type'] = ctype
480 res.headers[b'Content-Type'] = ctype
480 return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
481 return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
481 except error.Abort as e:
482 except error.Abort as e:
482 res.status = b'403 Forbidden'
483 res.status = b'403 Forbidden'
483 res.headers[b'Content-Type'] = ctype
484 res.headers[b'Content-Type'] = ctype
484 return rctx.sendtemplate(b'error', error=e.message)
485 return rctx.sendtemplate(b'error', error=e.message)
485 except ErrorResponse as e:
486 except ErrorResponse as e:
486 for k, v in e.headers:
487 for k, v in e.headers:
487 res.headers[k] = v
488 res.headers[k] = v
488 res.status = statusmessage(e.code, pycompat.bytestr(e))
489 res.status = statusmessage(e.code, pycompat.bytestr(e))
489 res.headers[b'Content-Type'] = ctype
490 res.headers[b'Content-Type'] = ctype
490 return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
491 return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
491
492
492 def check_perm(self, rctx, req, op):
493 def check_perm(self, rctx, req, op):
493 for permhook in permhooks:
494 for permhook in permhooks:
494 permhook(rctx, req, op)
495 permhook(rctx, req, op)
495
496
496
497
497 def getwebview(repo):
498 def getwebview(repo):
498 """The 'web.view' config controls changeset filter to hgweb. Possible
499 """The 'web.view' config controls changeset filter to hgweb. Possible
499 values are ``served``, ``visible`` and ``all``. Default is ``served``.
500 values are ``served``, ``visible`` and ``all``. Default is ``served``.
500 The ``served`` filter only shows changesets that can be pulled from the
501 The ``served`` filter only shows changesets that can be pulled from the
501 hgweb instance. The``visible`` filter includes secret changesets but
502 hgweb instance. The``visible`` filter includes secret changesets but
502 still excludes "hidden" one.
503 still excludes "hidden" one.
503
504
504 See the repoview module for details.
505 See the repoview module for details.
505
506
506 The option has been around undocumented since Mercurial 2.5, but no
507 The option has been around undocumented since Mercurial 2.5, but no
507 user ever asked about it. So we better keep it undocumented for now."""
508 user ever asked about it. So we better keep it undocumented for now."""
508 # experimental config: web.view
509 # experimental config: web.view
509 viewconfig = repo.ui.config(b'web', b'view', untrusted=True)
510 viewconfig = repo.ui.config(b'web', b'view', untrusted=True)
510 if viewconfig == b'all':
511 if viewconfig == b'all':
511 return repo.unfiltered()
512 return repo.unfiltered()
512 elif viewconfig in repoview.filtertable:
513 elif viewconfig in repoview.filtertable:
513 return repo.filtered(viewconfig)
514 return repo.filtered(viewconfig)
514 else:
515 else:
515 return repo.filtered(b'served')
516 return repo.filtered(b'served')
@@ -1,632 +1,635 b''
1 # hgweb/request.py - An http request from either CGI or the standalone server.
1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9
9
10 # import wsgiref.validate
10 # import wsgiref.validate
11
11
12 from ..thirdparty import attr
12 from ..thirdparty import attr
13 from .. import (
13 from .. import (
14 encoding,
14 encoding,
15 error,
15 error,
16 pycompat,
16 pycompat,
17 util,
17 util,
18 )
18 )
19 from ..utils import (
19 from ..utils import (
20 urlutil,
20 urlutil,
21 )
21 )
22
22
23
23
24 class multidict:
24 class multidict:
25 """A dict like object that can store multiple values for a key.
25 """A dict like object that can store multiple values for a key.
26
26
27 Used to store parsed request parameters.
27 Used to store parsed request parameters.
28
28
29 This is inspired by WebOb's class of the same name.
29 This is inspired by WebOb's class of the same name.
30 """
30 """
31
31
32 def __init__(self):
32 def __init__(self):
33 self._items = {}
33 self._items = {}
34
34
35 def __getitem__(self, key):
35 def __getitem__(self, key):
36 """Returns the last set value for a key."""
36 """Returns the last set value for a key."""
37 return self._items[key][-1]
37 return self._items[key][-1]
38
38
39 def __setitem__(self, key, value):
39 def __setitem__(self, key, value):
40 """Replace a values for a key with a new value."""
40 """Replace a values for a key with a new value."""
41 self._items[key] = [value]
41 self._items[key] = [value]
42
42
43 def __delitem__(self, key):
43 def __delitem__(self, key):
44 """Delete all values for a key."""
44 """Delete all values for a key."""
45 del self._items[key]
45 del self._items[key]
46
46
47 def __contains__(self, key):
47 def __contains__(self, key):
48 return key in self._items
48 return key in self._items
49
49
50 def __len__(self):
50 def __len__(self):
51 return len(self._items)
51 return len(self._items)
52
52
53 def get(self, key, default=None):
53 def get(self, key, default=None):
54 try:
54 try:
55 return self.__getitem__(key)
55 return self.__getitem__(key)
56 except KeyError:
56 except KeyError:
57 return default
57 return default
58
58
59 def add(self, key, value):
59 def add(self, key, value):
60 """Add a new value for a key. Does not replace existing values."""
60 """Add a new value for a key. Does not replace existing values."""
61 self._items.setdefault(key, []).append(value)
61 self._items.setdefault(key, []).append(value)
62
62
63 def getall(self, key):
63 def getall(self, key):
64 """Obtains all values for a key."""
64 """Obtains all values for a key."""
65 return self._items.get(key, [])
65 return self._items.get(key, [])
66
66
67 def getone(self, key):
67 def getone(self, key):
68 """Obtain a single value for a key.
68 """Obtain a single value for a key.
69
69
70 Raises KeyError if key not defined or it has multiple values set.
70 Raises KeyError if key not defined or it has multiple values set.
71 """
71 """
72 vals = self._items[key]
72 vals = self._items[key]
73
73
74 if len(vals) > 1:
74 if len(vals) > 1:
75 raise KeyError(b'multiple values for %r' % key)
75 raise KeyError(b'multiple values for %r' % key)
76
76
77 return vals[0]
77 return vals[0]
78
78
79 def asdictoflists(self):
79 def asdictoflists(self):
80 return {k: list(v) for k, v in self._items.items()}
80 return {k: list(v) for k, v in self._items.items()}
81
81
82
82
83 @attr.s(frozen=True)
83 @attr.s(frozen=True)
84 class parsedrequest:
84 class parsedrequest:
85 """Represents a parsed WSGI request.
85 """Represents a parsed WSGI request.
86
86
87 Contains both parsed parameters as well as a handle on the input stream.
87 Contains both parsed parameters as well as a handle on the input stream.
88 """
88 """
89
89
90 # Request method.
90 # Request method.
91 method = attr.ib()
91 method = attr.ib()
92 # Full URL for this request.
92 # Full URL for this request.
93 url = attr.ib()
93 url = attr.ib()
94 # URL without any path components. Just <proto>://<host><port>.
94 # URL without any path components. Just <proto>://<host><port>.
95 baseurl = attr.ib()
95 baseurl = attr.ib()
96 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
96 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
97 # of HTTP: Host header for hostname. This is likely what clients used.
97 # of HTTP: Host header for hostname. This is likely what clients used.
98 advertisedurl = attr.ib()
98 advertisedurl = attr.ib()
99 advertisedbaseurl = attr.ib()
99 advertisedbaseurl = attr.ib()
100 # URL scheme (part before ``://``). e.g. ``http`` or ``https``.
100 # URL scheme (part before ``://``). e.g. ``http`` or ``https``.
101 urlscheme = attr.ib()
101 urlscheme = attr.ib()
102 # Value of REMOTE_USER, if set, or None.
102 # Value of REMOTE_USER, if set, or None.
103 remoteuser = attr.ib()
103 remoteuser = attr.ib()
104 # Value of REMOTE_HOST, if set, or None.
104 # Value of REMOTE_HOST, if set, or None.
105 remotehost = attr.ib()
105 remotehost = attr.ib()
106 # Relative WSGI application path. If defined, will begin with a
106 # Relative WSGI application path. If defined, will begin with a
107 # ``/``.
107 # ``/``.
108 apppath = attr.ib()
108 apppath = attr.ib()
109 # List of path parts to be used for dispatch.
109 # List of path parts to be used for dispatch.
110 dispatchparts = attr.ib()
110 dispatchparts = attr.ib()
111 # URL path component (no query string) used for dispatch. Can be
111 # URL path component (no query string) used for dispatch. Can be
112 # ``None`` to signal no path component given to the request, an
112 # ``None`` to signal no path component given to the request, an
113 # empty string to signal a request to the application's root URL,
113 # empty string to signal a request to the application's root URL,
114 # or a string not beginning with ``/`` containing the requested
114 # or a string not beginning with ``/`` containing the requested
115 # path under the application.
115 # path under the application.
116 dispatchpath = attr.ib()
116 dispatchpath = attr.ib()
117 # The name of the repository being accessed.
117 # The name of the repository being accessed.
118 reponame = attr.ib()
118 reponame = attr.ib()
119 # Raw query string (part after "?" in URL).
119 # Raw query string (part after "?" in URL).
120 querystring = attr.ib()
120 querystring = attr.ib()
121 # multidict of query string parameters.
121 # multidict of query string parameters.
122 qsparams = attr.ib()
122 qsparams = attr.ib()
123 # wsgiref.headers.Headers instance. Operates like a dict with case
123 # wsgiref.headers.Headers instance. Operates like a dict with case
124 # insensitive keys.
124 # insensitive keys.
125 headers = attr.ib()
125 headers = attr.ib()
126 # Request body input stream.
126 # Request body input stream.
127 bodyfh = attr.ib()
127 bodyfh = attr.ib()
128 # WSGI environment dict, unmodified.
128 # WSGI environment dict, unmodified.
129 rawenv = attr.ib()
129 rawenv = attr.ib()
130
130
131
131
132 def parserequestfromenv(env, reponame=None, altbaseurl=None, bodyfh=None):
132 def parserequestfromenv(env, reponame=None, altbaseurl=None, bodyfh=None):
133 """Parse URL components from environment variables.
133 """Parse URL components from environment variables.
134
134
135 WSGI defines request attributes via environment variables. This function
135 WSGI defines request attributes via environment variables. This function
136 parses the environment variables into a data structure.
136 parses the environment variables into a data structure.
137
137
138 If ``reponame`` is defined, the leading path components matching that
138 If ``reponame`` is defined, the leading path components matching that
139 string are effectively shifted from ``PATH_INFO`` to ``SCRIPT_NAME``.
139 string are effectively shifted from ``PATH_INFO`` to ``SCRIPT_NAME``.
140 This simulates the world view of a WSGI application that processes
140 This simulates the world view of a WSGI application that processes
141 requests from the base URL of a repo.
141 requests from the base URL of a repo.
142
142
143 If ``altbaseurl`` (typically comes from ``web.baseurl`` config option)
143 If ``altbaseurl`` (typically comes from ``web.baseurl`` config option)
144 is defined, it is used - instead of the WSGI environment variables - for
144 is defined, it is used - instead of the WSGI environment variables - for
145 constructing URL components up to and including the WSGI application path.
145 constructing URL components up to and including the WSGI application path.
146 For example, if the current WSGI application is at ``/repo`` and a request
146 For example, if the current WSGI application is at ``/repo`` and a request
147 is made to ``/rev/@`` with this argument set to
147 is made to ``/rev/@`` with this argument set to
148 ``http://myserver:9000/prefix``, the URL and path components will resolve as
148 ``http://myserver:9000/prefix``, the URL and path components will resolve as
149 if the request were to ``http://myserver:9000/prefix/rev/@``. In other
149 if the request were to ``http://myserver:9000/prefix/rev/@``. In other
150 words, ``wsgi.url_scheme``, ``SERVER_NAME``, ``SERVER_PORT``, and
150 words, ``wsgi.url_scheme``, ``SERVER_NAME``, ``SERVER_PORT``, and
151 ``SCRIPT_NAME`` are all effectively replaced by components from this URL.
151 ``SCRIPT_NAME`` are all effectively replaced by components from this URL.
152
152
153 ``bodyfh`` can be used to specify a file object to read the request body
153 ``bodyfh`` can be used to specify a file object to read the request body
154 from. If not defined, ``wsgi.input`` from the environment dict is used.
154 from. If not defined, ``wsgi.input`` from the environment dict is used.
155 """
155 """
156 # PEP 3333 defines the WSGI spec and is a useful reference for this code.
156 # PEP 3333 defines the WSGI spec and is a useful reference for this code.
157
157
158 # We first validate that the incoming object conforms with the WSGI spec.
158 # We first validate that the incoming object conforms with the WSGI spec.
159 # We only want to be dealing with spec-conforming WSGI implementations.
159 # We only want to be dealing with spec-conforming WSGI implementations.
160 # TODO enable this once we fix internal violations.
160 # TODO enable this once we fix internal violations.
161 # wsgiref.validate.check_environ(env)
161 # wsgiref.validate.check_environ(env)
162
162
163 # PEP-0333 states that environment keys and values are native strings.
163 # PEP-0333 states that environment keys and values are native strings.
164 # The code points for the Unicode strings on Python 3 must be between
164 # The code points for the Unicode strings on Python 3 must be between
165 # \00000-\000FF. We deal with bytes in Mercurial, so mass convert string
165 # \00000-\000FF. We deal with bytes in Mercurial, so mass convert string
166 # keys and values to bytes.
166 # keys and values to bytes.
167 def tobytes(s):
167 def tobytes(s):
168 if not isinstance(s, str):
168 if not isinstance(s, str):
169 return s
169 return s
170 if pycompat.iswindows:
170 if pycompat.iswindows:
171 # This is what mercurial.encoding does for os.environ on
171 # This is what mercurial.encoding does for os.environ on
172 # Windows.
172 # Windows.
173 return encoding.strtolocal(s)
173 return encoding.strtolocal(s)
174 else:
174 else:
175 # This is what is documented to be used for os.environ on Unix.
175 # This is what is documented to be used for os.environ on Unix.
176 return pycompat.fsencode(s)
176 return pycompat.fsencode(s)
177
177
178 env = {tobytes(k): tobytes(v) for k, v in env.items()}
178 env = {tobytes(k): tobytes(v) for k, v in env.items()}
179
179
180 # Some hosting solutions are emulating hgwebdir, and dispatching directly
180 # Some hosting solutions are emulating hgwebdir, and dispatching directly
181 # to an hgweb instance using this environment variable. This was always
181 # to an hgweb instance using this environment variable. This was always
182 # checked prior to d7fd203e36cc; keep doing so to avoid breaking them.
182 # checked prior to d7fd203e36cc; keep doing so to avoid breaking them.
183 if not reponame:
183 if not reponame:
184 reponame = env.get(b'REPO_NAME')
184 reponame = env.get(b'REPO_NAME')
185
185
186 if altbaseurl:
186 if altbaseurl:
187 altbaseurl = urlutil.url(altbaseurl)
187 altbaseurl = urlutil.url(altbaseurl)
188
188
189 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
189 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
190 # the environment variables.
190 # the environment variables.
191 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
191 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
192 # how URLs are reconstructed.
192 # how URLs are reconstructed.
193 fullurl = env[b'wsgi.url_scheme'] + b'://'
193 fullurl = env[b'wsgi.url_scheme'] + b'://'
194
194
195 if altbaseurl and altbaseurl.scheme:
195 if altbaseurl and altbaseurl.scheme:
196 advertisedfullurl = altbaseurl.scheme + b'://'
196 advertisedfullurl = altbaseurl.scheme + b'://'
197 else:
197 else:
198 advertisedfullurl = fullurl
198 advertisedfullurl = fullurl
199
199
200 def addport(s, port):
200 def addport(s, port):
201 if s.startswith(b'https://'):
201 if s.startswith(b'https://'):
202 if port != b'443':
202 if port != b'443':
203 s += b':' + port
203 s += b':' + port
204 else:
204 else:
205 if port != b'80':
205 if port != b'80':
206 s += b':' + port
206 s += b':' + port
207
207
208 return s
208 return s
209
209
210 if env.get(b'HTTP_HOST'):
210 if env.get(b'HTTP_HOST'):
211 fullurl += env[b'HTTP_HOST']
211 fullurl += env[b'HTTP_HOST']
212 else:
212 else:
213 fullurl += env[b'SERVER_NAME']
213 fullurl += env[b'SERVER_NAME']
214 fullurl = addport(fullurl, env[b'SERVER_PORT'])
214 fullurl = addport(fullurl, env[b'SERVER_PORT'])
215
215
216 if altbaseurl and altbaseurl.host:
216 if altbaseurl and altbaseurl.host:
217 advertisedfullurl += altbaseurl.host
217 advertisedfullurl += altbaseurl.host
218
218
219 if altbaseurl.port:
219 if altbaseurl.port:
220 port = altbaseurl.port
220 port = altbaseurl.port
221 elif altbaseurl.scheme == b'http' and not altbaseurl.port:
221 elif altbaseurl.scheme == b'http' and not altbaseurl.port:
222 port = b'80'
222 port = b'80'
223 elif altbaseurl.scheme == b'https' and not altbaseurl.port:
223 elif altbaseurl.scheme == b'https' and not altbaseurl.port:
224 port = b'443'
224 port = b'443'
225 else:
225 else:
226 port = env[b'SERVER_PORT']
226 port = env[b'SERVER_PORT']
227
227
228 advertisedfullurl = addport(advertisedfullurl, port)
228 advertisedfullurl = addport(advertisedfullurl, port)
229 else:
229 else:
230 advertisedfullurl += env[b'SERVER_NAME']
230 advertisedfullurl += env[b'SERVER_NAME']
231 advertisedfullurl = addport(advertisedfullurl, env[b'SERVER_PORT'])
231 advertisedfullurl = addport(advertisedfullurl, env[b'SERVER_PORT'])
232
232
233 baseurl = fullurl
233 baseurl = fullurl
234 advertisedbaseurl = advertisedfullurl
234 advertisedbaseurl = advertisedfullurl
235
235
236 fullurl += util.urlreq.quote(env.get(b'SCRIPT_NAME', b''))
236 fullurl += util.urlreq.quote(env.get(b'SCRIPT_NAME', b''))
237 fullurl += util.urlreq.quote(env.get(b'PATH_INFO', b''))
237 fullurl += util.urlreq.quote(env.get(b'PATH_INFO', b''))
238
238
239 if altbaseurl:
239 if altbaseurl:
240 path = altbaseurl.path or b''
240 path = altbaseurl.path or b''
241 if path and not path.startswith(b'/'):
241 if path and not path.startswith(b'/'):
242 path = b'/' + path
242 path = b'/' + path
243 advertisedfullurl += util.urlreq.quote(path)
243 advertisedfullurl += util.urlreq.quote(path)
244 else:
244 else:
245 advertisedfullurl += util.urlreq.quote(env.get(b'SCRIPT_NAME', b''))
245 advertisedfullurl += util.urlreq.quote(env.get(b'SCRIPT_NAME', b''))
246
246
247 advertisedfullurl += util.urlreq.quote(env.get(b'PATH_INFO', b''))
247 advertisedfullurl += util.urlreq.quote(env.get(b'PATH_INFO', b''))
248
248
249 if env.get(b'QUERY_STRING'):
249 if env.get(b'QUERY_STRING'):
250 fullurl += b'?' + env[b'QUERY_STRING']
250 fullurl += b'?' + env[b'QUERY_STRING']
251 advertisedfullurl += b'?' + env[b'QUERY_STRING']
251 advertisedfullurl += b'?' + env[b'QUERY_STRING']
252
252
253 # If ``reponame`` is defined, that must be a prefix on PATH_INFO
253 # If ``reponame`` is defined, that must be a prefix on PATH_INFO
254 # that represents the repository being dispatched to. When computing
254 # that represents the repository being dispatched to. When computing
255 # the dispatch info, we ignore these leading path components.
255 # the dispatch info, we ignore these leading path components.
256
256
257 if altbaseurl:
257 if altbaseurl:
258 apppath = altbaseurl.path or b''
258 apppath = altbaseurl.path or b''
259 if apppath and not apppath.startswith(b'/'):
259 if apppath and not apppath.startswith(b'/'):
260 apppath = b'/' + apppath
260 apppath = b'/' + apppath
261 else:
261 else:
262 apppath = env.get(b'SCRIPT_NAME', b'')
262 apppath = env.get(b'SCRIPT_NAME', b'')
263
263
264 if reponame:
264 if reponame:
265 repoprefix = b'/' + reponame.strip(b'/')
265 repoprefix = b'/' + reponame.strip(b'/')
266
266
267 if not env.get(b'PATH_INFO'):
267 if not env.get(b'PATH_INFO'):
268 raise error.ProgrammingError(b'reponame requires PATH_INFO')
268 raise error.ProgrammingError(b'reponame requires PATH_INFO')
269
269
270 if not env[b'PATH_INFO'].startswith(repoprefix):
270 if not env[b'PATH_INFO'].startswith(repoprefix):
271 raise error.ProgrammingError(
271 raise error.ProgrammingError(
272 b'PATH_INFO does not begin with repo '
272 b'PATH_INFO does not begin with repo '
273 b'name: %s (%s)' % (env[b'PATH_INFO'], reponame)
273 b'name: %s (%s)' % (env[b'PATH_INFO'], reponame)
274 )
274 )
275
275
276 dispatchpath = env[b'PATH_INFO'][len(repoprefix) :]
276 dispatchpath = env[b'PATH_INFO'][len(repoprefix) :]
277
277
278 if dispatchpath and not dispatchpath.startswith(b'/'):
278 if dispatchpath and not dispatchpath.startswith(b'/'):
279 raise error.ProgrammingError(
279 raise error.ProgrammingError(
280 b'reponame prefix of PATH_INFO does '
280 b'reponame prefix of PATH_INFO does '
281 b'not end at path delimiter: %s (%s)'
281 b'not end at path delimiter: %s (%s)'
282 % (env[b'PATH_INFO'], reponame)
282 % (env[b'PATH_INFO'], reponame)
283 )
283 )
284
284
285 apppath = apppath.rstrip(b'/') + repoprefix
285 apppath = apppath.rstrip(b'/') + repoprefix
286 dispatchparts = dispatchpath.strip(b'/').split(b'/')
286 dispatchparts = dispatchpath.strip(b'/').split(b'/')
287 dispatchpath = b'/'.join(dispatchparts)
287 dispatchpath = b'/'.join(dispatchparts)
288
288
289 elif b'PATH_INFO' in env:
289 elif b'PATH_INFO' in env:
290 if env[b'PATH_INFO'].strip(b'/'):
290 if env[b'PATH_INFO'].strip(b'/'):
291 dispatchparts = env[b'PATH_INFO'].strip(b'/').split(b'/')
291 dispatchparts = env[b'PATH_INFO'].strip(b'/').split(b'/')
292 dispatchpath = b'/'.join(dispatchparts)
292 dispatchpath = b'/'.join(dispatchparts)
293 else:
293 else:
294 dispatchparts = []
294 dispatchparts = []
295 dispatchpath = b''
295 dispatchpath = b''
296 else:
296 else:
297 dispatchparts = []
297 dispatchparts = []
298 dispatchpath = None
298 dispatchpath = None
299
299
300 querystring = env.get(b'QUERY_STRING', b'')
300 querystring = env.get(b'QUERY_STRING', b'')
301
301
302 # We store as a list so we have ordering information. We also store as
302 # We store as a list so we have ordering information. We also store as
303 # a dict to facilitate fast lookup.
303 # a dict to facilitate fast lookup.
304 qsparams = multidict()
304 qsparams = multidict()
305 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True):
305 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True):
306 qsparams.add(k, v)
306 qsparams.add(k, v)
307
307
308 # HTTP_* keys contain HTTP request headers. The Headers structure should
308 # HTTP_* keys contain HTTP request headers. The Headers structure should
309 # perform case normalization for us. We just rewrite underscore to dash
309 # perform case normalization for us. We just rewrite underscore to dash
310 # so keys match what likely went over the wire.
310 # so keys match what likely went over the wire.
311 headers = []
311 headers = []
312 for k, v in env.items():
312 for k, v in env.items():
313 if k.startswith(b'HTTP_'):
313 if k.startswith(b'HTTP_'):
314 headers.append((k[len(b'HTTP_') :].replace(b'_', b'-'), v))
314 headers.append((k[len(b'HTTP_') :].replace(b'_', b'-'), v))
315
315
316 from . import wsgiheaders # avoid cycle
316 from . import wsgiheaders # avoid cycle
317
317
318 headers = wsgiheaders.Headers(headers)
318 headers = wsgiheaders.Headers(headers)
319
319
320 # This is kind of a lie because the HTTP header wasn't explicitly
320 # This is kind of a lie because the HTTP header wasn't explicitly
321 # sent. But for all intents and purposes it should be OK to lie about
321 # sent. But for all intents and purposes it should be OK to lie about
322 # this, since a consumer will either either value to determine how many
322 # this, since a consumer will either either value to determine how many
323 # bytes are available to read.
323 # bytes are available to read.
324 if b'CONTENT_LENGTH' in env and b'HTTP_CONTENT_LENGTH' not in env:
324 if b'CONTENT_LENGTH' in env and b'HTTP_CONTENT_LENGTH' not in env:
325 headers[b'Content-Length'] = env[b'CONTENT_LENGTH']
325 headers[b'Content-Length'] = env[b'CONTENT_LENGTH']
326
326
327 if b'CONTENT_TYPE' in env and b'HTTP_CONTENT_TYPE' not in env:
327 if b'CONTENT_TYPE' in env and b'HTTP_CONTENT_TYPE' not in env:
328 headers[b'Content-Type'] = env[b'CONTENT_TYPE']
328 headers[b'Content-Type'] = env[b'CONTENT_TYPE']
329
329
330 if bodyfh is None:
330 if bodyfh is None:
331 bodyfh = env[b'wsgi.input']
331 bodyfh = env[b'wsgi.input']
332 if b'Content-Length' in headers:
332 if b'Content-Length' in headers:
333 bodyfh = util.cappedreader(
333 bodyfh = util.cappedreader(
334 bodyfh, int(headers[b'Content-Length'] or b'0')
334 bodyfh, int(headers[b'Content-Length'] or b'0')
335 )
335 )
336
336
337 return parsedrequest(
337 return parsedrequest(
338 method=env[b'REQUEST_METHOD'],
338 method=env[b'REQUEST_METHOD'],
339 url=fullurl,
339 url=fullurl,
340 baseurl=baseurl,
340 baseurl=baseurl,
341 advertisedurl=advertisedfullurl,
341 advertisedurl=advertisedfullurl,
342 advertisedbaseurl=advertisedbaseurl,
342 advertisedbaseurl=advertisedbaseurl,
343 urlscheme=env[b'wsgi.url_scheme'],
343 urlscheme=env[b'wsgi.url_scheme'],
344 remoteuser=env.get(b'REMOTE_USER'),
344 remoteuser=env.get(b'REMOTE_USER'),
345 remotehost=env.get(b'REMOTE_HOST'),
345 remotehost=env.get(b'REMOTE_HOST'),
346 apppath=apppath,
346 apppath=apppath,
347 dispatchparts=dispatchparts,
347 dispatchparts=dispatchparts,
348 dispatchpath=dispatchpath,
348 dispatchpath=dispatchpath,
349 reponame=reponame,
349 reponame=reponame,
350 querystring=querystring,
350 querystring=querystring,
351 qsparams=qsparams,
351 qsparams=qsparams,
352 headers=headers,
352 headers=headers,
353 bodyfh=bodyfh,
353 bodyfh=bodyfh,
354 rawenv=env,
354 rawenv=env,
355 )
355 )
356
356
357
357
358 class offsettrackingwriter:
358 class offsettrackingwriter:
359 """A file object like object that is append only and tracks write count.
359 """A file object like object that is append only and tracks write count.
360
360
361 Instances are bound to a callable. This callable is called with data
361 Instances are bound to a callable. This callable is called with data
362 whenever a ``write()`` is attempted.
362 whenever a ``write()`` is attempted.
363
363
364 Instances track the amount of written data so they can answer ``tell()``
364 Instances track the amount of written data so they can answer ``tell()``
365 requests.
365 requests.
366
366
367 The intent of this class is to wrap the ``write()`` function returned by
367 The intent of this class is to wrap the ``write()`` function returned by
368 a WSGI ``start_response()`` function. Since ``write()`` is a callable and
368 a WSGI ``start_response()`` function. Since ``write()`` is a callable and
369 not a file object, it doesn't implement other file object methods.
369 not a file object, it doesn't implement other file object methods.
370 """
370 """
371
371
372 def __init__(self, writefn):
372 def __init__(self, writefn):
373 self._write = writefn
373 self._write = writefn
374 self._offset = 0
374 self._offset = 0
375
375
376 def write(self, s):
376 def write(self, s):
377 res = self._write(s)
377 res = self._write(s)
378 # Some Python objects don't report the number of bytes written.
378 # Some Python objects don't report the number of bytes written.
379 if res is None:
379 if res is None:
380 self._offset += len(s)
380 self._offset += len(s)
381 else:
381 else:
382 self._offset += res
382 self._offset += res
383
383
384 def flush(self):
384 def flush(self):
385 pass
385 pass
386
386
387 def tell(self):
387 def tell(self):
388 return self._offset
388 return self._offset
389
389
390
390
391 class wsgiresponse:
391 class wsgiresponse:
392 """Represents a response to a WSGI request.
392 """Represents a response to a WSGI request.
393
393
394 A response consists of a status line, headers, and a body.
394 A response consists of a status line, headers, and a body.
395
395
396 Consumers must populate the ``status`` and ``headers`` fields and
396 Consumers must populate the ``status`` and ``headers`` fields and
397 make a call to a ``setbody*()`` method before the response can be
397 make a call to a ``setbody*()`` method before the response can be
398 issued.
398 issued.
399
399
400 When it is time to start sending the response over the wire,
400 When it is time to start sending the response over the wire,
401 ``sendresponse()`` is called. It handles emitting the header portion
401 ``sendresponse()`` is called. It handles emitting the header portion
402 of the response message. It then yields chunks of body data to be
402 of the response message. It then yields chunks of body data to be
403 written to the peer. Typically, the WSGI application itself calls
403 written to the peer. Typically, the WSGI application itself calls
404 and returns the value from ``sendresponse()``.
404 and returns the value from ``sendresponse()``.
405 """
405 """
406
406
407 def __init__(self, req, startresponse):
407 def __init__(self, req, startresponse):
408 """Create an empty response tied to a specific request.
408 """Create an empty response tied to a specific request.
409
409
410 ``req`` is a ``parsedrequest``. ``startresponse`` is the
410 ``req`` is a ``parsedrequest``. ``startresponse`` is the
411 ``start_response`` function passed to the WSGI application.
411 ``start_response`` function passed to the WSGI application.
412 """
412 """
413 self._req = req
413 self._req = req
414 self._startresponse = startresponse
414 self._startresponse = startresponse
415
415
416 self.status = None
416 self.status = None
417 from . import wsgiheaders # avoid cycle
417 from . import wsgiheaders # avoid cycle
418
418
419 self.headers = wsgiheaders.Headers([])
419 self.headers = wsgiheaders.Headers([])
420
420
421 self._bodybytes = None
421 self._bodybytes = None
422 self._bodygen = None
422 self._bodygen = None
423 self._bodywillwrite = False
423 self._bodywillwrite = False
424 self._started = False
424 self._started = False
425 self._bodywritefn = None
425 self._bodywritefn = None
426
426
427 def _verifybody(self):
427 def _verifybody(self):
428 if (
428 if (
429 self._bodybytes is not None
429 self._bodybytes is not None
430 or self._bodygen is not None
430 or self._bodygen is not None
431 or self._bodywillwrite
431 or self._bodywillwrite
432 ):
432 ):
433 raise error.ProgrammingError(b'cannot define body multiple times')
433 raise error.ProgrammingError(b'cannot define body multiple times')
434
434
435 def setbodybytes(self, b):
435 def setbodybytes(self, b):
436 """Define the response body as static bytes.
436 """Define the response body as static bytes.
437
437
438 The empty string signals that there is no response body.
438 The empty string signals that there is no response body.
439 """
439 """
440 self._verifybody()
440 self._verifybody()
441 self._bodybytes = b
441 self._bodybytes = b
442 self.headers[b'Content-Length'] = b'%d' % len(b)
442 self.headers[b'Content-Length'] = b'%d' % len(b)
443
443
444 def setbodygen(self, gen):
444 def setbodygen(self, gen):
445 """Define the response body as a generator of bytes."""
445 """Define the response body as a generator of bytes."""
446 self._verifybody()
446 self._verifybody()
447 self._bodygen = gen
447 self._bodygen = gen
448
448
449 def setbodywillwrite(self):
449 def setbodywillwrite(self):
450 """Signal an intent to use write() to emit the response body.
450 """Signal an intent to use write() to emit the response body.
451
451
452 **This is the least preferred way to send a body.**
452 **This is the least preferred way to send a body.**
453
453
454 It is preferred for WSGI applications to emit a generator of chunks
454 It is preferred for WSGI applications to emit a generator of chunks
455 constituting the response body. However, some consumers can't emit
455 constituting the response body. However, some consumers can't emit
456 data this way. So, WSGI provides a way to obtain a ``write(data)``
456 data this way. So, WSGI provides a way to obtain a ``write(data)``
457 function that can be used to synchronously perform an unbuffered
457 function that can be used to synchronously perform an unbuffered
458 write.
458 write.
459
459
460 Calling this function signals an intent to produce the body in this
460 Calling this function signals an intent to produce the body in this
461 manner.
461 manner.
462 """
462 """
463 self._verifybody()
463 self._verifybody()
464 self._bodywillwrite = True
464 self._bodywillwrite = True
465
465
466 def sendresponse(self):
466 def sendresponse(self):
467 """Send the generated response to the client.
467 """Send the generated response to the client.
468
468
469 Before this is called, ``status`` must be set and one of
469 Before this is called, ``status`` must be set and one of
470 ``setbodybytes()`` or ``setbodygen()`` must be called.
470 ``setbodybytes()`` or ``setbodygen()`` must be called.
471
471
472 Calling this method multiple times is not allowed.
472 Calling this method multiple times is not allowed.
473 """
473 """
474 if self._started:
474 if self._started:
475 raise error.ProgrammingError(
475 raise error.ProgrammingError(
476 b'sendresponse() called multiple times'
476 b'sendresponse() called multiple times'
477 )
477 )
478
478
479 self._started = True
479 self._started = True
480
480
481 if not self.status:
481 if not self.status:
482 raise error.ProgrammingError(b'status line not defined')
482 raise error.ProgrammingError(b'status line not defined')
483
483
484 if (
484 if (
485 self._bodybytes is None
485 self._bodybytes is None
486 and self._bodygen is None
486 and self._bodygen is None
487 and not self._bodywillwrite
487 and not self._bodywillwrite
488 and self._req.method != b'HEAD'
488 ):
489 ):
489 raise error.ProgrammingError(b'response body not defined')
490 raise error.ProgrammingError(b'response body not defined')
490
491
491 # RFC 7232 Section 4.1 states that a 304 MUST generate one of
492 # RFC 7232 Section 4.1 states that a 304 MUST generate one of
492 # {Cache-Control, Content-Location, Date, ETag, Expires, Vary}
493 # {Cache-Control, Content-Location, Date, ETag, Expires, Vary}
493 # and SHOULD NOT generate other headers unless they could be used
494 # and SHOULD NOT generate other headers unless they could be used
494 # to guide cache updates. Furthermore, RFC 7230 Section 3.3.2
495 # to guide cache updates. Furthermore, RFC 7230 Section 3.3.2
495 # states that no response body can be issued. Content-Length can
496 # states that no response body can be issued. Content-Length can
496 # be sent. But if it is present, it should be the size of the response
497 # be sent. But if it is present, it should be the size of the response
497 # that wasn't transferred.
498 # that wasn't transferred.
498 if self.status.startswith(b'304 '):
499 if self.status.startswith(b'304 '):
499 # setbodybytes('') will set C-L to 0. This doesn't conform with the
500 # setbodybytes('') will set C-L to 0. This doesn't conform with the
500 # spec. So remove it.
501 # spec. So remove it.
501 if self.headers.get(b'Content-Length') == b'0':
502 if self.headers.get(b'Content-Length') == b'0':
502 del self.headers[b'Content-Length']
503 del self.headers[b'Content-Length']
503
504
504 # Strictly speaking, this is too strict. But until it causes
505 # Strictly speaking, this is too strict. But until it causes
505 # problems, let's be strict.
506 # problems, let's be strict.
506 badheaders = {
507 badheaders = {
507 k
508 k
508 for k in self.headers.keys()
509 for k in self.headers.keys()
509 if k.lower()
510 if k.lower()
510 not in (
511 not in (
511 b'date',
512 b'date',
512 b'etag',
513 b'etag',
513 b'expires',
514 b'expires',
514 b'cache-control',
515 b'cache-control',
515 b'content-location',
516 b'content-location',
516 b'content-security-policy',
517 b'content-security-policy',
517 b'vary',
518 b'vary',
518 )
519 )
519 }
520 }
520 if badheaders:
521 if badheaders:
521 raise error.ProgrammingError(
522 raise error.ProgrammingError(
522 b'illegal header on 304 response: %s'
523 b'illegal header on 304 response: %s'
523 % b', '.join(sorted(badheaders))
524 % b', '.join(sorted(badheaders))
524 )
525 )
525
526
526 if self._bodygen is not None or self._bodywillwrite:
527 if self._bodygen is not None or self._bodywillwrite:
527 raise error.ProgrammingError(
528 raise error.ProgrammingError(
528 b"must use setbodybytes('') with 304 responses"
529 b"must use setbodybytes('') with 304 responses"
529 )
530 )
530
531
531 # Various HTTP clients (notably httplib) won't read the HTTP response
532 # Various HTTP clients (notably httplib) won't read the HTTP response
532 # until the HTTP request has been sent in full. If servers (us) send a
533 # until the HTTP request has been sent in full. If servers (us) send a
533 # response before the HTTP request has been fully sent, the connection
534 # response before the HTTP request has been fully sent, the connection
534 # may deadlock because neither end is reading.
535 # may deadlock because neither end is reading.
535 #
536 #
536 # We work around this by "draining" the request data before
537 # We work around this by "draining" the request data before
537 # sending any response in some conditions.
538 # sending any response in some conditions.
538 drain = False
539 drain = False
539 close = False
540 close = False
540
541
541 # If the client sent Expect: 100-continue, we assume it is smart enough
542 # If the client sent Expect: 100-continue, we assume it is smart enough
542 # to deal with the server sending a response before reading the request.
543 # to deal with the server sending a response before reading the request.
543 # (httplib doesn't do this.)
544 # (httplib doesn't do this.)
544 if self._req.headers.get(b'Expect', b'').lower() == b'100-continue':
545 if self._req.headers.get(b'Expect', b'').lower() == b'100-continue':
545 pass
546 pass
546 # Only tend to request methods that have bodies. Strictly speaking,
547 # Only tend to request methods that have bodies. Strictly speaking,
547 # we should sniff for a body. But this is fine for our existing
548 # we should sniff for a body. But this is fine for our existing
548 # WSGI applications.
549 # WSGI applications.
549 elif self._req.method not in (b'POST', b'PUT'):
550 elif self._req.method not in (b'POST', b'PUT'):
550 pass
551 pass
551 else:
552 else:
552 # If we don't know how much data to read, there's no guarantee
553 # If we don't know how much data to read, there's no guarantee
553 # that we can drain the request responsibly. The WSGI
554 # that we can drain the request responsibly. The WSGI
554 # specification only says that servers *should* ensure the
555 # specification only says that servers *should* ensure the
555 # input stream doesn't overrun the actual request. So there's
556 # input stream doesn't overrun the actual request. So there's
556 # no guarantee that reading until EOF won't corrupt the stream
557 # no guarantee that reading until EOF won't corrupt the stream
557 # state.
558 # state.
558 if not isinstance(self._req.bodyfh, util.cappedreader):
559 if not isinstance(self._req.bodyfh, util.cappedreader):
559 close = True
560 close = True
560 else:
561 else:
561 # We /could/ only drain certain HTTP response codes. But 200 and
562 # We /could/ only drain certain HTTP response codes. But 200 and
562 # non-200 wire protocol responses both require draining. Since
563 # non-200 wire protocol responses both require draining. Since
563 # we have a capped reader in place for all situations where we
564 # we have a capped reader in place for all situations where we
564 # drain, it is safe to read from that stream. We'll either do
565 # drain, it is safe to read from that stream. We'll either do
565 # a drain or no-op if we're already at EOF.
566 # a drain or no-op if we're already at EOF.
566 drain = True
567 drain = True
567
568
568 if close:
569 if close:
569 self.headers[b'Connection'] = b'Close'
570 self.headers[b'Connection'] = b'Close'
570
571
571 if drain:
572 if drain:
572 assert isinstance(self._req.bodyfh, util.cappedreader)
573 assert isinstance(self._req.bodyfh, util.cappedreader)
573 while True:
574 while True:
574 chunk = self._req.bodyfh.read(32768)
575 chunk = self._req.bodyfh.read(32768)
575 if not chunk:
576 if not chunk:
576 break
577 break
577
578
578 strheaders = [
579 strheaders = [
579 (pycompat.strurl(k), pycompat.strurl(v))
580 (pycompat.strurl(k), pycompat.strurl(v))
580 for k, v in self.headers.items()
581 for k, v in self.headers.items()
581 ]
582 ]
582 write = self._startresponse(pycompat.sysstr(self.status), strheaders)
583 write = self._startresponse(pycompat.sysstr(self.status), strheaders)
583
584
584 if self._bodybytes:
585 if self._bodybytes:
585 yield self._bodybytes
586 yield self._bodybytes
586 elif self._bodygen:
587 elif self._bodygen:
587 for chunk in self._bodygen:
588 for chunk in self._bodygen:
588 # PEP-3333 says that output must be bytes. And some WSGI
589 # PEP-3333 says that output must be bytes. And some WSGI
589 # implementations enforce this. We cast bytes-like types here
590 # implementations enforce this. We cast bytes-like types here
590 # for convenience.
591 # for convenience.
591 if isinstance(chunk, bytearray):
592 if isinstance(chunk, bytearray):
592 chunk = bytes(chunk)
593 chunk = bytes(chunk)
593
594
594 yield chunk
595 yield chunk
595 elif self._bodywillwrite:
596 elif self._bodywillwrite:
596 self._bodywritefn = write
597 self._bodywritefn = write
598 elif self._req.method == b'HEAD':
599 pass
597 else:
600 else:
598 error.ProgrammingError(b'do not know how to send body')
601 error.ProgrammingError(b'do not know how to send body')
599
602
600 def getbodyfile(self):
603 def getbodyfile(self):
601 """Obtain a file object like object representing the response body.
604 """Obtain a file object like object representing the response body.
602
605
603 For this to work, you must call ``setbodywillwrite()`` and then
606 For this to work, you must call ``setbodywillwrite()`` and then
604 ``sendresponse()`` first. ``sendresponse()`` is a generator and the
607 ``sendresponse()`` first. ``sendresponse()`` is a generator and the
605 function won't run to completion unless the generator is advanced. The
608 function won't run to completion unless the generator is advanced. The
606 generator yields not items. The easiest way to consume it is with
609 generator yields not items. The easiest way to consume it is with
607 ``list(res.sendresponse())``, which should resolve to an empty list -
610 ``list(res.sendresponse())``, which should resolve to an empty list -
608 ``[]``.
611 ``[]``.
609 """
612 """
610 if not self._bodywillwrite:
613 if not self._bodywillwrite:
611 raise error.ProgrammingError(b'must call setbodywillwrite() first')
614 raise error.ProgrammingError(b'must call setbodywillwrite() first')
612
615
613 if not self._started:
616 if not self._started:
614 raise error.ProgrammingError(
617 raise error.ProgrammingError(
615 b'must call sendresponse() first; did '
618 b'must call sendresponse() first; did '
616 b'you remember to consume it since it '
619 b'you remember to consume it since it '
617 b'is a generator?'
620 b'is a generator?'
618 )
621 )
619
622
620 assert self._bodywritefn
623 assert self._bodywritefn
621 return offsettrackingwriter(self._bodywritefn)
624 return offsettrackingwriter(self._bodywritefn)
622
625
623
626
624 def wsgiapplication(app_maker):
627 def wsgiapplication(app_maker):
625 """For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
628 """For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
626 can and should now be used as a WSGI application."""
629 can and should now be used as a WSGI application."""
627 application = app_maker()
630 application = app_maker()
628
631
629 def run_wsgi(env, respond):
632 def run_wsgi(env, respond):
630 return application(env, respond)
633 return application(env, respond)
631
634
632 return run_wsgi
635 return run_wsgi
@@ -1,417 +1,424 b''
1 # hgweb/server.py - The standalone hg web server.
1 # hgweb/server.py - The standalone hg web server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9
9
10 import errno
10 import errno
11 import os
11 import os
12 import socket
12 import socket
13 import sys
13 import sys
14 import traceback
14 import traceback
15 import wsgiref.validate
15 import wsgiref.validate
16
16
17 from ..i18n import _
17 from ..i18n import _
18 from ..pycompat import (
18 from ..pycompat import (
19 getattr,
19 getattr,
20 open,
20 open,
21 )
21 )
22
22
23 from .. import (
23 from .. import (
24 encoding,
24 encoding,
25 error,
25 error,
26 pycompat,
26 pycompat,
27 util,
27 util,
28 )
28 )
29 from ..utils import (
29 from ..utils import (
30 urlutil,
30 urlutil,
31 )
31 )
32
32
33 httpservermod = util.httpserver
33 httpservermod = util.httpserver
34 socketserver = util.socketserver
34 socketserver = util.socketserver
35 urlerr = util.urlerr
35 urlerr = util.urlerr
36 urlreq = util.urlreq
36 urlreq = util.urlreq
37
37
38 from . import common
38 from . import common
39
39
40
40
41 def _splitURI(uri):
41 def _splitURI(uri):
42 """Return path and query that has been split from uri
42 """Return path and query that has been split from uri
43
43
44 Just like CGI environment, the path is unquoted, the query is
44 Just like CGI environment, the path is unquoted, the query is
45 not.
45 not.
46 """
46 """
47 if '?' in uri:
47 if '?' in uri:
48 path, query = uri.split('?', 1)
48 path, query = uri.split('?', 1)
49 else:
49 else:
50 path, query = uri, r''
50 path, query = uri, r''
51 return urlreq.unquote(path), query
51 return urlreq.unquote(path), query
52
52
53
53
54 class _error_logger:
54 class _error_logger:
55 def __init__(self, handler):
55 def __init__(self, handler):
56 self.handler = handler
56 self.handler = handler
57
57
58 def flush(self):
58 def flush(self):
59 pass
59 pass
60
60
61 def write(self, str):
61 def write(self, str):
62 self.writelines(str.split(b'\n'))
62 self.writelines(str.split(b'\n'))
63
63
64 def writelines(self, seq):
64 def writelines(self, seq):
65 for msg in seq:
65 for msg in seq:
66 self.handler.log_error("HG error: %s", encoding.strfromlocal(msg))
66 self.handler.log_error("HG error: %s", encoding.strfromlocal(msg))
67
67
68
68
69 class _httprequesthandler(httpservermod.basehttprequesthandler):
69 class _httprequesthandler(httpservermod.basehttprequesthandler):
70
70
71 url_scheme = b'http'
71 url_scheme = b'http'
72
72
73 @staticmethod
73 @staticmethod
74 def preparehttpserver(httpserver, ui):
74 def preparehttpserver(httpserver, ui):
75 """Prepare .socket of new HTTPServer instance"""
75 """Prepare .socket of new HTTPServer instance"""
76
76
77 def __init__(self, *args, **kargs):
77 def __init__(self, *args, **kargs):
78 self.protocol_version = r'HTTP/1.1'
78 self.protocol_version = r'HTTP/1.1'
79 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
79 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
80
80
81 def _log_any(self, fp, format, *args):
81 def _log_any(self, fp, format, *args):
82 fp.write(
82 fp.write(
83 pycompat.sysbytes(
83 pycompat.sysbytes(
84 r"%s - - [%s] %s"
84 r"%s - - [%s] %s"
85 % (
85 % (
86 self.client_address[0],
86 self.client_address[0],
87 self.log_date_time_string(),
87 self.log_date_time_string(),
88 format % args,
88 format % args,
89 )
89 )
90 )
90 )
91 + b'\n'
91 + b'\n'
92 )
92 )
93 fp.flush()
93 fp.flush()
94
94
95 def log_error(self, format, *args):
95 def log_error(self, format, *args):
96 self._log_any(self.server.errorlog, format, *args)
96 self._log_any(self.server.errorlog, format, *args)
97
97
98 def log_message(self, format, *args):
98 def log_message(self, format, *args):
99 self._log_any(self.server.accesslog, format, *args)
99 self._log_any(self.server.accesslog, format, *args)
100
100
101 def log_request(self, code='-', size='-'):
101 def log_request(self, code='-', size='-'):
102 xheaders = []
102 xheaders = []
103 if util.safehasattr(self, b'headers'):
103 if util.safehasattr(self, b'headers'):
104 xheaders = [
104 xheaders = [
105 h for h in self.headers.items() if h[0].startswith('x-')
105 h for h in self.headers.items() if h[0].startswith('x-')
106 ]
106 ]
107 self.log_message(
107 self.log_message(
108 '"%s" %s %s%s',
108 '"%s" %s %s%s',
109 self.requestline,
109 self.requestline,
110 str(code),
110 str(code),
111 str(size),
111 str(size),
112 ''.join([' %s:%s' % h for h in sorted(xheaders)]),
112 ''.join([' %s:%s' % h for h in sorted(xheaders)]),
113 )
113 )
114
114
115 def do_write(self):
115 def do_write(self):
116 try:
116 try:
117 self.do_hgweb()
117 self.do_hgweb()
118 except BrokenPipeError:
118 except BrokenPipeError:
119 pass
119 pass
120
120
121 def do_POST(self):
121 def do_POST(self):
122 try:
122 try:
123 self.do_write()
123 self.do_write()
124 except Exception as e:
124 except Exception as e:
125 # I/O below could raise another exception. So log the original
125 # I/O below could raise another exception. So log the original
126 # exception first to ensure it is recorded.
126 # exception first to ensure it is recorded.
127 if not (
127 if not (
128 isinstance(e, (OSError, socket.error))
128 isinstance(e, (OSError, socket.error))
129 and e.errno == errno.ECONNRESET
129 and e.errno == errno.ECONNRESET
130 ):
130 ):
131 tb = "".join(traceback.format_exception(*sys.exc_info()))
131 tb = "".join(traceback.format_exception(*sys.exc_info()))
132 # We need a native-string newline to poke in the log
132 # We need a native-string newline to poke in the log
133 # message, because we won't get a newline when using an
133 # message, because we won't get a newline when using an
134 # r-string. This is the easy way out.
134 # r-string. This is the easy way out.
135 newline = chr(10)
135 newline = chr(10)
136 self.log_error(
136 self.log_error(
137 r"Exception happened during processing "
137 r"Exception happened during processing "
138 "request '%s':%s%s",
138 "request '%s':%s%s",
139 self.path,
139 self.path,
140 newline,
140 newline,
141 tb,
141 tb,
142 )
142 )
143
143
144 self._start_response("500 Internal Server Error", [])
144 self._start_response("500 Internal Server Error", [])
145 self._write(b"Internal Server Error")
145 self._write(b"Internal Server Error")
146 self._done()
146 self._done()
147
147
148 def do_PUT(self):
148 def do_PUT(self):
149 self.do_POST()
149 self.do_POST()
150
150
151 def do_GET(self):
151 def do_GET(self):
152 self.do_POST()
152 self.do_POST()
153
153
154 def do_HEAD(self):
155 self.do_POST()
156
154 def do_hgweb(self):
157 def do_hgweb(self):
155 self.sent_headers = False
158 self.sent_headers = False
156 path, query = _splitURI(self.path)
159 path, query = _splitURI(self.path)
157
160
158 # Ensure the slicing of path below is valid
161 # Ensure the slicing of path below is valid
159 if path != self.server.prefix and not path.startswith(
162 if path != self.server.prefix and not path.startswith(
160 self.server.prefix + b'/'
163 self.server.prefix + b'/'
161 ):
164 ):
162 self._start_response(pycompat.strurl(common.statusmessage(404)), [])
165 self._start_response(pycompat.strurl(common.statusmessage(404)), [])
163 if self.command == 'POST':
166 if self.command == 'POST':
164 # Paranoia: tell the client we're going to close the
167 # Paranoia: tell the client we're going to close the
165 # socket so they don't try and reuse a socket that
168 # socket so they don't try and reuse a socket that
166 # might have a POST body waiting to confuse us. We do
169 # might have a POST body waiting to confuse us. We do
167 # this by directly munging self.saved_headers because
170 # this by directly munging self.saved_headers because
168 # self._start_response ignores Connection headers.
171 # self._start_response ignores Connection headers.
169 self.saved_headers = [('Connection', 'Close')]
172 self.saved_headers = [('Connection', 'Close')]
170 self._write(b"Not Found")
173 self._write(b"Not Found")
171 self._done()
174 self._done()
172 return
175 return
173
176
174 env = {}
177 env = {}
175 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
178 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
176 env['REQUEST_METHOD'] = self.command
179 env['REQUEST_METHOD'] = self.command
177 env['SERVER_NAME'] = self.server.server_name
180 env['SERVER_NAME'] = self.server.server_name
178 env['SERVER_PORT'] = str(self.server.server_port)
181 env['SERVER_PORT'] = str(self.server.server_port)
179 env['REQUEST_URI'] = self.path
182 env['REQUEST_URI'] = self.path
180 env['SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix)
183 env['SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix)
181 env['PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix) :])
184 env['PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix) :])
182 env['REMOTE_HOST'] = self.client_address[0]
185 env['REMOTE_HOST'] = self.client_address[0]
183 env['REMOTE_ADDR'] = self.client_address[0]
186 env['REMOTE_ADDR'] = self.client_address[0]
184 env['QUERY_STRING'] = query or ''
187 env['QUERY_STRING'] = query or ''
185
188
186 if self.headers.get_content_type() is None:
189 if self.headers.get_content_type() is None:
187 env['CONTENT_TYPE'] = self.headers.get_default_type()
190 env['CONTENT_TYPE'] = self.headers.get_default_type()
188 else:
191 else:
189 env['CONTENT_TYPE'] = self.headers.get_content_type()
192 env['CONTENT_TYPE'] = self.headers.get_content_type()
190 length = self.headers.get('content-length')
193 length = self.headers.get('content-length')
191 if length:
194 if length:
192 env['CONTENT_LENGTH'] = length
195 env['CONTENT_LENGTH'] = length
193 for header in [
196 for header in [
194 h
197 h
195 for h in self.headers.keys()
198 for h in self.headers.keys()
196 if h.lower() not in ('content-type', 'content-length')
199 if h.lower() not in ('content-type', 'content-length')
197 ]:
200 ]:
198 hkey = 'HTTP_' + header.replace('-', '_').upper()
201 hkey = 'HTTP_' + header.replace('-', '_').upper()
199 hval = self.headers.get(header)
202 hval = self.headers.get(header)
200 hval = hval.replace('\n', '').strip()
203 hval = hval.replace('\n', '').strip()
201 if hval:
204 if hval:
202 env[hkey] = hval
205 env[hkey] = hval
203 env['SERVER_PROTOCOL'] = self.request_version
206 env['SERVER_PROTOCOL'] = self.request_version
204 env['wsgi.version'] = (1, 0)
207 env['wsgi.version'] = (1, 0)
205 env['wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme)
208 env['wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme)
206 if env.get('HTTP_EXPECT', b'').lower() == b'100-continue':
209 if env.get('HTTP_EXPECT', b'').lower() == b'100-continue':
207 self.rfile = common.continuereader(self.rfile, self.wfile.write)
210 self.rfile = common.continuereader(self.rfile, self.wfile.write)
208
211
209 env['wsgi.input'] = self.rfile
212 env['wsgi.input'] = self.rfile
210 env['wsgi.errors'] = _error_logger(self)
213 env['wsgi.errors'] = _error_logger(self)
211 env['wsgi.multithread'] = isinstance(
214 env['wsgi.multithread'] = isinstance(
212 self.server, socketserver.ThreadingMixIn
215 self.server, socketserver.ThreadingMixIn
213 )
216 )
214 if util.safehasattr(socketserver, b'ForkingMixIn'):
217 if util.safehasattr(socketserver, b'ForkingMixIn'):
215 env['wsgi.multiprocess'] = isinstance(
218 env['wsgi.multiprocess'] = isinstance(
216 self.server, socketserver.ForkingMixIn
219 self.server, socketserver.ForkingMixIn
217 )
220 )
218 else:
221 else:
219 env['wsgi.multiprocess'] = False
222 env['wsgi.multiprocess'] = False
220
223
221 env['wsgi.run_once'] = 0
224 env['wsgi.run_once'] = 0
222
225
223 wsgiref.validate.check_environ(env)
226 wsgiref.validate.check_environ(env)
224
227
225 self.saved_status = None
228 self.saved_status = None
226 self.saved_headers = []
229 self.saved_headers = []
227 self.length = None
230 self.length = None
228 self._chunked = None
231 self._chunked = None
229 for chunk in self.server.application(env, self._start_response):
232 for chunk in self.server.application(env, self._start_response):
230 self._write(chunk)
233 self._write(chunk)
231 if not self.sent_headers:
234 if not self.sent_headers:
232 self.send_headers()
235 self.send_headers()
233 self._done()
236 self._done()
234
237
235 def send_headers(self):
238 def send_headers(self):
236 if not self.saved_status:
239 if not self.saved_status:
237 raise AssertionError(
240 raise AssertionError(
238 b"Sending headers before start_response() called"
241 b"Sending headers before start_response() called"
239 )
242 )
240 saved_status = self.saved_status.split(None, 1)
243 saved_status = self.saved_status.split(None, 1)
241 saved_status[0] = int(saved_status[0])
244 saved_status[0] = int(saved_status[0])
242 self.send_response(*saved_status)
245 self.send_response(*saved_status)
243 self.length = None
246 self.length = None
244 self._chunked = False
247 self._chunked = False
245 for h in self.saved_headers:
248 for h in self.saved_headers:
246 self.send_header(*h)
249 self.send_header(*h)
247 if h[0].lower() == 'content-length':
250 if h[0].lower() == 'content-length':
248 self.length = int(h[1])
251 self.length = int(h[1])
249 if self.length is None and saved_status[0] != common.HTTP_NOT_MODIFIED:
252 if (
253 self.length is None
254 and saved_status[0] != common.HTTP_NOT_MODIFIED
255 and self.command != 'HEAD'
256 ):
250 self._chunked = (
257 self._chunked = (
251 not self.close_connection and self.request_version == 'HTTP/1.1'
258 not self.close_connection and self.request_version == 'HTTP/1.1'
252 )
259 )
253 if self._chunked:
260 if self._chunked:
254 self.send_header('Transfer-Encoding', 'chunked')
261 self.send_header('Transfer-Encoding', 'chunked')
255 else:
262 else:
256 self.send_header('Connection', 'close')
263 self.send_header('Connection', 'close')
257 self.end_headers()
264 self.end_headers()
258 self.sent_headers = True
265 self.sent_headers = True
259
266
260 def _start_response(self, http_status, headers, exc_info=None):
267 def _start_response(self, http_status, headers, exc_info=None):
261 assert isinstance(http_status, str)
268 assert isinstance(http_status, str)
262 code, msg = http_status.split(None, 1)
269 code, msg = http_status.split(None, 1)
263 code = int(code)
270 code = int(code)
264 self.saved_status = http_status
271 self.saved_status = http_status
265 bad_headers = ('connection', 'transfer-encoding')
272 bad_headers = ('connection', 'transfer-encoding')
266 self.saved_headers = [
273 self.saved_headers = [
267 h for h in headers if h[0].lower() not in bad_headers
274 h for h in headers if h[0].lower() not in bad_headers
268 ]
275 ]
269 return self._write
276 return self._write
270
277
271 def _write(self, data):
278 def _write(self, data):
272 if not self.saved_status:
279 if not self.saved_status:
273 raise AssertionError(b"data written before start_response() called")
280 raise AssertionError(b"data written before start_response() called")
274 elif not self.sent_headers:
281 elif not self.sent_headers:
275 self.send_headers()
282 self.send_headers()
276 if self.length is not None:
283 if self.length is not None:
277 if len(data) > self.length:
284 if len(data) > self.length:
278 raise AssertionError(
285 raise AssertionError(
279 b"Content-length header sent, but more "
286 b"Content-length header sent, but more "
280 b"bytes than specified are being written."
287 b"bytes than specified are being written."
281 )
288 )
282 self.length = self.length - len(data)
289 self.length = self.length - len(data)
283 elif self._chunked and data:
290 elif self._chunked and data:
284 data = b'%x\r\n%s\r\n' % (len(data), data)
291 data = b'%x\r\n%s\r\n' % (len(data), data)
285 self.wfile.write(data)
292 self.wfile.write(data)
286 self.wfile.flush()
293 self.wfile.flush()
287
294
288 def _done(self):
295 def _done(self):
289 if self._chunked:
296 if self._chunked:
290 self.wfile.write(b'0\r\n\r\n')
297 self.wfile.write(b'0\r\n\r\n')
291 self.wfile.flush()
298 self.wfile.flush()
292
299
293 def version_string(self):
300 def version_string(self):
294 if self.server.serverheader:
301 if self.server.serverheader:
295 return encoding.strfromlocal(self.server.serverheader)
302 return encoding.strfromlocal(self.server.serverheader)
296 return httpservermod.basehttprequesthandler.version_string(self)
303 return httpservermod.basehttprequesthandler.version_string(self)
297
304
298
305
299 class _httprequesthandlerssl(_httprequesthandler):
306 class _httprequesthandlerssl(_httprequesthandler):
300 """HTTPS handler based on Python's ssl module"""
307 """HTTPS handler based on Python's ssl module"""
301
308
302 url_scheme = b'https'
309 url_scheme = b'https'
303
310
304 @staticmethod
311 @staticmethod
305 def preparehttpserver(httpserver, ui):
312 def preparehttpserver(httpserver, ui):
306 try:
313 try:
307 from .. import sslutil
314 from .. import sslutil
308
315
309 sslutil.wrapserversocket
316 sslutil.wrapserversocket
310 except ImportError:
317 except ImportError:
311 raise error.Abort(_(b"SSL support is unavailable"))
318 raise error.Abort(_(b"SSL support is unavailable"))
312
319
313 certfile = ui.config(b'web', b'certificate')
320 certfile = ui.config(b'web', b'certificate')
314
321
315 # These config options are currently only meant for testing. Use
322 # These config options are currently only meant for testing. Use
316 # at your own risk.
323 # at your own risk.
317 cafile = ui.config(b'devel', b'servercafile')
324 cafile = ui.config(b'devel', b'servercafile')
318 reqcert = ui.configbool(b'devel', b'serverrequirecert')
325 reqcert = ui.configbool(b'devel', b'serverrequirecert')
319
326
320 httpserver.socket = sslutil.wrapserversocket(
327 httpserver.socket = sslutil.wrapserversocket(
321 httpserver.socket,
328 httpserver.socket,
322 ui,
329 ui,
323 certfile=certfile,
330 certfile=certfile,
324 cafile=cafile,
331 cafile=cafile,
325 requireclientcert=reqcert,
332 requireclientcert=reqcert,
326 )
333 )
327
334
328 def setup(self):
335 def setup(self):
329 self.connection = self.request
336 self.connection = self.request
330 self.rfile = self.request.makefile("rb", self.rbufsize)
337 self.rfile = self.request.makefile("rb", self.rbufsize)
331 self.wfile = self.request.makefile("wb", self.wbufsize)
338 self.wfile = self.request.makefile("wb", self.wbufsize)
332
339
333
340
334 try:
341 try:
335 import threading
342 import threading
336
343
337 threading.active_count() # silence pyflakes and bypass demandimport
344 threading.active_count() # silence pyflakes and bypass demandimport
338 _mixin = socketserver.ThreadingMixIn
345 _mixin = socketserver.ThreadingMixIn
339 except ImportError:
346 except ImportError:
340 if util.safehasattr(os, b"fork"):
347 if util.safehasattr(os, b"fork"):
341 _mixin = socketserver.ForkingMixIn
348 _mixin = socketserver.ForkingMixIn
342 else:
349 else:
343
350
344 class _mixin:
351 class _mixin:
345 pass
352 pass
346
353
347
354
348 def openlog(opt, default):
355 def openlog(opt, default):
349 if opt and opt != b'-':
356 if opt and opt != b'-':
350 return open(opt, b'ab')
357 return open(opt, b'ab')
351 return default
358 return default
352
359
353
360
354 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
361 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
355
362
356 # SO_REUSEADDR has broken semantics on windows
363 # SO_REUSEADDR has broken semantics on windows
357 if pycompat.iswindows:
364 if pycompat.iswindows:
358 allow_reuse_address = 0
365 allow_reuse_address = 0
359
366
360 def __init__(self, ui, app, addr, handler, **kwargs):
367 def __init__(self, ui, app, addr, handler, **kwargs):
361 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
368 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
362 self.daemon_threads = True
369 self.daemon_threads = True
363 self.application = app
370 self.application = app
364
371
365 handler.preparehttpserver(self, ui)
372 handler.preparehttpserver(self, ui)
366
373
367 prefix = ui.config(b'web', b'prefix')
374 prefix = ui.config(b'web', b'prefix')
368 if prefix:
375 if prefix:
369 prefix = b'/' + prefix.strip(b'/')
376 prefix = b'/' + prefix.strip(b'/')
370 self.prefix = prefix
377 self.prefix = prefix
371
378
372 alog = openlog(ui.config(b'web', b'accesslog'), ui.fout)
379 alog = openlog(ui.config(b'web', b'accesslog'), ui.fout)
373 elog = openlog(ui.config(b'web', b'errorlog'), ui.ferr)
380 elog = openlog(ui.config(b'web', b'errorlog'), ui.ferr)
374 self.accesslog = alog
381 self.accesslog = alog
375 self.errorlog = elog
382 self.errorlog = elog
376
383
377 self.addr, self.port = self.socket.getsockname()[0:2]
384 self.addr, self.port = self.socket.getsockname()[0:2]
378 self.fqaddr = self.server_name
385 self.fqaddr = self.server_name
379
386
380 self.serverheader = ui.config(b'web', b'server-header')
387 self.serverheader = ui.config(b'web', b'server-header')
381
388
382
389
383 class IPv6HTTPServer(MercurialHTTPServer):
390 class IPv6HTTPServer(MercurialHTTPServer):
384 address_family = getattr(socket, 'AF_INET6', None)
391 address_family = getattr(socket, 'AF_INET6', None)
385
392
386 def __init__(self, *args, **kwargs):
393 def __init__(self, *args, **kwargs):
387 if self.address_family is None:
394 if self.address_family is None:
388 raise error.RepoError(_(b'IPv6 is not available on this system'))
395 raise error.RepoError(_(b'IPv6 is not available on this system'))
389 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
396 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
390
397
391
398
392 def create_server(ui, app):
399 def create_server(ui, app):
393
400
394 if ui.config(b'web', b'certificate'):
401 if ui.config(b'web', b'certificate'):
395 handler = _httprequesthandlerssl
402 handler = _httprequesthandlerssl
396 else:
403 else:
397 handler = _httprequesthandler
404 handler = _httprequesthandler
398
405
399 if ui.configbool(b'web', b'ipv6'):
406 if ui.configbool(b'web', b'ipv6'):
400 cls = IPv6HTTPServer
407 cls = IPv6HTTPServer
401 else:
408 else:
402 cls = MercurialHTTPServer
409 cls = MercurialHTTPServer
403
410
404 # ugly hack due to python issue5853 (for threaded use)
411 # ugly hack due to python issue5853 (for threaded use)
405 import mimetypes
412 import mimetypes
406
413
407 mimetypes.init()
414 mimetypes.init()
408
415
409 address = ui.config(b'web', b'address')
416 address = ui.config(b'web', b'address')
410 port = urlutil.getport(ui.config(b'web', b'port'))
417 port = urlutil.getport(ui.config(b'web', b'port'))
411 try:
418 try:
412 return cls(ui, app, (address, port), handler)
419 return cls(ui, app, (address, port), handler)
413 except socket.error as inst:
420 except socket.error as inst:
414 raise error.Abort(
421 raise error.Abort(
415 _(b"cannot start server at '%s:%d': %s")
422 _(b"cannot start server at '%s:%d': %s")
416 % (address, port, encoding.strtolocal(inst.args[1]))
423 % (address, port, encoding.strtolocal(inst.args[1]))
417 )
424 )
@@ -1,1594 +1,1597 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 import copy
9 import copy
10 import mimetypes
10 import mimetypes
11 import os
11 import os
12 import re
12 import re
13
13
14 from ..i18n import _
14 from ..i18n import _
15 from ..node import hex, short
15 from ..node import hex, short
16 from ..pycompat import getattr
16 from ..pycompat import getattr
17
17
18 from .common import (
18 from .common import (
19 ErrorResponse,
19 ErrorResponse,
20 HTTP_FORBIDDEN,
20 HTTP_FORBIDDEN,
21 HTTP_NOT_FOUND,
21 HTTP_NOT_FOUND,
22 get_contact,
22 get_contact,
23 paritygen,
23 paritygen,
24 staticfile,
24 staticfile,
25 )
25 )
26
26
27 from .. import (
27 from .. import (
28 archival,
28 archival,
29 dagop,
29 dagop,
30 encoding,
30 encoding,
31 error,
31 error,
32 graphmod,
32 graphmod,
33 pycompat,
33 pycompat,
34 revset,
34 revset,
35 revsetlang,
35 revsetlang,
36 scmutil,
36 scmutil,
37 smartset,
37 smartset,
38 templateutil,
38 templateutil,
39 )
39 )
40
40
41 from ..utils import stringutil
41 from ..utils import stringutil
42
42
43 from . import webutil
43 from . import webutil
44
44
45 __all__ = []
45 __all__ = []
46 commands = {}
46 commands = {}
47
47
48
48
49 class webcommand:
49 class webcommand:
50 """Decorator used to register a web command handler.
50 """Decorator used to register a web command handler.
51
51
52 The decorator takes as its positional arguments the name/path the
52 The decorator takes as its positional arguments the name/path the
53 command should be accessible under.
53 command should be accessible under.
54
54
55 When called, functions receive as arguments a ``requestcontext``,
55 When called, functions receive as arguments a ``requestcontext``,
56 ``wsgirequest``, and a templater instance for generatoring output.
56 ``wsgirequest``, and a templater instance for generatoring output.
57 The functions should populate the ``rctx.res`` object with details
57 The functions should populate the ``rctx.res`` object with details
58 about the HTTP response.
58 about the HTTP response.
59
59
60 The function returns a generator to be consumed by the WSGI application.
60 The function returns a generator to be consumed by the WSGI application.
61 For most commands, this should be the result from
61 For most commands, this should be the result from
62 ``web.res.sendresponse()``. Many commands will call ``web.sendtemplate()``
62 ``web.res.sendresponse()``. Many commands will call ``web.sendtemplate()``
63 to render a template.
63 to render a template.
64
64
65 Usage:
65 Usage:
66
66
67 @webcommand('mycommand')
67 @webcommand('mycommand')
68 def mycommand(web):
68 def mycommand(web):
69 pass
69 pass
70 """
70 """
71
71
72 def __init__(self, name):
72 def __init__(self, name):
73 self.name = name
73 self.name = name
74
74
75 def __call__(self, func):
75 def __call__(self, func):
76 __all__.append(self.name)
76 __all__.append(self.name)
77 commands[self.name] = func
77 commands[self.name] = func
78 return func
78 return func
79
79
80
80
81 @webcommand(b'log')
81 @webcommand(b'log')
82 def log(web):
82 def log(web):
83 """
83 """
84 /log[/{revision}[/{path}]]
84 /log[/{revision}[/{path}]]
85 --------------------------
85 --------------------------
86
86
87 Show repository or file history.
87 Show repository or file history.
88
88
89 For URLs of the form ``/log/{revision}``, a list of changesets starting at
89 For URLs of the form ``/log/{revision}``, a list of changesets starting at
90 the specified changeset identifier is shown. If ``{revision}`` is not
90 the specified changeset identifier is shown. If ``{revision}`` is not
91 defined, the default is ``tip``. This form is equivalent to the
91 defined, the default is ``tip``. This form is equivalent to the
92 ``changelog`` handler.
92 ``changelog`` handler.
93
93
94 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
94 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
95 file will be shown. This form is equivalent to the ``filelog`` handler.
95 file will be shown. This form is equivalent to the ``filelog`` handler.
96 """
96 """
97
97
98 if web.req.qsparams.get(b'file'):
98 if web.req.qsparams.get(b'file'):
99 return filelog(web)
99 return filelog(web)
100 else:
100 else:
101 return changelog(web)
101 return changelog(web)
102
102
103
103
104 @webcommand(b'rawfile')
104 @webcommand(b'rawfile')
105 def rawfile(web):
105 def rawfile(web):
106 guessmime = web.configbool(b'web', b'guessmime')
106 guessmime = web.configbool(b'web', b'guessmime')
107
107
108 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b''))
108 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b''))
109 if not path:
109 if not path:
110 return manifest(web)
110 return manifest(web)
111
111
112 try:
112 try:
113 fctx = webutil.filectx(web.repo, web.req)
113 fctx = webutil.filectx(web.repo, web.req)
114 except error.LookupError as inst:
114 except error.LookupError as inst:
115 try:
115 try:
116 return manifest(web)
116 return manifest(web)
117 except ErrorResponse:
117 except ErrorResponse:
118 raise inst
118 raise inst
119
119
120 path = fctx.path()
120 path = fctx.path()
121 text = fctx.data()
121 text = fctx.data()
122 mt = b'application/binary'
122 mt = b'application/binary'
123 if guessmime:
123 if guessmime:
124 mt = mimetypes.guess_type(pycompat.fsdecode(path))[0]
124 mt = mimetypes.guess_type(pycompat.fsdecode(path))[0]
125 if mt is None:
125 if mt is None:
126 if stringutil.binary(text):
126 if stringutil.binary(text):
127 mt = b'application/binary'
127 mt = b'application/binary'
128 else:
128 else:
129 mt = b'text/plain'
129 mt = b'text/plain'
130 else:
130 else:
131 mt = pycompat.sysbytes(mt)
131 mt = pycompat.sysbytes(mt)
132
132
133 if mt.startswith(b'text/'):
133 if mt.startswith(b'text/'):
134 mt += b'; charset="%s"' % encoding.encoding
134 mt += b'; charset="%s"' % encoding.encoding
135
135
136 web.res.headers[b'Content-Type'] = mt
136 web.res.headers[b'Content-Type'] = mt
137 filename = (
137 filename = (
138 path.rpartition(b'/')[-1].replace(b'\\', b'\\\\').replace(b'"', b'\\"')
138 path.rpartition(b'/')[-1].replace(b'\\', b'\\\\').replace(b'"', b'\\"')
139 )
139 )
140 web.res.headers[b'Content-Disposition'] = (
140 web.res.headers[b'Content-Disposition'] = (
141 b'inline; filename="%s"' % filename
141 b'inline; filename="%s"' % filename
142 )
142 )
143 web.res.setbodybytes(text)
143 web.res.setbodybytes(text)
144 return web.res.sendresponse()
144 return web.res.sendresponse()
145
145
146
146
147 def _filerevision(web, fctx):
147 def _filerevision(web, fctx):
148 f = fctx.path()
148 f = fctx.path()
149 text = fctx.data()
149 text = fctx.data()
150 parity = paritygen(web.stripecount)
150 parity = paritygen(web.stripecount)
151 ishead = fctx.filenode() in fctx.filelog().heads()
151 ishead = fctx.filenode() in fctx.filelog().heads()
152
152
153 if stringutil.binary(text):
153 if stringutil.binary(text):
154 mt = pycompat.sysbytes(
154 mt = pycompat.sysbytes(
155 mimetypes.guess_type(pycompat.fsdecode(f))[0]
155 mimetypes.guess_type(pycompat.fsdecode(f))[0]
156 or r'application/octet-stream'
156 or r'application/octet-stream'
157 )
157 )
158 text = b'(binary:%s)' % mt
158 text = b'(binary:%s)' % mt
159
159
160 def lines(context):
160 def lines(context):
161 for lineno, t in enumerate(text.splitlines(True)):
161 for lineno, t in enumerate(text.splitlines(True)):
162 yield {
162 yield {
163 b"line": t,
163 b"line": t,
164 b"lineid": b"l%d" % (lineno + 1),
164 b"lineid": b"l%d" % (lineno + 1),
165 b"linenumber": b"% 6d" % (lineno + 1),
165 b"linenumber": b"% 6d" % (lineno + 1),
166 b"parity": next(parity),
166 b"parity": next(parity),
167 }
167 }
168
168
169 return web.sendtemplate(
169 return web.sendtemplate(
170 b'filerevision',
170 b'filerevision',
171 file=f,
171 file=f,
172 path=webutil.up(f),
172 path=webutil.up(f),
173 text=templateutil.mappinggenerator(lines),
173 text=templateutil.mappinggenerator(lines),
174 symrev=webutil.symrevorshortnode(web.req, fctx),
174 symrev=webutil.symrevorshortnode(web.req, fctx),
175 rename=webutil.renamelink(fctx),
175 rename=webutil.renamelink(fctx),
176 permissions=fctx.manifest().flags(f),
176 permissions=fctx.manifest().flags(f),
177 ishead=int(ishead),
177 ishead=int(ishead),
178 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
178 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
179 )
179 )
180
180
181
181
182 @webcommand(b'file')
182 @webcommand(b'file')
183 def file(web):
183 def file(web):
184 """
184 """
185 /file/{revision}[/{path}]
185 /file/{revision}[/{path}]
186 -------------------------
186 -------------------------
187
187
188 Show information about a directory or file in the repository.
188 Show information about a directory or file in the repository.
189
189
190 Info about the ``path`` given as a URL parameter will be rendered.
190 Info about the ``path`` given as a URL parameter will be rendered.
191
191
192 If ``path`` is a directory, information about the entries in that
192 If ``path`` is a directory, information about the entries in that
193 directory will be rendered. This form is equivalent to the ``manifest``
193 directory will be rendered. This form is equivalent to the ``manifest``
194 handler.
194 handler.
195
195
196 If ``path`` is a file, information about that file will be shown via
196 If ``path`` is a file, information about that file will be shown via
197 the ``filerevision`` template.
197 the ``filerevision`` template.
198
198
199 If ``path`` is not defined, information about the root directory will
199 If ``path`` is not defined, information about the root directory will
200 be rendered.
200 be rendered.
201 """
201 """
202 if web.req.qsparams.get(b'style') == b'raw':
202 if web.req.qsparams.get(b'style') == b'raw':
203 return rawfile(web)
203 return rawfile(web)
204
204
205 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b''))
205 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b''))
206 if not path:
206 if not path:
207 return manifest(web)
207 return manifest(web)
208 try:
208 try:
209 return _filerevision(web, webutil.filectx(web.repo, web.req))
209 return _filerevision(web, webutil.filectx(web.repo, web.req))
210 except error.LookupError as inst:
210 except error.LookupError as inst:
211 try:
211 try:
212 return manifest(web)
212 return manifest(web)
213 except ErrorResponse:
213 except ErrorResponse:
214 raise inst
214 raise inst
215
215
216
216
217 def _search(web):
217 def _search(web):
218 MODE_REVISION = b'rev'
218 MODE_REVISION = b'rev'
219 MODE_KEYWORD = b'keyword'
219 MODE_KEYWORD = b'keyword'
220 MODE_REVSET = b'revset'
220 MODE_REVSET = b'revset'
221
221
222 def revsearch(ctx):
222 def revsearch(ctx):
223 yield ctx
223 yield ctx
224
224
225 def keywordsearch(query):
225 def keywordsearch(query):
226 lower = encoding.lower
226 lower = encoding.lower
227 qw = lower(query).split()
227 qw = lower(query).split()
228
228
229 def revgen():
229 def revgen():
230 cl = web.repo.changelog
230 cl = web.repo.changelog
231 for i in range(len(web.repo) - 1, 0, -100):
231 for i in range(len(web.repo) - 1, 0, -100):
232 l = []
232 l = []
233 for j in cl.revs(max(0, i - 99), i):
233 for j in cl.revs(max(0, i - 99), i):
234 ctx = web.repo[j]
234 ctx = web.repo[j]
235 l.append(ctx)
235 l.append(ctx)
236 l.reverse()
236 l.reverse()
237 for e in l:
237 for e in l:
238 yield e
238 yield e
239
239
240 for ctx in revgen():
240 for ctx in revgen():
241 miss = 0
241 miss = 0
242 for q in qw:
242 for q in qw:
243 if not (
243 if not (
244 q in lower(ctx.user())
244 q in lower(ctx.user())
245 or q in lower(ctx.description())
245 or q in lower(ctx.description())
246 or q in lower(b" ".join(ctx.files()))
246 or q in lower(b" ".join(ctx.files()))
247 ):
247 ):
248 miss = 1
248 miss = 1
249 break
249 break
250 if miss:
250 if miss:
251 continue
251 continue
252
252
253 yield ctx
253 yield ctx
254
254
255 def revsetsearch(revs):
255 def revsetsearch(revs):
256 for r in revs:
256 for r in revs:
257 yield web.repo[r]
257 yield web.repo[r]
258
258
259 searchfuncs = {
259 searchfuncs = {
260 MODE_REVISION: (revsearch, b'exact revision search'),
260 MODE_REVISION: (revsearch, b'exact revision search'),
261 MODE_KEYWORD: (keywordsearch, b'literal keyword search'),
261 MODE_KEYWORD: (keywordsearch, b'literal keyword search'),
262 MODE_REVSET: (revsetsearch, b'revset expression search'),
262 MODE_REVSET: (revsetsearch, b'revset expression search'),
263 }
263 }
264
264
265 def getsearchmode(query):
265 def getsearchmode(query):
266 try:
266 try:
267 ctx = scmutil.revsymbol(web.repo, query)
267 ctx = scmutil.revsymbol(web.repo, query)
268 except (error.RepoError, error.LookupError):
268 except (error.RepoError, error.LookupError):
269 # query is not an exact revision pointer, need to
269 # query is not an exact revision pointer, need to
270 # decide if it's a revset expression or keywords
270 # decide if it's a revset expression or keywords
271 pass
271 pass
272 else:
272 else:
273 return MODE_REVISION, ctx
273 return MODE_REVISION, ctx
274
274
275 revdef = b'reverse(%s)' % query
275 revdef = b'reverse(%s)' % query
276 try:
276 try:
277 tree = revsetlang.parse(revdef)
277 tree = revsetlang.parse(revdef)
278 except error.ParseError:
278 except error.ParseError:
279 # can't parse to a revset tree
279 # can't parse to a revset tree
280 return MODE_KEYWORD, query
280 return MODE_KEYWORD, query
281
281
282 if revsetlang.depth(tree) <= 2:
282 if revsetlang.depth(tree) <= 2:
283 # no revset syntax used
283 # no revset syntax used
284 return MODE_KEYWORD, query
284 return MODE_KEYWORD, query
285
285
286 if any(
286 if any(
287 (token, (value or b'')[:3]) == (b'string', b're:')
287 (token, (value or b'')[:3]) == (b'string', b're:')
288 for token, value, pos in revsetlang.tokenize(revdef)
288 for token, value, pos in revsetlang.tokenize(revdef)
289 ):
289 ):
290 return MODE_KEYWORD, query
290 return MODE_KEYWORD, query
291
291
292 funcsused = revsetlang.funcsused(tree)
292 funcsused = revsetlang.funcsused(tree)
293 if not funcsused.issubset(revset.safesymbols):
293 if not funcsused.issubset(revset.safesymbols):
294 return MODE_KEYWORD, query
294 return MODE_KEYWORD, query
295
295
296 try:
296 try:
297 mfunc = revset.match(
297 mfunc = revset.match(
298 web.repo.ui, revdef, lookup=revset.lookupfn(web.repo)
298 web.repo.ui, revdef, lookup=revset.lookupfn(web.repo)
299 )
299 )
300 revs = mfunc(web.repo)
300 revs = mfunc(web.repo)
301 return MODE_REVSET, revs
301 return MODE_REVSET, revs
302 # ParseError: wrongly placed tokens, wrongs arguments, etc
302 # ParseError: wrongly placed tokens, wrongs arguments, etc
303 # RepoLookupError: no such revision, e.g. in 'revision:'
303 # RepoLookupError: no such revision, e.g. in 'revision:'
304 # Abort: bookmark/tag not exists
304 # Abort: bookmark/tag not exists
305 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
305 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
306 except (
306 except (
307 error.ParseError,
307 error.ParseError,
308 error.RepoLookupError,
308 error.RepoLookupError,
309 error.Abort,
309 error.Abort,
310 LookupError,
310 LookupError,
311 ):
311 ):
312 return MODE_KEYWORD, query
312 return MODE_KEYWORD, query
313
313
314 def changelist(context):
314 def changelist(context):
315 count = 0
315 count = 0
316
316
317 for ctx in searchfunc[0](funcarg):
317 for ctx in searchfunc[0](funcarg):
318 count += 1
318 count += 1
319 n = scmutil.binnode(ctx)
319 n = scmutil.binnode(ctx)
320 showtags = webutil.showtag(web.repo, b'changelogtag', n)
320 showtags = webutil.showtag(web.repo, b'changelogtag', n)
321 files = webutil.listfilediffs(ctx.files(), n, web.maxfiles)
321 files = webutil.listfilediffs(ctx.files(), n, web.maxfiles)
322
322
323 lm = webutil.commonentry(web.repo, ctx)
323 lm = webutil.commonentry(web.repo, ctx)
324 lm.update(
324 lm.update(
325 {
325 {
326 b'parity': next(parity),
326 b'parity': next(parity),
327 b'changelogtag': showtags,
327 b'changelogtag': showtags,
328 b'files': files,
328 b'files': files,
329 }
329 }
330 )
330 )
331 yield lm
331 yield lm
332
332
333 if count >= revcount:
333 if count >= revcount:
334 break
334 break
335
335
336 query = web.req.qsparams[b'rev']
336 query = web.req.qsparams[b'rev']
337 revcount = web.maxchanges
337 revcount = web.maxchanges
338 if b'revcount' in web.req.qsparams:
338 if b'revcount' in web.req.qsparams:
339 try:
339 try:
340 revcount = int(web.req.qsparams.get(b'revcount', revcount))
340 revcount = int(web.req.qsparams.get(b'revcount', revcount))
341 revcount = max(revcount, 1)
341 revcount = max(revcount, 1)
342 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
342 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
343 except ValueError:
343 except ValueError:
344 pass
344 pass
345
345
346 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
346 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
347 lessvars[b'revcount'] = max(revcount // 2, 1)
347 lessvars[b'revcount'] = max(revcount // 2, 1)
348 lessvars[b'rev'] = query
348 lessvars[b'rev'] = query
349 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
349 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
350 morevars[b'revcount'] = revcount * 2
350 morevars[b'revcount'] = revcount * 2
351 morevars[b'rev'] = query
351 morevars[b'rev'] = query
352
352
353 mode, funcarg = getsearchmode(query)
353 mode, funcarg = getsearchmode(query)
354
354
355 if b'forcekw' in web.req.qsparams:
355 if b'forcekw' in web.req.qsparams:
356 showforcekw = b''
356 showforcekw = b''
357 showunforcekw = searchfuncs[mode][1]
357 showunforcekw = searchfuncs[mode][1]
358 mode = MODE_KEYWORD
358 mode = MODE_KEYWORD
359 funcarg = query
359 funcarg = query
360 else:
360 else:
361 if mode != MODE_KEYWORD:
361 if mode != MODE_KEYWORD:
362 showforcekw = searchfuncs[MODE_KEYWORD][1]
362 showforcekw = searchfuncs[MODE_KEYWORD][1]
363 else:
363 else:
364 showforcekw = b''
364 showforcekw = b''
365 showunforcekw = b''
365 showunforcekw = b''
366
366
367 searchfunc = searchfuncs[mode]
367 searchfunc = searchfuncs[mode]
368
368
369 tip = web.repo[b'tip']
369 tip = web.repo[b'tip']
370 parity = paritygen(web.stripecount)
370 parity = paritygen(web.stripecount)
371
371
372 return web.sendtemplate(
372 return web.sendtemplate(
373 b'search',
373 b'search',
374 query=query,
374 query=query,
375 node=tip.hex(),
375 node=tip.hex(),
376 symrev=b'tip',
376 symrev=b'tip',
377 entries=templateutil.mappinggenerator(changelist, name=b'searchentry'),
377 entries=templateutil.mappinggenerator(changelist, name=b'searchentry'),
378 archives=web.archivelist(b'tip'),
378 archives=web.archivelist(b'tip'),
379 morevars=morevars,
379 morevars=morevars,
380 lessvars=lessvars,
380 lessvars=lessvars,
381 modedesc=searchfunc[1],
381 modedesc=searchfunc[1],
382 showforcekw=showforcekw,
382 showforcekw=showforcekw,
383 showunforcekw=showunforcekw,
383 showunforcekw=showunforcekw,
384 )
384 )
385
385
386
386
387 @webcommand(b'changelog')
387 @webcommand(b'changelog')
388 def changelog(web, shortlog=False):
388 def changelog(web, shortlog=False):
389 """
389 """
390 /changelog[/{revision}]
390 /changelog[/{revision}]
391 -----------------------
391 -----------------------
392
392
393 Show information about multiple changesets.
393 Show information about multiple changesets.
394
394
395 If the optional ``revision`` URL argument is absent, information about
395 If the optional ``revision`` URL argument is absent, information about
396 all changesets starting at ``tip`` will be rendered. If the ``revision``
396 all changesets starting at ``tip`` will be rendered. If the ``revision``
397 argument is present, changesets will be shown starting from the specified
397 argument is present, changesets will be shown starting from the specified
398 revision.
398 revision.
399
399
400 If ``revision`` is absent, the ``rev`` query string argument may be
400 If ``revision`` is absent, the ``rev`` query string argument may be
401 defined. This will perform a search for changesets.
401 defined. This will perform a search for changesets.
402
402
403 The argument for ``rev`` can be a single revision, a revision set,
403 The argument for ``rev`` can be a single revision, a revision set,
404 or a literal keyword to search for in changeset data (equivalent to
404 or a literal keyword to search for in changeset data (equivalent to
405 :hg:`log -k`).
405 :hg:`log -k`).
406
406
407 The ``revcount`` query string argument defines the maximum numbers of
407 The ``revcount`` query string argument defines the maximum numbers of
408 changesets to render.
408 changesets to render.
409
409
410 For non-searches, the ``changelog`` template will be rendered.
410 For non-searches, the ``changelog`` template will be rendered.
411 """
411 """
412
412
413 query = b''
413 query = b''
414 if b'node' in web.req.qsparams:
414 if b'node' in web.req.qsparams:
415 ctx = webutil.changectx(web.repo, web.req)
415 ctx = webutil.changectx(web.repo, web.req)
416 symrev = webutil.symrevorshortnode(web.req, ctx)
416 symrev = webutil.symrevorshortnode(web.req, ctx)
417 elif b'rev' in web.req.qsparams:
417 elif b'rev' in web.req.qsparams:
418 return _search(web)
418 return _search(web)
419 else:
419 else:
420 ctx = web.repo[b'tip']
420 ctx = web.repo[b'tip']
421 symrev = b'tip'
421 symrev = b'tip'
422
422
423 def changelist(maxcount):
423 def changelist(maxcount):
424 revs = []
424 revs = []
425 if pos != -1:
425 if pos != -1:
426 revs = web.repo.changelog.revs(pos, 0)
426 revs = web.repo.changelog.revs(pos, 0)
427
427
428 for entry in webutil.changelistentries(web, revs, maxcount, parity):
428 for entry in webutil.changelistentries(web, revs, maxcount, parity):
429 yield entry
429 yield entry
430
430
431 if shortlog:
431 if shortlog:
432 revcount = web.maxshortchanges
432 revcount = web.maxshortchanges
433 else:
433 else:
434 revcount = web.maxchanges
434 revcount = web.maxchanges
435
435
436 if b'revcount' in web.req.qsparams:
436 if b'revcount' in web.req.qsparams:
437 try:
437 try:
438 revcount = int(web.req.qsparams.get(b'revcount', revcount))
438 revcount = int(web.req.qsparams.get(b'revcount', revcount))
439 revcount = max(revcount, 1)
439 revcount = max(revcount, 1)
440 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
440 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
441 except ValueError:
441 except ValueError:
442 pass
442 pass
443
443
444 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
444 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
445 lessvars[b'revcount'] = max(revcount // 2, 1)
445 lessvars[b'revcount'] = max(revcount // 2, 1)
446 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
446 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
447 morevars[b'revcount'] = revcount * 2
447 morevars[b'revcount'] = revcount * 2
448
448
449 count = len(web.repo)
449 count = len(web.repo)
450 pos = ctx.rev()
450 pos = ctx.rev()
451 parity = paritygen(web.stripecount)
451 parity = paritygen(web.stripecount)
452
452
453 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
453 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
454
454
455 entries = list(changelist(revcount + 1))
455 entries = list(changelist(revcount + 1))
456 latestentry = entries[:1]
456 latestentry = entries[:1]
457 if len(entries) > revcount:
457 if len(entries) > revcount:
458 nextentry = entries[-1:]
458 nextentry = entries[-1:]
459 entries = entries[:-1]
459 entries = entries[:-1]
460 else:
460 else:
461 nextentry = []
461 nextentry = []
462
462
463 return web.sendtemplate(
463 return web.sendtemplate(
464 b'shortlog' if shortlog else b'changelog',
464 b'shortlog' if shortlog else b'changelog',
465 changenav=changenav,
465 changenav=changenav,
466 node=ctx.hex(),
466 node=ctx.hex(),
467 rev=pos,
467 rev=pos,
468 symrev=symrev,
468 symrev=symrev,
469 changesets=count,
469 changesets=count,
470 entries=templateutil.mappinglist(entries),
470 entries=templateutil.mappinglist(entries),
471 latestentry=templateutil.mappinglist(latestentry),
471 latestentry=templateutil.mappinglist(latestentry),
472 nextentry=templateutil.mappinglist(nextentry),
472 nextentry=templateutil.mappinglist(nextentry),
473 archives=web.archivelist(b'tip'),
473 archives=web.archivelist(b'tip'),
474 revcount=revcount,
474 revcount=revcount,
475 morevars=morevars,
475 morevars=morevars,
476 lessvars=lessvars,
476 lessvars=lessvars,
477 query=query,
477 query=query,
478 )
478 )
479
479
480
480
481 @webcommand(b'shortlog')
481 @webcommand(b'shortlog')
482 def shortlog(web):
482 def shortlog(web):
483 """
483 """
484 /shortlog
484 /shortlog
485 ---------
485 ---------
486
486
487 Show basic information about a set of changesets.
487 Show basic information about a set of changesets.
488
488
489 This accepts the same parameters as the ``changelog`` handler. The only
489 This accepts the same parameters as the ``changelog`` handler. The only
490 difference is the ``shortlog`` template will be rendered instead of the
490 difference is the ``shortlog`` template will be rendered instead of the
491 ``changelog`` template.
491 ``changelog`` template.
492 """
492 """
493 return changelog(web, shortlog=True)
493 return changelog(web, shortlog=True)
494
494
495
495
496 @webcommand(b'changeset')
496 @webcommand(b'changeset')
497 def changeset(web):
497 def changeset(web):
498 """
498 """
499 /changeset[/{revision}]
499 /changeset[/{revision}]
500 -----------------------
500 -----------------------
501
501
502 Show information about a single changeset.
502 Show information about a single changeset.
503
503
504 A URL path argument is the changeset identifier to show. See ``hg help
504 A URL path argument is the changeset identifier to show. See ``hg help
505 revisions`` for possible values. If not defined, the ``tip`` changeset
505 revisions`` for possible values. If not defined, the ``tip`` changeset
506 will be shown.
506 will be shown.
507
507
508 The ``changeset`` template is rendered. Contents of the ``changesettag``,
508 The ``changeset`` template is rendered. Contents of the ``changesettag``,
509 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
509 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
510 templates related to diffs may all be used to produce the output.
510 templates related to diffs may all be used to produce the output.
511 """
511 """
512 ctx = webutil.changectx(web.repo, web.req)
512 ctx = webutil.changectx(web.repo, web.req)
513
513
514 return web.sendtemplate(b'changeset', **webutil.changesetentry(web, ctx))
514 return web.sendtemplate(b'changeset', **webutil.changesetentry(web, ctx))
515
515
516
516
517 rev = webcommand(b'rev')(changeset)
517 rev = webcommand(b'rev')(changeset)
518
518
519
519
520 def decodepath(path):
520 def decodepath(path):
521 # type: (bytes) -> bytes
521 # type: (bytes) -> bytes
522 """Hook for mapping a path in the repository to a path in the
522 """Hook for mapping a path in the repository to a path in the
523 working copy.
523 working copy.
524
524
525 Extensions (e.g., largefiles) can override this to remap files in
525 Extensions (e.g., largefiles) can override this to remap files in
526 the virtual file system presented by the manifest command below."""
526 the virtual file system presented by the manifest command below."""
527 return path
527 return path
528
528
529
529
530 @webcommand(b'manifest')
530 @webcommand(b'manifest')
531 def manifest(web):
531 def manifest(web):
532 """
532 """
533 /manifest[/{revision}[/{path}]]
533 /manifest[/{revision}[/{path}]]
534 -------------------------------
534 -------------------------------
535
535
536 Show information about a directory.
536 Show information about a directory.
537
537
538 If the URL path arguments are omitted, information about the root
538 If the URL path arguments are omitted, information about the root
539 directory for the ``tip`` changeset will be shown.
539 directory for the ``tip`` changeset will be shown.
540
540
541 Because this handler can only show information for directories, it
541 Because this handler can only show information for directories, it
542 is recommended to use the ``file`` handler instead, as it can handle both
542 is recommended to use the ``file`` handler instead, as it can handle both
543 directories and files.
543 directories and files.
544
544
545 The ``manifest`` template will be rendered for this handler.
545 The ``manifest`` template will be rendered for this handler.
546 """
546 """
547 if b'node' in web.req.qsparams:
547 if b'node' in web.req.qsparams:
548 ctx = webutil.changectx(web.repo, web.req)
548 ctx = webutil.changectx(web.repo, web.req)
549 symrev = webutil.symrevorshortnode(web.req, ctx)
549 symrev = webutil.symrevorshortnode(web.req, ctx)
550 else:
550 else:
551 ctx = web.repo[b'tip']
551 ctx = web.repo[b'tip']
552 symrev = b'tip'
552 symrev = b'tip'
553 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b''))
553 path = webutil.cleanpath(web.repo, web.req.qsparams.get(b'file', b''))
554 mf = ctx.manifest()
554 mf = ctx.manifest()
555 node = scmutil.binnode(ctx)
555 node = scmutil.binnode(ctx)
556
556
557 files = {}
557 files = {}
558 dirs = {}
558 dirs = {}
559 parity = paritygen(web.stripecount)
559 parity = paritygen(web.stripecount)
560
560
561 if path and path[-1:] != b"/":
561 if path and path[-1:] != b"/":
562 path += b"/"
562 path += b"/"
563 l = len(path)
563 l = len(path)
564 abspath = b"/" + path
564 abspath = b"/" + path
565
565
566 for full, n in mf.items():
566 for full, n in mf.items():
567 # the virtual path (working copy path) used for the full
567 # the virtual path (working copy path) used for the full
568 # (repository) path
568 # (repository) path
569 f = decodepath(full)
569 f = decodepath(full)
570
570
571 if f[:l] != path:
571 if f[:l] != path:
572 continue
572 continue
573 remain = f[l:]
573 remain = f[l:]
574 elements = remain.split(b'/')
574 elements = remain.split(b'/')
575 if len(elements) == 1:
575 if len(elements) == 1:
576 files[remain] = full
576 files[remain] = full
577 else:
577 else:
578 h = dirs # need to retain ref to dirs (root)
578 h = dirs # need to retain ref to dirs (root)
579 for elem in elements[0:-1]:
579 for elem in elements[0:-1]:
580 if elem not in h:
580 if elem not in h:
581 h[elem] = {}
581 h[elem] = {}
582 h = h[elem]
582 h = h[elem]
583 if len(h) > 1:
583 if len(h) > 1:
584 break
584 break
585 h[None] = None # denotes files present
585 h[None] = None # denotes files present
586
586
587 if mf and not files and not dirs:
587 if mf and not files and not dirs:
588 raise ErrorResponse(HTTP_NOT_FOUND, b'path not found: ' + path)
588 raise ErrorResponse(HTTP_NOT_FOUND, b'path not found: ' + path)
589
589
590 def filelist(context):
590 def filelist(context):
591 for f in sorted(files):
591 for f in sorted(files):
592 full = files[f]
592 full = files[f]
593
593
594 fctx = ctx.filectx(full)
594 fctx = ctx.filectx(full)
595 yield {
595 yield {
596 b"file": full,
596 b"file": full,
597 b"parity": next(parity),
597 b"parity": next(parity),
598 b"basename": f,
598 b"basename": f,
599 b"date": fctx.date(),
599 b"date": fctx.date(),
600 b"size": fctx.size(),
600 b"size": fctx.size(),
601 b"permissions": mf.flags(full),
601 b"permissions": mf.flags(full),
602 }
602 }
603
603
604 def dirlist(context):
604 def dirlist(context):
605 for d in sorted(dirs):
605 for d in sorted(dirs):
606
606
607 emptydirs = []
607 emptydirs = []
608 h = dirs[d]
608 h = dirs[d]
609 while isinstance(h, dict) and len(h) == 1:
609 while isinstance(h, dict) and len(h) == 1:
610 k, v = next(iter(h.items()))
610 k, v = next(iter(h.items()))
611 if v:
611 if v:
612 emptydirs.append(k)
612 emptydirs.append(k)
613 h = v
613 h = v
614
614
615 path = b"%s%s" % (abspath, d)
615 path = b"%s%s" % (abspath, d)
616 yield {
616 yield {
617 b"parity": next(parity),
617 b"parity": next(parity),
618 b"path": path,
618 b"path": path,
619 # pytype: disable=wrong-arg-types
619 # pytype: disable=wrong-arg-types
620 b"emptydirs": b"/".join(emptydirs),
620 b"emptydirs": b"/".join(emptydirs),
621 # pytype: enable=wrong-arg-types
621 # pytype: enable=wrong-arg-types
622 b"basename": d,
622 b"basename": d,
623 }
623 }
624
624
625 return web.sendtemplate(
625 return web.sendtemplate(
626 b'manifest',
626 b'manifest',
627 symrev=symrev,
627 symrev=symrev,
628 path=abspath,
628 path=abspath,
629 up=webutil.up(abspath),
629 up=webutil.up(abspath),
630 upparity=next(parity),
630 upparity=next(parity),
631 fentries=templateutil.mappinggenerator(filelist),
631 fentries=templateutil.mappinggenerator(filelist),
632 dentries=templateutil.mappinggenerator(dirlist),
632 dentries=templateutil.mappinggenerator(dirlist),
633 archives=web.archivelist(hex(node)),
633 archives=web.archivelist(hex(node)),
634 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
634 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
635 )
635 )
636
636
637
637
638 @webcommand(b'tags')
638 @webcommand(b'tags')
639 def tags(web):
639 def tags(web):
640 """
640 """
641 /tags
641 /tags
642 -----
642 -----
643
643
644 Show information about tags.
644 Show information about tags.
645
645
646 No arguments are accepted.
646 No arguments are accepted.
647
647
648 The ``tags`` template is rendered.
648 The ``tags`` template is rendered.
649 """
649 """
650 i = list(reversed(web.repo.tagslist()))
650 i = list(reversed(web.repo.tagslist()))
651 parity = paritygen(web.stripecount)
651 parity = paritygen(web.stripecount)
652
652
653 def entries(context, notip, latestonly):
653 def entries(context, notip, latestonly):
654 t = i
654 t = i
655 if notip:
655 if notip:
656 t = [(k, n) for k, n in i if k != b"tip"]
656 t = [(k, n) for k, n in i if k != b"tip"]
657 if latestonly:
657 if latestonly:
658 t = t[:1]
658 t = t[:1]
659 for k, n in t:
659 for k, n in t:
660 yield {
660 yield {
661 b"parity": next(parity),
661 b"parity": next(parity),
662 b"tag": k,
662 b"tag": k,
663 b"date": web.repo[n].date(),
663 b"date": web.repo[n].date(),
664 b"node": hex(n),
664 b"node": hex(n),
665 }
665 }
666
666
667 return web.sendtemplate(
667 return web.sendtemplate(
668 b'tags',
668 b'tags',
669 node=hex(web.repo.changelog.tip()),
669 node=hex(web.repo.changelog.tip()),
670 entries=templateutil.mappinggenerator(entries, args=(False, False)),
670 entries=templateutil.mappinggenerator(entries, args=(False, False)),
671 entriesnotip=templateutil.mappinggenerator(entries, args=(True, False)),
671 entriesnotip=templateutil.mappinggenerator(entries, args=(True, False)),
672 latestentry=templateutil.mappinggenerator(entries, args=(True, True)),
672 latestentry=templateutil.mappinggenerator(entries, args=(True, True)),
673 )
673 )
674
674
675
675
676 @webcommand(b'bookmarks')
676 @webcommand(b'bookmarks')
677 def bookmarks(web):
677 def bookmarks(web):
678 """
678 """
679 /bookmarks
679 /bookmarks
680 ----------
680 ----------
681
681
682 Show information about bookmarks.
682 Show information about bookmarks.
683
683
684 No arguments are accepted.
684 No arguments are accepted.
685
685
686 The ``bookmarks`` template is rendered.
686 The ``bookmarks`` template is rendered.
687 """
687 """
688 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
688 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
689 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
689 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
690 i = sorted(i, key=sortkey, reverse=True)
690 i = sorted(i, key=sortkey, reverse=True)
691 parity = paritygen(web.stripecount)
691 parity = paritygen(web.stripecount)
692
692
693 def entries(context, latestonly):
693 def entries(context, latestonly):
694 t = i
694 t = i
695 if latestonly:
695 if latestonly:
696 t = i[:1]
696 t = i[:1]
697 for k, n in t:
697 for k, n in t:
698 yield {
698 yield {
699 b"parity": next(parity),
699 b"parity": next(parity),
700 b"bookmark": k,
700 b"bookmark": k,
701 b"date": web.repo[n].date(),
701 b"date": web.repo[n].date(),
702 b"node": hex(n),
702 b"node": hex(n),
703 }
703 }
704
704
705 if i:
705 if i:
706 latestrev = i[0][1]
706 latestrev = i[0][1]
707 else:
707 else:
708 latestrev = -1
708 latestrev = -1
709 lastdate = web.repo[latestrev].date()
709 lastdate = web.repo[latestrev].date()
710
710
711 return web.sendtemplate(
711 return web.sendtemplate(
712 b'bookmarks',
712 b'bookmarks',
713 node=hex(web.repo.changelog.tip()),
713 node=hex(web.repo.changelog.tip()),
714 lastchange=templateutil.mappinglist([{b'date': lastdate}]),
714 lastchange=templateutil.mappinglist([{b'date': lastdate}]),
715 entries=templateutil.mappinggenerator(entries, args=(False,)),
715 entries=templateutil.mappinggenerator(entries, args=(False,)),
716 latestentry=templateutil.mappinggenerator(entries, args=(True,)),
716 latestentry=templateutil.mappinggenerator(entries, args=(True,)),
717 )
717 )
718
718
719
719
720 @webcommand(b'branches')
720 @webcommand(b'branches')
721 def branches(web):
721 def branches(web):
722 """
722 """
723 /branches
723 /branches
724 ---------
724 ---------
725
725
726 Show information about branches.
726 Show information about branches.
727
727
728 All known branches are contained in the output, even closed branches.
728 All known branches are contained in the output, even closed branches.
729
729
730 No arguments are accepted.
730 No arguments are accepted.
731
731
732 The ``branches`` template is rendered.
732 The ``branches`` template is rendered.
733 """
733 """
734 entries = webutil.branchentries(web.repo, web.stripecount)
734 entries = webutil.branchentries(web.repo, web.stripecount)
735 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
735 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
736
736
737 return web.sendtemplate(
737 return web.sendtemplate(
738 b'branches',
738 b'branches',
739 node=hex(web.repo.changelog.tip()),
739 node=hex(web.repo.changelog.tip()),
740 entries=entries,
740 entries=entries,
741 latestentry=latestentry,
741 latestentry=latestentry,
742 )
742 )
743
743
744
744
745 @webcommand(b'summary')
745 @webcommand(b'summary')
746 def summary(web):
746 def summary(web):
747 """
747 """
748 /summary
748 /summary
749 --------
749 --------
750
750
751 Show a summary of repository state.
751 Show a summary of repository state.
752
752
753 Information about the latest changesets, bookmarks, tags, and branches
753 Information about the latest changesets, bookmarks, tags, and branches
754 is captured by this handler.
754 is captured by this handler.
755
755
756 The ``summary`` template is rendered.
756 The ``summary`` template is rendered.
757 """
757 """
758 i = reversed(web.repo.tagslist())
758 i = reversed(web.repo.tagslist())
759
759
760 def tagentries(context):
760 def tagentries(context):
761 parity = paritygen(web.stripecount)
761 parity = paritygen(web.stripecount)
762 count = 0
762 count = 0
763 for k, n in i:
763 for k, n in i:
764 if k == b"tip": # skip tip
764 if k == b"tip": # skip tip
765 continue
765 continue
766
766
767 count += 1
767 count += 1
768 if count > 10: # limit to 10 tags
768 if count > 10: # limit to 10 tags
769 break
769 break
770
770
771 yield {
771 yield {
772 b'parity': next(parity),
772 b'parity': next(parity),
773 b'tag': k,
773 b'tag': k,
774 b'node': hex(n),
774 b'node': hex(n),
775 b'date': web.repo[n].date(),
775 b'date': web.repo[n].date(),
776 }
776 }
777
777
778 def bookmarks(context):
778 def bookmarks(context):
779 parity = paritygen(web.stripecount)
779 parity = paritygen(web.stripecount)
780 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
780 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
781 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
781 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
782 marks = sorted(marks, key=sortkey, reverse=True)
782 marks = sorted(marks, key=sortkey, reverse=True)
783 for k, n in marks[:10]: # limit to 10 bookmarks
783 for k, n in marks[:10]: # limit to 10 bookmarks
784 yield {
784 yield {
785 b'parity': next(parity),
785 b'parity': next(parity),
786 b'bookmark': k,
786 b'bookmark': k,
787 b'date': web.repo[n].date(),
787 b'date': web.repo[n].date(),
788 b'node': hex(n),
788 b'node': hex(n),
789 }
789 }
790
790
791 def changelist(context):
791 def changelist(context):
792 parity = paritygen(web.stripecount, offset=start - end)
792 parity = paritygen(web.stripecount, offset=start - end)
793 l = [] # build a list in forward order for efficiency
793 l = [] # build a list in forward order for efficiency
794 revs = []
794 revs = []
795 if start < end:
795 if start < end:
796 revs = web.repo.changelog.revs(start, end - 1)
796 revs = web.repo.changelog.revs(start, end - 1)
797 for i in revs:
797 for i in revs:
798 ctx = web.repo[i]
798 ctx = web.repo[i]
799 lm = webutil.commonentry(web.repo, ctx)
799 lm = webutil.commonentry(web.repo, ctx)
800 lm[b'parity'] = next(parity)
800 lm[b'parity'] = next(parity)
801 l.append(lm)
801 l.append(lm)
802
802
803 for entry in reversed(l):
803 for entry in reversed(l):
804 yield entry
804 yield entry
805
805
806 tip = web.repo[b'tip']
806 tip = web.repo[b'tip']
807 count = len(web.repo)
807 count = len(web.repo)
808 start = max(0, count - web.maxchanges)
808 start = max(0, count - web.maxchanges)
809 end = min(count, start + web.maxchanges)
809 end = min(count, start + web.maxchanges)
810
810
811 desc = web.config(b"web", b"description")
811 desc = web.config(b"web", b"description")
812 if not desc:
812 if not desc:
813 desc = b'unknown'
813 desc = b'unknown'
814 labels = web.configlist(b'web', b'labels')
814 labels = web.configlist(b'web', b'labels')
815
815
816 return web.sendtemplate(
816 return web.sendtemplate(
817 b'summary',
817 b'summary',
818 desc=desc,
818 desc=desc,
819 owner=get_contact(web.config) or b'unknown',
819 owner=get_contact(web.config) or b'unknown',
820 lastchange=tip.date(),
820 lastchange=tip.date(),
821 tags=templateutil.mappinggenerator(tagentries, name=b'tagentry'),
821 tags=templateutil.mappinggenerator(tagentries, name=b'tagentry'),
822 bookmarks=templateutil.mappinggenerator(bookmarks),
822 bookmarks=templateutil.mappinggenerator(bookmarks),
823 branches=webutil.branchentries(web.repo, web.stripecount, 10),
823 branches=webutil.branchentries(web.repo, web.stripecount, 10),
824 shortlog=templateutil.mappinggenerator(
824 shortlog=templateutil.mappinggenerator(
825 changelist, name=b'shortlogentry'
825 changelist, name=b'shortlogentry'
826 ),
826 ),
827 node=tip.hex(),
827 node=tip.hex(),
828 symrev=b'tip',
828 symrev=b'tip',
829 archives=web.archivelist(b'tip'),
829 archives=web.archivelist(b'tip'),
830 labels=templateutil.hybridlist(labels, name=b'label'),
830 labels=templateutil.hybridlist(labels, name=b'label'),
831 )
831 )
832
832
833
833
834 @webcommand(b'filediff')
834 @webcommand(b'filediff')
835 def filediff(web):
835 def filediff(web):
836 """
836 """
837 /diff/{revision}/{path}
837 /diff/{revision}/{path}
838 -----------------------
838 -----------------------
839
839
840 Show how a file changed in a particular commit.
840 Show how a file changed in a particular commit.
841
841
842 The ``filediff`` template is rendered.
842 The ``filediff`` template is rendered.
843
843
844 This handler is registered under both the ``/diff`` and ``/filediff``
844 This handler is registered under both the ``/diff`` and ``/filediff``
845 paths. ``/diff`` is used in modern code.
845 paths. ``/diff`` is used in modern code.
846 """
846 """
847 fctx, ctx = None, None
847 fctx, ctx = None, None
848 try:
848 try:
849 fctx = webutil.filectx(web.repo, web.req)
849 fctx = webutil.filectx(web.repo, web.req)
850 except LookupError:
850 except LookupError:
851 ctx = webutil.changectx(web.repo, web.req)
851 ctx = webutil.changectx(web.repo, web.req)
852 path = webutil.cleanpath(web.repo, web.req.qsparams[b'file'])
852 path = webutil.cleanpath(web.repo, web.req.qsparams[b'file'])
853 if path not in ctx.files():
853 if path not in ctx.files():
854 raise
854 raise
855
855
856 if fctx is not None:
856 if fctx is not None:
857 path = fctx.path()
857 path = fctx.path()
858 ctx = fctx.changectx()
858 ctx = fctx.changectx()
859 basectx = ctx.p1()
859 basectx = ctx.p1()
860
860
861 style = web.config(b'web', b'style')
861 style = web.config(b'web', b'style')
862 if b'style' in web.req.qsparams:
862 if b'style' in web.req.qsparams:
863 style = web.req.qsparams[b'style']
863 style = web.req.qsparams[b'style']
864
864
865 diffs = webutil.diffs(web, ctx, basectx, [path], style)
865 diffs = webutil.diffs(web, ctx, basectx, [path], style)
866 if fctx is not None:
866 if fctx is not None:
867 rename = webutil.renamelink(fctx)
867 rename = webutil.renamelink(fctx)
868 ctx = fctx
868 ctx = fctx
869 else:
869 else:
870 rename = templateutil.mappinglist([])
870 rename = templateutil.mappinglist([])
871 ctx = ctx
871 ctx = ctx
872
872
873 return web.sendtemplate(
873 return web.sendtemplate(
874 b'filediff',
874 b'filediff',
875 file=path,
875 file=path,
876 symrev=webutil.symrevorshortnode(web.req, ctx),
876 symrev=webutil.symrevorshortnode(web.req, ctx),
877 rename=rename,
877 rename=rename,
878 diff=diffs,
878 diff=diffs,
879 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
879 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
880 )
880 )
881
881
882
882
883 diff = webcommand(b'diff')(filediff)
883 diff = webcommand(b'diff')(filediff)
884
884
885
885
886 @webcommand(b'comparison')
886 @webcommand(b'comparison')
887 def comparison(web):
887 def comparison(web):
888 """
888 """
889 /comparison/{revision}/{path}
889 /comparison/{revision}/{path}
890 -----------------------------
890 -----------------------------
891
891
892 Show a comparison between the old and new versions of a file from changes
892 Show a comparison between the old and new versions of a file from changes
893 made on a particular revision.
893 made on a particular revision.
894
894
895 This is similar to the ``diff`` handler. However, this form features
895 This is similar to the ``diff`` handler. However, this form features
896 a split or side-by-side diff rather than a unified diff.
896 a split or side-by-side diff rather than a unified diff.
897
897
898 The ``context`` query string argument can be used to control the lines of
898 The ``context`` query string argument can be used to control the lines of
899 context in the diff.
899 context in the diff.
900
900
901 The ``filecomparison`` template is rendered.
901 The ``filecomparison`` template is rendered.
902 """
902 """
903 ctx = webutil.changectx(web.repo, web.req)
903 ctx = webutil.changectx(web.repo, web.req)
904 if b'file' not in web.req.qsparams:
904 if b'file' not in web.req.qsparams:
905 raise ErrorResponse(HTTP_NOT_FOUND, b'file not given')
905 raise ErrorResponse(HTTP_NOT_FOUND, b'file not given')
906 path = webutil.cleanpath(web.repo, web.req.qsparams[b'file'])
906 path = webutil.cleanpath(web.repo, web.req.qsparams[b'file'])
907
907
908 parsecontext = lambda v: v == b'full' and -1 or int(v)
908 parsecontext = lambda v: v == b'full' and -1 or int(v)
909 if b'context' in web.req.qsparams:
909 if b'context' in web.req.qsparams:
910 context = parsecontext(web.req.qsparams[b'context'])
910 context = parsecontext(web.req.qsparams[b'context'])
911 else:
911 else:
912 context = parsecontext(web.config(b'web', b'comparisoncontext'))
912 context = parsecontext(web.config(b'web', b'comparisoncontext'))
913
913
914 def filelines(f):
914 def filelines(f):
915 if f.isbinary():
915 if f.isbinary():
916 mt = pycompat.sysbytes(
916 mt = pycompat.sysbytes(
917 mimetypes.guess_type(pycompat.fsdecode(f.path()))[0]
917 mimetypes.guess_type(pycompat.fsdecode(f.path()))[0]
918 or r'application/octet-stream'
918 or r'application/octet-stream'
919 )
919 )
920 return [_(b'(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
920 return [_(b'(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
921 return f.data().splitlines()
921 return f.data().splitlines()
922
922
923 fctx = None
923 fctx = None
924 parent = ctx.p1()
924 parent = ctx.p1()
925 leftrev = parent.rev()
925 leftrev = parent.rev()
926 leftnode = parent.node()
926 leftnode = parent.node()
927 rightrev = ctx.rev()
927 rightrev = ctx.rev()
928 rightnode = scmutil.binnode(ctx)
928 rightnode = scmutil.binnode(ctx)
929 if path in ctx:
929 if path in ctx:
930 fctx = ctx[path]
930 fctx = ctx[path]
931 rightlines = filelines(fctx)
931 rightlines = filelines(fctx)
932 if path not in parent:
932 if path not in parent:
933 leftlines = ()
933 leftlines = ()
934 else:
934 else:
935 pfctx = parent[path]
935 pfctx = parent[path]
936 leftlines = filelines(pfctx)
936 leftlines = filelines(pfctx)
937 else:
937 else:
938 rightlines = ()
938 rightlines = ()
939 pfctx = ctx.p1()[path]
939 pfctx = ctx.p1()[path]
940 leftlines = filelines(pfctx)
940 leftlines = filelines(pfctx)
941
941
942 comparison = webutil.compare(context, leftlines, rightlines)
942 comparison = webutil.compare(context, leftlines, rightlines)
943 if fctx is not None:
943 if fctx is not None:
944 rename = webutil.renamelink(fctx)
944 rename = webutil.renamelink(fctx)
945 ctx = fctx
945 ctx = fctx
946 else:
946 else:
947 rename = templateutil.mappinglist([])
947 rename = templateutil.mappinglist([])
948 ctx = ctx
948 ctx = ctx
949
949
950 return web.sendtemplate(
950 return web.sendtemplate(
951 b'filecomparison',
951 b'filecomparison',
952 file=path,
952 file=path,
953 symrev=webutil.symrevorshortnode(web.req, ctx),
953 symrev=webutil.symrevorshortnode(web.req, ctx),
954 rename=rename,
954 rename=rename,
955 leftrev=leftrev,
955 leftrev=leftrev,
956 leftnode=hex(leftnode),
956 leftnode=hex(leftnode),
957 rightrev=rightrev,
957 rightrev=rightrev,
958 rightnode=hex(rightnode),
958 rightnode=hex(rightnode),
959 comparison=comparison,
959 comparison=comparison,
960 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
960 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))
961 )
961 )
962
962
963
963
964 @webcommand(b'annotate')
964 @webcommand(b'annotate')
965 def annotate(web):
965 def annotate(web):
966 """
966 """
967 /annotate/{revision}/{path}
967 /annotate/{revision}/{path}
968 ---------------------------
968 ---------------------------
969
969
970 Show changeset information for each line in a file.
970 Show changeset information for each line in a file.
971
971
972 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
972 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
973 ``ignoreblanklines`` query string arguments have the same meaning as
973 ``ignoreblanklines`` query string arguments have the same meaning as
974 their ``[annotate]`` config equivalents. It uses the hgrc boolean
974 their ``[annotate]`` config equivalents. It uses the hgrc boolean
975 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
975 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
976 false and ``1`` and ``true`` are true. If not defined, the server
976 false and ``1`` and ``true`` are true. If not defined, the server
977 default settings are used.
977 default settings are used.
978
978
979 The ``fileannotate`` template is rendered.
979 The ``fileannotate`` template is rendered.
980 """
980 """
981 fctx = webutil.filectx(web.repo, web.req)
981 fctx = webutil.filectx(web.repo, web.req)
982 f = fctx.path()
982 f = fctx.path()
983 parity = paritygen(web.stripecount)
983 parity = paritygen(web.stripecount)
984 ishead = fctx.filenode() in fctx.filelog().heads()
984 ishead = fctx.filenode() in fctx.filelog().heads()
985
985
986 # parents() is called once per line and several lines likely belong to
986 # parents() is called once per line and several lines likely belong to
987 # same revision. So it is worth caching.
987 # same revision. So it is worth caching.
988 # TODO there are still redundant operations within basefilectx.parents()
988 # TODO there are still redundant operations within basefilectx.parents()
989 # and from the fctx.annotate() call itself that could be cached.
989 # and from the fctx.annotate() call itself that could be cached.
990 parentscache = {}
990 parentscache = {}
991
991
992 def parents(context, f):
992 def parents(context, f):
993 rev = f.rev()
993 rev = f.rev()
994 if rev not in parentscache:
994 if rev not in parentscache:
995 parentscache[rev] = []
995 parentscache[rev] = []
996 for p in f.parents():
996 for p in f.parents():
997 entry = {
997 entry = {
998 b'node': p.hex(),
998 b'node': p.hex(),
999 b'rev': p.rev(),
999 b'rev': p.rev(),
1000 }
1000 }
1001 parentscache[rev].append(entry)
1001 parentscache[rev].append(entry)
1002
1002
1003 for p in parentscache[rev]:
1003 for p in parentscache[rev]:
1004 yield p
1004 yield p
1005
1005
1006 def annotate(context):
1006 def annotate(context):
1007 if fctx.isbinary():
1007 if fctx.isbinary():
1008 mt = pycompat.sysbytes(
1008 mt = pycompat.sysbytes(
1009 mimetypes.guess_type(pycompat.fsdecode(fctx.path()))[0]
1009 mimetypes.guess_type(pycompat.fsdecode(fctx.path()))[0]
1010 or r'application/octet-stream'
1010 or r'application/octet-stream'
1011 )
1011 )
1012 lines = [
1012 lines = [
1013 dagop.annotateline(
1013 dagop.annotateline(
1014 fctx=fctx.filectx(fctx.filerev()),
1014 fctx=fctx.filectx(fctx.filerev()),
1015 lineno=1,
1015 lineno=1,
1016 text=b'(binary:%s)' % mt,
1016 text=b'(binary:%s)' % mt,
1017 )
1017 )
1018 ]
1018 ]
1019 else:
1019 else:
1020 lines = webutil.annotate(web.req, fctx, web.repo.ui)
1020 lines = webutil.annotate(web.req, fctx, web.repo.ui)
1021
1021
1022 previousrev = None
1022 previousrev = None
1023 blockparitygen = paritygen(1)
1023 blockparitygen = paritygen(1)
1024 for lineno, aline in enumerate(lines):
1024 for lineno, aline in enumerate(lines):
1025 f = aline.fctx
1025 f = aline.fctx
1026 rev = f.rev()
1026 rev = f.rev()
1027 if rev != previousrev:
1027 if rev != previousrev:
1028 blockhead = True
1028 blockhead = True
1029 blockparity = next(blockparitygen)
1029 blockparity = next(blockparitygen)
1030 else:
1030 else:
1031 blockhead = None
1031 blockhead = None
1032 previousrev = rev
1032 previousrev = rev
1033 yield {
1033 yield {
1034 b"parity": next(parity),
1034 b"parity": next(parity),
1035 b"node": f.hex(),
1035 b"node": f.hex(),
1036 b"rev": rev,
1036 b"rev": rev,
1037 b"author": f.user(),
1037 b"author": f.user(),
1038 b"parents": templateutil.mappinggenerator(parents, args=(f,)),
1038 b"parents": templateutil.mappinggenerator(parents, args=(f,)),
1039 b"desc": f.description(),
1039 b"desc": f.description(),
1040 b"extra": f.extra(),
1040 b"extra": f.extra(),
1041 b"file": f.path(),
1041 b"file": f.path(),
1042 b"blockhead": blockhead,
1042 b"blockhead": blockhead,
1043 b"blockparity": blockparity,
1043 b"blockparity": blockparity,
1044 b"targetline": aline.lineno,
1044 b"targetline": aline.lineno,
1045 b"line": aline.text,
1045 b"line": aline.text,
1046 b"lineno": lineno + 1,
1046 b"lineno": lineno + 1,
1047 b"lineid": b"l%d" % (lineno + 1),
1047 b"lineid": b"l%d" % (lineno + 1),
1048 b"linenumber": b"% 6d" % (lineno + 1),
1048 b"linenumber": b"% 6d" % (lineno + 1),
1049 b"revdate": f.date(),
1049 b"revdate": f.date(),
1050 }
1050 }
1051
1051
1052 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, b'annotate')
1052 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, b'annotate')
1053 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
1053 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
1054
1054
1055 return web.sendtemplate(
1055 return web.sendtemplate(
1056 b'fileannotate',
1056 b'fileannotate',
1057 file=f,
1057 file=f,
1058 annotate=templateutil.mappinggenerator(annotate),
1058 annotate=templateutil.mappinggenerator(annotate),
1059 path=webutil.up(f),
1059 path=webutil.up(f),
1060 symrev=webutil.symrevorshortnode(web.req, fctx),
1060 symrev=webutil.symrevorshortnode(web.req, fctx),
1061 rename=webutil.renamelink(fctx),
1061 rename=webutil.renamelink(fctx),
1062 permissions=fctx.manifest().flags(f),
1062 permissions=fctx.manifest().flags(f),
1063 ishead=int(ishead),
1063 ishead=int(ishead),
1064 diffopts=templateutil.hybriddict(diffopts),
1064 diffopts=templateutil.hybriddict(diffopts),
1065 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
1065 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
1066 )
1066 )
1067
1067
1068
1068
1069 @webcommand(b'filelog')
1069 @webcommand(b'filelog')
1070 def filelog(web):
1070 def filelog(web):
1071 """
1071 """
1072 /filelog/{revision}/{path}
1072 /filelog/{revision}/{path}
1073 --------------------------
1073 --------------------------
1074
1074
1075 Show information about the history of a file in the repository.
1075 Show information about the history of a file in the repository.
1076
1076
1077 The ``revcount`` query string argument can be defined to control the
1077 The ``revcount`` query string argument can be defined to control the
1078 maximum number of entries to show.
1078 maximum number of entries to show.
1079
1079
1080 The ``filelog`` template will be rendered.
1080 The ``filelog`` template will be rendered.
1081 """
1081 """
1082
1082
1083 try:
1083 try:
1084 fctx = webutil.filectx(web.repo, web.req)
1084 fctx = webutil.filectx(web.repo, web.req)
1085 f = fctx.path()
1085 f = fctx.path()
1086 fl = fctx.filelog()
1086 fl = fctx.filelog()
1087 except error.LookupError:
1087 except error.LookupError:
1088 f = webutil.cleanpath(web.repo, web.req.qsparams[b'file'])
1088 f = webutil.cleanpath(web.repo, web.req.qsparams[b'file'])
1089 fl = web.repo.file(f)
1089 fl = web.repo.file(f)
1090 numrevs = len(fl)
1090 numrevs = len(fl)
1091 if not numrevs: # file doesn't exist at all
1091 if not numrevs: # file doesn't exist at all
1092 raise
1092 raise
1093 rev = webutil.changectx(web.repo, web.req).rev()
1093 rev = webutil.changectx(web.repo, web.req).rev()
1094 first = fl.linkrev(0)
1094 first = fl.linkrev(0)
1095 if rev < first: # current rev is from before file existed
1095 if rev < first: # current rev is from before file existed
1096 raise
1096 raise
1097 frev = numrevs - 1
1097 frev = numrevs - 1
1098 while fl.linkrev(frev) > rev:
1098 while fl.linkrev(frev) > rev:
1099 frev -= 1
1099 frev -= 1
1100 fctx = web.repo.filectx(f, fl.linkrev(frev))
1100 fctx = web.repo.filectx(f, fl.linkrev(frev))
1101
1101
1102 revcount = web.maxshortchanges
1102 revcount = web.maxshortchanges
1103 if b'revcount' in web.req.qsparams:
1103 if b'revcount' in web.req.qsparams:
1104 try:
1104 try:
1105 revcount = int(web.req.qsparams.get(b'revcount', revcount))
1105 revcount = int(web.req.qsparams.get(b'revcount', revcount))
1106 revcount = max(revcount, 1)
1106 revcount = max(revcount, 1)
1107 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
1107 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
1108 except ValueError:
1108 except ValueError:
1109 pass
1109 pass
1110
1110
1111 lrange = webutil.linerange(web.req)
1111 lrange = webutil.linerange(web.req)
1112
1112
1113 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1113 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1114 lessvars[b'revcount'] = max(revcount // 2, 1)
1114 lessvars[b'revcount'] = max(revcount // 2, 1)
1115 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1115 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1116 morevars[b'revcount'] = revcount * 2
1116 morevars[b'revcount'] = revcount * 2
1117
1117
1118 patch = b'patch' in web.req.qsparams
1118 patch = b'patch' in web.req.qsparams
1119 if patch:
1119 if patch:
1120 lessvars[b'patch'] = morevars[b'patch'] = web.req.qsparams[b'patch']
1120 lessvars[b'patch'] = morevars[b'patch'] = web.req.qsparams[b'patch']
1121 descend = b'descend' in web.req.qsparams
1121 descend = b'descend' in web.req.qsparams
1122 if descend:
1122 if descend:
1123 lessvars[b'descend'] = morevars[b'descend'] = web.req.qsparams[
1123 lessvars[b'descend'] = morevars[b'descend'] = web.req.qsparams[
1124 b'descend'
1124 b'descend'
1125 ]
1125 ]
1126
1126
1127 count = fctx.filerev() + 1
1127 count = fctx.filerev() + 1
1128 start = max(0, count - revcount) # first rev on this page
1128 start = max(0, count - revcount) # first rev on this page
1129 end = min(count, start + revcount) # last rev on this page
1129 end = min(count, start + revcount) # last rev on this page
1130 parity = paritygen(web.stripecount, offset=start - end)
1130 parity = paritygen(web.stripecount, offset=start - end)
1131
1131
1132 repo = web.repo
1132 repo = web.repo
1133 filelog = fctx.filelog()
1133 filelog = fctx.filelog()
1134 revs = [
1134 revs = [
1135 filerev
1135 filerev
1136 for filerev in filelog.revs(start, end - 1)
1136 for filerev in filelog.revs(start, end - 1)
1137 if filelog.linkrev(filerev) in repo
1137 if filelog.linkrev(filerev) in repo
1138 ]
1138 ]
1139 entries = []
1139 entries = []
1140
1140
1141 diffstyle = web.config(b'web', b'style')
1141 diffstyle = web.config(b'web', b'style')
1142 if b'style' in web.req.qsparams:
1142 if b'style' in web.req.qsparams:
1143 diffstyle = web.req.qsparams[b'style']
1143 diffstyle = web.req.qsparams[b'style']
1144
1144
1145 def diff(fctx, linerange=None):
1145 def diff(fctx, linerange=None):
1146 ctx = fctx.changectx()
1146 ctx = fctx.changectx()
1147 basectx = ctx.p1()
1147 basectx = ctx.p1()
1148 path = fctx.path()
1148 path = fctx.path()
1149 return webutil.diffs(
1149 return webutil.diffs(
1150 web,
1150 web,
1151 ctx,
1151 ctx,
1152 basectx,
1152 basectx,
1153 [path],
1153 [path],
1154 diffstyle,
1154 diffstyle,
1155 linerange=linerange,
1155 linerange=linerange,
1156 lineidprefix=b'%s-' % ctx.hex()[:12],
1156 lineidprefix=b'%s-' % ctx.hex()[:12],
1157 )
1157 )
1158
1158
1159 linerange = None
1159 linerange = None
1160 if lrange is not None:
1160 if lrange is not None:
1161 assert lrange is not None # help pytype (!?)
1161 assert lrange is not None # help pytype (!?)
1162 linerange = webutil.formatlinerange(*lrange)
1162 linerange = webutil.formatlinerange(*lrange)
1163 # deactivate numeric nav links when linerange is specified as this
1163 # deactivate numeric nav links when linerange is specified as this
1164 # would required a dedicated "revnav" class
1164 # would required a dedicated "revnav" class
1165 nav = templateutil.mappinglist([])
1165 nav = templateutil.mappinglist([])
1166 if descend:
1166 if descend:
1167 it = dagop.blockdescendants(fctx, *lrange)
1167 it = dagop.blockdescendants(fctx, *lrange)
1168 else:
1168 else:
1169 it = dagop.blockancestors(fctx, *lrange)
1169 it = dagop.blockancestors(fctx, *lrange)
1170 for i, (c, lr) in enumerate(it, 1):
1170 for i, (c, lr) in enumerate(it, 1):
1171 diffs = None
1171 diffs = None
1172 if patch:
1172 if patch:
1173 diffs = diff(c, linerange=lr)
1173 diffs = diff(c, linerange=lr)
1174 # follow renames accross filtered (not in range) revisions
1174 # follow renames accross filtered (not in range) revisions
1175 path = c.path()
1175 path = c.path()
1176 lm = webutil.commonentry(repo, c)
1176 lm = webutil.commonentry(repo, c)
1177 lm.update(
1177 lm.update(
1178 {
1178 {
1179 b'parity': next(parity),
1179 b'parity': next(parity),
1180 b'filerev': c.rev(),
1180 b'filerev': c.rev(),
1181 b'file': path,
1181 b'file': path,
1182 b'diff': diffs,
1182 b'diff': diffs,
1183 b'linerange': webutil.formatlinerange(*lr),
1183 b'linerange': webutil.formatlinerange(*lr),
1184 b'rename': templateutil.mappinglist([]),
1184 b'rename': templateutil.mappinglist([]),
1185 }
1185 }
1186 )
1186 )
1187 entries.append(lm)
1187 entries.append(lm)
1188 if i == revcount:
1188 if i == revcount:
1189 break
1189 break
1190 lessvars[b'linerange'] = webutil.formatlinerange(*lrange)
1190 lessvars[b'linerange'] = webutil.formatlinerange(*lrange)
1191 morevars[b'linerange'] = lessvars[b'linerange']
1191 morevars[b'linerange'] = lessvars[b'linerange']
1192 else:
1192 else:
1193 for i in revs:
1193 for i in revs:
1194 iterfctx = fctx.filectx(i)
1194 iterfctx = fctx.filectx(i)
1195 diffs = None
1195 diffs = None
1196 if patch:
1196 if patch:
1197 diffs = diff(iterfctx)
1197 diffs = diff(iterfctx)
1198 lm = webutil.commonentry(repo, iterfctx)
1198 lm = webutil.commonentry(repo, iterfctx)
1199 lm.update(
1199 lm.update(
1200 {
1200 {
1201 b'parity': next(parity),
1201 b'parity': next(parity),
1202 b'filerev': i,
1202 b'filerev': i,
1203 b'file': f,
1203 b'file': f,
1204 b'diff': diffs,
1204 b'diff': diffs,
1205 b'rename': webutil.renamelink(iterfctx),
1205 b'rename': webutil.renamelink(iterfctx),
1206 }
1206 }
1207 )
1207 )
1208 entries.append(lm)
1208 entries.append(lm)
1209 entries.reverse()
1209 entries.reverse()
1210 revnav = webutil.filerevnav(web.repo, fctx.path())
1210 revnav = webutil.filerevnav(web.repo, fctx.path())
1211 nav = revnav.gen(end - 1, revcount, count)
1211 nav = revnav.gen(end - 1, revcount, count)
1212
1212
1213 latestentry = entries[:1]
1213 latestentry = entries[:1]
1214
1214
1215 return web.sendtemplate(
1215 return web.sendtemplate(
1216 b'filelog',
1216 b'filelog',
1217 file=f,
1217 file=f,
1218 nav=nav,
1218 nav=nav,
1219 symrev=webutil.symrevorshortnode(web.req, fctx),
1219 symrev=webutil.symrevorshortnode(web.req, fctx),
1220 entries=templateutil.mappinglist(entries),
1220 entries=templateutil.mappinglist(entries),
1221 descend=descend,
1221 descend=descend,
1222 patch=patch,
1222 patch=patch,
1223 latestentry=templateutil.mappinglist(latestentry),
1223 latestentry=templateutil.mappinglist(latestentry),
1224 linerange=linerange,
1224 linerange=linerange,
1225 revcount=revcount,
1225 revcount=revcount,
1226 morevars=morevars,
1226 morevars=morevars,
1227 lessvars=lessvars,
1227 lessvars=lessvars,
1228 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
1228 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))
1229 )
1229 )
1230
1230
1231
1231
1232 @webcommand(b'archive')
1232 @webcommand(b'archive')
1233 def archive(web):
1233 def archive(web):
1234 """
1234 """
1235 /archive/{revision}.{format}[/{path}]
1235 /archive/{revision}.{format}[/{path}]
1236 -------------------------------------
1236 -------------------------------------
1237
1237
1238 Obtain an archive of repository content.
1238 Obtain an archive of repository content.
1239
1239
1240 The content and type of the archive is defined by a URL path parameter.
1240 The content and type of the archive is defined by a URL path parameter.
1241 ``format`` is the file extension of the archive type to be generated. e.g.
1241 ``format`` is the file extension of the archive type to be generated. e.g.
1242 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1242 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1243 server configuration.
1243 server configuration.
1244
1244
1245 The optional ``path`` URL parameter controls content to include in the
1245 The optional ``path`` URL parameter controls content to include in the
1246 archive. If omitted, every file in the specified revision is present in the
1246 archive. If omitted, every file in the specified revision is present in the
1247 archive. If included, only the specified file or contents of the specified
1247 archive. If included, only the specified file or contents of the specified
1248 directory will be included in the archive.
1248 directory will be included in the archive.
1249
1249
1250 No template is used for this handler. Raw, binary content is generated.
1250 No template is used for this handler. Raw, binary content is generated.
1251 """
1251 """
1252
1252
1253 type_ = web.req.qsparams.get(b'type')
1253 type_ = web.req.qsparams.get(b'type')
1254 allowed = web.configlist(b"web", b"allow-archive")
1254 allowed = web.configlist(b"web", b"allow-archive")
1255 key = web.req.qsparams[b'node']
1255 key = web.req.qsparams[b'node']
1256
1256
1257 if type_ not in webutil.archivespecs:
1257 if type_ not in webutil.archivespecs:
1258 msg = b'Unsupported archive type: %s' % stringutil.pprint(type_)
1258 msg = b'Unsupported archive type: %s' % stringutil.pprint(type_)
1259 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1259 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1260
1260
1261 if not ((type_ in allowed or web.configbool(b"web", b"allow" + type_))):
1261 if not ((type_ in allowed or web.configbool(b"web", b"allow" + type_))):
1262 msg = b'Archive type not allowed: %s' % type_
1262 msg = b'Archive type not allowed: %s' % type_
1263 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1263 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1264
1264
1265 reponame = re.sub(br"\W+", b"-", os.path.basename(web.reponame))
1265 reponame = re.sub(br"\W+", b"-", os.path.basename(web.reponame))
1266 cnode = web.repo.lookup(key)
1266 cnode = web.repo.lookup(key)
1267 arch_version = key
1267 arch_version = key
1268 if cnode == key or key == b'tip':
1268 if cnode == key or key == b'tip':
1269 arch_version = short(cnode)
1269 arch_version = short(cnode)
1270 name = b"%s-%s" % (reponame, arch_version)
1270 name = b"%s-%s" % (reponame, arch_version)
1271
1271
1272 ctx = webutil.changectx(web.repo, web.req)
1272 ctx = webutil.changectx(web.repo, web.req)
1273 match = scmutil.match(ctx, [])
1273 match = scmutil.match(ctx, [])
1274 file = web.req.qsparams.get(b'file')
1274 file = web.req.qsparams.get(b'file')
1275 if file:
1275 if file:
1276 pats = [b'path:' + file]
1276 pats = [b'path:' + file]
1277 match = scmutil.match(ctx, pats, default=b'path')
1277 match = scmutil.match(ctx, pats, default=b'path')
1278 if pats:
1278 if pats:
1279 files = [f for f in ctx.manifest().keys() if match(f)]
1279 files = [f for f in ctx.manifest().keys() if match(f)]
1280 if not files:
1280 if not files:
1281 raise ErrorResponse(
1281 raise ErrorResponse(
1282 HTTP_NOT_FOUND, b'file(s) not found: %s' % file
1282 HTTP_NOT_FOUND, b'file(s) not found: %s' % file
1283 )
1283 )
1284
1284
1285 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1285 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1286
1286
1287 web.res.headers[b'Content-Type'] = mimetype
1287 web.res.headers[b'Content-Type'] = mimetype
1288 web.res.headers[b'Content-Disposition'] = b'attachment; filename=%s%s' % (
1288 web.res.headers[b'Content-Disposition'] = b'attachment; filename=%s%s' % (
1289 name,
1289 name,
1290 extension,
1290 extension,
1291 )
1291 )
1292
1292
1293 if encoding:
1293 if encoding:
1294 web.res.headers[b'Content-Encoding'] = encoding
1294 web.res.headers[b'Content-Encoding'] = encoding
1295
1295
1296 web.res.setbodywillwrite()
1296 web.res.setbodywillwrite()
1297 if list(web.res.sendresponse()):
1297 if list(web.res.sendresponse()):
1298 raise error.ProgrammingError(
1298 raise error.ProgrammingError(
1299 b'sendresponse() should not emit data if writing later'
1299 b'sendresponse() should not emit data if writing later'
1300 )
1300 )
1301
1301
1302 if web.req.method == b'HEAD':
1303 return []
1304
1302 bodyfh = web.res.getbodyfile()
1305 bodyfh = web.res.getbodyfile()
1303
1306
1304 archival.archive(
1307 archival.archive(
1305 web.repo,
1308 web.repo,
1306 bodyfh,
1309 bodyfh,
1307 cnode,
1310 cnode,
1308 artype,
1311 artype,
1309 prefix=name,
1312 prefix=name,
1310 match=match,
1313 match=match,
1311 subrepos=web.configbool(b"web", b"archivesubrepos"),
1314 subrepos=web.configbool(b"web", b"archivesubrepos"),
1312 )
1315 )
1313
1316
1314 return []
1317 return []
1315
1318
1316
1319
1317 @webcommand(b'static')
1320 @webcommand(b'static')
1318 def static(web):
1321 def static(web):
1319 fname = web.req.qsparams[b'file']
1322 fname = web.req.qsparams[b'file']
1320 # a repo owner may set web.static in .hg/hgrc to get any file
1323 # a repo owner may set web.static in .hg/hgrc to get any file
1321 # readable by the user running the CGI script
1324 # readable by the user running the CGI script
1322 static = web.config(b"web", b"static", untrusted=False)
1325 static = web.config(b"web", b"static", untrusted=False)
1323 staticfile(web.templatepath, static, fname, web.res)
1326 staticfile(web.templatepath, static, fname, web.res)
1324 return web.res.sendresponse()
1327 return web.res.sendresponse()
1325
1328
1326
1329
1327 @webcommand(b'graph')
1330 @webcommand(b'graph')
1328 def graph(web):
1331 def graph(web):
1329 """
1332 """
1330 /graph[/{revision}]
1333 /graph[/{revision}]
1331 -------------------
1334 -------------------
1332
1335
1333 Show information about the graphical topology of the repository.
1336 Show information about the graphical topology of the repository.
1334
1337
1335 Information rendered by this handler can be used to create visual
1338 Information rendered by this handler can be used to create visual
1336 representations of repository topology.
1339 representations of repository topology.
1337
1340
1338 The ``revision`` URL parameter controls the starting changeset. If it's
1341 The ``revision`` URL parameter controls the starting changeset. If it's
1339 absent, the default is ``tip``.
1342 absent, the default is ``tip``.
1340
1343
1341 The ``revcount`` query string argument can define the number of changesets
1344 The ``revcount`` query string argument can define the number of changesets
1342 to show information for.
1345 to show information for.
1343
1346
1344 The ``graphtop`` query string argument can specify the starting changeset
1347 The ``graphtop`` query string argument can specify the starting changeset
1345 for producing ``jsdata`` variable that is used for rendering graph in
1348 for producing ``jsdata`` variable that is used for rendering graph in
1346 JavaScript. By default it has the same value as ``revision``.
1349 JavaScript. By default it has the same value as ``revision``.
1347
1350
1348 This handler will render the ``graph`` template.
1351 This handler will render the ``graph`` template.
1349 """
1352 """
1350
1353
1351 if b'node' in web.req.qsparams:
1354 if b'node' in web.req.qsparams:
1352 ctx = webutil.changectx(web.repo, web.req)
1355 ctx = webutil.changectx(web.repo, web.req)
1353 symrev = webutil.symrevorshortnode(web.req, ctx)
1356 symrev = webutil.symrevorshortnode(web.req, ctx)
1354 else:
1357 else:
1355 ctx = web.repo[b'tip']
1358 ctx = web.repo[b'tip']
1356 symrev = b'tip'
1359 symrev = b'tip'
1357 rev = ctx.rev()
1360 rev = ctx.rev()
1358
1361
1359 bg_height = 39
1362 bg_height = 39
1360 revcount = web.maxshortchanges
1363 revcount = web.maxshortchanges
1361 if b'revcount' in web.req.qsparams:
1364 if b'revcount' in web.req.qsparams:
1362 try:
1365 try:
1363 revcount = int(web.req.qsparams.get(b'revcount', revcount))
1366 revcount = int(web.req.qsparams.get(b'revcount', revcount))
1364 revcount = max(revcount, 1)
1367 revcount = max(revcount, 1)
1365 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
1368 web.tmpl.defaults[b'sessionvars'][b'revcount'] = revcount
1366 except ValueError:
1369 except ValueError:
1367 pass
1370 pass
1368
1371
1369 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1372 lessvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1370 lessvars[b'revcount'] = max(revcount // 2, 1)
1373 lessvars[b'revcount'] = max(revcount // 2, 1)
1371 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1374 morevars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1372 morevars[b'revcount'] = revcount * 2
1375 morevars[b'revcount'] = revcount * 2
1373
1376
1374 graphtop = web.req.qsparams.get(b'graphtop', ctx.hex())
1377 graphtop = web.req.qsparams.get(b'graphtop', ctx.hex())
1375 graphvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1378 graphvars = copy.copy(web.tmpl.defaults[b'sessionvars'])
1376 graphvars[b'graphtop'] = graphtop
1379 graphvars[b'graphtop'] = graphtop
1377
1380
1378 count = len(web.repo)
1381 count = len(web.repo)
1379 pos = rev
1382 pos = rev
1380
1383
1381 uprev = min(max(0, count - 1), rev + revcount)
1384 uprev = min(max(0, count - 1), rev + revcount)
1382 downrev = max(0, rev - revcount)
1385 downrev = max(0, rev - revcount)
1383 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1386 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1384
1387
1385 tree = []
1388 tree = []
1386 nextentry = []
1389 nextentry = []
1387 lastrev = 0
1390 lastrev = 0
1388 if pos != -1:
1391 if pos != -1:
1389 allrevs = web.repo.changelog.revs(pos, 0)
1392 allrevs = web.repo.changelog.revs(pos, 0)
1390 revs = []
1393 revs = []
1391 for i in allrevs:
1394 for i in allrevs:
1392 revs.append(i)
1395 revs.append(i)
1393 if len(revs) >= revcount + 1:
1396 if len(revs) >= revcount + 1:
1394 break
1397 break
1395
1398
1396 if len(revs) > revcount:
1399 if len(revs) > revcount:
1397 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1400 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1398 revs = revs[:-1]
1401 revs = revs[:-1]
1399
1402
1400 lastrev = revs[-1]
1403 lastrev = revs[-1]
1401
1404
1402 # We have to feed a baseset to dagwalker as it is expecting smartset
1405 # We have to feed a baseset to dagwalker as it is expecting smartset
1403 # object. This does not have a big impact on hgweb performance itself
1406 # object. This does not have a big impact on hgweb performance itself
1404 # since hgweb graphing code is not itself lazy yet.
1407 # since hgweb graphing code is not itself lazy yet.
1405 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1408 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1406 # As we said one line above... not lazy.
1409 # As we said one line above... not lazy.
1407 tree = list(
1410 tree = list(
1408 item
1411 item
1409 for item in graphmod.colored(dag, web.repo)
1412 for item in graphmod.colored(dag, web.repo)
1410 if item[1] == graphmod.CHANGESET
1413 if item[1] == graphmod.CHANGESET
1411 )
1414 )
1412
1415
1413 def fulltree():
1416 def fulltree():
1414 pos = web.repo[graphtop].rev()
1417 pos = web.repo[graphtop].rev()
1415 tree = []
1418 tree = []
1416 if pos != -1:
1419 if pos != -1:
1417 revs = web.repo.changelog.revs(pos, lastrev)
1420 revs = web.repo.changelog.revs(pos, lastrev)
1418 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1421 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1419 tree = list(
1422 tree = list(
1420 item
1423 item
1421 for item in graphmod.colored(dag, web.repo)
1424 for item in graphmod.colored(dag, web.repo)
1422 if item[1] == graphmod.CHANGESET
1425 if item[1] == graphmod.CHANGESET
1423 )
1426 )
1424 return tree
1427 return tree
1425
1428
1426 def jsdata(context):
1429 def jsdata(context):
1427 for (id, type, ctx, vtx, edges) in fulltree():
1430 for (id, type, ctx, vtx, edges) in fulltree():
1428 yield {
1431 yield {
1429 b'node': pycompat.bytestr(ctx),
1432 b'node': pycompat.bytestr(ctx),
1430 b'graphnode': webutil.getgraphnode(web.repo, ctx),
1433 b'graphnode': webutil.getgraphnode(web.repo, ctx),
1431 b'vertex': vtx,
1434 b'vertex': vtx,
1432 b'edges': edges,
1435 b'edges': edges,
1433 }
1436 }
1434
1437
1435 def nodes(context):
1438 def nodes(context):
1436 parity = paritygen(web.stripecount)
1439 parity = paritygen(web.stripecount)
1437 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1440 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1438 entry = webutil.commonentry(web.repo, ctx)
1441 entry = webutil.commonentry(web.repo, ctx)
1439 edgedata = [
1442 edgedata = [
1440 {
1443 {
1441 b'col': edge[0],
1444 b'col': edge[0],
1442 b'nextcol': edge[1],
1445 b'nextcol': edge[1],
1443 b'color': (edge[2] - 1) % 6 + 1,
1446 b'color': (edge[2] - 1) % 6 + 1,
1444 b'width': edge[3],
1447 b'width': edge[3],
1445 b'bcolor': edge[4],
1448 b'bcolor': edge[4],
1446 }
1449 }
1447 for edge in edges
1450 for edge in edges
1448 ]
1451 ]
1449
1452
1450 entry.update(
1453 entry.update(
1451 {
1454 {
1452 b'col': vtx[0],
1455 b'col': vtx[0],
1453 b'color': (vtx[1] - 1) % 6 + 1,
1456 b'color': (vtx[1] - 1) % 6 + 1,
1454 b'parity': next(parity),
1457 b'parity': next(parity),
1455 b'edges': templateutil.mappinglist(edgedata),
1458 b'edges': templateutil.mappinglist(edgedata),
1456 b'row': row,
1459 b'row': row,
1457 b'nextrow': row + 1,
1460 b'nextrow': row + 1,
1458 }
1461 }
1459 )
1462 )
1460
1463
1461 yield entry
1464 yield entry
1462
1465
1463 rows = len(tree)
1466 rows = len(tree)
1464
1467
1465 return web.sendtemplate(
1468 return web.sendtemplate(
1466 b'graph',
1469 b'graph',
1467 rev=rev,
1470 rev=rev,
1468 symrev=symrev,
1471 symrev=symrev,
1469 revcount=revcount,
1472 revcount=revcount,
1470 uprev=uprev,
1473 uprev=uprev,
1471 lessvars=lessvars,
1474 lessvars=lessvars,
1472 morevars=morevars,
1475 morevars=morevars,
1473 downrev=downrev,
1476 downrev=downrev,
1474 graphvars=graphvars,
1477 graphvars=graphvars,
1475 rows=rows,
1478 rows=rows,
1476 bg_height=bg_height,
1479 bg_height=bg_height,
1477 changesets=count,
1480 changesets=count,
1478 nextentry=templateutil.mappinglist(nextentry),
1481 nextentry=templateutil.mappinglist(nextentry),
1479 jsdata=templateutil.mappinggenerator(jsdata),
1482 jsdata=templateutil.mappinggenerator(jsdata),
1480 nodes=templateutil.mappinggenerator(nodes),
1483 nodes=templateutil.mappinggenerator(nodes),
1481 node=ctx.hex(),
1484 node=ctx.hex(),
1482 archives=web.archivelist(b'tip'),
1485 archives=web.archivelist(b'tip'),
1483 changenav=changenav,
1486 changenav=changenav,
1484 )
1487 )
1485
1488
1486
1489
1487 def _getdoc(e):
1490 def _getdoc(e):
1488 doc = e[0].__doc__
1491 doc = e[0].__doc__
1489 if doc:
1492 if doc:
1490 doc = _(doc).partition(b'\n')[0]
1493 doc = _(doc).partition(b'\n')[0]
1491 else:
1494 else:
1492 doc = _(b'(no help text available)')
1495 doc = _(b'(no help text available)')
1493 return doc
1496 return doc
1494
1497
1495
1498
1496 @webcommand(b'help')
1499 @webcommand(b'help')
1497 def help(web):
1500 def help(web):
1498 """
1501 """
1499 /help[/{topic}]
1502 /help[/{topic}]
1500 ---------------
1503 ---------------
1501
1504
1502 Render help documentation.
1505 Render help documentation.
1503
1506
1504 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1507 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1505 is defined, that help topic will be rendered. If not, an index of
1508 is defined, that help topic will be rendered. If not, an index of
1506 available help topics will be rendered.
1509 available help topics will be rendered.
1507
1510
1508 The ``help`` template will be rendered when requesting help for a topic.
1511 The ``help`` template will be rendered when requesting help for a topic.
1509 ``helptopics`` will be rendered for the index of help topics.
1512 ``helptopics`` will be rendered for the index of help topics.
1510 """
1513 """
1511 from .. import commands, help as helpmod # avoid cycle
1514 from .. import commands, help as helpmod # avoid cycle
1512
1515
1513 topicname = web.req.qsparams.get(b'node')
1516 topicname = web.req.qsparams.get(b'node')
1514 if not topicname:
1517 if not topicname:
1515
1518
1516 def topics(context):
1519 def topics(context):
1517 for h in helpmod.helptable:
1520 for h in helpmod.helptable:
1518 entries, summary, _doc = h[0:3]
1521 entries, summary, _doc = h[0:3]
1519 yield {b'topic': entries[0], b'summary': summary}
1522 yield {b'topic': entries[0], b'summary': summary}
1520
1523
1521 early, other = [], []
1524 early, other = [], []
1522 primary = lambda s: s.partition(b'|')[0]
1525 primary = lambda s: s.partition(b'|')[0]
1523 for c, e in commands.table.items():
1526 for c, e in commands.table.items():
1524 doc = _getdoc(e)
1527 doc = _getdoc(e)
1525 if b'DEPRECATED' in doc or c.startswith(b'debug'):
1528 if b'DEPRECATED' in doc or c.startswith(b'debug'):
1526 continue
1529 continue
1527 cmd = primary(c)
1530 cmd = primary(c)
1528 if getattr(e[0], 'helpbasic', False):
1531 if getattr(e[0], 'helpbasic', False):
1529 early.append((cmd, doc))
1532 early.append((cmd, doc))
1530 else:
1533 else:
1531 other.append((cmd, doc))
1534 other.append((cmd, doc))
1532
1535
1533 early.sort()
1536 early.sort()
1534 other.sort()
1537 other.sort()
1535
1538
1536 def earlycommands(context):
1539 def earlycommands(context):
1537 for c, doc in early:
1540 for c, doc in early:
1538 yield {b'topic': c, b'summary': doc}
1541 yield {b'topic': c, b'summary': doc}
1539
1542
1540 def othercommands(context):
1543 def othercommands(context):
1541 for c, doc in other:
1544 for c, doc in other:
1542 yield {b'topic': c, b'summary': doc}
1545 yield {b'topic': c, b'summary': doc}
1543
1546
1544 return web.sendtemplate(
1547 return web.sendtemplate(
1545 b'helptopics',
1548 b'helptopics',
1546 topics=templateutil.mappinggenerator(topics),
1549 topics=templateutil.mappinggenerator(topics),
1547 earlycommands=templateutil.mappinggenerator(earlycommands),
1550 earlycommands=templateutil.mappinggenerator(earlycommands),
1548 othercommands=templateutil.mappinggenerator(othercommands),
1551 othercommands=templateutil.mappinggenerator(othercommands),
1549 title=b'Index',
1552 title=b'Index',
1550 )
1553 )
1551
1554
1552 # Render an index of sub-topics.
1555 # Render an index of sub-topics.
1553 if topicname in helpmod.subtopics:
1556 if topicname in helpmod.subtopics:
1554 topics = []
1557 topics = []
1555 for entries, summary, _doc in helpmod.subtopics[topicname]:
1558 for entries, summary, _doc in helpmod.subtopics[topicname]:
1556 topics.append(
1559 topics.append(
1557 {
1560 {
1558 b'topic': b'%s.%s' % (topicname, entries[0]),
1561 b'topic': b'%s.%s' % (topicname, entries[0]),
1559 b'basename': entries[0],
1562 b'basename': entries[0],
1560 b'summary': summary,
1563 b'summary': summary,
1561 }
1564 }
1562 )
1565 )
1563
1566
1564 return web.sendtemplate(
1567 return web.sendtemplate(
1565 b'helptopics',
1568 b'helptopics',
1566 topics=templateutil.mappinglist(topics),
1569 topics=templateutil.mappinglist(topics),
1567 title=topicname,
1570 title=topicname,
1568 subindex=True,
1571 subindex=True,
1569 )
1572 )
1570
1573
1571 u = webutil.wsgiui.load()
1574 u = webutil.wsgiui.load()
1572 u.verbose = True
1575 u.verbose = True
1573
1576
1574 # Render a page from a sub-topic.
1577 # Render a page from a sub-topic.
1575 if b'.' in topicname:
1578 if b'.' in topicname:
1576 # TODO implement support for rendering sections, like
1579 # TODO implement support for rendering sections, like
1577 # `hg help` works.
1580 # `hg help` works.
1578 topic, subtopic = topicname.split(b'.', 1)
1581 topic, subtopic = topicname.split(b'.', 1)
1579 if topic not in helpmod.subtopics:
1582 if topic not in helpmod.subtopics:
1580 raise ErrorResponse(HTTP_NOT_FOUND)
1583 raise ErrorResponse(HTTP_NOT_FOUND)
1581 else:
1584 else:
1582 topic = topicname
1585 topic = topicname
1583 subtopic = None
1586 subtopic = None
1584
1587
1585 try:
1588 try:
1586 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1589 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1587 except error.Abort:
1590 except error.Abort:
1588 raise ErrorResponse(HTTP_NOT_FOUND)
1591 raise ErrorResponse(HTTP_NOT_FOUND)
1589
1592
1590 return web.sendtemplate(b'help', topic=topicname, doc=doc)
1593 return web.sendtemplate(b'help', topic=topicname, doc=doc)
1591
1594
1592
1595
1593 # tell hggettext to extract docstrings from these functions:
1596 # tell hggettext to extract docstrings from these functions:
1594 i18nfunctions = commands.values()
1597 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now