repo-smith reference
repo-smith is a test-support library that creates and mutates local and remote Git repository states through helper objects, so exercise verification scenarios can be set up deterministically in tests.
The YAML-based spec format from repo-smith v1 is deprecated. All Git-Mastery exercises use the Python v2 API directly through create_repo_smith and the RepoSmith helper objects.
Installation
pip install -U repo-smith
Core API
create_repo_smith
The main entrypoint. Returns a context manager that yields a RepoSmith instance.
from repo_smith.repo_smith import create_repo_smith
with create_repo_smith(verbose=False) as rs:
rs.git.commit(message="Initial commit", allow_empty=True)
Options
| Option | Type | Description |
|---|---|---|
verbose | bool | Print commands as they run |
existing_path | str | Use an existing directory instead of creating a temp one |
clone_from | str | Clone from a URL before starting |
null_repo | bool | Create a RepoSmith with no underlying repository |
RepoSmith
Yielded by create_repo_smith. Provides three built-in helpers:
| Attribute | Description |
|---|---|
rs.git | Git operations |
rs.files | File system operations |
rs.gh | GitHub CLI operations |
rs.repo | Underlying GitPython Repo object |
Custom helpers can be registered with rs.add_helper(MyHelper) and accessed with rs.helper(MyHelper).
rs.git — Git operations
Commit and stage
rs.git.add("notes.txt")
rs.git.add(["a.txt", "b.txt"])
rs.git.add(all=True)
rs.git.commit(message="Initial commit", allow_empty=True)
rs.git.commit(message="Add file")
Branches
rs.git.checkout("feature/login", branch=True) # create and switch
rs.git.checkout("main") # switch to existing branch
rs.git.branch("feature/payments") # create without switching
rs.git.branch("new-name", old_branch="old-name", move=True) # rename
rs.git.branch("old-branch", delete=True) # delete
Merge
rs.git.merge("feature/login", no_ff=True)
rs.git.merge("feature/dashboard")
Tags
rs.git.tag("v1.0")
rs.git.tag("v1.0", "abc1234") # tag a specific commit
Reset and revert
rs.git.reset("HEAD~1", hard=True)
rs.git.revert("HEAD")
Remotes and push/fetch
rs.git.remote_add("origin", "https://github.com/example/repo.git")
rs.git.remote_rename("origin", "upstream")
rs.git.remote_remove("origin")
rs.git.push("origin", "main", set_upstream=True)
rs.git.push("origin", ":old-branch") # the colon prefix deletes the remote branch
rs.git.fetch("origin")
rs.git.fetch(all=True)
Restore
rs.git.restore("notes.txt")
rs.git.restore("notes.txt", staged=True)
rs.files — File operations
rs.files.create_or_update("notes.txt", "hello world") # create or overwrite
rs.files.create_or_update("empty.txt") # create empty file
rs.files.append("notes.txt", "\nmore content")
rs.files.delete("notes.txt")
rs.files.delete("some-dir/")
rs.files.mkdir("subdir")
rs.files.cd("subdir")
rs.gh — GitHub CLI operations
Used for exercises that test remote repository behavior.
rs.gh.repo_create(owner=None, repo="my-repo", public=True)
rs.gh.repo_clone(owner="git-mastery", repo="exercises", directory="local-dir")
rs.gh.repo_fork(owner="git-mastery", repo="exercises", clone=True)
rs.gh.repo_delete(owner=None, repo="my-repo")
rs.gh.repo_view(owner="git-mastery", repo="exercises")
Custom helpers
Custom helpers extend RepoSmith with domain-specific operations. Subclass Helper and register it before use:
from git import Repo
from repo_smith.helpers.helper import Helper
class GitMasteryHelper(Helper):
def __init__(self, repo: Repo, verbose: bool) -> None:
super().__init__(repo, verbose)
def create_start_tag(self) -> None:
"""Creates the git-mastery-start-<hash> tag required by git-autograder."""
all_commits = list(self.repo.iter_commits())
first_commit = list(reversed(all_commits))[0]
tag = f"git-mastery-start-{first_commit.hexsha[:7]}"
self.repo.create_tag(tag)
# Register once on the RepoSmith instance, then call via rs.helper(...)
rs.add_helper(GitMasteryHelper)
rs.helper(GitMasteryHelper).create_start_tag()
GitMasteryHelper is already defined in exercise_utils.test and is registered automatically when using GitAutograderTestLoader. You only need to implement it yourself if you are calling create_repo_smith directly outside of exercises.
Typical test pattern in exercises
Tests in exercises do not call create_repo_smith directly. Instead they use GitAutograderTestLoader from exercise_utils.test, which sets up the exercise environment and provides rs automatically.
Basic test
from exercise_utils.test import GitAutograderTestLoader, GitMasteryHelper, assert_output
from git_autograder import GitAutograderStatus
from .verify import verify, NO_MERGES, MISSING_MERGES
REPOSITORY_NAME = "branch-bender"
loader = GitAutograderTestLoader(REPOSITORY_NAME, verify)
def test_successful_merge() -> None:
with loader.start() as (test, rs):
rs.git.commit(message="Initial commit", allow_empty=True)
rs.helper(GitMasteryHelper).create_start_tag()
rs.git.checkout("feature/login", branch=True)
rs.git.commit(message="Add login", allow_empty=True)
rs.git.checkout("main")
rs.git.merge("feature/login", no_ff=True)
output = test.run()
assert_output(output, GitAutograderStatus.SUCCESSFUL)
def test_no_merges() -> None:
with loader.start() as (test, rs):
rs.git.commit(message="Initial commit", allow_empty=True)
rs.helper(GitMasteryHelper).create_start_tag()
output = test.run()
assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [NO_MERGES])
With a remote repository
def test_remote_branch_rename() -> None:
with loader.start(include_remote_repo=True) as (test, rs, rs_remote):
remote_path = str(rs_remote.repo.git_dir)
rs.git.commit(message="Initial commit", allow_empty=True)
rs.git.remote_add("origin", remote_path)
rs.git.branch("feature/old")
rs.git.push("origin", "feature/old")
# Simulate the student's action
rs.git.branch("feature/new", old_branch="feature/old", move=True)
rs.git.push("origin", "feature/new")
rs.git.push("origin", ":feature/old")
output = test.run()
assert_output(output, GitAutograderStatus.SUCCESSFUL)
Starting from a cloned repository
def test_from_clone() -> None:
with loader.start(clone_from="https://github.com/git-mastery/some-repo") as (test, rs):
output = test.run()
assert_output(output, GitAutograderStatus.SUCCESSFUL)
See Testing guide for the full GitAutograderTestLoader API.