bazel: Allow delayed writing of new hashes

Allow delaying the writing of new commit hashes. This will make it
possible to only apply new updates if they result in forward rolls.
Backward rolls would otherwise need to be undone if done from a
multiple-element roller.

Bug: b/359925419, b/341756093
Change-Id: I9b3d6dd76419097ae7b259595576c3266696096f
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/235014
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
Reviewed-by: Ted Pudlik <tpudlik@google.com>
diff --git a/recipe_modules/bazel/api.py b/recipe_modules/bazel/api.py
index 97ceb53..1e534c5 100644
--- a/recipe_modules/bazel/api.py
+++ b/recipe_modules/bazel/api.py
@@ -24,7 +24,7 @@
 from recipe_engine import recipe_api
 
 if TYPE_CHECKING:  # pragma: no cover
-    from typing import Any, Sequence
+    from typing import Any, Callable, Sequence
     from recipe_engine import config_types
     from RECIPE_MODULES.pigweed.checkout import api as checkout_api
 
@@ -222,6 +222,7 @@
 class UpdateCommitHashResult:
     old_revision: str
     project_name: str | None
+    finalize: Callable[[], None] | None = None
 
 
 class LineProxy:
@@ -382,6 +383,7 @@
         num_nearby_lines: int = 10,
         path: config_types.Path | None,
         replace_remote: bool = False,
+        delay_write: bool = False,
     ) -> UpdateCommitHashResult | None:
         if not path:
             path = checkout.root / 'WORKSPACE'
@@ -457,17 +459,28 @@
                     project_names.append(match.group('name'))
                     break
 
-        self.m.file.write_text(
-            f'write new {path.name}',
-            path,
-            ''.join(
-                f'{x}\n'
-                for x in lines[num_nearby_lines:-num_nearby_lines]
-                if x is not None
-            ),
-        )
+        def write():
+            self.m.file.write_text(
+                f'write new {path.name}',
+                path,
+                ''.join(
+                    f'{x}\n'
+                    for x in lines[num_nearby_lines:-num_nearby_lines]
+                    if x is not None
+                ),
+            )
 
-        return UpdateCommitHashResult(
-            old_revision=old_revision,
-            project_name=', '.join(project_names),
-        )
+        if delay_write:
+            return UpdateCommitHashResult(
+                old_revision=old_revision,
+                project_name=', '.join(project_names),
+                finalize=write,
+            )
+
+        else:
+            write()
+
+            return UpdateCommitHashResult(
+                old_revision=old_revision,
+                project_name=', '.join(project_names),
+            )
diff --git a/recipe_modules/bazel/tests/update_commit_hash.proto b/recipe_modules/bazel/tests/update_commit_hash.proto
index 84123ca..71bb3c8 100644
--- a/recipe_modules/bazel/tests/update_commit_hash.proto
+++ b/recipe_modules/bazel/tests/update_commit_hash.proto
@@ -22,4 +22,7 @@
 
   // Repository referred to by the WORKSPACE file.
   string project_remote = 2;
+
+  // Provide a callback to write data instead of writing it immediately.
+  bool delay_write = 3;
 }
diff --git a/recipe_modules/bazel/tests/update_commit_hash.py b/recipe_modules/bazel/tests/update_commit_hash.py
index 80ec58b..6cf9dff 100644
--- a/recipe_modules/bazel/tests/update_commit_hash.py
+++ b/recipe_modules/bazel/tests/update_commit_hash.py
@@ -66,9 +66,13 @@
         new_revision='ffffffffffffffffffffffffffffffffffffffff',
         path=path,
         replace_remote=True,
+        delay_write=props.delay_write,
     )
 
     if update_result:
+        if update_result.finalize:
+            update_result.finalize()
+
         with api.step.nest('update result'):
             api.step.empty(f'old revision {update_result.old_revision}')
             api.step.empty(f'project name {update_result.project_name}')
@@ -112,7 +116,10 @@
 
     yield api.test(
         'multiple',
-        properties(project_remote=_url('pigweed/pigweed')),
+        properties(
+            project_remote=_url('pigweed/pigweed'),
+            delay_write=True,
+        ),
         api.step_data(
             'read old WORKSPACE',
             api.file.read_text(api.bazel.MULTIPLE_ROLL_WORKSPACE_FILE),