diff --git a/glossary_branch_pull/.gitmastery-exercise.json b/glossary_branch_pull/.gitmastery-exercise.json new file mode 100644 index 00000000..e5f78bf5 --- /dev/null +++ b/glossary_branch_pull/.gitmastery-exercise.json @@ -0,0 +1,16 @@ +{ + "exercise_name": "glossary-branch-pull", + "tags": [ + "git-push" + ], + "requires_git": true, + "requires_github": true, + "base_files": {}, + "exercise_repo": { + "repo_type": "local", + "repo_name": "funny-glossary", + "repo_title": null, + "create_fork": null, + "init": false + } +} \ No newline at end of file diff --git a/glossary_branch_pull/README.md b/glossary_branch_pull/README.md new file mode 100644 index 00000000..db082fda --- /dev/null +++ b/glossary_branch_pull/README.md @@ -0,0 +1 @@ +See https://git-mastery.github.io/lessons/remoteBranchPull/exercise-glossary-branch-pull.html diff --git a/glossary_branch_pull/__init__.py b/glossary_branch_pull/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/glossary_branch_pull/download.py b/glossary_branch_pull/download.py new file mode 100644 index 00000000..a033e71c --- /dev/null +++ b/glossary_branch_pull/download.py @@ -0,0 +1,40 @@ +from exercise_utils.cli import run_command +from exercise_utils.file import create_or_update_file +from exercise_utils.git import add, checkout, commit, remove_remote +from exercise_utils.github_cli import ( + clone_repo_with_gh, + get_github_username, + fork_repo, + has_repo, + delete_repo, +) + +TARGET_REPO = "git-mastery/samplerepo-funny-glossary" +FORK_NAME = "gitmastery-samplerepo-funny-glossary" + + +def setup(verbose: bool = False): + username = get_github_username(verbose) + full_repo_name = f"{username}/{FORK_NAME}" + + if has_repo(full_repo_name, True, verbose): + delete_repo(full_repo_name, verbose) + + fork_repo(TARGET_REPO, FORK_NAME, verbose, False) + clone_repo_with_gh(f"{username}/{FORK_NAME}", verbose, ".") + remove_remote("upstream", verbose) + + run_command(["git", "branch", "-dr", "origin/VWX"], verbose) + + checkout("ABC", False, verbose) + run_command(["git", "reset", "--hard", "HEAD~1"], verbose) + + checkout("DEF", False, verbose) + run_command(["git", "reset", "--hard", "HEAD~1"], verbose) + create_or_update_file( + "d.txt", + """ + documentation: Evidence that someone once cared. + """) + add(["d.txt"], verbose) + commit("Add 'documentation'", verbose) diff --git a/glossary_branch_pull/test_verify.py b/glossary_branch_pull/test_verify.py new file mode 100644 index 00000000..2111d111 --- /dev/null +++ b/glossary_branch_pull/test_verify.py @@ -0,0 +1,113 @@ +from contextlib import contextmanager +from typing import Iterator, Tuple + +from exercise_utils.test import GitAutograderTest, GitAutograderTestLoader, assert_output +from git_autograder import GitAutograderStatus +from repo_smith.repo_smith import RepoSmith + +from .verify import ( + verify, + BRANCH_MISSING, + BRANCH_NOT_TRACKING, + REMOTE_COMMIT_MISSING, + LOCAL_COMMIT_MISSING, +) + +REPOSITORY_NAME = "glossary-branch-pull" + +loader = GitAutograderTestLoader(REPOSITORY_NAME, verify) + + +@contextmanager +def base_setup() -> Iterator[Tuple[GitAutograderTest, RepoSmith]]: + with loader.start(include_remote_repo=True) as (test, rs, rs_remote): + remote_path = str(rs_remote.repo.git_dir) + rs.git.remote_add("origin", remote_path) + + rs.git.commit(allow_empty=True, message="Initial commit") + + rs.git.checkout("ABC", branch=True) + rs.git.commit(allow_empty=True, message="Add 'cache'") + rs.git.push("origin", "ABC", set_upstream=True) + rs.git.reset("HEAD~1", hard=True) + + rs.git.checkout("DEF", branch=True) + rs.git.commit(allow_empty=True, message="Add 'exception'") + rs.git.push("origin", "DEF", set_upstream=True) + rs.git.reset("HEAD~1", hard=True) + rs.git.commit(allow_empty=True, message="Add 'documentation'") + + yield (test, rs) + +def test_no_changes(): + with base_setup() as (test, rs): + output = test.run() + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ + BRANCH_MISSING.format(branch="STU"), + BRANCH_MISSING.format(branch="VWX"), + REMOTE_COMMIT_MISSING.format(branch="ABC"), + REMOTE_COMMIT_MISSING.format(branch="DEF") + ]) + + +def test_branch_not_tracking(): + with base_setup() as (test, rs): + rs.git.checkout("STU", branch=True) + rs.git.checkout("VWX", branch=True) + + output = test.run() + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ + BRANCH_NOT_TRACKING.format(branch="STU"), + BRANCH_NOT_TRACKING.format(branch="VWX"), + REMOTE_COMMIT_MISSING.format(branch="ABC"), + REMOTE_COMMIT_MISSING.format(branch="DEF") + ]) + + +def test_def_local_commit_missing(): + with base_setup() as (test, rs): + rs.git.checkout("STU", branch=True) + rs.git.push("origin", "STU") + + rs.git.checkout("VWX", branch=True) + rs.git.push("origin", "VWX") + + rs.git.checkout("ABC") + rs.git.commit(allow_empty=True, message="Add 'cache'") + + rs.git.checkout("DEF") + rs.git.reset("HEAD~1", hard=True) + rs.git.fetch("origin") + rs.git.merge("origin/DEF") + + output = test.run() + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [LOCAL_COMMIT_MISSING]) + + +def test_successful_changes(): + with base_setup() as (test, rs): + rs.git.checkout("VWX", branch=True) + rs.git.push("origin", "VWX", set_upstream=True) + + rs.git.checkout("STU", branch=True) + rs.git.push("origin", "STU", set_upstream=True) + + rs.git.checkout("ABC") + rs.git.fetch("origin") + rs.git.merge("origin/ABC") + + rs.git.checkout("DEF") + rs.git.fetch("origin") + rs.git.merge("origin/DEF") + + output = test.run() + assert_output(output, GitAutograderStatus.SUCCESSFUL) \ No newline at end of file diff --git a/glossary_branch_pull/verify.py b/glossary_branch_pull/verify.py new file mode 100644 index 00000000..725add3c --- /dev/null +++ b/glossary_branch_pull/verify.py @@ -0,0 +1,80 @@ +from typing import List +from git_autograder import ( + GitAutograderCommit, + GitAutograderOutput, + GitAutograderExercise, + GitAutograderStatus, +) + +BRANCH_MISSING = "The local {branch} branch is not created." +BRANCH_NOT_TRACKING = "The local {branch} branch does not track origin/{branch}." +REMOTE_COMMIT_MISSING = "New commit in the remote {branch} branch is not pulled to the local {branch} branch." +LOCAL_COMMIT_MISSING = "The original local commit on DEF is missing. " \ +"You may have lost your work instead of merging." + + +def get_commit_from_message(commits: List[GitAutograderCommit], message: str) \ + -> GitAutograderCommit | None: + """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 get_commit_from_hexsha(commits: List[GitAutograderCommit], hexsha: str) \ + -> GitAutograderCommit | None: + """Find a commit with the given hexsha from a list of commits.""" + for commit in commits: + if hexsha.strip() == commit.commit.hexsha.strip(): + return commit + return None + +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + repo = exercise.repo + comments = [] + + if not repo.branches.has_branch("STU"): + comments.append(BRANCH_MISSING.format(branch="STU")) + else: + stu_branch = repo.branches.branch("STU").branch + remote_stu = stu_branch.tracking_branch() + if not remote_stu or remote_stu.name != "origin/STU": + comments.append(BRANCH_NOT_TRACKING.format(branch="STU")) + + if not repo.branches.has_branch("VWX"): + comments.append(BRANCH_MISSING.format(branch="VWX")) + else: + vwx_branch = repo.branches.branch("VWX").branch + remote_vwx = vwx_branch.tracking_branch() + if not remote_vwx or remote_vwx.name != "origin/VWX": + comments.append(BRANCH_NOT_TRACKING.format(branch="VWX")) + + if not repo.branches.has_branch("ABC"): + comments.append(BRANCH_MISSING.format(branch="ABC")) + else: + abc_commits = repo.branches.branch("ABC").commits + abc_branch = repo.branches.branch("ABC").branch + remote_abc = abc_branch.tracking_branch() + if not remote_abc or remote_abc.name != "origin/ABC": + comments.append(BRANCH_NOT_TRACKING.format(branch="ABC")) + elif not get_commit_from_hexsha(abc_commits, remote_abc.commit.hexsha): + comments.append(REMOTE_COMMIT_MISSING.format(branch="ABC")) + + if not repo.branches.has_branch("DEF"): + comments.append(BRANCH_MISSING.format(branch="DEF")) + else: + def_commits = repo.branches.branch("DEF").commits + if not get_commit_from_message(def_commits, "Add 'documentation'"): + comments.append(LOCAL_COMMIT_MISSING) + def_branch = repo.branches.branch("DEF").branch + remote_def = def_branch.tracking_branch() + if not remote_def or remote_def.name != "origin/DEF": + comments.append(BRANCH_NOT_TRACKING.format(branch="DEF")) + elif not get_commit_from_hexsha(def_commits, remote_def.commit.hexsha): + comments.append(REMOTE_COMMIT_MISSING.format(branch="DEF")) + + if comments: + raise exercise.wrong_answer(comments) + return exercise.to_output([ + "Great work! All required branches are present and correctly set up." + ], GitAutograderStatus.SUCCESSFUL)