/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
1
# Copyright (C) 2018 Breezy Developers
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
0.434.1 by Jelmer Vernooij
Use absolute_import.
17
"""Support for GitHub."""
18
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
19
from __future__ import absolute_import
20
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
21
import os
22
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
23
from .propose import (
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
24
    Hoster,
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
25
    HosterLoginRequired,
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
26
    MergeProposal,
0.432.2 by Jelmer Vernooij
Publish command sort of works.
27
    MergeProposalBuilder,
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
28
    MergeProposalExists,
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
29
    PrerequisiteBranchUnsupported,
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
30
    UnsupportedHoster,
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
31
    )
32
33
from ... import (
0.431.33 by Jelmer Vernooij
Fix URLs from gitlab.
34
    branch as _mod_branch,
0.432.3 by Jelmer Vernooij
Publish command works for github.
35
    controldir,
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
36
    errors,
37
    hooks,
38
    urlutils,
0.432.3 by Jelmer Vernooij
Publish command works for github.
39
    version_string as breezy_version,
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
40
    )
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
41
from ...config import AuthenticationConfig, GlobalStack, config_dir
0.431.32 by Jelmer Vernooij
Properly resolve git+ssh URLs.
42
from ...git.urls import git_url_to_bzr_url
0.432.3 by Jelmer Vernooij
Publish command works for github.
43
from ...i18n import gettext
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
44
from ...sixish import PY3
0.432.3 by Jelmer Vernooij
Publish command works for github.
45
from ...trace import note
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
46
from ...lazy_import import lazy_import
47
lazy_import(globals(), """
48
from github import Github
49
""")
50
51
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
52
def store_github_token(scheme, host, token):
53
    with open(os.path.join(config_dir(), 'github.conf'), 'w') as f:
54
        f.write(token)
55
56
57
def retrieve_github_token(scheme, host):
58
    path = os.path.join(config_dir(), 'github.conf')
59
    if not os.path.exists(path):
60
        return None
0.435.1 by Jelmer Vernooij
Fix reading github credentials.
61
    with open(path, 'r') as f:
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
62
        return f.read().strip()
63
64
0.431.44 by Jelmer Vernooij
Support get/set description.
65
def determine_title(description):
66
    return description.splitlines()[0]
67
68
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
69
class NotGitHubUrl(errors.BzrError):
70
71
    _fmt = "Not a GitHub URL: %(url)s"
72
73
    def __init__(self, url):
74
        errors.BzrError.__init__(self)
75
        self.url = url
76
77
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
78
class GitHubLoginRequired(HosterLoginRequired):
79
80
    _fmt = "Action requires GitHub login."
81
82
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
83
def connect_github():
7211.13.7 by Jelmer Vernooij
Fix formatting.
84
    """Connect to GitHub.
85
    """
86
    user_agent = "Breezy/%s" % breezy_version
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
87
88
    auth = AuthenticationConfig()
89
90
    credentials = auth.get_credentials('https', 'github.com')
91
    if credentials is not None:
0.432.3 by Jelmer Vernooij
Publish command works for github.
92
        return Github(credentials['user'], credentials['password'],
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
93
                      user_agent=user_agent)
94
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
95
    # TODO(jelmer): token = auth.get_token('https', 'github.com')
96
    token = retrieve_github_token('https', 'github.com')
97
    if token is not None:
0.431.61 by Jelmer Vernooij
Fix token login.
98
        return Github(token, user_agent=user_agent)
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
99
    else:
100
        note('Accessing GitHub anonymously. To log in, run \'brz gh-login\'.')
101
        return Github(user_agent=user_agent)
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
102
103
0.431.44 by Jelmer Vernooij
Support get/set description.
104
class GitHubMergeProposal(MergeProposal):
105
106
    def __init__(self, pr):
107
        self._pr = pr
108
109
    @property
110
    def url(self):
111
        return self._pr.html_url
112
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
113
    def _branch_from_part(self, part):
114
        return github_url_to_bzr_url(part.repo.html_url, part.ref)
115
116
    def get_source_branch_url(self):
117
        return self._branch_from_part(self._pr.head)
118
119
    def get_target_branch_url(self):
