events: add event system for RepoEvents

This commit is contained in:
Daniel Dourvaris 2016-07-06 06:00:18 +03:00
parent 9875dca82f
commit 9727a3880f
13 changed files with 325 additions and 22 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
#
# 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,
)

115
rhodecode/events/repo.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
#
# 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

View file

@ -17,12 +17,13 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/ # and proprietary license terms, please see https://rhodecode.com/licenses/
from zope.interface import implementer from zope.interface import implementer
from rhodecode.interfaces import ( from rhodecode.events import RhodecodeEvent
from rhodecode.events.interfaces import (
IUserRegistered, IUserPreCreate, IUserPreUpdate) IUserRegistered, IUserPreCreate, IUserPreUpdate)
@implementer(IUserRegistered) @implementer(IUserRegistered)
class UserRegistered(object): class UserRegistered(RhodecodeEvent):
""" """
An instance of this class is emitted as an :term:`event` whenever a user An instance of this class is emitted as an :term:`event` whenever a user
account is registered. account is registered.
@ -33,7 +34,7 @@ class UserRegistered(object):
@implementer(IUserPreCreate) @implementer(IUserPreCreate)
class UserPreCreate(object): class UserPreCreate(RhodecodeEvent):
""" """
An instance of this class is emitted as an :term:`event` before a new user An instance of this class is emitted as an :term:`event` before a new user
object is created. object is created.
@ -43,7 +44,7 @@ class UserPreCreate(object):
@implementer(IUserPreUpdate) @implementer(IUserPreUpdate)
class UserPreUpdate(object): class UserPreUpdate(RhodecodeEvent):
""" """
An instance of this class is emitted as an :term:`event` before a user An instance of this class is emitted as an :term:`event` before a user
object is updated. object is updated.

View file

@ -27,6 +27,7 @@ import os
import collections import collections
import rhodecode import rhodecode
from rhodecode import events
from rhodecode.lib import helpers as h from rhodecode.lib import helpers as h
from rhodecode.lib.utils import action_logger from rhodecode.lib.utils import action_logger
from rhodecode.lib.utils2 import safe_str from rhodecode.lib.utils2 import safe_str
@ -102,6 +103,9 @@ def pre_push(extras):
# Calling hooks after checking the lock, for consistent behavior # Calling hooks after checking the lock, for consistent behavior
pre_push_extension(repo_store_path=Repository.base_path(), **extras) pre_push_extension(repo_store_path=Repository.base_path(), **extras)
events.trigger(events.RepoPrePushEvent(repo_name=extras.repository,
extras=extras))
return HookResponse(0, output) return HookResponse(0, output)
@ -128,6 +132,8 @@ def pre_pull(extras):
# Calling hooks after checking the lock, for consistent behavior # Calling hooks after checking the lock, for consistent behavior
pre_pull_extension(**extras) pre_pull_extension(**extras)
events.trigger(events.RepoPrePullEvent(repo_name=extras.repository,
extras=extras))
return HookResponse(0, output) return HookResponse(0, output)
@ -138,6 +144,8 @@ def post_pull(extras):
action = 'pull' action = 'pull'
action_logger(user, action, extras.repository, extras.ip, commit=True) action_logger(user, action, extras.repository, extras.ip, commit=True)
events.trigger(events.RepoPullEvent(repo_name=extras.repository,
extras=extras))
# extension hook call # extension hook call
post_pull_extension(**extras) post_pull_extension(**extras)
@ -171,6 +179,10 @@ def post_push(extras):
action_logger( action_logger(
extras.username, action, extras.repository, extras.ip, commit=True) 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 # extension hook call
post_push_extension( post_push_extension(
repo_store_path=Repository.base_path(), repo_store_path=Repository.base_path(),

View file

@ -145,17 +145,6 @@ class BaseModel(object):
return self._get_instance( return self._get_instance(
db.Permission, permission, callback=db.Permission.get_by_key) 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 @classmethod
def get_all(cls): def get_all(cls):
""" """

View file

@ -34,6 +34,7 @@ from sqlalchemy.sql import func
from sqlalchemy.sql.expression import true, or_ from sqlalchemy.sql.expression import true, or_
from zope.cachedescriptors.property import Lazy as LazyProperty from zope.cachedescriptors.property import Lazy as LazyProperty
from rhodecode import events
from rhodecode.lib import helpers as h from rhodecode.lib import helpers as h
from rhodecode.lib.auth import HasUserGroupPermissionAny from rhodecode.lib.auth import HasUserGroupPermissionAny
from rhodecode.lib.caching_query import FromCache from rhodecode.lib.caching_query import FromCache
@ -470,6 +471,8 @@ class RepoModel(BaseModel):
parent_repo = fork_of parent_repo = fork_of
new_repo.fork = parent_repo new_repo.fork = parent_repo
events.trigger(events.RepoPreCreateEvent(new_repo))
self.sa.add(new_repo) self.sa.add(new_repo)
EMPTY_PERM = 'repository.none' EMPTY_PERM = 'repository.none'
@ -525,11 +528,13 @@ class RepoModel(BaseModel):
# now automatically start following this repository as owner # now automatically start following this repository as owner
ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
owner.user_id) owner.user_id)
# we need to flush here, in order to check if database won't # we need to flush here, in order to check if database won't
# throw any exceptions, create filesystem dirs at the very end # throw any exceptions, create filesystem dirs at the very end
self.sa.flush() self.sa.flush()
events.trigger(events.RepoCreatedEvent(new_repo))
return new_repo return new_repo
except Exception: except Exception:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise
@ -633,6 +638,7 @@ class RepoModel(BaseModel):
raise AttachedForksError() raise AttachedForksError()
old_repo_dict = repo.get_dict() old_repo_dict = repo.get_dict()
events.trigger(events.RepoPreDeleteEvent(repo))
try: try:
self.sa.delete(repo) self.sa.delete(repo)
if fs_remove: if fs_remove:
@ -644,6 +650,7 @@ class RepoModel(BaseModel):
'deleted_on': time.time(), 'deleted_on': time.time(),
}) })
log_delete_repository(**old_repo_dict) log_delete_repository(**old_repo_dict)
events.trigger(events.RepoDeletedEvent(repo))
except Exception: except Exception:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise raise

