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