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