Merge "Provide method to get unmarked text from GR-FORMATTED-TEXT"
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 775fe21..a5a4f90 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -97,6 +97,7 @@
 ====
 
 
+[[git_commit_settings]]
 === A sample good Gerrit commit message:
 ====
   Add sample commit message to guidelines doc
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index 116cf61..8f5b4d7 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -43,6 +43,64 @@
 . Click on *Inherit project compile output path*.
 . Click on *Use module compile output path*.
 
+== Recommended settings
+
+=== Code style
+. Go to *File -> Settings -> Editor -> Code Style*.
+. Click on *Manage*.
+. Click on *Import*.
+. Choose `IntelliJ IDEA Code Style XML`.
+. Select the file `$(gerrit_source_code)/tools/intellij/Gerrit_Code_Style.xml`.
+. Make sure that `Google Format (Gerrit)` is chosen as *Scheme*.
+
+In addition, the EditorConfig settings (which ensure a consistent style between
+Eclipse, IntelliJ, and other editors) should be applied on top of that. Those
+settings are in the file `.editorconfig` of the Gerrit source code. IntelliJ
+will automatically pick up those settings if the EditorConfig plugin is enabled
+and configured correctly as can be verified by:
+
+. Go to *File -> Settings -> Plugins*.
+. Ensure that the EditorConfig plugin is enabled.
+. Go to *File -> Settings -> Editor -> Code Style*.
+. Ensure that *Enable EditorConfig support* is checked.
+
+NOTE: If IntelliJ notifies you later on that the EditorConfig settings override
+the code style settings, simply confirm that.
+
+=== Copyright
+Copy the folder `$(gerrit_source_code)/tools/intellij/copyright` (not just the
+contents) to `$(project_data_directory)/.idea`. If it already exists, replace
+it.
+
+=== File header
+By default, IntelliJ adds a file header containing the name of the author and
+the current date to new files. To disable that, follow these steps:
+
+. Go to *File -> Settings -> Editor -> File and Code Templates*.
+. Select the tab *Includes*.
+. Select *File Header*.
+. Remove the template code in the right editor.
+
+=== Commit message
+To simplify the creation of commit messages which are compliant with the
+<<dev-contributing#commit-message,Commit Message>> format, do the following:
+
+. Go to *File -> Settings -> Version Control*.
+. Check *Commit message right margin (columns)*.
+. Make sure that 72 is specified as value.
+. Check *Wrap when typing reaches right margin*.
+
+In addition, you should follow the instructions of
+<<dev-contributing#git_commit_settings,this section>> (if you haven't
+done so already):
+
+* Install the Git hook for the `Change-Id` line.
+* Set up the HTTP access.
+
+Setting up the HTTP access will allow you to commit changes via IntelliJ without
+specifying your credentials. The Git hook won't be noticeable during a commit
+as it's executed after the commit dialog of IntelliJ was closed.
+
 == Run configurations
 Run configurations can be accessed on the toolbar. To edit them or add new ones,
 choose *Edit Configurations* on the drop-down list of the run configurations
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index 1a43784..f523354 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -75,8 +75,9 @@
     GeneralPreferencesInfo o = gApi.accounts()
         .id(user42.id.toString())
         .getPreferences();
-    assertPrefs(o, GeneralPreferencesInfo.defaults(), "my");
+    assertPrefs(o, GeneralPreferencesInfo.defaults(), "my", "changeTable");
     assertThat(o.my).hasSize(7);
+    assertThat(o.changeTable).isEmpty();
 
     GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults();
 
@@ -99,6 +100,8 @@
     i.diffView = DiffView.UNIFIED_DIFF;
     i.my = new ArrayList<>();
     i.my.add(new MenuItem("name", "url"));
+    i.changeTable = new ArrayList<>();
+    i.changeTable.add("Status");
     i.urlAliases = new HashMap<>();
     i.urlAliases.put("foo", "bar");
 
@@ -107,6 +110,7 @@
         .setPreferences(i);
     assertPrefs(o, i, "my");
     assertThat(o.my).hasSize(1);
+    assertThat(o.changeTable).hasSize(1);
   }
 
   @Test
@@ -125,6 +129,6 @@
     assertThat(o.changesPerPage).isEqualTo(newChangesPerPage);
 
     // assert hard-coded defaults
-    assertPrefs(o, d, "my", "changesPerPage");
+    assertPrefs(o, d, "my", "changeTable", "changesPerPage");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 37ced5f..a39f300 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.RawInputUtil;
@@ -648,6 +649,7 @@
   }
 
   @Test
+  @GerritConfig(name = "index.testReindexAfterUpdate", value = "false")
   public void getRelatedForStaleChange() throws Exception {
     RevCommit c1_1 = commitBuilder()
         .add("a.txt", "1")
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index a2b93cc..7ab1753 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -110,6 +110,13 @@
   public static Config defaultConfig() {
     Config cfg = new Config();
     cfg.setBoolean("noteDb", null, "testRebuilderWrapper", true);
+
+    // Disable async reindex-if-stale check after index update. This avoids
+    // unintentional auto-rebuilding of the change in NoteDb during the read
+    // path of the reindex-if-stale check. For the purposes of this test, we
+    // want precise control over when auto-rebuilding happens.
+    cfg.setBoolean("index", null, "testReindexAfterUpdate", false);
+
     return cfg;
   }
 
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index a4c74ed..13d9ad6 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
@@ -363,6 +364,21 @@
           ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(),
           ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd);
 
+      if (source.get(ChangeField.REF_STATE.getName()) != null) {
+        JsonArray refStates =
+            source.get(ChangeField.REF_STATE.getName()).getAsJsonArray();
+        cd.setRefStates(
+            Iterables.transform(
+                refStates, e -> Base64.decodeBase64(e.getAsString())));
+      }
+      if (source.get(ChangeField.REF_STATE_PATTERN.getName()) != null) {
+        JsonArray refStatePatterns = source.get(
+            ChangeField.REF_STATE_PATTERN.getName()).getAsJsonArray();
+        cd.setRefStatePatterns(
+            Iterables.transform(
+                refStatePatterns, e -> Base64.decodeBase64(e.getAsString())));
+      }
+
       return cd;
     }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 8d82e3a..3813644 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -141,6 +141,7 @@
   public Boolean muteCommonPathPrefixes;
   public Boolean signedOffBy;
   public List<MenuItem> my;
+  public List<String> changeTable;
   public Map<String, String> urlAliases;
   public EmailStrategy emailStrategy;
   public DefaultBase defaultBaseForMerges;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
index 7cfb1fc..ae93a83 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
@@ -19,6 +19,7 @@
 public class ProjectAccessInfo extends JavaScriptObject {
   public final native boolean canAddRefs() /*-{ return this.can_add ? true : false; }-*/;
   public final native boolean isOwner() /*-{ return this.is_owner ? true : false; }-*/;
+  public final native boolean configVisible() /*-{ return this.config_visible ? true : false; }-*/;
 
   protected ProjectAccessInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index e1cfa90..24c2da7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -72,6 +72,7 @@
 
 public class ProjectInfoScreen extends ProjectScreen {
   private boolean isOwner;
+  private boolean configVisible;
 
   private LabeledWidgetsGrid grid;
   private Panel pluginOptionsPanel;
@@ -154,6 +155,7 @@
           @Override
           public void onSuccess(ProjectAccessInfo result) {
             isOwner = result.isOwner();
+            configVisible = result.configVisible();
             enableForm();
             saveProject.setVisible(isOwner);
           }
@@ -625,7 +627,7 @@
       actionsPanel.add(createChangeAction());
     }
 
-    if (isOwner) {
+    if (isOwner && configVisible) {
       actionsPanel.add(createEditConfigAction());
     }
   }
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 8097543..faddd6c 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
@@ -22,6 +22,8 @@
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.base.Throwables;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Collections2;
@@ -120,6 +122,9 @@
   private static final String DELETED_FIELD = ChangeField.DELETED.getName();
   private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName();
   private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName();
+  private static final String REF_STATE_FIELD = ChangeField.REF_STATE.getName();
+  private static final String REF_STATE_PATTERN_FIELD =
+      ChangeField.REF_STATE_PATTERN.getName();
   private static final String REVIEWEDBY_FIELD =
       ChangeField.REVIEWEDBY.getName();
   private static final String REVIEWER_FIELD = ChangeField.REVIEWER.getName();
@@ -472,6 +477,12 @@
         ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd);
     decodeSubmitRecords(doc, SUBMIT_RECORD_LENIENT_FIELD,
         ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd);
+    if (fields.contains(REF_STATE_FIELD)) {
+      decodeRefStates(doc, cd);
+    }
+    if (fields.contains(REF_STATE_PATTERN_FIELD)) {
+      decodeRefStatePatterns(doc, cd);
+    }
     return cd;
   }
 
@@ -572,6 +583,16 @@
         opts, cd);
   }
 
+  private void decodeRefStates(Multimap<String, IndexableField> doc,
+      ChangeData cd) {
+    cd.setRefStates(copyAsBytes(doc.get(REF_STATE_FIELD)));
+  }
+
+  private void decodeRefStatePatterns(Multimap<String, IndexableField> doc,
+      ChangeData cd) {
+    cd.setRefStatePatterns(copyAsBytes(doc.get(REF_STATE_PATTERN_FIELD)));
+  }
+
   private static <T> List<T> decodeProtos(Multimap<String, IndexableField> doc,
       String fieldName, ProtobufCodec<T> codec) {
     Collection<IndexableField> fields = doc.get(fieldName);
@@ -586,4 +607,16 @@
     }
     return result;
   }
+
+  private static List<byte[]> copyAsBytes(Collection<IndexableField> fields) {
+    return fields.stream()
+        .map(
+            f -> {
+              BytesRef ref = f.binaryValue();
+              byte[] b = new byte[ref.length];
+              System.arraycopy(ref.bytes, ref.offset, b, 0, ref.length);
+              return b;
+            })
+        .collect(toList());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
index 0ee10f0..91b568c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
@@ -188,10 +188,10 @@
 
   private Map<Account.Id, MutableDouble> baseRankingForEmptyQuery(
       double baseWeight) throws OrmException{
-    // Get the user's last 50 changes, check approvals
+    // Get the user's last 25 changes, check approvals
     try {
       List<ChangeData> result = internalChangeQuery
-          .setLimit(50)
+          .setLimit(25)
           .setRequestedFields(ImmutableSet.of(ChangeField.REVIEWER.getName()))
           .query(changeQueryBuilder.owner("self"));
       Map<Account.Id, MutableDouble> suggestions = new HashMap<>();
@@ -260,7 +260,7 @@
     }
 
     List<List<ChangeData>> result = internalChangeQuery
-        .setLimit(100 * predicates.size())
+        .setLimit(25)
         .setRequestedFields(ImmutableSet.of())
         .query(predicates);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
index 8339baf..c0d81cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
@@ -22,8 +22,12 @@
 import static com.google.gerrit.server.git.UserConfigSections.KEY_TOKEN;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
 import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
+
 
 import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.MenuItem;
 import com.google.gerrit.reviewdb.client.Account;
@@ -91,7 +95,7 @@
           loadSection(p.getConfig(), UserConfigSections.GENERAL, null,
           new GeneralPreferencesInfo(),
           updateDefaults(allUserPrefs), in);
-
+      loadChangeTableColumns(r, p, dp);
       return loadMyMenusAndUrlAliases(r, p, dp);
     }
   }
@@ -161,6 +165,22 @@
     return !Strings.isNullOrEmpty(val) ? val : defaultValue;
   }
 
+  public GeneralPreferencesInfo loadChangeTableColumns(GeneralPreferencesInfo r,
+      VersionedAccountPreferences v, VersionedAccountPreferences d) {
+    r.changeTable = changeTable(v);
+
+    Config cfg = v.getConfig();
+    if (r.changeTable.isEmpty() && !v.isDefaults()) {
+      r.changeTable = changeTable(d);
+    }
+    return r;
+  }
+
+  private static List<String> changeTable(VersionedAccountPreferences v) {
+    return Lists.newArrayList(v.getConfig().getStringList(
+        CHANGE_TABLE, null, CHANGE_TABLE_COLUMN));
+  }
+
   private static Map<String, String> urlAliases(VersionedAccountPreferences v) {
     HashMap<String, String> urlAliases = new HashMap<>();
     Config cfg = v.getConfig();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index b70cabd..08b7b0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -21,6 +21,8 @@
 import static com.google.gerrit.server.git.UserConfigSections.KEY_TOKEN;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
 import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -87,6 +89,7 @@
     Account.Id id = rsrc.getUser().getAccountId();
     GeneralPreferencesInfo n = loader.merge(id, i);
 
+    n.changeTable = i.changeTable;
     n.my = i.my;
     n.urlAliases = i.urlAliases;
 
@@ -105,6 +108,7 @@
       storeSection(prefs.getConfig(), UserConfigSections.GENERAL, null, i,
           GeneralPreferencesInfo.defaults());
 
+      storeMyChangeTableColumns(prefs, i.changeTable);
       storeMyMenus(prefs, i.my);
       storeUrlAliases(prefs, i.urlAliases);
       prefs.commit(md);
@@ -125,6 +129,16 @@
     }
   }
 
+  public static void storeMyChangeTableColumns(VersionedAccountPreferences
+      prefs, List<String> changeTable) {
+    Config cfg = prefs.getConfig();
+    if (changeTable != null) {
+      unsetSection(cfg, UserConfigSections.CHANGE_TABLE);
+      cfg.setStringList(UserConfigSections.CHANGE_TABLE, null,
+          CHANGE_TABLE_COLUMN, changeTable);
+    }
+  }
+
   private static void set(Config cfg, String section, String key, String val) {
     if (Strings.isNullOrEmpty(val)) {
       cfg.unset(UserConfigSections.MY, section, key);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
index a09466d..bbd55f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
@@ -29,6 +29,10 @@
   public static final String KEY_MATCH = "match";
   public static final String KEY_TOKEN = "token";
 
+  /** The table column user preferences. */
+  public static final String CHANGE_TABLE = "changeTable";
+  public static final String CHANGE_TABLE_COLUMN = "column";
+
   /** The edit user preferences. */
   public static final String EDIT = "edit";
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
index 386092d..92938cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Preconditions;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gwtorm.server.OrmException;
@@ -65,14 +66,17 @@
   public static class FillArgs {
     public final TrackingFooters trackingFooters;
     public final boolean allowsDrafts;
+    public final AllUsersName allUsers;
 
     @Inject
     FillArgs(TrackingFooters trackingFooters,
-        @GerritServerConfig Config cfg) {
+        @GerritServerConfig Config cfg,
+        AllUsersName allUsers) {
       this.trackingFooters = trackingFooters;
       this.allowsDrafts = cfg == null
           ? true
           : cfg.getBoolean("change", "allowDrafts", true);
+      this.allUsers = allUsers;
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java
index 533e57c..84eb3bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java
@@ -17,8 +17,11 @@
 import com.google.gerrit.server.query.DataSource;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
 
 import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
 
 /**
  * Secondary index implementation for arbitrary documents.
@@ -91,6 +94,44 @@
       throws QueryParseException;
 
   /**
+   * Get a single document from the index.
+   *
+   * @param key document key.
+   * @param opts query options. Options that do not make sense in the context of
+   *     a single document, such as start, will be ignored.
+   * @return a single document if present.
+   * @throws IOException
+   */
+  default Optional<V> get(K key, QueryOptions opts) throws IOException {
+    opts = opts.withStart(0).withLimit(2);
+    List<V> results;
+    try {
+      results = getSource(keyPredicate(key), opts).read().toList();
+    } catch (QueryParseException e) {
+      throw new IOException("Unexpected QueryParseException during get()", e);
+    } catch (OrmException e) {
+      throw new IOException(e);
+    }
+    switch (results.size()) {
+      case 0:
+        return Optional.empty();
+      case 1:
+        return Optional.of(results.get(0));
+      default:
+        throw new IOException("Multiple results found in index for key "
+            + key + ": " + results);
+    }
+  }
+
+  /**
+   * Get a predicate that looks up a single document by key.
+   *
+   * @param key document key.
+   * @return a single predicate.
+   */
+  Predicate<V> keyPredicate(K key);
+
+  /**
    * Mark whether this index is up-to-date and ready to serve reads.
    *
    * @param ready whether the index is ready
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
index cb7b3ef..406982a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
@@ -18,9 +18,16 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.Index;
 import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.account.AccountPredicates;
 
 public interface AccountIndex extends Index<Account.Id, AccountState> {
   public interface Factory extends
       IndexDefinition.IndexFactory<Account.Id, AccountState, AccountIndex> {
   }
+
+  @Override
+  default Predicate<AccountState> keyPredicate(Account.Id id) {
+    return AccountPredicates.id(id);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index 225b756..1672eea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -36,13 +36,20 @@
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.FieldType;
 import com.google.gerrit.server.index.SchemaUtil;
+import com.google.gerrit.server.index.change.StalenessChecker.RefState;
+import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.notedb.RobotCommentNotes;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -945,6 +952,76 @@
     return result;
   }
 
+  /**
+   * All values of all refs that were used in the course of indexing this
+   * document.
+   * <p>
+   * Emitted as UTF-8 encoded strings of the form
+   * {@code project:ref/name:[hex sha]}.
+   */
+  public static final FieldDef<ChangeData, Iterable<byte[]>> REF_STATE =
+      new FieldDef.Repeatable<ChangeData, byte[]>(
+          "ref_state", FieldType.STORED_ONLY, true) {
+        @Override
+        public Iterable<byte[]> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          List<byte[]> result = new ArrayList<>();
+          Project.NameKey project = input.change().getProject();
+
+          input.editRefs().values().forEach(
+              r -> result.add(RefState.of(r).toByteArray(project)));
+          input.starRefs().values().forEach(
+              r -> result.add(RefState.of(r.ref()).toByteArray(project)));
+
+          if (PrimaryStorage.of(input.change()) == PrimaryStorage.NOTE_DB) {
+            ChangeNotes notes = input.notes();
+            result.add(RefState.create(notes.getRefName(), notes.getMetaId())
+                .toByteArray(project));
+            notes.getRobotComments(); // Force loading robot comments.
+            RobotCommentNotes robotNotes = notes.getRobotCommentNotes();
+            result.add(
+                RefState.create(robotNotes.getRefName(), robotNotes.getMetaId())
+                    .toByteArray(project));
+            input.draftRefs().values().forEach(
+                r -> result.add(RefState.of(r).toByteArray(args.allUsers)));
+          }
+
+          return result;
+        }
+      };
+
+  /**
+   * All ref wildcard patterns that were used in the course of indexing this
+   * document.
+   * <p>
+   * Emitted as UTF-8 encoded strings of the form {@code project:ref/name/*}.
+   * See {@link RefStatePattern} for the pattern format.
+   */
+  public static final FieldDef<ChangeData, Iterable<byte[]>>
+      REF_STATE_PATTERN = new FieldDef.Repeatable<ChangeData, byte[]>(
+          "ref_state_pattern", FieldType.STORED_ONLY, true) {
+        @Override
+        public Iterable<byte[]> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Change.Id id = input.getId();
+          Project.NameKey project = input.change().getProject();
+          List<byte[]> result = new ArrayList<>(3);
+          result.add(RefStatePattern.create(
+                  RefNames.REFS_USERS + "*/" + RefNames.EDIT_PREFIX + id + "/*")
+              .toByteArray(project));
+          if (PrimaryStorage.of(input.change()) == PrimaryStorage.NOTE_DB) {
+            result.add(
+                RefStatePattern.create(
+                    RefNames.refsStarredChangesPrefix(id) + "*")
+                .toByteArray(args.allUsers));
+            result.add(RefStatePattern.create(
+                    RefNames.refsDraftCommentsPrefix(id) + "*")
+                .toByteArray(args.allUsers));
+          }
+          return result;
+        }
+      };
+
   public static final Integer NOT_REVIEWED = -1;
 
   private static String getTopic(ChangeData input) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java
index 9545c0a..c56880f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -17,10 +17,17 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.Index;
 import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.LegacyChangeIdPredicate;
 
 public interface ChangeIndex extends Index<Change.Id, ChangeData> {
   public interface Factory extends
       IndexDefinition.IndexFactory<Change.Id, ChangeData, ChangeIndex> {
   }
+
+  @Override
+  default Predicate<ChangeData> keyPredicate(Change.Id id) {
+    return new LegacyChangeIdPredicate(id);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index f881f2e..f256707 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.index.change;
 
 import static com.google.gerrit.server.extensions.events.EventUtil.logEventListenerError;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 
 import com.google.common.base.Function;
 import com.google.common.util.concurrent.Atomics;
@@ -28,7 +29,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -43,6 +46,7 @@
 import com.google.inject.assistedinject.AssistedInject;
 import com.google.inject.util.Providers;
 
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -102,16 +106,23 @@
   private final ChangeNotes.Factory changeNotesFactory;
   private final ChangeData.Factory changeDataFactory;
   private final ThreadLocalRequestContext context;
+  private final ListeningExecutorService batchExecutor;
   private final ListeningExecutorService executor;
   private final DynamicSet<ChangeIndexedListener> indexedListeners;
+  private final StalenessChecker stalenessChecker;
+  private final boolean reindexAfterIndexUpdate;
 
   @AssistedInject
-  ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory,
+  ChangeIndexer(
+      @GerritServerConfig Config cfg,
+      SchemaFactory<ReviewDb> schemaFactory,
       NotesMigration notesMigration,
       ChangeNotes.Factory changeNotesFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
       DynamicSet<ChangeIndexedListener> indexedListeners,
+      StalenessChecker stalenessChecker,
+      @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
       @Assisted ListeningExecutorService executor,
       @Assisted ChangeIndex index) {
     this.executor = executor;
@@ -121,17 +132,23 @@
     this.changeDataFactory = changeDataFactory;
     this.context = context;
     this.indexedListeners = indexedListeners;
+    this.stalenessChecker = stalenessChecker;
+    this.batchExecutor = batchExecutor;
+    this.reindexAfterIndexUpdate = reindexAfterIndexUpdate(cfg);
     this.index = index;
     this.indexes = null;
   }
 
   @AssistedInject
   ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory,
+      @GerritServerConfig Config cfg,
       NotesMigration notesMigration,
       ChangeNotes.Factory changeNotesFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
       DynamicSet<ChangeIndexedListener> indexedListeners,
+      StalenessChecker stalenessChecker,
+      @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
       @Assisted ListeningExecutorService executor,
       @Assisted ChangeIndexCollection indexes) {
     this.executor = executor;
@@ -141,10 +158,17 @@
     this.changeDataFactory = changeDataFactory;
     this.context = context;
     this.indexedListeners = indexedListeners;
+    this.stalenessChecker = stalenessChecker;
+    this.batchExecutor = batchExecutor;
+    this.reindexAfterIndexUpdate = reindexAfterIndexUpdate(cfg);
     this.index = null;
     this.indexes = indexes;
   }
 
+  private static boolean reindexAfterIndexUpdate(Config cfg) {
+    return cfg.getBoolean("index", null, "testReindexAfterUpdate", true);
+  }
+
   /**
    * Start indexing a change.
    *
@@ -181,6 +205,26 @@
       i.replace(cd);
     }
     fireChangeIndexedEvent(cd.getId().get());
+
+    // Always double-check whether the change might be stale immediately after
+    // interactively indexing it. This fixes up the case where two writers write
+    // to the primary storage in one order, and the corresponding index writes
+    // happen in the opposite order:
+    //  1. Writer A writes to primary storage.
+    //  2. Writer B writes to primary storage.
+    //  3. Writer B updates index.
+    //  4. Writer A updates index.
+    //
+    // Without the extra reindexIfStale step, A has no way of knowing that it's
+    // about to overwrite the index document with stale data. It doesn't work to
+    // have A check for staleness before attempting its index update, because
+    // B's index update might not have happened when it does the check.
+    //
+    // With the extra reindexIfStale step after (3)/(4), we are able to detect
+    // and fix the staleness. It doesn't matter which order the two
+    // reindexIfStale calls actually execute in; we are guaranteed that at least
+    // one of them will execute after the second index write, (4).
+    reindexAfterIndexUpdate(cd);
   }
 
   private void fireChangeIndexedEvent(int id) {
@@ -212,6 +256,8 @@
   public void index(ReviewDb db, Change change)
       throws IOException, OrmException {
     index(newChangeData(db, change));
+    // See comment in #index(ChangeData).
+    reindexAfterIndexUpdate(change.getProject(), change.getId());
   }
 
   /**
@@ -223,7 +269,10 @@
    */
   public void index(ReviewDb db, Project.NameKey project, Change.Id changeId)
       throws IOException, OrmException {
-    index(newChangeData(db, project, changeId));
+    ChangeData cd = newChangeData(db, project, changeId);
+    index(cd);
+    // See comment in #index(ChangeData).
+    reindexAfterIndexUpdate(cd);
   }
 
   /**
@@ -245,28 +294,68 @@
     new DeleteTask(id).call();
   }
 
+  /**
+   * Asynchronously check if a change is stale, and reindex if it is.
+   * <p>
+   * Always run on the batch executor, even if this indexer instance is
+   * configured to use a different executor.
+   *
+   * @param project the project to which the change belongs.
+   * @param id ID of the change to index.
+   * @return future for reindexing the change; returns true if the change was
+   *     stale.
+   */
+  public CheckedFuture<Boolean, IOException> reindexIfStale(
+      Project.NameKey project, Change.Id id) {
+    return submit(new ReindexIfStaleTask(project, id), batchExecutor);
+  }
+
+  private void reindexAfterIndexUpdate(ChangeData cd) throws IOException {
+    try {
+      reindexAfterIndexUpdate(cd.project(), cd.getId());
+    } catch (OrmException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private void reindexAfterIndexUpdate(Project.NameKey project, Change.Id id) {
+    if (reindexAfterIndexUpdate) {
+      reindexIfStale(project, id);
+    }
+  }
+
   private Collection<ChangeIndex> getWriteIndexes() {
     return indexes != null
         ? indexes.getWriteIndexes()
         : Collections.singleton(index);
   }
 
-  private CheckedFuture<?, IOException> submit(Callable<?> task) {
+  private <T> CheckedFuture<T, IOException> submit(Callable<T> task) {
+    return submit(task, executor);
+  }
+
+  private static <T> CheckedFuture<T, IOException> submit(Callable<T> task,
+      ListeningExecutorService executor) {
     return Futures.makeChecked(
         Futures.nonCancellationPropagating(executor.submit(task)), MAPPER);
   }
 
-  private class IndexTask implements Callable<Void> {
-    private final Project.NameKey project;
-    private final Change.Id id;
+  private abstract class AbstractIndexTask<T> implements Callable<T> {
+    protected final Project.NameKey project;
+    protected final Change.Id id;
 
-    private IndexTask(Project.NameKey project, Change.Id id) {
+    protected AbstractIndexTask(Project.NameKey project, Change.Id id) {
       this.project = project;
       this.id = id;
     }
 
+    protected abstract T callImpl(Provider<ReviewDb> db) throws Exception;
+
     @Override
-    public Void call() throws Exception {
+    public abstract String toString();
+
+    @Override
+    public final T call() throws Exception {
       try {
         final AtomicReference<Provider<ReviewDb>> dbRef =
             Atomics.newReference();
@@ -295,10 +384,7 @@
         };
         RequestContext oldCtx = context.setContext(newCtx);
         try {
-          ChangeData cd = newChangeData(
-              newCtx.getReviewDbProvider().get(), project, id);
-          index(cd);
-          return null;
+          return callImpl(newCtx.getReviewDbProvider());
         } finally  {
           context.setContext(oldCtx);
           Provider<ReviewDb> db = dbRef.get();
@@ -307,17 +393,31 @@
           }
         }
       } catch (Exception e) {
-        log.error(String.format("Failed to index change %d", id.get()), e);
+        log.error("Failed to execute " + this, e);
         throw e;
       }
     }
+  }
+
+  private class IndexTask extends AbstractIndexTask<Void> {
+    private IndexTask(Project.NameKey project, Change.Id id) {
+      super(project, id);
+    }
+
+    @Override
+    public Void callImpl(Provider<ReviewDb> db) throws Exception {
+      ChangeData cd = newChangeData(db.get(), project, id);
+      index(cd);
+      return null;
+    }
 
     @Override
     public String toString() {
-      return "index-change-" + id.get();
+      return "index-change-" + id;
     }
   }
 
+  // Not AbstractIndexTask as it doesn't need ReviewDb.
   private class DeleteTask implements Callable<Void> {
     private final Change.Id id;
 
@@ -339,6 +439,26 @@
     }
   }
 
+  private class ReindexIfStaleTask extends AbstractIndexTask<Boolean> {
+    private ReindexIfStaleTask(Project.NameKey project, Change.Id id) {
+      super(project, id);
+    }
+
+    @Override
+    public Boolean callImpl(Provider<ReviewDb> db) throws Exception {
+      if (!stalenessChecker.isStale(id)) {
+        return false;
+      }
+      index(newChangeData(db.get(), project, id));
+      return true;
+    }
+
+    @Override
+    public String toString() {
+      return "reindex-if-stale-change-" + id;
+    }
+  }
+
   // Avoid auto-rebuilding when reindexing if reading is disabled. This just
   // increases contention on the meta ref from a background indexing thread
   // with little benefit. The next actual write to the entity may still incur a
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 8a793e5..49bccf2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -73,12 +73,18 @@
       .add(ChangeField.LABEL2)
       .build();
 
+  @Deprecated
   static final Schema<ChangeData> V35 =
       schema(V34,
           ChangeField.SUBMIT_RECORD,
           ChangeField.STORED_SUBMIT_RECORD_LENIENT,
           ChangeField.STORED_SUBMIT_RECORD_STRICT);
 
+  static final Schema<ChangeData> V36 =
+      schema(V35,
+          ChangeField.REF_STATE,
+          ChangeField.REF_STATE_PATTERN);
+
   public static final String NAME = "changes";
   public static final ChangeSchemaDefinitions INSTANCE =
       new ChangeSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
new file mode 100644
index 0000000..0c3d89c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -0,0 +1,308 @@
+// Copyright (C) 2016 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.change;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+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.git.GitRepositoryManager;
+import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.StreamSupport;
+
+@Singleton
+public class StalenessChecker {
+  private static final Logger log =
+      LoggerFactory.getLogger(StalenessChecker.class);
+
+  private final ImmutableSet<String> FIELDS = ImmutableSet.of(
+      ChangeField.CHANGE.getName(),
+      ChangeField.REF_STATE.getName(),
+      ChangeField.REF_STATE_PATTERN.getName());
+
+  private final ChangeIndexCollection indexes;
+  private final GitRepositoryManager repoManager;
+  private final IndexConfig indexConfig;
+  private final Provider<ReviewDb> db;
+
+  @Inject
+  StalenessChecker(
+      ChangeIndexCollection indexes,
+      GitRepositoryManager repoManager,
+      IndexConfig indexConfig,
+      Provider<ReviewDb> db) {
+    this.indexes = indexes;
+    this.repoManager = repoManager;
+    this.indexConfig = indexConfig;
+    this.db = db;
+  }
+
+  boolean isStale(Change.Id id) throws IOException, OrmException {
+    ChangeIndex i = indexes.getSearchIndex();
+    if (i == null) {
+      return false; // No index; caller couldn't do anything if it is stale.
+    }
+    if (!i.getSchema().hasField(ChangeField.REF_STATE)
+        || !i.getSchema().hasField(ChangeField.REF_STATE_PATTERN)) {
+      return false; // Index version not new enough for this check.
+    }
+
+    Optional<ChangeData> result = i.get(
+        id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, FIELDS));
+    if (!result.isPresent()) {
+      return true; // Not in index, but caller wants it to be.
+    }
+    ChangeData cd = result.get();
+    if (reviewDbChangeIsStale(
+        cd.change(),
+        ChangeNotes.readOneReviewDbChange(db.get(), cd.getId()))) {
+      return true;
+    }
+
+    return isStale(repoManager, id, parseStates(cd), parsePatterns(cd));
+  }
+
+  @VisibleForTesting
+  static boolean isStale(GitRepositoryManager repoManager,
+      Change.Id id,
+      SetMultimap<Project.NameKey, RefState> states,
+      Multimap<Project.NameKey, RefStatePattern> patterns) {
+    Set<Project.NameKey> projects =
+        Sets.union(states.keySet(), patterns.keySet());
+
+    for (Project.NameKey p : projects) {
+      if (isStale(repoManager, id, p, states, patterns)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  @VisibleForTesting
+  static boolean reviewDbChangeIsStale(
+      Change indexChange, @Nullable Change reviewDbChange) {
+    if (reviewDbChange == null) {
+      return false; // Nothing the caller can do.
+    }
+    checkArgument(indexChange.getId().equals(reviewDbChange.getId()),
+        "mismatched change ID: %s != %s",
+        indexChange.getId(), reviewDbChange.getId());
+    if (PrimaryStorage.of(reviewDbChange) != PrimaryStorage.REVIEW_DB) {
+      return false; // Not a ReviewDb change, don't check rowVersion.
+    }
+    return reviewDbChange.getRowVersion() != indexChange.getRowVersion();
+  }
+
+  private SetMultimap<Project.NameKey, RefState> parseStates(ChangeData cd) {
+    return parseStates(cd.getRefStates());
+  }
+
+  @VisibleForTesting
+  static SetMultimap<Project.NameKey, RefState> parseStates(
+      Iterable<byte[]> states) {
+    RefState.check(states != null, null);
+    SetMultimap<Project.NameKey, RefState> result = HashMultimap.create();
+    for (byte[] b : states) {
+      RefState.check(b != null, null);
+      String s = new String(b, UTF_8);
+      List<String> parts = Splitter.on(':').splitToList(s);
+      RefState.check(
+          parts.size() == 3
+              && !parts.get(0).isEmpty()
+              && !parts.get(1).isEmpty(),
+          s);
+      result.put(
+          new Project.NameKey(parts.get(0)),
+          RefState.create(parts.get(1), parts.get(2)));
+    }
+    return result;
+  }
+
+  private Multimap<Project.NameKey, RefStatePattern> parsePatterns(
+      ChangeData cd) {
+    return parsePatterns(cd.getRefStatePatterns());
+  }
+
+  public static ListMultimap<Project.NameKey, RefStatePattern> parsePatterns(
+      Iterable<byte[]> patterns) {
+    RefStatePattern.check(patterns != null, null);
+    ListMultimap<Project.NameKey, RefStatePattern> result =
+        ArrayListMultimap.create();
+    for (byte[] b : patterns) {
+      RefStatePattern.check(b != null, null);
+      String s = new String(b, UTF_8);
+      List<String> parts = Splitter.on(':').splitToList(s);
+      RefStatePattern.check(parts.size() == 2, s);
+      result.put(
+          new Project.NameKey(parts.get(0)),
+          RefStatePattern.create(parts.get(1)));
+    }
+    return result;
+  }
+
+  private static boolean isStale(GitRepositoryManager repoManager,
+      Change.Id id, Project.NameKey project,
+      SetMultimap<Project.NameKey, RefState> allStates,
+      Multimap<Project.NameKey, RefStatePattern> allPatterns) {
+    try (Repository repo = repoManager.openRepository(project)) {
+      Set<RefState> states = allStates.get(project);
+      for (RefState state : states) {
+        if (!state.match(repo)) {
+          return true;
+        }
+      }
+      for (RefStatePattern pattern : allPatterns.get(project)) {
+        if (!pattern.match(repo, states)) {
+          return true;
+        }
+      }
+      return false;
+    } catch (IOException e) {
+      log.warn(
+          String.format("error checking staleness of %s in %s", id, project),
+          e);
+      return true;
+    }
+  }
+
+  @AutoValue
+  public abstract static class RefState {
+    static RefState create(String ref, String sha) {
+      return new AutoValue_StalenessChecker_RefState(
+          ref, ObjectId.fromString(sha));
+    }
+
+    static RefState create(String ref, @Nullable ObjectId id) {
+      return new AutoValue_StalenessChecker_RefState(
+          ref, firstNonNull(id, ObjectId.zeroId()));
+    }
+
+    static RefState of(Ref ref) {
+      return new AutoValue_StalenessChecker_RefState(
+          ref.getName(), ref.getObjectId());
+    }
+
+    byte[] toByteArray(Project.NameKey project) {
+      byte[] a = (project.toString() + ':' + ref() + ':').getBytes(UTF_8);
+      byte[] b = new byte[a.length + Constants.OBJECT_ID_STRING_LENGTH];
+      System.arraycopy(a, 0, b, 0, a.length);
+      id().copyTo(b, a.length);
+      return b;
+    }
+
+    private static void check(boolean condition, String str) {
+      checkArgument(condition, "invalid RefState: %s", str);
+    }
+
+    abstract String ref();
+    abstract ObjectId id();
+
+    private boolean match(Repository repo) throws IOException {
+      Ref ref = repo.exactRef(ref());
+      ObjectId expected = ref != null ? ref.getObjectId() : ObjectId.zeroId();
+      return id().equals(expected);
+    }
+  }
+
+  /**
+   * Pattern for matching refs.
+   * <p>
+   * Similar to '*' syntax for native Git refspecs, but slightly more powerful:
+   * the pattern may contain arbitrarily many asterisks. There must be at least
+   * one '*' and the first one must immediately follow a '/'.
+   */
+  @AutoValue
+  public abstract static class RefStatePattern {
+    static RefStatePattern create(String pattern) {
+      int star = pattern.indexOf('*');
+      check(star > 0 && pattern.charAt(star - 1) == '/', pattern);
+      String prefix = pattern.substring(0, star);
+      check(Repository.isValidRefName(pattern.replace('*', 'x')), pattern);
+
+      // Quote everything except the '*'s, which become ".*".
+      String regex =
+          StreamSupport.stream(Splitter.on('*').split(pattern).spliterator(), false)
+              .map(Pattern::quote)
+              .collect(joining(".*", "^", "$"));
+      return new AutoValue_StalenessChecker_RefStatePattern(
+          pattern, prefix, Pattern.compile(regex));
+    }
+
+    byte[] toByteArray(Project.NameKey project) {
+      return (project.toString() + ':' + pattern()).getBytes(UTF_8);
+    }
+
+    private static void check(boolean condition, String str) {
+      checkArgument(condition, "invalid RefStatePattern: %s", str);
+    }
+
+    abstract String pattern();
+    abstract String prefix();
+    abstract Pattern regex();
+
+    boolean match(String refName) {
+      return regex().matcher(refName).find();
+    }
+
+    private boolean match(Repository repo, Set<RefState> expected)
+        throws IOException {
+      for (Ref r : repo.getRefDatabase().getRefs(prefix()).values()) {
+        if (!match(r.getName())) {
+          continue;
+        }
+        if (!expected.contains(RefState.of(r))) {
+          return false;
+        }
+      }
+      return true;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index d0b0c54..a3ea381 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -372,6 +372,10 @@
     return change;
   }
 
+  public ObjectId getMetaId() {
+    return state.metaId();
+  }
+
   public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() {
     if (patchSets == null) {
       ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b =
@@ -510,7 +514,7 @@
     return draftCommentNotes;
   }
 
-  RobotCommentNotes getRobotCommentNotes() {
+  public RobotCommentNotes getRobotCommentNotes() {
     return robotCommentNotes;
   }
 
@@ -532,7 +536,7 @@
   }
 
   @Override
-  protected String getRefName() {
+  public String getRefName() {
     return changeMetaRef(getChangeId());
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
index f60fd2d..ae280f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Multimap;
+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.client.RefNames;
@@ -43,6 +44,7 @@
 
   private ImmutableListMultimap<RevId, RobotComment> comments;
   private RevisionNoteMap<RobotCommentsRevisionNote> revisionNoteMap;
+  private ObjectId metaId;
 
   @AssistedInject
   RobotCommentNotes(
@@ -70,20 +72,26 @@
   }
 
   @Override
-  protected String getRefName() {
+  public String getRefName() {
     return RefNames.robotCommentsRef(getChangeId());
   }
 
+  @Nullable
+  public ObjectId getMetaId() {
+    return metaId;
+  }
+
   @Override
   protected void onLoad(LoadHandle handle)
       throws IOException, ConfigInvalidException {
-    ObjectId rev = handle.id();
-    if (rev == null) {
+    metaId = handle.id();
+    if (metaId == null) {
       loadDefaults();
       return;
     }
+    metaId = metaId.copy();
 
-    RevCommit tipCommit = handle.walk().parseCommit(rev);
+    RevCommit tipCommit = handle.walk().parseCommit(metaId);
     ObjectReader reader = handle.walk().getObjectReader();
     revisionNoteMap = RevisionNoteMap.parseRobotComments(args.noteUtil, reader,
         NoteMap.read(reader, tipCommit));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
index b3f92ff..9a9ec5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -47,7 +47,7 @@
     return Predicate.or(preds);
   }
 
-  static Predicate<AccountState> id(Account.Id accountId) {
+  public static Predicate<AccountState> id(Account.Id accountId) {
     return new AccountPredicate(AccountField.ID,
         AccountQueryBuilder.FIELD_ACCOUNT, accountId.toString());
   }
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 913cffd..01b4bc1 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
@@ -89,7 +89,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -348,9 +347,9 @@
   private SubmitTypeRecord submitTypeRecord;
   private Boolean mergeable;
   private Set<String> hashtags;
-  private Set<Account.Id> editsByUser;
+  private Map<Account.Id, Ref> editsByUser;
   private Set<Account.Id> reviewedBy;
-  private Set<Account.Id> draftsByUser;
+  private Map<Account.Id, Ref> draftsByUser;
   @Deprecated
   private Set<Account.Id> starredByUser;
   private ImmutableMultimap<Account.Id, String> stars;
@@ -360,6 +359,9 @@
   private PersonIdent author;
   private PersonIdent committer;
 
+  private ImmutableList<byte[]> refStates;
+  private ImmutableList<byte[]> refStatePatterns;
+
   @AssistedInject
   private ChangeData(
       GitRepositoryManager repoManager,
@@ -1098,21 +1100,25 @@
   }
 
   public Set<Account.Id> editsByUser() throws OrmException {
+    return editRefs().keySet();
+  }
+
+  public Map<Account.Id, Ref> editRefs() throws OrmException {
     if (editsByUser == null) {
       if (!lazyLoad) {
-        return Collections.emptySet();
+        return Collections.emptyMap();
       }
       Change c = change();
       if (c == null) {
-        return Collections.emptySet();
+        return Collections.emptyMap();
       }
-      editsByUser = new HashSet<>();
+      editsByUser = new HashMap<>();
       Change.Id id = checkNotNull(change.getId());
       try (Repository repo = repoManager.openRepository(project())) {
-        for (String ref
-            : repo.getRefDatabase().getRefs(RefNames.REFS_USERS).keySet()) {
-          if (id.equals(Change.Id.fromEditRefPart(ref))) {
-            editsByUser.add(Account.Id.fromRefPart(ref));
+        for (Map.Entry<String, Ref> e
+            : repo.getRefDatabase().getRefs(RefNames.REFS_USERS).entrySet()) {
+          if (id.equals(Change.Id.fromEditRefPart(e.getKey()))) {
+            editsByUser.put(Account.Id.fromRefPart(e.getKey()), e.getValue());
           }
         }
       } catch (IOException e) {
@@ -1123,17 +1129,31 @@
   }
 
   public Set<Account.Id> draftsByUser() throws OrmException {
+    return draftRefs().keySet();
+  }
+
+  public Map<Account.Id, Ref> draftRefs() throws OrmException {
     if (draftsByUser == null) {
       if (!lazyLoad) {
-        return Collections.emptySet();
+        return Collections.emptyMap();
       }
       Change c = change();
       if (c == null) {
-        return Collections.emptySet();
+        return Collections.emptyMap();
       }
-      draftsByUser = new HashSet<>();
-      for (Comment sc : commentsUtil.draftByChange(db, notes())) {
-        draftsByUser.add(sc.author.getId());
+
+      draftsByUser = new HashMap<>();
+      if (notesMigration.readChanges()) {
+        for (Ref ref : commentsUtil.getDraftRefs(notes.getChangeId())) {
+          Account.Id account = Account.Id.fromRefSuffix(ref.getName());
+          if (account != null) {
+            draftsByUser.put(account, ref);
+          }
+        }
+      } else {
+        for (Comment sc : commentsUtil.draftByChange(db, notes())) {
+          draftsByUser.put(sc.author.getId(), null);
+        }
       }
     }
     return draftsByUser;
@@ -1262,4 +1282,20 @@
       this.deletions = deletions;
     }
   }
+
+  public ImmutableList<byte[]> getRefStates() {
+    return refStates;
+  }
+
+  public void setRefStates(Iterable<byte[]> refStates) {
+    this.refStates = ImmutableList.copyOf(refStates);
+  }
+
+  public ImmutableList<byte[]> getRefStatePatterns() {
+    return refStatePatterns;
+  }
+
+  public void setRefStatePatterns(Iterable<byte[]> refStatePatterns) {
+    this.refStatePatterns = ImmutableList.copyOf(refStatePatterns);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index 425eb00..f7f98d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -22,7 +22,7 @@
 public class LegacyChangeIdPredicate extends ChangeIndexPredicate {
   private final Change.Id id;
 
-  LegacyChangeIdPredicate(Change.Id id) {
+  public LegacyChangeIdPredicate(Change.Id id) {
     super(LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
     this.id = id;
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
new file mode 100644
index 0000000..cdd1e07
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -0,0 +1,359 @@
+// Copyright (C) 2016 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.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.server.index.change.StalenessChecker.isStale;
+import static com.google.gerrit.testutil.TestChanges.newChange;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.change.StalenessChecker.RefState;
+import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
+import com.google.gerrit.server.notedb.NoteDbChangeState;
+import com.google.gerrit.testutil.GerritBaseTests;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.stream.Stream;
+
+public class StalenessCheckerTest extends GerritBaseTests {
+  private static final String SHA1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+  private static final String SHA2 = "badc0feebadc0feebadc0feebadc0feebadc0fee";
+
+  private static final Project.NameKey P1 = new Project.NameKey("project1");
+  private static final Project.NameKey P2 = new Project.NameKey("project2");
+
+  private static final Change.Id C = new Change.Id(1234);
+
+  private static final ProtobufCodec<Change> CHANGE_CODEC =
+      CodecFactory.encoder(Change.class);
+
+  private GitRepositoryManager repoManager;
+  private Repository r1;
+  private Repository r2;
+  private TestRepository<Repository> tr1;
+  private TestRepository<Repository> tr2;
+
+  @Before
+  public void setUp() throws Exception {
+    repoManager = new InMemoryRepositoryManager();
+    r1 = repoManager.createRepository(P1);
+    tr1 = new TestRepository<>(r1);
+    r2 = repoManager.createRepository(P2);
+    tr2 = new TestRepository<>(r2);
+  }
+
+  @Test
+  public void parseStates() {
+    assertInvalidState(null);
+    assertInvalidState("");
+    assertInvalidState("project1:refs/heads/foo");
+    assertInvalidState("project1:refs/heads/foo:notasha");
+    assertInvalidState("project1:refs/heads/foo:");
+
+    assertThat(
+            StalenessChecker.parseStates(
+                byteArrays(
+                    P1 + ":refs/heads/foo:" + SHA1,
+                    P1 + ":refs/heads/bar:" + SHA2,
+                    P2 + ":refs/heads/baz:" + SHA1)))
+        .isEqualTo(
+            ImmutableSetMultimap.of(
+                P1, RefState.create("refs/heads/foo", SHA1),
+                P1, RefState.create("refs/heads/bar", SHA2),
+                P2, RefState.create("refs/heads/baz", SHA1)));
+  }
+
+  private static void assertInvalidState(String state) {
+    try {
+      StalenessChecker.parseStates(byteArrays(state));
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void refStateToByteArray() {
+    assertThat(
+            new String(
+                RefState.create("refs/heads/foo", ObjectId.fromString(SHA1))
+                    .toByteArray(P1),
+                UTF_8))
+        .isEqualTo(P1 + ":refs/heads/foo:" + SHA1);
+    assertThat(
+            new String(
+                RefState.create("refs/heads/foo", (ObjectId) null)
+                    .toByteArray(P1),
+                UTF_8))
+        .isEqualTo(P1 + ":refs/heads/foo:" + ObjectId.zeroId().name());
+  }
+
+  @Test
+  public void parsePatterns() {
+    assertInvalidPattern(null);
+    assertInvalidPattern("");
+    assertInvalidPattern("project:");
+    assertInvalidPattern("project:refs/heads/foo");
+    assertInvalidPattern("project:refs/he*ds/bar");
+    assertInvalidPattern("project:refs/(he)*ds/bar");
+    assertInvalidPattern("project:invalidrefname");
+
+    ListMultimap<Project.NameKey, RefStatePattern> r =
+        StalenessChecker.parsePatterns(
+            byteArrays(
+                P1 + ":refs/heads/*",
+                P2 + ":refs/heads/foo/*/bar",
+                P2 + ":refs/heads/foo/*-baz/*/quux"));
+
+    assertThat(r.keySet()).containsExactly(P1, P2);
+    RefStatePattern p = r.get(P1).get(0);
+    assertThat(p.pattern()).isEqualTo("refs/heads/*");
+    assertThat(p.prefix()).isEqualTo("refs/heads/");
+    assertThat(p.regex().pattern()).isEqualTo("^\\Qrefs/heads/\\E.*\\Q\\E$");
+    assertThat(p.match("refs/heads/foo")).isTrue();
+    assertThat(p.match("xrefs/heads/foo")).isFalse();
+    assertThat(p.match("refs/tags/foo")).isFalse();
+
+    p = r.get(P2).get(0);
+    assertThat(p.pattern()).isEqualTo("refs/heads/foo/*/bar");
+    assertThat(p.prefix()).isEqualTo("refs/heads/foo/");
+    assertThat(p.regex().pattern())
+        .isEqualTo("^\\Qrefs/heads/foo/\\E.*\\Q/bar\\E$");
+    assertThat(p.match("refs/heads/foo//bar")).isTrue();
+    assertThat(p.match("refs/heads/foo/x/bar")).isTrue();
+    assertThat(p.match("refs/heads/foo/x/y/bar")).isTrue();
+    assertThat(p.match("refs/heads/foo/x/baz")).isFalse();
+
+    p = r.get(P2).get(1);
+    assertThat(p.pattern()).isEqualTo("refs/heads/foo/*-baz/*/quux");
+    assertThat(p.prefix()).isEqualTo("refs/heads/foo/");
+    assertThat(p.regex().pattern())
+        .isEqualTo("^\\Qrefs/heads/foo/\\E.*\\Q-baz/\\E.*\\Q/quux\\E$");
+    assertThat(p.match("refs/heads/foo/-baz//quux")).isTrue();
+    assertThat(p.match("refs/heads/foo/x-baz/x/quux")).isTrue();
+    assertThat(p.match("refs/heads/foo/x/y-baz/x/y/quux")).isTrue();
+    assertThat(p.match("refs/heads/foo/x-baz/x/y")).isFalse();
+  }
+
+  @Test
+  public void refStatePatternToByteArray() {
+    assertThat(
+            new String(RefStatePattern.create("refs/*").toByteArray(P1), UTF_8))
+        .isEqualTo(P1 + ":refs/*");
+  }
+
+  private static void assertInvalidPattern(String state) {
+    try {
+      StalenessChecker.parsePatterns(byteArrays(state));
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void isStaleRefStatesOnly() throws Exception {
+    String ref1 = "refs/heads/foo";
+    ObjectId id1 = tr1.update(ref1, tr1.commit().message("commit 1"));
+    String ref2 = "refs/heads/bar";
+    ObjectId id2 = tr2.update(ref2, tr2.commit().message("commit 2"));
+
+    // Not stale.
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id1.name()),
+                    P2, RefState.create(ref2, id2.name())),
+                ImmutableMultimap.of()))
+        .isFalse();
+
+    // Wrong ref value.
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, SHA1),
+                    P2, RefState.create(ref2, id2.name())),
+                ImmutableMultimap.of()))
+        .isTrue();
+
+    // Swapped repos.
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id2.name()),
+                    P2, RefState.create(ref2, id1.name())),
+                ImmutableMultimap.of()))
+        .isTrue();
+
+    // Two refs in same repo, not stale.
+    String ref3 = "refs/heads/baz";
+    ObjectId id3 = tr1.update(ref3, tr1.commit().message("commit 3"));
+    tr1.update(ref3, id3);
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id1.name()),
+                    P1, RefState.create(ref3, id3.name())),
+                ImmutableMultimap.of()))
+        .isFalse();
+
+    // Ignore ref not mentioned.
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id1.name())),
+                ImmutableMultimap.of()))
+        .isFalse();
+
+    // One ref wrong.
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id1.name()),
+                    P1, RefState.create(ref3, SHA1)),
+                ImmutableMultimap.of()))
+        .isTrue();
+  }
+
+  @Test
+  public void isStaleWithRefStatePatterns() throws Exception {
+    String ref1 = "refs/heads/foo";
+    ObjectId id1 = tr1.update(ref1, tr1.commit().message("commit 1"));
+
+    // ref1 is only ref matching pattern.
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id1.name())),
+                ImmutableMultimap.of(
+                    P1, RefStatePattern.create("refs/heads/*"))))
+        .isFalse();
+
+    // Now ref2 matches pattern, so stale unless ref2 is present in state map.
+    String ref2 = "refs/heads/bar";
+    ObjectId id2 = tr1.update(ref2, tr1.commit().message("commit 2"));
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id1.name())),
+                ImmutableMultimap.of(
+                    P1, RefStatePattern.create("refs/heads/*"))))
+        .isTrue();
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id1.name()),
+                    P1, RefState.create(ref2, id2.name())),
+                ImmutableMultimap.of(
+                    P1, RefStatePattern.create("refs/heads/*"))))
+        .isFalse();
+  }
+
+  @Test
+  public void isStaleWithNonPrefixPattern() throws Exception {
+    String ref1 = "refs/heads/foo";
+    ObjectId id1 = tr1.update(ref1, tr1.commit().message("commit 1"));
+    tr1.update("refs/heads/bar", tr1.commit().message("commit 2"));
+
+    // ref1 is only ref matching pattern.
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id1.name())),
+                ImmutableMultimap.of(
+                    P1, RefStatePattern.create("refs/*/foo"))))
+        .isFalse();
+
+    // Now ref2 matches pattern, so stale unless ref2 is present in state map.
+    String ref3 = "refs/other/foo";
+    ObjectId id3 = tr1.update(ref3, tr1.commit().message("commit 3"));
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id1.name())),
+                ImmutableMultimap.of(
+                    P1, RefStatePattern.create("refs/*/foo"))))
+        .isTrue();
+    assertThat(
+            isStale(
+                repoManager, C,
+                ImmutableSetMultimap.of(
+                    P1, RefState.create(ref1, id1.name()),
+                    P1, RefState.create(ref3, id3.name())),
+                ImmutableMultimap.of(
+                    P1, RefStatePattern.create("refs/*/foo"))))
+        .isFalse();
+  }
+
+  @Test
+  public void reviewDbChangeIsStale() throws Exception {
+    Change indexChange = newChange(P1, new Account.Id(1));
+    indexChange.setNoteDbState(SHA1);
+
+    assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, null))
+        .isFalse();
+
+    Change noteDbPrimary = clone(indexChange);
+    noteDbPrimary.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
+    assertThat(
+            StalenessChecker.reviewDbChangeIsStale(indexChange, noteDbPrimary))
+        .isFalse();
+
+    assertThat(
+            StalenessChecker.reviewDbChangeIsStale(
+                indexChange, clone(indexChange)))
+        .isFalse();
+
+    // Can't easily change row version to check true case.
+  }
+
+  private static Iterable<byte[]> byteArrays(String... strs) {
+    return Stream.of(strs).map(s -> s != null ? s.getBytes(UTF_8) : null)
+        .collect(toList());
+  }
+
+  private static Change clone(Change change) {
+    return CHANGE_CODEC.decode(CHANGE_CODEC.encodeToByteArray(change));
+  }
+
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 5899e11..d22e7a8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -52,6 +52,7 @@
 import com.google.gerrit.reviewdb.client.Patch;
 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;
