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