Allow `DiffAwareness` to share precomputed information about the workspace and
propagate it to the `WorkspaceStatusAction`.

Each successful Bazel build issue 2 `BuildInfo` events -- one based on the
result of running the command indicated by `workspace_status_command` and a
dummy one based on a subset of available information. Receivers of those
discard all but the first one. If a build fails before execution phase, the
dummy event will be the only even issued, hence it will be used.

`DiffAwareness` operates on the workspace to figure out the diffs and in the
process probes properties of it. Allow `DiffAwareness` to share such
information with the intention to make it available for the rest of the build.

Propagate unanimous precomputed workspace information when it is available from
`DiffAwareness` through the `DiffAwarenessManager` and `SkyframeExecutor` to
`CommandEnvironment` and store it there to make it available for the rest of
the build.

Make precomputed workspace information available to the dummy `BuildInfo`
event.

PiperOrigin-RevId: 355072552
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index 32a3459..e4aa445 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -366,6 +366,7 @@
         "//src/main/java/com/google/devtools/build/lib/skyframe:skyframe_cluster",
         "//src/main/java/com/google/devtools/build/lib/skyframe:target_pattern_phase_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:top_down_action_cache",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:workspace_info",
         "//src/main/java/com/google/devtools/build/lib/unix",
         "//src/main/java/com/google/devtools/build/lib/util",
         "//src/main/java/com/google/devtools/build/lib/util:TestType",
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BUILD b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
index 220d4d5..52ef94a 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
@@ -1206,6 +1206,7 @@
         "//src/main/java/com/google/devtools/build/lib/actions:artifacts",
         "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
         "//src/main/java/com/google/devtools/build/lib/shell",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:workspace_info",
         "//src/main/java/com/google/devtools/build/lib/util",
         "//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code",
         "//src/main/java/com/google/devtools/build/lib/vfs",
@@ -1213,6 +1214,7 @@
         "//src/main/java/com/google/devtools/common/options",
         "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
+        "//third_party:jsr305",
     ],
 )
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java b/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java
index a4cd247..3e1205f 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/WorkspaceStatusAction.java
@@ -26,6 +26,7 @@
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.server.FailureDetails.WorkspaceStatus;
 import com.google.devtools.build.lib.server.FailureDetails.WorkspaceStatus.Code;
+import com.google.devtools.build.lib.skyframe.WorkspaceInfoFromDiff;
 import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.OptionsUtils;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
@@ -42,6 +43,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import javax.annotation.Nullable;
 
 /**
  * An action writing the workspace status files.
@@ -176,6 +178,10 @@
   public interface DummyEnvironment {
     Path getWorkspace();
 
+    /** Returns optional precomputed workspace info to include in the build info event. */
+    @Nullable
+    WorkspaceInfoFromDiff getWorkspaceInfoFromDiff();
+
     String getBuildRequestId();
 
     OptionsProvider getOptions();
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
index 09ddea4..7cc89e5 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
@@ -55,6 +55,7 @@
 import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutor;
+import com.google.devtools.build.lib.skyframe.WorkspaceInfoFromDiff;
 import com.google.devtools.build.lib.skyframe.actiongraph.v2.ActionGraphDump;
 import com.google.devtools.build.lib.skyframe.actiongraph.v2.AqueryOutputHandler;
 import com.google.devtools.build.lib.skyframe.actiongraph.v2.AqueryOutputHandler.OutputType;
@@ -274,6 +275,12 @@
                               public OptionsProvider getOptions() {
                                 return env.getOptions();
                               }
+
+                              @Nullable
+                              @Override
+                              public WorkspaceInfoFromDiff getWorkspaceInfoFromDiff() {
+                                return env.getWorkspaceInfoFromDiff();
+                              }
                             })));
       }
     }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java b/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java
index 7639bcc..39ddcc9 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java
@@ -14,6 +14,8 @@
 
 package com.google.devtools.build.lib.runtime;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -41,6 +43,7 @@
 import com.google.devtools.build.lib.skyframe.SkyframeBuildView;
 import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
 import com.google.devtools.build.lib.skyframe.TopDownActionCache;
+import com.google.devtools.build.lib.skyframe.WorkspaceInfoFromDiff;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.io.OutErr;
@@ -103,6 +106,7 @@
   private TopDownActionCache topDownActionCache;
   private String workspaceName;
   private boolean hasSyncedPackageLoading = false;
+  @Nullable private WorkspaceInfoFromDiff workspaceInfoFromDiff;
 
   // This AtomicReference is set to:
   //   - null, if neither BlazeModuleEnvironment#exit nor #precompleteCommand have been called
