bazel_roll: Make roller code more flexible

Don't add any features for rolling anything but git_repository and
git_override rules, but change the code that does roll them to not be so
tied to exactly those rules and their arguments.

Bug: b/376915917
Change-Id: Ic4a6281cea3b376f69e7c25e7c92dc8b9a9c4b17
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/246392
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Reviewed-by: Taylor Cramer <cramertj@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
diff --git a/recipe_modules/bazel_roll/api.py b/recipe_modules/bazel_roll/api.py
index 2b9d573..cec6fdc 100644
--- a/recipe_modules/bazel_roll/api.py
+++ b/recipe_modules/bazel_roll/api.py
@@ -74,12 +74,15 @@
 
 
 @dataclasses.dataclass
-class UpdateCommitHashResult:
-    old_revision: str
+class UpdateResult:
+    old_value: str
     project_name: str | None
     finalize: Callable[[], None] | None = None
 
 
+GIT_HASH_PATTERN = '[0-9a-fA-F]{40}'
+
+
 class LineProxy:
     def __init__(self, lines, idx):
         self._lines = lines
@@ -107,7 +110,8 @@
 
 class BazelRollApi(recipe_api.RecipeApi):
 
-    UpdateCommitHashResult = UpdateCommitHashResult
+    UpdateResult = UpdateResult
+    GIT_HASH_PATTERN = GIT_HASH_PATTERN
 
     def workspace_path(
         self,
@@ -190,12 +194,15 @@
                 checkout.remotes_equivalent,
             )
 
