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),
149
help='Set commit message for merge, if supported', type=str),
150
ListOption('labels', short_name='l', type=text_type,
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):
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
except _mod_propose.MergeProposalExists as e:
202
note(gettext('There is already a branch merge proposal: %s'), e.url)
204
note(gettext('Merge proposal created: %s') % proposal.url)
207
class cmd_find_merge_proposal(Command):
208
__doc__ = """Find a merge proposal.
212
takes_options = ['directory']
213
takes_args = ['submit_branch?']
214
aliases = ['find-proposal']
216
def run(self, directory='.', submit_branch=None):
217
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
219
public_location = branch.get_public_branch()
221
branch = _mod_branch.Branch.open(public_location)
222
if submit_branch is None:
223
submit_branch = branch.get_submit_branch()
224
if submit_branch is None:
225
submit_branch = branch.get_parent()
226
if submit_branch is None:
227
raise errors.BzrCommandError(
228
gettext("No target location specified or remembered"))
230
target = _mod_branch.Branch.open(submit_branch)
231
hoster = _mod_propose.get_hoster(branch)
232
for mp in hoster.iter_proposals(branch, target):
233
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
236
class cmd_github_login(Command):
237
__doc__ = """Log into GitHub.
239
When communicating with GitHub, some commands need to authenticate to
243
takes_args = ['username?']
245
def run(self, username=None):
246
from github import Github, GithubException
247
from breezy.config import AuthenticationConfig
248
authconfig = AuthenticationConfig()
250
username = authconfig.get_user(
251
'https', 'github.com', prompt=u'GitHub username', ask=True)
252
password = authconfig.get_password('https', 'github.com', username)
253
client = Github(username, password)
254
user = client.get_user()
256
authorization = user.create_authorization(
257
scopes=['user', 'repo', 'delete_repo'], note='Breezy',
258
note_url='https://github.com/breezy-team/breezy')
259
except GithubException as e:
260
errs = e.data.get('errors', [])
262
err_code = errs[0].get('code')
263
if err_code == u'already_exists':
264
raise errors.BzrCommandError('token already exists')
265
raise errors.BzrCommandError(e.data['message'])
266
# TODO(jelmer): This should really use something in
267
# AuthenticationConfig
268
from .github import store_github_token
269
store_github_token(scheme='https', host='github.com',
270
token=authorization.token)
273
class cmd_gitlab_login(Command):
274
__doc__ = """Log into a GitLab instance.
276
This command takes a GitLab instance URL (e.g. https://gitlab.com)
277
as well as an optional private token. Private tokens can be created via the
282
Log into GNOME's GitLab (prompts for a token):
284
brz gitlab-login https://gitlab.gnome.org/
286
Log into Debian's salsa, using a token created earlier:
288
brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
291
takes_args = ['url', 'private_token?']
294
Option('name', help='Name for GitLab site in configuration.',
297
"Don't check that the token is valid."),
300
def run(self, url, private_token=None, name=None, no_check=False):
301
from breezy import ui
302
from .gitlabs import store_gitlab_token
305
name = urlutils.parse_url(url)[3].split('.')[-2]
306
except (ValueError, IndexError):
307
raise errors.BzrCommandError(
308
'please specify a site name with --name')
309
if private_token is None:
310
note("Please visit %s to obtain a private token.",
311
urlutils.join(url, "profile/personal_access_tokens"))
312
private_token = ui.ui_factory.get_password(u'Private token')
314
from breezy.transport import get_transport
315
from .gitlabs import GitLab
316
GitLab(get_transport(url), private_token=private_token)
317
store_gitlab_token(name=name, url=url, private_token=private_token)
320
class cmd_my_merge_proposals(Command):
321
__doc__ = """List all merge proposals owned by the logged-in user.
329
RegistryOption.from_kwargs(
331
title='Proposal Status',
332
help='Only include proposals with specified status.',
335
all='All merge proposals',
336
open='Open merge proposals',
337
merged='Merged merge proposals',
338
closed='Closed merge proposals')]
340
def run(self, status='open', verbose=False):
341
from .propose import hosters
342
for name, hoster_cls in 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
from .propose import get_proposal_by_url
368
proposal = get_proposal_by_url(url)
369
proposal.merge(commit_message=message)