diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..474e735a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +sudo: false +language: python +python: + - 3.3 + - 3.4 + - 3.5 +install: + - pip install flake8 +script: + - flake8 --ignore E501 homu +notifications: + webhooks: http://build.servo.org:54856/travis diff --git a/cfg.sample.toml b/cfg.sample.toml index 43fc1cd6..47a6f15d 100644 --- a/cfg.sample.toml +++ b/cfg.sample.toml @@ -75,7 +75,7 @@ secret = "" #token = "" ## Use the Status API -#[repo.NAME.status] +#[repo.NAME.status.LABEL] # ## String label set by status updates #context = "" diff --git a/homu/git_helper.py b/homu/git_helper.py index 4bc2f92c..2d44340c 100755 --- a/homu/git_helper.py +++ b/homu/git_helper.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 import sys -import subprocess import os SSH_KEY_FILE = os.path.join(os.path.dirname(__file__), '../cache/key') + def main(): args = ['ssh', '-i', SSH_KEY_FILE, '-S', 'none'] + sys.argv[1:] os.execvp('ssh', args) diff --git a/homu/main.py b/homu/main.py index a0f0f703..38d048d6 100644 --- a/homu/main.py +++ b/homu/main.py @@ -17,7 +17,6 @@ import subprocess from .git_helper import SSH_KEY_FILE import shlex -import sys STATUS_TO_PRIORITY = { 'success': 0, @@ -30,9 +29,9 @@ INTERRUPTED_BY_HOMU_FMT = 'Interrupted by Homu ({})' INTERRUPTED_BY_HOMU_RE = re.compile(r'Interrupted by Homu \((.+?)\)') - TEST_TIMEOUT = 3600 * 10 + @contextmanager def buildbot_sess(repo_cfg): sess = requests.Session() @@ -47,10 +46,13 @@ def buildbot_sess(repo_cfg): sess.get(repo_cfg['buildbot']['url'] + '/logout', allow_redirects=False) db_query_lock = Lock() + + def db_query(db, *args): with db_query_lock: db.execute(*args) + class PullReqState: num = 0 priority = 0 @@ -75,7 +77,7 @@ def __init__(self, num, head_sha, status, db, repo_label, mergeable_que, gh, own self.owner = owner self.name = name self.repos = repos - self.test_started = time.time() # FIXME: Save in the local database + self.test_started = time.time() # FIXME: Save in the local database def head_advanced(self, head_sha, *, use_db=True): self.head_sha = head_sha @@ -239,17 +241,20 @@ def fail(err): utils.retry_until(inner, fail, self) + def sha_cmp(short, full): return len(short) >= 4 and short == full[:len(short)] + def sha_or_blank(sha): return sha if re.match(r'^[0-9a-f]+$', sha) else '' + def parse_commands(body, username, repo_cfg, state, my_username, db, states, *, realtime=False, sha=''): try_only = False if username not in repo_cfg['reviewers'] and username != my_username: if username.lower() == state.delegate.lower(): - pass # Allow users who have been delegated review powers + pass # Allow users who have been delegated review powers elif username in repo_cfg.get('try_users', []): try_only = True else: @@ -263,11 +268,12 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states, *, if word == 'r+' or word.startswith('r='): if try_only: - if realtime: state.add_comment(':key: Insufficient privileges') + if realtime: + state.add_comment(':key: Insufficient privileges') continue - if not sha and i+1 < len(words): - cur_sha = sha_or_blank(words[i+1]) + if not sha and i + 1 < len(words): + cur_sha = sha_or_blank(words[i + 1]) else: cur_sha = sha @@ -282,7 +288,8 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states, *, # condition. Last time, it happened when squashing commits in a PR. In this case, we # just try to retrieve the head SHA manually. if all(x == '0' for x in state.head_sha): - if realtime: state.add_comment(':bangbang: Invalid head SHA found, retrying: `{}`'.format(state.head_sha)) + if realtime: + state.add_comment(':bangbang: Invalid head SHA found, retrying: `{}`'.format(state.head_sha)) state.head_sha = state.get_repo().pull_request(state.num).head.sha state.save() @@ -307,7 +314,8 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states, *, else: lines.append('- There\'s another pull request that is currently being tested, blocking this pull request: #{}'.format(_state.num)) - if lines: lines.insert(0, '') + if lines: + lines.insert(0, '') lines.insert(0, ':bulb: This pull request was already approved, no need to approve it again.') state.add_comment('\n'.join(lines)) @@ -325,7 +333,8 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states, *, elif word == 'r-': if try_only: - if realtime: state.add_comment(':key: Insufficient privileges') + if realtime: + state.add_comment(':key: Insufficient privileges') continue state.approved_by = '' @@ -333,20 +342,24 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states, *, state.save() elif word.startswith('p='): - try: state.priority = int(word[len('p='):]) - except ValueError: pass + try: + state.priority = int(word[len('p='):]) + except ValueError: + pass state.save() elif word.startswith('delegate='): if try_only: - if realtime: state.add_comment(':key: Insufficient privileges') + if realtime: + state.add_comment(':key: Insufficient privileges') continue state.delegate = word[len('delegate='):] state.save() - if realtime: state.add_comment(':v: @{} can now approve this pull request'.format(state.delegate)) + if realtime: + state.add_comment(':v: @{} can now approve this pull request'.format(state.delegate)) elif word == 'delegate-': state.delegate = '' @@ -354,13 +367,15 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states, *, elif word == 'delegate+': if try_only: - if realtime: state.add_comment(':key: Insufficient privileges') + if realtime: + state.add_comment(':key: Insufficient privileges') continue state.delegate = state.get_repo().pull_request(state.num).user.login state.save() - if realtime: state.add_comment(':v: @{} can now approve this pull request'.format(state.delegate)) + if realtime: + state.add_comment(':v: @{} can now approve this pull request'.format(state.delegate)) elif word == 'retry' and realtime: state.set_status('') @@ -391,7 +406,8 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states, *, mat = re.search('(?s)
(.*?)
', res.text) if mat: err = mat.group(1).strip() - if not err: err = 'Unknown error' + if not err: + err = 'Unknown error' else: err = '' @@ -414,6 +430,7 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states, *, return state_changed + def git_push(fpath, branch, state): merge_sha = subprocess.check_output(['git', '-C', fpath, 'rev-parse', 'HEAD']).decode('ascii').strip() @@ -433,6 +450,7 @@ def fail(err): return merge_sha + def create_merge(state, repo_cfg, branch, git_cfg): base_sha = state.get_repo().ref('heads/' + state.base_ref).object.sha @@ -449,7 +467,7 @@ def create_merge(state, repo_cfg, branch, git_cfg): desc = 'Merge conflict' if git_cfg['local_git']: - pull = state.get_repo().pull_request(state.num) + state.get_repo().pull_request(state.num) fpath = 'cache/{}/{}'.format(repo_cfg['owner'], repo_cfg['name']) url = 'git@github.com:{}/{}.git'.format(repo_cfg['owner'], repo_cfg['name']) @@ -516,9 +534,11 @@ def create_merge(state, repo_cfg, branch, git_cfg): force=True, ) - try: merge_commit = state.get_repo().merge(branch, state.head_sha, merge_msg) + try: + merge_commit = state.get_repo().merge(branch, state.head_sha, merge_msg) except github3.models.GitHubError as e: - if e.code != 409: raise + if e.code != 409: + raise else: return merge_commit.sha if merge_commit else '' @@ -529,6 +549,7 @@ def create_merge(state, repo_cfg, branch, git_cfg): return '' + def start_build(state, repo_cfgs, buildbot_slots, logger, db, git_cfg): if buildbot_slots[0]: return True @@ -537,20 +558,21 @@ def start_build(state, repo_cfgs, buildbot_slots, logger, db, git_cfg): repo_cfg = repo_cfgs[state.repo_label] + builders = [] if 'buildbot' in repo_cfg: branch = 'try' if state.try_ else 'auto' branch = repo_cfg.get('branch', {}).get(branch, branch) - builders = repo_cfg['buildbot']['try_builders' if state.try_ else 'builders'] - elif 'travis' in repo_cfg: + builders += repo_cfg['buildbot']['try_builders' if state.try_ else 'builders'] + if 'travis' in repo_cfg: branch = repo_cfg.get('branch', {}).get('auto', 'auto') - builders = ['travis'] - elif 'status' in repo_cfg: + builders += ['travis'] + if 'status' in repo_cfg: branch = repo_cfg.get('branch', {}).get('auto', 'auto') - builders = ['status'] - else: + builders += ['status-' + key for key, value in repo_cfg['status'].items() if 'context' in value] + if len(builders) is 0: raise RuntimeError('Invalid configuration') - if state.approved_by and builders == ['status'] and repo_cfg['status']['context'] == 'continuous-integration/travis-ci/push': + if state.approved_by and len(builders) == 1 and 'status' in repo_cfg and len(repo_cfg['status']) == 1 and 'context' in repo_cfg['status'][0] and repo_cfg['status'][0]['context'] == 'continuous-integration/travis-ci/push': for info in utils.github_iter_statuses(state.get_repo(), state.head_sha): if info.context == 'continuous-integration/travis-ci/pr': if info.state == 'success': @@ -563,7 +585,8 @@ def start_build(state, repo_cfgs, buildbot_slots, logger, db, git_cfg): if travis_commit: base_sha = state.get_repo().ref('heads/' + state.base_ref).object.sha if [travis_commit.parents[0]['sha'], travis_commit.parents[1]['sha']] == [base_sha, state.head_sha]: - try: merge_sha = create_merge(state, repo_cfg, state.base_ref, git_cfg) + try: + merge_sha = create_merge(state, repo_cfg, state.base_ref, git_cfg) except subprocess.CalledProcessError: print('* Unable to create a merge commit for the exempted PR: {}'.format(state)) traceback.print_exc() @@ -608,6 +631,7 @@ def start_build(state, repo_cfgs, buildbot_slots, logger, db, git_cfg): return True + def start_rebuild(state, repo_cfgs): repo_cfg = repo_cfgs[state.repo_label] @@ -673,12 +697,14 @@ def start_rebuild(state, repo_cfgs): return True + def start_build_or_rebuild(state, repo_cfgs, *args): if start_rebuild(state, repo_cfgs): return True return start_build(state, repo_cfgs, *args) + def process_queue(states, repos, repo_cfgs, logger, buildbot_slots, db, git_cfg): for repo_label, repo in repos.items(): repo_states = sorted(states[repo_label].values()) @@ -707,6 +733,7 @@ def process_queue(states, repos, repo_cfgs, logger, buildbot_slots, db, git_cfg) if start_build(state, repo_cfgs, buildbot_slots, logger, db, git_cfg): return + def fetch_mergeability(mergeable_que): re_pull_num = re.compile('(?i)merge (?:of|pull request) #([0-9]+)') @@ -726,8 +753,10 @@ def fetch_mergeability(mergeable_que): if cause: mat = re_pull_num.search(cause['title']) - if mat: issue_or_commit = '#' + mat.group(1) - else: issue_or_commit = cause['sha'][:7] + if mat: + issue_or_commit = '#' + mat.group(1) + else: + issue_or_commit = cause['sha'][:7] else: issue_or_commit = '' @@ -744,6 +773,7 @@ def fetch_mergeability(mergeable_que): finally: mergeable_que.task_done() + def check_timeout(states, queue_handler): while True: try: @@ -769,6 +799,7 @@ def check_timeout(states, queue_handler): finally: time.sleep(3600) + def synchronize(repo_label, repo_cfg, logger, gh, states, repos, db, mergeable_que, my_username, repo_labels): logger.info('Synchronizing {}...'.format(repo_label)) @@ -843,15 +874,17 @@ def synchronize(repo_label, repo_cfg, logger, gh, states, repos, db, mergeable_q logger.info('Done synchronizing {}!'.format(repo_label)) + def arguments(): - parser = argparse.ArgumentParser(description = - 'A bot that integrates with GitHub and ' - 'your favorite continuous integration service') + parser = argparse.ArgumentParser( + description='A bot that integrates with GitHub and your favorite ' + 'continuous integration service') parser.add_argument('-v', '--verbose', action='store_true', help='Enable more verbose logging') return parser.parse_args() + def main(): args = arguments() @@ -868,7 +901,8 @@ def main(): gh = github3.login(token=cfg['github']['access_token']) user = gh.user() - try: user_email = [x for x in gh.iter_emails() if x['primary']][0]['email'] + try: + user_email = [x for x in gh.iter_emails() if x['primary']][0]['email'] except IndexError: raise RuntimeError('Primary email not set, or "user" scope not granted') @@ -946,15 +980,15 @@ def main(): state.try_ = bool(try_) state.rollup = bool(rollup) state.delegate = delegate - + builders = [] if merge_sha: if 'buildbot' in repo_cfg: - builders = repo_cfg['buildbot']['builders'] - elif 'travis' in repo_cfg: - builders = ['travis'] - elif 'status' in repo_cfg: - builders = ['status'] - else: + builders += repo_cfg['buildbot']['builders'] + if 'travis' in repo_cfg: + builders += ['travis'] + if 'status' in repo_cfg: + builders += ['status-' + key for key, value in repo_cfg['status'].items() if 'context' in value] + if len(builders) is 0: raise RuntimeError('Invalid configuration') state.init_build_res(builders, use_db=False) @@ -974,8 +1008,10 @@ def main(): for repo_label, num, builder, res, url, merge_sha in db.fetchall(): try: state = states[repo_label][num] - if builder not in state.build_res: raise KeyError - if state.merge_sha != merge_sha: raise KeyError + if builder not in state.build_res: + raise KeyError + if state.merge_sha != merge_sha: + raise KeyError except KeyError: db_query(db, 'DELETE FROM build_res WHERE repo = ? AND num = ? AND builder = ?', [repo_label, num, builder]) continue @@ -987,7 +1023,8 @@ def main(): db_query(db, 'SELECT repo, num, mergeable FROM mergeable') for repo_label, num, mergeable in db.fetchall(): - try: state = states[repo_label][num] + try: + state = states[repo_label][num] except KeyError: db_query(db, 'DELETE FROM mergeable WHERE repo = ? AND num = ?', [repo_label, num]) continue @@ -1000,6 +1037,7 @@ def main(): db_query(db, 'DELETE FROM pull WHERE repo = ?', [repo_label]) queue_handler_lock = Lock() + def queue_handler(): with queue_handler_lock: return process_queue(states, repos, repo_cfgs, logger, buildbot_slots, db, git_cfg) diff --git a/homu/server.py b/homu/server.py index 25f06fa3..1cf5665d 100644 --- a/homu/server.py +++ b/homu/server.py @@ -11,16 +11,19 @@ from bottle import get, post, run, request, redirect, abort, response import hashlib from threading import Thread -import time import sys import os import traceback -import bottle; bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 * 10 +import bottle +bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 * 10 -class G: pass + +class G: + pass g = G() + def find_state(sha): for repo_label, repo_states in g.states.items(): for state in repo_states.values(): @@ -29,6 +32,7 @@ def find_state(sha): raise ValueError('Invalid SHA') + def get_repo(repo_label, repo_cfg): repo = g.repos[repo_label] if not repo: @@ -38,10 +42,12 @@ def get_repo(repo_label, repo_cfg): assert repo.name == repo_cfg['name'] return repo + @get('/') def index(): return g.tpls['index'].render(repos=sorted(g.repos)) + @get('/queue/') def queue(repo_label): logger = g.logger.getChild('queue') @@ -57,7 +63,8 @@ def queue(repo_label): states = [] for label in labels: - try: states += g.states[label].values() + try: + states += g.states[label].values() except KeyError: abort(404, 'No such repository: {}'.format(label)) @@ -81,16 +88,17 @@ def queue(repo_label): }) return g.tpls['queue'].render( - repo_label = repo_label, - states = rows, - oauth_client_id = g.cfg['github']['app_client_id'], - total = len(pull_states), - approved = len([x for x in pull_states if x.approved_by]), - rolled_up = len([x for x in pull_states if x.rollup]), - failed = len([x for x in pull_states if x.status == 'failure' or x.status == 'error']), - multiple = multiple, + repo_label=repo_label, + states=rows, + oauth_client_id=g.cfg['github']['app_client_id'], + total=len(pull_states), + approved=len([x for x in pull_states if x.approved_by]), + rolled_up=len([x for x in pull_states if x.rollup]), + failed=len([x for x in pull_states if x.status == 'failure' or x.status == 'error']), + multiple=multiple, ) + @get('/callback') def callback(): logger = g.logger.getChild('callback') @@ -123,14 +131,17 @@ def callback(): else: abort(400, 'Invalid command') + def rollup(user_gh, state, repo_label, repo_cfg, repo): user_repo = user_gh.repository(user_gh.user().login, repo.name) base_repo = user_gh.repository(repo.owner.login, repo.name) nums = state.get('nums', []) if nums: - try: rollup_states = [g.states[repo_label][num] for num in nums] - except KeyError as e: return 'Invalid PR number: {}'.format(e.args[0]) + try: + rollup_states = [g.states[repo_label][num] for num in nums] + except KeyError as e: + return 'Invalid PR number: {}'.format(e.args[0]) else: rollup_states = [x for x in g.states[repo_label].values() if x.rollup] rollup_states = [x for x in rollup_states if x.approved_by] @@ -165,9 +176,11 @@ def rollup(user_gh, state, repo_label, repo_cfg, repo): state.body, ) - try: user_repo.merge(repo_cfg.get('branch', {}).get('rollup', 'rollup'), state.head_sha, merge_msg) + try: + user_repo.merge(repo_cfg.get('branch', {}).get('rollup', 'rollup'), state.head_sha, merge_msg) except github3.models.GitHubError as e: - if e.code != 409: raise + if e.code != 409: + raise failures.append(state.num) else: @@ -191,6 +204,7 @@ def rollup(user_gh, state, repo_label, repo_cfg, repo): else: redirect(pull.html_url) + @post('/github') def github(): logger = g.logger.getChild('github') @@ -371,14 +385,16 @@ def fail(err): g.queue_handler() elif event_type == 'status': - try: state, repo_label = find_state(info['sha']) + try: + state, repo_label = find_state(info['sha']) except ValueError: return 'OK' - if 'status' not in state.build_res: - return 'OK' - - if info['context'] != repo_cfg['status']['context']: + status_name = "" + for name, value in repo_cfg['status'].items(): + if 'context' in value and value['context'] == info['context']: + status_name = name + if status_name is "": return 'OK' if info['state'] == 'pending': @@ -388,14 +404,15 @@ def fail(err): if row['name'] == state.base_ref: return 'OK' - report_build_res(info['state'] == 'success', info['target_url'], 'status', state, logger, repo_cfg) + report_build_res(info['state'] == 'success', info['target_url'], 'status-' + status_name, state, logger, repo_cfg) return 'OK' + def report_build_res(succ, url, builder, state, logger, repo_cfg): lazy_debug(logger, lambda: 'build result {}: builder = {}, succ = {}, current build_res = {}' - .format(state, builder, succ, state.build_res_summary())) + .format(state, builder, succ, state.build_res_summary())) state.set_build_res(builder, succ, url) @@ -410,7 +427,8 @@ def report_build_res(succ, url, builder, state, logger, repo_cfg): if state.approved_by and not state.try_: try: - try: utils.github_set_ref(state.get_repo(), 'heads/' + state.base_ref, state.merge_sha) + try: + utils.github_set_ref(state.get_repo(), 'heads/' + state.base_ref, state.merge_sha) except github3.models.GitHubError: utils.github_create_status(state.get_repo(), state.merge_sha, 'success', '', 'Branch protection bypassed', context='homu') utils.github_set_ref(state.get_repo(), 'heads/' + state.base_ref, state.merge_sha) @@ -434,6 +452,7 @@ def report_build_res(succ, url, builder, state, logger, repo_cfg): g.queue_handler() + @post('/buildbot') def buildbot(): logger = g.logger.getChild('buildbot') @@ -446,11 +465,14 @@ def buildbot(): lazy_debug(logger, lambda: 'info: {}'.format(info)) props = dict(x[:2] for x in info['properties']) - if 'retry' in info['text']: continue + if 'retry' in info['text']: + continue - if not props['revision']: continue + if not props['revision']: + continue - try: state, repo_label = find_state(props['revision']) + try: + state, repo_label = find_state(props['revision']) except ValueError: lazy_debug(logger, lambda: 'Invalid commit ID from Buildbot: {}'.format(props['revision'])) @@ -518,10 +540,13 @@ def buildbot(): lazy_debug(logger, lambda: 'info: {}'.format(info)) props = dict(x[:2] for x in info['properties']) - if not props['revision']: continue + if not props['revision']: + continue - try: state, repo_label = find_state(props['revision']) - except ValueError: pass + try: + state, repo_label = find_state(props['revision']) + except ValueError: + pass else: if info['builderName'] in state.build_res: repo_cfg = g.repo_cfgs[repo_label] @@ -544,6 +569,7 @@ def buildbot(): return 'OK' + @post('/travis') def travis(): logger = g.logger.getChild('travis') @@ -552,7 +578,8 @@ def travis(): lazy_debug(logger, lambda: 'info: {}'.format(utils.remove_url_keys_from_json(info))) - try: state, repo_label = find_state(info['commit']) + try: + state, repo_label = find_state(info['commit']) except ValueError: lazy_debug(logger, lambda: 'Invalid commit ID from Travis: {}'.format(info['commit'])) return 'OK' @@ -572,7 +599,7 @@ def travis(): # fabricating travis notifications to try to trick Homu, but, # I imagine that this will most often occur because a repo is # misconfigured. - logger.warn('authorization failed for {}, maybe the repo has the wrong travis token? ' \ + logger.warn('authorization failed for {}, maybe the repo has the wrong travis token? ' 'header = {}, computed = {}' .format(state, auth_header, code)) abort(400, 'Authorization failed') @@ -583,6 +610,7 @@ def travis(): return 'OK' + def synch(user_gh, state, repo_label, repo_cfg, repo): if not repo.is_collaborator(user_gh.user().login): abort(400, 'You are not a collaborator') @@ -591,6 +619,7 @@ def synch(user_gh, state, repo_label, repo_cfg, repo): return 'Synchronizing {}...'.format(repo_label) + @post('/admin') def admin(): if request.json['secret'] != g.cfg['web']['secret']: @@ -638,7 +667,8 @@ def admin(): elif request.json['cmd'] == 'sync_all': def inner(): for repo_label in g.repos: - try: synchronize(repo_label, g.repo_cfgs[repo_label], g.logger, g.gh, g.states, g.repos, g.db, g.mergeable_que, g.my_username, g.repo_labels) + try: + synchronize(repo_label, g.repo_cfgs[repo_label], g.logger, g.gh, g.states, g.repos, g.db, g.mergeable_que, g.my_username, g.repo_labels) except: print('* Error while synchronizing {}'.format(repo_label)) traceback.print_exc() @@ -651,10 +681,11 @@ def inner(): return 'Unrecognized command' + def start(cfg, states, queue_handler, repo_cfgs, repos, logger, buildbot_slots, my_username, db, repo_labels, mergeable_que, gh): env = jinja2.Environment( - loader = jinja2.FileSystemLoader(pkg_resources.resource_filename(__name__, 'html')), - autoescape = True, + loader=jinja2.FileSystemLoader(pkg_resources.resource_filename(__name__, 'html')), + autoescape=True, ) tpls = {} tpls['index'] = env.get_template('index.html') @@ -674,7 +705,8 @@ def start(cfg, states, queue_handler, repo_cfgs, repos, logger, buildbot_slots, g.mergeable_que = mergeable_que g.gh = gh - try: run(host=cfg['web'].get('host', ''), port=cfg['web']['port'], server='waitress') + try: + run(host=cfg['web'].get('host', ''), port=cfg['web']['port'], server='waitress') except OSError as e: print(e, file=sys.stderr) os._exit(1) diff --git a/homu/utils.py b/homu/utils.py index 4ce574b1..afa3fa1a 100644 --- a/homu/utils.py +++ b/homu/utils.py @@ -7,30 +7,37 @@ import requests import time + def github_set_ref(repo, ref, sha, *, force=False, auto_create=True): url = repo._build_url('git', 'refs', ref, base_url=repo._api) data = {'sha': sha, 'force': force} - try: js = repo._json(repo._patch(url, data=json.dumps(data)), 200) + try: + js = repo._json(repo._patch(url, data=json.dumps(data)), 200) except github3.models.GitHubError as e: if e.code == 422 and auto_create: - try: return repo.create_ref('refs/' + ref, sha) - except github3.models.GitHubError: raise e + try: + return repo.create_ref('refs/' + ref, sha) + except github3.models.GitHubError: + raise e else: raise return github3.git.Reference(js, repo) if js else None + class Status(github3.repos.status.Status): def __init__(self, info): super(Status, self).__init__(info) self.context = info.get('context') + def github_iter_statuses(repo, sha): url = repo._build_url('statuses', sha, base_url=repo._api) return repo._iter(-1, url, Status) + def github_create_status(repo, sha, state, target_url='', description='', *, context=''): data = {'state': state, 'target_url': target_url, @@ -39,6 +46,7 @@ def github_create_status(repo, sha, state, target_url='', description='', *, js = repo._json(repo._post(url, data=data), 201) return Status(js) if js else None + def remove_url_keys_from_json(json): if isinstance(json, dict): return {key: remove_url_keys_from_json(value) @@ -49,19 +57,24 @@ def remove_url_keys_from_json(json): else: return json + def lazy_debug(logger, f): if logger.isEnabledFor(logging.DEBUG): logger.debug(f()) + def logged_call(args): - try: subprocess.check_call(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessError as e: + try: + subprocess.check_call(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: print('* Failed to execute command: {}'.format(args)) raise + def silent_call(args): return subprocess.call(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + def retry_until(inner, fail, state): err = None exc_info = None @@ -75,7 +88,8 @@ def retry_until(inner, fail, state): err = e exc_info = sys.exc_info() - if i != 1: time.sleep(1) + if i != 1: + time.sleep(1) else: err = None break