120
        return self._branch_from_part(self._pr.base)
121
0.431.44 by Jelmer Vernooij
Support get/set description.
122
    def get_description(self):
123
        return self._pr.body
124
125
    def set_description(self, description):
126
        self._pr.edit(body=description, title=determine_title(description))
127
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
128
    def is_merged(self):
129
        return self._pr.merged
130
7260.2.1 by Jelmer Vernooij
Implement .close on merge proposals.
131
    def close(self):
132
        self._pr.edit(state='closed')
133
0.431.44 by Jelmer Vernooij
Support get/set description.
134
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
135
def parse_github_url(branch):
136
    url = urlutils.split_segment_parameters(branch.user_url)[0]
137
    (scheme, user, password, host, port, path) = urlutils.parse_url(
138
        url)
139
    if host != 'github.com':
140
        raise NotGitHubUrl(url)
141
    (owner, repo_name) = path.strip('/').split('/')
0.432.12 by Jelmer Vernooij
Fix .git ends.
142
    if repo_name.endswith('.git'):
143
        repo_name = repo_name[:-4]
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
144
    return owner, repo_name, branch.name
145
146
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
147
def github_url_to_bzr_url(url, branch_name):
148
    if not PY3:
149
        branch_name = branch_name.encode('utf-8')
150
    return urlutils.join_segment_parameters(
7211.13.7 by Jelmer Vernooij
Fix formatting.
151
        git_url_to_bzr_url(url), {"branch": branch_name})
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
152
153
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
154
def convert_github_error(fn):
155
    def convert(self, *args, **kwargs):
156
        import github
157
        try:
158
            return fn(self, *args, **kwargs)
159
        except github.GithubException as e:
160
            if e.args[0] == 401:
161
                raise GitHubLoginRequired(self)
162
            raise
163
    return convert
164
165
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
166
class GitHub(Hoster):
167
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
168
    name = 'github'
169
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
170
    supports_merge_proposal_labels = True
171
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
172
    def __repr__(self):
173
        return "GitHub()"
174
7260.1.1 by Jelmer Vernooij
Add .base_url property to Hoster.
175
    @property
176
    def base_url(self):
177
        # TODO(jelmer): Can we get the default URL from the Python API package
178
        # somehow?
179
        return "https://github.com"
180
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
181
    def __init__(self):
182
        self.gh = connect_github()
183
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
184
    @convert_github_error
0.431.20 by Jelmer Vernooij
publish -> publish_derived.
185
    def publish_derived(self, local_branch, base_branch, name, project=None,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
186
                        owner=None, revision_id=None, overwrite=False,
187
                        allow_lossy=True):
0.432.12 by Jelmer Vernooij
Fix .git ends.
188
        import github
0.432.3 by Jelmer Vernooij
Publish command works for github.
189
        base_owner, base_project, base_branch_name = parse_github_url(base_branch)
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
190
        base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
0.432.3 by Jelmer Vernooij
Publish command works for github.
191
        if owner is None:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
192
            owner = self.gh.get_user().login
0.432.3 by Jelmer Vernooij
Publish command works for github.
193
        if project is None:
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
194
            project = base_repo.name
0.432.3 by Jelmer Vernooij
Publish command works for github.
195
        try:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
196
            remote_repo = self.gh.get_repo('%s/%s' % (owner, project))
0.432.12 by Jelmer Vernooij
Fix .git ends.
197
            remote_repo.id
198
        except github.UnknownObjectException:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
199
            base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
200
            if owner == self.gh.get_user().login:
201
                owner_obj = self.gh.get_user()
0.432.3 by Jelmer Vernooij
Publish command works for github.
202
            else:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
203
                owner_obj = self.gh.get_organization(owner)
0.432.12 by Jelmer Vernooij
Fix .git ends.
204
            remote_repo = owner_obj.create_fork(base_repo)
0.432.3 by Jelmer Vernooij
Publish command works for github.
205
            note(gettext('Forking new repository %s from %s') %
7211.13.7 by Jelmer Vernooij
Fix formatting.
206
                 (remote_repo.html_url, base_repo.html_url))
0.432.3 by Jelmer Vernooij
Publish command works for github.
207
        else:
