/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
7359.1.3 by Jelmer Vernooij
Fix GitHub API interaction.
21
import json
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
22
import os
23
7408.3.1 by Jelmer Vernooij
Move propose module into core.
24
from ...propose import (
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
25
    determine_title,
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
26
    Hoster,
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
27
    HosterLoginRequired,
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
28
    MergeProposal,
0.432.2 by Jelmer Vernooij
Publish command sort of works.
29
    MergeProposalBuilder,
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
30
    MergeProposalExists,
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
31
    NoSuchProject,
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
32
    PrerequisiteBranchUnsupported,
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
33
    ReopenFailed,
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
34
    UnsupportedHoster,
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
35
    )
36
37
from ... import (
7340.1.1 by Martin
Fix use of config_dir in propose plugin
38
    bedding,
0.431.33 by Jelmer Vernooij
Fix URLs from gitlab.
39
    branch as _mod_branch,
0.432.3 by Jelmer Vernooij
Publish command works for github.
40
    controldir,
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
41
    errors,
42
    hooks,
43
    urlutils,
0.432.3 by Jelmer Vernooij
Publish command works for github.
44
    version_string as breezy_version,
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
45
    )
7340.1.1 by Martin
Fix use of config_dir in propose plugin
46
from ...config import AuthenticationConfig, GlobalStack
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
47
from ...errors import (
48
    InvalidHttpResponse,
49
    PermissionDenied,
50
    UnexpectedHttpStatus,
51
    )
0.431.32 by Jelmer Vernooij
Properly resolve git+ssh URLs.
52
from ...git.urls import git_url_to_bzr_url
0.432.3 by Jelmer Vernooij
Publish command works for github.
53
from ...i18n import gettext
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
54
from ...sixish import PY3
0.432.3 by Jelmer Vernooij
Publish command works for github.
55
from ...trace import note
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
56
from ...transport import get_transport
7296.10.5 by Jelmer Vernooij
use default_user_agent function.
57
from ...transport.http import default_user_agent
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
58
59
60
GITHUB_HOST = 'github.com'
61
WEB_GITHUB_URL = 'https://github.com'
62
API_GITHUB_URL = 'https://api.github.com'
7408.1.3 by Jelmer Vernooij
Support pagination for github.
63
DEFAULT_PER_PAGE = 50
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
64
65
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
66
def store_github_token(scheme, host, token):
7340.1.1 by Martin
Fix use of config_dir in propose plugin
67
    with open(os.path.join(bedding.config_dir(), 'github.conf'), 'w') as f:
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
68
        f.write(token)
69
70
71
def retrieve_github_token(scheme, host):
7340.1.1 by Martin
Fix use of config_dir in propose plugin
72
    path = os.path.join(bedding.config_dir(), 'github.conf')
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
73
    if not os.path.exists(path):
74
        return None
0.435.1 by Jelmer Vernooij
Fix reading github credentials.
75
    with open(path, 'r') as f:
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
76
        return f.read().strip()
77
78
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
79
class ValidationFailed(errors.BzrError):
80
81
    _fmt = "GitHub validation failed: %(error)s"
82
83
    def __init__(self, error):
84
        errors.BzrError.__init__(self)
85
        self.error = error
86
87
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
88
class NotGitHubUrl(errors.BzrError):
89
90
    _fmt = "Not a GitHub URL: %(url)s"
91
92
    def __init__(self, url):
93
        errors.BzrError.__init__(self)
94
        self.url = url
95
96
7268.8.2 by Jelmer Vernooij
Handle GitHub errors.
97
class GitHubLoginRequired(HosterLoginRequired):
98
99
    _fmt = "Action requires GitHub login."
100
101
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
102
def connect_github():
7211.13.7 by Jelmer Vernooij
Fix formatting.
103
    """Connect to GitHub.
104
    """
7296.10.5 by Jelmer Vernooij
use default_user_agent function.
105
    user_agent = default_user_agent()
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
106
    auth = AuthenticationConfig()
107
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
108
    credentials = auth.get_credentials('https', GITHUB_HOST)
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
109
    if credentials is not None:
0.432.3 by Jelmer Vernooij
Publish command works for github.
110
        return Github(credentials['user'], credentials['password'],
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
111
                      user_agent=user_agent)
112
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
113
    # TODO(jelmer): token = auth.get_token('https', GITHUB_HOST)
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
114
    if token is not None:
