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