##// END OF EJS Templates
git-hooks: store git-env into hooks, so we can use the sandbox storage area, and execute...
marcink -
r510:ef7f8525 default
parent child Browse files
Show More
@@ -1,653 +1,657 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 146 writer.write(result['output'])
147 147 _handle_exception(result)
148 148
149 149 return result['status']
150 150
151 151
152 152 def _extras_from_ui(ui):
153 153 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
154 154 if not hook_data:
155 155 # maybe it's inside environ ?
156 156 env_hook_data = os.environ.get('RC_SCM_DATA')
157 157 if env_hook_data:
158 158 hook_data = env_hook_data
159 159
160 160 extras = {}
161 161 if hook_data:
162 162 extras = json.loads(hook_data)
163 163 return extras
164 164
165 165
166 166 def _rev_range_hash(repo, node, check_heads=False):
167 167
168 168 commits = []
169 169 revs = []
170 170 start = repo[node].rev()
171 171 end = len(repo)
172 172 for rev in range(start, end):
173 173 revs.append(rev)
174 174 ctx = repo[rev]
175 175 commit_id = mercurial.node.hex(ctx.node())
176 176 branch = ctx.branch()
177 177 commits.append((commit_id, branch))
178 178
179 179 parent_heads = []
180 180 if check_heads:
181 181 parent_heads = _check_heads(repo, start, end, revs)
182 182 return commits, parent_heads
183 183
184 184
185 185 def _check_heads(repo, start, end, commits):
186 186 changelog = repo.changelog
187 187 parents = set()
188 188
189 189 for new_rev in commits:
190 190 for p in changelog.parentrevs(new_rev):
191 191 if p == mercurial.node.nullrev:
192 192 continue
193 193 if p < start:
194 194 parents.add(p)
195 195
196 196 for p in parents:
197 197 branch = repo[p].branch()
198 198 # The heads descending from that parent, on the same branch
199 199 parent_heads = set([p])
200 200 reachable = set([p])
201 201 for x in xrange(p + 1, end):
202 202 if repo[x].branch() != branch:
203 203 continue
204 204 for pp in changelog.parentrevs(x):
205 205 if pp in reachable:
206 206 reachable.add(x)
207 207 parent_heads.discard(pp)
208 208 parent_heads.add(x)
209 209 # More than one head? Suggest merging
210 210 if len(parent_heads) > 1:
211 211 return list(parent_heads)
212 212
213 213 return []
214 214
215 215
216 216 def repo_size(ui, repo, **kwargs):
217 217 extras = _extras_from_ui(ui)
218 218 return _call_hook('repo_size', extras, HgMessageWriter(ui))
219 219
220 220
221 221 def pre_pull(ui, repo, **kwargs):
222 222 extras = _extras_from_ui(ui)
223 223 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
224 224
225 225
226 226 def pre_pull_ssh(ui, repo, **kwargs):
227 227 extras = _extras_from_ui(ui)
228 228 if extras and extras.get('SSH'):
229 229 return pre_pull(ui, repo, **kwargs)
230 230 return 0
231 231
232 232
233 233 def post_pull(ui, repo, **kwargs):
234 234 extras = _extras_from_ui(ui)
235 235 return _call_hook('post_pull', extras, HgMessageWriter(ui))
236 236
237 237
238 238 def post_pull_ssh(ui, repo, **kwargs):
239 239 extras = _extras_from_ui(ui)
240 240 if extras and extras.get('SSH'):
241 241 return post_pull(ui, repo, **kwargs)
242 242 return 0
243 243
244 244
245 245 def pre_push(ui, repo, node=None, **kwargs):
246 246 """
247 247 Mercurial pre_push hook
248 248 """
249 249 extras = _extras_from_ui(ui)
250 250 detect_force_push = extras.get('detect_force_push')
251 251
252 252 rev_data = []
253 253 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
254 254 branches = collections.defaultdict(list)
255 255 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
256 256 for commit_id, branch in commits:
257 257 branches[branch].append(commit_id)
258 258
259 259 for branch, commits in branches.items():
260 260 old_rev = kwargs.get('node_last') or commits[0]
261 261 rev_data.append({
262 262 'old_rev': old_rev,
263 263 'new_rev': commits[-1],
264 264 'ref': '',
265 265 'type': 'branch',
266 266 'name': branch,
267 267 })
268 268
269 269 for push_ref in rev_data:
270 270 push_ref['multiple_heads'] = _heads
271 271
272 272 extras['commit_ids'] = rev_data
273 273 return _call_hook('pre_push', extras, HgMessageWriter(ui))
274 274
275 275
276 276 def pre_push_ssh(ui, repo, node=None, **kwargs):
277 if _extras_from_ui(ui).get('SSH'):
277 extras = _extras_from_ui(ui)
278 if extras.get('SSH'):
278 279 return pre_push(ui, repo, node, **kwargs)
279 280
280 281 return 0
281 282
282 283
283 284 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
284 285 """
285 286 Mercurial pre_push hook for SSH
286 287 """
287 288 extras = _extras_from_ui(ui)
288 289 if extras.get('SSH'):
289 290 permission = extras['SSH_PERMISSIONS']
290 291
291 292 if 'repository.write' == permission or 'repository.admin' == permission:
292 293 return 0
293 294
294 295 # non-zero ret code
295 296 return 1
296 297
297 298 return 0
298 299
299 300
300 301 def post_push(ui, repo, node, **kwargs):
301 302 """
302 303 Mercurial post_push hook
303 304 """
304 305 extras = _extras_from_ui(ui)
305 306
306 307 commit_ids = []
307 308 branches = []
308 309 bookmarks = []
309 310 tags = []
310 311
311 312 commits, _heads = _rev_range_hash(repo, node)
312 313 for commit_id, branch in commits:
313 314 commit_ids.append(commit_id)
314 315 if branch not in branches:
315 316 branches.append(branch)
316 317
317 318 if hasattr(ui, '_rc_pushkey_branches'):
318 319 bookmarks = ui._rc_pushkey_branches
319 320
320 321 extras['commit_ids'] = commit_ids
321 322 extras['new_refs'] = {
322 323 'branches': branches,
323 324 'bookmarks': bookmarks,
324 325 'tags': tags
325 326 }
326 327
327 328 return _call_hook('post_push', extras, HgMessageWriter(ui))
328 329
329 330
330 331 def post_push_ssh(ui, repo, node, **kwargs):
331 332 """
332 333 Mercurial post_push hook for SSH
333 334 """
334 335 if _extras_from_ui(ui).get('SSH'):
335 336 return post_push(ui, repo, node, **kwargs)
336 337 return 0
337 338
338 339
339 340 def key_push(ui, repo, **kwargs):
340 341 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
341 342 # store new bookmarks in our UI object propagated later to post_push
342 343 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
343 344 return
344 345
345 346
346 347 # backward compat
347 348 log_pull_action = post_pull
348 349
349 350 # backward compat
350 351 log_push_action = post_push
351 352
352 353
353 354 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
354 355 """
355 356 Old hook name: keep here for backward compatibility.
356 357
357 358 This is only required when the installed git hooks are not upgraded.
358 359 """
359 360 pass
360 361
361 362
362 363 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
363 364 """
364 365 Old hook name: keep here for backward compatibility.
365 366
366 367 This is only required when the installed git hooks are not upgraded.
367 368 """
368 369 pass
369 370
370 371
371 372 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
372 373
373 374
374 375 def git_pre_pull(extras):
375 376 """
376 377 Pre pull hook.
377 378
378 379 :param extras: dictionary containing the keys defined in simplevcs
379 380 :type extras: dict
380 381
381 382 :return: status code of the hook. 0 for success.
382 383 :rtype: int
383 384 """
384 385 if 'pull' not in extras['hooks']:
385 386 return HookResponse(0, '')
386 387
387 388 stdout = io.BytesIO()
388 389 try:
389 390 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
390 391 except Exception as error:
391 392 status = 128
392 393 stdout.write('ERROR: %s\n' % str(error))
393 394
394 395 return HookResponse(status, stdout.getvalue())
395 396
396 397
397 398 def git_post_pull(extras):
398 399 """
399 400 Post pull hook.
400 401
401 402 :param extras: dictionary containing the keys defined in simplevcs
402 403 :type extras: dict
403 404
404 405 :return: status code of the hook. 0 for success.
405 406 :rtype: int
406 407 """
407 408 if 'pull' not in extras['hooks']:
408 409 return HookResponse(0, '')
409 410
410 411 stdout = io.BytesIO()
411 412 try:
412 413 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
413 414 except Exception as error:
414 415 status = 128
415 416 stdout.write('ERROR: %s\n' % error)
416 417
417 418 return HookResponse(status, stdout.getvalue())
418 419
419 420
420 421 def _parse_git_ref_lines(revision_lines):
421 422 rev_data = []
422 423 for revision_line in revision_lines or []:
423 424 old_rev, new_rev, ref = revision_line.strip().split(' ')
424 425 ref_data = ref.split('/', 2)
425 426 if ref_data[1] in ('tags', 'heads'):
426 427 rev_data.append({
427 428 'old_rev': old_rev,
428 429 'new_rev': new_rev,
429 430 'ref': ref,
430 431 'type': ref_data[1],
431 432 'name': ref_data[2],
432 433 })
433 434 return rev_data
434 435
435 436
436 437 def git_pre_receive(unused_repo_path, revision_lines, env):
437 438 """
438 439 Pre push hook.
439 440
440 441 :param extras: dictionary containing the keys defined in simplevcs
441 442 :type extras: dict
442 443
443 444 :return: status code of the hook. 0 for success.
444 445 :rtype: int
445 446 """
446 447 extras = json.loads(env['RC_SCM_DATA'])
447 448 rev_data = _parse_git_ref_lines(revision_lines)
448 449 if 'push' not in extras['hooks']:
449 450 return 0
450 451 empty_commit_id = '0' * 40
451 452
452 453 detect_force_push = extras.get('detect_force_push')
453 454
454 455 for push_ref in rev_data:
456 # store our git-env which holds the temp store
457 push_ref['git_env'] = [
458 (k, v) for k, v in os.environ.items() if k.startswith('GIT')]
455 459 push_ref['pruned_sha'] = ''
456 460 if not detect_force_push:
457 461 # don't check for forced-push when we don't need to
458 462 continue
459 463
460 464 type_ = push_ref['type']
461 465 new_branch = push_ref['old_rev'] == empty_commit_id
462 466 if type_ == 'heads' and not new_branch:
463 467 old_rev = push_ref['old_rev']
464 468 new_rev = push_ref['new_rev']
465 469 cmd = [settings.GIT_EXECUTABLE, 'rev-list',
466 470 old_rev, '^{}'.format(new_rev)]
467 471 stdout, stderr = subprocessio.run_command(
468 472 cmd, env=os.environ.copy())
469 473 # means we're having some non-reachable objects, this forced push
470 474 # was used
471 475 if stdout:
472 476 push_ref['pruned_sha'] = stdout.splitlines()
473 477
474 478 extras['commit_ids'] = rev_data
475 479 return _call_hook('pre_push', extras, GitMessageWriter())
476 480
477 481
478 482 def git_post_receive(unused_repo_path, revision_lines, env):
479 483 """
480 484 Post push hook.
481 485
482 486 :param extras: dictionary containing the keys defined in simplevcs
483 487 :type extras: dict
484 488
485 489 :return: status code of the hook. 0 for success.
486 490 :rtype: int
487 491 """
488 492 extras = json.loads(env['RC_SCM_DATA'])
489 493 if 'push' not in extras['hooks']:
490 494 return 0
491 495
492 496 rev_data = _parse_git_ref_lines(revision_lines)
493 497
494 498 git_revs = []
495 499
496 500 # N.B.(skreft): it is ok to just call git, as git before calling a
497 501 # subcommand sets the PATH environment variable so that it point to the
498 502 # correct version of the git executable.
499 503 empty_commit_id = '0' * 40
500 504 branches = []
501 505 tags = []
502 506 for push_ref in rev_data:
503 507 type_ = push_ref['type']
504 508
505 509 if type_ == 'heads':
506 510 if push_ref['old_rev'] == empty_commit_id:
507 511 # starting new branch case
508 512 if push_ref['name'] not in branches:
509 513 branches.append(push_ref['name'])
510 514
511 515 # Fix up head revision if needed
512 516 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
513 517 try:
514 518 subprocessio.run_command(cmd, env=os.environ.copy())
515 519 except Exception:
516 520 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
517 521 'refs/heads/%s' % push_ref['name']]
518 522 print("Setting default branch to %s" % push_ref['name'])
519 523 subprocessio.run_command(cmd, env=os.environ.copy())
520 524
521 525 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
522 526 '--format=%(refname)', 'refs/heads/*']
523 527 stdout, stderr = subprocessio.run_command(
524 528 cmd, env=os.environ.copy())
525 529 heads = stdout
526 530 heads = heads.replace(push_ref['ref'], '')
527 531 heads = ' '.join(head for head
528 532 in heads.splitlines() if head) or '.'
529 533 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
530 534 '--pretty=format:%H', '--', push_ref['new_rev'],
531 535 '--not', heads]
532 536 stdout, stderr = subprocessio.run_command(
533 537 cmd, env=os.environ.copy())
534 538 git_revs.extend(stdout.splitlines())
535 539 elif push_ref['new_rev'] == empty_commit_id:
536 540 # delete branch case
537 541 git_revs.append('delete_branch=>%s' % push_ref['name'])
538 542 else:
539 543 if push_ref['name'] not in branches:
540 544 branches.append(push_ref['name'])
541 545
542 546 cmd = [settings.GIT_EXECUTABLE, 'log',
543 547 '{old_rev}..{new_rev}'.format(**push_ref),
544 548 '--reverse', '--pretty=format:%H']
545 549 stdout, stderr = subprocessio.run_command(
546 550 cmd, env=os.environ.copy())
547 551 git_revs.extend(stdout.splitlines())
548 552 elif type_ == 'tags':
549 553 if push_ref['name'] not in tags:
550 554 tags.append(push_ref['name'])
551 555 git_revs.append('tag=>%s' % push_ref['name'])
552 556
553 557 extras['commit_ids'] = git_revs
554 558 extras['new_refs'] = {
555 559 'branches': branches,
556 560 'bookmarks': [],
557 561 'tags': tags,
558 562 }
559 563
560 564 if 'repo_size' in extras['hooks']:
561 565 try:
562 566 _call_hook('repo_size', extras, GitMessageWriter())
563 567 except:
564 568 pass
565 569
566 570 return _call_hook('post_push', extras, GitMessageWriter())
567 571
568 572
569 573 def _get_extras_from_txn_id(path, txn_id):
570 574 extras = {}
571 575 try:
572 576 cmd = ['svnlook', 'pget',
573 577 '-t', txn_id,
574 578 '--revprop', path, 'rc-scm-extras']
575 579 stdout, stderr = subprocessio.run_command(
576 580 cmd, env=os.environ.copy())
577 581 extras = json.loads(base64.urlsafe_b64decode(stdout))
578 582 except Exception:
579 583 log.exception('Failed to extract extras info from txn_id')
580 584
581 585 return extras
582 586
583 587
584 588 def svn_pre_commit(repo_path, commit_data, env):
585 589 path, txn_id = commit_data
586 590 branches = []
587 591 tags = []
588 592
589 593 if env.get('RC_SCM_DATA'):
590 594 extras = json.loads(env['RC_SCM_DATA'])
591 595 else:
592 596 # fallback method to read from TXN-ID stored data
593 597 extras = _get_extras_from_txn_id(path, txn_id)
594 598 if not extras:
595 599 return 0
596 600
597 601 extras['commit_ids'] = []
598 602 extras['txn_id'] = txn_id
599 603 extras['new_refs'] = {
600 604 'branches': branches,
601 605 'bookmarks': [],
602 606 'tags': tags,
603 607 }
604 608
605 609 return _call_hook('pre_push', extras, SvnMessageWriter())
606 610
607 611
608 612 def _get_extras_from_commit_id(commit_id, path):
609 613 extras = {}
610 614 try:
611 615 cmd = ['svnlook', 'pget',
612 616 '-r', commit_id,
613 617 '--revprop', path, 'rc-scm-extras']
614 618 stdout, stderr = subprocessio.run_command(
615 619 cmd, env=os.environ.copy())
616 620 extras = json.loads(base64.urlsafe_b64decode(stdout))
617 621 except Exception:
618 622 log.exception('Failed to extract extras info from commit_id')
619 623
620 624 return extras
621 625
622 626
623 627 def svn_post_commit(repo_path, commit_data, env):
624 628 """
625 629 commit_data is path, rev, txn_id
626 630 """
627 631 path, commit_id, txn_id = commit_data
628 632 branches = []
629 633 tags = []
630 634
631 635 if env.get('RC_SCM_DATA'):
632 636 extras = json.loads(env['RC_SCM_DATA'])
633 637 else:
634 638 # fallback method to read from TXN-ID stored data
635 639 extras = _get_extras_from_commit_id(commit_id, path)
636 640 if not extras:
637 641 return 0
638 642
639 643 extras['commit_ids'] = [commit_id]
640 644 extras['txn_id'] = txn_id
641 645 extras['new_refs'] = {
642 646 'branches': branches,
643 647 'bookmarks': [],
644 648 'tags': tags,
645 649 }
646 650
647 651 if 'repo_size' in extras['hooks']:
648 652 try:
649 653 _call_hook('repo_size', extras, SvnMessageWriter())
650 654 except Exception:
651 655 pass
652 656
653 657 return _call_hook('post_push', extras, SvnMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now