From efddc8590e19d8481962536caa1b281ce88614c1 Mon Sep 17 00:00:00 2001 From: Vikram Goyal Date: Tue, 20 Jan 2026 17:52:27 +0800 Subject: [PATCH 1/3] Add local exercise repo fetching for testing local changes --- app/configs/gitmastery_config.py | 50 +++++++++++++++++++++++++------- app/utils/gitmastery.py | 34 +++++++++++++++------- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/app/configs/gitmastery_config.py b/app/configs/gitmastery_config.py index 6a3c0c4..c435783 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" or ("path" in raw and typ is None and raw.get("path")): + return cls(type="local", path=raw.get("path")) + # 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..3871253 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": + info(f"Using local exercises source at {exercises_source.path}") + # copy local repo into temp dir for isolation + if exercises_source.path is None: + raise ValueError("Path is required for using local exercises source") + 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=True, 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__( From 3096d00a7897ab92f484946972eca8781d51134b Mon Sep 17 00:00:00 2001 From: Vikram Goyal Date: Wed, 21 Jan 2026 11:37:54 +0800 Subject: [PATCH 2/3] Always assume remote for no type --- app/configs/gitmastery_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/configs/gitmastery_config.py b/app/configs/gitmastery_config.py index c435783..ad2b0cb 100644 --- a/app/configs/gitmastery_config.py +++ b/app/configs/gitmastery_config.py @@ -37,7 +37,7 @@ def from_raw(cls, raw: Union["GitMasteryConfig.ExercisesSource", dict, None]) -> if isinstance(raw, dict): typ = raw.get("type") # explicit local - if typ == "local" or ("path" in raw and typ is None and raw.get("path")): + if typ == "local": return cls(type="local", path=raw.get("path")) # legacy/detected remote return cls( From 5cada961f8002642a5dd1bb708159258a7836ad0 Mon Sep 17 00:00:00 2001 From: Vikram Goyal Date: Fri, 23 Jan 2026 21:15:34 +0800 Subject: [PATCH 3/3] Fix according to comments - Typecheck `path` beofre use - Remove symlink copying - Clarify legacy fallthrough --- app/configs/gitmastery_config.py | 2 +- app/utils/gitmastery.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/configs/gitmastery_config.py b/app/configs/gitmastery_config.py index ad2b0cb..f4712f5 100644 --- a/app/configs/gitmastery_config.py +++ b/app/configs/gitmastery_config.py @@ -39,7 +39,7 @@ def from_raw(cls, raw: Union["GitMasteryConfig.ExercisesSource", dict, None]) -> # explicit local if typ == "local": return cls(type="local", path=raw.get("path")) - # legacy/detected remote + # fallthrough for None (legacy)/detected remote return cls( type="remote", username=raw.get("username", "git-mastery"), diff --git a/app/utils/gitmastery.py b/app/utils/gitmastery.py index 3871253..aa218f8 100644 --- a/app/utils/gitmastery.py +++ b/app/utils/gitmastery.py @@ -93,14 +93,14 @@ def __enter__(self) -> Self: exercises_source = GIT_MASTERY_EXERCISES_SOURCE if exercises_source.type == "local": - info(f"Using local exercises source at {exercises_source.path}") # 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=True, copy_function=shutil.copy2) + 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(