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