##// END OF EJS Templates
hgweb: avoid config object race with hgwebdir (issue4326)...
Matt Mackall -
r22087:af62f028 stable
parent child Browse files
Show More
@@ -1,395 +1,396
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
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-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 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
9 import os, re
10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
11 from mercurial.templatefilters import websub
11 from mercurial.templatefilters import websub
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from common import get_stat, ErrorResponse, permhooks, caching
13 from common import get_stat, ErrorResponse, permhooks, caching
14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 from request import wsgirequest
16 from request import wsgirequest
17 import webcommands, protocol, webutil
17 import webcommands, protocol, webutil
18
18
19 perms = {
19 perms = {
20 'changegroup': 'pull',
20 'changegroup': 'pull',
21 'changegroupsubset': 'pull',
21 'changegroupsubset': 'pull',
22 'getbundle': 'pull',
22 'getbundle': 'pull',
23 'stream_out': 'pull',
23 'stream_out': 'pull',
24 'listkeys': 'pull',
24 'listkeys': 'pull',
25 'unbundle': 'push',
25 'unbundle': 'push',
26 'pushkey': 'push',
26 'pushkey': 'push',
27 }
27 }
28
28
29 def makebreadcrumb(url, prefix=''):
29 def makebreadcrumb(url, prefix=''):
30 '''Return a 'URL breadcrumb' list
30 '''Return a 'URL breadcrumb' list
31
31
32 A 'URL breadcrumb' is a list of URL-name pairs,
32 A 'URL breadcrumb' is a list of URL-name pairs,
33 corresponding to each of the path items on a URL.
33 corresponding to each of the path items on a URL.
34 This can be used to create path navigation entries.
34 This can be used to create path navigation entries.
35 '''
35 '''
36 if url.endswith('/'):
36 if url.endswith('/'):
37 url = url[:-1]
37 url = url[:-1]
38 if prefix:
38 if prefix:
39 url = '/' + prefix + url
39 url = '/' + prefix + url
40 relpath = url
40 relpath = url
41 if relpath.startswith('/'):
41 if relpath.startswith('/'):
42 relpath = relpath[1:]
42 relpath = relpath[1:]
43
43
44 breadcrumb = []
44 breadcrumb = []
45 urlel = url
45 urlel = url
46 pathitems = [''] + relpath.split('/')
46 pathitems = [''] + relpath.split('/')
47 for pathel in reversed(pathitems):
47 for pathel in reversed(pathitems):
48 if not pathel or not urlel:
48 if not pathel or not urlel:
49 break
49 break
50 breadcrumb.append({'url': urlel, 'name': pathel})
50 breadcrumb.append({'url': urlel, 'name': pathel})
51 urlel = os.path.dirname(urlel)
51 urlel = os.path.dirname(urlel)
52 return reversed(breadcrumb)
52 return reversed(breadcrumb)
53
53
54
54
55 class hgweb(object):
55 class hgweb(object):
56 def __init__(self, repo, name=None, baseui=None):
56 def __init__(self, repo, name=None, baseui=None):
57 if isinstance(repo, str):
57 if isinstance(repo, str):
58 if baseui:
58 if baseui:
59 u = baseui.copy()
59 u = baseui.copy()
60 else:
60 else:
61 u = ui.ui()
61 u = ui.ui()
62 r = hg.repository(u, repo)
62 r = hg.repository(u, repo)
63 else:
63 else:
64 # we trust caller to give us a private copy
64 r = repo
65 r = repo
65
66
66 r = self._getview(r)
67 r = self._getview(r)
67 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
68 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
68 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
69 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
69 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
70 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
70 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
71 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
71 self.repo = r
72 self.repo = r
72 hook.redirect(True)
73 hook.redirect(True)
73 self.mtime = -1
74 self.mtime = -1
74 self.size = -1
75 self.size = -1
75 self.reponame = name
76 self.reponame = name
76 self.archives = 'zip', 'gz', 'bz2'
77 self.archives = 'zip', 'gz', 'bz2'
77 self.stripecount = 1
78 self.stripecount = 1
78 # a repo owner may set web.templates in .hg/hgrc to get any file
79 # a repo owner may set web.templates in .hg/hgrc to get any file
79 # readable by the user running the CGI script
80 # readable by the user running the CGI script
80 self.templatepath = self.config('web', 'templates')
81 self.templatepath = self.config('web', 'templates')
81 self.websubtable = self.loadwebsub()
82 self.websubtable = self.loadwebsub()
82
83
83 # The CGI scripts are often run by a user different from the repo owner.
84 # The CGI scripts are often run by a user different from the repo owner.
84 # Trust the settings from the .hg/hgrc files by default.
85 # Trust the settings from the .hg/hgrc files by default.
85 def config(self, section, name, default=None, untrusted=True):
86 def config(self, section, name, default=None, untrusted=True):
86 return self.repo.ui.config(section, name, default,
87 return self.repo.ui.config(section, name, default,
87 untrusted=untrusted)
88 untrusted=untrusted)
88
89
89 def configbool(self, section, name, default=False, untrusted=True):
90 def configbool(self, section, name, default=False, untrusted=True):
90 return self.repo.ui.configbool(section, name, default,
91 return self.repo.ui.configbool(section, name, default,
91 untrusted=untrusted)
92 untrusted=untrusted)
92
93
93 def configlist(self, section, name, default=None, untrusted=True):
94 def configlist(self, section, name, default=None, untrusted=True):
94 return self.repo.ui.configlist(section, name, default,
95 return self.repo.ui.configlist(section, name, default,
95 untrusted=untrusted)
96 untrusted=untrusted)
96
97
97 def _getview(self, repo):
98 def _getview(self, repo):
98 viewconfig = repo.ui.config('web', 'view', 'served',
99 viewconfig = repo.ui.config('web', 'view', 'served',
99 untrusted=True)
100 untrusted=True)
100 if viewconfig == 'all':
101 if viewconfig == 'all':
101 return repo.unfiltered()
102 return repo.unfiltered()
102 elif viewconfig in repoview.filtertable:
103 elif viewconfig in repoview.filtertable:
103 return repo.filtered(viewconfig)
104 return repo.filtered(viewconfig)
104 else:
105 else:
105 return repo.filtered('served')
106 return repo.filtered('served')
106
107
107 def refresh(self, request=None):
108 def refresh(self, request=None):
108 st = get_stat(self.repo.spath)
109 st = get_stat(self.repo.spath)
109 # compare changelog size in addition to mtime to catch
110 # compare changelog size in addition to mtime to catch
110 # rollbacks made less than a second ago
111 # rollbacks made less than a second ago
111 if st.st_mtime != self.mtime or st.st_size != self.size:
112 if st.st_mtime != self.mtime or st.st_size != self.size:
112 r = hg.repository(self.repo.baseui, self.repo.root)
113 r = hg.repository(self.repo.baseui, self.repo.root)
113 self.repo = self._getview(r)
114 self.repo = self._getview(r)
114 self.maxchanges = int(self.config("web", "maxchanges", 10))
115 self.maxchanges = int(self.config("web", "maxchanges", 10))
115 self.stripecount = int(self.config("web", "stripes", 1))
116 self.stripecount = int(self.config("web", "stripes", 1))
116 self.maxshortchanges = int(self.config("web", "maxshortchanges",
117 self.maxshortchanges = int(self.config("web", "maxshortchanges",
117 60))
118 60))
118 self.maxfiles = int(self.config("web", "maxfiles", 10))
119 self.maxfiles = int(self.config("web", "maxfiles", 10))
119 self.allowpull = self.configbool("web", "allowpull", True)
120 self.allowpull = self.configbool("web", "allowpull", True)
120 encoding.encoding = self.config("web", "encoding",
121 encoding.encoding = self.config("web", "encoding",
121 encoding.encoding)
122 encoding.encoding)
122 # update these last to avoid threads seeing empty settings
123 # update these last to avoid threads seeing empty settings
123 self.mtime = st.st_mtime
124 self.mtime = st.st_mtime
124 self.size = st.st_size
125 self.size = st.st_size
125 if request:
126 if request:
126 self.repo.ui.environ = request.env
127 self.repo.ui.environ = request.env
127
128
128 def run(self):
129 def run(self):
129 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
130 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
130 raise RuntimeError("This function is only intended to be "
131 raise RuntimeError("This function is only intended to be "
131 "called while running as a CGI script.")
132 "called while running as a CGI script.")
132 import mercurial.hgweb.wsgicgi as wsgicgi
133 import mercurial.hgweb.wsgicgi as wsgicgi
133 wsgicgi.launch(self)
134 wsgicgi.launch(self)
134
135
135 def __call__(self, env, respond):
136 def __call__(self, env, respond):
136 req = wsgirequest(env, respond)
137 req = wsgirequest(env, respond)
137 return self.run_wsgi(req)
138 return self.run_wsgi(req)
138
139
139 def run_wsgi(self, req):
140 def run_wsgi(self, req):
140
141
141 self.refresh(req)
142 self.refresh(req)
142
143
143 # work with CGI variables to create coherent structure
144 # work with CGI variables to create coherent structure
144 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
145 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
145
146
146 req.url = req.env['SCRIPT_NAME']
147 req.url = req.env['SCRIPT_NAME']
147 if not req.url.endswith('/'):
148 if not req.url.endswith('/'):
148 req.url += '/'
149 req.url += '/'
149 if 'REPO_NAME' in req.env:
150 if 'REPO_NAME' in req.env:
150 req.url += req.env['REPO_NAME'] + '/'
151 req.url += req.env['REPO_NAME'] + '/'
151
152
152 if 'PATH_INFO' in req.env:
153 if 'PATH_INFO' in req.env:
153 parts = req.env['PATH_INFO'].strip('/').split('/')
154 parts = req.env['PATH_INFO'].strip('/').split('/')
154 repo_parts = req.env.get('REPO_NAME', '').split('/')
155 repo_parts = req.env.get('REPO_NAME', '').split('/')
155 if parts[:len(repo_parts)] == repo_parts:
156 if parts[:len(repo_parts)] == repo_parts:
156 parts = parts[len(repo_parts):]
157 parts = parts[len(repo_parts):]
157 query = '/'.join(parts)
158 query = '/'.join(parts)
158 else:
159 else:
159 query = req.env['QUERY_STRING'].split('&', 1)[0]
160 query = req.env['QUERY_STRING'].split('&', 1)[0]
160 query = query.split(';', 1)[0]
161 query = query.split(';', 1)[0]
161
162
162 # process this if it's a protocol request
163 # process this if it's a protocol request
163 # protocol bits don't need to create any URLs
164 # protocol bits don't need to create any URLs
164 # and the clients always use the old URL structure
165 # and the clients always use the old URL structure
165
166
166 cmd = req.form.get('cmd', [''])[0]
167 cmd = req.form.get('cmd', [''])[0]
167 if protocol.iscmd(cmd):
168 if protocol.iscmd(cmd):
168 try:
169 try:
169 if query:
170 if query:
170 raise ErrorResponse(HTTP_NOT_FOUND)
171 raise ErrorResponse(HTTP_NOT_FOUND)
171 if cmd in perms:
172 if cmd in perms:
172 self.check_perm(req, perms[cmd])
173 self.check_perm(req, perms[cmd])
173 return protocol.call(self.repo, req, cmd)
174 return protocol.call(self.repo, req, cmd)
174 except ErrorResponse, inst:
175 except ErrorResponse, inst:
175 # A client that sends unbundle without 100-continue will
176 # A client that sends unbundle without 100-continue will
176 # break if we respond early.
177 # break if we respond early.
177 if (cmd == 'unbundle' and
178 if (cmd == 'unbundle' and
178 (req.env.get('HTTP_EXPECT',
179 (req.env.get('HTTP_EXPECT',
179 '').lower() != '100-continue') or
180 '').lower() != '100-continue') or
180 req.env.get('X-HgHttp2', '')):
181 req.env.get('X-HgHttp2', '')):
181 req.drain()
182 req.drain()
182 else:
183 else:
183 req.headers.append(('Connection', 'Close'))
184 req.headers.append(('Connection', 'Close'))
184 req.respond(inst, protocol.HGTYPE,
185 req.respond(inst, protocol.HGTYPE,
185 body='0\n%s\n' % inst.message)
186 body='0\n%s\n' % inst.message)
186 return ''
187 return ''
187
188
188 # translate user-visible url structure to internal structure
189 # translate user-visible url structure to internal structure
189
190
190 args = query.split('/', 2)
191 args = query.split('/', 2)
191 if 'cmd' not in req.form and args and args[0]:
192 if 'cmd' not in req.form and args and args[0]:
192
193
193 cmd = args.pop(0)
194 cmd = args.pop(0)
194 style = cmd.rfind('-')
195 style = cmd.rfind('-')
195 if style != -1:
196 if style != -1:
196 req.form['style'] = [cmd[:style]]
197 req.form['style'] = [cmd[:style]]
197 cmd = cmd[style + 1:]
198 cmd = cmd[style + 1:]
198
199
199 # avoid accepting e.g. style parameter as command
200 # avoid accepting e.g. style parameter as command
200 if util.safehasattr(webcommands, cmd):
201 if util.safehasattr(webcommands, cmd):
201 req.form['cmd'] = [cmd]
202 req.form['cmd'] = [cmd]
202 else:
203 else:
203 cmd = ''
204 cmd = ''
204
205
205 if cmd == 'static':
206 if cmd == 'static':
206 req.form['file'] = ['/'.join(args)]
207 req.form['file'] = ['/'.join(args)]
207 else:
208 else:
208 if args and args[0]:
209 if args and args[0]:
209 node = args.pop(0)
210 node = args.pop(0)
210 req.form['node'] = [node]
211 req.form['node'] = [node]
211 if args:
212 if args:
212 req.form['file'] = args
213 req.form['file'] = args
213
214
214 ua = req.env.get('HTTP_USER_AGENT', '')
215 ua = req.env.get('HTTP_USER_AGENT', '')
215 if cmd == 'rev' and 'mercurial' in ua:
216 if cmd == 'rev' and 'mercurial' in ua:
216 req.form['style'] = ['raw']
217 req.form['style'] = ['raw']
217
218
218 if cmd == 'archive':
219 if cmd == 'archive':
219 fn = req.form['node'][0]
220 fn = req.form['node'][0]
220 for type_, spec in self.archive_specs.iteritems():
221 for type_, spec in self.archive_specs.iteritems():
221 ext = spec[2]
222 ext = spec[2]
222 if fn.endswith(ext):
223 if fn.endswith(ext):
223 req.form['node'] = [fn[:-len(ext)]]
224 req.form['node'] = [fn[:-len(ext)]]
224 req.form['type'] = [type_]
225 req.form['type'] = [type_]
225
226
226 # process the web interface request
227 # process the web interface request
227
228
228 try:
229 try:
229 tmpl = self.templater(req)
230 tmpl = self.templater(req)
230 ctype = tmpl('mimetype', encoding=encoding.encoding)
231 ctype = tmpl('mimetype', encoding=encoding.encoding)
231 ctype = templater.stringify(ctype)
232 ctype = templater.stringify(ctype)
232
233
233 # check read permissions non-static content
234 # check read permissions non-static content
234 if cmd != 'static':
235 if cmd != 'static':
235 self.check_perm(req, None)
236 self.check_perm(req, None)
236
237
237 if cmd == '':
238 if cmd == '':
238 req.form['cmd'] = [tmpl.cache['default']]
239 req.form['cmd'] = [tmpl.cache['default']]
239 cmd = req.form['cmd'][0]
240 cmd = req.form['cmd'][0]
240
241
241 if self.configbool('web', 'cache', True):
242 if self.configbool('web', 'cache', True):
242 caching(self, req) # sets ETag header or raises NOT_MODIFIED
243 caching(self, req) # sets ETag header or raises NOT_MODIFIED
243 if cmd not in webcommands.__all__:
244 if cmd not in webcommands.__all__:
244 msg = 'no such method: %s' % cmd
245 msg = 'no such method: %s' % cmd
245 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
246 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
246 elif cmd == 'file' and 'raw' in req.form.get('style', []):
247 elif cmd == 'file' and 'raw' in req.form.get('style', []):
247 self.ctype = ctype
248 self.ctype = ctype
248 content = webcommands.rawfile(self, req, tmpl)
249 content = webcommands.rawfile(self, req, tmpl)
249 else:
250 else:
250 content = getattr(webcommands, cmd)(self, req, tmpl)
251 content = getattr(webcommands, cmd)(self, req, tmpl)
251 req.respond(HTTP_OK, ctype)
252 req.respond(HTTP_OK, ctype)
252
253
253 return content
254 return content
254
255
255 except (error.LookupError, error.RepoLookupError), err:
256 except (error.LookupError, error.RepoLookupError), err:
256 req.respond(HTTP_NOT_FOUND, ctype)
257 req.respond(HTTP_NOT_FOUND, ctype)
257 msg = str(err)
258 msg = str(err)
258 if (util.safehasattr(err, 'name') and
259 if (util.safehasattr(err, 'name') and
259 not isinstance(err, error.ManifestLookupError)):
260 not isinstance(err, error.ManifestLookupError)):
260 msg = 'revision not found: %s' % err.name
261 msg = 'revision not found: %s' % err.name
261 return tmpl('error', error=msg)
262 return tmpl('error', error=msg)
262 except (error.RepoError, error.RevlogError), inst:
263 except (error.RepoError, error.RevlogError), inst:
263 req.respond(HTTP_SERVER_ERROR, ctype)
264 req.respond(HTTP_SERVER_ERROR, ctype)
264 return tmpl('error', error=str(inst))
265 return tmpl('error', error=str(inst))
265 except ErrorResponse, inst:
266 except ErrorResponse, inst:
266 req.respond(inst, ctype)
267 req.respond(inst, ctype)
267 if inst.code == HTTP_NOT_MODIFIED:
268 if inst.code == HTTP_NOT_MODIFIED:
268 # Not allowed to return a body on a 304
269 # Not allowed to return a body on a 304
269 return ['']
270 return ['']
270 return tmpl('error', error=inst.message)
271 return tmpl('error', error=inst.message)
271
272
272 def loadwebsub(self):
273 def loadwebsub(self):
273 websubtable = []
274 websubtable = []
274 websubdefs = self.repo.ui.configitems('websub')
275 websubdefs = self.repo.ui.configitems('websub')
275 # we must maintain interhg backwards compatibility
276 # we must maintain interhg backwards compatibility
276 websubdefs += self.repo.ui.configitems('interhg')
277 websubdefs += self.repo.ui.configitems('interhg')
277 for key, pattern in websubdefs:
278 for key, pattern in websubdefs:
278 # grab the delimiter from the character after the "s"
279 # grab the delimiter from the character after the "s"
279 unesc = pattern[1]
280 unesc = pattern[1]
280 delim = re.escape(unesc)
281 delim = re.escape(unesc)
281
282
282 # identify portions of the pattern, taking care to avoid escaped
283 # identify portions of the pattern, taking care to avoid escaped
283 # delimiters. the replace format and flags are optional, but
284 # delimiters. the replace format and flags are optional, but
284 # delimiters are required.
285 # delimiters are required.
285 match = re.match(
286 match = re.match(
286 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
287 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
287 % (delim, delim, delim), pattern)
288 % (delim, delim, delim), pattern)
288 if not match:
289 if not match:
289 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
290 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
290 % (key, pattern))
291 % (key, pattern))
291 continue
292 continue
292
293
293 # we need to unescape the delimiter for regexp and format
294 # we need to unescape the delimiter for regexp and format
294 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
295 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
295 regexp = delim_re.sub(unesc, match.group(1))
296 regexp = delim_re.sub(unesc, match.group(1))
296 format = delim_re.sub(unesc, match.group(2))
297 format = delim_re.sub(unesc, match.group(2))
297
298
298 # the pattern allows for 6 regexp flags, so set them if necessary
299 # the pattern allows for 6 regexp flags, so set them if necessary
299 flagin = match.group(3)
300 flagin = match.group(3)
300 flags = 0
301 flags = 0
301 if flagin:
302 if flagin:
302 for flag in flagin.upper():
303 for flag in flagin.upper():
303 flags |= re.__dict__[flag]
304 flags |= re.__dict__[flag]
304
305
305 try:
306 try:
306 regexp = re.compile(regexp, flags)
307 regexp = re.compile(regexp, flags)
307 websubtable.append((regexp, format))
308 websubtable.append((regexp, format))
308 except re.error:
309 except re.error:
309 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
310 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
310 % (key, regexp))
311 % (key, regexp))
311 return websubtable
312 return websubtable
312
313
313 def templater(self, req):
314 def templater(self, req):
314
315
315 # determine scheme, port and server name
316 # determine scheme, port and server name
316 # this is needed to create absolute urls
317 # this is needed to create absolute urls
317
318
318 proto = req.env.get('wsgi.url_scheme')
319 proto = req.env.get('wsgi.url_scheme')
319 if proto == 'https':
320 if proto == 'https':
320 proto = 'https'
321 proto = 'https'
321 default_port = "443"
322 default_port = "443"
322 else:
323 else:
323 proto = 'http'
324 proto = 'http'
324 default_port = "80"
325 default_port = "80"
325
326
326 port = req.env["SERVER_PORT"]
327 port = req.env["SERVER_PORT"]
327 port = port != default_port and (":" + port) or ""
328 port = port != default_port and (":" + port) or ""
328 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
329 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
329 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
330 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
330 logoimg = self.config("web", "logoimg", "hglogo.png")
331 logoimg = self.config("web", "logoimg", "hglogo.png")
331 staticurl = self.config("web", "staticurl") or req.url + 'static/'
332 staticurl = self.config("web", "staticurl") or req.url + 'static/'
332 if not staticurl.endswith('/'):
333 if not staticurl.endswith('/'):
333 staticurl += '/'
334 staticurl += '/'
334
335
335 # some functions for the templater
336 # some functions for the templater
336
337
337 def motd(**map):
338 def motd(**map):
338 yield self.config("web", "motd", "")
339 yield self.config("web", "motd", "")
339
340
340 # figure out which style to use
341 # figure out which style to use
341
342
342 vars = {}
343 vars = {}
343 styles = (
344 styles = (
344 req.form.get('style', [None])[0],
345 req.form.get('style', [None])[0],
345 self.config('web', 'style'),
346 self.config('web', 'style'),
346 'paper',
347 'paper',
347 )
348 )
348 style, mapfile = templater.stylemap(styles, self.templatepath)
349 style, mapfile = templater.stylemap(styles, self.templatepath)
349 if style == styles[0]:
350 if style == styles[0]:
350 vars['style'] = style
351 vars['style'] = style
351
352
352 start = req.url[-1] == '?' and '&' or '?'
353 start = req.url[-1] == '?' and '&' or '?'
353 sessionvars = webutil.sessionvars(vars, start)
354 sessionvars = webutil.sessionvars(vars, start)
354
355
355 if not self.reponame:
356 if not self.reponame:
356 self.reponame = (self.config("web", "name")
357 self.reponame = (self.config("web", "name")
357 or req.env.get('REPO_NAME')
358 or req.env.get('REPO_NAME')
358 or req.url.strip('/') or self.repo.root)
359 or req.url.strip('/') or self.repo.root)
359
360
360 def websubfilter(text):
361 def websubfilter(text):
361 return websub(text, self.websubtable)
362 return websub(text, self.websubtable)
362
363
363 # create the templater
364 # create the templater
364
365
365 tmpl = templater.templater(mapfile,
366 tmpl = templater.templater(mapfile,
366 filters={"websub": websubfilter},
367 filters={"websub": websubfilter},
367 defaults={"url": req.url,
368 defaults={"url": req.url,
368 "logourl": logourl,
369 "logourl": logourl,
369 "logoimg": logoimg,
370 "logoimg": logoimg,
370 "staticurl": staticurl,
371 "staticurl": staticurl,
371 "urlbase": urlbase,
372 "urlbase": urlbase,
372 "repo": self.reponame,
373 "repo": self.reponame,
373 "encoding": encoding.encoding,
374 "encoding": encoding.encoding,
374 "motd": motd,
375 "motd": motd,
375 "sessionvars": sessionvars,
376 "sessionvars": sessionvars,
376 "pathdef": makebreadcrumb(req.url),
377 "pathdef": makebreadcrumb(req.url),
377 "style": style,
378 "style": style,
378 })
379 })
379 return tmpl
380 return tmpl
380
381
381 def archivelist(self, nodeid):
382 def archivelist(self, nodeid):
382 allowed = self.configlist("web", "allow_archive")
383 allowed = self.configlist("web", "allow_archive")
383 for i, spec in self.archive_specs.iteritems():
384 for i, spec in self.archive_specs.iteritems():
384 if i in allowed or self.configbool("web", "allow" + i):
385 if i in allowed or self.configbool("web", "allow" + i):
385 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
386 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
386
387
387 archive_specs = {
388 archive_specs = {
388 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
389 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
389 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
390 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
390 'zip': ('application/zip', 'zip', '.zip', None),
391 'zip': ('application/zip', 'zip', '.zip', None),
391 }
392 }
392
393
393 def check_perm(self, req, op):
394 def check_perm(self, req, op):
394 for hook in permhooks:
395 for hook in permhooks:
395 hook(self, req, op)
396 hook(self, req, op)
@@ -1,463 +1,464
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
9 import os, re, time
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import ui, hg, scmutil, util, templater
11 from mercurial import ui, hg, scmutil, util, templater
12 from mercurial import error, encoding
12 from mercurial import error, encoding
13 from common import ErrorResponse, get_mtime, staticfile, paritygen, ismember, \
13 from common import ErrorResponse, get_mtime, staticfile, paritygen, ismember, \
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, makebreadcrumb
15 from hgweb_mod import hgweb, makebreadcrumb
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/*" or "foo = /bar/**" lets every repo /bar/N in or below
26 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
27 # /bar/ be served as as foo/N .
27 # /bar/ be served as as foo/N .
28 # '*' will not search inside dirs with .hg (except .hg/patches),
28 # '*' will not search inside dirs with .hg (except .hg/patches),
29 # '**' will search inside dirs with .hg (and thus also find subrepos).
29 # '**' will search inside dirs with .hg (and thus also find subrepos).
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(os.path.abspath(roothead))
35 roothead = os.path.normpath(os.path.abspath(roothead))
36 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
36 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
37 repos.extend(urlrepos(prefix, roothead, paths))
37 repos.extend(urlrepos(prefix, roothead, paths))
38 return repos
38 return repos
39
39
40 def urlrepos(prefix, roothead, paths):
40 def urlrepos(prefix, roothead, paths):
41 """yield url paths and filesystem paths from a list of repo paths
41 """yield url paths and filesystem paths from a list of repo paths
42
42
43 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
43 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
44 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
44 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
45 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
45 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
46 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
46 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
47 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
47 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
48 """
48 """
49 for path in paths:
49 for path in paths:
50 path = os.path.normpath(path)
50 path = os.path.normpath(path)
51 yield (prefix + '/' +
51 yield (prefix + '/' +
52 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
52 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
53
53
54 def geturlcgivars(baseurl, port):
54 def geturlcgivars(baseurl, port):
55 """
55 """
56 Extract CGI variables from baseurl
56 Extract CGI variables from baseurl
57
57
58 >>> geturlcgivars("http://host.org/base", "80")
58 >>> geturlcgivars("http://host.org/base", "80")
59 ('host.org', '80', '/base')
59 ('host.org', '80', '/base')
60 >>> geturlcgivars("http://host.org:8000/base", "80")
60 >>> geturlcgivars("http://host.org:8000/base", "80")
61 ('host.org', '8000', '/base')
61 ('host.org', '8000', '/base')
62 >>> geturlcgivars('/base', 8000)
62 >>> geturlcgivars('/base', 8000)
63 ('', '8000', '/base')
63 ('', '8000', '/base')
64 >>> geturlcgivars("base", '8000')
64 >>> geturlcgivars("base", '8000')
65 ('', '8000', '/base')
65 ('', '8000', '/base')
66 >>> geturlcgivars("http://host", '8000')
66 >>> geturlcgivars("http://host", '8000')
67 ('host', '8000', '/')
67 ('host', '8000', '/')
68 >>> geturlcgivars("http://host/", '8000')
68 >>> geturlcgivars("http://host/", '8000')
69 ('host', '8000', '/')
69 ('host', '8000', '/')
70 """
70 """
71 u = util.url(baseurl)
71 u = util.url(baseurl)
72 name = u.host or ''
72 name = u.host or ''
73 if u.port:
73 if u.port:
74 port = u.port
74 port = u.port
75 path = u.path or ""
75 path = u.path or ""
76 if not path.startswith('/'):
76 if not path.startswith('/'):
77 path = '/' + path
77 path = '/' + path
78
78
79 return name, str(port), path
79 return name, str(port), path
80
80
81 class hgwebdir(object):
81 class hgwebdir(object):
82 refreshinterval = 20
82 refreshinterval = 20
83
83
84 def __init__(self, conf, baseui=None):
84 def __init__(self, conf, baseui=None):
85 self.conf = conf
85 self.conf = conf
86 self.baseui = baseui
86 self.baseui = baseui
87 self.lastrefresh = 0
87 self.lastrefresh = 0
88 self.motd = None
88 self.motd = None
89 self.refresh()
89 self.refresh()
90
90
91 def refresh(self):
91 def refresh(self):
92 if self.lastrefresh + self.refreshinterval > time.time():
92 if self.lastrefresh + self.refreshinterval > time.time():
93 return
93 return
94
94
95 if self.baseui:
95 if self.baseui:
96 u = self.baseui.copy()
96 u = self.baseui.copy()
97 else:
97 else:
98 u = ui.ui()
98 u = ui.ui()
99 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
99 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
100 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
100 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
101
101
102 if not isinstance(self.conf, (dict, list, tuple)):
102 if not isinstance(self.conf, (dict, list, tuple)):
103 map = {'paths': 'hgweb-paths'}
103 map = {'paths': 'hgweb-paths'}
104 if not os.path.exists(self.conf):
104 if not os.path.exists(self.conf):
105 raise util.Abort(_('config file %s not found!') % self.conf)
105 raise util.Abort(_('config file %s not found!') % self.conf)
106 u.readconfig(self.conf, remap=map, trust=True)
106 u.readconfig(self.conf, remap=map, trust=True)
107 paths = []
107 paths = []
108 for name, ignored in u.configitems('hgweb-paths'):
108 for name, ignored in u.configitems('hgweb-paths'):
109 for path in u.configlist('hgweb-paths', name):
109 for path in u.configlist('hgweb-paths', name):
110 paths.append((name, path))
110 paths.append((name, path))
111 elif isinstance(self.conf, (list, tuple)):
111 elif isinstance(self.conf, (list, tuple)):
112 paths = self.conf
112 paths = self.conf
113 elif isinstance(self.conf, dict):
113 elif isinstance(self.conf, dict):
114 paths = self.conf.items()
114 paths = self.conf.items()
115
115
116 repos = findrepos(paths)
116 repos = findrepos(paths)
117 for prefix, root in u.configitems('collections'):
117 for prefix, root in u.configitems('collections'):
118 prefix = util.pconvert(prefix)
118 prefix = util.pconvert(prefix)
119 for path in scmutil.walkrepos(root, followsym=True):
119 for path in scmutil.walkrepos(root, followsym=True):
120 repo = os.path.normpath(path)
120 repo = os.path.normpath(path)
121 name = util.pconvert(repo)
121 name = util.pconvert(repo)
122 if name.startswith(prefix):
122 if name.startswith(prefix):
123 name = name[len(prefix):]
123 name = name[len(prefix):]
124 repos.append((name.lstrip('/'), repo))
124 repos.append((name.lstrip('/'), repo))
125
125
126 self.repos = repos
126 self.repos = repos
127 self.ui = u
127 self.ui = u
128 encoding.encoding = self.ui.config('web', 'encoding',
128 encoding.encoding = self.ui.config('web', 'encoding',
129 encoding.encoding)
129 encoding.encoding)
130 self.style = self.ui.config('web', 'style', 'paper')
130 self.style = self.ui.config('web', 'style', 'paper')
131 self.templatepath = self.ui.config('web', 'templates', None)
131 self.templatepath = self.ui.config('web', 'templates', None)
132 self.stripecount = self.ui.config('web', 'stripes', 1)
132 self.stripecount = self.ui.config('web', 'stripes', 1)
133 if self.stripecount:
133 if self.stripecount:
134 self.stripecount = int(self.stripecount)
134 self.stripecount = int(self.stripecount)
135 self._baseurl = self.ui.config('web', 'baseurl')
135 self._baseurl = self.ui.config('web', 'baseurl')
136 prefix = self.ui.config('web', 'prefix', '')
136 prefix = self.ui.config('web', 'prefix', '')
137 if prefix.startswith('/'):
137 if prefix.startswith('/'):
138 prefix = prefix[1:]
138 prefix = prefix[1:]
139 if prefix.endswith('/'):
139 if prefix.endswith('/'):
140 prefix = prefix[:-1]
140 prefix = prefix[:-1]
141 self.prefix = prefix
141 self.prefix = prefix
142 self.lastrefresh = time.time()
142 self.lastrefresh = time.time()
143
143
144 def run(self):
144 def run(self):
145 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
145 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
146 raise RuntimeError("This function is only intended to be "
146 raise RuntimeError("This function is only intended to be "
147 "called while running as a CGI script.")
147 "called while running as a CGI script.")
148 import mercurial.hgweb.wsgicgi as wsgicgi
148 import mercurial.hgweb.wsgicgi as wsgicgi
149 wsgicgi.launch(self)
149 wsgicgi.launch(self)
150
150
151 def __call__(self, env, respond):
151 def __call__(self, env, respond):
152 req = wsgirequest(env, respond)
152 req = wsgirequest(env, respond)
153 return self.run_wsgi(req)
153 return self.run_wsgi(req)
154
154
155 def read_allowed(self, ui, req):
155 def read_allowed(self, ui, req):
156 """Check allow_read and deny_read config options of a repo's ui object
156 """Check allow_read and deny_read config options of a repo's ui object
157 to determine user permissions. By default, with neither option set (or
157 to determine user permissions. By default, with neither option set (or
158 both empty), allow all users to read the repo. There are two ways a
158 both empty), allow all users to read the repo. There are two ways a
159 user can be denied read access: (1) deny_read is not empty, and the
159 user can be denied read access: (1) deny_read is not empty, and the
160 user is unauthenticated or deny_read contains user (or *), and (2)
160 user is unauthenticated or deny_read contains user (or *), and (2)
161 allow_read is not empty and the user is not in allow_read. Return True
161 allow_read is not empty and the user is not in allow_read. Return True
162 if user is allowed to read the repo, else return False."""
162 if user is allowed to read the repo, else return False."""
163
163
164 user = req.env.get('REMOTE_USER')
164 user = req.env.get('REMOTE_USER')
165
165
166 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
166 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
167 if deny_read and (not user or ismember(ui, user, deny_read)):
167 if deny_read and (not user or ismember(ui, user, deny_read)):
168 return False
168 return False
169
169
170 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
170 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
171 # by default, allow reading if no allow_read option has been set
171 # by default, allow reading if no allow_read option has been set
172 if (not allow_read) or ismember(ui, user, allow_read):
172 if (not allow_read) or ismember(ui, user, allow_read):
173 return True
173 return True
174
174
175 return False
175 return False
176
176
177 def run_wsgi(self, req):
177 def run_wsgi(self, req):
178 try:
178 try:
179 try:
179 try:
180 self.refresh()
180 self.refresh()
181
181
182 virtual = req.env.get("PATH_INFO", "").strip('/')
182 virtual = req.env.get("PATH_INFO", "").strip('/')
183 tmpl = self.templater(req)
183 tmpl = self.templater(req)
184 ctype = tmpl('mimetype', encoding=encoding.encoding)
184 ctype = tmpl('mimetype', encoding=encoding.encoding)
185 ctype = templater.stringify(ctype)
185 ctype = templater.stringify(ctype)
186
186
187 # a static file
187 # a static file
188 if virtual.startswith('static/') or 'static' in req.form:
188 if virtual.startswith('static/') or 'static' in req.form:
189 if virtual.startswith('static/'):
189 if virtual.startswith('static/'):
190 fname = virtual[7:]
190 fname = virtual[7:]
191 else:
191 else:
192 fname = req.form['static'][0]
192 fname = req.form['static'][0]
193 static = self.ui.config("web", "static", None,
193 static = self.ui.config("web", "static", None,
194 untrusted=False)
194 untrusted=False)
195 if not static:
195 if not static:
196 tp = self.templatepath or templater.templatepath()
196 tp = self.templatepath or templater.templatepath()
197 if isinstance(tp, str):
197 if isinstance(tp, str):
198 tp = [tp]
198 tp = [tp]
199 static = [os.path.join(p, 'static') for p in tp]
199 static = [os.path.join(p, 'static') for p in tp]
200 staticfile(static, fname, req)
200 staticfile(static, fname, req)
201 return []
201 return []
202
202
203 # top-level index
203 # top-level index
204 elif not virtual:
204 elif not virtual:
205 req.respond(HTTP_OK, ctype)
205 req.respond(HTTP_OK, ctype)
206 return self.makeindex(req, tmpl)
206 return self.makeindex(req, tmpl)
207
207
208 # nested indexes and hgwebs
208 # nested indexes and hgwebs
209
209
210 repos = dict(self.repos)
210 repos = dict(self.repos)
211 virtualrepo = virtual
211 virtualrepo = virtual
212 while virtualrepo:
212 while virtualrepo:
213 real = repos.get(virtualrepo)
213 real = repos.get(virtualrepo)
214 if real:
214 if real:
215 req.env['REPO_NAME'] = virtualrepo
215 req.env['REPO_NAME'] = virtualrepo
216 try:
216 try:
217 repo = hg.repository(self.ui, real)
217 # ensure caller gets private copy of ui
218 repo = hg.repository(self.ui.copy(), real)
218 return hgweb(repo).run_wsgi(req)
219 return hgweb(repo).run_wsgi(req)
219 except IOError, inst:
220 except IOError, inst:
220 msg = inst.strerror
221 msg = inst.strerror
221 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
222 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
222 except error.RepoError, inst:
223 except error.RepoError, inst:
223 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
224 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
224
225
225 up = virtualrepo.rfind('/')
226 up = virtualrepo.rfind('/')
226 if up < 0:
227 if up < 0:
227 break
228 break
228 virtualrepo = virtualrepo[:up]
229 virtualrepo = virtualrepo[:up]
229
230
230 # browse subdirectories
231 # browse subdirectories
231 subdir = virtual + '/'
232 subdir = virtual + '/'
232 if [r for r in repos if r.startswith(subdir)]:
233 if [r for r in repos if r.startswith(subdir)]:
233 req.respond(HTTP_OK, ctype)
234 req.respond(HTTP_OK, ctype)
234 return self.makeindex(req, tmpl, subdir)
235 return self.makeindex(req, tmpl, subdir)
235
236
236 # prefixes not found
237 # prefixes not found
237 req.respond(HTTP_NOT_FOUND, ctype)
238 req.respond(HTTP_NOT_FOUND, ctype)
238 return tmpl("notfound", repo=virtual)
239 return tmpl("notfound", repo=virtual)
239
240
240 except ErrorResponse, err:
241 except ErrorResponse, err:
241 req.respond(err, ctype)
242 req.respond(err, ctype)
242 return tmpl('error', error=err.message or '')
243 return tmpl('error', error=err.message or '')
243 finally:
244 finally:
244 tmpl = None
245 tmpl = None
245
246
246 def makeindex(self, req, tmpl, subdir=""):
247 def makeindex(self, req, tmpl, subdir=""):
247
248
248 def archivelist(ui, nodeid, url):
249 def archivelist(ui, nodeid, url):
249 allowed = ui.configlist("web", "allow_archive", untrusted=True)
250 allowed = ui.configlist("web", "allow_archive", untrusted=True)
250 archives = []
251 archives = []
251 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
252 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
252 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
253 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
253 untrusted=True):
254 untrusted=True):
254 archives.append({"type" : i[0], "extension": i[1],
255 archives.append({"type" : i[0], "extension": i[1],
255 "node": nodeid, "url": url})
256 "node": nodeid, "url": url})
256 return archives
257 return archives
257
258
258 def rawentries(subdir="", **map):
259 def rawentries(subdir="", **map):
259
260
260 descend = self.ui.configbool('web', 'descend', True)
261 descend = self.ui.configbool('web', 'descend', True)
261 collapse = self.ui.configbool('web', 'collapse', False)
262 collapse = self.ui.configbool('web', 'collapse', False)
262 seenrepos = set()
263 seenrepos = set()
263 seendirs = set()
264 seendirs = set()
264 for name, path in self.repos:
265 for name, path in self.repos:
265
266
266 if not name.startswith(subdir):
267 if not name.startswith(subdir):
267 continue
268 continue
268 name = name[len(subdir):]
269 name = name[len(subdir):]
269 directory = False
270 directory = False
270
271
271 if '/' in name:
272 if '/' in name:
272 if not descend:
273 if not descend:
273 continue
274 continue
274
275
275 nameparts = name.split('/')
276 nameparts = name.split('/')
276 rootname = nameparts[0]
277 rootname = nameparts[0]
277
278
278 if not collapse:
279 if not collapse:
279 pass
280 pass
280 elif rootname in seendirs:
281 elif rootname in seendirs:
281 continue
282 continue
282 elif rootname in seenrepos:
283 elif rootname in seenrepos:
283 pass
284 pass
284 else:
285 else:
285 directory = True
286 directory = True
286 name = rootname
287 name = rootname
287
288
288 # redefine the path to refer to the directory
289 # redefine the path to refer to the directory
289 discarded = '/'.join(nameparts[1:])
290 discarded = '/'.join(nameparts[1:])
290
291
291 # remove name parts plus accompanying slash
292 # remove name parts plus accompanying slash
292 path = path[:-len(discarded) - 1]
293 path = path[:-len(discarded) - 1]
293
294
294 parts = [name]
295 parts = [name]
295 if 'PATH_INFO' in req.env:
296 if 'PATH_INFO' in req.env:
296 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
297 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
297 if req.env['SCRIPT_NAME']:
298 if req.env['SCRIPT_NAME']:
298 parts.insert(0, req.env['SCRIPT_NAME'])
299 parts.insert(0, req.env['SCRIPT_NAME'])
299 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
300 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
300
301
301 # show either a directory entry or a repository
302 # show either a directory entry or a repository
302 if directory:
303 if directory:
303 # get the directory's time information
304 # get the directory's time information
304 try:
305 try:
305 d = (get_mtime(path), util.makedate()[1])
306 d = (get_mtime(path), util.makedate()[1])
306 except OSError:
307 except OSError:
307 continue
308 continue
308
309
309 # add '/' to the name to make it obvious that
310 # add '/' to the name to make it obvious that
310 # the entry is a directory, not a regular repository
311 # the entry is a directory, not a regular repository
311 row = {'contact': "",
312 row = {'contact': "",
312 'contact_sort': "",
313 'contact_sort': "",
313 'name': name + '/',
314 'name': name + '/',
314 'name_sort': name,
315 'name_sort': name,
315 'url': url,
316 'url': url,
316 'description': "",
317 'description': "",
317 'description_sort': "",
318 'description_sort': "",
318 'lastchange': d,
319 'lastchange': d,
319 'lastchange_sort': d[1]-d[0],
320 'lastchange_sort': d[1]-d[0],
320 'archives': [],
321 'archives': [],
321 'isdirectory': True}
322 'isdirectory': True}
322
323
323 seendirs.add(name)
324 seendirs.add(name)
324 yield row
325 yield row
325 continue
326 continue
326
327
327 u = self.ui.copy()
328 u = self.ui.copy()
328 try:
329 try:
329 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
330 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
330 except Exception, e:
331 except Exception, e:
331 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
332 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
332 continue
333 continue
333 def get(section, name, default=None):
334 def get(section, name, default=None):
334 return u.config(section, name, default, untrusted=True)
335 return u.config(section, name, default, untrusted=True)
335
336
336 if u.configbool("web", "hidden", untrusted=True):
337 if u.configbool("web", "hidden", untrusted=True):
337 continue
338 continue
338
339
339 if not self.read_allowed(u, req):
340 if not self.read_allowed(u, req):
340 continue
341 continue
341
342
342 # update time with local timezone
343 # update time with local timezone
343 try:
344 try:
344 r = hg.repository(self.ui, path)
345 r = hg.repository(self.ui, path)
345 except IOError:
346 except IOError:
346 u.warn(_('error accessing repository at %s\n') % path)
347 u.warn(_('error accessing repository at %s\n') % path)
347 continue
348 continue
348 except error.RepoError:
349 except error.RepoError:
349 u.warn(_('error accessing repository at %s\n') % path)
350 u.warn(_('error accessing repository at %s\n') % path)
350 continue
351 continue
351 try:
352 try:
352 d = (get_mtime(r.spath), util.makedate()[1])
353 d = (get_mtime(r.spath), util.makedate()[1])
353 except OSError:
354 except OSError:
354 continue
355 continue
355
356
356 contact = get_contact(get)
357 contact = get_contact(get)
357 description = get("web", "description", "")
358 description = get("web", "description", "")
358 name = get("web", "name", name)
359 name = get("web", "name", name)
359 row = {'contact': contact or "unknown",
360 row = {'contact': contact or "unknown",
360 'contact_sort': contact.upper() or "unknown",
361 'contact_sort': contact.upper() or "unknown",
361 'name': name,
362 'name': name,
362 'name_sort': name,
363 'name_sort': name,
363 'url': url,
364 'url': url,
364 'description': description or "unknown",
365 'description': description or "unknown",
365 'description_sort': description.upper() or "unknown",
366 'description_sort': description.upper() or "unknown",
366 'lastchange': d,
367 'lastchange': d,
367 'lastchange_sort': d[1]-d[0],
368 'lastchange_sort': d[1]-d[0],
368 'archives': archivelist(u, "tip", url),
369 'archives': archivelist(u, "tip", url),
369 'isdirectory': None,
370 'isdirectory': None,
370 }
371 }
371
372
372 seenrepos.add(name)
373 seenrepos.add(name)
373 yield row
374 yield row
374
375
375 sortdefault = None, False
376 sortdefault = None, False
376 def entries(sortcolumn="", descending=False, subdir="", **map):
377 def entries(sortcolumn="", descending=False, subdir="", **map):
377 rows = rawentries(subdir=subdir, **map)
378 rows = rawentries(subdir=subdir, **map)
378
379
379 if sortcolumn and sortdefault != (sortcolumn, descending):
380 if sortcolumn and sortdefault != (sortcolumn, descending):
380 sortkey = '%s_sort' % sortcolumn
381 sortkey = '%s_sort' % sortcolumn
381 rows = sorted(rows, key=lambda x: x[sortkey],
382 rows = sorted(rows, key=lambda x: x[sortkey],
382 reverse=descending)
383 reverse=descending)
383 for row, parity in zip(rows, paritygen(self.stripecount)):
384 for row, parity in zip(rows, paritygen(self.stripecount)):
384 row['parity'] = parity
385 row['parity'] = parity
385 yield row
386 yield row
386
387
387 self.refresh()
388 self.refresh()
388 sortable = ["name", "description", "contact", "lastchange"]
389 sortable = ["name", "description", "contact", "lastchange"]
389 sortcolumn, descending = sortdefault
390 sortcolumn, descending = sortdefault
390 if 'sort' in req.form:
391 if 'sort' in req.form:
391 sortcolumn = req.form['sort'][0]
392 sortcolumn = req.form['sort'][0]
392 descending = sortcolumn.startswith('-')
393 descending = sortcolumn.startswith('-')
393 if descending:
394 if descending:
394 sortcolumn = sortcolumn[1:]
395 sortcolumn = sortcolumn[1:]
395 if sortcolumn not in sortable:
396 if sortcolumn not in sortable:
396 sortcolumn = ""
397 sortcolumn = ""
397
398
398 sort = [("sort_%s" % column,
399 sort = [("sort_%s" % column,
399 "%s%s" % ((not descending and column == sortcolumn)
400 "%s%s" % ((not descending and column == sortcolumn)
400 and "-" or "", column))
401 and "-" or "", column))
401 for column in sortable]
402 for column in sortable]
402
403
403 self.refresh()
404 self.refresh()
404 self.updatereqenv(req.env)
405 self.updatereqenv(req.env)
405
406
406 return tmpl("index", entries=entries, subdir=subdir,
407 return tmpl("index", entries=entries, subdir=subdir,
407 pathdef=makebreadcrumb('/' + subdir, self.prefix),
408 pathdef=makebreadcrumb('/' + subdir, self.prefix),
408 sortcolumn=sortcolumn, descending=descending,
409 sortcolumn=sortcolumn, descending=descending,
409 **dict(sort))
410 **dict(sort))
410
411
411 def templater(self, req):
412 def templater(self, req):
412
413
413 def motd(**map):
414 def motd(**map):
414 if self.motd is not None:
415 if self.motd is not None:
415 yield self.motd
416 yield self.motd
416 else:
417 else:
417 yield config('web', 'motd', '')
418 yield config('web', 'motd', '')
418
419
419 def config(section, name, default=None, untrusted=True):
420 def config(section, name, default=None, untrusted=True):
420 return self.ui.config(section, name, default, untrusted)
421 return self.ui.config(section, name, default, untrusted)
421
422
422 self.updatereqenv(req.env)
423 self.updatereqenv(req.env)
423
424
424 url = req.env.get('SCRIPT_NAME', '')
425 url = req.env.get('SCRIPT_NAME', '')
425 if not url.endswith('/'):
426 if not url.endswith('/'):
426 url += '/'
427 url += '/'
427
428
428 vars = {}
429 vars = {}
429 styles = (
430 styles = (
430 req.form.get('style', [None])[0],
431 req.form.get('style', [None])[0],
431 config('web', 'style'),
432 config('web', 'style'),
432 'paper'
433 'paper'
433 )
434 )
434 style, mapfile = templater.stylemap(styles, self.templatepath)
435 style, mapfile = templater.stylemap(styles, self.templatepath)
435 if style == styles[0]:
436 if style == styles[0]:
436 vars['style'] = style
437 vars['style'] = style
437
438
438 start = url[-1] == '?' and '&' or '?'
439 start = url[-1] == '?' and '&' or '?'
439 sessionvars = webutil.sessionvars(vars, start)
440 sessionvars = webutil.sessionvars(vars, start)
440 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
441 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
441 logoimg = config('web', 'logoimg', 'hglogo.png')
442 logoimg = config('web', 'logoimg', 'hglogo.png')
442 staticurl = config('web', 'staticurl') or url + 'static/'
443 staticurl = config('web', 'staticurl') or url + 'static/'
443 if not staticurl.endswith('/'):
444 if not staticurl.endswith('/'):
444 staticurl += '/'
445 staticurl += '/'
445
446
446 tmpl = templater.templater(mapfile,
447 tmpl = templater.templater(mapfile,
447 defaults={"encoding": encoding.encoding,
448 defaults={"encoding": encoding.encoding,
448 "motd": motd,
449 "motd": motd,
449 "url": url,
450 "url": url,
450 "logourl": logourl,
451 "logourl": logourl,
451 "logoimg": logoimg,
452 "logoimg": logoimg,
452 "staticurl": staticurl,
453 "staticurl": staticurl,
453 "sessionvars": sessionvars,
454 "sessionvars": sessionvars,
454 "style": style,
455 "style": style,
455 })
456 })
456 return tmpl
457 return tmpl
457
458
458 def updatereqenv(self, env):
459 def updatereqenv(self, env):
459 if self._baseurl is not None:
460 if self._baseurl is not None:
460 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
461 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
461 env['SERVER_NAME'] = name
462 env['SERVER_NAME'] = name
462 env['SERVER_PORT'] = port
463 env['SERVER_PORT'] = port
463 env['SCRIPT_NAME'] = path
464 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now