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