/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.35 by Jelmer Vernooij
Add Hoster.get_proposal.
28
    NoMergeProposal,
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
78
def connect_github():
79
    user_agent = user_agent="Breezy/%s" % breezy_version
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
80
81
    auth = AuthenticationConfig()
82
83
    credentials = auth.get_credentials('https', 'github.com')
84
    if credentials is not None:
0.432.3 by Jelmer Vernooij
Publish command works for github.
85
        return Github(credentials['user'], credentials['password'],
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
86
                      user_agent=user_agent)
87
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
88
    # TODO(jelmer): token = auth.get_token('https', 'github.com')
89
    token = retrieve_github_token('https', 'github.com')
90
    if token is not None:
0.431.61 by Jelmer Vernooij
Fix token login.
91
        return Github(token, user_agent=user_agent)
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
92
    else:
93
        note('Accessing GitHub anonymously. To log in, run \'brz gh-login\'.')
94
        return Github(user_agent=user_agent)
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
95
96
0.431.44 by Jelmer Vernooij
Support get/set description.
97
class GitHubMergeProposal(MergeProposal):
98
99
    def __init__(self, pr):
100
        self._pr = pr
101
102
    @property
103
    def url(self):
104
        return self._pr.html_url
105
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
106
    def _branch_from_part(self, part):
107
        return github_url_to_bzr_url(part.repo.html_url, part.ref)
108
109
    def get_source_branch_url(self):
110
        return self._branch_from_part(self._pr.head)
111
112
    def get_target_branch_url(self):
113
        return self._branch_from_part(self._pr.base)
114
0.431.44 by Jelmer Vernooij
Support get/set description.
115
    def get_description(self):
116
        return self._pr.body
117
118
    def set_description(self, description):
119
        self._pr.edit(body=description, title=determine_title(description))
120
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
121
    def is_merged(self):
122
        return self._pr.merged
123
0.431.44 by Jelmer Vernooij
Support get/set description.
124
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
125
def parse_github_url(branch):
126
    url = urlutils.split_segment_parameters(branch.user_url)[0]
127
    (scheme, user, password, host, port, path) = urlutils.parse_url(
128
        url)
129
    if host != 'github.com':
130
        raise NotGitHubUrl(url)
131
    (owner, repo_name) = path.strip('/').split('/')
0.432.12 by Jelmer Vernooij
Fix .git ends.
132
    if repo_name.endswith('.git'):
133
        repo_name = repo_name[:-4]
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
134
    return owner, repo_name, branch.name
135
136
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
137
def github_url_to_bzr_url(url, branch_name):
138
    if not PY3:
139
        branch_name = branch_name.encode('utf-8')
140
    return urlutils.join_segment_parameters(
141
            git_url_to_bzr_url(url), {"branch": branch_name})
142
143
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
144
class GitHub(Hoster):
145
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
146
    name = 'github'
147
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
148
    supports_merge_proposal_labels = True
149
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
150
    def __repr__(self):
151
        return "GitHub()"
152
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
153
    def __init__(self):
154
        self.gh = connect_github()
155
0.431.20 by Jelmer Vernooij
publish -> publish_derived.
156
    def publish_derived(self, local_branch, base_branch, name, project=None,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
157
                        owner=None, revision_id=None, overwrite=False,
158
                        allow_lossy=True):
0.432.12 by Jelmer Vernooij
Fix .git ends.
159
        import github
0.432.3 by Jelmer Vernooij
Publish command works for github.
160
        base_owner, base_project, base_branch_name = parse_github_url(base_branch)
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
161
        base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
0.432.3 by Jelmer Vernooij
Publish command works for github.
162
        if owner is None:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
163
            owner = self.gh.get_user().login
0.432.3 by Jelmer Vernooij
Publish command works for github.
164
        if project is None:
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
165
            project = base_repo.name
0.432.3 by Jelmer Vernooij
Publish command works for github.
166
        try:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
167
            remote_repo = self.gh.get_repo('%s/%s' % (owner, project))
0.432.12 by Jelmer Vernooij
Fix .git ends.
168
            remote_repo.id
169
        except github.UnknownObjectException:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
170
            base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
171
            if owner == self.gh.get_user().login:
172
                owner_obj = self.gh.get_user()
0.432.3 by Jelmer Vernooij
Publish command works for github.
173
            else:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
174
                owner_obj = self.gh.get_organization(owner)
0.432.12 by Jelmer Vernooij
Fix .git ends.
175
            remote_repo = owner_obj.create_fork(base_repo)
0.432.3 by Jelmer Vernooij
Publish command works for github.
176
            note(gettext('Forking new repository %s from %s') %
177
                    (remote_repo.html_url, base_repo.html_url))
178
        else:
179
            note(gettext('Reusing existing repository %s') % remote_repo.html_url)
0.431.32 by Jelmer Vernooij
Properly resolve git+ssh URLs.
180
        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.
181
        try:
182
            push_result = remote_dir.push_branch(local_branch, revision_id=revision_id,
183
                overwrite=overwrite, name=name)
184
        except errors.NoRoundtrippingSupport:
185
            if not allow_lossy:
186
                raise
187
            push_result = remote_dir.push_branch(local_branch, revision_id=revision_id,
188
                overwrite=overwrite, name=name, lossy=True)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
189
        return push_result.target_branch, github_url_to_bzr_url(
190
                remote_repo.html_url, name)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
191
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
192
    def get_push_url(self, branch):
