Merge lp:~jelmer/brz/custom-github into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/custom-github
Merge into: lp:brz
Prerequisite: lp:~jelmer/brz/custom-gitlab
Diff against target: 327 lines (+110/-71)
1 file modified
breezy/plugins/propose/github.py (+110/-71)
To merge this branch: bzr merge lp:~jelmer/brz/custom-github
Reviewer Review Type Date Requested Status
Martin Packman Approve
Review via email: mp+367669@code.launchpad.net

Commit message

Use local REST implementation to access GitHub API.

Description of the change

Use local REST implementation to access GitHub API.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Unrelated to PR content, REST when talking about apis is always all caps, ReST is only used for the python markup format?

Change looks good, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/plugins/propose/github.py'
2--- breezy/plugins/propose/github.py 2019-05-21 01:25:23 +0000
3+++ breezy/plugins/propose/github.py 2019-05-21 01:25:23 +0000
4@@ -26,6 +26,7 @@
5 MergeProposal,
6 MergeProposalBuilder,
7 MergeProposalExists,
8+ NoSuchProject,
9 PrerequisiteBranchUnsupported,
10 UnsupportedHoster,
11 )
12@@ -39,15 +40,18 @@
13 version_string as breezy_version,
14 )
15 from ...config import AuthenticationConfig, GlobalStack, config_dir
16+from ...errors import InvalidHttpResponse
17 from ...git.urls import git_url_to_bzr_url
18 from ...i18n import gettext
19 from ...sixish import PY3
20 from ...trace import note
21+from ...transport import get_transport
22 from ...transport.http import default_user_agent
23-from ...lazy_import import lazy_import
24-lazy_import(globals(), """
25-from github import Github
26-""")
27+
28+
29+GITHUB_HOST = 'github.com'
30+WEB_GITHUB_URL = 'https://github.com'
31+API_GITHUB_URL = 'https://api.github.com'
32
33
34 def store_github_token(scheme, host, token):
35@@ -87,13 +91,12 @@
36 user_agent = default_user_agent()
37 auth = AuthenticationConfig()
38
39- credentials = auth.get_credentials('https', 'github.com')
40+ credentials = auth.get_credentials('https', GITHUB_HOST)
41 if credentials is not None:
42 return Github(credentials['user'], credentials['password'],
43 user_agent=user_agent)
44
45- # TODO(jelmer): token = auth.get_token('https', 'github.com')
46- token = retrieve_github_token('https', 'github.com')
47+ # TODO(jelmer): token = auth.get_token('https', GITHUB_HOST)
48 if token is not None:
49 return Github(token, user_agent=user_agent)
50 else:
51@@ -108,25 +111,25 @@
52
53 @property
54 def url(self):
55- return self._pr.html_url
56+ return self._pr['html_url']
57
58 def _branch_from_part(self, part):
59- return github_url_to_bzr_url(part.repo.html_url, part.ref)
60+ return github_url_to_bzr_url(part['repo']['html_url'], part['ref'])
61
62 def get_source_branch_url(self):
63- return self._branch_from_part(self._pr.head)
64+ return self._branch_from_part(self._pr['head'])
65
66 def get_target_branch_url(self):
67- return self._branch_from_part(self._pr.base)
68+ return self._branch_from_part(self._pr['base'])
69
70 def get_description(self):
71- return self._pr.body
72+ return self._pr['body']
73
74 def set_description(self, description):
75 self._pr.edit(body=description, title=determine_title(description))
76
77 def is_merged(self):
78- return self._pr.merged
79+ return self._pr['merged']
80
81 def close(self):
82 self._pr.edit(state='closed')
83@@ -135,7 +138,7 @@
84 def parse_github_url(url):
85 (scheme, user, password, host, port, path) = urlutils.parse_url(
86 url)
87- if host != 'github.com':
88+ if host != GITHUB_HOST:
89 raise NotGitHubUrl(url)
90 (owner, repo_name) = path.strip('/').split('/')
91 if repo_name.endswith('.git'):
92@@ -156,18 +159,6 @@
93 git_url_to_bzr_url(url), {"branch": branch_name})
94
95
96-def convert_github_error(fn):
97- def convert(self, *args, **kwargs):
98- import github
99- try:
100- return fn(self, *args, **kwargs)
101- except github.GithubException as e:
102- if e.args[0] == 401:
103- raise GitHubLoginRequired(self)
104- raise
105- return convert
106-
107-
108 class GitHub(Hoster):
109
110 name = 'github'
111@@ -177,41 +168,91 @@
112 def __repr__(self):
113 return "GitHub()"
114
115+ def _api_request(self, method, path):
116+ headers = {
117+ 'Accept': 'application/vnd.github.v3+json'}
118+ if self._token:
119+ headers['Authorization'] = 'token %s' % self._token
120+ response = self.transport.request(
121+ method, urlutils.join(self.transport.base, path),
122+ headers=headers)
123+ if response.status == 401:
124+ raise GitHubLoginRequired(self)
125+ return response
126+
127+ def _get_repo(self, path):
128+ path = 'repos/' + path
129+ response = self._api_request('GET', path)
130+ if response.status == 404:
131+ raise NoSuchProject(path)
132+ if response.status == 200:
133+ return response.json
134+ raise InvalidHttpResponse(path, response.text)
135+
136+ def _get_user(self, username=None):
137+ if username:
138+ path = 'users/:%s' % username
139+ else:
140+ path = 'user'
141+ response = self._api_request('GET', path)
142+ if response.status != 200:
143+ raise InvalidHttpResponse(path, response.text)
144+ return response.json
145+
146+ def _get_organization(self, name):
147+ path = 'orgs/:%s' % name
148+ response = self._api_request('GET', path)
149+ if response.status != 200:
150+ raise InvalidHttpResponse(path, response.text)
151+ return response.json
152+
153+ def _search_issues(self, query):
154+ path = 'search/issues'
155+ response = self._api_request(
156+ 'GET', path + '?q=' + urlutils.quote(query))
157+ if response.status != 200:
158+ raise InvalidHttpResponse(path, response.text)
159+ return response.json
160+
161+ def _create_fork(self, repo, owner=None):
162+ (orig_owner, orig_repo) = repo.split('/')
163+ path = '/repos/:%s/:%s/forks' % (orig_owner, orig_repo)
164+ if owner:
165+ path += '?organization=%s' % owner
166+ response = self._api_request('POST', path)
167+ if response != 202:
168+ raise InvalidHttpResponse(path, response.text)
169+ return response.json
170+
171 @property
172 def base_url(self):
173- # TODO(jelmer): Can we get the default URL from the Python API package
174- # somehow?
175- return "https://github.com"
176-
177- def __init__(self):
178- self.gh = connect_github()
179-
180- @convert_github_error
181+ return WEB_GITHUB_URL
182+
183+ def __init__(self, transport):
184+ self._token = retrieve_github_token('https', GITHUB_HOST)
185+ self.transport = transport
186+ self._current_user = self._get_user()
187+
188 def publish_derived(self, local_branch, base_branch, name, project=None,
189 owner=None, revision_id=None, overwrite=False,
190 allow_lossy=True):
191 import github
192 base_owner, base_project, base_branch_name = parse_github_branch_url(base_branch)
193- base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
194+ base_repo = self._get_repo('%s/%s' % (base_owner, base_project))
195 if owner is None:
196- owner = self.gh.get_user().login
197+ owner = self._current_user['login']
198 if project is None:
199- project = base_repo.name
200+ project = base_repo['name']
201 try:
202- remote_repo = self.gh.get_repo('%s/%s' % (owner, project))
203- remote_repo.id
204+ remote_repo = self._get_repo('%s/%s' % (owner, project))
205 except github.UnknownObjectException:
206- base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
207- if owner == self.gh.get_user().login:
208- owner_obj = self.gh.get_user()
209- else:
210- owner_obj = self.gh.get_organization(owner)
211- remote_repo = owner_obj.create_fork(base_repo)
212+ base_repo = self._get_repo('%s/%s' % (base_owner, base_project))
213+ remote_repo = self._create_fork(base_repo, owner)
214 note(gettext('Forking new repository %s from %s') %
215- (remote_repo.html_url, base_repo.html_url))
216+ (remote_repo['html_url'], base_repo['html_url']))
217 else:
218- note(gettext('Reusing existing repository %s') % remote_repo.html_url)
219- remote_dir = controldir.ControlDir.open(git_url_to_bzr_url(remote_repo.ssh_url))
220+ note(gettext('Reusing existing repository %s') % remote_repo['html_url'])
221+ remote_dir = controldir.ControlDir.open(git_url_to_bzr_url(remote_repo['ssh_url']))
222 try:
223 push_result = remote_dir.push_branch(
224 local_branch, revision_id=revision_id, overwrite=overwrite,
225@@ -223,41 +264,37 @@
226 local_branch, revision_id=revision_id,
227 overwrite=overwrite, name=name, lossy=True)
228 return push_result.target_branch, github_url_to_bzr_url(
229- remote_repo.html_url, name)
230+ remote_repo['html_url'], name)
231
232- @convert_github_error
233 def get_push_url(self, branch):
234 owner, project, branch_name = parse_github_branch_url(branch)
235- repo = self.gh.get_repo('%s/%s' % (owner, project))
236- return github_url_to_bzr_url(repo.ssh_url, branch_name)
237+ repo = self._get_repo('%s/%s' % (owner, project))
238+ return github_url_to_bzr_url(repo['ssh_url'], branch_name)
239
240- @convert_github_error
241 def get_derived_branch(self, base_branch, name, project=None, owner=None):
242 import github
243 base_owner, base_project, base_branch_name = parse_github_branch_url(base_branch)
244- base_repo = self.gh.get_repo('%s/%s' % (base_owner, base_project))
245+ base_repo = self._get_repo('%s/%s' % (base_owner, base_project))
246 if owner is None:
247- owner = self.gh.get_user().login
248+ owner = self._current_user['login']
249 if project is None:
250- project = base_repo.name
251+ project = base_repo['name']
252 try:
253- remote_repo = self.gh.get_repo('%s/%s' % (owner, project))
254- full_url = github_url_to_bzr_url(remote_repo.ssh_url, name)
255+ remote_repo = self._get_repo('%s/%s' % (owner, project))
256+ full_url = github_url_to_bzr_url(remote_repo['ssh_url'], name)
257 return _mod_branch.Branch.open(full_url)
258 except github.UnknownObjectException:
259- raise errors.NotBranchError('https://github.com/%s/%s' % (owner, project))
260+ raise errors.NotBranchError('%s/%s/%s' % (WEB_GITHUB_URL, owner, project))
261
262- @convert_github_error
263 def get_proposer(self, source_branch, target_branch):
264- return GitHubMergeProposalBuilder(self.gh, source_branch, target_branch)
265+ return GitHubMergeProposalBuilder(self, source_branch, target_branch)
266
267- @convert_github_error
268 def iter_proposals(self, source_branch, target_branch, status='open'):
269 (source_owner, source_repo_name, source_branch_name) = (
270 parse_github_branch_url(source_branch))
271 (target_owner, target_repo_name, target_branch_name) = (
272 parse_github_branch_url(target_branch))
273- target_repo = self.gh.get_repo(
274+ target_repo = self._get_repo(
275 "%s/%s" % (target_owner, target_repo_name))
276 state = {
277 'open': 'open',
278@@ -294,13 +331,14 @@
279 parse_github_url(url)
280 except NotGitHubUrl:
281 raise UnsupportedHoster(url)
282- return cls()
283+ transport = get_transport(
284+ API_GITHUB_URL, possible_transports=possible_transports)
285+ return cls(transport)
286
287 @classmethod
288 def iter_instances(cls):
289- yield cls()
290+ yield cls(get_transport(API_GITHUB_URL))
291
292- @convert_github_error
293 def iter_my_proposals(self, status='open'):
294 query = ['is:pr']
295 if status == 'open':
296@@ -312,9 +350,10 @@
297 query.append('is:closed')
298 elif status == 'merged':
299 query.append('is:merged')
300- query.append('author:%s' % self.gh.get_user().login)
301- for issue in self.gh.search_issues(query=' '.join(query)):
302- yield GitHubMergeProposal(issue.as_pull_request())
303+ query.append('author:%s' % self._current_user['login'])
304+ for issue in self._search_issues(query=' '.join(query))['items']:
305+ yield GitHubMergeProposal(
306+ self.transport.request('GET', issue['pull_request']['url']).json)
307
308
309 class GitHubMergeProposalBuilder(MergeProposalBuilder):
310@@ -354,7 +393,7 @@
311 # TODO(jelmer): Probe for right repo name
312 if self.target_repo_name.endswith('.git'):
313 self.target_repo_name = self.target_repo_name[:-4]
314- target_repo = self.gh.get_repo("%s/%s" % (self.target_owner, self.target_repo_name))
315+ target_repo = self.gh._get_repo("%s/%s" % (self.target_owner, self.target_repo_name))
316 # TODO(jelmer): Allow setting title explicitly?
317 title = determine_title(description)
318 # TOOD(jelmer): Set maintainers_can_modify?
319@@ -370,7 +409,7 @@
320 if reviewers:
321 for reviewer in reviewers:
322 pull_request.assignees.append(
323- self.gh.get_user(reviewer))
324+ self.gh._get_user(reviewer)['login'])
325 if labels:
326 for label in labels:
327 pull_request.issue.labels.append(label)

Subscribers

People subscribed via source and target branches