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
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=text_type,
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),
148
help='Set commit message for merge, if supported', type=str),
149
ListOption('labels', short_name='l', type=text_type,
150
help='Labels to apply.'),
151
Option('no-allow-lossy',
152
help='Allow fallback to lossy push, if necessary.'),
154
takes_args = ['submit_branch?']
156
aliases = ['propose']
158
def run(self, submit_branch=None, directory='.', hoster=None,
159
reviewers=None, name=None, no_allow_lossy=False, description=None,
160
labels=None, prerequisite=None, commit_message=None):
161
tree, branch, relpath = (
162
controldir.ControlDir.open_containing_tree_or_branch(directory))
163
if submit_branch is None:
164
submit_branch = branch.get_submit_branch()
165
if submit_branch is None:
166
submit_branch = branch.get_parent()
167
if submit_branch is None:
168
raise errors.BzrCommandError(
169
gettext("No target location specified or remembered"))
171
target = _mod_branch.Branch.open(submit_branch)
173
hoster = _mod_propose.get_hoster(target)
175
hoster = hoster.probe(target)
177
name = branch_name(branch)
178
remote_branch, public_branch_url = hoster.publish_derived(
179
branch, target, name=name, allow_lossy=not no_allow_lossy)
180
branch.set_push_location(remote_branch.user_url)
181
note(gettext('Published branch to %s') % public_branch_url)
182
if prerequisite is not None:
183
prerequisite_branch = _mod_branch.Branch.open(prerequisite)
185
prerequisite_branch = None
186
proposal_builder = hoster.get_proposer(remote_branch, target)
187
if description is None:
188
body = proposal_builder.get_initial_body()
189
info = proposal_builder.get_infotext()
190
info += "\n\n" + summarize_unmerged(
191
branch, remote_branch, target, prerequisite_branch)
192
description = msgeditor.edit_commit_message(
193
info, start_message=body)
195
proposal = proposal_builder.create_proposal(
196
description=description, reviewers=reviewers,
197
prerequisite_branch=prerequisite_branch, labels=labels,
198
commit_message=commit_message)
199
except _mod_propose.MergeProposalExists as e:
200
raise errors.BzrCommandError(gettext(
201
'There is already a branch merge proposal: %s') % e.url)
202
note(gettext('Merge proposal created: %s') % proposal.url)
205
class cmd_find_merge_proposal(Command):
206
__doc__ = """Find a merge proposal.
210
takes_options = ['directory']
211
takes_args = ['submit_branch?']
212
aliases = ['find-proposal']
214
def run(self, directory='.', submit_branch=None):
215
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
217
public_location = branch.get_public_branch()
219
branch = _mod_branch.Branch.open(public_location)
220
if submit_branch is None:
221
submit_branch = branch.get_submit_branch()
222
if submit_branch is None:
223
submit_branch = branch.get_parent()
224
if submit_branch is None:
225
raise errors.BzrCommandError(
226
gettext("No target location specified or remembered"))
228
target = _mod_branch.Branch.open(submit_branch)
229
hoster = _mod_propose.get_hoster(branch)
230
for mp in hoster.iter_proposals(branch, target):
231
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
234
class cmd_github_login(Command):
235
__doc__ = """Log into GitHub.
237
When communicating with GitHub, some commands need to authenticate to
241
takes_args = ['username?']
243
def run(self, username=None):
244
from github import Github, GithubException
245
from breezy.config import AuthenticationConfig
246
authconfig = AuthenticationConfig()
248
username = authconfig.get_user(
249
'https', 'github.com', prompt=u'GitHub username', ask=True)
250
password = authconfig.get_password('https', 'github.com', username)
251
client = Github(username, password)
252
user = client.get_user()
254
authorization = user.create_authorization(
255
scopes=['user', 'repo', 'delete_repo'], note='Breezy',
256
note_url='https://github.com/breezy-team/breezy')
257
except GithubException as e:
258
errs = e.data.get('errors', [])
260
err_code = errs[0].get('code')
261
if err_code == u'already_exists':
262
raise errors.BzrCommandError('token already exists')
263
raise errors.BzrCommandError(e.data['message'])
264
# TODO(jelmer): This should really use something in
265
# AuthenticationConfig
266
from .github import store_github_token
267
store_github_token(scheme='https', host='github.com',
268
token=authorization.token)
271
class cmd_gitlab_login(Command):
272
__doc__ = """Log into a GitLab instance.
274
This command takes a GitLab instance URL (e.g. https://gitlab.com)
275
as well as an optional private token. Private tokens can be created via the
280
Log into GNOME's GitLab (prompts for a token):
282
brz gitlab-login https://gitlab.gnome.org/
284
Log into Debian's salsa, using a token created earlier:
286
brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
289
takes_args = ['url', 'private_token?']
292
Option('name', help='Name for GitLab site in configuration.',
295
"Don't check that the token is valid."),
298
def run(self, url, private_token=None, name=None, no_check=False):
299
from breezy import ui
300
from .gitlabs import store_gitlab_token
303
name = urlutils.parse_url(url)[3].split('.')[-2]
304
except (ValueError, IndexError):
305
raise errors.BzrCommandError(
306
'please specify a site name with --name')
307
if private_token is None:
308
note("Please visit %s to obtain a private token.",
309
urlutils.join(url, "profile/personal_access_tokens"))
310
private_token = ui.ui_factory.get_password(u'Private token')
312
from breezy.transport import get_transport
313
from .gitlabs import GitLab
314
GitLab(get_transport(url), private_token=private_token)
315
store_gitlab_token(name=name, url=url, private_token=private_token)
318
class cmd_my_merge_proposals(Command):
319
__doc__ = """List all merge proposals owned by the logged-in user.
327
RegistryOption.from_kwargs(
329
title='Proposal Status',
330
help='Only include proposals with specified status.',
333
all='All merge proposals',
334
open='Open merge proposals',
335
merged='Merged merge proposals',
336
closed='Closed merge proposals')]
338
def run(self, status='open', verbose=False):
339
from .propose import hosters
340
for name, hoster_cls in hosters.items():
341
for instance in hoster_cls.iter_instances():
342
for mp in instance.iter_my_proposals(status=status):
343
self.outf.write('%s\n' % mp.url)
346
'(Merging %s into %s)\n' %
347
(mp.get_source_branch_url(),
348
mp.get_target_branch_url()))
349
self.outf.writelines(
351
for l in mp.get_description().splitlines()])
352
self.outf.write('\n')
355
class cmd_land_merge_proposal(Command):
356
__doc__ = """Land a merge proposal."""
360
Option('message', help='Commit message to use.', type=str)]
362
def run(self, url, message=None):
363
from .propose import get_proposal_by_url
364
proposal = get_proposal_by_url(url)
365
proposal.merge(commit_message=message)