##// END OF EJS Templates
events: add event system for RepoEvents
dan -
r375:41f1288c default
parent child Browse files
Show More
@@ -0,0 +1,49 b''
1 # Copyright (C) 2016-2016 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
19 from pyramid.threadlocal import get_current_registry
20
21
22 class RhodecodeEvent(object):
23 """
24 Base event class for all Rhodecode events
25 """
26
27
28 def trigger(event):
29 """
30 Helper method to send an event. This wraps the pyramid logic to send an
31 event.
32 """
33 # For the first step we are using pyramids thread locals here. If the
34 # event mechanism works out as a good solution we should think about
35 # passing the registry as an argument to get rid of it.
36 registry = get_current_registry()
37 registry.notify(event)
38
39
40 from rhodecode.events.user import (
41 UserPreCreate, UserPreUpdate, UserRegistered
42 )
43
44 from rhodecode.events.repo import (
45 RepoPreCreateEvent, RepoCreatedEvent,
46 RepoPreDeleteEvent, RepoDeletedEvent,
47 RepoPrePushEvent, RepoPushEvent,
48 RepoPrePullEvent, RepoPullEvent,
49 ) No newline at end of file
@@ -0,0 +1,115 b''
1 # Copyright (C) 2016-2016 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
19 from rhodecode.model.db import Repository, Session
20 from rhodecode.events import RhodecodeEvent
21
22
23 class RepoEvent(RhodecodeEvent):
24 """
25 Base class for events acting on a repository.
26
27 :param repo: a :class:`Repository` instance
28 """
29 def __init__(self, repo):
30 self.repo = repo
31
32
33 class RepoPreCreateEvent(RepoEvent):
34 """
35 An instance of this class is emitted as an :term:`event` before a repo is
36 created.
37
38 :param repo_name: repository name
39 """
40 name = 'repo-pre-create'
41
42
43 class RepoCreatedEvent(RepoEvent):
44 """
45 An instance of this class is emitted as an :term:`event` whenever a repo is
46 created.
47 """
48 name = 'repo-created'
49
50
51 class RepoPreDeleteEvent(RepoEvent):
52 """
53 An instance of this class is emitted as an :term:`event` whenever a repo is
54 created.
55 """
56 name = 'repo-pre-delete'
57
58
59 class RepoDeletedEvent(RepoEvent):
60 """
61 An instance of this class is emitted as an :term:`event` whenever a repo is
62 created.
63 """
64 name = 'repo-deleted'
65
66
67 class RepoVCSEvent(RepoEvent):
68 """
69 Base class for events triggered by the VCS
70 """
71 def __init__(self, repo_name, extras):
72 self.repo = Repository.get_by_repo_name(repo_name)
73 if not self.repo:
74 raise Exception('repo by this name %s does not exist' % repo_name)
75 self.extras = extras
76 super(RepoVCSEvent, self).__init__(self.repo)
77
78
79 class RepoPrePullEvent(RepoVCSEvent):
80 """
81 An instance of this class is emitted as an :term:`event` before commits
82 are pulled from a repo.
83 """
84 name = 'repo-pre-pull'
85
86
87 class RepoPullEvent(RepoVCSEvent):
88 """
89 An instance of this class is emitted as an :term:`event` after commits
90 are pulled from a repo.
91 """
92 name = 'repo-pull'
93
94
95 class RepoPrePushEvent(RepoVCSEvent):
96 """
97 An instance of this class is emitted as an :term:`event` before commits
98 are pushed to a repo.
99 """
100 name = 'repo-pre-push'
101
102
103 class RepoPushEvent(RepoVCSEvent):
104 """
105 An instance of this class is emitted as an :term:`event` after commits
106 are pushed to a repo.
107
108 :param extras: (optional) dict of data from proxied VCS actions
109 """
110 name = 'repo-push'
111
112 def __init__(self, repo_name, pushed_commit_ids, extras):
113 super(RepoPushEvent, self).__init__(repo_name, extras)
114 self.pushed_commit_ids = pushed_commit_ids
115
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -0,0 +1,38 b''
1 # Copyright (C) 2016-2016 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
19 import mock
20 import decorator
21
22
23 def assert_fires_events(*expected_events):
24 """ Testing decorator to check if the function fires events in order """
25 def deco(func):
26 def wrapper(func, *args, **kwargs):
27 with mock.patch('rhodecode.events.trigger') as mock_trigger:
28 result = func(*args, **kwargs)
29
30 captured_events = []
31 for call in mock_trigger.call_args_list:
32 event = call[0][0]
33 captured_events.append(type(event))
34
35 assert set(captured_events) == set(expected_events)
36 return result
37 return decorator.decorator(wrapper, func)
38 return deco No newline at end of file
@@ -0,0 +1,68 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.tests.events.conftest import assert_fires_events
24
25 from rhodecode.lib import hooks_base, utils2
26 from rhodecode.model.repo import RepoModel
27 from rhodecode.events.repo import (
28 RepoPrePullEvent, RepoPullEvent,
29 RepoPrePushEvent, RepoPushEvent,
30 RepoPreCreateEvent, RepoCreatedEvent,
31 RepoPreDeleteEvent, RepoDeletedEvent,
32 )
33
34
35 @pytest.fixture
36 def scm_extras(user_regular, repo_stub):
37 extras = utils2.AttributeDict({
38 'ip': '127.0.0.1',
39 'username': user_regular.username,
40 'action': '',
41 'repository': repo_stub.repo_name,
42 'scm': repo_stub.scm_instance().alias,
43 'config': '',
44 'server_url': 'http://example.com',
45 'make_lock': None,
46 'locked_by': [None],
47 'commit_ids': ['a' * 40] * 3,
48 })
49 return extras
50
51
52 @assert_fires_events(
53 RepoPreCreateEvent, RepoCreatedEvent, RepoPreDeleteEvent, RepoDeletedEvent)
54 def test_create_delete_repo_fires_events(backend):
55 repo = backend.create_repo()
56 RepoModel().delete(repo)
57
58
59 @assert_fires_events(RepoPrePushEvent, RepoPushEvent)
60 def test_pull_fires_events(scm_extras):
61 hooks_base.pre_push(scm_extras)
62 hooks_base.post_push(scm_extras)
63
64
65 @assert_fires_events(RepoPrePullEvent, RepoPullEvent)
66 def test_push_fires_events(scm_extras):
67 hooks_base.pre_pull(scm_extras)
68 hooks_base.post_pull(scm_extras)
1 NO CONTENT: file renamed from rhodecode/interfaces.py to rhodecode/events/interfaces.py
@@ -17,12 +17,13 b''
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 from zope.interface import implementer
20 from rhodecode.interfaces import (
20 from rhodecode.events import RhodecodeEvent
21 from rhodecode.events.interfaces import (
21 22 IUserRegistered, IUserPreCreate, IUserPreUpdate)
22 23
23 24
24 25 @implementer(IUserRegistered)
25 class UserRegistered(object):
26 class UserRegistered(RhodecodeEvent):
26 27 """
27 28 An instance of this class is emitted as an :term:`event` whenever a user
28 29 account is registered.
@@ -33,7 +34,7 b' class UserRegistered(object):'
33 34
34 35
35 36 @implementer(IUserPreCreate)
36 class UserPreCreate(object):
37 class UserPreCreate(RhodecodeEvent):
37 38 """
38 39 An instance of this class is emitted as an :term:`event` before a new user
39 40 object is created.
@@ -43,7 +44,7 b' class UserPreCreate(object):'
43 44
44 45
45 46 @implementer(IUserPreUpdate)
46 class UserPreUpdate(object):
47 class UserPreUpdate(RhodecodeEvent):
47 48 """
48 49 An instance of this class is emitted as an :term:`event` before a user
49 50 object is updated.
@@ -27,6 +27,7 b' import os'
27 27 import collections
28 28
29 29 import rhodecode
30 from rhodecode import events
30 31 from rhodecode.lib import helpers as h
31 32 from rhodecode.lib.utils import action_logger
32 33 from rhodecode.lib.utils2 import safe_str
@@ -102,6 +103,9 b' def pre_push(extras):'
102 103 # Calling hooks after checking the lock, for consistent behavior
103 104 pre_push_extension(repo_store_path=Repository.base_path(), **extras)
104 105
106 events.trigger(events.RepoPrePushEvent(repo_name=extras.repository,
107 extras=extras))
108
105 109 return HookResponse(0, output)
106 110
107 111
@@ -128,6 +132,8 b' def pre_pull(extras):'
128 132
129 133 # Calling hooks after checking the lock, for consistent behavior
130 134 pre_pull_extension(**extras)
135 events.trigger(events.RepoPrePullEvent(repo_name=extras.repository,
136 extras=extras))
131 137
132 138 return HookResponse(0, output)
133 139
@@ -138,6 +144,8 b' def post_pull(extras):'
138 144 action = 'pull'
139 145 action_logger(user, action, extras.repository, extras.ip, commit=True)
140 146
147 events.trigger(events.RepoPullEvent(repo_name=extras.repository,
148 extras=extras))
141 149 # extension hook call
142 150 post_pull_extension(**extras)
143 151
@@ -171,6 +179,10 b' def post_push(extras):'
171 179 action_logger(
172 180 extras.username, action, extras.repository, extras.ip, commit=True)
173 181
182 events.trigger(events.RepoPushEvent(repo_name=extras.repository,
183 pushed_commit_ids=commit_ids,
184 extras=extras))
185
174 186 # extension hook call
175 187 post_push_extension(
176 188 repo_store_path=Repository.base_path(),
@@ -145,17 +145,6 b' class BaseModel(object):'
145 145 return self._get_instance(
146 146 db.Permission, permission, callback=db.Permission.get_by_key)
147 147
148 def send_event(self, event):
149 """
150 Helper method to send an event. This wraps the pyramid logic to send an
151 event.
152 """
153 # For the first step we are using pyramids thread locals here. If the
154 # event mechanism works out as a good solution we should think about
155 # passing the registry into the constructor to get rid of it.
156 registry = get_current_registry()
157 registry.notify(event)
158
159 148 @classmethod
160 149 def get_all(cls):
161 150 """
@@ -34,6 +34,7 b' from sqlalchemy.sql import func'
34 34 from sqlalchemy.sql.expression import true, or_
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36
37 from rhodecode import events
37 38 from rhodecode.lib import helpers as h
38 39 from rhodecode.lib.auth import HasUserGroupPermissionAny
39 40 from rhodecode.lib.caching_query import FromCache
@@ -470,6 +471,8 b' class RepoModel(BaseModel):'
470 471 parent_repo = fork_of
471 472 new_repo.fork = parent_repo
472 473
474 events.trigger(events.RepoPreCreateEvent(new_repo))
475
473 476 self.sa.add(new_repo)
474 477
475 478 EMPTY_PERM = 'repository.none'
@@ -525,11 +528,13 b' class RepoModel(BaseModel):'
525 528 # now automatically start following this repository as owner
526 529 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
527 530 owner.user_id)
531
528 532 # we need to flush here, in order to check if database won't
529 533 # throw any exceptions, create filesystem dirs at the very end
530 534 self.sa.flush()
535 events.trigger(events.RepoCreatedEvent(new_repo))
536 return new_repo
531 537
532 return new_repo
533 538 except Exception:
534 539 log.error(traceback.format_exc())
535 540 raise
@@ -633,6 +638,7 b' class RepoModel(BaseModel):'
633 638 raise AttachedForksError()
634 639
635 640 old_repo_dict = repo.get_dict()
641 events.trigger(events.RepoPreDeleteEvent(repo))
636 642 try:
637 643 self.sa.delete(repo)
638 644 if fs_remove:
@@ -644,6 +650,7 b' class RepoModel(BaseModel):'
644 650 'deleted_on': time.time(),
645 651 })
646 652 log_delete_repository(**old_repo_dict)
653 events.trigger(events.RepoDeletedEvent(repo))
647 654 except Exception:
648 655 log.error(traceback.format_exc())
649 656 raise
@@ -32,7 +32,7 b' import ipaddress'
32 32 from sqlalchemy.exc import DatabaseError
33 33 from sqlalchemy.sql.expression import true, false
34 34
35 from rhodecode.events import UserPreCreate, UserPreUpdate
35 from rhodecode import events
36 36 from rhodecode.lib.utils2 import (
37 37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 38 AttributeDict)
@@ -270,12 +270,12 b' class UserModel(BaseModel):'
270 270 # raises UserCreationError if it's not allowed for any reason to
271 271 # create new active user, this also executes pre-create hooks
272 272 check_allowed_create_user(user_data, cur_user, strict_check=True)
273 self.send_event(UserPreCreate(user_data))
273 events.trigger(events.UserPreCreate(user_data))
274 274 new_user = User()
275 275 edit = False
276 276 else:
277 277 log.debug('updating user %s', username)
278 self.send_event(UserPreUpdate(user, user_data))
278 events.trigger(events.UserPreUpdate(user, user_data))
279 279 new_user = user
280 280 edit = True
281 281
@@ -28,12 +28,12 b' from rhodecode.lib import hooks_base, ut'
28 28 action_logger=mock.Mock(),
29 29 post_push_extension=mock.Mock(),
30 30 Repository=mock.Mock())
31 def test_post_push_truncates_commits():
31 def test_post_push_truncates_commits(user_regular, repo_stub):
32 32 extras = {
33 33 'ip': '127.0.0.1',
34 'username': 'test',
34 'username': user_regular.username,
35 35 'action': 'push_local',
36 'repository': 'test',
36 'repository': repo_stub.repo_name,
37 37 'scm': 'git',
38 38 'config': '',
39 39 'server_url': 'http://example.com',
@@ -271,6 +271,7 b' class TestPullRequestModel:'
271 271 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
272 272 MergeFailureReason.NONE)
273 273
274 merge_extras['repository'] = pull_request.target_repo.repo_name
274 275 PullRequestModel().merge(
275 276 pull_request, pull_request.author, extras=merge_extras)
276 277
@@ -308,6 +309,7 b' class TestPullRequestModel:'
308 309 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
309 310 MergeFailureReason.MERGE_FAILED)
310 311
312 merge_extras['repository'] = pull_request.target_repo.repo_name
311 313 PullRequestModel().merge(
312 314 pull_request, pull_request.author, extras=merge_extras)
313 315
@@ -364,6 +366,7 b' class TestIntegrationMerge(object):'
364 366 pull_request = pr_util.create_pull_request(
365 367 approved=True, mergeable=True)
366 368 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
369 merge_extras['repository'] = pull_request.target_repo.repo_name
367 370 Session().commit()
368 371
369 372 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
@@ -379,6 +382,7 b' class TestIntegrationMerge(object):'
379 382 pull_request = pr_util.create_pull_request(
380 383 approved=True, mergeable=True)
381 384 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
385 merge_extras['repository'] = pull_request.target_repo.repo_name
382 386 Session().commit()
383 387
384 388 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
@@ -400,6 +404,7 b' class TestIntegrationMerge(object):'
400 404 # all data is pre-computed, that's why just updating the DB is not
401 405 # enough.
402 406 merge_extras['locked_by'] = locked_by
407 merge_extras['repository'] = pull_request.target_repo.repo_name
403 408 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
404 409 Session().commit()
405 410 merge_status = PullRequestModel().merge(
General Comments 0
You need to be logged in to leave comments. Login now