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
class cmd_publish_derived(Command):
53
__doc__ = """Publish a derived branch.
55
Try to create a public copy of a local branch on a hosting site,
56
derived from the specified base branch.
58
Reasonable defaults are picked for owner name, branch name and project
59
name, but they can also be overridden from the command-line.
64
Option('owner', help='Owner of the new remote branch.', type=str),
65
Option('project', help='Project name for the new remote branch.',
67
Option('name', help='Name of the new remote branch.', type=str),
68
Option('no-allow-lossy',
69
help='Allow fallback to lossy push, if necessary.'),
70
Option('overwrite', help="Overwrite existing commits."),
72
takes_args = ['submit_branch?']
74
def run(self, submit_branch=None, owner=None, name=None, project=None,
75
no_allow_lossy=False, overwrite=False, directory='.'):
76
local_branch = _mod_branch.Branch.open_containing(directory)[0]
77
self.add_cleanup(local_branch.lock_write().unlock)
78
if submit_branch is None:
79
submit_branch = local_branch.get_submit_branch()
80
note(gettext('Using submit branch %s') % submit_branch)
81
if submit_branch is None:
82
submit_branch = local_branch.get_parent()
83
note(gettext('Using parent branch %s') % submit_branch)
84
submit_branch = _mod_branch.Branch.open(submit_branch)
86
name = branch_name(local_branch)
87
hoster = _mod_propose.get_hoster(submit_branch)
88
remote_branch, public_url = hoster.publish_derived(
89
local_branch, submit_branch, name=name, project=project,
90
owner=owner, allow_lossy=not no_allow_lossy,
92
local_branch.set_push_location(remote_branch.user_url)
93
local_branch.set_public_branch(public_url)
94
local_branch.set_submit_branch(submit_branch.user_url)
95
note(gettext("Pushed to %s") % public_url)
98
def summarize_unmerged(local_branch, remote_branch, target,
99
prerequisite_branch=None):
100
"""Generate a text description of the unmerged revisions in branch.
102
:param branch: The proposed branch
103
:param target: Target branch
104
:param prerequisite_branch: Optional prerequisite branch
107
log_format = _mod_log.log_formatter_registry.get_default(local_branch)
109
lf = log_format(to_file=to_file, show_ids=False, show_timezone='original')
110
if prerequisite_branch:
111
local_extra = _mod_missing.find_unmerged(
112
remote_branch, prerequisite_branch, restrict='local')[0]
114
local_extra = _mod_missing.find_unmerged(
115
remote_branch, target, restrict='local')[0]
117
if remote_branch.supports_tags():
118
rev_tag_dict = remote_branch.tags.get_reverse_tag_dict()
122
for revision in _mod_missing.iter_log_revisions(
123
local_extra, local_branch.repository, False, rev_tag_dict):
124
lf.log_revision(revision)
125
return to_file.getvalue()
128
class cmd_propose_merge(Command):
129
__doc__ = """Propose a branch for merging.
131
This command creates a merge proposal for the local
132
branch to the target branch. The format of the merge
133
proposal depends on the submit branch.
140
help='Use the hoster.',
141
lazy_registry=('breezy.plugins.propose.propose', 'hosters')),
142
ListOption('reviewers', short_name='R', type=text_type,
143
help='Requested reviewers.'),
144
Option('name', help='Name of the new remote branch.', type=str),
145
Option('description', help='Description of the change.', type=str),
146
Option('prerequisite', help='Prerequisite branch.', type=str),
147
Option('wip', help='Mark merge request as work-in-progress'),
150
help='Set commit message for merge, if supported', type=str),
151
ListOption('labels', short_name='l', type=text_type,
152
help='Labels to apply.'),
153
Option('no-allow-lossy',
154
help='Allow fallback to lossy push, if necessary.'),
155
Option('allow-collaboration',
156
help='Allow collaboration from target branch maintainer(s)'),
158
takes_args = ['submit_branch?']
160
aliases = ['propose']
162
def run(self, submit_branch=None, directory='.', hoster=None,
163
reviewers=None, name=None, no_allow_lossy=False, description=None,
164
labels=None, prerequisite=None, commit_message=None, wip=False,
165
allow_collaboration=False):
166
tree, branch, relpath = (
167
controldir.ControlDir.open_containing_tree_or_branch(directory))
168
if submit_branch is None:
169
submit_branch = branch.get_submit_branch()
170
if submit_branch is None:
171
submit_branch = branch.get_parent()
172
if submit_branch is None:
173
raise errors.BzrCommandError(
174
gettext("No target location specified or remembered"))
176
target = _mod_branch.Branch.open(submit_branch)
178
hoster = _mod_propose.get_hoster(target)
180
hoster = hoster.probe(target)
182
name = branch_name(branch)
183
remote_branch, public_branch_url = hoster.publish_derived(
184
branch, target, name=name, allow_lossy=not no_allow_lossy)
185
branch.set_push_location(remote_branch.user_url)
186
branch.set_submit_branch(target.user_url)
187
note(gettext('Published branch to %s') % public_branch_url)
188
if prerequisite is not None:
189
prerequisite_branch = _mod_branch.Branch.open(prerequisite)
191
prerequisite_branch = None
192
proposal_builder = hoster.get_proposer(remote_branch, target)
193
if description is None:
194
body = proposal_builder.get_initial_body()
195
info = proposal_builder.get_infotext()
196
info += "\n\n" + summarize_unmerged(
197
branch, remote_branch, target, prerequisite_branch)
198
description = msgeditor.edit_commit_message(
199
info, start_message=body)
201
proposal = proposal_builder.create_proposal(
202
description=description, reviewers=reviewers,
203
prerequisite_branch=prerequisite_branch, labels=labels,
204
commit_message=commit_message,
205
work_in_progress=wip, allow_collaboration=allow_collaboration)
206
except _mod_propose.MergeProposalExists as e:
207
note(gettext('There is already a branch merge proposal: %s'), e.url)
209
note(gettext('Merge proposal created: %s') % proposal.url)
212
class cmd_find_merge_proposal(Command):
213
__doc__ = """Find a merge proposal.
217
takes_options = ['directory']
218
takes_args = ['submit_branch?']
219
aliases = ['find-proposal']
221
def run(self, directory='.', submit_branch=None):
222
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
224
public_location = branch.get_public_branch()
226
branch = _mod_branch.Branch.open(public_location)
227
if submit_branch is None:
228
submit_branch = branch.get_submit_branch()
229
if submit_branch is None:
230
submit_branch = branch.get_parent()
231
if submit_branch is None:
232
raise errors.BzrCommandError(
233
gettext("No target location specified or remembered"))
235
target = _mod_branch.Branch.open(submit_branch)
236
hoster = _mod_propose.get_hoster(branch)
237
for mp in hoster.iter_proposals(branch, target):
238
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
241
class cmd_github_login(Command):
242
__doc__ = """Log into GitHub.
244
When communicating with GitHub, some commands need to authenticate to
248
takes_args = ['username?']
250
def run(self, username=None):
251
from github import Github, GithubException
252
from breezy.config import AuthenticationConfig
253
authconfig = AuthenticationConfig()
255
username = authconfig.get_user(
256
'https', 'github.com', prompt=u'GitHub username', ask=True)
257
password = authconfig.get_password('https', 'github.com', username)
258
client = Github(username, password)
259
user = client.get_user()
261
authorization = user.create_authorization(
262
scopes=['user', 'repo', 'delete_repo'], note='Breezy',
263
note_url='https://github.com/breezy-team/breezy')
264
except GithubException as e:
265
errs = e.data.get('errors', [])
267
err_code = errs[0].get('code')
268
if err_code == u'already_exists':
269
raise errors.BzrCommandError('token already exists')
270
raise errors.BzrCommandError(e.data['message'])
271
# TODO(jelmer): This should really use something in
272
# AuthenticationConfig
273
from .github import store_github_token
274
store_github_token(scheme='https', host='github.com',
275
token=authorization.token)
278
class cmd_gitlab_login(Command):
279
__doc__ = """Log into a GitLab instance.
281
This command takes a GitLab instance URL (e.g. https://gitlab.com)
282
as well as an optional private token. Private tokens can be created via the
287
Log into GNOME's GitLab (prompts for a token):
289
brz gitlab-login https://gitlab.gnome.org/
291
Log into Debian's salsa, using a token created earlier:
293
brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
296
takes_args = ['url', 'private_token?']
299
Option('name', help='Name for GitLab site in configuration.',
302
"Don't check that the token is valid."),
305
def run(self, url, private_token=None, name=None, no_check=False):
306
from breezy import ui
307
from .gitlabs import store_gitlab_token
310
name = urlutils.parse_url(url)[3].split('.')[-2]
311
except (ValueError, IndexError):
312
raise errors.BzrCommandError(
313
'please specify a site name with --name')
314
if private_token is None:
315
note("Please visit %s to obtain a private token.",
316
urlutils.join(url, "profile/personal_access_tokens"))
317
private_token = ui.ui_factory.get_password(u'Private token')
319
from breezy.transport import get_transport
320
from .gitlabs import GitLab
321
GitLab(get_transport(url), private_token=private_token)
322
store_gitlab_token(name=name, url=url, private_token=private_token)
325
class cmd_my_merge_proposals(Command):
326
__doc__ = """List all merge proposals owned by the logged-in user.
334
RegistryOption.from_kwargs(
336
title='Proposal Status',
337
help='Only include proposals with specified status.',
340
all='All merge proposals',
341
open='Open merge proposals',
342
merged='Merged merge proposals',
343
closed='Closed merge proposals')]
345
def run(self, status='open', verbose=False):
346
for name, hoster_cls in _mod_propose.hosters.items():
347
for instance in hoster_cls.iter_instances():
348
for mp in instance.iter_my_proposals(status=status):
349
self.outf.write('%s\n' % mp.url)
352
'(Merging %s into %s)\n' %
353
(mp.get_source_branch_url(),
354
mp.get_target_branch_url()))
355
description = mp.get_description()
357
self.outf.writelines(
359
for l in description.splitlines()])
360
self.outf.write('\n')
363
class cmd_land_merge_proposal(Command):
364
__doc__ = """Land a merge proposal."""
368
Option('message', help='Commit message to use.', type=str)]
370
def run(self, url, message=None):
371
proposal = _mod_propose.get_proposal_by_url(url)
372
proposal.merge(commit_message=message)