-            update_result = self.update_commit_hash(
-                checkout=checkout,
-                project_remote=git_repository.remote,
-                new_revision=new_revision,
+            update_result = self.update_value(
+                project_value=git_repository.remote,
+                new_value=new_revision,
                 path=workspace_path,
                 delay_write=True,
+                search_key='remote',
+                value_key='commit',
+                value_pattern=GIT_HASH_PATTERN,
+                matches=checkout.remotes_equivalent,
             )
             if not update_result:
                 self.m.step.empty(
@@ -207,7 +214,7 @@
                 roll = self.m.git_roll_util.get_roll(
                     repo_url=git_repository.remote,
                     repo_short_name=project_name,
-                    old_rev=update_result.old_revision,
+                    old_rev=update_result.old_value,
                     new_rev=new_revision,
                 )
 
@@ -251,37 +258,47 @@
 
     def _get_matching_groups(
         self,
-        checkout: checkout_api.CheckoutContext,
         lines: Sequence[str],
         num_nearby_lines: int,
-        project_remote: str,
-        replace_remote: bool = False,
+        project_value: str,
+        search_key: str,
+        replace: bool = False,
+        matches: Callable[[str, str], bool] | None = None,
     ) -> list[tuple[LineProxy, list[LineProxy]]]:
         matching_groups: list[tuple[LineProxy, list[LineProxy]]] = []
 
+        # This is more constraining than necessary, but ensures there aren't
+        # special characters that confuse the regex.
+        assert search_key.isidentifier(), search_key
+
         for nearby_lines in nwise(proxy(lines), num_nearby_lines * 2 + 1):
             curr = nearby_lines[len(nearby_lines) // 2]
             match = re.search(
-                r'^\s*remote\s*=\s*"(?P<remote>[^"]+)",?\s*$', curr.text
+                rf'^\s*{search_key}\s*=\s*"(?P<search>[^"]+)",?\s*$',
+                curr.text,
             )
             if not match:
                 continue
 
-            match_remote = match.group('remote')
+            match_value = match.group('search')
 
-            if checkout.remotes_equivalent(match_remote, project_remote):
+            if not matches:
+                # TODO: b/376915917 - Remove no cover pragma.
+                matches = lambda x, y: x == y  # pragma: no cover
+
+            if matches(match_value, project_value):
                 pres = self.m.step.empty(
-                    f'found equivalent remote {match_remote!r}'
+                    f'found matching {search_key} {match_value!r}'
                 ).presentation
                 pres.logs['lines'] = [repr(x) for x in nearby_lines]
                 matching_groups.append((curr, nearby_lines))
 
-                if replace_remote and match_remote != project_remote:
-                    curr.text = curr.text.replace(match_remote, project_remote)
+                if replace and match_value != project_value:
+                    curr.text = curr.text.replace(match_value, project_value)
 
             else:
                 pres = self.m.step.empty(
-                    f'found other remote {match_remote!r}'
+                    f'found other {search_key} {match_value!r}'
                 ).presentation
                 pres.logs['lines'] = [repr(x) for x in nearby_lines]
 
@@ -302,23 +319,24 @@
 
         return nearby_lines
 
-    def retrieve_git_repository_attributes(
+    def retrieve_attributes(
         self,
-        checkout: checkout_api.CheckoutContext,
-        project_remote: str,
+        project_value: str,
+        path: config_types.Path,
         num_nearby_lines: int = 10,
-        path: config_types.Path | None = None,
+        title_keys: str | Sequence[str] = ('name', 'module_name'),
+        matches: Callable[[str, str], bool] | None = None,
     ) -> list[dict[str, str]]:
-        if not path:
-            path = checkout.root / 'WORKSPACE'
-
         lines = self._read(path, num_nearby_lines)
 
+        assert isinstance(title_keys, (list, tuple)), repr(title_keys)
+
         matching_groups = self._get_matching_groups(
-            checkout=checkout,
             lines=lines,
             num_nearby_lines=num_nearby_lines,
-            project_remote=project_remote,
+            project_value=project_value,
+            search_key='remote',
+            matches=matches,
         )
 
         results: list[dict[str, str]] = []
@@ -332,7 +350,7 @@
                     line.text,
                 ):
                     entry[match.group(1)] = match.group(2).strip('"')
-                    if match.group(1) == 'module_name' and 'name' not in entry:
+                    if match.group(1) in title_keys:
                         entry['name'] = match.group(2).strip('"')
 
             if entry:
@@ -340,63 +358,69 @@
 
         return results
 
-    def update_commit_hash(
+    def update_value(
         self,
         *,
-        checkout: checkout_api.CheckoutContext,
-        project_remote: str,
-        new_revision: str,
+        project_value: str,
+        new_value: str,
+        path: config_types.Path,
+        search_key: str,
+        value_key: str,
         num_nearby_lines: int = 10,
-        path: config_types.Path | None,
-        replace_remote: bool = False,
+        replace: bool = False,
         delay_write: bool = False,
-    ) -> UpdateCommitHashResult | None:
-        if not path:
-            path = checkout.root / 'WORKSPACE'
-
+        title_keys: Sequence[str] = ('name', 'module_name'),
+        value_pattern: str | None = None,
+        matches: Callable[[str, str], bool] | None = None,
+    ) -> UpdateResult | None:
         lines = self._read(path, num_nearby_lines)
 
         matching_groups = self._get_matching_groups(
-            checkout=checkout,
             lines=lines,
             num_nearby_lines=num_nearby_lines,
-            project_remote=project_remote,
-            replace_remote=replace_remote,
+            project_value=project_value,
+            replace=replace,
+            search_key=search_key,
+            matches=matches,
         )
 
         if not matching_groups:
             self.m.step.empty(
-                f'could not find remote {project_remote} in {path}',
+                f'could not find {search_key} {project_value} in {path}',
             )
             return None
 
         project_names: list[str] = []
+        if value_pattern is None:
+            # TODO: b/376915917 - Remove no cover pragma.
+            value_pattern = '[^"]+'  # pragma: no cover
 
         for matching_line, nearby_lines in matching_groups:
             nearby_lines = self._process_nearby_lines(nearby_lines)
 
-            commit_rx = re.compile(
-                r'^(?P<prefix>\s*commit\s*=\s*")'
-                r'(?P<commit>[0-9a-f]{40})'
+            value_rx = re.compile(
+                rf'^(?P<prefix>\s*{value_key}\s*=\s*")'
+                rf'(?P<value>{value_pattern})'
                 r'(?P<suffix>",?\s*)$'
             )
 
             for line in nearby_lines:
-                if match := commit_rx.search(line.text):
+                if match := value_rx.search(line.text):
                     idx = line.idx
                     break
             else:
-                self.m.step.empty(
-                    'could not find commit line adjacent to '
+                pres = self.m.step.empty(
+                    f'could not find {value_key} line adjacent to '
                     f'{matching_line.text!r} in {path}',
-                )
+                ).presentation
+                pres.step_summary_text = repr(nearby_lines)
                 return None
 
-            old_revision = match.group('commit')
+            old_value = match.group('value')
 
             prefix = match.group("prefix")
             suffix = match.group("suffix")
-            lines[idx] = f'{prefix}{new_revision}{suffix}'
+            lines[idx] = f'{prefix}{new_value}{suffix}'
 
             # Remove all existing metadata lines in this git_repository() entry.
             idx2 = idx - 1
@@ -417,12 +441,21 @@
 
             lines[idx] = '\n'.join(comment_lines + (lines[idx],))
 
+            assert isinstance(title_keys, (list, tuple)), repr(title_keys)
+
+            # This is more restrictive than necessary but helps keep the regex
+            # simple.
+            for key in title_keys:
+                assert isinstance(key, str), repr(key)
+                assert key.isidentifier(), repr(key)
+
             for line in nearby_lines:
+                keys = "|".join(title_keys)
                 if match := re.search(
-                    r'^\s*(?:module_)?name\s*=\s*"(?P<name>[^"]+)",?\s*$',
+                    rf'^\s*(?:{keys})\s*=\s*"(?P<value>[^"]+)",?\s*$',
                     line.text or '',
                 ):
-                    project_names.append(match.group('name'))
+                    project_names.append(match.group('value'))
                     break
 
         def write():
@@ -437,8 +470,8 @@
             )
 
         if delay_write:
-            return UpdateCommitHashResult(
-                old_revision=old_revision,
+            return UpdateResult(
+                old_value=old_value,
                 project_name=', '.join(project_names),
                 finalize=write,
             )
@@ -446,7 +479,7 @@
         else:
             write()
 
-            return UpdateCommitHashResult(
-                old_revision=old_revision,
+            return UpdateResult(
+                old_value=old_value,
                 project_name=', '.join(project_names),
             )
diff --git a/recipe_modules/bazel_roll/tests/full.expected/backwards.json b/recipe_modules/bazel_roll/tests/full.expected/backwards.json
index d7bfdff..8361393 100644
--- a/recipe_modules/bazel_roll/tests/full.expected/backwards.json
+++ b/recipe_modules/bazel_roll/tests/full.expected/backwards.json
@@ -92,7 +92,7 @@
   },
   {
     "cmd": [],
-    "name": "pigweed.found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
+    "name": "pigweed.found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@1@@@",
       "@@@STEP_LOG_LINE@lines@13     commit = \"invalid commit line won't be found\",@@@",
diff --git a/recipe_modules/bazel_roll/tests/full.expected/no-trigger.json b/recipe_modules/bazel_roll/tests/full.expected/no-trigger.json
index b75bf1e..df46ac0 100644
--- a/recipe_modules/bazel_roll/tests/full.expected/no-trigger.json
+++ b/recipe_modules/bazel_roll/tests/full.expected/no-trigger.json
@@ -92,7 +92,7 @@
   },
   {
     "cmd": [],
-    "name": "pigweed.found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
+    "name": "pigweed.found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@1@@@",
       "@@@STEP_LOG_LINE@lines@13     commit = \"invalid commit line won't be found\",@@@",
diff --git a/recipe_modules/bazel_roll/tests/full.expected/success.json b/recipe_modules/bazel_roll/tests/full.expected/success.json
index defd6e0..d39163e 100644
--- a/recipe_modules/bazel_roll/tests/full.expected/success.json
+++ b/recipe_modules/bazel_roll/tests/full.expected/success.json
@@ -88,7 +88,7 @@
   },
   {
     "cmd": [],
-    "name": "pigweed.found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
+    "name": "pigweed.found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@1@@@",
       "@@@STEP_LOG_LINE@lines@13     commit = \"invalid commit line won't be found\",@@@",
diff --git a/recipe_modules/bazel_roll/tests/retrieve_git_repository_attributes.expected/success.json b/recipe_modules/bazel_roll/tests/retrieve_attributes.expected/success.json
similarity index 99%
rename from recipe_modules/bazel_roll/tests/retrieve_git_repository_attributes.expected/success.json
rename to recipe_modules/bazel_roll/tests/retrieve_attributes.expected/success.json
index cd74e94..46d203e 100644
--- a/recipe_modules/bazel_roll/tests/retrieve_git_repository_attributes.expected/success.json
+++ b/recipe_modules/bazel_roll/tests/retrieve_attributes.expected/success.json
@@ -530,7 +530,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/co/WORKSPACE",
+      "[START_DIR]/WORKSPACE",
       "/path/to/tmp/"
     ],
     "infra_step": true,
@@ -593,7 +593,7 @@
   },
   {
     "cmd": [],
-    "name": "found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
+    "name": "found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@lines@13     commit = \"invalid commit line won't be found\",@@@",
       "@@@STEP_LOG_LINE@lines@14 )@@@",
diff --git a/recipe_modules/bazel_roll/tests/retrieve_git_repository_attributes.py b/recipe_modules/bazel_roll/tests/retrieve_attributes.py
similarity index 81%
rename from recipe_modules/bazel_roll/tests/retrieve_git_repository_attributes.py
rename to recipe_modules/bazel_roll/tests/retrieve_attributes.py
index 00b2742..97ae029 100644
--- a/recipe_modules/bazel_roll/tests/retrieve_git_repository_attributes.py
+++ b/recipe_modules/bazel_roll/tests/retrieve_attributes.py
@@ -11,15 +11,14 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under
 # the License.
-"""Full test of bazel module's update_commit_hash() method."""
+"""Test of git behavior in bazel_roll module's retrieve_attributes() method."""
 
 from __future__ import annotations
 
-import dataclasses
 from typing import TYPE_CHECKING
 
-from PB.recipe_modules.pigweed.bazel_roll.tests.update_commit_hash import (
-    UpdateCommitHashProperties,
+from PB.recipe_modules.pigweed.bazel_roll.tests.update_value import (
+    UpdateValueProperties,
 )
 from PB.recipe_modules.pigweed.checkout import options as checkout_pb2
 from recipe_engine import post_process
@@ -31,16 +30,12 @@
 DEPS = [
     'pigweed/bazel_roll',
     'pigweed/checkout',
+    'recipe_engine/path',
     'recipe_engine/properties',
     'recipe_engine/step',
 ]
 
-PROPERTIES = UpdateCommitHashProperties
-
-
-@dataclasses.dataclass
-class _FakeCheckoutContext:
-    root: config_types.Path
+PROPERTIES = UpdateValueProperties
 
 
 def RunSteps(api, props):
@@ -53,10 +48,11 @@
     options.equivalent_remotes.append(equiv)
     checkout = api.checkout(options)
 
-    entries = api.bazel_roll.retrieve_git_repository_attributes(
-        checkout=checkout,
-        project_remote=props.project_remote,
-        path=props.workspace_path,
+    entries = api.bazel_roll.retrieve_attributes(
+        project_value=props.project_remote,
+        path=api.path.start_dir / (props.workspace_path or 'WORKSPACE'),
+        matches=checkout.remotes_equivalent,
+        title_keys=('name', 'module_name'),
     )
 
     api.step.empty('debug').presentation.step_summary_text = repr(entries)
@@ -71,7 +67,7 @@
 
 def GenTests(api) -> Generator[recipe_test_api.TestData, None, None]:
     def properties(**kwargs):
-        return api.properties(UpdateCommitHashProperties(**kwargs))
+        return api.properties(UpdateValueProperties(**kwargs))
 
     def _url(x: str = 'pigweed/pigweed'):
         assert ':' not in x
diff --git a/recipe_modules/bazel_roll/tests/update_commit_hash.expected/equiv.json b/recipe_modules/bazel_roll/tests/update_value.expected/equiv.json
similarity index 99%
rename from recipe_modules/bazel_roll/tests/update_commit_hash.expected/equiv.json
rename to recipe_modules/bazel_roll/tests/update_value.expected/equiv.json
index a038a0b..acae4b0 100644
--- a/recipe_modules/bazel_roll/tests/update_commit_hash.expected/equiv.json
+++ b/recipe_modules/bazel_roll/tests/update_value.expected/equiv.json
@@ -530,7 +530,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/co/WORKSPACE",
+      "[START_DIR]/WORKSPACE",
       "/path/to/tmp/"
     ],
     "infra_step": true,
@@ -593,7 +593,7 @@
   },
   {
     "cmd": [],
-    "name": "found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
+    "name": "found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@lines@13     commit = \"invalid commit line won't be found\",@@@",
       "@@@STEP_LOG_LINE@lines@14 )@@@",
@@ -712,7 +712,7 @@
       "/path/to/tmp/json",
       "copy",
       "git_repository(\n    name = \"other-repo\"\n    remote = \"https://pigweed.googlesource.com/other/repo.git\",\n    commit = \"invalid commit line won't be found\",\n)\n\ngit_repository(\n    module_name = \"pigweed\",\n    # ROLL: Warning: this entry is automatically updated.\n    # ROLL: Last updated 2012-05-14.\n    # ROLL: By https://cr-buildbucket.appspot.com/build/0.\n    commit = \"ffffffffffffffffffffffffffffffffffffffff\",\n    remote = \"https://pigweed.googlesource.com/pigweed/equiv\",\n    git_repository_attribute_test = \"ignored\",\n    strip_prefix = \"pw_toolchain_bazel\",\n)\n\ngit_repository(\n    name = \"missing final quote/comma so will miss this line\n    remote = \"https://pigweed.googlesource.com/third/repo.git\",\n    commit = \"2222222222222222222222222222222222222222\",\n)\n",
-      "[START_DIR]/co/WORKSPACE"
+      "[START_DIR]/WORKSPACE"
     ],
     "infra_step": true,
     "name": "write new WORKSPACE",
diff --git a/recipe_modules/bazel_roll/tests/update_commit_hash.expected/multiple-2.json b/recipe_modules/bazel_roll/tests/update_value.expected/multiple-2.json
similarity index 98%
rename from recipe_modules/bazel_roll/tests/update_commit_hash.expected/multiple-2.json
rename to recipe_modules/bazel_roll/tests/update_value.expected/multiple-2.json
index c139a27..cfbf6b8 100644
--- a/recipe_modules/bazel_roll/tests/update_commit_hash.expected/multiple-2.json
+++ b/recipe_modules/bazel_roll/tests/update_value.expected/multiple-2.json
@@ -530,7 +530,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/co/WORKSPACE",
+      "[START_DIR]/WORKSPACE",
       "/path/to/tmp/"
     ],
     "infra_step": true,
@@ -561,7 +561,7 @@
   },
   {
     "cmd": [],
-    "name": "found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
+    "name": "found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@lines@10 git_repository(@@@",
       "@@@STEP_LOG_LINE@lines@11     name = \"pigweed\",@@@",
@@ -589,7 +589,7 @@
   },
   {
     "cmd": [],
-    "name": "found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed'",
+    "name": "found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed'",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@lines@16     # ROLL: Comment line 5.@@@",
       "@@@STEP_LOG_LINE@lines@17     # ROLL: Comment line 6.@@@",
@@ -729,7 +729,7 @@
       "/path/to/tmp/json",
       "copy",
       "git_repository(\n    name = \"pigweed\",\n    # ROLL: Warning: this entry is automatically updated.\n    # ROLL: Last updated 2012-05-14.\n    # ROLL: By https://cr-buildbucket.appspot.com/build/0.\n    commit = \"ffffffffffffffffffffffffffffffffffffffff\",\n    remote = \"https://pigweed.googlesource.com/pigweed/pigweed\",\n)\n\ngit_repository(\n    name = \"pw_toolchain\",\n    # ROLL: Warning: this entry is automatically updated.\n    # ROLL: Last updated 2012-05-14.\n    # ROLL: By https://cr-buildbucket.appspot.com/build/0.\n    commit = \"ffffffffffffffffffffffffffffffffffffffff\",\n    remote = \"https://pigweed.googlesource.com/pigweed/pigweed\",\n    strip_prefix = \"pw_toolchain_bazel\",\n)\n",
-      "[START_DIR]/co/WORKSPACE"
+      "[START_DIR]/WORKSPACE"
     ],
     "infra_step": true,
     "name": "write new WORKSPACE",
diff --git a/recipe_modules/bazel_roll/tests/update_commit_hash.expected/multiple.json b/recipe_modules/bazel_roll/tests/update_value.expected/multiple.json
similarity index 98%
rename from recipe_modules/bazel_roll/tests/update_commit_hash.expected/multiple.json
rename to recipe_modules/bazel_roll/tests/update_value.expected/multiple.json
index 5309493..f73aa19 100644
--- a/recipe_modules/bazel_roll/tests/update_commit_hash.expected/multiple.json
+++ b/recipe_modules/bazel_roll/tests/update_value.expected/multiple.json
@@ -530,7 +530,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/co/WORKSPACE",
+      "[START_DIR]/WORKSPACE",
       "/path/to/tmp/"
     ],
     "infra_step": true,
@@ -564,7 +564,7 @@
   },
   {
     "cmd": [],
-    "name": "found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
+    "name": "found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@lines@4 @@@",
       "@@@STEP_LOG_LINE@lines@5 @@@",
@@ -592,7 +592,7 @@
   },
   {
     "cmd": [],
-    "name": "found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed'",
+    "name": "found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed'",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@lines@19 # to@@@",
       "@@@STEP_LOG_LINE@lines@20 # pad@@@",
@@ -721,7 +721,7 @@
       "/path/to/tmp/json",
       "copy",
       "git_repository(\n    name = \"pigweed\",\n    # ROLL: Warning: this entry is automatically updated.\n    # ROLL: Last updated 2012-05-14.\n    # ROLL: By https://cr-buildbucket.appspot.com/build/0.\n    commit = \"ffffffffffffffffffffffffffffffffffffffff\",\n    remote = \"https://pigweed.googlesource.com/pigweed/pigweed\",\n)\n\n# Filler\n# lines\n# to\n# pad\n# out\n# the\n# file.\n\ngit_override(\n    module_name = \"pw_toolchain\",\n    # ROLL: Warning: this entry is automatically updated.\n    # ROLL: Last updated 2012-05-14.\n    # ROLL: By https://cr-buildbucket.appspot.com/build/0.\n    commit = \"ffffffffffffffffffffffffffffffffffffffff\",\n    remote = \"https://pigweed.googlesource.com/pigweed/pigweed\",\n    strip_prefix = \"pw_toolchain_bazel\",\n)\n",
-      "[START_DIR]/co/WORKSPACE"
+      "[START_DIR]/WORKSPACE"
     ],
     "infra_step": true,
     "name": "write new WORKSPACE",
