##// END OF EJS Templates
hooks: handle errors before trying to fetch the output.
marcink -
r553:7b8645d6 default
parent child Browse files
Show More
@@ -1,657 +1,658 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 4 # Copyright (C) 2014-2018 RhodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 20 import io
21 21 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import importlib
26 26 import base64
27 27
28 28 from httplib import HTTPConnection
29 29
30 30
31 31 import mercurial.scmutil
32 32 import mercurial.node
33 33 import simplejson as json
34 34
35 35 from vcsserver import exceptions, subprocessio, settings
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class HooksHttpClient(object):
41 41 connection = None
42 42
43 43 def __init__(self, hooks_uri):
44 44 self.hooks_uri = hooks_uri
45 45
46 46 def __call__(self, method, extras):
47 47 connection = HTTPConnection(self.hooks_uri)
48 48 body = self._serialize(method, extras)
49 49 try:
50 50 connection.request('POST', '/', body)
51 51 except Exception:
52 52 log.error('Connection failed on %s', connection)
53 53 raise
54 54 response = connection.getresponse()
55 55 return json.loads(response.read())
56 56
57 57 def _serialize(self, hook_name, extras):
58 58 data = {
59 59 'method': hook_name,
60 60 'extras': extras
61 61 }
62 62 return json.dumps(data)
63 63
64 64
65 65 class HooksDummyClient(object):
66 66 def __init__(self, hooks_module):
67 67 self._hooks_module = importlib.import_module(hooks_module)
68 68
69 69 def __call__(self, hook_name, extras):
70 70 with self._hooks_module.Hooks() as hooks:
71 71 return getattr(hooks, hook_name)(extras)
72 72
73 73
74 74 class RemoteMessageWriter(object):
75 75 """Writer base class."""
76 76 def write(self, message):
77 77 raise NotImplementedError()
78 78
79 79
80 80 class HgMessageWriter(RemoteMessageWriter):
81 81 """Writer that knows how to send messages to mercurial clients."""
82 82
83 83 def __init__(self, ui):
84 84 self.ui = ui
85 85
86 86 def write(self, message):
87 87 # TODO: Check why the quiet flag is set by default.
88 88 old = self.ui.quiet
89 89 self.ui.quiet = False
90 90 self.ui.status(message.encode('utf-8'))
91 91 self.ui.quiet = old
92 92
93 93
94 94 class GitMessageWriter(RemoteMessageWriter):
95 95 """Writer that knows how to send messages to git clients."""
96 96
97 97 def __init__(self, stdout=None):
98 98 self.stdout = stdout or sys.stdout
99 99
100 100 def write(self, message):
101 101 self.stdout.write(message.encode('utf-8'))
102 102
103 103
104 104 class SvnMessageWriter(RemoteMessageWriter):
105 105 """Writer that knows how to send messages to svn clients."""
106 106
107 107 def __init__(self, stderr=None):
108 108 # SVN needs data sent to stderr for back-to-client messaging
109 109 self.stderr = stderr or sys.stderr
110 110
111 111 def write(self, message):
112 112 self.stderr.write(message.encode('utf-8'))
113 113
114 114
115 115 def _handle_exception(result):
116 116 exception_class = result.get('exception')
117 117 exception_traceback = result.get('exception_traceback')
118 118
119 119 if exception_traceback:
120 120 log.error('Got traceback from remote call:%s', exception_traceback)
121 121
122 122 if exception_class == 'HTTPLockedRC':
123 123 raise exceptions.RepositoryLockedException()(*result['exception_args'])
124 124 elif exception_class == 'HTTPBranchProtected':
125 125 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
126 126 elif exception_class == 'RepositoryError':
127 127 raise exceptions.VcsException()(*result['exception_args'])
128 128 elif exception_class:
129 129 raise Exception('Got remote exception "%s" with args "%s"' %
130 130 (exception_class, result['exception_args']))
131 131
132 132
133 133 def _get_hooks_client(extras):
134 134 if 'hooks_uri' in extras:
135 135 protocol = extras.get('hooks_protocol')
136 136 return HooksHttpClient(extras['hooks_uri'])
137 137 else:
138 138 return HooksDummyClient(extras['hooks_module'])
139 139
140 140
141 141 def _call_hook(hook_name, extras, writer):
142 142 hooks_client = _get_hooks_client(extras)
143 143 log.debug('Hooks, using client:%s', hooks_client)
144 144 result = hooks_client(hook_name, extras)
145 145 log.debug('Hooks got result: %s', result)
146
147 _handle_exception(result)
146 148 writer.write(result['output'])
147 _handle_exception(result)
148 149
149 150 return result['status']
150 151
151 152
152 153 def _extras_from_ui(ui):
153 154 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
154 155 if not hook_data:
155 156 # maybe it's inside environ ?
156 157 env_hook_data = os.environ.get('RC_SCM_DATA')
157 158 if env_hook_data:
158 159 hook_data = env_hook_data
159 160
160 161 extras = {}
161 162 if hook_data:
162 163 extras = json.loads(hook_data)
163 164 return extras
164 165
165 166
166 167 def _rev_range_hash(repo, node, check_heads=False):
167 168
168 169 commits = []
169 170 revs = []
170 171 start = repo[node].rev()
171 172 end = len(repo)
172 173 for rev in range(start, end):
173 174 revs.append(rev)
174 175 ctx = repo[rev]
175 176 commit_id = mercurial.node.hex(ctx.node())
176 177 branch = ctx.branch()
177 178 commits.append((commit_id, branch))
178 179
179 180 parent_heads = []
180 181 if check_heads:
181 182 parent_heads = _check_heads(repo, start, end, revs)
182 183 return commits, parent_heads
183 184
184 185
185 186 def _check_heads(repo, start, end, commits):
186 187 changelog = repo.changelog
187 188 parents = set()
188 189
189 190 for new_rev in commits:
190 191 for p in changelog.parentrevs(new_rev):
191 192 if p == mercurial.node.nullrev:
192 193 continue
193 194 if p < start:
194 195 parents.add(p)
195 196
196 197 for p in parents:
197 198 branch = repo[p].branch()
198 199 # The heads descending from that parent, on the same branch
199 200 parent_heads = set([p])
200 201 reachable = set([p])
201 202 for x in xrange(p + 1, end):
202 203 if repo[x].branch() != branch:
203 204 continue
204 205 for pp in changelog.parentrevs(x):
205 206 if pp in reachable:
206 207 reachable.add(x)
207 208 parent_heads.discard(pp)
208 209 parent_heads.add(x)
209 210 # More than one head? Suggest merging
210 211 if len(parent_heads) > 1:
211 212 return list(parent_heads)
212 213
213 214 return []
214 215
215 216
216 217 def repo_size(ui, repo, **kwargs):
217 218 extras = _extras_from_ui(ui)
218 219 return _call_hook('repo_size', extras, HgMessageWriter(ui))
219 220
220 221
221 222 def pre_pull(ui, repo, **kwargs):
222 223 extras = _extras_from_ui(ui)
223 224 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
224 225
225 226
226 227 def pre_pull_ssh(ui, repo, **kwargs):
227 228 extras = _extras_from_ui(ui)
228 229 if extras and extras.get('SSH'):
229 230 return pre_pull(ui, repo, **kwargs)
230 231 return 0
231 232
232 233
233 234 def post_pull(ui, repo, **kwargs):
234 235 extras = _extras_from_ui(ui)
235 236 return _call_hook('post_pull', extras, HgMessageWriter(ui))
236 237
237 238
238 239 def post_pull_ssh(ui, repo, **kwargs):
239 240 extras = _extras_from_ui(ui)
240 241 if extras and extras.get('SSH'):
241 242 return post_pull(ui, repo, **kwargs)
242 243 return 0
243 244
244 245
245 246 def pre_push(ui, repo, node=None, **kwargs):
246 247 """
247 248 Mercurial pre_push hook
248 249 """
249 250 extras = _extras_from_ui(ui)
250 251 detect_force_push = extras.get('detect_force_push')
251 252
252 253 rev_data = []
253 254 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
254 255 branches = collections.defaultdict(list)
255 256 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
256 257 for commit_id, branch in commits:
257 258 branches[branch].append(commit_id)
258 259
259 260 for branch, commits in branches.items():
260 261 old_rev = kwargs.get('node_last') or commits[0]
261 262 rev_data.append({
262 263 'old_rev': old_rev,
263 264 'new_rev': commits[-1],
264 265 'ref': '',
265 266 'type': 'branch',
266 267 'name': branch,
267 268 })
268 269
269 270 for push_ref in rev_data:
270 271 push_ref['multiple_heads'] = _heads
271 272
272 273 extras['commit_ids'] = rev_data
273 274 return _call_hook('pre_push', extras, HgMessageWriter(ui))
274 275
275 276
276 277 def pre_push_ssh(ui, repo, node=None, **kwargs):
277 278 extras = _extras_from_ui(ui)
278 279 if extras.get('SSH'):
279 280 return pre_push(ui, repo, node, **kwargs)
280 281
281 282 return 0
282 283
283 284
284 285 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
285 286 """
286 287 Mercurial pre_push hook for SSH
287 288 """
288 289 extras = _extras_from_ui(ui)
289 290 if extras.get('SSH'):
290 291 permission = extras['SSH_PERMISSIONS']
291 292
292 293 if 'repository.write' == permission or 'repository.admin' == permission:
293 294 return 0
294 295
295 296 # non-zero ret code
296 297 return 1
297 298
298 299 return 0
299 300
300 301
301 302 def post_push(ui, repo, node, **kwargs):
302 303 """
303 304 Mercurial post_push hook
304 305 """
305 306 extras = _extras_from_ui(ui)
306 307
307 308 commit_ids = []
308 309 branches = []
309 310 bookmarks = []
310 311 tags = []
311 312
312 313 commits, _heads = _rev_range_hash(repo, node)
313 314 for commit_id, branch in commits:
314 315 commit_ids.append(commit_id)
315 316 if branch not in branches:
316 317 branches.append(branch)
317 318
318 319 if hasattr(ui, '_rc_pushkey_branches'):
319 320 bookmarks = ui._rc_pushkey_branches
320 321
321 322 extras['commit_ids'] = commit_ids
322 323 extras['new_refs'] = {
323 324 'branches': branches,
324 325 'bookmarks': bookmarks,
325 326 'tags': tags
326 327 }
327 328
328 329 return _call_hook('post_push', extras, HgMessageWriter(ui))
329 330
330 331
331 332 def post_push_ssh(ui, repo, node, **kwargs):
332 333 """
333 334 Mercurial post_push hook for SSH
334 335 """
335 336 if _extras_from_ui(ui).get('SSH'):
336 337 return post_push(ui, repo, node, **kwargs)
337 338 return 0
338 339
339 340
340 341 def key_push(ui, repo, **kwargs):
341 342 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
342 343 # store new bookmarks in our UI object propagated later to post_push
343 344 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
344 345 return
345 346
346 347
347 348 # backward compat
348 349 log_pull_action = post_pull
349 350
350 351 # backward compat
351 352 log_push_action = post_push
352 353
353 354
354 355 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
355 356 """
356 357 Old hook name: keep here for backward compatibility.
357 358
358 359 This is only required when the installed git hooks are not upgraded.
359 360 """
360 361 pass
361 362
362 363
363 364 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
364 365 """
365 366 Old hook name: keep here for backward compatibility.
366 367
367 368 This is only required when the installed git hooks are not upgraded.
368 369 """
369 370 pass
370 371
371 372
372 373 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
373 374
374 375
375 376 def git_pre_pull(extras):
376 377 """
377 378 Pre pull hook.
378 379
379 380 :param extras: dictionary containing the keys defined in simplevcs
380 381 :type extras: dict
381 382
382 383 :return: status code of the hook. 0 for success.
383 384 :rtype: int
384 385 """
385 386 if 'pull' not in extras['hooks']:
386 387 return HookResponse(0, '')
387 388
388 389 stdout = io.BytesIO()
389 390 try:
390 391 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
391 392 except Exception as error:
392 393 status = 128
393 394 stdout.write('ERROR: %s\n' % str(error))
394 395
395 396 return HookResponse(status, stdout.getvalue())
396 397
397 398
398 399 def git_post_pull(extras):
399 400 """
400 401 Post pull hook.
401 402
402 403 :param extras: dictionary containing the keys defined in simplevcs
403 404 :type extras: dict
404 405
405 406 :return: status code of the hook. 0 for success.
406 407 :rtype: int
407 408 """
408 409 if 'pull' not in extras['hooks']:
409 410 return HookResponse(0, '')
410 411
411 412 stdout = io.BytesIO()
412 413 try:
413 414 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
414 415 except Exception as error:
415 416 status = 128
416 417 stdout.write('ERROR: %s\n' % error)
417 418
418 419 return HookResponse(status, stdout.getvalue())
419 420
420 421
421 422 def _parse_git_ref_lines(revision_lines):
422 423 rev_data = []
423 424 for revision_line in revision_lines or []:
424 425 old_rev, new_rev, ref = revision_line.strip().split(' ')
425 426 ref_data = ref.split('/', 2)
426 427 if ref_data[1] in ('tags', 'heads'):
427 428 rev_data.append({
428 429 'old_rev': old_rev,
429 430 'new_rev': new_rev,
430 431 'ref': ref,
431 432 'type': ref_data[1],
432 433 'name': ref_data[2],
433 434 })
434 435 return rev_data
435 436
436 437
437 438 def git_pre_receive(unused_repo_path, revision_lines, env):
438 439 """
439 440 Pre push hook.
440 441
441 442 :param extras: dictionary containing the keys defined in simplevcs
442 443 :type extras: dict
443 444
444 445 :return: status code of the hook. 0 for success.
445 446 :rtype: int
446 447 """
447 448 extras = json.loads(env['RC_SCM_DATA'])
448 449 rev_data = _parse_git_ref_lines(revision_lines)
449 450 if 'push' not in extras['hooks']:
450 451 return 0
451 452 empty_commit_id = '0' * 40
452 453
453 454 detect_force_push = extras.get('detect_force_push')
454 455
455 456 for push_ref in rev_data:
456 457 # store our git-env which holds the temp store
457 458 push_ref['git_env'] = [
458 459 (k, v) for k, v in os.environ.items() if k.startswith('GIT')]
459 460 push_ref['pruned_sha'] = ''
460 461 if not detect_force_push:
461 462 # don't check for forced-push when we don't need to
462 463 continue
463 464
464 465 type_ = push_ref['type']
465 466 new_branch = push_ref['old_rev'] == empty_commit_id
466 467 if type_ == 'heads' and not new_branch:
467 468 old_rev = push_ref['old_rev']
468 469 new_rev = push_ref['new_rev']
469 470 cmd = [settings.GIT_EXECUTABLE, 'rev-list',
470 471 old_rev, '^{}'.format(new_rev)]
471 472 stdout, stderr = subprocessio.run_command(
472 473 cmd, env=os.environ.copy())
473 474 # means we're having some non-reachable objects, this forced push
474 475 # was used
475 476 if stdout:
476 477 push_ref['pruned_sha'] = stdout.splitlines()
477 478
478 479 extras['commit_ids'] = rev_data
479 480 return _call_hook('pre_push', extras, GitMessageWriter())
480 481
481 482
482 483 def git_post_receive(unused_repo_path, revision_lines, env):
483 484 """
484 485 Post push hook.
485 486
486 487 :param extras: dictionary containing the keys defined in simplevcs
487 488 :type extras: dict
488 489
489 490 :return: status code of the hook. 0 for success.
490 491 :rtype: int
491 492 """
492 493 extras = json.loads(env['RC_SCM_DATA'])
493 494 if 'push' not in extras['hooks']:
494 495 return 0
495 496
496 497 rev_data = _parse_git_ref_lines(revision_lines)
497 498
498 499 git_revs = []
499 500
500 501 # N.B.(skreft): it is ok to just call git, as git before calling a
501 502 # subcommand sets the PATH environment variable so that it point to the
502 503 # correct version of the git executable.
503 504 empty_commit_id = '0' * 40
504 505 branches = []
505 506 tags = []
506 507 for push_ref in rev_data:
507 508 type_ = push_ref['type']
508 509
509 510 if type_ == 'heads':
510 511 if push_ref['old_rev'] == empty_commit_id:
511 512 # starting new branch case
512 513 if push_ref['name'] not in branches:
513 514 branches.append(push_ref['name'])
514 515
515 516 # Fix up head revision if needed
516 517 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
517 518 try:
518 519 subprocessio.run_command(cmd, env=os.environ.copy())
519 520 except Exception:
520 521 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
521 522 'refs/heads/%s' % push_ref['name']]
522 523 print("Setting default branch to %s" % push_ref['name'])
523 524 subprocessio.run_command(cmd, env=os.environ.copy())
524 525
525 526 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
526 527 '--format=%(refname)', 'refs/heads/*']
527 528 stdout, stderr = subprocessio.run_command(
528 529 cmd, env=os.environ.copy())
529 530 heads = stdout
530 531 heads = heads.replace(push_ref['ref'], '')
531 532 heads = ' '.join(head for head
532 533 in heads.splitlines() if head) or '.'
533 534 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
534 535 '--pretty=format:%H', '--', push_ref['new_rev'],
535 536 '--not', heads]
536 537 stdout, stderr = subprocessio.run_command(
537 538 cmd, env=os.environ.copy())
538 539 git_revs.extend(stdout.splitlines())
539 540 elif push_ref['new_rev'] == empty_commit_id:
540 541 # delete branch case
541 542 git_revs.append('delete_branch=>%s' % push_ref['name'])
542 543 else:
543 544 if push_ref['name'] not in branches:
544 545 branches.append(push_ref['name'])
545 546
546 547 cmd = [settings.GIT_EXECUTABLE, 'log',
547 548 '{old_rev}..{new_rev}'.format(**push_ref),
548 549 '--reverse', '--pretty=format:%H']
549 550 stdout, stderr = subprocessio.run_command(
550 551 cmd, env=os.environ.copy())
551 552 git_revs.extend(stdout.splitlines())
552 553 elif type_ == 'tags':
553 554 if push_ref['name'] not in tags:
554 555 tags.append(push_ref['name'])
555 556 git_revs.append('tag=>%s' % push_ref['name'])
556 557
557 558 extras['commit_ids'] = git_revs
558 559 extras['new_refs'] = {
559 560 'branches': branches,
560 561 'bookmarks': [],
561 562 'tags': tags,
562 563 }
563 564
564 565 if 'repo_size' in extras['hooks']:
565 566 try:
566 567 _call_hook('repo_size', extras, GitMessageWriter())
567 568 except:
568 569 pass
569 570
570 571 return _call_hook('post_push', extras, GitMessageWriter())
571 572
572 573
573 574 def _get_extras_from_txn_id(path, txn_id):
574 575 extras = {}
575 576 try:
576 577 cmd = ['svnlook', 'pget',
577 578 '-t', txn_id,
578 579 '--revprop', path, 'rc-scm-extras']
579 580 stdout, stderr = subprocessio.run_command(
580 581 cmd, env=os.environ.copy())
581 582 extras = json.loads(base64.urlsafe_b64decode(stdout))
582 583 except Exception:
583 584 log.exception('Failed to extract extras info from txn_id')
584 585
585 586 return extras
586 587
587 588
588 589 def svn_pre_commit(repo_path, commit_data, env):
589 590 path, txn_id = commit_data
590 591 branches = []
591 592 tags = []
592 593
593 594 if env.get('RC_SCM_DATA'):
594 595 extras = json.loads(env['RC_SCM_DATA'])
595 596 else:
596 597 # fallback method to read from TXN-ID stored data
597 598 extras = _get_extras_from_txn_id(path, txn_id)
598 599 if not extras:
599 600 return 0
600 601
601 602 extras['commit_ids'] = []
602 603 extras['txn_id'] = txn_id
603 604 extras['new_refs'] = {
604 605 'branches': branches,
605 606 'bookmarks': [],
606 607 'tags': tags,
607 608 }
608 609
609 610 return _call_hook('pre_push', extras, SvnMessageWriter())
610 611
611 612
612 613 def _get_extras_from_commit_id(commit_id, path):
613 614 extras = {}
614 615 try:
615 616 cmd = ['svnlook', 'pget',
616 617 '-r', commit_id,
617 618 '--revprop', path, 'rc-scm-extras']
618 619 stdout, stderr = subprocessio.run_command(
619 620 cmd, env=os.environ.copy())
620 621 extras = json.loads(base64.urlsafe_b64decode(stdout))
621 622 except Exception:
622 623 log.exception('Failed to extract extras info from commit_id')
623 624
624 625 return extras
625 626
626 627
627 628 def svn_post_commit(repo_path, commit_data, env):
628 629 """
629 630 commit_data is path, rev, txn_id
630 631 """
631 632 path, commit_id, txn_id = commit_data
632 633 branches = []
633 634 tags = []
634 635
635 636 if env.get('RC_SCM_DATA'):
636 637 extras = json.loads(env['RC_SCM_DATA'])
637 638 else:
638 639 # fallback method to read from TXN-ID stored data
639 640 extras = _get_extras_from_commit_id(commit_id, path)
640 641 if not extras:
641 642 return 0
642 643
643 644 extras['commit_ids'] = [commit_id]
644 645 extras['txn_id'] = txn_id
645 646 extras['new_refs'] = {
646 647 'branches': branches,
647 648 'bookmarks': [],
648 649 'tags': tags,
649 650 }
650 651
651 652 if 'repo_size' in extras['hooks']:
652 653 try:
653 654 _call_hook('repo_size', extras, SvnMessageWriter())
654 655 except Exception:
655 656 pass
656 657
657 658 return _call_hook('post_push', extras, SvnMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now