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