##// END OF EJS Templates
hgweb: move some config options to requestcontext...
Gregory Szorc -
r26135:edfb4d3b default
parent child Browse files
Show More
@@ -1,467 +1,489 b''
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 import os, re
9 import os, re
10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
11 from mercurial.templatefilters import websub
11 from mercurial.templatefilters import websub
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from common import get_stat, ErrorResponse, permhooks, caching
13 from common import get_stat, ErrorResponse, permhooks, caching
14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 from request import wsgirequest
16 from request import wsgirequest
17 import webcommands, protocol, webutil
17 import webcommands, protocol, webutil
18
18
19 perms = {
19 perms = {
20 'changegroup': 'pull',
20 'changegroup': 'pull',
21 'changegroupsubset': 'pull',
21 'changegroupsubset': 'pull',
22 'getbundle': 'pull',
22 'getbundle': 'pull',
23 'stream_out': 'pull',
23 'stream_out': 'pull',
24 'listkeys': 'pull',
24 'listkeys': 'pull',
25 'unbundle': 'push',
25 'unbundle': 'push',
26 'pushkey': 'push',
26 'pushkey': 'push',
27 }
27 }
28
28
29 ## Files of interest
29 ## Files of interest
30 # Used to check if the repository has changed looking at mtime and size of
30 # Used to check if the repository has changed looking at mtime and size of
31 # theses files. This should probably be relocated a bit higher in core.
31 # theses files. This should probably be relocated a bit higher in core.
32 foi = [('spath', '00changelog.i'),
32 foi = [('spath', '00changelog.i'),
33 ('spath', 'phaseroots'), # ! phase can change content at the same size
33 ('spath', 'phaseroots'), # ! phase can change content at the same size
34 ('spath', 'obsstore'),
34 ('spath', 'obsstore'),
35 ('path', 'bookmarks'), # ! bookmark can change content at the same size
35 ('path', 'bookmarks'), # ! bookmark can change content at the same size
36 ]
36 ]
37
37
38 def makebreadcrumb(url, prefix=''):
38 def makebreadcrumb(url, prefix=''):
39 '''Return a 'URL breadcrumb' list
39 '''Return a 'URL breadcrumb' list
40
40
41 A 'URL breadcrumb' is a list of URL-name pairs,
41 A 'URL breadcrumb' is a list of URL-name pairs,
42 corresponding to each of the path items on a URL.
42 corresponding to each of the path items on a URL.
43 This can be used to create path navigation entries.
43 This can be used to create path navigation entries.
44 '''
44 '''
45 if url.endswith('/'):
45 if url.endswith('/'):
46 url = url[:-1]
46 url = url[:-1]
47 if prefix:
47 if prefix:
48 url = '/' + prefix + url
48 url = '/' + prefix + url
49 relpath = url
49 relpath = url
50 if relpath.startswith('/'):
50 if relpath.startswith('/'):
51 relpath = relpath[1:]
51 relpath = relpath[1:]
52
52
53 breadcrumb = []
53 breadcrumb = []
54 urlel = url
54 urlel = url
55 pathitems = [''] + relpath.split('/')
55 pathitems = [''] + relpath.split('/')
56 for pathel in reversed(pathitems):
56 for pathel in reversed(pathitems):
57 if not pathel or not urlel:
57 if not pathel or not urlel:
58 break
58 break
59 breadcrumb.append({'url': urlel, 'name': pathel})
59 breadcrumb.append({'url': urlel, 'name': pathel})
60 urlel = os.path.dirname(urlel)
60 urlel = os.path.dirname(urlel)
61 return reversed(breadcrumb)
61 return reversed(breadcrumb)
62
62
63
63
64 class requestcontext(object):
64 class requestcontext(object):
65 """Holds state/context for an individual request.
65 """Holds state/context for an individual request.
66
66
67 Servers can be multi-threaded. Holding state on the WSGI application
67 Servers can be multi-threaded. Holding state on the WSGI application
68 is prone to race conditions. Instances of this class exist to hold
68 is prone to race conditions. Instances of this class exist to hold
69 mutable and race-free state for requests.
69 mutable and race-free state for requests.
70 """
70 """
71 def __init__(self, app):
71 def __init__(self, app):
72 object.__setattr__(self, 'app', app)
72 object.__setattr__(self, 'app', app)
73 object.__setattr__(self, 'repo', app.repo)
73 object.__setattr__(self, 'repo', app.repo)
74
74
75 object.__setattr__(self, 'maxchanges',
76 self.configint('web', 'maxchanges', 10))
77 object.__setattr__(self, 'stripecount',
78 self.configint('web', 'stripes', 1))
79 object.__setattr__(self, 'maxshortchanges',
80 self.configint('web', 'maxshortchanges', 60))
81 object.__setattr__(self, 'maxfiles',
82 self.configint('web', 'maxfiles', 10))
83 object.__setattr__(self, 'allowpull',
84 self.configbool('web', 'allowpull', True))
85
75 # Proxy unknown reads and writes to the application instance
86 # Proxy unknown reads and writes to the application instance
76 # until everything is moved to us.
87 # until everything is moved to us.
77 def __getattr__(self, name):
88 def __getattr__(self, name):
78 return getattr(self.app, name)
89 return getattr(self.app, name)
79
90
80 def __setattr__(self, name, value):
91 def __setattr__(self, name, value):
81 return setattr(self.app, name, value)
92 return setattr(self.app, name, value)
82
93
94 # Servers are often run by a user different from the repo owner.
95 # Trust the settings from the .hg/hgrc files by default.
96 def config(self, section, name, default=None, untrusted=True):
97 return self.repo.ui.config(section, name, default,
98 untrusted=untrusted)
99
100 def configbool(self, section, name, default=False, untrusted=True):
101 return self.repo.ui.configbool(section, name, default,
102 untrusted=untrusted)
103
104 def configint(self, section, name, default=None, untrusted=True):
105 return self.repo.ui.configint(section, name, default,
106 untrusted=untrusted)
107
108 def configlist(self, section, name, default=None, untrusted=True):
109 return self.repo.ui.configlist(section, name, default,
110 untrusted=untrusted)
111
83 class hgweb(object):
112 class hgweb(object):
84 """HTTP server for individual repositories.
113 """HTTP server for individual repositories.
85
114
86 Instances of this class serve HTTP responses for a particular
115 Instances of this class serve HTTP responses for a particular
87 repository.
116 repository.
88
117
89 Instances are typically used as WSGI applications.
118 Instances are typically used as WSGI applications.
90
119
91 Some servers are multi-threaded. On these servers, there may
120 Some servers are multi-threaded. On these servers, there may
92 be multiple active threads inside __call__.
121 be multiple active threads inside __call__.
93 """
122 """
94 def __init__(self, repo, name=None, baseui=None):
123 def __init__(self, repo, name=None, baseui=None):
95 if isinstance(repo, str):
124 if isinstance(repo, str):
96 if baseui:
125 if baseui:
97 u = baseui.copy()
126 u = baseui.copy()
98 else:
127 else:
99 u = ui.ui()
128 u = ui.ui()
100 r = hg.repository(u, repo)
129 r = hg.repository(u, repo)
101 else:
130 else:
102 # we trust caller to give us a private copy
131 # we trust caller to give us a private copy
103 r = repo
132 r = repo
104
133
105 r = self._getview(r)
134 r = self._getview(r)
106 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
135 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
107 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
136 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
108 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
137 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
109 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
138 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
110 # displaying bundling progress bar while serving feel wrong and may
139 # displaying bundling progress bar while serving feel wrong and may
111 # break some wsgi implementation.
140 # break some wsgi implementation.
112 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
141 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
113 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
142 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
114 self.repo = r
143 self.repo = r
115 hook.redirect(True)
144 hook.redirect(True)
116 self.repostate = ((-1, -1), (-1, -1))
145 self.repostate = ((-1, -1), (-1, -1))
117 self.mtime = -1
146 self.mtime = -1
118 self.reponame = name
147 self.reponame = name
119 self.archives = 'zip', 'gz', 'bz2'
148 self.archives = 'zip', 'gz', 'bz2'
120 self.stripecount = 1
121 # a repo owner may set web.templates in .hg/hgrc to get any file
149 # a repo owner may set web.templates in .hg/hgrc to get any file
122 # readable by the user running the CGI script
150 # readable by the user running the CGI script
123 self.templatepath = self.config('web', 'templates')
151 self.templatepath = self.config('web', 'templates')
124 self.websubtable = self.loadwebsub()
152 self.websubtable = self.loadwebsub()
125
153
126 # The CGI scripts are often run by a user different from the repo owner.
154 # The CGI scripts are often run by a user different from the repo owner.
127 # Trust the settings from the .hg/hgrc files by default.
155 # Trust the settings from the .hg/hgrc files by default.
128 def config(self, section, name, default=None, untrusted=True):
156 def config(self, section, name, default=None, untrusted=True):
129 return self.repo.ui.config(section, name, default,
157 return self.repo.ui.config(section, name, default,
130 untrusted=untrusted)
158 untrusted=untrusted)
131
159
132 def configbool(self, section, name, default=False, untrusted=True):
160 def configbool(self, section, name, default=False, untrusted=True):
133 return self.repo.ui.configbool(section, name, default,
161 return self.repo.ui.configbool(section, name, default,
134 untrusted=untrusted)
162 untrusted=untrusted)
135
163
136 def configlist(self, section, name, default=None, untrusted=True):
164 def configlist(self, section, name, default=None, untrusted=True):
137 return self.repo.ui.configlist(section, name, default,
165 return self.repo.ui.configlist(section, name, default,
138 untrusted=untrusted)
166 untrusted=untrusted)
139
167
140 def _getview(self, repo):
168 def _getview(self, repo):
141 """The 'web.view' config controls changeset filter to hgweb. Possible
169 """The 'web.view' config controls changeset filter to hgweb. Possible
142 values are ``served``, ``visible`` and ``all``. Default is ``served``.
170 values are ``served``, ``visible`` and ``all``. Default is ``served``.
143 The ``served`` filter only shows changesets that can be pulled from the
171 The ``served`` filter only shows changesets that can be pulled from the
144 hgweb instance. The``visible`` filter includes secret changesets but
172 hgweb instance. The``visible`` filter includes secret changesets but
145 still excludes "hidden" one.
173 still excludes "hidden" one.
146
174
147 See the repoview module for details.
175 See the repoview module for details.
148
176
149 The option has been around undocumented since Mercurial 2.5, but no
177 The option has been around undocumented since Mercurial 2.5, but no
150 user ever asked about it. So we better keep it undocumented for now."""
178 user ever asked about it. So we better keep it undocumented for now."""
151 viewconfig = repo.ui.config('web', 'view', 'served',
179 viewconfig = repo.ui.config('web', 'view', 'served',
152 untrusted=True)
180 untrusted=True)
153 if viewconfig == 'all':
181 if viewconfig == 'all':
154 return repo.unfiltered()
182 return repo.unfiltered()
155 elif viewconfig in repoview.filtertable:
183 elif viewconfig in repoview.filtertable:
156 return repo.filtered(viewconfig)
184 return repo.filtered(viewconfig)
157 else:
185 else:
158 return repo.filtered('served')
186 return repo.filtered('served')
159
187
160 def refresh(self, request):
188 def refresh(self, request):
161 repostate = []
189 repostate = []
162 # file of interrests mtime and size
190 # file of interrests mtime and size
163 for meth, fname in foi:
191 for meth, fname in foi:
164 prefix = getattr(self.repo, meth)
192 prefix = getattr(self.repo, meth)
165 st = get_stat(prefix, fname)
193 st = get_stat(prefix, fname)
166 repostate.append((st.st_mtime, st.st_size))
194 repostate.append((st.st_mtime, st.st_size))
167 repostate = tuple(repostate)
195 repostate = tuple(repostate)
168 # we need to compare file size in addition to mtime to catch
196 # we need to compare file size in addition to mtime to catch
169 # changes made less than a second ago
197 # changes made less than a second ago
170 if repostate != self.repostate:
198 if repostate != self.repostate:
171 r = hg.repository(self.repo.baseui, self.repo.url())
199 r = hg.repository(self.repo.baseui, self.repo.url())
172 self.repo = self._getview(r)
200 self.repo = self._getview(r)
173 self.maxchanges = int(self.config("web", "maxchanges", 10))
174 self.stripecount = int(self.config("web", "stripes", 1))
175 self.maxshortchanges = int(self.config("web", "maxshortchanges",
176 60))
177 self.maxfiles = int(self.config("web", "maxfiles", 10))
178 self.allowpull = self.configbool("web", "allowpull", True)
179 encoding.encoding = self.config("web", "encoding",
201 encoding.encoding = self.config("web", "encoding",
180 encoding.encoding)
202 encoding.encoding)
181 # update these last to avoid threads seeing empty settings
203 # update these last to avoid threads seeing empty settings
182 self.repostate = repostate
204 self.repostate = repostate
183 # mtime is needed for ETag
205 # mtime is needed for ETag
184 self.mtime = st.st_mtime
206 self.mtime = st.st_mtime
185
207
186 self.repo.ui.environ = request.env
208 self.repo.ui.environ = request.env
187
209
188 def run(self):
210 def run(self):
189 """Start a server from CGI environment.
211 """Start a server from CGI environment.
190
212
191 Modern servers should be using WSGI and should avoid this
213 Modern servers should be using WSGI and should avoid this
192 method, if possible.
214 method, if possible.
193 """
215 """
194 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
216 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
195 raise RuntimeError("This function is only intended to be "
217 raise RuntimeError("This function is only intended to be "
196 "called while running as a CGI script.")
218 "called while running as a CGI script.")
197 import mercurial.hgweb.wsgicgi as wsgicgi
219 import mercurial.hgweb.wsgicgi as wsgicgi
198 wsgicgi.launch(self)
220 wsgicgi.launch(self)
199
221
200 def __call__(self, env, respond):
222 def __call__(self, env, respond):
201 """Run the WSGI application.
223 """Run the WSGI application.
202
224
203 This may be called by multiple threads.
225 This may be called by multiple threads.
204 """
226 """
205 req = wsgirequest(env, respond)
227 req = wsgirequest(env, respond)
206 return self.run_wsgi(req)
228 return self.run_wsgi(req)
207
229
208 def run_wsgi(self, req):
230 def run_wsgi(self, req):
209 """Internal method to run the WSGI application.
231 """Internal method to run the WSGI application.
210
232
211 This is typically only called by Mercurial. External consumers
233 This is typically only called by Mercurial. External consumers
212 should be using instances of this class as the WSGI application.
234 should be using instances of this class as the WSGI application.
213 """
235 """
214 self.refresh(req)
236 self.refresh(req)
215 rctx = requestcontext(self)
237 rctx = requestcontext(self)
216
238
217 # work with CGI variables to create coherent structure
239 # work with CGI variables to create coherent structure
218 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
240 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
219
241
220 req.url = req.env['SCRIPT_NAME']
242 req.url = req.env['SCRIPT_NAME']
221 if not req.url.endswith('/'):
243 if not req.url.endswith('/'):
222 req.url += '/'
244 req.url += '/'
223 if 'REPO_NAME' in req.env:
245 if 'REPO_NAME' in req.env:
224 req.url += req.env['REPO_NAME'] + '/'
246 req.url += req.env['REPO_NAME'] + '/'
225
247
226 if 'PATH_INFO' in req.env:
248 if 'PATH_INFO' in req.env:
227 parts = req.env['PATH_INFO'].strip('/').split('/')
249 parts = req.env['PATH_INFO'].strip('/').split('/')
228 repo_parts = req.env.get('REPO_NAME', '').split('/')
250 repo_parts = req.env.get('REPO_NAME', '').split('/')
229 if parts[:len(repo_parts)] == repo_parts:
251 if parts[:len(repo_parts)] == repo_parts:
230 parts = parts[len(repo_parts):]
252 parts = parts[len(repo_parts):]
231 query = '/'.join(parts)
253 query = '/'.join(parts)
232 else:
254 else:
233 query = req.env['QUERY_STRING'].split('&', 1)[0]
255 query = req.env['QUERY_STRING'].split('&', 1)[0]
234 query = query.split(';', 1)[0]
256 query = query.split(';', 1)[0]
235
257
236 # process this if it's a protocol request
258 # process this if it's a protocol request
237 # protocol bits don't need to create any URLs
259 # protocol bits don't need to create any URLs
238 # and the clients always use the old URL structure
260 # and the clients always use the old URL structure
239
261
240 cmd = req.form.get('cmd', [''])[0]
262 cmd = req.form.get('cmd', [''])[0]
241 if protocol.iscmd(cmd):
263 if protocol.iscmd(cmd):
242 try:
264 try:
243 if query:
265 if query:
244 raise ErrorResponse(HTTP_NOT_FOUND)
266 raise ErrorResponse(HTTP_NOT_FOUND)
245 if cmd in perms:
267 if cmd in perms:
246 self.check_perm(rctx, req, perms[cmd])
268 self.check_perm(rctx, req, perms[cmd])
247 return protocol.call(self.repo, req, cmd)
269 return protocol.call(self.repo, req, cmd)
248 except ErrorResponse as inst:
270 except ErrorResponse as inst:
249 # A client that sends unbundle without 100-continue will
271 # A client that sends unbundle without 100-continue will
250 # break if we respond early.
272 # break if we respond early.
251 if (cmd == 'unbundle' and
273 if (cmd == 'unbundle' and
252 (req.env.get('HTTP_EXPECT',
274 (req.env.get('HTTP_EXPECT',
253 '').lower() != '100-continue') or
275 '').lower() != '100-continue') or
254 req.env.get('X-HgHttp2', '')):
276 req.env.get('X-HgHttp2', '')):
255 req.drain()
277 req.drain()
256 else:
278 else:
257 req.headers.append(('Connection', 'Close'))
279 req.headers.append(('Connection', 'Close'))
258 req.respond(inst, protocol.HGTYPE,
280 req.respond(inst, protocol.HGTYPE,
259 body='0\n%s\n' % inst.message)
281 body='0\n%s\n' % inst.message)
260 return ''
282 return ''
261
283
262 # translate user-visible url structure to internal structure
284 # translate user-visible url structure to internal structure
263
285
264 args = query.split('/', 2)
286 args = query.split('/', 2)
265 if 'cmd' not in req.form and args and args[0]:
287 if 'cmd' not in req.form and args and args[0]:
266
288
267 cmd = args.pop(0)
289 cmd = args.pop(0)
268 style = cmd.rfind('-')
290 style = cmd.rfind('-')
269 if style != -1:
291 if style != -1:
270 req.form['style'] = [cmd[:style]]
292 req.form['style'] = [cmd[:style]]
271 cmd = cmd[style + 1:]
293 cmd = cmd[style + 1:]
272
294
273 # avoid accepting e.g. style parameter as command
295 # avoid accepting e.g. style parameter as command
274 if util.safehasattr(webcommands, cmd):
296 if util.safehasattr(webcommands, cmd):
275 req.form['cmd'] = [cmd]
297 req.form['cmd'] = [cmd]
276
298
277 if cmd == 'static':
299 if cmd == 'static':
278 req.form['file'] = ['/'.join(args)]
300 req.form['file'] = ['/'.join(args)]
279 else:
301 else:
280 if args and args[0]:
302 if args and args[0]:
281 node = args.pop(0).replace('%2F', '/')
303 node = args.pop(0).replace('%2F', '/')
282 req.form['node'] = [node]
304 req.form['node'] = [node]
283 if args:
305 if args:
284 req.form['file'] = args
306 req.form['file'] = args
285
307
286 ua = req.env.get('HTTP_USER_AGENT', '')
308 ua = req.env.get('HTTP_USER_AGENT', '')
287 if cmd == 'rev' and 'mercurial' in ua:
309 if cmd == 'rev' and 'mercurial' in ua:
288 req.form['style'] = ['raw']
310 req.form['style'] = ['raw']
289
311
290 if cmd == 'archive':
312 if cmd == 'archive':
291 fn = req.form['node'][0]
313 fn = req.form['node'][0]
292 for type_, spec in self.archive_specs.iteritems():
314 for type_, spec in self.archive_specs.iteritems():
293 ext = spec[2]
315 ext = spec[2]
294 if fn.endswith(ext):
316 if fn.endswith(ext):
295 req.form['node'] = [fn[:-len(ext)]]
317 req.form['node'] = [fn[:-len(ext)]]
296 req.form['type'] = [type_]
318 req.form['type'] = [type_]
297
319
298 # process the web interface request
320 # process the web interface request
299
321
300 try:
322 try:
301 tmpl = self.templater(req)
323 tmpl = self.templater(req)
302 ctype = tmpl('mimetype', encoding=encoding.encoding)
324 ctype = tmpl('mimetype', encoding=encoding.encoding)
303 ctype = templater.stringify(ctype)
325 ctype = templater.stringify(ctype)
304
326
305 # check read permissions non-static content
327 # check read permissions non-static content
306 if cmd != 'static':
328 if cmd != 'static':
307 self.check_perm(rctx, req, None)
329 self.check_perm(rctx, req, None)
308
330
309 if cmd == '':
331 if cmd == '':
310 req.form['cmd'] = [tmpl.cache['default']]
332 req.form['cmd'] = [tmpl.cache['default']]
311 cmd = req.form['cmd'][0]
333 cmd = req.form['cmd'][0]
312
334
313 if self.configbool('web', 'cache', True):
335 if self.configbool('web', 'cache', True):
314 caching(self, req) # sets ETag header or raises NOT_MODIFIED
336 caching(self, req) # sets ETag header or raises NOT_MODIFIED
315 if cmd not in webcommands.__all__:
337 if cmd not in webcommands.__all__:
316 msg = 'no such method: %s' % cmd
338 msg = 'no such method: %s' % cmd
317 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
339 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
318 elif cmd == 'file' and 'raw' in req.form.get('style', []):
340 elif cmd == 'file' and 'raw' in req.form.get('style', []):
319 self.ctype = ctype
341 self.ctype = ctype
320 content = webcommands.rawfile(rctx, req, tmpl)
342 content = webcommands.rawfile(rctx, req, tmpl)
321 else:
343 else:
322 content = getattr(webcommands, cmd)(rctx, req, tmpl)
344 content = getattr(webcommands, cmd)(rctx, req, tmpl)
323 req.respond(HTTP_OK, ctype)
345 req.respond(HTTP_OK, ctype)
324
346
325 return content
347 return content
326
348
327 except (error.LookupError, error.RepoLookupError) as err:
349 except (error.LookupError, error.RepoLookupError) as err:
328 req.respond(HTTP_NOT_FOUND, ctype)
350 req.respond(HTTP_NOT_FOUND, ctype)
329 msg = str(err)
351 msg = str(err)
330 if (util.safehasattr(err, 'name') and
352 if (util.safehasattr(err, 'name') and
331 not isinstance(err, error.ManifestLookupError)):
353 not isinstance(err, error.ManifestLookupError)):
332 msg = 'revision not found: %s' % err.name
354 msg = 'revision not found: %s' % err.name
333 return tmpl('error', error=msg)
355 return tmpl('error', error=msg)
334 except (error.RepoError, error.RevlogError) as inst:
356 except (error.RepoError, error.RevlogError) as inst:
335 req.respond(HTTP_SERVER_ERROR, ctype)
357 req.respond(HTTP_SERVER_ERROR, ctype)
336 return tmpl('error', error=str(inst))
358 return tmpl('error', error=str(inst))
337 except ErrorResponse as inst:
359 except ErrorResponse as inst:
338 req.respond(inst, ctype)
360 req.respond(inst, ctype)
339 if inst.code == HTTP_NOT_MODIFIED:
361 if inst.code == HTTP_NOT_MODIFIED:
340 # Not allowed to return a body on a 304
362 # Not allowed to return a body on a 304
341 return ['']
363 return ['']
342 return tmpl('error', error=inst.message)
364 return tmpl('error', error=inst.message)
343
365
344 def loadwebsub(self):
366 def loadwebsub(self):
345 websubtable = []
367 websubtable = []
346 websubdefs = self.repo.ui.configitems('websub')
368 websubdefs = self.repo.ui.configitems('websub')
347 # we must maintain interhg backwards compatibility
369 # we must maintain interhg backwards compatibility
348 websubdefs += self.repo.ui.configitems('interhg')
370 websubdefs += self.repo.ui.configitems('interhg')
349 for key, pattern in websubdefs:
371 for key, pattern in websubdefs:
350 # grab the delimiter from the character after the "s"
372 # grab the delimiter from the character after the "s"
351 unesc = pattern[1]
373 unesc = pattern[1]
352 delim = re.escape(unesc)
374 delim = re.escape(unesc)
353
375
354 # identify portions of the pattern, taking care to avoid escaped
376 # identify portions of the pattern, taking care to avoid escaped
355 # delimiters. the replace format and flags are optional, but
377 # delimiters. the replace format and flags are optional, but
356 # delimiters are required.
378 # delimiters are required.
357 match = re.match(
379 match = re.match(
358 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
380 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
359 % (delim, delim, delim), pattern)
381 % (delim, delim, delim), pattern)
360 if not match:
382 if not match:
361 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
383 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
362 % (key, pattern))
384 % (key, pattern))
363 continue
385 continue
364
386
365 # we need to unescape the delimiter for regexp and format
387 # we need to unescape the delimiter for regexp and format
366 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
388 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
367 regexp = delim_re.sub(unesc, match.group(1))
389 regexp = delim_re.sub(unesc, match.group(1))
368 format = delim_re.sub(unesc, match.group(2))
390 format = delim_re.sub(unesc, match.group(2))
369
391
370 # the pattern allows for 6 regexp flags, so set them if necessary
392 # the pattern allows for 6 regexp flags, so set them if necessary
371 flagin = match.group(3)
393 flagin = match.group(3)
372 flags = 0
394 flags = 0
373 if flagin:
395 if flagin:
374 for flag in flagin.upper():
396 for flag in flagin.upper():
375 flags |= re.__dict__[flag]
397 flags |= re.__dict__[flag]
376
398
377 try:
399 try:
378 regexp = re.compile(regexp, flags)
400 regexp = re.compile(regexp, flags)
379 websubtable.append((regexp, format))
401 websubtable.append((regexp, format))
380 except re.error:
402 except re.error:
381 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
403 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
382 % (key, regexp))
404 % (key, regexp))
383 return websubtable
405 return websubtable
384
406
385 def templater(self, req):
407 def templater(self, req):
386
408
387 # determine scheme, port and server name
409 # determine scheme, port and server name
388 # this is needed to create absolute urls
410 # this is needed to create absolute urls
389
411
390 proto = req.env.get('wsgi.url_scheme')
412 proto = req.env.get('wsgi.url_scheme')
391 if proto == 'https':
413 if proto == 'https':
392 proto = 'https'
414 proto = 'https'
393 default_port = "443"
415 default_port = "443"
394 else:
416 else:
395 proto = 'http'
417 proto = 'http'
396 default_port = "80"
418 default_port = "80"
397
419
398 port = req.env["SERVER_PORT"]
420 port = req.env["SERVER_PORT"]
399 port = port != default_port and (":" + port) or ""
421 port = port != default_port and (":" + port) or ""
400 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
422 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
401 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
423 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
402 logoimg = self.config("web", "logoimg", "hglogo.png")
424 logoimg = self.config("web", "logoimg", "hglogo.png")
403 staticurl = self.config("web", "staticurl") or req.url + 'static/'
425 staticurl = self.config("web", "staticurl") or req.url + 'static/'
404 if not staticurl.endswith('/'):
426 if not staticurl.endswith('/'):
405 staticurl += '/'
427 staticurl += '/'
406
428
407 # some functions for the templater
429 # some functions for the templater
408
430
409 def motd(**map):
431 def motd(**map):
410 yield self.config("web", "motd", "")
432 yield self.config("web", "motd", "")
411
433
412 # figure out which style to use
434 # figure out which style to use
413
435
414 vars = {}
436 vars = {}
415 styles = (
437 styles = (
416 req.form.get('style', [None])[0],
438 req.form.get('style', [None])[0],
417 self.config('web', 'style'),
439 self.config('web', 'style'),
418 'paper',
440 'paper',
419 )
441 )
420 style, mapfile = templater.stylemap(styles, self.templatepath)
442 style, mapfile = templater.stylemap(styles, self.templatepath)
421 if style == styles[0]:
443 if style == styles[0]:
422 vars['style'] = style
444 vars['style'] = style
423
445
424 start = req.url[-1] == '?' and '&' or '?'
446 start = req.url[-1] == '?' and '&' or '?'
425 sessionvars = webutil.sessionvars(vars, start)
447 sessionvars = webutil.sessionvars(vars, start)
426
448
427 if not self.reponame:
449 if not self.reponame:
428 self.reponame = (self.config("web", "name")
450 self.reponame = (self.config("web", "name")
429 or req.env.get('REPO_NAME')
451 or req.env.get('REPO_NAME')
430 or req.url.strip('/') or self.repo.root)
452 or req.url.strip('/') or self.repo.root)
431
453
432 def websubfilter(text):
454 def websubfilter(text):
433 return websub(text, self.websubtable)
455 return websub(text, self.websubtable)
434
456
435 # create the templater
457 # create the templater
436
458
437 tmpl = templater.templater(mapfile,
459 tmpl = templater.templater(mapfile,
438 filters={"websub": websubfilter},
460 filters={"websub": websubfilter},
439 defaults={"url": req.url,
461 defaults={"url": req.url,
440 "logourl": logourl,
462 "logourl": logourl,
441 "logoimg": logoimg,
463 "logoimg": logoimg,
442 "staticurl": staticurl,
464 "staticurl": staticurl,
443 "urlbase": urlbase,
465 "urlbase": urlbase,
444 "repo": self.reponame,
466 "repo": self.reponame,
445 "encoding": encoding.encoding,
467 "encoding": encoding.encoding,
446 "motd": motd,
468 "motd": motd,
447 "sessionvars": sessionvars,
469 "sessionvars": sessionvars,
448 "pathdef": makebreadcrumb(req.url),
470 "pathdef": makebreadcrumb(req.url),
449 "style": style,
471 "style": style,
450 })
472 })
451 return tmpl
473 return tmpl
452
474
453 def archivelist(self, nodeid):
475 def archivelist(self, nodeid):
454 allowed = self.configlist("web", "allow_archive")
476 allowed = self.configlist("web", "allow_archive")
455 for i, spec in self.archive_specs.iteritems():
477 for i, spec in self.archive_specs.iteritems():
456 if i in allowed or self.configbool("web", "allow" + i):
478 if i in allowed or self.configbool("web", "allow" + i):
457 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
479 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
458
480
459 archive_specs = {
481 archive_specs = {
460 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
482 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
461 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
483 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
462 'zip': ('application/zip', 'zip', '.zip', None),
484 'zip': ('application/zip', 'zip', '.zip', None),
463 }
485 }
464
486
465 def check_perm(self, rctx, req, op):
487 def check_perm(self, rctx, req, op):
466 for permhook in permhooks:
488 for permhook in permhooks:
467 permhook(rctx, req, op)
489 permhook(rctx, req, op)
General Comments 0
You need to be logged in to leave comments. Login now