Don't mistake a refetched repo for an externally modified one (#31108)

### Description

After `bazel fetch --force` (or any other command that refetches a repo without loading its packages), the next `bazel build` warned that a file of the repo "has been modified externally" and fetched the repo a second time.

The check for external modifications of repo files scanned all `FileStateValue` nodes in the graph via `MemoizingEvaluator#getValues()`, which also returns the last values of nodes that are already dirty. Refetching a repo re-evaluates its `RepositoryDirectoryValue`, which is not comparable and thus dirties the file state nodes of all files in the repo. A build re-evaluates these nodes when it loads the affected packages, but `fetch` doesn't request them and, unlike `BuildTool`, also doesn't delete dirty nodes at the end of the command. The stale nodes then survived until the next build, where the `ExternalDirtinessChecker` compared their old contents proxies with the rewritten files, reported an external modification and deleted the marker file to force another fetch.

Only scan nodes that are done: dirty nodes are re-evaluated when they are next requested anyway, so their last values can't be used to detect changes that Skyframe doesn't already know about.

### Motivation

Every `bazel fetch --force` was followed by a second fetch of the same repos in the next build, together with a misleading warning about external modifications.

### Build API Changes

No

### Release Notes

RELNOTES: `bazel fetch --force` no longer causes the fetched repos to be fetched again by the next build with a spurious warning about external modifications.

Closes #31108

COPYBARA_INTEGRATE_REVIEW=https://github.com/bazelbuild/bazel/pull/31108 from fmeum:fix-stale-external-repo-file-check 739a90bd6f1b0d3d1d9f2ad88078fca2762fe2bf
PiperOrigin-RevId: 981245657
Change-Id: I98bc3c1162e55361ca435823c749f25759a0469f
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
index 66c9544..63faf81 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -4101,7 +4101,7 @@
       try (SilentCloseable c = Profiler.instance().profile("fsvc.getDirtyKeys")) {
         batchDirtyResult =
             fsvc.getDirtyKeys(
-                memoizingEvaluator.getValues(),
+                memoizingEvaluator.getDoneValues(),
                 new UnionDirtinessChecker(ImmutableList.copyOf(dirtinessCheckers)));
       }
       if (externalDirtinessChecker != null) {
diff --git a/src/test/py/bazel/bzlmod/bazel_fetch_test.py b/src/test/py/bazel/bzlmod/bazel_fetch_test.py
index bd37ec6..37ddde6 100644
--- a/src/test/py/bazel/bzlmod/bazel_fetch_test.py
+++ b/src/test/py/bazel/bzlmod/bazel_fetch_test.py
@@ -18,6 +18,7 @@
 import tempfile
 
 from absl.testing import absltest
+
 from src.test.py.bazel import test_base
 from src.test.py.bazel.bzlmod.test_utils import BazelRegistry
 
@@ -328,6 +329,55 @@
     _, _, stderr = self.RunBazel(['fetch', '--repo=@hello', '--force'])
     self.assertIn('No more Orange Juice!', ''.join(stderr))
 
+  def testForceFetchDoesNotCauseRefetchInNextBuild(self):
+    self.ScratchFile(
+        'MODULE.bazel',
+        [
+            'ext = use_extension("extension.bzl", "ext")',
+            'use_repo(ext, "hello")',
+        ],
+    )
+    self.ScratchFile('BUILD')
+    self.ScratchFile(
+        'extension.bzl',
+        [
+            'def impl(ctx):',
+            '    print("JUST FETCHED")',
+            '    ctx.file("BUILD", "filegroup(name = \'lala\')")',
+            'repo_rule = repository_rule(implementation=impl)',
+            '',
+            'def _ext_impl(ctx):',
+            '    repo_rule(name="hello")',
+            'ext = module_extension(implementation=_ext_impl)',
+        ],
+    )
+
+    _, _, stderr = self.RunBazel(['build', '@hello//:lala'])
+    self.assertIn('JUST FETCHED', ''.join(stderr))
+
+    # A forced fetch rewrites the files of the repo without the build loading
+    # them again. This must not be mistaken for an external modification.
+    _, _, stderr = self.RunBazel(['fetch', '--repo=@hello', '--force'])
+    self.assertIn('JUST FETCHED', ''.join(stderr))
+    _, _, stderr = self.RunBazel(['build', '@hello//:lala'])
+    self.assertNotIn('modified externally', ''.join(stderr))
+    self.assertNotIn('JUST FETCHED', ''.join(stderr))
+
+    # An actual external modification is still detected.
+    _, stdout, _ = self.RunBazel(['info', 'output_base'])
+    build_file = os.path.join(
+        stdout[0].strip(), 'external', '+ext+hello', 'BUILD'
+    )
+    with open(build_file, 'a') as f:
+      f.write('# modified externally\n')
+    _, _, stderr = self.RunBazel(['build', '@hello//:lala'])
+    self.assertIn(
+        "Repository '@@+ext+hello' will be fetched again since the file 'BUILD'"
+        ' has been modified externally.',
+        ''.join(stderr),
+    )
+    self.assertIn('JUST FETCHED', ''.join(stderr))
+
   def testForceFetchWithRepoCache(self):
     self.ScratchFile(
         'MODULE.bazel',