##// END OF EJS Templates
windows: use abspath in hgwebdir...
marmoute -
r48427:aceede7c default
parent child Browse files
Show More
@@ -1,577 +1,577 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 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005, 2006 Olivia Mackall <olivia@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 gc
11 import gc
12 import os
12 import os
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_SERVER_ERROR,
19 HTTP_SERVER_ERROR,
20 cspvalues,
20 cspvalues,
21 get_contact,
21 get_contact,
22 get_mtime,
22 get_mtime,
23 ismember,
23 ismember,
24 paritygen,
24 paritygen,
25 staticfile,
25 staticfile,
26 statusmessage,
26 statusmessage,
27 )
27 )
28
28
29 from .. import (
29 from .. import (
30 configitems,
30 configitems,
31 encoding,
31 encoding,
32 error,
32 error,
33 extensions,
33 extensions,
34 hg,
34 hg,
35 pathutil,
35 pathutil,
36 profiling,
36 profiling,
37 pycompat,
37 pycompat,
38 rcutil,
38 rcutil,
39 registrar,
39 registrar,
40 scmutil,
40 scmutil,
41 templater,
41 templater,
42 templateutil,
42 templateutil,
43 ui as uimod,
43 ui as uimod,
44 util,
44 util,
45 )
45 )
46
46
47 from . import (
47 from . import (
48 hgweb_mod,
48 hgweb_mod,
49 request as requestmod,
49 request as requestmod,
50 webutil,
50 webutil,
51 wsgicgi,
51 wsgicgi,
52 )
52 )
53 from ..utils import dateutil
53 from ..utils import dateutil
54
54
55
55
56 def cleannames(items):
56 def cleannames(items):
57 return [(util.pconvert(name).strip(b'/'), path) for name, path in items]
57 return [(util.pconvert(name).strip(b'/'), path) for name, path in items]
58
58
59
59
60 def findrepos(paths):
60 def findrepos(paths):
61 repos = []
61 repos = []
62 for prefix, root in cleannames(paths):
62 for prefix, root in cleannames(paths):
63 roothead, roottail = os.path.split(root)
63 roothead, roottail = os.path.split(root)
64 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
64 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
65 # /bar/ be served as as foo/N .
65 # /bar/ be served as as foo/N .
66 # '*' will not search inside dirs with .hg (except .hg/patches),
66 # '*' will not search inside dirs with .hg (except .hg/patches),
67 # '**' will search inside dirs with .hg (and thus also find subrepos).
67 # '**' will search inside dirs with .hg (and thus also find subrepos).
68 try:
68 try:
69 recurse = {b'*': False, b'**': True}[roottail]
69 recurse = {b'*': False, b'**': True}[roottail]
70 except KeyError:
70 except KeyError:
71 repos.append((prefix, root))
71 repos.append((prefix, root))
72 continue
72 continue
73 roothead = os.path.normpath(os.path.abspath(roothead))
73 roothead = os.path.normpath(util.abspath(roothead))
74 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
74 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
75 repos.extend(urlrepos(prefix, roothead, paths))
75 repos.extend(urlrepos(prefix, roothead, paths))
76 return repos
76 return repos
77
77
78
78
79 def urlrepos(prefix, roothead, paths):
79 def urlrepos(prefix, roothead, paths):
80 """yield url paths and filesystem paths from a list of repo paths
80 """yield url paths and filesystem paths from a list of repo paths
81
81
82 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
82 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
83 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
83 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
84 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
84 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
85 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
85 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
86 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
86 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
87 """
87 """
88 for path in paths:
88 for path in paths:
89 path = os.path.normpath(path)
89 path = os.path.normpath(path)
90 yield (
90 yield (
91 prefix + b'/' + util.pconvert(path[len(roothead) :]).lstrip(b'/')
91 prefix + b'/' + util.pconvert(path[len(roothead) :]).lstrip(b'/')
92 ).strip(b'/'), path
92 ).strip(b'/'), path
93
93
94
94
95 def readallowed(ui, req):
95 def readallowed(ui, req):
96 """Check allow_read and deny_read config options of a repo's ui object
96 """Check allow_read and deny_read config options of a repo's ui object
97 to determine user permissions. By default, with neither option set (or
97 to determine user permissions. By default, with neither option set (or
98 both empty), allow all users to read the repo. There are two ways a
98 both empty), allow all users to read the repo. There are two ways a
99 user can be denied read access: (1) deny_read is not empty, and the
99 user can be denied read access: (1) deny_read is not empty, and the
100 user is unauthenticated or deny_read contains user (or *), and (2)
100 user is unauthenticated or deny_read contains user (or *), and (2)
101 allow_read is not empty and the user is not in allow_read. Return True
101 allow_read is not empty and the user is not in allow_read. Return True
102 if user is allowed to read the repo, else return False."""
102 if user is allowed to read the repo, else return False."""
103
103
104 user = req.remoteuser
104 user = req.remoteuser
105
105
106 deny_read = ui.configlist(b'web', b'deny_read', untrusted=True)
106 deny_read = ui.configlist(b'web', b'deny_read', untrusted=True)
107 if deny_read and (not user or ismember(ui, user, deny_read)):
107 if deny_read and (not user or ismember(ui, user, deny_read)):
108 return False
108 return False
109
109
110 allow_read = ui.configlist(b'web', b'allow_read', untrusted=True)
110 allow_read = ui.configlist(b'web', b'allow_read', untrusted=True)
111 # by default, allow reading if no allow_read option has been set
111 # by default, allow reading if no allow_read option has been set
112 if not allow_read or ismember(ui, user, allow_read):
112 if not allow_read or ismember(ui, user, allow_read):
113 return True
113 return True
114
114
115 return False
115 return False
116
116
117
117
118 def rawindexentries(ui, repos, req, subdir=b''):
118 def rawindexentries(ui, repos, req, subdir=b''):
119 descend = ui.configbool(b'web', b'descend')
119 descend = ui.configbool(b'web', b'descend')
120 collapse = ui.configbool(b'web', b'collapse')
120 collapse = ui.configbool(b'web', b'collapse')
121 seenrepos = set()
121 seenrepos = set()
122 seendirs = set()
122 seendirs = set()
123 for name, path in repos:
123 for name, path in repos:
124
124
125 if not name.startswith(subdir):
125 if not name.startswith(subdir):
126 continue
126 continue
127 name = name[len(subdir) :]
127 name = name[len(subdir) :]
128 directory = False
128 directory = False
129
129
130 if b'/' in name:
130 if b'/' in name:
131 if not descend:
131 if not descend:
132 continue
132 continue
133
133
134 nameparts = name.split(b'/')
134 nameparts = name.split(b'/')
135 rootname = nameparts[0]
135 rootname = nameparts[0]
136
136
137 if not collapse:
137 if not collapse:
138 pass
138 pass
139 elif rootname in seendirs:
139 elif rootname in seendirs:
140 continue
140 continue
141 elif rootname in seenrepos:
141 elif rootname in seenrepos:
142 pass
142 pass
143 else:
143 else:
144 directory = True
144 directory = True
145 name = rootname
145 name = rootname
146
146
147 # redefine the path to refer to the directory
147 # redefine the path to refer to the directory
148 discarded = b'/'.join(nameparts[1:])
148 discarded = b'/'.join(nameparts[1:])
149
149
150 # remove name parts plus accompanying slash
150 # remove name parts plus accompanying slash
151 path = path[: -len(discarded) - 1]
151 path = path[: -len(discarded) - 1]
152
152
153 try:
153 try:
154 hg.repository(ui, path)
154 hg.repository(ui, path)
155 directory = False
155 directory = False
156 except (IOError, error.RepoError):
156 except (IOError, error.RepoError):
157 pass
157 pass
158
158
159 parts = [
159 parts = [
160 req.apppath.strip(b'/'),
160 req.apppath.strip(b'/'),
161 subdir.strip(b'/'),
161 subdir.strip(b'/'),
162 name.strip(b'/'),
162 name.strip(b'/'),
163 ]
163 ]
164 url = b'/' + b'/'.join(p for p in parts if p) + b'/'
164 url = b'/' + b'/'.join(p for p in parts if p) + b'/'
165
165
166 # show either a directory entry or a repository
166 # show either a directory entry or a repository
167 if directory:
167 if directory:
168 # get the directory's time information
168 # get the directory's time information
169 try:
169 try:
170 d = (get_mtime(path), dateutil.makedate()[1])
170 d = (get_mtime(path), dateutil.makedate()[1])
171 except OSError:
171 except OSError:
172 continue
172 continue
173
173
174 # add '/' to the name to make it obvious that
174 # add '/' to the name to make it obvious that
175 # the entry is a directory, not a regular repository
175 # the entry is a directory, not a regular repository
176 row = {
176 row = {
177 b'contact': b"",
177 b'contact': b"",
178 b'contact_sort': b"",
178 b'contact_sort': b"",
179 b'name': name + b'/',
179 b'name': name + b'/',
180 b'name_sort': name,
180 b'name_sort': name,
181 b'url': url,
181 b'url': url,
182 b'description': b"",
182 b'description': b"",
183 b'description_sort': b"",
183 b'description_sort': b"",
184 b'lastchange': d,
184 b'lastchange': d,
185 b'lastchange_sort': d[1] - d[0],
185 b'lastchange_sort': d[1] - d[0],
186 b'archives': templateutil.mappinglist([]),
186 b'archives': templateutil.mappinglist([]),
187 b'isdirectory': True,
187 b'isdirectory': True,
188 b'labels': templateutil.hybridlist([], name=b'label'),
188 b'labels': templateutil.hybridlist([], name=b'label'),
189 }
189 }
190
190
191 seendirs.add(name)
191 seendirs.add(name)
192 yield row
192 yield row
193 continue
193 continue
194
194
195 u = ui.copy()
195 u = ui.copy()
196 if rcutil.use_repo_hgrc():
196 if rcutil.use_repo_hgrc():
197 try:
197 try:
198 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
198 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
199 except Exception as e:
199 except Exception as e:
200 u.warn(_(b'error reading %s/.hg/hgrc: %s\n') % (path, e))
200 u.warn(_(b'error reading %s/.hg/hgrc: %s\n') % (path, e))
201 continue
201 continue
202
202
203 def get(section, name, default=uimod._unset):
203 def get(section, name, default=uimod._unset):
204 return u.config(section, name, default, untrusted=True)
204 return u.config(section, name, default, untrusted=True)
205
205
206 if u.configbool(b"web", b"hidden", untrusted=True):
206 if u.configbool(b"web", b"hidden", untrusted=True):
207 continue
207 continue
208
208
209 if not readallowed(u, req):
209 if not readallowed(u, req):
210 continue
210 continue
211
211
212 # update time with local timezone
212 # update time with local timezone
213 try:
213 try:
214 r = hg.repository(ui, path)
214 r = hg.repository(ui, path)
215 except IOError:
215 except IOError:
216 u.warn(_(b'error accessing repository at %s\n') % path)
216 u.warn(_(b'error accessing repository at %s\n') % path)
217 continue
217 continue
218 except error.RepoError:
218 except error.RepoError:
219 u.warn(_(b'error accessing repository at %s\n') % path)
219 u.warn(_(b'error accessing repository at %s\n') % path)
220 continue
220 continue
221 try:
221 try:
222 d = (get_mtime(r.spath), dateutil.makedate()[1])
222 d = (get_mtime(r.spath), dateutil.makedate()[1])
223 except OSError:
223 except OSError:
224 continue
224 continue
225
225
226 contact = get_contact(get)
226 contact = get_contact(get)
227 description = get(b"web", b"description")
227 description = get(b"web", b"description")
228 seenrepos.add(name)
228 seenrepos.add(name)
229 name = get(b"web", b"name", name)
229 name = get(b"web", b"name", name)
230 labels = u.configlist(b'web', b'labels', untrusted=True)
230 labels = u.configlist(b'web', b'labels', untrusted=True)
231 row = {
231 row = {
232 b'contact': contact or b"unknown",
232 b'contact': contact or b"unknown",
233 b'contact_sort': contact.upper() or b"unknown",
233 b'contact_sort': contact.upper() or b"unknown",
234 b'name': name,
234 b'name': name,
235 b'name_sort': name,
235 b'name_sort': name,
236 b'url': url,
236 b'url': url,
237 b'description': description or b"unknown",
237 b'description': description or b"unknown",
238 b'description_sort': description.upper() or b"unknown",
238 b'description_sort': description.upper() or b"unknown",
239 b'lastchange': d,
239 b'lastchange': d,
240 b'lastchange_sort': d[1] - d[0],
240 b'lastchange_sort': d[1] - d[0],
241 b'archives': webutil.archivelist(u, b"tip", url),
241 b'archives': webutil.archivelist(u, b"tip", url),
242 b'isdirectory': None,
242 b'isdirectory': None,
243 b'labels': templateutil.hybridlist(labels, name=b'label'),
243 b'labels': templateutil.hybridlist(labels, name=b'label'),
244 }
244 }
245
245
246 yield row
246 yield row
247
247
248
248
249 def _indexentriesgen(
249 def _indexentriesgen(
250 context, ui, repos, req, stripecount, sortcolumn, descending, subdir
250 context, ui, repos, req, stripecount, sortcolumn, descending, subdir
251 ):
251 ):
252 rows = rawindexentries(ui, repos, req, subdir=subdir)
252 rows = rawindexentries(ui, repos, req, subdir=subdir)
253
253
254 sortdefault = None, False
254 sortdefault = None, False
255
255
256 if sortcolumn and sortdefault != (sortcolumn, descending):
256 if sortcolumn and sortdefault != (sortcolumn, descending):
257 sortkey = b'%s_sort' % sortcolumn
257 sortkey = b'%s_sort' % sortcolumn
258 rows = sorted(rows, key=lambda x: x[sortkey], reverse=descending)
258 rows = sorted(rows, key=lambda x: x[sortkey], reverse=descending)
259
259
260 for row, parity in zip(rows, paritygen(stripecount)):
260 for row, parity in zip(rows, paritygen(stripecount)):
261 row[b'parity'] = parity
261 row[b'parity'] = parity
262 yield row
262 yield row
263
263
264
264
265 def indexentries(
265 def indexentries(
266 ui, repos, req, stripecount, sortcolumn=b'', descending=False, subdir=b''
266 ui, repos, req, stripecount, sortcolumn=b'', descending=False, subdir=b''
267 ):
267 ):
268 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
268 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
269 return templateutil.mappinggenerator(_indexentriesgen, args=args)
269 return templateutil.mappinggenerator(_indexentriesgen, args=args)
270
270
271
271
272 class hgwebdir(object):
272 class hgwebdir(object):
273 """HTTP server for multiple repositories.
273 """HTTP server for multiple repositories.
274
274
275 Given a configuration, different repositories will be served depending
275 Given a configuration, different repositories will be served depending
276 on the request path.
276 on the request path.
277
277
278 Instances are typically used as WSGI applications.
278 Instances are typically used as WSGI applications.
279 """
279 """
280
280
281 def __init__(self, conf, baseui=None):
281 def __init__(self, conf, baseui=None):
282 self.conf = conf
282 self.conf = conf
283 self.baseui = baseui
283 self.baseui = baseui
284 self.ui = None
284 self.ui = None
285 self.lastrefresh = 0
285 self.lastrefresh = 0
286 self.motd = None
286 self.motd = None
287 self.refresh()
287 self.refresh()
288 if not baseui:
288 if not baseui:
289 # set up environment for new ui
289 # set up environment for new ui
290 extensions.loadall(self.ui)
290 extensions.loadall(self.ui)
291 extensions.populateui(self.ui)
291 extensions.populateui(self.ui)
292
292
293 def refresh(self):
293 def refresh(self):
294 if self.ui:
294 if self.ui:
295 refreshinterval = self.ui.configint(b'web', b'refreshinterval')
295 refreshinterval = self.ui.configint(b'web', b'refreshinterval')
296 else:
296 else:
297 item = configitems.coreitems[b'web'][b'refreshinterval']
297 item = configitems.coreitems[b'web'][b'refreshinterval']
298 refreshinterval = item.default
298 refreshinterval = item.default
299
299
300 # refreshinterval <= 0 means to always refresh.
300 # refreshinterval <= 0 means to always refresh.
301 if (
301 if (
302 refreshinterval > 0
302 refreshinterval > 0
303 and self.lastrefresh + refreshinterval > time.time()
303 and self.lastrefresh + refreshinterval > time.time()
304 ):
304 ):
305 return
305 return
306
306
307 if self.baseui:
307 if self.baseui:
308 u = self.baseui.copy()
308 u = self.baseui.copy()
309 else:
309 else:
310 u = uimod.ui.load()
310 u = uimod.ui.load()
311 u.setconfig(b'ui', b'report_untrusted', b'off', b'hgwebdir')
311 u.setconfig(b'ui', b'report_untrusted', b'off', b'hgwebdir')
312 u.setconfig(b'ui', b'nontty', b'true', b'hgwebdir')
312 u.setconfig(b'ui', b'nontty', b'true', b'hgwebdir')
313 # displaying bundling progress bar while serving feels wrong and may
313 # displaying bundling progress bar while serving feels wrong and may
314 # break some wsgi implementations.
314 # break some wsgi implementations.
315 u.setconfig(b'progress', b'disable', b'true', b'hgweb')
315 u.setconfig(b'progress', b'disable', b'true', b'hgweb')
316
316
317 if not isinstance(self.conf, (dict, list, tuple)):
317 if not isinstance(self.conf, (dict, list, tuple)):
318 map = {b'paths': b'hgweb-paths'}
318 map = {b'paths': b'hgweb-paths'}
319 if not os.path.exists(self.conf):
319 if not os.path.exists(self.conf):
320 raise error.Abort(_(b'config file %s not found!') % self.conf)
320 raise error.Abort(_(b'config file %s not found!') % self.conf)
321 u.readconfig(self.conf, remap=map, trust=True)
321 u.readconfig(self.conf, remap=map, trust=True)
322 paths = []
322 paths = []
323 for name, ignored in u.configitems(b'hgweb-paths'):
323 for name, ignored in u.configitems(b'hgweb-paths'):
324 for path in u.configlist(b'hgweb-paths', name):
324 for path in u.configlist(b'hgweb-paths', name):
325 paths.append((name, path))
325 paths.append((name, path))
326 elif isinstance(self.conf, (list, tuple)):
326 elif isinstance(self.conf, (list, tuple)):
327 paths = self.conf
327 paths = self.conf
328 elif isinstance(self.conf, dict):
328 elif isinstance(self.conf, dict):
329 paths = self.conf.items()
329 paths = self.conf.items()
330 extensions.populateui(u)
330 extensions.populateui(u)
331
331
332 repos = findrepos(paths)
332 repos = findrepos(paths)
333 for prefix, root in u.configitems(b'collections'):
333 for prefix, root in u.configitems(b'collections'):
334 prefix = util.pconvert(prefix)
334 prefix = util.pconvert(prefix)
335 for path in scmutil.walkrepos(root, followsym=True):
335 for path in scmutil.walkrepos(root, followsym=True):
336 repo = os.path.normpath(path)
336 repo = os.path.normpath(path)
337 name = util.pconvert(repo)
337 name = util.pconvert(repo)
338 if name.startswith(prefix):
338 if name.startswith(prefix):
339 name = name[len(prefix) :]
339 name = name[len(prefix) :]
340 repos.append((name.lstrip(b'/'), repo))
340 repos.append((name.lstrip(b'/'), repo))
341
341
342 self.repos = repos
342 self.repos = repos
343 self.ui = u
343 self.ui = u
344 encoding.encoding = self.ui.config(b'web', b'encoding')
344 encoding.encoding = self.ui.config(b'web', b'encoding')
345 self.style = self.ui.config(b'web', b'style')
345 self.style = self.ui.config(b'web', b'style')
346 self.templatepath = self.ui.config(
346 self.templatepath = self.ui.config(
347 b'web', b'templates', untrusted=False
347 b'web', b'templates', untrusted=False
348 )
348 )
349 self.stripecount = self.ui.config(b'web', b'stripes')
349 self.stripecount = self.ui.config(b'web', b'stripes')
350 if self.stripecount:
350 if self.stripecount:
351 self.stripecount = int(self.stripecount)
351 self.stripecount = int(self.stripecount)
352 prefix = self.ui.config(b'web', b'prefix')
352 prefix = self.ui.config(b'web', b'prefix')
353 if prefix.startswith(b'/'):
353 if prefix.startswith(b'/'):
354 prefix = prefix[1:]
354 prefix = prefix[1:]
355 if prefix.endswith(b'/'):
355 if prefix.endswith(b'/'):
356 prefix = prefix[:-1]
356 prefix = prefix[:-1]
357 self.prefix = prefix
357 self.prefix = prefix
358 self.lastrefresh = time.time()
358 self.lastrefresh = time.time()
359
359
360 def run(self):
360 def run(self):
361 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
361 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
362 b"CGI/1."
362 b"CGI/1."
363 ):
363 ):
364 raise RuntimeError(
364 raise RuntimeError(
365 b"This function is only intended to be "
365 b"This function is only intended to be "
366 b"called while running as a CGI script."
366 b"called while running as a CGI script."
367 )
367 )
368 wsgicgi.launch(self)
368 wsgicgi.launch(self)
369
369
370 def __call__(self, env, respond):
370 def __call__(self, env, respond):
371 baseurl = self.ui.config(b'web', b'baseurl')
371 baseurl = self.ui.config(b'web', b'baseurl')
372 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
372 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
373 res = requestmod.wsgiresponse(req, respond)
373 res = requestmod.wsgiresponse(req, respond)
374
374
375 return self.run_wsgi(req, res)
375 return self.run_wsgi(req, res)
376
376
377 def run_wsgi(self, req, res):
377 def run_wsgi(self, req, res):
378 profile = self.ui.configbool(b'profiling', b'enabled')
378 profile = self.ui.configbool(b'profiling', b'enabled')
379 with profiling.profile(self.ui, enabled=profile):
379 with profiling.profile(self.ui, enabled=profile):
380 try:
380 try:
381 for r in self._runwsgi(req, res):
381 for r in self._runwsgi(req, res):
382 yield r
382 yield r
383 finally:
383 finally:
384 # There are known cycles in localrepository that prevent
384 # There are known cycles in localrepository that prevent
385 # those objects (and tons of held references) from being
385 # those objects (and tons of held references) from being
386 # collected through normal refcounting. We mitigate those
386 # collected through normal refcounting. We mitigate those
387 # leaks by performing an explicit GC on every request.
387 # leaks by performing an explicit GC on every request.
388 # TODO remove this once leaks are fixed.
388 # TODO remove this once leaks are fixed.
389 # TODO only run this on requests that create localrepository
389 # TODO only run this on requests that create localrepository
390 # instances instead of every request.
390 # instances instead of every request.
391 gc.collect()
391 gc.collect()
392
392
393 def _runwsgi(self, req, res):
393 def _runwsgi(self, req, res):
394 try:
394 try:
395 self.refresh()
395 self.refresh()
396
396
397 csp, nonce = cspvalues(self.ui)
397 csp, nonce = cspvalues(self.ui)
398 if csp:
398 if csp:
399 res.headers[b'Content-Security-Policy'] = csp
399 res.headers[b'Content-Security-Policy'] = csp
400
400
401 virtual = req.dispatchpath.strip(b'/')
401 virtual = req.dispatchpath.strip(b'/')
402 tmpl = self.templater(req, nonce)
402 tmpl = self.templater(req, nonce)
403 ctype = tmpl.render(b'mimetype', {b'encoding': encoding.encoding})
403 ctype = tmpl.render(b'mimetype', {b'encoding': encoding.encoding})
404
404
405 # Global defaults. These can be overridden by any handler.
405 # Global defaults. These can be overridden by any handler.
406 res.status = b'200 Script output follows'
406 res.status = b'200 Script output follows'
407 res.headers[b'Content-Type'] = ctype
407 res.headers[b'Content-Type'] = ctype
408
408
409 # a static file
409 # a static file
410 if virtual.startswith(b'static/') or b'static' in req.qsparams:
410 if virtual.startswith(b'static/') or b'static' in req.qsparams:
411 if virtual.startswith(b'static/'):
411 if virtual.startswith(b'static/'):
412 fname = virtual[7:]
412 fname = virtual[7:]
413 else:
413 else:
414 fname = req.qsparams[b'static']
414 fname = req.qsparams[b'static']
415 static = self.ui.config(b"web", b"static", untrusted=False)
415 static = self.ui.config(b"web", b"static", untrusted=False)
416 staticfile(self.templatepath, static, fname, res)
416 staticfile(self.templatepath, static, fname, res)
417 return res.sendresponse()
417 return res.sendresponse()
418
418
419 # top-level index
419 # top-level index
420
420
421 repos = dict(self.repos)
421 repos = dict(self.repos)
422
422
423 if (not virtual or virtual == b'index') and virtual not in repos:
423 if (not virtual or virtual == b'index') and virtual not in repos:
424 return self.makeindex(req, res, tmpl)
424 return self.makeindex(req, res, tmpl)
425
425
426 # nested indexes and hgwebs
426 # nested indexes and hgwebs
427
427
428 if virtual.endswith(b'/index') and virtual not in repos:
428 if virtual.endswith(b'/index') and virtual not in repos:
429 subdir = virtual[: -len(b'index')]
429 subdir = virtual[: -len(b'index')]
430 if any(r.startswith(subdir) for r in repos):
430 if any(r.startswith(subdir) for r in repos):
431 return self.makeindex(req, res, tmpl, subdir)
431 return self.makeindex(req, res, tmpl, subdir)
432
432
433 def _virtualdirs():
433 def _virtualdirs():
434 # Check the full virtual path, and each parent
434 # Check the full virtual path, and each parent
435 yield virtual
435 yield virtual
436 for p in pathutil.finddirs(virtual):
436 for p in pathutil.finddirs(virtual):
437 yield p
437 yield p
438
438
439 for virtualrepo in _virtualdirs():
439 for virtualrepo in _virtualdirs():
440 real = repos.get(virtualrepo)
440 real = repos.get(virtualrepo)
441 if real:
441 if real:
442 # Re-parse the WSGI environment to take into account our
442 # Re-parse the WSGI environment to take into account our
443 # repository path component.
443 # repository path component.
444 uenv = req.rawenv
444 uenv = req.rawenv
445 if pycompat.ispy3:
445 if pycompat.ispy3:
446 uenv = {
446 uenv = {
447 k.decode('latin1'): v
447 k.decode('latin1'): v
448 for k, v in pycompat.iteritems(uenv)
448 for k, v in pycompat.iteritems(uenv)
449 }
449 }
450 req = requestmod.parserequestfromenv(
450 req = requestmod.parserequestfromenv(
451 uenv,
451 uenv,
452 reponame=virtualrepo,
452 reponame=virtualrepo,
453 altbaseurl=self.ui.config(b'web', b'baseurl'),
453 altbaseurl=self.ui.config(b'web', b'baseurl'),
454 # Reuse wrapped body file object otherwise state
454 # Reuse wrapped body file object otherwise state
455 # tracking can get confused.
455 # tracking can get confused.
456 bodyfh=req.bodyfh,
456 bodyfh=req.bodyfh,
457 )
457 )
458 try:
458 try:
459 # ensure caller gets private copy of ui
459 # ensure caller gets private copy of ui
460 repo = hg.repository(self.ui.copy(), real)
460 repo = hg.repository(self.ui.copy(), real)
461 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
461 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
462 except IOError as inst:
462 except IOError as inst:
463 msg = encoding.strtolocal(inst.strerror)
463 msg = encoding.strtolocal(inst.strerror)
464 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
464 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
465 except error.RepoError as inst:
465 except error.RepoError as inst:
466 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
466 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
467
467
468 # browse subdirectories
468 # browse subdirectories
469 subdir = virtual + b'/'
469 subdir = virtual + b'/'
470 if [r for r in repos if r.startswith(subdir)]:
470 if [r for r in repos if r.startswith(subdir)]:
471 return self.makeindex(req, res, tmpl, subdir)
471 return self.makeindex(req, res, tmpl, subdir)
472
472
473 # prefixes not found
473 # prefixes not found
474 res.status = b'404 Not Found'
474 res.status = b'404 Not Found'
475 res.setbodygen(tmpl.generate(b'notfound', {b'repo': virtual}))
475 res.setbodygen(tmpl.generate(b'notfound', {b'repo': virtual}))
476 return res.sendresponse()
476 return res.sendresponse()
477
477
478 except ErrorResponse as e:
478 except ErrorResponse as e:
479 res.status = statusmessage(e.code, pycompat.bytestr(e))
479 res.status = statusmessage(e.code, pycompat.bytestr(e))
480 res.setbodygen(
480 res.setbodygen(
481 tmpl.generate(b'error', {b'error': e.message or b''})
481 tmpl.generate(b'error', {b'error': e.message or b''})
482 )
482 )
483 return res.sendresponse()
483 return res.sendresponse()
484 finally:
484 finally:
485 del tmpl
485 del tmpl
486
486
487 def makeindex(self, req, res, tmpl, subdir=b""):
487 def makeindex(self, req, res, tmpl, subdir=b""):
488 self.refresh()
488 self.refresh()
489 sortable = [b"name", b"description", b"contact", b"lastchange"]
489 sortable = [b"name", b"description", b"contact", b"lastchange"]
490 sortcolumn, descending = None, False
490 sortcolumn, descending = None, False
491 if b'sort' in req.qsparams:
491 if b'sort' in req.qsparams:
492 sortcolumn = req.qsparams[b'sort']
492 sortcolumn = req.qsparams[b'sort']
493 descending = sortcolumn.startswith(b'-')
493 descending = sortcolumn.startswith(b'-')
494 if descending:
494 if descending:
495 sortcolumn = sortcolumn[1:]
495 sortcolumn = sortcolumn[1:]
496 if sortcolumn not in sortable:
496 if sortcolumn not in sortable:
497 sortcolumn = b""
497 sortcolumn = b""
498
498
499 sort = [
499 sort = [
500 (
500 (
501 b"sort_%s" % column,
501 b"sort_%s" % column,
502 b"%s%s"
502 b"%s%s"
503 % (
503 % (
504 (not descending and column == sortcolumn) and b"-" or b"",
504 (not descending and column == sortcolumn) and b"-" or b"",
505 column,
505 column,
506 ),
506 ),
507 )
507 )
508 for column in sortable
508 for column in sortable
509 ]
509 ]
510
510
511 self.refresh()
511 self.refresh()
512
512
513 entries = indexentries(
513 entries = indexentries(
514 self.ui,
514 self.ui,
515 self.repos,
515 self.repos,
516 req,
516 req,
517 self.stripecount,
517 self.stripecount,
518 sortcolumn=sortcolumn,
518 sortcolumn=sortcolumn,
519 descending=descending,
519 descending=descending,
520 subdir=subdir,
520 subdir=subdir,
521 )
521 )
522
522
523 mapping = {
523 mapping = {
524 b'entries': entries,
524 b'entries': entries,
525 b'subdir': subdir,
525 b'subdir': subdir,
526 b'pathdef': hgweb_mod.makebreadcrumb(b'/' + subdir, self.prefix),
526 b'pathdef': hgweb_mod.makebreadcrumb(b'/' + subdir, self.prefix),
527 b'sortcolumn': sortcolumn,
527 b'sortcolumn': sortcolumn,
528 b'descending': descending,
528 b'descending': descending,
529 }
529 }
530 mapping.update(sort)
530 mapping.update(sort)
531 res.setbodygen(tmpl.generate(b'index', mapping))
531 res.setbodygen(tmpl.generate(b'index', mapping))
532 return res.sendresponse()
532 return res.sendresponse()
533
533
534 def templater(self, req, nonce):
534 def templater(self, req, nonce):
535 def config(*args, **kwargs):
535 def config(*args, **kwargs):
536 kwargs.setdefault('untrusted', True)
536 kwargs.setdefault('untrusted', True)
537 return self.ui.config(*args, **kwargs)
537 return self.ui.config(*args, **kwargs)
538
538
539 vars = {}
539 vars = {}
540 styles, (style, mapfile, fp) = hgweb_mod.getstyle(
540 styles, (style, mapfile, fp) = hgweb_mod.getstyle(
541 req, config, self.templatepath
541 req, config, self.templatepath
542 )
542 )
543 if style == styles[0]:
543 if style == styles[0]:
544 vars[b'style'] = style
544 vars[b'style'] = style
545
545
546 sessionvars = webutil.sessionvars(vars, b'?')
546 sessionvars = webutil.sessionvars(vars, b'?')
547 logourl = config(b'web', b'logourl')
547 logourl = config(b'web', b'logourl')
548 logoimg = config(b'web', b'logoimg')
548 logoimg = config(b'web', b'logoimg')
549 staticurl = (
549 staticurl = (
550 config(b'web', b'staticurl')
550 config(b'web', b'staticurl')
551 or req.apppath.rstrip(b'/') + b'/static/'
551 or req.apppath.rstrip(b'/') + b'/static/'
552 )
552 )
553 if not staticurl.endswith(b'/'):
553 if not staticurl.endswith(b'/'):
554 staticurl += b'/'
554 staticurl += b'/'
555
555
556 defaults = {
556 defaults = {
557 b"encoding": encoding.encoding,
557 b"encoding": encoding.encoding,
558 b"url": req.apppath + b'/',
558 b"url": req.apppath + b'/',
559 b"logourl": logourl,
559 b"logourl": logourl,
560 b"logoimg": logoimg,
560 b"logoimg": logoimg,
561 b"staticurl": staticurl,
561 b"staticurl": staticurl,
562 b"sessionvars": sessionvars,
562 b"sessionvars": sessionvars,
563 b"style": style,
563 b"style": style,
564 b"nonce": nonce,
564 b"nonce": nonce,
565 }
565 }
566 templatekeyword = registrar.templatekeyword(defaults)
566 templatekeyword = registrar.templatekeyword(defaults)
567
567
568 @templatekeyword(b'motd', requires=())
568 @templatekeyword(b'motd', requires=())
569 def motd(context, mapping):
569 def motd(context, mapping):
570 if self.motd is not None:
570 if self.motd is not None:
571 yield self.motd
571 yield self.motd
572 else:
572 else:
573 yield config(b'web', b'motd')
573 yield config(b'web', b'motd')
574
574
575 return templater.templater.frommapfile(
575 return templater.templater.frommapfile(
576 mapfile, fp=fp, defaults=defaults
576 mapfile, fp=fp, defaults=defaults
577 )
577 )
General Comments 0
You need to be logged in to leave comments. Login now