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,
)
-