diff --git a/odev/_version.py b/odev/_version.py index 3273ca43..552d766e 100644 --- a/odev/_version.py +++ b/odev/_version.py @@ -22,4 +22,4 @@ # or merged change. # ------------------------------------------------------------------------------ -__version__ = "4.29.3" +__version__ = "4.30.0" diff --git a/odev/common/config.py b/odev/common/config.py index 5f7384cd..5566b477 100644 --- a/odev/common/config.py +++ b/odev/common/config.py @@ -264,6 +264,24 @@ def next_pull_date(self, version: str) -> datetime: return next_pull +class RepositoryPathsSection(Section): + """Per-repository path overrides. + Allows configuring a custom local path for a specific repository instead of + the default `//` structure. + """ + + _name = "repository_paths" + + def get_path(self, repo: str) -> Path | None: + """Return the configured path override for `repo`, or None if not set.""" + value = self.get(repo) + return Path(value).expanduser() if value else None + + def set_path(self, repo: str, path: str | Path): + """Set a custom path override for `repo`.""" + self.set(repo, path.as_posix() if isinstance(path, Path) else path) + + class SecuritySection(Section): """Security configuration.""" @@ -325,6 +343,9 @@ class Config: repositories: RepositoriesSection """Configuration for Odoo repositories.""" + repository_paths: RepositoryPathsSection + """Per-repository path overrides.""" + security: SecuritySection """Configuration for security and secrets encryption.""" diff --git a/odev/common/connectors/git.py b/odev/common/connectors/git.py index 3f0011d2..23eed5e5 100644 --- a/odev/common/connectors/git.py +++ b/odev/common/connectors/git.py @@ -310,7 +310,16 @@ def name(self) -> str: @property def path(self) -> Path: """The path to the repository.""" - return self._path or self.config.paths.repositories / self.name + if self._path: + return self._path + if override := self.config.repository_paths.get_path(self.name): + return override + standard = self.config.paths.repositories / self.name + if not (standard / ".git").exists(): + flat = self.config.paths.repositories / self._repository + if (flat / ".git").exists(): + return flat + return standard @property def exists(self) -> bool: diff --git a/tests/tests/common/test_git_connector.py b/tests/tests/common/test_git_connector.py index f91e7b9f..888cf68c 100644 --- a/tests/tests/common/test_git_connector.py +++ b/tests/tests/common/test_git_connector.py @@ -1,3 +1,6 @@ +from pathlib import Path +from unittest.mock import patch + from odev.common.connectors.git import GitConnector from odev.common.errors import ConnectorError @@ -25,3 +28,36 @@ def test_invalid_repo_format_raises(self): with self.assertRaises(ConnectorError) as ctx: GitConnector("onlyonepart") self.assertIn("Invalid repository format", str(ctx.exception)) + + +class TestGitConnectorPath(OdevTestCase): + def test_explicit_path_takes_priority(self): + explicit = Path("/explicit/path") + g = GitConnector("acme/myrepo", path=explicit) + self.assertEqual(g.path, explicit) + + def test_config_override_used_when_set(self): + override = Path("/custom/path/myrepo") + g = GitConnector("acme/myrepo") + with patch.object(type(g.config.repository_paths), "get_path", return_value=override): + self.assertEqual(g.path, override) + + def test_flat_fallback_when_standard_has_no_git(self): + g = GitConnector("acme/myrepo") + repositories = g.config.paths.repositories + flat = repositories / "myrepo" + with patch.object(type(g.config.repository_paths), "get_path", return_value=None): + with patch("pathlib.Path.exists", side_effect=lambda p=None: Path.__eq__(p or Path(), flat / ".git") if p else False): + # standard path has no .git, flat path does + def exists_side_effect(self): + return self == flat / ".git" + + with patch.object(Path, "exists", exists_side_effect): + self.assertEqual(g.path, flat) + + def test_standard_path_used_when_git_present(self): + g = GitConnector("acme/myrepo") + standard = g.config.paths.repositories / "acme" / "myrepo" + with patch.object(type(g.config.repository_paths), "get_path", return_value=None): + with patch.object(Path, "exists", lambda self: self == standard / ".git"): + self.assertEqual(g.path, standard)