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
class cmd_publish_derived(Command):
50
__doc__ = """Publish a derived branch.
52
Try to create a public copy of a local branch on a hosting site,
53
derived from the specified base branch.
55
Reasonable defaults are picked for owner name, branch name and project
56
name, but they can also be overridden from the command-line.
61
Option('owner', help='Owner of the new remote branch.', type=str),
62
Option('project', help='Project name for the new remote branch.',
64
Option('name', help='Name of the new remote branch.', type=str),
65
Option('no-allow-lossy',
66
help='Allow fallback to lossy push, if necessary.'),
67
Option('overwrite', help="Overwrite existing commits."),
69
takes_args = ['submit_branch?']
71
def run(self, submit_branch=None, owner=None, name=None, project=None,
72
no_allow_lossy=False, overwrite=False, directory='.'):
73
local_branch = _mod_branch.Branch.open_containing(directory)[0]
74
self.add_cleanup(local_branch.lock_write().unlock)
75
if submit_branch is None:
76
submit_branch = local_branch.get_submit_branch()
77
note(gettext('Using submit branch %s') % submit_branch)
78
if submit_branch is None:
79
submit_branch = local_branch.get_parent()
80
note(gettext('Using parent branch %s') % submit_branch)
81
submit_branch = _mod_branch.Branch.open(submit_branch)
83
name = branch_name(local_branch)
84
hoster = _mod_propose.get_hoster(submit_branch)
85
remote_branch, public_url = hoster.publish_derived(
86
local_branch, submit_branch, name=name, project=project,
87
owner=owner, allow_lossy=not no_allow_lossy,
89
local_branch.set_push_location(remote_branch.user_url)
90
local_branch.set_public_branch(public_url)
91
local_branch.set_submit_branch(submit_branch.user_url)
92
note(gettext("Pushed to %s") % public_url)
95
def summarize_unmerged(local_branch, remote_branch, target,
96
prerequisite_branch=None):
97
"""Generate a text description of the unmerged revisions in branch.
99
:param branch: The proposed branch
100
:param target: Target branch
101
:param prerequisite_branch: Optional prerequisite branch
104
log_format = _mod_log.log_formatter_registry.get_default(local_branch)
106
lf = log_format(to_file=to_file, show_ids=False, show_timezone='original')
107
if prerequisite_branch:
108
local_extra = _mod_missing.find_unmerged(
109
remote_branch, prerequisite_branch, restrict='local')[0]
111
local_extra = _mod_missing.find_unmerged(
112
remote_branch, target, restrict='local')[0]
114
if remote_branch.supports_tags():
115
rev_tag_dict = remote_branch.tags.get_reverse_tag_dict()
119
for revision in _mod_missing.iter_log_revisions(
120
local_extra, local_branch.repository, False, rev_tag_dict):
121
lf.log_revision(revision)
122
return to_file.getvalue()
125
class cmd_propose_merge(Command):
126
__doc__ = """Propose a branch for merging.
128
This command creates a merge proposal for the local
129
branch to the target branch. The format of the merge
130
proposal depends on the submit branch.
137
help='Use the hoster.',
138
lazy_registry=('breezy.plugins.propose.propose', 'hosters')),
139
ListOption('reviewers', short_name='R', type=str,
140
help='Requested reviewers.'),
141
Option('name', help='Name of the new remote branch.', type=str),
142
Option('description', help='Description of the change.', type=str),
143
Option('prerequisite', help='Prerequisite branch.', type=str),
144
Option('wip', help='Mark merge request as work-in-progress'),
147
help='Set commit message for merge, if supported', type=str),
148
ListOption('labels', short_name='l', type=str,
149
help='Labels to apply.'),
150
Option('no-allow-lossy',
151
help='Allow fallback to lossy push, if necessary.'),
152
Option('allow-collaboration',
153
help='Allow collaboration from target branch maintainer(s)'),
155
takes_args = ['submit_branch?']
157
aliases = ['propose']
159
def run(self, submit_branch=None, directory='.', hoster=None,
160
reviewers=None, name=None, no_allow_lossy=False, description=None,
161
labels=None, prerequisite=None, commit_message=None, wip=False,
162
allow_collaboration=False):
163
tree, branch, relpath = (
164
controldir.ControlDir.open_containing_tree_or_branch(directory))
165
if submit_branch is None:
166
submit_branch = branch.get_submit_branch()
167
if submit_branch is None:
168
submit_branch = branch.get_parent()
169
if submit_branch is None:
170
raise errors.BzrCommandError(
171
gettext("No target location specified or remembered"))
173
target = _mod_branch.Branch.open(submit_branch)
175
hoster = _mod_propose.get_hoster(target)
177
hoster = hoster.probe(target)
179
name = branch_name(branch)
180
remote_branch, public_branch_url = hoster.publish_derived(
181
branch, target, name=name, allow_lossy=not no_allow_lossy)
182
branch.set_push_location(remote_branch.user_url)
183
branch.set_submit_branch(target.user_url)
184
note(gettext('Published branch to %s') % public_branch_url)
185
if prerequisite is not None:
186
prerequisite_branch = _mod_branch.Branch.open(prerequisite)
188
prerequisite_branch = None
189
proposal_builder = hoster.get_proposer(remote_branch, target)
190
if description is None:
191
body = proposal_builder.get_initial_body()
192
info = proposal_builder.get_infotext()
193
info += "\n\n" + summarize_unmerged(
194
branch, remote_branch, target, prerequisite_branch)
195
description = msgeditor.edit_commit_message(
196
info, start_message=body)
198
proposal = proposal_builder.create_proposal(
199
description=description, reviewers=reviewers,
200
prerequisite_branch=prerequisite_branch, labels=labels,
201
commit_message=commit_message,
202
work_in_progress=wip, allow_collaboration=allow_collaboration)
203
except _mod_propose.MergeProposalExists as e:
204
note(gettext('There is already a branch merge proposal: %s'), e.url)
206
note(gettext('Merge proposal created: %s') % proposal.url)
209
class cmd_find_merge_proposal(Command):
210
__doc__ = """Find a merge proposal.
214
takes_options = ['directory']
215
takes_args = ['submit_branch?']
216
aliases = ['find-proposal']
218
def run(self, directory='.', submit_branch=None):
219
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
221
public_location = branch.get_public_branch()
223
branch = _mod_branch.Branch.open(public_location)
224
if submit_branch is None:
225
submit_branch = branch.get_submit_branch()
226
if submit_branch is None:
227
submit_branch = branch.get_parent()
228
if submit_branch is None:
229
raise errors.BzrCommandError(
230
gettext("No target location specified or remembered"))
232
target = _mod_branch.Branch.open(submit_branch)
233
hoster = _mod_propose.get_hoster(branch)
234
for mp in hoster.iter_proposals(branch, target):
235
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
238
class cmd_github_login(Command):
239
__doc__ = """Log into GitHub.
241
When communicating with GitHub, some commands need to authenticate to
245
takes_args = ['username?']
247
def run(self, username=None):
248
from github import Github, GithubException
249
from breezy.config import AuthenticationConfig
250
authconfig = AuthenticationConfig()
252
username = authconfig.get_user(
253
'https', 'github.com', prompt=u'GitHub username', ask=True)
254
password = authconfig.get_password('https', 'github.com', username)
255
client = Github(username, password)
256
user = client.get_user()
258
authorization = user.create_authorization(
259
scopes=['user', 'repo', 'delete_repo'], note='Breezy',
260
note_url='https://github.com/breezy-team/breezy')
261
except GithubException as e:
262
errs = e.data.get('errors', [])
264
err_code = errs[0].get('code')
265
if err_code == u'already_exists':
266
raise errors.BzrCommandError('token already exists')
267
raise errors.BzrCommandError(e.data['message'])
268
# TODO(jelmer): This should really use something in
269
# AuthenticationConfig
270
from .github import store_github_token
271
store_github_token(scheme='https', host='github.com',
272
token=authorization.token)
275
class cmd_gitlab_login(Command):
276
__doc__ = """Log into a GitLab instance.
278
This command takes a GitLab instance URL (e.g. https://gitlab.com)
279
as well as an optional private token. Private tokens can be created via the
284
Log into GNOME's GitLab (prompts for a token):
286
brz gitlab-login https://gitlab.gnome.org/
288
Log into Debian's salsa, using a token created earlier:
290
brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
293
takes_args = ['url', 'private_token?']
296
Option('name', help='Name for GitLab site in configuration.',
299
"Don't check that the token is valid."),
302
def run(self, url, private_token=None, name=None, no_check=False):
303
from breezy import ui
304
from .gitlabs import store_gitlab_token
307
name = urlutils.parse_url(url)[3].split('.')[-2]
308
except (ValueError, IndexError):
309
raise errors.BzrCommandError(
310
'please specify a site name with --name')
311
if private_token is None:
312
note("Please visit %s to obtain a private token.",
313
urlutils.join(url, "profile/personal_access_tokens"))
314
private_token = ui.ui_factory.get_password(u'Private token')
316
from breezy.transport import get_transport
317
from .gitlabs import GitLab
318
GitLab(get_transport(url), private_token=private_token)
319
store_gitlab_token(name=name, url=url, private_token=private_token)
322
class cmd_my_merge_proposals(Command):
323
__doc__ = """List all merge proposals owned by the logged-in user.
331
RegistryOption.from_kwargs(
333
title='Proposal Status',
334
help='Only include proposals with specified status.',
337
all='All merge proposals',
338
open='Open merge proposals',
339
merged='Merged merge proposals',
340
closed='Closed merge proposals')]
342
def run(self, status='open', verbose=False):
343
for name, hoster_cls in _mod_propose.hosters.items():
344
for instance in hoster_cls.iter_instances():
345
for mp in instance.iter_my_proposals(status=status):
346
self.outf.write('%s\n' % mp.url)
349
'(Merging %s into %s)\n' %
350
(mp.get_source_branch_url(),
351
mp.get_target_branch_url()))
352
description = mp.get_description()
354
self.outf.writelines(
356
for l in description.splitlines()])
357
self.outf.write('\n')
360
class cmd_land_merge_proposal(Command):
361
__doc__ = """Land a merge proposal."""
365
Option('message', help='Commit message to use.', type=str)]
367
def run(self, url, message=None):
368
proposal = _mod_propose.get_proposal_by_url(url)
369
proposal.merge(commit_message=message)