2018-03-14 16:37:46 +07:00
|
|
|
import collections
|
|
|
|
import functools
|
|
|
|
import logging
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
2018-06-19 16:48:33 +07:00
|
|
|
from odoo.exceptions import UserError
|
2018-03-14 16:37:46 +07:00
|
|
|
from . import exceptions
|
|
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
class GH(object):
|
|
|
|
def __init__(self, token, repo):
|
|
|
|
self._url = 'https://api.github.com'
|
|
|
|
self._repo = repo
|
|
|
|
session = self._session = requests.Session()
|
2018-03-26 18:08:49 +07:00
|
|
|
session.headers['Authorization'] = 'token {}'.format(token)
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
def __call__(self, method, path, json=None, check=True):
|
|
|
|
"""
|
|
|
|
:type check: bool | dict[int:Exception]
|
|
|
|
"""
|
|
|
|
r = self._session.request(
|
|
|
|
method,
|
2018-03-26 18:08:49 +07:00
|
|
|
'{}/repos/{}/{}'.format(self._url, self._repo, path),
|
2018-03-14 16:37:46 +07:00
|
|
|
json=json
|
|
|
|
)
|
|
|
|
if check:
|
|
|
|
if isinstance(check, collections.Mapping):
|
|
|
|
exc = check.get(r.status_code)
|
|
|
|
if exc:
|
|
|
|
raise exc(r.content)
|
|
|
|
r.raise_for_status()
|
|
|
|
return r
|
|
|
|
|
|
|
|
def head(self, branch):
|
2018-03-26 18:08:49 +07:00
|
|
|
d = self('get', 'git/refs/heads/{}'.format(branch)).json()
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2018-03-26 18:08:49 +07:00
|
|
|
assert d['ref'] == 'refs/heads/{}'.format(branch)
|
2018-03-14 16:37:46 +07:00
|
|
|
assert d['object']['type'] == 'commit'
|
|
|
|
return d['object']['sha']
|
|
|
|
|
|
|
|
def commit(self, sha):
|
2018-03-26 18:08:49 +07:00
|
|
|
return self('GET', 'git/commits/{}'.format(sha)).json()
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
def comment(self, pr, message):
|
2018-03-26 18:08:49 +07:00
|
|
|
self('POST', 'issues/{}/comments'.format(pr), json={'body': message})
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
def close(self, pr, message):
|
|
|
|
self.comment(pr, message)
|
2018-03-26 18:08:49 +07:00
|
|
|
self('PATCH', 'pulls/{}'.format(pr), json={'state': 'closed'})
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2018-03-28 21:43:48 +07:00
|
|
|
def change_tags(self, pr, from_, to_):
|
|
|
|
to_add, to_remove = to_ - from_, from_ - to_
|
|
|
|
for t in to_remove:
|
|
|
|
r = self('DELETE', 'issues/{}/labels/{}'.format(pr, t), check=False)
|
|
|
|
r.raise_for_status()
|
|
|
|
# successful deletion or attempt to delete a tag which isn't there
|
|
|
|
# is fine, otherwise trigger an error
|
|
|
|
if r.status_code not in (200, 404):
|
|
|
|
r.raise_for_status()
|
|
|
|
|
|
|
|
if to_add:
|
|
|
|
self('POST', 'issues/{}/labels'.format(pr), json=list(to_add))
|
|
|
|
|
2018-03-14 16:37:46 +07:00
|
|
|
def fast_forward(self, branch, sha):
|
|
|
|
try:
|
2018-03-26 18:08:49 +07:00
|
|
|
self('patch', 'git/refs/heads/{}'.format(branch), json={'sha': sha})
|
2018-03-14 16:37:46 +07:00
|
|
|
except requests.HTTPError:
|
|
|
|
raise exceptions.FastForwardError()
|
|
|
|
|
|
|
|
def set_ref(self, branch, sha):
|
|
|
|
# force-update ref
|
2018-03-26 18:08:49 +07:00
|
|
|
r = self('patch', 'git/refs/heads/{}'.format(branch), json={
|
2018-03-14 16:37:46 +07:00
|
|
|
'sha': sha,
|
|
|
|
'force': True,
|
|
|
|
}, check=False)
|
|
|
|
if r.status_code == 200:
|
|
|
|
return
|
|
|
|
|
2018-06-07 19:44:44 +07:00
|
|
|
# 422 makes no sense but that's what github returns, leaving 404 just
|
|
|
|
# in case
|
|
|
|
if r.status_code in (404, 422):
|
2018-03-14 16:37:46 +07:00
|
|
|
# fallback: create ref
|
|
|
|
r = self('post', 'git/refs', json={
|
2018-03-26 18:08:49 +07:00
|
|
|
'ref': 'refs/heads/{}'.format(branch),
|
2018-03-14 16:37:46 +07:00
|
|
|
'sha': sha,
|
|
|
|
}, check=False)
|
|
|
|
if r.status_code == 201:
|
|
|
|
return
|
2018-06-07 19:44:44 +07:00
|
|
|
raise AssertionError("{}: {}".format(r.status_code, r.json()))
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
def merge(self, sha, dest, message, squash=False, author=None):
|
|
|
|
if not squash:
|
|
|
|
r = self('post', 'merges', json={
|
|
|
|
'base': dest,
|
|
|
|
'head': sha,
|
|
|
|
'commit_message': message,
|
|
|
|
}, check={409: exceptions.MergeError})
|
|
|
|
r = r.json()
|
|
|
|
return dict(r['commit'], sha=r['sha'])
|
|
|
|
|
|
|
|
current_head = self.head(dest)
|
|
|
|
tree = self.merge(sha, dest, "temp")['tree']['sha']
|
|
|
|
c = self('post', 'git/commits', json={
|
|
|
|
'message': message,
|
|
|
|
'tree': tree,
|
|
|
|
'parents': [current_head],
|
|
|
|
'author': author,
|
|
|
|
}, check={409: exceptions.MergeError}).json()
|
|
|
|
self.set_ref(dest, c['sha'])
|
|
|
|
return c
|
|
|
|
|
|
|
|
# --
|
|
|
|
|
|
|
|
def prs(self):
|
|
|
|
cursor = None
|
|
|
|
owner, name = self._repo.split('/')
|
|
|
|
while True:
|
2018-06-19 16:48:33 +07:00
|
|
|
r = self._session.post('{}/graphql'.format(self._url), json={
|
2018-03-14 16:37:46 +07:00
|
|
|
'query': PR_QUERY,
|
|
|
|
'variables': {
|
|
|
|
'owner': owner,
|
|
|
|
'name': name,
|
|
|
|
'cursor': cursor,
|
|
|
|
}
|
2018-06-19 16:48:33 +07:00
|
|
|
})
|
|
|
|
response = r.json()
|
|
|
|
if 'data' not in response:
|
|
|
|
raise UserError('\n'.join(e['message'] for e in response.get('errors', map(str, [r.status_code, r.reason, r.text]))))
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
result = response['data']['repository']['pullRequests']
|
|
|
|
for pr in result['nodes']:
|
|
|
|
statuses = into(pr, 'headRef.target.status.contexts') or []
|
|
|
|
|
|
|
|
author = into(pr, 'author.login') or into(pr, 'headRepositoryOwner.login')
|
|
|
|
source = into(pr, 'headRepositoryOwner.login') or into(pr, 'author.login')
|
2018-03-26 18:08:49 +07:00
|
|
|
label = source and "{}:{}".format(source, pr['headRefName'])
|
2018-03-14 16:37:46 +07:00
|
|
|
yield {
|
|
|
|
'number': pr['number'],
|
|
|
|
'title': pr['title'],
|
|
|
|
'body': pr['body'],
|
|
|
|
'head': {
|
|
|
|
'ref': pr['headRefName'],
|
|
|
|
'sha': pr['headRefOid'],
|
|
|
|
# headRef may be null if the pr branch was ?deleted?
|
|
|
|
# (mostly closed PR concerns?)
|
|
|
|
'statuses': {
|
|
|
|
c['context']: c['state']
|
|
|
|
for c in statuses
|
|
|
|
},
|
|
|
|
'label': label,
|
|
|
|
},
|
|
|
|
'state': pr['state'].lower(),
|
|
|
|
'user': {'login': author},
|
|
|
|
'base': {
|
|
|
|
'ref': pr['baseRefName'],
|
|
|
|
'repo': {
|
|
|
|
'full_name': pr['repository']['nameWithOwner'],
|
|
|
|
}
|
|
|
|
},
|
|
|
|
'commits': pr['commits']['totalCount'],
|
|
|
|
}
|
|
|
|
|
|
|
|
if result['pageInfo']['hasPreviousPage']:
|
|
|
|
cursor = result['pageInfo']['startCursor']
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
def into(d, path):
|
|
|
|
return functools.reduce(
|
|
|
|
lambda v, segment: v and v.get(segment),
|
|
|
|
path.split('.'),
|
|
|
|
d
|
|
|
|
)
|
|
|
|
|
|
|
|
PR_QUERY = """
|
|
|
|
query($owner: String!, $name: String!, $cursor: String) {
|
|
|
|
rateLimit { remaining }
|
|
|
|
repository(owner: $owner, name: $name) {
|
|
|
|
pullRequests(last: 100, before: $cursor) {
|
|
|
|
pageInfo { startCursor hasPreviousPage }
|
|
|
|
nodes {
|
|
|
|
author { # optional
|
|
|
|
login
|
|
|
|
}
|
|
|
|
number
|
|
|
|
title
|
|
|
|
body
|
|
|
|
state
|
|
|
|
repository { nameWithOwner }
|
|
|
|
baseRefName
|
|
|
|
headRefOid
|
|
|
|
headRepositoryOwner { # optional
|
|
|
|
login
|
|
|
|
}
|
|
|
|
headRefName
|
|
|
|
headRef { # optional
|
|
|
|
target {
|
|
|
|
... on Commit {
|
|
|
|
status {
|
|
|
|
contexts {
|
|
|
|
context
|
|
|
|
state
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
commits { totalCount }
|
|
|
|
#comments(last: 100) {
|
|
|
|
# nodes {
|
|
|
|
# author {
|
|
|
|
# login
|
|
|
|
# }
|
|
|
|
# body
|
|
|
|
# bodyText
|
|
|
|
# }
|
|
|
|
#}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"""
|