##// END OF EJS Templates
hgweb: use archivespecs for links on repo index page too...
av6 -
r30749:e38e7ea2 default
parent child Browse files
Show More
@@ -1,468 +1,470 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 archivespecs = util.sortdict((
57 ('zip', ('application/zip', 'zip', '.zip', None)),
58 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
59 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
60 ))
61
56 def makebreadcrumb(url, prefix=''):
62 def makebreadcrumb(url, prefix=''):
57 '''Return a 'URL breadcrumb' list
63 '''Return a 'URL breadcrumb' list
58
64
59 A 'URL breadcrumb' is a list of URL-name pairs,
65 A 'URL breadcrumb' is a list of URL-name pairs,
60 corresponding to each of the path items on a URL.
66 corresponding to each of the path items on a URL.
61 This can be used to create path navigation entries.
67 This can be used to create path navigation entries.
62 '''
68 '''
63 if url.endswith('/'):
69 if url.endswith('/'):
64 url = url[:-1]
70 url = url[:-1]
65 if prefix:
71 if prefix:
66 url = '/' + prefix + url
72 url = '/' + prefix + url
67 relpath = url
73 relpath = url
68 if relpath.startswith('/'):
74 if relpath.startswith('/'):
69 relpath = relpath[1:]
75 relpath = relpath[1:]
70
76
71 breadcrumb = []
77 breadcrumb = []
72 urlel = url
78 urlel = url
73 pathitems = [''] + relpath.split('/')
79 pathitems = [''] + relpath.split('/')
74 for pathel in reversed(pathitems):
80 for pathel in reversed(pathitems):
75 if not pathel or not urlel:
81 if not pathel or not urlel:
76 break
82 break
77 breadcrumb.append({'url': urlel, 'name': pathel})
83 breadcrumb.append({'url': urlel, 'name': pathel})
78 urlel = os.path.dirname(urlel)
84 urlel = os.path.dirname(urlel)
79 return reversed(breadcrumb)
85 return reversed(breadcrumb)
80
86
81 class requestcontext(object):
87 class requestcontext(object):
82 """Holds state/context for an individual request.
88 """Holds state/context for an individual request.
83
89
84 Servers can be multi-threaded. Holding state on the WSGI application
90 Servers can be multi-threaded. Holding state on the WSGI application
85 is prone to race conditions. Instances of this class exist to hold
91 is prone to race conditions. Instances of this class exist to hold
86 mutable and race-free state for requests.
92 mutable and race-free state for requests.
87 """
93 """
88 def __init__(self, app, repo):
94 def __init__(self, app, repo):
89 self.repo = repo
95 self.repo = repo
90 self.reponame = app.reponame
96 self.reponame = app.reponame
91
97
98 self.archivespecs = archivespecs
99
92 self.maxchanges = self.configint('web', 'maxchanges', 10)
100 self.maxchanges = self.configint('web', 'maxchanges', 10)
93 self.stripecount = self.configint('web', 'stripes', 1)
101 self.stripecount = self.configint('web', 'stripes', 1)
94 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
102 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
95 self.maxfiles = self.configint('web', 'maxfiles', 10)
103 self.maxfiles = self.configint('web', 'maxfiles', 10)
96 self.allowpull = self.configbool('web', 'allowpull', True)
104 self.allowpull = self.configbool('web', 'allowpull', True)
97
105
98 # we use untrusted=False to prevent a repo owner from using
106 # we use untrusted=False to prevent a repo owner from using
99 # web.templates in .hg/hgrc to get access to any file readable
107 # web.templates in .hg/hgrc to get access to any file readable
100 # by the user running the CGI script
108 # by the user running the CGI script
101 self.templatepath = self.config('web', 'templates', untrusted=False)
109 self.templatepath = self.config('web', 'templates', untrusted=False)
102
110
103 # This object is more expensive to build than simple config values.
111 # This object is more expensive to build than simple config values.
104 # It is shared across requests. The app will replace the object
112 # It is shared across requests. The app will replace the object
105 # if it is updated. Since this is a reference and nothing should
113 # if it is updated. Since this is a reference and nothing should
106 # modify the underlying object, it should be constant for the lifetime
114 # modify the underlying object, it should be constant for the lifetime
107 # of the request.
115 # of the request.
108 self.websubtable = app.websubtable
116 self.websubtable = app.websubtable
109
117
110 # Trust the settings from the .hg/hgrc files by default.
118 # Trust the settings from the .hg/hgrc files by default.
111 def config(self, section, name, default=None, untrusted=True):
119 def config(self, section, name, default=None, untrusted=True):
112 return self.repo.ui.config(section, name, default,
120 return self.repo.ui.config(section, name, default,
113 untrusted=untrusted)
121 untrusted=untrusted)
114
122
115 def configbool(self, section, name, default=False, untrusted=True):
123 def configbool(self, section, name, default=False, untrusted=True):
116 return self.repo.ui.configbool(section, name, default,
124 return self.repo.ui.configbool(section, name, default,
117 untrusted=untrusted)
125 untrusted=untrusted)
118
126
119 def configint(self, section, name, default=None, untrusted=True):
127 def configint(self, section, name, default=None, untrusted=True):
120 return self.repo.ui.configint(section, name, default,
128 return self.repo.ui.configint(section, name, default,
121 untrusted=untrusted)
129 untrusted=untrusted)
122
130
123 def configlist(self, section, name, default=None, untrusted=True):
131 def configlist(self, section, name, default=None, untrusted=True):
124 return self.repo.ui.configlist(section, name, default,
132 return self.repo.ui.configlist(section, name, default,
125 untrusted=untrusted)
133 untrusted=untrusted)
126
134
127 archivespecs = util.sortdict((
128 ('zip', ('application/zip', 'zip', '.zip', None)),
129 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
130 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
131 ))
132
133 def archivelist(self, nodeid):
135 def archivelist(self, nodeid):
134 allowed = self.configlist('web', 'allow_archive')
136 allowed = self.configlist('web', 'allow_archive')
135 for typ, spec in self.archivespecs.iteritems():
137 for typ, spec in self.archivespecs.iteritems():
136 if typ in allowed or self.configbool('web', 'allow%s' % typ):
138 if typ in allowed or self.configbool('web', 'allow%s' % typ):
137 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
139 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
138
140
139 def templater(self, req):
141 def templater(self, req):
140 # determine scheme, port and server name
142 # determine scheme, port and server name
141 # this is needed to create absolute urls
143 # this is needed to create absolute urls
142
144
143 proto = req.env.get('wsgi.url_scheme')
145 proto = req.env.get('wsgi.url_scheme')
144 if proto == 'https':
146 if proto == 'https':
145 proto = 'https'
147 proto = 'https'
146 default_port = '443'
148 default_port = '443'
147 else:
149 else:
148 proto = 'http'
150 proto = 'http'
149 default_port = '80'
151 default_port = '80'
150
152
151 port = req.env['SERVER_PORT']
153 port = req.env['SERVER_PORT']
152 port = port != default_port and (':' + port) or ''
154 port = port != default_port and (':' + port) or ''
153 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
155 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
154 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
156 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
155 logoimg = self.config('web', 'logoimg', 'hglogo.png')
157 logoimg = self.config('web', 'logoimg', 'hglogo.png')
156 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
158 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
157 if not staticurl.endswith('/'):
159 if not staticurl.endswith('/'):
158 staticurl += '/'
160 staticurl += '/'
159
161
160 # some functions for the templater
162 # some functions for the templater
161
163
162 def motd(**map):
164 def motd(**map):
163 yield self.config('web', 'motd', '')
165 yield self.config('web', 'motd', '')
164
166
165 # figure out which style to use
167 # figure out which style to use
166
168
167 vars = {}
169 vars = {}
168 styles = (
170 styles = (
169 req.form.get('style', [None])[0],
171 req.form.get('style', [None])[0],
170 self.config('web', 'style'),
172 self.config('web', 'style'),
171 'paper',
173 'paper',
172 )
174 )
173 style, mapfile = templater.stylemap(styles, self.templatepath)
175 style, mapfile = templater.stylemap(styles, self.templatepath)
174 if style == styles[0]:
176 if style == styles[0]:
175 vars['style'] = style
177 vars['style'] = style
176
178
177 start = req.url[-1] == '?' and '&' or '?'
179 start = req.url[-1] == '?' and '&' or '?'
178 sessionvars = webutil.sessionvars(vars, start)
180 sessionvars = webutil.sessionvars(vars, start)
179
181
180 if not self.reponame:
182 if not self.reponame:
181 self.reponame = (self.config('web', 'name')
183 self.reponame = (self.config('web', 'name')
182 or req.env.get('REPO_NAME')
184 or req.env.get('REPO_NAME')
183 or req.url.strip('/') or self.repo.root)
185 or req.url.strip('/') or self.repo.root)
184
186
185 def websubfilter(text):
187 def websubfilter(text):
186 return templatefilters.websub(text, self.websubtable)
188 return templatefilters.websub(text, self.websubtable)
187
189
188 # create the templater
190 # create the templater
189
191
190 defaults = {
192 defaults = {
191 'url': req.url,
193 'url': req.url,
192 'logourl': logourl,
194 'logourl': logourl,
193 'logoimg': logoimg,
195 'logoimg': logoimg,
194 'staticurl': staticurl,
196 'staticurl': staticurl,
195 'urlbase': urlbase,
197 'urlbase': urlbase,
196 'repo': self.reponame,
198 'repo': self.reponame,
197 'encoding': encoding.encoding,
199 'encoding': encoding.encoding,
198 'motd': motd,
200 'motd': motd,
199 'sessionvars': sessionvars,
201 'sessionvars': sessionvars,
200 'pathdef': makebreadcrumb(req.url),
202 'pathdef': makebreadcrumb(req.url),
201 'style': style,
203 'style': style,
202 }
204 }
203 tmpl = templater.templater.frommapfile(mapfile,
205 tmpl = templater.templater.frommapfile(mapfile,
204 filters={'websub': websubfilter},
206 filters={'websub': websubfilter},
205 defaults=defaults)
207 defaults=defaults)
206 return tmpl
208 return tmpl
207
209
208
210
209 class hgweb(object):
211 class hgweb(object):
210 """HTTP server for individual repositories.
212 """HTTP server for individual repositories.
211
213
212 Instances of this class serve HTTP responses for a particular
214 Instances of this class serve HTTP responses for a particular
213 repository.
215 repository.
214
216
215 Instances are typically used as WSGI applications.
217 Instances are typically used as WSGI applications.
216
218
217 Some servers are multi-threaded. On these servers, there may
219 Some servers are multi-threaded. On these servers, there may
218 be multiple active threads inside __call__.
220 be multiple active threads inside __call__.
219 """
221 """
220 def __init__(self, repo, name=None, baseui=None):
222 def __init__(self, repo, name=None, baseui=None):
221 if isinstance(repo, str):
223 if isinstance(repo, str):
222 if baseui:
224 if baseui:
223 u = baseui.copy()
225 u = baseui.copy()
224 else:
226 else:
225 u = uimod.ui.load()
227 u = uimod.ui.load()
226 r = hg.repository(u, repo)
228 r = hg.repository(u, repo)
227 else:
229 else:
228 # we trust caller to give us a private copy
230 # we trust caller to give us a private copy
229 r = repo
231 r = repo
230
232
231 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
233 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
232 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
234 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
233 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
235 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
234 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
236 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
235 # resolve file patterns relative to repo root
237 # resolve file patterns relative to repo root
236 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
238 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
237 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
239 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
238 # displaying bundling progress bar while serving feel wrong and may
240 # displaying bundling progress bar while serving feel wrong and may
239 # break some wsgi implementation.
241 # break some wsgi implementation.
240 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
242 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
241 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
243 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
242 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
244 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
243 self._lastrepo = self._repos[0]
245 self._lastrepo = self._repos[0]
244 hook.redirect(True)
246 hook.redirect(True)
245 self.reponame = name
247 self.reponame = name
246
248
247 def _webifyrepo(self, repo):
249 def _webifyrepo(self, repo):
248 repo = getwebview(repo)
250 repo = getwebview(repo)
249 self.websubtable = webutil.getwebsubs(repo)
251 self.websubtable = webutil.getwebsubs(repo)
250 return repo
252 return repo
251
253
252 @contextlib.contextmanager
254 @contextlib.contextmanager
253 def _obtainrepo(self):
255 def _obtainrepo(self):
254 """Obtain a repo unique to the caller.
256 """Obtain a repo unique to the caller.
255
257
256 Internally we maintain a stack of cachedlocalrepo instances
258 Internally we maintain a stack of cachedlocalrepo instances
257 to be handed out. If one is available, we pop it and return it,
259 to be handed out. If one is available, we pop it and return it,
258 ensuring it is up to date in the process. If one is not available,
260 ensuring it is up to date in the process. If one is not available,
259 we clone the most recently used repo instance and return it.
261 we clone the most recently used repo instance and return it.
260
262
261 It is currently possible for the stack to grow without bounds
263 It is currently possible for the stack to grow without bounds
262 if the server allows infinite threads. However, servers should
264 if the server allows infinite threads. However, servers should
263 have a thread limit, thus establishing our limit.
265 have a thread limit, thus establishing our limit.
264 """
266 """
265 if self._repos:
267 if self._repos:
266 cached = self._repos.pop()
268 cached = self._repos.pop()
267 r, created = cached.fetch()
269 r, created = cached.fetch()
268 else:
270 else:
269 cached = self._lastrepo.copy()
271 cached = self._lastrepo.copy()
270 r, created = cached.fetch()
272 r, created = cached.fetch()
271 if created:
273 if created:
272 r = self._webifyrepo(r)
274 r = self._webifyrepo(r)
273
275
274 self._lastrepo = cached
276 self._lastrepo = cached
275 self.mtime = cached.mtime
277 self.mtime = cached.mtime
276 try:
278 try:
277 yield r
279 yield r
278 finally:
280 finally:
279 self._repos.append(cached)
281 self._repos.append(cached)
280
282
281 def run(self):
283 def run(self):
282 """Start a server from CGI environment.
284 """Start a server from CGI environment.
283
285
284 Modern servers should be using WSGI and should avoid this
286 Modern servers should be using WSGI and should avoid this
285 method, if possible.
287 method, if possible.
286 """
288 """
287 if not encoding.environ.get('GATEWAY_INTERFACE',
289 if not encoding.environ.get('GATEWAY_INTERFACE',
288 '').startswith("CGI/1."):
290 '').startswith("CGI/1."):
289 raise RuntimeError("This function is only intended to be "
291 raise RuntimeError("This function is only intended to be "
290 "called while running as a CGI script.")
292 "called while running as a CGI script.")
291 wsgicgi.launch(self)
293 wsgicgi.launch(self)
292
294
293 def __call__(self, env, respond):
295 def __call__(self, env, respond):
294 """Run the WSGI application.
296 """Run the WSGI application.
295
297
296 This may be called by multiple threads.
298 This may be called by multiple threads.
297 """
299 """
298 req = wsgirequest(env, respond)
300 req = wsgirequest(env, respond)
299 return self.run_wsgi(req)
301 return self.run_wsgi(req)
300
302
301 def run_wsgi(self, req):
303 def run_wsgi(self, req):
302 """Internal method to run the WSGI application.
304 """Internal method to run the WSGI application.
303
305
304 This is typically only called by Mercurial. External consumers
306 This is typically only called by Mercurial. External consumers
305 should be using instances of this class as the WSGI application.
307 should be using instances of this class as the WSGI application.
306 """
308 """
307 with self._obtainrepo() as repo:
309 with self._obtainrepo() as repo:
308 with profiling.maybeprofile(repo.ui):
310 with profiling.maybeprofile(repo.ui):
309 for r in self._runwsgi(req, repo):
311 for r in self._runwsgi(req, repo):
310 yield r
312 yield r
311
313
312 def _runwsgi(self, req, repo):
314 def _runwsgi(self, req, repo):
313 rctx = requestcontext(self, repo)
315 rctx = requestcontext(self, repo)
314
316
315 # This state is global across all threads.
317 # This state is global across all threads.
316 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
318 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
317 rctx.repo.ui.environ = req.env
319 rctx.repo.ui.environ = req.env
318
320
319 # work with CGI variables to create coherent structure
321 # work with CGI variables to create coherent structure
320 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
322 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
321
323
322 req.url = req.env['SCRIPT_NAME']
324 req.url = req.env['SCRIPT_NAME']
323 if not req.url.endswith('/'):
325 if not req.url.endswith('/'):
324 req.url += '/'
326 req.url += '/'
325 if 'REPO_NAME' in req.env:
327 if 'REPO_NAME' in req.env:
326 req.url += req.env['REPO_NAME'] + '/'
328 req.url += req.env['REPO_NAME'] + '/'
327
329
328 if 'PATH_INFO' in req.env:
330 if 'PATH_INFO' in req.env:
329 parts = req.env['PATH_INFO'].strip('/').split('/')
331 parts = req.env['PATH_INFO'].strip('/').split('/')
330 repo_parts = req.env.get('REPO_NAME', '').split('/')
332 repo_parts = req.env.get('REPO_NAME', '').split('/')
331 if parts[:len(repo_parts)] == repo_parts:
333 if parts[:len(repo_parts)] == repo_parts:
332 parts = parts[len(repo_parts):]
334 parts = parts[len(repo_parts):]
333 query = '/'.join(parts)
335 query = '/'.join(parts)
334 else:
336 else:
335 query = req.env['QUERY_STRING'].partition('&')[0]
337 query = req.env['QUERY_STRING'].partition('&')[0]
336 query = query.partition(';')[0]
338 query = query.partition(';')[0]
337
339
338 # process this if it's a protocol request
340 # process this if it's a protocol request
339 # protocol bits don't need to create any URLs
341 # protocol bits don't need to create any URLs
340 # and the clients always use the old URL structure
342 # and the clients always use the old URL structure
341
343
342 cmd = req.form.get('cmd', [''])[0]
344 cmd = req.form.get('cmd', [''])[0]
343 if protocol.iscmd(cmd):
345 if protocol.iscmd(cmd):
344 try:
346 try:
345 if query:
347 if query:
346 raise ErrorResponse(HTTP_NOT_FOUND)
348 raise ErrorResponse(HTTP_NOT_FOUND)
347 if cmd in perms:
349 if cmd in perms:
348 self.check_perm(rctx, req, perms[cmd])
350 self.check_perm(rctx, req, perms[cmd])
349 return protocol.call(rctx.repo, req, cmd)
351 return protocol.call(rctx.repo, req, cmd)
350 except ErrorResponse as inst:
352 except ErrorResponse as inst:
351 # A client that sends unbundle without 100-continue will
353 # A client that sends unbundle without 100-continue will
352 # break if we respond early.
354 # break if we respond early.
353 if (cmd == 'unbundle' and
355 if (cmd == 'unbundle' and
354 (req.env.get('HTTP_EXPECT',
356 (req.env.get('HTTP_EXPECT',
355 '').lower() != '100-continue') or
357 '').lower() != '100-continue') or
356 req.env.get('X-HgHttp2', '')):
358 req.env.get('X-HgHttp2', '')):
357 req.drain()
359 req.drain()
358 else:
360 else:
359 req.headers.append(('Connection', 'Close'))
361 req.headers.append(('Connection', 'Close'))
360 req.respond(inst, protocol.HGTYPE,
362 req.respond(inst, protocol.HGTYPE,
361 body='0\n%s\n' % inst)
363 body='0\n%s\n' % inst)
362 return ''
364 return ''
363
365
364 # translate user-visible url structure to internal structure
366 # translate user-visible url structure to internal structure
365
367
366 args = query.split('/', 2)
368 args = query.split('/', 2)
367 if 'cmd' not in req.form and args and args[0]:
369 if 'cmd' not in req.form and args and args[0]:
368
370
369 cmd = args.pop(0)
371 cmd = args.pop(0)
370 style = cmd.rfind('-')
372 style = cmd.rfind('-')
371 if style != -1:
373 if style != -1:
372 req.form['style'] = [cmd[:style]]
374 req.form['style'] = [cmd[:style]]
373 cmd = cmd[style + 1:]
375 cmd = cmd[style + 1:]
374
376
375 # avoid accepting e.g. style parameter as command
377 # avoid accepting e.g. style parameter as command
376 if util.safehasattr(webcommands, cmd):
378 if util.safehasattr(webcommands, cmd):
377 req.form['cmd'] = [cmd]
379 req.form['cmd'] = [cmd]
378
380
379 if cmd == 'static':
381 if cmd == 'static':
380 req.form['file'] = ['/'.join(args)]
382 req.form['file'] = ['/'.join(args)]
381 else:
383 else:
382 if args and args[0]:
384 if args and args[0]:
383 node = args.pop(0).replace('%2F', '/')
385 node = args.pop(0).replace('%2F', '/')
384 req.form['node'] = [node]
386 req.form['node'] = [node]
385 if args:
387 if args:
386 req.form['file'] = args
388 req.form['file'] = args
387
389
388 ua = req.env.get('HTTP_USER_AGENT', '')
390 ua = req.env.get('HTTP_USER_AGENT', '')
389 if cmd == 'rev' and 'mercurial' in ua:
391 if cmd == 'rev' and 'mercurial' in ua:
390 req.form['style'] = ['raw']
392 req.form['style'] = ['raw']
391
393
392 if cmd == 'archive':
394 if cmd == 'archive':
393 fn = req.form['node'][0]
395 fn = req.form['node'][0]
394 for type_, spec in rctx.archivespecs.iteritems():
396 for type_, spec in rctx.archivespecs.iteritems():
395 ext = spec[2]
397 ext = spec[2]
396 if fn.endswith(ext):
398 if fn.endswith(ext):
397 req.form['node'] = [fn[:-len(ext)]]
399 req.form['node'] = [fn[:-len(ext)]]
398 req.form['type'] = [type_]
400 req.form['type'] = [type_]
399
401
400 # process the web interface request
402 # process the web interface request
401
403
402 try:
404 try:
403 tmpl = rctx.templater(req)
405 tmpl = rctx.templater(req)
404 ctype = tmpl('mimetype', encoding=encoding.encoding)
406 ctype = tmpl('mimetype', encoding=encoding.encoding)
405 ctype = templater.stringify(ctype)
407 ctype = templater.stringify(ctype)
406
408
407 # check read permissions non-static content
409 # check read permissions non-static content
408 if cmd != 'static':
410 if cmd != 'static':
409 self.check_perm(rctx, req, None)
411 self.check_perm(rctx, req, None)
410
412
411 if cmd == '':
413 if cmd == '':
412 req.form['cmd'] = [tmpl.cache['default']]
414 req.form['cmd'] = [tmpl.cache['default']]
413 cmd = req.form['cmd'][0]
415 cmd = req.form['cmd'][0]
414
416
415 if rctx.configbool('web', 'cache', True):
417 if rctx.configbool('web', 'cache', True):
416 caching(self, req) # sets ETag header or raises NOT_MODIFIED
418 caching(self, req) # sets ETag header or raises NOT_MODIFIED
417 if cmd not in webcommands.__all__:
419 if cmd not in webcommands.__all__:
418 msg = 'no such method: %s' % cmd
420 msg = 'no such method: %s' % cmd
419 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
421 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
420 elif cmd == 'file' and 'raw' in req.form.get('style', []):
422 elif cmd == 'file' and 'raw' in req.form.get('style', []):
421 rctx.ctype = ctype
423 rctx.ctype = ctype
422 content = webcommands.rawfile(rctx, req, tmpl)
424 content = webcommands.rawfile(rctx, req, tmpl)
423 else:
425 else:
424 content = getattr(webcommands, cmd)(rctx, req, tmpl)
426 content = getattr(webcommands, cmd)(rctx, req, tmpl)
425 req.respond(HTTP_OK, ctype)
427 req.respond(HTTP_OK, ctype)
426
428
427 return content
429 return content
428
430
429 except (error.LookupError, error.RepoLookupError) as err:
431 except (error.LookupError, error.RepoLookupError) as err:
430 req.respond(HTTP_NOT_FOUND, ctype)
432 req.respond(HTTP_NOT_FOUND, ctype)
431 msg = str(err)
433 msg = str(err)
432 if (util.safehasattr(err, 'name') and
434 if (util.safehasattr(err, 'name') and
433 not isinstance(err, error.ManifestLookupError)):
435 not isinstance(err, error.ManifestLookupError)):
434 msg = 'revision not found: %s' % err.name
436 msg = 'revision not found: %s' % err.name
435 return tmpl('error', error=msg)
437 return tmpl('error', error=msg)
436 except (error.RepoError, error.RevlogError) as inst:
438 except (error.RepoError, error.RevlogError) as inst:
437 req.respond(HTTP_SERVER_ERROR, ctype)
439 req.respond(HTTP_SERVER_ERROR, ctype)
438 return tmpl('error', error=str(inst))
440 return tmpl('error', error=str(inst))
439 except ErrorResponse as inst:
441 except ErrorResponse as inst:
440 req.respond(inst, ctype)
442 req.respond(inst, ctype)
441 if inst.code == HTTP_NOT_MODIFIED:
443 if inst.code == HTTP_NOT_MODIFIED:
442 # Not allowed to return a body on a 304
444 # Not allowed to return a body on a 304
443 return ['']
445 return ['']
444 return tmpl('error', error=str(inst))
446 return tmpl('error', error=str(inst))
445
447
446 def check_perm(self, rctx, req, op):
448 def check_perm(self, rctx, req, op):
447 for permhook in permhooks:
449 for permhook in permhooks:
448 permhook(rctx, req, op)
450 permhook(rctx, req, op)
449
451
450 def getwebview(repo):
452 def getwebview(repo):
451 """The 'web.view' config controls changeset filter to hgweb. Possible
453 """The 'web.view' config controls changeset filter to hgweb. Possible
452 values are ``served``, ``visible`` and ``all``. Default is ``served``.
454 values are ``served``, ``visible`` and ``all``. Default is ``served``.
453 The ``served`` filter only shows changesets that can be pulled from the
455 The ``served`` filter only shows changesets that can be pulled from the
454 hgweb instance. The``visible`` filter includes secret changesets but
456 hgweb instance. The``visible`` filter includes secret changesets but
455 still excludes "hidden" one.
457 still excludes "hidden" one.
456
458
457 See the repoview module for details.
459 See the repoview module for details.
458
460
459 The option has been around undocumented since Mercurial 2.5, but no
461 The option has been around undocumented since Mercurial 2.5, but no
460 user ever asked about it. So we better keep it undocumented for now."""
462 user ever asked about it. So we better keep it undocumented for now."""
461 viewconfig = repo.ui.config('web', 'view', 'served',
463 viewconfig = repo.ui.config('web', 'view', 'served',
462 untrusted=True)
464 untrusted=True)
463 if viewconfig == 'all':
465 if viewconfig == 'all':
464 return repo.unfiltered()
466 return repo.unfiltered()
465 elif viewconfig in repoview.filtertable:
467 elif viewconfig in repoview.filtertable:
466 return repo.filtered(viewconfig)
468 return repo.filtered(viewconfig)
467 else:
469 else:
468 return repo.filtered('served')
470 return repo.filtered('served')
@@ -1,522 +1,522 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 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 os
11 import os
12 import re
12 import re
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_NOT_FOUND,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
21 HTTP_SERVER_ERROR,
22 get_contact,
22 get_contact,
23 get_mtime,
23 get_mtime,
24 ismember,
24 ismember,
25 paritygen,
25 paritygen,
26 staticfile,
26 staticfile,
27 )
27 )
28 from .request import wsgirequest
28 from .request import wsgirequest
29
29
30 from .. import (
30 from .. import (
31 encoding,
31 encoding,
32 error,
32 error,
33 hg,
33 hg,
34 profiling,
34 profiling,
35 scmutil,
35 scmutil,
36 templater,
36 templater,
37 ui as uimod,
37 ui as uimod,
38 util,
38 util,
39 )
39 )
40
40
41 from . import (
41 from . import (
42 hgweb_mod,
42 hgweb_mod,
43 webutil,
43 webutil,
44 wsgicgi,
44 wsgicgi,
45 )
45 )
46
46
47 def cleannames(items):
47 def cleannames(items):
48 return [(util.pconvert(name).strip('/'), path) for name, path in items]
48 return [(util.pconvert(name).strip('/'), path) for name, path in items]
49
49
50 def findrepos(paths):
50 def findrepos(paths):
51 repos = []
51 repos = []
52 for prefix, root in cleannames(paths):
52 for prefix, root in cleannames(paths):
53 roothead, roottail = os.path.split(root)
53 roothead, roottail = os.path.split(root)
54 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
54 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
55 # /bar/ be served as as foo/N .
55 # /bar/ be served as as foo/N .
56 # '*' will not search inside dirs with .hg (except .hg/patches),
56 # '*' will not search inside dirs with .hg (except .hg/patches),
57 # '**' will search inside dirs with .hg (and thus also find subrepos).
57 # '**' will search inside dirs with .hg (and thus also find subrepos).
58 try:
58 try:
59 recurse = {'*': False, '**': True}[roottail]
59 recurse = {'*': False, '**': True}[roottail]
60 except KeyError:
60 except KeyError:
61 repos.append((prefix, root))
61 repos.append((prefix, root))
62 continue
62 continue
63 roothead = os.path.normpath(os.path.abspath(roothead))
63 roothead = os.path.normpath(os.path.abspath(roothead))
64 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
64 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
65 repos.extend(urlrepos(prefix, roothead, paths))
65 repos.extend(urlrepos(prefix, roothead, paths))
66 return repos
66 return repos
67
67
68 def urlrepos(prefix, roothead, paths):
68 def urlrepos(prefix, roothead, paths):
69 """yield url paths and filesystem paths from a list of repo paths
69 """yield url paths and filesystem paths from a list of repo paths
70
70
71 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
71 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
72 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
72 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
73 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
73 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
74 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
74 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
75 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
75 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
76 """
76 """
77 for path in paths:
77 for path in paths:
78 path = os.path.normpath(path)
78 path = os.path.normpath(path)
79 yield (prefix + '/' +
79 yield (prefix + '/' +
80 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
80 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
81
81
82 def geturlcgivars(baseurl, port):
82 def geturlcgivars(baseurl, port):
83 """
83 """
84 Extract CGI variables from baseurl
84 Extract CGI variables from baseurl
85
85
86 >>> geturlcgivars("http://host.org/base", "80")
86 >>> geturlcgivars("http://host.org/base", "80")
87 ('host.org', '80', '/base')
87 ('host.org', '80', '/base')
88 >>> geturlcgivars("http://host.org:8000/base", "80")
88 >>> geturlcgivars("http://host.org:8000/base", "80")
89 ('host.org', '8000', '/base')
89 ('host.org', '8000', '/base')
90 >>> geturlcgivars('/base', 8000)
90 >>> geturlcgivars('/base', 8000)
91 ('', '8000', '/base')
91 ('', '8000', '/base')
92 >>> geturlcgivars("base", '8000')
92 >>> geturlcgivars("base", '8000')
93 ('', '8000', '/base')
93 ('', '8000', '/base')
94 >>> geturlcgivars("http://host", '8000')
94 >>> geturlcgivars("http://host", '8000')
95 ('host', '8000', '/')
95 ('host', '8000', '/')
96 >>> geturlcgivars("http://host/", '8000')
96 >>> geturlcgivars("http://host/", '8000')
97 ('host', '8000', '/')
97 ('host', '8000', '/')
98 """
98 """
99 u = util.url(baseurl)
99 u = util.url(baseurl)
100 name = u.host or ''
100 name = u.host or ''
101 if u.port:
101 if u.port:
102 port = u.port
102 port = u.port
103 path = u.path or ""
103 path = u.path or ""
104 if not path.startswith('/'):
104 if not path.startswith('/'):
105 path = '/' + path
105 path = '/' + path
106
106
107 return name, str(port), path
107 return name, str(port), path
108
108
109 class hgwebdir(object):
109 class hgwebdir(object):
110 """HTTP server for multiple repositories.
110 """HTTP server for multiple repositories.
111
111
112 Given a configuration, different repositories will be served depending
112 Given a configuration, different repositories will be served depending
113 on the request path.
113 on the request path.
114
114
115 Instances are typically used as WSGI applications.
115 Instances are typically used as WSGI applications.
116 """
116 """
117 def __init__(self, conf, baseui=None):
117 def __init__(self, conf, baseui=None):
118 self.conf = conf
118 self.conf = conf
119 self.baseui = baseui
119 self.baseui = baseui
120 self.ui = None
120 self.ui = None
121 self.lastrefresh = 0
121 self.lastrefresh = 0
122 self.motd = None
122 self.motd = None
123 self.refresh()
123 self.refresh()
124
124
125 def refresh(self):
125 def refresh(self):
126 refreshinterval = 20
126 refreshinterval = 20
127 if self.ui:
127 if self.ui:
128 refreshinterval = self.ui.configint('web', 'refreshinterval',
128 refreshinterval = self.ui.configint('web', 'refreshinterval',
129 refreshinterval)
129 refreshinterval)
130
130
131 # refreshinterval <= 0 means to always refresh.
131 # refreshinterval <= 0 means to always refresh.
132 if (refreshinterval > 0 and
132 if (refreshinterval > 0 and
133 self.lastrefresh + refreshinterval > time.time()):
133 self.lastrefresh + refreshinterval > time.time()):
134 return
134 return
135
135
136 if self.baseui:
136 if self.baseui:
137 u = self.baseui.copy()
137 u = self.baseui.copy()
138 else:
138 else:
139 u = uimod.ui.load()
139 u = uimod.ui.load()
140 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
140 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
141 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
141 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
142 # displaying bundling progress bar while serving feels wrong and may
142 # displaying bundling progress bar while serving feels wrong and may
143 # break some wsgi implementations.
143 # break some wsgi implementations.
144 u.setconfig('progress', 'disable', 'true', 'hgweb')
144 u.setconfig('progress', 'disable', 'true', 'hgweb')
145
145
146 if not isinstance(self.conf, (dict, list, tuple)):
146 if not isinstance(self.conf, (dict, list, tuple)):
147 map = {'paths': 'hgweb-paths'}
147 map = {'paths': 'hgweb-paths'}
148 if not os.path.exists(self.conf):
148 if not os.path.exists(self.conf):
149 raise error.Abort(_('config file %s not found!') % self.conf)
149 raise error.Abort(_('config file %s not found!') % self.conf)
150 u.readconfig(self.conf, remap=map, trust=True)
150 u.readconfig(self.conf, remap=map, trust=True)
151 paths = []
151 paths = []
152 for name, ignored in u.configitems('hgweb-paths'):
152 for name, ignored in u.configitems('hgweb-paths'):
153 for path in u.configlist('hgweb-paths', name):
153 for path in u.configlist('hgweb-paths', name):
154 paths.append((name, path))
154 paths.append((name, path))
155 elif isinstance(self.conf, (list, tuple)):
155 elif isinstance(self.conf, (list, tuple)):
156 paths = self.conf
156 paths = self.conf
157 elif isinstance(self.conf, dict):
157 elif isinstance(self.conf, dict):
158 paths = self.conf.items()
158 paths = self.conf.items()
159
159
160 repos = findrepos(paths)
160 repos = findrepos(paths)
161 for prefix, root in u.configitems('collections'):
161 for prefix, root in u.configitems('collections'):
162 prefix = util.pconvert(prefix)
162 prefix = util.pconvert(prefix)
163 for path in scmutil.walkrepos(root, followsym=True):
163 for path in scmutil.walkrepos(root, followsym=True):
164 repo = os.path.normpath(path)
164 repo = os.path.normpath(path)
165 name = util.pconvert(repo)
165 name = util.pconvert(repo)
166 if name.startswith(prefix):
166 if name.startswith(prefix):
167 name = name[len(prefix):]
167 name = name[len(prefix):]
168 repos.append((name.lstrip('/'), repo))
168 repos.append((name.lstrip('/'), repo))
169
169
170 self.repos = repos
170 self.repos = repos
171 self.ui = u
171 self.ui = u
172 encoding.encoding = self.ui.config('web', 'encoding',
172 encoding.encoding = self.ui.config('web', 'encoding',
173 encoding.encoding)
173 encoding.encoding)
174 self.style = self.ui.config('web', 'style', 'paper')
174 self.style = self.ui.config('web', 'style', 'paper')
175 self.templatepath = self.ui.config('web', 'templates', None)
175 self.templatepath = self.ui.config('web', 'templates', None)
176 self.stripecount = self.ui.config('web', 'stripes', 1)
176 self.stripecount = self.ui.config('web', 'stripes', 1)
177 if self.stripecount:
177 if self.stripecount:
178 self.stripecount = int(self.stripecount)
178 self.stripecount = int(self.stripecount)
179 self._baseurl = self.ui.config('web', 'baseurl')
179 self._baseurl = self.ui.config('web', 'baseurl')
180 prefix = self.ui.config('web', 'prefix', '')
180 prefix = self.ui.config('web', 'prefix', '')
181 if prefix.startswith('/'):
181 if prefix.startswith('/'):
182 prefix = prefix[1:]
182 prefix = prefix[1:]
183 if prefix.endswith('/'):
183 if prefix.endswith('/'):
184 prefix = prefix[:-1]
184 prefix = prefix[:-1]
185 self.prefix = prefix
185 self.prefix = prefix
186 self.lastrefresh = time.time()
186 self.lastrefresh = time.time()
187
187
188 def run(self):
188 def run(self):
189 if not encoding.environ.get('GATEWAY_INTERFACE',
189 if not encoding.environ.get('GATEWAY_INTERFACE',
190 '').startswith("CGI/1."):
190 '').startswith("CGI/1."):
191 raise RuntimeError("This function is only intended to be "
191 raise RuntimeError("This function is only intended to be "
192 "called while running as a CGI script.")
192 "called while running as a CGI script.")
193 wsgicgi.launch(self)
193 wsgicgi.launch(self)
194
194
195 def __call__(self, env, respond):
195 def __call__(self, env, respond):
196 req = wsgirequest(env, respond)
196 req = wsgirequest(env, respond)
197 return self.run_wsgi(req)
197 return self.run_wsgi(req)
198
198
199 def read_allowed(self, ui, req):
199 def read_allowed(self, ui, req):
200 """Check allow_read and deny_read config options of a repo's ui object
200 """Check allow_read and deny_read config options of a repo's ui object
201 to determine user permissions. By default, with neither option set (or
201 to determine user permissions. By default, with neither option set (or
202 both empty), allow all users to read the repo. There are two ways a
202 both empty), allow all users to read the repo. There are two ways a
203 user can be denied read access: (1) deny_read is not empty, and the
203 user can be denied read access: (1) deny_read is not empty, and the
204 user is unauthenticated or deny_read contains user (or *), and (2)
204 user is unauthenticated or deny_read contains user (or *), and (2)
205 allow_read is not empty and the user is not in allow_read. Return True
205 allow_read is not empty and the user is not in allow_read. Return True
206 if user is allowed to read the repo, else return False."""
206 if user is allowed to read the repo, else return False."""
207
207
208 user = req.env.get('REMOTE_USER')
208 user = req.env.get('REMOTE_USER')
209
209
210 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
210 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
211 if deny_read and (not user or ismember(ui, user, deny_read)):
211 if deny_read and (not user or ismember(ui, user, deny_read)):
212 return False
212 return False
213
213
214 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
214 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
215 # by default, allow reading if no allow_read option has been set
215 # by default, allow reading if no allow_read option has been set
216 if (not allow_read) or ismember(ui, user, allow_read):
216 if (not allow_read) or ismember(ui, user, allow_read):
217 return True
217 return True
218
218
219 return False
219 return False
220
220
221 def run_wsgi(self, req):
221 def run_wsgi(self, req):
222 with profiling.maybeprofile(self.ui):
222 with profiling.maybeprofile(self.ui):
223 for r in self._runwsgi(req):
223 for r in self._runwsgi(req):
224 yield r
224 yield r
225
225
226 def _runwsgi(self, req):
226 def _runwsgi(self, req):
227 try:
227 try:
228 self.refresh()
228 self.refresh()
229
229
230 virtual = req.env.get("PATH_INFO", "").strip('/')
230 virtual = req.env.get("PATH_INFO", "").strip('/')
231 tmpl = self.templater(req)
231 tmpl = self.templater(req)
232 ctype = tmpl('mimetype', encoding=encoding.encoding)
232 ctype = tmpl('mimetype', encoding=encoding.encoding)
233 ctype = templater.stringify(ctype)
233 ctype = templater.stringify(ctype)
234
234
235 # a static file
235 # a static file
236 if virtual.startswith('static/') or 'static' in req.form:
236 if virtual.startswith('static/') or 'static' in req.form:
237 if virtual.startswith('static/'):
237 if virtual.startswith('static/'):
238 fname = virtual[7:]
238 fname = virtual[7:]
239 else:
239 else:
240 fname = req.form['static'][0]
240 fname = req.form['static'][0]
241 static = self.ui.config("web", "static", None,
241 static = self.ui.config("web", "static", None,
242 untrusted=False)
242 untrusted=False)
243 if not static:
243 if not static:
244 tp = self.templatepath or templater.templatepaths()
244 tp = self.templatepath or templater.templatepaths()
245 if isinstance(tp, str):
245 if isinstance(tp, str):
246 tp = [tp]
246 tp = [tp]
247 static = [os.path.join(p, 'static') for p in tp]
247 static = [os.path.join(p, 'static') for p in tp]
248 staticfile(static, fname, req)
248 staticfile(static, fname, req)
249 return []
249 return []
250
250
251 # top-level index
251 # top-level index
252 elif not virtual:
252 elif not virtual:
253 req.respond(HTTP_OK, ctype)
253 req.respond(HTTP_OK, ctype)
254 return self.makeindex(req, tmpl)
254 return self.makeindex(req, tmpl)
255
255
256 # nested indexes and hgwebs
256 # nested indexes and hgwebs
257
257
258 repos = dict(self.repos)
258 repos = dict(self.repos)
259 virtualrepo = virtual
259 virtualrepo = virtual
260 while virtualrepo:
260 while virtualrepo:
261 real = repos.get(virtualrepo)
261 real = repos.get(virtualrepo)
262 if real:
262 if real:
263 req.env['REPO_NAME'] = virtualrepo
263 req.env['REPO_NAME'] = virtualrepo
264 try:
264 try:
265 # ensure caller gets private copy of ui
265 # ensure caller gets private copy of ui
266 repo = hg.repository(self.ui.copy(), real)
266 repo = hg.repository(self.ui.copy(), real)
267 return hgweb_mod.hgweb(repo).run_wsgi(req)
267 return hgweb_mod.hgweb(repo).run_wsgi(req)
268 except IOError as inst:
268 except IOError as inst:
269 msg = inst.strerror
269 msg = inst.strerror
270 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
270 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
271 except error.RepoError as inst:
271 except error.RepoError as inst:
272 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
272 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
273
273
274 up = virtualrepo.rfind('/')
274 up = virtualrepo.rfind('/')
275 if up < 0:
275 if up < 0:
276 break
276 break
277 virtualrepo = virtualrepo[:up]
277 virtualrepo = virtualrepo[:up]
278
278
279 # browse subdirectories
279 # browse subdirectories
280 subdir = virtual + '/'
280 subdir = virtual + '/'
281 if [r for r in repos if r.startswith(subdir)]:
281 if [r for r in repos if r.startswith(subdir)]:
282 req.respond(HTTP_OK, ctype)
282 req.respond(HTTP_OK, ctype)
283 return self.makeindex(req, tmpl, subdir)
283 return self.makeindex(req, tmpl, subdir)
284
284
285 # prefixes not found
285 # prefixes not found
286 req.respond(HTTP_NOT_FOUND, ctype)
286 req.respond(HTTP_NOT_FOUND, ctype)
287 return tmpl("notfound", repo=virtual)
287 return tmpl("notfound", repo=virtual)
288
288
289 except ErrorResponse as err:
289 except ErrorResponse as err:
290 req.respond(err, ctype)
290 req.respond(err, ctype)
291 return tmpl('error', error=err.message or '')
291 return tmpl('error', error=err.message or '')
292 finally:
292 finally:
293 tmpl = None
293 tmpl = None
294
294
295 def makeindex(self, req, tmpl, subdir=""):
295 def makeindex(self, req, tmpl, subdir=""):
296
296
297 def archivelist(ui, nodeid, url):
297 def archivelist(ui, nodeid, url):
298 allowed = ui.configlist("web", "allow_archive", untrusted=True)
298 allowed = ui.configlist("web", "allow_archive", untrusted=True)
299 archives = []
299 archives = []
300 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
300 for typ, spec in hgweb_mod.archivespecs.iteritems():
301 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
301 if typ in allowed or ui.configbool("web", "allow" + typ,
302 untrusted=True):
302 untrusted=True):
303 archives.append({"type" : i[0], "extension": i[1],
303 archives.append({"type" : typ, "extension": spec[2],
304 "node": nodeid, "url": url})
304 "node": nodeid, "url": url})
305 return archives
305 return archives
306
306
307 def rawentries(subdir="", **map):
307 def rawentries(subdir="", **map):
308
308
309 descend = self.ui.configbool('web', 'descend', True)
309 descend = self.ui.configbool('web', 'descend', True)
310 collapse = self.ui.configbool('web', 'collapse', False)
310 collapse = self.ui.configbool('web', 'collapse', False)
311 seenrepos = set()
311 seenrepos = set()
312 seendirs = set()
312 seendirs = set()
313 for name, path in self.repos:
313 for name, path in self.repos:
314
314
315 if not name.startswith(subdir):
315 if not name.startswith(subdir):
316 continue
316 continue
317 name = name[len(subdir):]
317 name = name[len(subdir):]
318 directory = False
318 directory = False
319
319
320 if '/' in name:
320 if '/' in name:
321 if not descend:
321 if not descend:
322 continue
322 continue
323
323
324 nameparts = name.split('/')
324 nameparts = name.split('/')
325 rootname = nameparts[0]
325 rootname = nameparts[0]
326
326
327 if not collapse:
327 if not collapse:
328 pass
328 pass
329 elif rootname in seendirs:
329 elif rootname in seendirs:
330 continue
330 continue
331 elif rootname in seenrepos:
331 elif rootname in seenrepos:
332 pass
332 pass
333 else:
333 else:
334 directory = True
334 directory = True
335 name = rootname
335 name = rootname
336
336
337 # redefine the path to refer to the directory
337 # redefine the path to refer to the directory
338 discarded = '/'.join(nameparts[1:])
338 discarded = '/'.join(nameparts[1:])
339
339
340 # remove name parts plus accompanying slash
340 # remove name parts plus accompanying slash
341 path = path[:-len(discarded) - 1]
341 path = path[:-len(discarded) - 1]
342
342
343 try:
343 try:
344 r = hg.repository(self.ui, path)
344 r = hg.repository(self.ui, path)
345 directory = False
345 directory = False
346 except (IOError, error.RepoError):
346 except (IOError, error.RepoError):
347 pass
347 pass
348
348
349 parts = [name]
349 parts = [name]
350 if 'PATH_INFO' in req.env:
350 if 'PATH_INFO' in req.env:
351 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
351 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
352 if req.env['SCRIPT_NAME']:
352 if req.env['SCRIPT_NAME']:
353 parts.insert(0, req.env['SCRIPT_NAME'])
353 parts.insert(0, req.env['SCRIPT_NAME'])
354 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
354 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
355
355
356 # show either a directory entry or a repository
356 # show either a directory entry or a repository
357 if directory:
357 if directory:
358 # get the directory's time information
358 # get the directory's time information
359 try:
359 try:
360 d = (get_mtime(path), util.makedate()[1])
360 d = (get_mtime(path), util.makedate()[1])
361 except OSError:
361 except OSError:
362 continue
362 continue
363
363
364 # add '/' to the name to make it obvious that
364 # add '/' to the name to make it obvious that
365 # the entry is a directory, not a regular repository
365 # the entry is a directory, not a regular repository
366 row = {'contact': "",
366 row = {'contact': "",
367 'contact_sort': "",
367 'contact_sort': "",
368 'name': name + '/',
368 'name': name + '/',
369 'name_sort': name,
369 'name_sort': name,
370 'url': url,
370 'url': url,
371 'description': "",
371 'description': "",
372 'description_sort': "",
372 'description_sort': "",
373 'lastchange': d,
373 'lastchange': d,
374 'lastchange_sort': d[1]-d[0],
374 'lastchange_sort': d[1]-d[0],
375 'archives': [],
375 'archives': [],
376 'isdirectory': True,
376 'isdirectory': True,
377 'labels': [],
377 'labels': [],
378 }
378 }
379
379
380 seendirs.add(name)
380 seendirs.add(name)
381 yield row
381 yield row
382 continue
382 continue
383
383
384 u = self.ui.copy()
384 u = self.ui.copy()
385 try:
385 try:
386 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
386 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
387 except Exception as e:
387 except Exception as e:
388 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
388 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
389 continue
389 continue
390 def get(section, name, default=None):
390 def get(section, name, default=None):
391 return u.config(section, name, default, untrusted=True)
391 return u.config(section, name, default, untrusted=True)
392
392
393 if u.configbool("web", "hidden", untrusted=True):
393 if u.configbool("web", "hidden", untrusted=True):
394 continue
394 continue
395
395
396 if not self.read_allowed(u, req):
396 if not self.read_allowed(u, req):
397 continue
397 continue
398
398
399 # update time with local timezone
399 # update time with local timezone
400 try:
400 try:
401 r = hg.repository(self.ui, path)
401 r = hg.repository(self.ui, path)
402 except IOError:
402 except IOError:
403 u.warn(_('error accessing repository at %s\n') % path)
403 u.warn(_('error accessing repository at %s\n') % path)
404 continue
404 continue
405 except error.RepoError:
405 except error.RepoError:
406 u.warn(_('error accessing repository at %s\n') % path)
406 u.warn(_('error accessing repository at %s\n') % path)
407 continue
407 continue
408 try:
408 try:
409 d = (get_mtime(r.spath), util.makedate()[1])
409 d = (get_mtime(r.spath), util.makedate()[1])
410 except OSError:
410 except OSError:
411 continue
411 continue
412
412
413 contact = get_contact(get)
413 contact = get_contact(get)
414 description = get("web", "description", "")
414 description = get("web", "description", "")
415 seenrepos.add(name)
415 seenrepos.add(name)
416 name = get("web", "name", name)
416 name = get("web", "name", name)
417 row = {'contact': contact or "unknown",
417 row = {'contact': contact or "unknown",
418 'contact_sort': contact.upper() or "unknown",
418 'contact_sort': contact.upper() or "unknown",
419 'name': name,
419 'name': name,
420 'name_sort': name,
420 'name_sort': name,
421 'url': url,
421 'url': url,
422 'description': description or "unknown",
422 'description': description or "unknown",
423 'description_sort': description.upper() or "unknown",
423 'description_sort': description.upper() or "unknown",
424 'lastchange': d,
424 'lastchange': d,
425 'lastchange_sort': d[1]-d[0],
425 'lastchange_sort': d[1]-d[0],
426 'archives': archivelist(u, "tip", url),
426 'archives': archivelist(u, "tip", url),
427 'isdirectory': None,
427 'isdirectory': None,
428 'labels': u.configlist('web', 'labels', untrusted=True),
428 'labels': u.configlist('web', 'labels', untrusted=True),
429 }
429 }
430
430
431 yield row
431 yield row
432
432
433 sortdefault = None, False
433 sortdefault = None, False
434 def entries(sortcolumn="", descending=False, subdir="", **map):
434 def entries(sortcolumn="", descending=False, subdir="", **map):
435 rows = rawentries(subdir=subdir, **map)
435 rows = rawentries(subdir=subdir, **map)
436
436
437 if sortcolumn and sortdefault != (sortcolumn, descending):
437 if sortcolumn and sortdefault != (sortcolumn, descending):
438 sortkey = '%s_sort' % sortcolumn
438 sortkey = '%s_sort' % sortcolumn
439 rows = sorted(rows, key=lambda x: x[sortkey],
439 rows = sorted(rows, key=lambda x: x[sortkey],
440 reverse=descending)
440 reverse=descending)
441 for row, parity in zip(rows, paritygen(self.stripecount)):
441 for row, parity in zip(rows, paritygen(self.stripecount)):
442 row['parity'] = parity
442 row['parity'] = parity
443 yield row
443 yield row
444
444
445 self.refresh()
445 self.refresh()
446 sortable = ["name", "description", "contact", "lastchange"]
446 sortable = ["name", "description", "contact", "lastchange"]
447 sortcolumn, descending = sortdefault
447 sortcolumn, descending = sortdefault
448 if 'sort' in req.form:
448 if 'sort' in req.form:
449 sortcolumn = req.form['sort'][0]
449 sortcolumn = req.form['sort'][0]
450 descending = sortcolumn.startswith('-')
450 descending = sortcolumn.startswith('-')
451 if descending:
451 if descending:
452 sortcolumn = sortcolumn[1:]
452 sortcolumn = sortcolumn[1:]
453 if sortcolumn not in sortable:
453 if sortcolumn not in sortable:
454 sortcolumn = ""
454 sortcolumn = ""
455
455
456 sort = [("sort_%s" % column,
456 sort = [("sort_%s" % column,
457 "%s%s" % ((not descending and column == sortcolumn)
457 "%s%s" % ((not descending and column == sortcolumn)
458 and "-" or "", column))
458 and "-" or "", column))
459 for column in sortable]
459 for column in sortable]
460
460
461 self.refresh()
461 self.refresh()
462 self.updatereqenv(req.env)
462 self.updatereqenv(req.env)
463
463
464 return tmpl("index", entries=entries, subdir=subdir,
464 return tmpl("index", entries=entries, subdir=subdir,
465 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
465 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
466 sortcolumn=sortcolumn, descending=descending,
466 sortcolumn=sortcolumn, descending=descending,
467 **dict(sort))
467 **dict(sort))
468
468
469 def templater(self, req):
469 def templater(self, req):
470
470
471 def motd(**map):
471 def motd(**map):
472 if self.motd is not None:
472 if self.motd is not None:
473 yield self.motd
473 yield self.motd
474 else:
474 else:
475 yield config('web', 'motd', '')
475 yield config('web', 'motd', '')
476
476
477 def config(section, name, default=None, untrusted=True):
477 def config(section, name, default=None, untrusted=True):
478 return self.ui.config(section, name, default, untrusted)
478 return self.ui.config(section, name, default, untrusted)
479
479
480 self.updatereqenv(req.env)
480 self.updatereqenv(req.env)
481
481
482 url = req.env.get('SCRIPT_NAME', '')
482 url = req.env.get('SCRIPT_NAME', '')
483 if not url.endswith('/'):
483 if not url.endswith('/'):
484 url += '/'
484 url += '/'
485
485
486 vars = {}
486 vars = {}
487 styles = (
487 styles = (
488 req.form.get('style', [None])[0],
488 req.form.get('style', [None])[0],
489 config('web', 'style'),
489 config('web', 'style'),
490 'paper'
490 'paper'
491 )
491 )
492 style, mapfile = templater.stylemap(styles, self.templatepath)
492 style, mapfile = templater.stylemap(styles, self.templatepath)
493 if style == styles[0]:
493 if style == styles[0]:
494 vars['style'] = style
494 vars['style'] = style
495
495
496 start = url[-1] == '?' and '&' or '?'
496 start = url[-1] == '?' and '&' or '?'
497 sessionvars = webutil.sessionvars(vars, start)
497 sessionvars = webutil.sessionvars(vars, start)
498 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
498 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
499 logoimg = config('web', 'logoimg', 'hglogo.png')
499 logoimg = config('web', 'logoimg', 'hglogo.png')
500 staticurl = config('web', 'staticurl') or url + 'static/'
500 staticurl = config('web', 'staticurl') or url + 'static/'
501 if not staticurl.endswith('/'):
501 if not staticurl.endswith('/'):
502 staticurl += '/'
502 staticurl += '/'
503
503
504 defaults = {
504 defaults = {
505 "encoding": encoding.encoding,
505 "encoding": encoding.encoding,
506 "motd": motd,
506 "motd": motd,
507 "url": url,
507 "url": url,
508 "logourl": logourl,
508 "logourl": logourl,
509 "logoimg": logoimg,
509 "logoimg": logoimg,
510 "staticurl": staticurl,
510 "staticurl": staticurl,
511 "sessionvars": sessionvars,
511 "sessionvars": sessionvars,
512 "style": style,
512 "style": style,
513 }
513 }
514 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
514 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
515 return tmpl
515 return tmpl
516
516
517 def updatereqenv(self, env):
517 def updatereqenv(self, env):
518 if self._baseurl is not None:
518 if self._baseurl is not None:
519 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
519 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
520 env['SERVER_NAME'] = name
520 env['SERVER_NAME'] = name
521 env['SERVER_PORT'] = port
521 env['SERVER_PORT'] = port
522 env['SCRIPT_NAME'] = path
522 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now