diff --git a/recipe_modules/bazel_roll/tests/update_commit_hash.expected/success.json b/recipe_modules/bazel_roll/tests/update_value.expected/success.json
similarity index 99%
rename from recipe_modules/bazel_roll/tests/update_commit_hash.expected/success.json
rename to recipe_modules/bazel_roll/tests/update_value.expected/success.json
index 8c2fc2f..8f6e0e5 100644
--- a/recipe_modules/bazel_roll/tests/update_commit_hash.expected/success.json
+++ b/recipe_modules/bazel_roll/tests/update_value.expected/success.json
@@ -530,7 +530,7 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "[START_DIR]/co/WORKSPACE",
+      "[START_DIR]/WORKSPACE",
       "/path/to/tmp/"
     ],
     "infra_step": true,
@@ -593,7 +593,7 @@
   },
   {
     "cmd": [],
-    "name": "found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
+    "name": "found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@lines@13     commit = \"invalid commit line won't be found\",@@@",
       "@@@STEP_LOG_LINE@lines@14 )@@@",
@@ -712,7 +712,7 @@
       "/path/to/tmp/json",
       "copy",
       "git_repository(\n    name = \"other-repo\"\n    remote = \"https://pigweed.googlesource.com/other/repo.git\",\n    commit = \"invalid commit line won't be found\",\n)\n\ngit_repository(\n    module_name = \"pigweed\",\n    # ROLL: Warning: this entry is automatically updated.\n    # ROLL: Last updated 2012-05-14.\n    # ROLL: By https://cr-buildbucket.appspot.com/build/0.\n    commit = \"ffffffffffffffffffffffffffffffffffffffff\",\n    remote = \"https://pigweed.googlesource.com/pigweed/pigweed\",\n    git_repository_attribute_test = \"ignored\",\n    strip_prefix = \"pw_toolchain_bazel\",\n)\n\ngit_repository(\n    name = \"missing final quote/comma so will miss this line\n    remote = \"https://pigweed.googlesource.com/third/repo.git\",\n    commit = \"2222222222222222222222222222222222222222\",\n)\n",
-      "[START_DIR]/co/WORKSPACE"
+      "[START_DIR]/WORKSPACE"
     ],
     "infra_step": true,
     "name": "write new WORKSPACE",
