From bbf8d54a10653b956aa4cbea880ae5ae69c43ee4 Mon Sep 17 00:00:00 2001 From: "Jiahao, Woo" Date: Fri, 3 Oct 2025 20:28:52 +0100 Subject: [PATCH 1/4] [mix-messy-graph] Add setup --- mix_messy_graph/.gitmastery-exercise.json | 17 +++++++++++++++++ mix_messy_graph/README.md | 18 ++++++++++++++++++ mix_messy_graph/__init__.py | 0 mix_messy_graph/download.py | 10 ++++++++++ mix_messy_graph/tests/__init__.py | 0 mix_messy_graph/tests/specs/base.yml | 6 ++++++ mix_messy_graph/tests/test_verify.py | 12 ++++++++++++ mix_messy_graph/verify.py | 11 +++++++++++ 8 files changed, 74 insertions(+) create mode 100644 mix_messy_graph/.gitmastery-exercise.json create mode 100644 mix_messy_graph/README.md create mode 100644 mix_messy_graph/__init__.py create mode 100644 mix_messy_graph/download.py create mode 100644 mix_messy_graph/tests/__init__.py create mode 100644 mix_messy_graph/tests/specs/base.yml create mode 100644 mix_messy_graph/tests/test_verify.py create mode 100644 mix_messy_graph/verify.py diff --git a/mix_messy_graph/.gitmastery-exercise.json b/mix_messy_graph/.gitmastery-exercise.json new file mode 100644 index 0000000..2fc799f --- /dev/null +++ b/mix_messy_graph/.gitmastery-exercise.json @@ -0,0 +1,17 @@ +{ + "exercise_name": "mix-messy-graph", + "tags": [ + "git-branch", + "git-merge" + ], + "requires_git": true, + "requires_github": true, + "base_files": {}, + "exercise_repo": { + "repo_type": "remote", + "repo_name": "user-docs", + "repo_title": "gm-user-docs", + "create_fork": false, + "init": null + } +} \ No newline at end of file diff --git a/mix_messy_graph/README.md b/mix_messy_graph/README.md new file mode 100644 index 0000000..7b34c7e --- /dev/null +++ b/mix_messy_graph/README.md @@ -0,0 +1,18 @@ +# mix-messy-graph + + + +## Task + + + +## Hints + + + diff --git a/mix_messy_graph/__init__.py b/mix_messy_graph/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mix_messy_graph/download.py b/mix_messy_graph/download.py new file mode 100644 index 0000000..f480bf0 --- /dev/null +++ b/mix_messy_graph/download.py @@ -0,0 +1,10 @@ +from exercise_utils.git import merge, merge_with_message, track_remote_branch + + +def setup(verbose: bool = False): + remote_name = "origin" + remote_branches = ["feature-search", "feature-delete", "list"] + for remote_branch_name in remote_branches: + track_remote_branch(remote_name, remote_branch_name, verbose) + + merge_with_message("feature-search", False, "Merge search feature", verbose) diff --git a/mix_messy_graph/tests/__init__.py b/mix_messy_graph/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mix_messy_graph/tests/specs/base.yml b/mix_messy_graph/tests/specs/base.yml new file mode 100644 index 0000000..00c3a53 --- /dev/null +++ b/mix_messy_graph/tests/specs/base.yml @@ -0,0 +1,6 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start diff --git a/mix_messy_graph/tests/test_verify.py b/mix_messy_graph/tests/test_verify.py new file mode 100644 index 0000000..7d9c4ed --- /dev/null +++ b/mix_messy_graph/tests/test_verify.py @@ -0,0 +1,12 @@ +from git_autograder import GitAutograderTestLoader + +from ..verify import verify + +REPOSITORY_NAME = "mix-messy-graph" + +loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify) + + +def test_base(): + with loader.load("specs/base.yml", "start"): + pass diff --git a/mix_messy_graph/verify.py b/mix_messy_graph/verify.py new file mode 100644 index 0000000..1288d3d --- /dev/null +++ b/mix_messy_graph/verify.py @@ -0,0 +1,11 @@ +from git_autograder import ( + GitAutograderOutput, + GitAutograderExercise, + GitAutograderStatus, +) + + +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + # INSERT YOUR GRADING CODE HERE + + return exercise.to_output([], GitAutograderStatus.SUCCESSFUL) From 2f995c6483b68218295b32a7bc35b62692463b60 Mon Sep 17 00:00:00 2001 From: "Jiahao, Woo" Date: Fri, 3 Oct 2025 20:34:15 +0100 Subject: [PATCH 2/4] [mix-messy-graph] Add README --- mix_messy_graph/README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/mix_messy_graph/README.md b/mix_messy_graph/README.md index 7b34c7e..eca41c1 100644 --- a/mix_messy_graph/README.md +++ b/mix_messy_graph/README.md @@ -1,18 +1,20 @@ # mix-messy-graph - +You are writing user documentation for a product. You have already written documentation for a few new features, each in a separate branch. After merging the `feature-search` branch, you realise this way of merging can result in a complicated revision graph. Instead, you wish to merge these changes in a way that results in a simple linear revision graph. ## Task - +1. Undo the merging of `feature-search`. +2. Squash-merge the `feature-search` branch onto the `main` branch. Delete the `feature-search` branch. +3. Similarly, squash-merge and delete the `feature-delete` branch, while resolving any merge conflicts -- in the `features.md`, the delete feature should appear after the search feature. +4. The `list` branch is not needed, as you have decided not to have that feature. Delete that branch. -## Hints +The resulting revision graph should be as follows: - - +```mermaid +gitGraph BT: + commit id: "Add features.md" + commit id: "Mention feature for creating books" tag: "v10" + commit id: "Add the delete feature" + commit id: "Add the search feature" +``` From 6a88d56496056ebec900e588dd9d20c34f840df4 Mon Sep 17 00:00:00 2001 From: "Jiahao, Woo" Date: Fri, 3 Oct 2025 21:05:26 +0100 Subject: [PATCH 3/4] [mix-messy-graph] Implement verification --- mix_messy_graph/verify.py | 87 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/mix_messy_graph/verify.py b/mix_messy_graph/verify.py index 1288d3d..dfdbc1c 100644 --- a/mix_messy_graph/verify.py +++ b/mix_messy_graph/verify.py @@ -3,9 +3,94 @@ GitAutograderExercise, GitAutograderStatus, ) +from itertools import zip_longest + +SQUASH_NOT_USED = ( + "You should be using squash merges for both 'feature-search' and 'feature-delete'" +) +WRONG_ORDER_OF_MERGING = "You need to merge 'feature-search' before 'feature-delete'" +FEATURE_SEARCH_MERGE_MESSAGE = ( + "The message for merging 'feature-search' should be 'Add the search feature'" +) +FEATURE_DELETE_MERGE_MESSAGE = ( + "The message for merging 'feature-delete' should be 'Add the delete feature'" +) +MISMATCH_COMMIT_MESSAGE = ( + "Expected commit message of '{expected}', got '{given}' instead." +) + +FEATURE_SEARCH_BRANCH_STILL_EXISTS = "Branch 'feature-search' still exists." +FEATURE_DELETE_BRANCH_STILL_EXISTS = "Branch 'feature-delete' still exists." +LIST_BRANCH_STILL_EXISTS = "Branch 'list' still exists." + +MISSING_FEATURES_FILE = "You are missing 'features.md'!" +FEATURES_FILE_CONTENT_INVALID = "Contents of 'features.md' is not valid! Try again!" + +EXPECTED_COMMIT_MESSAGES = [ + "Add features.md", + "Mention feature for creating books", + "Fix phrasing of heading", + "Add the search feature", + "Add the delete feature", +] + +EXPECTED_LINES = [ + "# Features", + "## Create Book", + "Allows creating one book at a time.", + "## Searching for Books", + "Allows searching for books by keywords.", + "Works only for book titles.", + "## Deleting Books", + "Allows deleting books.", +] + + +def ensure_str(val) -> str: + if isinstance(val, bytes): + return val.decode("utf-8", errors="replace").strip() + return str(val).strip() def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: - # INSERT YOUR GRADING CODE HERE + main_branch = exercise.repo.branches.branch("main") + merge_commits = [c for c in main_branch.commits if len(c.parents) > 1] + if len(merge_commits) > 0: + raise exercise.wrong_answer([SQUASH_NOT_USED]) + + commit_messages = [ensure_str(c.commit.message) for c in main_branch.commits][::-1] + for expected, given in zip_longest(EXPECTED_COMMIT_MESSAGES, commit_messages): + if expected != given: + raise exercise.wrong_answer( + [ + MISMATCH_COMMIT_MESSAGE.format( + expected=expected, given=(given or "") + ) + ] + ) + + feature_search_branch = exercise.repo.branches.branch_or_none("feature-search") + feature_delete_branch = exercise.repo.branches.branch_or_none("feature-delete") + list_branch = exercise.repo.branches.branch_or_none("list") + branch_exists_messages = [] + if feature_search_branch is not None: + branch_exists_messages.append(FEATURE_SEARCH_BRANCH_STILL_EXISTS) + if feature_delete_branch is not None: + branch_exists_messages.append(FEATURE_DELETE_BRANCH_STILL_EXISTS) + if list_branch is not None: + branch_exists_messages.append(LIST_BRANCH_STILL_EXISTS) + + if branch_exists_messages: + raise exercise.wrong_answer(branch_exists_messages) + + with exercise.repo.files.file_or_none("features.md") as features_file: + if features_file is None: + raise exercise.wrong_answer([MISSING_FEATURES_FILE]) + + contents = [ + line.strip() for line in features_file.readlines() if line.strip() != "" + ] + if contents != EXPECTED_LINES: + raise exercise.wrong_answer([FEATURES_FILE_CONTENT_INVALID]) return exercise.to_output([], GitAutograderStatus.SUCCESSFUL) From 22f7a817b5572ddd69f6b5fb51b3c45f3edf654d Mon Sep 17 00:00:00 2001 From: "Jiahao, Woo" Date: Fri, 3 Oct 2025 21:16:21 +0100 Subject: [PATCH 4/4] [mix-messy-graph] Add tests --- mix_messy_graph/tests/specs/base.yml | 36 +++++++++- .../tests/specs/branches_not_deleted.yml | 51 ++++++++++++++ .../tests/specs/features_content_invalid.yml | 40 +++++++++++ .../tests/specs/missing_commit.yml | 36 ++++++++++ .../tests/specs/non_squash_merge_used.yml | 46 +++++++++++++ .../tests/specs/wrong_commit_message.yml | 40 +++++++++++ mix_messy_graph/tests/test_verify.py | 69 +++++++++++++++++-- mix_messy_graph/verify.py | 3 +- 8 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 mix_messy_graph/tests/specs/branches_not_deleted.yml create mode 100644 mix_messy_graph/tests/specs/features_content_invalid.yml create mode 100644 mix_messy_graph/tests/specs/missing_commit.yml create mode 100644 mix_messy_graph/tests/specs/non_squash_merge_used.yml create mode 100644 mix_messy_graph/tests/specs/wrong_commit_message.yml diff --git a/mix_messy_graph/tests/specs/base.yml b/mix_messy_graph/tests/specs/base.yml index 00c3a53..3fc4fb1 100644 --- a/mix_messy_graph/tests/specs/base.yml +++ b/mix_messy_graph/tests/specs/base.yml @@ -2,5 +2,39 @@ initialization: steps: - type: commit empty: true - message: Empty commit + message: Add features.md id: start + - type: commit + empty: true + message: Mention feature for creating books + - type: tag + tag-name: v1.0 + - type: commit + empty: true + message: Fix phrasing of heading + + - type: commit + empty: true + message: Add the search feature + + - type: commit + empty: true + message: Add the delete feature + + - type: new-file + filename: features.md + contents: | + # Features + + ## Create Book + + Allows creating one book at a time. + + ## Searching for Books + + Allows searching for books by keywords. + Works only for book titles. + + ## Deleting Books + + Allows deleting books. diff --git a/mix_messy_graph/tests/specs/branches_not_deleted.yml b/mix_messy_graph/tests/specs/branches_not_deleted.yml new file mode 100644 index 0000000..a1def09 --- /dev/null +++ b/mix_messy_graph/tests/specs/branches_not_deleted.yml @@ -0,0 +1,51 @@ +initialization: + steps: + - type: commit + empty: true + message: Add features.md + id: start + - type: commit + empty: true + message: Mention feature for creating books + - type: tag + tag-name: v1.0 + - type: commit + empty: true + message: Fix phrasing of heading + + - type: commit + empty: true + message: Add the search feature + + - type: commit + empty: true + message: Add the delete feature + + - type: branch + branch-name: feature-search + - type: checkout + branch-name: main + - type: branch + branch-name: feature-delete + - type: checkout + branch-name: main + - type: branch + branch-name: list + + - type: new-file + filename: features.md + contents: | + # Features + + ## Create Book + + Allows creating one book at a time. + + ## Searching for Books + + Allows searching for books by keywords. + Works only for book titles. + + ## Deleting Books + + Allows deleting books. diff --git a/mix_messy_graph/tests/specs/features_content_invalid.yml b/mix_messy_graph/tests/specs/features_content_invalid.yml new file mode 100644 index 0000000..a62c4c2 --- /dev/null +++ b/mix_messy_graph/tests/specs/features_content_invalid.yml @@ -0,0 +1,40 @@ +initialization: + steps: + - type: commit + empty: true + message: Add features.md + id: start + - type: commit + empty: true + message: Mention feature for creating books + - type: tag + tag-name: v1.0 + - type: commit + empty: true + message: Fix phrasing of heading + + - type: commit + empty: true + message: Add the search feature + + - type: commit + empty: true + message: Add the delete feature + + - type: new-file + filename: features.md + contents: | + # Features + + ## Searching for Books + + Allows searching for books by keywords. + Works only for book titles. + + ## Create Book + + Allows creating one book at a time. + + ## Deleting Books + + Allows deleting books. diff --git a/mix_messy_graph/tests/specs/missing_commit.yml b/mix_messy_graph/tests/specs/missing_commit.yml new file mode 100644 index 0000000..e07e3a5 --- /dev/null +++ b/mix_messy_graph/tests/specs/missing_commit.yml @@ -0,0 +1,36 @@ +initialization: + steps: + - type: commit + empty: true + message: Add features.md + id: start + - type: commit + empty: true + message: Mention feature for creating books + - type: tag + tag-name: v1.0 + - type: commit + empty: true + message: Fix phrasing of heading + + - type: commit + empty: true + message: Add the search feature + + - type: new-file + filename: features.md + contents: | + # Features + + ## Create Book + + Allows creating one book at a time. + + ## Searching for Books + + Allows searching for books by keywords. + Works only for book titles. + + ## Deleting Books + + Allows deleting books. diff --git a/mix_messy_graph/tests/specs/non_squash_merge_used.yml b/mix_messy_graph/tests/specs/non_squash_merge_used.yml new file mode 100644 index 0000000..da47692 --- /dev/null +++ b/mix_messy_graph/tests/specs/non_squash_merge_used.yml @@ -0,0 +1,46 @@ +initialization: + steps: + - type: commit + empty: true + message: Add features.md + id: start + - type: commit + empty: true + message: Mention feature for creating books + - type: tag + tag-name: v1.0 + - type: commit + empty: true + message: Fix phrasing of heading + + - type: branch + branch-name: feature-search + - type: commit + empty: true + message: Feature search + - type: checkout + branch-name: main + - type: merge + branch-name: feature-search + + - type: commit + empty: true + message: Add the delete feature + + - type: new-file + filename: features.md + contents: | + # Features + + ## Create Book + + Allows creating one book at a time. + + ## Searching for Books + + Allows searching for books by keywords. + Works only for book titles. + + ## Deleting Books + + Allows deleting books. diff --git a/mix_messy_graph/tests/specs/wrong_commit_message.yml b/mix_messy_graph/tests/specs/wrong_commit_message.yml new file mode 100644 index 0000000..1d95675 --- /dev/null +++ b/mix_messy_graph/tests/specs/wrong_commit_message.yml @@ -0,0 +1,40 @@ +initialization: + steps: + - type: commit + empty: true + message: Add features.md + id: start + - type: commit + empty: true + message: Mention feature for creating books + - type: tag + tag-name: v1.0 + - type: commit + empty: true + message: Fix phrasing of heading + + - type: commit + empty: true + message: Add the search feature! + + - type: commit + empty: true + message: Add the delete feature + + - type: new-file + filename: features.md + contents: | + # Features + + ## Create Book + + Allows creating one book at a time. + + ## Searching for Books + + Allows searching for books by keywords. + Works only for book titles. + + ## Deleting Books + + Allows deleting books. diff --git a/mix_messy_graph/tests/test_verify.py b/mix_messy_graph/tests/test_verify.py index 7d9c4ed..87ba349 100644 --- a/mix_messy_graph/tests/test_verify.py +++ b/mix_messy_graph/tests/test_verify.py @@ -1,6 +1,14 @@ -from git_autograder import GitAutograderTestLoader +from git_autograder import GitAutograderStatus, GitAutograderTestLoader, assert_output -from ..verify import verify +from ..verify import ( + FEATURE_SEARCH_BRANCH_STILL_EXISTS, + FEATURE_DELETE_BRANCH_STILL_EXISTS, + FEATURES_FILE_CONTENT_INVALID, + LIST_BRANCH_STILL_EXISTS, + MISMATCH_COMMIT_MESSAGE, + SQUASH_NOT_USED, + verify, +) REPOSITORY_NAME = "mix-messy-graph" @@ -8,5 +16,58 @@ def test_base(): - with loader.load("specs/base.yml", "start"): - pass + with loader.load("specs/base.yml") as output: + assert_output(output, GitAutograderStatus.SUCCESSFUL) + + +def test_non_squash_merge_used(): + with loader.load("specs/non_squash_merge_used.yml") as output: + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [SQUASH_NOT_USED]) + + +def test_wrong_commit_message(): + with loader.load("specs/wrong_commit_message.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ + MISMATCH_COMMIT_MESSAGE.format( + expected="Add the search feature", given="Add the search feature!" + ) + ], + ) + + +def test_missing_commit(): + with loader.load("specs/missing_commit.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ + MISMATCH_COMMIT_MESSAGE.format( + expected="Add the delete feature", given="" + ) + ], + ) + + +def test_branches_not_deleted(): + with loader.load("specs/branches_not_deleted.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ + FEATURE_SEARCH_BRANCH_STILL_EXISTS, + FEATURE_DELETE_BRANCH_STILL_EXISTS, + LIST_BRANCH_STILL_EXISTS, + ], + ) + + +def test_features_content_invalid(): + with loader.load("specs/features_content_invalid.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [FEATURES_FILE_CONTENT_INVALID], + ) diff --git a/mix_messy_graph/verify.py b/mix_messy_graph/verify.py index dfdbc1c..7719d58 100644 --- a/mix_messy_graph/verify.py +++ b/mix_messy_graph/verify.py @@ -55,7 +55,8 @@ def ensure_str(val) -> str: def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: main_branch = exercise.repo.branches.branch("main") merge_commits = [c for c in main_branch.commits if len(c.parents) > 1] - if len(merge_commits) > 0: + merge_reflogs = [e for e in main_branch.reflog if "merge" in e.action] + if merge_commits or merge_reflogs: raise exercise.wrong_answer([SQUASH_NOT_USED]) commit_messages = [ensure_str(c.commit.message) for c in main_branch.commits][::-1]