Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support a gitlab dialect for codeowners #88

Merged
merged 1 commit into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ following sample config:
### Unreleased

<!-- bumpversion-changelog -->
- 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!

Expand Down
64 changes: 59 additions & 5 deletions src/texthooks/alphabetize_codeowners.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,41 @@
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(
__doc__,
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:
Expand All @@ -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
Expand All @@ -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())
83 changes: 75 additions & 8 deletions tests/acceptance/test_alphabetize_codeowners.py
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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 (
Expand All @@ -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,
"""
Expand All @@ -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"""
)
Loading