##// END OF EJS Templates
hgweb: fix race in refreshing repo list (issue2188)
Matt Mackall -
r11239:99bc18d1 stable
parent child Browse files
Show More
@@ -1,353 +1,354 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 sortdefault = None, False
199 sortdefault = None, False
199 def entries(sortcolumn="", descending=False, subdir="", **map):
200 def entries(sortcolumn="", descending=False, subdir="", **map):
200
201
201 rows = []
202 rows = []
202 parity = paritygen(self.stripecount)
203 parity = paritygen(self.stripecount)
203 descend = self.ui.configbool('web', 'descend', True)
204 descend = self.ui.configbool('web', 'descend', True)
204 for name, path in self.repos:
205 for name, path in self.repos:
205
206
206 if not name.startswith(subdir):
207 if not name.startswith(subdir):
207 continue
208 continue
208 name = name[len(subdir):]
209 name = name[len(subdir):]
209 if not descend and '/' in name:
210 if not descend and '/' in name:
210 continue
211 continue
211
212
212 u = self.ui.copy()
213 u = self.ui.copy()
213 try:
214 try:
214 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
215 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
215 except Exception, e:
216 except Exception, e:
216 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
217 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
217 continue
218 continue
218 def get(section, name, default=None):
219 def get(section, name, default=None):
219 return u.config(section, name, default, untrusted=True)
220 return u.config(section, name, default, untrusted=True)
220
221
221 if u.configbool("web", "hidden", untrusted=True):
222 if u.configbool("web", "hidden", untrusted=True):
222 continue
223 continue
223
224
224 if not self.read_allowed(u, req):
225 if not self.read_allowed(u, req):
225 continue
226 continue
226
227
227 parts = [name]
228 parts = [name]
228 if 'PATH_INFO' in req.env:
229 if 'PATH_INFO' in req.env:
229 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
230 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
230 if req.env['SCRIPT_NAME']:
231 if req.env['SCRIPT_NAME']:
231 parts.insert(0, req.env['SCRIPT_NAME'])
232 parts.insert(0, req.env['SCRIPT_NAME'])
232 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
233 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
233
234
234 # update time with local timezone
235 # update time with local timezone
235 try:
236 try:
236 r = hg.repository(self.ui, path)
237 r = hg.repository(self.ui, path)
237 d = (get_mtime(r.spath), util.makedate()[1])
238 d = (get_mtime(r.spath), util.makedate()[1])
238 except OSError:
239 except OSError:
239 continue
240 continue
240
241
241 contact = get_contact(get)
242 contact = get_contact(get)
242 description = get("web", "description", "")
243 description = get("web", "description", "")
243 name = get("web", "name", name)
244 name = get("web", "name", name)
244 row = dict(contact=contact or "unknown",
245 row = dict(contact=contact or "unknown",
245 contact_sort=contact.upper() or "unknown",
246 contact_sort=contact.upper() or "unknown",
246 name=name,
247 name=name,
247 name_sort=name,
248 name_sort=name,
248 url=url,
249 url=url,
249 description=description or "unknown",
250 description=description or "unknown",
250 description_sort=description.upper() or "unknown",
251 description_sort=description.upper() or "unknown",
251 lastchange=d,
252 lastchange=d,
252 lastchange_sort=d[1]-d[0],
253 lastchange_sort=d[1]-d[0],
253 archives=archivelist(u, "tip", url))
254 archives=archivelist(u, "tip", url))
254 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
255 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
255 # fast path for unsorted output
256 # fast path for unsorted output
256 row['parity'] = parity.next()
257 row['parity'] = parity.next()
257 yield row
258 yield row
258 else:
259 else:
259 rows.append((row["%s_sort" % sortcolumn], row))
260 rows.append((row["%s_sort" % sortcolumn], row))
260 if rows:
261 if rows:
261 rows.sort()
262 rows.sort()
262 if descending:
263 if descending:
263 rows.reverse()
264 rows.reverse()
264 for key, row in rows:
265 for key, row in rows:
265 row['parity'] = parity.next()
266 row['parity'] = parity.next()
266 yield row
267 yield row
267
268
268 self.refresh()
269 self.refresh()
269 sortable = ["name", "description", "contact", "lastchange"]
270 sortable = ["name", "description", "contact", "lastchange"]
270 sortcolumn, descending = sortdefault
271 sortcolumn, descending = sortdefault
271 if 'sort' in req.form:
272 if 'sort' in req.form:
272 sortcolumn = req.form['sort'][0]
273 sortcolumn = req.form['sort'][0]
273 descending = sortcolumn.startswith('-')
274 descending = sortcolumn.startswith('-')
274 if descending:
275 if descending:
275 sortcolumn = sortcolumn[1:]
276 sortcolumn = sortcolumn[1:]
276 if sortcolumn not in sortable:
277 if sortcolumn not in sortable:
277 sortcolumn = ""
278 sortcolumn = ""
278
279
279 sort = [("sort_%s" % column,
280 sort = [("sort_%s" % column,
280 "%s%s" % ((not descending and column == sortcolumn)
281 "%s%s" % ((not descending and column == sortcolumn)
281 and "-" or "", column))
282 and "-" or "", column))
282 for column in sortable]
283 for column in sortable]
283
284
284 self.refresh()
285 self.refresh()
285 self.updatereqenv(req.env)
286 self.updatereqenv(req.env)
286
287
287 return tmpl("index", entries=entries, subdir=subdir,
288 return tmpl("index", entries=entries, subdir=subdir,
288 sortcolumn=sortcolumn, descending=descending,
289 sortcolumn=sortcolumn, descending=descending,
289 **dict(sort))
290 **dict(sort))
290
291
291 def templater(self, req):
292 def templater(self, req):
292
293
293 def header(**map):
294 def header(**map):
294 yield tmpl('header', encoding=encoding.encoding, **map)
295 yield tmpl('header', encoding=encoding.encoding, **map)
295
296
296 def footer(**map):
297 def footer(**map):
297 yield tmpl("footer", **map)
298 yield tmpl("footer", **map)
298
299
299 def motd(**map):
300 def motd(**map):
300 if self.motd is not None:
301 if self.motd is not None:
301 yield self.motd
302 yield self.motd
302 else:
303 else:
303 yield config('web', 'motd', '')
304 yield config('web', 'motd', '')
304
305
305 def config(section, name, default=None, untrusted=True):
306 def config(section, name, default=None, untrusted=True):
306 return self.ui.config(section, name, default, untrusted)
307 return self.ui.config(section, name, default, untrusted)
307
308
308 self.updatereqenv(req.env)
309 self.updatereqenv(req.env)
309
310
310 url = req.env.get('SCRIPT_NAME', '')
311 url = req.env.get('SCRIPT_NAME', '')
311 if not url.endswith('/'):
312 if not url.endswith('/'):
312 url += '/'
313 url += '/'
313
314
314 vars = {}
315 vars = {}
315 styles = (
316 styles = (
316 req.form.get('style', [None])[0],
317 req.form.get('style', [None])[0],
317 config('web', 'style'),
318 config('web', 'style'),
318 'paper'
319 'paper'
319 )
320 )
320 style, mapfile = templater.stylemap(styles)
321 style, mapfile = templater.stylemap(styles)
321 if style == styles[0]:
322 if style == styles[0]:
322 vars['style'] = style
323 vars['style'] = style
323
324
324 start = url[-1] == '?' and '&' or '?'
325 start = url[-1] == '?' and '&' or '?'
325 sessionvars = webutil.sessionvars(vars, start)
326 sessionvars = webutil.sessionvars(vars, start)
326 staticurl = config('web', 'staticurl') or url + 'static/'
327 staticurl = config('web', 'staticurl') or url + 'static/'
327 if not staticurl.endswith('/'):
328 if not staticurl.endswith('/'):
328 staticurl += '/'
329 staticurl += '/'
329
330
330 tmpl = templater.templater(mapfile,
331 tmpl = templater.templater(mapfile,
331 defaults={"header": header,
332 defaults={"header": header,
332 "footer": footer,
333 "footer": footer,
333 "motd": motd,
334 "motd": motd,
334 "url": url,
335 "url": url,
335 "staticurl": staticurl,
336 "staticurl": staticurl,
336 "sessionvars": sessionvars})
337 "sessionvars": sessionvars})
337 return tmpl
338 return tmpl
338
339
339 def updatereqenv(self, env):
340 def updatereqenv(self, env):
340 def splitnetloc(netloc):
341 def splitnetloc(netloc):
341 if ':' in netloc:
342 if ':' in netloc:
342 return netloc.split(':', 1)
343 return netloc.split(':', 1)
343 else:
344 else:
344 return (netloc, None)
345 return (netloc, None)
345
346
346 if self._baseurl is not None:
347 if self._baseurl is not None:
347 urlcomp = urlparse.urlparse(self._baseurl)
348 urlcomp = urlparse.urlparse(self._baseurl)
348 host, port = splitnetloc(urlcomp[1])
349 host, port = splitnetloc(urlcomp[1])
349 path = urlcomp[2]
350 path = urlcomp[2]
350 env['SERVER_NAME'] = host
351 env['SERVER_NAME'] = host
351 if port:
352 if port:
352 env['SERVER_PORT'] = port
353 env['SERVER_PORT'] = port
353 env['SCRIPT_NAME'] = path
354 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now