View file

@ -32,7 +32,7 @@ import ipaddress
from sqlalchemy.exc import DatabaseError from sqlalchemy.exc import DatabaseError
from sqlalchemy.sql.expression import true, false from sqlalchemy.sql.expression import true, false
from rhodecode.events import UserPreCreate, UserPreUpdate from rhodecode import events
from rhodecode.lib.utils2 import ( from rhodecode.lib.utils2 import (
safe_unicode, get_current_rhodecode_user, action_logger_generic, safe_unicode, get_current_rhodecode_user, action_logger_generic,
AttributeDict) AttributeDict)
@ -270,12 +270,12 @@ class UserModel(BaseModel):
# raises UserCreationError if it's not allowed for any reason to # raises UserCreationError if it's not allowed for any reason to
# create new active user, this also executes pre-create hooks # create new active user, this also executes pre-create hooks
check_allowed_create_user(user_data, cur_user, strict_check=True) 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() new_user = User()
edit = False edit = False
else: else:
log.debug('updating user %s', username) log.debug('updating user %s', username)
self.send_event(UserPreUpdate(user, user_data)) events.trigger(events.UserPreUpdate(user, user_data))
new_user = user new_user = user
edit = True edit = True

View file

@ -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 <http://www.gnu.org/licenses/>.
#
# 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/

View file

@ -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 <http://www.gnu.org/licenses/>.
#
# 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

View file

@ -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 <http://www.gnu.org/licenses/>.
#
# 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)

View file

@ -28,12 +28,12 @@ from rhodecode.lib import hooks_base, utils2
action_logger=mock.Mock(), action_logger=mock.Mock(),
post_push_extension=mock.Mock(), post_push_extension=mock.Mock(),
Repository=mock.Mock()) Repository=mock.Mock())
def test_post_push_truncates_commits(): def test_post_push_truncates_commits(user_regular, repo_stub):
extras = { extras = {
'ip': '127.0.0.1', 'ip': '127.0.0.1',
'username': 'test', 'username': user_regular.username,
'action': 'push_local', 'action': 'push_local',
'repository': 'test', 'repository': repo_stub.repo_name,
'scm': 'git', 'scm': 'git',
'config': '', 'config': '',
'server_url': 'http://example.com', 'server_url': 'http://example.com',

View file

@ -271,6 +271,7 @@ class TestPullRequestModel:
'6126b7bfcc82ad2d3deaee22af926b082ce54cc6', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
MergeFailureReason.NONE) MergeFailureReason.NONE)
merge_extras['repository'] = pull_request.target_repo.repo_name
PullRequestModel().merge( PullRequestModel().merge(
pull_request, pull_request.author, extras=merge_extras) pull_request, pull_request.author, extras=merge_extras)
@ -308,6 +309,7 @@ class TestPullRequestModel:
'6126b7bfcc82ad2d3deaee22af926b082ce54cc6', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
MergeFailureReason.MERGE_FAILED) MergeFailureReason.MERGE_FAILED)
merge_extras['repository'] = pull_request.target_repo.repo_name
PullRequestModel().merge( PullRequestModel().merge(
pull_request, pull_request.author, extras=merge_extras) pull_request, pull_request.author, extras=merge_extras)
@ -364,6 +366,7 @@ class TestIntegrationMerge(object):
pull_request = pr_util.create_pull_request( pull_request = pr_util.create_pull_request(
approved=True, mergeable=True) approved=True, mergeable=True)
# TODO: johbo: Needed for sqlite, try to find an automatic way for it # TODO: johbo: Needed for sqlite, try to find an automatic way for it
merge_extras['repository'] = pull_request.target_repo.repo_name
Session().commit() Session().commit()
with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False): with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
@ -379,6 +382,7 @@ class TestIntegrationMerge(object):
pull_request = pr_util.create_pull_request( pull_request = pr_util.create_pull_request(
approved=True, mergeable=True) approved=True, mergeable=True)
# TODO: johbo: Needed for sqlite, try to find an automatic way for it # TODO: johbo: Needed for sqlite, try to find an automatic way for it
merge_extras['repository'] = pull_request.target_repo.repo_name
Session().commit() Session().commit()
with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull: 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 # all data is pre-computed, that's why just updating the DB is not
# enough. # enough.
merge_extras['locked_by'] = locked_by 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 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
Session().commit() Session().commit()
merge_status = PullRequestModel().merge( merge_status = PullRequestModel().merge(