runbot/runbot_merge/github.py

206 lines
6.0 KiB
Python
Raw Normal View History

import collections
import functools
import logging
import pprint
import requests
from odoo.exceptions import UserError
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()
session.headers['Authorization'] = f'token {token}'
def __call__(self, method, path, json=None, check=True):
"""
:type check: bool | dict[int:Exception]
"""
r = self._session.request(
method,
f'{self._url}/repos/{self._repo}/{path}',
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):
d = self('get', f'git/refs/heads/{branch}').json()
assert d['ref'] == f'refs/heads/{branch}'
assert d['object']['type'] == 'commit'
return d['object']['sha']
def commit(self, sha):
return self('GET', f'git/commits/{sha}').json()
def comment(self, pr, message):
self('POST', f'issues/{pr}/comments', json={'body': message})
def close(self, pr, message):
self.comment(pr, message)
self('PATCH', f'pulls/{pr}', json={'state': 'closed'})
def fast_forward(self, branch, sha):
try:
self('patch', f'git/refs/heads/{branch}', json={'sha': sha})
except requests.HTTPError:
raise exceptions.FastForwardError()
def set_ref(self, branch, sha):
# force-update ref
r = self('patch', f'git/refs/heads/{branch}', json={
'sha': sha,
'force': True,
}, check=False)
if r.status_code == 200:
return
if r.status_code == 404:
# fallback: create ref
r = self('post', 'git/refs', json={
'ref': f'refs/heads/{branch}',
'sha': sha,
}, check=False)
if r.status_code == 201:
return
r.raise_for_status()
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:
response = self._session.post(f'{self._url}/graphql', json={
'query': PR_QUERY,
'variables': {
'owner': owner,
'name': name,
'cursor': cursor,
}
}).json()
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')
label = source and f"{source}:{pr['headRefName']}"
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
# }
#}
}
}
}
}
"""