/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-01-13 18:59:26 UTC
  • mto: This revision was merged to the branch mainline in revision 7250.
  • Revision ID: jelmer@jelmer.uk-20190113185926-m11b4z1ygh5c4tfb
Fix test.

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
        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.'),
 
150
        ]
 
151
    takes_args = ['submit_branch?']
 
152
 
 
153
    aliases = ['propose']
 
154
 
 
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"))
 
167
        else:
 
168
            target = _mod_branch.Branch.open(submit_branch)
 
169
        if hoster is None:
 
170
            hoster = _mod_propose.get_hoster(target)
 
171
        else:
 
172
            hoster = hoster.probe(target)
 
173
        if name is None:
 
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)
 
181
        else:
 
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)
 
191
        try:
 
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)
 
199
 
 
200
 
 
201
class cmd_find_merge_proposal(Command):
 
202
    __doc__ = """Find a merge proposal.
 
203
 
 
204
    """
 
205
 
 
206
    takes_options = ['directory']
 
207
    takes_args = ['submit_branch?']
 
208
    aliases = ['find-proposal']
 
209
 
 
210
    def run(self, directory='.', submit_branch=None):
 
211
        tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
 
212
            directory)
 
213
        public_location = branch.get_public_branch()
 
214
        if public_location:
 
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"))
 
223
        else:
 
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)
 
228
 
 
229
 
 
230
class cmd_github_login(Command):
 
231
    __doc__ = """Log into GitHub.
 
232
 
 
233
    When communicating with GitHub, some commands need to authenticate to
 
234
    GitHub.
 
235
    """
 
236
 
 
237
    takes_args = ['username?']
 
238
 
 
239
    def run(self, username=None):
 
240
        from github import Github, GithubException
 
241
        from breezy.config import AuthenticationConfig
 
242
        authconfig = AuthenticationConfig()
 
243
        if username is None:
 
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()
 
249
        try:
 
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', [])
 
255
            if errs:
 
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)
 
265
 
 
266
 
 
267
class cmd_gitlab_login(Command):
 
268
    __doc__ = """Log into a GitLab instance.
 
269
 
 
270
    This command takes a GitLab instance URL (e.g. https://gitlab.com)
 
271
    as well as a private token. Private tokens can be created via the
 
272
    web UI.
 
273
 
 
274
    :Examples:
 
275
 
 
276
      Log into Debian's salsa:
 
277
 
 
278
         brz gitlab-login https://salsa.debian.org if4Theis6Eich7aef0zo
 
279
    """
 
280
 
 
281
    takes_args = ['url', 'private_token']
 
282
 
 
283
    takes_options = [
 
284
        Option('name', help='Name for GitLab site in configuration.',
 
285
               type=str),
 
286
        Option('no-check',
 
287
               "Don't check that the token is valid."),
 
288
        ]
 
289
 
 
290
    def run(self, url, private_token, name=None, no_check=False):
 
291
        from .gitlabs import store_gitlab_token
 
292
        if name is None:
 
293
            try:
 
294
                name = urlutils.parse_url(url)[3].split('.')[-2]
 
295
            except (ValueError, IndexError):
 
296
                raise errors.BzrCommandError(
 
297
                    'please specify a site name with --name')
 
298
        if not no_check:
 
299
            from gitlab import Gitlab
 
300
            gl = Gitlab(url=url, private_token=private_token)
 
301
            gl.auth()
 
302
        store_gitlab_token(name=name, url=url, private_token=private_token)
 
303
 
 
304
 
 
305
class cmd_my_merge_proposals(Command):
 
306
    __doc__ = """List all merge proposals owned by the logged-in user.
 
307
 
 
308
    """
 
309
 
 
310
    hidden = True
 
311
 
 
312
    takes_options = [
 
313
        RegistryOption.from_kwargs(
 
314
            'status',
 
315
            title='Proposal Status',
 
316
            help='Only include proposals with specified status.',
 
317
            value_switches=True,
 
318
            enum_switch=True,
 
319
            all='All merge proposals',
 
320
            open='Open merge proposals',
 
321
            merged='Merged merge proposals',
 
322
            closed='Closed merge proposals')]
 
323
 
 
324
    def run(self, status='open'):
 
325
        from .propose import hosters
 
326
        for name, hoster_cls in hosters.items():
 
327
            for instance in hoster_cls.iter_instances():
 
328
                for mp in instance.iter_my_proposals(status=status):
 
329
                    self.outf.write('%s\n' % mp.url)