0.431.61 by Jelmer Vernooij
Fix token login.
115
        return Github(token, user_agent=user_agent)
0.431.49 by Jelmer Vernooij
Store GitHub tokens in a magic file, for now.
116
    else:
117
        note('Accessing GitHub anonymously. To log in, run \'brz gh-login\'.')
118
        return Github(user_agent=user_agent)
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
119
120
0.431.44 by Jelmer Vernooij
Support get/set description.
121
class GitHubMergeProposal(MergeProposal):
122
7371.4.2 by Jelmer Vernooij
More fixes.
123
    def __init__(self, gh, pr):
124
        self._gh = gh
0.431.44 by Jelmer Vernooij
Support get/set description.
125
        self._pr = pr
126
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
127
    def __repr__(self):
128
        return "<%s at %r>" % (type(self).__name__, self.url)
129
7490.105.1 by Jelmer Vernooij
Add some hoster metadata fields.
130
    name = 'GitHub'
131
0.431.44 by Jelmer Vernooij
Support get/set description.
132
    @property
133
    def url(self):
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
134
        return self._pr['html_url']
0.431.44 by Jelmer Vernooij
Support get/set description.
135
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
136
    def _branch_from_part(self, part):
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
137
        if part['repo'] is None:
138
            return None
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
139
        return github_url_to_bzr_url(part['repo']['html_url'], part['ref'])
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
140
141
    def get_source_branch_url(self):
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
142
        return self._branch_from_part(self._pr['head'])
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
143
7490.65.1 by Jelmer Vernooij
Add functions fo retrieving merge proposal source revision.
144
    def get_source_revision(self):
145
        """Return the latest revision for the source branch."""
146
        from breezy.git.mapping import default_mapping
147
        return default_mapping.revision_id_foreign_to_bzr(
148
            self._pr['head']['sha'].encode('ascii'))
149
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
150
    def get_target_branch_url(self):
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
151
        return self._branch_from_part(self._pr['base'])
0.431.64 by Jelmer Vernooij
Add get_source_branch_url/get_target_branch_url methods.
152
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
153
    def get_source_project(self):
154
        return self._pr['head']['repo']['full_name']
155
156
    def get_target_project(self):
157
        return self._pr['base']['repo']['full_name']
158
0.431.44 by Jelmer Vernooij
Support get/set description.
159
    def get_description(self):
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
160
        return self._pr['body']
0.431.44 by Jelmer Vernooij
Support get/set description.
161
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
162
    def get_commit_message(self):
163
        return None
164
7371.4.2 by Jelmer Vernooij
More fixes.
165
    def set_commit_message(self, message):
7443.1.1 by Jelmer Vernooij
Don't attempt to set the title when updating the commit message in GitHub.
166
        raise errors.UnsupportedOperation(self.set_commit_message, self)
7371.4.2 by Jelmer Vernooij
More fixes.
167
168
    def _patch(self, data):
169
        response = self._gh._api_request(
170
            'PATCH', self._pr['url'], body=json.dumps(data).encode('utf-8'))
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
171
        if response.status == 422:
172
            raise ValidationFailed(json.loads(response.text))
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
173
        if response.status != 200:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
174
            raise UnexpectedHttpStatus(self._pr['url'], response.status)
7371.4.2 by Jelmer Vernooij
More fixes.
175
        self._pr = json.loads(response.text)
176
0.431.44 by Jelmer Vernooij
Support get/set description.
177
    def set_description(self, description):
7371.4.2 by Jelmer Vernooij
More fixes.
178
        self._patch({
179
            'body': description,
180
            'title': determine_title(description),
181
            })
0.431.44 by Jelmer Vernooij
Support get/set description.
182
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
183
    def is_merged(self):
7381.5.4 by Jelmer Vernooij
'merged' doesn't appear to always be set.
184
        return bool(self._pr.get('merged_at'))
0.431.46 by Jelmer Vernooij
Add MergeProposal.is_merged.
185
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
186
    def is_closed(self):
7381.5.4 by Jelmer Vernooij
'merged' doesn't appear to always be set.
187
        return self._pr['state'] == 'closed' and not bool(self._pr.get('merged_at'))
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
188
189
    def reopen(self):
190
        try:
191
            self._patch({'state': 'open'})
192
        except ValidationFailed as e:
193
            raise ReopenFailed(e.error['errors'][0]['message'])
194
7260.2.1 by Jelmer Vernooij
Implement .close on merge proposals.
195
    def close(self):
