Skip to content

Conversation

@VikramGoyal23
Copy link
Contributor

Allows developers to set the exercise source as a local path, enabling fully local exercise testing. Backwards compatible and assumes lack of type field as remote.

@VikramGoyal23 VikramGoyal23 added the enhancement New feature or request label Jan 20, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds support for using local exercise repositories for testing, enabling developers to test exercise changes locally before pushing to remote. The implementation maintains backwards compatibility by defaulting to "remote" type when the type field is absent.

Changes:

  • Extended ExercisesSource dataclass to support both "remote" and "local" repository types with appropriate fields for each
  • Added from_raw class method for flexible deserialization with backwards compatibility
  • Implemented local repository copying logic using shutil.copytree for isolation

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
app/configs/gitmastery_config.py Extended ExercisesSource to support local/remote types, added from_raw factory method for backwards-compatible deserialization
app/utils/gitmastery.py Added local repo handling branch in enter method with path validation and directory copying logic

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 95 to 104
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)
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkout method calls sparse_checkout on the Git repo, which is specifically designed for sparse clones. When using a local exercises source, the copied repository may not be a sparse clone, which could cause the sparse_checkout command to fail or behave unexpectedly. Consider checking if the repository is a sparse clone before calling sparse_checkout, or handling local sources differently in the checkout, has_file, and fetch_file_contents methods.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works fine during testing.

Comment on lines 96 to 100
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()
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path validation check occurs after already logging the path. If path is None, the log message will display "None" before raising the ValueError. Consider moving the validation check before the info log to provide clearer error handling flow.

Suggested change
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()
# 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()
info(f"Using local exercises source at {src}")

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using symlinks=True in shutil.copytree could lead to issues if the local exercises source contains symlinks pointing to locations outside the repository. Consider using symlinks=False or adding validation to ensure symlinks only point to locations within the source directory.

Copilot uses AI. Check for mistakes.
Comment on lines 38 to 43
typ = raw.get("type")
# explicit local
if typ == "local":
return cls(type="local", path=raw.get("path"))
# legacy/detected remote
return cls(
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the type field is not present in the dict (backwards compatibility case), the code defaults to treating it as "remote". However, the from_raw method gets the type using raw.get("type"), which returns None when the key is not present. The conditional check 'if typ == "local"' will skip this None case, correctly falling through to the legacy remote case. Consider adding a comment to clarify this intended backwards compatibility behavior.

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code copies the local path and then initializes it as a Repo, but it doesn't validate that the local path is actually a git repository. If the path exists but is not a git repository, Repo(self.__temp_dir.name) will fail with an unclear error. Consider validating that the local path is a git repository before copying, or provide a clearer error message if it's not.

Suggested change
shutil.copytree(src, self.__temp_dir.name, dirs_exist_ok=True, symlinks=True, copy_function=shutil.copy2)
if not (src / ".git").is_dir():
raise ValueError(f"Local exercises source is not a git repository: {src}")
shutil.copytree(
src,
self.__temp_dir.name,
dirs_exist_ok=True,
symlinks=True,
copy_function=shutil.copy2,
)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error is InvalidGitRepositoryError, which is mostly clear.


def to_url(self) -> str:
if self.type != "remote":
raise ValueError("to_url only valid for remote ExercisesSource")
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The to_url method references self.username and self.repository without checking if they are None. For remote type ExercisesSource instances, these fields could be None if not properly validated during construction. Consider adding validation to ensure username and repository are not None when type is "remote".

Suggested change
raise ValueError("to_url only valid for remote ExercisesSource")
raise ValueError("to_url only valid for remote ExercisesSource")
if self.username is None or self.repository is None:
raise ValueError("Remote ExercisesSource requires 'username' and 'repository' to be set")

Copilot uses AI. Check for mistakes.
- Typecheck `path` beofre use
- Remove symlink copying
- Clarify legacy fallthrough
@jovnc
Copy link
Collaborator

jovnc commented Jan 24, 2026

@VikramGoyal23 Is this ready for review? Would also be good to resolve copilot comments if they have been addressed as well to reduce cognitive load on reviewer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants