##// END OF EJS Templates
hgweb: fix race in refreshing repo list (issue2188)
Matt Mackall -
r11176:ed5d2a7c default
parent child Browse files
Show More
@@ -1,350 +1,351 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 import os, re, time, urlparse
9 import os, re, time, urlparse
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import ui, hg, util, templater
11 from mercurial import ui, hg, util, templater
12 from mercurial import error, encoding
12 from mercurial import error, encoding
13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from hgweb_mod import hgweb
15 from hgweb_mod import hgweb
16 from request import wsgirequest
16 from request import wsgirequest
17 import webutil
17 import webutil
18
18
19 def cleannames(items):
19 def cleannames(items):
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21
21
22 def findrepos(paths):
22 def findrepos(paths):
23 repos = []
23 repos = []
24 for prefix, root in cleannames(paths):
24 for prefix, root in cleannames(paths):
25 roothead, roottail = os.path.split(root)
25 roothead, roottail = os.path.split(root)
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
27 # mounted as foo/subrepo
27 # mounted as foo/subrepo
28 # and "foo = /bar/**" also recurses into the subdirectories,
28 # and "foo = /bar/**" also recurses into the subdirectories,
29 # remember to use it without working dir.
29 # remember to use it without working dir.
30 try:
30 try:
31 recurse = {'*': False, '**': True}[roottail]
31 recurse = {'*': False, '**': True}[roottail]
32 except KeyError:
32 except KeyError:
33 repos.append((prefix, root))
33 repos.append((prefix, root))
34 continue
34 continue
35 roothead = os.path.normpath(roothead)
35 roothead = os.path.normpath(roothead)
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
37 path = os.path.normpath(path)
37 path = os.path.normpath(path)
38 name = util.pconvert(path[len(roothead):]).strip('/')
38 name = util.pconvert(path[len(roothead):]).strip('/')
39 if prefix:
39 if prefix:
40 name = prefix + '/' + name
40 name = prefix + '/' + name
41 repos.append((name, path))
41 repos.append((name, path))
42 return repos
42 return repos
43
43
44 class hgwebdir(object):
44 class hgwebdir(object):
45 refreshinterval = 20
45 refreshinterval = 20
46
46
47 def __init__(self, conf, baseui=None):
47 def __init__(self, conf, baseui=None):
48 self.conf = conf
48 self.conf = conf
49 self.baseui = baseui
49 self.baseui = baseui
50 self.lastrefresh = 0
50 self.lastrefresh = 0
51 self.motd = None
51 self.motd = None
52 self.refresh()
52 self.refresh()
53
53
54 def refresh(self):
54 def refresh(self):
55 if self.lastrefresh + self.refreshinterval > time.time():
55 if self.lastrefresh + self.refreshinterval > time.time():
56 return
56 return
57
57
58 if self.baseui:
58 if self.baseui:
59 self.ui = self.baseui.copy()
59 u = self.baseui.copy()
60 else:
60 else:
61 self.ui = ui.ui()
61 u = ui.ui()
62 self.ui.setconfig('ui', 'report_untrusted', 'off')
62 u.setconfig('ui', 'report_untrusted', 'off')
63 self.ui.setconfig('ui', 'interactive', 'off')
63 u.setconfig('ui', 'interactive', 'off')
64
64
65 if not isinstance(self.conf, (dict, list, tuple)):
65 if not isinstance(self.conf, (dict, list, tuple)):
66 map = {'paths': 'hgweb-paths'}
66 map = {'paths': 'hgweb-paths'}
67 self.ui.readconfig(self.conf, remap=map, trust=True)
67 u.readconfig(self.conf, remap=map, trust=True)
68 paths = self.ui.configitems('hgweb-paths')
68 paths = u.configitems('hgweb-paths')
69 elif isinstance(self.conf, (list, tuple)):
69 elif isinstance(self.conf, (list, tuple)):
70 paths = self.conf
70 paths = self.conf
71 elif isinstance(self.conf, dict):
71 elif isinstance(self.conf, dict):
72 paths = self.conf.items()
72 paths = self.conf.items()
73
73
74 repos = findrepos(paths)
75 for prefix, root in u.configitems('collections'):
76 prefix = util.pconvert(prefix)
77 for path in util.walkrepos(root, followsym=True):
78 repo = os.path.normpath(path)
79 name = util.pconvert(repo)
80 if name.startswith(prefix):
81 name = name[len(prefix):]
82 repos.append((name.lstrip('/'), repo))
83
84 self.repos = repos
85 self.ui = u
74 encoding.encoding = self.ui.config('web', 'encoding',
86 encoding.encoding = self.ui.config('web', 'encoding',
75 encoding.encoding)
87 encoding.encoding)
76 self.style = self.ui.config('web', 'style', 'paper')
88 self.style = self.ui.config('web', 'style', 'paper')
77 self.stripecount = self.ui.config('web', 'stripes', 1)
89 self.stripecount = self.ui.config('web', 'stripes', 1)
78 if self.stripecount:
90 if self.stripecount:
79 self.stripecount = int(self.stripecount)
91 self.stripecount = int(self.stripecount)
80 self._baseurl = self.ui.config('web', 'baseurl')
92 self._baseurl = self.ui.config('web', 'baseurl')
81
82 self.repos = findrepos(paths)
83 for prefix, root in self.ui.configitems('collections'):
84 prefix = util.pconvert(prefix)
85 for path in util.walkrepos(root, followsym=True):
86 repo = os.path.normpath(path)
87 name = util.pconvert(repo)
88 if name.startswith(prefix):
89 name = name[len(prefix):]
90 self.repos.append((name.lstrip('/'), repo))
91
92 self.lastrefresh = time.time()
93 self.lastrefresh = time.time()
93
94
94 def run(self):
95 def run(self):
95 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
96 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
96 raise RuntimeError("This function is only intended to be "
97 raise RuntimeError("This function is only intended to be "
97 "called while running as a CGI script.")
98 "called while running as a CGI script.")
98 import mercurial.hgweb.wsgicgi as wsgicgi
99 import mercurial.hgweb.wsgicgi as wsgicgi
99 wsgicgi.launch(self)
100 wsgicgi.launch(self)
100
101
101 def __call__(self, env, respond):
102 def __call__(self, env, respond):
102 req = wsgirequest(env, respond)
103 req = wsgirequest(env, respond)
103 return self.run_wsgi(req)
104 return self.run_wsgi(req)
104
105
105 def read_allowed(self, ui, req):
106 def read_allowed(self, ui, req):
106 """Check allow_read and deny_read config options of a repo's ui object
107 """Check allow_read and deny_read config options of a repo's ui object
107 to determine user permissions. By default, with neither option set (or
108 to determine user permissions. By default, with neither option set (or
108 both empty), allow all users to read the repo. There are two ways a
109 both empty), allow all users to read the repo. There are two ways a
109 user can be denied read access: (1) deny_read is not empty, and the
110 user can be denied read access: (1) deny_read is not empty, and the
110 user is unauthenticated or deny_read contains user (or *), and (2)
111 user is unauthenticated or deny_read contains user (or *), and (2)
111 allow_read is not empty and the user is not in allow_read. Return True
112 allow_read is not empty and the user is not in allow_read. Return True
112 if user is allowed to read the repo, else return False."""
113 if user is allowed to read the repo, else return False."""
113
114
114 user = req.env.get('REMOTE_USER')
115 user = req.env.get('REMOTE_USER')
115
116
116 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
117 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
117 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
118 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
118 return False
119 return False
119
120
120 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
121 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
121 # by default, allow reading if no allow_read option has been set
122 # by default, allow reading if no allow_read option has been set
122 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
123 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
123 return True
124 return True
124
125
125 return False
126 return False
126
127
127 def run_wsgi(self, req):
128 def run_wsgi(self, req):
128 try:
129 try:
129 try:
130 try:
130 self.refresh()
131 self.refresh()
131
132
132 virtual = req.env.get("PATH_INFO", "").strip('/')
133 virtual = req.env.get("PATH_INFO", "").strip('/')
133 tmpl = self.templater(req)
134 tmpl = self.templater(req)
134 ctype = tmpl('mimetype', encoding=encoding.encoding)
135 ctype = tmpl('mimetype', encoding=encoding.encoding)
135 ctype = templater.stringify(ctype)
136 ctype = templater.stringify(ctype)
136
137
137 # a static file
138 # a static file
138 if virtual.startswith('static/') or 'static' in req.form:
139 if virtual.startswith('static/') or 'static' in req.form:
139 if virtual.startswith('static/'):
140 if virtual.startswith('static/'):
140 fname = virtual[7:]
141 fname = virtual[7:]
141 else:
142 else:
142 fname = req.form['static'][0]
143 fname = req.form['static'][0]
143 static = templater.templatepath('static')
144 static = templater.templatepath('static')
144 return (staticfile(static, fname, req),)
145 return (staticfile(static, fname, req),)
145
146
146 # top-level index
147 # top-level index
147 elif not virtual:
148 elif not virtual:
148 req.respond(HTTP_OK, ctype)
149 req.respond(HTTP_OK, ctype)
149 return self.makeindex(req, tmpl)
150 return self.makeindex(req, tmpl)
150
151
151 # nested indexes and hgwebs
152 # nested indexes and hgwebs
152
153
153 repos = dict(self.repos)
154 repos = dict(self.repos)
154 while virtual:
155 while virtual:
155 real = repos.get(virtual)
156 real = repos.get(virtual)
156 if real:
157 if real:
157 req.env['REPO_NAME'] = virtual
158 req.env['REPO_NAME'] = virtual
158 try:
159 try:
159 repo = hg.repository(self.ui, real)
160 repo = hg.repository(self.ui, real)
160 return hgweb(repo).run_wsgi(req)
161 return hgweb(repo).run_wsgi(req)
161 except IOError, inst:
162 except IOError, inst:
162 msg = inst.strerror
163 msg = inst.strerror
163 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
164 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
164 except error.RepoError, inst:
165 except error.RepoError, inst:
165 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
166 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
166
167
167 # browse subdirectories
168 # browse subdirectories
168 subdir = virtual + '/'
169 subdir = virtual + '/'
169 if [r for r in repos if r.startswith(subdir)]:
170 if [r for r in repos if r.startswith(subdir)]:
170 req.respond(HTTP_OK, ctype)
171 req.respond(HTTP_OK, ctype)
171 return self.makeindex(req, tmpl, subdir)
172 return self.makeindex(req, tmpl, subdir)
172
173
173 up = virtual.rfind('/')
174 up = virtual.rfind('/')
174 if up < 0:
175 if up < 0:
175 break
176 break
176 virtual = virtual[:up]
177 virtual = virtual[:up]
177
178
178 # prefixes not found
179 # prefixes not found
179 req.respond(HTTP_NOT_FOUND, ctype)
180 req.respond(HTTP_NOT_FOUND, ctype)
180 return tmpl("notfound", repo=virtual)
181 return tmpl("notfound", repo=virtual)
181
182
182 except ErrorResponse, err:
183 except ErrorResponse, err:
183 req.respond(err, ctype)
184 req.respond(err, ctype)
184 return tmpl('error', error=err.message or '')
185 return tmpl('error', error=err.message or '')
185 finally:
186 finally:
186 tmpl = None
187 tmpl = None
187
188
188 def makeindex(self, req, tmpl, subdir=""):
189 def makeindex(self, req, tmpl, subdir=""):
189
190
190 def archivelist(ui, nodeid, url):
191 def archivelist(ui, nodeid, url):
191 allowed = ui.configlist("web", "allow_archive", untrusted=True)
192 allowed = ui.configlist("web", "allow_archive", untrusted=True)
192 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
193 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
193 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
194 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
194 untrusted=True):
195 untrusted=True):
195 yield {"type" : i[0], "extension": i[1],
196 yield {"type" : i[0], "extension": i[1],
196 "node": nodeid, "url": url}
197 "node": nodeid, "url": url}
197
198
198 def rawentries(subdir="", **map):
199 def rawentries(subdir="", **map):
199
200
200 descend = self.ui.configbool('web', 'descend', True)
201 descend = self.ui.configbool('web', 'descend', True)
201 for name, path in self.repos:
202 for name, path in self.repos:
202
203
203 if not name.startswith(subdir):
204 if not name.startswith(subdir):
204 continue
205 continue
205 name = name[len(subdir):]
206 name = name[len(subdir):]
206 if not descend and '/' in name:
207 if not descend and '/' in name:
207 continue
208 continue
208
209
209 u = self.ui.copy()
210 u = self.ui.copy()
210 try:
211 try:
211 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
212 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
212 except Exception, e:
213 except Exception, e:
213 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
214 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
214 continue
215 continue
215 def get(section, name, default=None):
216 def get(section, name, default=None):
216 return u.config(section, name, default, untrusted=True)
217 return u.config(section, name, default, untrusted=True)
217
218
218 if u.configbool("web", "hidden", untrusted=True):
219 if u.configbool("web", "hidden", untrusted=True):
219 continue
220 continue
220
221
221 if not self.read_allowed(u, req):
222 if not self.read_allowed(u, req):
222 continue
223 continue
223
224
224 parts = [name]
225 parts = [name]
225 if 'PATH_INFO' in req.env:
226 if 'PATH_INFO' in req.env:
226 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
227 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
227 if req.env['SCRIPT_NAME']:
228 if req.env['SCRIPT_NAME']:
228 parts.insert(0, req.env['SCRIPT_NAME'])
229 parts.insert(0, req.env['SCRIPT_NAME'])
229 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
230 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
230
231
231 # update time with local timezone
232 # update time with local timezone
232 try:
233 try:
233 r = hg.repository(self.ui, path)
234 r = hg.repository(self.ui, path)
234 d = (get_mtime(r.spath), util.makedate()[1])
235 d = (get_mtime(r.spath), util.makedate()[1])
235 except OSError:
236 except OSError:
236 continue
237 continue
237
238
238 contact = get_contact(get)
239 contact = get_contact(get)
239 description = get("web", "description", "")
240 description = get("web", "description", "")
240 name = get("web", "name", name)
241 name = get("web", "name", name)
241 row = dict(contact=contact or "unknown",
242 row = dict(contact=contact or "unknown",
242 contact_sort=contact.upper() or "unknown",
243 contact_sort=contact.upper() or "unknown",
243 name=name,
244 name=name,
244 name_sort=name,
245 name_sort=name,
245 url=url,
246 url=url,
246 description=description or "unknown",
247 description=description or "unknown",
247 description_sort=description.upper() or "unknown",
248 description_sort=description.upper() or "unknown",
248 lastchange=d,
249 lastchange=d,
249 lastchange_sort=d[1]-d[0],
250 lastchange_sort=d[1]-d[0],
250 archives=archivelist(u, "tip", url))
251 archives=archivelist(u, "tip", url))
251 yield row
252 yield row
252
253
253 sortdefault = None, False
254 sortdefault = None, False
254 def entries(sortcolumn="", descending=False, subdir="", **map):
255 def entries(sortcolumn="", descending=False, subdir="", **map):
255 rows = rawentries(subdir=subdir, **map)
256 rows = rawentries(subdir=subdir, **map)
256
257
257 if sortcolumn and sortdefault != (sortcolumn, descending):
258 if sortcolumn and sortdefault != (sortcolumn, descending):
258 sortkey = '%s_sort' % sortcolumn
259 sortkey = '%s_sort' % sortcolumn
259 rows = sorted(rows, key=lambda x: x[sortkey],
260 rows = sorted(rows, key=lambda x: x[sortkey],
260 reverse=descending)
261 reverse=descending)
261 for row, parity in zip(rows, paritygen(self.stripecount)):
262 for row, parity in zip(rows, paritygen(self.stripecount)):
262 row['parity'] = parity
263 row['parity'] = parity
263 yield row
264 yield row
264
265
265 self.refresh()
266 self.refresh()
266 sortable = ["name", "description", "contact", "lastchange"]
267 sortable = ["name", "description", "contact", "lastchange"]
267 sortcolumn, descending = sortdefault
268 sortcolumn, descending = sortdefault
268 if 'sort' in req.form:
269 if 'sort' in req.form:
269 sortcolumn = req.form['sort'][0]
270 sortcolumn = req.form['sort'][0]
270 descending = sortcolumn.startswith('-')
271 descending = sortcolumn.startswith('-')
271 if descending:
272 if descending:
272 sortcolumn = sortcolumn[1:]
273 sortcolumn = sortcolumn[1:]
273 if sortcolumn not in sortable:
274 if sortcolumn not in sortable:
274 sortcolumn = ""
275 sortcolumn = ""
275
276
276 sort = [("sort_%s" % column,
277 sort = [("sort_%s" % column,
277 "%s%s" % ((not descending and column == sortcolumn)
278 "%s%s" % ((not descending and column == sortcolumn)
278 and "-" or "", column))
279 and "-" or "", column))
279 for column in sortable]
280 for column in sortable]
280
281
281 self.refresh()
282 self.refresh()
282 self.updatereqenv(req.env)
283 self.updatereqenv(req.env)
283
284
284 return tmpl("index", entries=entries, subdir=subdir,
285 return tmpl("index", entries=entries, subdir=subdir,
285 sortcolumn=sortcolumn, descending=descending,
286 sortcolumn=sortcolumn, descending=descending,
286 **dict(sort))
287 **dict(sort))
287
288
288 def templater(self, req):
289 def templater(self, req):
289
290
290 def header(**map):
291 def header(**map):
291 yield tmpl('header', encoding=encoding.encoding, **map)
292 yield tmpl('header', encoding=encoding.encoding, **map)
292
293
293 def footer(**map):
294 def footer(**map):
294 yield tmpl("footer", **map)
295 yield tmpl("footer", **map)
295
296
296 def motd(**map):
297 def motd(**map):
297 if self.motd is not None:
298 if self.motd is not None:
298 yield self.motd
299 yield self.motd
299 else:
300 else:
300 yield config('web', 'motd', '')
301 yield config('web', 'motd', '')
301
302
302 def config(section, name, default=None, untrusted=True):
303 def config(section, name, default=None, untrusted=True):
303 return self.ui.config(section, name, default, untrusted)
304 return self.ui.config(section, name, default, untrusted)
304
305
305 self.updatereqenv(req.env)
306 self.updatereqenv(req.env)
306
307
307 url = req.env.get('SCRIPT_NAME', '')
308 url = req.env.get('SCRIPT_NAME', '')
308 if not url.endswith('/'):
309 if not url.endswith('/'):
309 url += '/'
310 url += '/'
310
311
311 vars = {}
312 vars = {}
312 styles = (
313 styles = (
313 req.form.get('style', [None])[0],
314 req.form.get('style', [None])[0],
314 config('web', 'style'),
315 config('web', 'style'),
315 'paper'
316 'paper'
316 )
317 )
317 style, mapfile = templater.stylemap(styles)
318 style, mapfile = templater.stylemap(styles)
318 if style == styles[0]:
319 if style == styles[0]:
319 vars['style'] = style
320 vars['style'] = style
320
321
321 start = url[-1] == '?' and '&' or '?'
322 start = url[-1] == '?' and '&' or '?'
322 sessionvars = webutil.sessionvars(vars, start)
323 sessionvars = webutil.sessionvars(vars, start)
323 staticurl = config('web', 'staticurl') or url + 'static/'
324 staticurl = config('web', 'staticurl') or url + 'static/'
324 if not staticurl.endswith('/'):
325 if not staticurl.endswith('/'):
325 staticurl += '/'
326 staticurl += '/'
326
327
327 tmpl = templater.templater(mapfile,
328 tmpl = templater.templater(mapfile,
328 defaults={"header": header,
329 defaults={"header": header,
329 "footer": footer,
330 "footer": footer,
330 "motd": motd,
331 "motd": motd,
331 "url": url,
332 "url": url,
332 "staticurl": staticurl,
333 "staticurl": staticurl,
333 "sessionvars": sessionvars})
334 "sessionvars": sessionvars})
334 return tmpl
335 return tmpl
335
336
336 def updatereqenv(self, env):
337 def updatereqenv(self, env):
337 def splitnetloc(netloc):
338 def splitnetloc(netloc):
338 if ':' in netloc:
339 if ':' in netloc:
339 return netloc.split(':', 1)
340 return netloc.split(':', 1)
340 else:
341 else:
341 return (netloc, None)
342 return (netloc, None)
342
343
343 if self._baseurl is not None:
344 if self._baseurl is not None:
344 urlcomp = urlparse.urlparse(self._baseurl)
345 urlcomp = urlparse.urlparse(self._baseurl)
345 host, port = splitnetloc(urlcomp[1])
346 host, port = splitnetloc(urlcomp[1])
346 path = urlcomp[2]
347 path = urlcomp[2]
347 env['SERVER_NAME'] = host
348 env['SERVER_NAME'] = host
348 if port:
349 if port:
349 env['SERVER_PORT'] = port
350 env['SERVER_PORT'] = port
350 env['SCRIPT_NAME'] = path
351 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now