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