Last active
February 3, 2020 18:51
-
-
Save AlexRiina/473bde90e38b1234748d2c5fd741596c to your computer and use it in GitHub Desktop.
Utility for grouping files by codeower and generating commands
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Utility for grouping files by codeower and generating commands | |
""" | |
import argparse | |
import re | |
import shlex | |
from collections import defaultdict | |
from glob import fnmatch | |
from typing import DefaultDict, FrozenSet, Iterator, Optional, Set, Tuple | |
def main(): | |
parser = argparse.ArgumentParser( | |
description=""" | |
Designed for use with massive editing tools. E.g. | |
black $(git ls-files '*.py') | |
git diff --name-only | codemapper --from-file - --template 'git checkout -b black-{owners}; git commit {filenames} -m "autoformat"; git checkout master;' | |
""" | |
) | |
parser.add_argument("filenames", nargs="*") | |
parser.add_argument( | |
"--from-file", | |
dest="filenames_file", | |
type=argparse.FileType("r"), | |
help="read file names from file or pipe", | |
) | |
parser.add_argument( | |
"--codeowners", type=argparse.FileType("r"), default=".github/CODEOWNERS" | |
) | |
parser.add_argument( | |
"--template", | |
help="command template with optional variables like {filenames} and {owners}", | |
default="{owners}: {filenames}", | |
) | |
args = parser.parse_args() | |
if args.filenames_file: | |
args.filenames.extend( | |
[filename.rstrip('\n') for filename in args.filenames_file.readlines()] | |
) | |
for owners, filenames in assign(args.codeowners, set(args.filenames)).items(): | |
print( | |
args.template.format( | |
owners="-".join(owners), | |
filenames=" ".join(map(shlex.quote, filenames))) | |
) | |
def assign(codeowners, filenames: Set[str]): | |
""" assign each of the filenames to their codeowners """ | |
unassigned_filenames = filenames | |
by_owner: DefaultDict[FrozenSet[str], Set[str]] = defaultdict(set) | |
for file_pattern, owners in list(parse_codeowners(codeowners))[::-1]: | |
matches = fnmatch.filter(unassigned_filenames, file_pattern) | |
if matches: | |
by_owner[owners].update(matches) | |
unassigned_filenames.difference_update(matches) | |
if unassigned_filenames: | |
by_owner[frozenset()] = unassigned_filenames | |
return by_owner | |
def parse_codeowners(codeowners) -> Iterator[Tuple[str, FrozenSet[str]]]: | |
for line in codeowners.readlines(): | |
line = re.sub("#.*", "", line).strip() | |
if line: | |
file_pattern, *owners = re.split(r"\s+", line) | |
file_pattern = file_pattern.lstrip('/') # / is root of repo | |
yield file_pattern, frozenset(owners) | |
if not file_pattern.endswith("*"): | |
# can't tell file_pattern will match files or directories | |
# so yield a file_pattern which treats it as a directory | |
yield file_pattern + "*", frozenset(owners) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment