Merge "Preserve topic when cherry-picking"
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 3cc353b..b0cf0cb 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -33,7 +33,7 @@
 org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
 org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
 org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
-org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
 org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning
 org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
 org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
@@ -48,12 +48,12 @@
 org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
 org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
 org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
-org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
 org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
 org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
 org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
 org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
-org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning
 org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning
 org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
 org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 0c13b23..9bcbdf4 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -820,19 +820,22 @@
 
 [[changeMerge.threadPoolSize]]changeMerge.threadPoolSize::
 +
-Maximum size of the thread pool in which the mergeability flag of open
-changes is updated.
+_Deprecated:_ Formerly used to control thread pool size for background
+mergeability checks. These checks were moved to the indexing threadpool,
+so this value is now used for
+link:#index.batchThreads[index.batchThreads], only if that value is not
+provided.
 +
-Default is 1.
+This option may be removed in a future version.
 
 [[changeMerge.interactiveThreadPoolSize]]changeMerge.interactiveThreadPoolSize::
 +
-Maximum size of the thread pool in which the mergeability flag of open
-changes is updated, when processing interactive user requests (e.g.
-pushes to refs/for/*). Set to 0 or negative to share the pool for
-background mergeability checks.
+_Deprecated:_ Formerly used to control thread pool size for interactive
+mergeability checks. These checks were moved to the indexing threadpool,
+so this value is now used for link:#index.threads[index.threads], only
+if that value is not provided.
 +
-Default is 1.
+This option may be removed in a future version.
 
 [[commentlink]]
 === Section commentlink
@@ -2036,9 +2039,20 @@
 
 [[index.threads]]index.threads::
 +
-Determines the number of threads to use for indexing.
+Number of threads to use for indexing in normal interactive operations.
 +
-Defaults to 1 if not set, or set to a negative value.
+Defaults to 1 if not set, or set to a negative value (unless
+link:#changeMerge.interactiveThreadPoolSize[changeMerge.interactiveThreadPoolSize]
+is iset).
+
+[[index.batchThreads]]index.batchThreads::
++
+Number of threads to use for indexing in background operations, such as
+online schema upgrades.
++
+If not set or set to a negative value, defaults to using the same
+thread pool as interactive operations (unless
+link:#changeMerge.threadPoolSize[changeMerge.threadPoolSize] is set).
 
 ==== Lucene configuration
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
index 8733611..faace57 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -43,18 +43,10 @@
   @Test
   public void reindexEmptySite() throws Exception {
     initSite();
-    runGerrit("reindex", "-d", sitePath.getPath(),
+    runGerrit("reindex", "-d", sitePath.toString(),
         "--show-stack-trace");
   }
 
-  @Test
-  public void reindexEmptySiteWithRecheckMergeable() throws Exception {
-    initSite();
-    runGerrit("reindex", "-d", sitePath.getPath(),
-        "--show-stack-trace",
-        "--recheck-mergeable");
-  }
-
   private void initSite() throws Exception {
     runGerrit("init", "-d", sitePath.getPath(),
         "--batch", "--no-auto-start", "--skip-plugins", "--show-stack-trace");
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index 448ce86..1b50a47 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -45,6 +45,7 @@
   protected SubmitType submitType;
   protected SubmitTypeRecord submitTypeRecord;
   protected boolean canSubmit;
+  protected boolean mergeable;
   protected List<ChangeMessage> messages;
   protected PatchSet.Id currentPatchSetId;
   protected PatchSetDetail currentDetail;
@@ -274,4 +275,12 @@
   public boolean canEdit() {
     return canEdit;
   }
+
+  public void setMergeable(boolean m) {
+    mergeable = m;
+  }
+
+  public boolean isMergeable() {
+    return mergeable;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
index f2c97c1..3ad90d9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
@@ -91,6 +91,7 @@
     r.setChange(toChange(info));
     r.setStarred(info.starred());
     r.setPatchSets(toPatchSets(info));
+    r.setMergeable(info.mergeable());
     r.setMessages(toMessages(info));
     r.setAccounts(users(info));
     r.setCurrentPatchSetId(new PatchSet.Id(info.legacy_id(), rev._number()));
@@ -224,7 +225,6 @@
     c.setStatus(info.status());
     c.setCurrentPatchSet(p);
     c.setLastUpdatedOn(info.updated());
-    c.setMergeable(info.mergeable());
     return c;
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
index 8fe7d56..5f5cf8f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
@@ -125,7 +125,7 @@
     if (Gerrit.getConfig().getNewFeatures()
         && (status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT))) {
       table.getRowFormatter().setVisible(R_MERGE_TEST, true);
-      table.setText(R_MERGE_TEST, 1, chg.isMergeable() ? Util.C
+      table.setText(R_MERGE_TEST, 1, changeDetail.isMergeable() ? Util.C
           .changeInfoBlockCanMergeYes() : Util.C.changeInfoBlockCanMergeNo());
     } else {
       table.getRowFormatter().setVisible(R_MERGE_TEST, false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
index 6c7423c..3afa208 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
@@ -17,6 +17,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /** Helper class to handle calculations involving line gaps. */
 class LineMapper {
@@ -193,6 +194,11 @@
     }
 
     @Override
+    public int hashCode() {
+      return Objects.hash(this);
+    }
+
+    @Override
     public String toString() {
       return line + " " + aligned;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 4180c70..63ec4ae 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -36,12 +36,12 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
-import com.google.gerrit.server.change.MergeabilityChecker;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ProjectCache;
@@ -79,7 +79,7 @@
   private final IdentifiedUser user;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final Provider<PostReviewers> reviewersProvider;
-  private final MergeabilityChecker mergeabilityChecker;
+  private final ChangeIndexer indexer;
   private final ChangeHooks hooks;
   private final CreateChangeSender.Factory createChangeSenderFactory;
   private final ProjectCache projectCache;
@@ -91,7 +91,8 @@
       MetaDataUpdate.User metaDataUpdateFactory, ReviewDb db,
       IdentifiedUser user, PatchSetInfoFactory patchSetInfoFactory,
       Provider<PostReviewers> reviewersProvider,
-      MergeabilityChecker mergeabilityChecker, ChangeHooks hooks,
+      ChangeIndexer indexer,
+      ChangeHooks hooks,
       CreateChangeSender.Factory createChangeSenderFactory,
       ProjectCache projectCache,
       AllProjectsNameProvider allProjects,
@@ -110,7 +111,7 @@
     this.user = user;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.reviewersProvider = reviewersProvider;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.hooks = hooks;
     this.createChangeSenderFactory = createChangeSenderFactory;
     this.projectCache = projectCache;
@@ -155,7 +156,7 @@
     } finally {
       db.rollback();
     }
-    mergeabilityChecker.newCheck().addChange(change).reindex().run();
+    indexer.index(db, change);
     hooks.doPatchsetCreatedHook(change, ps, db);
     try {
       CreateChangeSender cm =
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 9550055..676bc71 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.lucene;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
 import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
 import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -119,8 +120,10 @@
   private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
   private static final String DELETED_FIELD = ChangeField.DELETED.getName();
   private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+  private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName();
   private static final ImmutableSet<String> FIELDS = ImmutableSet.of(
-      ADDED_FIELD, APPROVAL_FIELD, CHANGE_FIELD, DELETED_FIELD, ID_FIELD);
+      ADDED_FIELD, APPROVAL_FIELD, CHANGE_FIELD, DELETED_FIELD, ID_FIELD,
+      MERGEABLE_FIELD);
   private static final Map<String, String> CUSTOM_CHAR_MAPPING = ImmutableMap.of(
       "_", " ", ".", " ");
 
@@ -138,6 +141,8 @@
     Version lucene47 = Version.LUCENE_47;
     @SuppressWarnings("deprecation")
     Version lucene48 = Version.LUCENE_48;
+    @SuppressWarnings("deprecation")
+    Version lucene410 = Version.LUCENE_4_10_0;
     for (Map.Entry<Integer, Schema<ChangeData>> e
         : ChangeSchemas.ALL.entrySet()) {
       if (e.getKey() <= 3) {
@@ -150,8 +155,10 @@
         versions.put(e.getValue(), lucene47);
       } else if (e.getKey() <= 11) {
         versions.put(e.getValue(), lucene48);
+      } else if (e.getKey() <= 13) {
+        versions.put(e.getValue(), lucene410);
       } else {
-        versions.put(e.getValue(), Version.LUCENE_4_10_0);
+        versions.put(e.getValue(), Version.LUCENE_4_10_1);
       }
     }
     LUCENE_VERSIONS = versions.build();
@@ -223,7 +230,7 @@
   LuceneChangeIndex(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      @IndexExecutor ListeningExecutorService executor,
+      @IndexExecutor(INTERACTIVE)  ListeningExecutorService executor,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       FillArgs fillArgs,
@@ -480,6 +487,14 @@
           deleted.numericValue().intValue());
     }
 
+    // Mergeable.
+    String mergeable = doc.get(MERGEABLE_FIELD);
+    if ("1".equals(mergeable)) {
+      cd.setMergeable(true);
+    } else if ("0".equals(mergeable)) {
+      cd.setMergeable(false);
+    }
+
     return cd;
   }
 
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
index d3dc963..99bf7e0 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
@@ -17,9 +17,9 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.collect.Lists;
-import com.google.gerrit.server.index.ChangeBatchIndexer;
 import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.SiteIndexer;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -39,14 +39,14 @@
   }
 
   private final IndexCollection indexes;
-  private final ChangeBatchIndexer batchIndexer;
+  private final SiteIndexer batchIndexer;
   private final ProjectCache projectCache;
   private final int version;
 
   @Inject
   OnlineReindexer(
       IndexCollection indexes,
-      ChangeBatchIndexer batchIndexer,
+      SiteIndexer batchIndexer,
       ProjectCache projectCache,
       @Assisted int version) {
     this.indexes = indexes;
@@ -76,8 +76,8 @@
         "not an active write schema version: %s", version);
     log.info("Starting online reindex from schema version {} to {}",
         version(indexes.getSearchIndex()), version(index));
-    ChangeBatchIndexer.Result result = batchIndexer.indexAll(
-        index, projectCache.all(), -1, -1, null, null);
+    SiteIndexer.Result result =
+        batchIndexer.indexAll(index, projectCache.all());
     if (!result.success()) {
       log.error("Online reindex of schema version {} failed", version(index));
       return;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index cca6700..361fe98 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -44,7 +44,6 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
-import com.google.gerrit.server.change.MergeabilityChecksExecutorModule;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -317,7 +316,6 @@
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
-    modules.add(new MergeabilityChecksExecutorModule());
     modules.add(new IntraLineWorkerPool.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new InternalAccountDirectory.Module());
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
index cf4fdbf..67f4cb2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -21,37 +21,23 @@
 import com.google.gerrit.common.Die;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lucene.LuceneIndexModule;
-import com.google.gerrit.pgm.util.BatchGitModule;
 import com.google.gerrit.pgm.util.BatchProgramModule;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.pgm.util.ThreadLimiter;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.MergeabilityChecker;
-import com.google.gerrit.server.change.MergeabilityChecksExecutor;
-import com.google.gerrit.server.change.MergeabilityChecksExecutor.Priority;
-import com.google.gerrit.server.change.PatchSetInserter;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.index.ChangeBatchIndexer;
 import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.IndexModule.IndexType;
-import com.google.gerrit.server.mail.ReplacePatchSetSender;
-import com.google.gerrit.server.notedb.NoteDbModule;
+import com.google.gerrit.server.index.SiteIndexer;
 import com.google.gerrit.solr.SolrIndexModule;
-import com.google.inject.AbstractModule;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
-import com.google.inject.Provides;
-import com.google.inject.Singleton;
-import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ProgressMonitor;
@@ -74,9 +60,6 @@
   @Option(name = "--output", usage = "Prefix for output; path for local disk index, or prefix for remote index")
   private String outputBase;
 
-  @Option(name = "--recheck-mergeable", usage = "Recheck mergeable flag on all changes")
-  private boolean recheckMergeable;
-
   @Option(name = "--verbose", usage = "Output debug information for each change")
   private boolean verbose;
 
@@ -144,18 +127,6 @@
     }
     modules.add(changeIndexModule);
     modules.add(dbInjector.getInstance(BatchProgramModule.class));
