summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgram <git@orsinium.dev>2023-02-28 11:18:17 +0100
committergram <git@orsinium.dev>2023-02-28 11:18:17 +0100
commit2e02490fa12406d1753d83be8f56a25edcc23c55 (patch)
tree0733da095332d8bb6c56681d78e86467c50c69b5
init core logic
-rw-r--r--owners/__init__.py0
-rw-r--r--owners/__main__.py0
-rw-r--r--owners/_cli.py0
-rw-r--r--owners/_owners.py62
-rw-r--r--owners/_rule.py42
-rw-r--r--owners/_section.py38
6 files changed, 142 insertions, 0 deletions
diff --git a/owners/__init__.py b/owners/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/owners/__init__.py
diff --git a/owners/__main__.py b/owners/__main__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/owners/__main__.py
diff --git a/owners/_cli.py b/owners/_cli.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/owners/_cli.py
diff --git a/owners/_owners.py b/owners/_owners.py
new file mode 100644
index 0000000..cd57d62
--- /dev/null
+++ b/owners/_owners.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from functools import cached_property
+from pathlib import Path
+
+from ._section import Section
+from ._rule import Rule
+
+
+@dataclass(frozen=True)
+class CodeOwners:
+ root: Path
+
+ @cached_property
+ def file_path(self) -> Path:
+ paths = [
+ self.root / 'CODEOWNERS',
+ self.root / 'docs' / 'CODEOWNERS',
+ self.root / '.gitlab' / 'CODEOWNERS',
+ ]
+ for path in paths:
+ if path.exists():
+ return path
+ raise FileNotFoundError('cannot find CODEOWNERS')
+
+ @cached_property
+ def sections(self) -> tuple[Section, ...]:
+ sections: list[Section] = []
+ section_name = ''
+ rules: list[Rule] = []
+ with self.file_path.open('r', encoding='utf8') as stream:
+ for line in stream:
+ line = line.strip()
+
+ # comment line
+ if not line or line.startswith('#'):
+ continue
+ is_section = line.startswith(('^[', '['))
+
+ # rule line
+ if not is_section:
+ rules.append(Rule(root=self.root, raw=line))
+ continue
+
+ # section line
+ if rules:
+ sections.append(Section(
+ root=self.root,
+ raw=section_name,
+ rules=tuple(rules)
+ ))
+ section_name = line
+
+ return tuple(sections)
+
+ def find_rule(self, path: Path) -> Rule | None:
+ for section in self.sections:
+ rule = section.find_rule(path)
+ if rule is not None:
+ return rule
+ return None
diff --git a/owners/_rule.py b/owners/_rule.py
new file mode 100644
index 0000000..c900d0e
--- /dev/null
+++ b/owners/_rule.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from functools import cached_property
+from pathlib import Path
+
+
+@dataclass(frozen=True)
+class Rule:
+ root: Path
+ raw: str
+
+ @cached_property
+ def raw_path(self) -> str:
+ return self.raw.split()[0]
+
+ @cached_property
+ def paths(self) -> tuple[Path, ...]:
+ raw = self.raw_path.lstrip('/')
+ if '*' in raw:
+ paths = self.root.glob(raw)
+ return tuple(path.absolute() for path in paths)
+ path = Path(raw)
+ if not path.exists():
+ return tuple()
+ return (path,)
+
+ @cached_property
+ def owners(self) -> tuple[str, ...]:
+ without_comments = self.raw.split('#')[0]
+ return tuple(without_comments.split()[1:])
+
+ def includes(self, path: Path) -> bool:
+ """Check if the given path is included in the rule.
+ """
+ path = path.absolute()
+ if path in self.paths:
+ return True
+ for parent in path.parents:
+ if parent in self.paths:
+ return True
+ return False
diff --git a/owners/_section.py b/owners/_section.py
new file mode 100644
index 0000000..c5f85bc
--- /dev/null
+++ b/owners/_section.py
@@ -0,0 +1,38 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from functools import cached_property
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from ._rule import Rule
+
+
+@dataclass(frozen=True)
+class Section:
+ root: Path
+ raw: str
+ rules: tuple[Rule, ...]
+
+ @cached_property
+ def name(self) -> str | None:
+ raw = self.raw.strip()
+ without_comments = raw.split('#')[0]
+ if not raw:
+ return None
+ return without_comments.lstrip('^[').rstrip(']')
+
+ @cached_property
+ def required(self) -> bool:
+ return not self.raw.lstrip().startswith('^')
+
+ def find_rule(self, path: Path) -> Rule | None:
+ path = path.absolute()
+ for rule in self.rules:
+ if path in rule.paths:
+ return rule
+ for rule in self.rules:
+ if rule.includes(path):
+ return rule
+ return None