Files
brave-core/script/lib/github.py
T
Brian Clifton 15ddd25be0 Script now considers npm config when looking for GitHub token
Also, each PR created is opened in the default browser
2019-02-13 11:09:16 -07:00

280 lines
9.8 KiB
Python

#!/usr/bin/env python
import json
import os
import re
import requests
import sys
import base64
from util import execute, scoped_cwd
REQUESTS_DIR = os.path.abspath(os.path.join(__file__, '..', '..', '..',
'vendor', 'requests'))
sys.path.append(os.path.join(REQUESTS_DIR, 'build', 'lib'))
sys.path.append(os.path.join(REQUESTS_DIR, 'build', 'lib.linux-x86_64-2.7'))
GITHUB_URL = 'https://api.github.com'
GITHUB_UPLOAD_ASSET_URL = 'https://uploads.github.com'
class GitHub:
def __init__(self, access_token):
self._authorization = 'token %s' % access_token
pattern = '^/repos/{0}/{0}/releases/{1}/assets$'.format(
'[^/]+', '[0-9]+')
self._releases_upload_api_pattern = re.compile(pattern)
def __getattr__(self, attr):
return _Callable(self, '/%s' % attr)
def send(self, method, path, **kw):
if 'headers' not in kw:
kw['headers'] = dict()
headers = kw['headers']
headers['Authorization'] = self._authorization
headers['Accept'] = 'application/vnd.github.manifold-preview'
# Switch to a different domain for the releases uploading API.
if self._releases_upload_api_pattern.match(path):
url = '%s%s' % (GITHUB_UPLOAD_ASSET_URL, path)
else:
url = '%s%s' % (GITHUB_URL, path)
# Data are sent in JSON format.
if 'data' in kw:
kw['data'] = json.dumps(kw['data'])
try:
r = getattr(requests, method)(url, **kw).json()
except ValueError:
# Returned response may be empty in some cases
r = {}
if 'message' in r:
raise Exception(json.dumps(r, indent=2, separators=(',', ': ')))
return r
class _Executable:
def __init__(self, gh, method, path):
self._gh = gh
self._method = method
self._path = path
def __call__(self, **kw):
return self._gh.send(self._method, self._path, **kw)
class _Callable(object):
def __init__(self, gh, name):
self._gh = gh
self._name = name
def __call__(self, *args):
if len(args) == 0:
return self
name = '%s/%s' % (self._name, '/'.join([str(arg) for arg in args]))
return _Callable(self._gh, name)
def __getattr__(self, attr):
if attr in ['get', 'put', 'post', 'patch', 'delete']:
return _Executable(self._gh, attr, self._name)
name = '%s/%s' % (self._name, attr)
return _Callable(self._gh, name)
def get_authenticated_user_login(token):
"""given a valid GitHub access token, return the associated GitHub user login"""
# for more info see: https://developer.github.com/v3/users/#get-the-authenticated-user
user = GitHub(token).user()
try:
response = user.get()
return response['login']
except Exception as e:
print('[ERROR] ' + str(e))
def parse_user_logins(token, login_csv, verbose=False):
"""given a list of logins in csv format, parse into a list and validate logins"""
if login_csv is None:
return []
login_csv = login_csv.replace(" ", "")
parsed_logins = login_csv.split(',')
users = GitHub(token).users()
invalid_logins = []
# check login/username against GitHub
# for more info see: https://developer.github.com/v3/users/#get-a-single-user
for login in parsed_logins:
try:
response = users(login).get()
if verbose:
print('[INFO] Login "' + login + '" found: ' + str(response))
except Exception as e:
if verbose:
print('[INFO] Login "' + login + '" does not appear to be valid. ' + str(e))
invalid_logins.append(login)
if len(invalid_logins) > 0:
raise Exception('Invalid logins found. Are they misspelled? ' + ','.join(invalid_logins))
return parsed_logins
def parse_labels(token, repo_name, label_csv, verbose=False):
global config
if label_csv is None:
return []
label_csv = label_csv.replace(" ", "")
parsed_labels = label_csv.split(',')
invalid_labels = []
# validate labels passed in are correct
# for more info see: https://developer.github.com/v3/issues/labels/#get-a-single-label
repo = GitHub(token).repos(repo_name)
for label in parsed_labels:
try:
response = repo.labels(label).get()
if verbose:
print('[INFO] Label "' + label + '" found: ' + str(response))
except Exception as e:
if verbose:
print('[INFO] Label "' + label + '" does not appear to be valid. ' + str(e))
invalid_labels.append(label)
if len(invalid_labels) > 0:
raise Exception('Invalid labels found. Are they misspelled? ' + ','.join(invalid_labels))
return parsed_labels
def get_file_contents(token, repo_name, filename, branch=None):
# NOTE: API only supports files up to 1MB in size
# for more info see: https://developer.github.com/v3/repos/contents/
repo = GitHub(token).repos(repo_name)
get_data = {}
if branch:
get_data['ref'] = branch
file = repo.contents(filename).get(params=get_data)
if file['encoding'] == 'base64':
return base64.b64decode(file['content'])
return file['content']
def add_reviewers_to_pull_request(token, repo_name, pr_number, reviewers=[], verbose=False, dryrun=False):
# add reviewers to pull request
# for more info see: https://developer.github.com/v3/pulls/review_requests/
repo = GitHub(token).repos(repo_name)
patch_data = {}
if len(reviewers) > 0:
patch_data['reviewers'] = reviewers
if dryrun:
print('[INFO] would call `repo.pulls(' + str(pr_number) +
').requested_reviewers.post(' + str(patch_data) + ')`')
return
response = repo.pulls(pr_number).requested_reviewers.post(data=patch_data)
if verbose:
print('repo.pulls(' + str(pr_number) + ').requested_reviewers.post(data) response:\n' + str(response))
return response
def get_milestones(token, repo_name, verbose=False):
# get all milestones for a repo
# for more info see: https://developer.github.com/v3/issues/milestones/
repo = GitHub(token).repos(repo_name)
response = repo.milestones.get()
if verbose:
print('repo.milestones.get() response:\n' + str(response))
return response
def create_pull_request(token, repo_name, title, body, branch_src, branch_dst, open_in_browser=False, verbose=False, dryrun=False):
post_data = {
'title': title,
'head': branch_src,
'base': branch_dst,
'body': body,
'maintainer_can_modify': True
}
# create the pull request
# for more info see: http://developer.github.com/v3/pulls
if dryrun:
print('[INFO] would call `repo.pulls.post(' + str(post_data) + ')`')
if open_in_browser:
print('[INFO] would open PR in web browser')
return 1234
repo = GitHub(token).repos(repo_name)
response = repo.pulls.post(data=post_data)
if verbose:
print('repo.pulls.post(data) response:\n' + str(response))
if open_in_browser:
import webbrowser
webbrowser.open(response['html_url'])
return int(response['number'])
def set_issue_details(token, repo_name, issue_number, milestone_number=None,
assignees=[], labels=[], verbose=False, dryrun=False):
patch_data = {}
if milestone_number:
patch_data['milestone'] = milestone_number
if len(assignees) > 0:
patch_data['assignees'] = assignees
if len(labels) > 0:
patch_data['labels'] = labels
# TODO: error if no keys in patch_data
# add milestone and assignee to issue / pull request
# for more info see: https://developer.github.com/v3/issues/#edit-an-issue
if dryrun:
print('[INFO] would call `repo.issues(' + str(issue_number) + ').patch(' + str(patch_data) + ')`')
return
repo = GitHub(token).repos(repo_name)
response = repo.issues(issue_number).patch(data=patch_data)
if verbose:
print('repo.issues(' + str(issue_number) + ').patch(data) response:\n' + str(response))
def fetch_origin_check_staged(path):
"""given a path on disk (to a git repo), fetch origin and ensure there aren't unstaged files"""
with scoped_cwd(path):
execute(['git', 'fetch', 'origin'])
status = execute(['git', 'status', '-s']).strip()
if len(status) > 0:
print('[ERROR] There appear to be unstaged changes.\n' +
'Please resolve these before running (ex: `git status`).')
return 1
return 0
def get_local_branch_name(path):
with scoped_cwd(path):
return execute(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
def get_title_from_first_commit(path, branch_to_compare):
"""get the first commit subject (useful for the title of a pull request)"""
with scoped_cwd(path):
local_branch = execute(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
title_list = execute(['git', 'log', 'origin/' + branch_to_compare +
'..HEAD', '--pretty=format:%s', '--reverse'])
title_list = title_list.split('\n')
if len(title_list) == 0:
raise Exception('No commits found! Local branch matches "' + branch_to_compare + '"')
return title_list[0]
def push_branches_to_remote(path, branches_to_push, dryrun=False):
if dryrun:
print('[INFO] would push the following local branches to remote: ' + str(branches_to_push))
else:
with scoped_cwd(path):
for branch_to_push in branches_to_push:
print('- pushing ' + branch_to_push + '...')
# TODO: if they already exist, force push?? or error??
execute(['git', 'push', '-u', 'origin', branch_to_push])