Mercurial > libervia-backend
diff libervia/backend/plugins/plugin_misc_merge_requests.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/plugins/plugin_misc_merge_requests.py@524856bd7b19 |
children | 0d7bb4df2343 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/plugins/plugin_misc_merge_requests.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 + + +# SAT plugin for Pubsub Schemas +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# 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 Affero 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/>. + +from collections import namedtuple +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from libervia.backend.core.i18n import _ +from libervia.backend.core.constants import Const as C +from libervia.backend.core import exceptions +from libervia.backend.tools.common import data_format +from libervia.backend.core.log import getLogger + + +log = getLogger(__name__) + +APP_NS_MERGE_REQUESTS = 'org.salut-a-toi.merge_requests:0' + +PLUGIN_INFO = { + C.PI_NAME: _("Merge requests management"), + C.PI_IMPORT_NAME: "MERGE_REQUESTS", + C.PI_TYPE: "EXP", + C.PI_PROTOCOLS: [], + C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0346", "LISTS", "TEXT_SYNTAXES"], + C.PI_MAIN: "MergeRequests", + C.PI_HANDLER: "no", + C.PI_DESCRIPTION: _("""Merge requests management plugin""") +} + +FIELD_DATA_TYPE = 'type' +FIELD_DATA = 'request_data' + + +MergeRequestHandler = namedtuple("MergeRequestHandler", ['name', + 'handler', + 'data_types', + 'short_desc', + 'priority']) + + +class MergeRequests(object): + META_AUTHOR = 'author' + META_EMAIL = 'email' + META_TIMESTAMP = 'timestamp' + META_HASH = 'hash' + META_PARENT_HASH = 'parent_hash' + META_COMMIT_MSG = 'commit_msg' + META_DIFF = 'diff' + # index of the diff in the whole data + # needed to retrieve comments location + META_DIFF_IDX = 'diff_idx' + + def __init__(self, host): + log.info(_("Merge requests plugin initialization")) + self.host = host + self._s = self.host.plugins["XEP-0346"] + self.namespace = self._s.get_submitted_ns(APP_NS_MERGE_REQUESTS) + host.register_namespace('merge_requests', self.namespace) + self._p = self.host.plugins["XEP-0060"] + self._t = self.host.plugins["LISTS"] + self._handlers = {} + self._handlers_list = [] # handlers sorted by priority + self._type_handlers = {} # data type => handler map + host.bridge.add_method("merge_requests_get", ".plugin", + in_sign='ssiassss', out_sign='s', + method=self._get, + async_=True + ) + host.bridge.add_method("merge_request_set", ".plugin", + in_sign='ssssa{sas}ssss', out_sign='s', + method=self._set, + async_=True) + host.bridge.add_method("merge_requests_schema_get", ".plugin", + in_sign='sss', out_sign='s', + method=lambda service, nodeIdentifier, profile_key: + self._s._get_ui_schema(service, + nodeIdentifier, + default_node=self.namespace, + profile_key=profile_key), + async_=True) + host.bridge.add_method("merge_request_parse_data", ".plugin", + in_sign='ss', out_sign='aa{ss}', + method=self._parse_data, + async_=True) + host.bridge.add_method("merge_requests_import", ".plugin", + in_sign='ssssa{ss}s', out_sign='', + method=self._import, + async_=True + ) + + def register(self, name, handler, data_types, short_desc, priority=0): + """register an merge request handler + + @param name(unicode): name of the handler + @param handler(object): instance of the handler. + It must have the following methods, which may all return a Deferred: + - check(repository)->bool: True if repository can be handled + - export(repository)->str: return export data, i.e. the patches + - parse(export_data): parse report data and return a list of dict + (1 per patch) with: + - title: title of the commit message (first line) + - body: body of the commit message + @aram data_types(list[unicode]): data types that his handler can generate or parse + """ + if name in self._handlers: + raise exceptions.ConflictError(_("a handler with name {name} already " + "exists!").format(name = name)) + self._handlers[name] = MergeRequestHandler(name, + handler, + data_types, + short_desc, + priority) + self._handlers_list.append(name) + self._handlers_list.sort(key=lambda name: self._handlers[name].priority) + if isinstance(data_types, str): + data_types = [data_types] + for data_type in data_types: + if data_type in self._type_handlers: + log.warning(_('merge requests of type {type} are already handled by ' + '{old_handler}, ignoring {new_handler}').format( + type = data_type, + old_handler = self._type_handlers[data_type].name, + new_handler = name)) + continue + self._type_handlers[data_type] = self._handlers[name] + + def serialise(self, get_data): + tickets_xmlui, metadata, items_patches = get_data + tickets_xmlui_s, metadata = self._p.trans_items_data((tickets_xmlui, metadata)) + return data_format.serialise({ + "items": tickets_xmlui_s, + "metadata": metadata, + "items_patches": items_patches, + }) + + def _get(self, service='', node='', max_items=10, item_ids=None, sub_id=None, + extra="", profile_key=C.PROF_KEY_NONE): + extra = data_format.deserialise(extra) + client, service, node, max_items, extra, sub_id = self._s.prepare_bridge_get( + service, node, max_items, sub_id, extra, profile_key) + d = self.get(client, service, node or None, max_items, item_ids, sub_id or None, + extra.rsm_request, extra.extra) + d.addCallback(self.serialise) + return d + + @defer.inlineCallbacks + def get(self, client, service=None, node=None, max_items=None, item_ids=None, + sub_id=None, rsm_request=None, extra=None): + """Retrieve merge requests and convert them to XMLUI + + @param extra(XEP-0060.parse, None): can have following keys: + - update(bool): if True, will return list of parsed request data + other params are the same as for [TICKETS._get] + @return (tuple[list[unicode], list[dict[unicode, unicode]])): tuple with + - XMLUI of the tickets, like [TICKETS._get] + - node metadata + - list of parsed request data (if extra['parse'] is set, else empty list) + """ + if not node: + node = self.namespace + if extra is None: + extra = {} + # XXX: Q&D way to get list for labels when displaying them, but text when we + # have to modify them + if C.bool(extra.get('labels_as_list', C.BOOL_FALSE)): + filters = {'labels': self._s.textbox_2_list_filter} + else: + filters = {} + tickets_xmlui, metadata = yield defer.ensureDeferred( + self._s.get_data_form_items( + client, + service, + node, + max_items=max_items, + item_ids=item_ids, + sub_id=sub_id, + rsm_request=rsm_request, + extra=extra, + form_ns=APP_NS_MERGE_REQUESTS, + filters = filters) + ) + parsed_patches = [] + if extra.get('parse', False): + for ticket in tickets_xmlui: + request_type = ticket.named_widgets[FIELD_DATA_TYPE].value + request_data = ticket.named_widgets[FIELD_DATA].value + parsed_data = yield self.parse_data(request_type, request_data) + parsed_patches.append(parsed_data) + defer.returnValue((tickets_xmlui, metadata, parsed_patches)) + + def _set(self, service, node, repository, method, values, schema=None, item_id=None, + extra="", profile_key=C.PROF_KEY_NONE): + client, service, node, schema, item_id, extra = self._s.prepare_bridge_set( + service, node, schema, item_id, extra, profile_key) + d = defer.ensureDeferred( + self.set( + client, service, node, repository, method, values, schema, + item_id or None, extra, deserialise=True + ) + ) + d.addCallback(lambda ret: ret or '') + return d + + async def set(self, client, service, node, repository, method='auto', values=None, + schema=None, item_id=None, extra=None, deserialise=False): + """Publish a tickets + + @param service(None, jid.JID): Pubsub service to use + @param node(unicode, None): Pubsub node to use + None to use default tickets node + @param repository(unicode): path to the repository where the code stands + @param method(unicode): name of one of the registered handler, + or "auto" to try autodetection. + other arguments are same as for [TICKETS.set] + @return (unicode): id of the created item + """ + if not node: + node = self.namespace + if values is None: + values = {} + update = extra.get('update', False) + if not repository and not update: + # in case of update, we may re-user former patches data + # so repository is not mandatory + raise exceptions.DataError(_("repository must be specified")) + + if FIELD_DATA in values: + raise exceptions.DataError(_("{field} is set by backend, you must not set " + "it in frontend").format(field = FIELD_DATA)) + + if repository: + if method == 'auto': + for name in self._handlers_list: + handler = self._handlers[name].handler + can_handle = await handler.check(repository) + if can_handle: + log.info(_("{name} handler will be used").format(name=name)) + break + else: + log.warning(_("repository {path} can't be handled by any installed " + "handler").format( + path = repository)) + raise exceptions.NotFound(_("no handler for this repository has " + "been found")) + else: + try: + handler = self._handlers[name].handler + except KeyError: + raise exceptions.NotFound(_("No handler of this name found")) + + data = await handler.export(repository) + if not data.strip(): + raise exceptions.DataError(_('export data is empty, do you have any ' + 'change to send?')) + + if not values.get('title') or not values.get('body'): + patches = handler.parse(data, values.get(FIELD_DATA_TYPE)) + commits_msg = patches[-1][self.META_COMMIT_MSG] + msg_lines = commits_msg.splitlines() + if not values.get('title'): + values['title'] = msg_lines[0] + if not values.get('body'): + ts = self.host.plugins['TEXT_SYNTAXES'] + xhtml = await ts.convert( + '\n'.join(msg_lines[1:]), + syntax_from = ts.SYNTAX_TEXT, + syntax_to = ts.SYNTAX_XHTML, + profile = client.profile) + values['body'] = '<div xmlns="{ns}">{xhtml}</div>'.format( + ns=C.NS_XHTML, xhtml=xhtml) + + values[FIELD_DATA] = data + + item_id = await self._t.set(client, service, node, values, schema, item_id, extra, + deserialise, form_ns=APP_NS_MERGE_REQUESTS) + return item_id + + def _parse_data(self, data_type, data): + d = self.parse_data(data_type, data) + d.addCallback(lambda parsed_patches: + {key: str(value) for key, value in parsed_patches.items()}) + return d + + def parse_data(self, data_type, data): + """Parse a merge request data according to type + + @param data_type(unicode): type of the data to parse + @param data(unicode): data to parse + @return(list[dict[unicode, unicode]]): parsed data + key of dictionary are self.META_* or keys specifics to handler + @raise NotFound: no handler can parse this data_type + """ + try: + handler = self._type_handlers[data_type] + except KeyError: + raise exceptions.NotFound(_('No handler can handle data type "{type}"') + .format(type=data_type)) + return defer.maybeDeferred(handler.handler.parse, data, data_type) + + def _import(self, repository, item_id, service=None, node=None, extra=None, + profile_key=C.PROF_KEY_NONE): + client = self.host.get_client(profile_key) + service = jid.JID(service) if service else None + d = self.import_request(client, repository, item_id, service, node or None, + extra=extra or None) + return d + + @defer.inlineCallbacks + def import_request(self, client, repository, item, service=None, node=None, + extra=None): + """import a merge request in specified directory + + @param repository(unicode): path to the repository where the code stands + """ + if not node: + node = self.namespace + tickets_xmlui, metadata = yield defer.ensureDeferred( + self._s.get_data_form_items( + client, + service, + node, + max_items=1, + item_ids=[item], + form_ns=APP_NS_MERGE_REQUESTS) + ) + ticket_xmlui = tickets_xmlui[0] + data = ticket_xmlui.named_widgets[FIELD_DATA].value + data_type = ticket_xmlui.named_widgets[FIELD_DATA_TYPE].value + try: + handler = self._type_handlers[data_type] + except KeyError: + raise exceptions.NotFound(_('No handler found to import {data_type}') + .format(data_type=data_type)) + log.info(_("Importing patch [{item_id}] using {name} handler").format( + item_id = item, + name = handler.name)) + yield handler.handler.import_(repository, data, data_type, item, service, node, + extra)