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 __future__ import absolute_import
21
from io import StringIO
24
branch as _mod_branch,
28
missing as _mod_missing,
32
from ...i18n import gettext
33
from ...commands import Command
34
from ...option import (
39
from ...sixish import text_type
40
from ...trace import note, warning
42
propose as _mod_propose,
46
def branch_name(branch):
49
return urlutils.basename(branch.user_url)
52
def _check_already_merged(branch, target):
53
# TODO(jelmer): Check entire ancestry rather than just last revision?
54
if branch.last_revision() == target.last_revision():
55
raise errors.CommandError(gettext(
56
'All local changes are already present in target.'))
59
class cmd_publish_derived(Command):
60
__doc__ = """Publish a derived branch.
62
Try to create a public copy of a local branch on a hosting site,
63
derived from the specified base branch.
65
Reasonable defaults are picked for owner name, branch name and project
66
name, but they can also be overridden from the command-line.
71
Option('owner', help='Owner of the new remote branch.', type=str),
72
Option('project', help='Project name for the new remote branch.',
74
Option('name', help='Name of the new remote branch.', type=str),
75
Option('no-allow-lossy',
76
help='Allow fallback to lossy push, if necessary.'),
77
Option('overwrite', help="Overwrite existing commits."),
79
takes_args = ['submit_branch?']
81
def run(self, submit_branch=None, owner=None, name=None, project=None,
82
no_allow_lossy=False, overwrite=False, directory='.'):
83
local_branch = _mod_branch.Branch.open_containing(directory)[0]
84
self.add_cleanup(local_branch.lock_write().unlock)
85
if submit_branch is None:
86
submit_branch = local_branch.get_submit_branch()
87
note(gettext('Using submit branch %s') % submit_branch)
88
if submit_branch is None:
89
submit_branch = local_branch.get_parent()
90
note(gettext('Using parent branch %s') % submit_branch)
91
submit_branch = _mod_branch.Branch.open(submit_branch)
92
_check_already_merged(local_branch, submit_branch)
94
name = branch_name(local_branch)
95
hoster = _mod_propose.get_hoster(submit_branch)
96
remote_branch, public_url = hoster.publish_derived(
97
local_branch, submit_branch, name=name, project=project,
98
owner=owner, allow_lossy=not no_allow_lossy,
100
local_branch.set_push_location(remote_branch.user_url)
101
local_branch.set_public_branch(public_url)
102
local_branch.set_submit_branch(submit_branch.user_url)
103
note(gettext("Pushed to %s") % public_url)
106
def summarize_unmerged(local_branch, remote_branch, target,
107
prerequisite_branch=None):
108
"""Generate a text description of the unmerged revisions in branch.
110
:param branch: The proposed branch
111
:param target: Target branch
112
:param prerequisite_branch: Optional prerequisite branch
115
log_format = _mod_log.log_formatter_registry.get_default(local_branch)
117
lf = log_format(to_file=to_file, show_ids=False, show_timezone='original')
118
if prerequisite_branch:
119
local_extra = _mod_missing.find_unmerged(
120
remote_branch, prerequisite_branch, restrict='local')[0]
122
local_extra = _mod_missing.find_unmerged(
123
remote_branch, target, restrict='local')[0]
125
if remote_branch.supports_tags():
126
rev_tag_dict = remote_branch.tags.get_reverse_tag_dict()
130
for revision in _mod_missing.iter_log_revisions(
131
local_extra, local_branch.repository, False, rev_tag_dict):
132
lf.log_revision(revision)
133
return to_file.getvalue()
136
class cmd_propose_merge(Command):
137
__doc__ = """Propose a branch for merging.
139
This command creates a merge proposal for the local
140
branch to the target branch. The format of the merge
141
proposal depends on the submit branch.
148
help='Use the hoster.',
149
lazy_registry=('breezy.propose', 'hosters')),
150
ListOption('reviewers', short_name='R', type=text_type,
151
help='Requested reviewers.'),
152
Option('name', help='Name of the new remote branch.', type=str),
153
Option('description', help='Description of the change.', type=str),
154
Option('prerequisite', help='Prerequisite branch.', type=str),
155
Option('wip', help='Mark merge request as work-in-progress'),
158
help='Set commit message for merge, if supported', type=str),
159
ListOption('labels', short_name='l', type=text_type,
160
help='Labels to apply.'),
161
Option('no-allow-lossy',
162
help='Allow fallback to lossy push, if necessary.'),
163
Option('allow-collaboration',
164
help='Allow collaboration from target branch maintainer(s)'),
165
Option('allow-empty',
166
help='Do not prevent empty merge proposals.'),
167
Option('overwrite', help="Overwrite existing commits."),
169
takes_args = ['submit_branch?']
171
aliases = ['propose']
173
def run(self, submit_branch=None, directory='.', hoster=None,
174
reviewers=None, name=None, no_allow_lossy=False, description=None,
175
labels=None, prerequisite=None, commit_message=None, wip=False,
176
allow_collaboration=False, allow_empty=False, overwrite=False):
177
tree, branch, relpath = (
178
controldir.ControlDir.open_containing_tree_or_branch(directory))
179
if submit_branch is None:
180
submit_branch = branch.get_submit_branch()
181
if submit_branch is None:
182
submit_branch = branch.get_parent()
183
if submit_branch is None:
184
raise errors.CommandError(
185
gettext("No target location specified or remembered"))
186
target = _mod_branch.Branch.open(submit_branch)
188
_check_already_merged(branch, target)
190
hoster = _mod_propose.get_hoster(target)
192
hoster = hoster.probe(target)
194
name = branch_name(branch)
195
remote_branch, public_branch_url = hoster.publish_derived(
196
branch, target, name=name, allow_lossy=not no_allow_lossy,
198
branch.set_push_location(remote_branch.user_url)
199
branch.set_submit_branch(target.user_url)
200
note(gettext('Published branch to %s') % public_branch_url)
201
if prerequisite is not None:
202
prerequisite_branch = _mod_branch.Branch.open(prerequisite)
204
prerequisite_branch = None
205
proposal_builder = hoster.get_proposer(remote_branch, target)
206
if description is None:
207
body = proposal_builder.get_initial_body()
208
info = proposal_builder.get_infotext()
209
info += "\n\n" + summarize_unmerged(
210
branch, remote_branch, target, prerequisite_branch)
211
description = msgeditor.edit_commit_message(
212
info, start_message=body)
214
proposal = proposal_builder.create_proposal(
215
description=description, reviewers=reviewers,
216
prerequisite_branch=prerequisite_branch, labels=labels,
217
commit_message=commit_message,
218
work_in_progress=wip, allow_collaboration=allow_collaboration)
219
except _mod_propose.MergeProposalExists as e:
220
note(gettext('There is already a branch merge proposal: %s'), e.url)
222
note(gettext('Merge proposal created: %s') % proposal.url)
225
class cmd_find_merge_proposal(Command):
226
__doc__ = """Find a merge proposal.
230
takes_options = ['directory']
231
takes_args = ['submit_branch?']
232
aliases = ['find-proposal']
234
def run(self, directory='.', submit_branch=None):
235
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
237
public_location = branch.get_public_branch()
239
branch = _mod_branch.Branch.open(public_location)
240
if submit_branch is None:
241
submit_branch = branch.get_submit_branch()
242
if submit_branch is None:
243
submit_branch = branch.get_parent()
244
if submit_branch is None:
245
raise errors.CommandError(
246
gettext("No target location specified or remembered"))
248
target = _mod_branch.Branch.open(submit_branch)
249
hoster = _mod_propose.get_hoster(branch)
250
for mp in hoster.iter_proposals(branch, target):
251
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
254
class cmd_my_merge_proposals(Command):
255
__doc__ = """List all merge proposals owned by the logged-in user.
261
takes_args = ['base-url?']
264
RegistryOption.from_kwargs(
266
title='Proposal Status',
267
help='Only include proposals with specified status.',
270
all='All merge proposals',
271
open='Open merge proposals',
272
merged='Merged merge proposals',
273
closed='Closed merge proposals'),
276
help='Use the hoster.',
277
lazy_registry=('breezy.propose', 'hosters')),
280
def run(self, status='open', verbose=False, hoster=None, base_url=None):
282
for instance in _mod_propose.iter_hoster_instances(hoster=hoster):
283
if base_url is not None and instance.base_url != base_url:
286
for mp in instance.iter_my_proposals(status=status):
287
self.outf.write('%s\n' % mp.url)
289
source_branch_url = mp.get_source_branch_url()
290
if source_branch_url:
292
'(Merging %s into %s)\n' %
294
mp.get_target_branch_url()))
297
'(Merging into %s)\n' %
298
mp.get_target_branch_url())
299
description = mp.get_description()
301
self.outf.writelines(
303
for l in description.splitlines()])
304
self.outf.write('\n')
305
except _mod_propose.HosterLoginRequired as e:
306
warning('Skipping %r, login required.', instance)
309
class cmd_land_merge_proposal(Command):
310
__doc__ = """Land a merge proposal."""
314
Option('message', help='Commit message to use.', type=str)]
316
def run(self, url, message=None):
317
proposal = _mod_propose.get_proposal_by_url(url)
318
proposal.merge(commit_message=message)
321
class cmd_hosters(Command):
322
__doc__ = """List all known hosting sites and user details."""
327
for instance in _mod_propose.iter_hoster_instances():
328
current_user = instance.get_current_user()
329
if current_user is not None:
330
current_user_url = instance.get_user_url(current_user)
331
if current_user_url is not None:
333
gettext('%s (%s) - user: %s (%s)\n') % (
334
instance.name, instance.base_url,
335
current_user, current_user_url))
338
gettext('%s (%s) - user: %s\n') % (
339
instance.name, instance.base_url,
343
gettext('%s (%s) - not logged in\n') % (
344
instance.name, instance.base_url))