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