diff --git a/app/configs/gitmastery_config.py b/app/configs/gitmastery_config.py index 6a3c0c4..f4712f5 100644 --- a/app/configs/gitmastery_config.py +++ b/app/configs/gitmastery_config.py @@ -1,7 +1,7 @@ import json from dataclasses import dataclass from pathlib import Path -from typing import Self, Type +from typing import Self, Type, Optional, Union from app.configs.utils import read_config @@ -12,13 +12,43 @@ class GitMasteryConfig: @dataclass class ExercisesSource: - username: str - repository: str - branch: str + # "remote" or "local" + type: str = "remote" + # remote fields (legacy uses username/repository/branch) + username: Optional[str] = None + repository: Optional[str] = None + branch: Optional[str] = "main" + # local field + path: Optional[str] = None def to_url(self) -> str: + if self.type != "remote": + raise ValueError("to_url only valid for remote ExercisesSource") return f"https://github.com/{self.username}/{self.repository}.git" + @classmethod + def from_raw(cls, raw: Union["GitMasteryConfig.ExercisesSource", dict, None]) -> "GitMasteryConfig.ExercisesSource": + # Pass-through if already the correct instance + if isinstance(raw, cls): + return raw + # Default remote + if raw is None: + return cls(type="remote", username="git-mastery", repository="exercises", branch="main") + if isinstance(raw, dict): + typ = raw.get("type") + # explicit local + if typ == "local": + return cls(type="local", path=raw.get("path")) + # fallthrough for None (legacy)/detected remote + return cls( + type="remote", + username=raw.get("username", "git-mastery"), + repository=raw.get("repository", "exercises"), + branch=raw.get("branch", "main"), + ) + raise ValueError("Unsupported exercises_source shape") + + progress_local: bool progress_remote: bool exercises_source: ExercisesSource @@ -44,19 +74,17 @@ def read(cls: Type[Self], path: Path, cds: int) -> Self: raw_config = read_config(path, GITMASTERY_CONFIG_NAME) exercises_source_raw = raw_config.get("exercises_source", {}) + exercises_source = GitMasteryConfig.ExercisesSource.from_raw(exercises_source_raw) + return cls( path=path, cds=cds, progress_local=raw_config.get("progress_local", True), progress_remote=raw_config.get("progress_remote", False), - exercises_source=GitMasteryConfig.ExercisesSource( - username=exercises_source_raw.get("username", "git-mastery"), - repository=exercises_source_raw.get("repository", "exercises"), - branch=exercises_source_raw.get("branch", "main"), - ), + exercises_source=exercises_source, ) -GIT_MASTERY_EXERCISES_SOURCE = GitMasteryConfig.ExercisesSource( - username="git-mastery", repository="exercises", branch="main" +GIT_MASTERY_EXERCISES_SOURCE = GitMasteryConfig.ExercisesSource.from_raw( + {"type": "remote", "username": "git-mastery", "repository": "exercises", "branch": "main"} ) diff --git a/app/utils/gitmastery.py b/app/utils/gitmastery.py index a2f5ff6..aa218f8 100644 --- a/app/utils/gitmastery.py +++ b/app/utils/gitmastery.py @@ -12,6 +12,7 @@ TypeVar, Union, ) +import shutil from git import Repo @@ -91,17 +92,28 @@ def __enter__(self) -> Self: else: exercises_source = GIT_MASTERY_EXERCISES_SOURCE - info( - f"Fetching exercise information from {exercises_source.to_url()} on branch {exercises_source.branch}" - ) - - self.__repo = Repo.clone_from( - exercises_source.to_url(), - self.__temp_dir.name, - depth=1, - branch=exercises_source.branch, - multi_options=["--filter=blob:none", "--sparse"], - ) + if exercises_source.type == "local": + # copy local repo into temp dir for isolation + if exercises_source.path is None: + raise ValueError("Path is required for using local exercises source") + info(f"Using local exercises source at {exercises_source.path}") + src = Path(exercises_source.path).expanduser().resolve() + if not src.exists(): + raise FileNotFoundError(f"Local exercises source not found: {src}") + shutil.copytree(src, self.__temp_dir.name, dirs_exist_ok=True, symlinks=False, copy_function=shutil.copy2) + self.__repo = Repo(self.__temp_dir.name) + else: + info( + f"Fetching exercise information from {exercises_source.to_url()} on branch {exercises_source.branch}" + ) + + self.__repo = Repo.clone_from( + exercises_source.to_url(), + self.__temp_dir.name, + depth=1, + branch=exercises_source.branch, + multi_options=["--filter=blob:none", "--sparse"], + ) return self def __exit__(