#!/usr/bin/env python # -*- coding: utf-8 -*- """Simple, on demand git checker""" import argparse import errno import json import os import subprocess import sys import git class ValidationError(Exception): """ValidationError""" def eprint(*arg, **kwargs): """stdderr print wrapper""" print(*arg, file=sys.stderr, flush=True, **kwargs) def is_git_repo(abspath): """ True of abspath points to a git repo, False otherwise """ if not os.path.isdir(abspath): return False try: git.Repo(abspath) return True except git.InvalidGitRepositoryError: return False def modifications_exist(abspath): """ True if modifications exist for git repo representing abspath False if no modifications exist git.InvalidGitRepositoryError raised if abspath points to illegal repo """ repo = git.Repo(abspath) return bool(repo.is_dirty() or repo.index.diff(None)) def print_modification_status(abspath, status, args): """Prints the modification status for the given abspath""" if status: if args.verbose: print(f'{abspath} -> modified') else: print(abspath) return status if args.verbose: print(f'{abspath} -> unchanged') return status def fetch_git_repos(abspath: str): """Returns a list of git repo paths under abspath""" repo_list = [] for root, dirs, _ in os.walk(abspath): if '/.git' in root: continue if '.git' in dirs: try: git.Repo(root) repo_list.append(root) except git.InvalidGitRepositoryError: continue return repo_list def fetch_git_files(args): """Fetches new and modified .py-files using git""" try: repo = git.Repo(args.repo_path) return [mfile.a_path for mfile in repo.index.diff(None) if mfile.a_path.lower().endswith('.py')] except git.InvalidGitRepositoryError: eprint(f'{args.repo_path} is not a valid git repository') sys.exit(1) def fetch_all_files(abspath): """Fetches all .py-files""" all_files_list = [] for root, _, files in os.walk(abspath): for f in files: if os.path.splitext(f)[1].lower() == '.py': all_files_list.append(os.path.join(abspath, root, f)) return all_files_list def validate_python_file(pfile, tests, args): """ Validates a single .py file against tests :param tests: A sequence of test dicts :type tests: collections.Sequence :param pfile: Python file (abs. path) :type pfile: str :param args: Argparse Namespace-object :type args: argparse.Namespace :return: False if validation fails, False otherwise :rtype: bool """ for test in tests: if args.verbose: print(f'{pfile} <- {test["name"]}') try: params = [param.replace('%p', pfile) for param in test['params']] subprocess.run(params, check=test['check']) except subprocess.CalledProcessError: if not args.continue_checks: if args.verbose: eprint(f'{pfile}: {test["name"]} validation failed') sys.exit(0) def main(inargs=None): """Main entry point""" parser = argparse.ArgumentParser( description='The following options are available') parser.add_argument( 'repo_path', metavar='REPO', nargs='?', default=os.getcwd(), help='Git repo path (default: cwd)') parser.add_argument( '-a', '--all-files', action='store_true', dest='all_files', help='Check all files, not only the modified ones') parser.add_argument( '-C', '--continue-checks', action='store_true', dest='continue_checks', help='Continue with the checks if validation fails') parser.add_argument( '-c', '--config', metavar='FILE', type=str, default=os.path.expanduser('~/.pygitchecker.json'), dest='config_file', help='Config file (default: ~/.pygitchecker.json)') parser.add_argument( '-f', '--full', action='store_true', dest='full_search', help='Consider repos that contain untracked files (used with -m)') parser.add_argument( '-m', '--modifications-only', action='store_true', dest='mod_only', help='Only check for git modifications') parser.add_argument( '-r', '--recursive', action='store_true', dest='recursive', help='Look for several repos recursively') parser.add_argument( '-v', '--verbose', action='store_true', dest='verbose', help='Verbosity') args = parser.parse_args(inargs) abs_repo_path = os.path.abspath(os.path.expanduser(args.repo_path)) if args.mod_only: if not args.recursive: if not is_git_repo(abs_repo_path): eprint(f'{abs_repo_path} is not a valid git repository') sys.exit(1) print_modification_status(abs_repo_path, modifications_exist(abs_repo_path), args) sys.exit(0) # resursive repo_list = fetch_git_repos(abs_repo_path) if not repo_list: print('No git-repos found') sys.exit(0) repo_list.sort() for repo_path in repo_list: print_modification_status(repo_path, modifications_exist(repo_path), args) sys.exit(0) if args.all_files: selected_files = fetch_all_files(abs_repo_path) else: selected_files = fetch_git_files(args) if not selected_files: print('No files selected') sys.exit(0) # now run the real tests try: with open(args.config_file, 'r') as fp: config_dict = json.load(fp) except Exception as e: eprint(f'Unable to parse {args.config_file}: {e}') sys.exit(errno.EIO) for target_file in selected_files: if args.verbose: print(f'Validating {target_file}:') validate_python_file(target_file, config_dict.get('tests'), args) if __name__ == '__main__': main()