From b8d12d1a4d9f688fb697c5b89146851647f9ca5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 25 Oct 2016 12:23:52 +0200 Subject: [PATCH] Start of support for pushing activities from SVN hooks. --- attract/cli.py | 24 +++++++++++++- attract/routes.py | 66 ++++++++++++++++++++++++++++++++++++--- attract/subversion.py | 27 ++++++++++++++-- attract/tasks/__init__.py | 4 +-- 4 files changed, 111 insertions(+), 10 deletions(-) diff --git a/attract/cli.py b/attract/cli.py index 1edb6a1..0d3ed01 100644 --- a/attract/cli.py +++ b/attract/cli.py @@ -2,7 +2,9 @@ import logging -from pillar.cli import manager +from flask import current_app + +from pillar.cli import manager, create_service_account from pillar.api.utils import authentication import attract.setup @@ -22,3 +24,23 @@ def setup_for_attract(project_url, replace=False, svn_url=None): authentication.force_cli_user() attract.setup.setup_for_attract(project_url, replace=replace, svn_url=svn_url) + + +@manager.command +def create_svner_account(email, project_url): + """Creates an account that can push SVN activity to an Attract project. + + :param email: email address associated with the account + :param project_url: + """ + + authentication.force_cli_user() + + projs_coll = current_app.db()['projects'] + proj = projs_coll.find_one({'url': project_url}, + projection={'_id': 1}) + if not proj: + log.error('Unable to find project url=%s', project_url) + return 1 + + create_service_account(email, [u'svner'], {'svner': {'project': proj['_id']}}) diff --git a/attract/routes.py b/attract/routes.py index 40dbd50..59c71f5 100644 --- a/attract/routes.py +++ b/attract/routes.py @@ -1,16 +1,18 @@ import functools import logging -from flask import Blueprint, render_template, url_for +from flask import Blueprint, render_template, url_for, request, current_app import flask_login from pillar.web.utils import attach_project_pictures from pillar.api.utils import jsonify +from pillar.api.utils import authorization, authentication import pillar.web.subquery from pillar.web.system_util import pillar_api import pillarsdk +import werkzeug.exceptions as wz_exceptions -from attract import current_attract +from attract import current_attract, EXTENSION_NAME from attract.node_types.task import node_type_task from attract.node_types.shot import node_type_shot @@ -38,7 +40,7 @@ def index(): projs_with_summaries = [ (proj, current_attract.shot_manager.shot_status_summary(proj['_id'])) for proj in projects['_items'] - ] + ] # Fetch all activities for all Attract projects. id_to_proj = {p['_id']: p for p in projects['_items']} @@ -168,10 +170,65 @@ def subversion_kick(project, attract_props): }) +@blueprint.route('/api//subversion/log', methods=['POST']) +@authorization.require_login(require_roles={u'service', u'svner'}, require_all=True) +def subversion_log(project_url): + if request.mimetype != 'application/json': + log.debug('Received %s instead of application/json', request.mimetype) + raise wz_exceptions.BadRequest() + + # Parse the request + args = request.json + revision = args['revision'] + commit_message = args['log'] + commit_author = args['author'] + + current_user_id = authentication.current_user_id() + log.info('Service account %s registers SVN commit %s of user %s', + current_user_id, revision, commit_author) + assert current_user_id + + users_coll = current_app.db()['users'] + projects_coll = current_app.db()['projects'] + project = projects_coll.find_one({'url': project_url}, + projection={'_id': 1, 'url': 1, + 'extension_props': 1}) + if not project: + return 'Project not found', 403 + + # Check that the service user is allowed to log on this project. + srv_user = users_coll.find_one(current_user_id, + projection={'service.svner': 1}) + if srv_user is None: + log.error('subversion_log(%s): current user %s not found -- how did they log in?', + project['url'], current_user_id) + return 'User not found', 403 + + allowed_project = srv_user.get('service', {}).get('svner', {}).get('project') + if allowed_project != project['_id']: + log.warning('subversion_log(%s): current user %s not authorized to project %s', + project['url'], current_user_id, project['_id']) + return 'Project not allowed', 403 + + from . import subversion + + try: + attract_props = project['extension_props'][EXTENSION_NAME] + except KeyError: + return 'Not set up for Attract', 400 + + svn_server_url = attract_props['svn_url'] + log.info('Re-examining SVN server %s', svn_server_url) + client = subversion.obtain(svn_server_url) + observer = subversion.CommitLogObserver(client) + observer.process_log(revision, commit_author, commit_message) + + return 'Registered in Attract' + + @blueprint.route('/') @attract_project_view(extension_props=True) def project_index(project, attract_props): - return render_template('attract/project.html', project=project, attract_props=attract_props) @@ -180,7 +237,6 @@ def project_index(project, attract_props): @blueprint.route('//help') @attract_project_view(extension_props=False) def help(project): - nt_task = project.get_node_type(node_type_task['name']) nt_shot = project.get_node_type(node_type_shot['name']) diff --git a/attract/subversion.py b/attract/subversion.py index 270e329..4f4dc3c 100644 --- a/attract/subversion.py +++ b/attract/subversion.py @@ -11,7 +11,13 @@ import svn.common from pillar import attrs_extra task_logged = blinker.NamedSignal('task_logged') -task_marker_re = re.compile(r'\[(?PT\d+)\]') +shot_logged = blinker.NamedSignal('shot_logged') +marker_re = re.compile(r'\[(?P[TS])(?P[0-9a-zA-Z]+)\]') + +signals = { + 'T': task_logged, + 'S': shot_logged, +} def obtain(server_location): @@ -47,6 +53,23 @@ class CommitLogObserver(object): self._log.exception('Error calling self.svn_client.log_default()') return + def process_log(self, revision, commit_author, commit_message): + """Obtains task IDs without accessing the SVN server directly.""" + + self._log.debug('%s: process_log(%s, %s, ...)', self, revision, commit_author) + for node_id, node_type, in self._find_ids(commit_message): + signal = signals[node_type] + signal.send(self, node_id=node_id, log_entry=commit_message, author=commit_author) + + def _find_ids(self, message): + # Parse the commit log to see if there are any task/shot markers. + lines = message.split('\n', 100)[:100] + for line in lines: + for match in marker_re.finditer(line): + type = match.group('type') + node_id = match.group('task_id') + yield node_id, type + def _parse_log_entry(self, log_entry): """Parses the commit log to see if there are any task markers.""" @@ -57,7 +80,7 @@ class CommitLogObserver(object): # Parse the commit log to see if there are any task markers. lines = log_entry.msg.split('\n', 1) first_line = lines[0] - for match in task_marker_re.finditer(first_line): + for match in marker_re.finditer(first_line): task_id = match.group('task_id') # Send a Blinker signal for each observed task identifier. diff --git a/attract/tasks/__init__.py b/attract/tasks/__init__.py index f298fd2..cc7b219 100644 --- a/attract/tasks/__init__.py +++ b/attract/tasks/__init__.py @@ -14,7 +14,7 @@ from attract.node_types.task import node_type_task class TaskManager(object): _log = attrs_extra.log('%s.TaskManager' % __name__) - def task_logged_in_svn(self, sender, task_id, log_entry): + def task_logged_in_svn(self, sender, task_id, log_entry, author): """Blinker signal receiver; connects the logged commit with the task. :param sender: sender of the signal @@ -23,7 +23,7 @@ class TaskManager(object): :type task_info: dict """ - self._log.info("Task '%s' logged in SVN: %s", task_id, log_entry) + self._log.info("Task '%s' logged in SVN by %s: %s", task_id, author, log_entry) def create_task(self, project, task_type=None, parent=None): """Creates a new task, owned by the current user.