##// 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
NO CONTENT: file renamed from rhodecode/interfaces.py to rhodecode/events/interfaces.py
@@ -17,12 +17,13 b''
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 from zope.interface import implementer
19 from zope.interface import implementer
20 from rhodecode.interfaces import (
20 from rhodecode.events import RhodecodeEvent
21 from rhodecode.events.interfaces import (
21 IUserRegistered, IUserPreCreate, IUserPreUpdate)
22 IUserRegistered, IUserPreCreate, IUserPreUpdate)
22
23
23
24
24 @implementer(IUserRegistered)
25 @implementer(IUserRegistered)
25 class UserRegistered(object):
26 class UserRegistered(RhodecodeEvent):
26 """
27 """
27 An instance of this class is emitted as an :term:`event` whenever a user
28 An instance of this class is emitted as an :term:`event` whenever a user
28 account is registered.
29 account is registered.
@@ -33,7 +34,7 b' class UserRegistered(object):'
33
34
34
35
35 @implementer(IUserPreCreate)
36 @implementer(IUserPreCreate)
36 class UserPreCreate(object):
37 class UserPreCreate(RhodecodeEvent):
37 """
38 """
38 An instance of this class is emitted as an :term:`event` before a new user
39 An instance of this class is emitted as an :term:`event` before a new user
39 object is created.
40 object is created.
@@ -43,7 +44,7 b' class UserPreCreate(object):'
43
44
44
45
45 @implementer(IUserPreUpdate)
46 @implementer(IUserPreUpdate)
46 class UserPreUpdate(object):
47 class UserPreUpdate(RhodecodeEvent):
47 """
48 """
48 An instance of this class is emitted as an :term:`event` before a user
49 An instance of this class is emitted as an :term:`event` before a user
49 object is updated.
50 object is updated.
@@ -27,6 +27,7 b' import os'
27 import collections
27 import collections
28
28
29 import rhodecode
29 import rhodecode
30 from rhodecode import events
30 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib.utils import action_logger
32 from rhodecode.lib.utils import action_logger
32 from rhodecode.lib.utils2 import safe_str
33 from rhodecode.lib.utils2 import safe_str
@@ -102,6 +103,9 b' def pre_push(extras):'
102 # Calling hooks after checking the lock, for consistent behavior
103 # Calling hooks after checking the lock, for consistent behavior
103 pre_push_extension(repo_store_path=Repository.base_path(), **extras)
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 return HookResponse(0, output)
109 return HookResponse(0, output)
106
110
107
111
@@ -128,6 +132,8 b' def pre_pull(extras):'
128
132
129 # Calling hooks after checking the lock, for consistent behavior
133 # Calling hooks after checking the lock, for consistent behavior
130 pre_pull_extension(**extras)
134 pre_pull_extension(**extras)
135 events.trigger(events.RepoPrePullEvent(repo_name=extras.repository,
136 extras=extras))
131
137
132 return HookResponse(0, output)
138 return HookResponse(0, output)
133
139
@@ -138,6 +144,8 b' def post_pull(extras):'
138 action = 'pull'
144 action = 'pull'
139 action_logger(user, action, extras.repository, extras.ip, commit=True)
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 # extension hook call
149 # extension hook call
142 post_pull_extension(**extras)
150 post_pull_extension(**extras)
143
151
@@ -171,6 +179,10 b' def post_push(extras):'
171 action_logger(
179 action_logger(
172 extras.username, action, extras.repository, extras.ip, commit=True)
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 # extension hook call
186 # extension hook call
175 post_push_extension(
187 post_push_extension(
176 repo_store_path=Repository.base_path(),
188 repo_store_path=Repository.base_path(),
@@ -145,17 +145,6 b' class BaseModel(object):'
145 return self._get_instance(
145 return self._get_instance(
146 db.Permission, permission, callback=db.Permission.get_by_key)
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 @classmethod
148 @classmethod
160 def get_all(cls):
149 def get_all(cls):
161 """
150 """
@@ -34,6 +34,7 b' from sqlalchemy.sql import func'
34 from sqlalchemy.sql.expression import true, or_
34 from sqlalchemy.sql.expression import true, or_
35 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
36
36
37 from rhodecode import events
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib import helpers as h
38 from rhodecode.lib.auth import HasUserGroupPermissionAny
39 from rhodecode.lib.auth import HasUserGroupPermissionAny
39 from rhodecode.lib.caching_query import FromCache
40 from rhodecode.lib.caching_query import FromCache
@@ -470,6 +471,8 b' class RepoModel(BaseModel):'
470 parent_repo = fork_of
471 parent_repo = fork_of
471 new_repo.fork = parent_repo
472 new_repo.fork = parent_repo
472
473
474 events.trigger(events.RepoPreCreateEvent(new_repo))
475
473 self.sa.add(new_repo)
476 self.sa.add(new_repo)
474
477
475 EMPTY_PERM = 'repository.none'
478 EMPTY_PERM = 'repository.none'
@@ -525,11 +528,13 b' class RepoModel(BaseModel):'
525 # now automatically start following this repository as owner
528 # now automatically start following this repository as owner
526 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
529 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
527 owner.user_id)
530 owner.user_id)
531
528 # we need to flush here, in order to check if database won't
532 # we need to flush here, in order to check if database won't
529 # throw any exceptions, create filesystem dirs at the very end
533 # throw any exceptions, create filesystem dirs at the very end
530 self.sa.flush()
534 self.sa.flush()
535 events.trigger(events.RepoCreatedEvent(new_repo))
536 return new_repo
531
537
532 return new_repo
533 except Exception:
538 except Exception:
534 log.error(traceback.format_exc())
539 log.error(traceback.format_exc())
535 raise
540 raise
@@ -633,6 +638,7 b' class RepoModel(BaseModel):'
633 raise AttachedForksError()
638 raise AttachedForksError()
634
639
635 old_repo_dict = repo.get_dict()
640 old_repo_dict = repo.get_dict()
641 events.trigger(events.RepoPreDeleteEvent(repo))
636 try:
642 try:
637 self.sa.delete(repo)
643 self.sa.delete(repo)
638 if fs_remove:
644 if fs_remove:
@@ -644,6 +650,7 b' class RepoModel(BaseModel):'
644 'deleted_on': time.time(),
650 'deleted_on': time.time(),
645 })
651 })
646 log_delete_repository(**old_repo_dict)
652 log_delete_repository(**old_repo_dict)
653 events.trigger(events.RepoDeletedEvent(repo))
647 except Exception:
654 except Exception:
648 log.error(traceback.format_exc())
655 log.error(traceback.format_exc())
649 raise
656 raise
@@ -32,7 +32,7 b' import ipaddress'
32 from sqlalchemy.exc import DatabaseError
32 from sqlalchemy.exc import DatabaseError
33 from sqlalchemy.sql.expression import true, false
33 from sqlalchemy.sql.expression import true, false
34
34
35 from rhodecode.events import UserPreCreate, UserPreUpdate
35 from rhodecode import events
36 from rhodecode.lib.utils2 import (
36 from rhodecode.lib.utils2 import (
37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 AttributeDict)
38 AttributeDict)
@@ -270,12 +270,12 b' class UserModel(BaseModel):'
270 # raises UserCreationError if it's not allowed for any reason to
270 # raises UserCreationError if it's not allowed for any reason to
271 # create new active user, this also executes pre-create hooks
271 # create new active user, this also executes pre-create hooks
272 check_allowed_create_user(user_data, cur_user, strict_check=True)
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 new_user = User()
274 new_user = User()
275 edit = False
275 edit = False
276 else:
276 else:
277 log.debug('updating user %s', username)
277 log.debug('updating user %s', username)
278 self.send_event(UserPreUpdate(user, user_data))
278 events.trigger(events.UserPreUpdate(user, user_data))
279 new_user = user
279 new_user = user
280 edit = True
280 edit = True
281
281
@@ -28,12 +28,12 b' from rhodecode.lib import hooks_base, ut'
28 action_logger=mock.Mock(),
28 action_logger=mock.Mock(),
29 post_push_extension=mock.Mock(),
29 post_push_extension=mock.Mock(),
30 Repository=mock.Mock())
30 Repository=mock.Mock())
31 def test_post_push_truncates_commits():
31 def test_post_push_truncates_commits(user_regular, repo_stub):
32 extras = {
32 extras = {
33 'ip': '127.0.0.1',
33 'ip': '127.0.0.1',
34 'username': 'test',
34 'username': user_regular.username,
35 'action': 'push_local',
35 'action': 'push_local',
36 'repository': 'test',
36 'repository': repo_stub.repo_name,
37 'scm': 'git',
37 'scm': 'git',
38 'config': '',
38 'config': '',
39 'server_url': 'http://example.com',
39 'server_url': 'http://example.com',
@@ -271,6 +271,7 b' class TestPullRequestModel:'
271 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
271 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
272 MergeFailureReason.NONE)
272 MergeFailureReason.NONE)
273
273
274 merge_extras['repository'] = pull_request.target_repo.repo_name
274 PullRequestModel().merge(
275 PullRequestModel().merge(
275 pull_request, pull_request.author, extras=merge_extras)
276 pull_request, pull_request.author, extras=merge_extras)
276
277
@@ -308,6 +309,7 b' class TestPullRequestModel:'
308 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
309 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
309 MergeFailureReason.MERGE_FAILED)
310 MergeFailureReason.MERGE_FAILED)
310
311
312 merge_extras['repository'] = pull_request.target_repo.repo_name
311 PullRequestModel().merge(
313 PullRequestModel().merge(
312 pull_request, pull_request.author, extras=merge_extras)
314 pull_request, pull_request.author, extras=merge_extras)
313
315
@@ -364,6 +366,7 b' class TestIntegrationMerge(object):'
364 pull_request = pr_util.create_pull_request(
366 pull_request = pr_util.create_pull_request(
365 approved=True, mergeable=True)
367 approved=True, mergeable=True)
366 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
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 Session().commit()
370 Session().commit()
368
371
369 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
372 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
@@ -379,6 +382,7 b' class TestIntegrationMerge(object):'
379 pull_request = pr_util.create_pull_request(
382 pull_request = pr_util.create_pull_request(
380 approved=True, mergeable=True)
383 approved=True, mergeable=True)
381 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
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 Session().commit()
386 Session().commit()
383
387
384 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
388 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
@@ -400,6 +404,7 b' class TestIntegrationMerge(object):'
400 # all data is pre-computed, that's why just updating the DB is not
404 # all data is pre-computed, that's why just updating the DB is not
401 # enough.
405 # enough.
402 merge_extras['locked_by'] = locked_by
406 merge_extras['locked_by'] = locked_by
407 merge_extras['repository'] = pull_request.target_repo.repo_name
403 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
408 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
404 Session().commit()
409 Session().commit()
405 merge_status = PullRequestModel().merge(
410 merge_status = PullRequestModel().merge(
General Comments 0
You need to be logged in to leave comments. Login now