-    modules.add(new AbstractModule() {
-      @Override
-      protected void configure() {
-        if (recheckMergeable) {
-          install(new MergeabilityModule());
-        } else {
-          bind(MergeabilityChecker.class)
-              .toProvider(Providers.<MergeabilityChecker> of(null));
-        }
-      }
-    });
-
     return dbInjector.createChildInjector(modules);
   }
 
@@ -168,36 +139,6 @@
     }
   }
 
-  private static class MergeabilityModule extends FactoryModule {
-    @Override
-    public void configure() {
-      factory(PatchSetInserter.Factory.class);
-      bind(ReplacePatchSetSender.Factory.class).toProvider(
-          Providers.<ReplacePatchSetSender.Factory>of(null));
-
-      factory(MergeUtil.Factory.class);
-      install(new NoteDbModule());
-      install(new BatchGitModule());
-    }
-
-    @Provides
-    @Singleton
-    @MergeabilityChecksExecutor(Priority.BACKGROUND)
-    public WorkQueue.Executor createMergeabilityChecksExecutor(
-        WorkQueue queues) {
-      return queues.createQueue(1, "MergeabilityChecks");
-    }
-
-    @Provides
-    @Singleton
-    @MergeabilityChecksExecutor(Priority.INTERACTIVE)
-    public WorkQueue.Executor createInteractiveMergeabilityChecksExecutor(
-        @MergeabilityChecksExecutor(Priority.BACKGROUND)
-          WorkQueue.Executor bg) {
-      return bg;
-    }
-  }
-
   private int indexAll() throws Exception {
     ReviewDb db = sysInjector.getInstance(ReviewDb.class);
     ProgressMonitor pm = new TextProgressMonitor();
@@ -217,11 +158,12 @@
     }
     pm.endTask();
 
-    ChangeBatchIndexer batchIndexer =
-        sysInjector.getInstance(ChangeBatchIndexer.class);
-    ChangeBatchIndexer.Result result = batchIndexer.indexAll(
-      index, projects, projects.size(), changeCount, System.err,
-      verbose ? System.out : NullOutputStream.INSTANCE);
+    SiteIndexer batchIndexer =
+        sysInjector.getInstance(SiteIndexer.class);
+    SiteIndexer.Result result = batchIndexer.setNumChanges(changeCount)
+        .setProgressOut(System.err)
+        .setVerboseOut(verbose ? System.out : NullOutputStream.INSTANCE)
+        .indexAll(index, projects);
     int n = result.doneCount() + result.failedCount();
     double t = result.elapsed(TimeUnit.MILLISECONDS) / 1000d;
     System.out.format("Reindexed %d changes in %.01fs (%.01f/s)\n", n, t, n/t);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 8f9f1f4..91ffa91 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -30,14 +30,19 @@
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
+import com.google.gerrit.server.change.MergeabilityCache;
+import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.DisableReverseDnsLookup;
 import com.google.gerrit.server.config.DisableReverseDnsLookupProvider;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.git.ChangeCache;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.group.GroupModule;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.notedb.NoteDbModule;
 import com.google.gerrit.server.patch.PatchListCacheImpl;
 import com.google.gerrit.server.project.AccessControlModule;
 import com.google.gerrit.server.project.CommentLinkInfo;
@@ -87,10 +92,16 @@
         .toProvider(DisableReverseDnsLookupProvider.class).in(SINGLETON);
     bind(IdentifiedUser.class)
       .toProvider(Providers.<IdentifiedUser> of(null));
+    bind(ReplacePatchSetSender.Factory.class).toProvider(
+        Providers.<ReplacePatchSetSender.Factory>of(null));
     bind(CurrentUser.class).to(IdentifiedUser.class);
+    factory(MergeUtil.Factory.class);
+    factory(PatchSetInserter.Factory.class);
     install(new AccessControlModule());
+    install(new BatchGitModule());
     install(new DefaultCacheFactory.Module());
     install(new GroupModule());
+    install(new NoteDbModule());
     install(new PrologModule());
     install(AccountByEmailCacheImpl.module());
     install(AccountCacheImpl.module());
@@ -100,6 +111,7 @@
     install(SectionSortCache.module());
     install(ChangeKindCacheImpl.module());
     install(ChangeCache.module());
+    install(MergeabilityCache.module());
     install(TagCache.module());
     factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index b443c4d..575f4ac 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -51,6 +51,18 @@
       </includes>
     </fileSet>
 
