Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_merge_req_mercurial.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_merge_req_mercurial.py@e3c1f4736ab2 |
children | 0d7bb4df2343 |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # SàT plugin managing Mercurial VCS | |
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 import re | |
20 from twisted.python.procutils import which | |
21 from libervia.backend.tools.common import async_process | |
22 from libervia.backend.tools import utils | |
23 from libervia.backend.core.i18n import _, D_ | |
24 from libervia.backend.core.constants import Const as C | |
25 from libervia.backend.core import exceptions | |
26 from libervia.backend.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: _("""Merge request handler for Mercurial""") | |
38 } | |
39 | |
40 SHORT_DESC = D_("handle Mercurial repository") | |
41 CLEAN_RE = re.compile(r'[^\w -._]', flags=re.UNICODE) | |
42 | |
43 | |
44 class MercurialProtocol(async_process.CommandProtocol): | |
45 """handle hg commands""" | |
46 name = "Mercurial" | |
47 command = None | |
48 | |
49 @classmethod | |
50 def run(cls, path, command, *args, **kwargs): | |
51 """Create a new MercurialRegisterProtocol and execute the given mercurial command. | |
52 | |
53 @param path(unicode): path to the repository | |
54 @param command(unicode): hg command to run | |
55 @return D(bytes): stdout of the command | |
56 """ | |
57 assert "path" not in kwargs | |
58 kwargs["path"] = path | |
59 # FIXME: we have to use this workaround because Twisted's protocol.ProcessProtocol | |
60 # is not using new style classes. This can be removed once moved to | |
61 # Python 3 (super can be used normally then). | |
62 d = async_process.CommandProtocol.run.__func__(cls, command, *args, **kwargs) | |
63 d.addErrback(utils.logError) | |
64 return d | |
65 | |
66 | |
67 class MercurialHandler(object): | |
68 data_types = ('mercurial_changeset',) | |
69 | |
70 def __init__(self, host): | |
71 log.info(_("Mercurial merge request handler initialization")) | |
72 try: | |
73 MercurialProtocol.command = which('hg')[0] | |
74 except IndexError: | |
75 raise exceptions.NotFound(_("Mercurial executable (hg) not found, " | |
76 "can't use Mercurial handler")) | |
77 self.host = host | |
78 self._m = host.plugins['MERGE_REQUESTS'] | |
79 self._m.register('mercurial', self, self.data_types, SHORT_DESC) | |
80 | |
81 | |
82 def check(self, repository): | |
83 d = MercurialProtocol.run(repository, 'identify') | |
84 d.addCallback(lambda __: True) | |
85 d.addErrback(lambda __: False) | |
86 return d | |
87 | |
88 def export(self, repository): | |
89 d = MercurialProtocol.run( | |
90 repository, 'export', '-g', '-r', 'outgoing() and ancestors(.)', | |
91 '--encoding=utf-8' | |
92 ) | |
93 d.addCallback(lambda data: data.decode('utf-8')) | |
94 return d | |
95 | |
96 def import_(self, repository, data, data_type, item_id, service, node, extra): | |
97 parsed_data = self.parse(data) | |
98 try: | |
99 parsed_name = parsed_data[0]['commit_msg'].split('\n')[0] | |
100 parsed_name = CLEAN_RE.sub('', parsed_name)[:40] | |
101 except Exception: | |
102 parsed_name = '' | |
103 name = 'mr_{item_id}_{parsed_name}'.format(item_id=CLEAN_RE.sub('', item_id), | |
104 parsed_name=parsed_name) | |
105 return MercurialProtocol.run(repository, 'qimport', '-g', '--name', name, | |
106 '--encoding=utf-8', '-', stdin=data) | |
107 | |
108 def parse(self, data, data_type=None): | |
109 lines = data.splitlines() | |
110 total_lines = len(lines) | |
111 patches = [] | |
112 while lines: | |
113 patch = {} | |
114 commit_msg = [] | |
115 diff = [] | |
116 state = 'init' | |
117 if lines[0] != '# HG changeset patch': | |
118 raise exceptions.DataError(_('invalid changeset signature')) | |
119 # line index of this patch in the whole data | |
120 patch_idx = total_lines - len(lines) | |
121 del lines[0] | |
122 | |
123 for idx, line in enumerate(lines): | |
124 if state == 'init': | |
125 if line.startswith('# '): | |
126 if line.startswith('# User '): | |
127 elems = line[7:].split() | |
128 if not elems: | |
129 continue | |
130 last = elems[-1] | |
131 if (last.startswith('<') and last.endswith('>') | |
132 and '@' in last): | |
133 patch[self._m.META_EMAIL] = elems.pop()[1:-1] | |
134 patch[self._m.META_AUTHOR] = ' '.join(elems) | |
135 elif line.startswith('# Date '): | |
136 time_data = line[7:].split() | |
137 if len(time_data) != 2: | |
138 log.warning(_('unexpected time data: {data}') | |
139 .format(data=line[7:])) | |
140 continue | |
141 patch[self._m.META_TIMESTAMP] = (int(time_data[0]) | |
142 + int(time_data[1])) | |
143 elif line.startswith('# Node ID '): | |
144 patch[self._m.META_HASH] = line[10:] | |
145 elif line.startswith('# Parent '): | |
146 patch[self._m.META_PARENT_HASH] = line[10:] | |
147 else: | |
148 state = 'commit_msg' | |
149 if state == 'commit_msg': | |
150 if line.startswith('diff --git a/'): | |
151 state = 'diff' | |
152 patch[self._m.META_DIFF_IDX] = patch_idx + idx + 1 | |
153 else: | |
154 commit_msg.append(line) | |
155 if state == 'diff': | |
156 if line.startswith('# ') or idx == len(lines)-1: | |
157 # a new patch is starting or we have reached end of patches | |
158 if idx == len(lines)-1: | |
159 # end of patches, we need to keep the line | |
160 diff.append(line) | |
161 patch[self._m.META_COMMIT_MSG] = '\n'.join(commit_msg) | |
162 patch[self._m.META_DIFF] = '\n'.join(diff) | |
163 patches.append(patch) | |
164 if idx == len(lines)-1: | |
165 del lines[:] | |
166 else: | |
167 del lines[:idx] | |
168 break | |
169 else: | |
170 diff.append(line) | |
171 return patches |