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