##// END OF EJS Templates
hgwebdir_mod: move from dict() construction to {} literals...
Augie Fackler -
r20677:0e757575 default
parent child Browse files
Show More
@@ -1,462 +1,463 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import os, re, time
10 10 from mercurial.i18n import _
11 11 from mercurial import ui, hg, scmutil, util, templater
12 12 from mercurial import error, encoding
13 13 from common import ErrorResponse, get_mtime, staticfile, paritygen, ismember, \
14 14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 15 from hgweb_mod import hgweb, makebreadcrumb
16 16 from request import wsgirequest
17 17 import webutil
18 18
19 19 def cleannames(items):
20 20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21 21
22 22 def findrepos(paths):
23 23 repos = []
24 24 for prefix, root in cleannames(paths):
25 25 roothead, roottail = os.path.split(root)
26 26 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
27 27 # /bar/ be served as as foo/N .
28 28 # '*' will not search inside dirs with .hg (except .hg/patches),
29 29 # '**' will search inside dirs with .hg (and thus also find subrepos).
30 30 try:
31 31 recurse = {'*': False, '**': True}[roottail]
32 32 except KeyError:
33 33 repos.append((prefix, root))
34 34 continue
35 35 roothead = os.path.normpath(os.path.abspath(roothead))
36 36 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
37 37 repos.extend(urlrepos(prefix, roothead, paths))
38 38 return repos
39 39
40 40 def urlrepos(prefix, roothead, paths):
41 41 """yield url paths and filesystem paths from a list of repo paths
42 42
43 43 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
44 44 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
45 45 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
46 46 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
47 47 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
48 48 """
49 49 for path in paths:
50 50 path = os.path.normpath(path)
51 51 yield (prefix + '/' +
52 52 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
53 53
54 54 def geturlcgivars(baseurl, port):
55 55 """
56 56 Extract CGI variables from baseurl
57 57
58 58 >>> geturlcgivars("http://host.org/base", "80")
59 59 ('host.org', '80', '/base')
60 60 >>> geturlcgivars("http://host.org:8000/base", "80")
61 61 ('host.org', '8000', '/base')
62 62 >>> geturlcgivars('/base', 8000)
63 63 ('', '8000', '/base')
64 64 >>> geturlcgivars("base", '8000')
65 65 ('', '8000', '/base')
66 66 >>> geturlcgivars("http://host", '8000')
67 67 ('host', '8000', '/')
68 68 >>> geturlcgivars("http://host/", '8000')
69 69 ('host', '8000', '/')
70 70 """
71 71 u = util.url(baseurl)
72 72 name = u.host or ''
73 73 if u.port:
74 74 port = u.port
75 75 path = u.path or ""
76 76 if not path.startswith('/'):
77 77 path = '/' + path
78 78
79 79 return name, str(port), path
80 80
81 81 class hgwebdir(object):
82 82 refreshinterval = 20
83 83
84 84 def __init__(self, conf, baseui=None):
85 85 self.conf = conf
86 86 self.baseui = baseui
87 87 self.lastrefresh = 0
88 88 self.motd = None
89 89 self.refresh()
90 90
91 91 def refresh(self):
92 92 if self.lastrefresh + self.refreshinterval > time.time():
93 93 return
94 94
95 95 if self.baseui:
96 96 u = self.baseui.copy()
97 97 else:
98 98 u = ui.ui()
99 99 u.setconfig('ui', 'report_untrusted', 'off')
100 100 u.setconfig('ui', 'nontty', 'true')
101 101
102 102 if not isinstance(self.conf, (dict, list, tuple)):
103 103 map = {'paths': 'hgweb-paths'}
104 104 if not os.path.exists(self.conf):
105 105 raise util.Abort(_('config file %s not found!') % self.conf)
106 106 u.readconfig(self.conf, remap=map, trust=True)
107 107 paths = []
108 108 for name, ignored in u.configitems('hgweb-paths'):
109 109 for path in u.configlist('hgweb-paths', name):
110 110 paths.append((name, path))
111 111 elif isinstance(self.conf, (list, tuple)):
112 112 paths = self.conf
113 113 elif isinstance(self.conf, dict):
114 114 paths = self.conf.items()
115 115
116 116 repos = findrepos(paths)
117 117 for prefix, root in u.configitems('collections'):
118 118 prefix = util.pconvert(prefix)
119 119 for path in scmutil.walkrepos(root, followsym=True):
120 120 repo = os.path.normpath(path)
121 121 name = util.pconvert(repo)
122 122 if name.startswith(prefix):
123 123 name = name[len(prefix):]
124 124 repos.append((name.lstrip('/'), repo))
125 125
126 126 self.repos = repos
127 127 self.ui = u
128 128 encoding.encoding = self.ui.config('web', 'encoding',
129 129 encoding.encoding)
130 130 self.style = self.ui.config('web', 'style', 'paper')
131 131 self.templatepath = self.ui.config('web', 'templates', None)
132 132 self.stripecount = self.ui.config('web', 'stripes', 1)
133 133 if self.stripecount:
134 134 self.stripecount = int(self.stripecount)
135 135 self._baseurl = self.ui.config('web', 'baseurl')
136 136 prefix = self.ui.config('web', 'prefix', '')
137 137 if prefix.startswith('/'):
138 138 prefix = prefix[1:]
139 139 if prefix.endswith('/'):
140 140 prefix = prefix[:-1]
141 141 self.prefix = prefix
142 142 self.lastrefresh = time.time()
143 143
144 144 def run(self):
145 145 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
146 146 raise RuntimeError("This function is only intended to be "
147 147 "called while running as a CGI script.")
148 148 import mercurial.hgweb.wsgicgi as wsgicgi
149 149 wsgicgi.launch(self)
150 150
151 151 def __call__(self, env, respond):
152 152 req = wsgirequest(env, respond)
153 153 return self.run_wsgi(req)
154 154
155 155 def read_allowed(self, ui, req):
156 156 """Check allow_read and deny_read config options of a repo's ui object
157 157 to determine user permissions. By default, with neither option set (or
158 158 both empty), allow all users to read the repo. There are two ways a
159 159 user can be denied read access: (1) deny_read is not empty, and the
160 160 user is unauthenticated or deny_read contains user (or *), and (2)
161 161 allow_read is not empty and the user is not in allow_read. Return True
162 162 if user is allowed to read the repo, else return False."""
163 163
164 164 user = req.env.get('REMOTE_USER')
165 165
166 166 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
167 167 if deny_read and (not user or ismember(ui, user, deny_read)):
168 168 return False
169 169
170 170 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
171 171 # by default, allow reading if no allow_read option has been set
172 172 if (not allow_read) or ismember(ui, user, allow_read):
173 173 return True
174 174
175 175 return False
176 176
177 177 def run_wsgi(self, req):
178 178 try:
179 179 try:
180 180 self.refresh()
181 181
182 182 virtual = req.env.get("PATH_INFO", "").strip('/')
183 183 tmpl = self.templater(req)
184 184 ctype = tmpl('mimetype', encoding=encoding.encoding)
185 185 ctype = templater.stringify(ctype)
186 186
187 187 # a static file
188 188 if virtual.startswith('static/') or 'static' in req.form:
189 189 if virtual.startswith('static/'):
190 190 fname = virtual[7:]
191 191 else:
192 192 fname = req.form['static'][0]
193 193 static = self.ui.config("web", "static", None,
194 194 untrusted=False)
195 195 if not static:
196 196 tp = self.templatepath or templater.templatepath()
197 197 if isinstance(tp, str):
198 198 tp = [tp]
199 199 static = [os.path.join(p, 'static') for p in tp]
200 200 staticfile(static, fname, req)
201 201 return []
202 202
203 203 # top-level index
204 204 elif not virtual:
205 205 req.respond(HTTP_OK, ctype)
206 206 return self.makeindex(req, tmpl)
207 207
208 208 # nested indexes and hgwebs
209 209
210 210 repos = dict(self.repos)
211 211 virtualrepo = virtual
212 212 while virtualrepo:
213 213 real = repos.get(virtualrepo)
214 214 if real:
215 215 req.env['REPO_NAME'] = virtualrepo
216 216 try:
217 217 repo = hg.repository(self.ui, real)
218 218 return hgweb(repo).run_wsgi(req)
219 219 except IOError, inst:
220 220 msg = inst.strerror
221 221 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
222 222 except error.RepoError, inst:
223 223 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
224 224
225 225 up = virtualrepo.rfind('/')
226 226 if up < 0:
227 227 break
228 228 virtualrepo = virtualrepo[:up]
229 229
230 230 # browse subdirectories
231 231 subdir = virtual + '/'
232 232 if [r for r in repos if r.startswith(subdir)]:
233 233 req.respond(HTTP_OK, ctype)
234 234 return self.makeindex(req, tmpl, subdir)
235 235
236 236 # prefixes not found
237 237 req.respond(HTTP_NOT_FOUND, ctype)
238 238 return tmpl("notfound", repo=virtual)
239 239
240 240 except ErrorResponse, err:
241 241 req.respond(err, ctype)
242 242 return tmpl('error', error=err.message or '')
243 243 finally:
244 244 tmpl = None
245 245
246 246 def makeindex(self, req, tmpl, subdir=""):
247 247
248 248 def archivelist(ui, nodeid, url):
249 249 allowed = ui.configlist("web", "allow_archive", untrusted=True)
250 250 archives = []
251 251 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
252 252 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
253 253 untrusted=True):
254 254 archives.append({"type" : i[0], "extension": i[1],
255 255 "node": nodeid, "url": url})
256 256 return archives
257 257
258 258 def rawentries(subdir="", **map):
259 259
260 260 descend = self.ui.configbool('web', 'descend', True)
261 261 collapse = self.ui.configbool('web', 'collapse', False)
262 262 seenrepos = set()
263 263 seendirs = set()
264 264 for name, path in self.repos:
265 265
266 266 if not name.startswith(subdir):
267 267 continue
268 268 name = name[len(subdir):]
269 269 directory = False
270 270
271 271 if '/' in name:
272 272 if not descend:
273 273 continue
274 274
275 275 nameparts = name.split('/')
276 276 rootname = nameparts[0]
277 277
278 278 if not collapse:
279 279 pass
280 280 elif rootname in seendirs:
281 281 continue
282 282 elif rootname in seenrepos:
283 283 pass
284 284 else:
285 285 directory = True
286 286 name = rootname
287 287
288 288 # redefine the path to refer to the directory
289 289 discarded = '/'.join(nameparts[1:])
290 290
291 291 # remove name parts plus accompanying slash
292 292 path = path[:-len(discarded) - 1]
293 293
294 294 parts = [name]
295 295 if 'PATH_INFO' in req.env:
296 296 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
297 297 if req.env['SCRIPT_NAME']:
298 298 parts.insert(0, req.env['SCRIPT_NAME'])
299 299 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
300 300
301 301 # show either a directory entry or a repository
302 302 if directory:
303 303 # get the directory's time information
304 304 try:
305 305 d = (get_mtime(path), util.makedate()[1])
306 306 except OSError:
307 307 continue
308 308
309 309 # add '/' to the name to make it obvious that
310 310 # the entry is a directory, not a regular repository
311 row = dict(contact="",
312 contact_sort="",
313 name=name + '/',
314 name_sort=name,
315 url=url,
316 description="",
317 description_sort="",
318 lastchange=d,
319 lastchange_sort=d[1]-d[0],
320 archives=[],
321 isdirectory=True)
311 row = {'contact': "",
312 'contact_sort': "",
313 'name': name + '/',
314 'name_sort': name,
315 'url': url,
316 'description': "",
317 'description_sort': "",
318 'lastchange': d,
319 'lastchange_sort': d[1]-d[0],
320 'archives': [],
321 'isdirectory': True}
322 322
323 323 seendirs.add(name)
324 324 yield row
325 325 continue
326 326
327 327 u = self.ui.copy()
328 328 try:
329 329 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
330 330 except Exception, e:
331 331 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
332 332 continue
333 333 def get(section, name, default=None):
334 334 return u.config(section, name, default, untrusted=True)
335 335
336 336 if u.configbool("web", "hidden", untrusted=True):
337 337 continue
338 338
339 339 if not self.read_allowed(u, req):
340 340 continue
341 341
342 342 # update time with local timezone
343 343 try:
344 344 r = hg.repository(self.ui, path)
345 345 except IOError:
346 346 u.warn(_('error accessing repository at %s\n') % path)
347 347 continue
348 348 except error.RepoError:
349 349 u.warn(_('error accessing repository at %s\n') % path)
350 350 continue
351 351 try:
352 352 d = (get_mtime(r.spath), util.makedate()[1])
353 353 except OSError:
354 354 continue
355 355
356 356 contact = get_contact(get)
357 357 description = get("web", "description", "")
358 358 name = get("web", "name", name)
359 row = dict(contact=contact or "unknown",
360 contact_sort=contact.upper() or "unknown",
361 name=name,
362 name_sort=name,
363 url=url,
364 description=description or "unknown",
365 description_sort=description.upper() or "unknown",
366 lastchange=d,
367 lastchange_sort=d[1]-d[0],
368 archives=archivelist(u, "tip", url),
369 isdirectory=None)
359 row = {'contact': contact or "unknown",
360 'contact_sort': contact.upper() or "unknown",
361 'name': name,
362 'name_sort': name,
363 'url': url,
364 'description': description or "unknown",
365 'description_sort': description.upper() or "unknown",
366 'lastchange': d,
367 'lastchange_sort': d[1]-d[0],
368 'archives': archivelist(u, "tip", url),
369 'isdirectory': None,
370 }
370 371
371 372 seenrepos.add(name)
372 373 yield row
373 374
374 375 sortdefault = None, False
375 376 def entries(sortcolumn="", descending=False, subdir="", **map):
376 377 rows = rawentries(subdir=subdir, **map)
377 378
378 379 if sortcolumn and sortdefault != (sortcolumn, descending):
379 380 sortkey = '%s_sort' % sortcolumn
380 381 rows = sorted(rows, key=lambda x: x[sortkey],
381 382 reverse=descending)
382 383 for row, parity in zip(rows, paritygen(self.stripecount)):
383 384 row['parity'] = parity
384 385 yield row
385 386
386 387 self.refresh()
387 388 sortable = ["name", "description", "contact", "lastchange"]
388 389 sortcolumn, descending = sortdefault
389 390 if 'sort' in req.form:
390 391 sortcolumn = req.form['sort'][0]
391 392 descending = sortcolumn.startswith('-')
392 393 if descending:
393 394 sortcolumn = sortcolumn[1:]
394 395 if sortcolumn not in sortable:
395 396 sortcolumn = ""
396 397
397 398 sort = [("sort_%s" % column,
398 399 "%s%s" % ((not descending and column == sortcolumn)
399 400 and "-" or "", column))
400 401 for column in sortable]
401 402
402 403 self.refresh()
403 404 self.updatereqenv(req.env)
404 405
405 406 return tmpl("index", entries=entries, subdir=subdir,
406 407 pathdef=makebreadcrumb('/' + subdir, self.prefix),
407 408 sortcolumn=sortcolumn, descending=descending,
408 409 **dict(sort))
409 410
410 411 def templater(self, req):
411 412
412 413 def motd(**map):
413 414 if self.motd is not None:
414 415 yield self.motd
415 416 else:
416 417 yield config('web', 'motd', '')
417 418
418 419 def config(section, name, default=None, untrusted=True):
419 420 return self.ui.config(section, name, default, untrusted)
420 421
421 422 self.updatereqenv(req.env)
422 423
423 424 url = req.env.get('SCRIPT_NAME', '')
424 425 if not url.endswith('/'):
425 426 url += '/'
426 427
427 428 vars = {}
428 429 styles = (
429 430 req.form.get('style', [None])[0],
430 431 config('web', 'style'),
431 432 'paper'
432 433 )
433 434 style, mapfile = templater.stylemap(styles, self.templatepath)
434 435 if style == styles[0]:
435 436 vars['style'] = style
436 437
437 438 start = url[-1] == '?' and '&' or '?'
438 439 sessionvars = webutil.sessionvars(vars, start)
439 440 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
440 441 logoimg = config('web', 'logoimg', 'hglogo.png')
441 442 staticurl = config('web', 'staticurl') or url + 'static/'
442 443 if not staticurl.endswith('/'):
443 444 staticurl += '/'
444 445
445 446 tmpl = templater.templater(mapfile,
446 447 defaults={"encoding": encoding.encoding,
447 448 "motd": motd,
448 449 "url": url,
449 450 "logourl": logourl,
450 451 "logoimg": logoimg,
451 452 "staticurl": staticurl,
452 453 "sessionvars": sessionvars,
453 454 "style": style,
454 455 })
455 456 return tmpl
456 457
457 458 def updatereqenv(self, env):
458 459 if self._baseurl is not None:
459 460 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
460 461 env['SERVER_NAME'] = name
461 462 env['SERVER_PORT'] = port
462 463 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now