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