Skip to content
Draft
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: 1 addition & 1 deletion odev/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
# or merged change.
# ------------------------------------------------------------------------------

__version__ = "4.29.3"
__version__ = "4.30.0"
21 changes: 21 additions & 0 deletions odev/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<repositories>/<organization>/<name>` 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."""

Expand Down Expand Up @@ -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."""

Expand Down
11 changes: 10 additions & 1 deletion odev/common/connectors/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
36 changes: 36 additions & 0 deletions tests/tests/common/test_git_connector.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)