@@ -1611,6 +1612,32 @@
     cd.currentApprovals();
   }
 
+  @Test
+  public void reindexIfStale() throws Exception {
+    Account.Id user = createAccount("user");
+    Project.NameKey project = new Project.NameKey("repo");
+    TestRepository<Repo> repo = createProject(project.get());
+    Change change = insert(repo, newChange(repo));
+    PatchSet ps = db.patchSets().get(change.currentPatchSetId());
+
+    requestContext.setContext(newRequestContext(user));
+    assertThat(changeEditModifier.createEdit(change, ps))
+        .isEqualTo(RefUpdate.Result.NEW);
+    assertQuery("has:edit", change);
+    assertThat(indexer.reindexIfStale(project, change.getId()).get()).isFalse();
+
+    // Delete edit ref behind index's back.
+    RefUpdate ru = repo.getRepository().updateRef(
+        RefNames.refsEdit(user, change.getId(), ps.getId()));
+    ru.setForceUpdate(true);
+    assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+
+    // Index is stale.
+    assertQuery("has:edit", change);
+    assertThat(indexer.reindexIfStale(project, change.getId()).get()).isTrue();
+    assertQuery("has:edit");
+  }
+
   protected ChangeInserter newChange(TestRepository<Repo> repo)
       throws Exception {
     return newChange(repo, null, null, null, null);
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index 1103c03..eca40a6 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -78,6 +78,10 @@
     'gr-diff gr-syntax gr-syntax-selector-class': true,
   };
 
