Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/misc/scripts/validate_codeowners.py
45997 views
1
#!/usr/bin/env python3
2
3
if __name__ != "__main__":
4
raise SystemExit(f'Utility script "{__file__}" should not be used as a module!')
5
6
import argparse
7
import re
8
import subprocess
9
import sys
10
11
sys.path.insert(0, "./")
12
13
try:
14
from methods import print_error, print_info
15
except ImportError:
16
raise SystemExit(f"Utility script {__file__} must be run from repository root!")
17
18
19
def glob_to_regex(glob: str) -> re.Pattern[str]:
20
"""Convert a CODEOWNERS glob to a RegEx pattern."""
21
22
# Heavily inspired by: https://github.com/hmarr/codeowners/blob/main/match.go
23
24
# Handle specific edgecases first.
25
if "***" in glob:
26
raise SyntaxError("Pattern cannot contain three consecutive asterisks")
27
if glob == "/":
28
raise SyntaxError('Standalone "/" will not match anything')
29
if not glob:
30
raise ValueError("Empty pattern")
31
32
segments = glob.split("/")
33
if not segments[0]:
34
# Leading slash; relative to root.
35
segments = segments[1:]
36
else:
37
# Check for single-segment pattern, which matches relative to any descendent path.
38
# This is equivalent to a leading `**/`.
39
if len(segments) == 1 or (len(segments) == 2 and not segments[1]):
40
if segments[0] != "**":
41
segments.insert(0, "**")
42
43
if len(segments) > 1 and not segments[-1]:
44
# A trailing slash is equivalent to `/**`.
45
segments[-1] = "**"
46
47
last_index = len(segments) - 1
48
need_slash = False
49
pattern = r"\A"
50
51
for index, segment in enumerate(segments):
52
if segment == "**":
53
if index == 0 and index == last_index:
54
pattern += r".+" # Pattern is just `**`; match everything.
55
elif index == 0:
56
pattern += r"(?:.+/)?" # Pattern starts with `**`; match any leading path segment.
57
need_slash = False
58
elif index == last_index:
59
pattern += r"/.*" # Pattern ends with `**`; match any trailing path segment.
60
else:
61
pattern += r"(?:/.+)?" # Pattern contains `**`; match zero or more path segments.
62
need_slash = True
63
64
elif segment == "*":
65
if need_slash:
66
pattern += "/"
67
# Regular wildcard; match any non-separator characters.
68
pattern += r"[^/]+"
69
need_slash = True
70
71
else:
72
if need_slash:
73
pattern += "/"
74
75
escape = False
76
for char in segment:
77
if escape:
78
escape = False
79
pattern += re.escape(char)
80
continue
81
elif char == "\\":
82
escape = True
83
elif char == "*":
84
# Multi-character wildcard.
85
pattern += r"[^/]*"
86
elif char == "?":
87
# Single-character wildcard.
88
pattern += r"[^/]"
89
else:
90
# Regular character
91
pattern += re.escape(char)
92
93
if index == last_index:
94
pattern += r"(?:/.*)?" # No trailing slash; match descendent paths.
95
need_slash = True
96
97
pattern += r"\Z"
98
return re.compile(pattern)
99
100
101
RE_CODEOWNERS = re.compile(r"^(?P<code>[^#](?:\\ |[^\s])+) +(?P<owners>(?:[^#][^\s]+ ?)+)")
102
103
104
def parse_codeowners() -> list[tuple[re.Pattern[str], list[str]]]:
105
codeowners = []
106
with open(".github/CODEOWNERS", encoding="utf-8", newline="\n") as file:
107
for line in reversed(file.readlines()): # Lower items have higher precedence.
108
if match := RE_CODEOWNERS.match(line):
109
codeowners.append((glob_to_regex(match["code"]), match["owners"].split()))
110
return codeowners
111
112
113
def main() -> int:
114
parser = argparse.ArgumentParser(description="Utility script for validating CODEOWNERS assignment.")
115
parser.add_argument("files", nargs="*", help="A list of files to validate. If excluded, checks all owned files.")
116
parser.add_argument("-u", "--unowned", action="store_true", help="Only output files without an owner.")
117
args = parser.parse_args()
118
119
files: list[str] = args.files
120
if not files:
121
files = subprocess.run(["git", "ls-files"], text=True, capture_output=True).stdout.splitlines()
122
123
ret = 0
124
codeowners = parse_codeowners()
125
126
for file in files:
127
matched = False
128
for code, owners in codeowners:
129
if code.match(file):
130
matched = True
131
if not args.unowned:
132
print_info(f"{file}: {owners}")
133
break
134
if not matched:
135
print_error(f"{file}: <UNOWNED>")
136
ret += 1
137
138
return ret
139
140
141
try:
142
raise SystemExit(main())
143
except KeyboardInterrupt:
144
import os
145
import signal
146
147
signal.signal(signal.SIGINT, signal.SIG_DFL)
148
os.kill(os.getpid(), signal.SIGINT)
149
150