##// END OF EJS Templates
hgweb: reuse body file object when hgwebdir calls hgweb (issue5851)...
Gregory Szorc -
r37836:877185de stable
parent child Browse files
Show More
@@ -1,530 +1,533
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 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import gc
11 import gc
12 import os
12 import os
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_SERVER_ERROR,
19 HTTP_SERVER_ERROR,
20 cspvalues,
20 cspvalues,
21 get_contact,
21 get_contact,
22 get_mtime,
22 get_mtime,
23 ismember,
23 ismember,
24 paritygen,
24 paritygen,
25 staticfile,
25 staticfile,
26 statusmessage,
26 statusmessage,
27 )
27 )
28
28
29 from .. import (
29 from .. import (
30 configitems,
30 configitems,
31 encoding,
31 encoding,
32 error,
32 error,
33 hg,
33 hg,
34 profiling,
34 profiling,
35 pycompat,
35 pycompat,
36 scmutil,
36 scmutil,
37 templater,
37 templater,
38 templateutil,
38 templateutil,
39 ui as uimod,
39 ui as uimod,
40 util,
40 util,
41 )
41 )
42
42
43 from . import (
43 from . import (
44 hgweb_mod,
44 hgweb_mod,
45 request as requestmod,
45 request as requestmod,
46 webutil,
46 webutil,
47 wsgicgi,
47 wsgicgi,
48 )
48 )
49 from ..utils import dateutil
49 from ..utils import dateutil
50
50
51 def cleannames(items):
51 def cleannames(items):
52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53
53
54 def findrepos(paths):
54 def findrepos(paths):
55 repos = []
55 repos = []
56 for prefix, root in cleannames(paths):
56 for prefix, root in cleannames(paths):
57 roothead, roottail = os.path.split(root)
57 roothead, roottail = os.path.split(root)
58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 # /bar/ be served as as foo/N .
59 # /bar/ be served as as foo/N .
60 # '*' will not search inside dirs with .hg (except .hg/patches),
60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 # '**' will search inside dirs with .hg (and thus also find subrepos).
61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 try:
62 try:
63 recurse = {'*': False, '**': True}[roottail]
63 recurse = {'*': False, '**': True}[roottail]
64 except KeyError:
64 except KeyError:
65 repos.append((prefix, root))
65 repos.append((prefix, root))
66 continue
66 continue
67 roothead = os.path.normpath(os.path.abspath(roothead))
67 roothead = os.path.normpath(os.path.abspath(roothead))
68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 repos.extend(urlrepos(prefix, roothead, paths))
69 repos.extend(urlrepos(prefix, roothead, paths))
70 return repos
70 return repos
71
71
72 def urlrepos(prefix, roothead, paths):
72 def urlrepos(prefix, roothead, paths):
73 """yield url paths and filesystem paths from a list of repo paths
73 """yield url paths and filesystem paths from a list of repo paths
74
74
75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 """
80 """
81 for path in paths:
81 for path in paths:
82 path = os.path.normpath(path)
82 path = os.path.normpath(path)
83 yield (prefix + '/' +
83 yield (prefix + '/' +
84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85
85
86 def readallowed(ui, req):
86 def readallowed(ui, req):
87 """Check allow_read and deny_read config options of a repo's ui object
87 """Check allow_read and deny_read config options of a repo's ui object
88 to determine user permissions. By default, with neither option set (or
88 to determine user permissions. By default, with neither option set (or
89 both empty), allow all users to read the repo. There are two ways a
89 both empty), allow all users to read the repo. There are two ways a
90 user can be denied read access: (1) deny_read is not empty, and the
90 user can be denied read access: (1) deny_read is not empty, and the
91 user is unauthenticated or deny_read contains user (or *), and (2)
91 user is unauthenticated or deny_read contains user (or *), and (2)
92 allow_read is not empty and the user is not in allow_read. Return True
92 allow_read is not empty and the user is not in allow_read. Return True
93 if user is allowed to read the repo, else return False."""
93 if user is allowed to read the repo, else return False."""
94
94
95 user = req.remoteuser
95 user = req.remoteuser
96
96
97 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
97 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
98 if deny_read and (not user or ismember(ui, user, deny_read)):
98 if deny_read and (not user or ismember(ui, user, deny_read)):
99 return False
99 return False
100
100
101 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
101 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
102 # by default, allow reading if no allow_read option has been set
102 # by default, allow reading if no allow_read option has been set
103 if not allow_read or ismember(ui, user, allow_read):
103 if not allow_read or ismember(ui, user, allow_read):
104 return True
104 return True
105
105
106 return False
106 return False
107
107
108 def rawindexentries(ui, repos, req, subdir=''):
108 def rawindexentries(ui, repos, req, subdir=''):
109 descend = ui.configbool('web', 'descend')
109 descend = ui.configbool('web', 'descend')
110 collapse = ui.configbool('web', 'collapse')
110 collapse = ui.configbool('web', 'collapse')
111 seenrepos = set()
111 seenrepos = set()
112 seendirs = set()
112 seendirs = set()
113 for name, path in repos:
113 for name, path in repos:
114
114
115 if not name.startswith(subdir):
115 if not name.startswith(subdir):
116 continue
116 continue
117 name = name[len(subdir):]
117 name = name[len(subdir):]
118 directory = False
118 directory = False
119
119
120 if '/' in name:
120 if '/' in name:
121 if not descend:
121 if not descend:
122 continue
122 continue
123
123
124 nameparts = name.split('/')
124 nameparts = name.split('/')
125 rootname = nameparts[0]
125 rootname = nameparts[0]
126
126
127 if not collapse:
127 if not collapse:
128 pass
128 pass
129 elif rootname in seendirs:
129 elif rootname in seendirs:
130 continue
130 continue
131 elif rootname in seenrepos:
131 elif rootname in seenrepos:
132 pass
132 pass
133 else:
133 else:
134 directory = True
134 directory = True
135 name = rootname
135 name = rootname
136
136
137 # redefine the path to refer to the directory
137 # redefine the path to refer to the directory
138 discarded = '/'.join(nameparts[1:])
138 discarded = '/'.join(nameparts[1:])
139
139
140 # remove name parts plus accompanying slash
140 # remove name parts plus accompanying slash
141 path = path[:-len(discarded) - 1]
141 path = path[:-len(discarded) - 1]
142
142
143 try:
143 try:
144 r = hg.repository(ui, path)
144 r = hg.repository(ui, path)
145 directory = False
145 directory = False
146 except (IOError, error.RepoError):
146 except (IOError, error.RepoError):
147 pass
147 pass
148
148
149 parts = [
149 parts = [
150 req.apppath.strip('/'),
150 req.apppath.strip('/'),
151 subdir.strip('/'),
151 subdir.strip('/'),
152 name.strip('/'),
152 name.strip('/'),
153 ]
153 ]
154 url = '/' + '/'.join(p for p in parts if p) + '/'
154 url = '/' + '/'.join(p for p in parts if p) + '/'
155
155
156 # show either a directory entry or a repository
156 # show either a directory entry or a repository
157 if directory:
157 if directory:
158 # get the directory's time information
158 # get the directory's time information
159 try:
159 try:
160 d = (get_mtime(path), dateutil.makedate()[1])
160 d = (get_mtime(path), dateutil.makedate()[1])
161 except OSError:
161 except OSError:
162 continue
162 continue
163
163
164 # add '/' to the name to make it obvious that
164 # add '/' to the name to make it obvious that
165 # the entry is a directory, not a regular repository
165 # the entry is a directory, not a regular repository
166 row = {'contact': "",
166 row = {'contact': "",
167 'contact_sort': "",
167 'contact_sort': "",
168 'name': name + '/',
168 'name': name + '/',
169 'name_sort': name,
169 'name_sort': name,
170 'url': url,
170 'url': url,
171 'description': "",
171 'description': "",
172 'description_sort': "",
172 'description_sort': "",
173 'lastchange': d,
173 'lastchange': d,
174 'lastchange_sort': d[1] - d[0],
174 'lastchange_sort': d[1] - d[0],
175 'archives': templateutil.mappinglist([]),
175 'archives': templateutil.mappinglist([]),
176 'isdirectory': True,
176 'isdirectory': True,
177 'labels': templateutil.hybridlist([], name='label'),
177 'labels': templateutil.hybridlist([], name='label'),
178 }
178 }
179
179
180 seendirs.add(name)
180 seendirs.add(name)
181 yield row
181 yield row
182 continue
182 continue
183
183
184 u = ui.copy()
184 u = ui.copy()
185 try:
185 try:
186 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
186 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
187 except Exception as e:
187 except Exception as e:
188 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
188 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
189 continue
189 continue
190
190
191 def get(section, name, default=uimod._unset):
191 def get(section, name, default=uimod._unset):
192 return u.config(section, name, default, untrusted=True)
192 return u.config(section, name, default, untrusted=True)
193
193
194 if u.configbool("web", "hidden", untrusted=True):
194 if u.configbool("web", "hidden", untrusted=True):
195 continue
195 continue
196
196
197 if not readallowed(u, req):
197 if not readallowed(u, req):
198 continue
198 continue
199
199
200 # update time with local timezone
200 # update time with local timezone
201 try:
201 try:
202 r = hg.repository(ui, path)
202 r = hg.repository(ui, path)
203 except IOError:
203 except IOError:
204 u.warn(_('error accessing repository at %s\n') % path)
204 u.warn(_('error accessing repository at %s\n') % path)
205 continue
205 continue
206 except error.RepoError:
206 except error.RepoError:
207 u.warn(_('error accessing repository at %s\n') % path)
207 u.warn(_('error accessing repository at %s\n') % path)
208 continue
208 continue
209 try:
209 try:
210 d = (get_mtime(r.spath), dateutil.makedate()[1])
210 d = (get_mtime(r.spath), dateutil.makedate()[1])
211 except OSError:
211 except OSError:
212 continue
212 continue
213
213
214 contact = get_contact(get)
214 contact = get_contact(get)
215 description = get("web", "description")
215 description = get("web", "description")
216 seenrepos.add(name)
216 seenrepos.add(name)
217 name = get("web", "name", name)
217 name = get("web", "name", name)
218 labels = u.configlist('web', 'labels', untrusted=True)
218 labels = u.configlist('web', 'labels', untrusted=True)
219 row = {'contact': contact or "unknown",
219 row = {'contact': contact or "unknown",
220 'contact_sort': contact.upper() or "unknown",
220 'contact_sort': contact.upper() or "unknown",
221 'name': name,
221 'name': name,
222 'name_sort': name,
222 'name_sort': name,
223 'url': url,
223 'url': url,
224 'description': description or "unknown",
224 'description': description or "unknown",
225 'description_sort': description.upper() or "unknown",
225 'description_sort': description.upper() or "unknown",
226 'lastchange': d,
226 'lastchange': d,
227 'lastchange_sort': d[1] - d[0],
227 'lastchange_sort': d[1] - d[0],
228 'archives': webutil.archivelist(u, "tip", url),
228 'archives': webutil.archivelist(u, "tip", url),
229 'isdirectory': None,
229 'isdirectory': None,
230 'labels': templateutil.hybridlist(labels, name='label'),
230 'labels': templateutil.hybridlist(labels, name='label'),
231 }
231 }
232
232
233 yield row
233 yield row
234
234
235 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
235 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
236 descending, subdir):
236 descending, subdir):
237 rows = rawindexentries(ui, repos, req, subdir=subdir)
237 rows = rawindexentries(ui, repos, req, subdir=subdir)
238
238
239 sortdefault = None, False
239 sortdefault = None, False
240
240
241 if sortcolumn and sortdefault != (sortcolumn, descending):
241 if sortcolumn and sortdefault != (sortcolumn, descending):
242 sortkey = '%s_sort' % sortcolumn
242 sortkey = '%s_sort' % sortcolumn
243 rows = sorted(rows, key=lambda x: x[sortkey],
243 rows = sorted(rows, key=lambda x: x[sortkey],
244 reverse=descending)
244 reverse=descending)
245
245
246 for row, parity in zip(rows, paritygen(stripecount)):
246 for row, parity in zip(rows, paritygen(stripecount)):
247 row['parity'] = parity
247 row['parity'] = parity
248 yield row
248 yield row
249
249
250 def indexentries(ui, repos, req, stripecount, sortcolumn='',
250 def indexentries(ui, repos, req, stripecount, sortcolumn='',
251 descending=False, subdir=''):
251 descending=False, subdir=''):
252 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
252 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
253 return templateutil.mappinggenerator(_indexentriesgen, args=args)
253 return templateutil.mappinggenerator(_indexentriesgen, args=args)
254
254
255 class hgwebdir(object):
255 class hgwebdir(object):
256 """HTTP server for multiple repositories.
256 """HTTP server for multiple repositories.
257
257
258 Given a configuration, different repositories will be served depending
258 Given a configuration, different repositories will be served depending
259 on the request path.
259 on the request path.
260
260
261 Instances are typically used as WSGI applications.
261 Instances are typically used as WSGI applications.
262 """
262 """
263 def __init__(self, conf, baseui=None):
263 def __init__(self, conf, baseui=None):
264 self.conf = conf
264 self.conf = conf
265 self.baseui = baseui
265 self.baseui = baseui
266 self.ui = None
266 self.ui = None
267 self.lastrefresh = 0
267 self.lastrefresh = 0
268 self.motd = None
268 self.motd = None
269 self.refresh()
269 self.refresh()
270
270
271 def refresh(self):
271 def refresh(self):
272 if self.ui:
272 if self.ui:
273 refreshinterval = self.ui.configint('web', 'refreshinterval')
273 refreshinterval = self.ui.configint('web', 'refreshinterval')
274 else:
274 else:
275 item = configitems.coreitems['web']['refreshinterval']
275 item = configitems.coreitems['web']['refreshinterval']
276 refreshinterval = item.default
276 refreshinterval = item.default
277
277
278 # refreshinterval <= 0 means to always refresh.
278 # refreshinterval <= 0 means to always refresh.
279 if (refreshinterval > 0 and
279 if (refreshinterval > 0 and
280 self.lastrefresh + refreshinterval > time.time()):
280 self.lastrefresh + refreshinterval > time.time()):
281 return
281 return
282
282
283 if self.baseui:
283 if self.baseui:
284 u = self.baseui.copy()
284 u = self.baseui.copy()
285 else:
285 else:
286 u = uimod.ui.load()
286 u = uimod.ui.load()
287 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
287 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
288 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
288 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
289 # displaying bundling progress bar while serving feels wrong and may
289 # displaying bundling progress bar while serving feels wrong and may
290 # break some wsgi implementations.
290 # break some wsgi implementations.
291 u.setconfig('progress', 'disable', 'true', 'hgweb')
291 u.setconfig('progress', 'disable', 'true', 'hgweb')
292
292
293 if not isinstance(self.conf, (dict, list, tuple)):
293 if not isinstance(self.conf, (dict, list, tuple)):
294 map = {'paths': 'hgweb-paths'}
294 map = {'paths': 'hgweb-paths'}
295 if not os.path.exists(self.conf):
295 if not os.path.exists(self.conf):
296 raise error.Abort(_('config file %s not found!') % self.conf)
296 raise error.Abort(_('config file %s not found!') % self.conf)
297 u.readconfig(self.conf, remap=map, trust=True)
297 u.readconfig(self.conf, remap=map, trust=True)
298 paths = []
298 paths = []
299 for name, ignored in u.configitems('hgweb-paths'):
299 for name, ignored in u.configitems('hgweb-paths'):
300 for path in u.configlist('hgweb-paths', name):
300 for path in u.configlist('hgweb-paths', name):
301 paths.append((name, path))
301 paths.append((name, path))
302 elif isinstance(self.conf, (list, tuple)):
302 elif isinstance(self.conf, (list, tuple)):
303 paths = self.conf
303 paths = self.conf
304 elif isinstance(self.conf, dict):
304 elif isinstance(self.conf, dict):
305 paths = self.conf.items()
305 paths = self.conf.items()
306
306
307 repos = findrepos(paths)
307 repos = findrepos(paths)
308 for prefix, root in u.configitems('collections'):
308 for prefix, root in u.configitems('collections'):
309 prefix = util.pconvert(prefix)
309 prefix = util.pconvert(prefix)
310 for path in scmutil.walkrepos(root, followsym=True):
310 for path in scmutil.walkrepos(root, followsym=True):
311 repo = os.path.normpath(path)
311 repo = os.path.normpath(path)
312 name = util.pconvert(repo)
312 name = util.pconvert(repo)
313 if name.startswith(prefix):
313 if name.startswith(prefix):
314 name = name[len(prefix):]
314 name = name[len(prefix):]
315 repos.append((name.lstrip('/'), repo))
315 repos.append((name.lstrip('/'), repo))
316
316
317 self.repos = repos
317 self.repos = repos
318 self.ui = u
318 self.ui = u
319 encoding.encoding = self.ui.config('web', 'encoding')
319 encoding.encoding = self.ui.config('web', 'encoding')
320 self.style = self.ui.config('web', 'style')
320 self.style = self.ui.config('web', 'style')
321 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
321 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
322 self.stripecount = self.ui.config('web', 'stripes')
322 self.stripecount = self.ui.config('web', 'stripes')
323 if self.stripecount:
323 if self.stripecount:
324 self.stripecount = int(self.stripecount)
324 self.stripecount = int(self.stripecount)
325 prefix = self.ui.config('web', 'prefix')
325 prefix = self.ui.config('web', 'prefix')
326 if prefix.startswith('/'):
326 if prefix.startswith('/'):
327 prefix = prefix[1:]
327 prefix = prefix[1:]
328 if prefix.endswith('/'):
328 if prefix.endswith('/'):
329 prefix = prefix[:-1]
329 prefix = prefix[:-1]
330 self.prefix = prefix
330 self.prefix = prefix
331 self.lastrefresh = time.time()
331 self.lastrefresh = time.time()
332
332
333 def run(self):
333 def run(self):
334 if not encoding.environ.get('GATEWAY_INTERFACE',
334 if not encoding.environ.get('GATEWAY_INTERFACE',
335 '').startswith("CGI/1."):
335 '').startswith("CGI/1."):
336 raise RuntimeError("This function is only intended to be "
336 raise RuntimeError("This function is only intended to be "
337 "called while running as a CGI script.")
337 "called while running as a CGI script.")
338 wsgicgi.launch(self)
338 wsgicgi.launch(self)
339
339
340 def __call__(self, env, respond):
340 def __call__(self, env, respond):
341 baseurl = self.ui.config('web', 'baseurl')
341 baseurl = self.ui.config('web', 'baseurl')
342 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
342 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
343 res = requestmod.wsgiresponse(req, respond)
343 res = requestmod.wsgiresponse(req, respond)
344
344
345 return self.run_wsgi(req, res)
345 return self.run_wsgi(req, res)
346
346
347 def run_wsgi(self, req, res):
347 def run_wsgi(self, req, res):
348 profile = self.ui.configbool('profiling', 'enabled')
348 profile = self.ui.configbool('profiling', 'enabled')
349 with profiling.profile(self.ui, enabled=profile):
349 with profiling.profile(self.ui, enabled=profile):
350 try:
350 try:
351 for r in self._runwsgi(req, res):
351 for r in self._runwsgi(req, res):
352 yield r
352 yield r
353 finally:
353 finally:
354 # There are known cycles in localrepository that prevent
354 # There are known cycles in localrepository that prevent
355 # those objects (and tons of held references) from being
355 # those objects (and tons of held references) from being
356 # collected through normal refcounting. We mitigate those
356 # collected through normal refcounting. We mitigate those
357 # leaks by performing an explicit GC on every request.
357 # leaks by performing an explicit GC on every request.
358 # TODO remove this once leaks are fixed.
358 # TODO remove this once leaks are fixed.
359 # TODO only run this on requests that create localrepository
359 # TODO only run this on requests that create localrepository
360 # instances instead of every request.
360 # instances instead of every request.
361 gc.collect()
361 gc.collect()
362
362
363 def _runwsgi(self, req, res):
363 def _runwsgi(self, req, res):
364 try:
364 try:
365 self.refresh()
365 self.refresh()
366
366
367 csp, nonce = cspvalues(self.ui)
367 csp, nonce = cspvalues(self.ui)
368 if csp:
368 if csp:
369 res.headers['Content-Security-Policy'] = csp
369 res.headers['Content-Security-Policy'] = csp
370
370
371 virtual = req.dispatchpath.strip('/')
371 virtual = req.dispatchpath.strip('/')
372 tmpl = self.templater(req, nonce)
372 tmpl = self.templater(req, nonce)
373 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
373 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
374
374
375 # Global defaults. These can be overridden by any handler.
375 # Global defaults. These can be overridden by any handler.
376 res.status = '200 Script output follows'
376 res.status = '200 Script output follows'
377 res.headers['Content-Type'] = ctype
377 res.headers['Content-Type'] = ctype
378
378
379 # a static file
379 # a static file
380 if virtual.startswith('static/') or 'static' in req.qsparams:
380 if virtual.startswith('static/') or 'static' in req.qsparams:
381 if virtual.startswith('static/'):
381 if virtual.startswith('static/'):
382 fname = virtual[7:]
382 fname = virtual[7:]
383 else:
383 else:
384 fname = req.qsparams['static']
384 fname = req.qsparams['static']
385 static = self.ui.config("web", "static", None,
385 static = self.ui.config("web", "static", None,
386 untrusted=False)
386 untrusted=False)
387 if not static:
387 if not static:
388 tp = self.templatepath or templater.templatepaths()
388 tp = self.templatepath or templater.templatepaths()
389 if isinstance(tp, str):
389 if isinstance(tp, str):
390 tp = [tp]
390 tp = [tp]
391 static = [os.path.join(p, 'static') for p in tp]
391 static = [os.path.join(p, 'static') for p in tp]
392
392
393 staticfile(static, fname, res)
393 staticfile(static, fname, res)
394 return res.sendresponse()
394 return res.sendresponse()
395
395
396 # top-level index
396 # top-level index
397
397
398 repos = dict(self.repos)
398 repos = dict(self.repos)
399
399
400 if (not virtual or virtual == 'index') and virtual not in repos:
400 if (not virtual or virtual == 'index') and virtual not in repos:
401 return self.makeindex(req, res, tmpl)
401 return self.makeindex(req, res, tmpl)
402
402
403 # nested indexes and hgwebs
403 # nested indexes and hgwebs
404
404
405 if virtual.endswith('/index') and virtual not in repos:
405 if virtual.endswith('/index') and virtual not in repos:
406 subdir = virtual[:-len('index')]
406 subdir = virtual[:-len('index')]
407 if any(r.startswith(subdir) for r in repos):
407 if any(r.startswith(subdir) for r in repos):
408 return self.makeindex(req, res, tmpl, subdir)
408 return self.makeindex(req, res, tmpl, subdir)
409
409
410 def _virtualdirs():
410 def _virtualdirs():
411 # Check the full virtual path, each parent, and the root ('')
411 # Check the full virtual path, each parent, and the root ('')
412 if virtual != '':
412 if virtual != '':
413 yield virtual
413 yield virtual
414
414
415 for p in util.finddirs(virtual):
415 for p in util.finddirs(virtual):
416 yield p
416 yield p
417
417
418 yield ''
418 yield ''
419
419
420 for virtualrepo in _virtualdirs():
420 for virtualrepo in _virtualdirs():
421 real = repos.get(virtualrepo)
421 real = repos.get(virtualrepo)
422 if real:
422 if real:
423 # Re-parse the WSGI environment to take into account our
423 # Re-parse the WSGI environment to take into account our
424 # repository path component.
424 # repository path component.
425 uenv = req.rawenv
425 uenv = req.rawenv
426 if pycompat.ispy3:
426 if pycompat.ispy3:
427 uenv = {k.decode('latin1'): v for k, v in
427 uenv = {k.decode('latin1'): v for k, v in
428 uenv.iteritems()}
428 uenv.iteritems()}
429 req = requestmod.parserequestfromenv(
429 req = requestmod.parserequestfromenv(
430 uenv, reponame=virtualrepo,
430 uenv, reponame=virtualrepo,
431 altbaseurl=self.ui.config('web', 'baseurl'))
431 altbaseurl=self.ui.config('web', 'baseurl'),
432 # Reuse wrapped body file object otherwise state
433 # tracking can get confused.
434 bodyfh=req.bodyfh)
432 try:
435 try:
433 # ensure caller gets private copy of ui
436 # ensure caller gets private copy of ui
434 repo = hg.repository(self.ui.copy(), real)
437 repo = hg.repository(self.ui.copy(), real)
435 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
438 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
436 except IOError as inst:
439 except IOError as inst:
437 msg = encoding.strtolocal(inst.strerror)
440 msg = encoding.strtolocal(inst.strerror)
438 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
441 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
439 except error.RepoError as inst:
442 except error.RepoError as inst:
440 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
443 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
441
444
442 # browse subdirectories
445 # browse subdirectories
443 subdir = virtual + '/'
446 subdir = virtual + '/'
444 if [r for r in repos if r.startswith(subdir)]:
447 if [r for r in repos if r.startswith(subdir)]:
445 return self.makeindex(req, res, tmpl, subdir)
448 return self.makeindex(req, res, tmpl, subdir)
446
449
447 # prefixes not found
450 # prefixes not found
448 res.status = '404 Not Found'
451 res.status = '404 Not Found'
449 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
452 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
450 return res.sendresponse()
453 return res.sendresponse()
451
454
452 except ErrorResponse as e:
455 except ErrorResponse as e:
453 res.status = statusmessage(e.code, pycompat.bytestr(e))
456 res.status = statusmessage(e.code, pycompat.bytestr(e))
454 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
457 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
455 return res.sendresponse()
458 return res.sendresponse()
456 finally:
459 finally:
457 tmpl = None
460 tmpl = None
458
461
459 def makeindex(self, req, res, tmpl, subdir=""):
462 def makeindex(self, req, res, tmpl, subdir=""):
460 self.refresh()
463 self.refresh()
461 sortable = ["name", "description", "contact", "lastchange"]
464 sortable = ["name", "description", "contact", "lastchange"]
462 sortcolumn, descending = None, False
465 sortcolumn, descending = None, False
463 if 'sort' in req.qsparams:
466 if 'sort' in req.qsparams:
464 sortcolumn = req.qsparams['sort']
467 sortcolumn = req.qsparams['sort']
465 descending = sortcolumn.startswith('-')
468 descending = sortcolumn.startswith('-')
466 if descending:
469 if descending:
467 sortcolumn = sortcolumn[1:]
470 sortcolumn = sortcolumn[1:]
468 if sortcolumn not in sortable:
471 if sortcolumn not in sortable:
469 sortcolumn = ""
472 sortcolumn = ""
470
473
471 sort = [("sort_%s" % column,
474 sort = [("sort_%s" % column,
472 "%s%s" % ((not descending and column == sortcolumn)
475 "%s%s" % ((not descending and column == sortcolumn)
473 and "-" or "", column))
476 and "-" or "", column))
474 for column in sortable]
477 for column in sortable]
475
478
476 self.refresh()
479 self.refresh()
477
480
478 entries = indexentries(self.ui, self.repos, req,
481 entries = indexentries(self.ui, self.repos, req,
479 self.stripecount, sortcolumn=sortcolumn,
482 self.stripecount, sortcolumn=sortcolumn,
480 descending=descending, subdir=subdir)
483 descending=descending, subdir=subdir)
481
484
482 mapping = {
485 mapping = {
483 'entries': entries,
486 'entries': entries,
484 'subdir': subdir,
487 'subdir': subdir,
485 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
488 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
486 'sortcolumn': sortcolumn,
489 'sortcolumn': sortcolumn,
487 'descending': descending,
490 'descending': descending,
488 }
491 }
489 mapping.update(sort)
492 mapping.update(sort)
490 res.setbodygen(tmpl.generate('index', mapping))
493 res.setbodygen(tmpl.generate('index', mapping))
491 return res.sendresponse()
494 return res.sendresponse()
492
495
493 def templater(self, req, nonce):
496 def templater(self, req, nonce):
494
497
495 def motd(**map):
498 def motd(**map):
496 if self.motd is not None:
499 if self.motd is not None:
497 yield self.motd
500 yield self.motd
498 else:
501 else:
499 yield config('web', 'motd')
502 yield config('web', 'motd')
500
503
501 def config(section, name, default=uimod._unset, untrusted=True):
504 def config(section, name, default=uimod._unset, untrusted=True):
502 return self.ui.config(section, name, default, untrusted)
505 return self.ui.config(section, name, default, untrusted)
503
506
504 vars = {}
507 vars = {}
505 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
508 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
506 self.templatepath)
509 self.templatepath)
507 if style == styles[0]:
510 if style == styles[0]:
508 vars['style'] = style
511 vars['style'] = style
509
512
510 sessionvars = webutil.sessionvars(vars, r'?')
513 sessionvars = webutil.sessionvars(vars, r'?')
511 logourl = config('web', 'logourl')
514 logourl = config('web', 'logourl')
512 logoimg = config('web', 'logoimg')
515 logoimg = config('web', 'logoimg')
513 staticurl = (config('web', 'staticurl')
516 staticurl = (config('web', 'staticurl')
514 or req.apppath + '/static/')
517 or req.apppath + '/static/')
515 if not staticurl.endswith('/'):
518 if not staticurl.endswith('/'):
516 staticurl += '/'
519 staticurl += '/'
517
520
518 defaults = {
521 defaults = {
519 "encoding": encoding.encoding,
522 "encoding": encoding.encoding,
520 "motd": motd,
523 "motd": motd,
521 "url": req.apppath + '/',
524 "url": req.apppath + '/',
522 "logourl": logourl,
525 "logourl": logourl,
523 "logoimg": logoimg,
526 "logoimg": logoimg,
524 "staticurl": staticurl,
527 "staticurl": staticurl,
525 "sessionvars": sessionvars,
528 "sessionvars": sessionvars,
526 "style": style,
529 "style": style,
527 "nonce": nonce,
530 "nonce": nonce,
528 }
531 }
529 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
532 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
530 return tmpl
533 return tmpl
@@ -1,570 +1,574
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 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 #import wsgiref.validate
11 #import wsgiref.validate
12
12
13 from ..thirdparty import (
13 from ..thirdparty import (
14 attr,
14 attr,
15 )
15 )
16 from .. import (
16 from .. import (
17 error,
17 error,
18 pycompat,
18 pycompat,
19 util,
19 util,
20 )
20 )
21
21
22 class multidict(object):
22 class multidict(object):
23 """A dict like object that can store multiple values for a key.
23 """A dict like object that can store multiple values for a key.
24
24
25 Used to store parsed request parameters.
25 Used to store parsed request parameters.
26
26
27 This is inspired by WebOb's class of the same name.
27 This is inspired by WebOb's class of the same name.
28 """
28 """
29 def __init__(self):
29 def __init__(self):
30 self._items = {}
30 self._items = {}
31
31
32 def __getitem__(self, key):
32 def __getitem__(self, key):
33 """Returns the last set value for a key."""
33 """Returns the last set value for a key."""
34 return self._items[key][-1]
34 return self._items[key][-1]
35
35
36 def __setitem__(self, key, value):
36 def __setitem__(self, key, value):
37 """Replace a values for a key with a new value."""
37 """Replace a values for a key with a new value."""
38 self._items[key] = [value]
38 self._items[key] = [value]
39
39
40 def __delitem__(self, key):
40 def __delitem__(self, key):
41 """Delete all values for a key."""
41 """Delete all values for a key."""
42 del self._items[key]
42 del self._items[key]
43
43
44 def __contains__(self, key):
44 def __contains__(self, key):
45 return key in self._items
45 return key in self._items
46
46
47 def __len__(self):
47 def __len__(self):
48 return len(self._items)
48 return len(self._items)
49
49
50 def get(self, key, default=None):
50 def get(self, key, default=None):
51 try:
51 try:
52 return self.__getitem__(key)
52 return self.__getitem__(key)
53 except KeyError:
53 except KeyError:
54 return default
54 return default
55
55
56 def add(self, key, value):
56 def add(self, key, value):
57 """Add a new value for a key. Does not replace existing values."""
57 """Add a new value for a key. Does not replace existing values."""
58 self._items.setdefault(key, []).append(value)
58 self._items.setdefault(key, []).append(value)
59
59
60 def getall(self, key):
60 def getall(self, key):
61 """Obtains all values for a key."""
61 """Obtains all values for a key."""
62 return self._items.get(key, [])
62 return self._items.get(key, [])
63
63
64 def getone(self, key):
64 def getone(self, key):
65 """Obtain a single value for a key.
65 """Obtain a single value for a key.
66
66
67 Raises KeyError if key not defined or it has multiple values set.
67 Raises KeyError if key not defined or it has multiple values set.
68 """
68 """
69 vals = self._items[key]
69 vals = self._items[key]
70
70
71 if len(vals) > 1:
71 if len(vals) > 1:
72 raise KeyError('multiple values for %r' % key)
72 raise KeyError('multiple values for %r' % key)
73
73
74 return vals[0]
74 return vals[0]
75
75
76 def asdictoflists(self):
76 def asdictoflists(self):
77 return {k: list(v) for k, v in self._items.iteritems()}
77 return {k: list(v) for k, v in self._items.iteritems()}
78
78
79 @attr.s(frozen=True)
79 @attr.s(frozen=True)
80 class parsedrequest(object):
80 class parsedrequest(object):
81 """Represents a parsed WSGI request.
81 """Represents a parsed WSGI request.
82
82
83 Contains both parsed parameters as well as a handle on the input stream.
83 Contains both parsed parameters as well as a handle on the input stream.
84 """
84 """
85
85
86 # Request method.
86 # Request method.
87 method = attr.ib()
87 method = attr.ib()
88 # Full URL for this request.
88 # Full URL for this request.
89 url = attr.ib()
89 url = attr.ib()
90 # URL without any path components. Just <proto>://<host><port>.
90 # URL without any path components. Just <proto>://<host><port>.
91 baseurl = attr.ib()
91 baseurl = attr.ib()
92 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
92 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
93 # of HTTP: Host header for hostname. This is likely what clients used.
93 # of HTTP: Host header for hostname. This is likely what clients used.
94 advertisedurl = attr.ib()
94 advertisedurl = attr.ib()
95 advertisedbaseurl = attr.ib()
95 advertisedbaseurl = attr.ib()
96 # URL scheme (part before ``://``). e.g. ``http`` or ``https``.
96 # URL scheme (part before ``://``). e.g. ``http`` or ``https``.
97 urlscheme = attr.ib()
97 urlscheme = attr.ib()
98 # Value of REMOTE_USER, if set, or None.
98 # Value of REMOTE_USER, if set, or None.
99 remoteuser = attr.ib()
99 remoteuser = attr.ib()
100 # Value of REMOTE_HOST, if set, or None.
100 # Value of REMOTE_HOST, if set, or None.
101 remotehost = attr.ib()
101 remotehost = attr.ib()
102 # Relative WSGI application path. If defined, will begin with a
102 # Relative WSGI application path. If defined, will begin with a
103 # ``/``.
103 # ``/``.
104 apppath = attr.ib()
104 apppath = attr.ib()
105 # List of path parts to be used for dispatch.
105 # List of path parts to be used for dispatch.
106 dispatchparts = attr.ib()
106 dispatchparts = attr.ib()
107 # URL path component (no query string) used for dispatch. Can be
107 # URL path component (no query string) used for dispatch. Can be
108 # ``None`` to signal no path component given to the request, an
108 # ``None`` to signal no path component given to the request, an
109 # empty string to signal a request to the application's root URL,
109 # empty string to signal a request to the application's root URL,
110 # or a string not beginning with ``/`` containing the requested
110 # or a string not beginning with ``/`` containing the requested
111 # path under the application.
111 # path under the application.
112 dispatchpath = attr.ib()
112 dispatchpath = attr.ib()
113 # The name of the repository being accessed.
113 # The name of the repository being accessed.
114 reponame = attr.ib()
114 reponame = attr.ib()
115 # Raw query string (part after "?" in URL).
115 # Raw query string (part after "?" in URL).
116 querystring = attr.ib()
116 querystring = attr.ib()
117 # multidict of query string parameters.
117 # multidict of query string parameters.
118 qsparams = attr.ib()
118 qsparams = attr.ib()
119 # wsgiref.headers.Headers instance. Operates like a dict with case
119 # wsgiref.headers.Headers instance. Operates like a dict with case
120 # insensitive keys.
120 # insensitive keys.
121 headers = attr.ib()
121 headers = attr.ib()
122 # Request body input stream.
122 # Request body input stream.
123 bodyfh = attr.ib()
123 bodyfh = attr.ib()
124 # WSGI environment dict, unmodified.
124 # WSGI environment dict, unmodified.
125 rawenv = attr.ib()
125 rawenv = attr.ib()
126
126
127 def parserequestfromenv(env, reponame=None, altbaseurl=None):
127 def parserequestfromenv(env, reponame=None, altbaseurl=None, bodyfh=None):
128 """Parse URL components from environment variables.
128 """Parse URL components from environment variables.
129
129
130 WSGI defines request attributes via environment variables. This function
130 WSGI defines request attributes via environment variables. This function
131 parses the environment variables into a data structure.
131 parses the environment variables into a data structure.
132
132
133 If ``reponame`` is defined, the leading path components matching that
133 If ``reponame`` is defined, the leading path components matching that
134 string are effectively shifted from ``PATH_INFO`` to ``SCRIPT_NAME``.
134 string are effectively shifted from ``PATH_INFO`` to ``SCRIPT_NAME``.
135 This simulates the world view of a WSGI application that processes
135 This simulates the world view of a WSGI application that processes
136 requests from the base URL of a repo.
136 requests from the base URL of a repo.
137
137
138 If ``altbaseurl`` (typically comes from ``web.baseurl`` config option)
138 If ``altbaseurl`` (typically comes from ``web.baseurl`` config option)
139 is defined, it is used - instead of the WSGI environment variables - for
139 is defined, it is used - instead of the WSGI environment variables - for
140 constructing URL components up to and including the WSGI application path.
140 constructing URL components up to and including the WSGI application path.
141 For example, if the current WSGI application is at ``/repo`` and a request
141 For example, if the current WSGI application is at ``/repo`` and a request
142 is made to ``/rev/@`` with this argument set to
142 is made to ``/rev/@`` with this argument set to
143 ``http://myserver:9000/prefix``, the URL and path components will resolve as
143 ``http://myserver:9000/prefix``, the URL and path components will resolve as
144 if the request were to ``http://myserver:9000/prefix/rev/@``. In other
144 if the request were to ``http://myserver:9000/prefix/rev/@``. In other
145 words, ``wsgi.url_scheme``, ``SERVER_NAME``, ``SERVER_PORT``, and
145 words, ``wsgi.url_scheme``, ``SERVER_NAME``, ``SERVER_PORT``, and
146 ``SCRIPT_NAME`` are all effectively replaced by components from this URL.
146 ``SCRIPT_NAME`` are all effectively replaced by components from this URL.
147
148 ``bodyfh`` can be used to specify a file object to read the request body
149 from. If not defined, ``wsgi.input`` from the environment dict is used.
147 """
150 """
148 # PEP 3333 defines the WSGI spec and is a useful reference for this code.
151 # PEP 3333 defines the WSGI spec and is a useful reference for this code.
149
152
150 # We first validate that the incoming object conforms with the WSGI spec.
153 # We first validate that the incoming object conforms with the WSGI spec.
151 # We only want to be dealing with spec-conforming WSGI implementations.
154 # We only want to be dealing with spec-conforming WSGI implementations.
152 # TODO enable this once we fix internal violations.
155 # TODO enable this once we fix internal violations.
153 #wsgiref.validate.check_environ(env)
156 #wsgiref.validate.check_environ(env)
154
157
155 # PEP-0333 states that environment keys and values are native strings
158 # PEP-0333 states that environment keys and values are native strings
156 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
159 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
157 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
160 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
158 # in Mercurial, so mass convert string keys and values to bytes.
161 # in Mercurial, so mass convert string keys and values to bytes.
159 if pycompat.ispy3:
162 if pycompat.ispy3:
160 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
163 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
161 env = {k: v.encode('latin-1') if isinstance(v, str) else v
164 env = {k: v.encode('latin-1') if isinstance(v, str) else v
162 for k, v in env.iteritems()}
165 for k, v in env.iteritems()}
163
166
164 # Some hosting solutions are emulating hgwebdir, and dispatching directly
167 # Some hosting solutions are emulating hgwebdir, and dispatching directly
165 # to an hgweb instance using this environment variable. This was always
168 # to an hgweb instance using this environment variable. This was always
166 # checked prior to d7fd203e36cc; keep doing so to avoid breaking them.
169 # checked prior to d7fd203e36cc; keep doing so to avoid breaking them.
167 if not reponame:
170 if not reponame:
168 reponame = env.get('REPO_NAME')
171 reponame = env.get('REPO_NAME')
169
172
170 if altbaseurl:
173 if altbaseurl:
171 altbaseurl = util.url(altbaseurl)
174 altbaseurl = util.url(altbaseurl)
172
175
173 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
176 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
174 # the environment variables.
177 # the environment variables.
175 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
178 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
176 # how URLs are reconstructed.
179 # how URLs are reconstructed.
177 fullurl = env['wsgi.url_scheme'] + '://'
180 fullurl = env['wsgi.url_scheme'] + '://'
178
181
179 if altbaseurl and altbaseurl.scheme:
182 if altbaseurl and altbaseurl.scheme:
180 advertisedfullurl = altbaseurl.scheme + '://'
183 advertisedfullurl = altbaseurl.scheme + '://'
181 else:
184 else:
182 advertisedfullurl = fullurl
185 advertisedfullurl = fullurl
183
186
184 def addport(s, port):
187 def addport(s, port):
185 if s.startswith('https://'):
188 if s.startswith('https://'):
186 if port != '443':
189 if port != '443':
187 s += ':' + port
190 s += ':' + port
188 else:
191 else:
189 if port != '80':
192 if port != '80':
190 s += ':' + port
193 s += ':' + port
191
194
192 return s
195 return s
193
196
194 if env.get('HTTP_HOST'):
197 if env.get('HTTP_HOST'):
195 fullurl += env['HTTP_HOST']
198 fullurl += env['HTTP_HOST']
196 else:
199 else:
197 fullurl += env['SERVER_NAME']
200 fullurl += env['SERVER_NAME']
198 fullurl = addport(fullurl, env['SERVER_PORT'])
201 fullurl = addport(fullurl, env['SERVER_PORT'])
199
202
200 if altbaseurl and altbaseurl.host:
203 if altbaseurl and altbaseurl.host:
201 advertisedfullurl += altbaseurl.host
204 advertisedfullurl += altbaseurl.host
202
205
203 if altbaseurl.port:
206 if altbaseurl.port:
204 port = altbaseurl.port
207 port = altbaseurl.port
205 elif altbaseurl.scheme == 'http' and not altbaseurl.port:
208 elif altbaseurl.scheme == 'http' and not altbaseurl.port:
206 port = '80'
209 port = '80'
207 elif altbaseurl.scheme == 'https' and not altbaseurl.port:
210 elif altbaseurl.scheme == 'https' and not altbaseurl.port:
208 port = '443'
211 port = '443'
209 else:
212 else:
210 port = env['SERVER_PORT']
213 port = env['SERVER_PORT']
211
214
212 advertisedfullurl = addport(advertisedfullurl, port)
215 advertisedfullurl = addport(advertisedfullurl, port)
213 else:
216 else:
214 advertisedfullurl += env['SERVER_NAME']
217 advertisedfullurl += env['SERVER_NAME']
215 advertisedfullurl = addport(advertisedfullurl, env['SERVER_PORT'])
218 advertisedfullurl = addport(advertisedfullurl, env['SERVER_PORT'])
216
219
217 baseurl = fullurl
220 baseurl = fullurl
218 advertisedbaseurl = advertisedfullurl
221 advertisedbaseurl = advertisedfullurl
219
222
220 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
223 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
221 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
224 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
222
225
223 if altbaseurl:
226 if altbaseurl:
224 path = altbaseurl.path or ''
227 path = altbaseurl.path or ''
225 if path and not path.startswith('/'):
228 if path and not path.startswith('/'):
226 path = '/' + path
229 path = '/' + path
227 advertisedfullurl += util.urlreq.quote(path)
230 advertisedfullurl += util.urlreq.quote(path)
228 else:
231 else:
229 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
232 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
230
233
231 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
234 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
232
235
233 if env.get('QUERY_STRING'):
236 if env.get('QUERY_STRING'):
234 fullurl += '?' + env['QUERY_STRING']
237 fullurl += '?' + env['QUERY_STRING']
235 advertisedfullurl += '?' + env['QUERY_STRING']
238 advertisedfullurl += '?' + env['QUERY_STRING']
236
239
237 # If ``reponame`` is defined, that must be a prefix on PATH_INFO
240 # If ``reponame`` is defined, that must be a prefix on PATH_INFO
238 # that represents the repository being dispatched to. When computing
241 # that represents the repository being dispatched to. When computing
239 # the dispatch info, we ignore these leading path components.
242 # the dispatch info, we ignore these leading path components.
240
243
241 if altbaseurl:
244 if altbaseurl:
242 apppath = altbaseurl.path or ''
245 apppath = altbaseurl.path or ''
243 if apppath and not apppath.startswith('/'):
246 if apppath and not apppath.startswith('/'):
244 apppath = '/' + apppath
247 apppath = '/' + apppath
245 else:
248 else:
246 apppath = env.get('SCRIPT_NAME', '')
249 apppath = env.get('SCRIPT_NAME', '')
247
250
248 if reponame:
251 if reponame:
249 repoprefix = '/' + reponame.strip('/')
252 repoprefix = '/' + reponame.strip('/')
250
253
251 if not env.get('PATH_INFO'):
254 if not env.get('PATH_INFO'):
252 raise error.ProgrammingError('reponame requires PATH_INFO')
255 raise error.ProgrammingError('reponame requires PATH_INFO')
253
256
254 if not env['PATH_INFO'].startswith(repoprefix):
257 if not env['PATH_INFO'].startswith(repoprefix):
255 raise error.ProgrammingError('PATH_INFO does not begin with repo '
258 raise error.ProgrammingError('PATH_INFO does not begin with repo '
256 'name: %s (%s)' % (env['PATH_INFO'],
259 'name: %s (%s)' % (env['PATH_INFO'],
257 reponame))
260 reponame))
258
261
259 dispatchpath = env['PATH_INFO'][len(repoprefix):]
262 dispatchpath = env['PATH_INFO'][len(repoprefix):]
260
263
261 if dispatchpath and not dispatchpath.startswith('/'):
264 if dispatchpath and not dispatchpath.startswith('/'):
262 raise error.ProgrammingError('reponame prefix of PATH_INFO does '
265 raise error.ProgrammingError('reponame prefix of PATH_INFO does '
263 'not end at path delimiter: %s (%s)' %
266 'not end at path delimiter: %s (%s)' %
264 (env['PATH_INFO'], reponame))
267 (env['PATH_INFO'], reponame))
265
268
266 apppath = apppath.rstrip('/') + repoprefix
269 apppath = apppath.rstrip('/') + repoprefix
267 dispatchparts = dispatchpath.strip('/').split('/')
270 dispatchparts = dispatchpath.strip('/').split('/')
268 dispatchpath = '/'.join(dispatchparts)
271 dispatchpath = '/'.join(dispatchparts)
269
272
270 elif 'PATH_INFO' in env:
273 elif 'PATH_INFO' in env:
271 if env['PATH_INFO'].strip('/'):
274 if env['PATH_INFO'].strip('/'):
272 dispatchparts = env['PATH_INFO'].strip('/').split('/')
275 dispatchparts = env['PATH_INFO'].strip('/').split('/')
273 dispatchpath = '/'.join(dispatchparts)
276 dispatchpath = '/'.join(dispatchparts)
274 else:
277 else:
275 dispatchparts = []
278 dispatchparts = []
276 dispatchpath = ''
279 dispatchpath = ''
277 else:
280 else:
278 dispatchparts = []
281 dispatchparts = []
279 dispatchpath = None
282 dispatchpath = None
280
283
281 querystring = env.get('QUERY_STRING', '')
284 querystring = env.get('QUERY_STRING', '')
282
285
283 # We store as a list so we have ordering information. We also store as
286 # We store as a list so we have ordering information. We also store as
284 # a dict to facilitate fast lookup.
287 # a dict to facilitate fast lookup.
285 qsparams = multidict()
288 qsparams = multidict()
286 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True):
289 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True):
287 qsparams.add(k, v)
290 qsparams.add(k, v)
288
291
289 # HTTP_* keys contain HTTP request headers. The Headers structure should
292 # HTTP_* keys contain HTTP request headers. The Headers structure should
290 # perform case normalization for us. We just rewrite underscore to dash
293 # perform case normalization for us. We just rewrite underscore to dash
291 # so keys match what likely went over the wire.
294 # so keys match what likely went over the wire.
292 headers = []
295 headers = []
293 for k, v in env.iteritems():
296 for k, v in env.iteritems():
294 if k.startswith('HTTP_'):
297 if k.startswith('HTTP_'):
295 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
298 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
296
299
297 from . import wsgiheaders # avoid cycle
300 from . import wsgiheaders # avoid cycle
298 headers = wsgiheaders.Headers(headers)
301 headers = wsgiheaders.Headers(headers)
299
302
300 # This is kind of a lie because the HTTP header wasn't explicitly
303 # This is kind of a lie because the HTTP header wasn't explicitly
301 # sent. But for all intents and purposes it should be OK to lie about
304 # sent. But for all intents and purposes it should be OK to lie about
302 # this, since a consumer will either either value to determine how many
305 # this, since a consumer will either either value to determine how many
303 # bytes are available to read.
306 # bytes are available to read.
304 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
307 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
305 headers['Content-Length'] = env['CONTENT_LENGTH']
308 headers['Content-Length'] = env['CONTENT_LENGTH']
306
309
307 if 'CONTENT_TYPE' in env and 'HTTP_CONTENT_TYPE' not in env:
310 if 'CONTENT_TYPE' in env and 'HTTP_CONTENT_TYPE' not in env:
308 headers['Content-Type'] = env['CONTENT_TYPE']
311 headers['Content-Type'] = env['CONTENT_TYPE']
309
312
313 if bodyfh is None:
310 bodyfh = env['wsgi.input']
314 bodyfh = env['wsgi.input']
311 if 'Content-Length' in headers:
315 if 'Content-Length' in headers:
312 bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
316 bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
313
317
314 return parsedrequest(method=env['REQUEST_METHOD'],
318 return parsedrequest(method=env['REQUEST_METHOD'],
315 url=fullurl, baseurl=baseurl,
319 url=fullurl, baseurl=baseurl,
316 advertisedurl=advertisedfullurl,
320 advertisedurl=advertisedfullurl,
317 advertisedbaseurl=advertisedbaseurl,
321 advertisedbaseurl=advertisedbaseurl,
318 urlscheme=env['wsgi.url_scheme'],
322 urlscheme=env['wsgi.url_scheme'],
319 remoteuser=env.get('REMOTE_USER'),
323 remoteuser=env.get('REMOTE_USER'),
320 remotehost=env.get('REMOTE_HOST'),
324 remotehost=env.get('REMOTE_HOST'),
321 apppath=apppath,
325 apppath=apppath,
322 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
326 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
323 reponame=reponame,
327 reponame=reponame,
324 querystring=querystring,
328 querystring=querystring,
325 qsparams=qsparams,
329 qsparams=qsparams,
326 headers=headers,
330 headers=headers,
327 bodyfh=bodyfh,
331 bodyfh=bodyfh,
328 rawenv=env)
332 rawenv=env)
329
333
330 class offsettrackingwriter(object):
334 class offsettrackingwriter(object):
331 """A file object like object that is append only and tracks write count.
335 """A file object like object that is append only and tracks write count.
332
336
333 Instances are bound to a callable. This callable is called with data
337 Instances are bound to a callable. This callable is called with data
334 whenever a ``write()`` is attempted.
338 whenever a ``write()`` is attempted.
335
339
336 Instances track the amount of written data so they can answer ``tell()``
340 Instances track the amount of written data so they can answer ``tell()``
337 requests.
341 requests.
338
342
339 The intent of this class is to wrap the ``write()`` function returned by
343 The intent of this class is to wrap the ``write()`` function returned by
340 a WSGI ``start_response()`` function. Since ``write()`` is a callable and
344 a WSGI ``start_response()`` function. Since ``write()`` is a callable and
341 not a file object, it doesn't implement other file object methods.
345 not a file object, it doesn't implement other file object methods.
342 """
346 """
343 def __init__(self, writefn):
347 def __init__(self, writefn):
344 self._write = writefn
348 self._write = writefn
345 self._offset = 0
349 self._offset = 0
346
350
347 def write(self, s):
351 def write(self, s):
348 res = self._write(s)
352 res = self._write(s)
349 # Some Python objects don't report the number of bytes written.
353 # Some Python objects don't report the number of bytes written.
350 if res is None:
354 if res is None:
351 self._offset += len(s)
355 self._offset += len(s)
352 else:
356 else:
353 self._offset += res
357 self._offset += res
354
358
355 def flush(self):
359 def flush(self):
356 pass
360 pass
357
361
358 def tell(self):
362 def tell(self):
359 return self._offset
363 return self._offset
360
364
361 class wsgiresponse(object):
365 class wsgiresponse(object):
362 """Represents a response to a WSGI request.
366 """Represents a response to a WSGI request.
363
367
364 A response consists of a status line, headers, and a body.
368 A response consists of a status line, headers, and a body.
365
369
366 Consumers must populate the ``status`` and ``headers`` fields and
370 Consumers must populate the ``status`` and ``headers`` fields and
367 make a call to a ``setbody*()`` method before the response can be
371 make a call to a ``setbody*()`` method before the response can be
368 issued.
372 issued.
369
373
370 When it is time to start sending the response over the wire,
374 When it is time to start sending the response over the wire,
371 ``sendresponse()`` is called. It handles emitting the header portion
375 ``sendresponse()`` is called. It handles emitting the header portion
372 of the response message. It then yields chunks of body data to be
376 of the response message. It then yields chunks of body data to be
373 written to the peer. Typically, the WSGI application itself calls
377 written to the peer. Typically, the WSGI application itself calls
374 and returns the value from ``sendresponse()``.
378 and returns the value from ``sendresponse()``.
375 """
379 """
376
380
377 def __init__(self, req, startresponse):
381 def __init__(self, req, startresponse):
378 """Create an empty response tied to a specific request.
382 """Create an empty response tied to a specific request.
379
383
380 ``req`` is a ``parsedrequest``. ``startresponse`` is the
384 ``req`` is a ``parsedrequest``. ``startresponse`` is the
381 ``start_response`` function passed to the WSGI application.
385 ``start_response`` function passed to the WSGI application.
382 """
386 """
383 self._req = req
387 self._req = req
384 self._startresponse = startresponse
388 self._startresponse = startresponse
385
389
386 self.status = None
390 self.status = None
387 from . import wsgiheaders # avoid cycle
391 from . import wsgiheaders # avoid cycle
388 self.headers = wsgiheaders.Headers([])
392 self.headers = wsgiheaders.Headers([])
389
393
390 self._bodybytes = None
394 self._bodybytes = None
391 self._bodygen = None
395 self._bodygen = None
392 self._bodywillwrite = False
396 self._bodywillwrite = False
393 self._started = False
397 self._started = False
394 self._bodywritefn = None
398 self._bodywritefn = None
395
399
396 def _verifybody(self):
400 def _verifybody(self):
397 if (self._bodybytes is not None or self._bodygen is not None
401 if (self._bodybytes is not None or self._bodygen is not None
398 or self._bodywillwrite):
402 or self._bodywillwrite):
399 raise error.ProgrammingError('cannot define body multiple times')
403 raise error.ProgrammingError('cannot define body multiple times')
400
404
401 def setbodybytes(self, b):
405 def setbodybytes(self, b):
402 """Define the response body as static bytes.
406 """Define the response body as static bytes.
403
407
404 The empty string signals that there is no response body.
408 The empty string signals that there is no response body.
405 """
409 """
406 self._verifybody()
410 self._verifybody()
407 self._bodybytes = b
411 self._bodybytes = b
408 self.headers['Content-Length'] = '%d' % len(b)
412 self.headers['Content-Length'] = '%d' % len(b)
409
413
410 def setbodygen(self, gen):
414 def setbodygen(self, gen):
411 """Define the response body as a generator of bytes."""
415 """Define the response body as a generator of bytes."""
412 self._verifybody()
416 self._verifybody()
413 self._bodygen = gen
417 self._bodygen = gen
414
418
415 def setbodywillwrite(self):
419 def setbodywillwrite(self):
416 """Signal an intent to use write() to emit the response body.
420 """Signal an intent to use write() to emit the response body.
417
421
418 **This is the least preferred way to send a body.**
422 **This is the least preferred way to send a body.**
419
423
420 It is preferred for WSGI applications to emit a generator of chunks
424 It is preferred for WSGI applications to emit a generator of chunks
421 constituting the response body. However, some consumers can't emit
425 constituting the response body. However, some consumers can't emit
422 data this way. So, WSGI provides a way to obtain a ``write(data)``
426 data this way. So, WSGI provides a way to obtain a ``write(data)``
423 function that can be used to synchronously perform an unbuffered
427 function that can be used to synchronously perform an unbuffered
424 write.
428 write.
425
429
426 Calling this function signals an intent to produce the body in this
430 Calling this function signals an intent to produce the body in this
427 manner.
431 manner.
428 """
432 """
429 self._verifybody()
433 self._verifybody()
430 self._bodywillwrite = True
434 self._bodywillwrite = True
431
435
432 def sendresponse(self):
436 def sendresponse(self):
433 """Send the generated response to the client.
437 """Send the generated response to the client.
434
438
435 Before this is called, ``status`` must be set and one of
439 Before this is called, ``status`` must be set and one of
436 ``setbodybytes()`` or ``setbodygen()`` must be called.
440 ``setbodybytes()`` or ``setbodygen()`` must be called.
437
441
438 Calling this method multiple times is not allowed.
442 Calling this method multiple times is not allowed.
439 """
443 """
440 if self._started:
444 if self._started:
441 raise error.ProgrammingError('sendresponse() called multiple times')
445 raise error.ProgrammingError('sendresponse() called multiple times')
442
446
443 self._started = True
447 self._started = True
444
448
445 if not self.status:
449 if not self.status:
446 raise error.ProgrammingError('status line not defined')
450 raise error.ProgrammingError('status line not defined')
447
451
448 if (self._bodybytes is None and self._bodygen is None
452 if (self._bodybytes is None and self._bodygen is None
449 and not self._bodywillwrite):
453 and not self._bodywillwrite):
450 raise error.ProgrammingError('response body not defined')
454 raise error.ProgrammingError('response body not defined')
451
455
452 # RFC 7232 Section 4.1 states that a 304 MUST generate one of
456 # RFC 7232 Section 4.1 states that a 304 MUST generate one of
453 # {Cache-Control, Content-Location, Date, ETag, Expires, Vary}
457 # {Cache-Control, Content-Location, Date, ETag, Expires, Vary}
454 # and SHOULD NOT generate other headers unless they could be used
458 # and SHOULD NOT generate other headers unless they could be used
455 # to guide cache updates. Furthermore, RFC 7230 Section 3.3.2
459 # to guide cache updates. Furthermore, RFC 7230 Section 3.3.2
456 # states that no response body can be issued. Content-Length can
460 # states that no response body can be issued. Content-Length can
457 # be sent. But if it is present, it should be the size of the response
461 # be sent. But if it is present, it should be the size of the response
458 # that wasn't transferred.
462 # that wasn't transferred.
459 if self.status.startswith('304 '):
463 if self.status.startswith('304 '):
460 # setbodybytes('') will set C-L to 0. This doesn't conform with the
464 # setbodybytes('') will set C-L to 0. This doesn't conform with the
461 # spec. So remove it.
465 # spec. So remove it.
462 if self.headers.get('Content-Length') == '0':
466 if self.headers.get('Content-Length') == '0':
463 del self.headers['Content-Length']
467 del self.headers['Content-Length']
464
468
465 # Strictly speaking, this is too strict. But until it causes
469 # Strictly speaking, this is too strict. But until it causes
466 # problems, let's be strict.
470 # problems, let's be strict.
467 badheaders = {k for k in self.headers.keys()
471 badheaders = {k for k in self.headers.keys()
468 if k.lower() not in ('date', 'etag', 'expires',
472 if k.lower() not in ('date', 'etag', 'expires',
469 'cache-control',
473 'cache-control',
470 'content-location',
474 'content-location',
471 'vary')}
475 'vary')}
472 if badheaders:
476 if badheaders:
473 raise error.ProgrammingError(
477 raise error.ProgrammingError(
474 'illegal header on 304 response: %s' %
478 'illegal header on 304 response: %s' %
475 ', '.join(sorted(badheaders)))
479 ', '.join(sorted(badheaders)))
476
480
477 if self._bodygen is not None or self._bodywillwrite:
481 if self._bodygen is not None or self._bodywillwrite:
478 raise error.ProgrammingError("must use setbodybytes('') with "
482 raise error.ProgrammingError("must use setbodybytes('') with "
479 "304 responses")
483 "304 responses")
480
484
481 # Various HTTP clients (notably httplib) won't read the HTTP response
485 # Various HTTP clients (notably httplib) won't read the HTTP response
482 # until the HTTP request has been sent in full. If servers (us) send a
486 # until the HTTP request has been sent in full. If servers (us) send a
483 # response before the HTTP request has been fully sent, the connection
487 # response before the HTTP request has been fully sent, the connection
484 # may deadlock because neither end is reading.
488 # may deadlock because neither end is reading.
485 #
489 #
486 # We work around this by "draining" the request data before
490 # We work around this by "draining" the request data before
487 # sending any response in some conditions.
491 # sending any response in some conditions.
488 drain = False
492 drain = False
489 close = False
493 close = False
490
494
491 # If the client sent Expect: 100-continue, we assume it is smart enough
495 # If the client sent Expect: 100-continue, we assume it is smart enough
492 # to deal with the server sending a response before reading the request.
496 # to deal with the server sending a response before reading the request.
493 # (httplib doesn't do this.)
497 # (httplib doesn't do this.)
494 if self._req.headers.get('Expect', '').lower() == '100-continue':
498 if self._req.headers.get('Expect', '').lower() == '100-continue':
495 pass
499 pass
496 # Only tend to request methods that have bodies. Strictly speaking,
500 # Only tend to request methods that have bodies. Strictly speaking,
497 # we should sniff for a body. But this is fine for our existing
501 # we should sniff for a body. But this is fine for our existing
498 # WSGI applications.
502 # WSGI applications.
499 elif self._req.method not in ('POST', 'PUT'):
503 elif self._req.method not in ('POST', 'PUT'):
500 pass
504 pass
501 else:
505 else:
502 # If we don't know how much data to read, there's no guarantee
506 # If we don't know how much data to read, there's no guarantee
503 # that we can drain the request responsibly. The WSGI
507 # that we can drain the request responsibly. The WSGI
504 # specification only says that servers *should* ensure the
508 # specification only says that servers *should* ensure the
505 # input stream doesn't overrun the actual request. So there's
509 # input stream doesn't overrun the actual request. So there's
506 # no guarantee that reading until EOF won't corrupt the stream
510 # no guarantee that reading until EOF won't corrupt the stream
507 # state.
511 # state.
508 if not isinstance(self._req.bodyfh, util.cappedreader):
512 if not isinstance(self._req.bodyfh, util.cappedreader):
509 close = True
513 close = True
510 else:
514 else:
511 # We /could/ only drain certain HTTP response codes. But 200 and
515 # We /could/ only drain certain HTTP response codes. But 200 and
512 # non-200 wire protocol responses both require draining. Since
516 # non-200 wire protocol responses both require draining. Since
513 # we have a capped reader in place for all situations where we
517 # we have a capped reader in place for all situations where we
514 # drain, it is safe to read from that stream. We'll either do
518 # drain, it is safe to read from that stream. We'll either do
515 # a drain or no-op if we're already at EOF.
519 # a drain or no-op if we're already at EOF.
516 drain = True
520 drain = True
517
521
518 if close:
522 if close:
519 self.headers['Connection'] = 'Close'
523 self.headers['Connection'] = 'Close'
520
524
521 if drain:
525 if drain:
522 assert isinstance(self._req.bodyfh, util.cappedreader)
526 assert isinstance(self._req.bodyfh, util.cappedreader)
523 while True:
527 while True:
524 chunk = self._req.bodyfh.read(32768)
528 chunk = self._req.bodyfh.read(32768)
525 if not chunk:
529 if not chunk:
526 break
530 break
527
531
528 strheaders = [(pycompat.strurl(k), pycompat.strurl(v)) for
532 strheaders = [(pycompat.strurl(k), pycompat.strurl(v)) for
529 k, v in self.headers.items()]
533 k, v in self.headers.items()]
530 write = self._startresponse(pycompat.sysstr(self.status),
534 write = self._startresponse(pycompat.sysstr(self.status),
531 strheaders)
535 strheaders)
532
536
533 if self._bodybytes:
537 if self._bodybytes:
534 yield self._bodybytes
538 yield self._bodybytes
535 elif self._bodygen:
539 elif self._bodygen:
536 for chunk in self._bodygen:
540 for chunk in self._bodygen:
537 yield chunk
541 yield chunk
538 elif self._bodywillwrite:
542 elif self._bodywillwrite:
539 self._bodywritefn = write
543 self._bodywritefn = write
540 else:
544 else:
541 error.ProgrammingError('do not know how to send body')
545 error.ProgrammingError('do not know how to send body')
542
546
543 def getbodyfile(self):
547 def getbodyfile(self):
544 """Obtain a file object like object representing the response body.
548 """Obtain a file object like object representing the response body.
545
549
546 For this to work, you must call ``setbodywillwrite()`` and then
550 For this to work, you must call ``setbodywillwrite()`` and then
547 ``sendresponse()`` first. ``sendresponse()`` is a generator and the
551 ``sendresponse()`` first. ``sendresponse()`` is a generator and the
548 function won't run to completion unless the generator is advanced. The
552 function won't run to completion unless the generator is advanced. The
549 generator yields not items. The easiest way to consume it is with
553 generator yields not items. The easiest way to consume it is with
550 ``list(res.sendresponse())``, which should resolve to an empty list -
554 ``list(res.sendresponse())``, which should resolve to an empty list -
551 ``[]``.
555 ``[]``.
552 """
556 """
553 if not self._bodywillwrite:
557 if not self._bodywillwrite:
554 raise error.ProgrammingError('must call setbodywillwrite() first')
558 raise error.ProgrammingError('must call setbodywillwrite() first')
555
559
556 if not self._started:
560 if not self._started:
557 raise error.ProgrammingError('must call sendresponse() first; did '
561 raise error.ProgrammingError('must call sendresponse() first; did '
558 'you remember to consume it since it '
562 'you remember to consume it since it '
559 'is a generator?')
563 'is a generator?')
560
564
561 assert self._bodywritefn
565 assert self._bodywritefn
562 return offsettrackingwriter(self._bodywritefn)
566 return offsettrackingwriter(self._bodywritefn)
563
567
564 def wsgiapplication(app_maker):
568 def wsgiapplication(app_maker):
565 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
569 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
566 can and should now be used as a WSGI application.'''
570 can and should now be used as a WSGI application.'''
567 application = app_maker()
571 application = app_maker()
568 def run_wsgi(env, respond):
572 def run_wsgi(env, respond):
569 return application(env, respond)
573 return application(env, respond)
570 return run_wsgi
574 return run_wsgi
@@ -1,382 +1,426
1 #require killdaemons
1 #require killdaemons
2
2
3 #testcases bundle1 bundle2
3 #testcases bundle1 bundle2
4
4
5 #if bundle1
5 #if bundle1
6 $ cat << EOF >> $HGRCPATH
6 $ cat << EOF >> $HGRCPATH
7 > [devel]
7 > [devel]
8 > # This test is dedicated to interaction through old bundle
8 > # This test is dedicated to interaction through old bundle
9 > legacy.exchange = bundle1
9 > legacy.exchange = bundle1
10 > EOF
10 > EOF
11 #endif
11 #endif
12
12
13 $ hg init test
13 $ hg init test
14 $ cd test
14 $ cd test
15 $ echo a > a
15 $ echo a > a
16 $ hg ci -Ama
16 $ hg ci -Ama
17 adding a
17 adding a
18 $ cd ..
18 $ cd ..
19 $ hg clone test test2
19 $ hg clone test test2
20 updating to branch default
20 updating to branch default
21 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
21 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
22 $ cd test2
22 $ cd test2
23 $ echo a >> a
23 $ echo a >> a
24 $ hg ci -mb
24 $ hg ci -mb
25 $ req() {
25 $ req() {
26 > hg $1 serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
26 > hg $1 serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
27 > cat hg.pid >> $DAEMON_PIDS
27 > cat hg.pid >> $DAEMON_PIDS
28 > hg --cwd ../test2 push http://localhost:$HGPORT/
28 > hg --cwd ../test2 push http://localhost:$HGPORT/
29 > exitstatus=$?
29 > exitstatus=$?
30 > killdaemons.py
30 > killdaemons.py
31 > echo % serve errors
31 > echo % serve errors
32 > cat errors.log
32 > cat errors.log
33 > return $exitstatus
33 > return $exitstatus
34 > }
34 > }
35 $ cd ../test
35 $ cd ../test
36
36
37 expect ssl error
37 expect ssl error
38
38
39 $ req
39 $ req
40 pushing to http://localhost:$HGPORT/
40 pushing to http://localhost:$HGPORT/
41 searching for changes
41 searching for changes
42 abort: HTTP Error 403: ssl required
42 abort: HTTP Error 403: ssl required
43 % serve errors
43 % serve errors
44 [255]
44 [255]
45
45
46 expect authorization error
46 expect authorization error
47
47
48 $ echo '[web]' > .hg/hgrc
48 $ echo '[web]' > .hg/hgrc
49 $ echo 'push_ssl = false' >> .hg/hgrc
49 $ echo 'push_ssl = false' >> .hg/hgrc
50 $ req
50 $ req
51 pushing to http://localhost:$HGPORT/
51 pushing to http://localhost:$HGPORT/
52 searching for changes
52 searching for changes
53 abort: authorization failed
53 abort: authorization failed
54 % serve errors
54 % serve errors
55 [255]
55 [255]
56
56
57 expect authorization error: must have authorized user
57 expect authorization error: must have authorized user
58
58
59 $ echo 'allow_push = unperson' >> .hg/hgrc
59 $ echo 'allow_push = unperson' >> .hg/hgrc
60 $ req
60 $ req
61 pushing to http://localhost:$HGPORT/
61 pushing to http://localhost:$HGPORT/
62 searching for changes
62 searching for changes
63 abort: authorization failed
63 abort: authorization failed
64 % serve errors
64 % serve errors
65 [255]
65 [255]
66
66
67 expect success
67 expect success
68
68
69 $ cat > $TESTTMP/hook.sh <<'EOF'
69 $ cat > $TESTTMP/hook.sh <<'EOF'
70 > echo "phase-move: $HG_NODE: $HG_OLDPHASE -> $HG_PHASE"
70 > echo "phase-move: $HG_NODE: $HG_OLDPHASE -> $HG_PHASE"
71 > EOF
71 > EOF
72
72
73 #if bundle1
73 #if bundle1
74 $ cat >> .hg/hgrc <<EOF
74 $ cat >> .hg/hgrc <<EOF
75 > allow_push = *
75 > allow_push = *
76 > [hooks]
76 > [hooks]
77 > changegroup = sh -c "printenv.py changegroup 0"
77 > changegroup = sh -c "printenv.py changegroup 0"
78 > pushkey = sh -c "printenv.py pushkey 0"
78 > pushkey = sh -c "printenv.py pushkey 0"
79 > txnclose-phase.test = sh $TESTTMP/hook.sh
79 > txnclose-phase.test = sh $TESTTMP/hook.sh
80 > EOF
80 > EOF
81 $ req "--debug --config extensions.blackbox="
81 $ req "--debug --config extensions.blackbox="
82 listening at http://*:$HGPORT/ (bound to $LOCALIP:$HGPORT) (glob) (?)
82 listening at http://*:$HGPORT/ (bound to $LOCALIP:$HGPORT) (glob) (?)
83 pushing to http://localhost:$HGPORT/
83 pushing to http://localhost:$HGPORT/
84 searching for changes
84 searching for changes
85 remote: redirecting incoming bundle to */hg-unbundle-* (glob)
85 remote: redirecting incoming bundle to */hg-unbundle-* (glob)
86 remote: adding changesets
86 remote: adding changesets
87 remote: add changeset ba677d0156c1
87 remote: add changeset ba677d0156c1
88 remote: adding manifests
88 remote: adding manifests
89 remote: adding file changes
89 remote: adding file changes
90 remote: adding a revisions
90 remote: adding a revisions
91 remote: added 1 changesets with 1 changes to 1 files
91 remote: added 1 changesets with 1 changes to 1 files
92 remote: updating the branch cache
92 remote: updating the branch cache
93 remote: running hook txnclose-phase.test: sh $TESTTMP/hook.sh
93 remote: running hook txnclose-phase.test: sh $TESTTMP/hook.sh
94 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
94 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
95 remote: running hook txnclose-phase.test: sh $TESTTMP/hook.sh
95 remote: running hook txnclose-phase.test: sh $TESTTMP/hook.sh
96 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
96 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
97 remote: running hook changegroup: sh -c "printenv.py changegroup 0"
97 remote: running hook changegroup: sh -c "printenv.py changegroup 0"
98 remote: changegroup hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
98 remote: changegroup hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
99 % serve errors
99 % serve errors
100 $ hg rollback
100 $ hg rollback
101 repository tip rolled back to revision 0 (undo serve)
101 repository tip rolled back to revision 0 (undo serve)
102 $ req "--debug --config server.streamunbundle=True --config extensions.blackbox="
102 $ req "--debug --config server.streamunbundle=True --config extensions.blackbox="
103 listening at http://*:$HGPORT/ (bound to $LOCALIP:$HGPORT) (glob) (?)
103 listening at http://*:$HGPORT/ (bound to $LOCALIP:$HGPORT) (glob) (?)
104 pushing to http://localhost:$HGPORT/
104 pushing to http://localhost:$HGPORT/
105 searching for changes
105 searching for changes
106 remote: adding changesets
106 remote: adding changesets
107 remote: add changeset ba677d0156c1
107 remote: add changeset ba677d0156c1
108 remote: adding manifests
108 remote: adding manifests
109 remote: adding file changes
109 remote: adding file changes
110 remote: adding a revisions
110 remote: adding a revisions
111 remote: added 1 changesets with 1 changes to 1 files
111 remote: added 1 changesets with 1 changes to 1 files
112 remote: updating the branch cache
112 remote: updating the branch cache
113 remote: running hook txnclose-phase.test: sh $TESTTMP/hook.sh
113 remote: running hook txnclose-phase.test: sh $TESTTMP/hook.sh
114 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
114 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
115 remote: running hook txnclose-phase.test: sh $TESTTMP/hook.sh
115 remote: running hook txnclose-phase.test: sh $TESTTMP/hook.sh
116 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
116 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
117 remote: running hook changegroup: sh -c "printenv.py changegroup 0"
117 remote: running hook changegroup: sh -c "printenv.py changegroup 0"
118 remote: changegroup hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
118 remote: changegroup hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
119 % serve errors
119 % serve errors
120 $ hg rollback
120 $ hg rollback
121 repository tip rolled back to revision 0 (undo serve)
121 repository tip rolled back to revision 0 (undo serve)
122 #endif
122 #endif
123
123
124 #if bundle2
124 #if bundle2
125 $ cat >> .hg/hgrc <<EOF
125 $ cat >> .hg/hgrc <<EOF
126 > allow_push = *
126 > allow_push = *
127 > [hooks]
127 > [hooks]
128 > changegroup = sh -c "printenv.py changegroup 0"
128 > changegroup = sh -c "printenv.py changegroup 0"
129 > pushkey = sh -c "printenv.py pushkey 0"
129 > pushkey = sh -c "printenv.py pushkey 0"
130 > txnclose-phase.test = sh $TESTTMP/hook.sh
130 > txnclose-phase.test = sh $TESTTMP/hook.sh
131 > EOF
131 > EOF
132 $ req
132 $ req
133 pushing to http://localhost:$HGPORT/
133 pushing to http://localhost:$HGPORT/
134 searching for changes
134 searching for changes
135 remote: adding changesets
135 remote: adding changesets
136 remote: adding manifests
136 remote: adding manifests
137 remote: adding file changes
137 remote: adding file changes
138 remote: added 1 changesets with 1 changes to 1 files
138 remote: added 1 changesets with 1 changes to 1 files
139 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
139 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
140 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
140 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
141 remote: changegroup hook: HG_BUNDLE2=1 HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
141 remote: changegroup hook: HG_BUNDLE2=1 HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
142 % serve errors
142 % serve errors
143 $ hg rollback
143 $ hg rollback
144 repository tip rolled back to revision 0 (undo serve)
144 repository tip rolled back to revision 0 (undo serve)
145 #endif
145 #endif
146
146
147 expect success, server lacks the httpheader capability
147 expect success, server lacks the httpheader capability
148
148
149 $ CAP=httpheader
149 $ CAP=httpheader
150 $ . "$TESTDIR/notcapable"
150 $ . "$TESTDIR/notcapable"
151 $ req
151 $ req
152 pushing to http://localhost:$HGPORT/
152 pushing to http://localhost:$HGPORT/
153 searching for changes
153 searching for changes
154 remote: adding changesets
154 remote: adding changesets
155 remote: adding manifests
155 remote: adding manifests
156 remote: adding file changes
156 remote: adding file changes
157 remote: added 1 changesets with 1 changes to 1 files
157 remote: added 1 changesets with 1 changes to 1 files
158 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
158 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
159 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
159 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
160 remote: changegroup hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob) (bundle1 !)
160 remote: changegroup hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob) (bundle1 !)
161 remote: changegroup hook: HG_BUNDLE2=1 HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob) (bundle2 !)
161 remote: changegroup hook: HG_BUNDLE2=1 HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob) (bundle2 !)
162 % serve errors
162 % serve errors
163 $ hg rollback
163 $ hg rollback
164 repository tip rolled back to revision 0 (undo serve)
164 repository tip rolled back to revision 0 (undo serve)
165
165
166 expect success, server lacks the unbundlehash capability
166 expect success, server lacks the unbundlehash capability
167
167
168 $ CAP=unbundlehash
168 $ CAP=unbundlehash
169 $ . "$TESTDIR/notcapable"
169 $ . "$TESTDIR/notcapable"
170 $ req
170 $ req
171 pushing to http://localhost:$HGPORT/
171 pushing to http://localhost:$HGPORT/
172 searching for changes
172 searching for changes
173 remote: adding changesets
173 remote: adding changesets
174 remote: adding manifests
174 remote: adding manifests
175 remote: adding file changes
175 remote: adding file changes
176 remote: added 1 changesets with 1 changes to 1 files
176 remote: added 1 changesets with 1 changes to 1 files
177 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
177 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
178 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
178 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
179 remote: changegroup hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob) (bundle1 !)
179 remote: changegroup hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob) (bundle1 !)
180 remote: changegroup hook: HG_BUNDLE2=1 HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob) (bundle2 !)
180 remote: changegroup hook: HG_BUNDLE2=1 HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob) (bundle2 !)
181 % serve errors
181 % serve errors
182 $ hg rollback
182 $ hg rollback
183 repository tip rolled back to revision 0 (undo serve)
183 repository tip rolled back to revision 0 (undo serve)
184
184
185 expect success, pre-d1b16a746db6 server supports the unbundle capability, but
185 expect success, pre-d1b16a746db6 server supports the unbundle capability, but
186 has no parameter
186 has no parameter
187
187
188 $ cat <<EOF > notcapable-unbundleparam.py
188 $ cat <<EOF > notcapable-unbundleparam.py
189 > from mercurial import extensions, httppeer
189 > from mercurial import extensions, httppeer
190 > def capable(orig, self, name):
190 > def capable(orig, self, name):
191 > if name == 'unbundle':
191 > if name == 'unbundle':
192 > return True
192 > return True
193 > return orig(self, name)
193 > return orig(self, name)
194 > def uisetup(ui):
194 > def uisetup(ui):
195 > extensions.wrapfunction(httppeer.httppeer, 'capable', capable)
195 > extensions.wrapfunction(httppeer.httppeer, 'capable', capable)
196 > EOF
196 > EOF
197 $ cp $HGRCPATH $HGRCPATH.orig
197 $ cp $HGRCPATH $HGRCPATH.orig
198 $ cat <<EOF >> $HGRCPATH
198 $ cat <<EOF >> $HGRCPATH
199 > [extensions]
199 > [extensions]
200 > notcapable-unbundleparam = `pwd`/notcapable-unbundleparam.py
200 > notcapable-unbundleparam = `pwd`/notcapable-unbundleparam.py
201 > EOF
201 > EOF
202 $ req
202 $ req
203 pushing to http://localhost:$HGPORT/
203 pushing to http://localhost:$HGPORT/
204 searching for changes
204 searching for changes
205 remote: adding changesets
205 remote: adding changesets
206 remote: adding manifests
206 remote: adding manifests
207 remote: adding file changes
207 remote: adding file changes
208 remote: added 1 changesets with 1 changes to 1 files
208 remote: added 1 changesets with 1 changes to 1 files
209 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
209 remote: phase-move: cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b: draft -> public
210 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
210 remote: phase-move: ba677d0156c1196c1a699fa53f390dcfc3ce3872: -> public
211 remote: changegroup hook: * (glob)
211 remote: changegroup hook: * (glob)
212 % serve errors
212 % serve errors
213 $ hg rollback
213 $ hg rollback
214 repository tip rolled back to revision 0 (undo serve)
214 repository tip rolled back to revision 0 (undo serve)
215 $ mv $HGRCPATH.orig $HGRCPATH
215 $ mv $HGRCPATH.orig $HGRCPATH
216
216
217 Test pushing to a publishing repository with a failing prepushkey hook
217 Test pushing to a publishing repository with a failing prepushkey hook
218
218
219 $ cat > .hg/hgrc <<EOF
219 $ cat > .hg/hgrc <<EOF
220 > [web]
220 > [web]
221 > push_ssl = false
221 > push_ssl = false
222 > allow_push = *
222 > allow_push = *
223 > [hooks]
223 > [hooks]
224 > prepushkey = sh -c "printenv.py prepushkey 1"
224 > prepushkey = sh -c "printenv.py prepushkey 1"
225 > [devel]
225 > [devel]
226 > legacy.exchange=phases
226 > legacy.exchange=phases
227 > EOF
227 > EOF
228
228
229 #if bundle1
229 #if bundle1
230 Bundle1 works because a) phases are updated as part of changegroup application
230 Bundle1 works because a) phases are updated as part of changegroup application
231 and b) client checks phases after the "unbundle" command. Since it sees no
231 and b) client checks phases after the "unbundle" command. Since it sees no
232 phase changes are necessary, it doesn't send the "pushkey" command and the
232 phase changes are necessary, it doesn't send the "pushkey" command and the
233 prepushkey hook never has to fire.
233 prepushkey hook never has to fire.
234
234
235 $ req
235 $ req
236 pushing to http://localhost:$HGPORT/
236 pushing to http://localhost:$HGPORT/
237 searching for changes
237 searching for changes
238 remote: adding changesets
238 remote: adding changesets
239 remote: adding manifests
239 remote: adding manifests
240 remote: adding file changes
240 remote: adding file changes
241 remote: added 1 changesets with 1 changes to 1 files
241 remote: added 1 changesets with 1 changes to 1 files
242 % serve errors
242 % serve errors
243
243
244 #endif
244 #endif
245
245
246 #if bundle2
246 #if bundle2
247 Bundle2 sends a "pushkey" bundle2 part. This runs as part of the transaction
247 Bundle2 sends a "pushkey" bundle2 part. This runs as part of the transaction
248 and fails the entire push.
248 and fails the entire push.
249 $ req
249 $ req
250 pushing to http://localhost:$HGPORT/
250 pushing to http://localhost:$HGPORT/
251 searching for changes
251 searching for changes
252 remote: adding changesets
252 remote: adding changesets
253 remote: adding manifests
253 remote: adding manifests
254 remote: adding file changes
254 remote: adding file changes
255 remote: added 1 changesets with 1 changes to 1 files
255 remote: added 1 changesets with 1 changes to 1 files
256 remote: prepushkey hook: HG_BUNDLE2=1 HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_OLD=1 HG_PENDING=$TESTTMP/test HG_PHASES_MOVED=1 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
256 remote: prepushkey hook: HG_BUNDLE2=1 HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_OLD=1 HG_PENDING=$TESTTMP/test HG_PHASES_MOVED=1 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
257 remote: pushkey-abort: prepushkey hook exited with status 1
257 remote: pushkey-abort: prepushkey hook exited with status 1
258 remote: transaction abort!
258 remote: transaction abort!
259 remote: rollback completed
259 remote: rollback completed
260 abort: updating ba677d0156c1 to public failed
260 abort: updating ba677d0156c1 to public failed
261 % serve errors
261 % serve errors
262 [255]
262 [255]
263
263
264 #endif
264 #endif
265
265
266 Now remove the failing prepushkey hook.
266 Now remove the failing prepushkey hook.
267
267
268 $ cat >> .hg/hgrc <<EOF
268 $ cat >> .hg/hgrc <<EOF
269 > [hooks]
269 > [hooks]
270 > prepushkey = sh -c "printenv.py prepushkey 0"
270 > prepushkey = sh -c "printenv.py prepushkey 0"
271 > EOF
271 > EOF
272
272
273 We don't need to test bundle1 because it succeeded above.
273 We don't need to test bundle1 because it succeeded above.
274
274
275 #if bundle2
275 #if bundle2
276 $ req
276 $ req
277 pushing to http://localhost:$HGPORT/
277 pushing to http://localhost:$HGPORT/
278 searching for changes
278 searching for changes
279 remote: adding changesets
279 remote: adding changesets
280 remote: adding manifests
280 remote: adding manifests
281 remote: adding file changes
281 remote: adding file changes
282 remote: added 1 changesets with 1 changes to 1 files
282 remote: added 1 changesets with 1 changes to 1 files
283 remote: prepushkey hook: HG_BUNDLE2=1 HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_OLD=1 HG_PENDING=$TESTTMP/test HG_PHASES_MOVED=1 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
283 remote: prepushkey hook: HG_BUNDLE2=1 HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_OLD=1 HG_PENDING=$TESTTMP/test HG_PHASES_MOVED=1 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
284 % serve errors
284 % serve errors
285 #endif
285 #endif
286
286
287 $ hg --config extensions.strip= strip -r 1:
287 $ hg --config extensions.strip= strip -r 1:
288 saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-eea704d7-backup.hg
288 saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-eea704d7-backup.hg
289
289
290 Now do a variant of the above, except on a non-publishing repository
290 Now do a variant of the above, except on a non-publishing repository
291
291
292 $ cat >> .hg/hgrc <<EOF
292 $ cat >> .hg/hgrc <<EOF
293 > [phases]
293 > [phases]
294 > publish = false
294 > publish = false
295 > [hooks]
295 > [hooks]
296 > prepushkey = sh -c "printenv.py prepushkey 1"
296 > prepushkey = sh -c "printenv.py prepushkey 1"
297 > EOF
297 > EOF
298
298
299 #if bundle1
299 #if bundle1
300 $ req
300 $ req
301 pushing to http://localhost:$HGPORT/
301 pushing to http://localhost:$HGPORT/
302 searching for changes
302 searching for changes
303 remote: adding changesets
303 remote: adding changesets
304 remote: adding manifests
304 remote: adding manifests
305 remote: adding file changes
305 remote: adding file changes
306 remote: added 1 changesets with 1 changes to 1 files
306 remote: added 1 changesets with 1 changes to 1 files
307 remote: prepushkey hook: HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_OLD=1
307 remote: prepushkey hook: HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_OLD=1
308 remote: pushkey-abort: prepushkey hook exited with status 1
308 remote: pushkey-abort: prepushkey hook exited with status 1
309 updating ba677d0156c1 to public failed!
309 updating ba677d0156c1 to public failed!
310 % serve errors
310 % serve errors
311 #endif
311 #endif
312
312
313 #if bundle2
313 #if bundle2
314 $ req
314 $ req
315 pushing to http://localhost:$HGPORT/
315 pushing to http://localhost:$HGPORT/
316 searching for changes
316 searching for changes
317 remote: adding changesets
317 remote: adding changesets
318 remote: adding manifests
318 remote: adding manifests
319 remote: adding file changes
319 remote: adding file changes
320 remote: added 1 changesets with 1 changes to 1 files
320 remote: added 1 changesets with 1 changes to 1 files
321 remote: prepushkey hook: HG_BUNDLE2=1 HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_OLD=1 HG_PENDING=$TESTTMP/test HG_PHASES_MOVED=1 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
321 remote: prepushkey hook: HG_BUNDLE2=1 HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_OLD=1 HG_PENDING=$TESTTMP/test HG_PHASES_MOVED=1 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
322 remote: pushkey-abort: prepushkey hook exited with status 1
322 remote: pushkey-abort: prepushkey hook exited with status 1
323 remote: transaction abort!
323 remote: transaction abort!
324 remote: rollback completed
324 remote: rollback completed
325 abort: updating ba677d0156c1 to public failed
325 abort: updating ba677d0156c1 to public failed
326 % serve errors
326 % serve errors
327 [255]
327 [255]
328 #endif
328 #endif
329
329
330 Make phases updates work
330 Make phases updates work
331
331
332 $ cat >> .hg/hgrc <<EOF
332 $ cat >> .hg/hgrc <<EOF
333 > [hooks]
333 > [hooks]
334 > prepushkey = sh -c "printenv.py prepushkey 0"
334 > prepushkey = sh -c "printenv.py prepushkey 0"
335 > EOF
335 > EOF
336
336
337 #if bundle1
337 #if bundle1
338 $ req
338 $ req
339 pushing to http://localhost:$HGPORT/
339 pushing to http://localhost:$HGPORT/
340 searching for changes
340 searching for changes
341 no changes found
341 no changes found
342 remote: prepushkey hook: HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_OLD=1
342 remote: prepushkey hook: HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_OLD=1
343 % serve errors
343 % serve errors
344 [1]
344 [1]
345 #endif
345 #endif
346
346
347 #if bundle2
347 #if bundle2
348 $ req
348 $ req
349 pushing to http://localhost:$HGPORT/
349 pushing to http://localhost:$HGPORT/
350 searching for changes
350 searching for changes
351 remote: adding changesets
351 remote: adding changesets
352 remote: adding manifests
352 remote: adding manifests
353 remote: adding file changes
353 remote: adding file changes
354 remote: added 1 changesets with 1 changes to 1 files
354 remote: added 1 changesets with 1 changes to 1 files
355 remote: prepushkey hook: HG_BUNDLE2=1 HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_OLD=1 HG_PENDING=$TESTTMP/test HG_PHASES_MOVED=1 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
355 remote: prepushkey hook: HG_BUNDLE2=1 HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NAMESPACE=phases HG_NEW=0 HG_NODE=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_NODE_LAST=ba677d0156c1196c1a699fa53f390dcfc3ce3872 HG_OLD=1 HG_PENDING=$TESTTMP/test HG_PHASES_MOVED=1 HG_SOURCE=serve HG_TXNID=TXN:$ID$ HG_URL=remote:http:$LOCALIP: (glob)
356 % serve errors
356 % serve errors
357 #endif
357 #endif
358
358
359 $ hg --config extensions.strip= strip -r 1:
359 $ hg --config extensions.strip= strip -r 1:
360 saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-eea704d7-backup.hg
360 saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-eea704d7-backup.hg
361
361
362 #if bundle2
362 #if bundle2
363
363
364 $ cat > .hg/hgrc <<EOF
364 $ cat > .hg/hgrc <<EOF
365 > [web]
365 > [web]
366 > push_ssl = false
366 > push_ssl = false
367 > allow_push = *
367 > allow_push = *
368 > [experimental]
368 > [experimental]
369 > httppostargs=true
369 > httppostargs=true
370 > EOF
370 > EOF
371 $ req
371 $ req
372 pushing to http://localhost:$HGPORT/
372 pushing to http://localhost:$HGPORT/
373 searching for changes
373 searching for changes
374 remote: adding changesets
374 remote: adding changesets
375 remote: adding manifests
375 remote: adding manifests
376 remote: adding file changes
376 remote: adding file changes
377 remote: added 1 changesets with 1 changes to 1 files
377 remote: added 1 changesets with 1 changes to 1 files
378 % serve errors
378 % serve errors
379
379
380 #endif
380 #endif
381
381
382 $ cd ..
382 $ cd ..
383
384 Pushing via hgwebdir works
385
386 $ hg init hgwebdir
387 $ cd hgwebdir
388 $ echo 0 > a
389 $ hg -q commit -A -m initial
390 $ cd ..
391
392 $ cat > web.conf << EOF
393 > [paths]
394 > / = *
395 > [web]
396 > push_ssl = false
397 > allow_push = *
398 > EOF
399
400 $ hg serve --web-conf web.conf -p $HGPORT -d --pid-file hg.pid
401 $ cat hg.pid > $DAEMON_PIDS
402
403 $ hg clone http://localhost:$HGPORT/hgwebdir hgwebdir-local
404 requesting all changes
405 adding changesets
406 adding manifests
407 adding file changes
408 added 1 changesets with 1 changes to 1 files
409 new changesets 98a3f8f02ba7
410 updating to branch default
411 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
412 $ cd hgwebdir-local
413 $ echo commit > a
414 $ hg commit -m 'local commit'
415
416 $ hg push
417 pushing to http://localhost:$HGPORT/hgwebdir
418 searching for changes
419 remote: adding changesets
420 remote: adding manifests
421 remote: adding file changes
422 remote: added 1 changesets with 1 changes to 1 files
423
424 $ killdaemons.py
425
426 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now