+    <fileSet filtered="true">
+      <directory></directory>
+      <include>.buckconfig</include>
+      <include>BUCK</include>
+      <include>VERSION</include>
+      <include>lib/gerrit/BUCK</include>
+      <include>lib/gwt/BUCK</include>
+      <excludes>
+        <exclude>**/*.java</exclude>
+      </excludes>
+    </fileSet>
+
     <fileSet>
       <directory></directory>
       <includes>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.buckconfig b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.buckconfig
new file mode 100644
index 0000000..1044c12
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.buckconfig
@@ -0,0 +1,14 @@
+[alias]
+  ${pluginName} = //:${pluginName}
+  plugin = //:${pluginName}
+
+[java]
+  src_roots = java, resources
+
+[project]
+  ignore = .git
+
+[cache]
+  mode = dir
+  dir = buck-out/cache
+
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore
index 80d6257..43838b0 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore
@@ -1,3 +1,7 @@
+/.buckversion
+/.buckd
+/buck-out
+/bucklets
 /target
 /.classpath
 /.project
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK
new file mode 100644
index 0000000..b19312c
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK
@@ -0,0 +1,23 @@
+include_defs('//bucklets/gerrit_plugin.bucklet')
+
+gerrit_plugin(
+  name = '${pluginName}',
+  srcs = glob(['src/main/java/**/*.java']),
+  resources = glob(['src/main/**/*']),
+  gwt_module = '${package}.HelloPlugin',
+  manifest_entries = [
+    'Gerrit-PluginName: ${pluginName}',
+    'Gerrit-ApiType: plugin',
+    'Gerrit-ApiVersion: ${gerritApiVersion}',
+    'Gerrit-Module: ${package}.Module',
+    'Gerrit-SshModule: ${package}.SshModule',
+    'Gerrit-HttpModule: ${package}.HttpModule',
+  ],
+)
+
+# this is required for bucklets/tools/eclipse/project.py to work
+java_library(
+  name = 'classpath',
+  deps = [':${pluginName}__plugin'],
+)
+
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/VERSION b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/VERSION
new file mode 100644
index 0000000..8bbb460
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/VERSION
@@ -0,0 +1,5 @@
+# Used by BUCK to include "Implementation-Version" in plugin Manifest.
+# If this file doesn't exist the output of 'git describe' is used
+# instead.
+PLUGIN_VERSION = '${version}'
+
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK
new file mode 100644
index 0000000..0a0d8b9
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK
@@ -0,0 +1,20 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+VER = '${gerritApiVersion}'
+REPO = MAVEN_LOCAL
+
+maven_jar(
+  name = 'plugin-api',
+  id = 'com.google.gerrit:gerrit-plugin-api:' + VER,
+  attach_source = False,
+  repository = REPO,
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'gwtui-api',
+  id = 'com.google.gerrit:gerrit-plugin-gwtui:' + VER,
+  attach_source = False,
+  repository = REPO,
+  license = 'Apache2.0',
+)
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK
new file mode 100644
index 0000000..511a8ec
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK
@@ -0,0 +1,32 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+VERSION = '${Gwt-Version}'
+
+maven_jar(
+  name = 'user',
+  id = 'com.google.gwt:gwt-user:' + VERSION,
+  license = 'Apache2.0',
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'dev',
+  id = 'com.google.gwt:gwt-dev:' + VERSION,
+  license = 'Apache2.0',
+  deps = [
+    ':javax-validation',
+    ':javax-validation_src',
+  ],
+  attach_source = False,
+  exclude = ['org/eclipse/jetty/*'],
+)
+
+maven_jar(
+  name = 'javax-validation',
+  id = 'javax.validation:validation-api:1.0.0.GA',
+  bin_sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
+  src_sha1 = '7a561191db2203550fbfa40d534d4997624cd369',
+  license = 'Apache2.0',
+  visibility = [],
+)
+
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..4c56ed6
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
@@ -0,0 +1,77 @@
+Build
+=====
+
+This plugin can be built with Buck or Maven.
+
+Buck
+----
+
+Two build modes are supported: Standalone and in Gerrit tree.
+The standalone build mode is recommended, as this mode doesn't require
+the Gerrit tree to exist locally.
+
+
+
+Clone bucklets library:
+
+```
+  git clone https://gerrit.googlesource.com/bucklets
+
+```
+and link it to @PLUGIN@ plugin directory:
+
+```
+  cd @PLUGIN@ && ln -s ../bucklets .
+```
+
+Add link to the .buckversion file:
+
+```
+  cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion
+```
+
+To build the plugin, issue the following command:
+
+
+```
+  buck build plugin
+```
+
+The output is created in
+
+```
+  buck-out/gen/@PLUGIN@.jar
+```
+
+
+Clone or link this plugin to the plugins directory of Gerrit's source
+tree, and issue the command:
+
+```
+  buck build plugins/@PLUGIN@
+```
+
+The output is created in
+
+```
+  buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+This project can be imported into the Eclipse IDE:
+
+```
+  ./tools/eclipse/project.py
+```
+
+Maven
+-----
+
+Note that the Maven build is provided for compatibility reasons, but
+it is considered to be deprecated and will be removed in a future
+version of this plugin.
+
+To build with Maven, run
+
+```
+mvn clean package
+```
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 94d2f64..594f974 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -432,17 +432,6 @@
   @Column(id = 14, notNull = false)
   protected String topic;
 
-  /**
-   * Null if the change has never been tested.
-   * Empty if it has been tested but against a branch that does
-   * not exist.
-   */
-  @Column(id = 15, notNull = false)
-  protected RevId lastSha1MergeTested;
-
-  @Column(id = 16)
-  protected boolean mergeable;
-
   protected Change() {
   }
 
@@ -455,7 +444,6 @@
     owner = ownedBy;
     dest = forBranch;
     setStatus(Status.NEW);
-    setLastSha1MergeTested(null);
   }
 
   public Change(Change other) {
@@ -472,8 +460,6 @@
     currentPatchSetId = other.currentPatchSetId;
     subject = other.subject;
     topic = other.topic;
-    mergeable = other.mergeable;
-    lastSha1MergeTested = other.lastSha1MergeTested;
   }
 
   /** Legacy 32 bit integer identity for a change. */
@@ -564,20 +550,4 @@
   public void setTopic(String topic) {
     this.topic = topic;
   }
-
-  public RevId getLastSha1MergeTested() {
-    return lastSha1MergeTested;
-  }
-
-  public void setLastSha1MergeTested(RevId lastSha1MergeTested) {
-    this.lastSha1MergeTested = lastSha1MergeTested;
-  }
-
-  public boolean isMergeable() {
-    return mergeable;
-  }
-
-  public void setMergeable(boolean mergeable) {
-    this.mergeable = mergeable;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 4f19c8e..28e7a34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.auth.AuthException;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -68,7 +69,7 @@
   private final ChangeHooks hooks;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
-  private final MergeabilityChecker mergeabilityChecker;
+  private final ChangeIndexer indexer;
   private final CreateChangeSender.Factory createChangeSenderFactory;
   private final HashtagsUtil hashtagsUtil;
   private final AccountCache accountCache;
@@ -95,7 +96,7 @@
       ChangeHooks hooks,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
-      MergeabilityChecker mergeabilityChecker,
+      ChangeIndexer indexer,
       CreateChangeSender.Factory createChangeSenderFactory,
       HashtagsUtil hashtagsUtil,
       AccountCache accountCache,
@@ -108,7 +109,7 @@
     this.hooks = hooks;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.createChangeSenderFactory = createChangeSenderFactory;
     this.hashtagsUtil = hashtagsUtil;
     this.accountCache = accountCache;
@@ -221,10 +222,7 @@
       }
     }
 
-    CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
-        .addChange(change)
-        .reindex()
-        .runAsync();
+    CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
 
     if(!messageIsForChange()) {
       commitMessageNotForChange();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 66f1388..55f65d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -273,7 +273,10 @@
     out.topic = in.getTopic();
     out.hashtags = ctl.getNotes().load().getHashtags();
     out.changeId = in.getKey().get();
-    out.mergeable = isMergeable(in);
+    // TODO(dborowitz): This gets the submit type, so we could include that in
+    // the response and avoid making a request to /submit_type from the UI.
+    out.mergeable = in.getStatus() == Change.Status.MERGED
+        ? null : cd.isMergeable();
     ChangedLines changedLines = cd.changedLines();
     if (changedLines != null) {
       out.insertions = changedLines.insertions;
@@ -344,14 +347,6 @@
     return out;
   }
 
-  private Boolean isMergeable(Change c) {
-    if (c.getStatus() == Change.Status.MERGED
-        || c.getLastSha1MergeTested() == null) {
-      return null;
-    }
-    return c.isMergeable();
-  }
-
   private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException {
     if (cd.getSubmitRecords() != null) {
       return cd.getSubmitRecords();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
new file mode 100644
index 0000000..7589ea5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
@@ -0,0 +1,322 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// 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.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
+import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
+import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.inject.Inject;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+@Singleton
+public class MergeabilityCache {
+  private static final Logger log =
+      LoggerFactory.getLogger(MergeabilityCache.class);
+
+  private static final String CACHE_NAME = "mergeability";
+
+  public static final BiMap<SubmitType, Character> SUBMIT_TYPES = ImmutableBiMap.of(
+        SubmitType.FAST_FORWARD_ONLY, 'F',
+        SubmitType.MERGE_IF_NECESSARY, 'M',
+        SubmitType.REBASE_IF_NECESSARY, 'R',
+        SubmitType.MERGE_ALWAYS, 'A',
+        SubmitType.CHERRY_PICK, 'C');
+
+  static {
+    checkState(SUBMIT_TYPES.size() == SubmitType.values().length,
+        "SubmitType <-> char BiMap needs updating");
+  }
+
+  @SuppressWarnings("rawtypes")
+  public static Key bindingKey() {
+    return Key.get(new TypeLiteral<LoadingCache<EntryKey, Boolean>>() {},
+        Names.named(CACHE_NAME));
+  }
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        persist(CACHE_NAME, EntryKey.class, Boolean.class)
+            .maximumWeight(1 << 20)
+            .weigher(MergeabilityWeigher.class)
+            .loader(Loader.class);
+        bind(MergeabilityCache.class);
+      }
+    };
+  }
+
+  public static ObjectId toId(Ref ref) {
+    return ref != null && ref.getObjectId() != null
+        ? ref.getObjectId()
+        : ObjectId.zeroId();
+  }
+
+  public static class EntryKey implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private ObjectId commit;
+    private ObjectId into;
+    private SubmitType submitType;
+    private String mergeStrategy;
+
+    // Only used for loading, not stored.
+    private transient LoadHelper load;
+
+    public EntryKey(ObjectId commit, ObjectId into, SubmitType submitType,
+        String mergeStrategy) {
+      this.commit = checkNotNull(commit, "commit");
+      this.into = checkNotNull(into, "into");
+      this.submitType = checkNotNull(submitType, "submitType");
+      this.mergeStrategy = checkNotNull(mergeStrategy, "mergeStrategy");
+    }
+
+    private EntryKey(ObjectId commit, ObjectId into, SubmitType submitType,
+        String mergeStrategy, Branch.NameKey dest, Repository repo,
+        ReviewDb db) {
+      this(commit, into, submitType, mergeStrategy);
+      load = new LoadHelper(dest, repo, db);
+    }
+
+    public ObjectId getCommit() {
+      return commit;
+    }
+
+    public ObjectId getInto() {
+      return into;
+    }
+
+    public SubmitType getSubmitType() {
+      return submitType;
+    }
+
+    public String getMergeStrategy() {
+      return mergeStrategy;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o instanceof EntryKey) {
+        EntryKey k = (EntryKey) o;
+        return commit.equals(k.commit)
+            && into.equals(k.into)
+            && submitType == k.submitType
+            && mergeStrategy.equals(k.mergeStrategy);
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(commit, into, submitType, mergeStrategy);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("commit", commit.name())
+          .add("into", into.name())
+          .addValue(submitType)
+          .addValue(mergeStrategy)
+          .toString();
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+      writeNotNull(out, commit);
+      writeNotNull(out, into);
+      Character c = SUBMIT_TYPES.get(submitType);
+      if (c == null) {
+        throw new IOException("Invalid submit type: " + submitType);
+      }
+      out.writeChar(c);
+      writeString(out, mergeStrategy);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException {
+      commit = readNotNull(in);
+      into = readNotNull(in);
+      char t = in.readChar();
+      submitType = SUBMIT_TYPES.inverse().get(t);
+      if (submitType == null) {
+        throw new IOException("Invalid submit type code: " + t);
+      }
+      mergeStrategy = readString(in);
+    }
+  }
+
+  private static class LoadHelper {
+    private final Branch.NameKey dest;
+    private final Repository repo;
+    private final ReviewDb db;
+
+    private LoadHelper(Branch.NameKey dest, Repository repo, ReviewDb db) {
+      this.dest = checkNotNull(dest, "dest");
+      this.repo = checkNotNull(repo, "repo");
+      this.db = checkNotNull(db, "db");
+    }
+  }
+
+  @Singleton
+  public static class Loader extends CacheLoader<EntryKey, Boolean> {
+    private final SubmitStrategyFactory submitStrategyFactory;
+
+    @Inject
+    Loader(SubmitStrategyFactory submitStrategyFactory) {
+      this.submitStrategyFactory = submitStrategyFactory;
+    }
+
+    @Override
+    public Boolean load(EntryKey key)
+        throws NoSuchProjectException, MergeException, IOException {
+      checkArgument(key.load != null, "Key cannot be loaded: %s", key);
+      if (key.into.equals(ObjectId.zeroId())) {
+        return true; // Assume yes on new branch.
+      }
+      try {
+        Map<String, Ref> refs = key.load.repo.getAllRefs();
+        RevWalk rw = CodeReviewCommit.newRevWalk(key.load.repo);
+        try {
+          RevFlag canMerge = rw.newFlag("CAN_MERGE");
+          CodeReviewCommit rev = parse(rw, key.commit);
+          rev.add(canMerge);
+          CodeReviewCommit tip = parse(rw, key.into);
+          Set<RevCommit> accepted = alreadyAccepted(rw, refs.values());
+          accepted.add(tip);
+          accepted.addAll(Arrays.asList(rev.getParents()));
+          return submitStrategyFactory.create(
+              key.submitType,
+              key.load.db,
+              key.load.repo,
+              rw,
+              null /*inserter*/,
+              canMerge,
+              accepted,
+              key.load.dest).dryRun(tip, rev);
+        } finally {
+          rw.release();
+        }
+      } finally {
+        key.load = null;
+      }
+    }
+
+    private static Set<RevCommit> alreadyAccepted(RevWalk rw,
+        Collection<Ref> refs) throws MissingObjectException, IOException {
+      Set<RevCommit> accepted = Sets.newHashSet();
+      for (Ref r : refs) {
+        if (r.getName().startsWith(Constants.R_HEADS)
+            || r.getName().startsWith(Constants.R_TAGS)) {
+          try {
+            accepted.add(rw.parseCommit(r.getObjectId()));
+          } catch (IncorrectObjectTypeException nonCommit) {
+            // Not a commit? Skip over it.
+          }
+        }
+      }
+      return accepted;
+    }
+
+    private static CodeReviewCommit parse(RevWalk rw, ObjectId id)
+        throws MissingObjectException, IncorrectObjectTypeException,
+        IOException {
+      return (CodeReviewCommit) rw.parseCommit(id);
+    }
+  }
+
+  public static class MergeabilityWeigher
+      implements Weigher<EntryKey, Boolean> {
+    @Override
+    public int weigh(EntryKey k, Boolean v) {
+      return 16 + 2 * (16 + 20) + 3 * 8 // Size of EntryKey, 64-bit JVM.
+          + 8; // Size of Boolean.
+    }
+  }
+
+  private final LoadingCache<EntryKey, Boolean> cache;
+
+  @Inject
+  MergeabilityCache(@Named(CACHE_NAME) LoadingCache<EntryKey, Boolean> cache) {
+    this.cache = cache;
+  }
+
+  public boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
+      String mergeStrategy, Branch.NameKey dest, Repository repo, ReviewDb db) {
+    ObjectId into = intoRef != null ? intoRef.getObjectId() : ObjectId.zeroId();
+    EntryKey key =
+        new EntryKey(commit, into, submitType, mergeStrategy, dest, repo, db);
+    try {
+      return cache.get(key);
+    } catch (ExecutionException e) {
+      log.error(String.format("Error checking mergeability of %s into %s (%s)",
+            key.commit.name(), key.into.name(), key.submitType.name()),
+          e.getCause());
+      return false;
+    }
+  }
+
+  public boolean getIfPresent(ObjectId commit, Ref intoRef,
+      SubmitType submitType, String mergeStrategy) {
+    return cache.getIfPresent(new EntryKey(
+        commit, toId(intoRef), submitType, mergeStrategy, null, null, null));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCheckQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCheckQueue.java
deleted file mode 100644
index ae44ea2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCheckQueue.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// 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.gerrit.server.change;
-
-import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.inject.Singleton;
-
-import java.util.Collection;
-import java.util.Set;
-
-@Singleton
-class MergeabilityCheckQueue {
-  private final Set<Change.Id> pending = Sets.newHashSet();
-  private final Set<Change.Id> forcePending = Sets.newHashSet();
-
-  synchronized Set<Change> addAll(Collection<Change> changes, boolean force) {
-    Set<Change> r = Sets.newLinkedHashSetWithExpectedSize(changes.size());
-    for (Change c : changes) {
-      if (force ? forcePending.add(c.getId()) : pending.add(c.getId())) {
-        r.add(c);
-      }
-    }
-    return r;
-  }
-
-  synchronized void updatingMergeabilityFlag(Change change, boolean force) {
-    if (force) {
-      forcePending.remove(change.getId());
-    }
-    pending.remove(change.getId());
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
deleted file mode 100644
index 270f718..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
+++ /dev/null
@@ -1,366 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// 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.gerrit.server.change;
-
-import com.google.common.base.Function;
-import com.google.common.base.Throwables;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.AsyncFunction;
-import com.google.common.util.concurrent.CheckedFuture;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.MergeabilityChecksExecutor.Priority;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.WorkQueue.Executor;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-
-public class MergeabilityChecker implements GitReferenceUpdatedListener {
-  private static final Logger log = LoggerFactory
-      .getLogger(MergeabilityChecker.class);
-
-  private static final Function<Exception, IOException> MAPPER =
-      new Function<Exception, IOException>() {
-    @Override
-    public IOException apply(Exception in) {
-      if (in instanceof IOException) {
-        return (IOException) in;
-      } else if (in instanceof ExecutionException
-          && in.getCause() instanceof IOException) {
-        return (IOException) in.getCause();
-      } else {
-        return new IOException(in);
-      }
-    }
-  };
-
-  public class Check {
-    private List<Change> changes;
-    private List<Branch.NameKey> branches;
-    private List<Project.NameKey> projects;
-    private boolean force;
-    private boolean reindex;
-    private boolean interactive;
-
-    private Check() {
-      changes = Lists.newArrayListWithExpectedSize(1);
-      branches = Lists.newArrayListWithExpectedSize(1);
-      projects = Lists.newArrayListWithExpectedSize(1);
-      interactive = true;
-    }
-
-    public Check addChange(Change change) {
-      changes.add(change);
-      return this;
-    }
-
-    public Check addBranch(Branch.NameKey branch) {
-      branches.add(branch);
-      interactive = false;
-      return this;
-    }
-
-    public Check addProject(Project.NameKey project) {
-      projects.add(project);
-      interactive = false;
-      return this;
-    }
-
-    /** Force reindexing regardless of whether mergeable flag was modified. */
-    public Check reindex() {
-      reindex = true;
-      return this;
-    }
-
-    /** Force mergeability check even if change is not stale. */
-    private Check force() {
-      force = true;
-      return this;
-    }
-
-    private ListeningExecutorService getExecutor() {
-      return interactive ? interactiveExecutor : backgroundExecutor;
-    }
-
-    public CheckedFuture<?, IOException> runAsync() {
-      final ListeningExecutorService executor = getExecutor();
-      ListenableFuture<List<Change>> getChanges;
-      if (branches.isEmpty() && projects.isEmpty()) {
-        getChanges = Futures.immediateFuture(changes);
-      } else {
-        getChanges = executor.submit(
-            new Callable<List<Change>>() {
-              @Override
-              public List<Change> call() throws OrmException {
-                return getChanges();
-              }
-            });
-      }
-
-      return Futures.makeChecked(Futures.transform(getChanges,
-          new AsyncFunction<List<Change>, List<Object>>() {
-            @Override
-            public ListenableFuture<List<Object>> apply(List<Change> changes) {
-              List<ListenableFuture<?>> result =
-                  Lists.newArrayListWithCapacity(changes.size());
-              for (final Change c : changes) {
-                // Don't try to guess whether Mergeable will reindex; just turn
-                // off reindexing in that code path and do it explicitly below.
-                ListenableFuture<Void> b =
-                    executor.submit(new Task(c, force, !reindex));
-                if (reindex) {
-                  result.add(Futures.transform(
-                      b, new AsyncFunction<Void, Void>() {
-                        @SuppressWarnings("unchecked")
-                        @Override
-                        public ListenableFuture<Void> apply(Void o) {
-                          return (ListenableFuture<Void>)
-                              indexer.indexAsync(c.getId());
-                        }
-                      }));
-                } else {
-                  result.add(b);
-                }
-              }
-              return Futures.allAsList(result);
-            }
-          }), MAPPER);
-    }
-
-    public void run() throws IOException {
-      try {
-        runAsync().checkedGet();
-      } catch (Exception e) {
-        Throwables.propagateIfPossible(e, IOException.class);
-        throw MAPPER.apply(e);
-      }
-    }
-
-    private List<Change> getChanges() throws OrmException {
-      ReviewDb db = schemaFactory.open();
-      try {
-        List<Change> results = Lists.newArrayList();
-        results.addAll(changes);
-        for (Project.NameKey p : projects) {
-          Iterables.addAll(results, db.changes().byProjectOpenAll(p));
-        }
-        for (Branch.NameKey b : branches) {
-          Iterables.addAll(results, db.changes().byBranchOpenAll(b));
-        }
-        return results;
-      } catch (OrmException e) {
-        log.error("Failed to fetch changes for mergeability check", e);
-        throw e;
-      } finally {
-        db.close();
-      }
-    }
-  }
-
-  private final ThreadLocalRequestContext tl;
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final ChangeControl.GenericFactory changeControlFactory;
-  private final Provider<Mergeable> mergeable;
-  private final ChangeIndexer indexer;
-  private final ListeningExecutorService backgroundExecutor;
-  private final ListeningExecutorService interactiveExecutor;
-  private final MergeabilityCheckQueue mergeabilityCheckQueue;
-  private final MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  public MergeabilityChecker(ThreadLocalRequestContext tl,
-      SchemaFactory<ReviewDb> schemaFactory,
-      IdentifiedUser.GenericFactory identifiedUserFactory,
-      ChangeControl.GenericFactory changeControlFactory,
-      Provider<Mergeable> mergeable, ChangeIndexer indexer,
-      @MergeabilityChecksExecutor(Priority.BACKGROUND)
-        Executor backgroundExecutor,
-      @MergeabilityChecksExecutor(Priority.INTERACTIVE)
-        Executor interactiveExecutor,
-      MergeabilityCheckQueue mergeabilityCheckQueue,
-      MetaDataUpdate.Server metaDataUpdateFactory) {
-    this.tl = tl;
-    this.schemaFactory = schemaFactory;
-    this.identifiedUserFactory = identifiedUserFactory;
-    this.changeControlFactory = changeControlFactory;
-    this.mergeable = mergeable;
-    this.indexer = indexer;
-    this.backgroundExecutor =
-        MoreExecutors.listeningDecorator(backgroundExecutor);
-    this.interactiveExecutor =
-        MoreExecutors.listeningDecorator(interactiveExecutor);
-    this.mergeabilityCheckQueue = mergeabilityCheckQueue;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-  }
-
-  public Check newCheck() {
-    return new Check();
-  }
-
-  @Override
-  public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
-    String ref = event.getRefName();
-    if (ref.startsWith(Constants.R_HEADS) || ref.equals(RefNames.REFS_CONFIG)) {
-      Branch.NameKey branch = new Branch.NameKey(
-          new Project.NameKey(event.getProjectName()), ref);
-      newCheck().addBranch(branch).runAsync();
-    }
-    if (ref.equals(RefNames.REFS_CONFIG)) {
-      Project.NameKey p = new Project.NameKey(event.getProjectName());
-      try {
-        ProjectConfig oldCfg = parseConfig(p, event.getOldObjectId());
-        ProjectConfig newCfg = parseConfig(p, event.getNewObjectId());
-        if (recheckMerges(oldCfg, newCfg)) {
-          newCheck().addProject(p).force().runAsync();
-        }
-      } catch (ConfigInvalidException | IOException e) {
-        String msg = "Failed to update mergeability flags for project " + p.get()
-            + " on update of " + RefNames.REFS_CONFIG;
-        log.error(msg, e);
-        throw new RuntimeException(msg, e);
-      }
-    }
-  }
-
-  private boolean recheckMerges(ProjectConfig oldCfg, ProjectConfig newCfg) {
-    if (oldCfg == null || newCfg == null) {
-      return true;
-    }
-    return !oldCfg.getProject().getSubmitType().equals(newCfg.getProject().getSubmitType())
-        || oldCfg.getProject().getUseContentMerge() != newCfg.getProject().getUseContentMerge()
-        || (oldCfg.getRulesId() == null
-            ? newCfg.getRulesId() != null
-            : !oldCfg.getRulesId().equals(newCfg.getRulesId()));
-  }
-
-  private ProjectConfig parseConfig(Project.NameKey p, String idStr)
-      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
-    ObjectId id = ObjectId.fromString(idStr);
-    if (ObjectId.zeroId().equals(id)) {
-      return null;
-    }
-    return ProjectConfig.read(metaDataUpdateFactory.create(p), id);
-  }
-
-  private class Task implements Callable<Void> {
-    private final Change change;
-    private final boolean force;
-    private final boolean reindex;
-
-    private ReviewDb reviewDb;
-
-    Task(Change change, boolean force, boolean reindex) {
-      this.change = change;
-      this.force = force;
-      this.reindex = reindex;
-    }
-
-    @Override
-    public Void call() throws Exception {
-      mergeabilityCheckQueue.updatingMergeabilityFlag(change, force);
-
-      RequestContext context = new RequestContext() {
-        @Override
-        public CurrentUser getCurrentUser() {
-          return identifiedUserFactory.create(change.getOwner());
-        }
-
-        @Override
-        public Provider<ReviewDb> getReviewDbProvider() {
-          return new Provider<ReviewDb>() {
-            @Override
-            public ReviewDb get() {
-              if (reviewDb == null) {
-                try {
-                  reviewDb = schemaFactory.open();
-                } catch (OrmException e) {
-                  throw new ProvisionException("Cannot open ReviewDb", e);
-                }
-              }
-              return reviewDb;
-            }
-          };
-        }
-      };
-      RequestContext old = tl.setContext(context);
-      ReviewDb db = context.getReviewDbProvider().get();
-      try {
-        PatchSet ps = db.patchSets().get(change.currentPatchSetId());
-        if (ps == null) {
-          // Cannot compute mergeability if current patch set is missing.
-          return null;
-        }
-
-        Mergeable m = mergeable.get();
-        m.setForce(force);
-        m.setReindex(reindex);
-
-        ChangeControl control =
-            changeControlFactory.controlFor(change, context.getCurrentUser());
-        m.apply(new RevisionResource(new ChangeResource(control), ps));
-        return null;
-      } catch (ResourceConflictException e) {
-        // change is closed
-        return null;
-      } catch (Exception e) {
-        log.error(String.format(
-            "cannot update mergeability flag of change %d in project %s after update of %s",
-            change.getId().get(),
-            change.getDest().getParentKey(), change.getDest().get()), e);
-        throw e;
-      } finally {
-        tl.setContext(old);
-        if (reviewDb != null) {
-          reviewDb.close();
-          reviewDb = null;
-        }
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutor.java
deleted file mode 100644
index 632e6ac..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutor.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// 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.gerrit.server.change;
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import com.google.gerrit.server.git.WorkQueue.Executor;
-import com.google.inject.BindingAnnotation;
-
-import java.lang.annotation.Retention;
-
-/**
- * Marker on the global {@link Executor} used by
- * {@link MergeabilityChecker}.
- */
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface MergeabilityChecksExecutor {
-  public enum Priority {
-    BACKGROUND, INTERACTIVE;
-  }
-
-  Priority value();
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutorModule.java
deleted file mode 100644
index e5bcabe..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutorModule.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// 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.gerrit.server.change;
-
-import com.google.gerrit.server.change.MergeabilityChecksExecutor.Priority;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.inject.AbstractModule;
-import com.google.inject.Provides;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.Config;
-
-/** Module providing the {@link MergeabilityChecksExecutor}. */
-public class MergeabilityChecksExecutorModule extends AbstractModule {
-  @Override
-  protected void configure() {
-  }
-
-  @Provides
-  @Singleton
-  @MergeabilityChecksExecutor(Priority.BACKGROUND)
-  public WorkQueue.Executor createMergeabilityChecksExecutor(
-      @GerritServerConfig Config config,
-      WorkQueue queues) {
-    int poolSize = config.getInt("changeMerge", null, "threadPoolSize", 1);
-    return queues.createQueue(poolSize, "MergeabilityChecks-Background");
-  }
-
-  @Provides
-  @Singleton
-  @MergeabilityChecksExecutor(Priority.INTERACTIVE)
-  public WorkQueue.Executor createMergeabilityChecksExecutor(
-      @GerritServerConfig Config config,
-      WorkQueue queues,
-      @MergeabilityChecksExecutor(Priority.BACKGROUND)
-        WorkQueue.Executor backgroundExecutor) {
-    int poolSize =
-        config.getInt("changeMerge", null, "interactiveThreadPoolSize", 1);
-    if (poolSize <= 0) {
-      return backgroundExecutor;
-    }
-    return queues.createQueue(poolSize, "MergeabilityChecks-Interactive");
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index e5800e3..ca83a20 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -23,45 +22,32 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.git.BranchOrderSection;
-import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeException;
-import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.Objects;
 
 public class Mergeable implements RestReadView<RevisionResource> {
   private static final Logger log = LoggerFactory.getLogger(Mergeable.class);
@@ -82,34 +68,31 @@
     this.force = force;
   }
 
-  public void setReindex(boolean reindex) {
-    this.reindex = reindex;
-  }
-
   private final GitRepositoryManager gitManager;
   private final ProjectCache projectCache;
+  private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeData.Factory changeDataFactory;
-  private final SubmitStrategyFactory submitStrategyFactory;
   private final Provider<ReviewDb> db;
   private final ChangeIndexer indexer;
+  private final MergeabilityCache cache;
 
   private boolean force;
-  private boolean reindex;
 
   @Inject
   Mergeable(GitRepositoryManager gitManager,
       ProjectCache projectCache,
+      MergeUtil.Factory mergeUtilFactory,
       ChangeData.Factory changeDataFactory,
-      SubmitStrategyFactory submitStrategyFactory,
       Provider<ReviewDb> db,
-      ChangeIndexer indexer) {
+      ChangeIndexer indexer,
+      MergeabilityCache cache) {
     this.gitManager = gitManager;
     this.projectCache = projectCache;
+    this.mergeUtilFactory = mergeUtilFactory;
     this.changeDataFactory = changeDataFactory;
-    this.submitStrategyFactory = submitStrategyFactory;
     this.db = db;
     this.indexer = indexer;
-    reindex = true;
+    this.cache = cache;
   }
 
   @Override
@@ -134,29 +117,39 @@
       throw new OrmException("Submit type rule failed: " + rec);
     }
     result.submitType = rec.type;
-    result.mergeable = change.isMergeable();
 
     Repository git = gitManager.openRepository(change.getProject());
     try {
-      Map<String, Ref> refs = git.getRefDatabase().getRefs(RefDatabase.ALL);
-      Ref ref = refs.get(change.getDest().get());
-      if (force || isStale(change, ref)) {
-        result.mergeable =
-            refresh(change, ps, result.submitType, git, refs, ref);
+      ObjectId commit = toId(ps);
+      if (commit == null) {
+        result.mergeable = false;
+        return result;
+      }
+
+      Ref ref = git.getRef(change.getDest().get());
+      ProjectState projectState = projectCache.get(change.getProject());
+      String strategy = mergeUtilFactory.create(projectState)
+          .mergeStrategyName();
+      Boolean old =
+          cache.getIfPresent(commit, ref, result.submitType, strategy);
+
+      if (force || old == null) {
+        result.mergeable = refresh(change, commit, ref, result.submitType,
+            strategy, git, old);
       }
 
       if (otherBranches) {
         result.mergeableInto = new ArrayList<>();
-        BranchOrderSection branchOrder =
-            projectCache.get(change.getProject()).getBranchOrderSection();
+        BranchOrderSection branchOrder = projectState.getBranchOrderSection();
         if (branchOrder != null) {
           int prefixLen = Constants.R_HEADS.length();
           for (String n : branchOrder.getMoreStable(ref.getName())) {
-            Ref other = refs.get(n);
+            Ref other = git.getRef(n);
             if (other == null) {
               continue;
             }
-            if (isMergeable(change, ps, SubmitType.CHERRY_PICK, git, refs, other)) {
+            if (cache.get(commit, other, SubmitType.CHERRY_PICK, strategy,
+                change.getDest(), git, db.get())) {
               result.mergeableInto.add(other.getName().substring(prefixLen));
             }
           }
@@ -168,121 +161,25 @@
     return result;
   }
 
-  private static boolean isStale(Change change, Ref ref) {
-    return change.getLastSha1MergeTested() == null
-        || !toRevId(ref).equals(change.getLastSha1MergeTested());
+  private static ObjectId toId(PatchSet ps) {
+    try {
+      return ObjectId.fromString(ps.getRevision().get());
+    } catch (IllegalArgumentException e) {
+      log.error("Invalid revision on patch set " + ps);
+      return null;
+    }
   }
 
-  private static RevId toRevId(Ref ref) {
-    return new RevId(ref != null && ref.getObjectId() != null
-        ? ref.getObjectId().name()
-        : "");
-  }
-
-  private boolean refresh(Change change,
-      final PatchSet ps,
-      SubmitType type,
-      Repository git,
-      Map<String, Ref> refs,
-      final Ref ref) throws IOException, OrmException {
-
-    final boolean mergeable = isMergeable(change, ps, type, git, refs, ref);
-
-    Change c = db.get().changes().atomicUpdate(
-        change.getId(),
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change c) {
-            if (c.getStatus().isOpen()
-                && ps.getId().equals(c.currentPatchSetId())) {
-              c.setMergeable(mergeable);
-              c.setLastSha1MergeTested(toRevId(ref));
-              return c;
-            } else {
-              return null;
-            }
-          }
-        });
-    if (reindex && c != null) {
-      indexer.index(db.get(), c);
+  private boolean refresh(final Change change, ObjectId commit,
+      final Ref ref, SubmitType type, String strategy, Repository git,
+      Boolean old) throws OrmException, IOException {
+    final boolean mergeable =
+        cache.get(commit, ref, type, strategy, change.getDest(), git, db.get());
+    if (!Objects.equals(mergeable, old)) {
+      // TODO(dborowitz): Include cache info in ETag somehow instead.
+      ChangeUtil.bumpRowVersionNotLastUpdatedOn(change.getId(), db.get());
+      indexer.index(db.get(), change);
     }
     return mergeable;
   }
-
-  private boolean isMergeable(Change change,
-      final PatchSet ps,
-      SubmitType type,
-      Repository git,
-      Map<String, Ref> refs,
-      final Ref ref) {
-    RevWalk rw = new RevWalk(git) {
-      @Override
-      protected CodeReviewCommit createCommit(AnyObjectId id) {
-        return new CodeReviewCommit(id);
-      }
-    };
-    try {
-      ObjectId id;
-      try {
-        id = ObjectId.fromString(ps.getRevision().get());
-      } catch (IllegalArgumentException e) {
-        log.error(String.format(
-            "Invalid revision on patch set %d of %d",
-            ps.getId().get(),
-            change.getId().get()));
-        return false;
-      }
-
-      RevFlag canMerge = rw.newFlag("CAN_MERGE");
-      CodeReviewCommit rev = parse(rw, id);
-      rev.add(canMerge);
-
-      final boolean mergeable;
-      if (ref == null || ref.getObjectId() == null) {
-        mergeable = true; // Assume yes on new branch.
-      } else {
-        CodeReviewCommit tip = parse(rw, ref.getObjectId());
-        Set<RevCommit> accepted = alreadyAccepted(rw, refs.values());
-        accepted.add(tip);
-        accepted.addAll(Arrays.asList(rev.getParents()));
-        mergeable = submitStrategyFactory.create(
-            type,
-            db.get(),
-            git,
-            rw,
-            null /*inserter*/,
-            canMerge,
-            accepted,
-            change.getDest()).dryRun(tip, rev);
-      }
-      return mergeable;
-    } catch (MergeException | IOException | NoSuchProjectException e) {
-      log.error(String.format(
-          "Cannot merge test change %d", change.getId().get()), e);
-      return false;
-    } finally {
-      rw.release();
-    }
-  }
-
-  private static Set<RevCommit> alreadyAccepted(RevWalk rw, Collection<Ref> refs)
-      throws MissingObjectException, IOException {
-    Set<RevCommit> accepted = Sets.newHashSet();
-    for (Ref r : refs) {
-      if (r.getName().startsWith(Constants.R_HEADS)
-          || r.getName().startsWith(Constants.R_TAGS)) {
-        try {
-          accepted.add(rw.parseCommit(r.getObjectId()));
-        } catch (IncorrectObjectTypeException nonCommit) {
-          // Not a commit? Skip over it.
-        }
-      }
-    }
-    return accepted;
-  }
-
-  private static CodeReviewCommit parse(RevWalk rw, ObjectId id)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException {
-    return (CodeReviewCommit) rw.parseCommit(id);
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 7322f30..bc2a46e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.ReviewerState;
@@ -89,7 +90,7 @@
   private final ChangeControl.GenericFactory ctlFactory;
   private final GitReferenceUpdated gitRefUpdated;
   private final CommitValidators.Factory commitValidatorsFactory;
-  private final MergeabilityChecker mergeabilityChecker;
+  private final ChangeIndexer indexer;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ApprovalCopier approvalCopier;
@@ -122,7 +123,7 @@
       PatchSetInfoFactory patchSetInfoFactory,
       GitReferenceUpdated gitRefUpdated,
       CommitValidators.Factory commitValidatorsFactory,
-      MergeabilityChecker mergeabilityChecker,
+      ChangeIndexer indexer,
       ReplacePatchSetSender.Factory replacePatchSetFactory,
       @Assisted Repository git,
       @Assisted RevWalk revWalk,
@@ -141,7 +142,7 @@
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.commitValidatorsFactory = commitValidatorsFactory;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.replacePatchSetFactory = replacePatchSetFactory;
 
     this.git = git;
@@ -268,7 +269,6 @@
               if (change.getStatus() != Change.Status.DRAFT) {
                 change.setStatus(Change.Status.NEW);
               }
-              change.setLastSha1MergeTested(null);
               change.setCurrentPatchSet(patchSetInfoFactory.get(commit,
                   patchSet.getId()));
               ChangeUtil.updated(change);
@@ -316,10 +316,7 @@
     } finally {
       db.rollback();
     }
-    mergeabilityChecker.newCheck()
-        .addChange(updatedChange)
-        .reindex()
-        .run();
+    indexer.index(db, c);
     if (runHooks) {
       hooks.doPatchsetCreatedHook(updatedChange, patchSet, db);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index 88e2137..1e0fb2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.mail.RestoredSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -54,7 +55,7 @@
   private final RestoredSender.Factory restoredSenderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson json;
-  private final MergeabilityChecker mergeabilityChecker;
+  private final ChangeIndexer indexer;
   private final ChangeMessagesUtil cmUtil;
   private final ChangeUpdate.Factory updateFactory;
 
@@ -63,14 +64,14 @@
       RestoredSender.Factory restoredSenderFactory,
       Provider<ReviewDb> dbProvider,
       ChangeJson json,
-      MergeabilityChecker mergeabilityChecker,
+      ChangeIndexer indexer,
       ChangeMessagesUtil cmUtil,
       ChangeUpdate.Factory updateFactory) {
     this.hooks = hooks;
     this.restoredSenderFactory = restoredSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.cmUtil = cmUtil;
     this.updateFactory = updateFactory;
   }
@@ -121,10 +122,7 @@
     }
     update.commit();
 
-    CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
-        .addChange(change)
-        .reindex()
-        .runAsync();
+    CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
 
     try {
       ReplyToChangeSender cm = restoredSenderFactory.create(change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 8e06229..8d6cef2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -71,7 +71,7 @@
 import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
-import com.google.gerrit.server.change.MergeabilityChecker;
+import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.ChangeCache;
@@ -94,6 +94,7 @@
 import com.google.gerrit.server.git.validators.UploadValidationListener;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.group.GroupModule;
+import com.google.gerrit.server.index.ReindexAfterUpdate;
 import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.EmailModule;
@@ -166,6 +167,7 @@
     install(ConflictsCacheImpl.module());
     install(GroupCacheImpl.module());
     install(GroupIncludeCacheImpl.module());
+    install(MergeabilityCache.module());
     install(PatchListCacheImpl.module());
     install(ProjectCacheImpl.module());
     install(SectionSortCache.module());
@@ -266,7 +268,7 @@
     DynamicSet.setOf(binder(), HeadUpdatedListener.class);
     DynamicSet.setOf(binder(), UsageDataPublishedListener.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ChangeCache.class);
-    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(MergeabilityChecker.class);
+    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReindexAfterUpdate.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
         .to(ProjectConfigEntry.UpdateChecker.class);
     DynamicSet.setOf(binder(), ChangeListener.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 9e48e81..848d460 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -96,5 +96,11 @@
     public String getNewObjectId() {
       return newObjectId;
     }
+
+    @Override
+    public String toString() {
+      return String.format("%s[%s,%s: %s -> %s]", getClass().getSimpleName(),
+          projectName, ref, oldObjectId, newObjectId);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 1c49bc6..7d603b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -966,10 +966,6 @@
       @Override
       public Change update(Change c) {
         c.setStatus(Change.Status.MERGED);
-        // It could be possible that the change being merged
-        // has never had its mergeability tested. So we insure
-        // merged changes has mergeable field true.
-        c.setMergeable(true);
         if (!merged.equals(c.currentPatchSetId())) {
           // Uncool; the patch set changed after we merged it.
           // Go back to the patch set that was actually merged.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 5824c6f..9eb9945 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -634,8 +634,11 @@
 
   public ThreeWayMerger newThreeWayMerger(final Repository repo,
       final ObjectInserter inserter) {
-    return newThreeWayMerger(repo, inserter,
-        mergeStrategyName(useContentMerge, useRecursiveMerge));
+    return newThreeWayMerger(repo, inserter, mergeStrategyName());
+  }
+
+  public String mergeStrategyName() {
+    return mergeStrategyName(useContentMerge, useRecursiveMerge);
   }
 
   public static String mergeStrategyName(boolean useContentMerge,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 49080f7..b7c7679 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -82,7 +82,6 @@
 import com.google.gerrit.server.change.ChangeKind;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.ChangesCollection;
-import com.google.gerrit.server.change.MergeabilityChecker;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -300,7 +299,6 @@
   private final ListeningExecutorService changeUpdateExector;
   private final RequestScopePropagator requestScopePropagator;
   private final ChangeIndexer indexer;
-  private final MergeabilityChecker mergeabilityChecker;
   private final SshInfo sshInfo;
   private final AllProjectsName allProjectsName;
   private final ReceiveConfig receiveConfig;
@@ -370,7 +368,6 @@
       @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
       final RequestScopePropagator requestScopePropagator,
       final ChangeIndexer indexer,
-      final MergeabilityChecker mergeabilityChecker,
       final SshInfo sshInfo,
       final AllProjectsName allProjectsName,
       ReceiveConfig config,
@@ -410,7 +407,6 @@
     this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
     this.indexer = indexer;
-    this.mergeabilityChecker = mergeabilityChecker;
     this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
     this.receiveConfig = config;
@@ -2122,7 +2118,6 @@
                   } else {
                     change.setStatus(Change.Status.NEW);
                   }
-                  change.setLastSha1MergeTested(null);
                   change.setCurrentPatchSet(info);
 
                   final List<String> idList = newCommit.getFooterLines(CHANGE_ID);
@@ -2159,10 +2154,7 @@
       if (cmd.getResult() == NOT_ATTEMPTED) {
         cmd.execute(rp);
       }
-      CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
-          .addChange(change)
-          .reindex()
-          .runAsync();
+      CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
       workQueue.getDefaultQueue()
           .submit(requestScopePropagator.wrap(new Runnable() {
         @Override
@@ -2393,6 +2385,9 @@
               closeChange(cmd, PatchSet.Id.fromRef(ref.getName()), c);
           closeProgress.update(1);
           if (closedChange != null) {
+            if (byKey == null) {
+              byKey = openChangesByKey(branch);
+            }
             byKey.remove(closedChange);
           }
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 01db36f..2e2959c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -441,13 +441,25 @@
       };
 
   /** Whether the change is mergeable. */
-  public static final FieldDef<ChangeData, String> MERGEABLE =
+  @Deprecated
+  public static final FieldDef<ChangeData, String> LEGACY_MERGEABLE =
       new FieldDef.Single<ChangeData, String>(
           ChangeQueryBuilder.FIELD_MERGEABLE, FieldType.EXACT, false) {
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.change().isMergeable() ? "1" : null;
+          return input.isMergeable() ? "1" : null;
+        }
+      };
+
+  /** Whether the change is mergeable. */
+  public static final FieldDef<ChangeData, String> MERGEABLE =
+      new FieldDef.Single<ChangeData, String>(
+          "mergeable2", FieldType.EXACT, true) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.isMergeable() ? "1" : "0";
         }
       };
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
index 437f559..e235379 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -55,8 +55,9 @@
       LoggerFactory.getLogger(ChangeIndexer.class);
 
   public interface Factory {
-    ChangeIndexer create(ChangeIndex index);
-    ChangeIndexer create(IndexCollection indexes);
+    ChangeIndexer create(ListeningExecutorService executor, ChangeIndex index);
+    ChangeIndexer create(ListeningExecutorService executor,
+        IndexCollection indexes);
   }
 
   private static final Function<Exception, IOException> MAPPER =
@@ -82,10 +83,10 @@
   private final ListeningExecutorService executor;
 
   @AssistedInject
-  ChangeIndexer(@IndexExecutor ListeningExecutorService executor,
-      SchemaFactory<ReviewDb> schemaFactory,
+  ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
+      @Assisted ListeningExecutorService executor,
       @Assisted ChangeIndex index) {
     this.executor = executor;
     this.schemaFactory = schemaFactory;
@@ -96,10 +97,10 @@
   }
 
   @AssistedInject
-  ChangeIndexer(@IndexExecutor ListeningExecutorService executor,
-      SchemaFactory<ReviewDb> schemaFactory,
+  ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
+      @Assisted ListeningExecutorService executor,
       @Assisted IndexCollection indexes) {
     this.executor = executor;
     this.schemaFactory = schemaFactory;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index 031e741..4b7850a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -118,7 +118,7 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
+        ChangeField.LEGACY_MERGEABLE);
 
   // For upgrade to Lucene 4.6.0 index format only.
   static final Schema<ChangeData> V6 = release(V5.getFields().values());
@@ -145,7 +145,7 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
+        ChangeField.LEGACY_MERGEABLE);
 
   @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V8 = release(
@@ -168,7 +168,7 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
+        ChangeField.LEGACY_MERGEABLE);
 
   @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V9 = release(
@@ -192,8 +192,9 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
+        ChangeField.LEGACY_MERGEABLE);
 
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V10 = release(
         ChangeField.LEGACY_ID,
         ChangeField.ID,
@@ -215,8 +216,9 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
+        ChangeField.LEGACY_MERGEABLE);
 
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V11 = release(
         ChangeField.LEGACY_ID,
         ChangeField.ID,
@@ -238,7 +240,7 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE,
+        ChangeField.LEGACY_MERGEABLE,
         ChangeField.ADDED,
         ChangeField.DELETED,
         ChangeField.DELTA);
@@ -246,6 +248,7 @@
   // For upgrade to Lucene 4.10.0 index format only.
   static final Schema<ChangeData> V12 = release(V11.getFields().values());
 
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V13 = release(
       ChangeField.LEGACY_ID,
       ChangeField.ID,
@@ -267,6 +270,33 @@
       ChangeField.COMMENT,
       ChangeField.CHANGE,
       ChangeField.APPROVAL,
+      ChangeField.LEGACY_MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG);
+
+  static final Schema<ChangeData> V14 = release(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.REVIEWED,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
       ChangeField.MERGEABLE,
       ChangeField.ADDED,
       ChangeField.DELETED,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java
index 0a96d1d..eb97fdc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java
@@ -17,6 +17,7 @@
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.server.git.QueueProvider.QueueType;
 import com.google.inject.BindingAnnotation;
 
 import java.lang.annotation.Retention;
@@ -28,4 +29,5 @@
 @Retention(RUNTIME)
 @BindingAnnotation
 public @interface IndexExecutor {
+  QueueType value();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index 3aeeef2..41df287 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.index;
 
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
+
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -21,7 +24,6 @@
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.query.change.BasicChangeRewrites;
 import com.google.gerrit.server.query.change.ChangeQueryRewriter;
-import com.google.inject.AbstractModule;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Provides;
@@ -48,16 +50,20 @@
   }
 
   private final int threads;
-  private final ListeningExecutorService indexExecutor;
+  private final ListeningExecutorService interactiveExecutor;
+  private final ListeningExecutorService batchExecutor;
 
   public IndexModule(int threads) {
     this.threads = threads;
-    this.indexExecutor = null;
+    this.interactiveExecutor = null;
+    this.batchExecutor = null;
   }
 
-  public IndexModule(ListeningExecutorService indexExecutor) {
+  public IndexModule(ListeningExecutorService interactiveExecutor,
+      ListeningExecutorService batchExecutor) {
     this.threads = -1;
-    this.indexExecutor = indexExecutor;
+    this.interactiveExecutor = interactiveExecutor;
+    this.batchExecutor = batchExecutor;
   }
 
   @Override
@@ -67,49 +73,61 @@
     bind(IndexCollection.class);
     listener().to(IndexCollection.class);
     factory(ChangeIndexer.Factory.class);
-
-    if (indexExecutor != null) {
-      bind(ListeningExecutorService.class)
-          .annotatedWith(IndexExecutor.class)
-          .toInstance(indexExecutor);
-    } else {
-      install(new IndexExecutorModule(threads));
-    }
   }
 
   @Provides
+  @Singleton
   ChangeIndexer getChangeIndexer(
+      @IndexExecutor(INTERACTIVE) ListeningExecutorService executor,
       ChangeIndexer.Factory factory,
       IndexCollection indexes) {
-    return factory.create(indexes);
+    // Bind default indexer to interactive executor; callers who need a
+    // different executor can use the factory directly.
+    return factory.create(executor, indexes);
   }
 
-  private static class IndexExecutorModule extends AbstractModule {
-    private final int threads;
-
-    private IndexExecutorModule(int threads) {
-      this.threads = threads;
+  @Provides
+  @Singleton
+  @IndexExecutor(INTERACTIVE)
+  ListeningExecutorService getInteractiveIndexExecutor(
+      @GerritServerConfig Config config,
+      WorkQueue workQueue) {
+    if (interactiveExecutor != null) {
+      return interactiveExecutor;
     }
-
-    @Override
-    public void configure() {
+    int threads = this.threads;
+    if (threads <= 0) {
+      threads = config.getInt("index", null, "threads", 0);
     }
-
-    @Provides
-    @Singleton
-    @IndexExecutor
-    ListeningExecutorService getIndexExecutor(
-        @GerritServerConfig Config config,
-        WorkQueue workQueue) {
-      int threads = this.threads;
-      if (threads <= 0) {
-        threads = config.getInt("index", null, "threads", 0);
-      }
-      if (threads <= 0) {
-        return MoreExecutors.newDirectExecutorService();
-      }
-      return MoreExecutors.listeningDecorator(
-          workQueue.createQueue(threads, "index"));
+    if (threads <= 0) {
+      threads =
+          config.getInt("changeMerge", null, "interactiveThreadPoolSize", 0);
     }
+    if (threads <= 0) {
+      return MoreExecutors.newDirectExecutorService();
+    }
+    return MoreExecutors.listeningDecorator(
+        workQueue.createQueue(threads, "Index-Interactive"));
+  }
+
+  @Provides
+  @Singleton
+  @IndexExecutor(BATCH)
+  ListeningExecutorService getBatchIndexExecutor(
+      @IndexExecutor(INTERACTIVE) ListeningExecutorService interactive,
+      @GerritServerConfig Config config,
+      WorkQueue workQueue) {
+    if (batchExecutor != null) {
+      return batchExecutor;
+    }
+    int threads = config.getInt("index", null, "batchThreads", 0);
+    if (threads <= 0) {
+      threads = config.getInt("changeMerge", null, "threadPoolSize", 0);
+    }
+    if (threads <= 0) {
+      return interactive;
+    }
+    return MoreExecutors.listeningDecorator(
+        workQueue.createQueue(threads, "Index-Batch"));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
new file mode 100644
index 0000000..13d37fc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
@@ -0,0 +1,164 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// 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.gerrit.server.index;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.QueueProvider.QueueType;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+public class ReindexAfterUpdate implements GitReferenceUpdatedListener {
+  private static final Logger log = LoggerFactory
+      .getLogger(ReindexAfterUpdate.class);
+
+  private final ThreadLocalRequestContext tl;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeIndexer.Factory indexerFactory;
+  private final IndexCollection indexes;
+  private final ListeningExecutorService executor;
+
+  @Inject
+  ReindexAfterUpdate(
+      ThreadLocalRequestContext tl,
+      SchemaFactory<ReviewDb> schemaFactory,
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeIndexer.Factory indexerFactory,
+      IndexCollection indexes,
+      @IndexExecutor(QueueType.BATCH) ListeningExecutorService executor) {
+    this.tl = tl;
+    this.schemaFactory = schemaFactory;
+    this.userFactory = userFactory;
+    this.indexerFactory = indexerFactory;
+    this.indexes = indexes;
+    this.executor = executor;
+  }
+
+  @Override
+  public void onGitReferenceUpdated(final Event event) {
+    Futures.transform(
+        executor.submit(new GetChanges(event)),
+        new AsyncFunction<List<Change>, List<Void>>() {
+          @Override
+          public ListenableFuture<List<Void>> apply(List<Change> changes) {
+            List<ListenableFuture<Void>> result =
+                Lists.newArrayListWithCapacity(changes.size());
+            for (Change c : changes) {
+              result.add(executor.submit(new Index(event, c)));
+            }
+            return Futures.allAsList(result);
+          }
+        });
+  }
+
+  private abstract class Task<V> implements Callable<V> {
+    protected ReviewDb db;
+    protected Event event;
+
+    protected Task(Event event) {
+      this.event = event;
+    }
+
+    @Override
+    public final V call() throws Exception {
+      try {
+        db = schemaFactory.open();
+        return impl();
+      } catch (Exception e) {
+        log.error("Failed to reindex changes after " + event, e);
+        throw e;
+      } finally {
+        if (db != null) {
+          db.close();
+        }
+      }
+    }
+
+    protected abstract V impl() throws Exception;
+  }
+
+  private class GetChanges extends Task<List<Change>> {
+    private GetChanges(Event event) {
+      super(event);
+    }
+
+    @Override
+    protected List<Change> impl() throws OrmException {
+      String ref = event.getRefName();
+      Project.NameKey project = new Project.NameKey(event.getProjectName());
+      if (ref.equals(RefNames.REFS_CONFIG)) {
+        return db.changes().byProjectOpenAll(project).toList();
+      } else {
+        return db.changes().byBranchOpenAll(new Branch.NameKey(project, ref))
+            .toList();
+      }
+    }
+  }
+
+  private class Index extends Task<Void> {
+    private final Change change;
+
+    Index(Event event, Change change) {
+      super(event);
+      this.change = change;
+    }
+
+    @Override
+    protected Void impl() throws IOException {
+      RequestContext context = new RequestContext() {
+        @Override
+        public CurrentUser getCurrentUser() {
+          return userFactory.create(change.getOwner());
+        }
+
+        @Override
+        public Provider<ReviewDb> getReviewDbProvider() {
+          return Providers.of(db);
+        }
+      };
+      RequestContext old = tl.setContext(context);
+      try {
+        indexerFactory.create(executor, indexes).index(db, change);
+        return null;
+      } finally {
+        tl.setContext(old);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
similarity index 90%
rename from gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
index 9a992cf..8b029dd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.index;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
 import com.google.common.base.Stopwatch;
@@ -27,11 +29,9 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.MergeabilityChecker;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
@@ -65,6 +65,7 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -75,9 +76,9 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-public class ChangeBatchIndexer {
+public class SiteIndexer {
   private static final Logger log =
-      LoggerFactory.getLogger(ChangeBatchIndexer.class);
+      LoggerFactory.getLogger(SiteIndexer.class);
 
   public static class Result {
     private final long elapsedNanos;
@@ -114,40 +115,52 @@
   private final GitRepositoryManager repoManager;
   private final ListeningExecutorService executor;
   private final ChangeIndexer.Factory indexerFactory;
-  private final MergeabilityChecker mergeabilityChecker;
   private final ThreeWayMergeStrategy mergeStrategy;
 
+  private int numChanges = -1;
+  private OutputStream progressOut = NullOutputStream.INSTANCE;
+  private PrintWriter verboseWriter =
+      new PrintWriter(NullOutputStream.INSTANCE);
+
   @Inject
-  ChangeBatchIndexer(SchemaFactory<ReviewDb> schemaFactory,
+  SiteIndexer(SchemaFactory<ReviewDb> schemaFactory,
       ChangeData.Factory changeDataFactory,
       GitRepositoryManager repoManager,
-      @IndexExecutor ListeningExecutorService executor,
+      @IndexExecutor(BATCH) ListeningExecutorService executor,
       ChangeIndexer.Factory indexerFactory,
-      @GerritServerConfig Config config,
-      @Nullable MergeabilityChecker mergeabilityChecker) {
+      @GerritServerConfig Config config) {
     this.schemaFactory = schemaFactory;
     this.changeDataFactory = changeDataFactory;
     this.repoManager = repoManager;
     this.executor = executor;
     this.indexerFactory = indexerFactory;
-    this.mergeabilityChecker = mergeabilityChecker;
     this.mergeStrategy = MergeUtil.getMergeStrategy(config);
   }
 
-  public Result indexAll(ChangeIndex index, Iterable<Project.NameKey> projects,
-      int numProjects, int numChanges, OutputStream progressOut,
-      OutputStream verboseOut) {
-    if (progressOut == null) {
-      progressOut = NullOutputStream.INSTANCE;
-    }
-    PrintWriter verboseWriter = verboseOut != null ? new PrintWriter(verboseOut)
-        : null;
+  public SiteIndexer setNumChanges(int num) {
+    numChanges = num;
+    return this;
+  }
 
+  public SiteIndexer setProgressOut(OutputStream out) {
+    progressOut = checkNotNull(out);
+    return this;
+  }
+
+  public SiteIndexer setVerboseOut(OutputStream out) {
+    verboseWriter = new PrintWriter(checkNotNull(out));
+    return this;
+  }
+
+  public Result indexAll(ChangeIndex index,
+      Iterable<Project.NameKey> projects) {
     Stopwatch sw = Stopwatch.createStarted();
     final MultiProgressMonitor mpm =
         new MultiProgressMonitor(progressOut, "Reindexing changes");
     final Task projTask = mpm.beginSubTask("projects",
-        numProjects >= 0 ? numProjects : MultiProgressMonitor.UNKNOWN);
+        (projects instanceof Collection)
+          ? ((Collection<?>) projects).size()
+          : MultiProgressMonitor.UNKNOWN);
     final Task doneTask = mpm.beginSubTask(null,
         numChanges >= 0 ? numChanges : MultiProgressMonitor.UNKNOWN);
     final Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
@@ -156,11 +169,8 @@
     final AtomicBoolean ok = new AtomicBoolean(true);
 
     for (final Project.NameKey project : projects) {
-      if (!updateMergeable(project)) {
-        ok.set(false);
-      }
       final ListenableFuture<?> future = executor.submit(reindexProject(
-          indexerFactory.create(index), project, doneTask, failedTask,
+          indexerFactory.create(executor, index), project, doneTask, failedTask,
           verboseWriter));
       futures.add(future);
       future.addListener(new Runnable() {
@@ -214,18 +224,6 @@
     return new Result(sw, ok.get(), doneTask.getCount(), failedTask.getCount());
   }
 
-  private boolean updateMergeable(Project.NameKey project) {
-    if (mergeabilityChecker != null) {
-      try {
-        mergeabilityChecker.newCheck().addProject(project).run();
-      } catch (IOException e) {
-        log.error("Error in mergeability checker", e);
-        return false;
-      }
-    }
-    return true;
-  }
-
   private Callable<Void> reindexProject(final ChangeIndexer indexer,
       final Project.NameKey project, final Task done, final Task failed,
       final PrintWriter verboseWriter) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index 5f2ffcb..b587791 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -121,7 +121,7 @@
         try {
           patchList = getPatchList();
         } catch (PatchListNotAvailableException e) {
-          patchList = null;
+          log.error("Failed to get patch list", e);
         }
       }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
index 4c5ca27..87afc33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
@@ -26,11 +26,11 @@
 
 public class BasicChangeRewrites extends QueryRewriter<ChangeData> {
   private static final ChangeQueryBuilder BUILDER = new ChangeQueryBuilder(
-      new ChangeQueryBuilder.Arguments( //
-          new InvalidProvider<ReviewDb>(), //
-          new InvalidProvider<ChangeQueryRewriter>(), //
-          null, null, null, null, null, null, null, //
-          null, null, null, null, null, null, null, null, null, null, null),
+      new ChangeQueryBuilder.Arguments(
+          new InvalidProvider<ReviewDb>(),
+          new InvalidProvider<ChangeQueryRewriter>(),
+          null, null, null, null, null, null, null, null, null, null, null,
+          null, null, null, null, null, null, null, null),
           null);
 
   private static final QueryRewriter.Definition<ChangeData, BasicChangeRewrites> mydef =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 9707733..d45a58e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -36,7 +37,9 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerState;
@@ -46,6 +49,8 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.assistedinject.Assisted;
@@ -55,6 +60,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -154,8 +160,8 @@
    * @return instance for testing.
    */
   static ChangeData createForTest(Change.Id id, int currentPatchSetId) {
-    ChangeData cd = new ChangeData(null, null, null, null, null,
-        null, null, null, null, null, id);
+    ChangeData cd = new ChangeData(null, null, null, null, null, null, null,
+        null, null, null, null, null, null, id);
     cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
     return cd;
   }
@@ -164,12 +170,15 @@
   private final GitRepositoryManager repoManager;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final IdentifiedUser.GenericFactory userFactory;
+  private final ProjectCache projectCache;
+  private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeNotes.Factory notesFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
   private final PatchLineCommentsUtil plcUtil;
   private final PatchListCache patchListCache;
   private final NotesMigration notesMigration;
+  private final MergeabilityCache mergeabilityCache;
   private final Change.Id legacyId;
   private ChangeDataSource returnedBySource;
   private Change change;
@@ -187,30 +196,37 @@
   private List<ChangeMessage> messages;
   private List<SubmitRecord> submitRecords;
   private ChangedLines changedLines;
+  private Boolean mergeable;
 
   @AssistedInject
   private ChangeData(
       GitRepositoryManager repoManager,
       ChangeControl.GenericFactory changeControlFactory,
       IdentifiedUser.GenericFactory userFactory,
+      ProjectCache projectCache,
+      MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
       PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
+      MergeabilityCache mergeabilityCache,
       @Assisted ReviewDb db,
       @Assisted Change.Id id) {
     this.db = db;
     this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
     this.userFactory = userFactory;
+    this.projectCache = projectCache;
+    this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
     this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
+    this.mergeabilityCache = mergeabilityCache;
     legacyId = id;
   }
 
@@ -219,24 +235,30 @@
       GitRepositoryManager repoManager,
       ChangeControl.GenericFactory changeControlFactory,
       IdentifiedUser.GenericFactory userFactory,
+      ProjectCache projectCache,
+      MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
       PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
+      MergeabilityCache mergeabilityCache,
       @Assisted ReviewDb db,
       @Assisted Change c) {
     this.db = db;
     this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
     this.userFactory = userFactory;
+    this.projectCache = projectCache;
+    this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
     this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
+    this.mergeabilityCache = mergeabilityCache;
     legacyId = c.getId();
     change = c;
   }
@@ -246,24 +268,30 @@
       GitRepositoryManager repoManager,
       ChangeControl.GenericFactory changeControlFactory,
       IdentifiedUser.GenericFactory userFactory,
+      ProjectCache projectCache,
+      MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
       PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
+      MergeabilityCache mergeabilityCache,
       @Assisted ReviewDb db,
       @Assisted ChangeControl c) {
     this.db = db;
     this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
     this.userFactory = userFactory;
+    this.projectCache = projectCache;
+    this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
     this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
+    this.mergeabilityCache = mergeabilityCache;
     legacyId = c.getChange().getId();
     change = c.getChange();
     changeControl = c;
@@ -555,6 +583,45 @@
     return submitRecords;
   }
 
+  public void setMergeable(boolean mergeable) {
+    this.mergeable = mergeable;
+  }
+
+  public boolean isMergeable() throws OrmException {
+    if (mergeable == null) {
+      Change c = change();
+      if (c.getStatus() == Change.Status.MERGED) {
+        mergeable = true;
+      } else {
+        PatchSet ps = currentPatchSet();
+        Repository repo = null;
+        try {
+          repo = repoManager.openRepository(c.getProject());
+          Ref ref = repo.getRef(c.getDest().get());
+          SubmitTypeRecord rec = new SubmitRuleEvaluator(this)
+              .getSubmitType();
+          if (rec.status != SubmitTypeRecord.Status.OK) {
+            throw new OrmException(
+                "Error in mergeability check: " + rec.errorMessage);
+          }
+          String mergeStrategy = mergeUtilFactory
+              .create(projectCache.get(c.getProject()))
+              .mergeStrategyName();
+          mergeable = mergeabilityCache.get(
+              ObjectId.fromString(ps.getRevision().get()),
+              ref, rec.type, mergeStrategy, c.getDest(), repo, db);
+        } catch (IOException e) {
+          throw new OrmException(e);
+        } finally {
+          if (repo != null) {
+            repo.close();
+          }
+        }
+      }
+    }
+    return mergeable;
+  }
+
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this).addValue(getId()).toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 64ba2e9..5c39df8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -146,6 +147,7 @@
     final CapabilityControl.Factory capabilityControlFactory;
     final ChangeControl.GenericFactory changeControlGenericFactory;
     final ChangeData.Factory changeDataFactory;
+    final FieldDef.FillArgs fillArgs;
     final PatchLineCommentsUtil plcUtil;
     final AccountResolver accountResolver;
     final GroupBackend groupBackend;
@@ -169,6 +171,7 @@
         CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
         ChangeData.Factory changeDataFactory,
+        FieldDef.FillArgs fillArgs,
         PatchLineCommentsUtil plcUtil,
         AccountResolver accountResolver,
         GroupBackend groupBackend,
@@ -189,6 +192,7 @@
       this.capabilityControlFactory = capabilityControlFactory;
       this.changeControlGenericFactory = changeControlGenericFactory;
       this.changeDataFactory = changeDataFactory;
+      this.fillArgs = fillArgs;
       this.plcUtil = plcUtil;
       this.accountResolver = accountResolver;
       this.groupBackend = groupBackend;
@@ -326,7 +330,7 @@
     }
 
     if ("mergeable".equalsIgnoreCase(value)) {
-      return new IsMergeablePredicate();
+      return new IsMergeablePredicate(schema(args.indexes), args.fillArgs);
     }
 
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
index 787b90e..6ef4ab6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
@@ -14,20 +14,39 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
+import static com.google.gerrit.server.index.ChangeField.MERGEABLE;
+
 import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
 import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.Schema;
 import com.google.gwtorm.server.OrmException;
 
 class IsMergeablePredicate extends IndexPredicate<ChangeData> {
-  IsMergeablePredicate() {
-    super(ChangeField.MERGEABLE, "1");
+  @SuppressWarnings("deprecation")
+  static FieldDef<ChangeData, ?> mergeableField(Schema<ChangeData> schema) {
+    if (schema == null) {
+      return ChangeField.LEGACY_MERGEABLE;
+    }
+    FieldDef<ChangeData, ?> f = schema.getFields().get(MERGEABLE.getName());
+    if (f != null) {
+      return f;
+    }
+    return schema.getFields().get(ChangeField.LEGACY_MERGEABLE.getName());
+  }
+
+  private final FillArgs args;
+
+  IsMergeablePredicate(Schema<ChangeData> schema,
+      FillArgs args) {
+    super(mergeableField(schema), "1");
+    this.args = args;
   }
 
   @Override
   public boolean match(ChangeData object) throws OrmException {
-    Change c = object.change();
-    return c != null && c.isMergeable();
+    return getValue().equals(getField().get(object, args));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 44f0798..470fb14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_99> C = Schema_99.class;
+  public static final Class<Schema_100> C = Schema_100.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_100.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_100.java
new file mode 100644
index 0000000..0902194
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_100.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// 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.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_100 extends SchemaVersion {
+  @Inject
+  Schema_100(Provider<Schema_99> prior) {
+    super(prior);
+  }
+
+  // No database migration; merges are rechecked on reindex.
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java
index d90ecc1..1fdc182 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java
@@ -14,29 +14,14 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.sql.SQLException;
-import java.util.List;
-
 public class Schema_59 extends SchemaVersion {
   @Inject
   Schema_59(Provider<Schema_58> prior) {
     super(prior);
   }
 
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
-      SQLException {
-    List<Change> allChanges = db.changes().all().toList();
-    for (Change change : allChanges) {
-      change.setMergeable(true);
-      change.setLastSha1MergeTested(null);
-    }
-    db.changes().update(allChanges);
-  }
-}
\ No newline at end of file
+  // Don't migrate columns; they are removed in Schema_100.
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
index 7090f0d..c6425f5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
@@ -26,8 +26,8 @@
         new FakeQueryBuilder.Definition<>(
           FakeQueryBuilder.class),
         new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
-          null, null, null, null, null, null, null, null, null, indexes, null,
-          null, null, null),
+          null, null, null, null, null, null, null, null, null, null, indexes,
+          null, null, null, null),
         null);
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
index a44666b..9e58bea 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
+import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
@@ -50,6 +51,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -58,6 +60,7 @@
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
+import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -253,18 +256,20 @@
     };
 
     Injector injector = Guice.createInjector(new FactoryModule() {
+      @SuppressWarnings({"rawtypes", "unchecked"})
       @Override
       protected void configure() {
+        Provider nullProvider = Providers.of(null);
         bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(
             new Config());
-        bind(ReviewDb.class).toProvider(Providers.<ReviewDb> of(null));
+        bind(ReviewDb.class).toProvider(nullProvider);
         bind(GitRepositoryManager.class).toInstance(repoManager);
-        bind(PatchListCache.class)
-            .toProvider(Providers.<PatchListCache> of(null));
+        bind(PatchListCache.class).toProvider(nullProvider);
 
         factory(CapabilityControl.Factory.class);
         factory(ChangeControl.AssistedFactory.class);
         factory(ChangeData.Factory.class);
+        factory(MergeUtil.Factory.class);
         bind(ProjectCache.class).toInstance(projectCache);
         bind(AccountCache.class).toInstance(new FakeAccountCache());
         bind(GroupBackend.class).to(SystemGroupBackend.class);
@@ -275,6 +280,7 @@
         bind(String.class).annotatedWith(AnonymousCowardName.class)
             .toProvider(AnonymousCowardNameProvider.class);
         bind(ChangeKindCache.class).to(ChangeKindCacheImpl.NoCache.class);
+        bind(MergeabilityCache.bindingKey()).toProvider(nullProvider);
       }
     });
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 5e9858c..0053098 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
-import com.google.gerrit.server.change.MergeabilityChecksExecutorModule;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AllUsersName;
@@ -168,7 +167,6 @@
     install(new DefaultCacheFactory.Module());
     install(new SmtpEmailSender.Module());
     install(new SignedTokenEmailTokenVerifier.Module());
-    install(new MergeabilityChecksExecutorModule());
 
     IndexType indexType = null;
     try {
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index aba9013..253299d 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
-import com.google.gerrit.server.change.MergeabilityChecksExecutorModule;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -278,7 +277,6 @@
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
-    modules.add(new MergeabilityChecksExecutorModule());
     modules.add(new IntraLineWorkerPool.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new InternalAccountDirectory.Module());
diff --git a/lib/asciidoctor/java/DocIndexer.java b/lib/asciidoctor/java/DocIndexer.java
index 4736b0b..27bf30a 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -51,7 +51,7 @@
 import java.util.zip.ZipOutputStream;
 
 public class DocIndexer {
-  private static final Version LUCENE_VERSION = Version.LUCENE_4_10_0;
+  private static final Version LUCENE_VERSION = Version.LUCENE_4_10_1;
   private static final Pattern SECTION_HEADER = Pattern.compile("^=+ (.*)");
 
   @Option(name = "-o", usage = "output JAR file")
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index dd148b1..df14528 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,11 +1,11 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.10.0'
+VERSION = '4.10.1'
 
 maven_jar(
   name = 'core',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = 'a4ceea9a80e81fe84e81fe4fccce9e9930dc703a',
+  sha1 = '4ff28101d9de465b7f3cf59d7bc2892c1c118b4b',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -16,7 +16,7 @@
 maven_jar(
   name = 'analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  sha1 = '912962d436d9851dc90091e48251c802d3b65941',
+  sha1 = '6491c6019c32e7c4f7674f238d5beaa84d3108a6',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -27,6 +27,6 @@
 maven_jar(
   name = 'query-parser',
   id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  sha1 = '7a00eb4b97a6cb7a5a29957b62c7f002dd71b7ff',
+  sha1 = '0174ffd89d5289037ae24759f38111285b98636d',
   license = 'Apache2.0',
 )