/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: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

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 ...trace import note
 
40
from ... import (
 
41
    propose as _mod_propose,
 
42
    )
 
43
 
 
44
 
 
45
def branch_name(branch):
 
46
    if branch.name:
 
47
        return branch.name
 
48
    return urlutils.basename(branch.user_url)
 
49
 
 
50
 
 
51
class cmd_publish_derived(Command):
 
52
    __doc__ = """Publish a derived branch.
 
53
 
 
54
    Try to create a public copy of a local branch on a hosting site,
 
55
    derived from the specified base branch.
 
56
 
 
57
    Reasonable defaults are picked for owner name, branch name and project
 
58
    name, but they can also be overridden from the command-line.
 
59
    """
 
60
 
 
61
    takes_options = [
 
62
        'directory',
 
63
        Option('owner', help='Owner of the new remote branch.', type=str),
 
64
        Option('project', help='Project name for the new remote branch.',
 
65
               type=str),
 
66
        Option('name', help='Name of the new remote branch.', type=str),
 
67
        Option('no-allow-lossy',
 
68
               help='Allow fallback to lossy push, if necessary.'),
 
69
        Option('overwrite', help="Overwrite existing commits."),
 
70
        ]
 
71
    takes_args = ['submit_branch?']
 
72
 
 
73
    def run(self, submit_branch=None, owner=None, name=None, project=None,
 
74
            no_allow_lossy=False, overwrite=False, directory='.'):
 
75
        local_branch = _mod_branch.Branch.open_containing(directory)[0]
 
76
        self.add_cleanup(local_branch.lock_write().unlock)
 
77
        if submit_branch is None:
 
78
            submit_branch = local_branch.get_submit_branch()
 
79
            note(gettext('Using submit branch %s') % submit_branch)
 
80
        if submit_branch is None:
 
81
            submit_branch = local_branch.get_parent()
 
82
            note(gettext('Using parent branch %s') % submit_branch)
 
83
        submit_branch = _mod_branch.Branch.open(submit_branch)
 
84
        if name is None:
 
85
            name = branch_name(local_branch)
 
86
        hoster = _mod_propose.get_hoster(submit_branch)
 
87
        remote_branch, public_url = hoster.publish_derived(
 
88
            local_branch, submit_branch, name=name, project=project,
 
89
            owner=owner, allow_lossy=not no_allow_lossy,
 
90
            overwrite=overwrite)
 
91
        local_branch.set_push_location(remote_branch.user_url)
 
92
        local_branch.set_public_branch(public_url)
 
93
        local_branch.set_submit_branch(submit_branch.user_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=str,
 
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('wip', help='Mark merge request as work-in-progress'),
 
147
        Option(
 
148
            'commit-message',
 
149
            help='Set commit message for merge, if supported', type=str),
 
150
        ListOption('labels', short_name='l', type=str,
 
151
                   help='Labels to apply.'),
 
152
        Option('no-allow-lossy',
 
153
               help='Allow fallback to lossy push, if necessary.'),
 
154
        ]
 
155
    takes_args = ['submit_branch?']
 
156
 
 
157
    aliases = ['propose']
 
158
 
 
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, wip=False):
 
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"))
 
171
        else:
 
172
            target = _mod_branch.Branch.open(submit_branch)
 
173
        if hoster is None:
 
174
            hoster = _mod_propose.get_hoster(target)
 
175
        else:
 
176
            hoster = hoster.probe(target)
 
177
        if name is None:
 
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)
 
186
        else:
 
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)
 
196
        try:
 
197
            proposal = proposal_builder.create_proposal(
 
198
                description=description, reviewers=reviewers,
 
199
                prerequisite_branch=prerequisite_branch, labels=labels,
 
200
                commit_message=commit_message,
 
201
                work_in_progress=wip)
 
202
        except _mod_propose.MergeProposalExists as e:
 
203
            note(gettext('There is already a branch merge proposal: %s'), e.url)
 
204
        else:
 
205
            note(gettext('Merge proposal created: %s') % proposal.url)
 
206
 
 
207
 
 
208
class cmd_find_merge_proposal(Command):
 
209
    __doc__ = """Find a merge proposal.
 
210
 
 
211
    """
 
212
 
 
213
    takes_options = ['directory']
 
214
    takes_args = ['submit_branch?']
 
215
    aliases = ['find-proposal']
 
216
 
 
217
    def run(self, directory='.', submit_branch=None):
 
218
        tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
 
219
            directory)
 
220
        public_location = branch.get_public_branch()
 
221
        if public_location:
 
222
            branch = _mod_branch.Branch.open(public_location)
 
223
        if submit_branch is None:
 
224
            submit_branch = branch.get_submit_branch()
 
225
        if submit_branch is None:
 
226
            submit_branch = branch.get_parent()
 
227
        if submit_branch is None:
 
228
            raise errors.BzrCommandError(
 
229
                gettext("No target location specified or remembered"))
 
230
        else:
 
231
            target = _mod_branch.Branch.open(submit_branch)
 
232
        hoster = _mod_propose.get_hoster(branch)
 
233
        for mp in hoster.iter_proposals(branch, target):
 
234
            self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
 
235
 
 
236
 
 
237
class cmd_github_login(Command):
 
