/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/plugins/propose/cmds.py

  • Committer: Jelmer Vernooij
  • Date: 2019-06-15 19:09:08 UTC
  • mto: This revision was merged to the branch mainline in revision 7339.
  • Revision ID: jelmer@jelmer.uk-20190615190908-gyjn0ye90vu2lhim
Print sensible error message when an invalid argument is specified for an option.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2018 Jelmer Vernooij <jelmer@jelmer.uk>
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
"""Propose command implementations."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
from io import StringIO
 
22
 
 
23
from ... import (
 
24
    branch as _mod_branch,
 
25
    controldir,
 
26
    errors,
 
27
    log as _mod_log,
 
28
    missing as _mod_missing,
 
29
    msgeditor,
 
30
    urlutils,
 
31
    )
 
32
from ...i18n import gettext
 
33
from ...commands import Command
 
34
from ...option import (
 
35
    ListOption,
 
36
    Option,
 
37
    RegistryOption,
 
38
    )
 
39
from ...sixish import text_type
 
40
from ...trace import note
 
41
from . import (
 
42
    propose as _mod_propose,
 
43
    )
 
44
 
 
45
 
 
46
def branch_name(branch):
 
47
    if branch.name:
 
48
        return branch.name
 
49
    return urlutils.basename(branch.user_url)
 
50
 
 
51
 
 
52
class cmd_publish_derived(Command):
 
53
    __doc__ = """Publish a derived branch.
 
54
 
 
55
    Try to create a public copy of a local branch on a hosting site,
 
56
    derived from the specified base branch.
 
57
 
 
58
    Reasonable defaults are picked for owner name, branch name and project
 
59
    name, but they can also be overridden from the command-line.
 
60
    """
 
61
 
 
62
    takes_options = [
 
63
        'directory',
 
64
        Option('owner', help='Owner of the new remote branch.', type=str),
 
65
        Option('project', help='Project name for the new remote branch.',
 
66
               type=str),
 
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."),
 
71
        ]
 
72
    takes_args = ['submit_branch?']
 
73
 
 
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)
 
85
        if name is None:
 
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,
 
91
            overwrite=overwrite)
 
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)
 
95
 
 
96
 
 
97
def summarize_unmerged(local_branch, remote_branch, target,
 
98
                       prerequisite_branch=None):
 
99
    """Generate a text description of the unmerged revisions in branch.
 
100
 
 
101
    :param branch: The proposed branch
 
102
    :param target: Target branch
 
103
    :param prerequisite_branch: Optional prerequisite branch
 
104
    :return: A string
 
105
    """
 
106
    log_format = _mod_log.log_formatter_registry.get_default(local_branch)
 
107
    to_file = StringIO()
 
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]
 
112
    else:
 
113
        local_extra = _mod_missing.find_unmerged(
 
114
            remote_branch, target, restrict='local')[0]
 
115
 
 
116
    if remote_branch.supports_tags():
 
117
        rev_tag_dict = remote_branch.tags.get_reverse_tag_dict()
 
118
    else:
 
119
        rev_tag_dict = {}
 
120
 
 
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()
 
125
 
 
126
 
 
127
class cmd_propose_merge(Command):
 
128
    __doc__ = """Propose a branch for merging.
 
129
 
 
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.
 
133
    """
 
134
 
 
135
    takes_options = [
 
136
        'directory',
 
137
        RegistryOption(
 
138
            'hoster',
 
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
        Option(
 
147
            'commit-message',
 
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.'),
 
153
        ]
 
154
    takes_args = ['submit_branch?']
 
155
 
 
156
    aliases = ['propose']
 
157
 
 
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"))
 
170
        else:
 
171
            target = _mod_branch.Branch.open(submit_branch)
 
172
        if hoster is None:
 
173
            hoster = _mod_propose.get_hoster(target)
 
174
        else:
 
175
            hoster = hoster.probe(target)
 
176
        if name is None:
 
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)
 
184
        else:
 
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)
 
194
        try:
 
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)
 
203
 
 
204
 
 
205
class cmd_find_merge_proposal(Command):
 
206
    __doc__ = """Find a merge proposal.
 
207
 
 
208
    """
 
209
 
 
210
    takes_options = ['directory']
 
211
    takes_args = ['submit_branch?']
 
212
    aliases = ['find-proposal']
 
213
 
 
214
    def run(self, directory='.', submit_branch=None):
 
215
        tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
 
216
            directory)
 
217
        public_location = branch.get_public_branch()
 
218
        if public_location:
 
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"))
 
227
        else:
 
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)
 
232
 
 
233
 
 
234
class cmd_github_login(Command):
 
235
    __doc__ = """Log into GitHub.
 
236
 
 
237
    When communicating with GitHub, some commands need to authenticate to
 
238
    GitHub.
 
239
    """
 
240
 
 
241
    takes_args = ['username?']
 
242
 
 
243
    def run(self, username=None):
 
244
        from github import Github, GithubException
 
245
        from breezy.config import AuthenticationConfig
 
246
        authconfig = AuthenticationConfig()
 
247
        if username is None:
 
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()
 
253
        try:
 
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', [])
 
259
            if errs:
 
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)
 
269
 
 
270
 
 
271
class cmd_gitlab_login(Command):
 
272
    __doc__ = """Log into a GitLab instance.
 
273
 
 
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
 
276
    web UI.
 
277
 
 
278
    :Examples:
 
279
 
 
280
      Log into GNOME's GitLab (prompts for a token):
 
281
 
 
282
         brz gitlab-login https://gitlab.gnome.org/
 
283
 
 
284
      Log into Debian's salsa, using a token created earlier:
 
285
 
 
286
         brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
 
287
    """
 
288
 
 
289
    takes_args = ['url', 'private_token?']
 
290
 
 
291
    takes_options = [
 
292
        Option('name', help='Name for GitLab site in configuration.',
 
293
               type=str),
 
294
        Option('no-check',
 
295
               "Don't check that the token is valid."),
 
296
        ]
 
297
 
 
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
 
301
        if name is None:
 
302
            try:
 
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')
 
311
        if not no_check:
 
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)
 
316
 
 
317
 
 
318
class cmd_my_merge_proposals(Command):
 
319
    __doc__ = """List all merge proposals owned by the logged-in user.
 
320
 
 
321
    """
 
322
 
 
323
    hidden = True
 
324
 
 
325
    takes_options = [
 
326
        'verbose',
 
327
        RegistryOption.from_kwargs(
 
328
            'status',
 
329
            title='Proposal Status',
 
330
            help='Only include proposals with specified status.',
 
331
            value_switches=True,
 
332
            enum_switch=True,
 
333
            all='All merge proposals',
 
334
            open='Open merge proposals',
 
335
            merged='Merged merge proposals',
 
336
            closed='Closed merge proposals')]
 
337
 
 
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)
 
344
                    if verbose:
 
345
                        self.outf.write(
 
346
                            '(Merging %s into %s)\n' %
 
347
                            (mp.get_source_branch_url(),
 
348
                             mp.get_target_branch_url()))
 
349
                        self.outf.writelines(
 
350
                            ['\t%s\n' % l
 
351
                             for l in mp.get_description().splitlines()])
 
352
                        self.outf.write('\n')
 
353
 
 
354
 
 
355
class cmd_land_merge_proposal(Command):
 
356
    __doc__ = """Land a merge proposal."""
 
357
 
 
358
    takes_args = ['url']
 
359
    takes_options = [
 
360
        Option('message', help='Commit message to use.', type=str)]
 
361
 
 
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)