##// END OF EJS Templates
py3: get bytes-repr of network errors portably...
Augie Fackler -
r36272:af0a19d8 default
parent child Browse files
Show More
@@ -1,483 +1,483 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 HTTP_NOT_FOUND,
17 HTTP_NOT_FOUND,
18 HTTP_NOT_MODIFIED,
18 HTTP_NOT_MODIFIED,
19 HTTP_OK,
19 HTTP_OK,
20 HTTP_SERVER_ERROR,
20 HTTP_SERVER_ERROR,
21 caching,
21 caching,
22 cspvalues,
22 cspvalues,
23 permhooks,
23 permhooks,
24 )
24 )
25 from .request import wsgirequest
25 from .request import wsgirequest
26
26
27 from .. import (
27 from .. import (
28 encoding,
28 encoding,
29 error,
29 error,
30 hg,
30 hg,
31 hook,
31 hook,
32 profiling,
32 profiling,
33 pycompat,
33 pycompat,
34 repoview,
34 repoview,
35 templatefilters,
35 templatefilters,
36 templater,
36 templater,
37 ui as uimod,
37 ui as uimod,
38 util,
38 util,
39 wireprotoserver,
39 wireprotoserver,
40 )
40 )
41
41
42 from . import (
42 from . import (
43 webcommands,
43 webcommands,
44 webutil,
44 webutil,
45 wsgicgi,
45 wsgicgi,
46 )
46 )
47
47
48 perms = {
48 perms = {
49 'changegroup': 'pull',
49 'changegroup': 'pull',
50 'changegroupsubset': 'pull',
50 'changegroupsubset': 'pull',
51 'getbundle': 'pull',
51 'getbundle': 'pull',
52 'stream_out': 'pull',
52 'stream_out': 'pull',
53 'listkeys': 'pull',
53 'listkeys': 'pull',
54 'unbundle': 'push',
54 'unbundle': 'push',
55 'pushkey': 'push',
55 'pushkey': 'push',
56 }
56 }
57
57
58 archivespecs = util.sortdict((
58 archivespecs = util.sortdict((
59 ('zip', ('application/zip', 'zip', '.zip', None)),
59 ('zip', ('application/zip', 'zip', '.zip', None)),
60 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
60 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
61 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
61 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
62 ))
62 ))
63
63
64 def getstyle(req, configfn, templatepath):
64 def getstyle(req, configfn, templatepath):
65 fromreq = req.form.get('style', [None])[0]
65 fromreq = req.form.get('style', [None])[0]
66 if fromreq is not None:
66 if fromreq is not None:
67 fromreq = pycompat.sysbytes(fromreq)
67 fromreq = pycompat.sysbytes(fromreq)
68 styles = (
68 styles = (
69 fromreq,
69 fromreq,
70 configfn('web', 'style'),
70 configfn('web', 'style'),
71 'paper',
71 'paper',
72 )
72 )
73 return styles, templater.stylemap(styles, templatepath)
73 return styles, templater.stylemap(styles, templatepath)
74
74
75 def makebreadcrumb(url, prefix=''):
75 def makebreadcrumb(url, prefix=''):
76 '''Return a 'URL breadcrumb' list
76 '''Return a 'URL breadcrumb' list
77
77
78 A 'URL breadcrumb' is a list of URL-name pairs,
78 A 'URL breadcrumb' is a list of URL-name pairs,
79 corresponding to each of the path items on a URL.
79 corresponding to each of the path items on a URL.
80 This can be used to create path navigation entries.
80 This can be used to create path navigation entries.
81 '''
81 '''
82 if url.endswith('/'):
82 if url.endswith('/'):
83 url = url[:-1]
83 url = url[:-1]
84 if prefix:
84 if prefix:
85 url = '/' + prefix + url
85 url = '/' + prefix + url
86 relpath = url
86 relpath = url
87 if relpath.startswith('/'):
87 if relpath.startswith('/'):
88 relpath = relpath[1:]
88 relpath = relpath[1:]
89
89
90 breadcrumb = []
90 breadcrumb = []
91 urlel = url
91 urlel = url
92 pathitems = [''] + relpath.split('/')
92 pathitems = [''] + relpath.split('/')
93 for pathel in reversed(pathitems):
93 for pathel in reversed(pathitems):
94 if not pathel or not urlel:
94 if not pathel or not urlel:
95 break
95 break
96 breadcrumb.append({'url': urlel, 'name': pathel})
96 breadcrumb.append({'url': urlel, 'name': pathel})
97 urlel = os.path.dirname(urlel)
97 urlel = os.path.dirname(urlel)
98 return reversed(breadcrumb)
98 return reversed(breadcrumb)
99
99
100 class requestcontext(object):
100 class requestcontext(object):
101 """Holds state/context for an individual request.
101 """Holds state/context for an individual request.
102
102
103 Servers can be multi-threaded. Holding state on the WSGI application
103 Servers can be multi-threaded. Holding state on the WSGI application
104 is prone to race conditions. Instances of this class exist to hold
104 is prone to race conditions. Instances of this class exist to hold
105 mutable and race-free state for requests.
105 mutable and race-free state for requests.
106 """
106 """
107 def __init__(self, app, repo):
107 def __init__(self, app, repo):
108 self.repo = repo
108 self.repo = repo
109 self.reponame = app.reponame
109 self.reponame = app.reponame
110
110
111 self.archivespecs = archivespecs
111 self.archivespecs = archivespecs
112
112
113 self.maxchanges = self.configint('web', 'maxchanges')
113 self.maxchanges = self.configint('web', 'maxchanges')
114 self.stripecount = self.configint('web', 'stripes')
114 self.stripecount = self.configint('web', 'stripes')
115 self.maxshortchanges = self.configint('web', 'maxshortchanges')
115 self.maxshortchanges = self.configint('web', 'maxshortchanges')
116 self.maxfiles = self.configint('web', 'maxfiles')
116 self.maxfiles = self.configint('web', 'maxfiles')
117 self.allowpull = self.configbool('web', 'allow-pull')
117 self.allowpull = self.configbool('web', 'allow-pull')
118
118
119 # we use untrusted=False to prevent a repo owner from using
119 # we use untrusted=False to prevent a repo owner from using
120 # web.templates in .hg/hgrc to get access to any file readable
120 # web.templates in .hg/hgrc to get access to any file readable
121 # by the user running the CGI script
121 # by the user running the CGI script
122 self.templatepath = self.config('web', 'templates', untrusted=False)
122 self.templatepath = self.config('web', 'templates', untrusted=False)
123
123
124 # This object is more expensive to build than simple config values.
124 # This object is more expensive to build than simple config values.
125 # It is shared across requests. The app will replace the object
125 # It is shared across requests. The app will replace the object
126 # if it is updated. Since this is a reference and nothing should
126 # if it is updated. Since this is a reference and nothing should
127 # modify the underlying object, it should be constant for the lifetime
127 # modify the underlying object, it should be constant for the lifetime
128 # of the request.
128 # of the request.
129 self.websubtable = app.websubtable
129 self.websubtable = app.websubtable
130
130
131 self.csp, self.nonce = cspvalues(self.repo.ui)
131 self.csp, self.nonce = cspvalues(self.repo.ui)
132
132
133 # Trust the settings from the .hg/hgrc files by default.
133 # Trust the settings from the .hg/hgrc files by default.
134 def config(self, section, name, default=uimod._unset, untrusted=True):
134 def config(self, section, name, default=uimod._unset, untrusted=True):
135 return self.repo.ui.config(section, name, default,
135 return self.repo.ui.config(section, name, default,
136 untrusted=untrusted)
136 untrusted=untrusted)
137
137
138 def configbool(self, section, name, default=uimod._unset, untrusted=True):
138 def configbool(self, section, name, default=uimod._unset, untrusted=True):
139 return self.repo.ui.configbool(section, name, default,
139 return self.repo.ui.configbool(section, name, default,
140 untrusted=untrusted)
140 untrusted=untrusted)
141
141
142 def configint(self, section, name, default=uimod._unset, untrusted=True):
142 def configint(self, section, name, default=uimod._unset, untrusted=True):
143 return self.repo.ui.configint(section, name, default,
143 return self.repo.ui.configint(section, name, default,
144 untrusted=untrusted)
144 untrusted=untrusted)
145
145
146 def configlist(self, section, name, default=uimod._unset, untrusted=True):
146 def configlist(self, section, name, default=uimod._unset, untrusted=True):
147 return self.repo.ui.configlist(section, name, default,
147 return self.repo.ui.configlist(section, name, default,
148 untrusted=untrusted)
148 untrusted=untrusted)
149
149
150 def archivelist(self, nodeid):
150 def archivelist(self, nodeid):
151 allowed = self.configlist('web', 'allow_archive')
151 allowed = self.configlist('web', 'allow_archive')
152 for typ, spec in self.archivespecs.iteritems():
152 for typ, spec in self.archivespecs.iteritems():
153 if typ in allowed or self.configbool('web', 'allow%s' % typ):
153 if typ in allowed or self.configbool('web', 'allow%s' % typ):
154 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
154 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
155
155
156 def templater(self, req):
156 def templater(self, req):
157 # determine scheme, port and server name
157 # determine scheme, port and server name
158 # this is needed to create absolute urls
158 # this is needed to create absolute urls
159
159
160 proto = req.env.get('wsgi.url_scheme')
160 proto = req.env.get('wsgi.url_scheme')
161 if proto == 'https':
161 if proto == 'https':
162 proto = 'https'
162 proto = 'https'
163 default_port = '443'
163 default_port = '443'
164 else:
164 else:
165 proto = 'http'
165 proto = 'http'
166 default_port = '80'
166 default_port = '80'
167
167
168 port = req.env[r'SERVER_PORT']
168 port = req.env[r'SERVER_PORT']
169 port = port != default_port and (r':' + port) or r''
169 port = port != default_port and (r':' + port) or r''
170 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
170 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
171 logourl = self.config('web', 'logourl')
171 logourl = self.config('web', 'logourl')
172 logoimg = self.config('web', 'logoimg')
172 logoimg = self.config('web', 'logoimg')
173 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
173 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
174 if not staticurl.endswith('/'):
174 if not staticurl.endswith('/'):
175 staticurl += '/'
175 staticurl += '/'
176
176
177 # some functions for the templater
177 # some functions for the templater
178
178
179 def motd(**map):
179 def motd(**map):
180 yield self.config('web', 'motd')
180 yield self.config('web', 'motd')
181
181
182 # figure out which style to use
182 # figure out which style to use
183
183
184 vars = {}
184 vars = {}
185 styles, (style, mapfile) = getstyle(req, self.config,
185 styles, (style, mapfile) = getstyle(req, self.config,
186 self.templatepath)
186 self.templatepath)
187 if style == styles[0]:
187 if style == styles[0]:
188 vars['style'] = style
188 vars['style'] = style
189
189
190 start = '&' if req.url[-1] == r'?' else '?'
190 start = '&' if req.url[-1] == r'?' else '?'
191 sessionvars = webutil.sessionvars(vars, start)
191 sessionvars = webutil.sessionvars(vars, start)
192
192
193 if not self.reponame:
193 if not self.reponame:
194 self.reponame = (self.config('web', 'name', '')
194 self.reponame = (self.config('web', 'name', '')
195 or req.env.get('REPO_NAME')
195 or req.env.get('REPO_NAME')
196 or req.url.strip('/') or self.repo.root)
196 or req.url.strip('/') or self.repo.root)
197
197
198 def websubfilter(text):
198 def websubfilter(text):
199 return templatefilters.websub(text, self.websubtable)
199 return templatefilters.websub(text, self.websubtable)
200
200
201 # create the templater
201 # create the templater
202
202
203 defaults = {
203 defaults = {
204 'url': req.url,
204 'url': req.url,
205 'logourl': logourl,
205 'logourl': logourl,
206 'logoimg': logoimg,
206 'logoimg': logoimg,
207 'staticurl': staticurl,
207 'staticurl': staticurl,
208 'urlbase': urlbase,
208 'urlbase': urlbase,
209 'repo': self.reponame,
209 'repo': self.reponame,
210 'encoding': encoding.encoding,
210 'encoding': encoding.encoding,
211 'motd': motd,
211 'motd': motd,
212 'sessionvars': sessionvars,
212 'sessionvars': sessionvars,
213 'pathdef': makebreadcrumb(req.url),
213 'pathdef': makebreadcrumb(req.url),
214 'style': style,
214 'style': style,
215 'nonce': self.nonce,
215 'nonce': self.nonce,
216 }
216 }
217 tmpl = templater.templater.frommapfile(mapfile,
217 tmpl = templater.templater.frommapfile(mapfile,
218 filters={'websub': websubfilter},
218 filters={'websub': websubfilter},
219 defaults=defaults)
219 defaults=defaults)
220 return tmpl
220 return tmpl
221
221
222
222
223 class hgweb(object):
223 class hgweb(object):
224 """HTTP server for individual repositories.
224 """HTTP server for individual repositories.
225
225
226 Instances of this class serve HTTP responses for a particular
226 Instances of this class serve HTTP responses for a particular
227 repository.
227 repository.
228
228
229 Instances are typically used as WSGI applications.
229 Instances are typically used as WSGI applications.
230
230
231 Some servers are multi-threaded. On these servers, there may
231 Some servers are multi-threaded. On these servers, there may
232 be multiple active threads inside __call__.
232 be multiple active threads inside __call__.
233 """
233 """
234 def __init__(self, repo, name=None, baseui=None):
234 def __init__(self, repo, name=None, baseui=None):
235 if isinstance(repo, str):
235 if isinstance(repo, str):
236 if baseui:
236 if baseui:
237 u = baseui.copy()
237 u = baseui.copy()
238 else:
238 else:
239 u = uimod.ui.load()
239 u = uimod.ui.load()
240 r = hg.repository(u, repo)
240 r = hg.repository(u, repo)
241 else:
241 else:
242 # we trust caller to give us a private copy
242 # we trust caller to give us a private copy
243 r = repo
243 r = repo
244
244
245 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
245 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
246 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
246 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
247 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
247 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
248 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
248 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
249 # resolve file patterns relative to repo root
249 # resolve file patterns relative to repo root
250 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
250 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
251 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
251 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
252 # displaying bundling progress bar while serving feel wrong and may
252 # displaying bundling progress bar while serving feel wrong and may
253 # break some wsgi implementation.
253 # break some wsgi implementation.
254 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
254 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
255 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
255 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
256 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
256 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
257 self._lastrepo = self._repos[0]
257 self._lastrepo = self._repos[0]
258 hook.redirect(True)
258 hook.redirect(True)
259 self.reponame = name
259 self.reponame = name
260
260
261 def _webifyrepo(self, repo):
261 def _webifyrepo(self, repo):
262 repo = getwebview(repo)
262 repo = getwebview(repo)
263 self.websubtable = webutil.getwebsubs(repo)
263 self.websubtable = webutil.getwebsubs(repo)
264 return repo
264 return repo
265
265
266 @contextlib.contextmanager
266 @contextlib.contextmanager
267 def _obtainrepo(self):
267 def _obtainrepo(self):
268 """Obtain a repo unique to the caller.
268 """Obtain a repo unique to the caller.
269
269
270 Internally we maintain a stack of cachedlocalrepo instances
270 Internally we maintain a stack of cachedlocalrepo instances
271 to be handed out. If one is available, we pop it and return it,
271 to be handed out. If one is available, we pop it and return it,
272 ensuring it is up to date in the process. If one is not available,
272 ensuring it is up to date in the process. If one is not available,
273 we clone the most recently used repo instance and return it.
273 we clone the most recently used repo instance and return it.
274
274
275 It is currently possible for the stack to grow without bounds
275 It is currently possible for the stack to grow without bounds
276 if the server allows infinite threads. However, servers should
276 if the server allows infinite threads. However, servers should
277 have a thread limit, thus establishing our limit.
277 have a thread limit, thus establishing our limit.
278 """
278 """
279 if self._repos:
279 if self._repos:
280 cached = self._repos.pop()
280 cached = self._repos.pop()
281 r, created = cached.fetch()
281 r, created = cached.fetch()
282 else:
282 else:
283 cached = self._lastrepo.copy()
283 cached = self._lastrepo.copy()
284 r, created = cached.fetch()
284 r, created = cached.fetch()
285 if created:
285 if created:
286 r = self._webifyrepo(r)
286 r = self._webifyrepo(r)
287
287
288 self._lastrepo = cached
288 self._lastrepo = cached
289 self.mtime = cached.mtime
289 self.mtime = cached.mtime
290 try:
290 try:
291 yield r
291 yield r
292 finally:
292 finally:
293 self._repos.append(cached)
293 self._repos.append(cached)
294
294
295 def run(self):
295 def run(self):
296 """Start a server from CGI environment.
296 """Start a server from CGI environment.
297
297
298 Modern servers should be using WSGI and should avoid this
298 Modern servers should be using WSGI and should avoid this
299 method, if possible.
299 method, if possible.
300 """
300 """
301 if not encoding.environ.get('GATEWAY_INTERFACE',
301 if not encoding.environ.get('GATEWAY_INTERFACE',
302 '').startswith("CGI/1."):
302 '').startswith("CGI/1."):
303 raise RuntimeError("This function is only intended to be "
303 raise RuntimeError("This function is only intended to be "
304 "called while running as a CGI script.")
304 "called while running as a CGI script.")
305 wsgicgi.launch(self)
305 wsgicgi.launch(self)
306
306
307 def __call__(self, env, respond):
307 def __call__(self, env, respond):
308 """Run the WSGI application.
308 """Run the WSGI application.
309
309
310 This may be called by multiple threads.
310 This may be called by multiple threads.
311 """
311 """
312 req = wsgirequest(env, respond)
312 req = wsgirequest(env, respond)
313 return self.run_wsgi(req)
313 return self.run_wsgi(req)
314
314
315 def run_wsgi(self, req):
315 def run_wsgi(self, req):
316 """Internal method to run the WSGI application.
316 """Internal method to run the WSGI application.
317
317
318 This is typically only called by Mercurial. External consumers
318 This is typically only called by Mercurial. External consumers
319 should be using instances of this class as the WSGI application.
319 should be using instances of this class as the WSGI application.
320 """
320 """
321 with self._obtainrepo() as repo:
321 with self._obtainrepo() as repo:
322 profile = repo.ui.configbool('profiling', 'enabled')
322 profile = repo.ui.configbool('profiling', 'enabled')
323 with profiling.profile(repo.ui, enabled=profile):
323 with profiling.profile(repo.ui, enabled=profile):
324 for r in self._runwsgi(req, repo):
324 for r in self._runwsgi(req, repo):
325 yield r
325 yield r
326
326
327 def _runwsgi(self, req, repo):
327 def _runwsgi(self, req, repo):
328 rctx = requestcontext(self, repo)
328 rctx = requestcontext(self, repo)
329
329
330 # This state is global across all threads.
330 # This state is global across all threads.
331 encoding.encoding = rctx.config('web', 'encoding')
331 encoding.encoding = rctx.config('web', 'encoding')
332 rctx.repo.ui.environ = req.env
332 rctx.repo.ui.environ = req.env
333
333
334 if rctx.csp:
334 if rctx.csp:
335 # hgwebdir may have added CSP header. Since we generate our own,
335 # hgwebdir may have added CSP header. Since we generate our own,
336 # replace it.
336 # replace it.
337 req.headers = [h for h in req.headers
337 req.headers = [h for h in req.headers
338 if h[0] != 'Content-Security-Policy']
338 if h[0] != 'Content-Security-Policy']
339 req.headers.append(('Content-Security-Policy', rctx.csp))
339 req.headers.append(('Content-Security-Policy', rctx.csp))
340
340
341 # work with CGI variables to create coherent structure
341 # work with CGI variables to create coherent structure
342 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
342 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
343
343
344 req.url = req.env[r'SCRIPT_NAME']
344 req.url = req.env[r'SCRIPT_NAME']
345 if not req.url.endswith('/'):
345 if not req.url.endswith('/'):
346 req.url += '/'
346 req.url += '/'
347 if req.env.get('REPO_NAME'):
347 if req.env.get('REPO_NAME'):
348 req.url += req.env[r'REPO_NAME'] + r'/'
348 req.url += req.env[r'REPO_NAME'] + r'/'
349
349
350 if r'PATH_INFO' in req.env:
350 if r'PATH_INFO' in req.env:
351 parts = req.env[r'PATH_INFO'].strip('/').split('/')
351 parts = req.env[r'PATH_INFO'].strip('/').split('/')
352 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
352 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
353 if parts[:len(repo_parts)] == repo_parts:
353 if parts[:len(repo_parts)] == repo_parts:
354 parts = parts[len(repo_parts):]
354 parts = parts[len(repo_parts):]
355 query = '/'.join(parts)
355 query = '/'.join(parts)
356 else:
356 else:
357 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
357 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
358 query = query.partition(r';')[0]
358 query = query.partition(r';')[0]
359
359
360 # Route it to a wire protocol handler if it looks like a wire protocol
360 # Route it to a wire protocol handler if it looks like a wire protocol
361 # request.
361 # request.
362 protohandler = wireprotoserver.parsehttprequest(rctx.repo, req, query)
362 protohandler = wireprotoserver.parsehttprequest(rctx.repo, req, query)
363
363
364 if protohandler:
364 if protohandler:
365 cmd = protohandler['cmd']
365 cmd = protohandler['cmd']
366 try:
366 try:
367 if query:
367 if query:
368 raise ErrorResponse(HTTP_NOT_FOUND)
368 raise ErrorResponse(HTTP_NOT_FOUND)
369 if cmd in perms:
369 if cmd in perms:
370 self.check_perm(rctx, req, perms[cmd])
370 self.check_perm(rctx, req, perms[cmd])
371 except ErrorResponse as inst:
371 except ErrorResponse as inst:
372 return protohandler['handleerror'](inst)
372 return protohandler['handleerror'](inst)
373
373
374 return protohandler['dispatch']()
374 return protohandler['dispatch']()
375
375
376 # translate user-visible url structure to internal structure
376 # translate user-visible url structure to internal structure
377
377
378 args = query.split('/', 2)
378 args = query.split('/', 2)
379 if r'cmd' not in req.form and args and args[0]:
379 if r'cmd' not in req.form and args and args[0]:
380 cmd = args.pop(0)
380 cmd = args.pop(0)
381 style = cmd.rfind('-')
381 style = cmd.rfind('-')
382 if style != -1:
382 if style != -1:
383 req.form['style'] = [cmd[:style]]
383 req.form['style'] = [cmd[:style]]
384 cmd = cmd[style + 1:]
384 cmd = cmd[style + 1:]
385
385
386 # avoid accepting e.g. style parameter as command
386 # avoid accepting e.g. style parameter as command
387 if util.safehasattr(webcommands, cmd):
387 if util.safehasattr(webcommands, cmd):
388 req.form[r'cmd'] = [cmd]
388 req.form[r'cmd'] = [cmd]
389
389
390 if cmd == 'static':
390 if cmd == 'static':
391 req.form['file'] = ['/'.join(args)]
391 req.form['file'] = ['/'.join(args)]
392 else:
392 else:
393 if args and args[0]:
393 if args and args[0]:
394 node = args.pop(0).replace('%2F', '/')
394 node = args.pop(0).replace('%2F', '/')
395 req.form['node'] = [node]
395 req.form['node'] = [node]
396 if args:
396 if args:
397 req.form['file'] = args
397 req.form['file'] = args
398
398
399 ua = req.env.get('HTTP_USER_AGENT', '')
399 ua = req.env.get('HTTP_USER_AGENT', '')
400 if cmd == 'rev' and 'mercurial' in ua:
400 if cmd == 'rev' and 'mercurial' in ua:
401 req.form['style'] = ['raw']
401 req.form['style'] = ['raw']
402
402
403 if cmd == 'archive':
403 if cmd == 'archive':
404 fn = req.form['node'][0]
404 fn = req.form['node'][0]
405 for type_, spec in rctx.archivespecs.iteritems():
405 for type_, spec in rctx.archivespecs.iteritems():
406 ext = spec[2]
406 ext = spec[2]
407 if fn.endswith(ext):
407 if fn.endswith(ext):
408 req.form['node'] = [fn[:-len(ext)]]
408 req.form['node'] = [fn[:-len(ext)]]
409 req.form['type'] = [type_]
409 req.form['type'] = [type_]
410 else:
410 else:
411 cmd = pycompat.sysbytes(req.form.get(r'cmd', [r''])[0])
411 cmd = pycompat.sysbytes(req.form.get(r'cmd', [r''])[0])
412
412
413 # process the web interface request
413 # process the web interface request
414
414
415 try:
415 try:
416 tmpl = rctx.templater(req)
416 tmpl = rctx.templater(req)
417 ctype = tmpl('mimetype', encoding=encoding.encoding)
417 ctype = tmpl('mimetype', encoding=encoding.encoding)
418 ctype = templater.stringify(ctype)
418 ctype = templater.stringify(ctype)
419
419
420 # check read permissions non-static content
420 # check read permissions non-static content
421 if cmd != 'static':
421 if cmd != 'static':
422 self.check_perm(rctx, req, None)
422 self.check_perm(rctx, req, None)
423
423
424 if cmd == '':
424 if cmd == '':
425 req.form[r'cmd'] = [tmpl.cache['default']]
425 req.form[r'cmd'] = [tmpl.cache['default']]
426 cmd = req.form[r'cmd'][0]
426 cmd = req.form[r'cmd'][0]
427
427
428 # Don't enable caching if using a CSP nonce because then it wouldn't
428 # Don't enable caching if using a CSP nonce because then it wouldn't
429 # be a nonce.
429 # be a nonce.
430 if rctx.configbool('web', 'cache') and not rctx.nonce:
430 if rctx.configbool('web', 'cache') and not rctx.nonce:
431 caching(self, req) # sets ETag header or raises NOT_MODIFIED
431 caching(self, req) # sets ETag header or raises NOT_MODIFIED
432 if cmd not in webcommands.__all__:
432 if cmd not in webcommands.__all__:
433 msg = 'no such method: %s' % cmd
433 msg = 'no such method: %s' % cmd
434 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
434 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
435 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
435 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
436 rctx.ctype = ctype
436 rctx.ctype = ctype
437 content = webcommands.rawfile(rctx, req, tmpl)
437 content = webcommands.rawfile(rctx, req, tmpl)
438 else:
438 else:
439 content = getattr(webcommands, cmd)(rctx, req, tmpl)
439 content = getattr(webcommands, cmd)(rctx, req, tmpl)
440 req.respond(HTTP_OK, ctype)
440 req.respond(HTTP_OK, ctype)
441
441
442 return content
442 return content
443
443
444 except (error.LookupError, error.RepoLookupError) as err:
444 except (error.LookupError, error.RepoLookupError) as err:
445 req.respond(HTTP_NOT_FOUND, ctype)
445 req.respond(HTTP_NOT_FOUND, ctype)
446 msg = str(err)
446 msg = pycompat.bytestr(err)
447 if (util.safehasattr(err, 'name') and
447 if (util.safehasattr(err, 'name') and
448 not isinstance(err, error.ManifestLookupError)):
448 not isinstance(err, error.ManifestLookupError)):
449 msg = 'revision not found: %s' % err.name
449 msg = 'revision not found: %s' % err.name
450 return tmpl('error', error=msg)
450 return tmpl('error', error=msg)
451 except (error.RepoError, error.RevlogError) as inst:
451 except (error.RepoError, error.RevlogError) as inst:
452 req.respond(HTTP_SERVER_ERROR, ctype)
452 req.respond(HTTP_SERVER_ERROR, ctype)
453 return tmpl('error', error=str(inst))
453 return tmpl('error', error=pycompat.bytestr(inst))
454 except ErrorResponse as inst:
454 except ErrorResponse as inst:
455 req.respond(inst, ctype)
455 req.respond(inst, ctype)
456 if inst.code == HTTP_NOT_MODIFIED:
456 if inst.code == HTTP_NOT_MODIFIED:
457 # Not allowed to return a body on a 304
457 # Not allowed to return a body on a 304
458 return ['']
458 return ['']
459 return tmpl('error', error=str(inst))
459 return tmpl('error', error=pycompat.bytestr(inst))
460
460
461 def check_perm(self, rctx, req, op):
461 def check_perm(self, rctx, req, op):
462 for permhook in permhooks:
462 for permhook in permhooks:
463 permhook(rctx, req, op)
463 permhook(rctx, req, op)
464
464
465 def getwebview(repo):
465 def getwebview(repo):
466 """The 'web.view' config controls changeset filter to hgweb. Possible
466 """The 'web.view' config controls changeset filter to hgweb. Possible
467 values are ``served``, ``visible`` and ``all``. Default is ``served``.
467 values are ``served``, ``visible`` and ``all``. Default is ``served``.
468 The ``served`` filter only shows changesets that can be pulled from the
468 The ``served`` filter only shows changesets that can be pulled from the
469 hgweb instance. The``visible`` filter includes secret changesets but
469 hgweb instance. The``visible`` filter includes secret changesets but
470 still excludes "hidden" one.
470 still excludes "hidden" one.
471
471
472 See the repoview module for details.
472 See the repoview module for details.
473
473
474 The option has been around undocumented since Mercurial 2.5, but no
474 The option has been around undocumented since Mercurial 2.5, but no
475 user ever asked about it. So we better keep it undocumented for now."""
475 user ever asked about it. So we better keep it undocumented for now."""
476 # experimental config: web.view
476 # experimental config: web.view
477 viewconfig = repo.ui.config('web', 'view', untrusted=True)
477 viewconfig = repo.ui.config('web', 'view', untrusted=True)
478 if viewconfig == 'all':
478 if viewconfig == 'all':
479 return repo.unfiltered()
479 return repo.unfiltered()
480 elif viewconfig in repoview.filtertable:
480 elif viewconfig in repoview.filtertable:
481 return repo.filtered(viewconfig)
481 return repo.filtered(viewconfig)
482 else:
482 else:
483 return repo.filtered('served')
483 return repo.filtered('served')
@@ -1,155 +1,156 b''
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 cgi
11 import cgi
12 import errno
12 import errno
13 import socket
13 import socket
14
14
15 from .common import (
15 from .common import (
16 ErrorResponse,
16 ErrorResponse,
17 HTTP_NOT_MODIFIED,
17 HTTP_NOT_MODIFIED,
18 statusmessage,
18 statusmessage,
19 )
19 )
20
20
21 from .. import (
21 from .. import (
22 pycompat,
22 pycompat,
23 util,
23 util,
24 )
24 )
25
25
26 shortcuts = {
26 shortcuts = {
27 'cl': [('cmd', ['changelog']), ('rev', None)],
27 'cl': [('cmd', ['changelog']), ('rev', None)],
28 'sl': [('cmd', ['shortlog']), ('rev', None)],
28 'sl': [('cmd', ['shortlog']), ('rev', None)],
29 'cs': [('cmd', ['changeset']), ('node', None)],
29 'cs': [('cmd', ['changeset']), ('node', None)],
30 'f': [('cmd', ['file']), ('filenode', None)],
30 'f': [('cmd', ['file']), ('filenode', None)],
31 'fl': [('cmd', ['filelog']), ('filenode', None)],
31 'fl': [('cmd', ['filelog']), ('filenode', None)],
32 'fd': [('cmd', ['filediff']), ('node', None)],
32 'fd': [('cmd', ['filediff']), ('node', None)],
33 'fa': [('cmd', ['annotate']), ('filenode', None)],
33 'fa': [('cmd', ['annotate']), ('filenode', None)],
34 'mf': [('cmd', ['manifest']), ('manifest', None)],
34 'mf': [('cmd', ['manifest']), ('manifest', None)],
35 'ca': [('cmd', ['archive']), ('node', None)],
35 'ca': [('cmd', ['archive']), ('node', None)],
36 'tags': [('cmd', ['tags'])],
36 'tags': [('cmd', ['tags'])],
37 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
37 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
38 'static': [('cmd', ['static']), ('file', None)]
38 'static': [('cmd', ['static']), ('file', None)]
39 }
39 }
40
40
41 def normalize(form):
41 def normalize(form):
42 # first expand the shortcuts
42 # first expand the shortcuts
43 for k in shortcuts:
43 for k in shortcuts:
44 if k in form:
44 if k in form:
45 for name, value in shortcuts[k]:
45 for name, value in shortcuts[k]:
46 if value is None:
46 if value is None:
47 value = form[k]
47 value = form[k]
48 form[name] = value
48 form[name] = value
49 del form[k]
49 del form[k]
50 # And strip the values
50 # And strip the values
51 for k, v in form.iteritems():
51 for k, v in form.iteritems():
52 form[k] = [i.strip() for i in v]
52 form[k] = [i.strip() for i in v]
53 return form
53 return form
54
54
55 class wsgirequest(object):
55 class wsgirequest(object):
56 """Higher-level API for a WSGI request.
56 """Higher-level API for a WSGI request.
57
57
58 WSGI applications are invoked with 2 arguments. They are used to
58 WSGI applications are invoked with 2 arguments. They are used to
59 instantiate instances of this class, which provides higher-level APIs
59 instantiate instances of this class, which provides higher-level APIs
60 for obtaining request parameters, writing HTTP output, etc.
60 for obtaining request parameters, writing HTTP output, etc.
61 """
61 """
62 def __init__(self, wsgienv, start_response):
62 def __init__(self, wsgienv, start_response):
63 version = wsgienv[r'wsgi.version']
63 version = wsgienv[r'wsgi.version']
64 if (version < (1, 0)) or (version >= (2, 0)):
64 if (version < (1, 0)) or (version >= (2, 0)):
65 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
65 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
66 % version)
66 % version)
67 self.inp = wsgienv[r'wsgi.input']
67 self.inp = wsgienv[r'wsgi.input']
68 self.err = wsgienv[r'wsgi.errors']
68 self.err = wsgienv[r'wsgi.errors']
69 self.threaded = wsgienv[r'wsgi.multithread']
69 self.threaded = wsgienv[r'wsgi.multithread']
70 self.multiprocess = wsgienv[r'wsgi.multiprocess']
70 self.multiprocess = wsgienv[r'wsgi.multiprocess']
71 self.run_once = wsgienv[r'wsgi.run_once']
71 self.run_once = wsgienv[r'wsgi.run_once']
72 self.env = wsgienv
72 self.env = wsgienv
73 self.form = normalize(cgi.parse(self.inp,
73 self.form = normalize(cgi.parse(self.inp,
74 self.env,
74 self.env,
75 keep_blank_values=1))
75 keep_blank_values=1))
76 self._start_response = start_response
76 self._start_response = start_response
77 self.server_write = None
77 self.server_write = None
78 self.headers = []
78 self.headers = []
79
79
80 def __iter__(self):
80 def __iter__(self):
81 return iter([])
81 return iter([])
82
82
83 def read(self, count=-1):
83 def read(self, count=-1):
84 return self.inp.read(count)
84 return self.inp.read(count)
85
85
86 def drain(self):
86 def drain(self):
87 '''need to read all data from request, httplib is half-duplex'''
87 '''need to read all data from request, httplib is half-duplex'''
88 length = int(self.env.get('CONTENT_LENGTH') or 0)
88 length = int(self.env.get('CONTENT_LENGTH') or 0)
89 for s in util.filechunkiter(self.inp, limit=length):
89 for s in util.filechunkiter(self.inp, limit=length):
90 pass
90 pass
91
91
92 def respond(self, status, type, filename=None, body=None):
92 def respond(self, status, type, filename=None, body=None):
93 if not isinstance(type, str):
93 if not isinstance(type, str):
94 type = pycompat.sysstr(type)
94 type = pycompat.sysstr(type)
95 if self._start_response is not None:
95 if self._start_response is not None:
96 self.headers.append((r'Content-Type', type))
96 self.headers.append((r'Content-Type', type))
97 if filename:
97 if filename:
98 filename = (filename.rpartition('/')[-1]
98 filename = (filename.rpartition('/')[-1]
99 .replace('\\', '\\\\').replace('"', '\\"'))
99 .replace('\\', '\\\\').replace('"', '\\"'))
100 self.headers.append(('Content-Disposition',
100 self.headers.append(('Content-Disposition',
101 'inline; filename="%s"' % filename))
101 'inline; filename="%s"' % filename))
102 if body is not None:
102 if body is not None:
103 self.headers.append((r'Content-Length', str(len(body))))
103 self.headers.append((r'Content-Length', str(len(body))))
104
104
105 for k, v in self.headers:
105 for k, v in self.headers:
106 if not isinstance(v, str):
106 if not isinstance(v, str):
107 raise TypeError('header value must be string: %r' % (v,))
107 raise TypeError('header value must be string: %r' % (v,))
108
108
109 if isinstance(status, ErrorResponse):
109 if isinstance(status, ErrorResponse):
110 self.headers.extend(status.headers)
110 self.headers.extend(status.headers)
111 if status.code == HTTP_NOT_MODIFIED:
111 if status.code == HTTP_NOT_MODIFIED:
112 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
112 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
113 # it MUST NOT include any headers other than these and no
113 # it MUST NOT include any headers other than these and no
114 # body
114 # body
115 self.headers = [(k, v) for (k, v) in self.headers if
115 self.headers = [(k, v) for (k, v) in self.headers if
116 k in ('Date', 'ETag', 'Expires',
116 k in ('Date', 'ETag', 'Expires',
117 'Cache-Control', 'Vary')]
117 'Cache-Control', 'Vary')]
118 status = statusmessage(status.code, pycompat.bytestr(status))
118 status = statusmessage(status.code, pycompat.bytestr(status))
119 elif status == 200:
119 elif status == 200:
120 status = '200 Script output follows'
120 status = '200 Script output follows'
121 elif isinstance(status, int):
121 elif isinstance(status, int):
122 status = statusmessage(status)
122 status = statusmessage(status)
123
123
124 self.server_write = self._start_response(status, self.headers)
124 self.server_write = self._start_response(
125 pycompat.sysstr(status), self.headers)
125 self._start_response = None
126 self._start_response = None
126 self.headers = []
127 self.headers = []
127 if body is not None:
128 if body is not None:
128 self.write(body)
129 self.write(body)
129 self.server_write = None
130 self.server_write = None
130
131
131 def write(self, thing):
132 def write(self, thing):
132 if thing:
133 if thing:
133 try:
134 try:
134 self.server_write(thing)
135 self.server_write(thing)
135 except socket.error as inst:
136 except socket.error as inst:
136 if inst[0] != errno.ECONNRESET:
137 if inst[0] != errno.ECONNRESET:
137 raise
138 raise
138
139
139 def writelines(self, lines):
140 def writelines(self, lines):
140 for line in lines:
141 for line in lines:
141 self.write(line)
142 self.write(line)
142
143
143 def flush(self):
144 def flush(self):
144 return None
145 return None
145
146
146 def close(self):
147 def close(self):
147 return None
148 return None
148
149
149 def wsgiapplication(app_maker):
150 def wsgiapplication(app_maker):
150 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
151 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
151 can and should now be used as a WSGI application.'''
152 can and should now be used as a WSGI application.'''
152 application = app_maker()
153 application = app_maker()
153 def run_wsgi(env, respond):
154 def run_wsgi(env, respond):
154 return application(env, respond)
155 return application(env, respond)
155 return run_wsgi
156 return run_wsgi
@@ -1,654 +1,654 b''
1 # hgweb/webutil.py - utility library for the web interface.
1 # hgweb/webutil.py - utility library for the web interface.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import copy
11 import copy
12 import difflib
12 import difflib
13 import os
13 import os
14 import re
14 import re
15
15
16 from ..i18n import _
16 from ..i18n import _
17 from ..node import hex, nullid, short
17 from ..node import hex, nullid, short
18
18
19 from .common import (
19 from .common import (
20 ErrorResponse,
20 ErrorResponse,
21 HTTP_BAD_REQUEST,
21 HTTP_BAD_REQUEST,
22 HTTP_NOT_FOUND,
22 HTTP_NOT_FOUND,
23 paritygen,
23 paritygen,
24 )
24 )
25
25
26 from .. import (
26 from .. import (
27 context,
27 context,
28 error,
28 error,
29 match,
29 match,
30 mdiff,
30 mdiff,
31 patch,
31 patch,
32 pathutil,
32 pathutil,
33 pycompat,
33 pycompat,
34 templatefilters,
34 templatefilters,
35 templatekw,
35 templatekw,
36 ui as uimod,
36 ui as uimod,
37 util,
37 util,
38 )
38 )
39
39
40 def up(p):
40 def up(p):
41 if p[0] != "/":
41 if p[0] != "/":
42 p = "/" + p
42 p = "/" + p
43 if p[-1] == "/":
43 if p[-1] == "/":
44 p = p[:-1]
44 p = p[:-1]
45 up = os.path.dirname(p)
45 up = os.path.dirname(p)
46 if up == "/":
46 if up == "/":
47 return "/"
47 return "/"
48 return up + "/"
48 return up + "/"
49
49
50 def _navseq(step, firststep=None):
50 def _navseq(step, firststep=None):
51 if firststep:
51 if firststep:
52 yield firststep
52 yield firststep
53 if firststep >= 20 and firststep <= 40:
53 if firststep >= 20 and firststep <= 40:
54 firststep = 50
54 firststep = 50
55 yield firststep
55 yield firststep
56 assert step > 0
56 assert step > 0
57 assert firststep > 0
57 assert firststep > 0
58 while step <= firststep:
58 while step <= firststep:
59 step *= 10
59 step *= 10
60 while True:
60 while True:
61 yield 1 * step
61 yield 1 * step
62 yield 3 * step
62 yield 3 * step
63 step *= 10
63 step *= 10
64
64
65 class revnav(object):
65 class revnav(object):
66
66
67 def __init__(self, repo):
67 def __init__(self, repo):
68 """Navigation generation object
68 """Navigation generation object
69
69
70 :repo: repo object we generate nav for
70 :repo: repo object we generate nav for
71 """
71 """
72 # used for hex generation
72 # used for hex generation
73 self._revlog = repo.changelog
73 self._revlog = repo.changelog
74
74
75 def __nonzero__(self):
75 def __nonzero__(self):
76 """return True if any revision to navigate over"""
76 """return True if any revision to navigate over"""
77 return self._first() is not None
77 return self._first() is not None
78
78
79 __bool__ = __nonzero__
79 __bool__ = __nonzero__
80
80
81 def _first(self):
81 def _first(self):
82 """return the minimum non-filtered changeset or None"""
82 """return the minimum non-filtered changeset or None"""
83 try:
83 try:
84 return next(iter(self._revlog))
84 return next(iter(self._revlog))
85 except StopIteration:
85 except StopIteration:
86 return None
86 return None
87
87
88 def hex(self, rev):
88 def hex(self, rev):
89 return hex(self._revlog.node(rev))
89 return hex(self._revlog.node(rev))
90
90
91 def gen(self, pos, pagelen, limit):
91 def gen(self, pos, pagelen, limit):
92 """computes label and revision id for navigation link
92 """computes label and revision id for navigation link
93
93
94 :pos: is the revision relative to which we generate navigation.
94 :pos: is the revision relative to which we generate navigation.
95 :pagelen: the size of each navigation page
95 :pagelen: the size of each navigation page
96 :limit: how far shall we link
96 :limit: how far shall we link
97
97
98 The return is:
98 The return is:
99 - a single element tuple
99 - a single element tuple
100 - containing a dictionary with a `before` and `after` key
100 - containing a dictionary with a `before` and `after` key
101 - values are generator functions taking arbitrary number of kwargs
101 - values are generator functions taking arbitrary number of kwargs
102 - yield items are dictionaries with `label` and `node` keys
102 - yield items are dictionaries with `label` and `node` keys
103 """
103 """
104 if not self:
104 if not self:
105 # empty repo
105 # empty repo
106 return ({'before': (), 'after': ()},)
106 return ({'before': (), 'after': ()},)
107
107
108 targets = []
108 targets = []
109 for f in _navseq(1, pagelen):
109 for f in _navseq(1, pagelen):
110 if f > limit:
110 if f > limit:
111 break
111 break
112 targets.append(pos + f)
112 targets.append(pos + f)
113 targets.append(pos - f)
113 targets.append(pos - f)
114 targets.sort()
114 targets.sort()
115
115
116 first = self._first()
116 first = self._first()
117 navbefore = [("(%i)" % first, self.hex(first))]
117 navbefore = [("(%i)" % first, self.hex(first))]
118 navafter = []
118 navafter = []
119 for rev in targets:
119 for rev in targets:
120 if rev not in self._revlog:
120 if rev not in self._revlog:
121 continue
121 continue
122 if pos < rev < limit:
122 if pos < rev < limit:
123 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
123 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
124 if 0 < rev < pos:
124 if 0 < rev < pos:
125 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
125 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
126
126
127
127
128 navafter.append(("tip", "tip"))
128 navafter.append(("tip", "tip"))
129
129
130 data = lambda i: {"label": i[0], "node": i[1]}
130 data = lambda i: {"label": i[0], "node": i[1]}
131 return ({'before': lambda **map: (data(i) for i in navbefore),
131 return ({'before': lambda **map: (data(i) for i in navbefore),
132 'after': lambda **map: (data(i) for i in navafter)},)
132 'after': lambda **map: (data(i) for i in navafter)},)
133
133
134 class filerevnav(revnav):
134 class filerevnav(revnav):
135
135
136 def __init__(self, repo, path):
136 def __init__(self, repo, path):
137 """Navigation generation object
137 """Navigation generation object
138
138
139 :repo: repo object we generate nav for
139 :repo: repo object we generate nav for
140 :path: path of the file we generate nav for
140 :path: path of the file we generate nav for
141 """
141 """
142 # used for iteration
142 # used for iteration
143 self._changelog = repo.unfiltered().changelog
143 self._changelog = repo.unfiltered().changelog
144 # used for hex generation
144 # used for hex generation
145 self._revlog = repo.file(path)
145 self._revlog = repo.file(path)
146
146
147 def hex(self, rev):
147 def hex(self, rev):
148 return hex(self._changelog.node(self._revlog.linkrev(rev)))
148 return hex(self._changelog.node(self._revlog.linkrev(rev)))
149
149
150 class _siblings(object):
150 class _siblings(object):
151 def __init__(self, siblings=None, hiderev=None):
151 def __init__(self, siblings=None, hiderev=None):
152 if siblings is None:
152 if siblings is None:
153 siblings = []
153 siblings = []
154 self.siblings = [s for s in siblings if s.node() != nullid]
154 self.siblings = [s for s in siblings if s.node() != nullid]
155 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
155 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
156 self.siblings = []
156 self.siblings = []
157
157
158 def __iter__(self):
158 def __iter__(self):
159 for s in self.siblings:
159 for s in self.siblings:
160 d = {
160 d = {
161 'node': s.hex(),
161 'node': s.hex(),
162 'rev': s.rev(),
162 'rev': s.rev(),
163 'user': s.user(),
163 'user': s.user(),
164 'date': s.date(),
164 'date': s.date(),
165 'description': s.description(),
165 'description': s.description(),
166 'branch': s.branch(),
166 'branch': s.branch(),
167 }
167 }
168 if util.safehasattr(s, 'path'):
168 if util.safehasattr(s, 'path'):
169 d['file'] = s.path()
169 d['file'] = s.path()
170 yield d
170 yield d
171
171
172 def __len__(self):
172 def __len__(self):
173 return len(self.siblings)
173 return len(self.siblings)
174
174
175 def difffeatureopts(req, ui, section):
175 def difffeatureopts(req, ui, section):
176 diffopts = patch.difffeatureopts(ui, untrusted=True,
176 diffopts = patch.difffeatureopts(ui, untrusted=True,
177 section=section, whitespace=True)
177 section=section, whitespace=True)
178
178
179 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
179 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
180 v = req.form.get(k, [None])[0]
180 v = req.form.get(k, [None])[0]
181 if v is not None:
181 if v is not None:
182 v = util.parsebool(v)
182 v = util.parsebool(v)
183 setattr(diffopts, k, v if v is not None else True)
183 setattr(diffopts, k, v if v is not None else True)
184
184
185 return diffopts
185 return diffopts
186
186
187 def annotate(req, fctx, ui):
187 def annotate(req, fctx, ui):
188 diffopts = difffeatureopts(req, ui, 'annotate')
188 diffopts = difffeatureopts(req, ui, 'annotate')
189 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
189 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
190
190
191 def parents(ctx, hide=None):
191 def parents(ctx, hide=None):
192 if isinstance(ctx, context.basefilectx):
192 if isinstance(ctx, context.basefilectx):
193 introrev = ctx.introrev()
193 introrev = ctx.introrev()
194 if ctx.changectx().rev() != introrev:
194 if ctx.changectx().rev() != introrev:
195 return _siblings([ctx.repo()[introrev]], hide)
195 return _siblings([ctx.repo()[introrev]], hide)
196 return _siblings(ctx.parents(), hide)
196 return _siblings(ctx.parents(), hide)
197
197
198 def children(ctx, hide=None):
198 def children(ctx, hide=None):
199 return _siblings(ctx.children(), hide)
199 return _siblings(ctx.children(), hide)
200
200
201 def renamelink(fctx):
201 def renamelink(fctx):
202 r = fctx.renamed()
202 r = fctx.renamed()
203 if r:
203 if r:
204 return [{'file': r[0], 'node': hex(r[1])}]
204 return [{'file': r[0], 'node': hex(r[1])}]
205 return []
205 return []
206
206
207 def nodetagsdict(repo, node):
207 def nodetagsdict(repo, node):
208 return [{"name": i} for i in repo.nodetags(node)]
208 return [{"name": i} for i in repo.nodetags(node)]
209
209
210 def nodebookmarksdict(repo, node):
210 def nodebookmarksdict(repo, node):
211 return [{"name": i} for i in repo.nodebookmarks(node)]
211 return [{"name": i} for i in repo.nodebookmarks(node)]
212
212
213 def nodebranchdict(repo, ctx):
213 def nodebranchdict(repo, ctx):
214 branches = []
214 branches = []
215 branch = ctx.branch()
215 branch = ctx.branch()
216 # If this is an empty repo, ctx.node() == nullid,
216 # If this is an empty repo, ctx.node() == nullid,
217 # ctx.branch() == 'default'.
217 # ctx.branch() == 'default'.
218 try:
218 try:
219 branchnode = repo.branchtip(branch)
219 branchnode = repo.branchtip(branch)
220 except error.RepoLookupError:
220 except error.RepoLookupError:
221 branchnode = None
221 branchnode = None
222 if branchnode == ctx.node():
222 if branchnode == ctx.node():
223 branches.append({"name": branch})
223 branches.append({"name": branch})
224 return branches
224 return branches
225
225
226 def nodeinbranch(repo, ctx):
226 def nodeinbranch(repo, ctx):
227 branches = []
227 branches = []
228 branch = ctx.branch()
228 branch = ctx.branch()
229 try:
229 try:
230 branchnode = repo.branchtip(branch)
230 branchnode = repo.branchtip(branch)
231 except error.RepoLookupError:
231 except error.RepoLookupError:
232 branchnode = None
232 branchnode = None
233 if branch != 'default' and branchnode != ctx.node():
233 if branch != 'default' and branchnode != ctx.node():
234 branches.append({"name": branch})
234 branches.append({"name": branch})
235 return branches
235 return branches
236
236
237 def nodebranchnodefault(ctx):
237 def nodebranchnodefault(ctx):
238 branches = []
238 branches = []
239 branch = ctx.branch()
239 branch = ctx.branch()
240 if branch != 'default':
240 if branch != 'default':
241 branches.append({"name": branch})
241 branches.append({"name": branch})
242 return branches
242 return branches
243
243
244 def showtag(repo, tmpl, t1, node=nullid, **args):
244 def showtag(repo, tmpl, t1, node=nullid, **args):
245 for t in repo.nodetags(node):
245 for t in repo.nodetags(node):
246 yield tmpl(t1, tag=t, **args)
246 yield tmpl(t1, tag=t, **args)
247
247
248 def showbookmark(repo, tmpl, t1, node=nullid, **args):
248 def showbookmark(repo, tmpl, t1, node=nullid, **args):
249 for t in repo.nodebookmarks(node):
249 for t in repo.nodebookmarks(node):
250 yield tmpl(t1, bookmark=t, **args)
250 yield tmpl(t1, bookmark=t, **args)
251
251
252 def branchentries(repo, stripecount, limit=0):
252 def branchentries(repo, stripecount, limit=0):
253 tips = []
253 tips = []
254 heads = repo.heads()
254 heads = repo.heads()
255 parity = paritygen(stripecount)
255 parity = paritygen(stripecount)
256 sortkey = lambda item: (not item[1], item[0].rev())
256 sortkey = lambda item: (not item[1], item[0].rev())
257
257
258 def entries(**map):
258 def entries(**map):
259 count = 0
259 count = 0
260 if not tips:
260 if not tips:
261 for tag, hs, tip, closed in repo.branchmap().iterbranches():
261 for tag, hs, tip, closed in repo.branchmap().iterbranches():
262 tips.append((repo[tip], closed))
262 tips.append((repo[tip], closed))
263 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
263 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
264 if limit > 0 and count >= limit:
264 if limit > 0 and count >= limit:
265 return
265 return
266 count += 1
266 count += 1
267 if closed:
267 if closed:
268 status = 'closed'
268 status = 'closed'
269 elif ctx.node() not in heads:
269 elif ctx.node() not in heads:
270 status = 'inactive'
270 status = 'inactive'
271 else:
271 else:
272 status = 'open'
272 status = 'open'
273 yield {
273 yield {
274 'parity': next(parity),
274 'parity': next(parity),
275 'branch': ctx.branch(),
275 'branch': ctx.branch(),
276 'status': status,
276 'status': status,
277 'node': ctx.hex(),
277 'node': ctx.hex(),
278 'date': ctx.date()
278 'date': ctx.date()
279 }
279 }
280
280
281 return entries
281 return entries
282
282
283 def cleanpath(repo, path):
283 def cleanpath(repo, path):
284 path = path.lstrip('/')
284 path = path.lstrip('/')
285 return pathutil.canonpath(repo.root, '', path)
285 return pathutil.canonpath(repo.root, '', path)
286
286
287 def changeidctx(repo, changeid):
287 def changeidctx(repo, changeid):
288 try:
288 try:
289 ctx = repo[changeid]
289 ctx = repo[changeid]
290 except error.RepoError:
290 except error.RepoError:
291 man = repo.manifestlog._revlog
291 man = repo.manifestlog._revlog
292 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
292 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
293
293
294 return ctx
294 return ctx
295
295
296 def changectx(repo, req):
296 def changectx(repo, req):
297 changeid = "tip"
297 changeid = "tip"
298 if 'node' in req.form:
298 if 'node' in req.form:
299 changeid = req.form['node'][0]
299 changeid = req.form['node'][0]
300 ipos = changeid.find(':')
300 ipos = changeid.find(':')
301 if ipos != -1:
301 if ipos != -1:
302 changeid = changeid[(ipos + 1):]
302 changeid = changeid[(ipos + 1):]
303 elif 'manifest' in req.form:
303 elif 'manifest' in req.form:
304 changeid = req.form['manifest'][0]
304 changeid = req.form['manifest'][0]
305
305
306 return changeidctx(repo, changeid)
306 return changeidctx(repo, changeid)
307
307
308 def basechangectx(repo, req):
308 def basechangectx(repo, req):
309 if 'node' in req.form:
309 if 'node' in req.form:
310 changeid = req.form['node'][0]
310 changeid = req.form['node'][0]
311 ipos = changeid.find(':')
311 ipos = changeid.find(':')
312 if ipos != -1:
312 if ipos != -1:
313 changeid = changeid[:ipos]
313 changeid = changeid[:ipos]
314 return changeidctx(repo, changeid)
314 return changeidctx(repo, changeid)
315
315
316 return None
316 return None
317
317
318 def filectx(repo, req):
318 def filectx(repo, req):
319 if 'file' not in req.form:
319 if 'file' not in req.form:
320 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
320 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
321 path = cleanpath(repo, req.form['file'][0])
321 path = cleanpath(repo, req.form['file'][0])
322 if 'node' in req.form:
322 if 'node' in req.form:
323 changeid = req.form['node'][0]
323 changeid = req.form['node'][0]
324 elif 'filenode' in req.form:
324 elif 'filenode' in req.form:
325 changeid = req.form['filenode'][0]
325 changeid = req.form['filenode'][0]
326 else:
326 else:
327 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
327 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
328 try:
328 try:
329 fctx = repo[changeid][path]
329 fctx = repo[changeid][path]
330 except error.RepoError:
330 except error.RepoError:
331 fctx = repo.filectx(path, fileid=changeid)
331 fctx = repo.filectx(path, fileid=changeid)
332
332
333 return fctx
333 return fctx
334
334
335 def linerange(req):
335 def linerange(req):
336 linerange = req.form.get('linerange')
336 linerange = req.form.get('linerange')
337 if linerange is None:
337 if linerange is None:
338 return None
338 return None
339 if len(linerange) > 1:
339 if len(linerange) > 1:
340 raise ErrorResponse(HTTP_BAD_REQUEST,
340 raise ErrorResponse(HTTP_BAD_REQUEST,
341 'redundant linerange parameter')
341 'redundant linerange parameter')
342 try:
342 try:
343 fromline, toline = map(int, linerange[0].split(':', 1))
343 fromline, toline = map(int, linerange[0].split(':', 1))
344 except ValueError:
344 except ValueError:
345 raise ErrorResponse(HTTP_BAD_REQUEST,
345 raise ErrorResponse(HTTP_BAD_REQUEST,
346 'invalid linerange parameter')
346 'invalid linerange parameter')
347 try:
347 try:
348 return util.processlinerange(fromline, toline)
348 return util.processlinerange(fromline, toline)
349 except error.ParseError as exc:
349 except error.ParseError as exc:
350 raise ErrorResponse(HTTP_BAD_REQUEST, str(exc))
350 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
351
351
352 def formatlinerange(fromline, toline):
352 def formatlinerange(fromline, toline):
353 return '%d:%d' % (fromline + 1, toline)
353 return '%d:%d' % (fromline + 1, toline)
354
354
355 def succsandmarkers(repo, ctx):
355 def succsandmarkers(repo, ctx):
356 for item in templatekw.showsuccsandmarkers(repo, ctx):
356 for item in templatekw.showsuccsandmarkers(repo, ctx):
357 item['successors'] = _siblings(repo[successor]
357 item['successors'] = _siblings(repo[successor]
358 for successor in item['successors'])
358 for successor in item['successors'])
359 yield item
359 yield item
360
360
361 def commonentry(repo, ctx):
361 def commonentry(repo, ctx):
362 node = ctx.node()
362 node = ctx.node()
363 return {
363 return {
364 'rev': ctx.rev(),
364 'rev': ctx.rev(),
365 'node': hex(node),
365 'node': hex(node),
366 'author': ctx.user(),
366 'author': ctx.user(),
367 'desc': ctx.description(),
367 'desc': ctx.description(),
368 'date': ctx.date(),
368 'date': ctx.date(),
369 'extra': ctx.extra(),
369 'extra': ctx.extra(),
370 'phase': ctx.phasestr(),
370 'phase': ctx.phasestr(),
371 'obsolete': ctx.obsolete(),
371 'obsolete': ctx.obsolete(),
372 'succsandmarkers': lambda **x: succsandmarkers(repo, ctx),
372 'succsandmarkers': lambda **x: succsandmarkers(repo, ctx),
373 'instabilities': [{"instability": i} for i in ctx.instabilities()],
373 'instabilities': [{"instability": i} for i in ctx.instabilities()],
374 'branch': nodebranchnodefault(ctx),
374 'branch': nodebranchnodefault(ctx),
375 'inbranch': nodeinbranch(repo, ctx),
375 'inbranch': nodeinbranch(repo, ctx),
376 'branches': nodebranchdict(repo, ctx),
376 'branches': nodebranchdict(repo, ctx),
377 'tags': nodetagsdict(repo, node),
377 'tags': nodetagsdict(repo, node),
378 'bookmarks': nodebookmarksdict(repo, node),
378 'bookmarks': nodebookmarksdict(repo, node),
379 'parent': lambda **x: parents(ctx),
379 'parent': lambda **x: parents(ctx),
380 'child': lambda **x: children(ctx),
380 'child': lambda **x: children(ctx),
381 }
381 }
382
382
383 def changelistentry(web, ctx, tmpl):
383 def changelistentry(web, ctx, tmpl):
384 '''Obtain a dictionary to be used for entries in a changelist.
384 '''Obtain a dictionary to be used for entries in a changelist.
385
385
386 This function is called when producing items for the "entries" list passed
386 This function is called when producing items for the "entries" list passed
387 to the "shortlog" and "changelog" templates.
387 to the "shortlog" and "changelog" templates.
388 '''
388 '''
389 repo = web.repo
389 repo = web.repo
390 rev = ctx.rev()
390 rev = ctx.rev()
391 n = ctx.node()
391 n = ctx.node()
392 showtags = showtag(repo, tmpl, 'changelogtag', n)
392 showtags = showtag(repo, tmpl, 'changelogtag', n)
393 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
393 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
394
394
395 entry = commonentry(repo, ctx)
395 entry = commonentry(repo, ctx)
396 entry.update(
396 entry.update(
397 allparents=lambda **x: parents(ctx),
397 allparents=lambda **x: parents(ctx),
398 parent=lambda **x: parents(ctx, rev - 1),
398 parent=lambda **x: parents(ctx, rev - 1),
399 child=lambda **x: children(ctx, rev + 1),
399 child=lambda **x: children(ctx, rev + 1),
400 changelogtag=showtags,
400 changelogtag=showtags,
401 files=files,
401 files=files,
402 )
402 )
403 return entry
403 return entry
404
404
405 def symrevorshortnode(req, ctx):
405 def symrevorshortnode(req, ctx):
406 if 'node' in req.form:
406 if 'node' in req.form:
407 return templatefilters.revescape(req.form['node'][0])
407 return templatefilters.revescape(req.form['node'][0])
408 else:
408 else:
409 return short(ctx.node())
409 return short(ctx.node())
410
410
411 def changesetentry(web, req, tmpl, ctx):
411 def changesetentry(web, req, tmpl, ctx):
412 '''Obtain a dictionary to be used to render the "changeset" template.'''
412 '''Obtain a dictionary to be used to render the "changeset" template.'''
413
413
414 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
414 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
415 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
415 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
416 ctx.node())
416 ctx.node())
417 showbranch = nodebranchnodefault(ctx)
417 showbranch = nodebranchnodefault(ctx)
418
418
419 files = []
419 files = []
420 parity = paritygen(web.stripecount)
420 parity = paritygen(web.stripecount)
421 for blockno, f in enumerate(ctx.files()):
421 for blockno, f in enumerate(ctx.files()):
422 template = 'filenodelink' if f in ctx else 'filenolink'
422 template = 'filenodelink' if f in ctx else 'filenolink'
423 files.append(tmpl(template,
423 files.append(tmpl(template,
424 node=ctx.hex(), file=f, blockno=blockno + 1,
424 node=ctx.hex(), file=f, blockno=blockno + 1,
425 parity=next(parity)))
425 parity=next(parity)))
426
426
427 basectx = basechangectx(web.repo, req)
427 basectx = basechangectx(web.repo, req)
428 if basectx is None:
428 if basectx is None:
429 basectx = ctx.p1()
429 basectx = ctx.p1()
430
430
431 style = web.config('web', 'style')
431 style = web.config('web', 'style')
432 if 'style' in req.form:
432 if 'style' in req.form:
433 style = req.form['style'][0]
433 style = req.form['style'][0]
434
434
435 diff = diffs(web, tmpl, ctx, basectx, None, style)
435 diff = diffs(web, tmpl, ctx, basectx, None, style)
436
436
437 parity = paritygen(web.stripecount)
437 parity = paritygen(web.stripecount)
438 diffstatsgen = diffstatgen(ctx, basectx)
438 diffstatsgen = diffstatgen(ctx, basectx)
439 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
439 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
440
440
441 return dict(
441 return dict(
442 diff=diff,
442 diff=diff,
443 symrev=symrevorshortnode(req, ctx),
443 symrev=symrevorshortnode(req, ctx),
444 basenode=basectx.hex(),
444 basenode=basectx.hex(),
445 changesettag=showtags,
445 changesettag=showtags,
446 changesetbookmark=showbookmarks,
446 changesetbookmark=showbookmarks,
447 changesetbranch=showbranch,
447 changesetbranch=showbranch,
448 files=files,
448 files=files,
449 diffsummary=lambda **x: diffsummary(diffstatsgen),
449 diffsummary=lambda **x: diffsummary(diffstatsgen),
450 diffstat=diffstats,
450 diffstat=diffstats,
451 archives=web.archivelist(ctx.hex()),
451 archives=web.archivelist(ctx.hex()),
452 **commonentry(web.repo, ctx))
452 **commonentry(web.repo, ctx))
453
453
454 def listfilediffs(tmpl, files, node, max):
454 def listfilediffs(tmpl, files, node, max):
455 for f in files[:max]:
455 for f in files[:max]:
456 yield tmpl('filedifflink', node=hex(node), file=f)
456 yield tmpl('filedifflink', node=hex(node), file=f)
457 if len(files) > max:
457 if len(files) > max:
458 yield tmpl('fileellipses')
458 yield tmpl('fileellipses')
459
459
460 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
460 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
461 lineidprefix=''):
461 lineidprefix=''):
462
462
463 def prettyprintlines(lines, blockno):
463 def prettyprintlines(lines, blockno):
464 for lineno, l in enumerate(lines, 1):
464 for lineno, l in enumerate(lines, 1):
465 difflineno = "%d.%d" % (blockno, lineno)
465 difflineno = "%d.%d" % (blockno, lineno)
466 if l.startswith('+'):
466 if l.startswith('+'):
467 ltype = "difflineplus"
467 ltype = "difflineplus"
468 elif l.startswith('-'):
468 elif l.startswith('-'):
469 ltype = "difflineminus"
469 ltype = "difflineminus"
470 elif l.startswith('@'):
470 elif l.startswith('@'):
471 ltype = "difflineat"
471 ltype = "difflineat"
472 else:
472 else:
473 ltype = "diffline"
473 ltype = "diffline"
474 yield tmpl(ltype,
474 yield tmpl(ltype,
475 line=l,
475 line=l,
476 lineno=lineno,
476 lineno=lineno,
477 lineid=lineidprefix + "l%s" % difflineno,
477 lineid=lineidprefix + "l%s" % difflineno,
478 linenumber="% 8s" % difflineno)
478 linenumber="% 8s" % difflineno)
479
479
480 repo = web.repo
480 repo = web.repo
481 if files:
481 if files:
482 m = match.exact(repo.root, repo.getcwd(), files)
482 m = match.exact(repo.root, repo.getcwd(), files)
483 else:
483 else:
484 m = match.always(repo.root, repo.getcwd())
484 m = match.always(repo.root, repo.getcwd())
485
485
486 diffopts = patch.diffopts(repo.ui, untrusted=True)
486 diffopts = patch.diffopts(repo.ui, untrusted=True)
487 node1 = basectx.node()
487 node1 = basectx.node()
488 node2 = ctx.node()
488 node2 = ctx.node()
489 parity = paritygen(web.stripecount)
489 parity = paritygen(web.stripecount)
490
490
491 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
491 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
492 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
492 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
493 if style != 'raw':
493 if style != 'raw':
494 header = header[1:]
494 header = header[1:]
495 lines = [h + '\n' for h in header]
495 lines = [h + '\n' for h in header]
496 for hunkrange, hunklines in hunks:
496 for hunkrange, hunklines in hunks:
497 if linerange is not None and hunkrange is not None:
497 if linerange is not None and hunkrange is not None:
498 s1, l1, s2, l2 = hunkrange
498 s1, l1, s2, l2 = hunkrange
499 if not mdiff.hunkinrange((s2, l2), linerange):
499 if not mdiff.hunkinrange((s2, l2), linerange):
500 continue
500 continue
501 lines.extend(hunklines)
501 lines.extend(hunklines)
502 if lines:
502 if lines:
503 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
503 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
504 lines=prettyprintlines(lines, blockno))
504 lines=prettyprintlines(lines, blockno))
505
505
506 def compare(tmpl, context, leftlines, rightlines):
506 def compare(tmpl, context, leftlines, rightlines):
507 '''Generator function that provides side-by-side comparison data.'''
507 '''Generator function that provides side-by-side comparison data.'''
508
508
509 def compline(type, leftlineno, leftline, rightlineno, rightline):
509 def compline(type, leftlineno, leftline, rightlineno, rightline):
510 lineid = leftlineno and ("l%s" % leftlineno) or ''
510 lineid = leftlineno and ("l%s" % leftlineno) or ''
511 lineid += rightlineno and ("r%s" % rightlineno) or ''
511 lineid += rightlineno and ("r%s" % rightlineno) or ''
512 return tmpl('comparisonline',
512 return tmpl('comparisonline',
513 type=type,
513 type=type,
514 lineid=lineid,
514 lineid=lineid,
515 leftlineno=leftlineno,
515 leftlineno=leftlineno,
516 leftlinenumber="% 6s" % (leftlineno or ''),
516 leftlinenumber="% 6s" % (leftlineno or ''),
517 leftline=leftline or '',
517 leftline=leftline or '',
518 rightlineno=rightlineno,
518 rightlineno=rightlineno,
519 rightlinenumber="% 6s" % (rightlineno or ''),
519 rightlinenumber="% 6s" % (rightlineno or ''),
520 rightline=rightline or '')
520 rightline=rightline or '')
521
521
522 def getblock(opcodes):
522 def getblock(opcodes):
523 for type, llo, lhi, rlo, rhi in opcodes:
523 for type, llo, lhi, rlo, rhi in opcodes:
524 len1 = lhi - llo
524 len1 = lhi - llo
525 len2 = rhi - rlo
525 len2 = rhi - rlo
526 count = min(len1, len2)
526 count = min(len1, len2)
527 for i in xrange(count):
527 for i in xrange(count):
528 yield compline(type=type,
528 yield compline(type=type,
529 leftlineno=llo + i + 1,
529 leftlineno=llo + i + 1,
530 leftline=leftlines[llo + i],
530 leftline=leftlines[llo + i],
531 rightlineno=rlo + i + 1,
531 rightlineno=rlo + i + 1,
532 rightline=rightlines[rlo + i])
532 rightline=rightlines[rlo + i])
533 if len1 > len2:
533 if len1 > len2:
534 for i in xrange(llo + count, lhi):
534 for i in xrange(llo + count, lhi):
535 yield compline(type=type,
535 yield compline(type=type,
536 leftlineno=i + 1,
536 leftlineno=i + 1,
537 leftline=leftlines[i],
537 leftline=leftlines[i],
538 rightlineno=None,
538 rightlineno=None,
539 rightline=None)
539 rightline=None)
540 elif len2 > len1:
540 elif len2 > len1:
541 for i in xrange(rlo + count, rhi):
541 for i in xrange(rlo + count, rhi):
542 yield compline(type=type,
542 yield compline(type=type,
543 leftlineno=None,
543 leftlineno=None,
544 leftline=None,
544 leftline=None,
545 rightlineno=i + 1,
545 rightlineno=i + 1,
546 rightline=rightlines[i])
546 rightline=rightlines[i])
547
547
548 s = difflib.SequenceMatcher(None, leftlines, rightlines)
548 s = difflib.SequenceMatcher(None, leftlines, rightlines)
549 if context < 0:
549 if context < 0:
550 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
550 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
551 else:
551 else:
552 for oc in s.get_grouped_opcodes(n=context):
552 for oc in s.get_grouped_opcodes(n=context):
553 yield tmpl('comparisonblock', lines=getblock(oc))
553 yield tmpl('comparisonblock', lines=getblock(oc))
554
554
555 def diffstatgen(ctx, basectx):
555 def diffstatgen(ctx, basectx):
556 '''Generator function that provides the diffstat data.'''
556 '''Generator function that provides the diffstat data.'''
557
557
558 stats = patch.diffstatdata(
558 stats = patch.diffstatdata(
559 util.iterlines(ctx.diff(basectx, noprefix=False)))
559 util.iterlines(ctx.diff(basectx, noprefix=False)))
560 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
560 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
561 while True:
561 while True:
562 yield stats, maxname, maxtotal, addtotal, removetotal, binary
562 yield stats, maxname, maxtotal, addtotal, removetotal, binary
563
563
564 def diffsummary(statgen):
564 def diffsummary(statgen):
565 '''Return a short summary of the diff.'''
565 '''Return a short summary of the diff.'''
566
566
567 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
567 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
568 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
568 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
569 len(stats), addtotal, removetotal)
569 len(stats), addtotal, removetotal)
570
570
571 def diffstat(tmpl, ctx, statgen, parity):
571 def diffstat(tmpl, ctx, statgen, parity):
572 '''Return a diffstat template for each file in the diff.'''
572 '''Return a diffstat template for each file in the diff.'''
573
573
574 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
574 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
575 files = ctx.files()
575 files = ctx.files()
576
576
577 def pct(i):
577 def pct(i):
578 if maxtotal == 0:
578 if maxtotal == 0:
579 return 0
579 return 0
580 return (float(i) / maxtotal) * 100
580 return (float(i) / maxtotal) * 100
581
581
582 fileno = 0
582 fileno = 0
583 for filename, adds, removes, isbinary in stats:
583 for filename, adds, removes, isbinary in stats:
584 template = 'diffstatlink' if filename in files else 'diffstatnolink'
584 template = 'diffstatlink' if filename in files else 'diffstatnolink'
585 total = adds + removes
585 total = adds + removes
586 fileno += 1
586 fileno += 1
587 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
587 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
588 total=total, addpct=pct(adds), removepct=pct(removes),
588 total=total, addpct=pct(adds), removepct=pct(removes),
589 parity=next(parity))
589 parity=next(parity))
590
590
591 class sessionvars(object):
591 class sessionvars(object):
592 def __init__(self, vars, start='?'):
592 def __init__(self, vars, start='?'):
593 self.start = start
593 self.start = start
594 self.vars = vars
594 self.vars = vars
595 def __getitem__(self, key):
595 def __getitem__(self, key):
596 return self.vars[key]
596 return self.vars[key]
597 def __setitem__(self, key, value):
597 def __setitem__(self, key, value):
598 self.vars[key] = value
598 self.vars[key] = value
599 def __copy__(self):
599 def __copy__(self):
600 return sessionvars(copy.copy(self.vars), self.start)
600 return sessionvars(copy.copy(self.vars), self.start)
601 def __iter__(self):
601 def __iter__(self):
602 separator = self.start
602 separator = self.start
603 for key, value in sorted(self.vars.iteritems()):
603 for key, value in sorted(self.vars.iteritems()):
604 yield {'name': key,
604 yield {'name': key,
605 'value': pycompat.bytestr(value),
605 'value': pycompat.bytestr(value),
606 'separator': separator,
606 'separator': separator,
607 }
607 }
608 separator = '&'
608 separator = '&'
609
609
610 class wsgiui(uimod.ui):
610 class wsgiui(uimod.ui):
611 # default termwidth breaks under mod_wsgi
611 # default termwidth breaks under mod_wsgi
612 def termwidth(self):
612 def termwidth(self):
613 return 80
613 return 80
614
614
615 def getwebsubs(repo):
615 def getwebsubs(repo):
616 websubtable = []
616 websubtable = []
617 websubdefs = repo.ui.configitems('websub')
617 websubdefs = repo.ui.configitems('websub')
618 # we must maintain interhg backwards compatibility
618 # we must maintain interhg backwards compatibility
619 websubdefs += repo.ui.configitems('interhg')
619 websubdefs += repo.ui.configitems('interhg')
620 for key, pattern in websubdefs:
620 for key, pattern in websubdefs:
621 # grab the delimiter from the character after the "s"
621 # grab the delimiter from the character after the "s"
622 unesc = pattern[1:2]
622 unesc = pattern[1:2]
623 delim = re.escape(unesc)
623 delim = re.escape(unesc)
624
624
625 # identify portions of the pattern, taking care to avoid escaped
625 # identify portions of the pattern, taking care to avoid escaped
626 # delimiters. the replace format and flags are optional, but
626 # delimiters. the replace format and flags are optional, but
627 # delimiters are required.
627 # delimiters are required.
628 match = re.match(
628 match = re.match(
629 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
629 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
630 % (delim, delim, delim), pattern)
630 % (delim, delim, delim), pattern)
631 if not match:
631 if not match:
632 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
632 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
633 % (key, pattern))
633 % (key, pattern))
634 continue
634 continue
635
635
636 # we need to unescape the delimiter for regexp and format
636 # we need to unescape the delimiter for regexp and format
637 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
637 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
638 regexp = delim_re.sub(unesc, match.group(1))
638 regexp = delim_re.sub(unesc, match.group(1))
639 format = delim_re.sub(unesc, match.group(2))
639 format = delim_re.sub(unesc, match.group(2))
640
640
641 # the pattern allows for 6 regexp flags, so set them if necessary
641 # the pattern allows for 6 regexp flags, so set them if necessary
642 flagin = match.group(3)
642 flagin = match.group(3)
643 flags = 0
643 flags = 0
644 if flagin:
644 if flagin:
645 for flag in flagin.upper():
645 for flag in flagin.upper():
646 flags |= re.__dict__[flag]
646 flags |= re.__dict__[flag]
647
647
648 try:
648 try:
649 regexp = re.compile(regexp, flags)
649 regexp = re.compile(regexp, flags)
650 websubtable.append((regexp, format))
650 websubtable.append((regexp, format))
651 except re.error:
651 except re.error:
652 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
652 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
653 % (key, regexp))
653 % (key, regexp))
654 return websubtable
654 return websubtable
@@ -1,1066 +1,1066 b''
1 # wireproto.py - generic wire protocol support functions
1 # wireproto.py - generic wire protocol support functions
2 #
2 #
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import hashlib
10 import hashlib
11 import os
11 import os
12 import tempfile
12 import tempfile
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import (
15 from .node import (
16 bin,
16 bin,
17 hex,
17 hex,
18 nullid,
18 nullid,
19 )
19 )
20
20
21 from . import (
21 from . import (
22 bundle2,
22 bundle2,
23 changegroup as changegroupmod,
23 changegroup as changegroupmod,
24 discovery,
24 discovery,
25 encoding,
25 encoding,
26 error,
26 error,
27 exchange,
27 exchange,
28 peer,
28 peer,
29 pushkey as pushkeymod,
29 pushkey as pushkeymod,
30 pycompat,
30 pycompat,
31 repository,
31 repository,
32 streamclone,
32 streamclone,
33 util,
33 util,
34 wireprototypes,
34 wireprototypes,
35 )
35 )
36
36
37 urlerr = util.urlerr
37 urlerr = util.urlerr
38 urlreq = util.urlreq
38 urlreq = util.urlreq
39
39
40 bytesresponse = wireprototypes.bytesresponse
40 bytesresponse = wireprototypes.bytesresponse
41 ooberror = wireprototypes.ooberror
41 ooberror = wireprototypes.ooberror
42 pushres = wireprototypes.pushres
42 pushres = wireprototypes.pushres
43 pusherr = wireprototypes.pusherr
43 pusherr = wireprototypes.pusherr
44 streamres = wireprototypes.streamres
44 streamres = wireprototypes.streamres
45 streamres_legacy = wireprototypes.streamreslegacy
45 streamres_legacy = wireprototypes.streamreslegacy
46
46
47 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
47 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
48 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
48 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
49 'IncompatibleClient')
49 'IncompatibleClient')
50 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
50 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
51
51
52 class remoteiterbatcher(peer.iterbatcher):
52 class remoteiterbatcher(peer.iterbatcher):
53 def __init__(self, remote):
53 def __init__(self, remote):
54 super(remoteiterbatcher, self).__init__()
54 super(remoteiterbatcher, self).__init__()
55 self._remote = remote
55 self._remote = remote
56
56
57 def __getattr__(self, name):
57 def __getattr__(self, name):
58 # Validate this method is batchable, since submit() only supports
58 # Validate this method is batchable, since submit() only supports
59 # batchable methods.
59 # batchable methods.
60 fn = getattr(self._remote, name)
60 fn = getattr(self._remote, name)
61 if not getattr(fn, 'batchable', None):
61 if not getattr(fn, 'batchable', None):
62 raise error.ProgrammingError('Attempted to batch a non-batchable '
62 raise error.ProgrammingError('Attempted to batch a non-batchable '
63 'call to %r' % name)
63 'call to %r' % name)
64
64
65 return super(remoteiterbatcher, self).__getattr__(name)
65 return super(remoteiterbatcher, self).__getattr__(name)
66
66
67 def submit(self):
67 def submit(self):
68 """Break the batch request into many patch calls and pipeline them.
68 """Break the batch request into many patch calls and pipeline them.
69
69
70 This is mostly valuable over http where request sizes can be
70 This is mostly valuable over http where request sizes can be
71 limited, but can be used in other places as well.
71 limited, but can be used in other places as well.
72 """
72 """
73 # 2-tuple of (command, arguments) that represents what will be
73 # 2-tuple of (command, arguments) that represents what will be
74 # sent over the wire.
74 # sent over the wire.
75 requests = []
75 requests = []
76
76
77 # 4-tuple of (command, final future, @batchable generator, remote
77 # 4-tuple of (command, final future, @batchable generator, remote
78 # future).
78 # future).
79 results = []
79 results = []
80
80
81 for command, args, opts, finalfuture in self.calls:
81 for command, args, opts, finalfuture in self.calls:
82 mtd = getattr(self._remote, command)
82 mtd = getattr(self._remote, command)
83 batchable = mtd.batchable(mtd.__self__, *args, **opts)
83 batchable = mtd.batchable(mtd.__self__, *args, **opts)
84
84
85 commandargs, fremote = next(batchable)
85 commandargs, fremote = next(batchable)
86 assert fremote
86 assert fremote
87 requests.append((command, commandargs))
87 requests.append((command, commandargs))
88 results.append((command, finalfuture, batchable, fremote))
88 results.append((command, finalfuture, batchable, fremote))
89
89
90 if requests:
90 if requests:
91 self._resultiter = self._remote._submitbatch(requests)
91 self._resultiter = self._remote._submitbatch(requests)
92
92
93 self._results = results
93 self._results = results
94
94
95 def results(self):
95 def results(self):
96 for command, finalfuture, batchable, remotefuture in self._results:
96 for command, finalfuture, batchable, remotefuture in self._results:
97 # Get the raw result, set it in the remote future, feed it
97 # Get the raw result, set it in the remote future, feed it
98 # back into the @batchable generator so it can be decoded, and
98 # back into the @batchable generator so it can be decoded, and
99 # set the result on the final future to this value.
99 # set the result on the final future to this value.
100 remoteresult = next(self._resultiter)
100 remoteresult = next(self._resultiter)
101 remotefuture.set(remoteresult)
101 remotefuture.set(remoteresult)
102 finalfuture.set(next(batchable))
102 finalfuture.set(next(batchable))
103
103
104 # Verify our @batchable generators only emit 2 values.
104 # Verify our @batchable generators only emit 2 values.
105 try:
105 try:
106 next(batchable)
106 next(batchable)
107 except StopIteration:
107 except StopIteration:
108 pass
108 pass
109 else:
109 else:
110 raise error.ProgrammingError('%s @batchable generator emitted '
110 raise error.ProgrammingError('%s @batchable generator emitted '
111 'unexpected value count' % command)
111 'unexpected value count' % command)
112
112
113 yield finalfuture.value
113 yield finalfuture.value
114
114
115 # Forward a couple of names from peer to make wireproto interactions
115 # Forward a couple of names from peer to make wireproto interactions
116 # slightly more sensible.
116 # slightly more sensible.
117 batchable = peer.batchable
117 batchable = peer.batchable
118 future = peer.future
118 future = peer.future
119
119
120 # list of nodes encoding / decoding
120 # list of nodes encoding / decoding
121
121
122 def decodelist(l, sep=' '):
122 def decodelist(l, sep=' '):
123 if l:
123 if l:
124 return [bin(v) for v in l.split(sep)]
124 return [bin(v) for v in l.split(sep)]
125 return []
125 return []
126
126
127 def encodelist(l, sep=' '):
127 def encodelist(l, sep=' '):
128 try:
128 try:
129 return sep.join(map(hex, l))
129 return sep.join(map(hex, l))
130 except TypeError:
130 except TypeError:
131 raise
131 raise
132
132
133 # batched call argument encoding
133 # batched call argument encoding
134
134
135 def escapearg(plain):
135 def escapearg(plain):
136 return (plain
136 return (plain
137 .replace(':', ':c')
137 .replace(':', ':c')
138 .replace(',', ':o')
138 .replace(',', ':o')
139 .replace(';', ':s')
139 .replace(';', ':s')
140 .replace('=', ':e'))
140 .replace('=', ':e'))
141
141
142 def unescapearg(escaped):
142 def unescapearg(escaped):
143 return (escaped
143 return (escaped
144 .replace(':e', '=')
144 .replace(':e', '=')
145 .replace(':s', ';')
145 .replace(':s', ';')
146 .replace(':o', ',')
146 .replace(':o', ',')
147 .replace(':c', ':'))
147 .replace(':c', ':'))
148
148
149 def encodebatchcmds(req):
149 def encodebatchcmds(req):
150 """Return a ``cmds`` argument value for the ``batch`` command."""
150 """Return a ``cmds`` argument value for the ``batch`` command."""
151 cmds = []
151 cmds = []
152 for op, argsdict in req:
152 for op, argsdict in req:
153 # Old servers didn't properly unescape argument names. So prevent
153 # Old servers didn't properly unescape argument names. So prevent
154 # the sending of argument names that may not be decoded properly by
154 # the sending of argument names that may not be decoded properly by
155 # servers.
155 # servers.
156 assert all(escapearg(k) == k for k in argsdict)
156 assert all(escapearg(k) == k for k in argsdict)
157
157
158 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
158 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
159 for k, v in argsdict.iteritems())
159 for k, v in argsdict.iteritems())
160 cmds.append('%s %s' % (op, args))
160 cmds.append('%s %s' % (op, args))
161
161
162 return ';'.join(cmds)
162 return ';'.join(cmds)
163
163
164 # mapping of options accepted by getbundle and their types
164 # mapping of options accepted by getbundle and their types
165 #
165 #
166 # Meant to be extended by extensions. It is extensions responsibility to ensure
166 # Meant to be extended by extensions. It is extensions responsibility to ensure
167 # such options are properly processed in exchange.getbundle.
167 # such options are properly processed in exchange.getbundle.
168 #
168 #
169 # supported types are:
169 # supported types are:
170 #
170 #
171 # :nodes: list of binary nodes
171 # :nodes: list of binary nodes
172 # :csv: list of comma-separated values
172 # :csv: list of comma-separated values
173 # :scsv: list of comma-separated values return as set
173 # :scsv: list of comma-separated values return as set
174 # :plain: string with no transformation needed.
174 # :plain: string with no transformation needed.
175 gboptsmap = {'heads': 'nodes',
175 gboptsmap = {'heads': 'nodes',
176 'bookmarks': 'boolean',
176 'bookmarks': 'boolean',
177 'common': 'nodes',
177 'common': 'nodes',
178 'obsmarkers': 'boolean',
178 'obsmarkers': 'boolean',
179 'phases': 'boolean',
179 'phases': 'boolean',
180 'bundlecaps': 'scsv',
180 'bundlecaps': 'scsv',
181 'listkeys': 'csv',
181 'listkeys': 'csv',
182 'cg': 'boolean',
182 'cg': 'boolean',
183 'cbattempted': 'boolean',
183 'cbattempted': 'boolean',
184 'stream': 'boolean',
184 'stream': 'boolean',
185 }
185 }
186
186
187 # client side
187 # client side
188
188
189 class wirepeer(repository.legacypeer):
189 class wirepeer(repository.legacypeer):
190 """Client-side interface for communicating with a peer repository.
190 """Client-side interface for communicating with a peer repository.
191
191
192 Methods commonly call wire protocol commands of the same name.
192 Methods commonly call wire protocol commands of the same name.
193
193
194 See also httppeer.py and sshpeer.py for protocol-specific
194 See also httppeer.py and sshpeer.py for protocol-specific
195 implementations of this interface.
195 implementations of this interface.
196 """
196 """
197 # Begin of basewirepeer interface.
197 # Begin of basewirepeer interface.
198
198
199 def iterbatch(self):
199 def iterbatch(self):
200 return remoteiterbatcher(self)
200 return remoteiterbatcher(self)
201
201
202 @batchable
202 @batchable
203 def lookup(self, key):
203 def lookup(self, key):
204 self.requirecap('lookup', _('look up remote revision'))
204 self.requirecap('lookup', _('look up remote revision'))
205 f = future()
205 f = future()
206 yield {'key': encoding.fromlocal(key)}, f
206 yield {'key': encoding.fromlocal(key)}, f
207 d = f.value
207 d = f.value
208 success, data = d[:-1].split(" ", 1)
208 success, data = d[:-1].split(" ", 1)
209 if int(success):
209 if int(success):
210 yield bin(data)
210 yield bin(data)
211 else:
211 else:
212 self._abort(error.RepoError(data))
212 self._abort(error.RepoError(data))
213
213
214 @batchable
214 @batchable
215 def heads(self):
215 def heads(self):
216 f = future()
216 f = future()
217 yield {}, f
217 yield {}, f
218 d = f.value
218 d = f.value
219 try:
219 try:
220 yield decodelist(d[:-1])
220 yield decodelist(d[:-1])
221 except ValueError:
221 except ValueError:
222 self._abort(error.ResponseError(_("unexpected response:"), d))
222 self._abort(error.ResponseError(_("unexpected response:"), d))
223
223
224 @batchable
224 @batchable
225 def known(self, nodes):
225 def known(self, nodes):
226 f = future()
226 f = future()
227 yield {'nodes': encodelist(nodes)}, f
227 yield {'nodes': encodelist(nodes)}, f
228 d = f.value
228 d = f.value
229 try:
229 try:
230 yield [bool(int(b)) for b in d]
230 yield [bool(int(b)) for b in d]
231 except ValueError:
231 except ValueError:
232 self._abort(error.ResponseError(_("unexpected response:"), d))
232 self._abort(error.ResponseError(_("unexpected response:"), d))
233
233
234 @batchable
234 @batchable
235 def branchmap(self):
235 def branchmap(self):
236 f = future()
236 f = future()
237 yield {}, f
237 yield {}, f
238 d = f.value
238 d = f.value
239 try:
239 try:
240 branchmap = {}
240 branchmap = {}
241 for branchpart in d.splitlines():
241 for branchpart in d.splitlines():
242 branchname, branchheads = branchpart.split(' ', 1)
242 branchname, branchheads = branchpart.split(' ', 1)
243 branchname = encoding.tolocal(urlreq.unquote(branchname))
243 branchname = encoding.tolocal(urlreq.unquote(branchname))
244 branchheads = decodelist(branchheads)
244 branchheads = decodelist(branchheads)
245 branchmap[branchname] = branchheads
245 branchmap[branchname] = branchheads
246 yield branchmap
246 yield branchmap
247 except TypeError:
247 except TypeError:
248 self._abort(error.ResponseError(_("unexpected response:"), d))
248 self._abort(error.ResponseError(_("unexpected response:"), d))
249
249
250 @batchable
250 @batchable
251 def listkeys(self, namespace):
251 def listkeys(self, namespace):
252 if not self.capable('pushkey'):
252 if not self.capable('pushkey'):
253 yield {}, None
253 yield {}, None
254 f = future()
254 f = future()
255 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
255 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
256 yield {'namespace': encoding.fromlocal(namespace)}, f
256 yield {'namespace': encoding.fromlocal(namespace)}, f
257 d = f.value
257 d = f.value
258 self.ui.debug('received listkey for "%s": %i bytes\n'
258 self.ui.debug('received listkey for "%s": %i bytes\n'
259 % (namespace, len(d)))
259 % (namespace, len(d)))
260 yield pushkeymod.decodekeys(d)
260 yield pushkeymod.decodekeys(d)
261
261
262 @batchable
262 @batchable
263 def pushkey(self, namespace, key, old, new):
263 def pushkey(self, namespace, key, old, new):
264 if not self.capable('pushkey'):
264 if not self.capable('pushkey'):
265 yield False, None
265 yield False, None
266 f = future()
266 f = future()
267 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
267 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
268 yield {'namespace': encoding.fromlocal(namespace),
268 yield {'namespace': encoding.fromlocal(namespace),
269 'key': encoding.fromlocal(key),
269 'key': encoding.fromlocal(key),
270 'old': encoding.fromlocal(old),
270 'old': encoding.fromlocal(old),
271 'new': encoding.fromlocal(new)}, f
271 'new': encoding.fromlocal(new)}, f
272 d = f.value
272 d = f.value
273 d, output = d.split('\n', 1)
273 d, output = d.split('\n', 1)
274 try:
274 try:
275 d = bool(int(d))
275 d = bool(int(d))
276 except ValueError:
276 except ValueError:
277 raise error.ResponseError(
277 raise error.ResponseError(
278 _('push failed (unexpected response):'), d)
278 _('push failed (unexpected response):'), d)
279 for l in output.splitlines(True):
279 for l in output.splitlines(True):
280 self.ui.status(_('remote: '), l)
280 self.ui.status(_('remote: '), l)
281 yield d
281 yield d
282
282
283 def stream_out(self):
283 def stream_out(self):
284 return self._callstream('stream_out')
284 return self._callstream('stream_out')
285
285
286 def getbundle(self, source, **kwargs):
286 def getbundle(self, source, **kwargs):
287 kwargs = pycompat.byteskwargs(kwargs)
287 kwargs = pycompat.byteskwargs(kwargs)
288 self.requirecap('getbundle', _('look up remote changes'))
288 self.requirecap('getbundle', _('look up remote changes'))
289 opts = {}
289 opts = {}
290 bundlecaps = kwargs.get('bundlecaps')
290 bundlecaps = kwargs.get('bundlecaps')
291 if bundlecaps is not None:
291 if bundlecaps is not None:
292 kwargs['bundlecaps'] = sorted(bundlecaps)
292 kwargs['bundlecaps'] = sorted(bundlecaps)
293 else:
293 else:
294 bundlecaps = () # kwargs could have it to None
294 bundlecaps = () # kwargs could have it to None
295 for key, value in kwargs.iteritems():
295 for key, value in kwargs.iteritems():
296 if value is None:
296 if value is None:
297 continue
297 continue
298 keytype = gboptsmap.get(key)
298 keytype = gboptsmap.get(key)
299 if keytype is None:
299 if keytype is None:
300 raise error.ProgrammingError(
300 raise error.ProgrammingError(
301 'Unexpectedly None keytype for key %s' % key)
301 'Unexpectedly None keytype for key %s' % key)
302 elif keytype == 'nodes':
302 elif keytype == 'nodes':
303 value = encodelist(value)
303 value = encodelist(value)
304 elif keytype in ('csv', 'scsv'):
304 elif keytype in ('csv', 'scsv'):
305 value = ','.join(value)
305 value = ','.join(value)
306 elif keytype == 'boolean':
306 elif keytype == 'boolean':
307 value = '%i' % bool(value)
307 value = '%i' % bool(value)
308 elif keytype != 'plain':
308 elif keytype != 'plain':
309 raise KeyError('unknown getbundle option type %s'
309 raise KeyError('unknown getbundle option type %s'
310 % keytype)
310 % keytype)
311 opts[key] = value
311 opts[key] = value
312 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
312 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
313 if any((cap.startswith('HG2') for cap in bundlecaps)):
313 if any((cap.startswith('HG2') for cap in bundlecaps)):
314 return bundle2.getunbundler(self.ui, f)
314 return bundle2.getunbundler(self.ui, f)
315 else:
315 else:
316 return changegroupmod.cg1unpacker(f, 'UN')
316 return changegroupmod.cg1unpacker(f, 'UN')
317
317
318 def unbundle(self, cg, heads, url):
318 def unbundle(self, cg, heads, url):
319 '''Send cg (a readable file-like object representing the
319 '''Send cg (a readable file-like object representing the
320 changegroup to push, typically a chunkbuffer object) to the
320 changegroup to push, typically a chunkbuffer object) to the
321 remote server as a bundle.
321 remote server as a bundle.
322
322
323 When pushing a bundle10 stream, return an integer indicating the
323 When pushing a bundle10 stream, return an integer indicating the
324 result of the push (see changegroup.apply()).
324 result of the push (see changegroup.apply()).
325
325
326 When pushing a bundle20 stream, return a bundle20 stream.
326 When pushing a bundle20 stream, return a bundle20 stream.
327
327
328 `url` is the url the client thinks it's pushing to, which is
328 `url` is the url the client thinks it's pushing to, which is
329 visible to hooks.
329 visible to hooks.
330 '''
330 '''
331
331
332 if heads != ['force'] and self.capable('unbundlehash'):
332 if heads != ['force'] and self.capable('unbundlehash'):
333 heads = encodelist(['hashed',
333 heads = encodelist(['hashed',
334 hashlib.sha1(''.join(sorted(heads))).digest()])
334 hashlib.sha1(''.join(sorted(heads))).digest()])
335 else:
335 else:
336 heads = encodelist(heads)
336 heads = encodelist(heads)
337
337
338 if util.safehasattr(cg, 'deltaheader'):
338 if util.safehasattr(cg, 'deltaheader'):
339 # this a bundle10, do the old style call sequence
339 # this a bundle10, do the old style call sequence
340 ret, output = self._callpush("unbundle", cg, heads=heads)
340 ret, output = self._callpush("unbundle", cg, heads=heads)
341 if ret == "":
341 if ret == "":
342 raise error.ResponseError(
342 raise error.ResponseError(
343 _('push failed:'), output)
343 _('push failed:'), output)
344 try:
344 try:
345 ret = int(ret)
345 ret = int(ret)
346 except ValueError:
346 except ValueError:
347 raise error.ResponseError(
347 raise error.ResponseError(
348 _('push failed (unexpected response):'), ret)
348 _('push failed (unexpected response):'), ret)
349
349
350 for l in output.splitlines(True):
350 for l in output.splitlines(True):
351 self.ui.status(_('remote: '), l)
351 self.ui.status(_('remote: '), l)
352 else:
352 else:
353 # bundle2 push. Send a stream, fetch a stream.
353 # bundle2 push. Send a stream, fetch a stream.
354 stream = self._calltwowaystream('unbundle', cg, heads=heads)
354 stream = self._calltwowaystream('unbundle', cg, heads=heads)
355 ret = bundle2.getunbundler(self.ui, stream)
355 ret = bundle2.getunbundler(self.ui, stream)
356 return ret
356 return ret
357
357
358 # End of basewirepeer interface.
358 # End of basewirepeer interface.
359
359
360 # Begin of baselegacywirepeer interface.
360 # Begin of baselegacywirepeer interface.
361
361
362 def branches(self, nodes):
362 def branches(self, nodes):
363 n = encodelist(nodes)
363 n = encodelist(nodes)
364 d = self._call("branches", nodes=n)
364 d = self._call("branches", nodes=n)
365 try:
365 try:
366 br = [tuple(decodelist(b)) for b in d.splitlines()]
366 br = [tuple(decodelist(b)) for b in d.splitlines()]
367 return br
367 return br
368 except ValueError:
368 except ValueError:
369 self._abort(error.ResponseError(_("unexpected response:"), d))
369 self._abort(error.ResponseError(_("unexpected response:"), d))
370
370
371 def between(self, pairs):
371 def between(self, pairs):
372 batch = 8 # avoid giant requests
372 batch = 8 # avoid giant requests
373 r = []
373 r = []
374 for i in xrange(0, len(pairs), batch):
374 for i in xrange(0, len(pairs), batch):
375 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
375 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
376 d = self._call("between", pairs=n)
376 d = self._call("between", pairs=n)
377 try:
377 try:
378 r.extend(l and decodelist(l) or [] for l in d.splitlines())
378 r.extend(l and decodelist(l) or [] for l in d.splitlines())
379 except ValueError:
379 except ValueError:
380 self._abort(error.ResponseError(_("unexpected response:"), d))
380 self._abort(error.ResponseError(_("unexpected response:"), d))
381 return r
381 return r
382
382
383 def changegroup(self, nodes, kind):
383 def changegroup(self, nodes, kind):
384 n = encodelist(nodes)
384 n = encodelist(nodes)
385 f = self._callcompressable("changegroup", roots=n)
385 f = self._callcompressable("changegroup", roots=n)
386 return changegroupmod.cg1unpacker(f, 'UN')
386 return changegroupmod.cg1unpacker(f, 'UN')
387
387
388 def changegroupsubset(self, bases, heads, kind):
388 def changegroupsubset(self, bases, heads, kind):
389 self.requirecap('changegroupsubset', _('look up remote changes'))
389 self.requirecap('changegroupsubset', _('look up remote changes'))
390 bases = encodelist(bases)
390 bases = encodelist(bases)
391 heads = encodelist(heads)
391 heads = encodelist(heads)
392 f = self._callcompressable("changegroupsubset",
392 f = self._callcompressable("changegroupsubset",
393 bases=bases, heads=heads)
393 bases=bases, heads=heads)
394 return changegroupmod.cg1unpacker(f, 'UN')
394 return changegroupmod.cg1unpacker(f, 'UN')
395
395
396 # End of baselegacywirepeer interface.
396 # End of baselegacywirepeer interface.
397
397
398 def _submitbatch(self, req):
398 def _submitbatch(self, req):
399 """run batch request <req> on the server
399 """run batch request <req> on the server
400
400
401 Returns an iterator of the raw responses from the server.
401 Returns an iterator of the raw responses from the server.
402 """
402 """
403 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
403 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
404 chunk = rsp.read(1024)
404 chunk = rsp.read(1024)
405 work = [chunk]
405 work = [chunk]
406 while chunk:
406 while chunk:
407 while ';' not in chunk and chunk:
407 while ';' not in chunk and chunk:
408 chunk = rsp.read(1024)
408 chunk = rsp.read(1024)
409 work.append(chunk)
409 work.append(chunk)
410 merged = ''.join(work)
410 merged = ''.join(work)
411 while ';' in merged:
411 while ';' in merged:
412 one, merged = merged.split(';', 1)
412 one, merged = merged.split(';', 1)
413 yield unescapearg(one)
413 yield unescapearg(one)
414 chunk = rsp.read(1024)
414 chunk = rsp.read(1024)
415 work = [merged, chunk]
415 work = [merged, chunk]
416 yield unescapearg(''.join(work))
416 yield unescapearg(''.join(work))
417
417
418 def _submitone(self, op, args):
418 def _submitone(self, op, args):
419 return self._call(op, **pycompat.strkwargs(args))
419 return self._call(op, **pycompat.strkwargs(args))
420
420
421 def debugwireargs(self, one, two, three=None, four=None, five=None):
421 def debugwireargs(self, one, two, three=None, four=None, five=None):
422 # don't pass optional arguments left at their default value
422 # don't pass optional arguments left at their default value
423 opts = {}
423 opts = {}
424 if three is not None:
424 if three is not None:
425 opts[r'three'] = three
425 opts[r'three'] = three
426 if four is not None:
426 if four is not None:
427 opts[r'four'] = four
427 opts[r'four'] = four
428 return self._call('debugwireargs', one=one, two=two, **opts)
428 return self._call('debugwireargs', one=one, two=two, **opts)
429
429
430 def _call(self, cmd, **args):
430 def _call(self, cmd, **args):
431 """execute <cmd> on the server
431 """execute <cmd> on the server
432
432
433 The command is expected to return a simple string.
433 The command is expected to return a simple string.
434
434
435 returns the server reply as a string."""
435 returns the server reply as a string."""
436 raise NotImplementedError()
436 raise NotImplementedError()
437
437
438 def _callstream(self, cmd, **args):
438 def _callstream(self, cmd, **args):
439 """execute <cmd> on the server
439 """execute <cmd> on the server
440
440
441 The command is expected to return a stream. Note that if the
441 The command is expected to return a stream. Note that if the
442 command doesn't return a stream, _callstream behaves
442 command doesn't return a stream, _callstream behaves
443 differently for ssh and http peers.
443 differently for ssh and http peers.
444
444
445 returns the server reply as a file like object.
445 returns the server reply as a file like object.
446 """
446 """
447 raise NotImplementedError()
447 raise NotImplementedError()
448
448
449 def _callcompressable(self, cmd, **args):
449 def _callcompressable(self, cmd, **args):
450 """execute <cmd> on the server
450 """execute <cmd> on the server
451
451
452 The command is expected to return a stream.
452 The command is expected to return a stream.
453
453
454 The stream may have been compressed in some implementations. This
454 The stream may have been compressed in some implementations. This
455 function takes care of the decompression. This is the only difference
455 function takes care of the decompression. This is the only difference
456 with _callstream.
456 with _callstream.
457
457
458 returns the server reply as a file like object.
458 returns the server reply as a file like object.
459 """
459 """
460 raise NotImplementedError()
460 raise NotImplementedError()
461
461
462 def _callpush(self, cmd, fp, **args):
462 def _callpush(self, cmd, fp, **args):
463 """execute a <cmd> on server
463 """execute a <cmd> on server
464
464
465 The command is expected to be related to a push. Push has a special
465 The command is expected to be related to a push. Push has a special
466 return method.
466 return method.
467
467
468 returns the server reply as a (ret, output) tuple. ret is either
468 returns the server reply as a (ret, output) tuple. ret is either
469 empty (error) or a stringified int.
469 empty (error) or a stringified int.
470 """
470 """
471 raise NotImplementedError()
471 raise NotImplementedError()
472
472
473 def _calltwowaystream(self, cmd, fp, **args):
473 def _calltwowaystream(self, cmd, fp, **args):
474 """execute <cmd> on server
474 """execute <cmd> on server
475
475
476 The command will send a stream to the server and get a stream in reply.
476 The command will send a stream to the server and get a stream in reply.
477 """
477 """
478 raise NotImplementedError()
478 raise NotImplementedError()
479
479
480 def _abort(self, exception):
480 def _abort(self, exception):
481 """clearly abort the wire protocol connection and raise the exception
481 """clearly abort the wire protocol connection and raise the exception
482 """
482 """
483 raise NotImplementedError()
483 raise NotImplementedError()
484
484
485 # server side
485 # server side
486
486
487 # wire protocol command can either return a string or one of these classes.
487 # wire protocol command can either return a string or one of these classes.
488
488
489 def getdispatchrepo(repo, proto, command):
489 def getdispatchrepo(repo, proto, command):
490 """Obtain the repo used for processing wire protocol commands.
490 """Obtain the repo used for processing wire protocol commands.
491
491
492 The intent of this function is to serve as a monkeypatch point for
492 The intent of this function is to serve as a monkeypatch point for
493 extensions that need commands to operate on different repo views under
493 extensions that need commands to operate on different repo views under
494 specialized circumstances.
494 specialized circumstances.
495 """
495 """
496 return repo.filtered('served')
496 return repo.filtered('served')
497
497
498 def dispatch(repo, proto, command):
498 def dispatch(repo, proto, command):
499 repo = getdispatchrepo(repo, proto, command)
499 repo = getdispatchrepo(repo, proto, command)
500 func, spec = commands[command]
500 func, spec = commands[command]
501 args = proto.getargs(spec)
501 args = proto.getargs(spec)
502 return func(repo, proto, *args)
502 return func(repo, proto, *args)
503
503
504 def options(cmd, keys, others):
504 def options(cmd, keys, others):
505 opts = {}
505 opts = {}
506 for k in keys:
506 for k in keys:
507 if k in others:
507 if k in others:
508 opts[k] = others[k]
508 opts[k] = others[k]
509 del others[k]
509 del others[k]
510 if others:
510 if others:
511 util.stderr.write("warning: %s ignored unexpected arguments %s\n"
511 util.stderr.write("warning: %s ignored unexpected arguments %s\n"
512 % (cmd, ",".join(others)))
512 % (cmd, ",".join(others)))
513 return opts
513 return opts
514
514
515 def bundle1allowed(repo, action):
515 def bundle1allowed(repo, action):
516 """Whether a bundle1 operation is allowed from the server.
516 """Whether a bundle1 operation is allowed from the server.
517
517
518 Priority is:
518 Priority is:
519
519
520 1. server.bundle1gd.<action> (if generaldelta active)
520 1. server.bundle1gd.<action> (if generaldelta active)
521 2. server.bundle1.<action>
521 2. server.bundle1.<action>
522 3. server.bundle1gd (if generaldelta active)
522 3. server.bundle1gd (if generaldelta active)
523 4. server.bundle1
523 4. server.bundle1
524 """
524 """
525 ui = repo.ui
525 ui = repo.ui
526 gd = 'generaldelta' in repo.requirements
526 gd = 'generaldelta' in repo.requirements
527
527
528 if gd:
528 if gd:
529 v = ui.configbool('server', 'bundle1gd.%s' % action)
529 v = ui.configbool('server', 'bundle1gd.%s' % action)
530 if v is not None:
530 if v is not None:
531 return v
531 return v
532
532
533 v = ui.configbool('server', 'bundle1.%s' % action)
533 v = ui.configbool('server', 'bundle1.%s' % action)
534 if v is not None:
534 if v is not None:
535 return v
535 return v
536
536
537 if gd:
537 if gd:
538 v = ui.configbool('server', 'bundle1gd')
538 v = ui.configbool('server', 'bundle1gd')
539 if v is not None:
539 if v is not None:
540 return v
540 return v
541
541
542 return ui.configbool('server', 'bundle1')
542 return ui.configbool('server', 'bundle1')
543
543
544 def supportedcompengines(ui, role):
544 def supportedcompengines(ui, role):
545 """Obtain the list of supported compression engines for a request."""
545 """Obtain the list of supported compression engines for a request."""
546 assert role in (util.CLIENTROLE, util.SERVERROLE)
546 assert role in (util.CLIENTROLE, util.SERVERROLE)
547
547
548 compengines = util.compengines.supportedwireengines(role)
548 compengines = util.compengines.supportedwireengines(role)
549
549
550 # Allow config to override default list and ordering.
550 # Allow config to override default list and ordering.
551 if role == util.SERVERROLE:
551 if role == util.SERVERROLE:
552 configengines = ui.configlist('server', 'compressionengines')
552 configengines = ui.configlist('server', 'compressionengines')
553 config = 'server.compressionengines'
553 config = 'server.compressionengines'
554 else:
554 else:
555 # This is currently implemented mainly to facilitate testing. In most
555 # This is currently implemented mainly to facilitate testing. In most
556 # cases, the server should be in charge of choosing a compression engine
556 # cases, the server should be in charge of choosing a compression engine
557 # because a server has the most to lose from a sub-optimal choice. (e.g.
557 # because a server has the most to lose from a sub-optimal choice. (e.g.
558 # CPU DoS due to an expensive engine or a network DoS due to poor
558 # CPU DoS due to an expensive engine or a network DoS due to poor
559 # compression ratio).
559 # compression ratio).
560 configengines = ui.configlist('experimental',
560 configengines = ui.configlist('experimental',
561 'clientcompressionengines')
561 'clientcompressionengines')
562 config = 'experimental.clientcompressionengines'
562 config = 'experimental.clientcompressionengines'
563
563
564 # No explicit config. Filter out the ones that aren't supposed to be
564 # No explicit config. Filter out the ones that aren't supposed to be
565 # advertised and return default ordering.
565 # advertised and return default ordering.
566 if not configengines:
566 if not configengines:
567 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
567 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
568 return [e for e in compengines
568 return [e for e in compengines
569 if getattr(e.wireprotosupport(), attr) > 0]
569 if getattr(e.wireprotosupport(), attr) > 0]
570
570
571 # If compression engines are listed in the config, assume there is a good
571 # If compression engines are listed in the config, assume there is a good
572 # reason for it (like server operators wanting to achieve specific
572 # reason for it (like server operators wanting to achieve specific
573 # performance characteristics). So fail fast if the config references
573 # performance characteristics). So fail fast if the config references
574 # unusable compression engines.
574 # unusable compression engines.
575 validnames = set(e.name() for e in compengines)
575 validnames = set(e.name() for e in compengines)
576 invalidnames = set(e for e in configengines if e not in validnames)
576 invalidnames = set(e for e in configengines if e not in validnames)
577 if invalidnames:
577 if invalidnames:
578 raise error.Abort(_('invalid compression engine defined in %s: %s') %
578 raise error.Abort(_('invalid compression engine defined in %s: %s') %
579 (config, ', '.join(sorted(invalidnames))))
579 (config, ', '.join(sorted(invalidnames))))
580
580
581 compengines = [e for e in compengines if e.name() in configengines]
581 compengines = [e for e in compengines if e.name() in configengines]
582 compengines = sorted(compengines,
582 compengines = sorted(compengines,
583 key=lambda e: configengines.index(e.name()))
583 key=lambda e: configengines.index(e.name()))
584
584
585 if not compengines:
585 if not compengines:
586 raise error.Abort(_('%s config option does not specify any known '
586 raise error.Abort(_('%s config option does not specify any known '
587 'compression engines') % config,
587 'compression engines') % config,
588 hint=_('usable compression engines: %s') %
588 hint=_('usable compression engines: %s') %
589 ', '.sorted(validnames))
589 ', '.sorted(validnames))
590
590
591 return compengines
591 return compengines
592
592
593 class commandentry(object):
593 class commandentry(object):
594 """Represents a declared wire protocol command."""
594 """Represents a declared wire protocol command."""
595 def __init__(self, func, args=''):
595 def __init__(self, func, args=''):
596 self.func = func
596 self.func = func
597 self.args = args
597 self.args = args
598
598
599 def _merge(self, func, args):
599 def _merge(self, func, args):
600 """Merge this instance with an incoming 2-tuple.
600 """Merge this instance with an incoming 2-tuple.
601
601
602 This is called when a caller using the old 2-tuple API attempts
602 This is called when a caller using the old 2-tuple API attempts
603 to replace an instance. The incoming values are merged with
603 to replace an instance. The incoming values are merged with
604 data not captured by the 2-tuple and a new instance containing
604 data not captured by the 2-tuple and a new instance containing
605 the union of the two objects is returned.
605 the union of the two objects is returned.
606 """
606 """
607 return commandentry(func, args)
607 return commandentry(func, args)
608
608
609 # Old code treats instances as 2-tuples. So expose that interface.
609 # Old code treats instances as 2-tuples. So expose that interface.
610 def __iter__(self):
610 def __iter__(self):
611 yield self.func
611 yield self.func
612 yield self.args
612 yield self.args
613
613
614 def __getitem__(self, i):
614 def __getitem__(self, i):
615 if i == 0:
615 if i == 0:
616 return self.func
616 return self.func
617 elif i == 1:
617 elif i == 1:
618 return self.args
618 return self.args
619 else:
619 else:
620 raise IndexError('can only access elements 0 and 1')
620 raise IndexError('can only access elements 0 and 1')
621
621
622 class commanddict(dict):
622 class commanddict(dict):
623 """Container for registered wire protocol commands.
623 """Container for registered wire protocol commands.
624
624
625 It behaves like a dict. But __setitem__ is overwritten to allow silent
625 It behaves like a dict. But __setitem__ is overwritten to allow silent
626 coercion of values from 2-tuples for API compatibility.
626 coercion of values from 2-tuples for API compatibility.
627 """
627 """
628 def __setitem__(self, k, v):
628 def __setitem__(self, k, v):
629 if isinstance(v, commandentry):
629 if isinstance(v, commandentry):
630 pass
630 pass
631 # Cast 2-tuples to commandentry instances.
631 # Cast 2-tuples to commandentry instances.
632 elif isinstance(v, tuple):
632 elif isinstance(v, tuple):
633 if len(v) != 2:
633 if len(v) != 2:
634 raise ValueError('command tuples must have exactly 2 elements')
634 raise ValueError('command tuples must have exactly 2 elements')
635
635
636 # It is common for extensions to wrap wire protocol commands via
636 # It is common for extensions to wrap wire protocol commands via
637 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
637 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
638 # doing this aren't aware of the new API that uses objects to store
638 # doing this aren't aware of the new API that uses objects to store
639 # command entries, we automatically merge old state with new.
639 # command entries, we automatically merge old state with new.
640 if k in self:
640 if k in self:
641 v = self[k]._merge(v[0], v[1])
641 v = self[k]._merge(v[0], v[1])
642 else:
642 else:
643 v = commandentry(v[0], v[1])
643 v = commandentry(v[0], v[1])
644 else:
644 else:
645 raise ValueError('command entries must be commandentry instances '
645 raise ValueError('command entries must be commandentry instances '
646 'or 2-tuples')
646 'or 2-tuples')
647
647
648 return super(commanddict, self).__setitem__(k, v)
648 return super(commanddict, self).__setitem__(k, v)
649
649
650 def commandavailable(self, command, proto):
650 def commandavailable(self, command, proto):
651 """Determine if a command is available for the requested protocol."""
651 """Determine if a command is available for the requested protocol."""
652 # For now, commands are available for all protocols. So do a simple
652 # For now, commands are available for all protocols. So do a simple
653 # membership test.
653 # membership test.
654 return command in self
654 return command in self
655
655
656 commands = commanddict()
656 commands = commanddict()
657
657
658 def wireprotocommand(name, args=''):
658 def wireprotocommand(name, args=''):
659 """Decorator to declare a wire protocol command.
659 """Decorator to declare a wire protocol command.
660
660
661 ``name`` is the name of the wire protocol command being provided.
661 ``name`` is the name of the wire protocol command being provided.
662
662
663 ``args`` is a space-delimited list of named arguments that the command
663 ``args`` is a space-delimited list of named arguments that the command
664 accepts. ``*`` is a special value that says to accept all arguments.
664 accepts. ``*`` is a special value that says to accept all arguments.
665 """
665 """
666 def register(func):
666 def register(func):
667 commands[name] = commandentry(func, args)
667 commands[name] = commandentry(func, args)
668 return func
668 return func
669 return register
669 return register
670
670
671 @wireprotocommand('batch', 'cmds *')
671 @wireprotocommand('batch', 'cmds *')
672 def batch(repo, proto, cmds, others):
672 def batch(repo, proto, cmds, others):
673 repo = repo.filtered("served")
673 repo = repo.filtered("served")
674 res = []
674 res = []
675 for pair in cmds.split(';'):
675 for pair in cmds.split(';'):
676 op, args = pair.split(' ', 1)
676 op, args = pair.split(' ', 1)
677 vals = {}
677 vals = {}
678 for a in args.split(','):
678 for a in args.split(','):
679 if a:
679 if a:
680 n, v = a.split('=')
680 n, v = a.split('=')
681 vals[unescapearg(n)] = unescapearg(v)
681 vals[unescapearg(n)] = unescapearg(v)
682 func, spec = commands[op]
682 func, spec = commands[op]
683 if spec:
683 if spec:
684 keys = spec.split()
684 keys = spec.split()
685 data = {}
685 data = {}
686 for k in keys:
686 for k in keys:
687 if k == '*':
687 if k == '*':
688 star = {}
688 star = {}
689 for key in vals.keys():
689 for key in vals.keys():
690 if key not in keys:
690 if key not in keys:
691 star[key] = vals[key]
691 star[key] = vals[key]
692 data['*'] = star
692 data['*'] = star
693 else:
693 else:
694 data[k] = vals[k]
694 data[k] = vals[k]
695 result = func(repo, proto, *[data[k] for k in keys])
695 result = func(repo, proto, *[data[k] for k in keys])
696 else:
696 else:
697 result = func(repo, proto)
697 result = func(repo, proto)
698 if isinstance(result, ooberror):
698 if isinstance(result, ooberror):
699 return result
699 return result
700
700
701 # For now, all batchable commands must return bytesresponse or
701 # For now, all batchable commands must return bytesresponse or
702 # raw bytes (for backwards compatibility).
702 # raw bytes (for backwards compatibility).
703 assert isinstance(result, (bytesresponse, bytes))
703 assert isinstance(result, (bytesresponse, bytes))
704 if isinstance(result, bytesresponse):
704 if isinstance(result, bytesresponse):
705 result = result.data
705 result = result.data
706 res.append(escapearg(result))
706 res.append(escapearg(result))
707
707
708 return bytesresponse(';'.join(res))
708 return bytesresponse(';'.join(res))
709
709
710 @wireprotocommand('between', 'pairs')
710 @wireprotocommand('between', 'pairs')
711 def between(repo, proto, pairs):
711 def between(repo, proto, pairs):
712 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
712 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
713 r = []
713 r = []
714 for b in repo.between(pairs):
714 for b in repo.between(pairs):
715 r.append(encodelist(b) + "\n")
715 r.append(encodelist(b) + "\n")
716
716
717 return bytesresponse(''.join(r))
717 return bytesresponse(''.join(r))
718
718
719 @wireprotocommand('branchmap')
719 @wireprotocommand('branchmap')
720 def branchmap(repo, proto):
720 def branchmap(repo, proto):
721 branchmap = repo.branchmap()
721 branchmap = repo.branchmap()
722 heads = []
722 heads = []
723 for branch, nodes in branchmap.iteritems():
723 for branch, nodes in branchmap.iteritems():
724 branchname = urlreq.quote(encoding.fromlocal(branch))
724 branchname = urlreq.quote(encoding.fromlocal(branch))
725 branchnodes = encodelist(nodes)
725 branchnodes = encodelist(nodes)
726 heads.append('%s %s' % (branchname, branchnodes))
726 heads.append('%s %s' % (branchname, branchnodes))
727
727
728 return bytesresponse('\n'.join(heads))
728 return bytesresponse('\n'.join(heads))
729
729
730 @wireprotocommand('branches', 'nodes')
730 @wireprotocommand('branches', 'nodes')
731 def branches(repo, proto, nodes):
731 def branches(repo, proto, nodes):
732 nodes = decodelist(nodes)
732 nodes = decodelist(nodes)
733 r = []
733 r = []
734 for b in repo.branches(nodes):
734 for b in repo.branches(nodes):
735 r.append(encodelist(b) + "\n")
735 r.append(encodelist(b) + "\n")
736
736
737 return bytesresponse(''.join(r))
737 return bytesresponse(''.join(r))
738
738
739 @wireprotocommand('clonebundles', '')
739 @wireprotocommand('clonebundles', '')
740 def clonebundles(repo, proto):
740 def clonebundles(repo, proto):
741 """Server command for returning info for available bundles to seed clones.
741 """Server command for returning info for available bundles to seed clones.
742
742
743 Clients will parse this response and determine what bundle to fetch.
743 Clients will parse this response and determine what bundle to fetch.
744
744
745 Extensions may wrap this command to filter or dynamically emit data
745 Extensions may wrap this command to filter or dynamically emit data
746 depending on the request. e.g. you could advertise URLs for the closest
746 depending on the request. e.g. you could advertise URLs for the closest
747 data center given the client's IP address.
747 data center given the client's IP address.
748 """
748 """
749 return bytesresponse(repo.vfs.tryread('clonebundles.manifest'))
749 return bytesresponse(repo.vfs.tryread('clonebundles.manifest'))
750
750
751 wireprotocaps = ['lookup', 'changegroupsubset', 'branchmap', 'pushkey',
751 wireprotocaps = ['lookup', 'changegroupsubset', 'branchmap', 'pushkey',
752 'known', 'getbundle', 'unbundlehash', 'batch']
752 'known', 'getbundle', 'unbundlehash', 'batch']
753
753
754 def _capabilities(repo, proto):
754 def _capabilities(repo, proto):
755 """return a list of capabilities for a repo
755 """return a list of capabilities for a repo
756
756
757 This function exists to allow extensions to easily wrap capabilities
757 This function exists to allow extensions to easily wrap capabilities
758 computation
758 computation
759
759
760 - returns a lists: easy to alter
760 - returns a lists: easy to alter
761 - change done here will be propagated to both `capabilities` and `hello`
761 - change done here will be propagated to both `capabilities` and `hello`
762 command without any other action needed.
762 command without any other action needed.
763 """
763 """
764 # copy to prevent modification of the global list
764 # copy to prevent modification of the global list
765 caps = list(wireprotocaps)
765 caps = list(wireprotocaps)
766 if streamclone.allowservergeneration(repo):
766 if streamclone.allowservergeneration(repo):
767 if repo.ui.configbool('server', 'preferuncompressed'):
767 if repo.ui.configbool('server', 'preferuncompressed'):
768 caps.append('stream-preferred')
768 caps.append('stream-preferred')
769 requiredformats = repo.requirements & repo.supportedformats
769 requiredformats = repo.requirements & repo.supportedformats
770 # if our local revlogs are just revlogv1, add 'stream' cap
770 # if our local revlogs are just revlogv1, add 'stream' cap
771 if not requiredformats - {'revlogv1'}:
771 if not requiredformats - {'revlogv1'}:
772 caps.append('stream')
772 caps.append('stream')
773 # otherwise, add 'streamreqs' detailing our local revlog format
773 # otherwise, add 'streamreqs' detailing our local revlog format
774 else:
774 else:
775 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
775 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
776 if repo.ui.configbool('experimental', 'bundle2-advertise'):
776 if repo.ui.configbool('experimental', 'bundle2-advertise'):
777 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
777 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
778 caps.append('bundle2=' + urlreq.quote(capsblob))
778 caps.append('bundle2=' + urlreq.quote(capsblob))
779 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
779 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
780
780
781 if proto.name == 'http-v1':
781 if proto.name == 'http-v1':
782 caps.append('httpheader=%d' %
782 caps.append('httpheader=%d' %
783 repo.ui.configint('server', 'maxhttpheaderlen'))
783 repo.ui.configint('server', 'maxhttpheaderlen'))
784 if repo.ui.configbool('experimental', 'httppostargs'):
784 if repo.ui.configbool('experimental', 'httppostargs'):
785 caps.append('httppostargs')
785 caps.append('httppostargs')
786
786
787 # FUTURE advertise 0.2rx once support is implemented
787 # FUTURE advertise 0.2rx once support is implemented
788 # FUTURE advertise minrx and mintx after consulting config option
788 # FUTURE advertise minrx and mintx after consulting config option
789 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
789 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
790
790
791 compengines = supportedcompengines(repo.ui, util.SERVERROLE)
791 compengines = supportedcompengines(repo.ui, util.SERVERROLE)
792 if compengines:
792 if compengines:
793 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
793 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
794 for e in compengines)
794 for e in compengines)
795 caps.append('compression=%s' % comptypes)
795 caps.append('compression=%s' % comptypes)
796
796
797 return caps
797 return caps
798
798
799 # If you are writing an extension and consider wrapping this function. Wrap
799 # If you are writing an extension and consider wrapping this function. Wrap
800 # `_capabilities` instead.
800 # `_capabilities` instead.
801 @wireprotocommand('capabilities')
801 @wireprotocommand('capabilities')
802 def capabilities(repo, proto):
802 def capabilities(repo, proto):
803 return bytesresponse(' '.join(_capabilities(repo, proto)))
803 return bytesresponse(' '.join(_capabilities(repo, proto)))
804
804
805 @wireprotocommand('changegroup', 'roots')
805 @wireprotocommand('changegroup', 'roots')
806 def changegroup(repo, proto, roots):
806 def changegroup(repo, proto, roots):
807 nodes = decodelist(roots)
807 nodes = decodelist(roots)
808 outgoing = discovery.outgoing(repo, missingroots=nodes,
808 outgoing = discovery.outgoing(repo, missingroots=nodes,
809 missingheads=repo.heads())
809 missingheads=repo.heads())
810 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
810 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
811 gen = iter(lambda: cg.read(32768), '')
811 gen = iter(lambda: cg.read(32768), '')
812 return streamres(gen=gen)
812 return streamres(gen=gen)
813
813
814 @wireprotocommand('changegroupsubset', 'bases heads')
814 @wireprotocommand('changegroupsubset', 'bases heads')
815 def changegroupsubset(repo, proto, bases, heads):
815 def changegroupsubset(repo, proto, bases, heads):
816 bases = decodelist(bases)
816 bases = decodelist(bases)
817 heads = decodelist(heads)
817 heads = decodelist(heads)
818 outgoing = discovery.outgoing(repo, missingroots=bases,
818 outgoing = discovery.outgoing(repo, missingroots=bases,
819 missingheads=heads)
819 missingheads=heads)
820 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
820 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
821 gen = iter(lambda: cg.read(32768), '')
821 gen = iter(lambda: cg.read(32768), '')
822 return streamres(gen=gen)
822 return streamres(gen=gen)
823
823
824 @wireprotocommand('debugwireargs', 'one two *')
824 @wireprotocommand('debugwireargs', 'one two *')
825 def debugwireargs(repo, proto, one, two, others):
825 def debugwireargs(repo, proto, one, two, others):
826 # only accept optional args from the known set
826 # only accept optional args from the known set
827 opts = options('debugwireargs', ['three', 'four'], others)
827 opts = options('debugwireargs', ['three', 'four'], others)
828 return bytesresponse(repo.debugwireargs(one, two,
828 return bytesresponse(repo.debugwireargs(one, two,
829 **pycompat.strkwargs(opts)))
829 **pycompat.strkwargs(opts)))
830
830
831 @wireprotocommand('getbundle', '*')
831 @wireprotocommand('getbundle', '*')
832 def getbundle(repo, proto, others):
832 def getbundle(repo, proto, others):
833 opts = options('getbundle', gboptsmap.keys(), others)
833 opts = options('getbundle', gboptsmap.keys(), others)
834 for k, v in opts.iteritems():
834 for k, v in opts.iteritems():
835 keytype = gboptsmap[k]
835 keytype = gboptsmap[k]
836 if keytype == 'nodes':
836 if keytype == 'nodes':
837 opts[k] = decodelist(v)
837 opts[k] = decodelist(v)
838 elif keytype == 'csv':
838 elif keytype == 'csv':
839 opts[k] = list(v.split(','))
839 opts[k] = list(v.split(','))
840 elif keytype == 'scsv':
840 elif keytype == 'scsv':
841 opts[k] = set(v.split(','))
841 opts[k] = set(v.split(','))
842 elif keytype == 'boolean':
842 elif keytype == 'boolean':
843 # Client should serialize False as '0', which is a non-empty string
843 # Client should serialize False as '0', which is a non-empty string
844 # so it evaluates as a True bool.
844 # so it evaluates as a True bool.
845 if v == '0':
845 if v == '0':
846 opts[k] = False
846 opts[k] = False
847 else:
847 else:
848 opts[k] = bool(v)
848 opts[k] = bool(v)
849 elif keytype != 'plain':
849 elif keytype != 'plain':
850 raise KeyError('unknown getbundle option type %s'
850 raise KeyError('unknown getbundle option type %s'
851 % keytype)
851 % keytype)
852
852
853 if not bundle1allowed(repo, 'pull'):
853 if not bundle1allowed(repo, 'pull'):
854 if not exchange.bundle2requested(opts.get('bundlecaps')):
854 if not exchange.bundle2requested(opts.get('bundlecaps')):
855 if proto.name == 'http-v1':
855 if proto.name == 'http-v1':
856 return ooberror(bundle2required)
856 return ooberror(bundle2required)
857 raise error.Abort(bundle2requiredmain,
857 raise error.Abort(bundle2requiredmain,
858 hint=bundle2requiredhint)
858 hint=bundle2requiredhint)
859
859
860 prefercompressed = True
860 prefercompressed = True
861
861
862 try:
862 try:
863 if repo.ui.configbool('server', 'disablefullbundle'):
863 if repo.ui.configbool('server', 'disablefullbundle'):
864 # Check to see if this is a full clone.
864 # Check to see if this is a full clone.
865 clheads = set(repo.changelog.heads())
865 clheads = set(repo.changelog.heads())
866 changegroup = opts.get('cg', True)
866 changegroup = opts.get('cg', True)
867 heads = set(opts.get('heads', set()))
867 heads = set(opts.get('heads', set()))
868 common = set(opts.get('common', set()))
868 common = set(opts.get('common', set()))
869 common.discard(nullid)
869 common.discard(nullid)
870 if changegroup and not common and clheads == heads:
870 if changegroup and not common and clheads == heads:
871 raise error.Abort(
871 raise error.Abort(
872 _('server has pull-based clones disabled'),
872 _('server has pull-based clones disabled'),
873 hint=_('remove --pull if specified or upgrade Mercurial'))
873 hint=_('remove --pull if specified or upgrade Mercurial'))
874
874
875 info, chunks = exchange.getbundlechunks(repo, 'serve',
875 info, chunks = exchange.getbundlechunks(repo, 'serve',
876 **pycompat.strkwargs(opts))
876 **pycompat.strkwargs(opts))
877 prefercompressed = info.get('prefercompressed', True)
877 prefercompressed = info.get('prefercompressed', True)
878 except error.Abort as exc:
878 except error.Abort as exc:
879 # cleanly forward Abort error to the client
879 # cleanly forward Abort error to the client
880 if not exchange.bundle2requested(opts.get('bundlecaps')):
880 if not exchange.bundle2requested(opts.get('bundlecaps')):
881 if proto.name == 'http-v1':
881 if proto.name == 'http-v1':
882 return ooberror(str(exc) + '\n')
882 return ooberror(pycompat.bytestr(exc) + '\n')
883 raise # cannot do better for bundle1 + ssh
883 raise # cannot do better for bundle1 + ssh
884 # bundle2 request expect a bundle2 reply
884 # bundle2 request expect a bundle2 reply
885 bundler = bundle2.bundle20(repo.ui)
885 bundler = bundle2.bundle20(repo.ui)
886 manargs = [('message', str(exc))]
886 manargs = [('message', pycompat.bytestr(exc))]
887 advargs = []
887 advargs = []
888 if exc.hint is not None:
888 if exc.hint is not None:
889 advargs.append(('hint', exc.hint))
889 advargs.append(('hint', exc.hint))
890 bundler.addpart(bundle2.bundlepart('error:abort',
890 bundler.addpart(bundle2.bundlepart('error:abort',
891 manargs, advargs))
891 manargs, advargs))
892 chunks = bundler.getchunks()
892 chunks = bundler.getchunks()
893 prefercompressed = False
893 prefercompressed = False
894
894
895 return streamres(gen=chunks, prefer_uncompressed=not prefercompressed)
895 return streamres(gen=chunks, prefer_uncompressed=not prefercompressed)
896
896
897 @wireprotocommand('heads')
897 @wireprotocommand('heads')
898 def heads(repo, proto):
898 def heads(repo, proto):
899 h = repo.heads()
899 h = repo.heads()
900 return bytesresponse(encodelist(h) + '\n')
900 return bytesresponse(encodelist(h) + '\n')
901
901
902 @wireprotocommand('hello')
902 @wireprotocommand('hello')
903 def hello(repo, proto):
903 def hello(repo, proto):
904 """Called as part of SSH handshake to obtain server info.
904 """Called as part of SSH handshake to obtain server info.
905
905
906 Returns a list of lines describing interesting things about the
906 Returns a list of lines describing interesting things about the
907 server, in an RFC822-like format.
907 server, in an RFC822-like format.
908
908
909 Currently, the only one defined is ``capabilities``, which consists of a
909 Currently, the only one defined is ``capabilities``, which consists of a
910 line of space separated tokens describing server abilities:
910 line of space separated tokens describing server abilities:
911
911
912 capabilities: <token0> <token1> <token2>
912 capabilities: <token0> <token1> <token2>
913 """
913 """
914 caps = capabilities(repo, proto).data
914 caps = capabilities(repo, proto).data
915 return bytesresponse('capabilities: %s\n' % caps)
915 return bytesresponse('capabilities: %s\n' % caps)
916
916
917 @wireprotocommand('listkeys', 'namespace')
917 @wireprotocommand('listkeys', 'namespace')
918 def listkeys(repo, proto, namespace):
918 def listkeys(repo, proto, namespace):
919 d = repo.listkeys(encoding.tolocal(namespace)).items()
919 d = repo.listkeys(encoding.tolocal(namespace)).items()
920 return bytesresponse(pushkeymod.encodekeys(d))
920 return bytesresponse(pushkeymod.encodekeys(d))
921
921
922 @wireprotocommand('lookup', 'key')
922 @wireprotocommand('lookup', 'key')
923 def lookup(repo, proto, key):
923 def lookup(repo, proto, key):
924 try:
924 try:
925 k = encoding.tolocal(key)
925 k = encoding.tolocal(key)
926 c = repo[k]
926 c = repo[k]
927 r = c.hex()
927 r = c.hex()
928 success = 1
928 success = 1
929 except Exception as inst:
929 except Exception as inst:
930 r = str(inst)
930 r = str(inst)
931 success = 0
931 success = 0
932 return bytesresponse('%d %s\n' % (success, r))
932 return bytesresponse('%d %s\n' % (success, r))
933
933
934 @wireprotocommand('known', 'nodes *')
934 @wireprotocommand('known', 'nodes *')
935 def known(repo, proto, nodes, others):
935 def known(repo, proto, nodes, others):
936 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
936 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
937 return bytesresponse(v)
937 return bytesresponse(v)
938
938
939 @wireprotocommand('pushkey', 'namespace key old new')
939 @wireprotocommand('pushkey', 'namespace key old new')
940 def pushkey(repo, proto, namespace, key, old, new):
940 def pushkey(repo, proto, namespace, key, old, new):
941 # compatibility with pre-1.8 clients which were accidentally
941 # compatibility with pre-1.8 clients which were accidentally
942 # sending raw binary nodes rather than utf-8-encoded hex
942 # sending raw binary nodes rather than utf-8-encoded hex
943 if len(new) == 20 and util.escapestr(new) != new:
943 if len(new) == 20 and util.escapestr(new) != new:
944 # looks like it could be a binary node
944 # looks like it could be a binary node
945 try:
945 try:
946 new.decode('utf-8')
946 new.decode('utf-8')
947 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
947 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
948 except UnicodeDecodeError:
948 except UnicodeDecodeError:
949 pass # binary, leave unmodified
949 pass # binary, leave unmodified
950 else:
950 else:
951 new = encoding.tolocal(new) # normal path
951 new = encoding.tolocal(new) # normal path
952
952
953 with proto.mayberedirectstdio() as output:
953 with proto.mayberedirectstdio() as output:
954 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
954 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
955 encoding.tolocal(old), new) or False
955 encoding.tolocal(old), new) or False
956
956
957 output = output.getvalue() if output else ''
957 output = output.getvalue() if output else ''
958 return bytesresponse('%s\n%s' % (int(r), output))
958 return bytesresponse('%s\n%s' % (int(r), output))
959
959
960 @wireprotocommand('stream_out')
960 @wireprotocommand('stream_out')
961 def stream(repo, proto):
961 def stream(repo, proto):
962 '''If the server supports streaming clone, it advertises the "stream"
962 '''If the server supports streaming clone, it advertises the "stream"
963 capability with a value representing the version and flags of the repo
963 capability with a value representing the version and flags of the repo
964 it is serving. Client checks to see if it understands the format.
964 it is serving. Client checks to see if it understands the format.
965 '''
965 '''
966 return streamres_legacy(streamclone.generatev1wireproto(repo))
966 return streamres_legacy(streamclone.generatev1wireproto(repo))
967
967
968 @wireprotocommand('unbundle', 'heads')
968 @wireprotocommand('unbundle', 'heads')
969 def unbundle(repo, proto, heads):
969 def unbundle(repo, proto, heads):
970 their_heads = decodelist(heads)
970 their_heads = decodelist(heads)
971
971
972 with proto.mayberedirectstdio() as output:
972 with proto.mayberedirectstdio() as output:
973 try:
973 try:
974 exchange.check_heads(repo, their_heads, 'preparing changes')
974 exchange.check_heads(repo, their_heads, 'preparing changes')
975
975
976 # write bundle data to temporary file because it can be big
976 # write bundle data to temporary file because it can be big
977 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
977 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
978 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
978 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
979 r = 0
979 r = 0
980 try:
980 try:
981 proto.forwardpayload(fp)
981 proto.forwardpayload(fp)
982 fp.seek(0)
982 fp.seek(0)
983 gen = exchange.readbundle(repo.ui, fp, None)
983 gen = exchange.readbundle(repo.ui, fp, None)
984 if (isinstance(gen, changegroupmod.cg1unpacker)
984 if (isinstance(gen, changegroupmod.cg1unpacker)
985 and not bundle1allowed(repo, 'push')):
985 and not bundle1allowed(repo, 'push')):
986 if proto.name == 'http-v1':
986 if proto.name == 'http-v1':
987 # need to special case http because stderr do not get to
987 # need to special case http because stderr do not get to
988 # the http client on failed push so we need to abuse
988 # the http client on failed push so we need to abuse
989 # some other error type to make sure the message get to
989 # some other error type to make sure the message get to
990 # the user.
990 # the user.
991 return ooberror(bundle2required)
991 return ooberror(bundle2required)
992 raise error.Abort(bundle2requiredmain,
992 raise error.Abort(bundle2requiredmain,
993 hint=bundle2requiredhint)
993 hint=bundle2requiredhint)
994
994
995 r = exchange.unbundle(repo, gen, their_heads, 'serve',
995 r = exchange.unbundle(repo, gen, their_heads, 'serve',
996 proto.client())
996 proto.client())
997 if util.safehasattr(r, 'addpart'):
997 if util.safehasattr(r, 'addpart'):
998 # The return looks streamable, we are in the bundle2 case
998 # The return looks streamable, we are in the bundle2 case
999 # and should return a stream.
999 # and should return a stream.
1000 return streamres_legacy(gen=r.getchunks())
1000 return streamres_legacy(gen=r.getchunks())
1001 return pushres(r, output.getvalue() if output else '')
1001 return pushres(r, output.getvalue() if output else '')
1002
1002
1003 finally:
1003 finally:
1004 fp.close()
1004 fp.close()
1005 os.unlink(tempname)
1005 os.unlink(tempname)
1006
1006
1007 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
1007 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
1008 # handle non-bundle2 case first
1008 # handle non-bundle2 case first
1009 if not getattr(exc, 'duringunbundle2', False):
1009 if not getattr(exc, 'duringunbundle2', False):
1010 try:
1010 try:
1011 raise
1011 raise
1012 except error.Abort:
1012 except error.Abort:
1013 # The old code we moved used util.stderr directly.
1013 # The old code we moved used util.stderr directly.
1014 # We did not change it to minimise code change.
1014 # We did not change it to minimise code change.
1015 # This need to be moved to something proper.
1015 # This need to be moved to something proper.
1016 # Feel free to do it.
1016 # Feel free to do it.
1017 util.stderr.write("abort: %s\n" % exc)
1017 util.stderr.write("abort: %s\n" % exc)
1018 if exc.hint is not None:
1018 if exc.hint is not None:
1019 util.stderr.write("(%s)\n" % exc.hint)
1019 util.stderr.write("(%s)\n" % exc.hint)
1020 return pushres(0, output.getvalue() if output else '')
1020 return pushres(0, output.getvalue() if output else '')
1021 except error.PushRaced:
1021 except error.PushRaced:
1022 return pusherr(str(exc),
1022 return pusherr(str(exc),
1023 output.getvalue() if output else '')
1023 output.getvalue() if output else '')
1024
1024
1025 bundler = bundle2.bundle20(repo.ui)
1025 bundler = bundle2.bundle20(repo.ui)
1026 for out in getattr(exc, '_bundle2salvagedoutput', ()):
1026 for out in getattr(exc, '_bundle2salvagedoutput', ()):
1027 bundler.addpart(out)
1027 bundler.addpart(out)
1028 try:
1028 try:
1029 try:
1029 try:
1030 raise
1030 raise
1031 except error.PushkeyFailed as exc:
1031 except error.PushkeyFailed as exc:
1032 # check client caps
1032 # check client caps
1033 remotecaps = getattr(exc, '_replycaps', None)
1033 remotecaps = getattr(exc, '_replycaps', None)
1034 if (remotecaps is not None
1034 if (remotecaps is not None
1035 and 'pushkey' not in remotecaps.get('error', ())):
1035 and 'pushkey' not in remotecaps.get('error', ())):
1036 # no support remote side, fallback to Abort handler.
1036 # no support remote side, fallback to Abort handler.
1037 raise
1037 raise
1038 part = bundler.newpart('error:pushkey')
1038 part = bundler.newpart('error:pushkey')
1039 part.addparam('in-reply-to', exc.partid)
1039 part.addparam('in-reply-to', exc.partid)
1040 if exc.namespace is not None:
1040 if exc.namespace is not None:
1041 part.addparam('namespace', exc.namespace,
1041 part.addparam('namespace', exc.namespace,
1042 mandatory=False)
1042 mandatory=False)
1043 if exc.key is not None:
1043 if exc.key is not None:
1044 part.addparam('key', exc.key, mandatory=False)
1044 part.addparam('key', exc.key, mandatory=False)
1045 if exc.new is not None:
1045 if exc.new is not None:
1046 part.addparam('new', exc.new, mandatory=False)
1046 part.addparam('new', exc.new, mandatory=False)
1047 if exc.old is not None:
1047 if exc.old is not None:
1048 part.addparam('old', exc.old, mandatory=False)
1048 part.addparam('old', exc.old, mandatory=False)
1049 if exc.ret is not None:
1049 if exc.ret is not None:
1050 part.addparam('ret', exc.ret, mandatory=False)
1050 part.addparam('ret', exc.ret, mandatory=False)
1051 except error.BundleValueError as exc:
1051 except error.BundleValueError as exc:
1052 errpart = bundler.newpart('error:unsupportedcontent')
1052 errpart = bundler.newpart('error:unsupportedcontent')
1053 if exc.parttype is not None:
1053 if exc.parttype is not None:
1054 errpart.addparam('parttype', exc.parttype)
1054 errpart.addparam('parttype', exc.parttype)
1055 if exc.params:
1055 if exc.params:
1056 errpart.addparam('params', '\0'.join(exc.params))
1056 errpart.addparam('params', '\0'.join(exc.params))
1057 except error.Abort as exc:
1057 except error.Abort as exc:
1058 manargs = [('message', str(exc))]
1058 manargs = [('message', str(exc))]
1059 advargs = []
1059 advargs = []
1060 if exc.hint is not None:
1060 if exc.hint is not None:
1061 advargs.append(('hint', exc.hint))
1061 advargs.append(('hint', exc.hint))
1062 bundler.addpart(bundle2.bundlepart('error:abort',
1062 bundler.addpart(bundle2.bundlepart('error:abort',
1063 manargs, advargs))
1063 manargs, advargs))
1064 except error.PushRaced as exc:
1064 except error.PushRaced as exc:
1065 bundler.newpart('error:pushraced', [('message', str(exc))])
1065 bundler.newpart('error:pushraced', [('message', str(exc))])
1066 return streamres_legacy(gen=bundler.getchunks())
1066 return streamres_legacy(gen=bundler.getchunks())
@@ -1,648 +1,648 b''
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import abc
9 import abc
10 import contextlib
10 import contextlib
11 import struct
11 import struct
12 import sys
12 import sys
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 error,
17 error,
18 hook,
18 hook,
19 pycompat,
19 pycompat,
20 util,
20 util,
21 wireproto,
21 wireproto,
22 wireprototypes,
22 wireprototypes,
23 )
23 )
24
24
25 stringio = util.stringio
25 stringio = util.stringio
26
26
27 urlerr = util.urlerr
27 urlerr = util.urlerr
28 urlreq = util.urlreq
28 urlreq = util.urlreq
29
29
30 HTTP_OK = 200
30 HTTP_OK = 200
31
31
32 HGTYPE = 'application/mercurial-0.1'
32 HGTYPE = 'application/mercurial-0.1'
33 HGTYPE2 = 'application/mercurial-0.2'
33 HGTYPE2 = 'application/mercurial-0.2'
34 HGERRTYPE = 'application/hg-error'
34 HGERRTYPE = 'application/hg-error'
35
35
36 # Names of the SSH protocol implementations.
36 # Names of the SSH protocol implementations.
37 SSHV1 = 'ssh-v1'
37 SSHV1 = 'ssh-v1'
38 # This is advertised over the wire. Incremental the counter at the end
38 # This is advertised over the wire. Incremental the counter at the end
39 # to reflect BC breakages.
39 # to reflect BC breakages.
40 SSHV2 = 'exp-ssh-v2-0001'
40 SSHV2 = 'exp-ssh-v2-0001'
41
41
42 class baseprotocolhandler(object):
42 class baseprotocolhandler(object):
43 """Abstract base class for wire protocol handlers.
43 """Abstract base class for wire protocol handlers.
44
44
45 A wire protocol handler serves as an interface between protocol command
45 A wire protocol handler serves as an interface between protocol command
46 handlers and the wire protocol transport layer. Protocol handlers provide
46 handlers and the wire protocol transport layer. Protocol handlers provide
47 methods to read command arguments, redirect stdio for the duration of
47 methods to read command arguments, redirect stdio for the duration of
48 the request, handle response types, etc.
48 the request, handle response types, etc.
49 """
49 """
50
50
51 __metaclass__ = abc.ABCMeta
51 __metaclass__ = abc.ABCMeta
52
52
53 @abc.abstractproperty
53 @abc.abstractproperty
54 def name(self):
54 def name(self):
55 """The name of the protocol implementation.
55 """The name of the protocol implementation.
56
56
57 Used for uniquely identifying the transport type.
57 Used for uniquely identifying the transport type.
58 """
58 """
59
59
60 @abc.abstractmethod
60 @abc.abstractmethod
61 def getargs(self, args):
61 def getargs(self, args):
62 """return the value for arguments in <args>
62 """return the value for arguments in <args>
63
63
64 returns a list of values (same order as <args>)"""
64 returns a list of values (same order as <args>)"""
65
65
66 @abc.abstractmethod
66 @abc.abstractmethod
67 def forwardpayload(self, fp):
67 def forwardpayload(self, fp):
68 """Read the raw payload and forward to a file.
68 """Read the raw payload and forward to a file.
69
69
70 The payload is read in full before the function returns.
70 The payload is read in full before the function returns.
71 """
71 """
72
72
73 @abc.abstractmethod
73 @abc.abstractmethod
74 def mayberedirectstdio(self):
74 def mayberedirectstdio(self):
75 """Context manager to possibly redirect stdio.
75 """Context manager to possibly redirect stdio.
76
76
77 The context manager yields a file-object like object that receives
77 The context manager yields a file-object like object that receives
78 stdout and stderr output when the context manager is active. Or it
78 stdout and stderr output when the context manager is active. Or it
79 yields ``None`` if no I/O redirection occurs.
79 yields ``None`` if no I/O redirection occurs.
80
80
81 The intent of this context manager is to capture stdio output
81 The intent of this context manager is to capture stdio output
82 so it may be sent in the response. Some transports support streaming
82 so it may be sent in the response. Some transports support streaming
83 stdio to the client in real time. For these transports, stdio output
83 stdio to the client in real time. For these transports, stdio output
84 won't be captured.
84 won't be captured.
85 """
85 """
86
86
87 @abc.abstractmethod
87 @abc.abstractmethod
88 def client(self):
88 def client(self):
89 """Returns a string representation of this client (as bytes)."""
89 """Returns a string representation of this client (as bytes)."""
90
90
91 def decodevaluefromheaders(req, headerprefix):
91 def decodevaluefromheaders(req, headerprefix):
92 """Decode a long value from multiple HTTP request headers.
92 """Decode a long value from multiple HTTP request headers.
93
93
94 Returns the value as a bytes, not a str.
94 Returns the value as a bytes, not a str.
95 """
95 """
96 chunks = []
96 chunks = []
97 i = 1
97 i = 1
98 prefix = headerprefix.upper().replace(r'-', r'_')
98 prefix = headerprefix.upper().replace(r'-', r'_')
99 while True:
99 while True:
100 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
100 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
101 if v is None:
101 if v is None:
102 break
102 break
103 chunks.append(pycompat.bytesurl(v))
103 chunks.append(pycompat.bytesurl(v))
104 i += 1
104 i += 1
105
105
106 return ''.join(chunks)
106 return ''.join(chunks)
107
107
108 class httpv1protocolhandler(baseprotocolhandler):
108 class httpv1protocolhandler(baseprotocolhandler):
109 def __init__(self, req, ui):
109 def __init__(self, req, ui):
110 self._req = req
110 self._req = req
111 self._ui = ui
111 self._ui = ui
112
112
113 @property
113 @property
114 def name(self):
114 def name(self):
115 return 'http-v1'
115 return 'http-v1'
116
116
117 def getargs(self, args):
117 def getargs(self, args):
118 knownargs = self._args()
118 knownargs = self._args()
119 data = {}
119 data = {}
120 keys = args.split()
120 keys = args.split()
121 for k in keys:
121 for k in keys:
122 if k == '*':
122 if k == '*':
123 star = {}
123 star = {}
124 for key in knownargs.keys():
124 for key in knownargs.keys():
125 if key != 'cmd' and key not in keys:
125 if key != 'cmd' and key not in keys:
126 star[key] = knownargs[key][0]
126 star[key] = knownargs[key][0]
127 data['*'] = star
127 data['*'] = star
128 else:
128 else:
129 data[k] = knownargs[k][0]
129 data[k] = knownargs[k][0]
130 return [data[k] for k in keys]
130 return [data[k] for k in keys]
131
131
132 def _args(self):
132 def _args(self):
133 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
133 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
134 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
134 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
135 if postlen:
135 if postlen:
136 args.update(urlreq.parseqs(
136 args.update(urlreq.parseqs(
137 self._req.read(postlen), keep_blank_values=True))
137 self._req.read(postlen), keep_blank_values=True))
138 return args
138 return args
139
139
140 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
140 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
141 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
141 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
142 return args
142 return args
143
143
144 def forwardpayload(self, fp):
144 def forwardpayload(self, fp):
145 length = int(self._req.env[r'CONTENT_LENGTH'])
145 length = int(self._req.env[r'CONTENT_LENGTH'])
146 # If httppostargs is used, we need to read Content-Length
146 # If httppostargs is used, we need to read Content-Length
147 # minus the amount that was consumed by args.
147 # minus the amount that was consumed by args.
148 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
148 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
149 for s in util.filechunkiter(self._req, limit=length):
149 for s in util.filechunkiter(self._req, limit=length):
150 fp.write(s)
150 fp.write(s)
151
151
152 @contextlib.contextmanager
152 @contextlib.contextmanager
153 def mayberedirectstdio(self):
153 def mayberedirectstdio(self):
154 oldout = self._ui.fout
154 oldout = self._ui.fout
155 olderr = self._ui.ferr
155 olderr = self._ui.ferr
156
156
157 out = util.stringio()
157 out = util.stringio()
158
158
159 try:
159 try:
160 self._ui.fout = out
160 self._ui.fout = out
161 self._ui.ferr = out
161 self._ui.ferr = out
162 yield out
162 yield out
163 finally:
163 finally:
164 self._ui.fout = oldout
164 self._ui.fout = oldout
165 self._ui.ferr = olderr
165 self._ui.ferr = olderr
166
166
167 def client(self):
167 def client(self):
168 return 'remote:%s:%s:%s' % (
168 return 'remote:%s:%s:%s' % (
169 self._req.env.get('wsgi.url_scheme') or 'http',
169 self._req.env.get('wsgi.url_scheme') or 'http',
170 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
170 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
171 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
171 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
172
172
173 # This method exists mostly so that extensions like remotefilelog can
173 # This method exists mostly so that extensions like remotefilelog can
174 # disable a kludgey legacy method only over http. As of early 2018,
174 # disable a kludgey legacy method only over http. As of early 2018,
175 # there are no other known users, so with any luck we can discard this
175 # there are no other known users, so with any luck we can discard this
176 # hook if remotefilelog becomes a first-party extension.
176 # hook if remotefilelog becomes a first-party extension.
177 def iscmd(cmd):
177 def iscmd(cmd):
178 return cmd in wireproto.commands
178 return cmd in wireproto.commands
179
179
180 def parsehttprequest(repo, req, query):
180 def parsehttprequest(repo, req, query):
181 """Parse the HTTP request for a wire protocol request.
181 """Parse the HTTP request for a wire protocol request.
182
182
183 If the current request appears to be a wire protocol request, this
183 If the current request appears to be a wire protocol request, this
184 function returns a dict with details about that request, including
184 function returns a dict with details about that request, including
185 an ``abstractprotocolserver`` instance suitable for handling the
185 an ``abstractprotocolserver`` instance suitable for handling the
186 request. Otherwise, ``None`` is returned.
186 request. Otherwise, ``None`` is returned.
187
187
188 ``req`` is a ``wsgirequest`` instance.
188 ``req`` is a ``wsgirequest`` instance.
189 """
189 """
190 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
190 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
191 # string parameter. If it isn't present, this isn't a wire protocol
191 # string parameter. If it isn't present, this isn't a wire protocol
192 # request.
192 # request.
193 if r'cmd' not in req.form:
193 if r'cmd' not in req.form:
194 return None
194 return None
195
195
196 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
196 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
197
197
198 # The "cmd" request parameter is used by both the wire protocol and hgweb.
198 # The "cmd" request parameter is used by both the wire protocol and hgweb.
199 # While not all wire protocol commands are available for all transports,
199 # While not all wire protocol commands are available for all transports,
200 # if we see a "cmd" value that resembles a known wire protocol command, we
200 # if we see a "cmd" value that resembles a known wire protocol command, we
201 # route it to a protocol handler. This is better than routing possible
201 # route it to a protocol handler. This is better than routing possible
202 # wire protocol requests to hgweb because it prevents hgweb from using
202 # wire protocol requests to hgweb because it prevents hgweb from using
203 # known wire protocol commands and it is less confusing for machine
203 # known wire protocol commands and it is less confusing for machine
204 # clients.
204 # clients.
205 if not iscmd(cmd):
205 if not iscmd(cmd):
206 return None
206 return None
207
207
208 proto = httpv1protocolhandler(req, repo.ui)
208 proto = httpv1protocolhandler(req, repo.ui)
209
209
210 return {
210 return {
211 'cmd': cmd,
211 'cmd': cmd,
212 'proto': proto,
212 'proto': proto,
213 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
213 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
214 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
214 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
215 }
215 }
216
216
217 def _httpresponsetype(ui, req, prefer_uncompressed):
217 def _httpresponsetype(ui, req, prefer_uncompressed):
218 """Determine the appropriate response type and compression settings.
218 """Determine the appropriate response type and compression settings.
219
219
220 Returns a tuple of (mediatype, compengine, engineopts).
220 Returns a tuple of (mediatype, compengine, engineopts).
221 """
221 """
222 # Determine the response media type and compression engine based
222 # Determine the response media type and compression engine based
223 # on the request parameters.
223 # on the request parameters.
224 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
224 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
225
225
226 if '0.2' in protocaps:
226 if '0.2' in protocaps:
227 # All clients are expected to support uncompressed data.
227 # All clients are expected to support uncompressed data.
228 if prefer_uncompressed:
228 if prefer_uncompressed:
229 return HGTYPE2, util._noopengine(), {}
229 return HGTYPE2, util._noopengine(), {}
230
230
231 # Default as defined by wire protocol spec.
231 # Default as defined by wire protocol spec.
232 compformats = ['zlib', 'none']
232 compformats = ['zlib', 'none']
233 for cap in protocaps:
233 for cap in protocaps:
234 if cap.startswith('comp='):
234 if cap.startswith('comp='):
235 compformats = cap[5:].split(',')
235 compformats = cap[5:].split(',')
236 break
236 break
237
237
238 # Now find an agreed upon compression format.
238 # Now find an agreed upon compression format.
239 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
239 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
240 if engine.wireprotosupport().name in compformats:
240 if engine.wireprotosupport().name in compformats:
241 opts = {}
241 opts = {}
242 level = ui.configint('server', '%slevel' % engine.name())
242 level = ui.configint('server', '%slevel' % engine.name())
243 if level is not None:
243 if level is not None:
244 opts['level'] = level
244 opts['level'] = level
245
245
246 return HGTYPE2, engine, opts
246 return HGTYPE2, engine, opts
247
247
248 # No mutually supported compression format. Fall back to the
248 # No mutually supported compression format. Fall back to the
249 # legacy protocol.
249 # legacy protocol.
250
250
251 # Don't allow untrusted settings because disabling compression or
251 # Don't allow untrusted settings because disabling compression or
252 # setting a very high compression level could lead to flooding
252 # setting a very high compression level could lead to flooding
253 # the server's network or CPU.
253 # the server's network or CPU.
254 opts = {'level': ui.configint('server', 'zliblevel')}
254 opts = {'level': ui.configint('server', 'zliblevel')}
255 return HGTYPE, util.compengines['zlib'], opts
255 return HGTYPE, util.compengines['zlib'], opts
256
256
257 def _callhttp(repo, req, proto, cmd):
257 def _callhttp(repo, req, proto, cmd):
258 def genversion2(gen, engine, engineopts):
258 def genversion2(gen, engine, engineopts):
259 # application/mercurial-0.2 always sends a payload header
259 # application/mercurial-0.2 always sends a payload header
260 # identifying the compression engine.
260 # identifying the compression engine.
261 name = engine.wireprotosupport().name
261 name = engine.wireprotosupport().name
262 assert 0 < len(name) < 256
262 assert 0 < len(name) < 256
263 yield struct.pack('B', len(name))
263 yield struct.pack('B', len(name))
264 yield name
264 yield name
265
265
266 for chunk in gen:
266 for chunk in gen:
267 yield chunk
267 yield chunk
268
268
269 rsp = wireproto.dispatch(repo, proto, cmd)
269 rsp = wireproto.dispatch(repo, proto, cmd)
270
270
271 if not wireproto.commands.commandavailable(cmd, proto):
271 if not wireproto.commands.commandavailable(cmd, proto):
272 req.respond(HTTP_OK, HGERRTYPE,
272 req.respond(HTTP_OK, HGERRTYPE,
273 body=_('requested wire protocol command is not available '
273 body=_('requested wire protocol command is not available '
274 'over HTTP'))
274 'over HTTP'))
275 return []
275 return []
276
276
277 if isinstance(rsp, bytes):
277 if isinstance(rsp, bytes):
278 req.respond(HTTP_OK, HGTYPE, body=rsp)
278 req.respond(HTTP_OK, HGTYPE, body=rsp)
279 return []
279 return []
280 elif isinstance(rsp, wireprototypes.bytesresponse):
280 elif isinstance(rsp, wireprototypes.bytesresponse):
281 req.respond(HTTP_OK, HGTYPE, body=rsp.data)
281 req.respond(HTTP_OK, HGTYPE, body=rsp.data)
282 return []
282 return []
283 elif isinstance(rsp, wireprototypes.streamreslegacy):
283 elif isinstance(rsp, wireprototypes.streamreslegacy):
284 gen = rsp.gen
284 gen = rsp.gen
285 req.respond(HTTP_OK, HGTYPE)
285 req.respond(HTTP_OK, HGTYPE)
286 return gen
286 return gen
287 elif isinstance(rsp, wireprototypes.streamres):
287 elif isinstance(rsp, wireprototypes.streamres):
288 gen = rsp.gen
288 gen = rsp.gen
289
289
290 # This code for compression should not be streamres specific. It
290 # This code for compression should not be streamres specific. It
291 # is here because we only compress streamres at the moment.
291 # is here because we only compress streamres at the moment.
292 mediatype, engine, engineopts = _httpresponsetype(
292 mediatype, engine, engineopts = _httpresponsetype(
293 repo.ui, req, rsp.prefer_uncompressed)
293 repo.ui, req, rsp.prefer_uncompressed)
294 gen = engine.compressstream(gen, engineopts)
294 gen = engine.compressstream(gen, engineopts)
295
295
296 if mediatype == HGTYPE2:
296 if mediatype == HGTYPE2:
297 gen = genversion2(gen, engine, engineopts)
297 gen = genversion2(gen, engine, engineopts)
298
298
299 req.respond(HTTP_OK, mediatype)
299 req.respond(HTTP_OK, mediatype)
300 return gen
300 return gen
301 elif isinstance(rsp, wireprototypes.pushres):
301 elif isinstance(rsp, wireprototypes.pushres):
302 rsp = '%d\n%s' % (rsp.res, rsp.output)
302 rsp = '%d\n%s' % (rsp.res, rsp.output)
303 req.respond(HTTP_OK, HGTYPE, body=rsp)
303 req.respond(HTTP_OK, HGTYPE, body=rsp)
304 return []
304 return []
305 elif isinstance(rsp, wireprototypes.pusherr):
305 elif isinstance(rsp, wireprototypes.pusherr):
306 # This is the httplib workaround documented in _handlehttperror().
306 # This is the httplib workaround documented in _handlehttperror().
307 req.drain()
307 req.drain()
308
308
309 rsp = '0\n%s\n' % rsp.res
309 rsp = '0\n%s\n' % rsp.res
310 req.respond(HTTP_OK, HGTYPE, body=rsp)
310 req.respond(HTTP_OK, HGTYPE, body=rsp)
311 return []
311 return []
312 elif isinstance(rsp, wireprototypes.ooberror):
312 elif isinstance(rsp, wireprototypes.ooberror):
313 rsp = rsp.message
313 rsp = rsp.message
314 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
314 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
315 return []
315 return []
316 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
316 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
317
317
318 def _handlehttperror(e, req, cmd):
318 def _handlehttperror(e, req, cmd):
319 """Called when an ErrorResponse is raised during HTTP request processing."""
319 """Called when an ErrorResponse is raised during HTTP request processing."""
320
320
321 # Clients using Python's httplib are stateful: the HTTP client
321 # Clients using Python's httplib are stateful: the HTTP client
322 # won't process an HTTP response until all request data is
322 # won't process an HTTP response until all request data is
323 # sent to the server. The intent of this code is to ensure
323 # sent to the server. The intent of this code is to ensure
324 # we always read HTTP request data from the client, thus
324 # we always read HTTP request data from the client, thus
325 # ensuring httplib transitions to a state that allows it to read
325 # ensuring httplib transitions to a state that allows it to read
326 # the HTTP response. In other words, it helps prevent deadlocks
326 # the HTTP response. In other words, it helps prevent deadlocks
327 # on clients using httplib.
327 # on clients using httplib.
328
328
329 if (req.env[r'REQUEST_METHOD'] == r'POST' and
329 if (req.env[r'REQUEST_METHOD'] == r'POST' and
330 # But not if Expect: 100-continue is being used.
330 # But not if Expect: 100-continue is being used.
331 (req.env.get('HTTP_EXPECT',
331 (req.env.get('HTTP_EXPECT',
332 '').lower() != '100-continue') or
332 '').lower() != '100-continue') or
333 # Or the non-httplib HTTP library is being advertised by
333 # Or the non-httplib HTTP library is being advertised by
334 # the client.
334 # the client.
335 req.env.get('X-HgHttp2', '')):
335 req.env.get('X-HgHttp2', '')):
336 req.drain()
336 req.drain()
337 else:
337 else:
338 req.headers.append((r'Connection', r'Close'))
338 req.headers.append((r'Connection', r'Close'))
339
339
340 # TODO This response body assumes the failed command was
340 # TODO This response body assumes the failed command was
341 # "unbundle." That assumption is not always valid.
341 # "unbundle." That assumption is not always valid.
342 req.respond(e, HGTYPE, body='0\n%s\n' % e)
342 req.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
343
343
344 return ''
344 return ''
345
345
346 def _sshv1respondbytes(fout, value):
346 def _sshv1respondbytes(fout, value):
347 """Send a bytes response for protocol version 1."""
347 """Send a bytes response for protocol version 1."""
348 fout.write('%d\n' % len(value))
348 fout.write('%d\n' % len(value))
349 fout.write(value)
349 fout.write(value)
350 fout.flush()
350 fout.flush()
351
351
352 def _sshv1respondstream(fout, source):
352 def _sshv1respondstream(fout, source):
353 write = fout.write
353 write = fout.write
354 for chunk in source.gen:
354 for chunk in source.gen:
355 write(chunk)
355 write(chunk)
356 fout.flush()
356 fout.flush()
357
357
358 def _sshv1respondooberror(fout, ferr, rsp):
358 def _sshv1respondooberror(fout, ferr, rsp):
359 ferr.write(b'%s\n-\n' % rsp)
359 ferr.write(b'%s\n-\n' % rsp)
360 ferr.flush()
360 ferr.flush()
361 fout.write(b'\n')
361 fout.write(b'\n')
362 fout.flush()
362 fout.flush()
363
363
364 class sshv1protocolhandler(baseprotocolhandler):
364 class sshv1protocolhandler(baseprotocolhandler):
365 """Handler for requests services via version 1 of SSH protocol."""
365 """Handler for requests services via version 1 of SSH protocol."""
366 def __init__(self, ui, fin, fout):
366 def __init__(self, ui, fin, fout):
367 self._ui = ui
367 self._ui = ui
368 self._fin = fin
368 self._fin = fin
369 self._fout = fout
369 self._fout = fout
370
370
371 @property
371 @property
372 def name(self):
372 def name(self):
373 return SSHV1
373 return SSHV1
374
374
375 def getargs(self, args):
375 def getargs(self, args):
376 data = {}
376 data = {}
377 keys = args.split()
377 keys = args.split()
378 for n in xrange(len(keys)):
378 for n in xrange(len(keys)):
379 argline = self._fin.readline()[:-1]
379 argline = self._fin.readline()[:-1]
380 arg, l = argline.split()
380 arg, l = argline.split()
381 if arg not in keys:
381 if arg not in keys:
382 raise error.Abort(_("unexpected parameter %r") % arg)
382 raise error.Abort(_("unexpected parameter %r") % arg)
383 if arg == '*':
383 if arg == '*':
384 star = {}
384 star = {}
385 for k in xrange(int(l)):
385 for k in xrange(int(l)):
386 argline = self._fin.readline()[:-1]
386 argline = self._fin.readline()[:-1]
387 arg, l = argline.split()
387 arg, l = argline.split()
388 val = self._fin.read(int(l))
388 val = self._fin.read(int(l))
389 star[arg] = val
389 star[arg] = val
390 data['*'] = star
390 data['*'] = star
391 else:
391 else:
392 val = self._fin.read(int(l))
392 val = self._fin.read(int(l))
393 data[arg] = val
393 data[arg] = val
394 return [data[k] for k in keys]
394 return [data[k] for k in keys]
395
395
396 def forwardpayload(self, fpout):
396 def forwardpayload(self, fpout):
397 # The file is in the form:
397 # The file is in the form:
398 #
398 #
399 # <chunk size>\n<chunk>
399 # <chunk size>\n<chunk>
400 # ...
400 # ...
401 # 0\n
401 # 0\n
402 _sshv1respondbytes(self._fout, b'')
402 _sshv1respondbytes(self._fout, b'')
403 count = int(self._fin.readline())
403 count = int(self._fin.readline())
404 while count:
404 while count:
405 fpout.write(self._fin.read(count))
405 fpout.write(self._fin.read(count))
406 count = int(self._fin.readline())
406 count = int(self._fin.readline())
407
407
408 @contextlib.contextmanager
408 @contextlib.contextmanager
409 def mayberedirectstdio(self):
409 def mayberedirectstdio(self):
410 yield None
410 yield None
411
411
412 def client(self):
412 def client(self):
413 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
413 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
414 return 'remote:ssh:' + client
414 return 'remote:ssh:' + client
415
415
416 class sshv2protocolhandler(sshv1protocolhandler):
416 class sshv2protocolhandler(sshv1protocolhandler):
417 """Protocol handler for version 2 of the SSH protocol."""
417 """Protocol handler for version 2 of the SSH protocol."""
418
418
419 def _runsshserver(ui, repo, fin, fout):
419 def _runsshserver(ui, repo, fin, fout):
420 # This function operates like a state machine of sorts. The following
420 # This function operates like a state machine of sorts. The following
421 # states are defined:
421 # states are defined:
422 #
422 #
423 # protov1-serving
423 # protov1-serving
424 # Server is in protocol version 1 serving mode. Commands arrive on
424 # Server is in protocol version 1 serving mode. Commands arrive on
425 # new lines. These commands are processed in this state, one command
425 # new lines. These commands are processed in this state, one command
426 # after the other.
426 # after the other.
427 #
427 #
428 # protov2-serving
428 # protov2-serving
429 # Server is in protocol version 2 serving mode.
429 # Server is in protocol version 2 serving mode.
430 #
430 #
431 # upgrade-initial
431 # upgrade-initial
432 # The server is going to process an upgrade request.
432 # The server is going to process an upgrade request.
433 #
433 #
434 # upgrade-v2-filter-legacy-handshake
434 # upgrade-v2-filter-legacy-handshake
435 # The protocol is being upgraded to version 2. The server is expecting
435 # The protocol is being upgraded to version 2. The server is expecting
436 # the legacy handshake from version 1.
436 # the legacy handshake from version 1.
437 #
437 #
438 # upgrade-v2-finish
438 # upgrade-v2-finish
439 # The upgrade to version 2 of the protocol is imminent.
439 # The upgrade to version 2 of the protocol is imminent.
440 #
440 #
441 # shutdown
441 # shutdown
442 # The server is shutting down, possibly in reaction to a client event.
442 # The server is shutting down, possibly in reaction to a client event.
443 #
443 #
444 # And here are their transitions:
444 # And here are their transitions:
445 #
445 #
446 # protov1-serving -> shutdown
446 # protov1-serving -> shutdown
447 # When server receives an empty request or encounters another
447 # When server receives an empty request or encounters another
448 # error.
448 # error.
449 #
449 #
450 # protov1-serving -> upgrade-initial
450 # protov1-serving -> upgrade-initial
451 # An upgrade request line was seen.
451 # An upgrade request line was seen.
452 #
452 #
453 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
453 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
454 # Upgrade to version 2 in progress. Server is expecting to
454 # Upgrade to version 2 in progress. Server is expecting to
455 # process a legacy handshake.
455 # process a legacy handshake.
456 #
456 #
457 # upgrade-v2-filter-legacy-handshake -> shutdown
457 # upgrade-v2-filter-legacy-handshake -> shutdown
458 # Client did not fulfill upgrade handshake requirements.
458 # Client did not fulfill upgrade handshake requirements.
459 #
459 #
460 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
460 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
461 # Client fulfilled version 2 upgrade requirements. Finishing that
461 # Client fulfilled version 2 upgrade requirements. Finishing that
462 # upgrade.
462 # upgrade.
463 #
463 #
464 # upgrade-v2-finish -> protov2-serving
464 # upgrade-v2-finish -> protov2-serving
465 # Protocol upgrade to version 2 complete. Server can now speak protocol
465 # Protocol upgrade to version 2 complete. Server can now speak protocol
466 # version 2.
466 # version 2.
467 #
467 #
468 # protov2-serving -> protov1-serving
468 # protov2-serving -> protov1-serving
469 # Ths happens by default since protocol version 2 is the same as
469 # Ths happens by default since protocol version 2 is the same as
470 # version 1 except for the handshake.
470 # version 1 except for the handshake.
471
471
472 state = 'protov1-serving'
472 state = 'protov1-serving'
473 proto = sshv1protocolhandler(ui, fin, fout)
473 proto = sshv1protocolhandler(ui, fin, fout)
474 protoswitched = False
474 protoswitched = False
475
475
476 while True:
476 while True:
477 if state == 'protov1-serving':
477 if state == 'protov1-serving':
478 # Commands are issued on new lines.
478 # Commands are issued on new lines.
479 request = fin.readline()[:-1]
479 request = fin.readline()[:-1]
480
480
481 # Empty lines signal to terminate the connection.
481 # Empty lines signal to terminate the connection.
482 if not request:
482 if not request:
483 state = 'shutdown'
483 state = 'shutdown'
484 continue
484 continue
485
485
486 # It looks like a protocol upgrade request. Transition state to
486 # It looks like a protocol upgrade request. Transition state to
487 # handle it.
487 # handle it.
488 if request.startswith(b'upgrade '):
488 if request.startswith(b'upgrade '):
489 if protoswitched:
489 if protoswitched:
490 _sshv1respondooberror(fout, ui.ferr,
490 _sshv1respondooberror(fout, ui.ferr,
491 b'cannot upgrade protocols multiple '
491 b'cannot upgrade protocols multiple '
492 b'times')
492 b'times')
493 state = 'shutdown'
493 state = 'shutdown'
494 continue
494 continue
495
495
496 state = 'upgrade-initial'
496 state = 'upgrade-initial'
497 continue
497 continue
498
498
499 available = wireproto.commands.commandavailable(request, proto)
499 available = wireproto.commands.commandavailable(request, proto)
500
500
501 # This command isn't available. Send an empty response and go
501 # This command isn't available. Send an empty response and go
502 # back to waiting for a new command.
502 # back to waiting for a new command.
503 if not available:
503 if not available:
504 _sshv1respondbytes(fout, b'')
504 _sshv1respondbytes(fout, b'')
505 continue
505 continue
506
506
507 rsp = wireproto.dispatch(repo, proto, request)
507 rsp = wireproto.dispatch(repo, proto, request)
508
508
509 if isinstance(rsp, bytes):
509 if isinstance(rsp, bytes):
510 _sshv1respondbytes(fout, rsp)
510 _sshv1respondbytes(fout, rsp)
511 elif isinstance(rsp, wireprototypes.bytesresponse):
511 elif isinstance(rsp, wireprototypes.bytesresponse):
512 _sshv1respondbytes(fout, rsp.data)
512 _sshv1respondbytes(fout, rsp.data)
513 elif isinstance(rsp, wireprototypes.streamres):
513 elif isinstance(rsp, wireprototypes.streamres):
514 _sshv1respondstream(fout, rsp)
514 _sshv1respondstream(fout, rsp)
515 elif isinstance(rsp, wireprototypes.streamreslegacy):
515 elif isinstance(rsp, wireprototypes.streamreslegacy):
516 _sshv1respondstream(fout, rsp)
516 _sshv1respondstream(fout, rsp)
517 elif isinstance(rsp, wireprototypes.pushres):
517 elif isinstance(rsp, wireprototypes.pushres):
518 _sshv1respondbytes(fout, b'')
518 _sshv1respondbytes(fout, b'')
519 _sshv1respondbytes(fout, b'%d' % rsp.res)
519 _sshv1respondbytes(fout, b'%d' % rsp.res)
520 elif isinstance(rsp, wireprototypes.pusherr):
520 elif isinstance(rsp, wireprototypes.pusherr):
521 _sshv1respondbytes(fout, rsp.res)
521 _sshv1respondbytes(fout, rsp.res)
522 elif isinstance(rsp, wireprototypes.ooberror):
522 elif isinstance(rsp, wireprototypes.ooberror):
523 _sshv1respondooberror(fout, ui.ferr, rsp.message)
523 _sshv1respondooberror(fout, ui.ferr, rsp.message)
524 else:
524 else:
525 raise error.ProgrammingError('unhandled response type from '
525 raise error.ProgrammingError('unhandled response type from '
526 'wire protocol command: %s' % rsp)
526 'wire protocol command: %s' % rsp)
527
527
528 # For now, protocol version 2 serving just goes back to version 1.
528 # For now, protocol version 2 serving just goes back to version 1.
529 elif state == 'protov2-serving':
529 elif state == 'protov2-serving':
530 state = 'protov1-serving'
530 state = 'protov1-serving'
531 continue
531 continue
532
532
533 elif state == 'upgrade-initial':
533 elif state == 'upgrade-initial':
534 # We should never transition into this state if we've switched
534 # We should never transition into this state if we've switched
535 # protocols.
535 # protocols.
536 assert not protoswitched
536 assert not protoswitched
537 assert proto.name == SSHV1
537 assert proto.name == SSHV1
538
538
539 # Expected: upgrade <token> <capabilities>
539 # Expected: upgrade <token> <capabilities>
540 # If we get something else, the request is malformed. It could be
540 # If we get something else, the request is malformed. It could be
541 # from a future client that has altered the upgrade line content.
541 # from a future client that has altered the upgrade line content.
542 # We treat this as an unknown command.
542 # We treat this as an unknown command.
543 try:
543 try:
544 token, caps = request.split(b' ')[1:]
544 token, caps = request.split(b' ')[1:]
545 except ValueError:
545 except ValueError:
546 _sshv1respondbytes(fout, b'')
546 _sshv1respondbytes(fout, b'')
547 state = 'protov1-serving'
547 state = 'protov1-serving'
548 continue
548 continue
549
549
550 # Send empty response if we don't support upgrading protocols.
550 # Send empty response if we don't support upgrading protocols.
551 if not ui.configbool('experimental', 'sshserver.support-v2'):
551 if not ui.configbool('experimental', 'sshserver.support-v2'):
552 _sshv1respondbytes(fout, b'')
552 _sshv1respondbytes(fout, b'')
553 state = 'protov1-serving'
553 state = 'protov1-serving'
554 continue
554 continue
555
555
556 try:
556 try:
557 caps = urlreq.parseqs(caps)
557 caps = urlreq.parseqs(caps)
558 except ValueError:
558 except ValueError:
559 _sshv1respondbytes(fout, b'')
559 _sshv1respondbytes(fout, b'')
560 state = 'protov1-serving'
560 state = 'protov1-serving'
561 continue
561 continue
562
562
563 # We don't see an upgrade request to protocol version 2. Ignore
563 # We don't see an upgrade request to protocol version 2. Ignore
564 # the upgrade request.
564 # the upgrade request.
565 wantedprotos = caps.get(b'proto', [b''])[0]
565 wantedprotos = caps.get(b'proto', [b''])[0]
566 if SSHV2 not in wantedprotos:
566 if SSHV2 not in wantedprotos:
567 _sshv1respondbytes(fout, b'')
567 _sshv1respondbytes(fout, b'')
568 state = 'protov1-serving'
568 state = 'protov1-serving'
569 continue
569 continue
570
570
571 # It looks like we can honor this upgrade request to protocol 2.
571 # It looks like we can honor this upgrade request to protocol 2.
572 # Filter the rest of the handshake protocol request lines.
572 # Filter the rest of the handshake protocol request lines.
573 state = 'upgrade-v2-filter-legacy-handshake'
573 state = 'upgrade-v2-filter-legacy-handshake'
574 continue
574 continue
575
575
576 elif state == 'upgrade-v2-filter-legacy-handshake':
576 elif state == 'upgrade-v2-filter-legacy-handshake':
577 # Client should have sent legacy handshake after an ``upgrade``
577 # Client should have sent legacy handshake after an ``upgrade``
578 # request. Expected lines:
578 # request. Expected lines:
579 #
579 #
580 # hello
580 # hello
581 # between
581 # between
582 # pairs 81
582 # pairs 81
583 # 0000...-0000...
583 # 0000...-0000...
584
584
585 ok = True
585 ok = True
586 for line in (b'hello', b'between', b'pairs 81'):
586 for line in (b'hello', b'between', b'pairs 81'):
587 request = fin.readline()[:-1]
587 request = fin.readline()[:-1]
588
588
589 if request != line:
589 if request != line:
590 _sshv1respondooberror(fout, ui.ferr,
590 _sshv1respondooberror(fout, ui.ferr,
591 b'malformed handshake protocol: '
591 b'malformed handshake protocol: '
592 b'missing %s' % line)
592 b'missing %s' % line)
593 ok = False
593 ok = False
594 state = 'shutdown'
594 state = 'shutdown'
595 break
595 break
596
596
597 if not ok:
597 if not ok:
598 continue
598 continue
599
599
600 request = fin.read(81)
600 request = fin.read(81)
601 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
601 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
602 _sshv1respondooberror(fout, ui.ferr,
602 _sshv1respondooberror(fout, ui.ferr,
603 b'malformed handshake protocol: '
603 b'malformed handshake protocol: '
604 b'missing between argument value')
604 b'missing between argument value')
605 state = 'shutdown'
605 state = 'shutdown'
606 continue
606 continue
607
607
608 state = 'upgrade-v2-finish'
608 state = 'upgrade-v2-finish'
609 continue
609 continue
610
610
611 elif state == 'upgrade-v2-finish':
611 elif state == 'upgrade-v2-finish':
612 # Send the upgrade response.
612 # Send the upgrade response.
613 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
613 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
614 servercaps = wireproto.capabilities(repo, proto)
614 servercaps = wireproto.capabilities(repo, proto)
615 rsp = b'capabilities: %s' % servercaps.data
615 rsp = b'capabilities: %s' % servercaps.data
616 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
616 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
617 fout.flush()
617 fout.flush()
618
618
619 proto = sshv2protocolhandler(ui, fin, fout)
619 proto = sshv2protocolhandler(ui, fin, fout)
620 protoswitched = True
620 protoswitched = True
621
621
622 state = 'protov2-serving'
622 state = 'protov2-serving'
623 continue
623 continue
624
624
625 elif state == 'shutdown':
625 elif state == 'shutdown':
626 break
626 break
627
627
628 else:
628 else:
629 raise error.ProgrammingError('unhandled ssh server state: %s' %
629 raise error.ProgrammingError('unhandled ssh server state: %s' %
630 state)
630 state)
631
631
632 class sshserver(object):
632 class sshserver(object):
633 def __init__(self, ui, repo):
633 def __init__(self, ui, repo):
634 self._ui = ui
634 self._ui = ui
635 self._repo = repo
635 self._repo = repo
636 self._fin = ui.fin
636 self._fin = ui.fin
637 self._fout = ui.fout
637 self._fout = ui.fout
638
638
639 hook.redirect(True)
639 hook.redirect(True)
640 ui.fout = repo.ui.fout = ui.ferr
640 ui.fout = repo.ui.fout = ui.ferr
641
641
642 # Prevent insertion/deletion of CRs
642 # Prevent insertion/deletion of CRs
643 util.setbinary(self._fin)
643 util.setbinary(self._fin)
644 util.setbinary(self._fout)
644 util.setbinary(self._fout)
645
645
646 def serve_forever(self):
646 def serve_forever(self):
647 _runsshserver(self._ui, self._repo, self._fin, self._fout)
647 _runsshserver(self._ui, self._repo, self._fin, self._fout)
648 sys.exit(0)
648 sys.exit(0)
General Comments 0
You need to be logged in to leave comments. Login now