diff --git a/branch_previous/.gitmastery-exercise.json b/branch_previous/.gitmastery-exercise.json new file mode 100644 index 0000000..098ee28 --- /dev/null +++ b/branch_previous/.gitmastery-exercise.json @@ -0,0 +1,16 @@ +{ + "exercise_name": "branch-previous", + "tags": [ + "git-branch" + ], + "requires_git": true, + "requires_github": false, + "base_files": {}, + "exercise_repo": { + "repo_type": "local", + "repo_name": "horror-story", + "repo_title": null, + "create_fork": null, + "init": true + } +} \ No newline at end of file diff --git a/branch_previous/README.md b/branch_previous/README.md new file mode 100644 index 0000000..4132134 --- /dev/null +++ b/branch_previous/README.md @@ -0,0 +1,33 @@ +# branch-previous + +You are writing the outline for a story, in `story.txt`. + +You have written the first few steps of the storyline. + +You are not very happy with the way the story is progressing, and wish to explore a few alternative storylines starting from a previous step. + +## Task + +1. Start a new branch named `visitor-line`, starting from the second commit (i.e., commit `Add second line`). +2. Add the line `I heard someone knocking at the door`. to the `story.txt`. +3. Commit the change. You may use any suitable commit message. +4. Start a new branch named `sleep-line`, starting from the same starting point as before. +5. Add the line `I fell asleep on the couch`. to the `story.txt`. +6. Commit the change. You may use any suitable commit message. + +## Expected Revision Graph + +```mermaid +gitGraph BT: + commit id: "Describe night" + commit id: "Describe location" + branch visitor-line + checkout main + branch sleep-line + checkout main + commit id: "Mention noise" + checkout visitor-line + commit id: "Mention knocking" + checkout sleep-line + commit id: "Mention sleeping" +``` \ No newline at end of file diff --git a/branch_previous/__init__.py b/branch_previous/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/branch_previous/download.py b/branch_previous/download.py new file mode 100644 index 0000000..b49e3d9 --- /dev/null +++ b/branch_previous/download.py @@ -0,0 +1,30 @@ +from exercise_utils.file import create_or_update_file, append_to_file +from exercise_utils.git import add, commit + +def setup(verbose: bool = False): + create_or_update_file( + "story.txt", + """ + It was a dark and stormy night. + """ + ) + add(["story.txt"], verbose) + commit("Describe night", verbose) + + append_to_file( + "story.txt", + """ + I was alone in my room. + """ + ) + add(["story.txt"], verbose) + commit("Describe location", verbose) + + append_to_file( + "story.txt", + """ + I heard a strange noise. + """ + ) + add(["story.txt"], verbose) + commit("Mention noise", verbose) diff --git a/branch_previous/tests/__init__.py b/branch_previous/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/branch_previous/tests/specs/base.yml b/branch_previous/tests/specs/base.yml new file mode 100644 index 0000000..4ec8b3b --- /dev/null +++ b/branch_previous/tests/specs/base.yml @@ -0,0 +1,52 @@ +initialization: + steps: + - type: new-file + filename: story.txt + contents: "It was a dark and stormy night.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe night + id: start + - type: append-file + filename: story.txt + contents: "I was alone in my room.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe location + empty: false + id: describe_location_commit + - type: append-file + filename: story.txt + contents: "I heard a strange noise.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention noise + empty: false + - type: bash + runs: git checkout -b visitor-line HEAD~1 + - type: append-file + filename: story.txt + contents: "I heard someone knocking at the door.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention knocking + empty: false + - type: bash + runs: git checkout -b sleep-line HEAD~1 + - type: append-file + filename: story.txt + contents: "I fell asleep on the couch.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention sleeping + empty: false diff --git a/branch_previous/tests/specs/sleep_missing_branch.yml b/branch_previous/tests/specs/sleep_missing_branch.yml new file mode 100644 index 0000000..cab52a8 --- /dev/null +++ b/branch_previous/tests/specs/sleep_missing_branch.yml @@ -0,0 +1,43 @@ +initialization: + steps: + - type: new-file + filename: story.txt + contents: "It was a dark and stormy night.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe night + empty: false + id: start + - type: append-file + filename: story.txt + contents: "I was alone in my room.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe location + empty: false + id: describe_location_commit + - type: append-file + filename: story.txt + contents: "I heard a strange noise.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention noise + empty: false + # Only create visitor-line branch, not sleep-line + - type: bash + runs: git checkout -b visitor-line HEAD~1 + - type: append-file + filename: story.txt + contents: "I heard someone knocking at the door.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention knocking + empty: false diff --git a/branch_previous/tests/specs/sleep_wrong_content.yml b/branch_previous/tests/specs/sleep_wrong_content.yml new file mode 100644 index 0000000..8cd47ff --- /dev/null +++ b/branch_previous/tests/specs/sleep_wrong_content.yml @@ -0,0 +1,53 @@ +initialization: + steps: + - type: new-file + filename: story.txt + contents: "It was a dark and stormy night.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe night + empty: false + id: start + - type: append-file + filename: story.txt + contents: "I was alone in my room.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe location + empty: false + id: describe_location_commit + - type: append-file + filename: story.txt + contents: "I heard a strange noise.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention noise + empty: false + - type: bash + runs: git checkout -b visitor-line HEAD~1 + - type: append-file + filename: story.txt + contents: "I heard someone knocking at the door.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention knocking + empty: false + - type: bash + runs: git checkout -b sleep-line HEAD~1 + - type: append-file + filename: story.txt + contents: "Wrong content here.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention sleeping + empty: false diff --git a/branch_previous/tests/specs/visitor_missing_branch.yml b/branch_previous/tests/specs/visitor_missing_branch.yml new file mode 100644 index 0000000..4789ad3 --- /dev/null +++ b/branch_previous/tests/specs/visitor_missing_branch.yml @@ -0,0 +1,42 @@ +initialization: + steps: + - type: new-file + filename: story.txt + contents: "It was a dark and stormy night.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe night + id: start + - type: append-file + filename: story.txt + contents: "I was alone in my room.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe location + empty: false + id: second_commit + - type: append-file + filename: story.txt + contents: "I heard a strange noise.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention noise + empty: false + # Only create sleep-line branch, not visitor-line + - type: bash + runs: git checkout -b sleep-line HEAD~1 + - type: append-file + filename: story.txt + contents: "I fell asleep on the couch.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention sleeping + empty: false diff --git a/branch_previous/tests/specs/visitor_missing_commit.yml b/branch_previous/tests/specs/visitor_missing_commit.yml new file mode 100644 index 0000000..dc599ea --- /dev/null +++ b/branch_previous/tests/specs/visitor_missing_commit.yml @@ -0,0 +1,46 @@ +initialization: + steps: + - type: new-file + filename: story.txt + contents: "It was a dark and stormy night.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe night + empty: false + id: start + - type: append-file + filename: story.txt + contents: "I was alone in my room.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe location + empty: false + id: describe_location_commit + - type: append-file + filename: story.txt + contents: "I heard a strange noise.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention noise + empty: false + # visitor-line branch created but no commit made + - type: bash + runs: git checkout -b visitor-line HEAD~1 + # sleep-line branch created with commit + - type: bash + runs: git checkout -b sleep-line + - type: append-file + filename: story.txt + contents: "I fell asleep on the couch.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention sleeping + empty: false diff --git a/branch_previous/tests/specs/visitor_wrong_content.yml b/branch_previous/tests/specs/visitor_wrong_content.yml new file mode 100644 index 0000000..34e3bb8 --- /dev/null +++ b/branch_previous/tests/specs/visitor_wrong_content.yml @@ -0,0 +1,53 @@ +initialization: + steps: + - type: new-file + filename: story.txt + contents: "It was a dark and stormy night.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe night + empty: false + id: start + - type: append-file + filename: story.txt + contents: "I was alone in my room.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe location + empty: false + id: describe_location_commit + - type: append-file + filename: story.txt + contents: "I heard a strange noise.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention noise + empty: false + - type: bash + runs: git checkout -b visitor-line HEAD~1 + - type: append-file + filename: story.txt + contents: "Wrong content here.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention knocking + empty: false + - type: bash + runs: git checkout -b sleep-line HEAD~1 + - type: append-file + filename: story.txt + contents: "I fell asleep on my couch.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention sleeping + empty: false diff --git a/branch_previous/tests/specs/visitor_wrong_start_first_commit.yml b/branch_previous/tests/specs/visitor_wrong_start_first_commit.yml new file mode 100644 index 0000000..6946543 --- /dev/null +++ b/branch_previous/tests/specs/visitor_wrong_start_first_commit.yml @@ -0,0 +1,54 @@ +initialization: + steps: + - type: new-file + filename: story.txt + contents: "It was a dark and stormy night.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe night + empty: false + id: start + - type: append-file + filename: story.txt + contents: "I was alone in my room.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe location + empty: false + id: describe_location_commit + - type: append-file + filename: story.txt + contents: "I heard a strange noise.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention noise + empty: false + # visitor-line starts from wrong commit (first instead of second) + - type: bash + runs: git checkout -b visitor-line HEAD~2 + - type: append-file + filename: story.txt + contents: "I heard someone knocking at the door.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention knocking + empty: false + - type: bash + runs: git checkout -b sleep-line HEAD~1 + - type: append-file + filename: story.txt + contents: "I fell asleep on the couch.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention sleeping + empty: false diff --git a/branch_previous/tests/specs/visitor_wrong_start_third_commit.yml b/branch_previous/tests/specs/visitor_wrong_start_third_commit.yml new file mode 100644 index 0000000..c2f027c --- /dev/null +++ b/branch_previous/tests/specs/visitor_wrong_start_third_commit.yml @@ -0,0 +1,54 @@ +initialization: + steps: + - type: new-file + filename: story.txt + contents: "It was a dark and stormy night.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe night + empty: false + id: start + - type: append-file + filename: story.txt + contents: "I was alone in my room.\n" + - type: add + files: + - story.txt + - type: commit + message: Describe location + empty: false + id: describe_location_commit + - type: append-file + filename: story.txt + contents: "I heard a strange noise.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention noise + empty: false + # visitor-line starts from wrong commit (third instead of second) + - type: bash + runs: git checkout -b visitor-line + - type: append-file + filename: story.txt + contents: "I heard someone knocking at the door.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention knocking + empty: false + - type: bash + runs: git checkout -b sleep-line HEAD~2 + - type: append-file + filename: story.txt + contents: "I fell asleep on the couch.\n" + - type: add + files: + - story.txt + - type: commit + message: Mention sleeping + empty: false diff --git a/branch_previous/tests/test_verify.py b/branch_previous/tests/test_verify.py new file mode 100644 index 0000000..2630f5b --- /dev/null +++ b/branch_previous/tests/test_verify.py @@ -0,0 +1,92 @@ +from git_autograder import GitAutograderTestLoader, assert_output +from git_autograder.status import GitAutograderStatus + +from ..verify import ( + MISSING_BRANCH, + WRONG_CONTENT, + WRONG_START, + MISSING_COMMIT, + verify, +) + +REPOSITORY_NAME = "branch-previous" + +loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify) + + +def test_base(): + with loader.load("specs/base.yml") as output: + assert_output(output, GitAutograderStatus.SUCCESSFUL) + + +def test_visitor_missing_branch(): + with loader.load("specs/visitor_missing_branch.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [MISSING_BRANCH.format(branch_name="visitor-line")], + ) + + +def test_sleep_missing_branch(): + with loader.load("specs/sleep_missing_branch.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [MISSING_BRANCH.format(branch_name="sleep-line")], + ) + + +def test_visitor_wrong_start_first_commit(): + with loader.load("specs/visitor_wrong_start_first_commit.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [WRONG_START.format(branch_name="visitor-line")], + ) + + +def test_visitor_wrong_start_third_commit(): + with loader.load("specs/visitor_wrong_start_third_commit.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [WRONG_START.format(branch_name="visitor-line")], + ) + + +def test_visitor_wrong_content(): + with loader.load("specs/visitor_wrong_content.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ + WRONG_CONTENT.format( + branch_name="visitor-line", + expected_content="I heard someone knocking at the door.", + ) + ], + ) + + +def test_sleep_wrong_content(): + with loader.load("specs/sleep_wrong_content.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ + WRONG_CONTENT.format( + branch_name="sleep-line", + expected_content="I fell asleep on the couch.", + ) + ], + ) + + +def test_visitor_missing_commit(): + with loader.load("specs/visitor_missing_commit.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [MISSING_COMMIT.format(branch_name="visitor-line")], + ) diff --git a/branch_previous/verify.py b/branch_previous/verify.py new file mode 100644 index 0000000..99bbafb --- /dev/null +++ b/branch_previous/verify.py @@ -0,0 +1,91 @@ +from typing import List, Optional + +from git_autograder import ( + GitAutograderOutput, + GitAutograderExercise, + GitAutograderStatus, + GitAutograderCommit, +) + +MISSING_BRANCH = "The '{branch_name}' branch is missing." +MISSING_COMMIT = "No commits were made in the '{branch_name}' branch." +WRONG_START = ( + "The '{branch_name}' branch should start from the second commit " + "(with message 'Describe location')." +) +WRONG_CONTENT = ( + "The '{branch_name}' branch should have the line '{expected_content}' " + "added to story.txt." +) +SUCCESS_MESSAGE = ( + "Excellent work! You've successfully created branches from a " + "previous commit and explored alternative storylines!" +) + + +def get_commit_from_message(commits: List[GitAutograderCommit], message: str) -> Optional[GitAutograderCommit]: + """Find a commit with the given message from a list of commits.""" + for commit in commits: + if message.strip() == commit.commit.message.strip(): + return commit + return None + + +def verify_branch( + branch_name: str, + expected_start_commit: GitAutograderCommit, + expected_content: str, + exercise: GitAutograderExercise, +) -> None: + """ + Check that the given branch exists, starts from the expected commit, + and contains the expected content in story.txt. + """ + + # Check if branch exists + branch_helper = exercise.repo.branches + if not branch_helper.has_branch(branch_name): + raise exercise.wrong_answer([MISSING_BRANCH.format(branch_name=branch_name)]) + + branch = branch_helper.branch(branch_name) + latest_commit = branch.latest_commit + + # Check that user made commits in the branch + if latest_commit.hexsha == expected_start_commit.hexsha: + raise exercise.wrong_answer([MISSING_COMMIT.format(branch_name=branch_name)]) + + # Check that previous commit of latest commit is the expected start commit + if expected_start_commit.hexsha not in [parent.hexsha for parent in latest_commit.commit.parents]: + raise exercise.wrong_answer([WRONG_START.format(branch_name=branch_name)]) + + # Check that the expected content is in story.txt + with latest_commit.file("story.txt") as content: + if expected_content not in content: + raise exercise.wrong_answer([WRONG_CONTENT.format( + branch_name=branch_name, + expected_content=expected_content + )]) + + +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + commits = list(exercise.repo.branches.branch("main").commits) + describe_location_commit = get_commit_from_message(commits, "Describe location") + + verify_branch( + branch_name="visitor-line", + expected_start_commit=describe_location_commit, + expected_content="I heard someone knocking at the door.", + exercise=exercise + ) + + verify_branch( + branch_name="sleep-line", + expected_start_commit=describe_location_commit, + expected_content="I fell asleep on the couch.", + exercise=exercise + ) + + return exercise.to_output( + [SUCCESS_MESSAGE], + GitAutograderStatus.SUCCESSFUL, + )