diff --git a/reviewboard/hostingsvcs/bitbucket.py b/reviewboard/hostingsvcs/bitbucket.py
index e796ce0924c4b143283d447e9c7a9c1e5fb5854f..7a6f51508533aee3965fc00f45328cdc8c8ff4c2 100644
--- a/reviewboard/hostingsvcs/bitbucket.py
+++ b/reviewboard/hostingsvcs/bitbucket.py
@@ -5,6 +5,7 @@
 import json
 import logging
 from collections import defaultdict
+from typing import TYPE_CHECKING
 from urllib.error import HTTPError
 from urllib.parse import quote
 
@@ -42,6 +43,9 @@
                                          RepositoryNotFoundError)
 from reviewboard.site.urlresolvers import local_site_reverse
 
+if TYPE_CHECKING:
+    from django.http import HttpRequest
+
 
 logger = logging.getLogger(__name__)
 
@@ -151,32 +155,38 @@
 
     @staticmethod
     @require_POST
-    def post_receive_hook_close_submitted(request, local_site_name=None,
-                                          repository_id=None,
-                                          hosting_service_id=None,
-                                          hooks_uuid=None):
+    def post_receive_hook_close_submitted(
+        request: HttpRequest,
+        local_site_name: (str | None) = None,
+        repository_id: (int | None) = None,
+        hosting_service_id: (str | None) = None,
+        hooks_uuid: (str | None) = None,
+    ) -> HttpResponse:
         """Close review requests as submitted automatically after a push.
 
         Args:
             request (django.http.HttpRequest):
                 The request from the Bitbucket webhook.
 
-            local_site_name (unicode, optional):
+            local_site_name (str, optional):
                 The local site name, if available.
 
             repository_id (int, optional):
                 The pk of the repository, if available.
 
-            hosting_service_id (unicode, optional):
+            hosting_service_id (str, optional):
                 The name of the hosting service.
 
-            hooks_uuid (unicode, optional):
+            hooks_uuid (str, optional):
                 The UUID of the configured webhook.
 
         Returns:
             django.http.HttpResponse:
             A response for the request.
         """
+        assert repository_id is not None
+        assert hosting_service_id is not None
+
         repository = get_repository_for_hook(
             repository_id=repository_id,
             hosting_service_id=hosting_service_id,
@@ -204,9 +214,11 @@
                 'repository on Review Board.')
 
         if review_request_id_to_commits:
-            close_all_review_requests(review_request_id_to_commits,
-                                      local_site_name, repository,
-                                      hosting_service_id)
+            close_all_review_requests(
+                review_request_id_to_commits=review_request_id_to_commits,
+                local_site_name=local_site_name,
+                repository=repository,
+                hosting_service_id=hosting_service_id)
 
         return HttpResponse()
 
diff --git a/reviewboard/hostingsvcs/github.py b/reviewboard/hostingsvcs/github.py
index d6edad62829fcffd2e76189454d7f057b7cf052c..f7acd7848c1c9f7cfa8807ca5013e5c4ffc9ccae 100644
--- a/reviewboard/hostingsvcs/github.py
+++ b/reviewboard/hostingsvcs/github.py
@@ -50,6 +50,8 @@
 if TYPE_CHECKING:
     from urllib.error import URLError
 
+    from django.http import HttpRequest
+
     from reviewboard.hostingsvcs.base.bug_tracker import BugInfo
     from reviewboard.hostingsvcs.base.http import HostingServiceHTTPRequest
     from reviewboard.scmtools.models import Repository
@@ -497,22 +499,25 @@
 
     @staticmethod
     @require_POST
