comparison sat/plugins/plugin_misc_merge_requests.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_misc_merge_requests.py@48820e4a1f8a
children 5d4ac5415b40
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for Pubsub Schemas
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from sat.core.constants import Const as C
22 from sat.core import exceptions
23 from twisted.internet import defer
24 from twisted.words.protocols.jabber import jid
25 from collections import namedtuple
26 from sat.tools import utils
27 from sat.core.log import getLogger
28 log = getLogger(__name__)
29
30 NS_MERGE_REQUESTS = 'org.salut-a-toi.merge_requests:0'
31
32 PLUGIN_INFO = {
33 C.PI_NAME: _("Merge requests management"),
34 C.PI_IMPORT_NAME: "MERGE_REQUESTS",
35 C.PI_TYPE: "EXP",
36 C.PI_PROTOCOLS: [],
37 C.PI_DEPENDENCIES: ["XEP-0060", "PUBSUB_SCHEMA", "TICKETS"],
38 C.PI_MAIN: "MergeRequests",
39 C.PI_HANDLER: "no",
40 C.PI_DESCRIPTION: _("""Merge requests management plugin""")
41 }
42
43 FIELD_DATA_TYPE = u'type'
44 FIELD_DATA = u'request_data'
45
46
47 MergeRequestHandler = namedtuple("MergeRequestHandler", ['name',
48 'handler',
49 'data_types',
50 'short_desc',
51 'priority'])
52
53
54 class MergeRequests(object):
55 META_AUTHOR = u'author'
56 META_EMAIL = u'email'
57 META_TIMESTAMP = u'timestamp'
58 META_HASH = u'hash'
59 META_PARENT_HASH = u'parent_hash'
60 META_COMMIT_MSG = u'commit_msg'
61 META_DIFF = u'diff'
62 # index of the diff in the whole data
63 # needed to retrieve comments location
64 META_DIFF_IDX = u'diff_idx'
65
66 def __init__(self, host):
67 log.info(_(u"Merge requests plugin initialization"))
68 self.host = host
69 host.registerNamespace('merge_requests', NS_MERGE_REQUESTS)
70 self._p = self.host.plugins["XEP-0060"]
71 self._s = self.host.plugins["PUBSUB_SCHEMA"]
72 self._t = self.host.plugins["TICKETS"]
73 self._handlers = {}
74 self._handlers_list = [] # handlers sorted by priority
75 self._type_handlers = {} # data type => handler map
76 host.bridge.addMethod("mergeRequestsGet", ".plugin",
77 in_sign='ssiassa{ss}s', out_sign='(asa{ss}aaa{ss})',
78 method=self._get,
79 async=True
80 )
81 host.bridge.addMethod("mergeRequestSet", ".plugin",
82 in_sign='ssssa{sas}ssa{ss}s', out_sign='s',
83 method=self._set,
84 async=True)
85 host.bridge.addMethod("mergeRequestsSchemaGet", ".plugin",
86 in_sign='sss', out_sign='s',
87 method=utils.partial(self._s._getUISchema, default_node=NS_MERGE_REQUESTS),
88 async=True)
89 host.bridge.addMethod("mergeRequestParseData", ".plugin",
90 in_sign='ss', out_sign='aa{ss}',
91 method=self._parseData,
92 async=True)
93 host.bridge.addMethod("mergeRequestsImport", ".plugin",
94 in_sign='ssssa{ss}s', out_sign='',
95 method=self._import,
96 async=True
97 )
98
99 def register(self, name, handler, data_types, short_desc, priority=0):
100 """register an merge request handler
101
102 @param name(unicode): name of the handler
103 @param handler(object): instance of the handler.
104 It must have the following methods, which may all return a Deferred:
105 - check(repository): True if repository can be handled
106 - export(repository): return export data, i.e. the patches
107 - parse(export_data): parse report data and return a list of dict (1 per patch) with:
108 - title: title of the commit message (first line)
109 - body: body of the commit message
110 @aram data_types(list[unicode]): data types that his handler can generate or parse
111 """
112 if name in self._handlers:
113 raise exceptions.ConflictError(_(u"a handler with name {name} already exists!").format(
114 name = name))
115 self._handlers[name] = MergeRequestHandler(name,
116 handler,
117 data_types,
118 short_desc,
119 priority)
120 self._handlers_list.append(name)
121 self._handlers_list.sort(key=lambda name: self._handlers[name].priority)
122 if isinstance(data_types, basestring):
123 data_types = [data_types]
124 for data_type in data_types:
125 if data_type in self._type_handlers:
126 log.warning(_(u'merge requests of type {type} are already handled by {old_handler}, '
127 u'ignoring {new_handler}').format(
128 type = data_type,
129 old_handler = self._type_handlers[data_type].name,
130 new_handler = name))
131 continue
132 self._type_handlers[data_type] = self._handlers[name]
133
134 def _get(self, service='', node='', max_items=10, item_ids=None, sub_id=None, extra_dict=None, profile_key=C.PROF_KEY_NONE):
135 if extra_dict and 'parse' in extra_dict:
136 extra_dict['parse'] = C.bool(extra_dict['parse'])
137 client, service, node, max_items, extra, sub_id = self._s.prepareBridgeGet(service, node, max_items, sub_id, extra_dict, profile_key)
138 d = self.get(client, service, node or None, max_items, item_ids, sub_id or None, extra.rsm_request, extra.extra)
139 d.addCallback(lambda (tickets, metadata, parsed_patches): (
140 self._p.serItemsData((tickets, metadata)) +
141 ([[{key: unicode(value) for key, value in p.iteritems()}
142 for p in patches] for patches in parsed_patches],)))
143 return d
144
145 @defer.inlineCallbacks
146 def get(self, client, service=None, node=None, max_items=None, item_ids=None, sub_id=None, rsm_request=None, extra=None):
147 """Retrieve merge requests and convert them to XMLUI
148
149 @param extra(XEP-0060.parse, None): can have following keys:
150 - update(bool): if True, will return list of parsed request data
151 other params are the same as for [TICKETS._get]
152 @return (tuple[list[unicode], list[dict[unicode, unicode]])): tuple with
153 - XMLUI of the tickets, like [TICKETS._get]
154 - node metadata
155 - list of parsed request data (if extra['parse'] is set, else empty list)
156 """
157 if not node:
158 node = NS_MERGE_REQUESTS
159 tickets_xmlui, metadata = yield self._s.getDataFormItems(
160 client,
161 service,
162 node,
163 max_items=max_items,
164 item_ids=item_ids,
165 sub_id=sub_id,
166 rsm_request=rsm_request,
167 extra=extra,
168 form_ns=NS_MERGE_REQUESTS,
169 filters = {u'labels': self._s.textbox2ListFilter})
170 parsed_patches = []
171 if extra.get('parse', False):
172 for ticket in tickets_xmlui:
173 request_type = ticket.named_widgets[FIELD_DATA_TYPE].value
174 request_data = ticket.named_widgets[FIELD_DATA].value
175 parsed_data = yield self.parseData(request_type, request_data)
176 parsed_patches.append(parsed_data)
177 defer.returnValue((tickets_xmlui, metadata, parsed_patches))
178
179 def _set(self, service, node, repository, method, values, schema=None, item_id=None, extra=None, profile_key=C.PROF_KEY_NONE):
180 client, service, node, schema, item_id, extra = self._s.prepareBridgeSet(service, node, schema, item_id, extra, profile_key)
181 d = self.set(client, service, node, repository, method, values, schema, item_id, extra, deserialise=True)
182 d.addCallback(lambda ret: ret or u'')
183 return d
184
185 @defer.inlineCallbacks
186 def set(self, client, service, node, repository, method=u'auto', values=None, schema=None, item_id=None, extra=None, deserialise=False):
187 """Publish a tickets
188
189 @param service(None, jid.JID): Pubsub service to use
190 @param node(unicode, None): Pubsub node to use
191 None to use default tickets node
192 @param repository(unicode): path to the repository where the code stands
193 @param method(unicode): name of one of the registered handler, or "auto" to try autodetection.
194 other arguments are same as for [TICKETS.set]
195 @return (unicode): id of the created item
196 """
197 if not node:
198 node = NS_MERGE_REQUESTS
199 if values is None:
200 values = {}
201
202 if FIELD_DATA in values:
203 raise exceptions.DataError(_(u"{field} is set by backend, you must not set it in frontend").format(
204 field = FIELD_DATA))
205
206 if method == u'auto':
207 for name in self._handlers_list:
208 handler = self._handlers[name].handler
209 can_handle = yield handler.check(repository)
210 if can_handle:
211 log.info(_(u"{name} handler will be used").format(name=name))
212 break
213 else:
214 log.warning(_(u"repository {path} can't be handled by any installed handler").format(
215 path = repository))
216 raise exceptions.NotFound(_(u"no handler for this repository has been found"))
217 else:
218 try:
219 handler = self._handlers[name].handler
220 except KeyError:
221 raise exceptions.NotFound(_(u"No handler of this name found"))
222
223 data = yield handler.export(repository)
224 if not data.strip():
225 raise exceptions.DataError(_(u'export data is empty, do you have any change to send?'))
226
227 if not values.get(u'title') or not values.get(u'body'):
228 patches = yield handler.parse(data, values.get(FIELD_DATA_TYPE))
229 commits_msg = patches[-1][self.META_COMMIT_MSG]
230 msg_lines = commits_msg.splitlines()
231 if not values.get(u'title'):
232 values[u'title'] = msg_lines[0]
233 if not values.get(u'body'):
234 values[u'body'] = u'\n'.join(msg_lines[1:])
235
236 values[FIELD_DATA] = data
237
238 item_id = yield self._t.set(client, service, node, values, schema, item_id, extra, deserialise, form_ns=NS_MERGE_REQUESTS)
239 defer.returnValue(item_id)
240
241 def _parseData(self, data_type, data):
242 d = self.parseData(data_type, data)
243 d.addCallback(lambda parsed_patches:
244 {key: unicode(value) for key, value in parsed_patches.iteritems()})
245 return d
246
247 def parseData(self, data_type, data):
248 """Parse a merge request data according to type
249
250 @param data_type(unicode): type of the data to parse
251 @param data(unicode): data to parse
252 @return(list[dict[unicode, unicode]]): parsed data
253 key of dictionary are self.META_* or keys specifics to handler
254 @raise NotFound: no handler can parse this data_type
255 """
256 try:
257 handler = self._type_handlers[data_type]
258 except KeyError:
259 raise exceptions.NotFound(_(u'No handler can handle data type "{type}"').format(type=data_type))
260 return defer.maybeDeferred(handler.handler.parse, data, data_type)
261
262 def _import(self, repository, item_id, service=None, node=None, extra=None, profile_key=C.PROF_KEY_NONE):
263 client = self.host.getClient(profile_key)
264 service = jid.JID(service) if service else None
265 d = self.import_request(client, repository, item_id, service, node or None, extra=extra or None)
266 return d
267
268 @defer.inlineCallbacks
269 def import_request(self, client, repository, item, service=None, node=None, extra=None):
270 """Import a merge request in specified directory
271
272 @param repository(unicode): path to the repository where the code stands
273 """
274 if not node:
275 node = NS_MERGE_REQUESTS
276 tickets_xmlui, metadata = yield self._s.getDataFormItems(
277 client,
278 service,
279 node,
280 max_items=1,
281 item_ids=[item],
282 form_ns=NS_MERGE_REQUESTS)
283 ticket_xmlui = tickets_xmlui[0]
284 data = ticket_xmlui.named_widgets[FIELD_DATA].value
285 data_type = ticket_xmlui.named_widgets[FIELD_DATA_TYPE].value
286 try:
287 handler = self._type_handlers[data_type]
288 except KeyError:
289 raise exceptions.NotFound(_(u'No handler found to import {data_type}').format(data_type=data_type))
290 log.info(_(u"Importing patch [{item_id}] using {name} handler").format(
291 item_id = item,
292 name = handler.name))
293 yield handler.handler.import_(repository, data, data_type, item, service, node, extra)