##// END OF EJS Templates
hgweb: use util.sortdict for archivespecs...
av6 -
r30748:319914d5 default
parent child Browse files
Show More
@@ -1,471 +1,468
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 31 profiling,
32 32 repoview,
33 33 templatefilters,
34 34 templater,
35 35 ui as uimod,
36 36 util,
37 37 )
38 38
39 39 from . import (
40 40 protocol,
41 41 webcommands,
42 42 webutil,
43 43 wsgicgi,
44 44 )
45 45
46 46 perms = {
47 47 'changegroup': 'pull',
48 48 'changegroupsubset': 'pull',
49 49 'getbundle': 'pull',
50 50 'stream_out': 'pull',
51 51 'listkeys': 'pull',
52 52 'unbundle': 'push',
53 53 'pushkey': 'push',
54 54 }
55 55
56 56 def makebreadcrumb(url, prefix=''):
57 57 '''Return a 'URL breadcrumb' list
58 58
59 59 A 'URL breadcrumb' is a list of URL-name pairs,
60 60 corresponding to each of the path items on a URL.
61 61 This can be used to create path navigation entries.
62 62 '''
63 63 if url.endswith('/'):
64 64 url = url[:-1]
65 65 if prefix:
66 66 url = '/' + prefix + url
67 67 relpath = url
68 68 if relpath.startswith('/'):
69 69 relpath = relpath[1:]
70 70
71 71 breadcrumb = []
72 72 urlel = url
73 73 pathitems = [''] + relpath.split('/')
74 74 for pathel in reversed(pathitems):
75 75 if not pathel or not urlel:
76 76 break
77 77 breadcrumb.append({'url': urlel, 'name': pathel})
78 78 urlel = os.path.dirname(urlel)
79 79 return reversed(breadcrumb)
80 80
81 81 class requestcontext(object):
82 82 """Holds state/context for an individual request.
83 83
84 84 Servers can be multi-threaded. Holding state on the WSGI application
85 85 is prone to race conditions. Instances of this class exist to hold
86 86 mutable and race-free state for requests.
87 87 """
88 88 def __init__(self, app, repo):
89 89 self.repo = repo
90 90 self.reponame = app.reponame
91 91
92 self.archives = ('zip', 'gz', 'bz2')
93
94 92 self.maxchanges = self.configint('web', 'maxchanges', 10)
95 93 self.stripecount = self.configint('web', 'stripes', 1)
96 94 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
97 95 self.maxfiles = self.configint('web', 'maxfiles', 10)
98 96 self.allowpull = self.configbool('web', 'allowpull', True)
99 97
100 98 # we use untrusted=False to prevent a repo owner from using
101 99 # web.templates in .hg/hgrc to get access to any file readable
102 100 # by the user running the CGI script
103 101 self.templatepath = self.config('web', 'templates', untrusted=False)
104 102
105 103 # This object is more expensive to build than simple config values.
106 104 # It is shared across requests. The app will replace the object
107 105 # if it is updated. Since this is a reference and nothing should
108 106 # modify the underlying object, it should be constant for the lifetime
109 107 # of the request.
110 108 self.websubtable = app.websubtable
111 109
112 110 # Trust the settings from the .hg/hgrc files by default.
113 111 def config(self, section, name, default=None, untrusted=True):
114 112 return self.repo.ui.config(section, name, default,
115 113 untrusted=untrusted)
116 114
117 115 def configbool(self, section, name, default=False, untrusted=True):
118 116 return self.repo.ui.configbool(section, name, default,
119 117 untrusted=untrusted)
120 118
121 119 def configint(self, section, name, default=None, untrusted=True):
122 120 return self.repo.ui.configint(section, name, default,
123 121 untrusted=untrusted)
124 122
125 123 def configlist(self, section, name, default=None, untrusted=True):
126 124 return self.repo.ui.configlist(section, name, default,
127 125 untrusted=untrusted)
128 126
129 archivespecs = {
130 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
131 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
132 'zip': ('application/zip', 'zip', '.zip', None),
133 }
127 archivespecs = util.sortdict((
128 ('zip', ('application/zip', 'zip', '.zip', None)),
129 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
130 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
131 ))
134 132
135 133 def archivelist(self, nodeid):
136 134 allowed = self.configlist('web', 'allow_archive')
137 for typ in self.archives:
138 spec = self.archivespecs[typ]
135 for typ, spec in self.archivespecs.iteritems():
139 136 if typ in allowed or self.configbool('web', 'allow%s' % typ):
140 137 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
141 138
142 139 def templater(self, req):
143 140 # determine scheme, port and server name
144 141 # this is needed to create absolute urls
145 142
146 143 proto = req.env.get('wsgi.url_scheme')
147 144 if proto == 'https':
148 145 proto = 'https'
149 146 default_port = '443'
150 147 else:
151 148 proto = 'http'
152 149 default_port = '80'
153 150
154 151 port = req.env['SERVER_PORT']
155 152 port = port != default_port and (':' + port) or ''
156 153 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
157 154 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
158 155 logoimg = self.config('web', 'logoimg', 'hglogo.png')
159 156 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
160 157 if not staticurl.endswith('/'):
161 158 staticurl += '/'
162 159
163 160 # some functions for the templater
164 161
165 162 def motd(**map):
166 163 yield self.config('web', 'motd', '')
167 164
168 165 # figure out which style to use
169 166
170 167 vars = {}
171 168 styles = (
172 169 req.form.get('style', [None])[0],
173 170 self.config('web', 'style'),
174 171 'paper',
175 172 )
176 173 style, mapfile = templater.stylemap(styles, self.templatepath)
177 174 if style == styles[0]:
178 175 vars['style'] = style
179 176
180 177 start = req.url[-1] == '?' and '&' or '?'
181 178 sessionvars = webutil.sessionvars(vars, start)
182 179
183 180 if not self.reponame:
184 181 self.reponame = (self.config('web', 'name')
185 182 or req.env.get('REPO_NAME')
186 183 or req.url.strip('/') or self.repo.root)
187 184
188 185 def websubfilter(text):
189 186 return templatefilters.websub(text, self.websubtable)
190 187
191 188 # create the templater
192 189
193 190 defaults = {
194 191 'url': req.url,
195 192 'logourl': logourl,
196 193 'logoimg': logoimg,
197 194 'staticurl': staticurl,
198 195 'urlbase': urlbase,
199 196 'repo': self.reponame,
200 197 'encoding': encoding.encoding,
201 198 'motd': motd,
202 199 'sessionvars': sessionvars,
203 200 'pathdef': makebreadcrumb(req.url),
204 201 'style': style,
205 202 }
206 203 tmpl = templater.templater.frommapfile(mapfile,
207 204 filters={'websub': websubfilter},
208 205 defaults=defaults)
209 206 return tmpl
210 207
211 208
212 209 class hgweb(object):
213 210 """HTTP server for individual repositories.
214 211
215 212 Instances of this class serve HTTP responses for a particular
216 213 repository.
217 214
218 215 Instances are typically used as WSGI applications.
219 216
220 217 Some servers are multi-threaded. On these servers, there may
221 218 be multiple active threads inside __call__.
222 219 """
223 220 def __init__(self, repo, name=None, baseui=None):
224 221 if isinstance(repo, str):
225 222 if baseui:
226 223 u = baseui.copy()
227 224 else:
228 225 u = uimod.ui.load()
229 226 r = hg.repository(u, repo)
230 227 else:
231 228 # we trust caller to give us a private copy
232 229 r = repo
233 230
234 231 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
235 232 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
236 233 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
237 234 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
238 235 # resolve file patterns relative to repo root
239 236 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
240 237 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
241 238 # displaying bundling progress bar while serving feel wrong and may
242 239 # break some wsgi implementation.
243 240 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
244 241 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
245 242 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
246 243 self._lastrepo = self._repos[0]
247 244 hook.redirect(True)
248 245 self.reponame = name
249 246
250 247 def _webifyrepo(self, repo):
251 248 repo = getwebview(repo)
252 249 self.websubtable = webutil.getwebsubs(repo)
253 250 return repo
254 251
255 252 @contextlib.contextmanager
256 253 def _obtainrepo(self):
257 254 """Obtain a repo unique to the caller.
258 255
259 256 Internally we maintain a stack of cachedlocalrepo instances
260 257 to be handed out. If one is available, we pop it and return it,
261 258 ensuring it is up to date in the process. If one is not available,
262 259 we clone the most recently used repo instance and return it.
263 260
264 261 It is currently possible for the stack to grow without bounds
265 262 if the server allows infinite threads. However, servers should
266 263 have a thread limit, thus establishing our limit.
267 264 """
268 265 if self._repos:
269 266 cached = self._repos.pop()
270 267 r, created = cached.fetch()
271 268 else:
272 269 cached = self._lastrepo.copy()
273 270 r, created = cached.fetch()
274 271 if created:
275 272 r = self._webifyrepo(r)
276 273
277 274 self._lastrepo = cached
278 275 self.mtime = cached.mtime
279 276 try:
280 277 yield r
281 278 finally:
282 279 self._repos.append(cached)
283 280
284 281 def run(self):
285 282 """Start a server from CGI environment.
286 283
287 284 Modern servers should be using WSGI and should avoid this
288 285 method, if possible.
289 286 """
290 287 if not encoding.environ.get('GATEWAY_INTERFACE',
291 288 '').startswith("CGI/1."):
292 289 raise RuntimeError("This function is only intended to be "
293 290 "called while running as a CGI script.")
294 291 wsgicgi.launch(self)
295 292
296 293 def __call__(self, env, respond):
297 294 """Run the WSGI application.
298 295
299 296 This may be called by multiple threads.
300 297 """
301 298 req = wsgirequest(env, respond)
302 299 return self.run_wsgi(req)
303 300
304 301 def run_wsgi(self, req):
305 302 """Internal method to run the WSGI application.
306 303
307 304 This is typically only called by Mercurial. External consumers
308 305 should be using instances of this class as the WSGI application.
309 306 """
310 307 with self._obtainrepo() as repo:
311 308 with profiling.maybeprofile(repo.ui):
312 309 for r in self._runwsgi(req, repo):
313 310 yield r
314 311
315 312 def _runwsgi(self, req, repo):
316 313 rctx = requestcontext(self, repo)
317 314
318 315 # This state is global across all threads.
319 316 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
320 317 rctx.repo.ui.environ = req.env
321 318
322 319 # work with CGI variables to create coherent structure
323 320 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
324 321
325 322 req.url = req.env['SCRIPT_NAME']
326 323 if not req.url.endswith('/'):
327 324 req.url += '/'
328 325 if 'REPO_NAME' in req.env:
329 326 req.url += req.env['REPO_NAME'] + '/'
330 327
331 328 if 'PATH_INFO' in req.env:
332 329 parts = req.env['PATH_INFO'].strip('/').split('/')
333 330 repo_parts = req.env.get('REPO_NAME', '').split('/')
334 331 if parts[:len(repo_parts)] == repo_parts:
335 332 parts = parts[len(repo_parts):]
336 333 query = '/'.join(parts)
337 334 else:
338 335 query = req.env['QUERY_STRING'].partition('&')[0]
339 336 query = query.partition(';')[0]
340 337
341 338 # process this if it's a protocol request
342 339 # protocol bits don't need to create any URLs
343 340 # and the clients always use the old URL structure
344 341
345 342 cmd = req.form.get('cmd', [''])[0]
346 343 if protocol.iscmd(cmd):
347 344 try:
348 345 if query:
349 346 raise ErrorResponse(HTTP_NOT_FOUND)
350 347 if cmd in perms:
351 348 self.check_perm(rctx, req, perms[cmd])
352 349 return protocol.call(rctx.repo, req, cmd)
353 350 except ErrorResponse as inst:
354 351 # A client that sends unbundle without 100-continue will
355 352 # break if we respond early.
356 353 if (cmd == 'unbundle' and
357 354 (req.env.get('HTTP_EXPECT',
358 355 '').lower() != '100-continue') or
359 356 req.env.get('X-HgHttp2', '')):
360 357 req.drain()
361 358 else:
362 359 req.headers.append(('Connection', 'Close'))
363 360 req.respond(inst, protocol.HGTYPE,
364 361 body='0\n%s\n' % inst)
365 362 return ''
366 363
367 364 # translate user-visible url structure to internal structure
368 365
369 366 args = query.split('/', 2)
370 367 if 'cmd' not in req.form and args and args[0]:
371 368
372 369 cmd = args.pop(0)
373 370 style = cmd.rfind('-')
374 371 if style != -1:
375 372 req.form['style'] = [cmd[:style]]
376 373 cmd = cmd[style + 1:]
377 374
378 375 # avoid accepting e.g. style parameter as command
379 376 if util.safehasattr(webcommands, cmd):
380 377 req.form['cmd'] = [cmd]
381 378
382 379 if cmd == 'static':
383 380 req.form['file'] = ['/'.join(args)]
384 381 else:
385 382 if args and args[0]:
386 383 node = args.pop(0).replace('%2F', '/')
387 384 req.form['node'] = [node]
388 385 if args:
389 386 req.form['file'] = args
390 387
391 388 ua = req.env.get('HTTP_USER_AGENT', '')
392 389 if cmd == 'rev' and 'mercurial' in ua:
393 390 req.form['style'] = ['raw']
394 391
395 392 if cmd == 'archive':
396 393 fn = req.form['node'][0]
397 394 for type_, spec in rctx.archivespecs.iteritems():
398 395 ext = spec[2]
399 396 if fn.endswith(ext):
400 397 req.form['node'] = [fn[:-len(ext)]]
401 398 req.form['type'] = [type_]
402 399
403 400 # process the web interface request
404 401
405 402 try:
406 403 tmpl = rctx.templater(req)
407 404 ctype = tmpl('mimetype', encoding=encoding.encoding)
408 405 ctype = templater.stringify(ctype)
409 406
410 407 # check read permissions non-static content
411 408 if cmd != 'static':
412 409 self.check_perm(rctx, req, None)
413 410
414 411 if cmd == '':
415 412 req.form['cmd'] = [tmpl.cache['default']]
416 413 cmd = req.form['cmd'][0]
417 414
418 415 if rctx.configbool('web', 'cache', True):
419 416 caching(self, req) # sets ETag header or raises NOT_MODIFIED
420 417 if cmd not in webcommands.__all__:
421 418 msg = 'no such method: %s' % cmd
422 419 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
423 420 elif cmd == 'file' and 'raw' in req.form.get('style', []):
424 421 rctx.ctype = ctype
425 422 content = webcommands.rawfile(rctx, req, tmpl)
426 423 else:
427 424 content = getattr(webcommands, cmd)(rctx, req, tmpl)
428 425 req.respond(HTTP_OK, ctype)
429 426
430 427 return content
431 428
432 429 except (error.LookupError, error.RepoLookupError) as err:
433 430 req.respond(HTTP_NOT_FOUND, ctype)
434 431 msg = str(err)
435 432 if (util.safehasattr(err, 'name') and
436 433 not isinstance(err, error.ManifestLookupError)):
437 434 msg = 'revision not found: %s' % err.name
438 435 return tmpl('error', error=msg)
439 436 except (error.RepoError, error.RevlogError) as inst:
440 437 req.respond(HTTP_SERVER_ERROR, ctype)
441 438 return tmpl('error', error=str(inst))
442 439 except ErrorResponse as inst:
443 440 req.respond(inst, ctype)
444 441 if inst.code == HTTP_NOT_MODIFIED:
445 442 # Not allowed to return a body on a 304
446 443 return ['']
447 444 return tmpl('error', error=str(inst))
448 445
449 446 def check_perm(self, rctx, req, op):
450 447 for permhook in permhooks:
451 448 permhook(rctx, req, op)
452 449
453 450 def getwebview(repo):
454 451 """The 'web.view' config controls changeset filter to hgweb. Possible
455 452 values are ``served``, ``visible`` and ``all``. Default is ``served``.
456 453 The ``served`` filter only shows changesets that can be pulled from the
457 454 hgweb instance. The``visible`` filter includes secret changesets but
458 455 still excludes "hidden" one.
459 456
460 457 See the repoview module for details.
461 458
462 459 The option has been around undocumented since Mercurial 2.5, but no
463 460 user ever asked about it. So we better keep it undocumented for now."""
464 461 viewconfig = repo.ui.config('web', 'view', 'served',
465 462 untrusted=True)
466 463 if viewconfig == 'all':
467 464 return repo.unfiltered()
468 465 elif viewconfig in repoview.filtertable:
469 466 return repo.filtered(viewconfig)
470 467 else:
471 468 return repo.filtered('served')
General Comments 0
You need to be logged in to leave comments. Login now