diff --git a/recipe_modules/bazel_roll/tests/update_commit_hash.proto b/recipe_modules/bazel_roll/tests/update_value.proto
similarity index 95%
rename from recipe_modules/bazel_roll/tests/update_commit_hash.proto
rename to recipe_modules/bazel_roll/tests/update_value.proto
index b108e76..45645c4 100644
--- a/recipe_modules/bazel_roll/tests/update_commit_hash.proto
+++ b/recipe_modules/bazel_roll/tests/update_value.proto
@@ -16,7 +16,7 @@
 
 package recipe_modules.pigweed.bazel_roll.tests;
 
-message UpdateCommitHashProperties {
+message UpdateValueProperties {
   // The path of the WORKSPACE file to update. Default: WORKSPACE.
   string workspace_path = 1;
 
diff --git a/recipe_modules/bazel_roll/tests/update_commit_hash.py b/recipe_modules/bazel_roll/tests/update_value.py
similarity index 81%
rename from recipe_modules/bazel_roll/tests/update_commit_hash.py
rename to recipe_modules/bazel_roll/tests/update_value.py
index b3712cc..fda4439 100644
--- a/recipe_modules/bazel_roll/tests/update_commit_hash.py
+++ b/recipe_modules/bazel_roll/tests/update_value.py
@@ -11,15 +11,15 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under
 # the License.
-"""Full test of bazel module's update_commit_hash() method."""
+"""Full test of bazel_roll module's update_value() method."""
 
 from __future__ import annotations
 
 import dataclasses
 from typing import TYPE_CHECKING
 
-from PB.recipe_modules.pigweed.bazel_roll.tests.update_commit_hash import (
-    UpdateCommitHashProperties,
+from PB.recipe_modules.pigweed.bazel_roll.tests.update_value import (
+    UpdateValueProperties,
 )
 from PB.recipe_modules.pigweed.checkout import options as checkout_pb2
 from recipe_engine import post_process
@@ -37,12 +37,7 @@
     'recipe_engine/step',
 ]
 
-PROPERTIES = UpdateCommitHashProperties
-
-
-@dataclasses.dataclass
-class _FakeCheckoutContext:
-    root: config_types.Path
+PROPERTIES = UpdateValueProperties
 
 
 def RunSteps(api, props):
@@ -55,18 +50,18 @@
     options.equivalent_remotes.append(equiv)
     checkout = api.checkout(options)
 
-    # For coverage.
-    path: config_types.Path | None = None
-    if 'bar' in props.project_remote:
-        path = api.path.start_dir / 'WORKSPACE'
+    path = api.path.start_dir / 'WORKSPACE'
 
-    update_result = api.bazel_roll.update_commit_hash(
-        checkout=checkout,
-        project_remote=props.project_remote,
-        new_revision='ffffffffffffffffffffffffffffffffffffffff',
+    update_result = api.bazel_roll.update_value(
+        project_value=props.project_remote,
+        new_value='f' * 40,
         path=path,
-        replace_remote=True,
+        replace=True,
         delay_write=props.delay_write,
+        search_key='remote',
+        value_key='commit',
+        value_pattern=api.bazel_roll.GIT_HASH_PATTERN,
+        matches=checkout.remotes_equivalent,
     )
 
     if update_result:
@@ -74,13 +69,13 @@
             update_result.finalize()
 
         with api.step.nest('update result'):
-            api.step.empty(f'old revision {update_result.old_revision}')
+            api.step.empty(f'old revision {update_result.old_value}')
             api.step.empty(f'project name {update_result.project_name}')
 
 
 def GenTests(api) -> Generator[recipe_test_api.TestData, None, None]:
     def properties(**kwargs):
-        return api.properties(UpdateCommitHashProperties(**kwargs))
+        return api.properties(UpdateValueProperties(**kwargs))
 
     def _url(x: str = 'pigweed/pigweed'):
         assert ':' not in x
@@ -110,8 +105,8 @@
         'commit-not-found',
         properties(project_remote=_url('other/repo')),
         api.post_process(post_process.MustRunRE, r'could not find commit.*'),
-        api.post_process(post_process.DropExpectation),
         api.post_process(post_process.DoesNotRun, 'update result'),
+        api.post_process(post_process.DropExpectation),
     )
 
     yield api.test(
diff --git a/recipe_modules/checkout/api.py b/recipe_modules/checkout/api.py
index e2ed89d..cd1b095 100644
--- a/recipe_modules/checkout/api.py
+++ b/recipe_modules/checkout/api.py
@@ -1481,10 +1481,10 @@
 
                         with self.m.step.nest(workspace):
 
-                            repos = self.m.bazel_roll.retrieve_git_repository_attributes(
-                                checkout=ctx,
-                                project_remote=change.remote,
+                            repos = self.m.bazel_roll.retrieve_attributes(
+                                project_value=change.remote,
                                 path=workspace_path,
+                                matches=ctx.remotes_equivalent,
                             )
 
                             if not repos:
diff --git a/recipe_modules/checkout/tests/workspace.expected/found.json b/recipe_modules/checkout/tests/workspace.expected/found.json
index aab917a..c7f1456 100644
--- a/recipe_modules/checkout/tests/workspace.expected/found.json
+++ b/recipe_modules/checkout/tests/workspace.expected/found.json
@@ -1306,7 +1306,7 @@
   },
   {
     "cmd": [],
-    "name": "checkout foo.workspace.pigweed:123456.WORKSPACE.found equivalent remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
+    "name": "checkout foo.workspace.pigweed:123456.WORKSPACE.found matching remote 'https://pigweed.googlesource.com/pigweed/pigweed.git'",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
       "@@@STEP_LOG_LINE@lines@13     commit = \"invalid commit line won't be found\",@@@",