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, warning
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.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.'),
164
Option('overwrite', help="Overwrite existing commits."),
166
takes_args = ['submit_branch?']
168
aliases = ['propose']
170
def run(self, submit_branch=None, directory='.', hoster=None,
171
reviewers=None, name=None, no_allow_lossy=False, description=None,
172
labels=None, prerequisite=None, commit_message=None, wip=False,
173
allow_collaboration=False, allow_empty=False, overwrite=False):
174
tree, branch, relpath = (
175
controldir.ControlDir.open_containing_tree_or_branch(directory))
176
if submit_branch is None:
177
submit_branch = branch.get_submit_branch()
178
if submit_branch is None:
179
submit_branch = branch.get_parent()
180
if submit_branch is None:
181
raise errors.CommandError(
182
gettext("No target location specified or remembered"))
183
target = _mod_branch.Branch.open(submit_branch)
185
_check_already_merged(branch, target)
187
hoster = _mod_propose.get_hoster(target)
189
hoster = hoster.probe(target)
191
name = branch_name(branch)
192
remote_branch, public_branch_url = hoster.publish_derived(
193
branch, target, name=name, allow_lossy=not no_allow_lossy,
195
branch.set_push_location(remote_branch.user_url)
196
branch.set_submit_branch(target.user_url)
197
note(gettext('Published branch to %s') % public_branch_url)
198
if prerequisite is not None:
199
prerequisite_branch = _mod_branch.Branch.open(prerequisite)
201
prerequisite_branch = None
202
proposal_builder = hoster.get_proposer(remote_branch, target)
203
if description is None:
204
body = proposal_builder.get_initial_body()
205
info = proposal_builder.get_infotext()
206
info += "\n\n" + summarize_unmerged(
207
branch, remote_branch, target, prerequisite_branch)
208
description = msgeditor.edit_commit_message(
209
info, start_message=body)
211
proposal = proposal_builder.create_proposal(
212
description=description, reviewers=reviewers,
213
prerequisite_branch=prerequisite_branch, labels=labels,
214
commit_message=commit_message,
215
work_in_progress=wip, allow_collaboration=allow_collaboration)
216
except _mod_propose.MergeProposalExists as e:
217
note(gettext('There is already a branch merge proposal: %s'), e.url)
219
note(gettext('Merge proposal created: %s') % proposal.url)
222
class cmd_find_merge_proposal(Command):
223
__doc__ = """Find a merge proposal.
227
takes_options = ['directory']
228
takes_args = ['submit_branch?']
229
aliases = ['find-proposal']
231
def run(self, directory='.', submit_branch=None):
232
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
234
public_location = branch.get_public_branch()
236
branch = _mod_branch.Branch.open(public_location)
237
if submit_branch is None:
238
submit_branch = branch.get_submit_branch()
239
if submit_branch is None:
240
submit_branch = branch.get_parent()
241
if submit_branch is None:
242
raise errors.CommandError(
243
gettext("No target location specified or remembered"))
245
target = _mod_branch.Branch.open(submit_branch)
246
hoster = _mod_propose.get_hoster(branch)
247
for mp in hoster.iter_proposals(branch, target):
248
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
251
class cmd_my_merge_proposals(Command):
252
__doc__ = """List all merge proposals owned by the logged-in user.
258
takes_args = ['base-url?']
261
RegistryOption.from_kwargs(
263
title='Proposal Status',
264
help='Only include proposals with specified status.',
267
all='All merge proposals',
268
open='Open merge proposals',
269
merged='Merged merge proposals',
270
closed='Closed merge proposals'),
273
help='Use the hoster.',
274
lazy_registry=('breezy.propose', 'hosters')),
277
def run(self, status='open', verbose=False, hoster=None, base_url=None):
279
for instance in _mod_propose.iter_hoster_instances(hoster=hoster):
280
if base_url is not None and instance.base_url != base_url:
283
for mp in instance.iter_my_proposals(status=status):
284
self.outf.write('%s\n' % mp.url)
286
source_branch_url = mp.get_source_branch_url()
287
if source_branch_url:
289
'(Merging %s into %s)\n' %
291
mp.get_target_branch_url()))
294
'(Merging into %s)\n' %
295
mp.get_target_branch_url())
296
description = mp.get_description()
298
self.outf.writelines(
300
for l in description.splitlines()])
301
self.outf.write('\n')
302
except _mod_propose.HosterLoginRequired as e:
303
warning('Skipping %r, login required.', instance)
306
class cmd_land_merge_proposal(Command):
307
__doc__ = """Land a merge proposal."""
311
Option('message', help='Commit message to use.', type=str)]
313
def run(self, url, message=None):
314
proposal = _mod_propose.get_proposal_by_url(url)
315
proposal.merge(commit_message=message)
318
class cmd_hosters(Command):
319
__doc__ = """List all known hosting sites and user details."""
324
for instance in _mod_propose.iter_hoster_instances():
325
current_user = instance.get_current_user()
326
if current_user is not None:
327
current_user_url = instance.get_user_url(current_user)
328
if current_user_url is not None:
330
gettext('%s (%s) - user: %s (%s)\n') % (
331
instance.name, instance.base_url,
332
current_user, current_user_url))
335
gettext('%s (%s) - user: %s\n') % (
336
instance.name, instance.base_url,
340
gettext('%s (%s) - not logged in\n') % (
341
instance.name, instance.base_url))