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.'),
153
takes_args = ['submit_branch?']
155
aliases = ['propose']
157
def run(self, submit_branch=None, directory='.', hoster=None,
158
reviewers=None, name=None, no_allow_lossy=False, description=None,
159
labels=None, prerequisite=None, commit_message=None, wip=False):
160
tree, branch, relpath = (
161
controldir.ControlDir.open_containing_tree_or_branch(directory))
162
if submit_branch is None:
163
submit_branch = branch.get_submit_branch()
164
if submit_branch is None:
165
submit_branch = branch.get_parent()
166
if submit_branch is None:
167
raise errors.BzrCommandError(
168
gettext("No target location specified or remembered"))
170
target = _mod_branch.Branch.open(submit_branch)
172
hoster = _mod_propose.get_hoster(target)
174
hoster = hoster.probe(target)
176
name = branch_name(branch)
177
remote_branch, public_branch_url = hoster.publish_derived(
178
branch, target, name=name, allow_lossy=not no_allow_lossy)
179
branch.set_push_location(remote_branch.user_url)
180
branch.set_submit_branch(target.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
work_in_progress=wip)
200
except _mod_propose.MergeProposalExists as e:
201
note(gettext('There is already a branch merge proposal: %s'), e.url)
203
note(gettext('Merge proposal created: %s') % proposal.url)
206
class cmd_find_merge_proposal(Command):
207
__doc__ = """Find a merge proposal.
211
takes_options = ['directory']
212
takes_args = ['submit_branch?']
213
aliases = ['find-proposal']
215
def run(self, directory='.', submit_branch=None):
216
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
218
public_location = branch.get_public_branch()
220
branch = _mod_branch.Branch.open(public_location)
221
if submit_branch is None:
222
submit_branch = branch.get_submit_branch()
223
if submit_branch is None:
224
submit_branch = branch.get_parent()
225
if submit_branch is None:
226
raise errors.BzrCommandError(
227
gettext("No target location specified or remembered"))
229
target = _mod_branch.Branch.open(submit_branch)
230
hoster = _mod_propose.get_hoster(branch)
231
for mp in hoster.iter_proposals(branch, target):
232
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
235
class cmd_github_login(Command):
236
__doc__ = """Log into GitHub.
238
When communicating with GitHub, some commands need to authenticate to
242
takes_args = ['username?']
244
def run(self, username=None):
245
from github import Github, GithubException
246
from breezy.config import AuthenticationConfig
247
authconfig = AuthenticationConfig()
249
username = authconfig.get_user(
250
'https', 'github.com', prompt=u'GitHub username', ask=True)
251
password = authconfig.get_password('https', 'github.com', username)
252
client = Github(username, password)
253
user = client.get_user()
255
authorization = user.create_authorization(
256
scopes=['user', 'repo', 'delete_repo'], note='Breezy',
257
note_url='https://github.com/breezy-team/breezy')
258
except GithubException as e:
259
errs = e.data.get('errors', [])
261
err_code = errs[0].get('code')
262
if err_code == u'already_exists':
263
raise errors.BzrCommandError('token already exists')
264
raise errors.BzrCommandError(e.data['message'])
265
# TODO(jelmer): This should really use something in
266
# AuthenticationConfig
267
from .github import store_github_token
268
store_github_token(scheme='https', host='github.com',
269
token=authorization.token)
272
class cmd_gitlab_login(Command):
273
__doc__ = """Log into a GitLab instance.
275
This command takes a GitLab instance URL (e.g. https://gitlab.com)
276
as well as an optional private token. Private tokens can be created via the
281
Log into GNOME's GitLab (prompts for a token):
283
brz gitlab-login https://gitlab.gnome.org/
285
Log into Debian's salsa, using a token created earlier:
287
brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
290
takes_args = ['url', 'private_token?']
293
Option('name', help='Name for GitLab site in configuration.',
296
"Don't check that the token is valid."),
299
def run(self, url, private_token=None, name=None, no_check=False):
300
from breezy import ui
301
from .gitlabs import store_gitlab_token
304
name = urlutils.parse_url(url)[3].split('.')[-2]
305
except (ValueError, IndexError):
306
raise errors.BzrCommandError(
307
'please specify a site name with --name')
308
if private_token is None:
309
note("Please visit %s to obtain a private token.",
310
urlutils.join(url, "profile/personal_access_tokens"))
311
private_token = ui.ui_factory.get_password(u'Private token')
313
from breezy.transport import get_transport
314
from .gitlabs import GitLab
315
GitLab(get_transport(url), private_token=private_token)
316
store_gitlab_token(name=name, url=url, private_token=private_token)
319
class cmd_my_merge_proposals(Command):
320
__doc__ = """List all merge proposals owned by the logged-in user.
328
RegistryOption.from_kwargs(
330
title='Proposal Status',
331
help='Only include proposals with specified status.',
334
all='All merge proposals',
335
open='Open merge proposals',
336
merged='Merged merge proposals',
337
closed='Closed merge proposals')]
339
def run(self, status='open', verbose=False):
340
for name, hoster_cls in _mod_propose.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
description = mp.get_description()
351
self.outf.writelines(
353
for l in description.splitlines()])
354
self.outf.write('\n')
357
class cmd_land_merge_proposal(Command):
358
__doc__ = """Land a merge proposal."""
362
Option('message', help='Commit message to use.', type=str)]
364
def run(self, url, message=None):
365
proposal = _mod_propose.get_proposal_by_url(url)
366
proposal.merge(commit_message=message)