7371.4.2 by Jelmer Vernooij
More fixes.
196
        self._patch({'state': 'closed'})
7260.2.1 by Jelmer Vernooij
Implement .close on merge proposals.
197
7380.1.1 by Jelmer Vernooij
Several more fixes for git merge proposals.
198
    def can_be_merged(self):
199
        return self._pr['mergeable']
200
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
201
    def merge(self, commit_message=None):
202
        # https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button
7490.52.1 by Jelmer Vernooij
Add MergeProposal.post_comment.
203
        data = {}
204
        if commit_message:
205
            data['commit_message'] = commit_messae
206
        response = self._gh._api_request(
207
            'PUT', self._pr['url'] + "/merge", body=json.dumps(data).encode('utf-8'))
208
        if response.status == 422:
209
            raise ValidationFailed(json.loads(response.text))
210
        if response.status != 200:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
211
            raise UnexpectedHttpStatus(self._pr['url'], response.status)
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
212
7414.4.2 by Jelmer Vernooij
Fix gitlab / github merged_by fetching.
213
    def get_merged_by(self):
214
        merged_by = self._pr.get('merged_by')
215
        if merged_by is None:
216
            return None
217
        return merged_by['login']
218
7414.4.3 by Jelmer Vernooij
Add MergeProposal.get_merged_at.
219
    def get_merged_at(self):
220
        merged_at = self._pr.get('merged_at')
221
        if merged_at is None:
222
            return None
7414.4.4 by Jelmer Vernooij
Use iso8601 module.
223
        import iso8601
224
        return iso8601.parse_date(merged_at)
7414.4.3 by Jelmer Vernooij
Add MergeProposal.get_merged_at.
225
7490.52.1 by Jelmer Vernooij
Add MergeProposal.post_comment.
226
    def post_comment(self, body):
227
        data = {'body': body}
228
        response = self._gh._api_request(
229
            'POST', self._pr['comments_url'], body=json.dumps(data).encode('utf-8'))
230
        if response.status == 422:
231
            raise ValidationFailed(json.loads(response.text))
232
        if response.status != 201:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
233
            raise UnexpectedHttpStatus(
234
                self._pr['comments_url'], response.status)
7490.52.1 by Jelmer Vernooij
Add MergeProposal.post_comment.
235
        json.loads(response.text)
236
0.431.44 by Jelmer Vernooij
Support get/set description.
237
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
238
def parse_github_url(url):
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
239
    (scheme, user, password, host, port, path) = urlutils.parse_url(
240
        url)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
241
    if host != GITHUB_HOST:
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
242
        raise NotGitHubUrl(url)
243
    (owner, repo_name) = path.strip('/').split('/')
0.432.12 by Jelmer Vernooij
Fix .git ends.
244
    if repo_name.endswith('.git'):
245
        repo_name = repo_name[:-4]
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
246
    return owner, repo_name
247
248
249
def parse_github_branch_url(branch):
7441.1.1 by Jelmer Vernooij
Add strip_segment_parameters function.
250
    url = urlutils.strip_segment_parameters(branch.user_url)
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
251
    owner, repo_name = parse_github_url(url)
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
252
    return owner, repo_name, branch.name
253
254
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
255
def github_url_to_bzr_url(url, branch_name):
256
    if not PY3:
257
        branch_name = branch_name.encode('utf-8')
7408.2.1 by Jelmer Vernooij
Use standard functions for creating Git URLs.
258
    return git_url_to_bzr_url(url, branch_name)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
259
260
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
261
def strip_optional(url):
262
    return url.split('{')[0]
263
264
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
265
class GitHub(Hoster):
266
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
267
    name = 'github'
268
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
269
    supports_merge_proposal_labels = True
7296.8.2 by Jelmer Vernooij
Add feature flag for commit message.
270
    supports_merge_proposal_commit_message = False
7490.3.9 by Jelmer Vernooij
Add supports_allow_collaboration flag.
271
    supports_allow_collaboration = True
7445.1.1 by Jelmer Vernooij
Add Hoster.merge_proposal_description_format and common function for determining title.
272
    merge_proposal_description_format = 'markdown'
0.431.13 by Jelmer Vernooij
Add support for labels on merge proposals.
273
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
274
    def __repr__(self):
275
        return "GitHub()"
