project: make fast-forward merges explicit

Every caller expects _FastForward to reject a merge commit. Always pass
--ff-only and remove the optional parameter so a violated ancestry
assumption fails instead of silently creating a merge.

Bug: 553599402
Change-Id: I04690b5ca675cb7d3062df08d5e20cd82f5b9e82
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/634003
Tested-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Brian Gan <brgan@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
diff --git a/project.py b/project.py
index 26e7845..a10de99 100644
--- a/project.py
+++ b/project.py
@@ -3963,14 +3963,15 @@
         if GitCommand(self, cmd).Wait() != 0:
             raise GitError(f"{self.name} rebase {upstream} ", project=self.name)
 
-    def _FastForward(self, head, ffonly=False, quiet=True):
-        cmd = ["merge", "--no-stat", head]
-        if ffonly:
-            cmd.append("--ff-only")
+    def _FastForward(self, head: str, quiet: bool = True) -> None:
+        cmd = ["merge", "--no-stat", "--ff-only"]
         if quiet:
             cmd.append("-q")
+        cmd.append(head)
         if GitCommand(self, cmd).Wait() != 0:
-            raise GitError(f"{self.name} merge {head} ", project=self.name)
+            raise GitError(
+                f"{self.name} merge --ff-only {head}", project=self.name
+            )
 
     def _ReprojectCheckout(
         self, revid: str, head: Optional[str], verbose: bool = False
diff --git a/subcmds/download.py b/subcmds/download.py
index 1c0bf5c..edeadef 100644
--- a/subcmds/download.py
+++ b/subcmds/download.py
@@ -195,7 +195,7 @@
                 elif opt.revert:
                     project._Revert(dl.commit)
                 elif opt.ffonly:
-                    project._FastForward(dl.commit, ffonly=True)
+                    project._FastForward(dl.commit)
                 else:
                     if opt.branch:
                         project.StartBranch(opt.branch, revision=dl.commit)
diff --git a/tests/test_project.py b/tests/test_project.py
index e7a3bec..ab20105 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -775,6 +775,29 @@
             )
             self.assertEqual(2, proj.work_git.DiffZ.call_count)
 
+    def test_fast_forward_always_rejects_merge_commits(self) -> None:
+        """The fast-forward helper always passes --ff-only to Git."""
+        with utils_for_test.TempGitTree() as tempdir:
+            proj = _create_mock_project(tempdir)
+            with mock.patch.object(
+                project, "GitCommand", autospec=True
+            ) as run_git:
+                run_git.return_value.Wait.return_value = 0
+                proj._FastForward("revision")
+                run_git.assert_called_once_with(
+                    proj, ["merge", "--no-stat", "--ff-only", "-q", "revision"]
+                )
+
+                run_git.reset_mock()
+                proj._FastForward("revision", quiet=False)
+                run_git.assert_called_once_with(
+                    proj, ["merge", "--no-stat", "--ff-only", "revision"]
+                )
+
+                run_git.return_value.Wait.return_value = 1
+                with self.assertRaises(error.GitError):
+                    proj._FastForward("revision")
+
     @unittest.skipUnless(
         utils_for_test.supports_reftable(),
         "git reftable support is required for this test",