comparison sat/plugins/plugin_merge_req_mercurial.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_merge_req_mercurial.py@bd30dc3ffe5a
children 72f6f37ab648
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SàT plugin for import external blogs
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 _, 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 import re
27 from sat.core.log import getLogger
28 log = getLogger(__name__)
29
30
31 PLUGIN_INFO = {
32 C.PI_NAME: "Mercurial Merge Request handler",
33 C.PI_IMPORT_NAME: "MERGE_REQUEST_MERCURIAL",
34 C.PI_TYPE: C.PLUG_TYPE_MISC,
35 C.PI_DEPENDENCIES: ["MERGE_REQUESTS"],
36 C.PI_MAIN: "MercurialHandler",
37 C.PI_HANDLER: "no",
38 C.PI_DESCRIPTION: _(u"""Merge request handler for Mercurial""")
39 }
40
41 SHORT_DESC = D_(u"handle Mercurial repository")
42
43
44 class MercurialProtocol(protocol.ProcessProtocol):
45 """handle hg commands"""
46 hg = None
47
48 def __init__(self, deferred, stdin=None):
49 """
50 @param deferred(defer.Deferred): will be called when command is completed
51 @param stdin(str, None): if not None, will be push to standard input
52 """
53 self._stdin = stdin
54 self._deferred = deferred
55 self.data = []
56
57 def connectionMade(self):
58 if self._stdin is not None:
59 self.transport.write(self._stdin)
60 self.transport.closeStdin()
61
62 def outReceived(self, data):
63 self.data.append(data)
64
65 def errReceived(self, data):
66 self.data.append(data)
67
68 def processEnded(self, reason):
69 data = u''.join([d.decode('utf-8') for d in self.data])
70 if (reason.value.exitCode == 0):
71 log.debug(_('Mercurial command succeed'))
72 self._deferred.callback(data)
73 else:
74 msg = _(u"Can't complete Mercurial command (error code: {code}): {message}").format(
75 code = reason.value.exitCode,
76 message = data)
77 log.warning(msg)
78 self._deferred.errback(Failure(RuntimeError(msg)))
79
80 @classmethod
81 def run(cls, path, command, *args, **kwargs):
82 """Create a new MercurialRegisterProtocol and execute the given mercurialctl command.
83
84 @param path(unicode): path to the repository
85 @param command(unicode): command to run
86 @param *args(unicode): command arguments
87 @param **kwargs: used because Python2 doesn't handle normal kw args after *args
88 can only be:
89 - stdin(unicode, None): data to push to standard input
90 @return ((D)):
91 """
92 stdin = kwargs.pop('stdin', None)
93 if kwargs:
94 raise exceptions.InternalError(u'only stdin is allowed as keyword argument')
95 if stdin is not None:
96 stdin = stdin.encode('utf-8')
97 d = defer.Deferred()
98 mercurial_prot = MercurialProtocol(d, stdin=stdin)
99 cmd_args = [cls.hg, command.encode('utf-8')]
100 cmd_args.extend([a.encode('utf-8') for a in args])
101 reactor.spawnProcess(mercurial_prot,
102 cls.hg,
103 cmd_args,
104 path=path.encode('utf-8'))
105 return d
106
107
108 class MercurialHandler(object):
109 data_types = (u'mercurial_changeset',)
110
111 def __init__(self, host):
112 log.info(_(u"Mercurial merge request handler initialization"))
113 try:
114 MercurialProtocol.hg = which('hg')[0]
115 except IndexError:
116 raise exceptions.NotFound(_(u"Mercurial executable (hg) not found, can't use Mercurial handler"))
117 self.host = host
118 self._m = host.plugins['MERGE_REQUESTS']
119 self._m.register('mercurial', self, self.data_types, SHORT_DESC)
120
121 def check(self, repository):
122 d = MercurialProtocol.run(repository, 'identify')
123 d.addCallback(lambda dummy: True)
124 d.addErrback(lambda dummy: False)
125 return d
126
127 def export(self, repository):
128 return MercurialProtocol.run(repository, 'export', '-g', '-r', 'outgoing()', '--encoding=utf-8')
129
130 def import_(self, repository, data, data_type, item_id, service, node, extra):
131 parsed_data = self.parse(data)
132 try:
133 parsed_name = parsed_data[0][u'commit_msg'].split(u'\n')[0]
134 parsed_name = re.sub(ur'[^\w -.]', u'', parsed_name, flags=re.UNICODE)[:40]
135 except Exception:
136 parsed_name = u''
137 name = u'mr_{item_id}_{parsed_name}'.format(item_id=item_id, parsed_name=parsed_name)
138 return MercurialProtocol.run(repository, 'qimport', '-g', '--name', name, '--encoding=utf-8', '-', stdin=data)
139
140 def parse(self, data, data_type=None):
141 lines = data.splitlines()
142 total_lines = len(lines)
143 patches = []
144 while lines:
145 patch = {}
146 commit_msg = []
147 diff = []
148 state = 'init'
149 if lines[0] != '# HG changeset patch':
150 raise exceptions.DataError(_(u'invalid changeset signature'))
151 # line index of this patch in the whole data
152 patch_idx = total_lines - len(lines)
153 del lines[0]
154
155 for idx, line in enumerate(lines):
156 if state == 'init':
157 if line.startswith(u'# '):
158 if line.startswith(u'# User '):
159 elems = line[7:].split()
160 if not elems:
161 continue
162 last = elems[-1]
163 if last.startswith(u'<') and last.endswith(u'>') and u'@' in last:
164 patch[self._m.META_EMAIL] = elems.pop()[1:-1]
165 patch[self._m.META_AUTHOR] = u' '.join(elems)
166 elif line.startswith(u'# Date '):
167 time_data = line[7:].split()
168 if len(time_data) != 2:
169 log.warning(_(u'unexpected time data: {data}').format(data=line[7:]))
170 continue
171 patch[self._m.META_TIMESTAMP] = int(time_data[0]) + int(time_data[1])
172 elif line.startswith(u'# Node ID '):
173 patch[self._m.META_HASH] = line[10:]
174 elif line.startswith(u'# Parent '):
175 patch[self._m.META_PARENT_HASH] = line[10:]
176 else:
177 state = 'commit_msg'
178 if state == 'commit_msg':
179 if line.startswith(u'diff --git a/'):
180 state = 'diff'
181 patch[self._m.META_DIFF_IDX] = patch_idx + idx + 1
182 else:
183 commit_msg.append(line)
184 if state == 'diff':
185 if line.startswith(u'# ') or idx == len(lines)-1:
186 # a new patch is starting or we have reached end of patches
187 patch[self._m.META_COMMIT_MSG] = u'\n'.join(commit_msg)
188 patch[self._m.META_DIFF] = u'\n'.join(diff)
189 patches.append(patch)
190 if idx == len(lines)-1:
191 del lines[:]
192 else:
193 del lines[:idx]
194 break
195 else:
196 diff.append(line)
197 return patches