events: add event system for RepoEvents
This commit is contained in:
parent
9875dca82f
commit
9727a3880f
13 changed files with 325 additions and 22 deletions
49
rhodecode/events/__init__.py
Normal file
49
rhodecode/events/__init__.py
Normal 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
115
rhodecode/events/repo.py
Normal 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
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
19
rhodecode/tests/events/__init__.py
Normal file
19
rhodecode/tests/events/__init__.py
Normal 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/
|
||||
38
rhodecode/tests/events/conftest.py
Normal file
38
rhodecode/tests/events/conftest.py
Normal 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
|
||||
68
rhodecode/tests/events/test_repo.py
Normal file
68
rhodecode/tests/events/test_repo.py
Normal 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)
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue