From 92cb872470466d2576e649b8bd99a32541548b4d Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 15 Aug 2024 00:44:15 -0500 Subject: [PATCH] Support a gitlab dialect for codeowners Resolves #80. --- README.md | 2 + src/texthooks/alphabetize_codeowners.py | 64 ++++++++++++-- .../acceptance/test_alphabetize_codeowners.py | 83 +++++++++++++++++-- 3 files changed, 136 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f9dcbc5..f322840 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,8 @@ following sample config: ### Unreleased +- Support GitLab section headers in alphabetize-codeowners when + `--dialect=gitlab` is passed. - Casefold codeowner names for better unicode sorting. Thanks @adam-moss for the PR! diff --git a/src/texthooks/alphabetize_codeowners.py b/src/texthooks/alphabetize_codeowners.py index 7d919a3..3ff5ed7 100644 --- a/src/texthooks/alphabetize_codeowners.py +++ b/src/texthooks/alphabetize_codeowners.py @@ -3,14 +3,21 @@ Alphabetize the list of owners for each path in .github/CODEOWNERS Ignores empty lines and comments, but normalizes whitespace on semantically significant -lines +lines. + +Use '--dialect=gitlab' in order to support GitLab's extended CODEOWNERS syntax. """ +import re import sys +import typing as t from ._common import parse_cli_args from ._recorders import DiffRecorder +GITLAB_SECTION_TITLE_PATTERN = re.compile(r"\^?\[[^\]]+\]") +GITLAB_SECTION_N_APPROVALS_PATTERN = re.compile(r"\[\d+\]") + def main(*, argv=None) -> int: args = parse_cli_args( @@ -18,17 +25,19 @@ def main(*, argv=None) -> int: fixer=True, argv=argv, disable_args=["files"], - modify_parser=_add_files_arg, + modify_parser=_add_args, ) filenames = args.files if not filenames: filenames = [".github/CODEOWNERS"] + line_fixer = make_line_fixer(args.dialect) + recorder = DiffRecorder(args.verbosity) missing_file = False for fn in filenames: try: - recorder.run_line_fixer(sort_line, fn) + recorder.run_line_fixer(line_fixer, fn) except FileNotFoundError: missing_file = True if recorder or missing_file: @@ -38,11 +47,32 @@ def main(*, argv=None) -> int: return 0 -def _add_files_arg(parser): +def _add_args(parser): parser.add_argument("files", nargs="*", help="default: .github/CODEOWNERS") + parser.add_argument( + "--dialect", + default="standard", + choices=( + "standard", + "gitlab", + ), + help=( + "A dialect of codeowners parsing to use. " + "Defaults to the common syntax ('standard')." + ), + ) + + +def make_line_fixer(dialect: str) -> t.Callable[[str], str]: + if dialect == "standard": + return sort_line_standard + elif dialect == "gitlab": + return sort_line_gitlab + else: + raise NotImplementedError(f"Unrecognized dialect: {dialect}") -def sort_line(line: str) -> str: +def sort_line_standard(line: str) -> str: if line.strip() == "" or line.strip().startswith("#"): return line # also normalizes whitespace @@ -52,5 +82,29 @@ def sort_line(line: str) -> str: return " ".join([path] + sorted(owners, key=str.casefold)) +def sort_line_gitlab(line: str) -> str: + if line.strip() == "" or line.strip().startswith("#"): + return line + + title_match = GITLAB_SECTION_TITLE_PATTERN.match(line) + if title_match: + after_section = line[title_match.end() :] + n_approvals_match = GITLAB_SECTION_N_APPROVALS_PATTERN.match(after_section) + if n_approvals_match: + cut_point = title_match.end() + n_approvals_match.end() + else: + cut_point = title_match.end() + section = line[:cut_point] + default_owners = line[cut_point:].split() + if not default_owners: + return line + return " ".join([section] + sorted(default_owners, key=str.casefold)) + else: + path, *owners = line.split() + if not owners: + return line + return " ".join([path] + sorted(owners, key=str.casefold)) + + if __name__ == "__main__": sys.exit(main()) diff --git a/tests/acceptance/test_alphabetize_codeowners.py b/tests/acceptance/test_alphabetize_codeowners.py index 01a7ee0..ea7c93b 100644 --- a/tests/acceptance/test_alphabetize_codeowners.py +++ b/tests/acceptance/test_alphabetize_codeowners.py @@ -1,8 +1,11 @@ +import pytest + from texthooks.alphabetize_codeowners import main as alphabetize_codeowners_main -def test_alphabetize_codeowners_no_changes(runner): - result = runner(alphabetize_codeowners_main, "foo") +@pytest.mark.parametrize("dialect", ("standard", "gitlab")) +def test_alphabetize_codeowners_no_changes(runner, dialect): + result = runner(alphabetize_codeowners_main, "foo", add_args=["--dialect", dialect]) assert result.exit_code == 0 assert result.file_data == "foo" @@ -11,22 +14,34 @@ def test_alphabetize_codeowners_no_changes(runner): assert result.file_data == "/foo/bar.txt @alice @bob" -def test_alphabetize_codeowners_normalizes_spaces(runner): - result = runner(alphabetize_codeowners_main, " /foo/bar.txt @alice\t@bob ") +@pytest.mark.parametrize("dialect", ("standard", "gitlab")) +def test_alphabetize_codeowners_normalizes_spaces(runner, dialect): + result = runner( + alphabetize_codeowners_main, + " /foo/bar.txt @alice\t@bob ", + add_args=["--dialect", dialect], + ) assert result.exit_code == 1 assert result.file_data == "/foo/bar.txt @alice @bob" -def test_alphabetize_codeowners_sorts(runner): - result = runner(alphabetize_codeowners_main, "/foo/bar.txt @Bob @alice @charlie") +@pytest.mark.parametrize("dialect", ("standard", "gitlab")) +def test_alphabetize_codeowners_sorts(runner, dialect): + result = runner( + alphabetize_codeowners_main, + "/foo/bar.txt @Bob @alice @charlie", + add_args=["--dialect", dialect], + ) assert result.exit_code == 1 assert result.file_data == "/foo/bar.txt @alice @Bob @charlie" -def test_alphabetize_codeowners_sorts_other(runner): +@pytest.mark.parametrize("dialect", ("standard", "gitlab")) +def test_alphabetize_codeowners_sorts_other(runner, dialect): result = runner( alphabetize_codeowners_main, "/foo/bar.txt @Andy @adam @Bob @alice @charlie @groß @grost @grose", + add_args=["--dialect", dialect], ) assert result.exit_code == 1 assert ( @@ -35,7 +50,8 @@ def test_alphabetize_codeowners_sorts_other(runner): ) -def test_alphabetize_codeowners_ignores_non_semantic_lines(runner): +@pytest.mark.parametrize("dialect", ("standard", "gitlab")) +def test_alphabetize_codeowners_ignores_non_semantic_lines(runner, dialect): result = runner( alphabetize_codeowners_main, """ @@ -44,5 +60,56 @@ def test_alphabetize_codeowners_ignores_non_semantic_lines(runner): # comment 2: some non-alphabetized strings # d c b a /foo/bar.txt @alice @charlie""", + add_args=["--dialect", dialect], ) assert result.exit_code == 0 + + +def test_gitlab_alphabetize_codeowners_alphabetizes_default_owners(runner): + result = runner( + alphabetize_codeowners_main, + """\ +# section +[D A C B] +# optional section +^[D A C B E] +# section with owners +[D A C B] @mallory @alice +/foo/bar.txt +/foo/baz.txt""", + add_args=["--dialect", "gitlab"], + ) + assert result.exit_code == 1 + assert ( + result.file_data + == """\ +# section +[D A C B] +# optional section +^[D A C B E] +# section with owners +[D A C B] @alice @mallory +/foo/bar.txt +/foo/baz.txt""" + ) + + +def test_gitlab_alphabetize_codeowners_alphabetizes_default_owners_with_min_reviewers( + runner, +): + result = runner( + alphabetize_codeowners_main, + """\ +[D A C B][2] @bob @mallory @alice +/foo/bar.txt +/foo/baz.txt""", + add_args=["--dialect", "gitlab"], + ) + assert result.exit_code == 1 + assert ( + result.file_data + == """\ +[D A C B][2] @alice @bob @mallory +/foo/bar.txt +/foo/baz.txt""" + )