/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-05-06 02:13:25 UTC
  • mfrom: (7490.7.21 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200506021325-awbmmqu1zyorz7sj
Merge 3.1 branch.

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 io import StringIO
 
20
 
 
21
from ... import (
 
22
    branch as _mod_branch,
 
23
    controldir,
 
24
    errors,
 
25
    log as _mod_log,
 
26
    missing as _mod_missing,
 
27
    msgeditor,
 
28
    urlutils,
 
29
    )
 
30
from ...i18n import gettext
 
31
from ...commands import Command
 
32
from ...option import (
 
33
    ListOption,
 
34
    Option,
 
35
    RegistryOption,
 
36
    )
 
37
from ...trace import note
 
38
from ... import (
 
39
    propose as _mod_propose,
 
40
    )
 
41
 
 
42
 
 
43
def branch_name(branch):
 
44
    if branch.name:
 
45
        return branch.name
 
46
    return urlutils.basename(branch.user_url)
 
47
 
 
48
 
 
49
class cmd_publish_derived(Command):
 
50
    __doc__ = """Publish a derived branch.
 
51
 
 
52
    Try to create a public copy of a local branch on a hosting site,
 
53
    derived from the specified base branch.
 
54
 
 
55
    Reasonable defaults are picked for owner name, branch name and project
 
56
    name, but they can also be overridden from the command-line.
 
57
    """
 
58
 
 
59
    takes_options = [
 
60
        'directory',
 
61
        Option('owner', help='Owner of the new remote branch.', type=str),
 
62
        Option('project', help='Project name for the new remote branch.',
 
63
               type=str),
 
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."),
 
68
        ]
 
69
    takes_args = ['submit_branch?']
 
70
 
 
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)
 
82
        if name is None:
 
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,
 
88
            overwrite=overwrite)
 
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)
 
93
 
 
94
 
 
95
def summarize_unmerged(local_branch, remote_branch, target,
 
96
                       prerequisite_branch=None):
 
97
    """Generate a text description of the unmerged revisions in branch.
 
98
 
 
99
    :param branch: The proposed branch
 
100
    :param target: Target branch
 
101
    :param prerequisite_branch: Optional prerequisite branch
 
102
    :return: A string
 
103
    """
 
104
    log_format = _mod_log.log_formatter_registry.get_default(local_branch)
 
105
    to_file = StringIO()
 
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]
 
110
    else:
 
111
        local_extra = _mod_missing.find_unmerged(
 
112
            remote_branch, target, restrict='local')[0]
 
113
 
 
114
    if remote_branch.supports_tags():
 
115
        rev_tag_dict = remote_branch.tags.get_reverse_tag_dict()
 
116
    else:
 
117
        rev_tag_dict = {}
 
118
 
 
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()
 
123
 
 
124
 
 
125
class cmd_propose_merge(Command):
 
126
    __doc__ = """Propose a branch for merging.
 
127
 
 
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.
 
131
    """
 
