diff --git a/glossary_branch_rename/.gitmastery-exercise.json b/glossary_branch_rename/.gitmastery-exercise.json new file mode 100644 index 00000000..6ca3803b --- /dev/null +++ b/glossary_branch_rename/.gitmastery-exercise.json @@ -0,0 +1,16 @@ +{ + "exercise_name": "glossary-branch-rename", + "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_rename/README.md b/glossary_branch_rename/README.md new file mode 100644 index 00000000..83229cf5 --- /dev/null +++ b/glossary_branch_rename/README.md @@ -0,0 +1 @@ +See https://git-mastery.org/lessons/remoteBranchRename/exercise-glossary-branch-rename.html diff --git a/glossary_branch_rename/__init__.py b/glossary_branch_rename/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/glossary_branch_rename/download.py b/glossary_branch_rename/download.py new file mode 100644 index 00000000..95360198 --- /dev/null +++ b/glossary_branch_rename/download.py @@ -0,0 +1,25 @@ +from exercise_utils.github_cli import clone_repo_with_gh, delete_repo, fork_repo, get_github_username, has_repo +from exercise_utils.git import remove_remote, track_remote_branch + +UPSTREAM_REPO = "git-mastery/samplerepo-funny-glossary" +BRANCHES = ["ABC", "DEF", "STU", "VWX"] + + +def setup(verbose: bool = False): + username = get_github_username(verbose) + FORK_NAME = f"{username}-gitmastery-samplerepo-funny-glossary" + + if has_repo(FORK_NAME, True, verbose): + delete_repo(FORK_NAME, verbose) + + fork_repo(UPSTREAM_REPO, FORK_NAME, verbose, default_branch_only=False) + + clone_repo_with_gh( + f"{username}/{FORK_NAME}", + verbose, + ".", + ) + remove_remote("upstream", verbose) + + for branch in BRANCHES: + track_remote_branch("origin", branch, verbose) diff --git a/glossary_branch_rename/test_verify.py b/glossary_branch_rename/test_verify.py new file mode 100644 index 00000000..1196d669 --- /dev/null +++ b/glossary_branch_rename/test_verify.py @@ -0,0 +1,128 @@ +from contextlib import contextmanager +from typing import Iterator, Tuple + +from git_autograder import GitAutograderStatus +from exercise_utils.test import ( + GitAutograderTest, + GitAutograderTestLoader, + assert_output, +) +from repo_smith.repo_smith import RepoSmith + +from .verify import ( + verify, + STU_LOCAL_PRESENT, + STU_REMOTE_PRESENT, + RENAMED_LOCAL_MISSING, + RENAMED_REMOTE_MISSING, +) + +REPOSITORY_NAME = "glossary-branch-rename" +BRANCHES = ["ABC", "DEF", "STU", "VWX"] +EXPECTED_NEW_BRANCH_NAME = "S-to-Z" +BRANCH_TO_RENAME = "STU" + +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_worktree_dir = rs_remote.repo.working_tree_dir + + # set up local repo + rs.git.commit(message="Empty", allow_empty=True) + rs.git.remote_add("origin", str(remote_worktree_dir)) + + for remote_branch_name in BRANCHES: + rs_remote.git.branch(remote_branch_name) + + rs.git.push("origin", all=True) + + yield test, rs + + +def test_base(): + with base_setup() as (test, rs): + rs.git.branch(EXPECTED_NEW_BRANCH_NAME, old_branch=BRANCH_TO_RENAME, move=True) + rs.git.push("origin", EXPECTED_NEW_BRANCH_NAME) + rs.git.push("origin", f":{BRANCH_TO_RENAME}") + + output = test.run() + assert_output(output, GitAutograderStatus.SUCCESSFUL) + + +def test_no_change(): + with base_setup() as (test, _): + output = test.run() + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ + STU_LOCAL_PRESENT, + RENAMED_LOCAL_MISSING, + STU_REMOTE_PRESENT, + RENAMED_REMOTE_MISSING, + ], + ) + + +def test_changed_local_only(): + with base_setup() as (test, rs): + rs.git.branch(EXPECTED_NEW_BRANCH_NAME, old_branch=BRANCH_TO_RENAME, move=True) + output = test.run() + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [STU_REMOTE_PRESENT, RENAMED_REMOTE_MISSING], + ) + + +def test_changed_local_wrong_name(): + with base_setup() as (test, rs): + rs.git.branch("S-to-X", old_branch=BRANCH_TO_RENAME, move=True) + output = test.run() + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ + RENAMED_LOCAL_MISSING, + STU_REMOTE_PRESENT, + RENAMED_REMOTE_MISSING, + ], + ) + + +def test_changed_remote_wrong_name(): + with base_setup() as (test, rs): + rs.git.branch("S-to-X", old_branch=BRANCH_TO_RENAME, move=True) + rs.git.push("origin", "S-to-X") + rs.git.push("origin", f":{BRANCH_TO_RENAME}") + output = test.run() + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ + RENAMED_LOCAL_MISSING, + RENAMED_REMOTE_MISSING, + ], + ) + + +def test_local_old_branch_still_exists(): + with base_setup() as (test, rs): + rs.git.branch(EXPECTED_NEW_BRANCH_NAME) + rs.git.push("origin", EXPECTED_NEW_BRANCH_NAME) + rs.git.push("origin", f":{BRANCH_TO_RENAME}") + + output = test.run() + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [STU_LOCAL_PRESENT]) + + +def test_remote_old_branch_still_exists(): + with base_setup() as (test, rs): + rs.git.branch(EXPECTED_NEW_BRANCH_NAME, old_branch=BRANCH_TO_RENAME, move=True) + rs.git.push("origin", EXPECTED_NEW_BRANCH_NAME) + + output = test.run() + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [STU_REMOTE_PRESENT]) diff --git a/glossary_branch_rename/verify.py b/glossary_branch_rename/verify.py new file mode 100644 index 00000000..e68ff3b7 --- /dev/null +++ b/glossary_branch_rename/verify.py @@ -0,0 +1,47 @@ +from git_autograder import ( + GitAutograderOutput, + GitAutograderExercise, + GitAutograderStatus, +) + +STU_BRANCH = "STU" +RENAMED_BRANCH = "S-to-Z" + +STU_LOCAL_PRESENT = f"Local branch {STU_BRANCH} still exists." +RENAMED_LOCAL_MISSING = f"Local branch {RENAMED_BRANCH} is missing." +STU_REMOTE_PRESENT = f"Remote branch {STU_BRANCH} still exists." +RENAMED_REMOTE_MISSING = f"Remote branch {RENAMED_BRANCH} is missing." + + +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + local_raw = exercise.repo.repo.git.branch("--list") + local_branches = [line.strip().lstrip("* ").strip() for line in local_raw.splitlines()] + + remote_raw = exercise.repo.repo.git.ls_remote("--heads", "origin") + remote_branches = [] + for line in remote_raw.splitlines(): + parts = line.split() + if len(parts) == 2 and parts[1].startswith("refs/heads/"): + remote_branches.append(parts[1].split("/")[-1]) + + comments = [] + + if STU_BRANCH in local_branches: + comments.append(STU_LOCAL_PRESENT) + + if RENAMED_BRANCH not in local_branches: + comments.append(RENAMED_LOCAL_MISSING) + + if STU_BRANCH in remote_branches: + comments.append(STU_REMOTE_PRESENT) + + if RENAMED_BRANCH not in remote_branches: + comments.append(RENAMED_REMOTE_MISSING) + + if comments: + raise exercise.wrong_answer(comments) + + return exercise.to_output( + ["Nice work renaming the branch locally and on the remote!"], + GitAutograderStatus.SUCCESSFUL, + )