diff --git a/exercise_utils/git.py b/exercise_utils/git.py index c93af2c..fe5fc84 100644 --- a/exercise_utils/git.py +++ b/exercise_utils/git.py @@ -68,3 +68,8 @@ def init(verbose: bool) -> None: def push(remote: str, branch: str, verbose: bool) -> None: """Push the given branch on the remote.""" run_command(["git", "push", remote, branch], verbose) + + +def track_remote_branch(remote: str, branch: str, verbose: bool) -> None: + """Tracks a remote branch locally using the same name.""" + run_command(["git", "branch", branch, f"{remote}/{branch}"], verbose) diff --git a/mix_messy_docs/.gitmastery-exercise.json b/mix_messy_docs/.gitmastery-exercise.json new file mode 100644 index 0000000..58935b1 --- /dev/null +++ b/mix_messy_docs/.gitmastery-exercise.json @@ -0,0 +1,16 @@ +{ + "exercise_name": "mix-messy-docs", + "tags": [ + "git-branch" + ], + "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_docs/README.md b/mix_messy_docs/README.md new file mode 100644 index 0000000..06fc669 --- /dev/null +++ b/mix_messy_docs/README.md @@ -0,0 +1,27 @@ +# mix-messy-docs + +You are writing user documentation for a product. You have already written documentation for a few new features, each in a separate branch. You wish to accumulate this work in a separate branch called `development` until the next product release. + +## Task + +1. Create a new branch `development`, starting from the commit tagged `v1.0` +2. Merge the `feature-search` branch onto the `development` branch, without using fast-forwarding (i.e., create a merge commit). Delete the `feature-search` branch. +3. Similarly, merge the `feature-delete` branch onto the `development` branch. Resolve any merge conflicts -- in the `features.md`, the delete feature should appear after the search feature (see below). Delete the `feature-delete` branch. + ``` + # 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. + ``` +5. The `list` branch is not yet ready to be merged but rename it as `feature-list`, to be consistent with the naming convention you have been following in this repo. + diff --git a/mix_messy_docs/__init__.py b/mix_messy_docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mix_messy_docs/download.py b/mix_messy_docs/download.py new file mode 100644 index 0000000..1071347 --- /dev/null +++ b/mix_messy_docs/download.py @@ -0,0 +1,8 @@ +from exercise_utils.git import 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) diff --git a/mix_messy_docs/tests/__init__.py b/mix_messy_docs/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mix_messy_docs/tests/specs/contents_wrong.yml b/mix_messy_docs/tests/specs/contents_wrong.yml new file mode 100644 index 0000000..65cf434 --- /dev/null +++ b/mix_messy_docs/tests/specs/contents_wrong.yml @@ -0,0 +1,88 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: new-file + filename: conflict.txt + contents: | + Hello world + - type: add + files: + - conflict.txt + - type: commit + message: Expected branch point + - type: tag + tag-name: v1.0 + + - type: branch + branch-name: feature-search + - type: edit-file + filename: conflict.txt + contents: | + Hello world! + - type: add + files: + - conflict.txt + - type: commit + message: Feature search changes + - type: checkout + branch-name: main + + - type: branch + branch-name: feature-delete + - type: edit-file + filename: conflict.txt + contents: | + Hello world? + - type: add + files: + - conflict.txt + - type: commit + message: Feature delete changes + - type: checkout + branch-name: main + + - type: branch + branch-name: feature-list + - type: commit + empty: true + message: Feature list changes + + - type: checkout + branch-name: main + - type: branch + branch-name: development + - type: commit + empty: true + message: Commit on development + - type: merge + branch-name: feature-search + no-ff: true + - type: bash + runs: | + # Controlling everything through Bash to simplify workflow + (git merge feature-delete || true) > /dev/null + echo 'New contents' > conflict.txt + git add conflict.txt + (git commit --no-edit) > /dev/null + + - 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_docs/tests/specs/feature_list_branch_missing.yml b/mix_messy_docs/tests/specs/feature_list_branch_missing.yml new file mode 100644 index 0000000..83babe7 --- /dev/null +++ b/mix_messy_docs/tests/specs/feature_list_branch_missing.yml @@ -0,0 +1,87 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: new-file + filename: conflict.txt + contents: | + Hello world + - type: add + files: + - conflict.txt + - type: commit + message: Expected branch point + - type: tag + tag-name: v1.0 + + - type: branch + branch-name: feature-search + - type: edit-file + filename: conflict.txt + contents: | + Hello world! + - type: add + files: + - conflict.txt + - type: commit + message: Feature search changes + - type: checkout + branch-name: main + + - type: branch + branch-name: feature-delete + - type: edit-file + filename: conflict.txt + contents: | + Hello world? + - type: add + files: + - conflict.txt + - type: commit + message: Feature delete changes + - type: checkout + branch-name: main + + - type: branch + branch-name: other-list + - type: commit + empty: true + message: Feature list changes + + - type: checkout + branch-name: main + - type: branch + branch-name: development + - type: commit + empty: true + message: Commit on development + - type: merge + branch-name: feature-search + no-ff: true + - type: bash + runs: | + # Controlling everything through Bash to simplify workflow + (git merge feature-delete || true) > /dev/null + echo 'New contents' > conflict.txt + git add conflict.txt + (git commit --no-edit) > /dev/null + + - 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_docs/tests/specs/list_branch_exists.yml b/mix_messy_docs/tests/specs/list_branch_exists.yml new file mode 100644 index 0000000..fd02357 --- /dev/null +++ b/mix_messy_docs/tests/specs/list_branch_exists.yml @@ -0,0 +1,87 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: new-file + filename: conflict.txt + contents: | + Hello world + - type: add + files: + - conflict.txt + - type: commit + message: Expected branch point + - type: tag + tag-name: v1.0 + + - type: branch + branch-name: feature-search + - type: edit-file + filename: conflict.txt + contents: | + Hello world! + - type: add + files: + - conflict.txt + - type: commit + message: Feature search changes + - type: checkout + branch-name: main + + - type: branch + branch-name: feature-delete + - type: edit-file + filename: conflict.txt + contents: | + Hello world? + - type: add + files: + - conflict.txt + - type: commit + message: Feature delete changes + - type: checkout + branch-name: main + + - type: branch + branch-name: list + - type: commit + empty: true + message: Feature list changes + + - type: checkout + branch-name: main + - type: branch + branch-name: development + - type: commit + empty: true + message: Commit on development + - type: merge + branch-name: feature-search + no-ff: true + - type: bash + runs: | + # Controlling everything through Bash to simplify workflow + (git merge feature-delete || true) > /dev/null + echo 'New contents' > conflict.txt + git add conflict.txt + (git commit --no-edit) > /dev/null + + - 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_docs/tests/specs/missing_development.yml b/mix_messy_docs/tests/specs/missing_development.yml new file mode 100644 index 0000000..00c3a53 --- /dev/null +++ b/mix_messy_docs/tests/specs/missing_development.yml @@ -0,0 +1,6 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start diff --git a/mix_messy_docs/tests/specs/no_merge_feature_delete.yml b/mix_messy_docs/tests/specs/no_merge_feature_delete.yml new file mode 100644 index 0000000..3643efb --- /dev/null +++ b/mix_messy_docs/tests/specs/no_merge_feature_delete.yml @@ -0,0 +1,80 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: new-file + filename: conflict.txt + contents: | + Hello world + - type: add + files: + - conflict.txt + - type: commit + message: Expected branch point + - type: tag + tag-name: v1.0 + + - type: branch + branch-name: feature-search + - type: edit-file + filename: conflict.txt + contents: | + Hello world! + - type: add + files: + - conflict.txt + - type: commit + message: Feature search changes + - type: checkout + branch-name: main + + - type: branch + branch-name: feature-delete + - type: edit-file + filename: conflict.txt + contents: | + Hello world? + - type: add + files: + - conflict.txt + - type: commit + message: Feature delete changes + - type: checkout + branch-name: main + + - type: branch + branch-name: feature-list + - type: commit + empty: true + message: Feature list changes + + - type: checkout + branch-name: main + - type: branch + branch-name: development + - type: commit + empty: true + message: Commit on development + - type: merge + branch-name: feature-search + no-ff: true + + - 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_docs/tests/specs/no_merge_feature_search.yml b/mix_messy_docs/tests/specs/no_merge_feature_search.yml new file mode 100644 index 0000000..22b33c9 --- /dev/null +++ b/mix_messy_docs/tests/specs/no_merge_feature_search.yml @@ -0,0 +1,87 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: new-file + filename: conflict.txt + contents: | + Hello world + - type: add + files: + - conflict.txt + - type: commit + message: Expected branch point + - type: tag + tag-name: v1.0 + + - type: branch + branch-name: feature-search + - type: edit-file + filename: conflict.txt + contents: | + Hello world! + - type: add + files: + - conflict.txt + - type: commit + message: Feature search changes + - type: checkout + branch-name: main + + - type: branch + branch-name: feature-delete + - type: edit-file + filename: conflict.txt + contents: | + Hello world? + - type: add + files: + - conflict.txt + - type: commit + message: Feature delete changes + - type: checkout + branch-name: main + + - type: branch + branch-name: feature-list + - type: commit + empty: true + message: Feature list changes + + - type: checkout + branch-name: main + - type: branch + branch-name: development + - type: commit + empty: true + message: Commit on development + - type: merge + branch-name: feature-delete + no-ff: true + - type: bash + runs: | + # Controlling everything through Bash to simplify workflow + (git merge feature-search || true) > /dev/null + echo 'New contents' > conflict.txt + git add conflict.txt + (git commit --no-edit) > /dev/null + + - 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_docs/tests/specs/right_order.yml b/mix_messy_docs/tests/specs/right_order.yml new file mode 100644 index 0000000..16d4207 --- /dev/null +++ b/mix_messy_docs/tests/specs/right_order.yml @@ -0,0 +1,87 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: new-file + filename: conflict.txt + contents: | + Hello world + - type: add + files: + - conflict.txt + - type: commit + message: Expected branch point + - type: tag + tag-name: v1.0 + + - type: branch + branch-name: feature-search + - type: edit-file + filename: conflict.txt + contents: | + Hello world! + - type: add + files: + - conflict.txt + - type: commit + message: Feature search changes + - type: checkout + branch-name: main + + - type: branch + branch-name: feature-delete + - type: edit-file + filename: conflict.txt + contents: | + Hello world? + - type: add + files: + - conflict.txt + - type: commit + message: Feature delete changes + - type: checkout + branch-name: main + + - type: branch + branch-name: feature-list + - type: commit + empty: true + message: Feature list changes + + - type: checkout + branch-name: main + - type: branch + branch-name: development + - type: commit + empty: true + message: Commit on development + - type: merge + branch-name: feature-search + no-ff: true + - type: bash + runs: | + # Controlling everything through Bash to simplify workflow + (git merge feature-delete || true) > /dev/null + echo 'New contents' > conflict.txt + git add conflict.txt + (git commit --no-edit) > /dev/null + + - 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_docs/tests/specs/wrong_branch_point.yml b/mix_messy_docs/tests/specs/wrong_branch_point.yml new file mode 100644 index 0000000..ab794c1 --- /dev/null +++ b/mix_messy_docs/tests/specs/wrong_branch_point.yml @@ -0,0 +1,18 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + - type: branch + branch-name: development + - type: commit + empty: true + message: Commit on development + - type: checkout + branch-name: main + - type: commit + empty: true + message: Expected branch point + - type: tag + tag-name: v1.0 diff --git a/mix_messy_docs/tests/test_verify.py b/mix_messy_docs/tests/test_verify.py new file mode 100644 index 0000000..3391ee9 --- /dev/null +++ b/mix_messy_docs/tests/test_verify.py @@ -0,0 +1,81 @@ +from git_autograder import GitAutograderStatus, GitAutograderTestLoader +from git_autograder.test_utils import assert_output + +from ..verify import ( + FEATURE_LIST_BRANCH_MISSING, + FEATURES_FILE_CONTENT_INVALID, + LIST_BRANCH_STILL_EXISTS, + MERGE_FEATURE_DELETE_SECOND, + MERGE_FEATURE_SEARCH_FIRST, + MISSING_DEVELOPMENT_BRANCH, + MISSING_FEATURES_FILE, + RESET_MESSAGE, + WRONG_BRANCH_POINT, + verify, +) + +REPOSITORY_NAME = "mix-messy-docs" + +loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify) + + +def test_missing_development(): + with loader.load("specs/missing_development.yml") as output: + assert_output( + output, GitAutograderStatus.UNSUCCESSFUL, [MISSING_DEVELOPMENT_BRANCH] + ) + + +def test_wrong_branch_point(): + with loader.load("specs/wrong_branch_point.yml") as output: + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [WRONG_BRANCH_POINT]) + + +def test_right_order(): + with loader.load("specs/right_order.yml") as output: + assert_output(output, GitAutograderStatus.SUCCESSFUL) + + +def test_no_merge_feature_search(): + with loader.load("specs/no_merge_feature_search.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [MERGE_FEATURE_SEARCH_FIRST, RESET_MESSAGE], + ) + + +def test_no_merge_feature_delete(): + with loader.load("specs/no_merge_feature_delete.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [MERGE_FEATURE_DELETE_SECOND, RESET_MESSAGE], + ) + + +def test_list_branch_exists(): + with loader.load("specs/list_branch_exists.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [LIST_BRANCH_STILL_EXISTS], + ) + + +def test_feature_list_branch_missing(): + with loader.load("specs/feature_list_branch_missing.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [FEATURE_LIST_BRANCH_MISSING], + ) + + +def test_contents_wrong(): + with loader.load("specs/contents_wrong.yml") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [FEATURES_FILE_CONTENT_INVALID], + ) diff --git a/mix_messy_docs/verify.py b/mix_messy_docs/verify.py new file mode 100644 index 0000000..3f2fec5 --- /dev/null +++ b/mix_messy_docs/verify.py @@ -0,0 +1,94 @@ +from os import EX_TEMPFAIL +from git_autograder import ( + GitAutograderOutput, + GitAutograderExercise, + GitAutograderStatus, +) + +MISSING_DEVELOPMENT_BRANCH = "You are missing the 'development' branch!" +WRONG_BRANCH_POINT = "You did not branch from the commit with tag v1.0!" +FEATURE_SEARCH_BRANCH_STILL_EXISTS = ( + "Branch 'feature-search' still exists! Remember to delete it after merging!" +) +FEATURE_DELETE_BRANCH_STILL_EXISTS = ( + "Branch 'feature-delete' still exists! Remember to delete it after merging!" +) +LIST_BRANCH_STILL_EXISTS = ( + "Branch 'list' still exists! Remember to rename it to 'feature-list'!" +) +FEATURE_LIST_BRANCH_MISSING = "Branch 'feature-list' is missing. Did you misspell it?" +MERGE_FEATURE_SEARCH_FIRST = "You need to merge 'feature-search' first!" +MERGE_FEATURE_DELETE_SECOND = "You need to merge 'feature-delete' second!" +MISSING_FEATURES_FILE = "You are missing 'features.md'!" +FEATURES_FILE_CONTENT_INVALID = "Contents of 'features.md' is not valid! Try again!" + +RESET_MESSAGE = 'Reset the repository using "gitmastery progress reset" and start again' + + +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 verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + development_branch = exercise.repo.branches.branch_or_none("development") + if development_branch is None: + raise exercise.wrong_answer([MISSING_DEVELOPMENT_BRANCH]) + + tag_commit = exercise.repo.repo.tags["v1.0"].commit + development_commit = development_branch.latest_commit + branched_from_tag_hexsha = exercise.repo.repo.git.merge_base( + tag_commit.hexsha, development_commit.hexsha + ) + # Alternative is to use reflog which states where the branch is created from + if branched_from_tag_hexsha != tag_commit.hexsha: + # Not branched from this but maybe somewhere earlier + raise exercise.wrong_answer([WRONG_BRANCH_POINT]) + + reflog = development_branch.reflog + merge_logs = [log for log in reflog if "merge" in log.action][::-1] + has_feature_search_merge = ( + len(merge_logs) >= 1 and merge_logs[0].action == "merge feature-search" + ) + has_feature_delete_commit_merge = ( + len(merge_logs) >= 2 + and merge_logs[1].action == "commit (merge)" + and "feature-delete" in merge_logs[1].message + ) + if not has_feature_search_merge: + raise exercise.wrong_answer([MERGE_FEATURE_SEARCH_FIRST, RESET_MESSAGE]) + + if not has_feature_delete_commit_merge: + raise exercise.wrong_answer([MERGE_FEATURE_DELETE_SECOND, RESET_MESSAGE]) + + feature_list_branch = exercise.repo.branches.branch_or_none("feature-list") + list_branch = exercise.repo.branches.branch_or_none("list") + if feature_list_branch is None: + if list_branch is not None: + raise exercise.wrong_answer([LIST_BRANCH_STILL_EXISTS]) + else: + raise exercise.wrong_answer([FEATURE_LIST_BRANCH_MISSING]) + + 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( + [ + "Great work using all of the concepts you've learnt about branching to mix the messy documentation!" + ], + GitAutograderStatus.SUCCESSFUL, + )