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