-    def post_receive_hook_close_submitted(request, local_site_name=None,
-                                          repository_id=None,
-                                          hosting_service_id=None):
+    def post_receive_hook_close_submitted(
+        request: HttpRequest,
+        local_site_name: (str | None) = None,
+        repository_id: (int | None) = None,
+        hosting_service_id: (str | None) = None,
+    ) -> HttpResponse:
         """Close review requests as submitted automatically after a push.
 
         Args:
             request (django.http.HttpRequest):
                 The request from the Bitbucket webhook.
 
-            local_site_name (unicode):
+            local_site_name (str, optional):
                 The local site name, if available.
 
-            repository_id (int):
+            repository_id (int, optional):
                 The pk of the repository, if available.
 
-            hosting_service_id (unicode):
+            hosting_service_id (str, optional):
                 The name of the hosting service.
 
         Returns:
@@ -529,8 +534,13 @@
             return HttpResponseBadRequest(
                 'Only "ping" and "push" events are supported.')
 
-        repository = get_repository_for_hook(repository_id, hosting_service_id,
-                                             local_site_name)
+        assert repository_id is not None
+        assert hosting_service_id is not None
+
+        repository = get_repository_for_hook(
+            repository_id=repository_id,
+            hosting_service_id=hosting_service_id,
+            local_site_name=local_site_name)
 
         # Validate the hook against the stored UUID.
         m = hmac.new(repository.get_or_create_hooks_uuid().encode('utf-8'),
@@ -557,9 +567,11 @@
                 payload, server_url, repository)
 
         if review_request_id_to_commits:
-            close_all_review_requests(review_request_id_to_commits,
-                                      local_site_name, repository,
-                                      hosting_service_id)
+            close_all_review_requests(
+                review_request_id_to_commits=review_request_id_to_commits,
+                local_site_name=local_site_name,
+                repository=repository,
+                hosting_service_id=hosting_service_id)
 
         return HttpResponse()
 
@@ -601,7 +613,10 @@
             commit_hash = commit.get('id')
             commit_message = commit.get('message')
             review_request_id = get_review_request_id(
-                commit_message, server_url, commit_hash, repository)
+                commit_message=commit_message,
+                server_url=server_url,
+                commit_id=commit_hash,
+                repository=repository)
 
             review_request_id_to_commits_map[review_request_id].append(
                 '%s (%s)' % (branch_name, commit_hash[:7]))
diff --git a/reviewboard/hostingsvcs/hook_utils.py b/reviewboard/hostingsvcs/hook_utils.py
index 52fb4279004d7cec950a92c75e294f4ccb1a73a5..810552fc4ff8fb67e577a6c97989fe5fb3aa5450 100644
--- a/reviewboard/hostingsvcs/hook_utils.py
+++ b/reviewboard/hostingsvcs/hook_utils.py
@@ -1,29 +1,81 @@
+"""Utilities for WebHook handlers."""
+
+from __future__ import annotations
+
 import logging
 import re
+from typing import TYPE_CHECKING
 
 from django.conf import settings
 from django.db.models import Q
 from django.shortcuts import get_object_or_404
+from housekeeping import deprecate_non_keyword_only_args
 
+from reviewboard.deprecation import RemovedInReviewBoard10_0Warning
 from reviewboard.reviews.models import ReviewRequest
 from reviewboard.scmtools.models import Repository
 from reviewboard.site.models import LocalSite
 
+if TYPE_CHECKING:
+    from collections.abc import Mapping, Sequence
+
 
 logger = logging.getLogger(__name__)
 
 
-def get_git_branch_name(ref_name):
-    """Returns the branch name corresponding to the specified ref name."""
+def get_git_branch_name(
+    ref_name: str,
+) -> str:
+    """Return the branch name corresponding to the specified ref name.
+
+    Args:
+        ref_name (str):
+            The name of the Git reference.
+
+    Returns:
+        str:
+        The isolated branch name.
+    """
     branch_ref_prefix = 'refs/heads/'
 
-    if ref_name.startswith(branch_ref_prefix):
-        return ref_name[len(branch_ref_prefix):]
-
-
-def get_repository_for_hook(repository_id, hosting_service_id,
-                            local_site_name, hooks_uuid=None):
-    """Returns a Repository for the given hook parameters."""
+    return ref_name.removeprefix(branch_ref_prefix)
+
+
+@deprecate_non_keyword_only_args(RemovedInReviewBoard10_0Warning)
+def get_repository_for_hook(
+    *,
+    repository_id: int,
+    hosting_service_id: str,
+    local_site_name: str | None,
+    hooks_uuid: (str | None) = None,
+) -> Repository:
+    """Return a Repository for the given hook parameters.
+
+    Version Changed:
+        8.0:
+        Made arguments keyword-only.
+
+    Args:
+        repository_id (int):
+            The ID of the repository.
+
+        hosting_service_id (str):
+            The name of the hosting service.
+
+        local_site_name (str):
+            The Local Site name, if available.
+
+        hooks_uuid (str, optional):
+            The UUID to use for WebHooks for this repository.
+
+    Returns:
+        reviewboard.scmtools.models.Repository:
+        The repository object.
+
+    Raises:
+        django.http.Http404:
+            A repository with the given parameters was not found.
+    """
     q = (Q(pk=repository_id) &
          Q(hosting_account__service_name=hosting_service_id))
 
@@ -38,9 +90,15 @@
     return get_object_or_404(Repository, q)
 
 
-def get_review_request_id(commit_message, server_url, commit_id=None,
-                          repository=None):
-    """Returns the review request ID matching the pushed commit.
+@deprecate_non_keyword_only_args(RemovedInReviewBoard10_0Warning)
+def get_review_request_id(
+    *,
+    commit_message: str,
+    server_url: str,
+    commit_id: (str | None) = None,
+    repository: (Repository | None) = None,
+) -> int | None:
+    """Return the review request ID matching the pushed commit.
 
     We first use a regex (that can be overridden in settings_local.py) to try
     to find a matching review request ID in the commit message. If no match is
@@ -49,9 +107,27 @@
 
     We assume there is at most one review request associated with each commit.
     If a matching review request cannot be found, we return None.
+
+    Args:
+        commit_message (str):
+            The commit message from the hosting service.
+
+        server_url (str):
+            The URL to the Review Board server.
+
+        commit_id (str):
+            The ID of the commit.
+
+        repository (reviewboard.scmtools.models.Repository):
+            The repository for the commit.
+
+    Returns:
+        int:
+        The ID of the review request that corresponds to the pushed commit. If
+        no review request could be found, this will be ``None``.
     """
     regex = settings.HOSTINGSVCS_HOOK_REGEX % {
-        'server_url': server_url,
+        'server_url': re.escape(server_url),
     }
 
     pattern = re.compile(regex, settings.HOSTINGSVCS_HOOK_REGEX_FLAGS)
