From cb61409d88d8175e42e129042f05b58baf0252f8 Mon Sep 17 00:00:00 2001 From: keerthigkaarthik <120106412+keerthigkaarthik@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:08:15 +0100 Subject: [PATCH 1/6] Add branch forward exercise --- branch_forward/.gitmastery-exercise.json | 17 +++++ branch_forward/README.md | 26 ++++++++ branch_forward/__init__.py | 0 branch_forward/download.py | 63 +++++++++++++++++++ branch_forward/tests/__init__.py | 0 branch_forward/tests/specs/base.yml | 42 +++++++++++++ .../tests/specs/merge_with_sally_no_ff.yml | 43 +++++++++++++ branch_forward/tests/specs/no_merges.yml | 41 ++++++++++++ .../tests/specs/other_branch_ff.yml | 53 ++++++++++++++++ .../tests/specs/other_branch_non_ff.yml | 43 +++++++++++++ branch_forward/tests/test_verify.py | 52 +++++++++++++++ branch_forward/verify.py | 42 +++++++++++++ 12 files changed, 422 insertions(+) create mode 100644 branch_forward/.gitmastery-exercise.json create mode 100644 branch_forward/README.md create mode 100644 branch_forward/__init__.py create mode 100644 branch_forward/download.py create mode 100644 branch_forward/tests/__init__.py create mode 100644 branch_forward/tests/specs/base.yml create mode 100644 branch_forward/tests/specs/merge_with_sally_no_ff.yml create mode 100644 branch_forward/tests/specs/no_merges.yml create mode 100644 branch_forward/tests/specs/other_branch_ff.yml create mode 100644 branch_forward/tests/specs/other_branch_non_ff.yml create mode 100644 branch_forward/tests/test_verify.py create mode 100644 branch_forward/verify.py diff --git a/branch_forward/.gitmastery-exercise.json b/branch_forward/.gitmastery-exercise.json new file mode 100644 index 0000000..63acafd --- /dev/null +++ b/branch_forward/.gitmastery-exercise.json @@ -0,0 +1,17 @@ +{ + "exercise_name": "branch-forward", + "tags": [ + "git-branch", + "git-merge" + ], + "requires_git": true, + "requires_github": false, + "base_files": {}, + "exercise_repo": { + "repo_type": "local", + "repo_name": "love-story", + "repo_title": null, + "create_fork": null, + "init": true + } +} \ No newline at end of file diff --git a/branch_forward/README.md b/branch_forward/README.md new file mode 100644 index 0000000..af774d7 --- /dev/null +++ b/branch_forward/README.md @@ -0,0 +1,26 @@ +# branch-forward + +You are outlining a story and experimenting with different plotlines. Each version lives on its own branch, and you now need to fold the right storyline back into `main` without cluttering the history. + +## Learning objectives + +- Identify when a branch can be fast-forward merged +- Use fast-forward merges to keep the commit graph linear +- [Fast-forward merges](https://nus-cs2103-ay2526s1.github.io/website/book/gitAndGithub/merge/index.html) + +## Task + +1. Review the `with-sally` and `with-ginny` branches. +2. Merge only the branch(es) that can be fast-forwarded into `main`. +3. Leave other branches (if any) unmerged. + +## Hints + + +
+ +Hint 1 + +Ensure you have switched to the destination branch before initiating the merge. + +
\ No newline at end of file diff --git a/branch_forward/__init__.py b/branch_forward/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/branch_forward/download.py b/branch_forward/download.py new file mode 100644 index 0000000..1eff523 --- /dev/null +++ b/branch_forward/download.py @@ -0,0 +1,63 @@ +from exercise_utils.file import append_to_file +from exercise_utils.git import add, checkout, commit + + +def setup(verbose: bool = False): + append_to_file( + "story.txt", + """ + Harry was single. + """, + ) + add(["story.txt"], verbose) + commit("Introduce Harry", verbose) + + append_to_file( + "story.txt", + """ + Harry did not have a family. + """, + ) + add(["story.txt"], verbose) + commit("Add about family", verbose) + + checkout("with-ginny", True, verbose) + append_to_file( + "story.txt", + """ + Then he met Ginny. + """, + ) + add(["story.txt"], verbose) + commit("Add about Ginny", verbose) + + checkout("main", False, verbose) + append_to_file( + "cast.txt", + """ + Harry + """, + ) + add(["cast.txt"], verbose) + commit("Add cast.txt", verbose) + + checkout("with-sally", True, verbose) + append_to_file( + "story.txt", + """ + Then he met Sally + """, + ) + add(["story.txt"], verbose) + commit("Mention Sally", verbose) + + checkout("with-ginny", False, verbose) + append_to_file( + "story.txt", + """ + Ginny was single too + """, + ) + add(["story.txt"], verbose) + commit("Mention Ginny is single", verbose) + diff --git a/branch_forward/tests/__init__.py b/branch_forward/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/branch_forward/tests/specs/base.yml b/branch_forward/tests/specs/base.yml new file mode 100644 index 0000000..99af44c --- /dev/null +++ b/branch_forward/tests/specs/base.yml @@ -0,0 +1,42 @@ +initialization: + steps: + - type: commit + empty: true + message: Introduce Harry + id: start + - type: commit + empty: true + message: Add about family + + - type: branch + branch-name: with-ginny + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Add about Ginny + + - type: checkout + branch-name: main + - type: commit + empty: true + message: Add cast.txt + + - type: branch + branch-name: with-sally + - type: checkout + branch-name: with-sally + - type: commit + empty: true + message: Mention Sally + + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Mention Ginny is single + + - type: checkout + branch-name: main + - type: merge + branch-name: with-sally diff --git a/branch_forward/tests/specs/merge_with_sally_no_ff.yml b/branch_forward/tests/specs/merge_with_sally_no_ff.yml new file mode 100644 index 0000000..1b76e03 --- /dev/null +++ b/branch_forward/tests/specs/merge_with_sally_no_ff.yml @@ -0,0 +1,43 @@ +initialization: + steps: + - type: commit + empty: true + message: Introduce Harry + id: start + - type: commit + empty: true + message: Add about family + + - type: branch + branch-name: with-ginny + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Add about Ginny + + - type: checkout + branch-name: main + - type: commit + empty: true + message: Add cast.txt + + - type: branch + branch-name: with-sally + - type: checkout + branch-name: with-sally + - type: commit + empty: true + message: Mention Sally + + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Mention Ginny is single + + - type: checkout + branch-name: main + - type: merge + branch-name: with-sally + no-ff: true diff --git a/branch_forward/tests/specs/no_merges.yml b/branch_forward/tests/specs/no_merges.yml new file mode 100644 index 0000000..2d0ece0 --- /dev/null +++ b/branch_forward/tests/specs/no_merges.yml @@ -0,0 +1,41 @@ +initialization: + steps: + - type: commit + empty: true + message: Introduce Harry + id: start + - type: commit + empty: true + message: Add about family + + - type: branch + branch-name: with-ginny + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Add about Ginny + + - type: checkout + branch-name: main + - type: commit + empty: true + message: Add cast.txt + + - type: branch + branch-name: with-sally + - type: checkout + branch-name: with-sally + - type: commit + empty: true + message: Mention Sally + + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Mention Ginny is single + + - type: checkout + branch-name: main + diff --git a/branch_forward/tests/specs/other_branch_ff.yml b/branch_forward/tests/specs/other_branch_ff.yml new file mode 100644 index 0000000..e7f6377 --- /dev/null +++ b/branch_forward/tests/specs/other_branch_ff.yml @@ -0,0 +1,53 @@ +initialization: + steps: + - type: commit + empty: true + message: Introduce Harry + id: start + - type: commit + empty: true + message: Add about family + + - type: branch + branch-name: with-ginny + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Add about Ginny + + - type: checkout + branch-name: main + - type: commit + empty: true + message: Add cast.txt + + - type: branch + branch-name: with-sally + - type: checkout + branch-name: with-sally + - type: commit + empty: true + message: Mention Sally + + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Mention Ginny is single + + - type: checkout + branch-name: main + - type: branch + branch-name: with-ron + - type: checkout + branch-name: with-ron + - type: commit + empty: true + message: Mention Ron + + - type: checkout + branch-name: main + - type: merge + branch-name: with-ron + diff --git a/branch_forward/tests/specs/other_branch_non_ff.yml b/branch_forward/tests/specs/other_branch_non_ff.yml new file mode 100644 index 0000000..0bb536f --- /dev/null +++ b/branch_forward/tests/specs/other_branch_non_ff.yml @@ -0,0 +1,43 @@ +initialization: + steps: + - type: commit + empty: true + message: Introduce Harry + id: start + - type: commit + empty: true + message: Add about family + + - type: branch + branch-name: with-ginny + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Add about Ginny + + - type: checkout + branch-name: main + - type: commit + empty: true + message: Add cast.txt + + - type: branch + branch-name: with-sally + - type: checkout + branch-name: with-sally + - type: commit + empty: true + message: Mention Sally + + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Mention Ginny is single + + - type: checkout + branch-name: main + - type: merge + branch-name: with-ginny + diff --git a/branch_forward/tests/test_verify.py b/branch_forward/tests/test_verify.py new file mode 100644 index 0000000..f124449 --- /dev/null +++ b/branch_forward/tests/test_verify.py @@ -0,0 +1,52 @@ +from git_autograder import GitAutograderStatus, GitAutograderTestLoader, assert_output + +from ..verify import ( + FAST_FORWARD_REQUIRED, + ONLY_WITH_SALLY_MERGED, + verify, +) + +REPOSITORY_NAME = "branch-forward" + +loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify) + + +def test_success(): + with loader.load("specs/base.yml", "start") as output: + assert_output(output, GitAutograderStatus.SUCCESSFUL) + + +def test_no_merges(): + with loader.load("specs/no_merges.yml", "start") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [FAST_FORWARD_REQUIRED], + ) + + +def test_other_branch_non_ff(): + with loader.load("specs/other_branch_non_ff.yml", "start") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [FAST_FORWARD_REQUIRED], + ) + + +def test_other_branch_ff(): + with loader.load("specs/other_branch_ff.yml", "start") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [ONLY_WITH_SALLY_MERGED], + ) + + +def test_merge_with_sally_no_ff(): + with loader.load("specs/merge_with_sally_no_ff.yml", "start") as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [FAST_FORWARD_REQUIRED], + ) diff --git a/branch_forward/verify.py b/branch_forward/verify.py new file mode 100644 index 0000000..e10277d --- /dev/null +++ b/branch_forward/verify.py @@ -0,0 +1,42 @@ +from git_autograder import ( + GitAutograderExercise, + GitAutograderOutput, + GitAutograderStatus, +) + +FAST_FORWARD_REQUIRED = ( + "You must use a fast-forward merge to bring a branch into 'main'." +) + +ONLY_WITH_SALLY_MERGED = ( + "Only the 'with-sally' branch should be merged into 'main'. Do not merge other branches." +) + +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + main_branch = exercise.repo.branches.branch("main") + merge_logs = [ + entry for entry in main_branch.reflog if entry.action.startswith("merge ") + ] + + if not merge_logs: + raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) + + main_commits = list(main_branch.commits) + if any(len(commit.parents) > 1 for commit in main_commits): + raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) + + if any(entry.message != "Fast-forward" for entry in merge_logs): + raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) + + merged_branches = [entry.action[len("merge ") :] for entry in merge_logs] + + if "with-sally" not in merged_branches: + raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) + + if any(branch != "with-sally" for branch in merged_branches): + raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) + + return exercise.to_output( + ["Great job fast-forward merging only 'with-sally' and cleaning up the branch!"], + GitAutograderStatus.SUCCESSFUL, + ) From 88cdc862cb85253fd8819a520674f4ed611a5b43 Mon Sep 17 00:00:00 2001 From: keerthigkaarthik <120106412+keerthigkaarthik@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:11:40 +0100 Subject: [PATCH 2/6] Change wrong answer message --- branch_forward/verify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/branch_forward/verify.py b/branch_forward/verify.py index e10277d..f05a389 100644 --- a/branch_forward/verify.py +++ b/branch_forward/verify.py @@ -9,7 +9,7 @@ ) ONLY_WITH_SALLY_MERGED = ( - "Only the 'with-sally' branch should be merged into 'main'. Do not merge other branches." + "Only one of the two starting branches can be fast-forward merged into 'main'. Do not create new branches." ) def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: From d664fd36b09e4dc386c4b4c4b679ecbea283ec51 Mon Sep 17 00:00:00 2001 From: keerthigkaarthik <120106412+keerthigkaarthik@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:16:26 +0100 Subject: [PATCH 3/6] Add last empty line --- branch_forward/.gitmastery-exercise.json | 2 +- branch_forward/README.md | 2 +- branch_forward/tests/specs/base.yml | 1 + branch_forward/tests/specs/merge_with_sally_no_ff.yml | 1 + branch_forward/tests/test_verify.py | 1 + branch_forward/verify.py | 1 + 6 files changed, 6 insertions(+), 2 deletions(-) diff --git a/branch_forward/.gitmastery-exercise.json b/branch_forward/.gitmastery-exercise.json index 63acafd..a68c6c0 100644 --- a/branch_forward/.gitmastery-exercise.json +++ b/branch_forward/.gitmastery-exercise.json @@ -14,4 +14,4 @@ "create_fork": null, "init": true } -} \ No newline at end of file +} diff --git a/branch_forward/README.md b/branch_forward/README.md index af774d7..f32c1fe 100644 --- a/branch_forward/README.md +++ b/branch_forward/README.md @@ -23,4 +23,4 @@ You are outlining a story and experimenting with different plotlines. Each versi Ensure you have switched to the destination branch before initiating the merge. - \ No newline at end of file + diff --git a/branch_forward/tests/specs/base.yml b/branch_forward/tests/specs/base.yml index 99af44c..6ddea2c 100644 --- a/branch_forward/tests/specs/base.yml +++ b/branch_forward/tests/specs/base.yml @@ -40,3 +40,4 @@ initialization: branch-name: main - type: merge branch-name: with-sally + diff --git a/branch_forward/tests/specs/merge_with_sally_no_ff.yml b/branch_forward/tests/specs/merge_with_sally_no_ff.yml index 1b76e03..75169f8 100644 --- a/branch_forward/tests/specs/merge_with_sally_no_ff.yml +++ b/branch_forward/tests/specs/merge_with_sally_no_ff.yml @@ -41,3 +41,4 @@ initialization: - type: merge branch-name: with-sally no-ff: true + diff --git a/branch_forward/tests/test_verify.py b/branch_forward/tests/test_verify.py index f124449..95b68eb 100644 --- a/branch_forward/tests/test_verify.py +++ b/branch_forward/tests/test_verify.py @@ -50,3 +50,4 @@ def test_merge_with_sally_no_ff(): GitAutograderStatus.UNSUCCESSFUL, [FAST_FORWARD_REQUIRED], ) + diff --git a/branch_forward/verify.py b/branch_forward/verify.py index f05a389..cc213b8 100644 --- a/branch_forward/verify.py +++ b/branch_forward/verify.py @@ -40,3 +40,4 @@ def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: ["Great job fast-forward merging only 'with-sally' and cleaning up the branch!"], GitAutograderStatus.SUCCESSFUL, ) + From da787860f9d7db665f435f936ca7ff8227da8a35 Mon Sep 17 00:00:00 2001 From: keerthigkaarthik <120106412+keerthigkaarthik@users.noreply.github.com> Date: Tue, 25 Nov 2025 23:37:42 +0100 Subject: [PATCH 4/6] Apply changes from code review --- branch_forward/download.py | 6 +-- .../tests/specs/with_sally_non_ff_then_ff.yml | 46 +++++++++++++++++++ branch_forward/tests/test_verify.py | 11 ++++- branch_forward/verify.py | 20 ++++---- 4 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 branch_forward/tests/specs/with_sally_non_ff_then_ff.yml diff --git a/branch_forward/download.py b/branch_forward/download.py index 1eff523..e75ed57 100644 --- a/branch_forward/download.py +++ b/branch_forward/download.py @@ -1,9 +1,9 @@ -from exercise_utils.file import append_to_file +from exercise_utils.file import append_to_file, create_or_update_file from exercise_utils.git import add, checkout, commit def setup(verbose: bool = False): - append_to_file( + create_or_update_file( "story.txt", """ Harry was single. @@ -32,7 +32,7 @@ def setup(verbose: bool = False): commit("Add about Ginny", verbose) checkout("main", False, verbose) - append_to_file( + create_or_update_file( "cast.txt", """ Harry diff --git a/branch_forward/tests/specs/with_sally_non_ff_then_ff.yml b/branch_forward/tests/specs/with_sally_non_ff_then_ff.yml new file mode 100644 index 0000000..1464a99 --- /dev/null +++ b/branch_forward/tests/specs/with_sally_non_ff_then_ff.yml @@ -0,0 +1,46 @@ +initialization: + steps: + - type: commit + empty: true + message: Introduce Harry + id: start + - type: commit + empty: true + message: Add about family + + - type: branch + branch-name: with-ginny + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Add about Ginny + + - type: checkout + branch-name: main + - type: commit + empty: true + message: Add cast.txt + + - type: branch + branch-name: with-sally + - type: checkout + branch-name: with-sally + - type: commit + empty: true + message: Mention Sally + + - type: checkout + branch-name: with-ginny + - type: commit + empty: true + message: Mention Ginny is single + + - type: checkout + branch-name: main + - type: bash + runs: | + git merge with-sally --no-ff -m "Non fast-forward merge" + git reset --hard 'HEAD^' + git merge with-sally + diff --git a/branch_forward/tests/test_verify.py b/branch_forward/tests/test_verify.py index 95b68eb..8bac588 100644 --- a/branch_forward/tests/test_verify.py +++ b/branch_forward/tests/test_verify.py @@ -21,7 +21,7 @@ def test_no_merges(): assert_output( output, GitAutograderStatus.UNSUCCESSFUL, - [FAST_FORWARD_REQUIRED], + [ONLY_WITH_SALLY_MERGED], ) @@ -30,7 +30,7 @@ def test_other_branch_non_ff(): assert_output( output, GitAutograderStatus.UNSUCCESSFUL, - [FAST_FORWARD_REQUIRED], + [ONLY_WITH_SALLY_MERGED], ) @@ -51,3 +51,10 @@ def test_merge_with_sally_no_ff(): [FAST_FORWARD_REQUIRED], ) + +def test_merge_with_sally_fix_after_non_ff(): + with loader.load( + "specs/with_sally_non_ff_then_ff.yml", "start" + ) as output: + assert_output(output, GitAutograderStatus.SUCCESSFUL) + diff --git a/branch_forward/verify.py b/branch_forward/verify.py index cc213b8..bd74085 100644 --- a/branch_forward/verify.py +++ b/branch_forward/verify.py @@ -18,21 +18,21 @@ def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: entry for entry in main_branch.reflog if entry.action.startswith("merge ") ] - if not merge_logs: - raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) - - main_commits = list(main_branch.commits) - if any(len(commit.parents) > 1 for commit in main_commits): - raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) - - if any(entry.message != "Fast-forward" for entry in merge_logs): - raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) - merged_branches = [entry.action[len("merge ") :] for entry in merge_logs] if "with-sally" not in merged_branches: raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) + latest_with_sally_merge = next( + (entry for entry in merge_logs if entry.action == "merge with-sally"), None + ) + + if latest_with_sally_merge is None: + raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) + + if latest_with_sally_merge.message != "Fast-forward": + raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) + if any(branch != "with-sally" for branch in merged_branches): raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) From f54d88aff4ed42ba6c16b63f08f6643bedc66e87 Mon Sep 17 00:00:00 2001 From: keerthigkaarthik <120106412+keerthigkaarthik@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:22:37 +0100 Subject: [PATCH 5/6] Apply changes following code review --- ...f_then_ff.yml => reset_ginny_ff_sally.yml} | 5 +- branch_forward/tests/test_verify.py | 4 +- branch_forward/verify.py | 46 +++++++++++++------ 3 files changed, 35 insertions(+), 20 deletions(-) rename branch_forward/tests/specs/{with_sally_non_ff_then_ff.yml => reset_ginny_ff_sally.yml} (90%) diff --git a/branch_forward/tests/specs/with_sally_non_ff_then_ff.yml b/branch_forward/tests/specs/reset_ginny_ff_sally.yml similarity index 90% rename from branch_forward/tests/specs/with_sally_non_ff_then_ff.yml rename to branch_forward/tests/specs/reset_ginny_ff_sally.yml index 1464a99..a345350 100644 --- a/branch_forward/tests/specs/with_sally_non_ff_then_ff.yml +++ b/branch_forward/tests/specs/reset_ginny_ff_sally.yml @@ -40,7 +40,6 @@ initialization: branch-name: main - type: bash runs: | - git merge with-sally --no-ff -m "Non fast-forward merge" + git merge with-ginny git reset --hard 'HEAD^' - git merge with-sally - + git merge with-sally \ No newline at end of file diff --git a/branch_forward/tests/test_verify.py b/branch_forward/tests/test_verify.py index 8bac588..df476b6 100644 --- a/branch_forward/tests/test_verify.py +++ b/branch_forward/tests/test_verify.py @@ -52,9 +52,9 @@ def test_merge_with_sally_no_ff(): ) -def test_merge_with_sally_fix_after_non_ff(): +def test_reset_ginny_ff_sally(): with loader.load( - "specs/with_sally_non_ff_then_ff.yml", "start" + "specs/reset_ginny_ff_sally.yml", "start" ) as output: assert_output(output, GitAutograderStatus.SUCCESSFUL) diff --git a/branch_forward/verify.py b/branch_forward/verify.py index bd74085..898f4a4 100644 --- a/branch_forward/verify.py +++ b/branch_forward/verify.py @@ -12,32 +12,48 @@ "Only one of the two starting branches can be fast-forward merged into 'main'. Do not create new branches." ) -def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: - main_branch = exercise.repo.branches.branch("main") - merge_logs = [ - entry for entry in main_branch.reflog if entry.action.startswith("merge ") - ] - - merged_branches = [entry.action[len("merge ") :] for entry in merge_logs] +EXPECTED_MAIN_COMMIT_MESSAGES = { + "Introduce Harry", + "Add about family", + "Add cast.txt", + "Mention Sally", +} - if "with-sally" not in merged_branches: - raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + repo = exercise.repo.repo + head_commit = repo.head.commit + main_commits = list(repo.iter_commits("main")) - latest_with_sally_merge = next( - (entry for entry in merge_logs if entry.action == "merge with-sally"), None + sally_commit = next( + (commit for commit in main_commits if commit.message.strip() == "Mention Sally"), + None, ) + if sally_commit is None: + raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) - if latest_with_sally_merge is None: + # Confirm that the fast-forward landed exactly on the expected commit and did not + # introduce a merge commit. + if len(head_commit.parents) != 1: raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) - if latest_with_sally_merge.message != "Fast-forward": + if head_commit.message.strip() != "Mention Sally": + raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) + + if any(len(commit.parents) > 1 for commit in main_commits): raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) - if any(branch != "with-sally" for branch in merged_branches): + if len(main_commits) != len(EXPECTED_MAIN_COMMIT_MESSAGES): + raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) + + commit_messages = {commit.message.strip() for commit in main_commits} + if not commit_messages.issubset(EXPECTED_MAIN_COMMIT_MESSAGES): + raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) + + if "Mention Ginny is single" in commit_messages: raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) return exercise.to_output( - ["Great job fast-forward merging only 'with-sally' and cleaning up the branch!"], + ["Great job fast-forward merging only 'with-sally'!"], GitAutograderStatus.SUCCESSFUL, ) From 4b3ca64cc64f4257162fa3add1043fdfb41f368f Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:00:54 +0800 Subject: [PATCH 6/6] [branch-forward] Refactor verify.py and remove unnecessary test --- branch_forward/README.md | 9 +--- .../tests/specs/reset_ginny_ff_sally.yml | 45 ------------------- branch_forward/tests/test_verify.py | 8 ---- branch_forward/verify.py | 32 +++++++------ 4 files changed, 20 insertions(+), 74 deletions(-) delete mode 100644 branch_forward/tests/specs/reset_ginny_ff_sally.yml diff --git a/branch_forward/README.md b/branch_forward/README.md index f32c1fe..55567b6 100644 --- a/branch_forward/README.md +++ b/branch_forward/README.md @@ -1,12 +1,6 @@ # branch-forward -You are outlining a story and experimenting with different plotlines. Each version lives on its own branch, and you now need to fold the right storyline back into `main` without cluttering the history. - -## Learning objectives - -- Identify when a branch can be fast-forward merged -- Use fast-forward merges to keep the commit graph linear -- [Fast-forward merges](https://nus-cs2103-ay2526s1.github.io/website/book/gitAndGithub/merge/index.html) +You are writing the outline for a story. You now have two parallel storylines in two branches. ## Task @@ -16,7 +10,6 @@ You are outlining a story and experimenting with different plotlines. Each versi ## Hints -
Hint 1 diff --git a/branch_forward/tests/specs/reset_ginny_ff_sally.yml b/branch_forward/tests/specs/reset_ginny_ff_sally.yml deleted file mode 100644 index a345350..0000000 --- a/branch_forward/tests/specs/reset_ginny_ff_sally.yml +++ /dev/null @@ -1,45 +0,0 @@ -initialization: - steps: - - type: commit - empty: true - message: Introduce Harry - id: start - - type: commit - empty: true - message: Add about family - - - type: branch - branch-name: with-ginny - - type: checkout - branch-name: with-ginny - - type: commit - empty: true - message: Add about Ginny - - - type: checkout - branch-name: main - - type: commit - empty: true - message: Add cast.txt - - - type: branch - branch-name: with-sally - - type: checkout - branch-name: with-sally - - type: commit - empty: true - message: Mention Sally - - - type: checkout - branch-name: with-ginny - - type: commit - empty: true - message: Mention Ginny is single - - - type: checkout - branch-name: main - - type: bash - runs: | - git merge with-ginny - git reset --hard 'HEAD^' - git merge with-sally \ No newline at end of file diff --git a/branch_forward/tests/test_verify.py b/branch_forward/tests/test_verify.py index df476b6..433727d 100644 --- a/branch_forward/tests/test_verify.py +++ b/branch_forward/tests/test_verify.py @@ -50,11 +50,3 @@ def test_merge_with_sally_no_ff(): GitAutograderStatus.UNSUCCESSFUL, [FAST_FORWARD_REQUIRED], ) - - -def test_reset_ginny_ff_sally(): - with loader.load( - "specs/reset_ginny_ff_sally.yml", "start" - ) as output: - assert_output(output, GitAutograderStatus.SUCCESSFUL) - diff --git a/branch_forward/verify.py b/branch_forward/verify.py index 898f4a4..4874f07 100644 --- a/branch_forward/verify.py +++ b/branch_forward/verify.py @@ -1,4 +1,6 @@ +from typing import List, Optional from git_autograder import ( + GitAutograderCommit, GitAutograderExercise, GitAutograderOutput, GitAutograderStatus, @@ -8,9 +10,7 @@ "You must use a fast-forward merge to bring a branch into 'main'." ) -ONLY_WITH_SALLY_MERGED = ( - "Only one of the two starting branches can be fast-forward merged into 'main'. Do not create new branches." -) +ONLY_WITH_SALLY_MERGED = "Only one of the two starting branches can be fast-forward merged into 'main'. Do not create new branches." EXPECTED_MAIN_COMMIT_MESSAGES = { "Introduce Harry", @@ -19,15 +19,22 @@ "Mention Sally", } + +def get_commit_from_message( + commits: List[GitAutograderCommit], message: str +) -> Optional[GitAutograderCommit]: + """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 verify(exercise: GitAutograderExercise) -> GitAutograderOutput: - repo = exercise.repo.repo - head_commit = repo.head.commit - main_commits = list(repo.iter_commits("main")) + main_commits = exercise.repo.branches.branch("main").commits + head_commit = exercise.repo.branches.branch("main").latest_commit - sally_commit = next( - (commit for commit in main_commits if commit.message.strip() == "Mention Sally"), - None, - ) + sally_commit = get_commit_from_message(main_commits, "Mention Sally") if sally_commit is None: raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) @@ -36,7 +43,7 @@ def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: if len(head_commit.parents) != 1: raise exercise.wrong_answer([FAST_FORWARD_REQUIRED]) - if head_commit.message.strip() != "Mention Sally": + if head_commit.commit.message.strip() != "Mention Sally": raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) if any(len(commit.parents) > 1 for commit in main_commits): @@ -45,7 +52,7 @@ def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: if len(main_commits) != len(EXPECTED_MAIN_COMMIT_MESSAGES): raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) - commit_messages = {commit.message.strip() for commit in main_commits} + commit_messages = {commit.commit.message.strip() for commit in main_commits} if not commit_messages.issubset(EXPECTED_MAIN_COMMIT_MESSAGES): raise exercise.wrong_answer([ONLY_WITH_SALLY_MERGED]) @@ -56,4 +63,3 @@ def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: ["Great job fast-forward merging only 'with-sally'!"], GitAutograderStatus.SUCCESSFUL, ) -