diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0528927178..6ead6b2fb7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -40,10 +40,13 @@ /drivers/vulkan/ @godotengine/rendering ## OS +/drivers/apple*/ @godotengine/macos /drivers/unix/ @godotengine/linux-bsd /drivers/windows/ @godotengine/windows ## Misc +/drivers/register_driver_types.* @godotengine/buildsystem +/drivers/accesskit/ @godotengine/buildsystem /drivers/png/ @godotengine/asset-pipeline # Editor @@ -54,7 +57,6 @@ /editor/debugger/ @godotengine/editor @godotengine/debugger /editor/doc/ @godotengine/editor @godotengine/documentation /editor/gui/ @godotengine/editor @godotengine/gui-nodes -/editor/icons/ @godotengine/editor /editor/import/ @godotengine/editor @godotengine/asset-pipeline /editor/scene/2d/ @godotengine/editor @godotengine/2d-nodes /editor/scene/2d/physics/ @godotengine/editor @godotengine/physics @@ -125,7 +127,6 @@ /modules/multiplayer/ @godotengine/network /modules/multiplayer/doc_classes/ @godotengine/network @godotengine/documentation /modules/multiplayer/editor/ @godotengine/network @godotengine/editor -/modules/multiplayer/icons/ @godotengine/network /modules/multiplayer/tests/ @godotengine/network @godotengine/tests /modules/upnp/ @godotengine/network /modules/upnp/doc_classes/ @godotengine/network @godotengine/documentation @@ -152,14 +153,12 @@ /modules/gdscript/ @godotengine/gdscript /modules/gdscript/doc_classes/ @godotengine/gdscript @godotengine/documentation /modules/gdscript/editor/ @godotengine/gdscript @godotengine/script-editor -/modules/gdscript/icons/ @godotengine/gdscript /modules/gdscript/tests/ @godotengine/gdscript @godotengine/tests /modules/jsonrpc/ @godotengine/gdscript @godotengine/network /modules/jsonrpc/tests/ @godotengine/gdscript @godotengine/network @godotengine/tests /modules/mono/ @godotengine/dotnet /modules/mono/doc_classes/ @godotengine/dotnet @godotengine/documentation /modules/mono/editor/ @godotengine/dotnet @godotengine/script-editor -/modules/mono/icons/ @godotengine/dotnet ## Text /modules/freetype/ @godotengine/buildsystem @@ -180,15 +179,14 @@ /modules/webxr/doc_classes/ @godotengine/xr @godotengine/documentation ## Misc +/modules/register_module_types.* @godotengine/buildsystem /modules/csg/ @godotengine/3d-nodes /modules/csg/doc_classes/ @godotengine/3d-nodes @godotengine/documentation /modules/csg/editor/ @godotengine/3d-nodes @godotengine/editor -/modules/csg/icons/ @godotengine/3d-nodes /modules/csg/tests/ @godotengine/3d-nodes @godotengine/tests /modules/gridmap/ @godotengine/3d-nodes /modules/gridmap/doc_classes/ @godotengine/3d-nodes @godotengine/documentation /modules/gridmap/editor/ @godotengine/3d-nodes @godotengine/editor -/modules/gridmap/icons/ @godotengine/3d-nodes /modules/navigation_2d/ @godotengine/navigation /modules/navigation_2d/editor/ @godotengine/navigation @godotengine/editor /modules/navigation_3d/ @godotengine/navigation @@ -196,13 +194,11 @@ /modules/noise/ @godotengine/core /modules/noise/doc_classes/ @godotengine/core @godotengine/documentation /modules/noise/editor/ @godotengine/core @godotengine/editor -/modules/noise/icons/ @godotengine/core /modules/noise/tests/ @godotengine/core @godotengine/tests /modules/objectdb_profiler/ @godotengine/debugger /modules/objectdb_profiler/editor/ @godotengine/debugger @godotengine/editor /modules/regex/ @godotengine/core /modules/regex/doc_classes/ @godotengine/core @godotengine/documentation -/modules/regex/icons/ @godotengine/core /modules/regex/tests/ @godotengine/core @godotengine/tests /modules/zip/ @godotengine/core /modules/zip/doc_classes/ @godotengine/core @godotengine/documentation @@ -210,6 +206,7 @@ # Platform +/platform/register_platform_apis.* @godotengine/buildsystem /platform/android/ @godotengine/android /platform/android/doc_classes/ @godotengine/android @godotengine/documentation /platform/ios/ @godotengine/ios @@ -218,6 +215,8 @@ /platform/linuxbsd/doc_classes/ @godotengine/linux-bsd @godotengine/documentation /platform/macos/ @godotengine/macos /platform/macos/doc_classes/ @godotengine/macos @godotengine/documentation +/platform/visionos/ @godotengine/xr +/platform/visionos/doc_classes/ @godotengine/xr @godotengine/documentation /platform/web/ @godotengine/web /platform/web/doc_classes/ @godotengine/web @godotengine/documentation /platform/windows/ @godotengine/windows @@ -225,6 +224,9 @@ # Scene +/scene/property_* @godotengine/buildsystem +/scene/register_scene_types.* @godotengine/buildsystem +/scene/scene_string_names.* @godotengine/buildsystem /scene/2d/ @godotengine/2d-nodes /scene/2d/navigation/ @godotengine/2d-nodes @godotengine/navigation /scene/2d/physics/ @godotengine/2d-nodes @godotengine/physics @@ -237,24 +239,32 @@ /scene/debugger/ @godotengine/debugger /scene/gui/ @godotengine/gui-nodes /scene/main/ @godotengine/core +/scene/resources/* @godotengine/gui-nodes /scene/resources/2d/ @godotengine/2d-nodes /scene/resources/2d/skeleton/ @godotengine/2d-nodes @godotengine/animation /scene/resources/3d/ @godotengine/3d-nodes /scene/resources/animated* @godotengine/animation /scene/resources/animation* @godotengine/animation /scene/resources/audio* @godotengine/audio +/scene/resources/bone* @godotengine/animation /scene/resources/font* @godotengine/gui-nodes +/scene/resources/physics* @godotengine/physics /scene/resources/shader* @godotengine/shaders +/scene/resources/skeleton* @godotengine/animation /scene/resources/text_* @godotengine/gui-nodes /scene/resources/visual_shader* @godotengine/shaders /scene/theme/ @godotengine/gui-nodes -/scene/theme/icons/ @godotengine/gui-nodes # Servers +/servers/nav_heap.* @godotengine/navigation +/servers/register_server_types.* @godotengine/buildsystem +/servers/server_wrap_* @godotengine/buildsystem /servers/audio/ @godotengine/audio /servers/camera/ @godotengine/xr /servers/debugger/ @godotengine/debugger +/servers/display/ @godotengine/rendering +/servers/movie_writer/ @godotengine/rendering /servers/navigation_2d/ @godotengine/navigation /servers/navigation_3d/ @godotengine/navigation /servers/physics_2d/ @godotengine/physics diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 869cfb5801..5a4ee612fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -109,6 +109,12 @@ repos: files: ^(doc/classes|.*/doc_classes)/.*\.xml$ additional_dependencies: [xmlschema] + - id: validate-codeowners + name: validate codeowners + language: python + entry: python misc/scripts/validate_codeowners.py + args: [--unowned] + - id: eslint name: eslint language: node diff --git a/misc/scripts/validate_codeowners.py b/misc/scripts/validate_codeowners.py new file mode 100755 index 0000000000..8b74e1777c --- /dev/null +++ b/misc/scripts/validate_codeowners.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 + +if __name__ != "__main__": + raise SystemExit(f'Utility script "{__file__}" should not be used as a module!') + +import argparse +import re +import subprocess +import sys + +sys.path.insert(0, "./") + +try: + from methods import print_error, print_info +except ImportError: + raise SystemExit(f"Utility script {__file__} must be run from repository root!") + + +def glob_to_regex(glob: str) -> re.Pattern[str]: + """Convert a CODEOWNERS glob to a RegEx pattern.""" + + # Heavily inspired by: https://github.com/hmarr/codeowners/blob/main/match.go + + # Handle specific edgecases first. + if "***" in glob: + raise SyntaxError("Pattern cannot contain three consecutive asterisks") + if glob == "/": + raise SyntaxError('Standalone "/" will not match anything') + if not glob: + raise ValueError("Empty pattern") + + segments = glob.split("/") + if not segments[0]: + # Leading slash; relative to root. + segments = segments[1:] + else: + # Check for single-segment pattern, which matches relative to any descendent path. + # This is equivalent to a leading `**/`. + if len(segments) == 1 or (len(segments) == 2 and not segments[1]): + if segments[0] != "**": + segments.insert(0, "**") + + if len(segments) > 1 and not segments[-1]: + # A trailing slash is equivalent to `/**`. + segments[-1] = "**" + + last_index = len(segments) - 1 + need_slash = False + pattern = r"\A" + + for index, segment in enumerate(segments): + if segment == "**": + if index == 0 and index == last_index: + pattern += r".+" # Pattern is just `**`; match everything. + elif index == 0: + pattern += r"(?:.+/)?" # Pattern starts with `**`; match any leading path segment. + need_slash = False + elif index == last_index: + pattern += r"/.*" # Pattern ends with `**`; match any trailing path segment. + else: + pattern += r"(?:/.+)?" # Pattern contains `**`; match zero or more path segments. + need_slash = True + + elif segment == "*": + if need_slash: + pattern += "/" + # Regular wildcard; match any non-separator characters. + pattern += r"[^/]+" + need_slash = True + + else: + if need_slash: + pattern += "/" + + escape = False + for char in segment: + if escape: + escape = False + pattern += re.escape(char) + continue + elif char == "\\": + escape = True + elif char == "*": + # Multi-character wildcard. + pattern += r"[^/]*" + elif char == "?": + # Single-character wildcard. + pattern += r"[^/]" + else: + # Regular character + pattern += re.escape(char) + + if index == last_index: + pattern += r"(?:/.*)?" # No trailing slash; match descendent paths. + need_slash = True + + pattern += r"\Z" + return re.compile(pattern) + + +RE_CODEOWNERS = re.compile(r"^(?P[^#](?:\\ |[^\s])+) +(?P(?:[^#][^\s]+ ?)+)") + + +def parse_codeowners() -> list[tuple[re.Pattern[str], list[str]]]: + codeowners = [] + with open(".github/CODEOWNERS", encoding="utf-8", newline="\n") as file: + for line in reversed(file.readlines()): # Lower items have higher precedence. + if match := RE_CODEOWNERS.match(line): + codeowners.append((glob_to_regex(match["code"]), match["owners"].split())) + return codeowners + + +def main() -> int: + parser = argparse.ArgumentParser(description="Utility script for validating CODEOWNERS assignment.") + parser.add_argument("files", nargs="*", help="A list of files to validate. If excluded, checks all owned files.") + parser.add_argument("-u", "--unowned", action="store_true", help="Only output files without an owner.") + args = parser.parse_args() + + files: list[str] = args.files + if not files: + files = subprocess.run(["git", "ls-files"], text=True, capture_output=True).stdout.splitlines() + + ret = 0 + codeowners = parse_codeowners() + + for file in files: + matched = False + for code, owners in codeowners: + if code.match(file): + matched = True + if not args.unowned: + print_info(f"{file}: {owners}") + break + if not matched: + print_error(f"{file}: ") + ret += 1 + + return ret + + +try: + raise SystemExit(main()) +except KeyboardInterrupt: + import os + import signal + + signal.signal(signal.SIGINT, signal.SIG_DFL) + os.kill(os.getpid(), signal.SIGINT)