diff --git a/rhodecode/events/__init__.py b/rhodecode/events/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/events/__init__.py @@ -0,0 +1,49 @@ +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +from pyramid.threadlocal import get_current_registry + + +class RhodecodeEvent(object): + """ + Base event class for all Rhodecode events + """ + + +def trigger(event): + """ + Helper method to send an event. This wraps the pyramid logic to send an + event. + """ + # For the first step we are using pyramids thread locals here. If the + # event mechanism works out as a good solution we should think about + # passing the registry as an argument to get rid of it. + registry = get_current_registry() + registry.notify(event) + + +from rhodecode.events.user import ( + UserPreCreate, UserPreUpdate, UserRegistered +) + +from rhodecode.events.repo import ( + RepoPreCreateEvent, RepoCreatedEvent, + RepoPreDeleteEvent, RepoDeletedEvent, + RepoPrePushEvent, RepoPushEvent, + RepoPrePullEvent, RepoPullEvent, +) \ No newline at end of file diff --git a/rhodecode/interfaces.py b/rhodecode/events/interfaces.py rename from rhodecode/interfaces.py rename to rhodecode/events/interfaces.py diff --git a/rhodecode/events/repo.py b/rhodecode/events/repo.py new file mode 100644 --- /dev/null +++ b/rhodecode/events/repo.py @@ -0,0 +1,115 @@ +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +from rhodecode.model.db import Repository, Session +from rhodecode.events import RhodecodeEvent + + +class RepoEvent(RhodecodeEvent): + """ + Base class for events acting on a repository. + + :param repo: a :class:`Repository` instance + """ + def __init__(self, repo): + self.repo = repo + + +class RepoPreCreateEvent(RepoEvent): + """ + An instance of this class is emitted as an :term:`event` before a repo is + created. + + :param repo_name: repository name + """ + name = 'repo-pre-create' + + +class RepoCreatedEvent(RepoEvent): + """ + An instance of this class is emitted as an :term:`event` whenever a repo is + created. + """ + name = 'repo-created' + + +class RepoPreDeleteEvent(RepoEvent): + """ + An instance of this class is emitted as an :term:`event` whenever a repo is + created. + """ + name = 'repo-pre-delete' + + +class RepoDeletedEvent(RepoEvent): + """ + An instance of this class is emitted as an :term:`event` whenever a repo is + created. + """ + name = 'repo-deleted' + + +class RepoVCSEvent(RepoEvent): + """ + Base class for events triggered by the VCS + """ + def __init__(self, repo_name, extras): + self.repo = Repository.get_by_repo_name(repo_name) + if not self.repo: + raise Exception('repo by this name %s does not exist' % repo_name) + self.extras = extras + super(RepoVCSEvent, self).__init__(self.repo) + + +class RepoPrePullEvent(RepoVCSEvent): + """ + An instance of this class is emitted as an :term:`event` before commits + are pulled from a repo. + """ + name = 'repo-pre-pull' + + +class RepoPullEvent(RepoVCSEvent): + """ + An instance of this class is emitted as an :term:`event` after commits + are pulled from a repo. + """ + name = 'repo-pull' + + +class RepoPrePushEvent(RepoVCSEvent): + """ + An instance of this class is emitted as an :term:`event` before commits + are pushed to a repo. + """ + name = 'repo-pre-push' + + +class RepoPushEvent(RepoVCSEvent): + """ + An instance of this class is emitted as an :term:`event` after commits + are pushed to a repo. + + :param extras: (optional) dict of data from proxied VCS actions + """ + name = 'repo-push' + + def __init__(self, repo_name, pushed_commit_ids, extras): + super(RepoPushEvent, self).__init__(repo_name, extras) + self.pushed_commit_ids = pushed_commit_ids + diff --git a/rhodecode/events.py b/rhodecode/events/user.py rename from rhodecode/events.py rename to rhodecode/events/user.py --- a/rhodecode/events.py +++ b/rhodecode/events/user.py @@ -17,12 +17,13 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ from zope.interface import implementer -from rhodecode.interfaces import ( +from rhodecode.events import RhodecodeEvent +from rhodecode.events.interfaces import ( IUserRegistered, IUserPreCreate, IUserPreUpdate) @implementer(IUserRegistered) -class UserRegistered(object): +class UserRegistered(RhodecodeEvent): """ An instance of this class is emitted as an :term:`event` whenever a user account is registered. @@ -33,7 +34,7 @@ class UserRegistered(object): @implementer(IUserPreCreate) -class UserPreCreate(object): +class UserPreCreate(RhodecodeEvent): """ An instance of this class is emitted as an :term:`event` before a new user object is created. @@ -43,7 +44,7 @@ class UserPreCreate(object): @implementer(IUserPreUpdate) -class UserPreUpdate(object): +class UserPreUpdate(RhodecodeEvent): """ An instance of this class is emitted as an :term:`event` before a user object is updated. diff --git a/rhodecode/lib/hooks_base.py b/rhodecode/lib/hooks_base.py --- a/rhodecode/lib/hooks_base.py +++ b/rhodecode/lib/hooks_base.py @@ -27,6 +27,7 @@ import os import collections import rhodecode +from rhodecode import events from rhodecode.lib import helpers as h from rhodecode.lib.utils import action_logger from rhodecode.lib.utils2 import safe_str @@ -102,6 +103,9 @@ def pre_push(extras): # Calling hooks after checking the lock, for consistent behavior pre_push_extension(repo_store_path=Repository.base_path(), **extras) + events.trigger(events.RepoPrePushEvent(repo_name=extras.repository, + extras=extras)) + return HookResponse(0, output) @@ -128,6 +132,8 @@ def pre_pull(extras): # Calling hooks after checking the lock, for consistent behavior pre_pull_extension(**extras) + events.trigger(events.RepoPrePullEvent(repo_name=extras.repository, + extras=extras)) return HookResponse(0, output) @@ -138,6 +144,8 @@ def post_pull(extras): action = 'pull' action_logger(user, action, extras.repository, extras.ip, commit=True) + events.trigger(events.RepoPullEvent(repo_name=extras.repository, + extras=extras)) # extension hook call post_pull_extension(**extras) @@ -171,6 +179,10 @@ def post_push(extras): action_logger( extras.username, action, extras.repository, extras.ip, commit=True) + events.trigger(events.RepoPushEvent(repo_name=extras.repository, + pushed_commit_ids=commit_ids, + extras=extras)) + # extension hook call post_push_extension( repo_store_path=Repository.base_path(), diff --git a/rhodecode/model/__init__.py b/rhodecode/model/__init__.py --- a/rhodecode/model/__init__.py +++ b/rhodecode/model/__init__.py @@ -145,17 +145,6 @@ class BaseModel(object): return self._get_instance( db.Permission, permission, callback=db.Permission.get_by_key) - def send_event(self, event): - """ - Helper method to send an event. This wraps the pyramid logic to send an - event. - """ - # For the first step we are using pyramids thread locals here. If the - # event mechanism works out as a good solution we should think about - # passing the registry into the constructor to get rid of it. - registry = get_current_registry() - registry.notify(event) - @classmethod def get_all(cls): """ diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -34,6 +34,7 @@ from sqlalchemy.sql import func from sqlalchemy.sql.expression import true, or_ from zope.cachedescriptors.property import Lazy as LazyProperty +from rhodecode import events from rhodecode.lib import helpers as h from rhodecode.lib.auth import HasUserGroupPermissionAny from rhodecode.lib.caching_query import FromCache @@ -470,6 +471,8 @@ class RepoModel(BaseModel): parent_repo = fork_of new_repo.fork = parent_repo + events.trigger(events.RepoPreCreateEvent(new_repo)) + self.sa.add(new_repo) EMPTY_PERM = 'repository.none' @@ -525,11 +528,13 @@ class RepoModel(BaseModel): # now automatically start following this repository as owner ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, owner.user_id) + # we need to flush here, in order to check if database won't # throw any exceptions, create filesystem dirs at the very end self.sa.flush() + events.trigger(events.RepoCreatedEvent(new_repo)) + return new_repo - return new_repo except Exception: log.error(traceback.format_exc()) raise @@ -633,6 +638,7 @@ class RepoModel(BaseModel): raise AttachedForksError() old_repo_dict = repo.get_dict() + events.trigger(events.RepoPreDeleteEvent(repo)) try: self.sa.delete(repo) if fs_remove: @@ -644,6 +650,7 @@ class RepoModel(BaseModel): 'deleted_on': time.time(), }) log_delete_repository(**old_repo_dict) + events.trigger(events.RepoDeletedEvent(repo)) except Exception: log.error(traceback.format_exc()) raise diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -32,7 +32,7 @@ import ipaddress from sqlalchemy.exc import DatabaseError from sqlalchemy.sql.expression import true, false -from rhodecode.events import UserPreCreate, UserPreUpdate +from rhodecode import events from rhodecode.lib.utils2 import ( safe_unicode, get_current_rhodecode_user, action_logger_generic, AttributeDict) @@ -270,12 +270,12 @@ class UserModel(BaseModel): # raises UserCreationError if it's not allowed for any reason to # create new active user, this also executes pre-create hooks check_allowed_create_user(user_data, cur_user, strict_check=True) - self.send_event(UserPreCreate(user_data)) + events.trigger(events.UserPreCreate(user_data)) new_user = User() edit = False else: log.debug('updating user %s', username) - self.send_event(UserPreUpdate(user, user_data)) + events.trigger(events.UserPreUpdate(user, user_data)) new_user = user edit = True diff --git a/rhodecode/tests/events/__init__.py b/rhodecode/tests/events/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/events/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ diff --git a/rhodecode/tests/events/conftest.py b/rhodecode/tests/events/conftest.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/events/conftest.py @@ -0,0 +1,38 @@ +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import mock +import decorator + + +def assert_fires_events(*expected_events): + """ Testing decorator to check if the function fires events in order """ + def deco(func): + def wrapper(func, *args, **kwargs): + with mock.patch('rhodecode.events.trigger') as mock_trigger: + result = func(*args, **kwargs) + + captured_events = [] + for call in mock_trigger.call_args_list: + event = call[0][0] + captured_events.append(type(event)) + + assert set(captured_events) == set(expected_events) + return result + return decorator.decorator(wrapper, func) + return deco \ No newline at end of file diff --git a/rhodecode/tests/events/test_repo.py b/rhodecode/tests/events/test_repo.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/events/test_repo.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import pytest + +from rhodecode.tests.events.conftest import assert_fires_events + +from rhodecode.lib import hooks_base, utils2 +from rhodecode.model.repo import RepoModel +from rhodecode.events.repo import ( + RepoPrePullEvent, RepoPullEvent, + RepoPrePushEvent, RepoPushEvent, + RepoPreCreateEvent, RepoCreatedEvent, + RepoPreDeleteEvent, RepoDeletedEvent, +) + + +@pytest.fixture +def scm_extras(user_regular, repo_stub): + extras = utils2.AttributeDict({ + 'ip': '127.0.0.1', + 'username': user_regular.username, + 'action': '', + 'repository': repo_stub.repo_name, + 'scm': repo_stub.scm_instance().alias, + 'config': '', + 'server_url': 'http://example.com', + 'make_lock': None, + 'locked_by': [None], + 'commit_ids': ['a' * 40] * 3, + }) + return extras + + +@assert_fires_events( + RepoPreCreateEvent, RepoCreatedEvent, RepoPreDeleteEvent, RepoDeletedEvent) +def test_create_delete_repo_fires_events(backend): + repo = backend.create_repo() + RepoModel().delete(repo) + + +@assert_fires_events(RepoPrePushEvent, RepoPushEvent) +def test_pull_fires_events(scm_extras): + hooks_base.pre_push(scm_extras) + hooks_base.post_push(scm_extras) + + +@assert_fires_events(RepoPrePullEvent, RepoPullEvent) +def test_push_fires_events(scm_extras): + hooks_base.pre_pull(scm_extras) + hooks_base.post_pull(scm_extras) diff --git a/rhodecode/tests/lib/test_hooks_base.py b/rhodecode/tests/lib/test_hooks_base.py --- a/rhodecode/tests/lib/test_hooks_base.py +++ b/rhodecode/tests/lib/test_hooks_base.py @@ -28,12 +28,12 @@ from rhodecode.lib import hooks_base, ut action_logger=mock.Mock(), post_push_extension=mock.Mock(), Repository=mock.Mock()) -def test_post_push_truncates_commits(): +def test_post_push_truncates_commits(user_regular, repo_stub): extras = { 'ip': '127.0.0.1', - 'username': 'test', + 'username': user_regular.username, 'action': 'push_local', - 'repository': 'test', + 'repository': repo_stub.repo_name, 'scm': 'git', 'config': '', 'server_url': 'http://example.com', diff --git a/rhodecode/tests/models/test_pullrequest.py b/rhodecode/tests/models/test_pullrequest.py --- a/rhodecode/tests/models/test_pullrequest.py +++ b/rhodecode/tests/models/test_pullrequest.py @@ -271,6 +271,7 @@ class TestPullRequestModel: '6126b7bfcc82ad2d3deaee22af926b082ce54cc6', MergeFailureReason.NONE) + merge_extras['repository'] = pull_request.target_repo.repo_name PullRequestModel().merge( pull_request, pull_request.author, extras=merge_extras) @@ -308,6 +309,7 @@ class TestPullRequestModel: '6126b7bfcc82ad2d3deaee22af926b082ce54cc6', MergeFailureReason.MERGE_FAILED) + merge_extras['repository'] = pull_request.target_repo.repo_name PullRequestModel().merge( pull_request, pull_request.author, extras=merge_extras) @@ -364,6 +366,7 @@ class TestIntegrationMerge(object): pull_request = pr_util.create_pull_request( approved=True, mergeable=True) # TODO: johbo: Needed for sqlite, try to find an automatic way for it + merge_extras['repository'] = pull_request.target_repo.repo_name Session().commit() with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False): @@ -379,6 +382,7 @@ class TestIntegrationMerge(object): pull_request = pr_util.create_pull_request( approved=True, mergeable=True) # TODO: johbo: Needed for sqlite, try to find an automatic way for it + merge_extras['repository'] = pull_request.target_repo.repo_name Session().commit() with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull: @@ -400,6 +404,7 @@ class TestIntegrationMerge(object): # all data is pre-computed, that's why just updating the DB is not # enough. merge_extras['locked_by'] = locked_by + merge_extras['repository'] = pull_request.target_repo.repo_name # TODO: johbo: Needed for sqlite, try to find an automatic way for it Session().commit() merge_status = PullRequestModel().merge(