276
7371.4.2 by Jelmer Vernooij
More fixes.
277
    def _api_request(self, method, path, body=None):
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
278
        headers = {
7426.1.1 by Jelmer Vernooij
Fix setting of attributes on GitHub pull requests.
279
            'Content-Type': 'application/json',
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
280
            'Accept': 'application/vnd.github.v3+json'}
281
        if self._token:
282
            headers['Authorization'] = 'token %s' % self._token
7490.115.1 by Jelmer Vernooij
Raise UnexpectedHttpStatus rather than InvalidHttpResponse.
283
        try:
284
            response = self.transport.request(
285
                method, urlutils.join(self.transport.base, path),
286
                headers=headers, body=body, retries=3)
287
        except UnexpectedHttpStatus as e:
288
            if e.code == 401:
289
                raise GitHubLoginRequired(self)
290
            else:
291
                raise
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
292
        if response.status == 401:
293
            raise GitHubLoginRequired(self)
294
        return response
295
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
296
    def _get_repo(self, owner, repo):
297
        path = 'repos/%s/%s' % (owner, repo)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
298
        response = self._api_request('GET', path)
299
        if response.status == 404:
300
            raise NoSuchProject(path)
301
        if response.status == 200:
7359.1.3 by Jelmer Vernooij
Fix GitHub API interaction.
302
            return json.loads(response.text)
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
303
        raise UnexpectedHttpStatus(path, response.status)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
304
7371.4.1 by Jelmer Vernooij
Fix iter_proposals for GitHub.
305
    def _get_repo_pulls(self, path, head=None, state=None):
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
306
        path = path + '?'
7371.4.1 by Jelmer Vernooij
Fix iter_proposals for GitHub.
307
        params = {}
308
        if head is not None:
309
            params['head'] = head
310
        if state is not None:
311
            params['state'] = state
312
        path += ';'.join(['%s=%s' % (k, urlutils.quote(v))
313
                         for k, v in params.items()])
314
        response = self._api_request('GET', path)
315
        if response.status == 404:
316
            raise NoSuchProject(path)
317
        if response.status == 200:
318
            return json.loads(response.text)
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
319
        raise UnexpectedHttpStatus(path, response.status)
7371.4.1 by Jelmer Vernooij
Fix iter_proposals for GitHub.
320
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
321
    def _create_pull(self, path, title, head, base, body=None, labels=None,
7490.3.8 by Jelmer Vernooij
Drop extra s.
322
                     assignee=None, draft=False, maintainer_can_modify=False):
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
323
        data = {
324
            'title': title,
325
            'head': head,
326
            'base': base,
7490.2.1 by Jelmer Vernooij
Fix draft option for github merge proposals.
327
            'draft': draft,
7490.3.8 by Jelmer Vernooij
Drop extra s.
328
            'maintainer_can_modify': maintainer_can_modify,
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
329
        }
7426.1.1 by Jelmer Vernooij
Fix setting of attributes on GitHub pull requests.
330
        if labels is not None:
331
            data['labels'] = labels
332
        if assignee is not None:
333
            data['assignee'] = assignee
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
334
        if body:
335
            data['body'] = body
336
337
        response = self._api_request(
338
            'POST', path, body=json.dumps(data).encode('utf-8'))
7428.1.1 by Jelmer Vernooij
Handle 403s for pull request creation.
339
        if response.status == 403:
340
            raise PermissionDenied(path, response.text)
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
341
        if response.status != 201:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
342
            raise UnexpectedHttpStatus(path, response.status)
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
343
        return json.loads(response.text)
344
345
    def _get_user_by_email(self, email):
346
        path = 'search/users?q=%s+in:email' % email
347
        response = self._api_request('GET', path)
348
        if response.status != 200:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
349
            raise UnexpectedHttpStatus(path, response.status)
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
350
        ret = json.loads(response.text)
351
        if ret['total_count'] == 0:
352
            raise KeyError('no user with email %s' % email)
353
        elif ret['total_count'] > 1:
354
            raise ValueError('more than one result for email %s' % email)
355
        return ret['items'][0]
356
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
357
    def _get_user(self, username=None):
358
        if username:
7426.1.1 by Jelmer Vernooij
Fix setting of attributes on GitHub pull requests.
359
            path = 'users/%s' % username
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
360
        else:
361
            path = 'user'
362
        response = self._api_request('GET', path)
363
        if response.status != 200:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
364
            raise UnexpectedHttpStatus(path, response.status)
7359.1.3 by Jelmer Vernooij
Fix GitHub API interaction.
365
        return json.loads(response.text)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
366
367
    def _get_organization(self, name):
7426.1.1 by Jelmer Vernooij
Fix setting of attributes on GitHub pull requests.
368
        path = 'orgs/%s' % name
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
369
        response = self._api_request('GET', path)
370
        if response.status != 200:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
371
            raise UnexpectedHttpStatus(path, response.status)
7359.1.3 by Jelmer Vernooij
Fix GitHub API interaction.
372
        return json.loads(response.text)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
373
7408.1.3 by Jelmer Vernooij
Support pagination for github.
374
    def _list_paged(self, path, parameters=None, per_page=None):
375
        if parameters is None:
376
            parameters = {}
377
        else:
378
            parameters = dict(parameters.items())
379
        if per_page:
380
            parameters['per_page'] = str(per_page)
381
        page = 1
382
        i = 0
383
        while path:
384
            parameters['page'] = str(page)
385
            response = self._api_request(
386
                'GET', path + '?' +
387
                ';'.join(['%s=%s' % (k, urlutils.quote(v))
388
                          for (k, v) in parameters.items()]))
389
            if response.status != 200:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
390
                raise UnexpectedHttpStatus(path, response.status)
7408.1.3 by Jelmer Vernooij
Support pagination for github.
391
            data = json.loads(response.text)
392
            for entry in data['items']:
393
                i += 1
394
                yield entry
395
            if i >= data['total_count']:
396
                break
397
            page += 1
398
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
399
    def _search_issues(self, query):
400
        path = 'search/issues'
7408.1.3 by Jelmer Vernooij
Support pagination for github.
401
        return self._list_paged(path, {'q': query}, per_page=DEFAULT_PER_PAGE)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
402
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
403
    def _create_fork(self, path, owner=None):
7490.86.3 by Jelmer Vernooij
Access current user lazily.
404
        if owner and owner != self.current_user['login']:
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
405
            path += '?organization=%s' % owner
406
        response = self._api_request('POST', path)
7397.1.2 by Jelmer Vernooij
Fix fork creation on github.
407
        if response.status != 202:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
408
            raise UnexpectedHttpStatus(path, response.status)
7359.1.3 by Jelmer Vernooij
Fix GitHub API interaction.
409
        return json.loads(response.text)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
410
7260.1.1 by Jelmer Vernooij
Add .base_url property to Hoster.
411
    @property
412
    def base_url(self):
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
413
        return WEB_GITHUB_URL
414
7490.86.3 by Jelmer Vernooij
Access current user lazily.
415
    def __init__(self, transport):
416
        self._token = retrieve_github_token('https', GITHUB_HOST)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
417
        self.transport = transport
7490.86.3 by Jelmer Vernooij
Access current user lazily.
418
        self._current_user = None
419
420
    @property
421
    def current_user(self):
422
        if self._current_user is None:
423
            self._current_user = self._get_user()
424
        return self._current_user
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
425
0.431.20 by Jelmer Vernooij
publish -> publish_derived.
426
    def publish_derived(self, local_branch, base_branch, name, project=None,
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
427
                        owner=None, revision_id=None, overwrite=False,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
428
                        allow_lossy=True, tag_selector=None):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
429
        base_owner, base_project, base_branch_name = parse_github_branch_url(base_branch)
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
430
        base_repo = self._get_repo(base_owner, base_project)
0.432.3 by Jelmer Vernooij
Publish command works for github.
431
        if owner is None:
7490.86.3 by Jelmer Vernooij
Access current user lazily.
432
            owner = self.current_user['login']
0.432.3 by Jelmer Vernooij
Publish command works for github.
433
        if project is None:
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
434
            project = base_repo['name']
0.432.3 by Jelmer Vernooij
Publish command works for github.
435
        try:
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
436
            remote_repo = self._get_repo(owner, project)
7410.1.1 by Jelmer Vernooij
Remove last imports of the pygithub module.
437
        except NoSuchProject:
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
438
            base_repo = self._get_repo(base_owner, base_project)
439
            remote_repo = self._create_fork(base_repo['forks_url'], owner)
0.432.3 by Jelmer Vernooij
Publish command works for github.
440
            note(gettext('Forking new repository %s from %s') %
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
441
                 (remote_repo['html_url'], base_repo['html_url']))
