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(