##// END OF EJS Templates
fix: skip process exception is there isn't one
super-admin -
r1296:4ef2409a default
parent child Browse files
Show More
@@ -1,824 +1,828 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 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 logging
22 22 import collections
23 23 import base64
24 24 import msgpack
25 25 import dataclasses
26 26 import pygit2
27 27
28 28 import http.client
29 29 from celery import Celery
30 30
31 31 import mercurial.scmutil
32 32 import mercurial.node
33 33
34 34 from vcsserver import exceptions, subprocessio, settings
35 35 from vcsserver.lib.ext_json import json
36 36 from vcsserver.lib.str_utils import ascii_str, safe_str
37 37 from vcsserver.lib.svn_txn_utils import get_txn_id_from_store
38 38 from vcsserver.remote.git_remote import Repository
39 39
40 40 celery_app = Celery('__vcsserver__')
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class HooksHttpClient:
45 45 proto = 'msgpack.v1'
46 46 connection = None
47 47
48 48 def __init__(self, hooks_uri):
49 49 self.hooks_uri = hooks_uri
50 50
51 51 def __repr__(self):
52 52 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
53 53
54 54 def __call__(self, method, extras):
55 55 connection = http.client.HTTPConnection(self.hooks_uri)
56 56 # binary msgpack body
57 57 headers, body = self._serialize(method, extras)
58 58 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
59 59
60 60 try:
61 61 try:
62 62 connection.request('POST', '/', body, headers)
63 63 except Exception as error:
64 64 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
65 65 raise
66 66
67 67 response = connection.getresponse()
68 68 try:
69 69 return msgpack.load(response)
70 70 except Exception:
71 71 response_data = response.read()
72 72 log.exception('Failed to decode hook response json data. '
73 73 'response_code:%s, raw_data:%s',
74 74 response.status, response_data)
75 75 raise
76 76 finally:
77 77 connection.close()
78 78
79 79 @classmethod
80 80 def _serialize(cls, hook_name, extras):
81 81 data = {
82 82 'method': hook_name,
83 83 'extras': extras
84 84 }
85 85 headers = {
86 86 "rc-hooks-protocol": cls.proto,
87 87 "Connection": "keep-alive"
88 88 }
89 89 return headers, msgpack.packb(data)
90 90
91 91
92 92 class HooksCeleryClient:
93 93 TASK_TIMEOUT = 60 # time in seconds
94 94
95 95 def __init__(self, queue, backend):
96 96 celery_app.config_from_object({
97 97 'broker_url': queue, 'result_backend': backend,
98 98 'broker_connection_retry_on_startup': True,
99 99 'task_serializer': 'json',
100 100 'accept_content': ['json', 'msgpack'],
101 101 'result_serializer': 'json',
102 102 'result_accept_content': ['json', 'msgpack']
103 103 })
104 104 self.celery_app = celery_app
105 105
106 106 def __call__(self, method, extras):
107 107 inquired_task = self.celery_app.signature(
108 108 f'rhodecode.lib.celerylib.tasks.{method}'
109 109 )
110 return inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
110 result = inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
111
112 return result
111 113
112 114
113 115 class HooksShadowRepoClient:
114 116
115 117 def __call__(self, hook_name, extras):
116 118 return {'output': '', 'status': 0}
117 119
118 120
119 121 class RemoteMessageWriter:
120 122 """Writer base class."""
121 123 def write(self, message):
122 124 raise NotImplementedError()
123 125
124 126
125 127 class HgMessageWriter(RemoteMessageWriter):
126 128 """Writer that knows how to send messages to mercurial clients."""
127 129
128 130 def __init__(self, ui):
129 131 self.ui = ui
130 132
131 133 def write(self, message: str):
132 134 # TODO: Check why the quiet flag is set by default.
133 135 old = self.ui.quiet
134 136 self.ui.quiet = False
135 137 self.ui.status(message.encode('utf-8'))
136 138 self.ui.quiet = old
137 139
138 140
139 141 class GitMessageWriter(RemoteMessageWriter):
140 142 """Writer that knows how to send messages to git clients."""
141 143
142 144 def __init__(self, stdout=None):
143 145 self.stdout = stdout or sys.stdout
144 146
145 147 def write(self, message: str):
146 148 self.stdout.write(message)
147 149
148 150
149 151 class SvnMessageWriter(RemoteMessageWriter):
150 152 """Writer that knows how to send messages to svn clients."""
151 153
152 154 def __init__(self, stderr=None):
153 155 # SVN needs data sent to stderr for back-to-client messaging
154 156 self.stderr = stderr or sys.stderr
155 157
156 158 def write(self, message):
157 159 self.stderr.write(message)
158 160
159 161
160 def _handle_exception(result):
162 def _maybe_handle_exception(result):
161 163 exception_class = result.get('exception')
162 164 exception_traceback = result.get('exception_traceback')
165 if not (exception_class and exception_traceback):
166 return
163 167 log.debug('Handling hook-call exception: %s', exception_class)
164 168
165 169 if exception_traceback:
166 170 log.error('Got traceback from remote call:%s', exception_traceback)
167 171
168 172 if exception_class == 'HTTPLockedRC':
169 173 raise exceptions.RepositoryLockedException()(*result['exception_args'])
170 174 elif exception_class == 'ClientNotSupportedError':
171 175 raise exceptions.ClientNotSupportedException()(*result['exception_args'])
172 176 elif exception_class == 'HTTPBranchProtected':
173 177 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
174 178 elif exception_class == 'RepositoryError':
175 179 raise exceptions.VcsException()(*result['exception_args'])
176 180 elif exception_class:
177 181 raise Exception(
178 182 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
179 183 )
180 184
181 185
182 186 def _get_hooks_client(extras):
183 187 hooks_uri = extras.get('hooks_uri')
184 188 task_queue = extras.get('task_queue')
185 189 task_backend = extras.get('task_backend')
186 190 is_shadow_repo = extras.get('is_shadow_repo')
187 191
188 192 if hooks_uri:
189 193 return HooksHttpClient(hooks_uri)
190 194 elif task_queue and task_backend:
191 195 return HooksCeleryClient(task_queue, task_backend)
192 196 elif is_shadow_repo:
193 197 return HooksShadowRepoClient()
194 198 else:
195 199 raise Exception("Hooks client not found!")
196 200
197 201
198 202 def _call_hook(hook_name, extras, writer):
199 203 hooks_client = _get_hooks_client(extras)
200 204 log.debug('Hooks, using client:%s', hooks_client)
201 205 result = hooks_client(hook_name, extras)
202 206 log.debug('Hooks got result: %s', result)
203 _handle_exception(result)
207 _maybe_handle_exception(result)
204 208 writer.write(result['output'])
205 209
206 210 return result['status']
207 211
208 212
209 213 def _extras_from_ui(ui):
210 214 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
211 215 if not hook_data:
212 216 # maybe it's inside environ ?
213 217 env_hook_data = os.environ.get('RC_SCM_DATA')
214 218 if env_hook_data:
215 219 hook_data = env_hook_data
216 220
217 221 extras = {}
218 222 if hook_data:
219 223 extras = json.loads(hook_data)
220 224 return extras
221 225
222 226
223 227 def _rev_range_hash(repo, node, check_heads=False):
224 228 from vcsserver.hgcompat import get_ctx
225 229
226 230 commits = []
227 231 revs = []
228 232 start = get_ctx(repo, node).rev()
229 233 end = len(repo)
230 234 for rev in range(start, end):
231 235 revs.append(rev)
232 236 ctx = get_ctx(repo, rev)
233 237 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
234 238 branch = safe_str(ctx.branch())
235 239 commits.append((commit_id, branch))
236 240
237 241 parent_heads = []
238 242 if check_heads:
239 243 parent_heads = _check_heads(repo, start, end, revs)
240 244 return commits, parent_heads
241 245
242 246
243 247 def _check_heads(repo, start, end, commits):
244 248 from vcsserver.hgcompat import get_ctx
245 249 changelog = repo.changelog
246 250 parents = set()
247 251
248 252 for new_rev in commits:
249 253 for p in changelog.parentrevs(new_rev):
250 254 if p == mercurial.node.nullrev:
251 255 continue
252 256 if p < start:
253 257 parents.add(p)
254 258
255 259 for p in parents:
256 260 branch = get_ctx(repo, p).branch()
257 261 # The heads descending from that parent, on the same branch
258 262 parent_heads = {p}
259 263 reachable = {p}
260 264 for x in range(p + 1, end):
261 265 if get_ctx(repo, x).branch() != branch:
262 266 continue
263 267 for pp in changelog.parentrevs(x):
264 268 if pp in reachable:
265 269 reachable.add(x)
266 270 parent_heads.discard(pp)
267 271 parent_heads.add(x)
268 272 # More than one head? Suggest merging
269 273 if len(parent_heads) > 1:
270 274 return list(parent_heads)
271 275
272 276 return []
273 277
274 278
275 279 def _get_git_env():
276 280 env = {}
277 281 for k, v in os.environ.items():
278 282 if k.startswith('GIT'):
279 283 env[k] = v
280 284
281 285 # serialized version
282 286 return [(k, v) for k, v in env.items()]
283 287
284 288
285 289 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
286 290 env = {}
287 291 for k, v in os.environ.items():
288 292 if k.startswith('HG'):
289 293 env[k] = v
290 294
291 295 env['HG_NODE'] = old_rev
292 296 env['HG_NODE_LAST'] = new_rev
293 297 env['HG_TXNID'] = txnid
294 298 env['HG_PENDING'] = repo_path
295 299
296 300 return [(k, v) for k, v in env.items()]
297 301
298 302
299 303 def _get_ini_settings(ini_file):
300 304 from vcsserver.http_main import sanitize_settings_and_apply_defaults
301 305 from vcsserver.lib.config_utils import get_app_config_lightweight, configure_and_store_settings
302 306
303 307 global_config = {'__file__': ini_file}
304 308 ini_settings = get_app_config_lightweight(ini_file)
305 309 sanitize_settings_and_apply_defaults(global_config, ini_settings)
306 310 configure_and_store_settings(global_config, ini_settings)
307 311
308 312 return ini_settings
309 313
310 314
311 315 def _fix_hooks_executables(ini_path=''):
312 316 """
313 317 This is a trick to set proper settings.EXECUTABLE paths for certain execution patterns
314 318 especially for subversion where hooks strip entire env, and calling just 'svn' command will most likely fail
315 319 because svn is not on PATH
316 320 """
317 321 # set defaults, in case we can't read from ini_file
318 322 core_binary_dir = settings.BINARY_DIR or '/usr/local/bin/rhodecode_bin/vcs_bin'
319 323 if ini_path:
320 324 ini_settings = _get_ini_settings(ini_path)
321 325 core_binary_dir = ini_settings['core.binary_dir']
322 326
323 327 settings.BINARY_DIR = core_binary_dir
324 328
325 329
326 330 def repo_size(ui, repo, **kwargs):
327 331 extras = _extras_from_ui(ui)
328 332 return _call_hook('repo_size', extras, HgMessageWriter(ui))
329 333
330 334
331 335 def pre_pull(ui, repo, **kwargs):
332 336 extras = _extras_from_ui(ui)
333 337 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
334 338
335 339
336 340 def pre_pull_ssh(ui, repo, **kwargs):
337 341 extras = _extras_from_ui(ui)
338 342 if extras and extras.get('SSH'):
339 343 return pre_pull(ui, repo, **kwargs)
340 344 return 0
341 345
342 346
343 347 def post_pull(ui, repo, **kwargs):
344 348 extras = _extras_from_ui(ui)
345 349 return _call_hook('post_pull', extras, HgMessageWriter(ui))
346 350
347 351
348 352 def post_pull_ssh(ui, repo, **kwargs):
349 353 extras = _extras_from_ui(ui)
350 354 if extras and extras.get('SSH'):
351 355 return post_pull(ui, repo, **kwargs)
352 356 return 0
353 357
354 358
355 359 def pre_push(ui, repo, node=None, **kwargs):
356 360 """
357 361 Mercurial pre_push hook
358 362 """
359 363 extras = _extras_from_ui(ui)
360 364 detect_force_push = extras.get('detect_force_push')
361 365
362 366 rev_data = []
363 367 hook_type: str = safe_str(kwargs.get('hooktype'))
364 368
365 369 if node and hook_type == 'pretxnchangegroup':
366 370 branches = collections.defaultdict(list)
367 371 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
368 372 for commit_id, branch in commits:
369 373 branches[branch].append(commit_id)
370 374
371 375 for branch, commits in branches.items():
372 376 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
373 377 rev_data.append({
374 378 'total_commits': len(commits),
375 379 'old_rev': old_rev,
376 380 'new_rev': commits[-1],
377 381 'ref': '',
378 382 'type': 'branch',
379 383 'name': branch,
380 384 })
381 385
382 386 for push_ref in rev_data:
383 387 push_ref['multiple_heads'] = _heads
384 388
385 389 repo_path = os.path.join(
386 390 extras.get('repo_store', ''), extras.get('repository', ''))
387 391 push_ref['hg_env'] = _get_hg_env(
388 392 old_rev=push_ref['old_rev'],
389 393 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
390 394 repo_path=repo_path)
391 395
392 396 extras['hook_type'] = hook_type or 'pre_push'
393 397 extras['commit_ids'] = rev_data
394 398
395 399 return _call_hook('pre_push', extras, HgMessageWriter(ui))
396 400
397 401
398 402 def pre_push_ssh(ui, repo, node=None, **kwargs):
399 403 extras = _extras_from_ui(ui)
400 404 if extras.get('SSH'):
401 405 return pre_push(ui, repo, node, **kwargs)
402 406
403 407 return 0
404 408
405 409
406 410 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
407 411 """
408 412 Mercurial pre_push hook for SSH
409 413 """
410 414 extras = _extras_from_ui(ui)
411 415 if extras.get('SSH'):
412 416 permission = extras['SSH_PERMISSIONS']
413 417
414 418 if 'repository.write' == permission or 'repository.admin' == permission:
415 419 return 0
416 420
417 421 # non-zero ret code
418 422 return 1
419 423
420 424 return 0
421 425
422 426
423 427 def post_push(ui, repo, node, **kwargs):
424 428 """
425 429 Mercurial post_push hook
426 430 """
427 431 extras = _extras_from_ui(ui)
428 432
429 433 commit_ids = []
430 434 branches = []
431 435 bookmarks = []
432 436 tags = []
433 437 hook_type: str = safe_str(kwargs.get('hooktype'))
434 438
435 439 commits, _heads = _rev_range_hash(repo, node)
436 440 for commit_id, branch in commits:
437 441 commit_ids.append(commit_id)
438 442 if branch not in branches:
439 443 branches.append(branch)
440 444
441 445 if hasattr(ui, '_rc_pushkey_bookmarks'):
442 446 bookmarks = ui._rc_pushkey_bookmarks
443 447
444 448 extras['hook_type'] = hook_type or 'post_push'
445 449 extras['commit_ids'] = commit_ids
446 450
447 451 extras['new_refs'] = {
448 452 'branches': branches,
449 453 'bookmarks': bookmarks,
450 454 'tags': tags
451 455 }
452 456
453 457 return _call_hook('post_push', extras, HgMessageWriter(ui))
454 458
455 459
456 460 def post_push_ssh(ui, repo, node, **kwargs):
457 461 """
458 462 Mercurial post_push hook for SSH
459 463 """
460 464 if _extras_from_ui(ui).get('SSH'):
461 465 return post_push(ui, repo, node, **kwargs)
462 466 return 0
463 467
464 468
465 469 def key_push(ui, repo, **kwargs):
466 470 from vcsserver.hgcompat import get_ctx
467 471
468 472 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
469 473 # store new bookmarks in our UI object propagated later to post_push
470 474 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
471 475 return
472 476
473 477
474 478 # backward compat
475 479 log_pull_action = post_pull
476 480
477 481 # backward compat
478 482 log_push_action = post_push
479 483
480 484
481 485 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
482 486 """
483 487 Old hook name: keep here for backward compatibility.
484 488
485 489 This is only required when the installed git hooks are not upgraded.
486 490 """
487 491 pass
488 492
489 493
490 494 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
491 495 """
492 496 Old hook name: keep here for backward compatibility.
493 497
494 498 This is only required when the installed git hooks are not upgraded.
495 499 """
496 500 pass
497 501
498 502
499 503 @dataclasses.dataclass
500 504 class HookResponse:
501 505 status: int
502 506 output: str
503 507
504 508
505 509 def git_pre_pull(extras) -> HookResponse:
506 510 """
507 511 Pre pull hook.
508 512
509 513 :param extras: dictionary containing the keys defined in simplevcs
510 514 :type extras: dict
511 515
512 516 :return: status code of the hook. 0 for success.
513 517 :rtype: int
514 518 """
515 519
516 520 if 'pull' not in extras['hooks']:
517 521 return HookResponse(0, '')
518 522
519 523 stdout = io.StringIO()
520 524 try:
521 525 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
522 526
523 527 except Exception as error:
524 528 log.exception('Failed to call pre_pull hook')
525 529 status_code = 128
526 530 stdout.write(f'ERROR: {error}\n')
527 531
528 532 return HookResponse(status_code, stdout.getvalue())
529 533
530 534
531 535 def git_post_pull(extras) -> HookResponse:
532 536 """
533 537 Post pull hook.
534 538
535 539 :param extras: dictionary containing the keys defined in simplevcs
536 540 :type extras: dict
537 541
538 542 :return: status code of the hook. 0 for success.
539 543 :rtype: int
540 544 """
541 545 if 'pull' not in extras['hooks']:
542 546 return HookResponse(0, '')
543 547
544 548 stdout = io.StringIO()
545 549 try:
546 550 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
547 551 except Exception as error:
548 552 status = 128
549 553 stdout.write(f'ERROR: {error}\n')
550 554
551 555 return HookResponse(status, stdout.getvalue())
552 556
553 557
554 558 def _parse_git_ref_lines(revision_lines):
555 559 rev_data = []
556 560 for revision_line in revision_lines or []:
557 561 old_rev, new_rev, ref = revision_line.strip().split(' ')
558 562 ref_data = ref.split('/', 2)
559 563 if ref_data[1] in ('tags', 'heads'):
560 564 rev_data.append({
561 565 # NOTE(marcink):
562 566 # we're unable to tell total_commits for git at this point
563 567 # but we set the variable for consistency with GIT
564 568 'total_commits': -1,
565 569 'old_rev': old_rev,
566 570 'new_rev': new_rev,
567 571 'ref': ref,
568 572 'type': ref_data[1],
569 573 'name': ref_data[2],
570 574 })
571 575 return rev_data
572 576
573 577
574 578 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
575 579 """
576 580 Pre push hook.
577 581
578 582 :return: status code of the hook. 0 for success.
579 583 """
580 584 extras = json.loads(env['RC_SCM_DATA'])
581 585 rev_data = _parse_git_ref_lines(revision_lines)
582 586 if 'push' not in extras['hooks']:
583 587 return 0
584 588 _fix_hooks_executables(env.get('RC_INI_FILE'))
585 589
586 590 empty_commit_id = '0' * 40
587 591
588 592 detect_force_push = extras.get('detect_force_push')
589 593
590 594 for push_ref in rev_data:
591 595 # store our git-env which holds the temp store
592 596 push_ref['git_env'] = _get_git_env()
593 597 push_ref['pruned_sha'] = ''
594 598 if not detect_force_push:
595 599 # don't check for forced-push when we don't need to
596 600 continue
597 601
598 602 type_ = push_ref['type']
599 603 new_branch = push_ref['old_rev'] == empty_commit_id
600 604 delete_branch = push_ref['new_rev'] == empty_commit_id
601 605 if type_ == 'heads' and not (new_branch or delete_branch):
602 606 old_rev = push_ref['old_rev']
603 607 new_rev = push_ref['new_rev']
604 608 cmd = [settings.GIT_EXECUTABLE(), 'rev-list', old_rev, f'^{new_rev}']
605 609 stdout, stderr = subprocessio.run_command(
606 610 cmd, env=os.environ.copy())
607 611 # means we're having some non-reachable objects, this forced push was used
608 612 if stdout:
609 613 push_ref['pruned_sha'] = stdout.splitlines()
610 614
611 615 extras['hook_type'] = 'pre_receive'
612 616 extras['commit_ids'] = rev_data
613 617
614 618 stdout = sys.stdout
615 619 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
616 620
617 621 return status_code
618 622
619 623
620 624 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
621 625 """
622 626 Post push hook.
623 627
624 628 :return: status code of the hook. 0 for success.
625 629 """
626 630 extras = json.loads(env['RC_SCM_DATA'])
627 631 if 'push' not in extras['hooks']:
628 632 return 0
629 633
630 634 _fix_hooks_executables(env.get('RC_INI_FILE'))
631 635
632 636 rev_data = _parse_git_ref_lines(revision_lines)
633 637
634 638 git_revs = []
635 639
636 640 # N.B.(skreft): it is ok to just call git, as git before calling a
637 641 # subcommand sets the PATH environment variable so that it point to the
638 642 # correct version of the git executable.
639 643 empty_commit_id = '0' * 40
640 644 branches = []
641 645 tags = []
642 646 for push_ref in rev_data:
643 647 type_ = push_ref['type']
644 648
645 649 if type_ == 'heads':
646 650 # starting new branch case
647 651 if push_ref['old_rev'] == empty_commit_id:
648 652 push_ref_name = push_ref['name']
649 653
650 654 if push_ref_name not in branches:
651 655 branches.append(push_ref_name)
652 656
653 657 need_head_set = ''
654 658 with Repository(os.getcwd()) as repo:
655 659 try:
656 660 repo.head
657 661 except pygit2.GitError:
658 662 need_head_set = f'refs/heads/{push_ref_name}'
659 663
660 664 if need_head_set:
661 665 repo.set_head(need_head_set)
662 666 print(f"Setting default branch to {push_ref_name}")
663 667
664 668 cmd = [settings.GIT_EXECUTABLE(), 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
665 669 stdout, stderr = subprocessio.run_command(
666 670 cmd, env=os.environ.copy())
667 671 heads = safe_str(stdout)
668 672 heads = heads.replace(push_ref['ref'], '')
669 673 heads = ' '.join(head for head
670 674 in heads.splitlines() if head) or '.'
671 675 cmd = [settings.GIT_EXECUTABLE(), 'log', '--reverse',
672 676 '--pretty=format:%H', '--', push_ref['new_rev'],
673 677 '--not', heads]
674 678 stdout, stderr = subprocessio.run_command(
675 679 cmd, env=os.environ.copy())
676 680 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
677 681
678 682 # delete branch case
679 683 elif push_ref['new_rev'] == empty_commit_id:
680 684 git_revs.append(f'delete_branch=>{push_ref["name"]}')
681 685 else:
682 686 if push_ref['name'] not in branches:
683 687 branches.append(push_ref['name'])
684 688
685 689 cmd = [settings.GIT_EXECUTABLE(), 'log',
686 690 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
687 691 '--reverse', '--pretty=format:%H']
688 692 stdout, stderr = subprocessio.run_command(
689 693 cmd, env=os.environ.copy())
690 694 # we get bytes from stdout, we need str to be consistent
691 695 log_revs = list(map(ascii_str, stdout.splitlines()))
692 696 git_revs.extend(log_revs)
693 697
694 698 # Pure pygit2 impl. but still 2-3x slower :/
695 699 # results = []
696 700 #
697 701 # with Repository(os.getcwd()) as repo:
698 702 # repo_new_rev = repo[push_ref['new_rev']]
699 703 # repo_old_rev = repo[push_ref['old_rev']]
700 704 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
701 705 #
702 706 # for commit in walker:
703 707 # if commit.id == repo_old_rev.id:
704 708 # break
705 709 # results.append(commit.id.hex)
706 710 # # reverse the order, can't use GIT_SORT_REVERSE
707 711 # log_revs = results[::-1]
708 712
709 713 elif type_ == 'tags':
710 714 if push_ref['name'] not in tags:
711 715 tags.append(push_ref['name'])
712 716 git_revs.append(f'tag=>{push_ref["name"]}')
713 717
714 718 extras['hook_type'] = 'post_receive'
715 719 extras['commit_ids'] = git_revs
716 720 extras['new_refs'] = {
717 721 'branches': branches,
718 722 'bookmarks': [],
719 723 'tags': tags,
720 724 }
721 725
722 726 stdout = sys.stdout
723 727
724 728 if 'repo_size' in extras['hooks']:
725 729 try:
726 730 _call_hook('repo_size', extras, GitMessageWriter(stdout))
727 731 except Exception:
728 732 pass
729 733
730 734 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
731 735 return status_code
732 736
733 737
734 738 def get_extras_from_txn_id(repo_path, txn_id):
735 739 extras = get_txn_id_from_store(repo_path, txn_id)
736 740 return extras
737 741
738 742
739 743 def svn_pre_commit(repo_path, commit_data, env):
740 744
741 745 path, txn_id = commit_data
742 746 branches = []
743 747 tags = []
744 748
745 749 if env.get('RC_SCM_DATA'):
746 750 extras = json.loads(env['RC_SCM_DATA'])
747 751 else:
748 752 ini_path = env.get('RC_INI_FILE')
749 753 if ini_path:
750 754 _get_ini_settings(ini_path)
751 755 # fallback method to read from TXN-ID stored data
752 756 extras = get_extras_from_txn_id(path, txn_id)
753 757
754 758 if not extras:
755 759 raise ValueError('SVN-PRE-COMMIT: Failed to extract context data in called extras for hook execution')
756 760
757 761 if extras.get('rc_internal_commit'):
758 762 # special marker for internal commit, we don't call hooks client
759 763 return 0
760 764
761 765 extras['hook_type'] = 'pre_commit'
762 766 extras['commit_ids'] = [txn_id]
763 767 extras['txn_id'] = txn_id
764 768 extras['new_refs'] = {
765 769 'total_commits': 1,
766 770 'branches': branches,
767 771 'bookmarks': [],
768 772 'tags': tags,
769 773 }
770 774
771 775 return _call_hook('pre_push', extras, SvnMessageWriter())
772 776
773 777
774 778 def svn_post_commit(repo_path, commit_data, env):
775 779 """
776 780 commit_data is path, rev, txn_id
777 781 """
778 782
779 783 if len(commit_data) == 3:
780 784 path, commit_id, txn_id = commit_data
781 785 elif len(commit_data) == 2:
782 786 log.error('Failed to extract txn_id from commit_data using legacy method. '
783 787 'Some functionality might be limited')
784 788 path, commit_id = commit_data
785 789 txn_id = None
786 790 else:
787 791 return 0
788 792
789 793 branches = []
790 794 tags = []
791 795
792 796 if env.get('RC_SCM_DATA'):
793 797 extras = json.loads(env['RC_SCM_DATA'])
794 798 else:
795 799 ini_path = env.get('RC_INI_FILE')
796 800 if ini_path:
797 801 _get_ini_settings(ini_path)
798 802 # fallback method to read from TXN-ID stored data
799 803 extras = get_extras_from_txn_id(path, txn_id)
800 804
801 805 if not extras and txn_id:
802 806 raise ValueError('SVN-POST-COMMIT: Failed to extract context data in called extras for hook execution')
803 807
804 808 if extras.get('rc_internal_commit'):
805 809 # special marker for internal commit, we don't call hooks client
806 810 return 0
807 811
808 812 extras['hook_type'] = 'post_commit'
809 813 extras['commit_ids'] = [commit_id]
810 814 extras['txn_id'] = txn_id
811 815 extras['new_refs'] = {
812 816 'branches': branches,
813 817 'bookmarks': [],
814 818 'tags': tags,
815 819 'total_commits': 1,
816 820 }
817 821
818 822 if 'repo_size' in extras['hooks']:
819 823 try:
820 824 _call_hook('repo_size', extras, SvnMessageWriter())
821 825 except Exception:
822 826 pass
823 827
824 828 return _call_hook('post_push', extras, SvnMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now