0.432.3 by Jelmer Vernooij
Publish command works for github.
442
        else:
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
443
            note(gettext('Reusing existing repository %s') % remote_repo['html_url'])
444
        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.
445
        try:
7211.13.7 by Jelmer Vernooij
Fix formatting.
446
            push_result = remote_dir.push_branch(
447
                local_branch, revision_id=revision_id, overwrite=overwrite,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
448
                name=name, tag_selector=tag_selector)
0.431.51 by Jelmer Vernooij
Allow fallback to lossy by default.
449
        except errors.NoRoundtrippingSupport:
450
            if not allow_lossy:
451
                raise
7211.13.7 by Jelmer Vernooij
Fix formatting.
452
            push_result = remote_dir.push_branch(
453
                local_branch, revision_id=revision_id,
7489.4.2 by Jelmer Vernooij
Plumb through tag_selector.
454
                overwrite=overwrite, name=name, lossy=True,
455
                tag_selector=tag_selector)
0.433.3 by Jelmer Vernooij
Some python 3 compatibility.
456
        return push_result.target_branch, github_url_to_bzr_url(
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
457
            remote_repo['html_url'], name)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
458
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
459
    def get_push_url(self, branch):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
460
        owner, project, branch_name = parse_github_branch_url(branch)
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
461
        repo = self._get_repo(owner, project)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
462
        return github_url_to_bzr_url(repo['ssh_url'], branch_name)
0.431.28 by Jelmer Vernooij
Implement Hoster.get_push_url.
463
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
464
    def get_derived_branch(self, base_branch, name, project=None, owner=None):
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
465
        base_owner, base_project, base_branch_name = parse_github_branch_url(base_branch)
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
466
        base_repo = self._get_repo(base_owner, base_project)
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
467
        if owner is None:
7490.86.3 by Jelmer Vernooij
Access current user lazily.
468
            owner = self.current_user['login']
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
469
        if project is None:
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
470
            project = base_repo['name']
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
471
        try:
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
472
            remote_repo = self._get_repo(owner, project)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
473
            full_url = github_url_to_bzr_url(remote_repo['ssh_url'], name)
0.431.33 by Jelmer Vernooij
Fix URLs from gitlab.
474
            return _mod_branch.Branch.open(full_url)
7410.1.1 by Jelmer Vernooij
Remove last imports of the pygithub module.
475
        except NoSuchProject:
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
476
            raise errors.NotBranchError('%s/%s/%s' % (WEB_GITHUB_URL, owner, project))
0.431.22 by Jelmer Vernooij
Add Hoster.get_derived_branch.
477
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
478
    def get_proposer(self, source_branch, target_branch):
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
479
        return GitHubMergeProposalBuilder(self, source_branch, target_branch)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
480
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
481
    def iter_proposals(self, source_branch, target_branch, status='open'):
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
482
        (source_owner, source_repo_name, source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
483
            parse_github_branch_url(source_branch))
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
484
        (target_owner, target_repo_name, target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
485
            parse_github_branch_url(target_branch))
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
486
        target_repo = self._get_repo(target_owner, target_repo_name)
0.431.68 by Jelmer Vernooij
Add status to other Hosters.
487
        state = {
488
            'open': 'open',
489
            'merged': 'closed',
490
            'closed': 'closed',
491
            'all': 'all'}
7371.4.1 by Jelmer Vernooij
Fix iter_proposals for GitHub.
492
        pulls = self._get_repo_pulls(
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
493
            strip_optional(target_repo['pulls_url']),
7371.4.1 by Jelmer Vernooij
Fix iter_proposals for GitHub.
494
            head=target_branch_name,
495
            state=state[status])
496
        for pull in pulls:
497
            if (status == 'closed' and pull['merged'] or
498
                    status == 'merged' and not pull['merged']):
499
                continue
500
            if pull['head']['ref'] != source_branch_name:
501
                continue
502
            if pull['head']['repo'] is None:
7268.4.1 by Jelmer Vernooij
Don't attempt to resolve None when repo has gone away.
503
                # Repo has gone the way of the dodo
504
                continue
7371.4.1 by Jelmer Vernooij
Fix iter_proposals for GitHub.
505
            if (pull['head']['repo']['owner']['login'] != source_owner or
506
                    pull['head']['repo']['name'] != source_repo_name):
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
507
                continue
7371.4.2 by Jelmer Vernooij
More fixes.
508
            yield GitHubMergeProposal(self, pull)