208
            note(gettext('Reusing existing repository %s') % remote_repo.html_url)
0.431.32 by Jelmer Vernooij
Properly resolve git+ssh URLs.
209
        remote_dir = controldir.ControlDir.open(git_url_to_bzr_url(remote_repo.ssh_url))
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
210
        try:
7211.13.7 by Jelmer Vernooij
Fix formatting.
211
            push_result = remote_dir.push_branch(
212
                local_branch, revision_id=revision_id, overwrite=overwrite,
213
                name=name)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
214
        except errors.NoRoundtrippingSupport:
215
            if not allow_lossy:
216
                raise
7211.13.7 by Jelmer Vernooij
Fix formatting.
217
            push_result = remote_dir.push_branch(
218
                local_branch, revision_id=revision_id,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
219
                overwrite=overwrite, name=name, lossy=True)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
220
        return push_result.target_branch, github_url_to_bzr_url(
7211.13.7 by Jelmer Vernooij
Fix formatting.
221
            remote_repo.html_url, name)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
222
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
223
    @convert_github_error
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
224
    def get_push_url(self, branch):
225
        owner, project, branch_name = parse_github_url(branch)
226
        repo = self.gh.get_repo('%s/%s' % (owner, project))
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
227
        return github_url_to_bzr_url(repo.ssh_url, branch_name)
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
228
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
229
    @convert_github_error
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
230
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
231
        import github
232
        base_owner, base_project, base_branch_name = parse_github_url(base_branch)
233
        base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
234
        if owner is None:
235
            owner = self.gh.get_user().login
236
        if project is None:
237
            project = base_repo.name
238
        try:
239
            remote_repo = self.gh.get_repo('%s/%s' % (owner, project))
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
240
            full_url = github_url_to_bzr_url(remote_repo.ssh_url, name)
0.431.33 by Jelmer Vernooij
Fix URLs from gitlab.
241
            return _mod_branch.Branch.open(full_url)
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
242
        except github.UnknownObjectException:
243
            raise errors.NotBranchError('https://github.com/%s/%s' % (owner, project))
244
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
245
    @convert_github_error
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
246
    def get_proposer(self, source_branch, target_branch):
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
247
        return GitHubMergeProposalBuilder(self.gh, source_branch, target_branch)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