238
    __doc__ = """Log into GitHub.
 
239
 
 
240
    When communicating with GitHub, some commands need to authenticate to
 
241
    GitHub.
 
242
    """
 
243
 
 
244
    takes_args = ['username?']
 
245
 
 
246
    def run(self, username=None):
 
247
        from github import Github, GithubException
 
248
        from breezy.config import AuthenticationConfig
 
249
        authconfig = AuthenticationConfig()
 
250
        if username is None:
 
251
            username = authconfig.get_user(
 
252
                'https', 'github.com', prompt=u'GitHub username', ask=True)
 
253
        password = authconfig.get_password('https', 'github.com', username)
 
254
        client = Github(username, password)
 
255
        user = client.get_user()
 
256
        try:
 
257
            authorization = user.create_authorization(
 
258
                scopes=['user', 'repo', 'delete_repo'], note='Breezy',
 
259
                note_url='https://github.com/breezy-team/breezy')
 
260
        except GithubException as e:
 
261
            errs = e.data.get('errors', [])
 
262
            if errs:
 
263
                err_code = errs[0].get('code')
 
264
                if err_code == u'already_exists':
 
265
                    raise errors.BzrCommandError('token already exists')
 
266
            raise errors.BzrCommandError(e.data['message'])
 
267
        # TODO(jelmer): This should really use something in
 
268
        # AuthenticationConfig
 
269
        from .github import store_github_token
 
270
        store_github_token(scheme='https', host='github.com',
 
271
                           token=authorization.token)
 
272
 
 
273
 
 
274
class cmd_gitlab_login(Command):
 
275
    __doc__ = """Log into a GitLab instance.
 
276
 
 
277
    This command takes a GitLab instance URL (e.g. https://gitlab.com)
 
278
    as well as an optional private token. Private tokens can be created via the
 
279
    web UI.
 
280
 
 
281
    :Examples:
 
282
 
 
283
      Log into GNOME's GitLab (prompts for a token):
 
284
 
 
285
         brz gitlab-login https://gitlab.gnome.org/
 
286
 
 
287
      Log into Debian's salsa, using a token created earlier:
 
288
 
 
289
         brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
 
290
    """
 
291
 
 
292
    takes_args = ['url', 'private_token?']
 
293
 
 
294
    takes_options = [
 
295
        Option('name', help='Name for GitLab site in configuration.',
 
296
               type=str),
 
297
        Option('no-check',
 
298
               "Don't check that the token is valid."),
 
299
        ]
 
300
 
 
301
    def run(self, url, private_token=None, name=None, no_check=False):
 
302
        from breezy import ui
 
303
        from .gitlabs import store_gitlab_token
 
304
        if name is None:
 
305
            try:
 
306
                name = urlutils.parse_url(url)[3].split('.')[-2]
 
307
            except (ValueError, IndexError):
 
308
                raise errors.BzrCommandError(
 
309
                    'please specify a site name with --name')
 
310
        if private_token is None:
 
311
            note("Please visit %s to obtain a private token.",
 
312
                 urlutils.join(url, "profile/personal_access_tokens"))
 
313
            private_token = ui.ui_factory.get_password(u'Private token')
 
314
        if not no_check:
 
315
            from breezy.transport import get_transport
 
316
            from .gitlabs import GitLab
 
317
            GitLab(get_transport(url), private_token=private_token)
 
318
        store_gitlab_token(name=name, url=url, private_token=private_token)
 
319
 
 
320
 
 
321
class cmd_my_merge_proposals(Command):
 
322
    __doc__ = """List all merge proposals owned by the logged-in user.
 
323
 
 
324
    """
 
325
 
 
326
    hidden = True
 
327
 
 
328
    takes_options = [
 
329
        'verbose',
 
330
        RegistryOption.from_kwargs(
 
331
            'status',
 
332
            title='Proposal Status',
 
333
            help='Only include proposals with specified status.',
 
334
            value_switches=True,
 
335
            enum_switch=True,
 
336
            all='All merge proposals',
 
337
            open='Open merge proposals',
 
338
            merged='Merged merge proposals',
 
339
            closed='Closed merge proposals')]
 
340
 
 
341
    def run(self, status='open', verbose=False):
 
342
        for name, hoster_cls in _mod_propose.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)
 
346
                    if verbose:
 
347
                        self.outf.write(
 
348
                            '(Merging %s into %s)\n' %
 
349
                            (mp.get_source_branch_url(),
 
350
                             mp.get_target_branch_url()))
 
351
                        description = mp.get_description()
 
352
                        if description:
 
353
                            self.outf.writelines(
 
354
                                ['\t%s\n' % l
 
355
                                 for l in description.splitlines()])
 
356
                        self.outf.write('\n')
 
357
 
 
358
 
 
359
class cmd_land_merge_proposal(Command):
 
360
    __doc__ = """Land a merge proposal."""
 
361
 
 
362
    takes_args = ['url']
 
363
    takes_options = [
 
364
        Option('message', help='Commit message to use.', type=str)]
 
365
 
 
366
    def run(self, url, message=None):
 
367
        proposal = _mod_propose.get_proposal_by_url(url)
 
368
        proposal.merge(commit_message=message)