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