Add repository override option

RELNOTES: Adds a --override_repository option that takes a repository
name and path. This forces Bazel to use the directory at that path
for the repository. Example usage:
`--override_repository=foo=/home/user/gitroot/foo`.

Fixes #1266

PiperOrigin-RevId: 153599291
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index a3707e7..4c4a80e 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -752,6 +752,7 @@
         "//src/main/java/com/google/devtools/common/options",
         "//third_party:aether",
         "//third_party:apache_commons_compress",
+        "//third_party:auto_value",
         "//third_party:guava",
         "//third_party:jgit",
         "//third_party:jsr305",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
index 4c73880..91b2226 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
@@ -16,6 +16,8 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
 import com.google.devtools.build.lib.analysis.RuleDefinition;
@@ -31,6 +33,7 @@
 import com.google.devtools.build.lib.bazel.repository.NewGitRepositoryFunction;
 import com.google.devtools.build.lib.bazel.repository.NewHttpArchiveFunction;
 import com.google.devtools.build.lib.bazel.repository.RepositoryOptions;
+import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.RepositoryOverride;
 import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
 import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader;
 import com.google.devtools.build.lib.bazel.repository.skylark.SkylarkRepositoryFunction;
@@ -47,6 +50,7 @@
 import com.google.devtools.build.lib.bazel.rules.workspace.MavenServerRule;
 import com.google.devtools.build.lib.bazel.rules.workspace.NewGitRepositoryRule;
 import com.google.devtools.build.lib.bazel.rules.workspace.NewHttpArchiveRule;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
 import com.google.devtools.build.lib.rules.repository.LocalRepositoryFunction;
 import com.google.devtools.build.lib.rules.repository.LocalRepositoryRule;
@@ -61,12 +65,15 @@
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.runtime.ServerBuilder;
 import com.google.devtools.build.lib.runtime.WorkspaceBuilder;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue.Injected;
 import com.google.devtools.build.lib.skyframe.SkyFunctions;
 import com.google.devtools.build.lib.skyframe.SkyValueDirtinessChecker;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
 import com.google.devtools.build.lib.vfs.FileSystem;
 import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.SkyValue;
 import com.google.devtools.common.options.OptionsBase;
@@ -88,6 +95,7 @@
   private final RepositoryCache repositoryCache = new RepositoryCache();
   private final HttpDownloader httpDownloader = new HttpDownloader(repositoryCache);
   private final MavenDownloader mavenDownloader = new MavenDownloader(repositoryCache);
+  private ImmutableMap<RepositoryName, PathFragment> overrides = ImmutableMap.of();
   private FileSystem filesystem;
 
   public BazelRepositoryModule() {
@@ -152,7 +160,6 @@
     builder.addSkyFunction(SkyFunctions.REPOSITORY, new RepositoryLoaderFunction());
     builder.addSkyFunction(SkyFunctions.REPOSITORY_DIRECTORY, delegator);
     builder.addSkyFunction(MavenServerFunction.NAME, new MavenServerFunction());
-
     filesystem = directories.getFileSystem();
   }
 
@@ -184,17 +191,37 @@
       } else {
         repositoryCache.setRepositoryCachePath(null);
       }
+
+      if (repoOptions.repositoryOverrides != null) {
+        ImmutableMap.Builder<RepositoryName, PathFragment> builder = ImmutableMap.builder();
+        for (RepositoryOverride override : repoOptions.repositoryOverrides) {
+          builder.put(override.repositoryName(), override.path());
+        }
+        ImmutableMap<RepositoryName, PathFragment> newOverrides = builder.build();
+        if (!Maps.difference(overrides, newOverrides).areEqual()) {
+          overrides = newOverrides;
+        }
+      } else {
+        overrides = ImmutableMap.of();
+      }
     }
   }
 
   @Override
+  public ImmutableList<Injected> getPrecomputedValues() {
+    return ImmutableList.of(
+        PrecomputedValue.injected(
+            RepositoryDelegatorFunction.REPOSITORY_OVERRIDES, overrides));
+  }
+
+  @Override
   public void beforeCommand(Command command, CommandEnvironment env) throws AbruptExitException {
     delegator.setClientEnvironment(env.getActionClientEnv());
   }
 
   @Override
   public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
-    return "fetch".equals(command.name()) || "build".equals(command.name())
+    return ImmutableSet.of("fetch", "build", "query").contains(command.name())
         ? ImmutableList.<Class<? extends OptionsBase>>of(RepositoryOptions.class)
         : ImmutableList.<Class<? extends OptionsBase>>of();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java
