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