##// END OF EJS Templates
hgweb: profile HTTP requests...
Gregory Szorc -
r29787:80df0426 default
parent child Browse files
Show More
@@ -1,468 +1,470
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import contextlib
12 12 import os
13 13
14 14 from .common import (
15 15 ErrorResponse,
16 16 HTTP_BAD_REQUEST,
17 17 HTTP_NOT_FOUND,
18 18 HTTP_NOT_MODIFIED,
19 19 HTTP_OK,
20 20 HTTP_SERVER_ERROR,
21 21 caching,
22 22 permhooks,
23 23 )
24 24 from .request import wsgirequest
25 25
26 26 from .. import (
27 27 encoding,
28 28 error,
29 29 hg,
30 30 hook,
31 profiling,
31 32 repoview,
32 33 templatefilters,
33 34 templater,
34 35 ui as uimod,
35 36 util,
36 37 )
37 38
38 39 from . import (
39 40 protocol,
40 41 webcommands,
41 42 webutil,
42 43 wsgicgi,
43 44 )
44 45
45 46 perms = {
46 47 'changegroup': 'pull',
47 48 'changegroupsubset': 'pull',
48 49 'getbundle': 'pull',
49 50 'stream_out': 'pull',
50 51 'listkeys': 'pull',
51 52 'unbundle': 'push',
52 53 'pushkey': 'push',
53 54 }
54 55
55 56 def makebreadcrumb(url, prefix=''):
56 57 '''Return a 'URL breadcrumb' list
57 58
58 59 A 'URL breadcrumb' is a list of URL-name pairs,
59 60 corresponding to each of the path items on a URL.
60 61 This can be used to create path navigation entries.
61 62 '''
62 63 if url.endswith('/'):
63 64 url = url[:-1]
64 65 if prefix:
65 66 url = '/' + prefix + url
66 67 relpath = url
67 68 if relpath.startswith('/'):
68 69 relpath = relpath[1:]
69 70
70 71 breadcrumb = []
71 72 urlel = url
72 73 pathitems = [''] + relpath.split('/')
73 74 for pathel in reversed(pathitems):
74 75 if not pathel or not urlel:
75 76 break
76 77 breadcrumb.append({'url': urlel, 'name': pathel})
77 78 urlel = os.path.dirname(urlel)
78 79 return reversed(breadcrumb)
79 80
80 81 class requestcontext(object):
81 82 """Holds state/context for an individual request.
82 83
83 84 Servers can be multi-threaded. Holding state on the WSGI application
84 85 is prone to race conditions. Instances of this class exist to hold
85 86 mutable and race-free state for requests.
86 87 """
87 88 def __init__(self, app, repo):
88 89 self.repo = repo
89 90 self.reponame = app.reponame
90 91
91 92 self.archives = ('zip', 'gz', 'bz2')
92 93
93 94 self.maxchanges = self.configint('web', 'maxchanges', 10)
94 95 self.stripecount = self.configint('web', 'stripes', 1)
95 96 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
96 97 self.maxfiles = self.configint('web', 'maxfiles', 10)
97 98 self.allowpull = self.configbool('web', 'allowpull', True)
98 99
99 100 # we use untrusted=False to prevent a repo owner from using
100 101 # web.templates in .hg/hgrc to get access to any file readable
101 102 # by the user running the CGI script
102 103 self.templatepath = self.config('web', 'templates', untrusted=False)
103 104
104 105 # This object is more expensive to build than simple config values.
105 106 # It is shared across requests. The app will replace the object
106 107 # if it is updated. Since this is a reference and nothing should
107 108 # modify the underlying object, it should be constant for the lifetime
108 109 # of the request.
109 110 self.websubtable = app.websubtable
110 111
111 112 # Trust the settings from the .hg/hgrc files by default.
112 113 def config(self, section, name, default=None, untrusted=True):
113 114 return self.repo.ui.config(section, name, default,
114 115 untrusted=untrusted)
115 116
116 117 def configbool(self, section, name, default=False, untrusted=True):
117 118 return self.repo.ui.configbool(section, name, default,
118 119 untrusted=untrusted)
119 120
120 121 def configint(self, section, name, default=None, untrusted=True):
121 122 return self.repo.ui.configint(section, name, default,
122 123 untrusted=untrusted)
123 124
124 125 def configlist(self, section, name, default=None, untrusted=True):
125 126 return self.repo.ui.configlist(section, name, default,
126 127 untrusted=untrusted)
127 128
128 129 archivespecs = {
129 130 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
130 131 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
131 132 'zip': ('application/zip', 'zip', '.zip', None),
132 133 }
133 134
134 135 def archivelist(self, nodeid):
135 136 allowed = self.configlist('web', 'allow_archive')
136 137 for typ, spec in self.archivespecs.iteritems():
137 138 if typ in allowed or self.configbool('web', 'allow%s' % typ):
138 139 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
139 140
140 141 def templater(self, req):
141 142 # determine scheme, port and server name
142 143 # this is needed to create absolute urls
143 144
144 145 proto = req.env.get('wsgi.url_scheme')
145 146 if proto == 'https':
146 147 proto = 'https'
147 148 default_port = '443'
148 149 else:
149 150 proto = 'http'
150 151 default_port = '80'
151 152
152 153 port = req.env['SERVER_PORT']
153 154 port = port != default_port and (':' + port) or ''
154 155 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
155 156 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
156 157 logoimg = self.config('web', 'logoimg', 'hglogo.png')
157 158 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
158 159 if not staticurl.endswith('/'):
159 160 staticurl += '/'
160 161
161 162 # some functions for the templater
162 163
163 164 def motd(**map):
164 165 yield self.config('web', 'motd', '')
165 166
166 167 # figure out which style to use
167 168
168 169 vars = {}
169 170 styles = (
170 171 req.form.get('style', [None])[0],
171 172 self.config('web', 'style'),
172 173 'paper',
173 174 )
174 175 style, mapfile = templater.stylemap(styles, self.templatepath)
175 176 if style == styles[0]:
176 177 vars['style'] = style
177 178
178 179 start = req.url[-1] == '?' and '&' or '?'
179 180 sessionvars = webutil.sessionvars(vars, start)
180 181
181 182 if not self.reponame:
182 183 self.reponame = (self.config('web', 'name')
183 184 or req.env.get('REPO_NAME')
184 185 or req.url.strip('/') or self.repo.root)
185 186
186 187 def websubfilter(text):
187 188 return templatefilters.websub(text, self.websubtable)
188 189
189 190 # create the templater
190 191
191 192 defaults = {
192 193 'url': req.url,
193 194 'logourl': logourl,
194 195 'logoimg': logoimg,
195 196 'staticurl': staticurl,
196 197 'urlbase': urlbase,
197 198 'repo': self.reponame,
198 199 'encoding': encoding.encoding,
199 200 'motd': motd,
200 201 'sessionvars': sessionvars,
201 202 'pathdef': makebreadcrumb(req.url),
202 203 'style': style,
203 204 }
204 205 tmpl = templater.templater.frommapfile(mapfile,
205 206 filters={'websub': websubfilter},
206 207 defaults=defaults)
207 208 return tmpl
208 209
209 210
210 211 class hgweb(object):
211 212 """HTTP server for individual repositories.
212 213
213 214 Instances of this class serve HTTP responses for a particular
214 215 repository.
215 216
216 217 Instances are typically used as WSGI applications.
217 218
218 219 Some servers are multi-threaded. On these servers, there may
219 220 be multiple active threads inside __call__.
220 221 """
221 222 def __init__(self, repo, name=None, baseui=None):
222 223 if isinstance(repo, str):
223 224 if baseui:
224 225 u = baseui.copy()
225 226 else:
226 227 u = uimod.ui()
227 228 r = hg.repository(u, repo)
228 229 else:
229 230 # we trust caller to give us a private copy
230 231 r = repo
231 232
232 233 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
233 234 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
234 235 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
235 236 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
236 237 # resolve file patterns relative to repo root
237 238 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
238 239 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
239 240 # displaying bundling progress bar while serving feel wrong and may
240 241 # break some wsgi implementation.
241 242 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
242 243 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
243 244 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
244 245 self._lastrepo = self._repos[0]
245 246 hook.redirect(True)
246 247 self.reponame = name
247 248
248 249 def _webifyrepo(self, repo):
249 250 repo = getwebview(repo)
250 251 self.websubtable = webutil.getwebsubs(repo)
251 252 return repo
252 253
253 254 @contextlib.contextmanager
254 255 def _obtainrepo(self):
255 256 """Obtain a repo unique to the caller.
256 257
257 258 Internally we maintain a stack of cachedlocalrepo instances
258 259 to be handed out. If one is available, we pop it and return it,
259 260 ensuring it is up to date in the process. If one is not available,
260 261 we clone the most recently used repo instance and return it.
261 262
262 263 It is currently possible for the stack to grow without bounds
263 264 if the server allows infinite threads. However, servers should
264 265 have a thread limit, thus establishing our limit.
265 266 """
266 267 if self._repos:
267 268 cached = self._repos.pop()
268 269 r, created = cached.fetch()
269 270 else:
270 271 cached = self._lastrepo.copy()
271 272 r, created = cached.fetch()
272 273 if created:
273 274 r = self._webifyrepo(r)
274 275
275 276 self._lastrepo = cached
276 277 self.mtime = cached.mtime
277 278 try:
278 279 yield r
279 280 finally:
280 281 self._repos.append(cached)
281 282
282 283 def run(self):
283 284 """Start a server from CGI environment.
284 285
285 286 Modern servers should be using WSGI and should avoid this
286 287 method, if possible.
287 288 """
288 289 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
289 290 raise RuntimeError("This function is only intended to be "
290 291 "called while running as a CGI script.")
291 292 wsgicgi.launch(self)
292 293
293 294 def __call__(self, env, respond):
294 295 """Run the WSGI application.
295 296
296 297 This may be called by multiple threads.
297 298 """
298 299 req = wsgirequest(env, respond)
299 300 return self.run_wsgi(req)
300 301
301 302 def run_wsgi(self, req):
302 303 """Internal method to run the WSGI application.
303 304
304 305 This is typically only called by Mercurial. External consumers
305 306 should be using instances of this class as the WSGI application.
306 307 """
307 308 with self._obtainrepo() as repo:
308 for r in self._runwsgi(req, repo):
309 yield r
309 with profiling.maybeprofile(repo.ui):
310 for r in self._runwsgi(req, repo):
311 yield r
310 312
311 313 def _runwsgi(self, req, repo):
312 314 rctx = requestcontext(self, repo)
313 315
314 316 # This state is global across all threads.
315 317 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
316 318 rctx.repo.ui.environ = req.env
317 319
318 320 # work with CGI variables to create coherent structure
319 321 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
320 322
321 323 req.url = req.env['SCRIPT_NAME']
322 324 if not req.url.endswith('/'):
323 325 req.url += '/'
324 326 if 'REPO_NAME' in req.env:
325 327 req.url += req.env['REPO_NAME'] + '/'
326 328
327 329 if 'PATH_INFO' in req.env:
328 330 parts = req.env['PATH_INFO'].strip('/').split('/')
329 331 repo_parts = req.env.get('REPO_NAME', '').split('/')
330 332 if parts[:len(repo_parts)] == repo_parts:
331 333 parts = parts[len(repo_parts):]
332 334 query = '/'.join(parts)
333 335 else:
334 336 query = req.env['QUERY_STRING'].partition('&')[0]
335 337 query = query.partition(';')[0]
336 338
337 339 # process this if it's a protocol request
338 340 # protocol bits don't need to create any URLs
339 341 # and the clients always use the old URL structure
340 342
341 343 cmd = req.form.get('cmd', [''])[0]
342 344 if protocol.iscmd(cmd):
343 345 try:
344 346 if query:
345 347 raise ErrorResponse(HTTP_NOT_FOUND)
346 348 if cmd in perms:
347 349 self.check_perm(rctx, req, perms[cmd])
348 350 return protocol.call(rctx.repo, req, cmd)
349 351 except ErrorResponse as inst:
350 352 # A client that sends unbundle without 100-continue will
351 353 # break if we respond early.
352 354 if (cmd == 'unbundle' and
353 355 (req.env.get('HTTP_EXPECT',
354 356 '').lower() != '100-continue') or
355 357 req.env.get('X-HgHttp2', '')):
356 358 req.drain()
357 359 else:
358 360 req.headers.append(('Connection', 'Close'))
359 361 req.respond(inst, protocol.HGTYPE,
360 362 body='0\n%s\n' % inst)
361 363 return ''
362 364
363 365 # translate user-visible url structure to internal structure
364 366
365 367 args = query.split('/', 2)
366 368 if 'cmd' not in req.form and args and args[0]:
367 369
368 370 cmd = args.pop(0)
369 371 style = cmd.rfind('-')
370 372 if style != -1:
371 373 req.form['style'] = [cmd[:style]]
372 374 cmd = cmd[style + 1:]
373 375
374 376 # avoid accepting e.g. style parameter as command
375 377 if util.safehasattr(webcommands, cmd):
376 378 req.form['cmd'] = [cmd]
377 379
378 380 if cmd == 'static':
379 381 req.form['file'] = ['/'.join(args)]
380 382 else:
381 383 if args and args[0]:
382 384 node = args.pop(0).replace('%2F', '/')
383 385 req.form['node'] = [node]
384 386 if args:
385 387 req.form['file'] = args
386 388
387 389 ua = req.env.get('HTTP_USER_AGENT', '')
388 390 if cmd == 'rev' and 'mercurial' in ua:
389 391 req.form['style'] = ['raw']
390 392
391 393 if cmd == 'archive':
392 394 fn = req.form['node'][0]
393 395 for type_, spec in rctx.archivespecs.iteritems():
394 396 ext = spec[2]
395 397 if fn.endswith(ext):
396 398 req.form['node'] = [fn[:-len(ext)]]
397 399 req.form['type'] = [type_]
398 400
399 401 # process the web interface request
400 402
401 403 try:
402 404 tmpl = rctx.templater(req)
403 405 ctype = tmpl('mimetype', encoding=encoding.encoding)
404 406 ctype = templater.stringify(ctype)
405 407
406 408 # check read permissions non-static content
407 409 if cmd != 'static':
408 410 self.check_perm(rctx, req, None)
409 411
410 412 if cmd == '':
411 413 req.form['cmd'] = [tmpl.cache['default']]
412 414 cmd = req.form['cmd'][0]
413 415
414 416 if rctx.configbool('web', 'cache', True):
415 417 caching(self, req) # sets ETag header or raises NOT_MODIFIED
416 418 if cmd not in webcommands.__all__:
417 419 msg = 'no such method: %s' % cmd
418 420 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
419 421 elif cmd == 'file' and 'raw' in req.form.get('style', []):
420 422 rctx.ctype = ctype
421 423 content = webcommands.rawfile(rctx, req, tmpl)
422 424 else:
423 425 content = getattr(webcommands, cmd)(rctx, req, tmpl)
424 426 req.respond(HTTP_OK, ctype)
425 427
426 428 return content
427 429
428 430 except (error.LookupError, error.RepoLookupError) as err:
429 431 req.respond(HTTP_NOT_FOUND, ctype)
430 432 msg = str(err)
431 433 if (util.safehasattr(err, 'name') and
432 434 not isinstance(err, error.ManifestLookupError)):
433 435 msg = 'revision not found: %s' % err.name
434 436 return tmpl('error', error=msg)
435 437 except (error.RepoError, error.RevlogError) as inst:
436 438 req.respond(HTTP_SERVER_ERROR, ctype)
437 439 return tmpl('error', error=str(inst))
438 440 except ErrorResponse as inst:
439 441 req.respond(inst, ctype)
440 442 if inst.code == HTTP_NOT_MODIFIED:
441 443 # Not allowed to return a body on a 304
442 444 return ['']
443 445 return tmpl('error', error=str(inst))
444 446
445 447 def check_perm(self, rctx, req, op):
446 448 for permhook in permhooks:
447 449 permhook(rctx, req, op)
448 450
449 451 def getwebview(repo):
450 452 """The 'web.view' config controls changeset filter to hgweb. Possible
451 453 values are ``served``, ``visible`` and ``all``. Default is ``served``.
452 454 The ``served`` filter only shows changesets that can be pulled from the
453 455 hgweb instance. The``visible`` filter includes secret changesets but
454 456 still excludes "hidden" one.
455 457
456 458 See the repoview module for details.
457 459
458 460 The option has been around undocumented since Mercurial 2.5, but no
459 461 user ever asked about it. So we better keep it undocumented for now."""
460 462 viewconfig = repo.ui.config('web', 'view', 'served',
461 463 untrusted=True)
462 464 if viewconfig == 'all':
463 465 return repo.unfiltered()
464 466 elif viewconfig in repoview.filtertable:
465 467 return repo.filtered(viewconfig)
466 468 else:
467 469 return repo.filtered('served')
468 470
@@ -1,518 +1,521
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import os
12 12 import re
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_NOT_FOUND,
20 20 HTTP_OK,
21 21 HTTP_SERVER_ERROR,
22 22 get_contact,
23 23 get_mtime,
24 24 ismember,
25 25 paritygen,
26 26 staticfile,
27 27 )
28 28 from .request import wsgirequest
29 29
30 30 from .. import (
31 31 encoding,
32 32 error,
33 33 hg,
34 profiling,
34 35 scmutil,
35 36 templater,
36 37 ui as uimod,
37 38 util,
38 39 )
39 40
40 41 from . import (
41 42 hgweb_mod,
42 43 webutil,
43 44 wsgicgi,
44 45 )
45 46
46 47 def cleannames(items):
47 48 return [(util.pconvert(name).strip('/'), path) for name, path in items]
48 49
49 50 def findrepos(paths):
50 51 repos = []
51 52 for prefix, root in cleannames(paths):
52 53 roothead, roottail = os.path.split(root)
53 54 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
54 55 # /bar/ be served as as foo/N .
55 56 # '*' will not search inside dirs with .hg (except .hg/patches),
56 57 # '**' will search inside dirs with .hg (and thus also find subrepos).
57 58 try:
58 59 recurse = {'*': False, '**': True}[roottail]
59 60 except KeyError:
60 61 repos.append((prefix, root))
61 62 continue
62 63 roothead = os.path.normpath(os.path.abspath(roothead))
63 64 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
64 65 repos.extend(urlrepos(prefix, roothead, paths))
65 66 return repos
66 67
67 68 def urlrepos(prefix, roothead, paths):
68 69 """yield url paths and filesystem paths from a list of repo paths
69 70
70 71 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
71 72 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
72 73 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
73 74 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
74 75 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
75 76 """
76 77 for path in paths:
77 78 path = os.path.normpath(path)
78 79 yield (prefix + '/' +
79 80 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
80 81
81 82 def geturlcgivars(baseurl, port):
82 83 """
83 84 Extract CGI variables from baseurl
84 85
85 86 >>> geturlcgivars("http://host.org/base", "80")
86 87 ('host.org', '80', '/base')
87 88 >>> geturlcgivars("http://host.org:8000/base", "80")
88 89 ('host.org', '8000', '/base')
89 90 >>> geturlcgivars('/base', 8000)
90 91 ('', '8000', '/base')
91 92 >>> geturlcgivars("base", '8000')
92 93 ('', '8000', '/base')
93 94 >>> geturlcgivars("http://host", '8000')
94 95 ('host', '8000', '/')
95 96 >>> geturlcgivars("http://host/", '8000')
96 97 ('host', '8000', '/')
97 98 """
98 99 u = util.url(baseurl)
99 100 name = u.host or ''
100 101 if u.port:
101 102 port = u.port
102 103 path = u.path or ""
103 104 if not path.startswith('/'):
104 105 path = '/' + path
105 106
106 107 return name, str(port), path
107 108
108 109 class hgwebdir(object):
109 110 """HTTP server for multiple repositories.
110 111
111 112 Given a configuration, different repositories will be served depending
112 113 on the request path.
113 114
114 115 Instances are typically used as WSGI applications.
115 116 """
116 117 def __init__(self, conf, baseui=None):
117 118 self.conf = conf
118 119 self.baseui = baseui
119 120 self.ui = None
120 121 self.lastrefresh = 0
121 122 self.motd = None
122 123 self.refresh()
123 124
124 125 def refresh(self):
125 126 refreshinterval = 20
126 127 if self.ui:
127 128 refreshinterval = self.ui.configint('web', 'refreshinterval',
128 129 refreshinterval)
129 130
130 131 # refreshinterval <= 0 means to always refresh.
131 132 if (refreshinterval > 0 and
132 133 self.lastrefresh + refreshinterval > time.time()):
133 134 return
134 135
135 136 if self.baseui:
136 137 u = self.baseui.copy()
137 138 else:
138 139 u = uimod.ui()
139 140 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
140 141 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
141 142 # displaying bundling progress bar while serving feels wrong and may
142 143 # break some wsgi implementations.
143 144 u.setconfig('progress', 'disable', 'true', 'hgweb')
144 145
145 146 if not isinstance(self.conf, (dict, list, tuple)):
146 147 map = {'paths': 'hgweb-paths'}
147 148 if not os.path.exists(self.conf):
148 149 raise error.Abort(_('config file %s not found!') % self.conf)
149 150 u.readconfig(self.conf, remap=map, trust=True)
150 151 paths = []
151 152 for name, ignored in u.configitems('hgweb-paths'):
152 153 for path in u.configlist('hgweb-paths', name):
153 154 paths.append((name, path))
154 155 elif isinstance(self.conf, (list, tuple)):
155 156 paths = self.conf
156 157 elif isinstance(self.conf, dict):
157 158 paths = self.conf.items()
158 159
159 160 repos = findrepos(paths)
160 161 for prefix, root in u.configitems('collections'):
161 162 prefix = util.pconvert(prefix)
162 163 for path in scmutil.walkrepos(root, followsym=True):
163 164 repo = os.path.normpath(path)
164 165 name = util.pconvert(repo)
165 166 if name.startswith(prefix):
166 167 name = name[len(prefix):]
167 168 repos.append((name.lstrip('/'), repo))
168 169
169 170 self.repos = repos
170 171 self.ui = u
171 172 encoding.encoding = self.ui.config('web', 'encoding',
172 173 encoding.encoding)
173 174 self.style = self.ui.config('web', 'style', 'paper')
174 175 self.templatepath = self.ui.config('web', 'templates', None)
175 176 self.stripecount = self.ui.config('web', 'stripes', 1)
176 177 if self.stripecount:
177 178 self.stripecount = int(self.stripecount)
178 179 self._baseurl = self.ui.config('web', 'baseurl')
179 180 prefix = self.ui.config('web', 'prefix', '')
180 181 if prefix.startswith('/'):
181 182 prefix = prefix[1:]
182 183 if prefix.endswith('/'):
183 184 prefix = prefix[:-1]
184 185 self.prefix = prefix
185 186 self.lastrefresh = time.time()
186 187
187 188 def run(self):
188 189 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
189 190 raise RuntimeError("This function is only intended to be "
190 191 "called while running as a CGI script.")
191 192 wsgicgi.launch(self)
192 193
193 194 def __call__(self, env, respond):
194 195 req = wsgirequest(env, respond)
195 196 return self.run_wsgi(req)
196 197
197 198 def read_allowed(self, ui, req):
198 199 """Check allow_read and deny_read config options of a repo's ui object
199 200 to determine user permissions. By default, with neither option set (or
200 201 both empty), allow all users to read the repo. There are two ways a
201 202 user can be denied read access: (1) deny_read is not empty, and the
202 203 user is unauthenticated or deny_read contains user (or *), and (2)
203 204 allow_read is not empty and the user is not in allow_read. Return True
204 205 if user is allowed to read the repo, else return False."""
205 206
206 207 user = req.env.get('REMOTE_USER')
207 208
208 209 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
209 210 if deny_read and (not user or ismember(ui, user, deny_read)):
210 211 return False
211 212
212 213 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
213 214 # by default, allow reading if no allow_read option has been set
214 215 if (not allow_read) or ismember(ui, user, allow_read):
215 216 return True
216 217
217 218 return False
218 219
219 220 def run_wsgi(self, req):
220 return self._runwsgi(req)
221 with profiling.maybeprofile(self.ui):
222 for r in self._runwsgi(req):
223 yield r
221 224
222 225 def _runwsgi(self, req):
223 226 try:
224 227 self.refresh()
225 228
226 229 virtual = req.env.get("PATH_INFO", "").strip('/')
227 230 tmpl = self.templater(req)
228 231 ctype = tmpl('mimetype', encoding=encoding.encoding)
229 232 ctype = templater.stringify(ctype)
230 233
231 234 # a static file
232 235 if virtual.startswith('static/') or 'static' in req.form:
233 236 if virtual.startswith('static/'):
234 237 fname = virtual[7:]
235 238 else:
236 239 fname = req.form['static'][0]
237 240 static = self.ui.config("web", "static", None,
238 241 untrusted=False)
239 242 if not static:
240 243 tp = self.templatepath or templater.templatepaths()
241 244 if isinstance(tp, str):
242 245 tp = [tp]
243 246 static = [os.path.join(p, 'static') for p in tp]
244 247 staticfile(static, fname, req)
245 248 return []
246 249
247 250 # top-level index
248 251 elif not virtual:
249 252 req.respond(HTTP_OK, ctype)
250 253 return self.makeindex(req, tmpl)
251 254
252 255 # nested indexes and hgwebs
253 256
254 257 repos = dict(self.repos)
255 258 virtualrepo = virtual
256 259 while virtualrepo:
257 260 real = repos.get(virtualrepo)
258 261 if real:
259 262 req.env['REPO_NAME'] = virtualrepo
260 263 try:
261 264 # ensure caller gets private copy of ui
262 265 repo = hg.repository(self.ui.copy(), real)
263 266 return hgweb_mod.hgweb(repo).run_wsgi(req)
264 267 except IOError as inst:
265 268 msg = inst.strerror
266 269 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
267 270 except error.RepoError as inst:
268 271 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
269 272
270 273 up = virtualrepo.rfind('/')
271 274 if up < 0:
272 275 break
273 276 virtualrepo = virtualrepo[:up]
274 277
275 278 # browse subdirectories
276 279 subdir = virtual + '/'
277 280 if [r for r in repos if r.startswith(subdir)]:
278 281 req.respond(HTTP_OK, ctype)
279 282 return self.makeindex(req, tmpl, subdir)
280 283
281 284 # prefixes not found
282 285 req.respond(HTTP_NOT_FOUND, ctype)
283 286 return tmpl("notfound", repo=virtual)
284 287
285 288 except ErrorResponse as err:
286 289 req.respond(err, ctype)
287 290 return tmpl('error', error=err.message or '')
288 291 finally:
289 292 tmpl = None
290 293
291 294 def makeindex(self, req, tmpl, subdir=""):
292 295
293 296 def archivelist(ui, nodeid, url):
294 297 allowed = ui.configlist("web", "allow_archive", untrusted=True)
295 298 archives = []
296 299 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
297 300 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
298 301 untrusted=True):
299 302 archives.append({"type" : i[0], "extension": i[1],
300 303 "node": nodeid, "url": url})
301 304 return archives
302 305
303 306 def rawentries(subdir="", **map):
304 307
305 308 descend = self.ui.configbool('web', 'descend', True)
306 309 collapse = self.ui.configbool('web', 'collapse', False)
307 310 seenrepos = set()
308 311 seendirs = set()
309 312 for name, path in self.repos:
310 313
311 314 if not name.startswith(subdir):
312 315 continue
313 316 name = name[len(subdir):]
314 317 directory = False
315 318
316 319 if '/' in name:
317 320 if not descend:
318 321 continue
319 322
320 323 nameparts = name.split('/')
321 324 rootname = nameparts[0]
322 325
323 326 if not collapse:
324 327 pass
325 328 elif rootname in seendirs:
326 329 continue
327 330 elif rootname in seenrepos:
328 331 pass
329 332 else:
330 333 directory = True
331 334 name = rootname
332 335
333 336 # redefine the path to refer to the directory
334 337 discarded = '/'.join(nameparts[1:])
335 338
336 339 # remove name parts plus accompanying slash
337 340 path = path[:-len(discarded) - 1]
338 341
339 342 try:
340 343 r = hg.repository(self.ui, path)
341 344 directory = False
342 345 except (IOError, error.RepoError):
343 346 pass
344 347
345 348 parts = [name]
346 349 if 'PATH_INFO' in req.env:
347 350 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
348 351 if req.env['SCRIPT_NAME']:
349 352 parts.insert(0, req.env['SCRIPT_NAME'])
350 353 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
351 354
352 355 # show either a directory entry or a repository
353 356 if directory:
354 357 # get the directory's time information
355 358 try:
356 359 d = (get_mtime(path), util.makedate()[1])
357 360 except OSError:
358 361 continue
359 362
360 363 # add '/' to the name to make it obvious that
361 364 # the entry is a directory, not a regular repository
362 365 row = {'contact': "",
363 366 'contact_sort': "",
364 367 'name': name + '/',
365 368 'name_sort': name,
366 369 'url': url,
367 370 'description': "",
368 371 'description_sort': "",
369 372 'lastchange': d,
370 373 'lastchange_sort': d[1]-d[0],
371 374 'archives': [],
372 375 'isdirectory': True,
373 376 'labels': [],
374 377 }
375 378
376 379 seendirs.add(name)
377 380 yield row
378 381 continue
379 382
380 383 u = self.ui.copy()
381 384 try:
382 385 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
383 386 except Exception as e:
384 387 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
385 388 continue
386 389 def get(section, name, default=None):
387 390 return u.config(section, name, default, untrusted=True)
388 391
389 392 if u.configbool("web", "hidden", untrusted=True):
390 393 continue
391 394
392 395 if not self.read_allowed(u, req):
393 396 continue
394 397
395 398 # update time with local timezone
396 399 try:
397 400 r = hg.repository(self.ui, path)
398 401 except IOError:
399 402 u.warn(_('error accessing repository at %s\n') % path)
400 403 continue
401 404 except error.RepoError:
402 405 u.warn(_('error accessing repository at %s\n') % path)
403 406 continue
404 407 try:
405 408 d = (get_mtime(r.spath), util.makedate()[1])
406 409 except OSError:
407 410 continue
408 411
409 412 contact = get_contact(get)
410 413 description = get("web", "description", "")
411 414 seenrepos.add(name)
412 415 name = get("web", "name", name)
413 416 row = {'contact': contact or "unknown",
414 417 'contact_sort': contact.upper() or "unknown",
415 418 'name': name,
416 419 'name_sort': name,
417 420 'url': url,
418 421 'description': description or "unknown",
419 422 'description_sort': description.upper() or "unknown",
420 423 'lastchange': d,
421 424 'lastchange_sort': d[1]-d[0],
422 425 'archives': archivelist(u, "tip", url),
423 426 'isdirectory': None,
424 427 'labels': u.configlist('web', 'labels', untrusted=True),
425 428 }
426 429
427 430 yield row
428 431
429 432 sortdefault = None, False
430 433 def entries(sortcolumn="", descending=False, subdir="", **map):
431 434 rows = rawentries(subdir=subdir, **map)
432 435
433 436 if sortcolumn and sortdefault != (sortcolumn, descending):
434 437 sortkey = '%s_sort' % sortcolumn
435 438 rows = sorted(rows, key=lambda x: x[sortkey],
436 439 reverse=descending)
437 440 for row, parity in zip(rows, paritygen(self.stripecount)):
438 441 row['parity'] = parity
439 442 yield row
440 443
441 444 self.refresh()
442 445 sortable = ["name", "description", "contact", "lastchange"]
443 446 sortcolumn, descending = sortdefault
444 447 if 'sort' in req.form:
445 448 sortcolumn = req.form['sort'][0]
446 449 descending = sortcolumn.startswith('-')
447 450 if descending:
448 451 sortcolumn = sortcolumn[1:]
449 452 if sortcolumn not in sortable:
450 453 sortcolumn = ""
451 454
452 455 sort = [("sort_%s" % column,
453 456 "%s%s" % ((not descending and column == sortcolumn)
454 457 and "-" or "", column))
455 458 for column in sortable]
456 459
457 460 self.refresh()
458 461 self.updatereqenv(req.env)
459 462
460 463 return tmpl("index", entries=entries, subdir=subdir,
461 464 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
462 465 sortcolumn=sortcolumn, descending=descending,
463 466 **dict(sort))
464 467
465 468 def templater(self, req):
466 469
467 470 def motd(**map):
468 471 if self.motd is not None:
469 472 yield self.motd
470 473 else:
471 474 yield config('web', 'motd', '')
472 475
473 476 def config(section, name, default=None, untrusted=True):
474 477 return self.ui.config(section, name, default, untrusted)
475 478
476 479 self.updatereqenv(req.env)
477 480
478 481 url = req.env.get('SCRIPT_NAME', '')
479 482 if not url.endswith('/'):
480 483 url += '/'
481 484
482 485 vars = {}
483 486 styles = (
484 487 req.form.get('style', [None])[0],
485 488 config('web', 'style'),
486 489 'paper'
487 490 )
488 491 style, mapfile = templater.stylemap(styles, self.templatepath)
489 492 if style == styles[0]:
490 493 vars['style'] = style
491 494
492 495 start = url[-1] == '?' and '&' or '?'
493 496 sessionvars = webutil.sessionvars(vars, start)
494 497 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
495 498 logoimg = config('web', 'logoimg', 'hglogo.png')
496 499 staticurl = config('web', 'staticurl') or url + 'static/'
497 500 if not staticurl.endswith('/'):
498 501 staticurl += '/'
499 502
500 503 defaults = {
501 504 "encoding": encoding.encoding,
502 505 "motd": motd,
503 506 "url": url,
504 507 "logourl": logourl,
505 508 "logoimg": logoimg,
506 509 "staticurl": staticurl,
507 510 "sessionvars": sessionvars,
508 511 "style": style,
509 512 }
510 513 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
511 514 return tmpl
512 515
513 516 def updatereqenv(self, env):
514 517 if self._baseurl is not None:
515 518 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
516 519 env['SERVER_NAME'] = name
517 520 env['SERVER_PORT'] = port
518 521 env['SCRIPT_NAME'] = path
@@ -1,34 +1,48
1 1 test --time
2 2
3 3 $ hg --time help -q help 2>&1 | grep time > /dev/null
4 4 $ hg init a
5 5 $ cd a
6 6
7 7 #if lsprof
8 8
9 9 test --profile
10 10
11 11 $ hg --profile st 2>../out
12 12 $ grep CallCount ../out > /dev/null || cat ../out
13 13
14 14 $ hg --profile --config profiling.output=../out st
15 15 $ grep CallCount ../out > /dev/null || cat ../out
16 16
17 17 $ hg --profile --config profiling.output=blackbox --config extensions.blackbox= st
18 18 $ grep CallCount .hg/blackbox.log > /dev/null || cat .hg/blackbox.log
19 19
20 20 $ hg --profile --config profiling.format=text st 2>../out
21 21 $ grep CallCount ../out > /dev/null || cat ../out
22 22
23 23 $ echo "[profiling]" >> $HGRCPATH
24 24 $ echo "format=kcachegrind" >> $HGRCPATH
25 25
26 26 $ hg --profile st 2>../out
27 27 $ grep 'events: Ticks' ../out > /dev/null || cat ../out
28 28
29 29 $ hg --profile --config profiling.output=../out st
30 30 $ grep 'events: Ticks' ../out > /dev/null || cat ../out
31 31
32 32 #endif
33 33
34 #if lsprof serve
35
36 Profiling of HTTP requests works
37
38 $ hg --profile --config profiling.format=text --config profiling.output=../profile.log serve -d -p $HGPORT --pid-file ../hg.pid -A ../access.log
39 $ cat ../hg.pid >> $DAEMON_PIDS
40 $ hg -q clone -U http://localhost:$HGPORT ../clone
41
42 A single profile is logged because file logging doesn't append
43 $ grep CallCount ../profile.log | wc -l
44 \s*1 (re)
45
46 #endif
47
34 48 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now