##// END OF EJS Templates
deps: bumped msgpack to latest release and by default use byte only protocol
super-admin -
r1069:e378f012 python3
parent child Browse files
Show More
@@ -1,53 +1,53 b''
1 1 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
2 2
3 3 atomicwrites==1.4.1
4 4 contextlib2==21.6.0
5 5 cov-core==1.15.0
6 6 coverage==7.2.1
7 7 dogpile.cache==1.1.8
8 8 decorator==5.1.1
9 9 stevedore==5.0.0
10 10 pbr==5.11.1
11 11 dulwich==0.21.3
12 12 urllib3==1.26.14
13 13 gunicorn==20.1.0
14 14 hg-evolve==11.0.0
15 15 importlib-metadata==6.0.0
16 16 zipp==3.15.0
17 17 mercurial==6.3.3
18 18 mock==5.0.1
19 19 more-itertools==9.1.0
20 msgpack-python==0.5.6
20 msgpack==1.0.5
21 21 orjson==3.8.7
22 22 psutil==5.9.4
23 23 py==1.11.0
24 24 pygit2==1.11.1
25 25 cffi==1.15.1
26 26 pycparser==2.21
27 27 pygments==2.14.0
28 28 pyparsing==3.0.9
29 29 pyramid==2.0.1
30 30 hupper==1.11
31 31 plaster==1.1.2
32 32 plaster-pastedeploy==1.0.1
33 33 pastedeploy==3.0.1
34 34 plaster==1.1.2
35 35 translationstring==1.4
36 36 venusian==3.0.0
37 37 webob==1.8.7
38 38 zope.deprecation==4.4.0
39 39 zope.interface==5.5.2
40 40 redis==4.5.1
41 41 async-timeout==4.0.2
42 42 repoze.lru==0.7
43 43 scandir==1.10.0
44 44 setproctitle==1.3.2
45 45 subvertpy==0.11.0
46 46 wcwidth==0.2.6
47 47
48 48
49 49 ## test related requirements
50 50 -r requirements_test.txt
51 51
52 52 ## uncomment to add the debug libraries
53 53 #-r requirements_debug.txt
@@ -1,738 +1,738 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 4 # Copyright (C) 2014-2020 RhodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 20 import io
21 21 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import importlib
26 26 import base64
27 27 import msgpack
28 28
29 29 from http.client import HTTPConnection
30 30
31 31
32 32 import mercurial.scmutil
33 33 import mercurial.node
34 34
35 35 from vcsserver.lib.rc_json import json
36 36 from vcsserver import exceptions, subprocessio, settings
37 37 from vcsserver.str_utils import safe_bytes
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class HooksHttpClient(object):
43 43 proto = 'msgpack.v1'
44 44 connection = None
45 45
46 46 def __init__(self, hooks_uri):
47 47 self.hooks_uri = hooks_uri
48 48
49 49 def __call__(self, method, extras):
50 50 connection = HTTPConnection(self.hooks_uri)
51 51 # binary msgpack body
52 52 headers, body = self._serialize(method, extras)
53 53 try:
54 54 connection.request('POST', '/', body, headers)
55 55 except Exception as error:
56 56 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
57 57 raise
58 58 response = connection.getresponse()
59 59 try:
60 return msgpack.load(response, raw=False)
60 return msgpack.load(response)
61 61 except Exception:
62 62 response_data = response.read()
63 63 log.exception('Failed to decode hook response json data. '
64 64 'response_code:%s, raw_data:%s',
65 65 response.status, response_data)
66 66 raise
67 67
68 68 @classmethod
69 69 def _serialize(cls, hook_name, extras):
70 70 data = {
71 71 'method': hook_name,
72 72 'extras': extras
73 73 }
74 74 headers = {
75 75 'rc-hooks-protocol': cls.proto
76 76 }
77 77 return headers, msgpack.packb(data)
78 78
79 79
80 80 class HooksDummyClient(object):
81 81 def __init__(self, hooks_module):
82 82 self._hooks_module = importlib.import_module(hooks_module)
83 83
84 84 def __call__(self, hook_name, extras):
85 85 with self._hooks_module.Hooks() as hooks:
86 86 return getattr(hooks, hook_name)(extras)
87 87
88 88
89 89 class HooksShadowRepoClient(object):
90 90
91 91 def __call__(self, hook_name, extras):
92 92 return {'output': '', 'status': 0}
93 93
94 94
95 95 class RemoteMessageWriter(object):
96 96 """Writer base class."""
97 97 def write(self, message):
98 98 raise NotImplementedError()
99 99
100 100
101 101 class HgMessageWriter(RemoteMessageWriter):
102 102 """Writer that knows how to send messages to mercurial clients."""
103 103
104 104 def __init__(self, ui):
105 105 self.ui = ui
106 106
107 107 def write(self, message):
108 108 # TODO: Check why the quiet flag is set by default.
109 109 old = self.ui.quiet
110 110 self.ui.quiet = False
111 111 self.ui.status(message.encode('utf-8'))
112 112 self.ui.quiet = old
113 113
114 114
115 115 class GitMessageWriter(RemoteMessageWriter):
116 116 """Writer that knows how to send messages to git clients."""
117 117
118 118 def __init__(self, stdout=None):
119 119 self.stdout = stdout or sys.stdout
120 120
121 121 def write(self, message):
122 122 self.stdout.write(safe_bytes(message))
123 123
124 124
125 125 class SvnMessageWriter(RemoteMessageWriter):
126 126 """Writer that knows how to send messages to svn clients."""
127 127
128 128 def __init__(self, stderr=None):
129 129 # SVN needs data sent to stderr for back-to-client messaging
130 130 self.stderr = stderr or sys.stderr
131 131
132 132 def write(self, message):
133 133 self.stderr.write(message.encode('utf-8'))
134 134
135 135
136 136 def _handle_exception(result):
137 137 exception_class = result.get('exception')
138 138 exception_traceback = result.get('exception_traceback')
139 139
140 140 if exception_traceback:
141 141 log.error('Got traceback from remote call:%s', exception_traceback)
142 142
143 143 if exception_class == 'HTTPLockedRC':
144 144 raise exceptions.RepositoryLockedException()(*result['exception_args'])
145 145 elif exception_class == 'HTTPBranchProtected':
146 146 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
147 147 elif exception_class == 'RepositoryError':
148 148 raise exceptions.VcsException()(*result['exception_args'])
149 149 elif exception_class:
150 150 raise Exception('Got remote exception "%s" with args "%s"' %
151 151 (exception_class, result['exception_args']))
152 152
153 153
154 154 def _get_hooks_client(extras):
155 155 hooks_uri = extras.get('hooks_uri')
156 156 is_shadow_repo = extras.get('is_shadow_repo')
157 157 if hooks_uri:
158 158 return HooksHttpClient(extras['hooks_uri'])
159 159 elif is_shadow_repo:
160 160 return HooksShadowRepoClient()
161 161 else:
162 162 return HooksDummyClient(extras['hooks_module'])
163 163
164 164
165 165 def _call_hook(hook_name, extras, writer):
166 166 hooks_client = _get_hooks_client(extras)
167 167 log.debug('Hooks, using client:%s', hooks_client)
168 168 result = hooks_client(hook_name, extras)
169 169 log.debug('Hooks got result: %s', result)
170 170
171 171 _handle_exception(result)
172 172 writer.write(result['output'])
173 173
174 174 return result['status']
175 175
176 176
177 177 def _extras_from_ui(ui):
178 178 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
179 179 if not hook_data:
180 180 # maybe it's inside environ ?
181 181 env_hook_data = os.environ.get('RC_SCM_DATA')
182 182 if env_hook_data:
183 183 hook_data = env_hook_data
184 184
185 185 extras = {}
186 186 if hook_data:
187 187 extras = json.loads(hook_data)
188 188 return extras
189 189
190 190
191 191 def _rev_range_hash(repo, node, check_heads=False):
192 192 from vcsserver.hgcompat import get_ctx
193 193
194 194 commits = []
195 195 revs = []
196 196 start = get_ctx(repo, node).rev()
197 197 end = len(repo)
198 198 for rev in range(start, end):
199 199 revs.append(rev)
200 200 ctx = get_ctx(repo, rev)
201 201 commit_id = mercurial.node.hex(ctx.node())
202 202 branch = ctx.branch()
203 203 commits.append((commit_id, branch))
204 204
205 205 parent_heads = []
206 206 if check_heads:
207 207 parent_heads = _check_heads(repo, start, end, revs)
208 208 return commits, parent_heads
209 209
210 210
211 211 def _check_heads(repo, start, end, commits):
212 212 from vcsserver.hgcompat import get_ctx
213 213 changelog = repo.changelog
214 214 parents = set()
215 215
216 216 for new_rev in commits:
217 217 for p in changelog.parentrevs(new_rev):
218 218 if p == mercurial.node.nullrev:
219 219 continue
220 220 if p < start:
221 221 parents.add(p)
222 222
223 223 for p in parents:
224 224 branch = get_ctx(repo, p).branch()
225 225 # The heads descending from that parent, on the same branch
226 226 parent_heads = set([p])
227 227 reachable = set([p])
228 228 for x in range(p + 1, end):
229 229 if get_ctx(repo, x).branch() != branch:
230 230 continue
231 231 for pp in changelog.parentrevs(x):
232 232 if pp in reachable:
233 233 reachable.add(x)
234 234 parent_heads.discard(pp)
235 235 parent_heads.add(x)
236 236 # More than one head? Suggest merging
237 237 if len(parent_heads) > 1:
238 238 return list(parent_heads)
239 239
240 240 return []
241 241
242 242
243 243 def _get_git_env():
244 244 env = {}
245 245 for k, v in os.environ.items():
246 246 if k.startswith('GIT'):
247 247 env[k] = v
248 248
249 249 # serialized version
250 250 return [(k, v) for k, v in env.items()]
251 251
252 252
253 253 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
254 254 env = {}
255 255 for k, v in os.environ.items():
256 256 if k.startswith('HG'):
257 257 env[k] = v
258 258
259 259 env['HG_NODE'] = old_rev
260 260 env['HG_NODE_LAST'] = new_rev
261 261 env['HG_TXNID'] = txnid
262 262 env['HG_PENDING'] = repo_path
263 263
264 264 return [(k, v) for k, v in env.items()]
265 265
266 266
267 267 def repo_size(ui, repo, **kwargs):
268 268 extras = _extras_from_ui(ui)
269 269 return _call_hook('repo_size', extras, HgMessageWriter(ui))
270 270
271 271
272 272 def pre_pull(ui, repo, **kwargs):
273 273 extras = _extras_from_ui(ui)
274 274 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
275 275
276 276
277 277 def pre_pull_ssh(ui, repo, **kwargs):
278 278 extras = _extras_from_ui(ui)
279 279 if extras and extras.get('SSH'):
280 280 return pre_pull(ui, repo, **kwargs)
281 281 return 0
282 282
283 283
284 284 def post_pull(ui, repo, **kwargs):
285 285 extras = _extras_from_ui(ui)
286 286 return _call_hook('post_pull', extras, HgMessageWriter(ui))
287 287
288 288
289 289 def post_pull_ssh(ui, repo, **kwargs):
290 290 extras = _extras_from_ui(ui)
291 291 if extras and extras.get('SSH'):
292 292 return post_pull(ui, repo, **kwargs)
293 293 return 0
294 294
295 295
296 296 def pre_push(ui, repo, node=None, **kwargs):
297 297 """
298 298 Mercurial pre_push hook
299 299 """
300 300 extras = _extras_from_ui(ui)
301 301 detect_force_push = extras.get('detect_force_push')
302 302
303 303 rev_data = []
304 304 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
305 305 branches = collections.defaultdict(list)
306 306 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
307 307 for commit_id, branch in commits:
308 308 branches[branch].append(commit_id)
309 309
310 310 for branch, commits in branches.items():
311 311 old_rev = kwargs.get('node_last') or commits[0]
312 312 rev_data.append({
313 313 'total_commits': len(commits),
314 314 'old_rev': old_rev,
315 315 'new_rev': commits[-1],
316 316 'ref': '',
317 317 'type': 'branch',
318 318 'name': branch,
319 319 })
320 320
321 321 for push_ref in rev_data:
322 322 push_ref['multiple_heads'] = _heads
323 323
324 324 repo_path = os.path.join(
325 325 extras.get('repo_store', ''), extras.get('repository', ''))
326 326 push_ref['hg_env'] = _get_hg_env(
327 327 old_rev=push_ref['old_rev'],
328 328 new_rev=push_ref['new_rev'], txnid=kwargs.get('txnid'),
329 329 repo_path=repo_path)
330 330
331 331 extras['hook_type'] = kwargs.get('hooktype', 'pre_push')
332 332 extras['commit_ids'] = rev_data
333 333
334 334 return _call_hook('pre_push', extras, HgMessageWriter(ui))
335 335
336 336
337 337 def pre_push_ssh(ui, repo, node=None, **kwargs):
338 338 extras = _extras_from_ui(ui)
339 339 if extras.get('SSH'):
340 340 return pre_push(ui, repo, node, **kwargs)
341 341
342 342 return 0
343 343
344 344
345 345 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
346 346 """
347 347 Mercurial pre_push hook for SSH
348 348 """
349 349 extras = _extras_from_ui(ui)
350 350 if extras.get('SSH'):
351 351 permission = extras['SSH_PERMISSIONS']
352 352
353 353 if 'repository.write' == permission or 'repository.admin' == permission:
354 354 return 0
355 355
356 356 # non-zero ret code
357 357 return 1
358 358
359 359 return 0
360 360
361 361
362 362 def post_push(ui, repo, node, **kwargs):
363 363 """
364 364 Mercurial post_push hook
365 365 """
366 366 extras = _extras_from_ui(ui)
367 367
368 368 commit_ids = []
369 369 branches = []
370 370 bookmarks = []
371 371 tags = []
372 372
373 373 commits, _heads = _rev_range_hash(repo, node)
374 374 for commit_id, branch in commits:
375 375 commit_ids.append(commit_id)
376 376 if branch not in branches:
377 377 branches.append(branch)
378 378
379 379 if hasattr(ui, '_rc_pushkey_branches'):
380 380 bookmarks = ui._rc_pushkey_branches
381 381
382 382 extras['hook_type'] = kwargs.get('hooktype', 'post_push')
383 383 extras['commit_ids'] = commit_ids
384 384 extras['new_refs'] = {
385 385 'branches': branches,
386 386 'bookmarks': bookmarks,
387 387 'tags': tags
388 388 }
389 389
390 390 return _call_hook('post_push', extras, HgMessageWriter(ui))
391 391
392 392
393 393 def post_push_ssh(ui, repo, node, **kwargs):
394 394 """
395 395 Mercurial post_push hook for SSH
396 396 """
397 397 if _extras_from_ui(ui).get('SSH'):
398 398 return post_push(ui, repo, node, **kwargs)
399 399 return 0
400 400
401 401
402 402 def key_push(ui, repo, **kwargs):
403 403 from vcsserver.hgcompat import get_ctx
404 404 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
405 405 # store new bookmarks in our UI object propagated later to post_push
406 406 ui._rc_pushkey_branches = get_ctx(repo, kwargs['key']).bookmarks()
407 407 return
408 408
409 409
410 410 # backward compat
411 411 log_pull_action = post_pull
412 412
413 413 # backward compat
414 414 log_push_action = post_push
415 415
416 416
417 417 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
418 418 """
419 419 Old hook name: keep here for backward compatibility.
420 420
421 421 This is only required when the installed git hooks are not upgraded.
422 422 """
423 423 pass
424 424
425 425
426 426 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
427 427 """
428 428 Old hook name: keep here for backward compatibility.
429 429
430 430 This is only required when the installed git hooks are not upgraded.
431 431 """
432 432 pass
433 433
434 434
435 435 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
436 436
437 437
438 438 def git_pre_pull(extras):
439 439 """
440 440 Pre pull hook.
441 441
442 442 :param extras: dictionary containing the keys defined in simplevcs
443 443 :type extras: dict
444 444
445 445 :return: status code of the hook. 0 for success.
446 446 :rtype: int
447 447 """
448 448
449 449 if 'pull' not in extras['hooks']:
450 450 return HookResponse(0, '')
451 451
452 452 stdout = io.BytesIO()
453 453 try:
454 454 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
455 455
456 456 except Exception as error:
457 457 log.exception('Failed to call pre_pull hook')
458 458 status = 128
459 459 stdout.write(safe_bytes(f'ERROR: {error}\n'))
460 460
461 461 return HookResponse(status, stdout.getvalue())
462 462
463 463
464 464 def git_post_pull(extras):
465 465 """
466 466 Post pull hook.
467 467
468 468 :param extras: dictionary containing the keys defined in simplevcs
469 469 :type extras: dict
470 470
471 471 :return: status code of the hook. 0 for success.
472 472 :rtype: int
473 473 """
474 474 if 'pull' not in extras['hooks']:
475 475 return HookResponse(0, '')
476 476
477 477 stdout = io.BytesIO()
478 478 try:
479 479 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
480 480 except Exception as error:
481 481 status = 128
482 482 stdout.write(safe_bytes(f'ERROR: {error}\n'))
483 483
484 484 return HookResponse(status, stdout.getvalue())
485 485
486 486
487 487 def _parse_git_ref_lines(revision_lines):
488 488 rev_data = []
489 489 for revision_line in revision_lines or []:
490 490 old_rev, new_rev, ref = revision_line.strip().split(' ')
491 491 ref_data = ref.split('/', 2)
492 492 if ref_data[1] in ('tags', 'heads'):
493 493 rev_data.append({
494 494 # NOTE(marcink):
495 495 # we're unable to tell total_commits for git at this point
496 496 # but we set the variable for consistency with GIT
497 497 'total_commits': -1,
498 498 'old_rev': old_rev,
499 499 'new_rev': new_rev,
500 500 'ref': ref,
501 501 'type': ref_data[1],
502 502 'name': ref_data[2],
503 503 })
504 504 return rev_data
505 505
506 506
507 507 def git_pre_receive(unused_repo_path, revision_lines, env):
508 508 """
509 509 Pre push hook.
510 510
511 511 :param extras: dictionary containing the keys defined in simplevcs
512 512 :type extras: dict
513 513
514 514 :return: status code of the hook. 0 for success.
515 515 :rtype: int
516 516 """
517 517 extras = json.loads(env['RC_SCM_DATA'])
518 518 rev_data = _parse_git_ref_lines(revision_lines)
519 519 if 'push' not in extras['hooks']:
520 520 return 0
521 521 empty_commit_id = '0' * 40
522 522
523 523 detect_force_push = extras.get('detect_force_push')
524 524
525 525 for push_ref in rev_data:
526 526 # store our git-env which holds the temp store
527 527 push_ref['git_env'] = _get_git_env()
528 528 push_ref['pruned_sha'] = ''
529 529 if not detect_force_push:
530 530 # don't check for forced-push when we don't need to
531 531 continue
532 532
533 533 type_ = push_ref['type']
534 534 new_branch = push_ref['old_rev'] == empty_commit_id
535 535 delete_branch = push_ref['new_rev'] == empty_commit_id
536 536 if type_ == 'heads' and not (new_branch or delete_branch):
537 537 old_rev = push_ref['old_rev']
538 538 new_rev = push_ref['new_rev']
539 539 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, '^{}'.format(new_rev)]
540 540 stdout, stderr = subprocessio.run_command(
541 541 cmd, env=os.environ.copy())
542 542 # means we're having some non-reachable objects, this forced push was used
543 543 if stdout:
544 544 push_ref['pruned_sha'] = stdout.splitlines()
545 545
546 546 extras['hook_type'] = 'pre_receive'
547 547 extras['commit_ids'] = rev_data
548 548 return _call_hook('pre_push', extras, GitMessageWriter())
549 549
550 550
551 551 def git_post_receive(unused_repo_path, revision_lines, env):
552 552 """
553 553 Post push hook.
554 554
555 555 :param extras: dictionary containing the keys defined in simplevcs
556 556 :type extras: dict
557 557
558 558 :return: status code of the hook. 0 for success.
559 559 :rtype: int
560 560 """
561 561 extras = json.loads(env['RC_SCM_DATA'])
562 562 if 'push' not in extras['hooks']:
563 563 return 0
564 564
565 565 rev_data = _parse_git_ref_lines(revision_lines)
566 566
567 567 git_revs = []
568 568
569 569 # N.B.(skreft): it is ok to just call git, as git before calling a
570 570 # subcommand sets the PATH environment variable so that it point to the
571 571 # correct version of the git executable.
572 572 empty_commit_id = '0' * 40
573 573 branches = []
574 574 tags = []
575 575 for push_ref in rev_data:
576 576 type_ = push_ref['type']
577 577
578 578 if type_ == 'heads':
579 579 if push_ref['old_rev'] == empty_commit_id:
580 580 # starting new branch case
581 581 if push_ref['name'] not in branches:
582 582 branches.append(push_ref['name'])
583 583
584 584 # Fix up head revision if needed
585 585 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
586 586 try:
587 587 subprocessio.run_command(cmd, env=os.environ.copy())
588 588 except Exception:
589 589 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', '"HEAD"',
590 590 '"refs/heads/%s"' % push_ref['name']]
591 591 print(("Setting default branch to %s" % push_ref['name']))
592 592 subprocessio.run_command(cmd, env=os.environ.copy())
593 593
594 594 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
595 595 '--format=%(refname)', 'refs/heads/*']
596 596 stdout, stderr = subprocessio.run_command(
597 597 cmd, env=os.environ.copy())
598 598 heads = stdout
599 599 heads = heads.replace(push_ref['ref'], '')
600 600 heads = ' '.join(head for head
601 601 in heads.splitlines() if head) or '.'
602 602 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
603 603 '--pretty=format:%H', '--', push_ref['new_rev'],
604 604 '--not', heads]
605 605 stdout, stderr = subprocessio.run_command(
606 606 cmd, env=os.environ.copy())
607 607 git_revs.extend(stdout.splitlines())
608 608 elif push_ref['new_rev'] == empty_commit_id:
609 609 # delete branch case
610 610 git_revs.append('delete_branch=>%s' % push_ref['name'])
611 611 else:
612 612 if push_ref['name'] not in branches:
613 613 branches.append(push_ref['name'])
614 614
615 615 cmd = [settings.GIT_EXECUTABLE, 'log',
616 616 '{old_rev}..{new_rev}'.format(**push_ref),
617 617 '--reverse', '--pretty=format:%H']
618 618 stdout, stderr = subprocessio.run_command(
619 619 cmd, env=os.environ.copy())
620 620 git_revs.extend(stdout.splitlines())
621 621 elif type_ == 'tags':
622 622 if push_ref['name'] not in tags:
623 623 tags.append(push_ref['name'])
624 624 git_revs.append('tag=>%s' % push_ref['name'])
625 625
626 626 extras['hook_type'] = 'post_receive'
627 627 extras['commit_ids'] = git_revs
628 628 extras['new_refs'] = {
629 629 'branches': branches,
630 630 'bookmarks': [],
631 631 'tags': tags,
632 632 }
633 633
634 634 if 'repo_size' in extras['hooks']:
635 635 try:
636 636 _call_hook('repo_size', extras, GitMessageWriter())
637 637 except:
638 638 pass
639 639
640 640 return _call_hook('post_push', extras, GitMessageWriter())
641 641
642 642
643 643 def _get_extras_from_txn_id(path, txn_id):
644 644 extras = {}
645 645 try:
646 646 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
647 647 '-t', txn_id,
648 648 '--revprop', path, 'rc-scm-extras']
649 649 stdout, stderr = subprocessio.run_command(
650 650 cmd, env=os.environ.copy())
651 651 extras = json.loads(base64.urlsafe_b64decode(stdout))
652 652 except Exception:
653 653 log.exception('Failed to extract extras info from txn_id')
654 654
655 655 return extras
656 656
657 657
658 658 def _get_extras_from_commit_id(commit_id, path):
659 659 extras = {}
660 660 try:
661 661 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
662 662 '-r', commit_id,
663 663 '--revprop', path, 'rc-scm-extras']
664 664 stdout, stderr = subprocessio.run_command(
665 665 cmd, env=os.environ.copy())
666 666 extras = json.loads(base64.urlsafe_b64decode(stdout))
667 667 except Exception:
668 668 log.exception('Failed to extract extras info from commit_id')
669 669
670 670 return extras
671 671
672 672
673 673 def svn_pre_commit(repo_path, commit_data, env):
674 674 path, txn_id = commit_data
675 675 branches = []
676 676 tags = []
677 677
678 678 if env.get('RC_SCM_DATA'):
679 679 extras = json.loads(env['RC_SCM_DATA'])
680 680 else:
681 681 # fallback method to read from TXN-ID stored data
682 682 extras = _get_extras_from_txn_id(path, txn_id)
683 683 if not extras:
684 684 return 0
685 685
686 686 extras['hook_type'] = 'pre_commit'
687 687 extras['commit_ids'] = [txn_id]
688 688 extras['txn_id'] = txn_id
689 689 extras['new_refs'] = {
690 690 'total_commits': 1,
691 691 'branches': branches,
692 692 'bookmarks': [],
693 693 'tags': tags,
694 694 }
695 695
696 696 return _call_hook('pre_push', extras, SvnMessageWriter())
697 697
698 698
699 699 def svn_post_commit(repo_path, commit_data, env):
700 700 """
701 701 commit_data is path, rev, txn_id
702 702 """
703 703 if len(commit_data) == 3:
704 704 path, commit_id, txn_id = commit_data
705 705 elif len(commit_data) == 2:
706 706 log.error('Failed to extract txn_id from commit_data using legacy method. '
707 707 'Some functionality might be limited')
708 708 path, commit_id = commit_data
709 709 txn_id = None
710 710
711 711 branches = []
712 712 tags = []
713 713
714 714 if env.get('RC_SCM_DATA'):
715 715 extras = json.loads(env['RC_SCM_DATA'])
716 716 else:
717 717 # fallback method to read from TXN-ID stored data
718 718 extras = _get_extras_from_commit_id(commit_id, path)
719 719 if not extras:
720 720 return 0
721 721
722 722 extras['hook_type'] = 'post_commit'
723 723 extras['commit_ids'] = [commit_id]
724 724 extras['txn_id'] = txn_id
725 725 extras['new_refs'] = {
726 726 'branches': branches,
727 727 'bookmarks': [],
728 728 'tags': tags,
729 729 'total_commits': 1,
730 730 }
731 731
732 732 if 'repo_size' in extras['hooks']:
733 733 try:
734 734 _call_hook('repo_size', extras, SvnMessageWriter())
735 735 except Exception:
736 736 pass
737 737
738 738 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,740 +1,741 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 io
19 19 import os
20 20 import sys
21 21 import base64
22 22 import locale
23 23 import logging
24 24 import uuid
25 25 import time
26 26 import wsgiref.util
27 27 import traceback
28 28 import tempfile
29 29 import psutil
30 30
31 31 from itertools import chain
32 32
33 33 import msgpack
34 34 import configparser
35 35
36 36 from pyramid.config import Configurator
37 37 from pyramid.wsgi import wsgiapp
38 38 from pyramid.response import Response
39 39
40 40 from vcsserver.lib.rc_json import json
41 41 from vcsserver.config.settings_maker import SettingsMaker
42 42 from vcsserver.str_utils import safe_int
43 43 from vcsserver.lib.statsd_client import StatsdClient
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
48 48 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
49 49
50 50 try:
51 51 locale.setlocale(locale.LC_ALL, '')
52 52 except locale.Error as e:
53 53 log.error(
54 54 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
55 55 os.environ['LC_ALL'] = 'C'
56 56
57 57
58 58 import vcsserver
59 59 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
60 60 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
61 61 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
62 62 from vcsserver.echo_stub.echo_app import EchoApp
63 63 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
64 64 from vcsserver.lib.exc_tracking import store_exception
65 65 from vcsserver.server import VcsServer
66 66
67 67 strict_vcs = True
68 68
69 69 git_import_err = None
70 70 try:
71 71 from vcsserver.remote.git import GitFactory, GitRemote
72 72 except ImportError as e:
73 73 GitFactory = None
74 74 GitRemote = None
75 75 git_import_err = e
76 76 if strict_vcs:
77 77 raise
78 78
79 79
80 80 hg_import_err = None
81 81 try:
82 82 from vcsserver.remote.hg import MercurialFactory, HgRemote
83 83 except ImportError as e:
84 84 MercurialFactory = None
85 85 HgRemote = None
86 86 hg_import_err = e
87 87 if strict_vcs:
88 88 raise
89 89
90 90
91 91 svn_import_err = None
92 92 try:
93 93 from vcsserver.remote.svn import SubversionFactory, SvnRemote
94 94 except ImportError as e:
95 95 SubversionFactory = None
96 96 SvnRemote = None
97 97 svn_import_err = e
98 98 if strict_vcs:
99 99 raise
100 100
101 101
102 102 def _is_request_chunked(environ):
103 103 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
104 104 return stream
105 105
106 106
107 107 def log_max_fd():
108 108 try:
109 109 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
110 110 log.info('Max file descriptors value: %s', maxfd)
111 111 except Exception:
112 112 pass
113 113
114 114
115 115 class VCS(object):
116 116 def __init__(self, locale_conf=None, cache_config=None):
117 117 self.locale = locale_conf
118 118 self.cache_config = cache_config
119 119 self._configure_locale()
120 120
121 121 log_max_fd()
122 122
123 123 if GitFactory and GitRemote:
124 124 git_factory = GitFactory()
125 125 self._git_remote = GitRemote(git_factory)
126 126 else:
127 127 log.error("Git client import failed: %s", git_import_err)
128 128
129 129 if MercurialFactory and HgRemote:
130 130 hg_factory = MercurialFactory()
131 131 self._hg_remote = HgRemote(hg_factory)
132 132 else:
133 133 log.error("Mercurial client import failed: %s", hg_import_err)
134 134
135 135 if SubversionFactory and SvnRemote:
136 136 svn_factory = SubversionFactory()
137 137
138 138 # hg factory is used for svn url validation
139 139 hg_factory = MercurialFactory()
140 140 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
141 141 else:
142 142 log.error("Subversion client import failed: %s", svn_import_err)
143 143
144 144 self._vcsserver = VcsServer()
145 145
146 146 def _configure_locale(self):
147 147 if self.locale:
148 148 log.info('Settings locale: `LC_ALL` to %s', self.locale)
149 149 else:
150 150 log.info('Configuring locale subsystem based on environment variables')
151 151 try:
152 152 # If self.locale is the empty string, then the locale
153 153 # module will use the environment variables. See the
154 154 # documentation of the package `locale`.
155 155 locale.setlocale(locale.LC_ALL, self.locale)
156 156
157 157 language_code, encoding = locale.getlocale()
158 158 log.info(
159 159 'Locale set to language code "%s" with encoding "%s".',
160 160 language_code, encoding)
161 161 except locale.Error:
162 162 log.exception('Cannot set locale, not configuring the locale system')
163 163
164 164
165 165 class WsgiProxy(object):
166 166 def __init__(self, wsgi):
167 167 self.wsgi = wsgi
168 168
169 169 def __call__(self, environ, start_response):
170 170 input_data = environ['wsgi.input'].read()
171 171 input_data = msgpack.unpackb(input_data)
172 172
173 173 error = None
174 174 try:
175 175 data, status, headers = self.wsgi.handle(
176 176 input_data['environment'], input_data['input_data'],
177 177 *input_data['args'], **input_data['kwargs'])
178 178 except Exception as e:
179 179 data, status, headers = [], None, None
180 180 error = {
181 181 'message': str(e),
182 182 '_vcs_kind': getattr(e, '_vcs_kind', None)
183 183 }
184 184
185 185 start_response(200, {})
186 186 return self._iterator(error, status, headers, data)
187 187
188 188 def _iterator(self, error, status, headers, data):
189 189 initial_data = [
190 190 error,
191 191 status,
192 192 headers,
193 193 ]
194 194
195 195 for d in chain(initial_data, data):
196 196 yield msgpack.packb(d)
197 197
198 198
199 199 def not_found(request):
200 200 return {'status': '404 NOT FOUND'}
201 201
202 202
203 203 class VCSViewPredicate(object):
204 204 def __init__(self, val, config):
205 205 self.remotes = val
206 206
207 207 def text(self):
208 208 return 'vcs view method = %s' % (list(self.remotes.keys()),)
209 209
210 210 phash = text
211 211
212 212 def __call__(self, context, request):
213 213 """
214 214 View predicate that returns true if given backend is supported by
215 215 defined remotes.
216 216 """
217 217 backend = request.matchdict.get('backend')
218 218 return backend in self.remotes
219 219
220 220
221 221 class HTTPApplication(object):
222 222 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
223 223
224 224 remote_wsgi = remote_wsgi
225 225 _use_echo_app = False
226 226
227 227 def __init__(self, settings=None, global_config=None):
228 228
229 229 self.config = Configurator(settings=settings)
230 230 # Init our statsd at very start
231 231 self.config.registry.statsd = StatsdClient.statsd
232 232
233 233 self.global_config = global_config
234 234 self.config.include('vcsserver.lib.rc_cache')
235 235
236 236 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
237 237 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
238 238 self._remotes = {
239 239 'hg': vcs._hg_remote,
240 240 'git': vcs._git_remote,
241 241 'svn': vcs._svn_remote,
242 242 'server': vcs._vcsserver,
243 243 }
244 244 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
245 245 self._use_echo_app = True
246 246 log.warning("Using EchoApp for VCS operations.")
247 247 self.remote_wsgi = remote_wsgi_stub
248 248
249 249 self._configure_settings(global_config, settings)
250 250
251 251 self._configure()
252 252
253 253 def _configure_settings(self, global_config, app_settings):
254 254 """
255 255 Configure the settings module.
256 256 """
257 257 settings_merged = global_config.copy()
258 258 settings_merged.update(app_settings)
259 259
260 260 git_path = app_settings.get('git_path', None)
261 261 if git_path:
262 262 settings.GIT_EXECUTABLE = git_path
263 263 binary_dir = app_settings.get('core.binary_dir', None)
264 264 if binary_dir:
265 265 settings.BINARY_DIR = binary_dir
266 266
267 267 # Store the settings to make them available to other modules.
268 268 vcsserver.PYRAMID_SETTINGS = settings_merged
269 269 vcsserver.CONFIG = settings_merged
270 270
271 271 def _configure(self):
272 272 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
273 273
274 274 self.config.add_route('service', '/_service')
275 275 self.config.add_route('status', '/status')
276 276 self.config.add_route('hg_proxy', '/proxy/hg')
277 277 self.config.add_route('git_proxy', '/proxy/git')
278 278
279 279 # rpc methods
280 280 self.config.add_route('vcs', '/{backend}')
281 281
282 282 # streaming rpc remote methods
283 283 self.config.add_route('vcs_stream', '/{backend}/stream')
284 284
285 285 # vcs operations clone/push as streaming
286 286 self.config.add_route('stream_git', '/stream/git/*repo_name')
287 287 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
288 288
289 289 self.config.add_view(self.status_view, route_name='status', renderer='json')
290 290 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
291 291
292 292 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
293 293 self.config.add_view(self.git_proxy(), route_name='git_proxy')
294 294 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
295 295 vcs_view=self._remotes)
296 296 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
297 297 vcs_view=self._remotes)
298 298
299 299 self.config.add_view(self.hg_stream(), route_name='stream_hg')
300 300 self.config.add_view(self.git_stream(), route_name='stream_git')
301 301
302 302 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
303 303
304 304 self.config.add_notfound_view(not_found, renderer='json')
305 305
306 306 self.config.add_view(self.handle_vcs_exception, context=Exception)
307 307
308 308 self.config.add_tween(
309 309 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
310 310 )
311 311 self.config.add_request_method(
312 312 'vcsserver.lib.request_counter.get_request_counter',
313 313 'request_count')
314 314
315 315 def wsgi_app(self):
316 316 return self.config.make_wsgi_app()
317 317
318 318 def _vcs_view_params(self, request):
319 319 remote = self._remotes[request.matchdict['backend']]
320 payload = msgpack.unpackb(request.body, use_list=True, raw=False)
320 payload = msgpack.unpackb(request.body, use_list=True)
321 321
322 322 method = payload.get('method')
323 323 params = payload['params']
324 324 wire = params.get('wire')
325 325 args = params.get('args')
326 326 kwargs = params.get('kwargs')
327 327 context_uid = None
328 328
329 329 if wire:
330 330 try:
331 331 wire['context'] = context_uid = uuid.UUID(wire['context'])
332 332 except KeyError:
333 333 pass
334 334 args.insert(0, wire)
335 335 repo_state_uid = wire.get('repo_state_uid') if wire else None
336 336
337 337 # NOTE(marcink): trading complexity for slight performance
338 338 if log.isEnabledFor(logging.DEBUG):
339 339 no_args_methods = [
340 340
341 341 ]
342 342 if method in no_args_methods:
343 343 call_args = ''
344 344 else:
345 345 call_args = args[1:]
346 346
347 347 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
348 348 method, call_args, kwargs, context_uid, repo_state_uid)
349 349
350 350 statsd = request.registry.statsd
351 351 if statsd:
352 352 statsd.incr(
353 353 'vcsserver_method_total', tags=[
354 354 "method:{}".format(method),
355 355 ])
356 356 return payload, remote, method, args, kwargs
357 357
358 358 def vcs_view(self, request):
359 359
360 360 payload, remote, method, args, kwargs = self._vcs_view_params(request)
361 361 payload_id = payload.get('id')
362 362
363 363 try:
364 364 resp = getattr(remote, method)(*args, **kwargs)
365 365 except Exception as e:
366 366 exc_info = list(sys.exc_info())
367 367 exc_type, exc_value, exc_traceback = exc_info
368 368
369 369 org_exc = getattr(e, '_org_exc', None)
370 370 org_exc_name = None
371 371 org_exc_tb = ''
372 372 if org_exc:
373 373 org_exc_name = org_exc.__class__.__name__
374 374 org_exc_tb = getattr(e, '_org_exc_tb', '')
375 375 # replace our "faked" exception with our org
376 376 exc_info[0] = org_exc.__class__
377 377 exc_info[1] = org_exc
378 378
379 379 should_store_exc = True
380 380 if org_exc:
381 381 def get_exc_fqn(_exc_obj):
382 382 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
383 383 return module_name + '.' + org_exc_name
384 384
385 385 exc_fqn = get_exc_fqn(org_exc)
386 386
387 387 if exc_fqn in ['mercurial.error.RepoLookupError',
388 388 'vcsserver.exceptions.RefNotFoundException']:
389 389 should_store_exc = False
390 390
391 391 if should_store_exc:
392 392 store_exception(id(exc_info), exc_info, request_path=request.path)
393 393
394 394 tb_info = ''.join(
395 395 traceback.format_exception(exc_type, exc_value, exc_traceback))
396 396
397 397 type_ = e.__class__.__name__
398 398 if type_ not in self.ALLOWED_EXCEPTIONS:
399 399 type_ = None
400 400
401 401 resp = {
402 402 'id': payload_id,
403 403 'error': {
404 404 'message': str(e),
405 405 'traceback': tb_info,
406 406 'org_exc': org_exc_name,
407 407 'org_exc_tb': org_exc_tb,
408 408 'type': type_
409 409 }
410 410 }
411 411
412 412 try:
413 413 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
414 414 except AttributeError:
415 415 pass
416 416 else:
417 417 resp = {
418 418 'id': payload_id,
419 419 'result': resp
420 420 }
421 421
422 422 return resp
423 423
424 424 def vcs_stream_view(self, request):
425 425 payload, remote, method, args, kwargs = self._vcs_view_params(request)
426 426 # this method has a stream: marker we remove it here
427 427 method = method.split('stream:')[-1]
428 428 chunk_size = safe_int(payload.get('chunk_size')) or 4096
429 429
430 430 try:
431 431 resp = getattr(remote, method)(*args, **kwargs)
432 432 except Exception as e:
433 433 raise
434 434
435 435 def get_chunked_data(method_resp):
436 436 stream = io.BytesIO(method_resp)
437 437 while 1:
438 438 chunk = stream.read(chunk_size)
439 439 if not chunk:
440 440 break
441 441 yield chunk
442 442
443 443 response = Response(app_iter=get_chunked_data(resp))
444 444 response.content_type = 'application/octet-stream'
445 445
446 446 return response
447 447
448 448 def status_view(self, request):
449 449 import vcsserver
450 return {'status': 'OK', 'vcsserver_version': str(vcsserver.__version__),
450 return {'status': 'OK', 'vcsserver_version': safe_str(vcsserver.__version__),
451 451 'pid': os.getpid()}
452 452
453 453 def service_view(self, request):
454 454 import vcsserver
455 455
456 456 payload = msgpack.unpackb(request.body, use_list=True)
457 457 server_config, app_config = {}, {}
458 458
459 459 try:
460 460 path = self.global_config['__file__']
461 461 config = configparser.RawConfigParser()
462 462
463 463 config.read(path)
464 464
465 465 if config.has_section('server:main'):
466 466 server_config = dict(config.items('server:main'))
467 467 if config.has_section('app:main'):
468 468 app_config = dict(config.items('app:main'))
469 469
470 470 except Exception:
471 471 log.exception('Failed to read .ini file for display')
472 472
473 473 environ = list(os.environ.items())
474 474
475 475 resp = {
476 476 'id': payload.get('id'),
477 477 'result': dict(
478 version=vcsserver.__version__,
478 version=safe_str(vcsserver.__version__),
479 479 config=server_config,
480 480 app_config=app_config,
481 481 environ=environ,
482 482 payload=payload,
483 483 )
484 484 }
485 485 return resp
486 486
487 487 def _msgpack_renderer_factory(self, info):
488 488 def _render(value, system):
489 489 request = system.get('request')
490 490 if request is not None:
491 491 response = request.response
492 492 ct = response.content_type
493 493 if ct == response.default_content_type:
494 494 response.content_type = 'application/x-msgpack'
495 return msgpack.packb(value)
495
496 return msgpack.packb(value, use_bin_type=False)
496 497 return _render
497 498
498 499 def set_env_from_config(self, environ, config):
499 500 dict_conf = {}
500 501 try:
501 502 for elem in config:
502 503 if elem[0] == 'rhodecode':
503 504 dict_conf = json.loads(elem[2])
504 505 break
505 506 except Exception:
506 507 log.exception('Failed to fetch SCM CONFIG')
507 508 return
508 509
509 510 username = dict_conf.get('username')
510 511 if username:
511 512 environ['REMOTE_USER'] = username
512 513 # mercurial specific, some extension api rely on this
513 514 environ['HGUSER'] = username
514 515
515 516 ip = dict_conf.get('ip')
516 517 if ip:
517 518 environ['REMOTE_HOST'] = ip
518 519
519 520 if _is_request_chunked(environ):
520 521 # set the compatibility flag for webob
521 522 environ['wsgi.input_terminated'] = True
522 523
523 524 def hg_proxy(self):
524 525 @wsgiapp
525 526 def _hg_proxy(environ, start_response):
526 527 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
527 528 return app(environ, start_response)
528 529 return _hg_proxy
529 530
530 531 def git_proxy(self):
531 532 @wsgiapp
532 533 def _git_proxy(environ, start_response):
533 534 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
534 535 return app(environ, start_response)
535 536 return _git_proxy
536 537
537 538 def hg_stream(self):
538 539 if self._use_echo_app:
539 540 @wsgiapp
540 541 def _hg_stream(environ, start_response):
541 542 app = EchoApp('fake_path', 'fake_name', None)
542 543 return app(environ, start_response)
543 544 return _hg_stream
544 545 else:
545 546 @wsgiapp
546 547 def _hg_stream(environ, start_response):
547 548 log.debug('http-app: handling hg stream')
548 549 repo_path = environ['HTTP_X_RC_REPO_PATH']
549 550 repo_name = environ['HTTP_X_RC_REPO_NAME']
550 551 packed_config = base64.b64decode(
551 552 environ['HTTP_X_RC_REPO_CONFIG'])
552 553 config = msgpack.unpackb(packed_config)
553 554 app = scm_app.create_hg_wsgi_app(
554 555 repo_path, repo_name, config)
555 556
556 557 # Consistent path information for hgweb
557 558 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
558 559 environ['REPO_NAME'] = repo_name
559 560 self.set_env_from_config(environ, config)
560 561
561 562 log.debug('http-app: starting app handler '
562 563 'with %s and process request', app)
563 564 return app(environ, ResponseFilter(start_response))
564 565 return _hg_stream
565 566
566 567 def git_stream(self):
567 568 if self._use_echo_app:
568 569 @wsgiapp
569 570 def _git_stream(environ, start_response):
570 571 app = EchoApp('fake_path', 'fake_name', None)
571 572 return app(environ, start_response)
572 573 return _git_stream
573 574 else:
574 575 @wsgiapp
575 576 def _git_stream(environ, start_response):
576 577 log.debug('http-app: handling git stream')
577 578 repo_path = environ['HTTP_X_RC_REPO_PATH']
578 579 repo_name = environ['HTTP_X_RC_REPO_NAME']
579 580 packed_config = base64.b64decode(
580 581 environ['HTTP_X_RC_REPO_CONFIG'])
581 config = msgpack.unpackb(packed_config, raw=False)
582 config = msgpack.unpackb(packed_config)
582 583
583 584 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
584 585 self.set_env_from_config(environ, config)
585 586
586 587 content_type = environ.get('CONTENT_TYPE', '')
587 588
588 589 path = environ['PATH_INFO']
589 590 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
590 591 log.debug(
591 592 'LFS: Detecting if request `%s` is LFS server path based '
592 593 'on content type:`%s`, is_lfs:%s',
593 594 path, content_type, is_lfs_request)
594 595
595 596 if not is_lfs_request:
596 597 # fallback detection by path
597 598 if GIT_LFS_PROTO_PAT.match(path):
598 599 is_lfs_request = True
599 600 log.debug(
600 601 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
601 602 path, is_lfs_request)
602 603
603 604 if is_lfs_request:
604 605 app = scm_app.create_git_lfs_wsgi_app(
605 606 repo_path, repo_name, config)
606 607 else:
607 608 app = scm_app.create_git_wsgi_app(
608 609 repo_path, repo_name, config)
609 610
610 611 log.debug('http-app: starting app handler '
611 612 'with %s and process request', app)
612 613
613 614 return app(environ, start_response)
614 615
615 616 return _git_stream
616 617
617 618 def handle_vcs_exception(self, exception, request):
618 619 _vcs_kind = getattr(exception, '_vcs_kind', '')
619 620 if _vcs_kind == 'repo_locked':
620 621 # Get custom repo-locked status code if present.
621 622 status_code = request.headers.get('X-RC-Locked-Status-Code')
622 623 return HTTPRepoLocked(
623 624 title=exception.message, status_code=status_code)
624 625
625 626 elif _vcs_kind == 'repo_branch_protected':
626 627 # Get custom repo-branch-protected status code if present.
627 628 return HTTPRepoBranchProtected(title=exception.message)
628 629
629 630 exc_info = request.exc_info
630 631 store_exception(id(exc_info), exc_info)
631 632
632 633 traceback_info = 'unavailable'
633 634 if request.exc_info:
634 635 exc_type, exc_value, exc_tb = request.exc_info
635 636 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
636 637
637 638 log.error(
638 639 'error occurred handling this request for path: %s, \n tb: %s',
639 640 request.path, traceback_info)
640 641
641 642 statsd = request.registry.statsd
642 643 if statsd:
643 644 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
644 645 statsd.incr('vcsserver_exception_total',
645 646 tags=["type:{}".format(exc_type)])
646 647 raise exception
647 648
648 649
649 650 class ResponseFilter(object):
650 651
651 652 def __init__(self, start_response):
652 653 self._start_response = start_response
653 654
654 655 def __call__(self, status, response_headers, exc_info=None):
655 656 headers = tuple(
656 657 (h, v) for h, v in response_headers
657 658 if not wsgiref.util.is_hop_by_hop(h))
658 659 return self._start_response(status, headers, exc_info)
659 660
660 661
661 662 def sanitize_settings_and_apply_defaults(global_config, settings):
662 663 global_settings_maker = SettingsMaker(global_config)
663 664 settings_maker = SettingsMaker(settings)
664 665
665 666 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
666 667
667 668 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
668 669 settings_maker.enable_logging(logging_conf)
669 670
670 671 # Default includes, possible to change as a user
671 672 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
672 673 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
673 674
674 675 settings_maker.make_setting('__file__', global_config.get('__file__'))
675 676
676 677 settings_maker.make_setting('pyramid.default_locale_name', 'en')
677 678 settings_maker.make_setting('locale', 'en_US.UTF-8')
678 679
679 680 settings_maker.make_setting('core.binary_dir', '')
680 681
681 682 temp_store = tempfile.gettempdir()
682 683 default_cache_dir = os.path.join(temp_store, 'rc_cache')
683 684 # save default, cache dir, and use it for all backends later.
684 685 default_cache_dir = settings_maker.make_setting(
685 686 'cache_dir',
686 687 default=default_cache_dir, default_when_empty=True,
687 688 parser='dir:ensured')
688 689
689 690 # exception store cache
690 691 settings_maker.make_setting(
691 692 'exception_tracker.store_path',
692 693 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
693 694 parser='dir:ensured'
694 695 )
695 696
696 697 # repo_object cache defaults
697 698 settings_maker.make_setting(
698 699 'rc_cache.repo_object.backend',
699 700 default='dogpile.cache.rc.file_namespace',
700 701 parser='string')
701 702 settings_maker.make_setting(
702 703 'rc_cache.repo_object.expiration_time',
703 704 default=30 * 24 * 60 * 60, # 30days
704 705 parser='int')
705 706 settings_maker.make_setting(
706 707 'rc_cache.repo_object.arguments.filename',
707 708 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
708 709 parser='string')
709 710
710 711 # statsd
711 712 settings_maker.make_setting('statsd.enabled', False, parser='bool')
712 713 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
713 714 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
714 715 settings_maker.make_setting('statsd.statsd_prefix', '')
715 716 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
716 717
717 718 settings_maker.env_expand()
718 719
719 720
720 721 def main(global_config, **settings):
721 722 start_time = time.time()
722 723 log.info('Pyramid app config starting')
723 724
724 725 if MercurialFactory:
725 726 hgpatches.patch_largefiles_capabilities()
726 727 hgpatches.patch_subrepo_type_mapping()
727 728
728 729 # Fill in and sanitize the defaults & do ENV expansion
729 730 sanitize_settings_and_apply_defaults(global_config, settings)
730 731
731 732 # init and bootstrap StatsdClient
732 733 StatsdClient.setup(settings)
733 734
734 735 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
735 736 total_time = time.time() - start_time
736 737 log.info('Pyramid app `%s` created and configured in %.2fs',
737 738 getattr(pyramid_app, 'func_name', 'pyramid_app'), total_time)
738 739 return pyramid_app
739 740
740 741
General Comments 0
You need to be logged in to leave comments. Login now