0.431.35 by Jelmer Vernooij
Add Hoster.get_proposal.
509
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
510
    def hosts(self, branch):
511
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
512
            parse_github_branch_url(branch)
0.433.1 by Jelmer Vernooij
Add Hoster.hosts.
513
        except NotGitHubUrl:
514
            return False
515
        else:
516
            return True
517
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
518
    @classmethod
7296.10.1 by Jelmer Vernooij
Initial work making gitlab just directly use ReST.
519
    def probe_from_url(cls, url, possible_transports=None):
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
520
        try:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
521
            parse_github_url(url)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
522
        except NotGitHubUrl:
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
523
            raise UnsupportedHoster(url)
7296.11.1 by Jelmer Vernooij
Initial work migrating GitHub away from library.
524
        transport = get_transport(
525
            API_GITHUB_URL, possible_transports=possible_transports)
7490.86.3 by Jelmer Vernooij
Access current user lazily.
526
        return cls(transport)
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
527
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
528
    @classmethod
529
    def iter_instances(cls):
7490.86.3 by Jelmer Vernooij
Access current user lazily.
530
        yield cls(get_transport(API_GITHUB_URL))
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
531
0.431.66 by Jelmer Vernooij
Add support for status argument.
532
    def iter_my_proposals(self, status='open'):
533
        query = ['is:pr']
534
        if status == 'open':
535
            query.append('is:open')
536
        elif status == 'closed':
537
            query.append('is:unmerged')
7268.2.1 by Jelmer Vernooij
Don't include open unmerged pull requests in 'closed'.
538
            # Also use "is:closed" otherwise unmerged open pull requests are
539
            # also included.
540
            query.append('is:closed')
0.431.66 by Jelmer Vernooij
Add support for status argument.
541
        elif status == 'merged':
542
            query.append('is:merged')
7490.86.3 by Jelmer Vernooij
Access current user lazily.
543
        query.append('author:%s' % self.current_user['login'])
7408.1.3 by Jelmer Vernooij
Support pagination for github.
544
        for issue in self._search_issues(query=' '.join(query)):
7360.1.5 by Jelmer Vernooij
Use authenticated request when requesting pull requests from GitHub; authenticated connections have a much lower rate limit.
545
            url = issue['pull_request']['url']
546
            response = self._api_request('GET', url)
547
            if response.status != 200:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
548
                raise UnexpectedHttpStatus(url, response.status)
7371.4.2 by Jelmer Vernooij
More fixes.
549
            yield GitHubMergeProposal(self, json.loads(response.text))
0.431.63 by Jelmer Vernooij
Add 'brz my-proposals' command.
550
7296.9.1 by Jelmer Vernooij
Add 'brz land' subcommand.
551
    def get_proposal_by_url(self, url):
552
        raise UnsupportedHoster(url)
553
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
554
    def iter_my_forks(self):
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
555
        response = self._api_request('GET', '/user/repos')
556
        if response.status != 200:
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
557
            raise UnexpectedHttpStatus(self.transport.user_url, response.status)
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
558
        for project in json.loads(response.text):
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
559
            if not project['fork']:
560
                continue
561
            yield project['full_name']
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
562
563
    def delete_project(self, path):
564
        path = 'repos/' + path
565
        response = self._api_request('DELETE', path)
566
        if response.status == 404:
567
            raise NoSuchProject(path)
7414.5.2 by Jelmer Vernooij
Change iter_my_projects to iter_my_forks.
568
        if response.status == 204:
569
            return
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
570
        if response.status == 200:
571
            return json.loads(response.text)
7490.90.6 by Jelmer Vernooij
More fixes for hg probing.
572
        raise UnexpectedHttpStatus(path, response.status)
7414.5.1 by Jelmer Vernooij
Add functions for managing projects.
573
7490.105.1 by Jelmer Vernooij
Add some hoster metadata fields.
574
    def get_current_user(self):
7490.114.4 by Jelmer Vernooij
Don't attempt to get logged in user if no login token is set.
575
        if self._token is not None:
576
            return self.current_user['login']
577
        return None
7490.105.1 by Jelmer Vernooij
Add some hoster metadata fields.
578
579
    def get_user_url(self, username):
580
        return urlutils.join(self.base_url, username)
