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