##// END OF EJS Templates
hgweb: add some documentation...
Gregory Szorc -
r26132:9df8c729 default
parent child Browse files
Show More
@@ -1,424 +1,447
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 ## Files of interest
29 ## Files of interest
30 # Used to check if the repository has changed looking at mtime and size of
30 # Used to check if the repository has changed looking at mtime and size of
31 # theses files. This should probably be relocated a bit higher in core.
31 # theses files. This should probably be relocated a bit higher in core.
32 foi = [('spath', '00changelog.i'),
32 foi = [('spath', '00changelog.i'),
33 ('spath', 'phaseroots'), # ! phase can change content at the same size
33 ('spath', 'phaseroots'), # ! phase can change content at the same size
34 ('spath', 'obsstore'),
34 ('spath', 'obsstore'),
35 ('path', 'bookmarks'), # ! bookmark can change content at the same size
35 ('path', 'bookmarks'), # ! bookmark can change content at the same size
36 ]
36 ]
37
37
38 def makebreadcrumb(url, prefix=''):
38 def makebreadcrumb(url, prefix=''):
39 '''Return a 'URL breadcrumb' list
39 '''Return a 'URL breadcrumb' list
40
40
41 A 'URL breadcrumb' is a list of URL-name pairs,
41 A 'URL breadcrumb' is a list of URL-name pairs,
42 corresponding to each of the path items on a URL.
42 corresponding to each of the path items on a URL.
43 This can be used to create path navigation entries.
43 This can be used to create path navigation entries.
44 '''
44 '''
45 if url.endswith('/'):
45 if url.endswith('/'):
46 url = url[:-1]
46 url = url[:-1]
47 if prefix:
47 if prefix:
48 url = '/' + prefix + url
48 url = '/' + prefix + url
49 relpath = url
49 relpath = url
50 if relpath.startswith('/'):
50 if relpath.startswith('/'):
51 relpath = relpath[1:]
51 relpath = relpath[1:]
52
52
53 breadcrumb = []
53 breadcrumb = []
54 urlel = url
54 urlel = url
55 pathitems = [''] + relpath.split('/')
55 pathitems = [''] + relpath.split('/')
56 for pathel in reversed(pathitems):
56 for pathel in reversed(pathitems):
57 if not pathel or not urlel:
57 if not pathel or not urlel:
58 break
58 break
59 breadcrumb.append({'url': urlel, 'name': pathel})
59 breadcrumb.append({'url': urlel, 'name': pathel})
60 urlel = os.path.dirname(urlel)
60 urlel = os.path.dirname(urlel)
61 return reversed(breadcrumb)
61 return reversed(breadcrumb)
62
62
63
63
64 class hgweb(object):
64 class hgweb(object):
65 """HTTP server for individual repositories.
66
67 Instances of this class serve HTTP responses for a particular
68 repository.
69
70 Instances are typically used as WSGI applications.
71
72 Some servers are multi-threaded. On these servers, there may
73 be multiple active threads inside __call__.
74 """
65 def __init__(self, repo, name=None, baseui=None):
75 def __init__(self, repo, name=None, baseui=None):
66 if isinstance(repo, str):
76 if isinstance(repo, str):
67 if baseui:
77 if baseui:
68 u = baseui.copy()
78 u = baseui.copy()
69 else:
79 else:
70 u = ui.ui()
80 u = ui.ui()
71 r = hg.repository(u, repo)
81 r = hg.repository(u, repo)
72 else:
82 else:
73 # we trust caller to give us a private copy
83 # we trust caller to give us a private copy
74 r = repo
84 r = repo
75
85
76 r = self._getview(r)
86 r = self._getview(r)
77 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
87 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
78 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
88 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
79 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
89 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
80 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
90 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
81 # displaying bundling progress bar while serving feel wrong and may
91 # displaying bundling progress bar while serving feel wrong and may
82 # break some wsgi implementation.
92 # break some wsgi implementation.
83 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
93 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
84 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
94 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
85 self.repo = r
95 self.repo = r
86 hook.redirect(True)
96 hook.redirect(True)
87 self.repostate = ((-1, -1), (-1, -1))
97 self.repostate = ((-1, -1), (-1, -1))
88 self.mtime = -1
98 self.mtime = -1
89 self.reponame = name
99 self.reponame = name
90 self.archives = 'zip', 'gz', 'bz2'
100 self.archives = 'zip', 'gz', 'bz2'
91 self.stripecount = 1
101 self.stripecount = 1
92 # a repo owner may set web.templates in .hg/hgrc to get any file
102 # a repo owner may set web.templates in .hg/hgrc to get any file
93 # readable by the user running the CGI script
103 # readable by the user running the CGI script
94 self.templatepath = self.config('web', 'templates')
104 self.templatepath = self.config('web', 'templates')
95 self.websubtable = self.loadwebsub()
105 self.websubtable = self.loadwebsub()
96
106
97 # The CGI scripts are often run by a user different from the repo owner.
107 # The CGI scripts are often run by a user different from the repo owner.
98 # Trust the settings from the .hg/hgrc files by default.
108 # Trust the settings from the .hg/hgrc files by default.
99 def config(self, section, name, default=None, untrusted=True):
109 def config(self, section, name, default=None, untrusted=True):
100 return self.repo.ui.config(section, name, default,
110 return self.repo.ui.config(section, name, default,
101 untrusted=untrusted)
111 untrusted=untrusted)
102
112
103 def configbool(self, section, name, default=False, untrusted=True):
113 def configbool(self, section, name, default=False, untrusted=True):
104 return self.repo.ui.configbool(section, name, default,
114 return self.repo.ui.configbool(section, name, default,
105 untrusted=untrusted)
115 untrusted=untrusted)
106
116
107 def configlist(self, section, name, default=None, untrusted=True):
117 def configlist(self, section, name, default=None, untrusted=True):
108 return self.repo.ui.configlist(section, name, default,
118 return self.repo.ui.configlist(section, name, default,
109 untrusted=untrusted)
119 untrusted=untrusted)
110
120
111 def _getview(self, repo):
121 def _getview(self, repo):
112 """The 'web.view' config controls changeset filter to hgweb. Possible
122 """The 'web.view' config controls changeset filter to hgweb. Possible
113 values are ``served``, ``visible`` and ``all``. Default is ``served``.
123 values are ``served``, ``visible`` and ``all``. Default is ``served``.
114 The ``served`` filter only shows changesets that can be pulled from the
124 The ``served`` filter only shows changesets that can be pulled from the
115 hgweb instance. The``visible`` filter includes secret changesets but
125 hgweb instance. The``visible`` filter includes secret changesets but
116 still excludes "hidden" one.
126 still excludes "hidden" one.
117
127
118 See the repoview module for details.
128 See the repoview module for details.
119
129
120 The option has been around undocumented since Mercurial 2.5, but no
130 The option has been around undocumented since Mercurial 2.5, but no
121 user ever asked about it. So we better keep it undocumented for now."""
131 user ever asked about it. So we better keep it undocumented for now."""
122 viewconfig = repo.ui.config('web', 'view', 'served',
132 viewconfig = repo.ui.config('web', 'view', 'served',
123 untrusted=True)
133 untrusted=True)
124 if viewconfig == 'all':
134 if viewconfig == 'all':
125 return repo.unfiltered()
135 return repo.unfiltered()
126 elif viewconfig in repoview.filtertable:
136 elif viewconfig in repoview.filtertable:
127 return repo.filtered(viewconfig)
137 return repo.filtered(viewconfig)
128 else:
138 else:
129 return repo.filtered('served')
139 return repo.filtered('served')
130
140
131 def refresh(self, request=None):
141 def refresh(self, request=None):
132 repostate = []
142 repostate = []
133 # file of interrests mtime and size
143 # file of interrests mtime and size
134 for meth, fname in foi:
144 for meth, fname in foi:
135 prefix = getattr(self.repo, meth)
145 prefix = getattr(self.repo, meth)
136 st = get_stat(prefix, fname)
146 st = get_stat(prefix, fname)
137 repostate.append((st.st_mtime, st.st_size))
147 repostate.append((st.st_mtime, st.st_size))
138 repostate = tuple(repostate)
148 repostate = tuple(repostate)
139 # we need to compare file size in addition to mtime to catch
149 # we need to compare file size in addition to mtime to catch
140 # changes made less than a second ago
150 # changes made less than a second ago
141 if repostate != self.repostate:
151 if repostate != self.repostate:
142 r = hg.repository(self.repo.baseui, self.repo.url())
152 r = hg.repository(self.repo.baseui, self.repo.url())
143 self.repo = self._getview(r)
153 self.repo = self._getview(r)
144 self.maxchanges = int(self.config("web", "maxchanges", 10))
154 self.maxchanges = int(self.config("web", "maxchanges", 10))
145 self.stripecount = int(self.config("web", "stripes", 1))
155 self.stripecount = int(self.config("web", "stripes", 1))
146 self.maxshortchanges = int(self.config("web", "maxshortchanges",
156 self.maxshortchanges = int(self.config("web", "maxshortchanges",
147 60))
157 60))
148 self.maxfiles = int(self.config("web", "maxfiles", 10))
158 self.maxfiles = int(self.config("web", "maxfiles", 10))
149 self.allowpull = self.configbool("web", "allowpull", True)
159 self.allowpull = self.configbool("web", "allowpull", True)
150 encoding.encoding = self.config("web", "encoding",
160 encoding.encoding = self.config("web", "encoding",
151 encoding.encoding)
161 encoding.encoding)
152 # update these last to avoid threads seeing empty settings
162 # update these last to avoid threads seeing empty settings
153 self.repostate = repostate
163 self.repostate = repostate
154 # mtime is needed for ETag
164 # mtime is needed for ETag
155 self.mtime = st.st_mtime
165 self.mtime = st.st_mtime
156 if request:
166 if request:
157 self.repo.ui.environ = request.env
167 self.repo.ui.environ = request.env
158
168
159 def run(self):
169 def run(self):
170 """Start a server from CGI environment.
171
172 Modern servers should be using WSGI and should avoid this
173 method, if possible.
174 """
160 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
175 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
161 raise RuntimeError("This function is only intended to be "
176 raise RuntimeError("This function is only intended to be "
162 "called while running as a CGI script.")
177 "called while running as a CGI script.")
163 import mercurial.hgweb.wsgicgi as wsgicgi
178 import mercurial.hgweb.wsgicgi as wsgicgi
164 wsgicgi.launch(self)
179 wsgicgi.launch(self)
165
180
166 def __call__(self, env, respond):
181 def __call__(self, env, respond):
182 """Run the WSGI application.
183
184 This may be called by multiple threads.
185 """
167 req = wsgirequest(env, respond)
186 req = wsgirequest(env, respond)
168 return self.run_wsgi(req)
187 return self.run_wsgi(req)
169
188
170 def run_wsgi(self, req):
189 def run_wsgi(self, req):
190 """Internal method to run the WSGI application.
171
191
192 This is typically only called by Mercurial. External consumers
193 should be using instances of this class as the WSGI application.
194 """
172 self.refresh(req)
195 self.refresh(req)
173
196
174 # work with CGI variables to create coherent structure
197 # work with CGI variables to create coherent structure
175 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
198 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
176
199
177 req.url = req.env['SCRIPT_NAME']
200 req.url = req.env['SCRIPT_NAME']
178 if not req.url.endswith('/'):
201 if not req.url.endswith('/'):
179 req.url += '/'
202 req.url += '/'
180 if 'REPO_NAME' in req.env:
203 if 'REPO_NAME' in req.env:
181 req.url += req.env['REPO_NAME'] + '/'
204 req.url += req.env['REPO_NAME'] + '/'
182
205
183 if 'PATH_INFO' in req.env:
206 if 'PATH_INFO' in req.env:
184 parts = req.env['PATH_INFO'].strip('/').split('/')
207 parts = req.env['PATH_INFO'].strip('/').split('/')
185 repo_parts = req.env.get('REPO_NAME', '').split('/')
208 repo_parts = req.env.get('REPO_NAME', '').split('/')
186 if parts[:len(repo_parts)] == repo_parts:
209 if parts[:len(repo_parts)] == repo_parts:
187 parts = parts[len(repo_parts):]
210 parts = parts[len(repo_parts):]
188 query = '/'.join(parts)
211 query = '/'.join(parts)
189 else:
212 else:
190 query = req.env['QUERY_STRING'].split('&', 1)[0]
213 query = req.env['QUERY_STRING'].split('&', 1)[0]
191 query = query.split(';', 1)[0]
214 query = query.split(';', 1)[0]
192
215
193 # process this if it's a protocol request
216 # process this if it's a protocol request
194 # protocol bits don't need to create any URLs
217 # protocol bits don't need to create any URLs
195 # and the clients always use the old URL structure
218 # and the clients always use the old URL structure
196
219
197 cmd = req.form.get('cmd', [''])[0]
220 cmd = req.form.get('cmd', [''])[0]
198 if protocol.iscmd(cmd):
221 if protocol.iscmd(cmd):
199 try:
222 try:
200 if query:
223 if query:
201 raise ErrorResponse(HTTP_NOT_FOUND)
224 raise ErrorResponse(HTTP_NOT_FOUND)
202 if cmd in perms:
225 if cmd in perms:
203 self.check_perm(req, perms[cmd])
226 self.check_perm(req, perms[cmd])
204 return protocol.call(self.repo, req, cmd)
227 return protocol.call(self.repo, req, cmd)
205 except ErrorResponse as inst:
228 except ErrorResponse as inst:
206 # A client that sends unbundle without 100-continue will
229 # A client that sends unbundle without 100-continue will
207 # break if we respond early.
230 # break if we respond early.
208 if (cmd == 'unbundle' and
231 if (cmd == 'unbundle' and
209 (req.env.get('HTTP_EXPECT',
232 (req.env.get('HTTP_EXPECT',
210 '').lower() != '100-continue') or
233 '').lower() != '100-continue') or
211 req.env.get('X-HgHttp2', '')):
234 req.env.get('X-HgHttp2', '')):
212 req.drain()
235 req.drain()
213 else:
236 else:
214 req.headers.append(('Connection', 'Close'))
237 req.headers.append(('Connection', 'Close'))
215 req.respond(inst, protocol.HGTYPE,
238 req.respond(inst, protocol.HGTYPE,
216 body='0\n%s\n' % inst.message)
239 body='0\n%s\n' % inst.message)
217 return ''
240 return ''
218
241
219 # translate user-visible url structure to internal structure
242 # translate user-visible url structure to internal structure
220
243
221 args = query.split('/', 2)
244 args = query.split('/', 2)
222 if 'cmd' not in req.form and args and args[0]:
245 if 'cmd' not in req.form and args and args[0]:
223
246
224 cmd = args.pop(0)
247 cmd = args.pop(0)
225 style = cmd.rfind('-')
248 style = cmd.rfind('-')
226 if style != -1:
249 if style != -1:
227 req.form['style'] = [cmd[:style]]
250 req.form['style'] = [cmd[:style]]
228 cmd = cmd[style + 1:]
251 cmd = cmd[style + 1:]
229
252
230 # avoid accepting e.g. style parameter as command
253 # avoid accepting e.g. style parameter as command
231 if util.safehasattr(webcommands, cmd):
254 if util.safehasattr(webcommands, cmd):
232 req.form['cmd'] = [cmd]
255 req.form['cmd'] = [cmd]
233
256
234 if cmd == 'static':
257 if cmd == 'static':
235 req.form['file'] = ['/'.join(args)]
258 req.form['file'] = ['/'.join(args)]
236 else:
259 else:
237 if args and args[0]:
260 if args and args[0]:
238 node = args.pop(0).replace('%2F', '/')
261 node = args.pop(0).replace('%2F', '/')
239 req.form['node'] = [node]
262 req.form['node'] = [node]
240 if args:
263 if args:
241 req.form['file'] = args
264 req.form['file'] = args
242
265
243 ua = req.env.get('HTTP_USER_AGENT', '')
266 ua = req.env.get('HTTP_USER_AGENT', '')
244 if cmd == 'rev' and 'mercurial' in ua:
267 if cmd == 'rev' and 'mercurial' in ua:
245 req.form['style'] = ['raw']
268 req.form['style'] = ['raw']
246
269
247 if cmd == 'archive':
270 if cmd == 'archive':
248 fn = req.form['node'][0]
271 fn = req.form['node'][0]
249 for type_, spec in self.archive_specs.iteritems():
272 for type_, spec in self.archive_specs.iteritems():
250 ext = spec[2]
273 ext = spec[2]
251 if fn.endswith(ext):
274 if fn.endswith(ext):
252 req.form['node'] = [fn[:-len(ext)]]
275 req.form['node'] = [fn[:-len(ext)]]
253 req.form['type'] = [type_]
276 req.form['type'] = [type_]
254
277
255 # process the web interface request
278 # process the web interface request
256
279
257 try:
280 try:
258 tmpl = self.templater(req)
281 tmpl = self.templater(req)
259 ctype = tmpl('mimetype', encoding=encoding.encoding)
282 ctype = tmpl('mimetype', encoding=encoding.encoding)
260 ctype = templater.stringify(ctype)
283 ctype = templater.stringify(ctype)
261
284
262 # check read permissions non-static content
285 # check read permissions non-static content
263 if cmd != 'static':
286 if cmd != 'static':
264 self.check_perm(req, None)
287 self.check_perm(req, None)
265
288
266 if cmd == '':
289 if cmd == '':
267 req.form['cmd'] = [tmpl.cache['default']]
290 req.form['cmd'] = [tmpl.cache['default']]
268 cmd = req.form['cmd'][0]
291 cmd = req.form['cmd'][0]
269
292
270 if self.configbool('web', 'cache', True):
293 if self.configbool('web', 'cache', True):
271 caching(self, req) # sets ETag header or raises NOT_MODIFIED
294 caching(self, req) # sets ETag header or raises NOT_MODIFIED
272 if cmd not in webcommands.__all__:
295 if cmd not in webcommands.__all__:
273 msg = 'no such method: %s' % cmd
296 msg = 'no such method: %s' % cmd
274 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
297 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
275 elif cmd == 'file' and 'raw' in req.form.get('style', []):
298 elif cmd == 'file' and 'raw' in req.form.get('style', []):
276 self.ctype = ctype
299 self.ctype = ctype
277 content = webcommands.rawfile(self, req, tmpl)
300 content = webcommands.rawfile(self, req, tmpl)
278 else:
301 else:
279 content = getattr(webcommands, cmd)(self, req, tmpl)
302 content = getattr(webcommands, cmd)(self, req, tmpl)
280 req.respond(HTTP_OK, ctype)
303 req.respond(HTTP_OK, ctype)
281
304
282 return content
305 return content
283
306
284 except (error.LookupError, error.RepoLookupError) as err:
307 except (error.LookupError, error.RepoLookupError) as err:
285 req.respond(HTTP_NOT_FOUND, ctype)
308 req.respond(HTTP_NOT_FOUND, ctype)
286 msg = str(err)
309 msg = str(err)
287 if (util.safehasattr(err, 'name') and
310 if (util.safehasattr(err, 'name') and
288 not isinstance(err, error.ManifestLookupError)):
311 not isinstance(err, error.ManifestLookupError)):
289 msg = 'revision not found: %s' % err.name
312 msg = 'revision not found: %s' % err.name
290 return tmpl('error', error=msg)
313 return tmpl('error', error=msg)
291 except (error.RepoError, error.RevlogError) as inst:
314 except (error.RepoError, error.RevlogError) as inst:
292 req.respond(HTTP_SERVER_ERROR, ctype)
315 req.respond(HTTP_SERVER_ERROR, ctype)
293 return tmpl('error', error=str(inst))
316 return tmpl('error', error=str(inst))
294 except ErrorResponse as inst:
317 except ErrorResponse as inst:
295 req.respond(inst, ctype)
318 req.respond(inst, ctype)
296 if inst.code == HTTP_NOT_MODIFIED:
319 if inst.code == HTTP_NOT_MODIFIED:
297 # Not allowed to return a body on a 304
320 # Not allowed to return a body on a 304
298 return ['']
321 return ['']
299 return tmpl('error', error=inst.message)
322 return tmpl('error', error=inst.message)
300
323
301 def loadwebsub(self):
324 def loadwebsub(self):
302 websubtable = []
325 websubtable = []
303 websubdefs = self.repo.ui.configitems('websub')
326 websubdefs = self.repo.ui.configitems('websub')
304 # we must maintain interhg backwards compatibility
327 # we must maintain interhg backwards compatibility
305 websubdefs += self.repo.ui.configitems('interhg')
328 websubdefs += self.repo.ui.configitems('interhg')
306 for key, pattern in websubdefs:
329 for key, pattern in websubdefs:
307 # grab the delimiter from the character after the "s"
330 # grab the delimiter from the character after the "s"
308 unesc = pattern[1]
331 unesc = pattern[1]
309 delim = re.escape(unesc)
332 delim = re.escape(unesc)
310
333
311 # identify portions of the pattern, taking care to avoid escaped
334 # identify portions of the pattern, taking care to avoid escaped
312 # delimiters. the replace format and flags are optional, but
335 # delimiters. the replace format and flags are optional, but
313 # delimiters are required.
336 # delimiters are required.
314 match = re.match(
337 match = re.match(
315 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
338 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
316 % (delim, delim, delim), pattern)
339 % (delim, delim, delim), pattern)
317 if not match:
340 if not match:
318 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
341 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
319 % (key, pattern))
342 % (key, pattern))
320 continue
343 continue
321
344
322 # we need to unescape the delimiter for regexp and format
345 # we need to unescape the delimiter for regexp and format
323 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
346 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
324 regexp = delim_re.sub(unesc, match.group(1))
347 regexp = delim_re.sub(unesc, match.group(1))
325 format = delim_re.sub(unesc, match.group(2))
348 format = delim_re.sub(unesc, match.group(2))
326
349
327 # the pattern allows for 6 regexp flags, so set them if necessary
350 # the pattern allows for 6 regexp flags, so set them if necessary
328 flagin = match.group(3)
351 flagin = match.group(3)
329 flags = 0
352 flags = 0
330 if flagin:
353 if flagin:
331 for flag in flagin.upper():
354 for flag in flagin.upper():
332 flags |= re.__dict__[flag]
355 flags |= re.__dict__[flag]
333
356
334 try:
357 try:
335 regexp = re.compile(regexp, flags)
358 regexp = re.compile(regexp, flags)
336 websubtable.append((regexp, format))
359 websubtable.append((regexp, format))
337 except re.error:
360 except re.error:
338 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
361 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
339 % (key, regexp))
362 % (key, regexp))
340 return websubtable
363 return websubtable
341
364
342 def templater(self, req):
365 def templater(self, req):
343
366
344 # determine scheme, port and server name
367 # determine scheme, port and server name
345 # this is needed to create absolute urls
368 # this is needed to create absolute urls
346
369
347 proto = req.env.get('wsgi.url_scheme')
370 proto = req.env.get('wsgi.url_scheme')
348 if proto == 'https':
371 if proto == 'https':
349 proto = 'https'
372 proto = 'https'
350 default_port = "443"
373 default_port = "443"
351 else:
374 else:
352 proto = 'http'
375 proto = 'http'
353 default_port = "80"
376 default_port = "80"
354
377
355 port = req.env["SERVER_PORT"]
378 port = req.env["SERVER_PORT"]
356 port = port != default_port and (":" + port) or ""
379 port = port != default_port and (":" + port) or ""
357 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
380 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
358 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
381 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
359 logoimg = self.config("web", "logoimg", "hglogo.png")
382 logoimg = self.config("web", "logoimg", "hglogo.png")
360 staticurl = self.config("web", "staticurl") or req.url + 'static/'
383 staticurl = self.config("web", "staticurl") or req.url + 'static/'
361 if not staticurl.endswith('/'):
384 if not staticurl.endswith('/'):
362 staticurl += '/'
385 staticurl += '/'
363
386
364 # some functions for the templater
387 # some functions for the templater
365
388
366 def motd(**map):
389 def motd(**map):
367 yield self.config("web", "motd", "")
390 yield self.config("web", "motd", "")
368
391
369 # figure out which style to use
392 # figure out which style to use
370
393
371 vars = {}
394 vars = {}
372 styles = (
395 styles = (
373 req.form.get('style', [None])[0],
396 req.form.get('style', [None])[0],
374 self.config('web', 'style'),
397 self.config('web', 'style'),
375 'paper',
398 'paper',
376 )
399 )
377 style, mapfile = templater.stylemap(styles, self.templatepath)
400 style, mapfile = templater.stylemap(styles, self.templatepath)
378 if style == styles[0]:
401 if style == styles[0]:
379 vars['style'] = style
402 vars['style'] = style
380
403
381 start = req.url[-1] == '?' and '&' or '?'
404 start = req.url[-1] == '?' and '&' or '?'
382 sessionvars = webutil.sessionvars(vars, start)
405 sessionvars = webutil.sessionvars(vars, start)
383
406
384 if not self.reponame:
407 if not self.reponame:
385 self.reponame = (self.config("web", "name")
408 self.reponame = (self.config("web", "name")
386 or req.env.get('REPO_NAME')
409 or req.env.get('REPO_NAME')
387 or req.url.strip('/') or self.repo.root)
410 or req.url.strip('/') or self.repo.root)
388
411
389 def websubfilter(text):
412 def websubfilter(text):
390 return websub(text, self.websubtable)
413 return websub(text, self.websubtable)
391
414
392 # create the templater
415 # create the templater
393
416
394 tmpl = templater.templater(mapfile,
417 tmpl = templater.templater(mapfile,
395 filters={"websub": websubfilter},
418 filters={"websub": websubfilter},
396 defaults={"url": req.url,
419 defaults={"url": req.url,
397 "logourl": logourl,
420 "logourl": logourl,
398 "logoimg": logoimg,
421 "logoimg": logoimg,
399 "staticurl": staticurl,
422 "staticurl": staticurl,
400 "urlbase": urlbase,
423 "urlbase": urlbase,
401 "repo": self.reponame,
424 "repo": self.reponame,
402 "encoding": encoding.encoding,
425 "encoding": encoding.encoding,
403 "motd": motd,
426 "motd": motd,
404 "sessionvars": sessionvars,
427 "sessionvars": sessionvars,
405 "pathdef": makebreadcrumb(req.url),
428 "pathdef": makebreadcrumb(req.url),
406 "style": style,
429 "style": style,
407 })
430 })
408 return tmpl
431 return tmpl
409
432
410 def archivelist(self, nodeid):
433 def archivelist(self, nodeid):
411 allowed = self.configlist("web", "allow_archive")
434 allowed = self.configlist("web", "allow_archive")
412 for i, spec in self.archive_specs.iteritems():
435 for i, spec in self.archive_specs.iteritems():
413 if i in allowed or self.configbool("web", "allow" + i):
436 if i in allowed or self.configbool("web", "allow" + i):
414 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
437 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
415
438
416 archive_specs = {
439 archive_specs = {
417 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
440 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
418 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
441 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
419 'zip': ('application/zip', 'zip', '.zip', None),
442 'zip': ('application/zip', 'zip', '.zip', None),
420 }
443 }
421
444
422 def check_perm(self, req, op):
445 def check_perm(self, req, op):
423 for permhook in permhooks:
446 for permhook in permhooks:
424 permhook(self, req, op)
447 permhook(self, req, op)
@@ -1,478 +1,485
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 """HTTP server for multiple repositories.
83
84 Given a configuration, different repositories will be served depending
85 on the request path.
86
87 Instances are typically used as WSGI applications.
88 """
82 def __init__(self, conf, baseui=None):
89 def __init__(self, conf, baseui=None):
83 self.conf = conf
90 self.conf = conf
84 self.baseui = baseui
91 self.baseui = baseui
85 self.ui = None
92 self.ui = None
86 self.lastrefresh = 0
93 self.lastrefresh = 0
87 self.motd = None
94 self.motd = None
88 self.refresh()
95 self.refresh()
89
96
90 def refresh(self):
97 def refresh(self):
91 refreshinterval = 20
98 refreshinterval = 20
92 if self.ui:
99 if self.ui:
93 refreshinterval = self.ui.configint('web', 'refreshinterval',
100 refreshinterval = self.ui.configint('web', 'refreshinterval',
94 refreshinterval)
101 refreshinterval)
95
102
96 # refreshinterval <= 0 means to always refresh.
103 # refreshinterval <= 0 means to always refresh.
97 if (refreshinterval > 0 and
104 if (refreshinterval > 0 and
98 self.lastrefresh + refreshinterval > time.time()):
105 self.lastrefresh + refreshinterval > time.time()):
99 return
106 return
100
107
101 if self.baseui:
108 if self.baseui:
102 u = self.baseui.copy()
109 u = self.baseui.copy()
103 else:
110 else:
104 u = ui.ui()
111 u = ui.ui()
105 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
112 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
106 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
113 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
107 # displaying bundling progress bar while serving feels wrong and may
114 # displaying bundling progress bar while serving feels wrong and may
108 # break some wsgi implementations.
115 # break some wsgi implementations.
109 u.setconfig('progress', 'disable', 'true', 'hgweb')
116 u.setconfig('progress', 'disable', 'true', 'hgweb')
110
117
111 if not isinstance(self.conf, (dict, list, tuple)):
118 if not isinstance(self.conf, (dict, list, tuple)):
112 map = {'paths': 'hgweb-paths'}
119 map = {'paths': 'hgweb-paths'}
113 if not os.path.exists(self.conf):
120 if not os.path.exists(self.conf):
114 raise util.Abort(_('config file %s not found!') % self.conf)
121 raise util.Abort(_('config file %s not found!') % self.conf)
115 u.readconfig(self.conf, remap=map, trust=True)
122 u.readconfig(self.conf, remap=map, trust=True)
116 paths = []
123 paths = []
117 for name, ignored in u.configitems('hgweb-paths'):
124 for name, ignored in u.configitems('hgweb-paths'):
118 for path in u.configlist('hgweb-paths', name):
125 for path in u.configlist('hgweb-paths', name):
119 paths.append((name, path))
126 paths.append((name, path))
120 elif isinstance(self.conf, (list, tuple)):
127 elif isinstance(self.conf, (list, tuple)):
121 paths = self.conf
128 paths = self.conf
122 elif isinstance(self.conf, dict):
129 elif isinstance(self.conf, dict):
123 paths = self.conf.items()
130 paths = self.conf.items()
124
131
125 repos = findrepos(paths)
132 repos = findrepos(paths)
126 for prefix, root in u.configitems('collections'):
133 for prefix, root in u.configitems('collections'):
127 prefix = util.pconvert(prefix)
134 prefix = util.pconvert(prefix)
128 for path in scmutil.walkrepos(root, followsym=True):
135 for path in scmutil.walkrepos(root, followsym=True):
129 repo = os.path.normpath(path)
136 repo = os.path.normpath(path)
130 name = util.pconvert(repo)
137 name = util.pconvert(repo)
131 if name.startswith(prefix):
138 if name.startswith(prefix):
132 name = name[len(prefix):]
139 name = name[len(prefix):]
133 repos.append((name.lstrip('/'), repo))
140 repos.append((name.lstrip('/'), repo))
134
141
135 self.repos = repos
142 self.repos = repos
136 self.ui = u
143 self.ui = u
137 encoding.encoding = self.ui.config('web', 'encoding',
144 encoding.encoding = self.ui.config('web', 'encoding',
138 encoding.encoding)
145 encoding.encoding)
139 self.style = self.ui.config('web', 'style', 'paper')
146 self.style = self.ui.config('web', 'style', 'paper')
140 self.templatepath = self.ui.config('web', 'templates', None)
147 self.templatepath = self.ui.config('web', 'templates', None)
141 self.stripecount = self.ui.config('web', 'stripes', 1)
148 self.stripecount = self.ui.config('web', 'stripes', 1)
142 if self.stripecount:
149 if self.stripecount:
143 self.stripecount = int(self.stripecount)
150 self.stripecount = int(self.stripecount)
144 self._baseurl = self.ui.config('web', 'baseurl')
151 self._baseurl = self.ui.config('web', 'baseurl')
145 prefix = self.ui.config('web', 'prefix', '')
152 prefix = self.ui.config('web', 'prefix', '')
146 if prefix.startswith('/'):
153 if prefix.startswith('/'):
147 prefix = prefix[1:]
154 prefix = prefix[1:]
148 if prefix.endswith('/'):
155 if prefix.endswith('/'):
149 prefix = prefix[:-1]
156 prefix = prefix[:-1]
150 self.prefix = prefix
157 self.prefix = prefix
151 self.lastrefresh = time.time()
158 self.lastrefresh = time.time()
152
159
153 def run(self):
160 def run(self):
154 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
161 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
155 raise RuntimeError("This function is only intended to be "
162 raise RuntimeError("This function is only intended to be "
156 "called while running as a CGI script.")
163 "called while running as a CGI script.")
157 import mercurial.hgweb.wsgicgi as wsgicgi
164 import mercurial.hgweb.wsgicgi as wsgicgi
158 wsgicgi.launch(self)
165 wsgicgi.launch(self)
159
166
160 def __call__(self, env, respond):
167 def __call__(self, env, respond):
161 req = wsgirequest(env, respond)
168 req = wsgirequest(env, respond)
162 return self.run_wsgi(req)
169 return self.run_wsgi(req)
163
170
164 def read_allowed(self, ui, req):
171 def read_allowed(self, ui, req):
165 """Check allow_read and deny_read config options of a repo's ui object
172 """Check allow_read and deny_read config options of a repo's ui object
166 to determine user permissions. By default, with neither option set (or
173 to determine user permissions. By default, with neither option set (or
167 both empty), allow all users to read the repo. There are two ways a
174 both empty), allow all users to read the repo. There are two ways a
168 user can be denied read access: (1) deny_read is not empty, and the
175 user can be denied read access: (1) deny_read is not empty, and the
169 user is unauthenticated or deny_read contains user (or *), and (2)
176 user is unauthenticated or deny_read contains user (or *), and (2)
170 allow_read is not empty and the user is not in allow_read. Return True
177 allow_read is not empty and the user is not in allow_read. Return True
171 if user is allowed to read the repo, else return False."""
178 if user is allowed to read the repo, else return False."""
172
179
173 user = req.env.get('REMOTE_USER')
180 user = req.env.get('REMOTE_USER')
174
181
175 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
182 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
176 if deny_read and (not user or ismember(ui, user, deny_read)):
183 if deny_read and (not user or ismember(ui, user, deny_read)):
177 return False
184 return False
178
185
179 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
186 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
180 # by default, allow reading if no allow_read option has been set
187 # by default, allow reading if no allow_read option has been set
181 if (not allow_read) or ismember(ui, user, allow_read):
188 if (not allow_read) or ismember(ui, user, allow_read):
182 return True
189 return True
183
190
184 return False
191 return False
185
192
186 def run_wsgi(self, req):
193 def run_wsgi(self, req):
187 try:
194 try:
188 self.refresh()
195 self.refresh()
189
196
190 virtual = req.env.get("PATH_INFO", "").strip('/')
197 virtual = req.env.get("PATH_INFO", "").strip('/')
191 tmpl = self.templater(req)
198 tmpl = self.templater(req)
192 ctype = tmpl('mimetype', encoding=encoding.encoding)
199 ctype = tmpl('mimetype', encoding=encoding.encoding)
193 ctype = templater.stringify(ctype)
200 ctype = templater.stringify(ctype)
194
201
195 # a static file
202 # a static file
196 if virtual.startswith('static/') or 'static' in req.form:
203 if virtual.startswith('static/') or 'static' in req.form:
197 if virtual.startswith('static/'):
204 if virtual.startswith('static/'):
198 fname = virtual[7:]
205 fname = virtual[7:]
199 else:
206 else:
200 fname = req.form['static'][0]
207 fname = req.form['static'][0]
201 static = self.ui.config("web", "static", None,
208 static = self.ui.config("web", "static", None,
202 untrusted=False)
209 untrusted=False)
203 if not static:
210 if not static:
204 tp = self.templatepath or templater.templatepaths()
211 tp = self.templatepath or templater.templatepaths()
205 if isinstance(tp, str):
212 if isinstance(tp, str):
206 tp = [tp]
213 tp = [tp]
207 static = [os.path.join(p, 'static') for p in tp]
214 static = [os.path.join(p, 'static') for p in tp]
208 staticfile(static, fname, req)
215 staticfile(static, fname, req)
209 return []
216 return []
210
217
211 # top-level index
218 # top-level index
212 elif not virtual:
219 elif not virtual:
213 req.respond(HTTP_OK, ctype)
220 req.respond(HTTP_OK, ctype)
214 return self.makeindex(req, tmpl)
221 return self.makeindex(req, tmpl)
215
222
216 # nested indexes and hgwebs
223 # nested indexes and hgwebs
217
224
218 repos = dict(self.repos)
225 repos = dict(self.repos)
219 virtualrepo = virtual
226 virtualrepo = virtual
220 while virtualrepo:
227 while virtualrepo:
221 real = repos.get(virtualrepo)
228 real = repos.get(virtualrepo)
222 if real:
229 if real:
223 req.env['REPO_NAME'] = virtualrepo
230 req.env['REPO_NAME'] = virtualrepo
224 try:
231 try:
225 # ensure caller gets private copy of ui
232 # ensure caller gets private copy of ui
226 repo = hg.repository(self.ui.copy(), real)
233 repo = hg.repository(self.ui.copy(), real)
227 return hgweb(repo).run_wsgi(req)
234 return hgweb(repo).run_wsgi(req)
228 except IOError as inst:
235 except IOError as inst:
229 msg = inst.strerror
236 msg = inst.strerror
230 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
237 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
231 except error.RepoError as inst:
238 except error.RepoError as inst:
232 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
239 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
233
240
234 up = virtualrepo.rfind('/')
241 up = virtualrepo.rfind('/')
235 if up < 0:
242 if up < 0:
236 break
243 break
237 virtualrepo = virtualrepo[:up]
244 virtualrepo = virtualrepo[:up]
238
245
239 # browse subdirectories
246 # browse subdirectories
240 subdir = virtual + '/'
247 subdir = virtual + '/'
241 if [r for r in repos if r.startswith(subdir)]:
248 if [r for r in repos if r.startswith(subdir)]:
242 req.respond(HTTP_OK, ctype)
249 req.respond(HTTP_OK, ctype)
243 return self.makeindex(req, tmpl, subdir)
250 return self.makeindex(req, tmpl, subdir)
244
251
245 # prefixes not found
252 # prefixes not found
246 req.respond(HTTP_NOT_FOUND, ctype)
253 req.respond(HTTP_NOT_FOUND, ctype)
247 return tmpl("notfound", repo=virtual)
254 return tmpl("notfound", repo=virtual)
248
255
249 except ErrorResponse as err:
256 except ErrorResponse as err:
250 req.respond(err, ctype)
257 req.respond(err, ctype)
251 return tmpl('error', error=err.message or '')
258 return tmpl('error', error=err.message or '')
252 finally:
259 finally:
253 tmpl = None
260 tmpl = None
254
261
255 def makeindex(self, req, tmpl, subdir=""):
262 def makeindex(self, req, tmpl, subdir=""):
256
263
257 def archivelist(ui, nodeid, url):
264 def archivelist(ui, nodeid, url):
258 allowed = ui.configlist("web", "allow_archive", untrusted=True)
265 allowed = ui.configlist("web", "allow_archive", untrusted=True)
259 archives = []
266 archives = []
260 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
267 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
261 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
268 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
262 untrusted=True):
269 untrusted=True):
263 archives.append({"type" : i[0], "extension": i[1],
270 archives.append({"type" : i[0], "extension": i[1],
264 "node": nodeid, "url": url})
271 "node": nodeid, "url": url})
265 return archives
272 return archives
266
273
267 def rawentries(subdir="", **map):
274 def rawentries(subdir="", **map):
268
275
269 descend = self.ui.configbool('web', 'descend', True)
276 descend = self.ui.configbool('web', 'descend', True)
270 collapse = self.ui.configbool('web', 'collapse', False)
277 collapse = self.ui.configbool('web', 'collapse', False)
271 seenrepos = set()
278 seenrepos = set()
272 seendirs = set()
279 seendirs = set()
273 for name, path in self.repos:
280 for name, path in self.repos:
274
281
275 if not name.startswith(subdir):
282 if not name.startswith(subdir):
276 continue
283 continue
277 name = name[len(subdir):]
284 name = name[len(subdir):]
278 directory = False
285 directory = False
279
286
280 if '/' in name:
287 if '/' in name:
281 if not descend:
288 if not descend:
282 continue
289 continue
283
290
284 nameparts = name.split('/')
291 nameparts = name.split('/')
285 rootname = nameparts[0]
292 rootname = nameparts[0]
286
293
287 if not collapse:
294 if not collapse:
288 pass
295 pass
289 elif rootname in seendirs:
296 elif rootname in seendirs:
290 continue
297 continue
291 elif rootname in seenrepos:
298 elif rootname in seenrepos:
292 pass
299 pass
293 else:
300 else:
294 directory = True
301 directory = True
295 name = rootname
302 name = rootname
296
303
297 # redefine the path to refer to the directory
304 # redefine the path to refer to the directory
298 discarded = '/'.join(nameparts[1:])
305 discarded = '/'.join(nameparts[1:])
299
306
300 # remove name parts plus accompanying slash
307 # remove name parts plus accompanying slash
301 path = path[:-len(discarded) - 1]
308 path = path[:-len(discarded) - 1]
302
309
303 try:
310 try:
304 r = hg.repository(self.ui, path)
311 r = hg.repository(self.ui, path)
305 directory = False
312 directory = False
306 except (IOError, error.RepoError):
313 except (IOError, error.RepoError):
307 pass
314 pass
308
315
309 parts = [name]
316 parts = [name]
310 if 'PATH_INFO' in req.env:
317 if 'PATH_INFO' in req.env:
311 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
318 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
312 if req.env['SCRIPT_NAME']:
319 if req.env['SCRIPT_NAME']:
313 parts.insert(0, req.env['SCRIPT_NAME'])
320 parts.insert(0, req.env['SCRIPT_NAME'])
314 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
321 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
315
322
316 # show either a directory entry or a repository
323 # show either a directory entry or a repository
317 if directory:
324 if directory:
318 # get the directory's time information
325 # get the directory's time information
319 try:
326 try:
320 d = (get_mtime(path), util.makedate()[1])
327 d = (get_mtime(path), util.makedate()[1])
321 except OSError:
328 except OSError:
322 continue
329 continue
323
330
324 # add '/' to the name to make it obvious that
331 # add '/' to the name to make it obvious that
325 # the entry is a directory, not a regular repository
332 # the entry is a directory, not a regular repository
326 row = {'contact': "",
333 row = {'contact': "",
327 'contact_sort': "",
334 'contact_sort': "",
328 'name': name + '/',
335 'name': name + '/',
329 'name_sort': name,
336 'name_sort': name,
330 'url': url,
337 'url': url,
331 'description': "",
338 'description': "",
332 'description_sort': "",
339 'description_sort': "",
333 'lastchange': d,
340 'lastchange': d,
334 'lastchange_sort': d[1]-d[0],
341 'lastchange_sort': d[1]-d[0],
335 'archives': [],
342 'archives': [],
336 'isdirectory': True}
343 'isdirectory': True}
337
344
338 seendirs.add(name)
345 seendirs.add(name)
339 yield row
346 yield row
340 continue
347 continue
341
348
342 u = self.ui.copy()
349 u = self.ui.copy()
343 try:
350 try:
344 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
351 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
345 except Exception as e:
352 except Exception as e:
346 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
353 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
347 continue
354 continue
348 def get(section, name, default=None):
355 def get(section, name, default=None):
349 return u.config(section, name, default, untrusted=True)
356 return u.config(section, name, default, untrusted=True)
350
357
351 if u.configbool("web", "hidden", untrusted=True):
358 if u.configbool("web", "hidden", untrusted=True):
352 continue
359 continue
353
360
354 if not self.read_allowed(u, req):
361 if not self.read_allowed(u, req):
355 continue
362 continue
356
363
357 # update time with local timezone
364 # update time with local timezone
358 try:
365 try:
359 r = hg.repository(self.ui, path)
366 r = hg.repository(self.ui, path)
360 except IOError:
367 except IOError:
361 u.warn(_('error accessing repository at %s\n') % path)
368 u.warn(_('error accessing repository at %s\n') % path)
362 continue
369 continue
363 except error.RepoError:
370 except error.RepoError:
364 u.warn(_('error accessing repository at %s\n') % path)
371 u.warn(_('error accessing repository at %s\n') % path)
365 continue
372 continue
366 try:
373 try:
367 d = (get_mtime(r.spath), util.makedate()[1])
374 d = (get_mtime(r.spath), util.makedate()[1])
368 except OSError:
375 except OSError:
369 continue
376 continue
370
377
371 contact = get_contact(get)
378 contact = get_contact(get)
372 description = get("web", "description", "")
379 description = get("web", "description", "")
373 seenrepos.add(name)
380 seenrepos.add(name)
374 name = get("web", "name", name)
381 name = get("web", "name", name)
375 row = {'contact': contact or "unknown",
382 row = {'contact': contact or "unknown",
376 'contact_sort': contact.upper() or "unknown",
383 'contact_sort': contact.upper() or "unknown",
377 'name': name,
384 'name': name,
378 'name_sort': name,
385 'name_sort': name,
379 'url': url,
386 'url': url,
380 'description': description or "unknown",
387 'description': description or "unknown",
381 'description_sort': description.upper() or "unknown",
388 'description_sort': description.upper() or "unknown",
382 'lastchange': d,
389 'lastchange': d,
383 'lastchange_sort': d[1]-d[0],
390 'lastchange_sort': d[1]-d[0],
384 'archives': archivelist(u, "tip", url),
391 'archives': archivelist(u, "tip", url),
385 'isdirectory': None,
392 'isdirectory': None,
386 }
393 }
387
394
388 yield row
395 yield row
389
396
390 sortdefault = None, False
397 sortdefault = None, False
391 def entries(sortcolumn="", descending=False, subdir="", **map):
398 def entries(sortcolumn="", descending=False, subdir="", **map):
392 rows = rawentries(subdir=subdir, **map)
399 rows = rawentries(subdir=subdir, **map)
393
400
394 if sortcolumn and sortdefault != (sortcolumn, descending):
401 if sortcolumn and sortdefault != (sortcolumn, descending):
395 sortkey = '%s_sort' % sortcolumn
402 sortkey = '%s_sort' % sortcolumn
396 rows = sorted(rows, key=lambda x: x[sortkey],
403 rows = sorted(rows, key=lambda x: x[sortkey],
397 reverse=descending)
404 reverse=descending)
398 for row, parity in zip(rows, paritygen(self.stripecount)):
405 for row, parity in zip(rows, paritygen(self.stripecount)):
399 row['parity'] = parity
406 row['parity'] = parity
400 yield row
407 yield row
401
408
402 self.refresh()
409 self.refresh()
403 sortable = ["name", "description", "contact", "lastchange"]
410 sortable = ["name", "description", "contact", "lastchange"]
404 sortcolumn, descending = sortdefault
411 sortcolumn, descending = sortdefault
405 if 'sort' in req.form:
412 if 'sort' in req.form:
406 sortcolumn = req.form['sort'][0]
413 sortcolumn = req.form['sort'][0]
407 descending = sortcolumn.startswith('-')
414 descending = sortcolumn.startswith('-')
408 if descending:
415 if descending:
409 sortcolumn = sortcolumn[1:]
416 sortcolumn = sortcolumn[1:]
410 if sortcolumn not in sortable:
417 if sortcolumn not in sortable:
411 sortcolumn = ""
418 sortcolumn = ""
412
419
413 sort = [("sort_%s" % column,
420 sort = [("sort_%s" % column,
414 "%s%s" % ((not descending and column == sortcolumn)
421 "%s%s" % ((not descending and column == sortcolumn)
415 and "-" or "", column))
422 and "-" or "", column))
416 for column in sortable]
423 for column in sortable]
417
424
418 self.refresh()
425 self.refresh()
419 self.updatereqenv(req.env)
426 self.updatereqenv(req.env)
420
427
421 return tmpl("index", entries=entries, subdir=subdir,
428 return tmpl("index", entries=entries, subdir=subdir,
422 pathdef=makebreadcrumb('/' + subdir, self.prefix),
429 pathdef=makebreadcrumb('/' + subdir, self.prefix),
423 sortcolumn=sortcolumn, descending=descending,
430 sortcolumn=sortcolumn, descending=descending,
424 **dict(sort))
431 **dict(sort))
425
432
426 def templater(self, req):
433 def templater(self, req):
427
434
428 def motd(**map):
435 def motd(**map):
429 if self.motd is not None:
436 if self.motd is not None:
430 yield self.motd
437 yield self.motd
431 else:
438 else:
432 yield config('web', 'motd', '')
439 yield config('web', 'motd', '')
433
440
434 def config(section, name, default=None, untrusted=True):
441 def config(section, name, default=None, untrusted=True):
435 return self.ui.config(section, name, default, untrusted)
442 return self.ui.config(section, name, default, untrusted)
436
443
437 self.updatereqenv(req.env)
444 self.updatereqenv(req.env)
438
445
439 url = req.env.get('SCRIPT_NAME', '')
446 url = req.env.get('SCRIPT_NAME', '')
440 if not url.endswith('/'):
447 if not url.endswith('/'):
441 url += '/'
448 url += '/'
442
449
443 vars = {}
450 vars = {}
444 styles = (
451 styles = (
445 req.form.get('style', [None])[0],
452 req.form.get('style', [None])[0],
446 config('web', 'style'),
453 config('web', 'style'),
447 'paper'
454 'paper'
448 )
455 )
449 style, mapfile = templater.stylemap(styles, self.templatepath)
456 style, mapfile = templater.stylemap(styles, self.templatepath)
450 if style == styles[0]:
457 if style == styles[0]:
451 vars['style'] = style
458 vars['style'] = style
452
459
453 start = url[-1] == '?' and '&' or '?'
460 start = url[-1] == '?' and '&' or '?'
454 sessionvars = webutil.sessionvars(vars, start)
461 sessionvars = webutil.sessionvars(vars, start)
455 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
462 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
456 logoimg = config('web', 'logoimg', 'hglogo.png')
463 logoimg = config('web', 'logoimg', 'hglogo.png')
457 staticurl = config('web', 'staticurl') or url + 'static/'
464 staticurl = config('web', 'staticurl') or url + 'static/'
458 if not staticurl.endswith('/'):
465 if not staticurl.endswith('/'):
459 staticurl += '/'
466 staticurl += '/'
460
467
461 tmpl = templater.templater(mapfile,
468 tmpl = templater.templater(mapfile,
462 defaults={"encoding": encoding.encoding,
469 defaults={"encoding": encoding.encoding,
463 "motd": motd,
470 "motd": motd,
464 "url": url,
471 "url": url,
465 "logourl": logourl,
472 "logourl": logourl,
466 "logoimg": logoimg,
473 "logoimg": logoimg,
467 "staticurl": staticurl,
474 "staticurl": staticurl,
468 "sessionvars": sessionvars,
475 "sessionvars": sessionvars,
469 "style": style,
476 "style": style,
470 })
477 })
471 return tmpl
478 return tmpl
472
479
473 def updatereqenv(self, env):
480 def updatereqenv(self, env):
474 if self._baseurl is not None:
481 if self._baseurl is not None:
475 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
482 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
476 env['SERVER_NAME'] = name
483 env['SERVER_NAME'] = name
477 env['SERVER_PORT'] = port
484 env['SERVER_PORT'] = port
478 env['SCRIPT_NAME'] = path
485 env['SCRIPT_NAME'] = path
@@ -1,134 +1,140
1 # hgweb/request.py - An http request from either CGI or the standalone server.
1 # hgweb/request.py - An http request from either CGI or the standalone server.
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 socket, cgi, errno
9 import socket, cgi, errno
10 from mercurial import util
10 from mercurial import util
11 from common import ErrorResponse, statusmessage, HTTP_NOT_MODIFIED
11 from common import ErrorResponse, statusmessage, HTTP_NOT_MODIFIED
12
12
13 shortcuts = {
13 shortcuts = {
14 'cl': [('cmd', ['changelog']), ('rev', None)],
14 'cl': [('cmd', ['changelog']), ('rev', None)],
15 'sl': [('cmd', ['shortlog']), ('rev', None)],
15 'sl': [('cmd', ['shortlog']), ('rev', None)],
16 'cs': [('cmd', ['changeset']), ('node', None)],
16 'cs': [('cmd', ['changeset']), ('node', None)],
17 'f': [('cmd', ['file']), ('filenode', None)],
17 'f': [('cmd', ['file']), ('filenode', None)],
18 'fl': [('cmd', ['filelog']), ('filenode', None)],
18 'fl': [('cmd', ['filelog']), ('filenode', None)],
19 'fd': [('cmd', ['filediff']), ('node', None)],
19 'fd': [('cmd', ['filediff']), ('node', None)],
20 'fa': [('cmd', ['annotate']), ('filenode', None)],
20 'fa': [('cmd', ['annotate']), ('filenode', None)],
21 'mf': [('cmd', ['manifest']), ('manifest', None)],
21 'mf': [('cmd', ['manifest']), ('manifest', None)],
22 'ca': [('cmd', ['archive']), ('node', None)],
22 'ca': [('cmd', ['archive']), ('node', None)],
23 'tags': [('cmd', ['tags'])],
23 'tags': [('cmd', ['tags'])],
24 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
24 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
25 'static': [('cmd', ['static']), ('file', None)]
25 'static': [('cmd', ['static']), ('file', None)]
26 }
26 }
27
27
28 def normalize(form):
28 def normalize(form):
29 # first expand the shortcuts
29 # first expand the shortcuts
30 for k in shortcuts.iterkeys():
30 for k in shortcuts.iterkeys():
31 if k in form:
31 if k in form:
32 for name, value in shortcuts[k]:
32 for name, value in shortcuts[k]:
33 if value is None:
33 if value is None:
34 value = form[k]
34 value = form[k]
35 form[name] = value
35 form[name] = value
36 del form[k]
36 del form[k]
37 # And strip the values
37 # And strip the values
38 for k, v in form.iteritems():
38 for k, v in form.iteritems():
39 form[k] = [i.strip() for i in v]
39 form[k] = [i.strip() for i in v]
40 return form
40 return form
41
41
42 class wsgirequest(object):
42 class wsgirequest(object):
43 """Higher-level API for a WSGI request.
44
45 WSGI applications are invoked with 2 arguments. They are used to
46 instantiate instances of this class, which provides higher-level APIs
47 for obtaining request parameters, writing HTTP output, etc.
48 """
43 def __init__(self, wsgienv, start_response):
49 def __init__(self, wsgienv, start_response):
44 version = wsgienv['wsgi.version']
50 version = wsgienv['wsgi.version']
45 if (version < (1, 0)) or (version >= (2, 0)):
51 if (version < (1, 0)) or (version >= (2, 0)):
46 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
52 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
47 % version)
53 % version)
48 self.inp = wsgienv['wsgi.input']
54 self.inp = wsgienv['wsgi.input']
49 self.err = wsgienv['wsgi.errors']
55 self.err = wsgienv['wsgi.errors']
50 self.threaded = wsgienv['wsgi.multithread']
56 self.threaded = wsgienv['wsgi.multithread']
51 self.multiprocess = wsgienv['wsgi.multiprocess']
57 self.multiprocess = wsgienv['wsgi.multiprocess']
52 self.run_once = wsgienv['wsgi.run_once']
58 self.run_once = wsgienv['wsgi.run_once']
53 self.env = wsgienv
59 self.env = wsgienv
54 self.form = normalize(cgi.parse(self.inp,
60 self.form = normalize(cgi.parse(self.inp,
55 self.env,
61 self.env,
56 keep_blank_values=1))
62 keep_blank_values=1))
57 self._start_response = start_response
63 self._start_response = start_response
58 self.server_write = None
64 self.server_write = None
59 self.headers = []
65 self.headers = []
60
66
61 def __iter__(self):
67 def __iter__(self):
62 return iter([])
68 return iter([])
63
69
64 def read(self, count=-1):
70 def read(self, count=-1):
65 return self.inp.read(count)
71 return self.inp.read(count)
66
72
67 def drain(self):
73 def drain(self):
68 '''need to read all data from request, httplib is half-duplex'''
74 '''need to read all data from request, httplib is half-duplex'''
69 length = int(self.env.get('CONTENT_LENGTH') or 0)
75 length = int(self.env.get('CONTENT_LENGTH') or 0)
70 for s in util.filechunkiter(self.inp, limit=length):
76 for s in util.filechunkiter(self.inp, limit=length):
71 pass
77 pass
72
78
73 def respond(self, status, type, filename=None, body=None):
79 def respond(self, status, type, filename=None, body=None):
74 if self._start_response is not None:
80 if self._start_response is not None:
75 self.headers.append(('Content-Type', type))
81 self.headers.append(('Content-Type', type))
76 if filename:
82 if filename:
77 filename = (filename.split('/')[-1]
83 filename = (filename.split('/')[-1]
78 .replace('\\', '\\\\').replace('"', '\\"'))
84 .replace('\\', '\\\\').replace('"', '\\"'))
79 self.headers.append(('Content-Disposition',
85 self.headers.append(('Content-Disposition',
80 'inline; filename="%s"' % filename))
86 'inline; filename="%s"' % filename))
81 if body is not None:
87 if body is not None:
82 self.headers.append(('Content-Length', str(len(body))))
88 self.headers.append(('Content-Length', str(len(body))))
83
89
84 for k, v in self.headers:
90 for k, v in self.headers:
85 if not isinstance(v, str):
91 if not isinstance(v, str):
86 raise TypeError('header value must be string: %r' % (v,))
92 raise TypeError('header value must be string: %r' % (v,))
87
93
88 if isinstance(status, ErrorResponse):
94 if isinstance(status, ErrorResponse):
89 self.headers.extend(status.headers)
95 self.headers.extend(status.headers)
90 if status.code == HTTP_NOT_MODIFIED:
96 if status.code == HTTP_NOT_MODIFIED:
91 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
97 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
92 # it MUST NOT include any headers other than these and no
98 # it MUST NOT include any headers other than these and no
93 # body
99 # body
94 self.headers = [(k, v) for (k, v) in self.headers if
100 self.headers = [(k, v) for (k, v) in self.headers if
95 k in ('Date', 'ETag', 'Expires',
101 k in ('Date', 'ETag', 'Expires',
96 'Cache-Control', 'Vary')]
102 'Cache-Control', 'Vary')]
97 status = statusmessage(status.code, status.message)
103 status = statusmessage(status.code, status.message)
98 elif status == 200:
104 elif status == 200:
99 status = '200 Script output follows'
105 status = '200 Script output follows'
100 elif isinstance(status, int):
106 elif isinstance(status, int):
101 status = statusmessage(status)
107 status = statusmessage(status)
102
108
103 self.server_write = self._start_response(status, self.headers)
109 self.server_write = self._start_response(status, self.headers)
104 self._start_response = None
110 self._start_response = None
105 self.headers = []
111 self.headers = []
106 if body is not None:
112 if body is not None:
107 self.write(body)
113 self.write(body)
108 self.server_write = None
114 self.server_write = None
109
115
110 def write(self, thing):
116 def write(self, thing):
111 if thing:
117 if thing:
112 try:
118 try:
113 self.server_write(thing)
119 self.server_write(thing)
114 except socket.error as inst:
120 except socket.error as inst:
115 if inst[0] != errno.ECONNRESET:
121 if inst[0] != errno.ECONNRESET:
116 raise
122 raise
117
123
118 def writelines(self, lines):
124 def writelines(self, lines):
119 for line in lines:
125 for line in lines:
120 self.write(line)
126 self.write(line)
121
127
122 def flush(self):
128 def flush(self):
123 return None
129 return None
124
130
125 def close(self):
131 def close(self):
126 return None
132 return None
127
133
128 def wsgiapplication(app_maker):
134 def wsgiapplication(app_maker):
129 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
135 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
130 can and should now be used as a WSGI application.'''
136 can and should now be used as a WSGI application.'''
131 application = app_maker()
137 application = app_maker()
132 def run_wsgi(env, respond):
138 def run_wsgi(env, respond):
133 return application(env, respond)
139 return application(env, respond)
134 return run_wsgi
140 return run_wsgi
General Comments 0
You need to be logged in to leave comments. Login now