Merge lp:~jelmer/brz/gitea into lp:brz/3.3

Proposed by Jelmer Vernooij
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
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

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :
Download full text (68.0 KiB)

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
  Using cached setuptools_gettext-0.1.2-py3-none-any.whl (10 kB)
Requirement already satisfied: setuptools>=46.1 in ./lib/python3.11/site-packages (from setuptools-gettext) (66.1.1)
Installing collected packages: setuptools-gettext
Successfully installed setuptools-gettext-0.1.2
Obtaining file:///tmp/tarmac/branch.w5kh4zqy
  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/python3/dist-packages (from breezy==3.3.3.dev0) (0.2.3)
Requirement already satisfied: urllib3>=1.24.1 in /usr/lib/python3/dist-packages (from breezy==3.3.3.dev0) (1.26.12)
Requirement already satisfied: fastbencode in /usr/lib/python3/dist-packages (from breezy==3.3.3.dev0) (0.0.12)
Requirement already satisfied: configobj in /usr/lib/python3/dist-packages (from breezy==3.3.3.dev0) (5.1.0.dev0)
Collecting dulwich>=0.21.2
  Using cached dulwich-0.21.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (507 kB)
Requirement already satisfied: merge3 in /usr/lib/python3/dist-packages (from breezy==3.3.3.dev0) (0.0.8)
Requirement already satisfied: pyyaml in /usr/lib/python3/dist-packages (from breezy==3.3.3.dev0) (6.0)
Requirement already satisfied: launchpadlib>=1.6.3 in /usr/lib/python3/dist-packages (from breezy==3.3.3.dev0) (1.11.0)
Collecting python-subunit
  Using cached python_subunit-1.4.2-py3-none-any.whl (106 kB)
Collecting testscenarios
  Using cached testscenarios-0.5.0-py2.py3-none-any.whl (21 kB)
Collecting testtools
  Using cached testtools-2.5.0-py3-none-any.whl (181 kB)
Requirement already satisfied: flake8 in /usr/lib/python3/dist-packages (from breezy==3.3.3.dev0) (5.0.4)
Requirement already satisfied: gpg in /usr/lib/python3/dist-packages (from breezy==3.3.3.dev0) (1.18.0)
Collecting fastimport
  Using cached fastimport-0.9.14-py2.py3-none-any.whl
Requirement already satisfied: paramiko in /usr/local/lib/python3.11/dist-packages (from breezy==3.3.3.dev0) (2.12.0)
Requirement already satisfied: setuptools in ./lib/python3.11/site-packages (from breezy==3.3.3.dev0) (66.1.1)
Collecting docutils
  Using cached docutils-0.19-py3-none-any.whl (570 kB)
Collecting sphinx
  Using cached sphinx-6.1.3-py3-none-any.whl (3.0 MB)
Collecting sphinx-epytext
  Using cached sphinx_epytext-0.0.4-py3-none-any.whl
Requirement already satisfied: httplib2 in /usr/lib/python3/dist-packages (from launchpadlib>=1.6.3->breezy==3.3.3.dev0) (0.20.4)
Requirement already satisfied: lazr.restfulclient>=0.14.2 in /usr/lib/python3/dist-packages (from launchpadlib>=1.6.3->breezy==3.3.3.dev0) (0.14.5)
Requirement already satisfied: l...

Revision history for this message
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://code.launchpad.net/~jelmer/brz/gitea/+merge/436917
> Your team Breezy developers is subscribed to branch lp:brz/3.3.
>

Revision history for this message
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://code.launchpad.net/~jelmer/brz/gitea/+merge/436917
> > Your team Breezy developers is subscribed to branch lp:brz/3.3.
> >
>
> --
> https://code.launchpad.net/~jelmer/brz/gitea/+merge/436917
> 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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches