From d379f50bf0e3b41cbdfa9e62266b04db4ffb32c3 Mon Sep 17 00:00:00 2001 From: desmondwong1215 Date: Mon, 23 Mar 2026 10:48:32 +0800 Subject: [PATCH 1/4] Implement download --- create_pr_from_main/.gitmastery-exercise.json | 14 ++++++++++++++ create_pr_from_main/README.md | 1 + create_pr_from_main/__init__.py | 0 create_pr_from_main/download.py | 8 ++++++++ 4 files changed, 23 insertions(+) create mode 100644 create_pr_from_main/.gitmastery-exercise.json create mode 100644 create_pr_from_main/README.md create mode 100644 create_pr_from_main/__init__.py create mode 100644 create_pr_from_main/download.py diff --git a/create_pr_from_main/.gitmastery-exercise.json b/create_pr_from_main/.gitmastery-exercise.json new file mode 100644 index 00000000..214cf4ac --- /dev/null +++ b/create_pr_from_main/.gitmastery-exercise.json @@ -0,0 +1,14 @@ +{ + "exercise_name": "create-pr-from-main", + "tags": [], + "requires_git": true, + "requires_github": true, + "base_files": {}, + "exercise_repo": { + "repo_type": "remote", + "repo_name": "languages", + "repo_title": "gm-languages", + "create_fork": true, + "init": null + } +} \ No newline at end of file diff --git a/create_pr_from_main/README.md b/create_pr_from_main/README.md new file mode 100644 index 00000000..3e6955ee --- /dev/null +++ b/create_pr_from_main/README.md @@ -0,0 +1 @@ +See https://git-mastery.github.io/lessons/prsCreate/exercise-create-pr-from-main.html diff --git a/create_pr_from_main/__init__.py b/create_pr_from_main/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/create_pr_from_main/download.py b/create_pr_from_main/download.py new file mode 100644 index 00000000..d559aa84 --- /dev/null +++ b/create_pr_from_main/download.py @@ -0,0 +1,8 @@ +from pathlib import Path + +from exercise_utils.exercise_config import add_pr_config +from exercise_utils.gitmastery import create_start_tag + +def setup(verbose: bool = False): + create_start_tag(verbose) + add_pr_config(pr_repo_full_name="git-mastery/gm-languages", config_path=Path("../")) \ No newline at end of file From 065ff57f4264bb87dda26cb489ae25ad2a714256 Mon Sep 17 00:00:00 2001 From: desmondwong1215 Date: Mon, 23 Mar 2026 10:48:42 +0800 Subject: [PATCH 2/4] Implement verify --- create_pr_from_main/verify.py | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 create_pr_from_main/verify.py diff --git a/create_pr_from_main/verify.py b/create_pr_from_main/verify.py new file mode 100644 index 00000000..4d2f79a5 --- /dev/null +++ b/create_pr_from_main/verify.py @@ -0,0 +1,53 @@ +from pathlib import Path + +from git_autograder import ( + GitAutograderOutput, + GitAutograderExercise, + GitAutograderStatus, +) + +from exercise_utils.exercise_config import add_pr_config +from exercise_utils.github_cli import get_github_username, get_pr_numbers_by_author + + +JAVA_FILE_MISSING = "Java.txt file is missing in the latest commit on main branch." +JAVA_INVALID_CONTENT = "The content in Java.txt in main branch is not correct." +MUTIPLE_PRS = "Multiple PRs found. The lastest pr will be used in grading." +PR_MISSING = "No PR is found." +WRONG_HEAD_BRANCH = "The PR's head branch is not 'main'." + + +EXPECTED_CONTENT_STEP_3 = ["1955, by James Gosling"] + + +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + username = get_github_username(False) + target_repo = f"git-mastery/{exercise.config.exercise_repo.repo_title}" + comments = [] + + pr_numbers = get_pr_numbers_by_author(username, target_repo, False) + if not pr_numbers: + raise exercise.wrong_answer([PR_MISSING]) + if len(pr_numbers) > 1: + comments.append(MUTIPLE_PRS) + pr_number = pr_numbers[-1] + + add_pr_config(pr_number=pr_number, config_path=Path("./")) + exercise.fetch_pr() + + if exercise.repo.prs.pr.head_branch != "main": + comments.append(WRONG_HEAD_BRANCH) + raise exercise.wrong_answer(comments) + + latest_user_commit = exercise.repo.prs.pr.last_user_commit + with latest_user_commit.file("Java.txt") as content: + if content is None: + comments.append(JAVA_FILE_MISSING) + raise exercise.wrong_answer(comments) + extracted_content = [line.strip() for line in content.splitlines() if line.strip() != ""] + if extracted_content != EXPECTED_CONTENT_STEP_3: + comments.append(JAVA_INVALID_CONTENT) + raise exercise.wrong_answer(comments) + + comments.append("Good job creating the PR and pushing commits!") + return exercise.to_output(comments, GitAutograderStatus.SUCCESSFUL) From 7f2be6bbcb8750fbca86db0e56229fd05e2ca65a Mon Sep 17 00:00:00 2001 From: desmondwong1215 Date: Mon, 23 Mar 2026 10:48:52 +0800 Subject: [PATCH 3/4] Implement test-verify --- create_pr_from_main/test_verify.py | 166 +++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 create_pr_from_main/test_verify.py diff --git a/create_pr_from_main/test_verify.py b/create_pr_from_main/test_verify.py new file mode 100644 index 00000000..a1f86e06 --- /dev/null +++ b/create_pr_from_main/test_verify.py @@ -0,0 +1,166 @@ +import json +from contextlib import contextmanager +from pathlib import Path +from unittest.mock import PropertyMock, patch + +import pytest +from exercise_utils.test import assert_output +from git.repo import Repo +from git_autograder import ( + GitAutograderExercise, + GitAutograderStatus, + GitAutograderWrongAnswerException, +) +from git_autograder.pr import GitAutograderPr + +from .verify import ( + EXPECTED_CONTENT_STEP_3, + JAVA_FILE_MISSING, + JAVA_INVALID_CONTENT, + PR_MISSING, + WRONG_HEAD_BRANCH, + verify, +) + + +@pytest.fixture +def exercise(tmp_path: Path) -> GitAutograderExercise: + repo_dir = tmp_path / "ignore-me" + repo_dir.mkdir() + Repo.init(repo_dir) + + with open(tmp_path / ".gitmastery-exercise.json", "a") as config_file: + config_file.write( + json.dumps( + { + "exercise_name": "create_pr_from_main", + "tags": [], + "requires_git": True, + "requires_github": True, + "base_files": {}, + "exercise_repo": { + "repo_type": "local", + "repo_name": "ignore-me", + "init": True, + "create_fork": None, + "repo_title": "gm-shapes", + "pr_number": 1, + "pr_repo_full_name": "dummy/repo", + }, + "downloaded_at": None, + } + ) + ) + + with patch( + "git_autograder.pr.fetch_pull_request_data", + return_value={ + "title": "", + "body": "", + "state": "OPEN", + "author": {"login": "dummy"}, + "baseRefName": "main", + "headRefName": "main", + "isDraft": False, + "mergedAt": None, + "mergedBy": None, + "createdAt": None, + "latestReviews": {"nodes": []}, + "comments": {"nodes": []}, + "commits": {"nodes": []}, + }, + ): + return GitAutograderExercise(exercise_path=tmp_path) + + +class FakeCommit: + def __init__(self, java_content: str | None) -> None: + self._java_content = java_content + + @contextmanager + def file(self, file_path: str): + yield self._java_content + + +def _run_verify( + exercise: GitAutograderExercise, + pr_numbers: list[int] = [], + head_branch: str = "", + java_content: str | None = None, +): + fake_commit = FakeCommit(java_content) + with ( + patch("create_pr_from_main.verify.get_github_username", return_value="dummy"), + patch( + "create_pr_from_main.verify.get_pr_numbers_by_author", + return_value=pr_numbers, + ), + patch("create_pr_from_main.verify.add_pr_config"), + patch.object(exercise, "fetch_pr", return_value=None), + patch.object( + GitAutograderPr, + "head_branch", + new_callable=PropertyMock, + return_value=head_branch, + ), + patch.object( + GitAutograderPr, + "last_user_commit", + new_callable=PropertyMock, + return_value=fake_commit, + ), + ): + return verify(exercise) + + +def test_success(exercise: GitAutograderExercise): + output = _run_verify( + exercise, + pr_numbers=[123], + head_branch="main", + java_content="\n".join(EXPECTED_CONTENT_STEP_3), + ) + + assert_output(output, GitAutograderStatus.SUCCESSFUL) + + +def test_pr_missing(exercise: GitAutograderExercise): + with pytest.raises(GitAutograderWrongAnswerException) as exception: + _run_verify(exercise) + + assert exception.value.message == [PR_MISSING] + + +def test_wrong_head_branch(exercise: GitAutograderExercise): + with pytest.raises(GitAutograderWrongAnswerException) as exception: + _run_verify( + exercise, + pr_numbers=[1], + head_branch="feature/pr-branch" + ) + + assert exception.value.message == [WRONG_HEAD_BRANCH] + + +def test_java_file_missing(exercise: GitAutograderExercise): + with pytest.raises(GitAutograderWrongAnswerException) as exception: + _run_verify( + exercise, + pr_numbers=[1], + head_branch="main", + java_content=None, + ) + + assert exception.value.message == [JAVA_FILE_MISSING] + + +def test_java_content_invalid(exercise: GitAutograderExercise): + with pytest.raises(GitAutograderWrongAnswerException) as exception: + _run_verify( + exercise, + pr_numbers=[1], + head_branch="main", + java_content="wrong content\n", + ) + + assert exception.value.message == [JAVA_INVALID_CONTENT] From 4fdf2149c66facd018e7bec60d6f76567de97733 Mon Sep 17 00:00:00 2001 From: desmondwong1215 Date: Thu, 26 Mar 2026 08:10:20 +0800 Subject: [PATCH 4/4] Remove unnecessary fields in test_verify --- create_pr_from_main/test_verify.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/create_pr_from_main/test_verify.py b/create_pr_from_main/test_verify.py index a1f86e06..77975b19 100644 --- a/create_pr_from_main/test_verify.py +++ b/create_pr_from_main/test_verify.py @@ -54,21 +54,7 @@ def exercise(tmp_path: Path) -> GitAutograderExercise: with patch( "git_autograder.pr.fetch_pull_request_data", - return_value={ - "title": "", - "body": "", - "state": "OPEN", - "author": {"login": "dummy"}, - "baseRefName": "main", - "headRefName": "main", - "isDraft": False, - "mergedAt": None, - "mergedBy": None, - "createdAt": None, - "latestReviews": {"nodes": []}, - "comments": {"nodes": []}, - "commits": {"nodes": []}, - }, + return_value={}, ): return GitAutograderExercise(exercise_path=tmp_path)