@@ -79,8 +155,25 @@
     return review_request_id
 
 
-def close_review_request(review_request, review_request_id, description):
-    """Closes the specified review request as submitted."""
+@deprecate_non_keyword_only_args(RemovedInReviewBoard10_0Warning)
+def close_review_request(
+    *,
+    review_request: ReviewRequest,
+    review_request_id: int,
+    description: str,
+) -> None:
+    """Close the specified review request as submitted.
+
+    Args:
+        review_request (reviewboard.reviews.models.ReviewRequest):
+            The review request to close.
+
+        review_request_id (int):
+            The ID of the review request.
+
+        description (str):
+            The close description to use.
+    """
     if review_request.status == ReviewRequest.SUBMITTED:
         logger.warning('Review request #%s is already submitted.',
                        review_request_id)
@@ -97,13 +190,32 @@
                  review_request_id, review_request.status)
 
 
-def close_all_review_requests(review_request_id_to_commits, local_site_name,
-                              repository, hosting_service_id):
-    """Closes each review request in the given dictionary as submitted.
+@deprecate_non_keyword_only_args(RemovedInReviewBoard10_0Warning)
+def close_all_review_requests(
+    *,
+    review_request_id_to_commits: Mapping[int | None, Sequence[str]],
+    local_site_name: str | None,
+    repository: Repository,
+    hosting_service_id: str,
+) -> None:
+    """Close each review request in the given dictionary as submitted.
 
     The provided dictionary should map a review request ID (int) to commits
     associated with that review request ID (list of strings). Commits that are
     not associated with any review requests have the key None.