@@ -513,7 +517,7 @@
   }
 
   public void setWorkspaceName(String workspaceName) {
-    Preconditions.checkState(this.workspaceName == null, "workspace name can only be set once");
+    checkState(this.workspaceName == null, "workspace name can only be set once");
     this.workspaceName = workspaceName;
     eventBus.post(new ExecRootEvent(getExecRoot()));
   }
@@ -577,6 +581,18 @@
     this.outputService = outputService;
   }
 
+  /**
+   * Returns precomputed workspace information or null.
+   *
+   * <p>Precomputed workspace info is an optimization allowing to share information about the
+   * workspace if it was derived at the time of synchronizing the workspace. This way we can make it
+   * available earlier during the build and avoid retrieving it again.
+   */
+  @Nullable
+  public WorkspaceInfoFromDiff getWorkspaceInfoFromDiff() {
+    return workspaceInfoFromDiff;
+  }
+
   public ActionCache getPersistentActionCache() throws IOException {
     return workspace.getPersistentActionCache(reporter);
   }
@@ -675,16 +691,17 @@
           "We should never call this method more than once over the course of a single command");
     }
     hasSyncedPackageLoading = true;
-    getSkyframeExecutor()
-        .sync(
-            reporter,
-            options.getOptions(PackageOptions.class),
-            packageLocator,
-            options.getOptions(BuildLanguageOptions.class),
-            getCommandId(),
-            clientEnv,
-            timestampGranularityMonitor,
-            options);
+    workspaceInfoFromDiff =
+        getSkyframeExecutor()
+            .sync(
+                reporter,
+                options.getOptions(PackageOptions.class),
+                packageLocator,
+                options.getOptions(BuildLanguageOptions.class),
+                getCommandId(),
+                clientEnv,
+                timestampGranularityMonitor,
+                options);
   }
 
   public void recordLastExecutionTime() {
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
index a021adf..8b07bc2 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -214,6 +214,7 @@
         ":tree_artifact_value",
         ":unloaded_toolchain_context",
         ":unloaded_toolchain_context_impl",
+        ":workspace_info",
         ":workspace_name_function",
         ":workspace_name_value",
         ":workspace_status_function",
@@ -1286,6 +1287,7 @@
     deps = [
         ":broken_diff_awareness_exception",
         ":incompatible_view_exception",
+        ":workspace_info",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/common/options",
         "//third_party:jsr305",
@@ -1299,6 +1301,7 @@
         ":broken_diff_awareness_exception",
         ":diff_awareness",
         ":incompatible_view_exception",
+        ":workspace_info",
         "//src/main/java/com/google/devtools/build/lib/events",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/common/options",
@@ -2799,6 +2802,11 @@
 )
 
 java_library(
+    name = "workspace_info",
+    srcs = ["WorkspaceInfoFromDiff.java"],
+)
+
+java_library(
     name = "workspace_name_function",
     srcs = ["WorkspaceNameFunction.java"],
     deps = [
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java b/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java
index da0ee7d..a1e3ad7 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java
@@ -13,7 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.skyframe;
 
-import com.google.common.base.Preconditions;
+import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
  * Thrown on {@link DiffAwareness#getDiff} to indicate that something is wrong with the
@@ -22,6 +22,10 @@
 public class BrokenDiffAwarenessException extends Exception {
 
   public BrokenDiffAwarenessException(String msg) {
-    super(Preconditions.checkNotNull(msg));
+    super(checkNotNull(msg));
+  }
+
+  public BrokenDiffAwarenessException(String msg, Throwable cause) {
+    super(checkNotNull(msg), cause);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java
index 0579d9e..b05b070 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java
@@ -45,6 +45,11 @@
 
   /** Opaque view of the filesystem under a package path entry at a specific point in time. */
   interface View {
+    /** Returns workspace info unanimously associated with the package path or null. */
+    @Nullable
+    default WorkspaceInfoFromDiff getWorkspaceInfo() {
+      return null;
+    }
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java
index ed56b41..1958441 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java
@@ -69,6 +69,9 @@
   public interface ProcessableModifiedFileSet {
     ModifiedFileSet getModifiedFileSet();
 
+    @Nullable
+    WorkspaceInfoFromDiff getWorkspaceInfo();
+
     /**
      * This should be called when the changes have been noted. Otherwise, the result from the next
      * call to {@link #getDiff} will be from the baseline of the old, unprocessed, diff.
@@ -99,7 +102,7 @@
     if (baselineView == null) {
       logger.atInfo().log("Initial baseline view for %s is %s", pathEntry, newView);
       diffAwarenessState.baselineView = newView;
-      return BrokenProcessableModifiedFileSet.INSTANCE;
+      return new InitialModifiedFileSet(newView.getWorkspaceInfo());
     }
 
     ModifiedFileSet diff;
@@ -172,6 +175,12 @@
       return modifiedFileSet;
     }
 
+    @Nullable
+    @Override
+    public WorkspaceInfoFromDiff getWorkspaceInfo() {
+      return nextView.getWorkspaceInfo();
+    }
+
     @Override
     public void markProcessed() {
       DiffAwarenessState diffAwarenessState = currentDiffAwarenessStates.get(pathEntry);
@@ -191,6 +200,36 @@
       return ModifiedFileSet.EVERYTHING_MODIFIED;
     }
 
+    @Nullable
+    @Override
+    public WorkspaceInfoFromDiff getWorkspaceInfo() {
+      return null;
+    }
+
+    @Override
+    public void markProcessed() {}
+  }
+
+  /** Modified file set for a clean build. */
+  private static class InitialModifiedFileSet implements ProcessableModifiedFileSet {
+
+    @Nullable private final WorkspaceInfoFromDiff workspaceInfo;
+
+    InitialModifiedFileSet(@Nullable WorkspaceInfoFromDiff workspaceInfo) {
+      this.workspaceInfo = workspaceInfo;
+    }
+
+    @Override
+    public ModifiedFileSet getModifiedFileSet() {
+      return ModifiedFileSet.EVERYTHING_MODIFIED;
+    }
+
+    @Nullable
+    @Override
+    public WorkspaceInfoFromDiff getWorkspaceInfo() {
+      return workspaceInfo;
+    }
+
     @Override
     public void markProcessed() {
     }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java
index 35fdfe2..5f704bf 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java
@@ -226,8 +226,9 @@
     return recordingDiffer;
   }
 
+  @Nullable
   @Override
-  public void sync(
+  public WorkspaceInfoFromDiff sync(
       ExtendedEventHandler eventHandler,
       PackageOptions packageOptions,
       PathPackageLocator packageLocator,
@@ -262,11 +263,13 @@
         tsgm,
         options);
     long startTime = System.nanoTime();
-    handleDiffs(eventHandler, packageOptions.checkOutputFiles, options);
+    WorkspaceInfoFromDiff workspaceInfo =
+        handleDiffs(eventHandler, packageOptions.checkOutputFiles, options);
     long stopTime = System.nanoTime();
     Profiler.instance().logSimpleTask(startTime, stopTime, ProfilerTask.INFO, "handleDiffs");
     long duration = stopTime - startTime;
     sourceDiffCheckingDuration = duration > 0 ? Duration.ofNanos(duration) : Duration.ZERO;
+    return workspaceInfo;
   }
 
   /**
@@ -341,7 +344,8 @@
     handleDiffs(eventHandler, /*checkOutputFiles=*/false, OptionsProvider.EMPTY);
   }
 
-  private void handleDiffs(
+  @Nullable
+  private WorkspaceInfoFromDiff handleDiffs(
       ExtendedEventHandler eventHandler, boolean checkOutputFiles, OptionsProvider options)
       throws InterruptedException, AbruptExitException {
     TimestampGranularityMonitor tsgm = this.tsgm.get();
@@ -354,13 +358,18 @@
       invalidateCachedWorkspacePathsStates();
     }
 
+    WorkspaceInfoFromDiff workspaceInfo = null;
     Map<Root, DiffAwarenessManager.ProcessableModifiedFileSet> modifiedFilesByPathEntry =
         Maps.newHashMap();
     Set<Pair<Root, DiffAwarenessManager.ProcessableModifiedFileSet>>
         pathEntriesWithoutDiffInformation = Sets.newHashSet();
-    for (Root pathEntry : pkgLocator.get().getPathEntries()) {
+    ImmutableList<Root> pkgRoots = pkgLocator.get().getPathEntries();
+    for (Root pathEntry : pkgRoots) {
       DiffAwarenessManager.ProcessableModifiedFileSet modifiedFileSet =
           diffAwarenessManager.getDiff(eventHandler, pathEntry, options);
+      if (pkgRoots.size() == 1) {
+        workspaceInfo = modifiedFileSet.getWorkspaceInfo();
+      }
       if (modifiedFileSet.getModifiedFileSet().treatEverythingAsModified()) {
         pathEntriesWithoutDiffInformation.add(Pair.of(pathEntry, modifiedFileSet));
       } else {
@@ -381,6 +390,7 @@
         managedDirectoriesChanged,
         fsvcThreads);
     handleClientEnvironmentChanges();
+    return workspaceInfo;
   }
 
   /**
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 07713cf..52f8e24 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
@@ -2668,8 +2668,13 @@
 
   /**
    * Initializes and syncs the graph with the given options, readying it for the next evaluation.
+   *
+   * <p>Returns precomputed information about the workspace if it is available at this stage. This
+   * is an optimization allowing implementations which have such information to make it available
+   * early in the build.
    */
-  public void sync(
+  @Nullable
+  public WorkspaceInfoFromDiff sync(
       ExtendedEventHandler eventHandler,
       PackageOptions packageOptions,
       PathPackageLocator pathPackageLocator,
@@ -2696,6 +2701,7 @@
       dropConfiguredTargetsNow(eventHandler);
       lastAnalysisDiscarded = false;
     }
+    return null;
   }
 
   protected void syncPackageLoading(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceInfoFromDiff.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceInfoFromDiff.java
new file mode 100644
index 0000000..c8520d3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceInfoFromDiff.java
@@ -0,0 +1,17 @@
+// Copyright 2021 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+/** Information for a workspace computed at the time of collecting diff. */
+public interface WorkspaceInfoFromDiff {}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
index 6b2a59e..bc02a34 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -87,7 +87,6 @@
     ],
     deps = select({
         "//src/conditions:darwin": [
-            "//src/main/java/com/google/devtools/build/lib/skyframe:incompatible_view_exception",
             "//src/main/java/com/google/devtools/build/lib/skyframe:local_diff_awareness",
             "//src/main/java/com/google/devtools/build/lib/testing/common:fake-options",
         ],
@@ -201,6 +200,7 @@
         "//src/main/java/com/google/devtools/build/lib/skyframe:glob_descriptor",
         "//src/main/java/com/google/devtools/build/lib/skyframe:glob_function",
         "//src/main/java/com/google/devtools/build/lib/skyframe:glob_value",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:incompatible_view_exception",
         "//src/main/java/com/google/devtools/build/lib/skyframe:local_repository_lookup_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:managed_directories_knowledge",
         "//src/main/java/com/google/devtools/build/lib/skyframe:metadata_consumer_for_metrics",
@@ -234,6 +234,7 @@
         "//src/main/java/com/google/devtools/build/lib/skyframe:tree_artifact_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:unloaded_toolchain_context",
         "//src/main/java/com/google/devtools/build/lib/skyframe:toolchain_context_key",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:workspace_info",
         "//src/main/java/com/google/devtools/build/lib/skyframe:workspace_name_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManagerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManagerTest.java
index 8cd2ecb..853ebe7 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManagerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManagerTest.java
@@ -15,11 +15,16 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.skyframe.DiffAwareness.View;
 import com.google.devtools.build.lib.skyframe.DiffAwarenessManager.ProcessableModifiedFileSet;
 import com.google.devtools.build.lib.vfs.DigestHashFunction;
 import com.google.devtools.build.lib.vfs.FileSystem;
@@ -195,6 +200,135 @@
     processableDiff.markProcessed();
   }
 
+  @Test
+  public void getDiff_cleanBuild_propagatesWorkspaceInfo() throws Exception {
+    Root pathEntry = Root.fromPath(fs.getPath("/path"));
+    WorkspaceInfoFromDiff workspaceInfo = new WorkspaceInfoFromDiff() {};
+    DiffAwareness diffAwareness = mock(DiffAwareness.class);
+    when(diffAwareness.getCurrentView(any())).thenReturn(createView(workspaceInfo));
+    DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class);
+    when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness);
+    DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory));
+
+    ProcessableModifiedFileSet diff =
+        manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY);
+
+    assertThat(diff.getWorkspaceInfo()).isSameInstanceAs(workspaceInfo);
+  }
+
+  @Test
+  public void getDiff_incrementalBuild_propagatesLatestWorkspaceInfo() throws Exception {
+    Root pathEntry = Root.fromPath(fs.getPath("/path"));
+    WorkspaceInfoFromDiff workspaceInfo1 = new WorkspaceInfoFromDiff() {};
+    WorkspaceInfoFromDiff workspaceInfo2 = new WorkspaceInfoFromDiff() {};
+    DiffAwareness diffAwareness = mock(DiffAwareness.class);
+    View view1 = createView(workspaceInfo1);
+    View view2 = createView(workspaceInfo2);
+    when(diffAwareness.getCurrentView(any())).thenReturn(view1, view2);
+    when(diffAwareness.getDiff(view1, view2))
+        .thenReturn(ModifiedFileSet.builder().modify(PathFragment.create("file")).build());
+    DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class);
+    when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness);
+    DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory));
+    manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY);
+
+    ProcessableModifiedFileSet diff =
+        manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY);
+
+    assertThat(diff.getWorkspaceInfo()).isSameInstanceAs(workspaceInfo2);
+  }
+
+  @Test
+  public void getDiff_incrementalBuildNoChange_propagatesNewWorkspaceInfo() throws Exception {
+    Root pathEntry = Root.fromPath(fs.getPath("/path"));
+    WorkspaceInfoFromDiff workspaceInfo1 = new WorkspaceInfoFromDiff() {};
+    WorkspaceInfoFromDiff workspaceInfo2 = new WorkspaceInfoFromDiff() {};
+    DiffAwareness diffAwareness = mock(DiffAwareness.class);
+    View view1 = createView(workspaceInfo1);
+    View view2 = createView(workspaceInfo2);
+    when(diffAwareness.getCurrentView(any())).thenReturn(view1, view2);
+    when(diffAwareness.getDiff(view1, view2)).thenReturn(ModifiedFileSet.NOTHING_MODIFIED);
+    DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class);
+    when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness);
+    DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory));
+    manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY);
+
+    ProcessableModifiedFileSet diff =
+        manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY);
+
+    assertThat(diff.getWorkspaceInfo()).isSameInstanceAs(workspaceInfo2);
+  }
+
+  @Test
+  public void getDiff_incrementalBuildWithNoWorkspaceInfo_returnsDiffWithNullWorkspaceInfo()
+      throws Exception {
+    Root pathEntry = Root.fromPath(fs.getPath("/path"));
+    DiffAwareness diffAwareness = mock(DiffAwareness.class);
+    View view1 = createView(new WorkspaceInfoFromDiff() {});
+    View view2 = createView(/*workspaceInfo=*/ null);
+    when(diffAwareness.getCurrentView(any())).thenReturn(view1, view2);
+    when(diffAwareness.getDiff(view1, view2))
+        .thenReturn(ModifiedFileSet.builder().modify(PathFragment.create("file")).build());
+    DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class);
+    when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness);
+    DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory));
+    manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY);
+
+    ProcessableModifiedFileSet diff =
+        manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY);
+
+    assertThat(diff.getWorkspaceInfo()).isNull();
+  }
+
+  @Test
+  public void getDiff_brokenDiffAwareness_returnsDiffWithNullWorkspaceInfo() throws Exception {
+    Root pathEntry = Root.fromPath(fs.getPath("/path"));
+    WorkspaceInfoFromDiff workspaceInfo1 = new WorkspaceInfoFromDiff() {};
+    WorkspaceInfoFromDiff workspaceInfo2 = new WorkspaceInfoFromDiff() {};
+    DiffAwareness diffAwareness = mock(DiffAwareness.class);
+    View view1 = createView(workspaceInfo1);
+    View view2 = createView(workspaceInfo2);
+    when(diffAwareness.getCurrentView(any())).thenReturn(view1, view2);
+    when(diffAwareness.getDiff(view1, view2)).thenThrow(BrokenDiffAwarenessException.class);
+    DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class);
+    when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness);
+    DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory));
+    manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY);
+
+    ProcessableModifiedFileSet diff =
+        manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY);
+
+    assertThat(diff.getWorkspaceInfo()).isNull();
+  }
+
+  @Test
+  public void getDiff_incompatibleDiff_fails() throws Exception {
+    Root pathEntry = Root.fromPath(fs.getPath("/path"));
+    DiffAwareness diffAwareness = mock(DiffAwareness.class);
+    View view1 = createView(/*workspaceInfo=*/ null);
+    View view2 = createView(/*workspaceInfo=*/ null);
+    when(diffAwareness.getCurrentView(any())).thenReturn(view1, view2);
+    when(diffAwareness.getDiff(view1, view2)).thenThrow(IncompatibleViewException.class);
+    DiffAwareness.Factory factory = mock(DiffAwareness.Factory.class);
+    when(factory.maybeCreate(pathEntry)).thenReturn(diffAwareness);
+    DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory));
+    manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY);
+
+    assertThrows(
+        IllegalStateException.class,
+        () -> manager.getDiff(events.reporter(), pathEntry, OptionsProvider.EMPTY));
+  }
+
+  private static View createView(@Nullable WorkspaceInfoFromDiff workspaceInfo) {
+    return new View() {
+      @Nullable
+      @Override
+      public WorkspaceInfoFromDiff getWorkspaceInfo() {
+        return workspaceInfo;
+      }
+    };
+  }
+
   private static class DiffAwarenessFactoryStub implements DiffAwareness.Factory {
 
     private final Map<Root, DiffAwareness> diffAwarenesses = Maps.newHashMap();