132
 
 
133
    takes_options = [
 
134
        'directory',
 
135
        RegistryOption(
 
136
            'hoster',
 
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'),
 
145
        Option(
 
146
            'commit-message',
 
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.'),
 
152
        Option('allow-collaboration',
 
153
               help='Allow collaboration from target branch maintainer(s)'),
 
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
            allow_collaboration=False):
 
163
        tree, branch, relpath = (
 
164
            controldir.ControlDir.open_containing_tree_or_branch(directory))
 
165
        if submit_branch is None:
 
166
            submit_branch = branch.get_submit_branch()
 
167
        if submit_branch is None:
 
168
            submit_branch = branch.get_parent()
 
169
        if submit_branch is None:
 
170
            raise errors.BzrCommandError(
 
171
                gettext("No target location specified or remembered"))
 
172
        else:
 
173
            target = _mod_branch.Branch.open(submit_branch)
 
174
        if hoster is None:
 
175
            hoster = _mod_propose.get_hoster(target)
 
176
        else:
 
177
            hoster = hoster.probe(target)
 
178
        if name is None:
 
179
            name = branch_name(branch)
 
180
        remote_branch, public_branch_url = hoster.publish_derived(
 
181
            branch, target, name=name, allow_lossy=not no_allow_lossy)
 
182
        branch.set_push_location(remote_branch.user_url)
 
183
        branch.set_submit_branch(target.user_url)
 
184
        note(gettext('Published branch to %s') % public_branch_url)
 
185
        if prerequisite is not None:
 
186
            prerequisite_branch = _mod_branch.Branch.open(prerequisite)
 
187
        else:
 
188
            prerequisite_branch = None
 
189
        proposal_builder = hoster.get_proposer(remote_branch, target)
 
190
        if description is None:
 
191
            body = proposal_builder.get_initial_body()
 
192
            info = proposal_builder.get_infotext()
 
193
            info += "\n\n" + summarize_unmerged(
 
194
                branch, remote_branch, target, prerequisite_branch)
 
195
            description = msgeditor.edit_commit_message(
 
196
                info, start_message=body)
 
197
        try:
 
198
            proposal = proposal_builder.create_proposal(
 
199
                description=description, reviewers=reviewers,
 
200
                prerequisite_branch=prerequisite_branch, labels=labels,
 
201
                commit_message=commit_message,
 
202
                work_in_progress=wip, allow_collaboration=allow_collaboration)
 
203
        except _mod_propose.MergeProposalExists as e:
 
204
            note(gettext('There is already a branch merge proposal: %s'), e.url)
 
205
        else:
 
206
            note(gettext('Merge proposal created: %s') % proposal.url)
 
207
 
 
208
 
 
209
class cmd_find_merge_proposal(Command):
 
210
    __doc__ = """Find a merge proposal.
 
211
 
 
212
    """
 
213
 
 
214
    takes_options = ['directory']
 
215
    takes_args = ['submit_branch?']
 
216
    aliases = ['find-proposal']
 
217
 
 
218
    def run(self, directory='.', submit_branch=None):
 
219
        tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
 
220
            directory)
 
221
        public_location = branch.get_public_branch()
 
222
        if public_location:
 
223
            branch = _mod_branch.Branch.open(public_location)
 
224
        if submit_branch is None:
 
225
            submit_branch = branch.get_submit_branch()
 
226
        if submit_branch is None:
 
227
            submit_branch = branch.get_parent()
 
228
        if submit_branch is None:
 
229
            raise errors.BzrCommandError(
 
230
                gettext("No target location specified or remembered"))
 
231
        else:
 
232
            target = _mod_branch.Branch.open(submit_branch)
 
233
        hoster = _mod_propose.get_hoster(branch)
 
234
        for mp in hoster.iter_proposals(branch, target):
 
235
            self.outf.write(gettext('Merge proposal: %s\n') % mp.url)
 
236
 
 
237
 
 
238
class cmd_github_login(Command):
 
239
    __doc__ = """Log into GitHub.
 
240
 
 
241
    When communicating with GitHub, some commands need to authenticate to
 
242
    GitHub.
 
243
    """
 
244
 
 
245
    takes_args = ['username?']
 
246
 
 
247
    def run(self, username=None):
 
248
        from github import Github, GithubException
 
249
        from breezy.config import AuthenticationConfig
 
250
        authconfig = AuthenticationConfig()
 
251
        if username is None:
 
252
            username = authconfig.get_user(
 
253
                'https', 'github.com', prompt=u'GitHub username', ask=True)
 
254
        password = authconfig.get_password('https', 'github.com', username)
 
255
        client = Github(username, password)
 
256
        user = client.get_user()
 
257
        try:
 
258
            authorization = user.create_authorization(
 
259
                scopes=['user', 'repo', 'delete_repo'], note='Breezy',
 
260
                note_url='https://github.com/breezy-team/breezy')
 
261
        except GithubException as e:
 
262
            errs = e.data.get('errors', [])
 
263
            if errs:
 
264
                err_code = errs[0].get('code')
 
265
                if err_code == u'already_exists':
 
266
                    raise errors.BzrCommandError('token already exists')
 
267
            raise errors.BzrCommandError(e.data['message'])
 
268
        # TODO(jelmer): This should really use something in
 
269
        # AuthenticationConfig
 
270
        from .github import store_github_token
 
271
        store_github_token(scheme='https', host='github.com',
 
272
                           token=authorization.token)
 
273
 
 
274
 
 
275
class cmd_gitlab_login(Command):
 
276
    __doc__ = """Log into a GitLab instance.
 
277
 
 
278
    This command takes a GitLab instance URL (e.g. https://gitlab.com)
 
279
    as well as an optional private token. Private tokens can be created via the
 
280
    web UI.
 
281
 
 
282
    :Examples:
 
283
 
 
284
      Log into GNOME's GitLab (prompts for a token):
 
285
 
 
286
         brz gitlab-login https://gitlab.gnome.org/
 
287
 
 
288
      Log into Debian's salsa, using a token created earlier:
 
289
 
 
290
         brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
 
291
    """
 
292
 
 
293
    takes_args = ['url', 'private_token?']
 
294
 
 
295
    takes_options = [
 
296
        Option('name', help='Name for GitLab site in configuration.',
 
297
               type=str),
 
298
        Option('no-check',
 
299
               "Don't check that the token is valid."),
 
300
        ]
 
301
 
 
302
    def run(self, url, private_token=None, name=None, no_check=False):
 
303
        from breezy import ui
 
304
        from .gitlabs import store_gitlab_token
 
305
        if name is None:
 
306
            try:
 
307
                name = urlutils.parse_url(url)[3].split('.')[-2]
 
308
            except (ValueError, IndexError):
 
309
                raise errors.BzrCommandError(
 
310
                    'please specify a site name with --name')
 
311
        if private_token is None:
 
312
            note("Please visit %s to obtain a private token.",
 
313
                 urlutils.join(url, "profile/personal_access_tokens"))
 
314
            private_token = ui.ui_factory.get_password(u'Private token')
 
315
        if not no_check:
 
316
            from breezy.transport import get_transport
 
317
            from .gitlabs import GitLab
 
318
            GitLab(get_transport(url), private_token=private_token)
 
319
        store_gitlab_token(name=name, url=url, private_token=private_token)
 
320
 
 
321
 
 
322
class cmd_my_merge_proposals(Command):
 
323
    __doc__ = """List all merge proposals owned by the logged-in user.
 
324
 
 
325
    """
 
326
 
 
327
    hidden = True
 
328
 
 
329
    takes_options = [
 
330
        'verbose',
 
331
        RegistryOption.from_kwargs(
 
332
            'status',
 
333
            title='Proposal Status',
 
334
            help='Only include proposals with specified status.',
 
335
            value_switches=True,
 
336
            enum_switch=True,
 
337
            all='All merge proposals',
 
338
            open='Open merge proposals',
 
339
            merged='Merged merge proposals',
 
340
            closed='Closed merge proposals')]
 
341
 
 
342
    def run(self, status='open', verbose=False):
 
343
        for name, hoster_cls in _mod_propose.hosters.items():
 
344
            for instance in hoster_cls.iter_instances():
 
345
                for mp in instance.iter_my_proposals(status=status):
 
346
                    self.outf.write('%s\n' % mp.url)
 
347
                    if verbose:
 
348
                        self.outf.write(
 
349
                            '(Merging %s into %s)\n' %
 
350
                            (mp.get_source_branch_url(),
 
351
                             mp.get_target_branch_url()))
 
352
                        description = mp.get_description()
 
353
                        if description:
 
354
                            self.outf.writelines(
 
355
                                ['\t%s\n' % l
 
356
                                 for l in description.splitlines()])
 
357
                        self.outf.write('\n')
 
358
 
 
359
 
 
360
class cmd_land_merge_proposal(Command):
 
361
    __doc__ = """Land a merge proposal."""
 
362
 
 
363
    takes_args = ['url']
 
364
    takes_options = [
 
365
        Option('message', help='Commit message to use.', type=str)]
 
366
 
 
367
    def run(self, url, message=None):
 
368
        proposal = _mod_propose.get_proposal_by_url(url)
 
369
        proposal.merge(commit_message=message)