##// END OF EJS Templates
vcsserver: added streaming interface for streaming remote attributes
dan -
r768:304a5413 default
parent child Browse files
Show More
@@ -1,624 +1,667 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import sys
20 20 import base64
21 21 import locale
22 22 import logging
23 23 import uuid
24 24 import wsgiref.util
25 25 import traceback
26 26 import tempfile
27 27 from itertools import chain
28 from cStringIO import StringIO
28 29
29 30 import simplejson as json
30 31 import msgpack
31 32 from pyramid.config import Configurator
32 33 from pyramid.settings import asbool, aslist
33 34 from pyramid.wsgi import wsgiapp
34 35 from pyramid.compat import configparser
36 from pyramid.response import Response
35 37
38 from vcsserver.utils import safe_int
36 39
37 40 log = logging.getLogger(__name__)
38 41
39 42 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
40 43 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
41 44
42 45 try:
43 46 locale.setlocale(locale.LC_ALL, '')
44 47 except locale.Error as e:
45 48 log.error(
46 49 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
47 50 os.environ['LC_ALL'] = 'C'
48 51
49 52 import vcsserver
50 53 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
51 54 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
52 55 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
53 56 from vcsserver.echo_stub.echo_app import EchoApp
54 57 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
55 58 from vcsserver.lib.exc_tracking import store_exception
56 59 from vcsserver.server import VcsServer
57 60
58 61 try:
59 62 from vcsserver.git import GitFactory, GitRemote
60 63 except ImportError:
61 64 GitFactory = None
62 65 GitRemote = None
63 66
64 67 try:
65 68 from vcsserver.hg import MercurialFactory, HgRemote
66 69 except ImportError:
67 70 MercurialFactory = None
68 71 HgRemote = None
69 72
70 73 try:
71 74 from vcsserver.svn import SubversionFactory, SvnRemote
72 75 except ImportError:
73 76 SubversionFactory = None
74 77 SvnRemote = None
75 78
76 79
77 80 def _is_request_chunked(environ):
78 81 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
79 82 return stream
80 83
81 84
82 85 def _int_setting(settings, name, default):
83 86 settings[name] = int(settings.get(name, default))
84 87 return settings[name]
85 88
86 89
87 90 def _bool_setting(settings, name, default):
88 91 input_val = settings.get(name, default)
89 92 if isinstance(input_val, unicode):
90 93 input_val = input_val.encode('utf8')
91 94 settings[name] = asbool(input_val)
92 95 return settings[name]
93 96
94 97
95 98 def _list_setting(settings, name, default):
96 99 raw_value = settings.get(name, default)
97 100
98 101 # Otherwise we assume it uses pyramids space/newline separation.
99 102 settings[name] = aslist(raw_value)
100 103 return settings[name]
101 104
102 105
103 106 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
104 107 value = settings.get(name, default)
105 108
106 109 if default_when_empty and not value:
107 110 # use default value when value is empty
108 111 value = default
109 112
110 113 if lower:
111 114 value = value.lower()
112 115 settings[name] = value
113 116 return settings[name]
114 117
115 118
116 119 class VCS(object):
117 def __init__(self, locale=None, cache_config=None):
118 self.locale = locale
120 def __init__(self, locale_conf=None, cache_config=None):
121 self.locale = locale_conf
119 122 self.cache_config = cache_config
120 123 self._configure_locale()
121 124
122 125 if GitFactory and GitRemote:
123 126 git_factory = GitFactory()
124 127 self._git_remote = GitRemote(git_factory)
125 128 else:
126 129 log.info("Git client import failed")
127 130
128 131 if MercurialFactory and HgRemote:
129 132 hg_factory = MercurialFactory()
130 133 self._hg_remote = HgRemote(hg_factory)
131 134 else:
132 135 log.info("Mercurial client import failed")
133 136
134 137 if SubversionFactory and SvnRemote:
135 138 svn_factory = SubversionFactory()
136 139
137 140 # hg factory is used for svn url validation
138 141 hg_factory = MercurialFactory()
139 142 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
140 143 else:
141 144 log.info("Subversion client import failed")
142 145
143 146 self._vcsserver = VcsServer()
144 147
145 148 def _configure_locale(self):
146 149 if self.locale:
147 150 log.info('Settings locale: `LC_ALL` to %s', self.locale)
148 151 else:
149 152 log.info(
150 153 'Configuring locale subsystem based on environment variables')
151 154 try:
152 155 # If self.locale is the empty string, then the locale
153 156 # module will use the environment variables. See the
154 157 # documentation of the package `locale`.
155 158 locale.setlocale(locale.LC_ALL, self.locale)
156 159
157 160 language_code, encoding = locale.getlocale()
158 161 log.info(
159 162 'Locale set to language code "%s" with encoding "%s".',
160 163 language_code, encoding)
161 164 except locale.Error:
162 165 log.exception(
163 166 'Cannot set locale, not configuring the locale system')
164 167
165 168
166 169 class WsgiProxy(object):
167 170 def __init__(self, wsgi):
168 171 self.wsgi = wsgi
169 172
170 173 def __call__(self, environ, start_response):
171 174 input_data = environ['wsgi.input'].read()
172 175 input_data = msgpack.unpackb(input_data)
173 176
174 177 error = None
175 178 try:
176 179 data, status, headers = self.wsgi.handle(
177 180 input_data['environment'], input_data['input_data'],
178 181 *input_data['args'], **input_data['kwargs'])
179 182 except Exception as e:
180 183 data, status, headers = [], None, None
181 184 error = {
182 185 'message': str(e),
183 186 '_vcs_kind': getattr(e, '_vcs_kind', None)
184 187 }
185 188
186 189 start_response(200, {})
187 190 return self._iterator(error, status, headers, data)
188 191
189 192 def _iterator(self, error, status, headers, data):
190 193 initial_data = [
191 194 error,
192 195 status,
193 196 headers,
194 197 ]
195 198
196 199 for d in chain(initial_data, data):
197 200 yield msgpack.packb(d)
198 201
199 202
200 203 def not_found(request):
201 204 return {'status': '404 NOT FOUND'}
202 205
203 206
204 207 class VCSViewPredicate(object):
205 208 def __init__(self, val, config):
206 209 self.remotes = val
207 210
208 211 def text(self):
209 212 return 'vcs view method = %s' % (self.remotes.keys(),)
210 213
211 214 phash = text
212 215
213 216 def __call__(self, context, request):
214 217 """
215 218 View predicate that returns true if given backend is supported by
216 219 defined remotes.
217 220 """
218 221 backend = request.matchdict.get('backend')
219 222 return backend in self.remotes
220 223
221 224
222 225 class HTTPApplication(object):
223 226 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
224 227
225 228 remote_wsgi = remote_wsgi
226 229 _use_echo_app = False
227 230
228 231 def __init__(self, settings=None, global_config=None):
229 232 self._sanitize_settings_and_apply_defaults(settings)
230 233
231 234 self.config = Configurator(settings=settings)
232 235 self.global_config = global_config
233 236 self.config.include('vcsserver.lib.rc_cache')
234 237
235 238 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
236 vcs = VCS(locale=settings_locale, cache_config=settings)
239 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
237 240 self._remotes = {
238 241 'hg': vcs._hg_remote,
239 242 'git': vcs._git_remote,
240 243 'svn': vcs._svn_remote,
241 244 'server': vcs._vcsserver,
242 245 }
243 246 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
244 247 self._use_echo_app = True
245 248 log.warning("Using EchoApp for VCS operations.")
246 249 self.remote_wsgi = remote_wsgi_stub
247 250
248 251 self._configure_settings(global_config, settings)
249 252 self._configure()
250 253
251 254 def _configure_settings(self, global_config, app_settings):
252 255 """
253 256 Configure the settings module.
254 257 """
255 258 settings_merged = global_config.copy()
256 259 settings_merged.update(app_settings)
257 260
258 261 git_path = app_settings.get('git_path', None)
259 262 if git_path:
260 263 settings.GIT_EXECUTABLE = git_path
261 264 binary_dir = app_settings.get('core.binary_dir', None)
262 265 if binary_dir:
263 266 settings.BINARY_DIR = binary_dir
264 267
265 268 # Store the settings to make them available to other modules.
266 269 vcsserver.PYRAMID_SETTINGS = settings_merged
267 270 vcsserver.CONFIG = settings_merged
268 271
269 272 def _sanitize_settings_and_apply_defaults(self, settings):
270 273 temp_store = tempfile.gettempdir()
271 274 default_cache_dir = os.path.join(temp_store, 'rc_cache')
272 275
273 276 # save default, cache dir, and use it for all backends later.
274 277 default_cache_dir = _string_setting(
275 278 settings,
276 279 'cache_dir',
277 280 default_cache_dir, lower=False, default_when_empty=True)
278 281
279 282 # ensure we have our dir created
280 283 if not os.path.isdir(default_cache_dir):
281 284 os.makedirs(default_cache_dir, mode=0o755)
282 285
283 286 # exception store cache
284 287 _string_setting(
285 288 settings,
286 289 'exception_tracker.store_path',
287 290 temp_store, lower=False, default_when_empty=True)
288 291
289 292 # repo_object cache
290 293 _string_setting(
291 294 settings,
292 295 'rc_cache.repo_object.backend',
293 296 'dogpile.cache.rc.memory_lru')
294 297 _int_setting(
295 298 settings,
296 299 'rc_cache.repo_object.expiration_time',
297 300 300)
298 301 _int_setting(
299 302 settings,
300 303 'rc_cache.repo_object.max_size',
301 304 1024)
302 305
303 306 def _configure(self):
304 307 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
305 308
306 309 self.config.add_route('service', '/_service')
307 310 self.config.add_route('status', '/status')
308 311 self.config.add_route('hg_proxy', '/proxy/hg')
309 312 self.config.add_route('git_proxy', '/proxy/git')
313
314 # rpc methods
310 315 self.config.add_route('vcs', '/{backend}')
316
317 # streaming rpc remote methods
318 self.config.add_route('vcs_stream', '/{backend}/stream')
319
320 # vcs operations clone/push as streaming
311 321 self.config.add_route('stream_git', '/stream/git/*repo_name')
312 322 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
313 323
314 324 self.config.add_view(self.status_view, route_name='status', renderer='json')
315 325 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
316 326
317 327 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
318 328 self.config.add_view(self.git_proxy(), route_name='git_proxy')
319 329 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
320 330 vcs_view=self._remotes)
331 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
332 vcs_view=self._remotes)
321 333
322 334 self.config.add_view(self.hg_stream(), route_name='stream_hg')
323 335 self.config.add_view(self.git_stream(), route_name='stream_git')
324 336
325 337 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
326 338
327 339 self.config.add_notfound_view(not_found, renderer='json')
328 340
329 341 self.config.add_view(self.handle_vcs_exception, context=Exception)
330 342
331 343 self.config.add_tween(
332 344 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
333 345 )
334 346 self.config.add_request_method(
335 347 'vcsserver.lib.request_counter.get_request_counter',
336 348 'request_count')
337 349
338 350 def wsgi_app(self):
339 351 return self.config.make_wsgi_app()
340 352
341 def vcs_view(self, request):
353 def _vcs_view_params(self, request):
342 354 remote = self._remotes[request.matchdict['backend']]
343 355 payload = msgpack.unpackb(request.body, use_list=True)
344 356 method = payload.get('method')
345 params = payload.get('params')
357 params = payload['params']
346 358 wire = params.get('wire')
347 359 args = params.get('args')
348 360 kwargs = params.get('kwargs')
349 361 context_uid = None
350 362
351 363 if wire:
352 364 try:
353 365 wire['context'] = context_uid = uuid.UUID(wire['context'])
354 366 except KeyError:
355 367 pass
356 368 args.insert(0, wire)
369 repo_state_uid = wire.get('repo_state_uid') if wire else None
357 370
358 371 # NOTE(marcink): trading complexity for slight performance
359 372 if log.isEnabledFor(logging.DEBUG):
360 373 no_args_methods = [
361 374 'archive_repo'
362 375 ]
363 376 if method in no_args_methods:
364 377 call_args = ''
365 378 else:
366 379 call_args = args[1:]
367 380
368 repo_state_uid = wire.get('repo_state_uid') if wire else None
369 log.debug('method called:%s with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
381 log.debug('method requested:%s with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
370 382 method, call_args, kwargs, context_uid, repo_state_uid)
371 383
384 return payload, remote, method, args, kwargs
385
386 def vcs_view(self, request):
387
388 payload, remote, method, args, kwargs = self._vcs_view_params(request)
389 payload_id = payload.get('id')
390
372 391 try:
373 392 resp = getattr(remote, method)(*args, **kwargs)
374 393 except Exception as e:
375 394 exc_info = list(sys.exc_info())
376 395 exc_type, exc_value, exc_traceback = exc_info
377 396
378 397 org_exc = getattr(e, '_org_exc', None)
379 398 org_exc_name = None
380 399 org_exc_tb = ''
381 400 if org_exc:
382 401 org_exc_name = org_exc.__class__.__name__
383 402 org_exc_tb = getattr(e, '_org_exc_tb', '')
384 403 # replace our "faked" exception with our org
385 404 exc_info[0] = org_exc.__class__
386 405 exc_info[1] = org_exc
387 406
388 407 store_exception(id(exc_info), exc_info)
389 408
390 409 tb_info = ''.join(
391 410 traceback.format_exception(exc_type, exc_value, exc_traceback))
392 411
393 412 type_ = e.__class__.__name__
394 413 if type_ not in self.ALLOWED_EXCEPTIONS:
395 414 type_ = None
396 415
397 416 resp = {
398 'id': payload.get('id'),
417 'id': payload_id,
399 418 'error': {
400 419 'message': e.message,
401 420 'traceback': tb_info,
402 421 'org_exc': org_exc_name,
403 422 'org_exc_tb': org_exc_tb,
404 423 'type': type_
405 424 }
406 425 }
407 426 try:
408 427 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
409 428 except AttributeError:
410 429 pass
411 430 else:
412 431 resp = {
413 'id': payload.get('id'),
432 'id': payload_id,
414 433 'result': resp
415 434 }
416 435
417 436 return resp
418 437
438 def vcs_stream_view(self, request):
439 payload, remote, method, args, kwargs = self._vcs_view_params(request)
440 # this method has a stream: marker we remove it here
441 method = method.split('stream:')[-1]
442 chunk_size = safe_int(payload.get('chunk_size')) or 4096
443
444 try:
445 resp = getattr(remote, method)(*args, **kwargs)
446 except Exception as e:
447 raise
448
449 def get_chunked_data(method_resp):
450 stream = StringIO(method_resp)
451 while 1:
452 chunk = stream.read(chunk_size)
453 if not chunk:
454 break
455 yield chunk
456
457 response = Response(app_iter=get_chunked_data(resp))
458 response.content_type = 'application/octet-stream'
459
460 return response
461
419 462 def status_view(self, request):
420 463 import vcsserver
421 464 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
422 465 'pid': os.getpid()}
423 466
424 467 def service_view(self, request):
425 468 import vcsserver
426 469
427 470 payload = msgpack.unpackb(request.body, use_list=True)
428 471
429 472 try:
430 473 path = self.global_config['__file__']
431 474 config = configparser.ConfigParser()
432 475 config.read(path)
433 476 parsed_ini = config
434 477 if parsed_ini.has_section('server:main'):
435 478 parsed_ini = dict(parsed_ini.items('server:main'))
436 479 except Exception:
437 480 log.exception('Failed to read .ini file for display')
438 481 parsed_ini = {}
439 482
440 483 resp = {
441 484 'id': payload.get('id'),
442 485 'result': dict(
443 486 version=vcsserver.__version__,
444 487 config=parsed_ini,
445 488 payload=payload,
446 489 )
447 490 }
448 491 return resp
449 492
450 493 def _msgpack_renderer_factory(self, info):
451 494 def _render(value, system):
452 495 request = system.get('request')
453 496 if request is not None:
454 497 response = request.response
455 498 ct = response.content_type
456 499 if ct == response.default_content_type:
457 500 response.content_type = 'application/x-msgpack'
458 501 return msgpack.packb(value)
459 502 return _render
460 503
461 504 def set_env_from_config(self, environ, config):
462 505 dict_conf = {}
463 506 try:
464 507 for elem in config:
465 508 if elem[0] == 'rhodecode':
466 509 dict_conf = json.loads(elem[2])
467 510 break
468 511 except Exception:
469 512 log.exception('Failed to fetch SCM CONFIG')
470 513 return
471 514
472 515 username = dict_conf.get('username')
473 516 if username:
474 517 environ['REMOTE_USER'] = username
475 518 # mercurial specific, some extension api rely on this
476 519 environ['HGUSER'] = username
477 520
478 521 ip = dict_conf.get('ip')
479 522 if ip:
480 523 environ['REMOTE_HOST'] = ip
481 524
482 525 if _is_request_chunked(environ):
483 526 # set the compatibility flag for webob
484 527 environ['wsgi.input_terminated'] = True
485 528
486 529 def hg_proxy(self):
487 530 @wsgiapp
488 531 def _hg_proxy(environ, start_response):
489 532 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
490 533 return app(environ, start_response)
491 534 return _hg_proxy
492 535
493 536 def git_proxy(self):
494 537 @wsgiapp
495 538 def _git_proxy(environ, start_response):
496 539 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
497 540 return app(environ, start_response)
498 541 return _git_proxy
499 542
500 543 def hg_stream(self):
501 544 if self._use_echo_app:
502 545 @wsgiapp
503 546 def _hg_stream(environ, start_response):
504 547 app = EchoApp('fake_path', 'fake_name', None)
505 548 return app(environ, start_response)
506 549 return _hg_stream
507 550 else:
508 551 @wsgiapp
509 552 def _hg_stream(environ, start_response):
510 553 log.debug('http-app: handling hg stream')
511 554 repo_path = environ['HTTP_X_RC_REPO_PATH']
512 555 repo_name = environ['HTTP_X_RC_REPO_NAME']
513 556 packed_config = base64.b64decode(
514 557 environ['HTTP_X_RC_REPO_CONFIG'])
515 558 config = msgpack.unpackb(packed_config)
516 559 app = scm_app.create_hg_wsgi_app(
517 560 repo_path, repo_name, config)
518 561
519 562 # Consistent path information for hgweb
520 563 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
521 564 environ['REPO_NAME'] = repo_name
522 565 self.set_env_from_config(environ, config)
523 566
524 567 log.debug('http-app: starting app handler '
525 568 'with %s and process request', app)
526 569 return app(environ, ResponseFilter(start_response))
527 570 return _hg_stream
528 571
529 572 def git_stream(self):
530 573 if self._use_echo_app:
531 574 @wsgiapp
532 575 def _git_stream(environ, start_response):
533 576 app = EchoApp('fake_path', 'fake_name', None)
534 577 return app(environ, start_response)
535 578 return _git_stream
536 579 else:
537 580 @wsgiapp
538 581 def _git_stream(environ, start_response):
539 582 log.debug('http-app: handling git stream')
540 583 repo_path = environ['HTTP_X_RC_REPO_PATH']
541 584 repo_name = environ['HTTP_X_RC_REPO_NAME']
542 585 packed_config = base64.b64decode(
543 586 environ['HTTP_X_RC_REPO_CONFIG'])
544 587 config = msgpack.unpackb(packed_config)
545 588
546 589 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
547 590 self.set_env_from_config(environ, config)
548 591
549 592 content_type = environ.get('CONTENT_TYPE', '')
550 593
551 594 path = environ['PATH_INFO']
552 595 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
553 596 log.debug(
554 597 'LFS: Detecting if request `%s` is LFS server path based '
555 598 'on content type:`%s`, is_lfs:%s',
556 599 path, content_type, is_lfs_request)
557 600
558 601 if not is_lfs_request:
559 602 # fallback detection by path
560 603 if GIT_LFS_PROTO_PAT.match(path):
561 604 is_lfs_request = True
562 605 log.debug(
563 606 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
564 607 path, is_lfs_request)
565 608
566 609 if is_lfs_request:
567 610 app = scm_app.create_git_lfs_wsgi_app(
568 611 repo_path, repo_name, config)
569 612 else:
570 613 app = scm_app.create_git_wsgi_app(
571 614 repo_path, repo_name, config)
572 615
573 616 log.debug('http-app: starting app handler '
574 617 'with %s and process request', app)
575 618
576 619 return app(environ, start_response)
577 620
578 621 return _git_stream
579 622
580 623 def handle_vcs_exception(self, exception, request):
581 624 _vcs_kind = getattr(exception, '_vcs_kind', '')
582 625 if _vcs_kind == 'repo_locked':
583 626 # Get custom repo-locked status code if present.
584 627 status_code = request.headers.get('X-RC-Locked-Status-Code')
585 628 return HTTPRepoLocked(
586 629 title=exception.message, status_code=status_code)
587 630
588 631 elif _vcs_kind == 'repo_branch_protected':
589 632 # Get custom repo-branch-protected status code if present.
590 633 return HTTPRepoBranchProtected(title=exception.message)
591 634
592 635 exc_info = request.exc_info
593 636 store_exception(id(exc_info), exc_info)
594 637
595 638 traceback_info = 'unavailable'
596 639 if request.exc_info:
597 640 exc_type, exc_value, exc_tb = request.exc_info
598 641 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
599 642
600 643 log.error(
601 644 'error occurred handling this request for path: %s, \n tb: %s',
602 645 request.path, traceback_info)
603 646 raise exception
604 647
605 648
606 649 class ResponseFilter(object):
607 650
608 651 def __init__(self, start_response):
609 652 self._start_response = start_response
610 653
611 654 def __call__(self, status, response_headers, exc_info=None):
612 655 headers = tuple(
613 656 (h, v) for h, v in response_headers
614 657 if not wsgiref.util.is_hop_by_hop(h))
615 658 return self._start_response(status, headers, exc_info)
616 659
617 660
618 661 def main(global_config, **settings):
619 662 if MercurialFactory:
620 663 hgpatches.patch_largefiles_capabilities()
621 664 hgpatches.patch_subrepo_type_mapping()
622 665
623 666 app = HTTPApplication(settings=settings, global_config=global_config)
624 667 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now