Skip to content

Commit a684151

Browse files
authored
Merge pull request #26 from git-mastery/fork-repo
Create fork-repo exercise
2 parents a244438 + 9579d59 commit a684151

File tree

8 files changed

+220
-0
lines changed

8 files changed

+220
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"exercise_name": "fork-repo",
3+
"tags": [
4+
"github-fork"
5+
],
6+
"requires_git": true,
7+
"requires_github": true,
8+
"base_files": {},
9+
"exercise_repo": {
10+
"repo_type": "local",
11+
"repo_name": "ignore-me",
12+
"repo_title": null,
13+
"create_fork": null,
14+
"init": true
15+
}
16+
}

fork_repo/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# fork-repo
2+
3+
<!--- Insert exercise description -->
4+
5+
## Task
6+
7+
Create a fork of the repo <https://github.com/git-mastery/gm-shapes> to your
8+
GitHub account.
9+
10+
Retain the original repo name `gm-shapes`.

fork_repo/__init__.py

Whitespace-only changes.

fork_repo/download.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
def setup(verbose: bool = False): ...

fork_repo/tests/__init__.py

Whitespace-only changes.

fork_repo/tests/specs/base.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
initialization:
2+
steps:
3+
- type: commit
4+
empty: true
5+
message: Empty commit
6+
id: start

fork_repo/tests/test_verify.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import json
2+
from pathlib import Path
3+
from typing import Optional
4+
from unittest.mock import patch
5+
6+
import pytest
7+
from git.repo import Repo
8+
from git_autograder import (
9+
GitAutograderExercise,
10+
GitAutograderTestLoader,
11+
GitAutograderWrongAnswerException,
12+
assert_output,
13+
)
14+
from git_autograder.status import GitAutograderStatus
15+
16+
from ..verify import IMPROPER_GH_CLI_SETUP, NO_FORK_FOUND, NOT_GIT_MASTERY_FORK, verify
17+
18+
REPOSITORY_NAME = "fork-repo"
19+
20+
loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify)
21+
22+
# NOTE: This exercise is a special case where we do not require repo-smith. Instead,
23+
# we directly mock function calls to verify that all branches are covered for us.
24+
25+
26+
# TODO: The current tooling isn't mature enough to handle mock GitAutograderExercise in
27+
# cases like these. We would ideally need some abstraction rather than creating our own.
28+
29+
30+
@pytest.fixture
31+
def exercise(tmp_path: Path) -> GitAutograderExercise:
32+
repo_dir = tmp_path / "ignore-me"
33+
repo_dir.mkdir()
34+
35+
Repo.init(repo_dir)
36+
with open(tmp_path / ".gitmastery-exercise.json", "a") as config_file:
37+
config_file.write(
38+
json.dumps(
39+
{
40+
"exercise_name": "remote-control",
41+
"tags": [],
42+
"requires_git": True,
43+
"requires_github": True,
44+
"base_files": {},
45+
"exercise_repo": {
46+
"repo_type": "local",
47+
"repo_name": "ignore-me",
48+
"init": True,
49+
"create_fork": None,
50+
"repo_title": None,
51+
},
52+
"downloaded_at": None,
53+
}
54+
)
55+
)
56+
57+
exercise = GitAutograderExercise(exercise_path=tmp_path)
58+
return exercise
59+
60+
61+
def test_pass(exercise: GitAutograderExercise):
62+
with (
63+
patch("fork_repo.verify.get_username", return_value="dummy"),
64+
patch("fork_repo.verify.has_fork", return_value=True),
65+
patch("fork_repo.verify.is_parent_git_mastery", return_value=True),
66+
):
67+
output = verify(exercise)
68+
assert_output(output, GitAutograderStatus.SUCCESSFUL)
69+
70+
71+
def test_improper_gh_setup(exercise: GitAutograderExercise):
72+
with (
73+
patch("fork_repo.verify.get_username", return_value=None),
74+
patch("fork_repo.verify.has_fork", return_value=True),
75+
patch("fork_repo.verify.is_parent_git_mastery", return_value=True),
76+
pytest.raises(GitAutograderWrongAnswerException) as exception,
77+
):
78+
verify(exercise)
79+
80+
assert exception.value.message == [IMPROPER_GH_CLI_SETUP]
81+
82+
83+
def test_no_fork(exercise: GitAutograderExercise):
84+
with (
85+
patch("fork_repo.verify.get_username", return_value="dummy"),
86+
patch("fork_repo.verify.has_fork", return_value=False),
87+
patch("fork_repo.verify.is_parent_git_mastery", return_value=True),
88+
pytest.raises(GitAutograderWrongAnswerException) as exception,
89+
):
90+
verify(exercise)
91+
92+
assert exception.value.message == [NO_FORK_FOUND]
93+
94+
95+
def test_not_right_parent(exercise: GitAutograderExercise):
96+
with (
97+
patch("fork_repo.verify.get_username", return_value="dummy"),
98+
patch("fork_repo.verify.has_fork", return_value=True),
99+
patch("fork_repo.verify.is_parent_git_mastery", return_value=False),
100+
pytest.raises(GitAutograderWrongAnswerException) as exception,
101+
):
102+
verify(exercise)
103+
104+
assert exception.value.message == [NOT_GIT_MASTERY_FORK]

fork_repo/verify.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import os
2+
import subprocess
3+
from typing import List, Optional
4+
5+
from git_autograder import (
6+
GitAutograderExercise,
7+
GitAutograderOutput,
8+
GitAutograderStatus,
9+
)
10+
11+
# NOTE: that we create functions for each command to allow unit testing to mock the
12+
# return values directly.
13+
14+
ORIGINAL_FORK_NAME = "gm-shapes"
15+
IMPROPER_GH_CLI_SETUP = "Your Github CLI is not setup correctly"
16+
NO_FORK_FOUND = f"No fork of git-mastery/{ORIGINAL_FORK_NAME} found. Remember to fork it from https://github.com/git-mastery/gm-shapes and keep the name as gm-shapes"
17+
NOT_GIT_MASTERY_FORK = f"Your fork was not from git-mastery/{ORIGINAL_FORK_NAME}. Remember to fork it from https://github.com/git-mastery/gm-shapes and keep the name as gm-shapes"
18+
19+
20+
def run_command(command: List[str]) -> Optional[str]:
21+
try:
22+
result = subprocess.run(
23+
command,
24+
capture_output=True,
25+
text=True,
26+
check=True,
27+
env=dict(os.environ, **{"GH_PAGER": "cat"}),
28+
)
29+
return result.stdout.strip()
30+
except subprocess.CalledProcessError:
31+
return None
32+
33+
34+
def get_username() -> Optional[str]:
35+
return run_command(["gh", "api", "user", "-q", ".login"])
36+
37+
38+
def has_fork(username: str) -> bool:
39+
result = run_command(
40+
[
41+
"gh",
42+
"repo",
43+
"view",
44+
f"{username}/{ORIGINAL_FORK_NAME}",
45+
"--json",
46+
"isFork",
47+
"--jq",
48+
".isFork",
49+
]
50+
)
51+
return result is not None and result == "true"
52+
53+
54+
def is_parent_git_mastery(username: str) -> bool:
55+
result = run_command(
56+
[
57+
"gh",
58+
"repo",
59+
"view",
60+
f"{username}/{ORIGINAL_FORK_NAME}",
61+
"--json",
62+
"parent",
63+
"--jq",
64+
".parent.owner.login",
65+
]
66+
)
67+
return result is not None and result == "git-mastery"
68+
69+
70+
def verify(exercise: GitAutograderExercise) -> GitAutograderOutput:
71+
username = get_username()
72+
if username is None:
73+
raise exercise.wrong_answer([IMPROPER_GH_CLI_SETUP])
74+
75+
if not has_fork(username):
76+
raise exercise.wrong_answer([NO_FORK_FOUND])
77+
78+
if not is_parent_git_mastery(username):
79+
raise exercise.wrong_answer([NOT_GIT_MASTERY_FORK])
80+
81+
return exercise.to_output(
82+
["Great work creating a fork with Github!"], GitAutograderStatus.SUCCESSFUL
83+
)

0 commit comments

Comments
 (0)