index 3fdf939..0072f10 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java
@@ -14,11 +14,17 @@
 
 package com.google.devtools.build.lib.bazel.repository;
 
+import com.google.auto.value.AutoValue;
+import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.util.OptionsUtils;
 import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.Converter;
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionsBase;
 import com.google.devtools.common.options.OptionsParser.OptionUsageRestrictions;
+import com.google.devtools.common.options.OptionsParsingException;
+import java.util.List;
 
 /**
  * Command-line options for repositories.
@@ -36,4 +42,54 @@
   )
   public PathFragment experimentalRepositoryCache;
 
-}
\ No newline at end of file
+  @Option(name = "override_repository",
+      defaultValue = "null",
+      allowMultiple = true,
+      converter = RepositoryOverrideConverter.class,
+      help = "Overrides a repository with a local directory.")
+  public List<RepositoryOverride> repositoryOverrides;
+
+  /**
+   * Converts from an equals-separated pair of strings into RepositoryName->PathFragment mapping.
+   */
+  public static class RepositoryOverrideConverter implements Converter<RepositoryOverride> {
+
+    @Override
+    public RepositoryOverride convert(String input) throws OptionsParsingException {
+      String[] pieces = input.split("=");
+      if (pieces.length != 2) {
+        throw new OptionsParsingException(
+            "Repository overrides must be of the form 'repository-name=path'", input);
+      }
+      PathFragment path = PathFragment.create(pieces[1]);
+      if (!path.isAbsolute()) {
+        throw new OptionsParsingException(
+            "Repository override directory must be an absolute path", input);
+      }
+      try {
+        return RepositoryOverride.create(RepositoryName.create("@" + pieces[0]), path);
+      } catch (LabelSyntaxException e) {
+        throw new OptionsParsingException("Invalid repository name given to override", input);
+      }
+    }
+
+    @Override
+    public String getTypeDescription() {
+      return "an equals-separated mapping of repository name to path";
+    }
+  }
+
+  /**
+   * A repository override, represented by a name and an absolute path to a repository.
+   */
+  @AutoValue
+  public abstract static class RepositoryOverride {
+
+    private static RepositoryOverride create(RepositoryName repositoryName, PathFragment path) {
+      return new AutoValue_RepositoryOptions_RepositoryOverride(repositoryName, path);
+    }
+
+    public abstract RepositoryName repositoryName();
+    public abstract PathFragment path();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/repository/LocalRepositoryFunction.java b/src/main/java/com/google/devtools/build/lib/rules/repository/LocalRepositoryFunction.java
index ee387f7..916569c 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/repository/LocalRepositoryFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/repository/LocalRepositoryFunction.java
@@ -40,14 +40,20 @@
       BlazeDirectories directories, Environment env, Map<String, String> markerData)
       throws InterruptedException, RepositoryFunctionException {
     PathFragment pathFragment = RepositoryFunction.getTargetPath(rule, directories.getWorkspace());
+    return LocalRepositoryFunction.symlink(outputDirectory, pathFragment, env);
+  }
+
+  public static RepositoryDirectoryValue.Builder symlink(
+      Path source, PathFragment destination, Environment env)
+      throws RepositoryFunctionException, InterruptedException {
     try {
-      outputDirectory.createSymbolicLink(pathFragment);
+      source.createSymbolicLink(destination);
     } catch (IOException e) {
       throw new RepositoryFunctionException(
-          new IOException("Could not create symlink to repository " + pathFragment + ": "
+          new IOException("Could not create symlink to repository " + destination + ": "
               + e.getMessage(), e), Transience.TRANSIENT);
     }
-    FileValue repositoryValue = getRepositoryDirectory(outputDirectory, env);
+    FileValue repositoryValue = getRepositoryDirectory(source, env);
     if (repositoryValue == null) {
       // TODO(bazel-team): If this returns null, we unnecessarily recreate the symlink above on the
       // second execution.
@@ -56,10 +62,10 @@
 
     if (!repositoryValue.isDirectory()) {
       throw new RepositoryFunctionException(
-          new IOException(rule + " must specify an existing directory"), Transience.TRANSIENT);
+          new IOException(source + " must be an existing directory"), Transience.TRANSIENT);
     }
 
-    return RepositoryDirectoryValue.builder().setPath(outputDirectory);
+    return RepositoryDirectoryValue.builder().setPath(source);
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java b/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java
index 771231b..259950b 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java
@@ -25,10 +25,13 @@
 import com.google.devtools.build.lib.rules.repository.RepositoryFunction.RepositoryFunctionException;
 import com.google.devtools.build.lib.skyframe.FileValue;
 import com.google.devtools.build.lib.skyframe.PrecomputedValue;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue.Precomputed;
+import com.google.devtools.build.lib.skyframe.SkyFunctions;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.SkyFunction;
 import com.google.devtools.build.skyframe.SkyFunctionException;
 import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
@@ -49,6 +52,8 @@
  * this function.
  */
 public final class RepositoryDelegatorFunction implements SkyFunction {
+  public static final Precomputed<Map<RepositoryName, PathFragment>> REPOSITORY_OVERRIDES =
+      new Precomputed<>(SkyKey.create(SkyFunctions.PRECOMPUTED, "repository_overrides"));
 
   // The marker file version is inject in the rule key digest so the rule key is always different
   // when we decide to update the format.
@@ -93,13 +98,22 @@
   public SkyValue compute(SkyKey skyKey, Environment env)
       throws SkyFunctionException, InterruptedException {
     RepositoryName repositoryName = (RepositoryName) skyKey.argument();
-    Rule rule = RepositoryFunction.getRule(repositoryName, null, env);
-    if (rule == null) {
+    BlazeDirectories directories = PrecomputedValue.BLAZE_DIRECTORIES.get(env);
+    Map<RepositoryName, PathFragment> overrides = REPOSITORY_OVERRIDES.get(env);
+    if (env.valuesMissing()) {
       return null;
     }
 
-    BlazeDirectories directories = PrecomputedValue.BLAZE_DIRECTORIES.get(env);
-    if (directories == null) {
+    Path repoRoot = RepositoryFunction.getExternalRepositoryDirectory(directories)
+        .getRelative(repositoryName.strippedName());
+    Path markerPath = getMarkerPath(directories, repositoryName.strippedName());
+    if (overrides.containsKey(repositoryName)) {
+      return setupOverride(
+          repositoryName, overrides.get(repositoryName), env, repoRoot, markerPath);
+    }
+
+    Rule rule = RepositoryFunction.getRule(repositoryName, null, env);
+    if (rule == null) {
       return null;
     }
     RepositoryFunction handler;
@@ -110,23 +124,20 @@
     }
     if (handler == null) {
       throw new RepositoryFunctionException(
-          new EvalException(Location.fromFile(directories.getWorkspace().getRelative("WORKSPACE")),
+          new EvalException(
+              Location.fromFile(directories.getWorkspace().getRelative("WORKSPACE")),
               "Could not find handler for " + rule),
           Transience.PERSISTENT);
     }
 
     handler.setClientEnvironment(clientEnvironment);
 
-    Path repoRoot =
-        RepositoryFunction.getExternalRepositoryDirectory(directories).getRelative(rule.getName());
     byte[] ruleSpecificData = handler.getRuleSpecificMarkerData(rule, env);
     if (ruleSpecificData == null) {
       return null;
     }
     String ruleKey = computeRuleKey(rule, ruleSpecificData);
     Map<String, String> markerData = new TreeMap<>();
-    Path markerPath = getMarkerPath(directories, rule);
-
     if (handler.isLocal(rule)) {
       // Local repositories are always fetched because the operation is generally fast and they do
       // not depend on non-local data, so it does not make much sense to try to cache from across
@@ -207,7 +218,7 @@
         .setFetchingDelayed().build();
   }
 
-  private final String computeRuleKey(Rule rule, byte[] ruleSpecificData) {
+  private String computeRuleKey(Rule rule, byte[] ruleSpecificData) {
     return new Fingerprint().addBytes(RuleFormatter.serializeRule(rule).build().toByteArray())
         .addBytes(ruleSpecificData)
         .addInt(MARKER_FILE_VERSION).hexDigestAndReset();
@@ -226,7 +237,7 @@
    * system is up to date.
    */
   @Nullable
-  private final byte[] isFilesystemUpToDate(Path markerPath, Rule rule, String ruleKey,
+  private byte[] isFilesystemUpToDate(Path markerPath, Rule rule, String ruleKey,
       RepositoryFunction handler, Environment env)
       throws RepositoryFunctionException, InterruptedException {
     try {
@@ -310,7 +321,7 @@
     return result.toString();
   }
 
-  private final byte[] writeMarkerFile(
+  private byte[] writeMarkerFile(
       Path markerPath, Map<String, String> markerData, String ruleKey)
       throws RepositoryFunctionException {
     try {
@@ -329,13 +340,30 @@
     }
   }
 
-  private static Path getMarkerPath(BlazeDirectories directories, Rule rule) {
+  private static Path getMarkerPath(BlazeDirectories directories, String ruleName) {
     return RepositoryFunction.getExternalRepositoryDirectory(directories)
-        .getChild("@" + rule.getName() + ".marker");
+        .getChild("@" + ruleName + ".marker");
   }
 
   @Override
   public String extractTag(SkyKey skyKey) {
     return null;
   }
+
+  private RepositoryDirectoryValue setupOverride(
+      RepositoryName repositoryName, PathFragment sourcePath, Environment env, Path repoRoot,
+      Path markerPath)
+      throws RepositoryFunctionException, InterruptedException {
+    setupRepositoryRoot(repoRoot);
+    RepositoryDirectoryValue.Builder directoryValue = LocalRepositoryFunction.symlink(
+        repoRoot, sourcePath, env);
+    if (directoryValue == null) {
+      return null;
+    }
+    String ruleKey = new Fingerprint().addBytes(repositoryName.strippedName().getBytes())
+        .addBytes(repoRoot.getFileSystem().getPath(sourcePath).getPathString().getBytes())
+        .addInt(MARKER_FILE_VERSION).hexDigestAndReset();
+    byte[] digest = writeMarkerFile(markerPath, new TreeMap<String, String>(), ruleKey);
+    return directoryValue.setDigest(digest).build();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryFunction.java b/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryFunction.java
index 51486ed..8afb6ee 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryFunction.java
@@ -470,7 +470,12 @@
         // function so we get invalidation when the repository is fetched.
         // For the repository directory itself, we cannot depends on the RepositoryDirectoryValue
         // (cycle).
-        env.getValue(RepositoryDirectoryValue.key(RepositoryName.create("@" + repositoryName)));
+        env.getValue(
+            RepositoryDirectoryValue.key(
+                RepositoryName.createFromValidStrippedName(repositoryName)));
+      } else {
+        // Invalidate external/<repo> if the repository overrides change.
+        RepositoryDelegatorFunction.REPOSITORY_OVERRIDES.get(env);
       }
     } catch (RepositoryFunction.RepositoryNotFoundException ex) {
       // The repository we are looking for does not exist so we should depend on the whole
@@ -478,7 +483,7 @@
       // already requested all repository functions from the WORKSPACE file from Skyframe as part
       // of the resolution. Therefore we are safe to ignore that Exception.
       return;
-    } catch (RepositoryFunctionException | LabelSyntaxException ex) {
+    } catch (RepositoryFunctionException ex) {
       // This should never happen.
       throw new IllegalStateException(
           "Repository " + repositoryName + " cannot be resolved for path " + rootedPath, ex);
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
index 3b26e1e..26a96d0 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
@@ -511,6 +511,10 @@
 
       env.getEventBus().post(originalCommandLine);
 
+      for (BlazeModule module : runtime.getBlazeModules()) {
+        env.getSkyframeExecutor().injectExtraPrecomputedValues(module.getPrecomputedValues());
+      }
+
       ExitCode outcome = command.exec(env, optionsParser);
       outcome = env.precompleteCommand(outcome);
       numericExitCode = outcome.getNumericExitCode();
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
index 39d6d4a..f05586c 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.packages.PackageFactory;
 import com.google.devtools.build.lib.packages.RuleClassProvider;
 import com.google.devtools.build.lib.rules.test.CoverageReportActionFactory;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.Clock;
 import com.google.devtools.build.lib.vfs.FileSystem;
@@ -290,4 +291,8 @@
      */
     void exit(AbruptExitException exception);
   }
+
+  public ImmutableList<PrecomputedValue.Injected> getPrecomputedValues() {
+    return ImmutableList.of();
+  }
 }
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 4fcea31..7f2d2e8 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
@@ -495,6 +495,10 @@
     if (!skyframeExecutor.hasIncrementalState()) {
       skyframeExecutor.resetEvaluator();
     }
+
+    for (BlazeModule module : runtime.getBlazeModules()) {
+      skyframeExecutor.injectExtraPrecomputedValues(module.getPrecomputedValues());
+    }
     skyframeExecutor.sync(
         reporter,
         options.getOptions(PackageCacheOptions.class),
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
index f0d06e0..2619af2 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
@@ -60,6 +60,11 @@
     void inject(Injectable injectable) {
       injectable.inject(precomputed.key, new PrecomputedValue(supplier.get()));
     }
+
+    @Override
+    public String toString() {
+      return precomputed + ": " + supplier.get();
+    }
   }
 
   public static <T> Injected injected(Precomputed<T> precomputed, Supplier<T> value) {
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 fe5b1d4..dfc2657 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
@@ -701,12 +701,13 @@
       PrecomputedValue.BLAZE_DIRECTORIES.set(injectable(), directories);
       PrecomputedValue.PRODUCT_NAME.set(injectable(), productName);
       injectBuildInfoFactories();
-      injectExtraPrecomputedValues();
+      injectExtraPrecomputedValues(extraPrecomputedValues);
       needToInjectPrecomputedValuesForAnalysis = false;
     }
   }
 
-  private void injectExtraPrecomputedValues() {
+  public void injectExtraPrecomputedValues(
+      List<PrecomputedValue.Injected> extraPrecomputedValues) {
     for (PrecomputedValue.Injected injected : extraPrecomputedValues) {
       injected.inject(injectable());
     }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceNameValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceNameValue.java
index 1fccf6d..c752d58 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceNameValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceNameValue.java
@@ -98,5 +98,10 @@
     public int hashCode() {
       return HASHCODE;
     }
+
+    @Override
+    public String toString() {
+      return "#";
+    }
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestCase.java
index 032599f..1cd5685 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/AnalysisTestCase.java
@@ -38,6 +38,7 @@
 import com.google.devtools.build.lib.buildtool.BuildRequest.BuildRequestOptions;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.exec.ExecutionOptions;
 import com.google.devtools.build.lib.flags.InvocationPolicyEnforcer;
 import com.google.devtools.build.lib.packages.PackageFactory;
@@ -49,6 +50,7 @@
 import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
 import com.google.devtools.build.lib.pkgcache.PackageManager;
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
 import com.google.devtools.build.lib.skyframe.DiffAwareness;
 import com.google.devtools.build.lib.skyframe.PackageLookupFunction.CrossRepositoryLabelViolationStrategy;
@@ -192,6 +194,9 @@
         ImmutableMap.<String, String>of(),
         ImmutableMap.<String, String>of(),
         new TimestampGranularityMonitor(BlazeClock.instance()));
+    skyframeExecutor.injectExtraPrecomputedValues(ImmutableList.of(PrecomputedValue.injected(
+        RepositoryDelegatorFunction.REPOSITORY_OVERRIDES,
+        ImmutableMap.<RepositoryName, PathFragment>of())));
     packageManager = skyframeExecutor.getPackageManager();
     loadingPhaseRunner = skyframeExecutor.getLoadingPhaseRunner(
         pkgFactory.getRuleClassNames(), defaultFlags().contains(Flag.SKYFRAME_LOADING_PHASE));
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
index 140f99e..33ebdee 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
@@ -118,6 +118,7 @@
 import com.google.devtools.build.lib.pkgcache.PackageManager;
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
 import com.google.devtools.build.lib.rules.extra.ExtraAction;
+import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
 import com.google.devtools.build.lib.rules.test.BaselineCoverageAction;
 import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider;
 import com.google.devtools.build.lib.skyframe.AspectValue;
@@ -232,6 +233,9 @@
             analysisMock.getProductName(),
             CrossRepositoryLabelViolationStrategy.ERROR,
             ImmutableList.of(BuildFileName.BUILD_DOT_BAZEL, BuildFileName.BUILD));
+    skyframeExecutor.injectExtraPrecomputedValues(ImmutableList.of(PrecomputedValue.injected(
+        RepositoryDelegatorFunction.REPOSITORY_OVERRIDES,
+        ImmutableMap.<RepositoryName, PathFragment>of())));
     packageCacheOptions.defaultVisibility = ConstantRuleVisibility.PUBLIC;
     packageCacheOptions.showLoadingProgress = true;
     packageCacheOptions.globbingThreads = 7;
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/ConfigurationTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/util/ConfigurationTestCase.java
index 490b642..fbf1702 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/ConfigurationTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/ConfigurationTestCase.java
@@ -36,6 +36,7 @@
 import com.google.devtools.build.lib.packages.util.MockToolsConfig;
 import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
 import com.google.devtools.build.lib.skyframe.DiffAwareness;
 import com.google.devtools.build.lib.skyframe.PackageLookupFunction.CrossRepositoryLabelViolationStrategy;
 import com.google.devtools.build.lib.skyframe.PackageLookupValue.BuildFileName;
@@ -116,7 +117,9 @@
             analysisMock.getProductName(),
             CrossRepositoryLabelViolationStrategy.ERROR,
             ImmutableList.of(BuildFileName.BUILD_DOT_BAZEL, BuildFileName.BUILD));
-
+    skyframeExecutor.injectExtraPrecomputedValues(ImmutableList.of(PrecomputedValue.injected(
+        RepositoryDelegatorFunction.REPOSITORY_OVERRIDES,
+        ImmutableMap.<RepositoryName, PathFragment>of())));
     PackageCacheOptions packageCacheOptions = Options.getDefaults(PackageCacheOptions.class);
     packageCacheOptions.showLoadingProgress = true;
     packageCacheOptions.globbingThreads = 7;
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/BUILD b/src/test/java/com/google/devtools/build/lib/bazel/repository/BUILD
index 9605393..d3c387d 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/BUILD
@@ -29,6 +29,7 @@
         "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
         "//src/main/java/com/google/devtools/build/lib/rules/cpp",
         "//src/main/java/com/google/devtools/build/skyframe",
+        "//src/main/java/com/google/devtools/common/options",
         "//src/test/java/com/google/devtools/build/lib:analysis_testutil",
         "//src/test/java/com/google/devtools/build/lib:foundations_testutil",
         "//src/test/java/com/google/devtools/build/lib:packages_testutil",
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptionsTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptionsTest.java
new file mode 100644
index 0000000..d70b6d8
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptionsTest.java
@@ -0,0 +1,70 @@
+// Copyright 2017 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.bazel.repository;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.RepositoryOverride;
+import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.RepositoryOverrideConverter;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.OptionsParsingException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test for {@link RepositoryOptions}.
+ */
+@RunWith(JUnit4.class)
+public class RepositoryOptionsTest {
+
+  private final RepositoryOverrideConverter converter = new RepositoryOverrideConverter();
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void testOverrideConverter() throws Exception {
+    RepositoryOverride actual = converter.convert("foo=/bar");
+    assertThat(actual.repositoryName())
+        .isEqualTo(RepositoryName.createFromValidStrippedName("foo"));
+    assertThat(actual.path()).isEqualTo(PathFragment.create("/bar"));
+  }
+
+  @Test
+  public void testInvalidOverride() throws Exception {
+    expectedException.expect(OptionsParsingException.class);
+    expectedException.expectMessage(
+        "Repository overrides must be of the form 'repository-name=path'");
+    converter.convert("foo");
+  }
+
+  @Test
+  public void testInvalidRepoOverride() throws Exception {
+    expectedException.expect(OptionsParsingException.class);
+    expectedException.expectMessage("Invalid repository name given to override");
+    converter.convert("foo/bar=/baz");
+  }
+
+  @Test
+  public void testInvalidPathOverride() throws Exception {
+    expectedException.expect(OptionsParsingException.class);
+    expectedException.expectMessage("Repository override directory must be an absolute path");
+    converter.convert("foo=bar");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/rules/repository/BUILD b/src/test/java/com/google/devtools/build/lib/rules/repository/BUILD
index ed24a50..8d94ddb 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/repository/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/rules/repository/BUILD
@@ -19,6 +19,8 @@
         "//src/main/java/com/google/devtools/build/lib:bazel-main",
         "//src/main/java/com/google/devtools/build/lib:bazel-repository",
         "//src/main/java/com/google/devtools/build/lib:build-base",
+        "//src/main/java/com/google/devtools/build/lib:events",
+        "//src/main/java/com/google/devtools/build/lib:io",
         "//src/main/java/com/google/devtools/build/lib:packages-internal",
         "//src/main/java/com/google/devtools/build/lib:runtime",
         "//src/main/java/com/google/devtools/build/lib:unix",
diff --git a/src/test/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorTest.java b/src/test/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorTest.java
new file mode 100644
index 0000000..f6955e5
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorTest.java
@@ -0,0 +1,142 @@
+// Copyright 2017 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.rules.repository;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.skyframe.ExternalFilesHelper;
+import com.google.devtools.build.lib.skyframe.ExternalFilesHelper.ExternalFileAction;
+import com.google.devtools.build.lib.skyframe.ExternalPackageFunction;
+import com.google.devtools.build.lib.skyframe.FileFunction;
+import com.google.devtools.build.lib.skyframe.FileStateFunction;
+import com.google.devtools.build.lib.skyframe.LocalRepositoryLookupFunction;
+import com.google.devtools.build.lib.skyframe.PackageFunction;
+import com.google.devtools.build.lib.skyframe.PackageLookupFunction;
+import com.google.devtools.build.lib.skyframe.PackageLookupFunction.CrossRepositoryLabelViolationStrategy;
+import com.google.devtools.build.lib.skyframe.PackageLookupValue.BuildFileName;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue;
+import com.google.devtools.build.lib.skyframe.SkyFunctions;
+import com.google.devtools.build.lib.skyframe.WorkspaceASTFunction;
+import com.google.devtools.build.lib.skyframe.WorkspaceFileFunction;
+import com.google.devtools.build.lib.testutil.FoundationTestCase;
+import com.google.devtools.build.lib.testutil.TestConstants;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator;
+import com.google.devtools.build.skyframe.MemoizingEvaluator;
+import com.google.devtools.build.skyframe.RecordingDifferencer;
+import com.google.devtools.build.skyframe.SequentialBuildDriver;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link RepositoryDelegatorFunction}
+ */
+@RunWith(JUnit4.class)
+public class RepositoryDelegatorTest extends FoundationTestCase {
+  private RepositoryDelegatorFunction delegatorFunction;
+  private Path overrideDirectory;
+  private SequentialBuildDriver driver;
+
+  @Before
+  public void setupDelegator() throws Exception {
+    Path root = scratch.dir("/outputbase");
+    delegatorFunction = new RepositoryDelegatorFunction(
+        ImmutableMap.<String, RepositoryFunction>of(), null, new AtomicBoolean(true));
+    AtomicReference<PathPackageLocator> pkgLocator = new AtomicReference<>(
+        new PathPackageLocator(root, ImmutableList.of(root)));
+    BlazeDirectories directories = new BlazeDirectories(root, root, root,
+        TestConstants.PRODUCT_NAME);
+    ExternalFilesHelper externalFilesHelper = new ExternalFilesHelper(
+        pkgLocator,
+        ExternalFileAction.DEPEND_ON_EXTERNAL_PKG_FOR_EXTERNAL_REPO_PATHS,
+        directories);
+    RecordingDifferencer differencer = new RecordingDifferencer();
+    MemoizingEvaluator evaluator =
+        new InMemoryMemoizingEvaluator(
+            ImmutableMap.<SkyFunctionName, SkyFunction>builder()
+                .put(
+                    SkyFunctions.FILE_STATE,
+                    new FileStateFunction(
+                        new AtomicReference<TimestampGranularityMonitor>(), externalFilesHelper))
+                .put(SkyFunctions.FILE, new FileFunction(pkgLocator))
+                .put(SkyFunctions.REPOSITORY_DIRECTORY, delegatorFunction)
+                .put(
+                    SkyFunctions.PACKAGE,
+                    new PackageFunction(null, null, null, null, null, null, null))
+                .put(
+                    SkyFunctions.PACKAGE_LOOKUP,
+                    new PackageLookupFunction(
+                        null,
+                        CrossRepositoryLabelViolationStrategy.ERROR,
+                        ImmutableList.of(BuildFileName.BUILD_DOT_BAZEL, BuildFileName.BUILD)))
+                .put(
+                    SkyFunctions.WORKSPACE_AST,
+                    new WorkspaceASTFunction(TestRuleClassProvider.getRuleClassProvider()))
+                .put(
+                    SkyFunctions.WORKSPACE_FILE,
+                    new WorkspaceFileFunction(
+                        TestRuleClassProvider.getRuleClassProvider(),
+                        TestConstants.PACKAGE_FACTORY_FACTORY_FOR_TESTING.create(
+                            TestRuleClassProvider.getRuleClassProvider(), root.getFileSystem()),
+                        directories))
+                .put(SkyFunctions.LOCAL_REPOSITORY_LOOKUP, new LocalRepositoryLookupFunction())
+                .put(SkyFunctions.EXTERNAL_PACKAGE, new ExternalPackageFunction())
+                .build(),
+            differencer);
+    driver = new SequentialBuildDriver(evaluator);
+    overrideDirectory = scratch.dir("/foo");
+    RepositoryDelegatorFunction.REPOSITORY_OVERRIDES.set(
+        differencer,
+        ImmutableMap.<RepositoryName, PathFragment>builder()
+            .put(RepositoryName.createFromValidStrippedName("foo"), overrideDirectory.asFragment())
+            .build());
+    PrecomputedValue.BLAZE_DIRECTORIES.set(differencer, directories);
+    PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator.get());
+  }
+
+  @Test
+  public void testOverride() throws Exception {
+    StoredEventHandler eventHandler = new StoredEventHandler();
+    SkyKey key = RepositoryDirectoryValue.key(RepositoryName.createFromValidStrippedName("foo"));
+    EvaluationResult<SkyValue> result =
+        driver.evaluate(ImmutableList.of(key), false, 8, eventHandler);
+    assertThat(result.hasError()).isFalse();
+    RepositoryDirectoryValue repositoryDirectoryValue = (RepositoryDirectoryValue) result.get(key);
+    Path expectedPath = scratch.dir("/outputbase/external/foo");
+    Path actualPath = repositoryDirectoryValue.getPath();
+    assertThat(actualPath).isEqualTo(expectedPath);
+    assertThat(actualPath.isSymbolicLink()).isTrue();
+    assertThat(actualPath.readSymbolicLink()).isEqualTo(overrideDirectory.asFragment());
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java
index 68f0386..f825390 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FileFunctionTest.java
@@ -37,9 +37,11 @@
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.events.NullEventHandler;
 import com.google.devtools.build.lib.events.StoredEventHandler;
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
 import com.google.devtools.build.lib.skyframe.ExternalFilesHelper.ExternalFileAction;
 import com.google.devtools.build.lib.skyframe.PackageLookupFunction.CrossRepositoryLabelViolationStrategy;
 import com.google.devtools.build.lib.skyframe.PackageLookupValue.BuildFileName;
@@ -165,6 +167,8 @@
             differencer);
     PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID());
     PrecomputedValue.PATH_PACKAGE_LOCATOR.set(differencer, pkgLocator);
+    RepositoryDelegatorFunction.REPOSITORY_OVERRIDES.set(
+        differencer, ImmutableMap.<RepositoryName, PathFragment>of());
     return new SequentialBuildDriver(evaluator);
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PackageLookupFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PackageLookupFunctionTest.java
index 6ad7725..9fe0880 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/PackageLookupFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PackageLookupFunctionTest.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.util.AnalysisMock;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.events.NullEventHandler;
 import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
 import com.google.devtools.build.lib.packages.PackageFactory;
@@ -143,6 +144,8 @@
     PrecomputedValue.BLACKLISTED_PACKAGE_PREFIXES_FILE.set(
         differencer, PathFragment.EMPTY_FRAGMENT);
     PrecomputedValue.BLAZE_DIRECTORIES.set(differencer, directories);
+    RepositoryDelegatorFunction.REPOSITORY_OVERRIDES.set(
+        differencer, ImmutableMap.<RepositoryName, PathFragment>of());
   }
 
   protected PackageLookupValue lookupPackage(String packageName) throws InterruptedException {
diff --git a/src/test/shell/bazel/workspace_test.sh b/src/test/shell/bazel/workspace_test.sh
index a319cbb..5b4b7de 100755
--- a/src/test/shell/bazel/workspace_test.sh
+++ b/src/test/shell/bazel/workspace_test.sh
@@ -203,4 +203,44 @@
   bazel build @bar//:depend-on-foo || fail "Expected build to succeed"
 }
 
+function test_workspace_override() {
+  mkdir -p original
+  touch original/WORKSPACE
+  cat > original/BUILD <<'EOF'
+genrule(
+    name = "gen",
+    cmd = "echo 'original' > $@",
+    outs = ["gen.out"],
+)
+EOF
+
+  mkdir -p override
+  touch override/WORKSPACE
+  cat > override/BUILD <<'EOF'
+genrule(
+    name = "gen",
+    cmd = "echo 'override' > $@",
+    outs = ["gen.out"],
+)
+EOF
+
+  cat > WORKSPACE <<EOF
+local_repository(
+    name = "o",
+    path = "original",
+)
+EOF
+  bazel build --override_repository="o=$PWD/override" @o//:gen &> $TEST_log \
+    || fail "Expected build to succeed"
+  assert_contains "override" bazel-genfiles/external/o/gen.out
+
+  bazel build @o//:gen &> $TEST_log \
+    || fail "Expected build to succeed"
+  assert_contains "original" bazel-genfiles/external/o/gen.out
+
+  bazel build --override_repository="o=$PWD/override" @o//:gen &> $TEST_log \
+    || fail "Expected build to succeed"
+  assert_contains "override" bazel-genfiles/external/o/gen.out
+}
+
 run_suite "workspace tests"