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