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/
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.

View file

@ -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(),

View file

@ -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):
"""

View file

@ -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
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

View file

@ -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

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(),
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',

View file

@ -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(