581
0.432.1 by Jelmer Vernooij
Initial work on hoster support.
582
0.432.2 by Jelmer Vernooij
Publish command sort of works.
583
class GitHubMergeProposalBuilder(MergeProposalBuilder):
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
584
0.432.9 by Jelmer Vernooij
Drop is_compatible nonesense.
585
    def __init__(self, gh, source_branch, target_branch):
586
        self.gh = gh
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
587
        self.source_branch = source_branch
588
        self.target_branch = target_branch
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
589
        (self.target_owner, self.target_repo_name, self.target_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
590
            parse_github_branch_url(self.target_branch))
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
591
        (self.source_owner, self.source_repo_name, self.source_branch_name) = (
7268.12.1 by Jelmer Vernooij
Split out probe_from_url.
592
            parse_github_branch_url(self.source_branch))
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
593
594
    def get_infotext(self):
595
        """Determine the initial comment for the merge proposal."""
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
596
        info = []
597
        info.append("Merge %s into %s:%s\n" % (
598
            self.source_branch_name, self.target_owner,
599
            self.target_branch_name))
600
        info.append("Source: %s\n" % self.source_branch.user_url)
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
601
        info.append("Target: %s\n" % self.target_branch.user_url)
602
        return ''.join(info)
603
604
    def get_initial_body(self):
605
        """Get a body for the proposal for the user to modify.
606
607
        :return: a str or None.
608
        """
609
        return None
610
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
611
    def create_proposal(self, description, reviewers=None, labels=None,
7467.3.1 by Jelmer Vernooij
Add a work_in_progress flag.
612
                        prerequisite_branch=None, commit_message=None,
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
613
                        work_in_progress=False, allow_collaboration=False):
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
614
        """Perform the submission."""
0.431.56 by Jelmer Vernooij
Add support for prerequisite branches.
615
        if prerequisite_branch is not None:
616
            raise PrerequisiteBranchUnsupported(self)
7296.8.1 by Jelmer Vernooij
Add commit-message option to 'brz propose'.
617
        # Note that commit_message is ignored, since github doesn't support it.
0.432.7 by Jelmer Vernooij
propose works \o/
618
        # TODO(jelmer): Probe for right repo name
0.432.12 by Jelmer Vernooij
Fix .git ends.
619
        if self.target_repo_name.endswith('.git'):
620
            self.target_repo_name = self.target_repo_name[:-4]
0.431.4 by Jelmer Vernooij
Add basic GitHub support.
621
        # TODO(jelmer): Allow setting title explicitly?
0.431.44 by Jelmer Vernooij
Support get/set description.
622
        title = determine_title(description)
7414.2.1 by Jelmer Vernooij
Support following links in github repositories.
623
        target_repo = self.gh._get_repo(
624
            self.target_owner, self.target_repo_name)
7410.1.3 by Jelmer Vernooij
Fix issue handling.
625
        assignees = []
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
626
        if reviewers:
7426.1.1 by Jelmer Vernooij
Fix setting of attributes on GitHub pull requests.
627
            assignees = []
0.431.6 by Jelmer Vernooij
Initial gitlab support works.
628
            for reviewer in reviewers:
7381.5.1 by Jelmer Vernooij
Several more fixes for merge proposals. Add functions for reopening merge proposals.
629
                if '@' in reviewer:
630
                    user = self.gh._get_user_by_email(reviewer)
631
                else:
632
                    user = self.gh._get_user(reviewer)
7410.1.3 by Jelmer Vernooij
Fix issue handling.
633
                assignees.append(user['login'])
7426.1.1 by Jelmer Vernooij
Fix setting of attributes on GitHub pull requests.
634
        else:
635
            assignees = None
636
        try:
637
            pull_request = self.gh._create_pull(
638
                strip_optional(target_repo['pulls_url']),
639
                title=title, body=description,
640
                head="%s:%s" % (self.source_owner, self.source_branch_name),
641
                base=self.target_branch_name,
7467.3.1 by Jelmer Vernooij
Add a work_in_progress flag.
642
                labels=labels, assignee=assignees,
7490.6.1 by Jelmer Vernooij
Add allow-collaboration flag.
643
                draft=work_in_progress,
644
                maintainer_can_modify=allow_collaboration,
645
                )
7426.1.1 by Jelmer Vernooij
Fix setting of attributes on GitHub pull requests.
646
        except ValidationFailed:
647
            raise MergeProposalExists(self.source_branch.user_url)
7371.4.2 by Jelmer Vernooij
More fixes.
648
        return GitHubMergeProposal(self.gh, pull_request)