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