+
+    Args:
+        review_request_id_to_commits (dict):
+            A mapping from review request ID to a list of
+
+        local_site_name (str):
+            The Local Site name, if available.
+
+        repository (reviewboard.scmtools.models.Repository):
+            The repository for all review requests.
+
+        hosting_service_id (str):
+            The name of the hosting service.
     """
     if local_site_name:
         try:
@@ -140,10 +252,10 @@
 
     # Check if there are any listed that we couldn't find, and log them.
     if len(review_request_ids) != len(review_requests):
-        id_to_review_request = dict(
-            (review_request.display_id, review_request)
+        id_to_review_request = {
+            review_request.display_id: review_request
             for review_request in review_requests
-        )
+        }
 
         for review_request_id in review_request_ids:
             if review_request_id not in id_to_review_request:
@@ -155,8 +267,11 @@
     for review_request in review_requests:
         review_request_id = review_request.display_id
 
+        commits_info = ', '.join(
+            review_request_id_to_commits[review_request_id])
+
         close_review_request(
-            review_request,
-            review_request_id,
-            ('Pushed to ' +
-             ', '.join(review_request_id_to_commits[review_request_id])))
+            review_request=review_request,
+            review_request_id=review_request_id,
+            description=f'Pushed to {commits_info}',
+        )
diff --git a/reviewboard/hostingsvcs/rbgateway.py b/reviewboard/hostingsvcs/rbgateway.py
index 305dc68017b9eac22b5ff7a6145bf1450d501b2b..46eeea21bd5e40818b18ad39dbd017af0539f892 100644
--- a/reviewboard/hostingsvcs/rbgateway.py
+++ b/reviewboard/hostingsvcs/rbgateway.py
@@ -7,6 +7,7 @@
 import json
 import logging
 from collections import defaultdict
+from typing import TYPE_CHECKING
 from urllib.error import HTTPError
 from urllib.parse import quote
 
@@ -35,26 +36,32 @@
                                          RepositoryNotFoundError)
 from reviewboard.site.urlresolvers import local_site_reverse
 
+if TYPE_CHECKING:
+    from django.http import HttpRequest
+
 
 logger = logging.getLogger(__name__)
 
 
-def hook_close_submitted(request, local_site_name=None,
-                         repository_id=None,
-                         hosting_service_id=None):
+def hook_close_submitted(
+    request: HttpRequest,
+    local_site_name: (str | None) = None,
+    repository_id: (int | None) = None,
+    hosting_service_id: (str | None) = None,
+) -> HttpResponse:
     """Close review requests as submitted after a push.
 
     Args:
         request (django.http.HttpRequest):
             The request from the RB Gateway webhook.
 
-        local_site_name (unicode, optional):
+        local_site_name (str, optional):
             The local site name, if available.
 
         repository_id (int, optional):
             The ID of the repository, if available.
 
-        hosting_service_id (unicode, optional):
+        hosting_service_id (str, optional):
             The ID of the hosting service.
 
     Returns:
@@ -69,8 +76,13 @@
         return HttpResponseBadRequest(
             'Only "ping" and "push" events are supported.')
 
-    repository = get_repository_for_hook(repository_id, hosting_service_id,
-                                         local_site_name)
+    assert repository_id is not None
+    assert hosting_service_id is not None
+
+    repository = get_repository_for_hook(
+        repository_id=repository_id,
+        hosting_service_id=hosting_service_id,
+        local_site_name=local_site_name)
 
     sig = request.META.get('HTTP_X_RBG_SIGNATURE', '')
     m = hmac.new(repository.get_or_create_hooks_uuid().encode('utf-8'),
@@ -97,7 +109,10 @@
         commit_id = commit.get('id')
         commit_message = commit.get('message')
         review_request_id = get_review_request_id(
-            commit_message, server_url, commit_id, repository)
+            commit_message=commit_message,
+            server_url=server_url,
+            commit_id=commit_id,
+            repository=repository)
 
         targets = commit['target']
 
@@ -118,10 +133,11 @@
         review_request_ids_to_commits[review_request_id].append(target_str)
 
     if review_request_ids_to_commits:
-        close_all_review_requests(review_request_ids_to_commits,
-                                  local_site_name,
-                                  repository,
-                                  hosting_service_id)
+        close_all_review_requests(
+            review_request_id_to_commits=review_request_ids_to_commits,
+            local_site_name=local_site_name,
+            repository=repository,
+            hosting_service_id=hosting_service_id)
 
     return HttpResponse()
 