248
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
249
    @convert_github_error
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
250
    def iter_proposals(self, source_branch, target_branch, status='open'):
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
251
        (source_owner, source_repo_name, source_branch_name) = (
7211.13.7 by Jelmer Vernooij
Fix formatting.
252
            parse_github_url(source_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
253
        (target_owner, target_repo_name, target_branch_name) = (
7211.13.7 by Jelmer Vernooij
Fix formatting.
254
            parse_github_url(target_branch))
0.431.67 by Jelmer Vernooij
Support multiple merge proposals per branch.
255
        target_repo = self.gh.get_repo(
256
            "%s/%s" % (target_owner, target_repo_name))
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
257
        state = {
258
            'open': 'open',
259
            'merged': 'closed',
260
            'closed': 'closed',
261
            'all': 'all'}
262
        for pull in target_repo.get_pulls(
263
                head=target_branch_name,
264
                state=state[status]):
265
            if (status == 'closed' and pull.merged or
266
                    status == 'merged' and not pull.merged):
267
                continue
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
268
            if pull.head.ref != source_branch_name:
269
                continue
7268.4.1 by Jelmer Vernooij
Don't attempt to resolve None when repo has gone away.
270
            if pull.head.repo is None:
271
                # Repo has gone the way of the dodo
272
                continue
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
273
            if (pull.head.repo.owner.login != source_owner or
7211.13.7 by Jelmer Vernooij
Fix formatting.
274
                    pull.head.repo.name != source_repo_name):
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
275
                continue
0.431.67 by Jelmer Vernooij
Support multiple merge proposals per branch.
276
            yield GitHubMergeProposal(pull)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
277
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
278
    def hosts(self, branch):
279
        try:
280
            parse_github_url(branch)
281
        except NotGitHubUrl:
282
            return False
283
        else:
284
            return True
285
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
286
    @classmethod
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
287
    def probe(cls, branch):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
288
        try:
289
            parse_github_url(branch)
290
        except NotGitHubUrl:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
291
            raise UnsupportedHoster(branch)
292
        return cls()
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
293
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
294
    @classmethod
295
    def iter_instances(cls):
296
        yield cls()
297
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
298
    @convert_github_error
0.431.66 by Jelmer Vernooij
Add support for status argument.
299
    def iter_my_proposals(self, status='open'):
300
        query = ['is:pr']
301
        if status == 'open':
302
            query.append('is:open')
303
        elif status == 'closed':
304
            query.append('is:unmerged')
7268.2.1 by Jelmer Vernooij
Don't include open unmerged pull requests in 'closed'.
305
            # Also use "is:closed" otherwise unmerged open pull requests are
306
            # also included.
307
            query.append('is:closed')
0.431.66 by Jelmer Vernooij
Add support for status argument.
308
        elif status == 'merged':
309
            query.append('is:merged')
310
        query.append('author:%s' % self.gh.get_user().login)
311
        for issue in self.gh.search_issues(query=' '.join(query)):
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
312
            yield GitHubMergeProposal(issue.as_pull_request())
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
313
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
314
0.432.2 by Jelmer Vernooij
Publish command sort of works.
315
class GitHubMergeProposalBuilder(MergeProposalBuilder):
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
316
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
317
    def __init__(self, gh, source_branch, target_branch):
318
        self.gh = gh
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
319
        self.source_branch = source_branch
320
        self.target_branch = target_branch
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
321
        (self.target_owner, self.target_repo_name, self.target_branch_name) = (
7211.13.7 by Jelmer Vernooij
Fix formatting.
322
            parse_github_url(self.target_branch))
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
323
        (self.source_owner, self.source_repo_name, self.source_branch_name) = (
7211.13.7 by Jelmer Vernooij
Fix formatting.
324
            parse_github_url(self.source_branch))
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
325
326
    def get_infotext(self):
327
        """Determine the initial comment for the merge proposal."""
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
328
        info = []
329
        info.append("Merge %s into %s:%s\n" % (
330
            self.source_branch_name, self.target_owner,
331
            self.target_branch_name))
332
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
333
        info.append("Target: %s\n" % self.target_branch.user_url)
334
        return ''.join(info)
335
336
    def get_initial_body(self):
337
        """Get a body for the proposal for the user to modify.
338
339
        :return: a str or None.
340
        """
341
        return None
342
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
343
    def create_proposal(self, description, reviewers=None, labels=None,
344
                        prerequisite_branch=None):
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
345
        """Perform the submission."""
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
346
        if prerequisite_branch is not None:
347
            raise PrerequisiteBranchUnsupported(self)
0.432.10 by Jelmer Vernooij
More test fixes.
348
        import github
0.432.7 by Jelmer Vernooij
propose works \o/
349
        # TODO(jelmer): Probe for right repo name
0.432.12 by Jelmer Vernooij
Fix .git ends.
350
        if self.target_repo_name.endswith('.git'):
351
            self.target_repo_name = self.target_repo_name[:-4]
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
352
        target_repo = self.gh.get_repo("%s/%s" % (self.target_owner, self.target_repo_name))
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
353
        # TODO(jelmer): Allow setting title explicitly?
0.431.44 by Jelmer Vernooij
Support get/set description.
354
        title = determine_title(description)
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
355
        # TOOD(jelmer): Set maintainers_can_modify?
0.432.10 by Jelmer Vernooij
More test fixes.
356
        try:
357
            pull_request = target_repo.create_pull(
358
                title=title, body=description,
359
                head="%s:%s" % (self.source_owner, self.source_branch_name),
360
                base=self.target_branch_name)
361
        except github.GithubException as e:
362
            if e.status == 422:
363
                raise MergeProposalExists(self.source_branch.user_url)
364
            raise
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
365
        if reviewers:
366
            for reviewer in reviewers:
367
                pull_request.assignees.append(
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
368
                    self.gh.get_user(reviewer))
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
369
        if labels:
370
            for label in labels:
371
                pull_request.issue.labels.append(label)
0.431.44 by Jelmer Vernooij
Support get/set description.
372
        return GitHubMergeProposal(pull_request)