Skip to content

Instantly share code, notes, and snippets.

@AlexRiina
Last active February 3, 2020 18:51
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 AlexRiina/473bde90e38b1234748d2c5fd741596c to your computer and use it in GitHub Desktop.
Save AlexRiina/473bde90e38b1234748d2c5fd741596c to your computer and use it in GitHub Desktop.
Utility for grouping files by codeower and generating commands
"""
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