diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..7d13c4a
Binary files /dev/null and b/.DS_Store differ
diff --git a/ff_undo/.gitmastery-exercise.json b/ff_undo/.gitmastery-exercise.json
new file mode 100644
index 0000000..53fddb6
--- /dev/null
+++ b/ff_undo/.gitmastery-exercise.json
@@ -0,0 +1,18 @@
+{
+ "exercise_name": "ff-undo",
+ "tags": [
+ "git-branch",
+ "git-merge",
+ "git-reset"
+ ],
+ "requires_git": true,
+ "requires_github": false,
+ "base_files": {},
+ "exercise_repo": {
+ "repo_type": "local",
+ "repo_name": "play-characters",
+ "repo_title": null,
+ "create_fork": null,
+ "init": true
+ }
+}
\ No newline at end of file
diff --git a/ff_undo/README.md b/ff_undo/README.md
new file mode 100644
index 0000000..3eabfbc
--- /dev/null
+++ b/ff_undo/README.md
@@ -0,0 +1,41 @@
+# ff-undo
+
+This exercise focuses on **undoing a merge in Git**. You will practice how to revert unwanted merge commits while keeping branches and commits intact.
+
+## Task
+
+You have a repository with two branches:
+
+- `main` branch, which initially contains commits:
+ - `Add Rick`
+ - `Add Morty`
+- `others` branch, which contains commits:
+ - `Add Birdperson`
+ - `Add Cyborg to birdperson.txt`
+ - `Add Tammy`
+
+A merge with fast forward from `others` into `main` has been done incorrectly. Your task is:
+
+1. **Undo the merge on `main`**, so that only `Add Rick` and `Add Morty` remain on `main`.
+2. Ensure the `others` branch still exists with all its commits intact.
+3. Do not delete any commits; only undo the merge on `main`.
+
+## Hints
+
+
+Hint 1: Check your branches
+
+Use `git branch` to see the current branches and verify `main` and `others` exist.
+
+
+
+Hint 2: View commit history
+
+Use `git log --oneline` on `main` to identify the merge commit that needs to be undone.
+
+
+
+Hint 3: Undo the merge
+
+You can undo a merge using: `git reset --hard `
+
\ No newline at end of file
diff --git a/ff_undo/__init__.py b/ff_undo/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ff_undo/download.py b/ff_undo/download.py
new file mode 100644
index 0000000..686c15b
--- /dev/null
+++ b/ff_undo/download.py
@@ -0,0 +1,39 @@
+from exercise_utils.git import (
+ add,
+ commit,
+ checkout,
+ merge_with_message,
+)
+from exercise_utils.file import (
+ create_or_update_file,
+ append_to_file,
+)
+
+def setup(verbose: bool = False):
+ # Create initial files and commits
+ create_or_update_file("rick.txt", "Scientist\n")
+ add(["rick.txt"], verbose)
+ commit("Add Rick", verbose)
+
+ create_or_update_file("morty.txt", "Boy\n")
+ add(["morty.txt"], verbose)
+ commit("Add Morty", verbose)
+
+ # Create and switch to branch 'others'
+ checkout("others", create_branch=True, verbose=verbose)
+
+ create_or_update_file("birdperson.txt", "No job\n")
+ add(["birdperson.txt"], verbose)
+ commit("Add Birdperson", verbose)
+
+ append_to_file("birdperson.txt", "Cyborg\n")
+ add(["birdperson.txt"], verbose)
+ commit("Add Cyborg to birdperson.txt", verbose)
+
+ create_or_update_file("tammy.txt", "Spy\n")
+ add(["tammy.txt"], verbose)
+ commit("Add Tammy", verbose)
+
+ # Merge back into main
+ checkout("main", create_branch=False, verbose=verbose)
+ merge_with_message("others", ff=True, message="Introduce others", verbose=verbose)
\ No newline at end of file
diff --git a/ff_undo/tests/__init__.py b/ff_undo/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ff_undo/tests/specs/base.yml b/ff_undo/tests/specs/base.yml
new file mode 100644
index 0000000..23847df
--- /dev/null
+++ b/ff_undo/tests/specs/base.yml
@@ -0,0 +1,19 @@
+initialization:
+ steps:
+ - type: commit
+ id: start
+ message: "Add Rick"
+ - type: commit
+ message: "Add Morty"
+ - type: branch
+ branch-name: others
+ - type: checkout
+ branch-name: others
+ - type: commit
+ message: "Add Birdperson"
+ - type: commit
+ message: "Add Cyborg to birdperson.txt"
+ - type: commit
+ message: "Add Tammy"
+ - type: checkout
+ branch-name: main
\ No newline at end of file
diff --git a/ff_undo/tests/specs/branch_missing.yml b/ff_undo/tests/specs/branch_missing.yml
new file mode 100644
index 0000000..dcc9aaa
--- /dev/null
+++ b/ff_undo/tests/specs/branch_missing.yml
@@ -0,0 +1,7 @@
+initialization:
+ steps:
+ - type: commit
+ id: start
+ message: "Add Rick"
+ - type: commit
+ message: "Add Morty"
\ No newline at end of file
diff --git a/ff_undo/tests/specs/main_commits_incorrect.yml b/ff_undo/tests/specs/main_commits_incorrect.yml
new file mode 100644
index 0000000..0a6e399
--- /dev/null
+++ b/ff_undo/tests/specs/main_commits_incorrect.yml
@@ -0,0 +1,15 @@
+initialization:
+ steps:
+ - type: commit
+ id: start
+ message: "Add Morty"
+ - type: branch
+ branch-name: others
+ - type: checkout
+ branch-name: others
+ - type: commit
+ message: "Add Birdperson"
+ - type: commit
+ message: "Add Cyborg to birdperson.txt"
+ - type: commit
+ message: "Add Tammy"
\ No newline at end of file
diff --git a/ff_undo/tests/specs/merge_not_undone.yml b/ff_undo/tests/specs/merge_not_undone.yml
new file mode 100644
index 0000000..3778fe4
--- /dev/null
+++ b/ff_undo/tests/specs/merge_not_undone.yml
@@ -0,0 +1,23 @@
+initialization:
+ steps:
+ - type: commit
+ id: start
+ message: "Add Rick"
+ - type: commit
+ message: "Add Morty"
+ - type: branch
+ branch-name: others
+ - type: checkout
+ branch-name: others
+ - type: commit
+ message: "Add Birdperson"
+ - type: commit
+ message: "Add Cyborg to birdperson.txt"
+ - type: commit
+ message: "Add Tammy"
+ - type: checkout
+ branch-name: main
+ - type: merge
+ branch-name: others
+ no-ff: false
+ message: "Introduce others"
\ No newline at end of file
diff --git a/ff_undo/tests/specs/others_commits_incorrect.yml b/ff_undo/tests/specs/others_commits_incorrect.yml
new file mode 100644
index 0000000..59fa6c6
--- /dev/null
+++ b/ff_undo/tests/specs/others_commits_incorrect.yml
@@ -0,0 +1,15 @@
+initialization:
+ steps:
+ - type: commit
+ id: start
+ message: "Add Rick"
+ - type: commit
+ message: "Add Morty"
+ - type: branch
+ branch-name: others
+ - type: checkout
+ branch-name: others
+ - type: commit
+ message: "Add Birdperson"
+ - type: commit
+ message: "Add Tammy"
diff --git a/ff_undo/tests/test_verify.py b/ff_undo/tests/test_verify.py
new file mode 100644
index 0000000..097bd76
--- /dev/null
+++ b/ff_undo/tests/test_verify.py
@@ -0,0 +1,37 @@
+from git_autograder import GitAutograderTestLoader, assert_output
+from git_autograder.status import GitAutograderStatus
+from ..verify import (
+ MERGE_NOT_UNDONE,
+ MAIN_COMMITS_INCORRECT,
+ OTHERS_COMMITS_INCORRECT,
+ OTHERS_BRANCH_MISSING,
+ verify
+)
+
+REPOSITORY_NAME = "ff-undo"
+
+loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify)
+
+def test_correct_solution():
+ with loader.load("specs/base.yml") as output:
+ assert_output(output, GitAutograderStatus.SUCCESSFUL)
+
+
+def test_merge_not_undone():
+ with loader.load("specs/merge_not_undone.yml") as output:
+ assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [MERGE_NOT_UNDONE])
+
+
+def test_branch_missing():
+ with loader.load("specs/branch_missing.yml") as output:
+ assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [OTHERS_BRANCH_MISSING])
+
+
+def test_main_commits_incorrect():
+ with loader.load("specs/main_commits_incorrect.yml") as output:
+ assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [MAIN_COMMITS_INCORRECT])
+
+
+def test_others_commits_incorrect():
+ with loader.load("specs/others_commits_incorrect.yml") as output:
+ assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [OTHERS_COMMITS_INCORRECT])
diff --git a/ff_undo/verify.py b/ff_undo/verify.py
new file mode 100644
index 0000000..83c73f8
--- /dev/null
+++ b/ff_undo/verify.py
@@ -0,0 +1,62 @@
+from git_autograder import (
+ GitAutograderOutput,
+ GitAutograderExercise,
+ GitAutograderStatus,
+)
+
+ADD_RICK = "Add Rick"
+ADD_MORTY = "Add Morty"
+ADD_BIRDPERSON = "Add Birdperson"
+ADD_CYBORG = "Add Cyborg to birdperson.txt"
+ADD_TAMMY = "Add Tammy"
+
+MERGE_NOT_UNDONE = (
+ "You need to undo the merge."
+)
+MAIN_COMMITS_INCORRECT = (
+ "The main branch does not contain the expected commits "
+ "The main branch does not contain both commits 'Add Rick' and 'Add Morty'."
+)
+OTHERS_COMMITS_INCORRECT = (
+ "The others branch does not contain the expected commits "
+ "'Add Birdperson', 'Add Cyborg to birdperson.txt', and 'Add Tammy'."
+)
+OTHERS_BRANCH_MISSING = (
+ "The branch 'others' no longer exists. You should not delete it, only undo the merge on main."
+)
+
+def verify(exercise: GitAutograderExercise) -> GitAutograderOutput:
+ # Get branches
+ main_branch = exercise.repo.branches.branch("main")
+ others_branch = exercise.repo.branches.branch_or_none("others")
+
+ # Check if branch others exists
+ if others_branch is None:
+ raise exercise.wrong_answer([OTHERS_BRANCH_MISSING])
+
+ # Take all commit messages on main
+ commit_messages_in_main = [c.commit.message.strip() for c in main_branch.commits]
+
+ # Take all commit messages on others
+ commit_messages_in_others = [c.commit.message.strip() for c in others_branch.commits]
+
+ # Check that the merge commit is not present on main
+ if any(msg in commit_messages_in_main for msg in [ADD_BIRDPERSON, ADD_CYBORG, ADD_TAMMY]):
+ raise exercise.wrong_answer([MERGE_NOT_UNDONE])
+
+ # Check that commits in main are only the initial 2 commits
+ if len(commit_messages_in_main) != 2 or not all(
+ msg in commit_messages_in_main for msg in [ADD_RICK, ADD_MORTY]
+ ):
+ raise exercise.wrong_answer([MAIN_COMMITS_INCORRECT])
+
+ # Check that commits in others are only the initial 3 commits
+ if len(commit_messages_in_others) != 5 or not all(
+ msg in commit_messages_in_others for msg in [ADD_BIRDPERSON, ADD_CYBORG, ADD_TAMMY]
+ ):
+ raise exercise.wrong_answer([OTHERS_COMMITS_INCORRECT])
+
+ return exercise.to_output(
+ ["You have successfully undone the merge of branch 'others'."],
+ GitAutograderStatus.SUCCESSFUL,
+ )