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