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..eca41c1 --- /dev/null +++ b/mix_messy_graph/README.md @@ -0,0 +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. + +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" +``` 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..3fc4fb1 --- /dev/null +++ b/mix_messy_graph/tests/specs/base.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/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 new file mode 100644 index 0000000..87ba349 --- /dev/null +++ b/mix_messy_graph/tests/test_verify.py @@ -0,0 +1,73 @@ +from git_autograder import GitAutograderStatus, GitAutograderTestLoader, assert_output + +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" + +loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify) + + +def test_base(): + 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 new file mode 100644 index 0000000..7719d58 --- /dev/null +++ b/mix_messy_graph/verify.py @@ -0,0 +1,97 @@ +from git_autograder import ( + GitAutograderOutput, + 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: + main_branch = exercise.repo.branches.branch("main") + merge_commits = [c for c in main_branch.commits if len(c.parents) > 1] + 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] + 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)