diff --git a/merge_undo/.gitmastery-exercise.json b/merge_undo/.gitmastery-exercise.json new file mode 100644 index 0000000..60d3093 --- /dev/null +++ b/merge_undo/.gitmastery-exercise.json @@ -0,0 +1,18 @@ +{ + "exercise_name": "merge-undo", + "tags": [ + "git-branch", + "git-merge", + "git-reset" + ], + "requires_git": true, + "requires_github": false, + "base_files": {}, + "exercise_repo": { + "repo_type": "local", + "repo_name": "play-characters", + "repo_title": null, + "create_fork": null, + "init": true + } +} \ No newline at end of file diff --git a/merge_undo/README.md b/merge_undo/README.md new file mode 100644 index 0000000..e330c5e --- /dev/null +++ b/merge_undo/README.md @@ -0,0 +1,41 @@ +# merge-undo + +Scenario: You are keeping notes on the characters of a play that you are writing. In the main story line (which is in the `main` branch), you introduced two characters, Rick and Morty. You had two other characters in two separate branches `daughter` and `son-in-law`. Just now, you introduced these two characters to the main story line by merging the two branches to the `main` branch. + +```mermaid +gitGraph + commit id: "Add Rick" + commit id: "Add morty" + branch daughter + branch son-in-law + checkout daughter + commit id: "Add Beth" + checkout son-in-law + commit id: "Add Jerry" + checkout main + commit id: "Mention Morty is grandson" + merge daughter id: "Introduce Beth" + merge son-in-law id: "Introduce Jerry" +``` + +However, now you realise this is premature, and wish to undo that change. + +## Task + +Undo the merging of branches `son-in-law` and `daughter`. + +The result should be as follows: + +```mermaid +gitGraph + commit id: "Add Rick" + commit id: "Add morty" + branch daughter + branch son-in-law + checkout daughter + commit id: "Add Beth" + checkout son-in-law + commit id: "Add Jerry" + checkout main + commit id: "Mention Morty is grandson" +``` diff --git a/merge_undo/__init__.py b/merge_undo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/merge_undo/download.py b/merge_undo/download.py new file mode 100644 index 0000000..614ad4a --- /dev/null +++ b/merge_undo/download.py @@ -0,0 +1,68 @@ +__resources__ = {"README.md": "README.md"} + +from exercise_utils.file import create_or_update_file +from exercise_utils.git import add, checkout, commit, merge_with_message +from exercise_utils.gitmastery import create_start_tag + + +def setup(verbose: bool = False): + create_start_tag(verbose) + + # main branch + checkout("main", False, verbose) + create_or_update_file( + "rick.txt", + """ + Scientist + """, + ) + add(["rick.txt"], verbose) + commit("Add Rick", verbose) + create_or_update_file( + "morty.txt", + """ + Boy + """, + ) + add(["morty.txt"], verbose) + commit("Add morty", verbose) + + # daughter branch + checkout("daughter", True, verbose) + create_or_update_file( + "beth.txt", + """ + Vet + """, + ) + add(["beth.txt"], verbose) + commit("Add Beth", verbose) + + # son-in-law branch + checkout("main", False, verbose) # switch to main first + + checkout("son-in-law", True, verbose) + create_or_update_file( + "jerry.txt", + """ + Salesman + """, + ) + add(["jerry.txt"], verbose) + commit("Add Herry", verbose) + + # Append morty as a grandson + checkout("main", False, verbose) + create_or_update_file( + "morty.txt", + """ + Boy + Grandson + """, + ) + add(["morty.txt"], verbose) + commit("Mention Morty is grandson", verbose) + + # Merge daughter and son-in-law to main story + merge_with_message("daughter", False, "Introduce Beth", verbose) + merge_with_message("son-in-law", False, "Introduce Jerry", verbose) diff --git a/merge_undo/res/README.md b/merge_undo/res/README.md new file mode 100644 index 0000000..e330c5e --- /dev/null +++ b/merge_undo/res/README.md @@ -0,0 +1,41 @@ +# merge-undo + +Scenario: You are keeping notes on the characters of a play that you are writing. In the main story line (which is in the `main` branch), you introduced two characters, Rick and Morty. You had two other characters in two separate branches `daughter` and `son-in-law`. Just now, you introduced these two characters to the main story line by merging the two branches to the `main` branch. + +```mermaid +gitGraph + commit id: "Add Rick" + commit id: "Add morty" + branch daughter + branch son-in-law + checkout daughter + commit id: "Add Beth" + checkout son-in-law + commit id: "Add Jerry" + checkout main + commit id: "Mention Morty is grandson" + merge daughter id: "Introduce Beth" + merge son-in-law id: "Introduce Jerry" +``` + +However, now you realise this is premature, and wish to undo that change. + +## Task + +Undo the merging of branches `son-in-law` and `daughter`. + +The result should be as follows: + +```mermaid +gitGraph + commit id: "Add Rick" + commit id: "Add morty" + branch daughter + branch son-in-law + checkout daughter + commit id: "Add Beth" + checkout son-in-law + commit id: "Add Jerry" + checkout main + commit id: "Mention Morty is grandson" +``` diff --git a/merge_undo/tests/__init__.py b/merge_undo/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/merge_undo/tests/specs/base.yml b/merge_undo/tests/specs/base.yml new file mode 100644 index 0000000..1996521 --- /dev/null +++ b/merge_undo/tests/specs/base.yml @@ -0,0 +1,60 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + + # Add rick.txt and commit + - type: new-file + filename: rick.txt + contents: Scientist + - type: commit + message: Add Rick + + # Add morty.txt and commit + - type: new-file + filename: morty.txt + contents: Boy + - type: commit + message: Add morty + id: morty-initial-commit + + # Create 'daughter' branch, add beth.txt and commit + - type: branch + branch-name: daughter + from-commit: morty-initial-commit + - type: checkout + branch-name: daughter + - type: new-file + filename: beth.txt + contents: Vet + - type: commit + message: Add Beth + + # Create 'son-in-law' branch, add jerry.txt and commit + - type: checkout + branch-name: main + - type: branch + branch-name: son-in-law + from-commit: morty-initial-commit + - type: checkout + branch-name: son-in-law + - type: new-file + filename: jerry.txt + contents: Salesman + - type: commit + message: Add Jerry + + # Checkout back to main, edit morty.txt and commit. + - type: checkout + branch-name: main + - type: edit-file + filename: morty.txt + contents: | + Boy + Grandson + - type: commit + message: Mention Morty is grandson + id: final-main-commit + diff --git a/merge_undo/tests/specs/detached_head.yml b/merge_undo/tests/specs/detached_head.yml new file mode 100644 index 0000000..e2991c3 --- /dev/null +++ b/merge_undo/tests/specs/detached_head.yml @@ -0,0 +1,51 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: new-file + filename: rick.txt + contents: Scientist + - type: commit + message: Add Rick + - type: new-file + filename: morty.txt + contents: Boy + - type: commit + message: Add morty + id: morty-initial-commit + - type: branch + branch-name: daughter + from-commit: morty-initial-commit + - type: checkout + branch-name: daughter + - type: new-file + filename: beth.txt + contents: Vet + - type: commit + message: Add Beth + - type: checkout + branch-name: main + - type: branch + branch-name: son-in-law + from-commit: morty-initial-commit + - type: checkout + branch-name: son-in-law + - type: new-file + filename: jerry.txt + contents: Salesman + - type: commit + message: Add Jerry + - type: checkout + branch-name: main + - type: edit-file + filename: morty.txt + contents: | + Boy + Grandson + - type: commit + message: Mention Morty is grandson + - type: checkout + commit-hash: main + diff --git a/merge_undo/tests/specs/main_wrong_commit.yml b/merge_undo/tests/specs/main_wrong_commit.yml new file mode 100644 index 0000000..598b9c5 --- /dev/null +++ b/merge_undo/tests/specs/main_wrong_commit.yml @@ -0,0 +1,42 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: new-file + filename: rick.txt + contents: Scientist + - type: commit + message: Add Rick + - type: new-file + filename: morty.txt + contents: Boy + - type: commit + message: Add morty + id: morty-initial-commit + - type: branch + branch-name: daughter + from-commit: morty-initial-commit + - type: checkout + branch-name: daughter + - type: new-file + filename: beth.txt + contents: Vet + - type: commit + message: Add Beth + - type: checkout + branch-name: main + - type: branch + branch-name: son-in-law + from-commit: morty-initial-commit + - type: checkout + branch-name: son-in-law + - type: new-file + filename: jerry.txt + contents: Salesman + - type: commit + message: Add Jerry + - type: checkout + branch-name: main + diff --git a/merge_undo/tests/specs/merges_not_undone.yml b/merge_undo/tests/specs/merges_not_undone.yml new file mode 100644 index 0000000..227dbc5 --- /dev/null +++ b/merge_undo/tests/specs/merges_not_undone.yml @@ -0,0 +1,56 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: new-file + filename: rick.txt + contents: Scientist + - type: commit + message: Add Rick + - type: new-file + filename: morty.txt + contents: Boy + - type: commit + message: Add morty + id: morty-initial-commit + - type: branch + branch-name: daughter + from-commit: morty-initial-commit + - type: checkout + branch-name: daughter + - type: new-file + filename: beth.txt + contents: Vet + - type: commit + message: Add Beth + - type: checkout + branch-name: main + - type: branch + branch-name: son-in-law + from-commit: morty-initial-commit + - type: checkout + branch-name: son-in-law + - type: new-file + filename: jerry.txt + contents: Salesman + - type: commit + message: Add Jerry + - type: checkout + branch-name: main + - type: edit-file + filename: morty.txt + contents: | + Boy + Grandson + - type: commit + message: Mention Morty is grandson + - type: merge + branch-name: daughter + message: Introduce Beth + no-ff: true + - type: merge + branch-name: son-in-law + message: introduce Jerry + no-ff: true diff --git a/merge_undo/tests/specs/not_main.yml b/merge_undo/tests/specs/not_main.yml new file mode 100644 index 0000000..68995de --- /dev/null +++ b/merge_undo/tests/specs/not_main.yml @@ -0,0 +1,50 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: new-file + filename: rick.txt + contents: Scientist + - type: commit + message: Add Rick + - type: new-file + filename: morty.txt + contents: Boy + - type: commit + message: Add morty + id: morty-initial-commit + - type: branch + branch-name: daughter + from-commit: morty-initial-commit + - type: checkout + branch-name: daughter + - type: new-file + filename: beth.txt + contents: Vet + - type: commit + message: Add Beth + - type: checkout + branch-name: main + - type: branch + branch-name: son-in-law + from-commit: morty-initial-commit + - type: checkout + branch-name: son-in-law + - type: new-file + filename: jerry.txt + contents: Salesman + - type: commit + message: Add Jerry + - type: checkout + branch-name: main + - type: edit-file + filename: morty.txt + contents: | + Boy + Grandson + - type: commit + message: Mention Morty is grandson + - type: checkout + branch-name: daughter diff --git a/merge_undo/tests/test_verify.py b/merge_undo/tests/test_verify.py new file mode 100644 index 0000000..9c194e9 --- /dev/null +++ b/merge_undo/tests/test_verify.py @@ -0,0 +1,51 @@ +from git_autograder import GitAutograderStatus, GitAutograderTestLoader, assert_output + +from ..verify import ( + NOT_ON_MAIN, + RESET_MESSAGE, + DETACHED_HEAD, + MERGES_NOT_UNDONE, + MAIN_WRONG_COMMIT, + verify, +) + +REPOSITORY_NAME = "merge-undo" + +loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify) + + +def test_base(): + with loader.load("specs/base.yml", "start") as output: + assert_output(output, GitAutograderStatus.SUCCESSFUL) + + +def test_merges_not_undone(): + with loader.load("specs/merges_not_undone.yml", "start") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [MERGES_NOT_UNDONE, RESET_MESSAGE], + ) + + +def test_detached_head(): + with loader.load("specs/detached_head.yml", "start") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [DETACHED_HEAD, RESET_MESSAGE], + ) + + +def test_main_wrong_commit(): + with loader.load("specs/main_wrong_commit.yml", "start") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [MAIN_WRONG_COMMIT, RESET_MESSAGE], + ) + + +def test_not_main(): + with loader.load("specs/not_main.yml") as output: + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [NOT_ON_MAIN]) diff --git a/merge_undo/verify.py b/merge_undo/verify.py new file mode 100644 index 0000000..d4598d2 --- /dev/null +++ b/merge_undo/verify.py @@ -0,0 +1,46 @@ +from git_autograder import ( + GitAutograderOutput, + GitAutograderExercise, + GitAutograderStatus, +) + +NOT_ON_MAIN = ( + "You aren't currently on the main branch. Checkout to that branch and try again!" +) +DETACHED_HEAD = "You should not be in a detached HEAD state! Use git checkout main to get back to main" +MERGES_NOT_UNDONE = ( + "It appears the merge commits are still in the history of the 'main' branch. This shouldn't be the case" +) +MAIN_WRONG_COMMIT = "The 'main' branch is not pointing to the correct commit. It should be pointing to the commit made just before the merges." +RESET_MESSAGE = 'Reset the repository using "gitmastery progress reset" and start again' +SUCCESS_MESSAGE = "Great work with undoing the merges! Try listing the directory to see what has changed." + + +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + """ + Verifies that the user has successfully undone the last two merges on the main branch. + """ + repo = exercise.repo.repo + + try: + if repo.active_branch.name != "main": + raise exercise.wrong_answer([NOT_ON_MAIN]) + except TypeError: + raise exercise.wrong_answer([DETACHED_HEAD, RESET_MESSAGE]) + + main_branch = exercise.repo.branches.branch("main") + main_history = main_branch.commits + + if any(len(c.commit.parents) > 1 for c in main_history): + raise exercise.wrong_answer([MERGES_NOT_UNDONE, RESET_MESSAGE]) + + main_head_commit = main_branch.latest_commit + expected_commit_message = "Mention Morty is grandson" + if main_head_commit.commit.message.strip() != expected_commit_message: + raise exercise.wrong_answer([MAIN_WRONG_COMMIT, RESET_MESSAGE]) + + return exercise.to_output( + [SUCCESS_MESSAGE], + GitAutograderStatus.SUCCESSFUL, + ) +