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
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.plugins.propose.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.'),
168
takes_args = ['submit_branch?']
170
aliases = ['propose']
172
def run(self, submit_branch=None, directory='.', hoster=None,
173
reviewers=None, name=None, no_allow_lossy=False, description=None,
174
labels=None, prerequisite=None, commit_message=None, wip=False,
175
allow_collaboration=False, allow_empty=False):
176
tree, branch, relpath = (
177
controldir.ControlDir.open_containing_tree_or_branch(directory))
178
if submit_branch is None:
179
submit_branch = branch.get_submit_branch()
180
if submit_branch is None:
181
submit_branch = branch.get_parent()
182
if submit_branch is None:
183
raise errors.CommandError(
184
gettext("No target location specified or remembered"))
185
target = _mod_branch.Branch.open(submit_branch)
187
_check_already_merged(branch, target)
189
hoster = _mod_propose.get_hoster(target)
191
hoster = hoster.probe(target)
193
name = branch_name(branch)
194
remote_branch, public_branch_url = hoster.publish_derived(
195
branch, target, name=name, allow_lossy=not no_allow_lossy)
196
branch.set_push_location(remote_branch.user_url)
197
branch.set_submit_branch(target.user_url)
198
note(gettext('Published branch to %s') % public_branch_url)
199
if prerequisite is not None:
200
prerequisite_branch = _mod_branch.Branch.open(prerequisite)
202
prerequisite_branch = None
203
proposal_builder = hoster.get_proposer(remote_branch, target)
204
if description is None:
205
body = proposal_builder.get_initial_body()
206
info = proposal_builder.get_infotext()
207
info += "\n\n" + summarize_unmerged(
208
branch, remote_branch, target, prerequisite_branch)
209
description = msgeditor.edit_commit_message(
210
info, start_message=body)
212
proposal = proposal_builder.create_proposal(
213
description=description, reviewers=reviewers,
214
prerequisite_branch=prerequisite_branch, labels=labels,
215
commit_message=commit_message,
216
work_in_progress=wip, allow_collaboration=allow_collaboration)
217
except _mod_propose.MergeProposalExists as e:
218
note(gettext('There is already a branch merge proposal: %s'), e.url)
220
note(gettext('Merge proposal created: %s') % proposal.url)
223
class cmd_find_merge_proposal(Command):
224
__doc__ = """Find a merge proposal.
228
takes_options = ['directory']
229
takes_args = ['submit_branch?']
230
aliases = ['find-proposal']
232
def run(self, directory='.', submit_branch=None):
233
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
235
public_location = branch.get_public_branch()
237
branch = _mod_branch.Branch.open(public_location)
238
if submit_branch is None:
239
submit_branch = branch.get_submit_branch()
240
if submit_branch is None:
241
submit_branch = branch.get_parent()
242
if submit_branch is None:
243
raise errors.CommandError(
244
gettext("No target location specified or remembered"))
246
target = _mod_branch.Branch.open(submit_branch)
247
hoster = _mod_propose.get_hoster(branch)
248
for mp in hoster.iter_proposals(branch, target):
249
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
252
class cmd_my_merge_proposals(Command):
253
__doc__ = """List all merge proposals owned by the logged-in user.
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')]
272
def run(self, status='open', verbose=False):
273
for name, hoster_cls in _mod_propose.hosters.items():
274
for instance in hoster_cls.iter_instances():
275
for mp in instance.iter_my_proposals(status=status):
276
self.outf.write('%s\n' % mp.url)
279
'(Merging %s into %s)\n' %
280
(mp.get_source_branch_url(),
281
mp.get_target_branch_url()))
282
description = mp.get_description()
284
self.outf.writelines(
286
for l in description.splitlines()])
287
self.outf.write('\n')
290
class cmd_land_merge_proposal(Command):
291
__doc__ = """Land a merge proposal."""
295
Option('message', help='Commit message to use.', type=str)]
297
def run(self, url, message=None):
298
proposal = _mod_propose.get_proposal_by_url(url)
299
proposal.merge(commit_message=message)