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),
146
ListOption('labels', short_name='l', type=text_type,
147
help='Labels to apply.'),
148
Option('no-allow-lossy',
149
help='Allow fallback to lossy push, if necessary.'),
151
takes_args = ['submit_branch?']
153
aliases = ['propose']
155
def run(self, submit_branch=None, directory='.', hoster=None,
156
reviewers=None, name=None, no_allow_lossy=False, description=None,
157
labels=None, prerequisite=None):
158
tree, branch, relpath = (
159
controldir.ControlDir.open_containing_tree_or_branch(directory))
160
if submit_branch is None:
161
submit_branch = branch.get_submit_branch()
162
if submit_branch is None:
163
submit_branch = branch.get_parent()
164
if submit_branch is None:
165
raise errors.BzrCommandError(
166
gettext("No target location specified or remembered"))
168
target = _mod_branch.Branch.open(submit_branch)
170
hoster = _mod_propose.get_hoster(target)
172
hoster = hoster.probe(target)
174
name = branch_name(branch)
175
remote_branch, public_branch_url = hoster.publish_derived(
176
branch, target, name=name, allow_lossy=not no_allow_lossy)
177
branch.set_push_location(remote_branch.user_url)
178
note(gettext('Published branch to %s') % public_branch_url)
179
if prerequisite is not None:
180
prerequisite_branch = _mod_branch.Branch.open(prerequisite)
182
prerequisite_branch = None
183
proposal_builder = hoster.get_proposer(remote_branch, target)
184
if description is None:
185
body = proposal_builder.get_initial_body()
186
info = proposal_builder.get_infotext()
187
info += "\n\n" + summarize_unmerged(
188
branch, remote_branch, target, prerequisite_branch)
189
description = msgeditor.edit_commit_message(
190
info, start_message=body)
192
proposal = proposal_builder.create_proposal(
193
description=description, reviewers=reviewers,
194
prerequisite_branch=prerequisite_branch, labels=labels)
195
except _mod_propose.MergeProposalExists as e:
196
raise errors.BzrCommandError(gettext(
197
'There is already a branch merge proposal: %s') % e.url)
198
note(gettext('Merge proposal created: %s') % proposal.url)
201
class cmd_find_merge_proposal(Command):
202
__doc__ = """Find a merge proposal.
206
takes_options = ['directory']
207
takes_args = ['submit_branch?']
208
aliases = ['find-proposal']
210
def run(self, directory='.', submit_branch=None):
211
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
213
public_location = branch.get_public_branch()
215
branch = _mod_branch.Branch.open(public_location)
216
if submit_branch is None:
217
submit_branch = branch.get_submit_branch()
218
if submit_branch is None:
219
submit_branch = branch.get_parent()
220
if submit_branch is None:
221
raise errors.BzrCommandError(
222
gettext("No target location specified or remembered"))
224
target = _mod_branch.Branch.open(submit_branch)
225
hoster = _mod_propose.get_hoster(branch)
226
for mp in hoster.iter_proposals(branch, target):
227
self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
230
class cmd_github_login(Command):
231
__doc__ = """Log into GitHub.
233
When communicating with GitHub, some commands need to authenticate to
237
takes_args = ['username?']
239
def run(self, username=None):
240
from github import Github, GithubException
241
from breezy.config import AuthenticationConfig
242
authconfig = AuthenticationConfig()
244
username = authconfig.get_user(
245
'https', 'github.com', prompt=u'GitHub username', ask=True)
246
password = authconfig.get_password('https', 'github.com', username)
247
client = Github(username, password)
248
user = client.get_user()
250
authorization = user.create_authorization(
251
scopes=['user', 'repo', 'delete_repo'], note='Breezy',
252
note_url='https://github.com/breezy-team/breezy')
253
except GithubException as e:
254
errs = e.data.get('errors', [])
256
err_code = errs[0].get('code')
257
if err_code == u'already_exists':
258
raise errors.BzrCommandError('token already exists')
259
raise errors.BzrCommandError(e.data['message'])
260
# TODO(jelmer): This should really use something in
261
# AuthenticationConfig
262
from .github import store_github_token
263
store_github_token(scheme='https', host='github.com',
264
token=authorization.token)
267
class cmd_gitlab_login(Command):
268
__doc__ = """Log into a GitLab instance.
270
This command takes a GitLab instance URL (e.g. https://gitlab.com)
271
as well as an optional private token. Private tokens can be created via the
276
Log into GNOME's GitLab (prompts for a token):
278
brz gitlab-login https://gitlab.gnome.org/
280
Log into Debian's salsa, using a token created earlier:
282
brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
285
takes_args = ['url', 'private_token?']
288
Option('name', help='Name for GitLab site in configuration.',
291
"Don't check that the token is valid."),
294
def run(self, url, private_token=None, name=None, no_check=False):
295
from breezy import ui
296
from .gitlabs import store_gitlab_token
299
name = urlutils.parse_url(url)[3].split('.')[-2]
300
except (ValueError, IndexError):
301
raise errors.BzrCommandError(
302
'please specify a site name with --name')
303
if private_token is None:
304
note("Please visit %s to obtain a private token.",
305
urlutils.join(url, "profile/personal_access_tokens"))
306
private_token = ui.ui_factory.get_password(u'Private token')
308
from gitlab import Gitlab
309
gl = Gitlab(url=url, private_token=private_token)
311
store_gitlab_token(name=name, url=url, private_token=private_token)
314
class cmd_my_merge_proposals(Command):
315
__doc__ = """List all merge proposals owned by the logged-in user.
322
RegistryOption.from_kwargs(
324
title='Proposal Status',
325
help='Only include proposals with specified status.',
328
all='All merge proposals',
329
open='Open merge proposals',
330
merged='Merged merge proposals',
331
closed='Closed merge proposals')]
333
def run(self, status='open'):
334
from .propose import hosters
335
for name, hoster_cls in hosters.items():
336
for instance in hoster_cls.iter_instances():
337
for mp in instance.iter_my_proposals(status=status):
338
self.outf.write('%s\n' % mp.url)