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 ...trace import note
41
propose as _mod_propose,
45
def branch_name(branch):
48
return urlutils.basename(branch.user_url)
51
class cmd_publish_derived(Command):
52
__doc__ = """Publish a derived branch.
54
Try to create a public copy of a local branch on a hosting site,
55
derived from the specified base branch.
57
Reasonable defaults are picked for owner name, branch name and project
58
name, but they can also be overridden from the command-line.
63
Option('owner', help='Owner of the new remote branch.', type=str),
64
Option('project', help='Project name for the new remote branch.',
66
Option('name', help='Name of the new remote branch.', type=str),
67
Option('no-allow-lossy',
68
help='Allow fallback to lossy push, if necessary.'),
69
Option('overwrite', help="Overwrite existing commits."),
71
takes_args = ['submit_branch?']
73
def run(self, submit_branch=None, owner=None, name=None, project=None,
74
no_allow_lossy=False, overwrite=False, directory='.'):
75
local_branch = _mod_branch.Branch.open_containing(directory)[0]
76
self.add_cleanup(local_branch.lock_write().unlock)
77
if submit_branch is None:
78
submit_branch = local_branch.get_submit_branch()
79
note(gettext('Using submit branch %s') % submit_branch)
80
if submit_branch is None:
81
submit_branch = local_branch.get_parent()
82
note(gettext('Using parent branch %s') % submit_branch)
83
submit_branch = _mod_branch.Branch.open(submit_branch)
85
name = branch_name(local_branch)
86
hoster = _mod_propose.get_hoster(submit_branch)
87
remote_branch, public_url = hoster.publish_derived(
88
local_branch, submit_branch, name=name, project=project,
89
owner=owner, allow_lossy=not no_allow_lossy,
91
local_branch.set_push_location(remote_branch.user_url)
92
local_branch.set_public_branch(public_url)
93
local_branch.set_submit_branch(submit_branch.user_url)
94
note(gettext("Pushed to %s") % public_url)
97
def summarize_unmerged(local_branch, remote_branch, target,
98
prerequisite_branch=None):
99
"""Generate a text description of the unmerged revisions in branch.
101
:param branch: The proposed branch
102
:param target: Target branch
103
:param prerequisite_branch: Optional prerequisite branch
106
log_format = _mod_log.log_formatter_registry.get_default(local_branch)
108
lf = log_format(to_file=to_file, show_ids=False, show_timezone='original')
109
if prerequisite_branch:
110
local_extra = _mod_missing.find_unmerged(
111
remote_branch, prerequisite_branch, restrict='local')[0]
113
local_extra = _mod_missing.find_unmerged(
114
remote_branch, target, restrict='local')[0]
116
if remote_branch.supports_tags():
117
rev_tag_dict = remote_branch.tags.get_reverse_tag_dict()
121
for revision in _mod_missing.iter_log_revisions(
122
local_extra, local_branch.repository, False, rev_tag_dict):
123
lf.log_revision(revision)
124
return to_file.getvalue()
127
class cmd_propose_merge(Command):
128
__doc__ = """Propose a branch for merging.
130
This command creates a merge proposal for the local
131
branch to the target branch. The format of the merge
132
proposal depends on the submit branch.
139
help='Use the hoster.',
140
lazy_registry=('breezy.plugins.propose.propose', 'hosters')),
141
ListOption('reviewers', short_name='R', type=str,
142
help='Requested reviewers.'),
143
Option('name', help='Name of the new remote branch.', type=str),
144
Option('description', help='Description of the change.', type=str),
145
Option('prerequisite', help='Prerequisite branch.', type=str),
146
Option('wip', help='Mark merge request as work-in-progress'),
149
help='Set commit message for merge, if supported', type=str),
150
ListOption('labels', short_name='l', type=str,
151
help='Labels to apply.'),
152
Option('no-allow-lossy',
153
help='Allow fallback to lossy push, if necessary.'),
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
tree, branch, relpath = (
163
controldir.ControlDir.open_containing_tree_or_branch(directory))
164
if submit_branch is None:
165
submit_branch = branch.get_submit_branch()
166
if submit_branch is None:
167
submit_branch = branch.get_parent()
168
if submit_branch is None:
169
raise errors.BzrCommandError(
170
gettext("No target location specified or remembered"))
172
target = _mod_branch.Branch.open(submit_branch)
174
hoster = _mod_propose.get_hoster(target)
176
hoster = hoster.probe(target)
178
name = branch_name(branch)
179
remote_branch, public_branch_url = hoster.publish_derived(
180
branch, target, name=name, allow_lossy=not no_allow_lossy)
181
branch.set_push_location(remote_branch.user_url)
182
branch.set_submit_branch(target.user_url)
183
note(gettext('Published branch to %s') % public_branch_url)
184
if prerequisite is not None:
185
prerequisite_branch = _mod_branch.Branch.open(prerequisite)
187
prerequisite_branch = None
188
proposal_builder = hoster.get_proposer(remote_branch, target)
189
if description is None:
190
body = proposal_builder.get_initial_body()
191
info = proposal_builder.get_infotext()
192
info += "\n\n" + summarize_unmerged(
193
branch, remote_branch, target, prerequisite_branch)
194
description = msgeditor.edit_commit_message(
195
info, start_message=body)
197
proposal = proposal_builder.create_proposal(
198
description=description, reviewers=reviewers,
199
prerequisite_branch=prerequisite_branch, labels=labels,
200
commit_message=commit_message,
201
work_in_progress=wip)
202
except _mod_propose.MergeProposalExists as e:
203
note(gettext('There is already a branch merge proposal: %s'), e.url)
205
note(gettext('Merge proposal created: %s') % proposal.url)
208
class cmd_find_merge_proposal(Command):
209
__doc__ = """Find a merge proposal.
213
takes_options = ['directory']
214
takes_args = ['submit_branch?']
215
aliases = ['find-proposal']
217
def run(self, directory='.', submit_branch=None):
218
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
220
public_location = branch.get_public_branch()
222
branch = _mod_branch.Branch.open(public_location)
223
if submit_branch is None:
224
submit_branch = branch.get_submit_branch()
225
if submit_branch is None:
226
submit_branch = branch.get_parent()
227
if submit_branch is None:
228
raise errors.BzrCommandError(
229
gettext("No target location specified or remembered"))
231
target = _mod_branch.Branch.open(submit_branch)
232
hoster = _mod_propose.get_hoster(branch)
233
for mp in hoster.iter_proposals(branch, target):
234
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
237
class cmd_github_login(Command):
238
__doc__ = """Log into GitHub.
240
When communicating with GitHub, some commands need to authenticate to
244
takes_args = ['username?']
246
def run(self, username=None):
247
from github import Github, GithubException
248
from breezy.config import AuthenticationConfig
249
authconfig = AuthenticationConfig()
251
username = authconfig.get_user(
252
'https', 'github.com', prompt=u'GitHub username', ask=True)
253
password = authconfig.get_password('https', 'github.com', username)
254
client = Github(username, password)
255
user = client.get_user()
257
authorization = user.create_authorization(
258
scopes=['user', 'repo', 'delete_repo'], note='Breezy',
259
note_url='https://github.com/breezy-team/breezy')
260
except GithubException as e:
261
errs = e.data.get('errors', [])
263
err_code = errs[0].get('code')
264
if err_code == u'already_exists':
265
raise errors.BzrCommandError('token already exists')
266
raise errors.BzrCommandError(e.data['message'])
267
# TODO(jelmer): This should really use something in
268
# AuthenticationConfig
269
from .github import store_github_token
270
store_github_token(scheme='https', host='github.com',
271
token=authorization.token)
274
class cmd_gitlab_login(Command):
275
__doc__ = """Log into a GitLab instance.
277
This command takes a GitLab instance URL (e.g. https://gitlab.com)
278
as well as an optional private token. Private tokens can be created via the
283
Log into GNOME's GitLab (prompts for a token):
285
brz gitlab-login https://gitlab.gnome.org/
287
Log into Debian's salsa, using a token created earlier:
289
brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
292
takes_args = ['url', 'private_token?']
295
Option('name', help='Name for GitLab site in configuration.',
298
"Don't check that the token is valid."),
301
def run(self, url, private_token=None, name=None, no_check=False):
302
from breezy import ui
303
from .gitlabs import store_gitlab_token
306
name = urlutils.parse_url(url)[3].split('.')[-2]
307
except (ValueError, IndexError):
308
raise errors.BzrCommandError(
309
'please specify a site name with --name')
310
if private_token is None:
311
note("Please visit %s to obtain a private token.",
312
urlutils.join(url, "profile/personal_access_tokens"))
313
private_token = ui.ui_factory.get_password(u'Private token')
315
from breezy.transport import get_transport
316
from .gitlabs import GitLab
317
GitLab(get_transport(url), private_token=private_token)
318
store_gitlab_token(name=name, url=url, private_token=private_token)
321
class cmd_my_merge_proposals(Command):
322
__doc__ = """List all merge proposals owned by the logged-in user.
330
RegistryOption.from_kwargs(
332
title='Proposal Status',
333
help='Only include proposals with specified status.',
336
all='All merge proposals',
337
open='Open merge proposals',
338
merged='Merged merge proposals',
339
closed='Closed merge proposals')]
341
def run(self, status='open', verbose=False):
342
for name, hoster_cls in _mod_propose.hosters.items():
343
for instance in hoster_cls.iter_instances():
344
for mp in instance.iter_my_proposals(status=status):
345
self.outf.write('%s\n' % mp.url)
348
'(Merging %s into %s)\n' %
349
(mp.get_source_branch_url(),
350
mp.get_target_branch_url()))
351
description = mp.get_description()
353
self.outf.writelines(
355
for l in description.splitlines()])
356
self.outf.write('\n')
359
class cmd_land_merge_proposal(Command):
360
__doc__ = """Land a merge proposal."""
364
Option('message', help='Commit message to use.', type=str)]
366
def run(self, url, message=None):
367
proposal = _mod_propose.get_proposal_by_url(url)
368
proposal.merge(commit_message=message)