#!/usr/bin/env python3 import os import subprocess from pathlib import Path import optparse import sys import re def run(command, cwd=None): try: return subprocess.Popen( command, shell=True, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as err: raise OSError("Error in command '{0}': {1}".format(command, err)) def parse_location(line): # take substring between @@ # take second part of it location = line.split(b'@@')[1].strip().split(b' ')[1] tokens = location.split(b',') if len(tokens) == 1: return (int(tokens[0][1:]), 1) elif len(tokens) == 2: return (int(tokens[0][1:]), int(tokens[1])) def changed_files(directory, scope): result = {} proc = run('git diff --no-prefix --unified={0}'.format(scope), cwd=str(directory)) file_path = None for line in iter(proc.stdout.readline, b''): if line.startswith(b'diff --git '): # this would be problematic if directory has space in the name file_name = line.split(b' ')[3].strip() file_path = str(directory.joinpath(str(file_name, 'utf-8'))) result[file_path] = set() continue if line.startswith(b'@@'): start_pos, number_of_lines = parse_location(line) for line_number in range(start_pos, start_pos + number_of_lines): result[file_path].add(line_number) return result def print_changed(file_name, line_number): print('{0}:{1}'.format(str(file_name), str(line_number))) def changes(dirs, scope): result = {} for directory in dirs: result.update(changed_files(directory, scope)) return result def repositories(root): for directory in Path(root).rglob('.git'): if not directory.is_dir(): continue yield directory.parent def setup_argparse(): parser = optparse.OptionParser(description="Filter output to remove unrelated warning") parser.add_option( "-r", "--regexp", dest="regexp", default='(?P[^:]+):(?P\d+).*', help="Regexp used to extract file_name and line number", ) parser.add_option( "-s", "--scope", dest="scope", default=0, help="Number of lines surrounding the change we consider relevant", ) parser.add_option( "-p", "--print-only", action="store_true", dest="print_only", default=False, help="Print changed lines only", ) return parser.parse_args() def filter_stdin(regexp, changes): any_matches = False for line in iter(sys.stdin.readline, ''): matches = re.match(regexp, line) if matches: file_name = matches.group('file_name') line_number = int(matches.group('line')) if file_name in changes and line_number in changes[file_name]: print(line, end='') any_matches = True return any_matches def validate_regexp(regexp): index = regexp.groupindex if 'file_name' in index and 'line' in index: return True else: raise TypeError("Regexp must define following groups:\n - file_name\n - line") def main(): opts, args = setup_argparse() if opts.print_only: for file_name, changed_lines in changes(repositories('.'), opts.scope).items(): for line_number in changed_lines: print_changed(file_name, line_number) return 0 else: regexp = re.compile(opts.regexp) validate_regexp(regexp) if filter_stdin(regexp, changes(repositories('.'), opts.scope)): return 1 else: return 0 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: pass