1
# Copyright (C) 2018 Breezy Developers
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Helper functions for proposing merges."""
19
from __future__ import absolute_import
29
class NoSuchProject(errors.BzrError):
31
_fmt = "Project does not exist: %(project)s."
33
def __init__(self, project):
34
errors.BzrError.__init__(self)
35
self.project = project
38
class MergeProposalExists(errors.BzrError):
40
_fmt = "A merge proposal already exists: %(url)s."
42
def __init__(self, url):
43
errors.BzrError.__init__(self)
47
class UnsupportedHoster(errors.BzrError):
49
_fmt = "No supported hoster for %(branch)s."
51
def __init__(self, branch):
52
errors.BzrError.__init__(self)
56
class ProposeMergeHooks(hooks.Hooks):
57
"""Hooks for proposing a merge on Launchpad."""
60
hooks.Hooks.__init__(self, __name__, "Proposer.hooks")
63
"Return the prerequisite branch for proposing as merge.", (3, 0))
65
'merge_proposal_body',
66
"Return an initial body for the merge proposal message.", (3, 0))
69
class LabelsUnsupported(errors.BzrError):
70
"""Labels not supported by this hoster."""
72
_fmt = "Labels are not supported by %(hoster)r."
74
def __init__(self, hoster):
75
errors.BzrError.__init__(self)
79
class PrerequisiteBranchUnsupported(errors.BzrError):
80
"""Prerequisite branch not supported by this hoster."""
82
def __init__(self, hoster):
83
errors.BzrError.__init__(self)
87
class HosterLoginRequired(errors.BzrError):
88
"""Action requires hoster login credentials."""
90
_fmt = "Action requires credentials for hosting site %(hoster)r."""
92
def __init__(self, hoster):
93
errors.BzrError.__init__(self)
97
class MergeProposal(object):
100
:ivar url: URL for the merge proposal
103
def __init__(self, url=None):
106
def get_description(self):
107
"""Get the description of the merge proposal."""
108
raise NotImplementedError(self.get_description)
110
def set_description(self, description):
111
"""Set the description of the merge proposal."""
112
raise NotImplementedError(self.set_description)
114
def get_commit_message(self):
115
"""Get the proposed commit message."""
116
raise NotImplementedError(self.get_commit_message)
118
def set_commit_message(self, commit_message):
119
"""Set the propose commit message."""
120
raise NotImplementedError(self.set_commit_message)
122
def get_source_branch_url(self):
123
"""Return the source branch."""
124
raise NotImplementedError(self.get_source_branch_url)
126
def get_target_branch_url(self):
127
"""Return the target branch."""
128
raise NotImplementedError(self.get_target_branch_url)
131
"""Close the merge proposal (without merging it)."""
132
raise NotImplementedError(self.close)
135
"""Check whether this merge proposal has been merged."""
136
raise NotImplementedError(self.is_merged)
138
def merge(self, commit_message=None):
139
"""Merge this merge proposal."""
140
raise NotImplementedError(self.merge)
143
class MergeProposalBuilder(object):
144
"""Merge proposal creator.
146
:param source_branch: Branch to propose for merging
147
:param target_branch: Target branch
150
hooks = ProposeMergeHooks()
152
def __init__(self, source_branch, target_branch):
153
self.source_branch = source_branch
154
self.target_branch = target_branch
156
def get_initial_body(self):
157
"""Get a body for the proposal for the user to modify.
159
:return: a str or None.
161
raise NotImplementedError(self.get_initial_body)
163
def get_infotext(self):
164
"""Determine the initial comment for the merge proposal.
166
raise NotImplementedError(self.get_infotext)
168
def create_proposal(self, description, reviewers=None, labels=None,
169
prerequisite_branch=None, commit_message=None):
170
"""Create a proposal to merge a branch for merging.
172
:param description: Description for the merge proposal
173
:param reviewers: Optional list of people to ask reviews from
174
:param labels: Labels to attach to the proposal
175
:param prerequisite_branch: Optional prerequisite branch
176
:param commit_message: Optional commit message
177
:return: A `MergeProposal` object
179
raise NotImplementedError(self.create_proposal)
182
class Hoster(object):
183
"""A hosting site manager.
186
# Does this hoster support arbitrary labels being attached to merge
188
supports_merge_proposal_labels = None
190
# Does this hoster support suggesting a commit message in the
192
supports_merge_proposal_commit_message = None
194
# The base_url that would be visible to users. I.e. https://github.com/
195
# rather than https://api.github.com/
198
def publish_derived(self, new_branch, base_branch, name, project=None,
199
owner=None, revision_id=None, overwrite=False,
201
"""Publish a branch to the site, derived from base_branch.
203
:param base_branch: branch to derive the new branch from
204
:param new_branch: branch to publish
205
:return: resulting branch, public URL
206
:raise HosterLoginRequired: Action requires a hoster login, but none is
209
raise NotImplementedError(self.publish)
211
def get_derived_branch(self, base_branch, name, project=None, owner=None):
212
"""Get a derived branch ('a fork').
214
raise NotImplementedError(self.get_derived_branch)
216
def get_push_url(self, branch):
217
"""Get the push URL for a branch."""
218
raise NotImplementedError(self.get_push_url)
220
def get_proposer(self, source_branch, target_branch):
221
"""Get a merge proposal creator.
223
:note: source_branch does not have to be hosted by the hoster.
225
:param source_branch: Source branch
226
:param target_branch: Target branch
227
:return: A MergeProposalBuilder object
229
raise NotImplementedError(self.get_proposer)
231
def iter_proposals(self, source_branch, target_branch, status='open'):
232
"""Get the merge proposals for a specified branch tuple.
234
:param source_branch: Source branch
235
:param target_branch: Target branch
236
:param status: Status of proposals to iterate over
237
:return: Iterate over MergeProposal object
239
raise NotImplementedError(self.iter_proposals)
241
def get_proposal_by_url(self, url):
242
"""Retrieve a branch proposal by URL.
244
:param url: Merge proposal URL.
245
:return: MergeProposal object
246
:raise UnsupportedHoster: Hoster does not support this URL
248
raise NotImplementedError(self.get_proposal_by_url)
250
def hosts(self, branch):
251
"""Return true if this hoster hosts given branch."""
252
raise NotImplementedError(self.hosts)
255
def probe_from_branch(cls, branch):
256
"""Create a Hoster object if this hoster knows about a branch."""
257
url = urlutils.split_segment_parameters(branch.user_url)[0]
258
return cls.probe_from_url(
259
url, possible_transports=[branch.control_transport])
262
def probe_from_url(cls, url, possible_hosters=None):
263
"""Create a Hoster object if this hoster knows about a URL."""
264
raise NotImplementedError(cls.probe_from_url)
266
# TODO(jelmer): Some way of cleaning up old branch proposals/branches
268
def iter_my_proposals(self, status='open'):
269
"""Iterate over the proposals created by the currently logged in user.
271
:param status: Only yield proposals with this status
272
(one of: 'open', 'closed', 'merged', 'all')
273
:return: Iterator over MergeProposal objects
274
:raise HosterLoginRequired: Action requires a hoster login, but none is
277
raise NotImplementedError(self.iter_my_proposals)
280
def iter_instances(cls):
281
"""Iterate instances.
283
:return: Hoster instances
285
raise NotImplementedError(cls.iter_instances)
288
def get_hoster(branch, possible_hosters=None):
289
"""Find the hoster for a branch."""
291
for hoster in possible_hosters:
292
if hoster.hosts(branch):
294
for name, hoster_cls in hosters.items():
296
hoster = hoster_cls.probe_from_branch(branch)
297
except UnsupportedHoster:
300
if possible_hosters is not None:
301
possible_hosters.append(hoster)
303
raise UnsupportedHoster(branch)
306
def get_proposal_by_url(url):
307
for name, hoster_cls in hosters.items():
308
for instance in hoster_cls.iter_instances():
310
return instance.get_proposal_by_url(url)
311
except UnsupportedHoster:
313
raise UnsupportedHoster(url)
316
hosters = registry.Registry()
317
hosters.register_lazy(
318
"launchpad", "breezy.plugins.propose.launchpad",
320
hosters.register_lazy(
321
"github", "breezy.plugins.propose.github",
323
hosters.register_lazy(
324
"gitlab", "breezy.plugins.propose.gitlabs",