193
        owner, project, branch_name = parse_github_url(branch)
194
        repo = self.gh.get_repo('%s/%s' % (owner, project))
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
195
        return github_url_to_bzr_url(repo.ssh_url, branch_name)
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
196
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
197
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
198
        import github
199
        base_owner, base_project, base_branch_name = parse_github_url(base_branch)
200
        base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
201
        if owner is None:
202
            owner = self.gh.get_user().login
203
        if project is None:
204
            project = base_repo.name
205
        try:
206
            remote_repo = self.gh.get_repo('%s/%s' % (owner, project))
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
207
            full_url = github_url_to_bzr_url(remote_repo.ssh_url, name)
0.431.33 by Jelmer Vernooij
Fix URLs from gitlab.
208
            return _mod_branch.Branch.open(full_url)
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
209
        except github.UnknownObjectException:
210
            raise errors.NotBranchError('https://github.com/%s/%s' % (owner, project))
211
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
212
    def get_proposer(self, source_branch, target_branch):
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
213
        return GitHubMergeProposalBuilder(self.gh, source_branch, target_branch)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
214
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
215
    def get_proposal(self, source_branch, target_branch):
216
        (source_owner, source_repo_name, source_branch_name) = (
217
                parse_github_url(source_branch))
218
        (target_owner, target_repo_name, target_branch_name) = (
219
                parse_github_url(target_branch))
220
        target_repo = self.gh.get_repo("%s/%s" % (target_owner, target_repo_name))
221
        for pull in target_repo.get_pulls(head=target_branch_name):
222
            if pull.head.ref != source_branch_name:
223
                continue
224
            if (pull.head.repo.owner.login != source_owner or
225
                pull.head.repo.name != source_repo_name):
226
                continue
0.431.44 by Jelmer Vernooij
Support get/set description.
227
            return GitHubMergeProposal(pull)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
228
        raise NoMergeProposal()
229
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
230
    def hosts(self, branch):
231
        try:
232
            parse_github_url(branch)
233
        except NotGitHubUrl:
234
            return False
235
        else:
236
            return True
237
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
238
    @classmethod
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
239
    def probe(cls, branch):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
240
        try:
241
            parse_github_url(branch)
242
        except NotGitHubUrl:
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
243
            raise UnsupportedHoster(branch)
244
        return cls()
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
245
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
246
    @classmethod
247
    def iter_instances(cls):
248
        yield cls()
249
250
    def iter_my_proposals(self):
251
        for issue in self.gh.search_issues(
252
                query='is:pr is:open author:%s' % self.gh.get_user().login):
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
253
            yield GitHubMergeProposal(issue.as_pull_request())
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
254
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
255
0.432.2 by Jelmer Vernooij
Publish command sort of works.
256
class GitHubMergeProposalBuilder(MergeProposalBuilder):
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
257
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
258
    def __init__(self, gh, source_branch, target_branch):
259
        self.gh = gh
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
260
        self.source_branch = source_branch
261
        self.target_branch = target_branch
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
262
        (self.target_owner, self.target_repo_name, self.target_branch_name) = (
263
                parse_github_url(self.target_branch))
264
        (self.source_owner, self.source_repo_name, self.source_branch_name) = (
265
                parse_github_url(self.source_branch))
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
266
267
    def get_infotext(self):
268
        """Determine the initial comment for the merge proposal."""
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
269
        info = []
270
        info.append("Merge %s into %s:%s\n" % (
271
            self.source_branch_name, self.target_owner,
272
            self.target_branch_name))
273
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
274
        info.append("Target: %s\n" % self.target_branch.user_url)
275
        return ''.join(info)
276
277
    def get_initial_body(self):
278
        """Get a body for the proposal for the user to modify.
279
280
        :return: a str or None.
281
        """
282
        return None
283
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
284
    def create_proposal(self, description, reviewers=None, labels=None,
285
                        prerequisite_branch=None):
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
286
        """Perform the submission."""
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
287
        if prerequisite_branch is not None:
288
            raise PrerequisiteBranchUnsupported(self)
0.432.10 by Jelmer Vernooij
More test fixes.
289
        import github
0.432.7 by Jelmer Vernooij
propose works \o/
290
        # TODO(jelmer): Probe for right repo name
0.432.12 by Jelmer Vernooij
Fix .git ends.
291
        if self.target_repo_name.endswith('.git'):
292
            self.target_repo_name = self.target_repo_name[:-4]
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
293
        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.
294
        # TODO(jelmer): Allow setting title explicitly?
0.431.44 by Jelmer Vernooij
Support get/set description.
295
        title = determine_title(description)
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
296
        # TOOD(jelmer): Set maintainers_can_modify?
0.432.10 by Jelmer Vernooij
More test fixes.
297
        try:
298
            pull_request = target_repo.create_pull(
299
                title=title, body=description,
300
                head="%s:%s" % (self.source_owner, self.source_branch_name),
301
                base=self.target_branch_name)
302
        except github.GithubException as e:
303
            if e.status == 422:
304
                raise MergeProposalExists(self.source_branch.user_url)
305
            raise
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
306
        if reviewers:
307
            for reviewer in reviewers:
308
                pull_request.assignees.append(
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
309
                    self.gh.get_user(reviewer))
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
310
        if labels:
311
            for label in labels:
312
                pull_request.issue.labels.append(label)
0.431.44 by Jelmer Vernooij
Support get/set description.
313
        return GitHubMergeProposal(pull_request)