Mercurial > libervia-backend
comparison src/plugins/plugin_merge_req_mercurial.py @ 2449:67942ba2ee55
plugin merge requests Mercurial: first draft:
this plugin handle Mercurial VCS. It send merge requests using export command, and parse it to retrieve individual patches and metadata.
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 30 Nov 2017 20:49:20 +0100 |
parents | |
children | 0046283a285d |
comparison
equal
deleted
inserted
replaced
2448:637ac234424f | 2449:67942ba2ee55 |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SàT plugin for import external blogs | |
5 # Copyright (C) 2009-2017 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 _, D_ | |
21 from sat.core.constants import Const as C | |
22 from sat.core import exceptions | |
23 from twisted.internet import reactor, defer, protocol | |
24 from twisted.python.failure import Failure | |
25 from twisted.python.procutils import which | |
26 from sat.core.log import getLogger | |
27 log = getLogger(__name__) | |
28 | |
29 | |
30 PLUGIN_INFO = { | |
31 C.PI_NAME: "Mercurial Merge Request handler", | |
32 C.PI_IMPORT_NAME: "MERGE_REQUEST_MERCURIAL", | |
33 C.PI_TYPE: C.PLUG_TYPE_MISC, | |
34 C.PI_DEPENDENCIES: ["MERGE_REQUESTS"], | |
35 C.PI_MAIN: "MercurialHandler", | |
36 C.PI_HANDLER: "no", | |
37 C.PI_DESCRIPTION: _(u"""Merge request handler for Mercurial""") | |
38 } | |
39 | |
40 SHORT_DESC = D_(u"handle Mercurial repository") | |
41 | |
42 | |
43 class MercurialProtocol(protocol.ProcessProtocol): | |
44 """handle hg commands""" | |
45 hg = None | |
46 | |
47 def __init__(self, deferred): | |
48 self._deferred = deferred | |
49 self.data = [] | |
50 | |
51 def outReceived(self, data): | |
52 self.data.append(data) | |
53 | |
54 def errReceived(self, data): | |
55 self.data.append(data) | |
56 | |
57 def processEnded(self, reason): | |
58 data = u''.join([d.decode('utf-8') for d in self.data]) | |
59 if (reason.value.exitCode == 0): | |
60 log.debug(_('Mercurial command succeed')) | |
61 self._deferred.callback(data) | |
62 else: | |
63 log.error(_(u"Can't complete Mercurial command (error code: {code}): {message}").format( | |
64 code = reason.value.exitCode, | |
65 message = data)) | |
66 self._deferred.errback(Failure(RuntimeError)) | |
67 | |
68 @classmethod | |
69 def run(cls, path, command, *args): | |
70 """Create a new MercurialRegisterProtocol and execute the given mercurialctl command. | |
71 | |
72 @param path(unicode): path to the repository | |
73 @param command(unicode): command to run | |
74 @param *args(unicode): command arguments | |
75 @return ((D)): | |
76 """ | |
77 d = defer.Deferred() | |
78 mercurial_prot = MercurialProtocol(d) | |
79 cmd_args = [cls.hg, command.encode('utf-8')] | |
80 cmd_args.extend([a.encode('utf-8') for a in args]) | |
81 reactor.spawnProcess(mercurial_prot, | |
82 cls.hg, | |
83 cmd_args, | |
84 path=path.encode('utf-8')) | |
85 return d | |
86 | |
87 | |
88 class MercurialHandler(object): | |
89 | |
90 def __init__(self, host): | |
91 log.info(_(u"Mercurial merge request handler initialization")) | |
92 try: | |
93 MercurialProtocol.hg = which('hg')[0] | |
94 except IndexError: | |
95 raise exceptions.NotFound(_(u"Mercurial executable (hg) not found, can't use Mercurial handler")) | |
96 self.host = host | |
97 self._m = host.plugins['MERGE_REQUESTS'] | |
98 self._m.register('mercurial', self, [u'mercurial_changeset'], SHORT_DESC) | |
99 | |
100 def check(self, repository): | |
101 d = MercurialProtocol.run(repository, 'identify') | |
102 d.addCallback(lambda dummy: True) | |
103 d.addErrback(lambda dummy: False) | |
104 return d | |
105 | |
106 def export(self, repository): | |
107 return MercurialProtocol.run(repository, 'export', '-g', '-r', 'outgoing()', '--encoding=utf-8') | |
108 | |
109 def parse(self, data, data_type=None): | |
110 lines = data.splitlines() | |
111 total_lines = len(lines) | |
112 patches = [] | |
113 while lines: | |
114 patch = {} | |
115 commit_msg = [] | |
116 diff = [] | |
117 state = 'init' | |
118 if lines[0] != '# HG changeset patch': | |
119 raise exceptions.DataError(_(u'invalid changeset signature')) | |
120 # line index of this patch in the whole data | |
121 patch_idx = total_lines - len(lines) | |
122 del lines[0] | |
123 | |
124 for idx, line in enumerate(lines): | |
125 if state == 'init': | |
126 if line.startswith(u'# '): | |
127 if line.startswith(u'# User '): | |
128 elems = line[7:].split() | |
129 if not elems: | |
130 continue | |
131 last = elems[-1] | |
132 if last.startswith(u'<') and last.endswith(u'>') and u'@' in last: | |
133 patch[self._m.META_EMAIL] = elems.pop()[1:-1] | |
134 patch[self._m.META_AUTHOR] = u' '.join(elems) | |
135 elif line.startswith(u'# Date '): | |
136 time_data = line[7:].split() | |
137 if len(time_data) != 2: | |
138 log.warning(_(u'unexpected time data: {data}').format(data=line[7:])) | |
139 continue | |
140 patch[self._m.META_TIMESTAMP] = int(time_data[0]) + int(time_data[1]) | |
141 elif line.startswith(u'# Node ID '): | |
142 patch[self._m.META_HASH] = line[10:] | |
143 elif line.startswith(u'# Parent '): | |
144 patch[self._m.META_PARENT_HASH] = line[10:] | |
145 else: | |
146 state = 'commit_msg' | |
147 if state == 'commit_msg': | |
148 if line.startswith(u'diff --git a/'): | |
149 state = 'diff' | |
150 patch[self._m.META_DIFF_IDX] = patch_idx + idx + 1 | |
151 else: | |
152 commit_msg.append(line) | |
153 if state == 'diff': | |
154 if line.startswith(u'# ') or idx == len(lines)-1: | |
155 # a new patch is starting or we have reached end of patches | |
156 patch[self._m.META_COMMIT_MSG] = u'\n'.join(commit_msg) | |
157 patch[self._m.META_DIFF] = u'\n'.join(diff) | |
158 patches.append(patch) | |
159 if idx == len(lines)-1: | |
160 del lines[:] | |
161 else: | |
162 del lines[:idx] | |
163 break | |
164 else: | |
165 diff.append(line) | |
166 return patches |