Skip to content

Instantly share code, notes, and snippets.

@mossprescott
Last active October 10, 2018 18:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mossprescott/e126b12e32f529396c0531468863b833 to your computer and use it in GitHub Desktop.
Save mossprescott/e126b12e32f529396c0531468863b833 to your computer and use it in GitHub Desktop.
#! /usr/bin/env python3
"""Inspect stack.yaml in the current workspace. For each dependency referring to a git commit, check to see whether
the commit is 1) up-to-date with origin/master, 2) is ready to be merged, or 3) is behind.
Also check the stack.yaml of each referenced commit, and warn if any of _its_ dependency commits don't match.
Assumptions:
- the working directory is located somewhere within a git clone
- stack.yaml is present at the root of the clone
- any referenced package which has a `location` refers to a git repository
- referenced repositories are cloned in the same location as the current workspace
- the remote is called "origin" and the target branch is "master"
The files in the referenced clones are not inspected, so they don't have to be on any particular
branch or have clean trees. The only effect of the script on them is to fetch origin/master.
Requirements:
- pip install pyyaml
"""
import os
import re
import sys
import subprocess
import yaml
def main():
dir = sys.argv[1] if len(sys.argv) > 1 else '.'
root_path = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], cwd=dir).decode('utf-8').strip()
warnings = 0
pkgs = git_packages(open(os.path.join(root_path, 'stack.yaml'), 'r'))
print(f'Upstream packages ({len(pkgs)}):')
repo_to_commit = dict(pkgs)
for repo, commit in pkgs:
if commit is None:
print(f' local path: {repo}')
print(' WARNING: unable to check transitive dependencies')
warnings += 1
else:
print(f' {repo} at {commit}')
pkg_root = os.path.join(root_path, '../', repo)
git(pkg_root, ['fetch', 'origin', 'master'])
head = git_head(pkg_root, 'origin/master')
if head == commit:
print(' up-to-date')
else:
behind = git_count_commits(pkg_root, commit, 'origin/master')
ahead = git_count_commits(pkg_root, 'origin/master', commit)
if ahead > 0 and behind > 0:
print(f' WARNING: branch not up-to-date with master ({ahead} ahead, {behind} behind)')
warnings += 1
elif behind > 0:
print(f' {behind} behind master (refers to an earlier commit)')
elif ahead > 0:
print(f' {ahead} ahead of master (not merged yet)')
warnings += 1
else:
# Note: this happens when the referenced commit has been merged, so the only newer
# commit on master is the one that merged it.
print(' up-to-date (modulo merge commits)')
for child_repo, child_commit in git_packages(git(pkg_root, ['show', f'{commit}:stack.yaml'])):
ref_commit = repo_to_commit.get(child_repo)
if ref_commit and ref_commit != child_commit:
print(f' WARNING: {child_repo} {child_commit} does not match')
warnings += 1
# Note: no checking of references that appear more than once in the graph but not in the
# current repo
if warnings:
print(f'Not safe to merge! Found {warnings} problems(s)')
sys.exit(1)
else:
print('OK to merge')
def git_packages(stream):
"""Packages which are referred to using git links, as tuples (repo, commit)."""
stackCfg = yaml.load(stream)
def parsePackage(p):
loc = p['location']
if type(loc) == str:
return (loc, None)
else:
m = re.match(r'git.*:([A-Za-z0-9-]+)/([A-Za-z0-9-]+)\.git', loc['git'])
org, repo = m.groups()
return repo, loc['commit']
return [parsePackage(p) for p in stackCfg['packages'] if type(p) == dict]
def git_count_commits(workspace_path, base_ref, ref):
"""Count of non-merge commits on ref which are not on base_ref."""
return len([l for l in git(workspace_path, ['log', '--oneline', '--no-merges', f'{base_ref}..{ref}']).strip().split('\n') if l])
def git_head(workspace_path, branch):
"""SHA1 of the HEAD commit."""
return git(workspace_path, ['log', '-1', '--format=%H', 'origin/master']).strip()
def git(workspace_path, cmd):
return subprocess.check_output(['git', '-C', workspace_path] + cmd, stderr=subprocess.DEVNULL).decode('utf-8')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment