1
# Copyright (C) 2018 Jelmer Vernooij <jelmer@jelmer.uk>
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
"""Propose command implementations."""
19
from io import StringIO
22
branch as _mod_branch,
26
missing as _mod_missing,
30
from ...i18n import gettext
31
from ...commands import Command
32
from ...option import (
37
from ...trace import note
39
propose as _mod_propose,
43
def branch_name(branch):
46
return urlutils.basename(branch.user_url)
49
def _check_already_merged(branch, target):
50
# TODO(jelmer): Check entire ancestry rather than just last revision?
51
if branch.last_revision() == target.last_revision():
52
raise errors.CommandError(gettext(
53
'All local changes are already present in target.'))
56
class cmd_publish_derived(Command):
57
__doc__ = """Publish a derived branch.
59
Try to create a public copy of a local branch on a hosting site,
60
derived from the specified base branch.
62
Reasonable defaults are picked for owner name, branch name and project
63
name, but they can also be overridden from the command-line.
68
Option('owner', help='Owner of the new remote branch.', type=str),
69
Option('project', help='Project name for the new remote branch.',
71
Option('name', help='Name of the new remote branch.', type=str),
72
Option('no-allow-lossy',
73
help='Allow fallback to lossy push, if necessary.'),
74
Option('overwrite', help="Overwrite existing commits."),
76
takes_args = ['submit_branch?']
78
def run(self, submit_branch=None, owner=None, name=None, project=None,
79
no_allow_lossy=False, overwrite=False, directory='.'):
80
local_branch = _mod_branch.Branch.open_containing(directory)[0]
81
self.add_cleanup(local_branch.lock_write().unlock)
82
if submit_branch is None:
83
submit_branch = local_branch.get_submit_branch()
84
note(gettext('Using submit branch %s') % submit_branch)
85
if submit_branch is None:
86
submit_branch = local_branch.get_parent()
87
note(gettext('Using parent branch %s') % submit_branch)
88
submit_branch = _mod_branch.Branch.open(submit_branch)
89
_check_already_merged(local_branch, submit_branch)
91
name = branch_name(local_branch)
92
hoster = _mod_propose.get_hoster(submit_branch)
93
remote_branch, public_url = hoster.publish_derived(
94
local_branch, submit_branch, name=name, project=project,
95
owner=owner, allow_lossy=not no_allow_lossy,
97
local_branch.set_push_location(remote_branch.user_url)
98
local_branch.set_public_branch(public_url)
99
local_branch.set_submit_branch(submit_branch.user_url)
100
note(gettext("Pushed to %s") % public_url)
103
def summarize_unmerged(local_branch, remote_branch, target,
104
prerequisite_branch=None):
105
"""Generate a text description of the unmerged revisions in branch.
107
:param branch: The proposed branch
108
:param target: Target branch
109
:param prerequisite_branch: Optional prerequisite branch
112
log_format = _mod_log.log_formatter_registry.get_default(local_branch)
114
lf = log_format(to_file=to_file, show_ids=False, show_timezone='original')
115
if prerequisite_branch:
116
local_extra = _mod_missing.find_unmerged(
117
remote_branch, prerequisite_branch, restrict='local')[0]
119
local_extra = _mod_missing.find_unmerged(
120
remote_branch, target, restrict='local')[0]
122
if remote_branch.supports_tags():
123
rev_tag_dict = remote_branch.tags.get_reverse_tag_dict()
127
for revision in _mod_missing.iter_log_revisions(
128
local_extra, local_branch.repository, False, rev_tag_dict):
129
lf.log_revision(revision)
130
return to_file.getvalue()
133
class cmd_propose_merge(Command):
134
__doc__ = """Propose a branch for merging.
136
This command creates a merge proposal for the local
137
branch to the target branch. The format of the merge
138
proposal depends on the submit branch.
145
help='Use the hoster.',
146
lazy_registry=('breezy.plugins.propose.propose', 'hosters')),
147
ListOption('reviewers', short_name='R', type=str,
148
help='Requested reviewers.'),
149
Option('name', help='Name of the new remote branch.', type=str),
150
Option('description', help='Description of the change.', type=str),
151
Option('prerequisite', help='Prerequisite branch.', type=str),
152
Option('wip', help='Mark merge request as work-in-progress'),
155
help='Set commit message for merge, if supported', type=str),
156
ListOption('labels', short_name='l', type=str,
157
help='Labels to apply.'),
158
Option('no-allow-lossy',
159
help='Allow fallback to lossy push, if necessary.'),
160
Option('allow-collaboration',
161
help='Allow collaboration from target branch maintainer(s)'),
162
Option('allow-empty',
163
help='Do not prevent empty merge proposals.'),
165
takes_args = ['submit_branch?']
167
aliases = ['propose']
169
def run(self, submit_branch=None, directory='.', hoster=None,
170
reviewers=None, name=None, no_allow_lossy=False, description=None,
171
labels=None, prerequisite=None, commit_message=None, wip=False,
172
allow_collaboration=False, allow_empty=False):
173
tree, branch, relpath = (
174
controldir.ControlDir.open_containing_tree_or_branch(directory))
175
if submit_branch is None:
176
submit_branch = branch.get_submit_branch()
177
if submit_branch is None:
178
submit_branch = branch.get_parent()
179
if submit_branch is None:
180
raise errors.CommandError(
181
gettext("No target location specified or remembered"))
182
target = _mod_branch.Branch.open(submit_branch)
184
_check_already_merged(branch, target)
186
hoster = _mod_propose.get_hoster(target)
188
hoster = hoster.probe(target)
190
name = branch_name(branch)
191
remote_branch, public_branch_url = hoster.publish_derived(
192
branch, target, name=name, allow_lossy=not no_allow_lossy)
193
branch.set_push_location(remote_branch.user_url)
194
branch.set_submit_branch(target.user_url)
195
note(gettext('Published branch to %s') % public_branch_url)
196
if prerequisite is not None:
197
prerequisite_branch = _mod_branch.Branch.open(prerequisite)
199
prerequisite_branch = None
200
proposal_builder = hoster.get_proposer(remote_branch, target)
201
if description is None:
202
body = proposal_builder.get_initial_body()
203
info = proposal_builder.get_infotext()
204
info += "\n\n" + summarize_unmerged(
205
branch, remote_branch, target, prerequisite_branch)
206
description = msgeditor.edit_commit_message(
207
info, start_message=body)
209
proposal = proposal_builder.create_proposal(
210
description=description, reviewers=reviewers,
211
prerequisite_branch=prerequisite_branch, labels=labels,
212
commit_message=commit_message,
213
work_in_progress=wip, allow_collaboration=allow_collaboration)
214
except _mod_propose.MergeProposalExists as e:
215
note(gettext('There is already a branch merge proposal: %s'), e.url)
217
note(gettext('Merge proposal created: %s') % proposal.url)
220
class cmd_find_merge_proposal(Command):
221
__doc__ = """Find a merge proposal.
225
takes_options = ['directory']
226
takes_args = ['submit_branch?']
227
aliases = ['find-proposal']
229
def run(self, directory='.', submit_branch=None):
230
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
232
public_location = branch.get_public_branch()
234
branch = _mod_branch.Branch.open(public_location)
235
if submit_branch is None:
236
submit_branch = branch.get_submit_branch()
237
if submit_branch is None:
238
submit_branch = branch.get_parent()
239
if submit_branch is None:
240
raise errors.CommandError(
241
gettext("No target location specified or remembered"))
243
target = _mod_branch.Branch.open(submit_branch)
244
hoster = _mod_propose.get_hoster(branch)
245
for mp in hoster.iter_proposals(branch, target):
246
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
249
class cmd_my_merge_proposals(Command):
250
__doc__ = """List all merge proposals owned by the logged-in user.
258
RegistryOption.from_kwargs(
260
title='Proposal Status',
261
help='Only include proposals with specified status.',
264
all='All merge proposals',
265
open='Open merge proposals',
266
merged='Merged merge proposals',
267
closed='Closed merge proposals')]
269
def run(self, status='open', verbose=False):
270
for instance in _mod_propose.iter_hoster_instances():
271
for mp in instance.iter_my_proposals(status=status):
272
self.outf.write('%s\n' % mp.url)
275
'(Merging %s into %s)\n' %
276
(mp.get_source_branch_url(),
277
mp.get_target_branch_url()))
278
description = mp.get_description()
280
self.outf.writelines(
282
for l in description.splitlines()])
283
self.outf.write('\n')
286
class cmd_land_merge_proposal(Command):
287
__doc__ = """Land a merge proposal."""
291
Option('message', help='Commit message to use.', type=str)]
293
def run(self, url, message=None):
294
proposal = _mod_propose.get_proposal_by_url(url)
295
proposal.merge(commit_message=message)
298
class cmd_hosters(Command):
299
__doc__ = """List all known hosting sites and user details."""
304
for instance in _mod_propose.iter_hoster_instances():
305
current_user = instance.get_current_user()
307
gettext('%s (%s) - user: %s (%s)\n') % (
308
instance.name, instance.base_url,
309
current_user, instance.get_user_url(current_user)))