+  var CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
+  var JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
+  var GLOBAL_LT_PATTERN = /</g;
+
   Polymer({
     is: 'gr-syntax-layer',
 
@@ -301,6 +305,7 @@
       var result;
 
       if (this._baseLanguage && baseLine !== undefined) {
+        baseLine = this._workaround(this._baseLanguage, baseLine);
         result = this._hljs.highlight(this._baseLanguage, baseLine, true,
             state.baseContext);
         this.push('_baseRanges', this._rangesFromString(result.value));
@@ -308,6 +313,7 @@
       }
 
       if (this._revisionLanguage && revisionLine !== undefined) {
+        revisionLine = this._workaround(this._revisionLanguage, revisionLine);
         result = this._hljs.highlight(this._revisionLanguage, revisionLine,
             true, state.revisionContext);
         this.push('_revisionRanges', this._rangesFromString(result.value));
@@ -316,6 +322,50 @@
     },
 
     /**
+     * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained
+     * cases before sending them into HLJS so that they parse correctly.
+     *
+     * Important notes:
+     * * These tests should be as constrained as possible to avoid interfering
+     *   with code it shouldn't AND to avoid executing regexes as much as
+     *   possible.
+     * * These tests should document the issue clearly enough that the test can
+     *   be condidently removed when the issue is solved in HLJS.
+     * * These tests should rewrite the line of code to have the same number of
+     *   characters. This method rewrites the string that gets parsed, but NOT
+     *   the string that gets displayed and highlighted. Thus, the positions
+     *   must be consistent.
+     *
+     * @param {!string} language The name of the HLJS language plugin in use.
+     * @param {!string} line The line of code to potentially rewrite.
+     * @return {string} A potentially-rewritten line of code.
+     */
+    _workaround: function(language, line) {
+      /**
+       * Prevent confusing < and << operators for the start of a meta string by
+       * converting them to a different operator.
+       * {@see Issue 4864}
+       * {@see https://github.com/isagalaev/highlight.js/issues/1341}
+       */
+      if (language === 'cpp' && CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) {
+        return line.replace(GLOBAL_LT_PATTERN, '|');
+      }
+
+      /**
+       * Prevent confusing the closing paren of a parameterized Java annotation
+       * being applied to a formal argument as the closing paren of the argument
+       * list. Rewrite the parens as spaces.
+       * {@see Issue 4776}
+       * {@see https://github.com/isagalaev/highlight.js/issues/1324}
+       */
+      if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) {
+        return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 ');
+      }
+
+      return line;
+    },
+
+    /**
      * Tells whether the state has exhausted its current section.
      * @param {!Object} state
      * @return {boolean}
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index aa37f1a..01e9325 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -404,5 +404,41 @@
       state = {sectionIndex: 3, lineIndex: 4};
       assert.isTrue(element._isSectionDone(state));
     });
+
+    test('workaround CPP LT directive', function() {
+      // Does nothing to regular line.
+      var line = 'int main(int argc, char** argv) { return 0; }';
+      assert.equal(element._workaround('cpp', line), line);
+
+      // Does nothing to include directive.
+      line = '#include <stdio>';
+      assert.equal(element._workaround('cpp', line), line);
+
+      // Converts left-shift operator in #define.
+      line = '#define GiB (1ull << 30)';
+      var expected = '#define GiB (1ull || 30)';
+      assert.equal(element._workaround('cpp', line), expected);
+
+      // Converts less-than operator in #if.
+      line = '  #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)';
+      expected = '  #if __GNUC__ | 4 || (__GNUC__ == 4 && __GNUC_MINOR__ | 1)';
+      assert.equal(element._workaround('cpp', line), expected);
+    });
+
+    test('workaround Java param-annotation', function() {
+      // Does nothing to regular line.
+      var line = 'public static void foo(int bar) { }';
+      assert.equal(element._workaround('java', line), line);
+
+      // Does nothing to regular annotation.
+      var line = 'public static void foo(@Nullable int bar) { }';
+      assert.equal(element._workaround('java', line), line);
+
+      // Converts parameterized annotation.
+      line = 'public static void foo(@SuppressWarnings("unused") int bar) { }';
+      var expected = 'public static void foo(@SuppressWarnings "unused" ' +
+          ' int bar) { }';
+      assert.equal(element._workaround('java', line), expected);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 164bb2d..8ca0e72 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -75,7 +75,6 @@
       }
       :host([disabled]) {
         cursor: default;
-        pointer-events: none;
       }
       :host([loading]),
       :host([loading][disabled]) {
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 01d2585..7e91b0e 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -29,6 +29,11 @@
       },
     },
 
+    listeners: {
+      'tap': '_handleAction',
+      'click': '_handleAction',
+    },
+
     behaviors: [
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.TooltipBehavior,
@@ -43,6 +48,13 @@
       'space enter': '_handleCommitKey',
     },
 
+    _handleAction: function(e) {
+      if (this.disabled) {
+        e.preventDefault();
+        e.stopImmediatePropagation();
+      }
+    },
+
     _disabledChanged: function(disabled) {
       if (disabled) {
         this._enabledTabindex = this.getAttribute('tabindex');
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
new file mode 100644
index 0000000..70cf636
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-button</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-button.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-button></gr-button>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-select tests', function() {
+    var element;
+    var sandbox;
+
+    var addSpyOn = function(eventName) {
+      var spy = sandbox.spy();
+      element.addEventListener(eventName, spy);
+      return spy;
+    };
+
+    setup(function() {
+      element = fixture('basic');
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(function() {
+      sandbox.restore();
+    });
+
+    ['tap', 'click'].forEach(function(eventName) {
+      test('dispatches ' + eventName + ' event', function() {
+        var spy = addSpyOn(eventName);
+        MockInteractions.tap(element);
+        assert.isTrue(spy.calledOnce);
+      });
+    });
+
+    // Keycodes: 32 for Space, 13 for Enter.
+    [32, 13].forEach(function(key) {
+      test('dispatches tap event on keycode ' + key, function() {
+        var tapSpy = sandbox.spy();
+        element.addEventListener('tap', tapSpy);
+        MockInteractions.pressAndReleaseKeyOn(element, key);
+        assert.isTrue(tapSpy.calledOnce);
+      })});
+
+    suite('disabled', function() {
+      setup(function() {
+        element.disabled = true;
+      });
+
+      ['tap', 'click'].forEach(function(eventName) {
+        test('stops ' + eventName + ' event', function() {
+          var spy = addSpyOn(eventName);
+          MockInteractions.tap(element);
+          assert.isFalse(spy.called);
+        });
+      });
+
+      // Keycodes: 32 for Space, 13 for Enter.
+      [32, 13].forEach(function(key) {
+        test('stops tap event on keycode ' + key, function() {
+          var tapSpy = sandbox.spy();
+          element.addEventListener('tap', tapSpy);
+          MockInteractions.pressAndReleaseKeyOn(element, key);
+          assert.isFalse(tapSpy.called);
+        })});
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 733bf9d..7574d18 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -27,6 +27,9 @@
 
   // Elements tests.
   [
+    'change-list/gr-change-list-item/gr-change-list-item_test.html',
+    'change-list/gr-change-list-view/gr-change-list-view_test.html',
+    'change-list/gr-change-list/gr-change-list_test.html',
     'change/gr-account-entry/gr-account-entry_test.html',
     'change/gr-account-list/gr-account-list_test.html',
     'change/gr-change-actions/gr-change-actions_test.html',
@@ -43,19 +46,14 @@
     'change/gr-related-changes-list/gr-related-changes-list_test.html',
     'change/gr-reply-dialog/gr-reply-dialog_test.html',
     'change/gr-reviewer-list/gr-reviewer-list_test.html',
-    'change-list/gr-change-list/gr-change-list_test.html',
-    'change-list/gr-change-list-item/gr-change-list-item_test.html',
-    'change-list/gr-change-list-view/gr-change-list-view_test.html',
     'core/gr-account-dropdown/gr-account-dropdown_test.html',
     'core/gr-error-manager/gr-error-manager_test.html',
     'core/gr-main-header/gr-main-header_test.html',
     'core/gr-reporting/gr-reporting_test.html',
     'core/gr-search-bar/gr-search-bar_test.html',
-    'diff/gr-diff/gr-diff-group_test.html',
-    'diff/gr-diff/gr-diff_test.html',
     'diff/gr-diff-builder/gr-diff-builder_test.html',
-    'diff/gr-diff-comment/gr-diff-comment_test.html',
     'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
+    'diff/gr-diff-comment/gr-diff-comment_test.html',
     'diff/gr-diff-cursor/gr-diff-cursor_test.html',
     'diff/gr-diff-highlight/gr-annotation_test.html',
     'diff/gr-diff-highlight/gr-diff-highlight_test.html',
@@ -63,6 +61,8 @@
     'diff/gr-diff-processor/gr-diff-processor_test.html',
     'diff/gr-diff-selection/gr-diff-selection_test.html',
     'diff/gr-diff-view/gr-diff-view_test.html',
+    'diff/gr-diff/gr-diff-group_test.html',
+    'diff/gr-diff/gr-diff_test.html',
     'diff/gr-patch-range-select/gr-patch-range-select_test.html',
     'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
     'diff/gr-selection-action-box/gr-selection-action-box_test.html',
@@ -83,6 +83,7 @@
     'shared/gr-alert/gr-alert_test.html',
     'shared/gr-autocomplete/gr-autocomplete_test.html',
     'shared/gr-avatar/gr-avatar_test.html',
+    'shared/gr-button/gr-button_test.html',
     'shared/gr-change-star/gr-change-star_test.html',
     'shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
     'shared/gr-cursor-manager/gr-cursor-manager_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index cb6d236..33f33b7 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -32,6 +32,7 @@
 	restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
 	port     = flag.String("port", ":8081", "Port to serve HTTP requests on")
 	prod     = flag.Bool("prod", false, "Serve production assets")
+	scheme   = flag.String("scheme", "https", "URL scheme")
 )
 
 func main() {
@@ -57,7 +58,7 @@
 	req := &http.Request{
 		Method: "GET",
 		URL: &url.URL{
-			Scheme:   "https",
+			Scheme:   *scheme,
 			Host:     *restHost,
 			Opaque:   r.URL.EscapedPath(),
 			RawQuery: r.URL.RawQuery,
diff --git a/tools/intellij/Gerrit_Code_Style.xml b/tools/intellij/Gerrit_Code_Style.xml
new file mode 100644
index 0000000..b913e09
--- /dev/null
+++ b/tools/intellij/Gerrit_Code_Style.xml
@@ -0,0 +1,531 @@
+<code_scheme name="Google Format (Gerrit)">
+  <option name="OTHER_INDENT_OPTIONS">
+    <value>
+      <option name="INDENT_SIZE" value="2" />
+      <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      <option name="TAB_SIZE" value="2" />
+      <option name="USE_TAB_CHARACTER" value="false" />
+      <option name="SMART_TABS" value="false" />
+      <option name="LABEL_INDENT_SIZE" value="0" />
+      <option name="LABEL_INDENT_ABSOLUTE" value="false" />
+      <option name="USE_RELATIVE_INDENTS" value="false" />
+    </value>
+  </option>
+  <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
+  <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
+  <option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
+    <value />
+  </option>
+  <option name="IMPORT_LAYOUT_TABLE">
+    <value>
+      <package name="" withSubpackages="true" static="true" />
+      <emptyLine />
+      <package name="com.google" withSubpackages="true" static="false" />
+      <emptyLine />
+      <package name="org" withSubpackages="true" static="false" />
+      <emptyLine />
+      <package name="java" withSubpackages="true" static="false" />
+      <emptyLine />
+      <package name="" withSubpackages="true" static="false" />
+    </value>
+  </option>
+  <option name="RIGHT_MARGIN" value="80" />
+  <option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
+  <option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
+  <option name="JD_P_AT_EMPTY_LINES" value="false" />
+  <option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
+  <option name="JD_KEEP_EMPTY_EXCEPTION" value="false" />
+  <option name="JD_KEEP_EMPTY_RETURN" value="false" />
+  <option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
+  <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
+  <option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
+  <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+  <option name="ALIGN_MULTILINE_FOR" value="false" />
+  <option name="CALL_PARAMETERS_WRAP" value="1" />
+  <option name="METHOD_PARAMETERS_WRAP" value="1" />
+  <option name="EXTENDS_LIST_WRAP" value="1" />
+  <option name="THROWS_KEYWORD_WRAP" value="1" />
+  <option name="METHOD_CALL_CHAIN_WRAP" value="1" />
+  <option name="BINARY_OPERATION_WRAP" value="1" />
+  <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+  <option name="TERNARY_OPERATION_WRAP" value="1" />
+  <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+  <option name="FOR_STATEMENT_WRAP" value="1" />
+  <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+  <option name="WRAP_COMMENTS" value="true" />
+  <option name="IF_BRACE_FORCE" value="3" />
+  <option name="DOWHILE_BRACE_FORCE" value="3" />
+  <option name="WHILE_BRACE_FORCE" value="3" />
+  <option name="FOR_BRACE_FORCE" value="3" />
+  <AndroidXmlCodeStyleSettings>
+    <option name="USE_CUSTOM_SETTINGS" value="true" />
+    <option name="LAYOUT_SETTINGS">
+      <value>
+        <option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
+      </value>
+    </option>
+  </AndroidXmlCodeStyleSettings>
+  <JSCodeStyleSettings>
+    <option name="INDENT_CHAINED_CALLS" value="false" />
+  </JSCodeStyleSettings>
+  <Python>
+    <option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" />
+  </Python>
+  <TypeScriptCodeStyleSettings>
+    <option name="INDENT_CHAINED_CALLS" value="false" />
+  </TypeScriptCodeStyleSettings>
+  <XML>
+    <option name="XML_ALIGN_ATTRIBUTES" value="false" />
+    <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
+  </XML>
+  <codeStyleSettings language="CSS">
+    <indentOptions>
+      <option name="INDENT_SIZE" value="2" />
+      <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      <option name="TAB_SIZE" value="2" />
+    </indentOptions>
+  </codeStyleSettings>
+  <codeStyleSettings language="ECMA Script Level 4">
+    <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
+    <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+    <option name="ALIGN_MULTILINE_FOR" value="false" />
+    <option name="CALL_PARAMETERS_WRAP" value="1" />
+    <option name="METHOD_PARAMETERS_WRAP" value="1" />
+    <option name="EXTENDS_LIST_WRAP" value="1" />
+    <option name="BINARY_OPERATION_WRAP" value="1" />
+    <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+    <option name="TERNARY_OPERATION_WRAP" value="1" />
+    <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+    <option name="FOR_STATEMENT_WRAP" value="1" />
+    <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+    <option name="IF_BRACE_FORCE" value="3" />
+    <option name="DOWHILE_BRACE_FORCE" value="3" />
+    <option name="WHILE_BRACE_FORCE" value="3" />
+    <option name="FOR_BRACE_FORCE" value="3" />
+    <option name="PARENT_SETTINGS_INSTALLED" value="true" />
+  </codeStyleSettings>
+  <codeStyleSettings language="HTML">
+    <indentOptions>
+      <option name="INDENT_SIZE" value="2" />
+      <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      <option name="TAB_SIZE" value="2" />
+    </indentOptions>
+  </codeStyleSettings>
+  <codeStyleSettings language="JAVA">
+    <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
+    <option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
+    <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="3" />
+    <option name="KEEP_BLANK_LINES_IN_CODE" value="3" />
+    <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="3" />
+    <option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
+    <option name="BLANK_LINES_AROUND_CLASS" value="2" />
+    <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+    <option name="ALIGN_MULTILINE_RESOURCES" value="false" />
+    <option name="ALIGN_MULTILINE_FOR" value="false" />
+    <option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
+    <option name="CALL_PARAMETERS_WRAP" value="1" />
+    <option name="METHOD_PARAMETERS_WRAP" value="1" />
+    <option name="EXTENDS_LIST_WRAP" value="1" />
+    <option name="THROWS_LIST_WRAP" value="1" />
+    <option name="EXTENDS_KEYWORD_WRAP" value="1" />
+    <option name="THROWS_KEYWORD_WRAP" value="1" />
+    <option name="METHOD_CALL_CHAIN_WRAP" value="1" />
+    <option name="BINARY_OPERATION_WRAP" value="1" />
+    <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+    <option name="TERNARY_OPERATION_WRAP" value="1" />
+    <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+    <option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
+    <option name="FOR_STATEMENT_WRAP" value="1" />
+    <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+    <option name="ASSIGNMENT_WRAP" value="1" />
+    <option name="IF_BRACE_FORCE" value="3" />
+    <option name="DOWHILE_BRACE_FORCE" value="3" />
+    <option name="WHILE_BRACE_FORCE" value="3" />
+    <option name="FOR_BRACE_FORCE" value="3" />
+    <option name="PARENT_SETTINGS_INSTALLED" value="true" />
+    <indentOptions>
+      <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      <option name="TAB_SIZE" value="2" />
+    </indentOptions>
+  </codeStyleSettings>
+  <codeStyleSettings language="JSON">
+    <indentOptions>
+      <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      <option name="TAB_SIZE" value="2" />
+    </indentOptions>
+  </codeStyleSettings>
+  <codeStyleSettings language="JavaScript">
+    <option name="RIGHT_MARGIN" value="80" />
+    <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
+    <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+    <option name="ALIGN_MULTILINE_FOR" value="false" />
+    <option name="CALL_PARAMETERS_WRAP" value="1" />
+    <option name="METHOD_PARAMETERS_WRAP" value="1" />
+    <option name="BINARY_OPERATION_WRAP" value="1" />
+    <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+    <option name="TERNARY_OPERATION_WRAP" value="1" />
+    <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+    <option name="FOR_STATEMENT_WRAP" value="1" />
+    <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+    <option name="IF_BRACE_FORCE" value="3" />
+    <option name="DOWHILE_BRACE_FORCE" value="3" />
+    <option name="WHILE_BRACE_FORCE" value="3" />
+    <option name="FOR_BRACE_FORCE" value="3" />
+    <option name="PARENT_SETTINGS_INSTALLED" value="true" />
+    <indentOptions>
+      <option name="INDENT_SIZE" value="2" />
+      <option name="TAB_SIZE" value="2" />
+    </indentOptions>
+  </codeStyleSettings>
+  <codeStyleSettings language="Python">
+    <option name="RIGHT_MARGIN" value="80" />
+    <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+    <option name="PARENT_SETTINGS_INSTALLED" value="true" />
+    <indentOptions>
+      <option name="INDENT_SIZE" value="2" />
+      <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      <option name="TAB_SIZE" value="2" />
+    </indentOptions>
+  </codeStyleSettings>
+  <codeStyleSettings language="SASS">
+    <indentOptions>
+      <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      <option name="TAB_SIZE" value="2" />
+    </indentOptions>
+  </codeStyleSettings>
+  <codeStyleSettings language="SCSS">
+    <indentOptions>
+      <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      <option name="TAB_SIZE" value="2" />
+    </indentOptions>
+  </codeStyleSettings>
+  <codeStyleSettings language="TypeScript">
+    <indentOptions>
+      <option name="INDENT_SIZE" value="2" />
+      <option name="TAB_SIZE" value="2" />
+    </indentOptions>
+  </codeStyleSettings>
+  <codeStyleSettings language="XML">
+    <indentOptions>
+      <option name="INDENT_SIZE" value="2" />
+      <option name="CONTINUATION_INDENT_SIZE" value="2" />
+      <option name="TAB_SIZE" value="2" />
+    </indentOptions>
+    <arrangement>
+      <rules>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>xmlns:android</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>^$</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>xmlns:.*</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>^$</XML_NAMESPACE>
+              </AND>
+            </match>
+            <order>BY_NAME</order>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:id</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>style</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>^$</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>^$</XML_NAMESPACE>
+              </AND>
+            </match>
+            <order>BY_NAME</order>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:.*Style</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+            <order>BY_NAME</order>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_width</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_height</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_weight</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_margin</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_marginTop</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_marginBottom</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_marginStart</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_marginEnd</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_marginLeft</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_marginRight</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:layout_.*</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+            <order>BY_NAME</order>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:padding</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:paddingTop</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:paddingBottom</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:paddingStart</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:paddingEnd</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:paddingLeft</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*:paddingRight</NAME>
+                <XML_ATTRIBUTE />
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*</NAME>
+                <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+              </AND>
+            </match>
+            <order>BY_NAME</order>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*</NAME>
+                <XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
+              </AND>
+            </match>
+            <order>BY_NAME</order>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*</NAME>
+                <XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>
+              </AND>
+            </match>
+            <order>BY_NAME</order>
+          </rule>
+        </section>
+        <section>
+          <rule>
+            <match>
+              <AND>
+                <NAME>.*</NAME>
+                <XML_NAMESPACE>.*</XML_NAMESPACE>
+              </AND>
+            </match>
+            <order>BY_NAME</order>
+          </rule>
+        </section>
+      </rules>
+    </arrangement>
+  </codeStyleSettings>
+</code_scheme>
diff --git a/tools/intellij/copyright/Gerrit_Copyright.xml b/tools/intellij/copyright/Gerrit_Copyright.xml
new file mode 100644
index 0000000..5609cdc
--- /dev/null
+++ b/tools/intellij/copyright/Gerrit_Copyright.xml
@@ -0,0 +1,6 @@
+<component name="CopyrightManager">
+  <copyright>
+    <option name="myName" value="Gerrit Copyright" />
+    <option name="notice" value="Copyright (C) &amp;#36;today.year The Android Open Source Project&#10;&#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10;&#10;http://www.apache.org/licenses/LICENSE-2.0&#10;&#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License." />
+  </copyright>
+</component>
\ No newline at end of file
diff --git a/tools/intellij/copyright/profiles_settings.xml b/tools/intellij/copyright/profiles_settings.xml
new file mode 100644
index 0000000..dfb94d5
--- /dev/null
+++ b/tools/intellij/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@
+<component name="CopyrightManager">
+  <settings default="Gerrit Copyright">
+    <LanguageOptions name="__TEMPLATE__">
+      <option name="block" value="false" />
+    </LanguageOptions>
+  </settings>
+</component>
\ No newline at end of file