| #!/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<file_name>[^:]+):(?P<line>\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 |
| |