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