Status: | Needs review |
---|---|
Proposed branch: | lp:~jelmer/brz/gitea |
Merge into: | lp:brz/3.3 |
Diff against target: |
364 lines (+337/-2) 4 files modified
breezy/plugins/gitea/__init__.py (+36/-0) breezy/plugins/gitea/cmds.py (+70/-0) breezy/plugins/gitea/forge.py (+231/-0) breezy/plugins/github/forge.py (+0/-2) |
To merge this branch: | bzr merge lp:~jelmer/brz/gitea |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jelmer Vernooij | Approve | ||
Review via email: mp+436917@code.launchpad.net |
Commit message
Add basic gitea plugin
Description of the change
Add basic gitea plugin
The Breezy Bot (the-breezy-bot) wrote : | # |
Martin Packman (gz) wrote : | # |
Cool you've implemented this!
There are some comment references to gitlab, which are presumably
copy/paste rather than meaningful?
On Mon, 6 Feb 2023 at 20:34, Jelmer Vernooij
<email address hidden> wrote:
>
> Review: Approve
>
>
> --
> https:/
> Your team Breezy developers is subscribed to branch lp:brz/3.3.
>
Jelmer Vernooij (jelmer) wrote : | # |
On Sun, Feb 12, 2023 at 04:21:11PM -0000, Martin Packman wrote:
> Cool you've implemented this!
>
> There are some comment references to gitlab, which are presumably
> copy/paste rather than meaningful?
Yeah, it probably needs a little bit more work.. :)
> On Mon, 6 Feb 2023 at 20:34, Jelmer Vernooij
> <email address hidden> wrote:
> >
> > Review: Approve
> >
> >
> > --
> > https:/
> > Your team Breezy developers is subscribed to branch lp:brz/3.3.
> >
>
> --
> https:/
> You are the owner of lp:~jelmer/brz/gitea.
>
Unmerged revisions
- 7688. By Jelmer Vernooij
-
Add basic gitea implementation
- 7687. By Jelmer Vernooij
-
Merge lp:brz/3.3
- 7686. By Jelmer Vernooij
-
initial work on gitea support
Preview Diff
1 | === added directory 'breezy/plugins/gitea' |
2 | === added file 'breezy/plugins/gitea/__init__.py' |
3 | --- breezy/plugins/gitea/__init__.py 1970-01-01 00:00:00 +0000 |
4 | +++ breezy/plugins/gitea/__init__.py 2023-02-06 20:43:29 +0000 |
5 | @@ -0,0 +1,36 @@ |
6 | +# Copyright (C) 2021 Jelmer Vernooij <jelmer@jelmer.uk> |
7 | +# |
8 | +# This program is free software; you can redistribute it and/or modify |
9 | +# it under the terms of the GNU General Public License as published by |
10 | +# the Free Software Foundation; either version 2 of the License, or |
11 | +# (at your option) any later version. |
12 | +# |
13 | +# This program is distributed in the hope that it will be useful, |
14 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | +# GNU General Public License for more details. |
17 | +# |
18 | +# You should have received a copy of the GNU General Public License |
19 | +# along with this program; if not, write to the Free Software |
20 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
21 | + |
22 | +"""Management of hosted branches.""" |
23 | + |
24 | +from __future__ import absolute_import |
25 | + |
26 | +from ... import version_info # noqa: F401 |
27 | +from ...commands import plugin_cmds |
28 | +from ...commands import plugin_cmds |
29 | + |
30 | +plugin_cmds.register_lazy("cmd_gitea_login", ["gitea-login"], __name__ + ".cmds") |
31 | + |
32 | +from ...forge import forges |
33 | +forges.register_lazy("gitea", __name__ + '.forge', "Gitea") |
34 | + |
35 | + |
36 | +def test_suite(): |
37 | + from unittest import TestSuite |
38 | + from .tests import test_suite |
39 | + result = TestSuite() |
40 | + result.addTest(test_suite()) |
41 | + return result |
42 | |
43 | === added file 'breezy/plugins/gitea/cmds.py' |
44 | --- breezy/plugins/gitea/cmds.py 1970-01-01 00:00:00 +0000 |
45 | +++ breezy/plugins/gitea/cmds.py 2023-02-06 20:43:29 +0000 |
46 | @@ -0,0 +1,70 @@ |
47 | +# Copyright (C) 2023 Jelmer Vernooij <jelmer@jelmer.uk> |
48 | +# |
49 | +# This program is free software; you can redistribute it and/or modify |
50 | +# it under the terms of the GNU General Public License as published by |
51 | +# the Free Software Foundation; either version 2 of the License, or |
52 | +# (at your option) any later version. |
53 | +# |
54 | +# This program is distributed in the hope that it will be useful, |
55 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
56 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
57 | +# GNU General Public License for more details. |
58 | +# |
59 | +# You should have received a copy of the GNU General Public License |
60 | +# along with this program; if not, write to the Free Software |
61 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
62 | + |
63 | +"""Gitea command implementations.""" |
64 | + |
65 | +from ... import ( |
66 | + errors, |
67 | + urlutils, |
68 | + ) |
69 | +from ...commands import Command |
70 | +from ...option import ( |
71 | + Option, |
72 | + ) |
73 | +from ...trace import note |
74 | + |
75 | + |
76 | +class cmd_gitea_login(Command): |
77 | + __doc__ = """Log into a Gitea instance. |
78 | + |
79 | + This command takes a Gitea/Forgejo instance URL (e.g. https://codehut.org) |
80 | + as well as an optional private token. Private tokens can be created via the |
81 | + web UI. |
82 | + |
83 | + :Examples: |
84 | + |
85 | + Log into codehut.org (prompts for a token): |
86 | + |
87 | + brz gitea-login https://codehut.org/ |
88 | + """ |
89 | + |
90 | + takes_args = ['url', 'private_token?'] |
91 | + |
92 | + takes_options = [ |
93 | + Option('name', help='Name for Gitea site in configuration.', |
94 | + type=str), |
95 | + Option('no-check', |
96 | + "Don't check that the token is valid."), |
97 | + ] |
98 | + |
99 | + def run(self, url, private_token=None, name=None, no_check=False): |
100 | + from breezy import ui |
101 | + from .forge import store_gitea_token |
102 | + if name is None: |
103 | + try: |
104 | + name = urlutils.parse_url(url)[3].split('.')[-2] |
105 | + except (ValueError, IndexError): |
106 | + raise errors.CommandError( |
107 | + 'please specify a site name with --name') |
108 | + if private_token is None: |
109 | + note("Please visit %s to obtain a private token.", |
110 | + urlutils.join(url, "/user/settings/applications")) |
111 | + private_token = ui.ui_factory.get_password('Private token') |
112 | + if not no_check: |
113 | + from breezy.transport import get_transport |
114 | + from .forge import Gitea |
115 | + Gitea(get_transport(url), private_token=private_token) |
116 | + store_gitea_token(name=name, url=url, private_token=private_token) |
117 | |
118 | === added file 'breezy/plugins/gitea/forge.py' |
119 | --- breezy/plugins/gitea/forge.py 1970-01-01 00:00:00 +0000 |
120 | +++ breezy/plugins/gitea/forge.py 2023-02-06 20:43:29 +0000 |
121 | @@ -0,0 +1,231 @@ |
122 | +# Copyright (C) 2021 Breezy Developers |
123 | +# |
124 | +# This program is free software; you can redistribute it and/or modify |
125 | +# it under the terms of the GNU General Public License as published by |
126 | +# the Free Software Foundation; either version 2 of the License, or |
127 | +# (at your option) any later version. |
128 | +# |
129 | +# This program is distributed in the hope that it will be useful, |
130 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
131 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
132 | +# GNU General Public License for more details. |
133 | +# |
134 | +# You should have received a copy of the GNU General Public License |
135 | +# along with this program; if not, write to the Free Software |
136 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
137 | + |
138 | +"""Support for Gitea.""" |
139 | + |
140 | +from typing import Optional |
141 | + |
142 | +from ... import errors, urlutils |
143 | + |
144 | +from ...forge import ( |
145 | + determine_title, |
146 | + Forge, |
147 | + UnsupportedForge, |
148 | + PrerequisiteBranchUnsupported, |
149 | + MergeProposal, |
150 | + MergeProposalBuilder, |
151 | + ) |
152 | + |
153 | +from breezy.transport import get_transport |
154 | + |
155 | + |
156 | +class NotGiteaUrl(errors.BzrError): |
157 | + |
158 | + _fmt = "Not a Gitea URL: %(url)s" |
159 | + |
160 | + def __init__(self, url): |
161 | + errors.BzrError.__init__(self) |
162 | + self.url = url |
163 | + |
164 | + |
165 | +class DifferentGiteaInstances(errors.BzrError): |
166 | + |
167 | + _fmt = ("Can't create merge proposals across Gitea instances: " |
168 | + "%(source_host)s and %(target_host)s") |
169 | + |
170 | + def __init__(self, source_host, target_host): |
171 | + self.source_host = source_host |
172 | + self.target_host = target_host |
173 | + |
174 | + |
175 | +def store_gitea_token(name, url, private_token): |
176 | + """Store a gitea token in a configuration file.""" |
177 | + from breezy.config import AuthenticationConfig |
178 | + auth_config = AuthenticationConfig() |
179 | + auth_config._set_option(name, 'url', url) |
180 | + auth_config._set_option(name, 'private_token', private_token) |
181 | + |
182 | + |
183 | +def iter_tokens(): |
184 | + from breezy.config import AuthenticationConfig |
185 | + auth_config = AuthenticationConfig() |
186 | + yield from auth_config._get_config().iteritems() |
187 | + |
188 | + |
189 | +def get_credentials_by_url(url): |
190 | + for name, credentials in iter_tokens(): |
191 | + if 'url' not in credentials: |
192 | + continue |
193 | + if credentials['url'].rstrip('/') == url.rstrip('/'): |
194 | + return credentials |
195 | + else: |
196 | + return None |
197 | + |
198 | + |
199 | +def parse_gitea_url(url): |
200 | + (scheme, user, password, host, port, path) = urlutils.parse_url( |
201 | + url) |
202 | + if scheme not in ('git+ssh', 'https', 'http'): |
203 | + raise NotGiteaUrl(url) |
204 | + if not host: |
205 | + raise NotGiteaUrl(url) |
206 | + path = path.strip('/') |
207 | + if path.endswith('.git'): |
208 | + path = path[:-4] |
209 | + return host, path |
210 | + |
211 | + |
212 | +def parse_gitea_branch_url(branch): |
213 | + url = urlutils.strip_segment_parameters(branch.user_url) |
214 | + host, path = parse_gitea_url(url) |
215 | + return host, path, branch.name |
216 | + |
217 | + |
218 | +class Gitea(Forge): |
219 | + """Gitea hoster implementation.""" |
220 | + |
221 | + supports_merge_proposal_title = True |
222 | + supports_merge_proposal_labels = False |
223 | + supports_merge_proposal_commit_message = False |
224 | + supports_allow_collaboration = False |
225 | + merge_proposal_description_format = 'markdown' |
226 | + |
227 | + def __init__(self, transport, private_token): |
228 | + self.transport = transport |
229 | + self.headers = {"Private-Token": private_token} |
230 | + self._current_user = None |
231 | + |
232 | + def __repr__(self): |
233 | + return "<Gitea(%r)>" % self.base_url |
234 | + |
235 | + @property |
236 | + def base_url(self): |
237 | + return self.transport.base |
238 | + |
239 | + @property |
240 | + def base_hostname(self): |
241 | + return urlutils.parse_url(self.base_url)[3] |
242 | + |
243 | + def hosts(self, branch): |
244 | + try: |
245 | + (host, _project, _branch_name) = parse_gitea_branch_url(branch) |
246 | + except NotGiteaUrl: |
247 | + return False |
248 | + return self.base_hostname == host |
249 | + |
250 | + def iter_my_proposals(self, status='open', author=None): |
251 | + # TODO(jelmer): It's not clear to me how to list all |
252 | + raise NotImplementedError(self.iter_my_proposals) |
253 | + |
254 | + @classmethod |
255 | + def probe_from_url(cls, url, possible_transports=None): |
256 | + try: |
257 | + (host, _project) = parse_gitea_url(url) |
258 | + except NotGiteaUrl as e: |
259 | + raise UnsupportedForge(url) from e |
260 | + transport = get_transport( |
261 | + f'https://{host}', possible_transports=possible_transports) |
262 | + credentials = get_credentials_by_url(transport.base) |
263 | + if credentials is not None: |
264 | + return cls(transport, credentials.get('private_token')) |
265 | + raise UnsupportedForge(url) |
266 | + |
267 | + @classmethod |
268 | + def iter_instances(cls): |
269 | + for _name, credentials in iter_tokens(): |
270 | + if 'url' not in credentials: |
271 | + continue |
272 | + yield cls( |
273 | + get_transport(credentials['url']), |
274 | + private_token=credentials.get('private_token')) |
275 | + |
276 | + |
277 | +class GiteaMergeProposal(MergeProposal): |
278 | + |
279 | + supports_auto_merge = True |
280 | + |
281 | + def __init__(self, gitea, pr): |
282 | + self._gitea = gitea |
283 | + self._pr = pr |
284 | + |
285 | + def __repr__(self): |
286 | + return "<{} at {!r}>".format(type(self).__name__, self.url) |
287 | + |
288 | + def get_web_url(self): |
289 | + return self._pr['html_url'] |
290 | + |
291 | + @property |
292 | + def url(self): |
293 | + return self._pr['html_url'] |
294 | + |
295 | + def is_merged(self): |
296 | + return bool(self._pr.get('merged_at')) |
297 | + |
298 | + def is_closed(self): |
299 | + return self._pr['state'] == 'closed' and not bool(self._pr.get('merged_at')) |
300 | + |
301 | + |
302 | +class GiteaMergeProposalBuilder(MergeProposalBuilder): |
303 | + |
304 | + def __init__(self, gitea, source_branch, target_branch): |
305 | + self.gitea = gitea |
306 | + self.source_branch = source_branch |
307 | + (self.source_host, self.source_project_name, self.source_branch_name) = ( |
308 | + parse_gitea_branch_url(source_branch)) |
309 | + self.target_branch = target_branch |
310 | + (self.target_host, self.target_project_name, self.target_branch_name) = ( |
311 | + parse_gitea_branch_url(target_branch)) |
312 | + if self.source_host != self.target_host: |
313 | + raise DifferentGiteaInstances(self.source_host, self.target_host) |
314 | + |
315 | + def create_proposal(self, description, title=None, reviewers=None, |
316 | + labels=None, prerequisite_branch=None, |
317 | + commit_message=None, work_in_progress=False, |
318 | + allow_collaboration=False, |
319 | + delete_source_after_merge: Optional[bool] = None): |
320 | + """Perform the submission.""" |
321 | + # https://docs.gitlab.com/ee/api/merge_requests.html#create-mr |
322 | + if prerequisite_branch is not None: |
323 | + raise PrerequisiteBranchUnsupported(self) |
324 | + # Note that commit_message is ignored, since Gitlab doesn't support it. |
325 | + source_project = self.gitea._get_project(self.source_project_name) |
326 | + target_project = self.gitea._get_project(self.target_project_name) |
327 | + if title is None: |
328 | + title = determine_title(description) |
329 | + if work_in_progress: |
330 | + title = 'WIP: %s' % title |
331 | + # TODO(jelmer): Allow setting milestone field |
332 | + # TODO(jelmer): Allow setting squash field |
333 | + kwargs = { |
334 | + 'title': title, |
335 | + 'head': head, |
336 | + 'base': base, |
337 | + 'body': description, |
338 | + } |
339 | + if labels: |
340 | + # TODO(jelmer): Labels are apparently integers |
341 | + raise NotImplementedError |
342 | + if reviewers: |
343 | + kwargs['assignees'] = reviewers |
344 | + # TODO(jelmer): add milestone |
345 | + # TODO(jelmer): add due_date |
346 | + merge_request = self.gitea._create_pullrequest( |
347 | + title=title, |
348 | + assignees=reviewers, |
349 | + head="{}:{}".format(self.source_owner, self.source_branch_name), |
350 | + base=self.target_branch_name) |
351 | + |
352 | + return GiteaMergeProposal(self.gitea, merge_request) |
353 | |
354 | === modified file 'breezy/plugins/github/forge.py' |
355 | --- breezy/plugins/github/forge.py 2023-01-31 01:05:40 +0000 |
356 | +++ breezy/plugins/github/forge.py 2023-02-06 20:43:29 +0000 |
357 | @@ -112,8 +112,6 @@ |
358 | def __repr__(self): |
359 | return "<{} at {!r}>".format(type(self).__name__, self.url) |
360 | |
361 | - name = 'GitHub' |
362 | - |
363 | def get_web_url(self): |
364 | return self._pr['html_url'] |
365 |
The attempt to merge lp:~jelmer/brz/gitea into lp:brz/3.3 failed. Command exited with 2.
Below is the output from the failed tests.
Collecting setuptools-gettext gettext- 0.1.2-py3- none-any. whl (10 kB) 11/site- packages (from setuptools-gettext) (66.1.1) gettext- 0.1.2 /tmp/tarmac/ branch. w5kh4zqy python3/ dist-packages (from breezy==3.3.3.dev0) (0.2.3) python3/ dist-packages (from breezy==3.3.3.dev0) (1.26.12) python3/ dist-packages (from breezy==3.3.3.dev0) (0.0.12) python3/ dist-packages (from breezy==3.3.3.dev0) (5.1.0.dev0) 0.21.2- cp311-cp311- manylinux_ 2_17_x86_ 64.manylinux201 4_x86_64. whl (507 kB) python3/ dist-packages (from breezy==3.3.3.dev0) (0.0.8) python3/ dist-packages (from breezy==3.3.3.dev0) (6.0) python3/ dist-packages (from breezy==3.3.3.dev0) (1.11.0) subunit- 1.4.2-py3- none-any. whl (106 kB) 0.5.0-py2. py3-none- any.whl (21 kB) 2.5.0-py3- none-any. whl (181 kB) python3/ dist-packages (from breezy==3.3.3.dev0) (5.0.4) python3/ dist-packages (from breezy==3.3.3.dev0) (1.18.0) 0.9.14- py2.py3- none-any. whl lib/python3. 11/dist- packages (from breezy==3.3.3.dev0) (2.12.0) 11/site- packages (from breezy==3.3.3.dev0) (66.1.1) 0.19-py3- none-any. whl (570 kB) 6.1.3-py3- none-any. whl (3.0 MB) epytext- 0.0.4-py3- none-any. whl python3/ dist-packages (from launchpadlib> =1.6.3- >breezy= =3.3.3. dev0) (0.20.4) ent>=0. 14.2 in /usr/lib/ python3/ dist-packages (from launchpadlib> =1.6.3- >breezy= =3.3.3. dev0) (0.14.5)
Using cached setuptools_
Requirement already satisfied: setuptools>=46.1 in ./lib/python3.
Installing collected packages: setuptools-gettext
Successfully installed setuptools-
Obtaining file://
Installing build dependencies: started
Installing build dependencies: finished with status 'done'
Checking if build backend supports build_editable: started
Checking if build backend supports build_editable: finished with status 'done'
Getting requirements to build editable: started
Getting requirements to build editable: finished with status 'done'
Preparing editable metadata (pyproject.toml): started
Preparing editable metadata (pyproject.toml): finished with status 'done'
Requirement already satisfied: patiencediff in /usr/lib/
Requirement already satisfied: urllib3>=1.24.1 in /usr/lib/
Requirement already satisfied: fastbencode in /usr/lib/
Requirement already satisfied: configobj in /usr/lib/
Collecting dulwich>=0.21.2
Using cached dulwich-
Requirement already satisfied: merge3 in /usr/lib/
Requirement already satisfied: pyyaml in /usr/lib/
Requirement already satisfied: launchpadlib>=1.6.3 in /usr/lib/
Collecting python-subunit
Using cached python_
Collecting testscenarios
Using cached testscenarios-
Collecting testtools
Using cached testtools-
Requirement already satisfied: flake8 in /usr/lib/
Requirement already satisfied: gpg in /usr/lib/
Collecting fastimport
Using cached fastimport-
Requirement already satisfied: paramiko in /usr/local/
Requirement already satisfied: setuptools in ./lib/python3.
Collecting docutils
Using cached docutils-
Collecting sphinx
Using cached sphinx-
Collecting sphinx-epytext
Using cached sphinx_
Requirement already satisfied: httplib2 in /usr/lib/
Requirement already satisfied: lazr.restfulcli
Requirement already satisfied: l...