Merge "Add support for Robot Comments"
diff --git a/.bazelrc b/.bazelrc
index 00acd27..a991c76 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1 +1 @@
-build --strategy=Javac=worker
+build --workspace_status_command=./tools/workspace-status.sh --strategy=Javac=worker
diff --git a/BUILD b/BUILD
index 538b51c..7ae3589 100644
--- a/BUILD
+++ b/BUILD
@@ -1,11 +1,10 @@
-load('//tools/bzl:genrule2.bzl', 'genrule2')
 load('//tools/bzl:pkg_war.bzl', 'pkg_war')
 
-genrule2(
-  name = 'version',
-  srcs = ['VERSION'],
-  cmd = "grep GERRIT_VERSION $< | cut -d \"'\" -f 2 >$@",
-  out = 'version.txt',
+genrule(
+  name = 'gen_version',
+  stamp = 1,
+  cmd = "grep STABLE_BUILD_GERRIT_LABEL < bazel-out/volatile-status.txt | cut -d ' ' -f 2 > $@",
+  outs = ['version.txt'],
   visibility = ['//visibility:public'],
 )
 
diff --git a/Documentation/BUILD b/Documentation/BUILD
index 98b3ce4..c2acc9c 100644
--- a/Documentation/BUILD
+++ b/Documentation/BUILD
@@ -2,6 +2,30 @@
 load("//tools/bzl:license.bzl", "license_map")
 
 license_map(
-  name = "pgm-licenses",
-  target = "//gerrit-pgm:pgm",
+  name = "licenses",
+  targets = [
+    "//gerrit-pgm:pgm",
+    "//gerrit-gwtui:ui_module",
+  ],
+  opts = ["--asciidoctor"],
+)
+
+DOC_DIR = "Documentation"
+SRCS = glob(["*.txt"])
+
+genrule(
+  name = "index",
+  cmd = "$(location //lib/asciidoctor:doc_indexer) " +
+      "-o $(OUTS) " +
+      '--prefix "%s/" ' % DOC_DIR +
+      '--in-ext ".txt" ' +
+      '--out-ext ".html" ' +
+      "$(SRCS) " +
+      "$(location :licenses.txt)",
+  tools = [
+    ":licenses.txt",
+    "//lib/asciidoctor:doc_indexer",
+  ],
+  srcs = SRCS,
+  outs = ["index.jar"],
 )
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 004d32b..1c7981a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2461,6 +2461,10 @@
 +
 A link:http://lucene.apache.org/[Lucene] index is used.
 +
++
+* `ELASTICSEARCH`
++
+An link:http://www.elasticsearch.org/[Elasticsearch] index is used.
 
 +
 By default, `LUCENE`.
@@ -2585,6 +2589,43 @@
   maxBufferedDocs = 500
 ----
 
+
+==== Elasticsearch configuration
+
+WARNING: ElasticSearch implementation is incomplete. Right now it is
+still using parts of Lucene index.
+
+Open and closed changes are indexed in a single index, separated
+into types 'open_changes' and 'closed_changes' respectively.
+
+The following settings are only used when the index type is
+`ELASTICSEARCH`.
+
+[[index.protocol]]index.protocol::
++
+Elasticsearch server protocol [http|https].
++
+Defaults to `http`.
+
+[[index.hostname]]index.hostname::
++
+Elasticsearch server hostname.
+
+Defaults to `localhost`.
+
+[[index.port]]index.port::
++
+Elasticsearch server port.
++
+Defauls to `9200`.
+
+[[index.name]]index.name::
++
+This setting can be used to index changes from multiple Gerrit
+instances in a single Elasticsearch cluster.
++
+Defaults to 'gerrit'.
+
 [[ldap]]
 === Section ldap
 
diff --git a/ReleaseNotes/ReleaseNotes-2.13.2.txt b/ReleaseNotes/ReleaseNotes-2.13.2.txt
new file mode 100644
index 0000000..87e902c
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.13.2.txt
@@ -0,0 +1,34 @@
+= Release notes for Gerrit 2.13.2
+
+Gerrit 2.13.2 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.13.2.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.13.2.war]
+
+== Schema Upgrade
+
+There are no schema changes from link:ReleaseNotes-2.13.1.html[2.13.1].
+
+== Bug Fixes
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4630[Issue 4630]:
+Fix server error when navigating up to change while 'Working' is displayed.
+
+* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4631[Issue 4631]:
+Read project watches from database.
++
+Project watches were being read from the git backend by default, but the
+migration to git is not yet completed.
+
+* Hooks plugin: Fix incorrect value passed to `--change-url` parameter.
++
+The URL was being generated using the change's Change-Id rather than the
+change number.
+
+* Check for CLA when creating project config changes from the web UI.
++
+If contributor agreements were enabled and required for a project, and
+the user had not signed a CLA, it was still possible to upload changes
+for review on `refs/meta/config` by making changes in the project access
+editor and pressing 'Save for Review'.
+
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 2938c1c6..945f09f 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -2,6 +2,7 @@
 
 [[s2_13]]
 == Version 2.13.x
+* link:ReleaseNotes-2.13.2.html[2.13.2]
 * link:ReleaseNotes-2.13.1.html[2.13.1]
 * link:ReleaseNotes-2.13.html[2.13]
 
diff --git a/WORKSPACE b/WORKSPACE
index ca0e422..b07f289 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -415,36 +415,36 @@
   sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3',
 )
 
-LUCENE_VERS = '5.5.3'
+LUCENE_VERS = '5.5.2'
 
 maven_jar(
   name = 'lucene_core',
   artifact = 'org.apache.lucene:lucene-core:' + LUCENE_VERS,
-  sha1 = '20540c6347259f35a0d264605b22ce2a13917066',
+  sha1 = 'de5e5c3161ea01e89f2a09a14391f9b7ed66cdbb',
 )
 
 maven_jar(
   name = 'lucene_analyzers_common',
   artifact = 'org.apache.lucene:lucene-analyzers-common:' + LUCENE_VERS,
-  sha1 = 'cf734ab72813af33dc1544ce61abc5c17b9d35e9',
+  sha1 = 'f0bc3114a6b43f8e64a33c471d5b9e8ddc51564d',
 )
 
 maven_jar(
   name = 'backward_codecs',
   artifact = 'org.apache.lucene:lucene-backward-codecs:' + LUCENE_VERS,
-  sha1 = 'a167789e52a9dc6d93bf3b588f79fdc9d7559c15',
+  sha1 = 'c5cfcd7a8cf48a0144b61fb991c8e50a0bf868d5',
 )
 
 maven_jar(
   name = 'lucene_misc',
   artifact = 'org.apache.lucene:lucene-misc:' + LUCENE_VERS,
-  sha1 = 'e356975c46447f06c71842632d0af9ec1baecfce',
+  sha1 = '37bbe5a2fb429499dfbe75d750d1778881fff45d',
 )
 
 maven_jar(
   name = 'lucene_queryparser',
   artifact = 'org.apache.lucene:lucene-queryparser:' + LUCENE_VERS,
-  sha1 = 'e2452203d2c44cac5ac42b34e5dcc0a44bf29a53',
+  sha1 = '8ac921563e744463605284c6d9d2d95e1be5b87c',
 )
 
 maven_jar(
@@ -761,18 +761,18 @@
   sha1 = '9bfabe48876ec38f6cbaa6931bad05c64a9ea942',
 )
 
-CM_VERSION = '5.18.2'
+CM_VERSION = '5.19.0'
 
 maven_jar(
   name = 'codemirror_minified',
   artifact = 'org.webjars.npm:codemirror-minified:' + CM_VERSION,
-  sha1 = '6755af157a7eaf2401468906bef67bbacc3c97f6',
+  sha1 = '263bf4acb7c4429be3fe46908af240f9f629d51c',
 )
 
 maven_jar(
   name = 'codemirror_original',
   artifact = 'org.webjars.npm:codemirror:' + CM_VERSION,
-  sha1 = '18c721ae88eed27cddb458c42f5d221fa3d9713e',
+  sha1 = 'e9ab382c6be240d55f112051bba3f6c637b798ce',
 )
 
 maven_jar(
@@ -786,3 +786,15 @@
   artifact = 'commons-io:commons-io:1.4',
   sha1 = 'a8762d07e76cfde2395257a5da47ba7c1dbd3dce',
 )
+
+maven_jar(
+  name = "asciidoctor",
+  artifact = "org.asciidoctor:asciidoctorj:1.5.4.1",
+  sha1 = "f7ddfb2bbed2f8da3f9ad0d1a5514f04b4274a5a",
+)
+
+maven_jar(
+  name = "jruby",
+  artifact = "org.jruby:jruby-complete:9.1.5.0",
+  sha1 = "00d0003e99da3c4d830b12c099691ce910c84e39",
+)
diff --git a/gerrit-elasticsearch/BUCK b/gerrit-elasticsearch/BUCK
new file mode 100644
index 0000000..a2641df
--- /dev/null
+++ b/gerrit-elasticsearch/BUCK
@@ -0,0 +1,51 @@
+java_library(
+  name = 'elasticsearch',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    '//gerrit-antlr:query_exception',
+    '//gerrit-extension-api:api',
+    '//gerrit-lucene:lucene', # only for LuceneAccountIndex
+    '//gerrit-reviewdb:client',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//gerrit-index:index',
+    '//lib:gson',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:protobuf',
+    '//lib/commons:codec',
+    '//lib/commons:lang',
+    '//lib/elasticsearch:elasticsearch',
+    '//lib/elasticsearch:jest',
+    '//lib/elasticsearch:jest-common',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/joda:joda-time',
+    '//lib/log:api',
+    '//lib/lucene:lucene-analyzers-common',
+    '//lib/lucene:lucene-core',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_test(
+  name = 'elasticsearch_tests',
+  labels = ['elastic'],
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':elasticsearch',
+    '//gerrit-extension-api:api',
+    '//gerrit-server:server',
+    '//gerrit-server:testutil',
+    '//gerrit-server:query_tests',
+    '//lib:gson',
+    '//lib:guava',
+    '//lib:junit',
+    '//lib:truth',
+    '//lib/elasticsearch:elasticsearch',
+    '//lib/guice:guice',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/jgit/org.eclipse.jgit.junit:junit',
+  ],
+)
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
new file mode 100644
index 0000000..a46edc7
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -0,0 +1,207 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkState;
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.index.IndexUtils;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.Schema.Values;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.Config;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+
+import io.searchbox.client.JestClientFactory;
+import io.searchbox.client.JestResult;
+import io.searchbox.client.config.HttpClientConfig;
+import io.searchbox.client.http.JestHttpClient;
+import io.searchbox.core.Bulk;
+import io.searchbox.core.Delete;
+import io.searchbox.indices.CreateIndex;
+import io.searchbox.indices.DeleteIndex;
+import io.searchbox.indices.IndicesExists;
+
+abstract class AbstractElasticIndex<K, V> implements Index<K, V> {
+  private static final String DEFAULT_INDEX_NAME = "gerrit";
+
+  private final Schema<V> schema;
+  private final FillArgs fillArgs;
+  private final SitePaths sitePaths;
+
+  protected final boolean refresh;
+  protected final String indexName;
+  protected final JestHttpClient client;
+
+
+  @Inject
+  AbstractElasticIndex(@GerritServerConfig Config cfg,
+      FillArgs fillArgs,
+      SitePaths sitePaths,
+      @Assisted Schema<V> schema) {
+    this.fillArgs = fillArgs;
+    this.sitePaths = sitePaths;
+    this.schema = schema;
+    String protocol = getRequiredConfigOption(cfg, "protocol");
+    String hostname = getRequiredConfigOption(cfg, "hostname");
+    String port = getRequiredConfigOption(cfg, "port");
+
+    this.indexName =
+        firstNonNull(cfg.getString("index", null, "name"), DEFAULT_INDEX_NAME);
+
+    // By default Elasticsearch has a 1s delay before changes are available in
+    // the index.  Setting refresh(true) on calls to the index makes the index
+    // refresh immediately.
+    //
+    // Discovery should be disabled during test mode to prevent spurious
+    // connection failures caused by the client starting up and being ready
+    // before the test node.
+    //
+    // This setting should only be set to true during testing, and is not
+    // documented.
+    this.refresh = cfg.getBoolean("index", "elasticsearch", "test", false);
+
+    String url = buildUrl(protocol, hostname, port);
+    JestClientFactory factory = new JestClientFactory();
+    factory.setHttpClientConfig(new HttpClientConfig
+        .Builder(url)
+        .multiThreaded(true)
+        .discoveryEnabled(!refresh)
+        .discoveryFrequency(1L, TimeUnit.MINUTES)
+        .build());
+    client = (JestHttpClient) factory.getObject();
+  }
+
+  @Override
+  public Schema<V> getSchema() {
+    return schema;
+  }
+
+  @Override
+  public void close() {
+    client.shutdownClient();
+  }
+
+  @Override
+  public void markReady(boolean ready) throws IOException {
+    IndexUtils.setReady(sitePaths, indexName, schema.getVersion(), ready);
+  }
+
+  @Override
+  public void delete(K c) throws IOException {
+    Bulk bulk = addActions(new Bulk.Builder(), c).refresh(refresh).build();
+    JestResult result = client.execute(bulk);
+    if (!result.isSucceeded()) {
+      throw new IOException(String.format(
+          "Failed to delete change %s in index %s: %s", c, indexName,
+          result.getErrorMessage()));
+    }
+  }
+
+  @Override
+  public void deleteAll() throws IOException {
+    // Delete the index, if it exists.
+    JestResult result = client.execute(
+        new IndicesExists.Builder(indexName).build());
+    if (result.isSucceeded()) {
+      result = client.execute(
+          new DeleteIndex.Builder(indexName).build());
+      if (!result.isSucceeded()) {
+        throw new IOException(String.format(
+            "Failed to delete index %s: %s", indexName,
+            result.getErrorMessage()));
+      }
+    }
+
+    // Recreate the index.
+    result = client.execute(
+        new CreateIndex.Builder(indexName).settings(getMappings()).build());
+    if (!result.isSucceeded()) {
+      String error = String.format("Failed to create index %s: %s",
+          indexName, result.getErrorMessage());
+      throw new IOException(error);
+    }
+  }
+
+  protected abstract Bulk.Builder addActions(Bulk.Builder builder, K c);
+
+  protected abstract String getMappings();
+
+  protected abstract String getId(V v);
+
+  protected Delete delete(String type, K c) {
+    String id = c.toString();
+    return new Delete.Builder(id)
+        .index(indexName)
+        .type(type)
+        .build();
+  }
+
+  protected io.searchbox.core.Index insert(String type, V v) throws IOException {
+    String id = getId(v);
+    String doc = toDoc(v);
+    return new io.searchbox.core.Index.Builder(doc)
+        .index(indexName)
+        .type(type)
+        .id(id)
+        .build();
+  }
+
+  private String toDoc(V v) throws IOException {
+    XContentBuilder builder = jsonBuilder().startObject();
+    for (Values<V> values : schema.buildFields(v, fillArgs)) {
+      String name = values.getField().getName();
+      if (values.getField().isRepeatable()) {
+        builder.array(name, values.getValues());
+      } else {
+        Object element = Iterables.getOnlyElement(values.getValues(), "");
+        if (!(element instanceof String) || !((String) element).isEmpty()) {
+          builder.field(name, element);
+        }
+      }
+    }
+    return builder.endObject().string();
+  }
+
+  private String getRequiredConfigOption(Config cfg, String name) {
+    String option = cfg.getString("index", null, name);
+    checkState(!Strings.isNullOrEmpty(option), "index." + name + " must be supplied");
+    return option;
+  }
+
+  private String buildUrl(String protocol, String hostname, String port) {
+    try {
+      return new URL(protocol, hostname, Integer.parseInt(port), "").toString();
+    } catch (MalformedURLException | NumberFormatException e) {
+      throw new RuntimeException(
+          "Cannot build url to Elasticsearch from values: protocol=" + protocol
+              + " hostname=" + hostname + " port=" + port, e);
+    }
+  }
+}
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
new file mode 100644
index 0000000..c55ea1c
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -0,0 +1,389 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
+import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
+import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
+
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
+import com.google.gerrit.index.IndexUtils;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.QueryOptions;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.index.change.ChangeField.ChangeProtoField;
+import com.google.gerrit.server.index.change.ChangeField.PatchSetApprovalProtoField;
+import com.google.gerrit.server.index.change.ChangeField.PatchSetProtoField;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.index.change.ChangeIndexRewriter;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.apache.commons.codec.binary.Base64;
+import org.eclipse.jgit.lib.Config;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import io.searchbox.client.JestResult;
+import io.searchbox.core.Bulk;
+import io.searchbox.core.Bulk.Builder;
+import io.searchbox.core.Search;
+import io.searchbox.core.search.sort.Sort;
+import io.searchbox.core.search.sort.Sort.Sorting;
+
+/** Secondary index implementation using Elasticsearch. */
+class ElasticChangeIndex extends AbstractElasticIndex<Change.Id, ChangeData>
+    implements ChangeIndex {
+  private static final Logger log =
+      LoggerFactory.getLogger(ElasticChangeIndex.class);
+
+  static class ChangeMapping {
+    MappingProperties openChanges;
+    MappingProperties closedChanges;
+
+    ChangeMapping(Schema<ChangeData> schema) {
+      ElasticMapping.Builder mappingBuilder = new ElasticMapping.Builder();
+      for (FieldDef<?, ?> field : schema.getFields().values()) {
+        String name = field.getName();
+        FieldType<?> fieldType = field.getType();
+        if (fieldType == FieldType.EXACT) {
+          mappingBuilder.addExactField(name);
+        } else if (fieldType == FieldType.TIMESTAMP) {
+          mappingBuilder.addTimestamp(name);
+        } else if (fieldType == FieldType.INTEGER
+            || fieldType == FieldType.INTEGER_RANGE
+            || fieldType == FieldType.LONG) {
+          mappingBuilder.addNumber(name);
+        } else if (fieldType == FieldType.PREFIX
+            || fieldType == FieldType.FULL_TEXT
+            || fieldType == FieldType.STORED_ONLY) {
+          mappingBuilder.addString(name);
+        } else {
+          throw new IllegalArgumentException(
+              "Unsupported filed type " + fieldType.getName());
+        }
+      }
+      MappingProperties mapping = mappingBuilder.build();
+      openChanges = mapping;
+      closedChanges = mapping;
+    }
+  }
+
+  static final String OPEN_CHANGES = "open_changes";
+  static final String CLOSED_CHANGES = "closed_changes";
+
+  private final Gson gson;
+  private final ChangeMapping mapping;
+  private final Provider<ReviewDb> db;
+  private final ElasticQueryBuilder queryBuilder;
+  private final ChangeData.Factory changeDataFactory;
+
+  @AssistedInject
+  ElasticChangeIndex(
+      @GerritServerConfig Config cfg,
+      Provider<ReviewDb> db,
+      ChangeData.Factory changeDataFactory,
+      FillArgs fillArgs,
+      SitePaths sitePaths,
+      @Assisted Schema<ChangeData> schema) {
+    super(cfg, fillArgs, sitePaths, schema);
+    this.db = db;
+    this.changeDataFactory = changeDataFactory;
+    mapping = new ChangeMapping(schema);
+
+    this.queryBuilder = new ElasticQueryBuilder();
+    this.gson = new GsonBuilder()
+        .setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create();
+  }
+
+  private static <T> List<T> decodeProtos(JsonObject doc, String fieldName,
+      ProtobufCodec<T> codec) {
+    return FluentIterable.from(doc.getAsJsonArray(fieldName))
+        .transform(i -> codec.decode(Base64.decodeBase64(i.toString())))
+        .toList();
+  }
+
+  @Override
+  public void replace(ChangeData cd) throws IOException {
+    String deleteIndex;
+    String insertIndex;
+
+    try {
+      if (cd.change().getStatus().isOpen()) {
+        insertIndex = OPEN_CHANGES;
+        deleteIndex = CLOSED_CHANGES;
+      } else {
+        insertIndex = CLOSED_CHANGES;
+        deleteIndex = OPEN_CHANGES;
+      }
+    } catch (OrmException e) {
+      throw new IOException(e);
+    }
+
+    Bulk bulk = new Bulk.Builder()
+        .defaultIndex(indexName)
+        .defaultType("changes")
+        .addAction(insert(insertIndex, cd))
+        .addAction(delete(deleteIndex, cd.getId()))
+        .refresh(refresh)
+        .build();
+    JestResult result = client.execute(bulk);
+    if (!result.isSucceeded()) {
+      throw new IOException(String.format(
+          "Failed to replace change %s in index %s: %s", cd.getId(), indexName,
+          result.getErrorMessage()));
+    }
+  }
+
+  @Override
+  public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
+      throws QueryParseException {
+    Set<Change.Status> statuses = ChangeIndexRewriter.getPossibleStatus(p);
+    List<String> indexes = Lists.newArrayListWithCapacity(2);
+    if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
+      indexes.add(OPEN_CHANGES);
+    }
+    if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+      indexes.add(CLOSED_CHANGES);
+    }
+    return new QuerySource(indexes, p, opts);
+  }
+
+  @Override
+  protected Builder addActions(Builder builder, Id c) {
+    return builder
+        .addAction(delete(OPEN_CHANGES, c))
+        .addAction(delete(OPEN_CHANGES, c));
+  }
+
+  @Override
+  protected String getMappings() {
+    return gson.toJson(ImmutableMap.of("mappings", mapping));
+  }
+
+  @Override
+  protected String getId(ChangeData cd) {
+    return cd.getId().toString();
+  }
+
+  private class QuerySource implements ChangeDataSource {
+    private final Search search;
+    private final Set<String> fields;
+
+    public QuerySource(List<String> types, Predicate<ChangeData> p,
+        QueryOptions opts) throws QueryParseException {
+      List<Sort> sorts = ImmutableList.of(
+          new Sort(ChangeField.UPDATED.getName(), Sorting.DESC),
+          new Sort(ChangeField.LEGACY_ID.getName(), Sorting.DESC));
+      for (Sort sort : sorts) {
+        sort.setIgnoreUnmapped();
+      }
+      QueryBuilder qb = queryBuilder.toQueryBuilder(p);
+      fields = IndexUtils.fields(opts);
+      SearchSourceBuilder searchSource = new SearchSourceBuilder()
+          .query(qb)
+          .from(opts.start())
+          .size(opts.limit())
+          .fields(Lists.newArrayList(fields));
+
+      search = new Search.Builder(searchSource.toString())
+          .addType(types)
+          .addSort(sorts)
+          .addIndex(indexName)
+          .build();
+    }
+
+    @Override
+    public int getCardinality() {
+      return 10;
+    }
+
+    @Override
+    public ResultSet<ChangeData> read() throws OrmException {
+      try {
+        List<ChangeData> results = Collections.emptyList();
+        JestResult result = client.execute(search);
+        if (result.isSucceeded()) {
+          JsonObject obj = result.getJsonObject().getAsJsonObject("hits");
+          if (obj.get("hits") != null) {
+            JsonArray json = obj.getAsJsonArray("hits");
+            results = Lists.newArrayListWithCapacity(json.size());
+            for (int i = 0; i < json.size(); i++) {
+              results.add(toChangeData(json.get(i)));
+            }
+          }
+        } else {
+          log.error(result.getErrorMessage());
+        }
+        final List<ChangeData> r = Collections.unmodifiableList(results);
+        return new ResultSet<ChangeData>() {
+          @Override
+          public Iterator<ChangeData> iterator() {
+            return r.iterator();
+          }
+
+          @Override
+          public List<ChangeData> toList() {
+            return r;
+          }
+
+          @Override
+          public void close() {
+            // Do nothing.
+          }
+        };
+      } catch (IOException e) {
+        throw new OrmException(e);
+      }
+    }
+
+    @Override
+    public boolean hasChange() {
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return search.toString();
+    }
+
+    private ChangeData toChangeData(JsonElement json) {
+      JsonElement sourceElement = json.getAsJsonObject().get("_source");
+      if (sourceElement == null) {
+        sourceElement = json.getAsJsonObject().get("fields");
+      }
+      JsonObject source = sourceElement.getAsJsonObject();
+      JsonElement c = source.get(ChangeField.CHANGE.getName());
+
+      if (c == null) {
+        int id = source.get(ChangeField.LEGACY_ID.getName()).getAsInt();
+        String projectName =
+            source.get(ChangeField.PROJECT.getName()).getAsString();
+        if (projectName == null) {
+          return changeDataFactory.createOnlyWhenNoteDbDisabled(
+              db.get(), new Change.Id(id));
+        }
+        return changeDataFactory.create(
+            db.get(), new Project.NameKey(projectName), new Change.Id(id));
+      }
+
+      ChangeData cd = changeDataFactory.create(db.get(),
+          ChangeProtoField.CODEC.decode(Base64.decodeBase64(c.getAsString())));
+
+      // Patch sets.
+      cd.setPatchSets(decodeProtos(
+          source, ChangeField.PATCH_SET.getName(), PatchSetProtoField.CODEC));
+
+      // Approvals.
+      if (source.get(ChangeField.APPROVAL.getName()) != null) {
+        cd.setCurrentApprovals(decodeProtos(source,
+            ChangeField.APPROVAL.getName(), PatchSetApprovalProtoField.CODEC));
+      } else if (fields.contains(ChangeField.APPROVAL.getName())) {
+        cd.setCurrentApprovals(Collections.emptyList());
+      }
+
+      JsonElement addedElement = source.get(ChangeField.ADDED.getName());
+      JsonElement deletedElement = source.get(ChangeField.DELETED.getName());
+      if (addedElement != null && deletedElement != null) {
+        // Changed lines.
+        int added = addedElement.getAsInt();
+        int deleted = deletedElement.getAsInt();
+        if (added != 0 && deleted != 0) {
+          cd.setChangedLines(added, deleted);
+        }
+      }
+
+      // Mergeable.
+      JsonElement mergeableElement = source.get(ChangeField.MERGEABLE.getName());
+      if (mergeableElement != null) {
+        String mergeable = mergeableElement.getAsString();
+        if ("1".equals(mergeable)) {
+          cd.setMergeable(true);
+        } else if ("0".equals(mergeable)) {
+          cd.setMergeable(false);
+        }
+      }
+
+      // Reviewed-by.
+      if (source.get(ChangeField.REVIEWEDBY.getName()) != null) {
+        JsonArray reviewedBy =
+            source.get(ChangeField.REVIEWEDBY.getName()).getAsJsonArray();
+        if (reviewedBy.size() > 0) {
+          Set<Account.Id> accounts =
+              Sets.newHashSetWithExpectedSize(reviewedBy.size());
+          for (int i = 0; i < reviewedBy.size() ; i++) {
+            int aId = reviewedBy.get(i).getAsInt();
+            if (reviewedBy.size() == 1 && aId == ChangeField.NOT_REVIEWED) {
+              break;
+            }
+            accounts.add(new Account.Id(aId));
+          }
+          cd.setReviewedBy(accounts);
+        }
+      } else if (fields.contains(ChangeField.REVIEWEDBY.getName())) {
+        cd.setReviewedBy(Collections.emptySet());
+      }
+
+      if (source.get(ChangeField.REVIEWER.getName()) != null) {
+        cd.setReviewers(
+            ChangeField.parseReviewerFieldValues(FluentIterable
+                .from(
+                    source.get(ChangeField.REVIEWER.getName()).getAsJsonArray())
+                .transform(JsonElement::getAsString)));
+      } else if (fields.contains(ChangeField.REVIEWER.getName())) {
+        cd.setReviewers(ReviewerSet.empty());
+      }
+
+      return cd;
+    }
+  }
+}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
new file mode 100644
index 0000000..e108dca
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import com.google.gerrit.index.SingleVersionModule;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.lucene.LuceneAccountIndex;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.account.AccountIndex;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.Map;
+
+public class ElasticIndexModule extends LifecycleModule {
+  private final int threads;
+  private final Map<String, Integer> singleVersions;
+
+  public static ElasticIndexModule singleVersionWithExplicitVersions(
+      Map<String, Integer> versions, int threads) {
+    return new ElasticIndexModule(versions, threads);
+  }
+
+  public static ElasticIndexModule latestVersionWithOnlineUpgrade() {
+    return new ElasticIndexModule(null, 0);
+  }
+
+  private ElasticIndexModule(Map<String, Integer> singleVersions, int threads) {
+    this.singleVersions = singleVersions;
+    this.threads = threads;
+  }
+
+  @Override
+  protected void configure() {
+    install(
+        new FactoryModuleBuilder()
+            .implement(ChangeIndex.class, ElasticChangeIndex.class)
+            .build(ChangeIndex.Factory.class));
+    install(
+        new FactoryModuleBuilder()
+            // until we implement Elasticsearch index for accounts we need to
+            // use Lucene to make all tests green and Gerrit server to work
+            .implement(AccountIndex.class, LuceneAccountIndex.class)
+            .build(AccountIndex.Factory.class));
+
+    install(new IndexModule(threads));
+    install(new SingleVersionModule(singleVersions));
+  }
+
+  @Provides
+  @Singleton
+  IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
+    return IndexConfig.fromConfig(cfg);
+  }
+}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.java
new file mode 100644
index 0000000..e3f7e96
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.java
@@ -0,0 +1,79 @@
+// 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.elasticsearch;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+class ElasticMapping {
+  static class Builder {
+    private final ImmutableMap.Builder<String, FieldProperties> fields =
+        new ImmutableMap.Builder<>();
+
+    MappingProperties build() {
+      MappingProperties properties = new MappingProperties();
+      properties.properties = fields.build();
+      return properties;
+    }
+
+    Builder addExactField(String name) {
+      FieldProperties key = new FieldProperties("string");
+      key.index = "not_analyzed";
+      FieldProperties properties = new FieldProperties("string");
+      properties.fields = ImmutableMap.of("key", key);
+      fields.put(name, properties);
+      return this;
+    }
+
+    Builder addTimestamp(String name) {
+      FieldProperties properties = new FieldProperties("date");
+      properties.type = "date";
+      properties.format = "dateOptionalTime";
+      fields.put(name, properties);
+      return this;
+    }
+
+    Builder addNumber(String name) {
+      fields.put(name, new FieldProperties("long"));
+      return this;
+    }
+
+    Builder addString(String name) {
+      fields.put(name, new FieldProperties("string"));
+      return this;
+    }
+
+    Builder add(String name, String type) {
+      fields.put(name, new FieldProperties(type));
+      return this;
+    }
+  }
+
+  static class MappingProperties {
+    Map<String, FieldProperties> properties;
+  }
+
+  static class FieldProperties {
+    String type;
+    String index;
+    String format;
+    Map<String, FieldProperties> fields;
+
+    FieldProperties(String type) {
+      this.type = type;
+    }
+  }
+}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
new file mode 100644
index 0000000..51b14a4
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
@@ -0,0 +1,181 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.IntegerRangePredicate;
+import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.index.TimestampRangePredicate;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.NotPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.AfterPredicate;
+
+import org.apache.lucene.search.BooleanQuery;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.joda.time.DateTime;
+
+public class ElasticQueryBuilder {
+
+  protected <T> QueryBuilder toQueryBuilder(Predicate<T> p)
+      throws QueryParseException {
+    if (p instanceof AndPredicate) {
+      return and(p);
+    } else if (p instanceof OrPredicate) {
+      return or(p);
+    } else if (p instanceof NotPredicate) {
+      return not(p);
+    } else if (p instanceof IndexPredicate) {
+      return fieldQuery((IndexPredicate<T>) p);
+    } else {
+      throw new QueryParseException("cannot create query for index: " + p);
+    }
+  }
+
+  private <T> BoolQueryBuilder and(Predicate<T> p)
+      throws QueryParseException {
+    try {
+      BoolQueryBuilder b = QueryBuilders.boolQuery();
+      for (Predicate<T> c : p.getChildren()) {
+        b.must(toQueryBuilder(c));
+      }
+      return b;
+    } catch (BooleanQuery.TooManyClauses e) {
+      throw new QueryParseException("cannot create query for index: " + p, e);
+    }
+  }
+
+  private <T> BoolQueryBuilder or(Predicate<T> p)
+      throws QueryParseException {
+    try {
+      BoolQueryBuilder q = QueryBuilders.boolQuery();
+      for (Predicate<T> c : p.getChildren()) {
+        q.should(toQueryBuilder(c));
+      }
+      return q;
+    } catch (BooleanQuery.TooManyClauses e) {
+      throw new QueryParseException("cannot create query for index: " + p, e);
+    }
+  }
+
+  private <T> QueryBuilder not(Predicate<T> p)
+      throws QueryParseException {
+    Predicate<T> n = p.getChild(0);
+    if (n instanceof TimestampRangePredicate) {
+      return notTimestamp((TimestampRangePredicate<T>) n);
+    }
+
+    // Lucene does not support negation, start with all and subtract.
+    BoolQueryBuilder q = QueryBuilders.boolQuery();
+    q.must(QueryBuilders.matchAllQuery());
+    q.mustNot(toQueryBuilder(n));
+    return q;
+  }
+
+  private <T> QueryBuilder fieldQuery(IndexPredicate<T> p)
+      throws QueryParseException {
+    FieldType<?> type = p.getType();
+    FieldDef<?,?> field = p.getField();
+    String name = field.getName();
+    String value = p.getValue();
+
+    if (type == FieldType.INTEGER) {
+      // QueryBuilder encodes integer fields as prefix coded bits,
+      // which elasticsearch's queryString can't handle.
+      // Create integer terms with string representations instead.
+      return QueryBuilders.termQuery(name, value);
+    } else if (type == FieldType.INTEGER_RANGE) {
+      return intRangeQuery(p);
+    } else if (type == FieldType.TIMESTAMP) {
+      return timestampQuery(p);
+    } else if (type == FieldType.EXACT) {
+      return exactQuery(p);
+    } else if (type == FieldType.PREFIX) {
+      return QueryBuilders.matchPhrasePrefixQuery(name, value);
+    } else if (type == FieldType.FULL_TEXT) {
+      return QueryBuilders.matchPhraseQuery(name, value);
+    } else {
+      throw FieldType.badFieldType(p.getType());
+    }
+  }
+
+  private <T> QueryBuilder intRangeQuery(IndexPredicate<T> p)
+      throws QueryParseException {
+    if (p instanceof IntegerRangePredicate) {
+      IntegerRangePredicate<T> r = (IntegerRangePredicate<T>) p;
+      int minimum = r.getMinimumValue();
+      int maximum = r.getMaximumValue();
+      if (minimum == maximum) {
+        // Just fall back to a standard integer query.
+        return QueryBuilders.termQuery(p.getField().getName(), minimum);
+      }
+      return QueryBuilders.rangeQuery(p.getField().getName())
+          .gte(minimum)
+          .lte(maximum);
+    }
+    throw new QueryParseException("not an integer range: " + p);
+  }
+
+  private <T> QueryBuilder notTimestamp(TimestampRangePredicate<T> r)
+      throws QueryParseException {
+    if (r.getMinTimestamp().getTime() == 0) {
+      return QueryBuilders.rangeQuery(r.getField().getName())
+          .gt(new DateTime(r.getMaxTimestamp().getTime()));
+    }
+    throw new QueryParseException("cannot negate: " + r);
+  }
+
+  private <T> QueryBuilder timestampQuery(IndexPredicate<T> p)
+      throws QueryParseException {
+    if (p instanceof TimestampRangePredicate) {
+      TimestampRangePredicate<T> r =
+          (TimestampRangePredicate<T>) p;
+      if (p instanceof AfterPredicate) {
+        return QueryBuilders.rangeQuery(r.getField().getName())
+            .gte(new DateTime(r.getMinTimestamp().getTime()));
+      }
+      return QueryBuilders.rangeQuery(r.getField().getName())
+          .gte(new DateTime(r.getMinTimestamp().getTime()))
+          .lte(new DateTime(r.getMaxTimestamp().getTime()));
+    }
+    throw new QueryParseException("not a timestamp: " + p);
+  }
+
+  private <T> QueryBuilder exactQuery(IndexPredicate<T> p){
+    String name = p.getField().getName();
+    String value = p.getValue();
+
+    if (value.isEmpty()) {
+      return new BoolQueryBuilder().mustNot(QueryBuilders.existsQuery(name));
+    } else if (p instanceof RegexPredicate) {
+      if (value.startsWith("^")) {
+        value = value.substring(1);
+      }
+      if (value.endsWith("$") && !value.endsWith("\\$")
+          && !value.endsWith("\\\\$")) {
+        value = value.substring(0, value.length() - 1);
+      }
+      return QueryBuilders.regexpQuery(name + ".key", value);
+    } else {
+      return QueryBuilders.termQuery(name + ".key", value);
+    }
+  }
+}
diff --git a/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
new file mode 100644
index 0000000..e2e7585
--- /dev/null
+++ b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
@@ -0,0 +1,178 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.elasticsearch.ElasticChangeIndex.CLOSED_CHANGES;
+import static com.google.gerrit.elasticsearch.ElasticChangeIndex.OPEN_CHANGES;
+
+import com.google.common.base.Strings;
+import com.google.common.io.Files;
+import com.google.gerrit.elasticsearch.ElasticChangeIndex.ChangeMapping;
+import com.google.gerrit.server.index.IndexModule.IndexType;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.lib.Config;
+import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.node.Node;
+import org.elasticsearch.node.NodeBuilder;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+public class ElasticQueryChangesTest extends AbstractQueryChangesTest {
+  private static final Gson gson = new GsonBuilder()
+      .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+      .create();
+  private static Node node;
+  private static String port;
+  private static File elasticDir;
+
+  static class NodeInfo {
+    String httpAddress;
+  }
+
+  static class Info {
+    Map<String, NodeInfo> nodes;
+  }
+
+  @BeforeClass
+  public static void startIndexService()
+      throws InterruptedException, ExecutionException {
+    if (node != null) {
+      // do not start Elasticsearch twice
+      return;
+    }
+    elasticDir = Files.createTempDir();
+    Path elasticDirPath = elasticDir.toPath();
+    Settings settings = Settings.settingsBuilder()
+        .put("cluster.name", "gerrit")
+        .put("node.name", "Gerrit Elasticsearch Test Node")
+        .put("node.local", true)
+        .put("discovery.zen.ping.multicast.enabled", false)
+        .put("index.store.fs.memory.enabled", true)
+        .put("index.gateway.type", "none")
+        .put("index.max_result_window", Integer.MAX_VALUE)
+        .put("gateway.type", "default")
+        .put("http.port", 0)
+        .put("discovery.zen.ping.unicast.hosts", "[\"localhost\"]")
+        .put("path.home", elasticDirPath.toAbsolutePath())
+        .put("path.data", elasticDirPath.resolve("data").toAbsolutePath())
+        .put("path.work", elasticDirPath.resolve("work").toAbsolutePath())
+        .put("path.logs", elasticDirPath.resolve("logs").toAbsolutePath())
+        .build();
+
+    // Start the node
+    node = NodeBuilder.nodeBuilder()
+        .settings(settings)
+        .node();
+
+    // Wait for it to be ready
+    node.client()
+        .admin()
+        .cluster()
+        .prepareHealth()
+        .setWaitForYellowStatus()
+        .execute()
+        .actionGet();
+
+    createIndexes();
+
+    assertThat(node.isClosed()).isFalse();
+    port = getHttpPort();
+  }
+
+  @After
+  public void cleanupIndex() {
+    node.client().admin().indices().prepareDelete("gerrit").execute();
+    createIndexes();
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (node != null) {
+      node.close();
+      node = null;
+    }
+    if (elasticDir != null && elasticDir.delete()) {
+      elasticDir = null;
+    }
+  }
+
+  @Override
+  protected Injector createInjector() {
+    Config elasticsearchConfig = new Config(config);
+    InMemoryModule.setDefaults(elasticsearchConfig);
+    elasticsearchConfig.setEnum("index", null, "type", IndexType.ELASTICSEARCH);
+    elasticsearchConfig.setString("index", null, "protocol", "http");
+    elasticsearchConfig.setString("index", null, "hostname", "localhost");
+    elasticsearchConfig.setString("index", null, "port", port);
+    elasticsearchConfig.setString("index", null, "name", "gerrit");
+    elasticsearchConfig.setBoolean("index", "elasticsearch", "test", true);
+    return Guice.createInjector(
+        new InMemoryModule(elasticsearchConfig, notesMigration));
+  }
+
+  private static void createIndexes() {
+    ChangeMapping openChangesMapping =
+        new ChangeMapping(ChangeSchemaDefinitions.INSTANCE.getLatest());
+    ChangeMapping closedChangesMapping =
+        new ChangeMapping(ChangeSchemaDefinitions.INSTANCE.getLatest());
+    openChangesMapping.closedChanges = null;
+    closedChangesMapping.openChanges = null;
+    node.client()
+        .admin()
+        .indices()
+        .prepareCreate("gerrit")
+        .addMapping(OPEN_CHANGES, gson.toJson(openChangesMapping))
+        .addMapping(CLOSED_CHANGES, gson.toJson(closedChangesMapping))
+        .execute()
+        .actionGet();
+  }
+
+  private static String getHttpPort()
+      throws InterruptedException, ExecutionException {
+    String nodes = node.client().admin().cluster()
+        .nodesInfo(new NodesInfoRequest("*")).get().toString();
+    Gson gson = new GsonBuilder()
+        .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+        .create();
+    Info info = gson.fromJson(nodes, Info.class);
+
+    checkState(info.nodes != null && info.nodes.size() == 1);
+    Iterator<NodeInfo> values = info.nodes.values().iterator();
+    String httpAddress = values.next().httpAddress;
+
+    checkState(
+        !Strings.isNullOrEmpty(httpAddress) && httpAddress.indexOf(':') > 0);
+    return httpAddress.substring(httpAddress.indexOf(':') + 1,
+        httpAddress.length());
+  }
+}
diff --git a/gerrit-gwtexpui/BUILD b/gerrit-gwtexpui/BUILD
index d3b03ef..d74fc8b 100644
--- a/gerrit-gwtexpui/BUILD
+++ b/gerrit-gwtexpui/BUILD
@@ -19,6 +19,10 @@
     '//lib/gwt:user',
   ],
   visibility = ['//visibility:public'],
+  data = [
+    '//lib:LICENSE-clippy',
+    '//lib:LICENSE-silk_icons',
+  ],
 )
 
 java_library(
diff --git a/gerrit-gwtui-common/BUILD b/gerrit-gwtui-common/BUILD
index 01a82af..4bd2dfd 100644
--- a/gerrit-gwtui-common/BUILD
+++ b/gerrit-gwtui-common/BUILD
@@ -42,6 +42,10 @@
   name = 'diffy_logo',
   jars = [':diffy_image_files_ln'],
   visibility = ['//visibility:public'],
+  data = [
+    '//lib:LICENSE-diffy',
+    '//lib:LICENSE-CC-BY3.0-unported',
+  ],
 )
 
 genrule2(
diff --git a/gerrit-gwtui/BUILD b/gerrit-gwtui/BUILD
index c223885..cc5da12 100644
--- a/gerrit-gwtui/BUILD
+++ b/gerrit-gwtui/BUILD
@@ -1,5 +1,6 @@
 load('//tools/bzl:gwt.bzl', 'gwt_module')
 load('//tools/bzl:genrule2.bzl', 'genrule2')
+load('//tools/bzl:license.bzl', 'license_test')
 load(':gwt.bzl', 'gwt_binary', 'gwt_genrule', 'gen_ui_module')
 
 gwt_genrule()
@@ -7,3 +8,8 @@
 
 gen_ui_module(name = 'ui_module')
 gen_ui_module(name = 'ui_module', suffix = '_r')
+
+license_test(
+  name = "ui_module_license_test",
+  target = ":ui_module",
+)
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index 4a01128..111dfc9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -18,11 +18,13 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.common.errors.UpdateParentFailedException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -89,10 +91,15 @@
   @Override
   public final T call() throws NoSuchProjectException, IOException,
       ConfigInvalidException, InvalidNameException, NoSuchGroupException,
-      OrmException, UpdateParentFailedException {
+      OrmException, UpdateParentFailedException, PermissionDeniedException {
     final ProjectControl projectControl =
         projectControlFactory.controlFor(projectName);
 
+    Capable r = projectControl.canPushToAtLeastOneRef();
+    if (r != Capable.OK) {
+      throw new PermissionDeniedException(r.getMessage());
+    }
+
     try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
       ProjectConfig config = ProjectConfig.read(md, base);
       Set<String> toDelete = scanSectionNames(config);
diff --git a/gerrit-index/BUCK b/gerrit-index/BUCK
new file mode 100644
index 0000000..ea97f88
--- /dev/null
+++ b/gerrit-index/BUCK
@@ -0,0 +1,13 @@
+java_library(
+  name = 'index',
+  srcs = glob(['src/main/java/**/*.java']),
+  deps = [
+    '//gerrit-extension-api:api',
+    '//gerrit-server:server',
+    '//gerrit-patch-jgit:server',
+    '//lib/guice:guice',
+    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib:guava',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java b/gerrit-index/src/main/java/com/google/gerrit/index/GerritIndexStatus.java
similarity index 84%
rename from gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java
rename to gerrit-index/src/main/java/com/google/gerrit/index/GerritIndexStatus.java
index f43e385..cafd30e 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java
+++ b/gerrit-index/src/main/java/com/google/gerrit/index/GerritIndexStatus.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.lucene;
+package com.google.gerrit.index;
 
 import com.google.common.primitives.Ints;
 import com.google.gerrit.server.config.SitePaths;
@@ -24,13 +24,13 @@
 
 import java.io.IOException;
 
-class GerritIndexStatus {
+public class GerritIndexStatus {
   private static final String SECTION = "index";
   private static final String KEY_READY = "ready";
 
   private final FileBasedConfig cfg;
 
-  GerritIndexStatus(SitePaths sitePaths)
+  public GerritIndexStatus(SitePaths sitePaths)
       throws ConfigInvalidException, IOException {
     cfg = new FileBasedConfig(
         sitePaths.index_dir.resolve("gerrit_index.config").toFile(),
@@ -39,16 +39,16 @@
     convertLegacyConfig();
   }
 
-  void setReady(String indexName, int version, boolean ready) {
+  public void setReady(String indexName, int version, boolean ready) {
     cfg.setBoolean(SECTION, indexDirName(indexName, version), KEY_READY, ready);
   }
 
-  boolean getReady(String indexName, int version) {
+  public boolean getReady(String indexName, int version) {
     return cfg.getBoolean(SECTION, indexDirName(indexName, version), KEY_READY,
         false);
   }
 
-  void save() throws IOException {
+  public void save() throws IOException {
     cfg.save();
   }
 
@@ -62,8 +62,8 @@
         if (ready != null) {
           dirty = false;
           cfg.unset(SECTION, subsection, KEY_READY);
-          cfg.setString(SECTION,
-              indexDirName(ChangeSchemaDefinitions.NAME, v), KEY_READY, ready);
+          cfg.setString(SECTION, indexDirName(ChangeSchemaDefinitions.NAME, v),
+              KEY_READY, ready);
         }
       }
     }
diff --git a/gerrit-index/src/main/java/com/google/gerrit/index/IndexUtils.java b/gerrit-index/src/main/java/com/google/gerrit/index/IndexUtils.java
new file mode 100644
index 0000000..f00f5c2
--- /dev/null
+++ b/gerrit-index/src/main/java/com/google/gerrit/index/IndexUtils.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.index;
+
+import static com.google.gerrit.server.index.change.ChangeField.CHANGE;
+import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
+import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.QueryOptions;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+public final class IndexUtils {
+  public static final Map<String, String> CUSTOM_CHAR_MAPPING =
+      ImmutableMap.of("_", " ", ".", " ");
+
+  public static void setReady(SitePaths sitePaths, String name, int version,
+      boolean ready) throws IOException {
+    try {
+      GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
+      cfg.setReady(name, version, ready);
+      cfg.save();
+    } catch (ConfigInvalidException e) {
+      throw new IOException(e);
+    }
+  }
+
+  public static Set<String> fields(QueryOptions opts) {
+    // Ensure we request enough fields to construct a ChangeData. We need both
+    // change ID and project, which can either come via the Change field or
+    // separate fields.
+    Set<String> fs = opts.fields();
+    if (fs.contains(CHANGE.getName())) {
+      // A Change is always sufficient.
+      return fs;
+    }
+    if (fs.contains(PROJECT.getName()) && fs.contains(LEGACY_ID.getName())) {
+      return fs;
+    }
+    return Sets.union(fs,
+        ImmutableSet.of(LEGACY_ID.getName(), PROJECT.getName()));
+  }
+
+  private IndexUtils() {
+    // hide default constructor
+  }
+}
diff --git a/gerrit-index/src/main/java/com/google/gerrit/index/SingleVersionModule.java b/gerrit-index/src/main/java/com/google/gerrit/index/SingleVersionModule.java
new file mode 100644
index 0000000..55b9b57
--- /dev/null
+++ b/gerrit-index/src/main/java/com/google/gerrit/index/SingleVersionModule.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.index;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.index.Schema;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+@Singleton
+public class SingleVersionModule extends LifecycleModule {
+  static final String SINGLE_VERSIONS = "LuceneIndexModule/SingleVersions";
+
+  private final Map<String, Integer> singleVersions;
+
+  public SingleVersionModule(Map<String, Integer> singleVersions) {
+    this.singleVersions = singleVersions;
+  }
+
+  @Override
+  public void configure() {
+    listener().to(SingleVersionListener.class);
+    bind(new TypeLiteral<Map<String, Integer>>() {})
+        .annotatedWith(Names.named(SINGLE_VERSIONS))
+        .toInstance(singleVersions);
+  }
+
+  @Singleton
+  static class SingleVersionListener implements LifecycleListener {
+    private final Set<String> disabled;
+    private final Collection<IndexDefinition<?, ?, ?>> defs;
+    private final Map<String, Integer> singleVersions;
+
+    @Inject
+    SingleVersionListener(
+        @GerritServerConfig Config cfg,
+        Collection<IndexDefinition<?, ?, ?>> defs,
+        @Named(SINGLE_VERSIONS) Map<String, Integer> singleVersions) {
+      this.defs = defs;
+      this.singleVersions = singleVersions;
+
+      disabled = ImmutableSet.copyOf(
+          cfg.getStringList("index", null, "testDisable"));
+    }
+
+    @Override
+    public void start() {
+      for (IndexDefinition<?, ?, ?> def : defs) {
+        start(def);
+      }
+    }
+
+    private <K, V, I extends Index<K, V>> void start(
+        IndexDefinition<K, V, I> def) {
+      if (disabled.contains(def.getName())) {
+        return;
+      }
+      Schema<V> schema;
+      Integer v = singleVersions.get(def.getName());
+      if (v == null) {
+        schema = def.getLatest();
+      } else {
+        schema = def.getSchemas().get(v);
+        if (schema == null) {
+          throw new ProvisionException(String.format(
+                "Unrecognized %s schema version: %s", def.getName(), v));
+        }
+      }
+      I index = def.getIndexFactory().create(schema);
+      def.getIndexCollection().setSearchIndex(index);
+      def.getIndexCollection().addWriteIndex(index);
+    }
+
+    @Override
+    public void stop() {
+      // Do nothing; indexes are closed by IndexCollection.
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-lucene/BUCK b/gerrit-lucene/BUCK
index 771a021..f4f097c 100644
--- a/gerrit-lucene/BUCK
+++ b/gerrit-lucene/BUCK
@@ -27,6 +27,7 @@
     '//gerrit-extension-api:api',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
+    '//gerrit-index:index',
     '//lib:guava',
     '//lib:gwtorm',
     '//lib/guice:guice',
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index eb0dfaa..e869afb 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -25,6 +25,7 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.index.IndexUtils;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.FieldDef.FillArgs;
@@ -51,7 +52,6 @@
 import org.apache.lucene.search.SearcherFactory;
 import org.apache.lucene.store.AlreadyClosedException;
 import org.apache.lucene.store.Directory;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -75,17 +75,6 @@
     return f.getName() + "_SORT";
   }
 
-  public static void setReady(SitePaths sitePaths, String name, int version,
-      boolean ready) throws IOException {
-    try {
-      GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
-      cfg.setReady(name, version, ready);
-      cfg.save();
-    } catch (ConfigInvalidException e) {
-      throw new IOException(e);
-    }
-  }
-
   private final Schema<V> schema;
   private final SitePaths sitePaths;
   private final Directory dir;
@@ -198,7 +187,7 @@
 
   @Override
   public void markReady(boolean ready) throws IOException {
-    setReady(sitePaths, name, schema.getVersion(), ready);
+    IndexUtils.setReady(sitePaths, name, schema.getVersion(), ready);
   }
 
   @Override
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 2decff5..4775ac4 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
@@ -18,7 +18,6 @@
 import static com.google.gerrit.lucene.AbstractLuceneIndex.sortFieldName;
 import static com.google.gerrit.lucene.LuceneVersionManager.CHANGES_PREFIX;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
-import static com.google.gerrit.server.index.change.ChangeField.CHANGE;
 import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
 import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
@@ -27,7 +26,6 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
@@ -35,6 +33,7 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.index.IndexUtils;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -317,7 +316,7 @@
         throw new OrmException("interrupted");
       }
 
-      final Set<String> fields = fields(opts);
+      final Set<String> fields = IndexUtils.fields(opts);
       return new ChangeDataResults(
           executor.submit(new Callable<List<Document>>() {
             @Override
@@ -405,22 +404,6 @@
     }
   }
 
-  private Set<String> fields(QueryOptions opts) {
-    // Ensure we request enough fields to construct a ChangeData. We need both
-    // change ID and project, which can either come via the Change field or
-    // separate fields.
-    Set<String> fs = opts.fields();
-    if (fs.contains(CHANGE.getName())) {
-      // A Change is always sufficient.
-      return fs;
-    }
-    if (fs.contains(PROJECT.getName()) && fs.contains(LEGACY_ID.getName())) {
-      return fs;
-    }
-    return Sets.union(fs,
-        ImmutableSet.of(LEGACY_ID.getName(), PROJECT.getName()));
-  }
-
   private static Multimap<String, IndexableField> fields(Document doc,
       Set<String> fields) {
     Multimap<String, IndexableField> stored =
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index f5d5146..58890176 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -15,37 +15,23 @@
 package com.google.gerrit.lucene;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.index.SingleVersionModule;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.index.Index;
 import com.google.gerrit.server.index.IndexConfig;
-import com.google.gerrit.server.index.IndexDefinition;
 import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.change.ChangeIndex;
-import com.google.inject.Inject;
 import com.google.inject.Provides;
-import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
-import com.google.inject.name.Named;
-import com.google.inject.name.Names;
 
 import org.apache.lucene.search.BooleanQuery;
 import org.eclipse.jgit.lib.Config;
 
-import java.util.Collection;
 import java.util.Map;
-import java.util.Set;
 
 public class LuceneIndexModule extends LifecycleModule {
-  private static final String SINGLE_VERSIONS =
-      "LuceneIndexModule/SingleVersions";
-
   public static LuceneIndexModule singleVersionAllLatest(int threads) {
     return new LuceneIndexModule(ImmutableMap.<String, Integer> of(), threads);
   }
@@ -86,7 +72,7 @@
     if (singleVersions == null) {
       install(new MultiVersionModule());
     } else {
-      install(new SingleVersionModule());
+      install(new SingleVersionModule(singleVersions));
     }
   }
 
@@ -104,66 +90,4 @@
       listener().to(LuceneVersionManager.class);
     }
   }
-
-  private class SingleVersionModule extends LifecycleModule {
-    @Override
-    public void configure() {
-      listener().to(SingleVersionListener.class);
-      bind(new TypeLiteral<Map<String, Integer>>() {})
-          .annotatedWith(Names.named(SINGLE_VERSIONS))
-          .toInstance(singleVersions);
-    }
-  }
-
-  @Singleton
-  static class SingleVersionListener implements LifecycleListener {
-    private final Set<String> disabled;
-    private final Collection<IndexDefinition<?, ?, ?>> defs;
-    private final Map<String, Integer> singleVersions;
-
-    @Inject
-    SingleVersionListener(
-        @GerritServerConfig Config cfg,
-        Collection<IndexDefinition<?, ?, ?>> defs,
-        @Named(SINGLE_VERSIONS) Map<String, Integer> singleVersions) {
-      this.defs = defs;
-      this.singleVersions = singleVersions;
-
-      disabled = ImmutableSet.copyOf(
-          cfg.getStringList("index", null, "testDisable"));
-    }
-
-    @Override
-    public void start() {
-      for (IndexDefinition<?, ?, ?> def : defs) {
-        start(def);
-      }
-    }
-
-    private <K, V, I extends Index<K, V>> void start(
-        IndexDefinition<K, V, I> def) {
-      if (disabled.contains(def.getName())) {
-        return;
-      }
-      Schema<V> schema;
-      Integer v = singleVersions.get(def.getName());
-      if (v == null) {
-        schema = def.getLatest();
-      } else {
-        schema = def.getSchemas().get(v);
-        if (schema == null) {
-          throw new ProvisionException(String.format(
-                "Unrecognized %s schema version: %s", def.getName(), v));
-        }
-      }
-      I index = def.getIndexFactory().create(schema);
-      def.getIndexCollection().setSearchIndex(index);
-      def.getIndexCollection().addWriteIndex(index);
-    }
-
-    @Override
-    public void stop() {
-      // Do nothing; indexes are closed by IndexCollection.
-    }
-  }
 }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index b46f1f6..2f871fc 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.index.GerritIndexStatus;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.Index;
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index 8852133..5f2ef43 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -47,7 +47,7 @@
     ':init-api',
     ':util',
     '//gerrit-common:annotations',
-    '//gerrit-lucene:lucene',
+    '//gerrit-index:index',
     '//lib:args4j',
     '//lib:derby',
     '//lib:gwtjsonrpc',
@@ -66,6 +66,7 @@
 
 REST_UTIL_DEPS = [
   '//gerrit-cache-h2:cache-h2',
+  '//gerrit-elasticsearch:elasticsearch',
   '//gerrit-util-cli:cli',
   '//lib:args4j',
   '//lib:gwtorm',
@@ -120,6 +121,7 @@
   ':init-api',
   ':util',
   '//gerrit-cache-h2:cache-h2',
+  '//gerrit-elasticsearch:elasticsearch',
   '//gerrit-gpg:gpg',
   '//gerrit-lucene:lucene',
   '//gerrit-oauth:oauth',
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD
index 895b5f1..5bdc8fb 100644
--- a/gerrit-pgm/BUILD
+++ b/gerrit-pgm/BUILD
@@ -164,4 +164,5 @@
 
 license_test(
   name = "pgm_license_test",
-  target = ":pgm")
+  target = ":pgm",
+)
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index eb17530..9d4120c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -20,6 +20,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.EventBroker;
+import com.google.gerrit.elasticsearch.ElasticIndexModule;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.AllRequestFilter;
@@ -408,15 +409,18 @@
     return cfgInjector.createChildInjector(modules);
   }
 
-  private AbstractModule createIndexModule() {
+  private Module createIndexModule() {
     if (slave) {
       return new DummyIndexModule();
     }
+    if (luceneModule != null) {
+      return luceneModule;
+    }
     switch (indexType) {
       case LUCENE:
-        return luceneModule != null
-            ? luceneModule
-            : LuceneIndexModule.latestVersionWithOnlineUpgrade();
+        return LuceneIndexModule.latestVersionWithOnlineUpgrade();
+      case ELASTICSEARCH:
+        return ElasticIndexModule.latestVersionWithOnlineUpgrade();
       default:
         throw new IllegalStateException("unsupported index.type = " + indexType);
     }
@@ -426,6 +430,7 @@
     indexType = IndexModule.getIndexType(cfgInjector);
     switch (indexType) {
       case LUCENE:
+      case ELASTICSEARCH:
         break;
       default:
         throw new IllegalStateException("unsupported index.type = " + indexType);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
index 501b115..ee0d02f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Die;
+import com.google.gerrit.elasticsearch.ElasticIndexModule;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lucene.LuceneIndexModule;
@@ -161,6 +162,10 @@
         indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(
             versions, threads);
         break;
+      case ELASTICSEARCH:
+        indexModule = ElasticIndexModule
+            .singleVersionWithExplicitVersions(versions, threads);
+        break;
       default:
         throw new IllegalStateException("unsupported index.type");
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
index 185063b..c8d8edb 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -15,7 +15,8 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.collect.Iterables;
-import com.google.gerrit.lucene.AbstractLuceneIndex;
+import com.google.common.collect.Sets;
+import com.google.gerrit.index.IndexUtils;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -61,9 +62,17 @@
       type = index.select("Type", "type", type);
     }
 
+    if (type == IndexType.ELASTICSEARCH) {
+      index.select("Transport protocol", "protocol", "http",
+          Sets.newHashSet("http", "https"));
+      index.string("Hostname", "hostname", "localhost");
+      index.string("Port", "port", "9200");
+      index.string("Index Name", "name", "gerrit");
+    }
+
     if ((site.isNew || isEmptySite()) && type == IndexType.LUCENE) {
       for (SchemaDefinitions<?> def : IndexModule.ALL_SCHEMA_DEFS) {
-        AbstractLuceneIndex.setReady(
+        IndexUtils.setReady(
             site, def.getName(), def.getLatest().getVersion(), true);
       }
     } else {
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 66fc545..a50df82 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -91,6 +91,7 @@
   ':server',
   '//gerrit-common:server',
   '//gerrit-cache-h2:cache-h2',
+  '//gerrit-elasticsearch:elasticsearch',
   '//gerrit-extension-api:api',
   '//gerrit-gpg:gpg',
   '//gerrit-lucene:lucene',
@@ -181,6 +182,7 @@
     '//gerrit-server/src/main/prolog:common',
     '//lib/antlr:java_runtime',
   ],
+  visibility = ['PUBLIC'],
 )
 
 java_test(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 63d2ddb..149931d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -174,7 +174,7 @@
       this.loader = loader;
       this.byName = byUsername;
       this.readFromGit =
-          cfg.getBoolean("user", null, "readProjectWatchesFromGit", true);
+          cfg.getBoolean("user", null, "readProjectWatchesFromGit", false);
       this.watchConfig = watchConfig;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
index 7cda472..3748e17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
@@ -60,7 +60,7 @@
     this.dbProvider = dbProvider;
     this.self = self;
     this.readFromGit =
-        cfg.getBoolean("user", null, "readProjectWatchesFromGit", true);
+        cfg.getBoolean("user", null, "readProjectWatchesFromGit", false);
     this.watchConfig = watchConfig;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index 871b1cd..aa32d27 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -101,7 +101,7 @@
       return read(accountId).getKey(seq);
     }
 
-    public AccountSshKey addKey(Account.Id accountId, String pub)
+    public synchronized AccountSshKey addKey(Account.Id accountId, String pub)
         throws IOException, ConfigInvalidException, InvalidSshKeyException {
       VersionedAuthorizedKeys authorizedKeys = read(accountId);
       AccountSshKey key = authorizedKeys.addKey(pub);
@@ -109,7 +109,7 @@
       return key;
     }
 
-    public void deleteKey(Account.Id accountId, int seq)
+    public synchronized void deleteKey(Account.Id accountId, int seq)
         throws IOException, ConfigInvalidException {
       VersionedAuthorizedKeys authorizedKeys = read(accountId);
       if (authorizedKeys.deleteKey(seq)) {
@@ -117,7 +117,7 @@
       }
     }
 
-    public void markKeyInvalid(Account.Id accountId, int seq)
+    public synchronized void markKeyInvalid(Account.Id accountId, int seq)
         throws IOException, ConfigInvalidException {
       VersionedAuthorizedKeys authorizedKeys = read(accountId);
       if (authorizedKeys.markKeyInvalid(seq)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java
index c3d28ca..a3cd0c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java
@@ -121,7 +121,7 @@
       }
     }
 
-    public void upsertProjectWatches(Account.Id accountId,
+    public synchronized void upsertProjectWatches(Account.Id accountId,
         Map<ProjectWatchKey, Set<NotifyType>> newProjectWatches)
         throws IOException, ConfigInvalidException {
       WatchConfig watchConfig = read(accountId);
@@ -131,7 +131,7 @@
       commit(watchConfig);
     }
 
-    public void deleteProjectWatches(Account.Id accountId,
+    public synchronized void deleteProjectWatches(Account.Id accountId,
         Collection<ProjectWatchKey> projectWatchKeys)
             throws IOException, ConfigInvalidException {
       WatchConfig watchConfig = read(accountId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index c2c12f0..4139fc5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -2012,7 +2012,6 @@
       }
       cmd = new ReceiveCommand(ObjectId.zeroId(), commit,
           ins.getPatchSetId().toRefName());
-      ins.setUpdateRefCommand(cmd);
       if (rp.getPushCertificate() != null) {
         ins.setPushCertificate(rp.getPushCertificate().toTextWithSignature());
       }
@@ -2051,7 +2050,7 @@
             .setNotify(magicBranch.notify)
             .setRequestScopePropagator(requestScopePropagator)
             .setSendMail(true)
-            .setUpdateRef(true));
+            .setUpdateRef(false));
         if (!magicBranch.hashtags.isEmpty()) {
           bu.addOp(
               changeId,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index 6a25862..9e0be86 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -55,7 +55,7 @@
  */
 public class IndexModule extends LifecycleModule {
   public enum IndexType {
-    LUCENE
+    LUCENE, ELASTICSEARCH
   }
 
   public static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java
index 36e5792..e98211e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import com.google.gerrit.server.index.Index;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.IndexConfig;
@@ -74,6 +75,25 @@
     }
   }
 
+  /**
+   * Run multiple queries in parallel.
+   * <p>
+   * If a limit was specified using {@link #setLimit(int)}, that limit is
+   * applied to each query independently.
+   *
+   * @param queries list of queries.
+   * @return results of the queries, one list of results per input query, in the
+   *     same order as the input.
+   */
+  public List<List<T>> query(List<Predicate<T>> queries) throws OrmException {
+    try {
+      return Lists.transform(
+          queryProcessor.query(queries), QueryResult::entities);
+    } catch (QueryParseException e) {
+      throw new OrmException(e);
+    }
+  }
+
   protected Schema<T> schema() {
     Index<?, T> index = indexes != null ? indexes.getSearchIndex() : null;
     return index != null ? index.getSchema() : null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
index d08f05c..0997a40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
@@ -122,16 +122,12 @@
     return query(ImmutableList.of(query)).get(0);
   }
 
-  /*
-   * Perform multiple queries over a list of query strings.
-   * <p>
-   * If a limit was specified using {@link #setLimit(int)} this method may
-   * return up to {@code limit + 1} results, allowing the caller to determine if
-   * there are more than {@code limit} matches and suggest to its own caller
-   * that the query could be retried with {@link #setStart(int)}.
+  /**
+   * Perform multiple queries in parallel.
    *
-   * @param queries the queries.
-   * @return results of the queries, one list per input query.
+   * @param queries list of queries.
+   * @return results of the queries, one QueryResult per input query, in the
+   *     same order as the input.
    */
   public List<QueryResult<T>> query(List<Predicate<T>> queries)
       throws OrmException, QueryParseException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
index 9573f99..3a0c699 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
@@ -176,7 +176,11 @@
       }
       stmt.executeBatch();
     } catch (SQLException e) {
-      throw convertError("insert", e);
+      OrmException ormException = convertError("insert", e);
+      if (ormException instanceof OrmDuplicateKeyException) {
+        return;
+      }
+      throw ormException;
     }
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 84fd9d7..71401d7 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -220,6 +220,9 @@
         case LUCENE:
           install(luceneIndexModule());
           break;
+        case ELASTICSEARCH:
+          install(elasticIndexModule());
+          break;
         default:
           throw new ProvisionException(
               "index type unsupported in tests: " + indexType);
@@ -242,14 +245,21 @@
   }
 
   private Module luceneIndexModule() {
+    return indexModule("com.google.gerrit.lucene.LuceneIndexModule");
+  }
+
+  private Module elasticIndexModule() {
+    return indexModule("com.google.gerrit.elasticsearch.ElasticIndexModule");
+  }
+
+  private Module indexModule(String moduleClassName) {
     try {
       Map<String, Integer> singleVersions = new HashMap<>();
       int version = cfg.getInt("index", "lucene", "testVersion", -1);
       if (version > 0) {
         singleVersions.put(ChangeSchemaDefinitions.INSTANCE.getName(), version);
       }
-      Class<?> clazz =
-          Class.forName("com.google.gerrit.lucene.LuceneIndexModule");
+      Class<?> clazz = Class.forName(moduleClassName);
       Method m = clazz.getMethod(
           "singleVersionWithExplicitVersions", Map.class, int.class);
       return (Module) m.invoke(null, singleVersions, 0);
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
index 6d74a83..5dd1b04 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -5,6 +5,7 @@
   srcs = glob(['src/main/java/**/*.java']),
   deps = [
     '//gerrit-cache-h2:cache-h2',
+    '//gerrit-elasticsearch:elasticsearch',
     '//gerrit-extension-api:api',
     '//gerrit-gpg:gpg',
     '//gerrit-httpd:httpd',
diff --git a/gerrit-war/BUILD b/gerrit-war/BUILD
index 86c838f..ae210d4 100644
--- a/gerrit-war/BUILD
+++ b/gerrit-war/BUILD
@@ -62,9 +62,9 @@
   cmd = ' && '.join([
     'cd $$TMP',
     'mkdir -p com/google/gerrit/common',
-    'cat $$ROOT/$(location //:version) >com/google/gerrit/common/Version',
+    'cat $$ROOT/$(location //:version.txt) >com/google/gerrit/common/Version',
     'zip -9Dqr $$ROOT/$@ .',
   ]),
-  tools = ['//:version'],
+  tools = ['//:version.txt'],
   out = 'gen_version.jar',
 )
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index fc0beae..9dba629 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.Splitter;
 import com.google.gerrit.common.EventBroker;
+import com.google.gerrit.elasticsearch.ElasticIndexModule;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.auth.oauth.OAuthModule;
@@ -343,6 +344,8 @@
     switch (indexType) {
       case LUCENE:
         return LuceneIndexModule.latestVersionWithOnlineUpgrade();
+      case ELASTICSEARCH:
+        return ElasticIndexModule.latestVersionWithOnlineUpgrade();
       default:
         throw new IllegalStateException("unsupported index.type = " + indexType);
     }
diff --git a/lib/BUILD b/lib/BUILD
index 4bd5ad7..fd25243 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -1,55 +1,102 @@
 exports_files([
-  "LICENSE-DO_NOT_DISTRIBUTE"
+  "LICENSE-antlr",
+  "LICENSE-Apache1.1",
+  "LICENSE-Apache2.0",
+  "LICENSE-args4j",
+  "LICENSE-asciidoctor",
+  "LICENSE-automaton",
+  "LICENSE-bouncycastle",
+  "LICENSE-CC-BY3.0-unported",
+  "LICENSE-clippy",
+  "LICENSE-codemirror-minified",
+  "LICENSE-codemirror-original",
+  "LICENSE-diffy",
+  "LICENSE-es6-promise",
+  "LICENSE-fetch",
+  "LICENSE-h2",
+  "LICENSE-highlightjs",
+  "LICENSE-icu4j",
+  "LICENSE-jgit",
+  "LICENSE-jsch",
+  "LICENSE-MPL1.1",
+  "LICENSE-moment",
+  "LICENSE-OFL1.1",
+  "LICENSE-ow2",
+  "LICENSE-page.js",
+  "LICENSE-polymer",
+  "LICENSE-postgresql",
+  "LICENSE-prologcafe",
+  "LICENSE-promise-polyfill",
+  "LICENSE-protobuf",
+  "LICENSE-PublicDomain",
+  "LICENSE-silk_icons",
+  "LICENSE-slf4j",
+  "LICENSE-xz",
+
+  "LICENSE-DO_NOT_DISTRIBUTE",
 ])
 
+filegroup(
+  name = 'all-licenses',
+  srcs = glob(['LICENSE-*'], exclude = ['LICENSE-DO_NOT_DISTRIBUTE']),
+  visibility = ['//visibility:public'],
+)
+
 java_library(
   name = 'servlet-api-3_1',
   neverlink = 1,
-  data = [ ":LICENSE-Apache2.0" ],
   exports = ['@servlet_api_3_1//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'servlet-api-3_1-without-neverlink',
   exports = ['@servlet_api_3_1//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'gwtjsonrpc',
   exports = ['@gwtjsonrpc//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'gwtjsonrpc_src',
   exports = ['@gwtjsonrpc_src//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'gson',
   exports = ['@gson//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'gwtorm_client',
   exports = ['@gwtorm_client//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'gwtorm_client_src',
   exports = ['@gwtorm_client_src//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'protobuf',
   exports = ['@protobuf//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-protobuf'],
 )
 
 java_library(
@@ -63,6 +110,7 @@
   name = 'guava',
   exports = ['@guava//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -74,31 +122,35 @@
     '//lib/commons:oro',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'jsch',
   exports = ['@jsch//jar'],
   visibility = ['//visibility:public'],
-  data = [ ":LICENSE-jsch" ],
+  data = ['//lib:LICENSE-jsch'],
 )
 
 java_library(
   name = 'juniversalchardet',
   exports = ['@juniversalchardet//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-MPL1.1'],
 )
 
 java_library(
   name = 'args4j',
   exports = ['@args4j//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-args4j'],
 )
 
 java_library(
   name = 'automaton',
   exports = ['@automaton//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-automaton'],
 )
 
 java_library(
@@ -106,6 +158,7 @@
   exports = ['@pegdown//jar'],
   runtime_deps = [':grappa'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -119,24 +172,28 @@
     '//lib/ow2:ow2-asm-util',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'jitescript',
   exports = ['@jitescript//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'tukaani-xz',
   exports = ['@tukaani_xz//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-xz'],
 )
 
 java_library(
   name = 'mime-util',
   exports = ['@mime_util//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -144,17 +201,20 @@
   exports = ['@guava_retrying//jar'],
   runtime_deps = [':jsr305'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'jsr305',
   exports = ['@jsr305//jar'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'blame-cache',
   exports = ['@blame_cache//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 
@@ -162,6 +222,7 @@
   name = 'h2',
   exports = ['@h2//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-h2'],
 )
 
 
@@ -170,6 +231,7 @@
   exports = ['@jimfs//jar'],
   runtime_deps = [':guava'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
@@ -180,12 +242,14 @@
   ],
   runtime_deps = [':hamcrest-core'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
   name = 'hamcrest-core',
   exports = ['@hamcrest_core//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
@@ -196,18 +260,21 @@
     ':junit',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
   name = 'javassist',
   exports = ['@javassist//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
   name = 'derby',
   exports = ['@derby//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -230,22 +297,26 @@
     '//lib/ow2:ow2-asm-util',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'icu4j',
   exports = [ '@icu4j//jar' ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-icu4j'],
 )
 
 java_library(
   name = 'postgresql',
   exports = ['@postgresql//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-postgresql'],
 )
 
 java_library(
   name = 'commons-io',
   exports = ['@commons_io//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/antlr/BUILD b/lib/antlr/BUILD
index ede7665..92f3d0f 100644
--- a/lib/antlr/BUILD
+++ b/lib/antlr/BUILD
@@ -2,6 +2,7 @@
 [java_library(
   name = n,
   exports = ['@%s//jar' % n],
+  data = ['//lib:LICENSE-antlr'],
 ) for n in [
   'antlr27',
   'stringtemplate',
@@ -11,6 +12,7 @@
   name = 'java_runtime',
   exports = ['@java_runtime//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-antlr'],
 )
 
 java_binary(
@@ -28,4 +30,5 @@
     ':java_runtime',
     ':stringtemplate',
   ],
+  data = ['//lib:LICENSE-antlr'],
 )
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
index 733c670..5b4cd6b 100644
--- a/lib/asciidoctor/BUCK
+++ b/lib/asciidoctor/BUCK
@@ -53,8 +53,8 @@
 
 maven_jar(
   name = 'jruby',
-  id = 'org.jruby:jruby-complete:1.7.25',
-  sha1 = '8eb234259ec88edc05eedab05655f458a84bfcab',
+  id = 'org.jruby:jruby-complete:9.1.5.0',
+  sha1 = '00d0003e99da3c4d830b12c099691ce910c84e39',
   license = 'DO_NOT_DISTRIBUTE',
   visibility = [],
   attach_source = False,
diff --git a/lib/asciidoctor/BUILD b/lib/asciidoctor/BUILD
new file mode 100644
index 0000000..4b4e958
--- /dev/null
+++ b/lib/asciidoctor/BUILD
@@ -0,0 +1,47 @@
+java_library(
+  name = "asciidoc_lib",
+  srcs = ["java/AsciiDoctor.java"],
+  deps = [
+    ":asciidoctor",
+    "//lib:args4j",
+    "//lib:guava",
+    "//lib/log:api",
+    "//lib/log:nop",
+  ],
+  visibility = ["//visibility:public"],
+)
+
+java_binary(
+  name = "doc_indexer",
+  main_class = "DocIndexer",
+  runtime_deps = [":doc_indexer_lib"],
+  visibility = ["//visibility:public"],
+)
+
+java_library(
+  name = "doc_indexer_lib",
+  srcs = ["java/DocIndexer.java"],
+  deps = [
+    ":asciidoc_lib",
+    "//gerrit-server:constants",
+    "//lib:args4j",
+    "//lib:guava",
+    "//lib/lucene:lucene-analyzers-common",
+    "//lib/lucene:lucene-core-and-backward-codecs",
+  ],
+  visibility = ["//visibility:public"],
+)
+
+java_library(
+  name = "asciidoctor",
+  exports = ["@asciidoctor//jar"],
+  runtime_deps = [":jruby"],
+  visibility = ["//visibility:public"],
+  data = ["//lib:LICENSE-asciidoctor"],
+)
+
+java_library(
+  name = "jruby",
+  exports = ["@jruby//jar"],
+  data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
+)
diff --git a/lib/auto/BUILD b/lib/auto/BUILD
index e07c36d..c50c105 100644
--- a/lib/auto/BUILD
+++ b/lib/auto/BUILD
@@ -18,4 +18,5 @@
   ],
   exports = ['@auto_value//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/bouncycastle/BUILD b/lib/bouncycastle/BUILD
index 49c54ba..333c355 100644
--- a/lib/bouncycastle/BUILD
+++ b/lib/bouncycastle/BUILD
@@ -3,12 +3,14 @@
   neverlink = 1,
   exports = ['@bcprov//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
   name = 'bcprov-without-neverlink',
   exports = ['@bcprov//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
@@ -16,12 +18,14 @@
   neverlink = 1,
   exports = ['@bcpg//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
   name = 'bcpg-without-neverlink',
   exports = ['@bcpg//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
@@ -29,10 +33,12 @@
   neverlink = 1,
   exports = ['@bcpkix//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
   name = 'bcpkix-without-neverlink',
   exports = ['@bcpkix//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index 56145ea..be50417 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -1,14 +1,14 @@
 include_defs('//lib/maven.defs')
 include_defs('//lib/codemirror/cm.defs')
 
-VERSION = '5.18.2'
+VERSION = '5.19.0'
 TOP = 'META-INF/resources/webjars/codemirror/%s' % VERSION
 TOP_MINIFIED = 'META-INF/resources/webjars/codemirror-minified/%s' % VERSION
 
 maven_jar(
   name = 'codemirror-minified',
   id = 'org.webjars.npm:codemirror-minified:' + VERSION,
-  sha1 = '6755af157a7eaf2401468906bef67bbacc3c97f6',
+  sha1 = '263bf4acb7c4429be3fe46908af240f9f629d51c',
   attach_source = False,
   license = 'codemirror-minified',
   visibility = [],
@@ -17,7 +17,7 @@
 maven_jar(
   name = 'codemirror-original',
   id = 'org.webjars.npm:codemirror:' + VERSION,
-  sha1 = '18c721ae88eed27cddb458c42f5d221fa3d9713e',
+  sha1 = 'e9ab382c6be240d55f112051bba3f6c637b798ce',
   attach_source = False,
   license = 'codemirror-original',
   visibility = [],
diff --git a/lib/codemirror/cm.bzl b/lib/codemirror/cm.bzl
index b4e55fe..fbf1e91 100644
--- a/lib/codemirror/cm.bzl
+++ b/lib/codemirror/cm.bzl
@@ -212,18 +212,20 @@
   'z80',
 ]
 
-VERSION = '5.18.2'
+VERSION = '5.19.0'
 TOP = 'META-INF/resources/webjars/codemirror/%s' % VERSION
 TOP_MINIFIED = 'META-INF/resources/webjars/codemirror-minified/%s' % VERSION
+LICENSE = '//lib:LICENSE-codemirror-original'
+LICENSE_MINIFIED = '//lib:LICENSE-codemirror-original-minified'
 
 DIFF_MATCH_PATCH_VERSION = '20121119-1'
 DIFF_MATCH_PATCH_TOP = ('META-INF/resources/webjars/google-diff-match-patch/%s'
     % DIFF_MATCH_PATCH_VERSION)
 
 def pkg_cm():
-  for archive, suffix, top in [
-      ('@codemirror_original//jar', '', TOP),
-      ('@codemirror_minified//jar', '_r', TOP_MINIFIED)
+  for archive, suffix, top, license in [
+      ('@codemirror_original//jar', '', TOP, LICENSE),
+      ('@codemirror_minified//jar', '_r', TOP_MINIFIED, LICENSE_MINIFIED)
   ]:
     # Main JavaScript and addons
     genrule2(
@@ -352,4 +354,5 @@
       name = 'codemirror' + suffix,
       jars = [':jar%s' % suffix],
       visibility = ['//visibility:public'],
+      data = [license],
     )
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index 7c27477..55c07a6 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -47,6 +47,13 @@
 )
 
 maven_jar(
+  name = 'lang3',
+  id = 'org.apache.commons:commons-lang3:3.3.2',
+  sha1 = '90a3822c38ec8c996e84c16a3477ef632cbc87a3',
+  license = 'Apache2.0',
+)
+
+maven_jar(
   name = 'net',
   id = 'commons-net:commons-net:3.5',
   sha1 = '342fc284019f590e1308056990fdb24a08f06318',
diff --git a/lib/commons/BUILD b/lib/commons/BUILD
index 8c42e53f..7f6f6b2 100644
--- a/lib/commons/BUILD
+++ b/lib/commons/BUILD
@@ -2,30 +2,35 @@
   name = 'codec',
   exports = ['@commons_codec//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'collections',
   exports = ['@commons_collections//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'compress',
   exports = ['@commons_compress//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'lang',
   exports = ['@commons_lang//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'net',
   exports = ['@commons_net//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -33,22 +38,26 @@
   exports = ['@commons_dbcp//jar'],
   runtime_deps = [':pool'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'pool',
   exports = ['@commons_pool//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'oro',
   exports = ['@commons_oro//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache1.1'],
 )
 
 java_library(
   name = 'validator',
   exports = ['@commons_validator//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/dropwizard/BUILD b/lib/dropwizard/BUILD
index 9d4a8d3..b456d5e 100644
--- a/lib/dropwizard/BUILD
+++ b/lib/dropwizard/BUILD
@@ -2,4 +2,5 @@
   name = 'dropwizard-core',
   exports = ['@dropwizard_core//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/easymock/BUILD b/lib/easymock/BUILD
index 703eba7..fce3ff7 100644
--- a/lib/easymock/BUILD
+++ b/lib/easymock/BUILD
@@ -6,17 +6,20 @@
     ':objenesis',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
   name = 'cglib-3_2',
   exports = ['@cglib_3_2//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
   name = 'objenesis',
   exports = ['@objenesis//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
diff --git a/lib/elasticsearch/BUCK b/lib/elasticsearch/BUCK
new file mode 100644
index 0000000..86594ce
--- /dev/null
+++ b/lib/elasticsearch/BUCK
@@ -0,0 +1,104 @@
+include_defs('//lib/maven.defs')
+
+# Java client library for Elasticsearch.
+maven_jar(
+  name = 'elasticsearch',
+  id = 'org.elasticsearch:elasticsearch:2.4.0',
+  sha1 = 'aeb9704a76fa8654c348f38fcbb993a952a7ab07',
+  attach_source = True,
+  repository = MAVEN_CENTRAL,
+  license = 'Apache2.0',
+  deps = [
+    ':jna',
+    ':hppc',
+    ':jsr166e',
+    ':netty',
+    ':t-digest',
+    ':compress-lzf',
+    '//lib/joda:joda-time',
+    '//lib/lucene:lucene-codecs',
+    '//lib/lucene:lucene-highlighter',
+    '//lib/lucene:lucene-join',
+    '//lib/lucene:lucene-memory',
+    '//lib/lucene:lucene-sandbox',
+    '//lib/lucene:lucene-suggest',
+    '//lib/lucene:lucene-queries',
+    '//lib/lucene:lucene-spatial',
+    '//lib/jackson:jackson-core',
+    '//lib/jackson:jackson-dataformat-cbor',
+    '//lib/jackson:jackson-dataformat-smile',
+  ]
+)
+
+# Java REST client for Elasticsearch.
+VERSION = '0.1.7'
+
+maven_jar(
+  name = 'jest-common',
+  id = 'io.searchbox:jest-common:' + VERSION,
+  sha1 = 'ff6e2694405557a3a02b444cb7f7da28c4d99f07',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'jest',
+  id = 'io.searchbox:jest:' + VERSION,
+  sha1 = '686619c7141edb50b562ad2a39d32ea4cf20b567',
+  license = 'Apache2.0',
+  deps = [
+    ':elasticsearch',
+    ':jest-common',
+    '//lib/commons:lang3',
+    '//lib/httpcomponents:httpasyncclient',
+    '//lib/httpcomponents:httpclient',
+    '//lib/httpcomponents:httpcore-nio',
+    '//lib/httpcomponents:httpcore-niossl',
+  ],
+)
+
+maven_jar(
+  name = 'compress-lzf',
+  id = 'com.ning:compress-lzf:1.0.2',
+  sha1 = '62896e6fca184c79cc01a14d143f3ae2b4f4b4ae',
+  license = 'Apache2.0',
+  visibility = ['//lib/elasticsearch:elasticsearch'],
+)
+
+maven_jar(
+  name = 'hppc',
+  id = 'com.carrotsearch:hppc:0.7.1',
+  sha1 = '8b5057f74ea378c0150a1860874a3ebdcb713767',
+  license = 'Apache2.0',
+  visibility = ['//lib/elasticsearch:elasticsearch'],
+)
+
+maven_jar(
+  name = 'jsr166e',
+  id = 'com.twitter:jsr166e:1.1.0',
+  sha1 = '233098147123ee5ddcd39ffc57ff648be4b7e5b2',
+  license = 'Apache2.0',
+  visibility = ['//lib/elasticsearch:elasticsearch'],
+)
+
+maven_jar(
+  name = 'netty',
+  id = 'io.netty:netty:3.10.0.Final',
+  sha1 = 'ad61cd1bba067e6634ddd3e160edf0727391ac30',
+  license = 'Apache2.0',
+  visibility = ['//lib/elasticsearch:elasticsearch'],
+)
+
+maven_jar(
+  name = 't-digest',
+  id = 'com.tdunning:t-digest:3.0',
+  sha1 = '84ccf145ac2215e6bfa63baa3101c0af41017cfc',
+  license = 'Apache2.0',
+  visibility = ['//lib/elasticsearch:elasticsearch'],
+)
+
+maven_jar(
+  name = 'jna',
+  id = 'net.java.dev.jna:jna:4.1.0',
+  sha1 = '1c12d070e602efd8021891cdd7fd18bc129372d4',
+  license = 'Apache2.0',
+)
diff --git a/lib/guice/BUILD b/lib/guice/BUILD
index 5850af2..43018e0 100644
--- a/lib/guice/BUILD
+++ b/lib/guice/BUILD
@@ -6,6 +6,7 @@
     ':multibindings',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -13,6 +14,7 @@
   exports = ['@guice_library//jar'],
   runtime_deps = ['aopalliance'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -20,6 +22,7 @@
   exports = ['@guice_assistedinject//jar'],
   runtime_deps = [':guice'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -27,21 +30,25 @@
   exports = ['@guice_servlet//jar'],
   runtime_deps = [':guice'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'aopalliance',
   exports = ['@aopalliance//jar'],
+  data = ['//lib:LICENSE-PublicDomain'],
 )
 
 java_library(
   name = 'javax-inject',
   exports = ['@javax_inject//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'multibindings',
   exports = [ '@multibindings//jar' ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/gwt/BUILD b/lib/gwt/BUILD
index a494a8c..46d8f6d 100644
--- a/lib/gwt/BUILD
+++ b/lib/gwt/BUILD
@@ -2,6 +2,7 @@
   name = n,
   exports = ['@%s//jar' % n.replace("-", "_")],
   visibility = ["//visibility:public"],
+  data = ['//lib:LICENSE-Apache2.0'],
 ) for n in [
   'ant',
   'colt',
@@ -17,11 +18,13 @@
   name = 'javax-validation_src',
   exports = ['@javax_validation_src//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'jsinterop-annotations_src',
   exports = ['@jsinterop_annotations_src//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
diff --git a/lib/httpcomponents/BUCK b/lib/httpcomponents/BUCK
index 03669f2..1e56f94 100644
--- a/lib/httpcomponents/BUCK
+++ b/lib/httpcomponents/BUCK
@@ -39,3 +39,25 @@
   src_sha1 = '5394d3715181a87009032335a55b0a9789f6e26f',
   license = 'Apache2.0',
 )
+
+maven_jar(
+  name = 'httpasyncclient',
+  id = 'org.apache.httpcomponents:httpasyncclient:4.1.2',
+  sha1 = '95aa3e6fb520191a0970a73cf09f62948ee614be',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'httpcore-nio',
+  id = 'org.apache.httpcomponents:httpcore-nio:' + VERSION,
+  sha1 = 'a8c5e3c3bfea5ce23fb647c335897e415eb442e3',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'httpcore-niossl',
+  id = 'org.apache.httpcomponents:httpcore-niossl:4.0-alpha6',
+  sha1 = '9c662e7247ca8ceb1de5de629f685c9ef3e4ab58',
+  license = 'Apache2.0',
+  attach_source = False,
+)
diff --git a/lib/httpcomponents/BUILD b/lib/httpcomponents/BUILD
index 74ab00a..1dd3ccf 100644
--- a/lib/httpcomponents/BUILD
+++ b/lib/httpcomponents/BUILD
@@ -3,6 +3,7 @@
   exports = ['@fluent_hc//jar'],
   runtime_deps = [':httpclient'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -14,16 +15,19 @@
     '//lib/log:jcl-over-slf4j',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'httpcore',
   exports = ['@httpcore//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'httpmime',
   exports = ['@httpmime//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/jackson/BUCK b/lib/jackson/BUCK
new file mode 100644
index 0000000..46056b5
--- /dev/null
+++ b/lib/jackson/BUCK
@@ -0,0 +1,26 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '2.6.6'
+
+maven_jar(
+  name = 'jackson-core',
+  id = 'com.fasterxml.jackson.core:jackson-core:' + VERSION,
+  sha1 = '02eb801df67aacaf5b1deb4ac626e1964508e47b',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'jackson-dataformat-smile',
+  id = 'com.fasterxml.jackson.dataformat:jackson-dataformat-smile:' + VERSION,
+  sha1 = 'ccbfc948748ed2754a58c1af9e0a02b5cc1aed69',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'jackson-dataformat-cbor',
+  id = 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:' + VERSION,
+  sha1 = '34c7b7ff495fc6b049612bdc9db0900a68e112f8',
+  license = 'Apache2.0'
+)
+
+
diff --git a/lib/jetty/BUILD b/lib/jetty/BUILD
index da3af1c..d6e6355 100644
--- a/lib/jetty/BUILD
+++ b/lib/jetty/BUILD
@@ -3,6 +3,7 @@
   exports = ['@jetty_servlet//jar'],
   runtime_deps = [':security'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -10,12 +11,14 @@
   exports = ['@jetty_security//jar'],
   runtime_deps = [':server'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'servlets',
   exports = ['@jetty_servlets//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -26,6 +29,7 @@
     ':http',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -36,12 +40,14 @@
     ':http',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'continuation',
   exports = ['@jetty_continuation//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -51,6 +57,7 @@
     ':io',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -59,9 +66,11 @@
     '@jetty_io//jar',
     ':util',
   ],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'util',
   exports = ['@jetty_util//jar'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUILD b/lib/jgit/org.eclipse.jgit.archive/BUILD
index 8fa94f2..7d6fe22 100644
--- a/lib/jgit/org.eclipse.jgit.archive/BUILD
+++ b/lib/jgit/org.eclipse.jgit.archive/BUILD
@@ -3,4 +3,5 @@
   exports = ['@jgit_archive//jar'],
   runtime_deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-jgit'],
 )
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUILD b/lib/jgit/org.eclipse.jgit.http.server/BUILD
index decbd31..a453513 100644
--- a/lib/jgit/org.eclipse.jgit.http.server/BUILD
+++ b/lib/jgit/org.eclipse.jgit.http.server/BUILD
@@ -5,6 +5,7 @@
   exports = ['@jgit_servlet//jar'],
   runtime_deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-jgit'],
 )
 
 unsign_jars(
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUILD b/lib/jgit/org.eclipse.jgit.junit/BUILD
index c743631..7f31261 100644
--- a/lib/jgit/org.eclipse.jgit.junit/BUILD
+++ b/lib/jgit/org.eclipse.jgit.junit/BUILD
@@ -5,6 +5,7 @@
   exports = ['@jgit_junit//jar'],
   runtime_deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 unsign_jars(
diff --git a/lib/jgit/org.eclipse.jgit/BUILD b/lib/jgit/org.eclipse.jgit/BUILD
index 2d41ad6..bfebb7e 100644
--- a/lib/jgit/org.eclipse.jgit/BUILD
+++ b/lib/jgit/org.eclipse.jgit/BUILD
@@ -5,12 +5,14 @@
   exports = ['@jgit//jar'],
   runtime_deps = [':ewah'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-jgit'],
 )
 
 java_library(
   name = 'ewah',
   exports = ['@ewah//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 unsign_jars(
diff --git a/lib/joda/BUILD b/lib/joda/BUILD
index a673bf5..ef759d7 100644
--- a/lib/joda/BUILD
+++ b/lib/joda/BUILD
@@ -3,9 +3,11 @@
   exports = ['@joda_time//jar'],
   runtime_deps = ['joda-convert'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'joda-convert',
   exports = ['@joda_convert//jar'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/log/BUILD b/lib/log/BUILD
index ac92ab6..1e40372 100644
--- a/lib/log/BUILD
+++ b/lib/log/BUILD
@@ -2,6 +2,7 @@
   name = 'api',
   exports = ['@log_api//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-slf4j'],
 )
 
 java_library(
@@ -9,6 +10,7 @@
   exports = ['@log_nop//jar'],
   runtime_deps = [':api'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-slf4j'],
 )
 
 java_library(
@@ -16,18 +18,21 @@
   exports = ['@impl_log4j//jar'],
   runtime_deps = [':log4j'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-slf4j'],
 )
 
 java_library(
   name = 'jcl-over-slf4j',
   exports = ['@jcl_over_slf4j//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-slf4j'],
 )
 
 java_library(
   name = 'log4j',
   exports = ['@log4j//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -38,10 +43,12 @@
     '//lib/commons:lang'
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'json-smart',
   exports = ['@json_smart//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index 64dda1d..8f2efa2 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,6 +1,6 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '5.5.3'
+VERSION = '5.5.2'
 
 # core and backward-codecs both provide
 # META-INF/services/org.apache.lucene.codecs.Codec, so they must be merged.
@@ -14,21 +14,32 @@
 )
 
 maven_jar(
-  name = 'lucene-core',
-  id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = '20540c6347259f35a0d264605b22ce2a13917066',
+  name = 'lucene-codecs',
+  id = 'org.apache.lucene:lucene-codecs:' + VERSION,
+  sha1 = 'e01fe463d9490bb1b4a6a168e771f7b7255a50b1',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
     'META-INF/NOTICE.txt',
   ],
-  visibility = [],
+)
+
+maven_jar(
+  name = 'lucene-core',
+  id = 'org.apache.lucene:lucene-core:' + VERSION,
+  sha1 = 'de5e5c3161ea01e89f2a09a14391f9b7ed66cdbb',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+  visibility = ['//gerrit-elasticsearch:elasticsearch'],
 )
 
 maven_jar(
   name = 'lucene-analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  sha1 = 'cf734ab72813af33dc1544ce61abc5c17b9d35e9',
+  sha1 = 'f0bc3114a6b43f8e64a33c471d5b9e8ddc51564d',
   license = 'Apache2.0',
   deps = [':lucene-core-and-backward-codecs'],
   exclude = [
@@ -40,7 +51,7 @@
 maven_jar(
   name = 'backward-codecs_jar',
   id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION,
-  sha1 = 'a167789e52a9dc6d93bf3b588f79fdc9d7559c15',
+  sha1 = 'c5cfcd7a8cf48a0144b61fb991c8e50a0bf868d5',
   license = 'Apache2.0',
   deps = [':lucene-core'],
   exclude = [
@@ -51,9 +62,42 @@
 )
 
 maven_jar(
+  name = 'lucene-highlighter',
+  id = 'org.apache.lucene:lucene-highlighter:' + VERSION,
+  sha1 = 'd127ac514e9df965ab0b57d92bbe0c68d3d145b8',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
+  name = 'lucene-join',
+  id = 'org.apache.lucene:lucene-join:'+ VERSION,
+  sha1 = 'dac1b322508f3f2696ecc49a97311d34d8382054',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
+  name = 'lucene-memory',
+  id = 'org.apache.lucene:lucene-memory:' + VERSION,
+  sha1 = '7409db9863d8fbc265c27793c6cc7511304182c2',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
   name = 'lucene-misc',
   id = 'org.apache.lucene:lucene-misc:' + VERSION,
-  sha1 = 'e356975c46447f06c71842632d0af9ec1baecfce',
+  sha1 = '37bbe5a2fb429499dfbe75d750d1778881fff45d',
   license = 'Apache2.0',
   deps = [':lucene-core-and-backward-codecs'],
   exclude = [
@@ -63,9 +107,52 @@
 )
 
 maven_jar(
+  name = 'lucene-sandbox',
+  id = 'org.apache.lucene:lucene-sandbox:' + VERSION,
+  sha1 = '30a91f120706ba66732d5a974b56c6971b3c8a16',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
+  name = 'lucene-spatial',
+  id = 'org.apache.lucene:lucene-spatial:' + VERSION,
+  sha1 = '8ed7a9a43d78222038573dd1c295a61f3c0bb0db',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+maven_jar(
+  name = 'lucene-suggest',
+  id = 'org.apache.lucene:lucene-suggest:' + VERSION,
+  sha1 = 'e8316b37dddcf2092a54dab2ce6aad0d5ad78585',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
+  name = 'lucene-queries',
+  id = 'org.apache.lucene:lucene-queries:' + VERSION,
+  sha1 = '692f1ad887cf4e006a23f45019e6de30f3312d3f',
+  license = 'Apache2.0',
+  exclude = [
+    'META-INF/LICENSE.txt',
+    'META-INF/NOTICE.txt',
+  ],
+)
+
+maven_jar(
   name = 'lucene-queryparser',
   id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  sha1 = 'e2452203d2c44cac5ac42b34e5dcc0a44bf29a53',
+  sha1 = '8ac921563e744463605284c6d9d2d95e1be5b87c',
   license = 'Apache2.0',
   deps = [':lucene-core-and-backward-codecs'],
   exclude = [
@@ -73,3 +160,4 @@
     'META-INF/NOTICE.txt',
   ],
 )
+
diff --git a/lib/lucene/BUILD b/lib/lucene/BUILD
index 679c9f0..e228c45 100644
--- a/lib/lucene/BUILD
+++ b/lib/lucene/BUILD
@@ -9,6 +9,7 @@
     '@lucene_core//jar',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -16,6 +17,7 @@
   exports = ['@lucene_analyzers_common//jar'],
   runtime_deps = [':lucene-core-and-backward-codecs'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -23,6 +25,7 @@
   exports = ['@lucene_misc//jar'],
   runtime_deps = [':lucene-core-and-backward-codecs'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
@@ -30,4 +33,5 @@
   exports = ['@lucene_queryparser//jar'],
   runtime_deps = [':lucene-core-and-backward-codecs'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/mina/BUILD b/lib/mina/BUILD
index 52468a4..b3ba684 100644
--- a/lib/mina/BUILD
+++ b/lib/mina/BUILD
@@ -3,10 +3,12 @@
   exports = ['@sshd//jar'],
   visibility = ['//visibility:public'],
   runtime_deps = [':core'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'core',
   exports = ['@mina_core//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/openid/BUILD b/lib/openid/BUILD
index 7d97a86..8c5da45 100644
--- a/lib/openid/BUILD
+++ b/lib/openid/BUILD
@@ -9,15 +9,18 @@
     '//lib/guice:guice',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'nekohtml',
   exports = ['@nekohtml//jar'],
   runtime_deps = [':xerces'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
 
 java_library(
   name = 'xerces',
   exports = ['@xerces//jar'],
+  data = ['//lib:LICENSE-Apache2.0'],
 )
diff --git a/lib/ow2/BUILD b/lib/ow2/BUILD
index 0b99b6f..4c37357 100644
--- a/lib/ow2/BUILD
+++ b/lib/ow2/BUILD
@@ -2,12 +2,14 @@
   name = 'ow2-asm',
   exports = ['@ow2_asm//jar'],
   visibility = ["//visibility:public"],
+  data = ['//lib:LICENSE-ow2'],
 )
 
 java_library(
   name = 'ow2-asm-analysis',
   exports = ['@ow2_asm_analysis//jar'],
   visibility = ["//visibility:public"],
+  data = ['//lib:LICENSE-ow2'],
 )
 
 java_library(
@@ -15,16 +17,19 @@
   exports = ['@ow2_asm_commons//jar'],
   runtime_deps = [':ow2-asm-tree'],
   visibility = ["//visibility:public"],
+  data = ['//lib:LICENSE-ow2'],
 )
 
 java_library(
   name = 'ow2-asm-tree',
   exports = ['@ow2_asm_tree//jar'],
   visibility = ["//visibility:public"],
+  data = ['//lib:LICENSE-ow2'],
 )
 
 java_library(
   name = 'ow2-asm-util',
   exports = ['@ow2_asm_util//jar'],
   visibility = ["//visibility:public"],
+  data = ['//lib:LICENSE-ow2'],
 )
diff --git a/lib/powermock/BUILD b/lib/powermock/BUILD
index 8dc7d23..075b6bf 100644
--- a/lib/powermock/BUILD
+++ b/lib/powermock/BUILD
@@ -6,6 +6,7 @@
     '//lib:junit',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
@@ -16,6 +17,7 @@
     '//lib:junit',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
@@ -26,6 +28,7 @@
     '//lib/easymock:objenesis',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
@@ -36,6 +39,7 @@
     '//lib/easymock:easymock',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
@@ -47,6 +51,7 @@
     '//lib:junit',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
 
 java_library(
@@ -57,4 +62,5 @@
     '//lib:junit',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
 )
diff --git a/lib/prolog/BUILD b/lib/prolog/BUILD
index 74d8b80..a45cff2 100644
--- a/lib/prolog/BUILD
+++ b/lib/prolog/BUILD
@@ -2,6 +2,7 @@
   name = 'runtime',
   exports = ['@prolog_runtime//jar'],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-prologcafe'],
 )
 
 java_library(
@@ -12,11 +13,13 @@
     ':runtime',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-prologcafe'],
 )
 
 java_library(
   name = 'io',
   exports = ['@prolog_io//jar'],
+  data = ['//lib:LICENSE-prologcafe'],
 )
 
 java_library(
@@ -27,6 +30,7 @@
     'runtime',
   ],
   visibility = ['//visibility:public'],
+  data = ['//lib:LICENSE-prologcafe'],
 )
 
 java_binary(
diff --git a/plugins/hooks b/plugins/hooks
index c474972..052ecd9 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit c47497201b4b641ccd1786300297a8ac2848fc07
+Subproject commit 052ecd9c4ed12e1ca0beef53e7bc4fb2e17ee580
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index a7d99a7..f58f762 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -15,6 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
 
 <dom-module id="gr-comment-list">
   <template>
@@ -39,8 +40,6 @@
       }
       .message {
         flex: 1;
-        white-space: pre-wrap;
-        word-wrap: break-word;
       }
     </style>
     <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
@@ -60,7 +59,10 @@
                File comment:
              </span>
           </a>
-          <div class="message">[[comment.message]]</div>
+          <gr-linked-text class="message"
+              pre
+              content="[[comment.message]]"
+              config="[[projectConfig.commentlinks]]"></gr-linked-text>
         </div>
       </template>
     </template>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index eaafc447..0362089 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -21,6 +21,7 @@
       changeNum: Number,
       comments: Object,
       patchNum: Number,
+      projectConfig: Object,
     },
 
     _computeFilesFromComments: function(comments) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 2eb6646..0dbefc5 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -38,8 +38,12 @@
       var originalTitle = message.split('\n')[0];
       var revertTitle = 'Revert "' + originalTitle + '"';
       // Figure out what the revert commit message should be.
-      var commitRegex = /\n{1,2}\nChange-Id: (\w+)\n/gm;
+      var commitRegex = /\nChange-Id: (\w+)\n\s*/g;
       var match = commitRegex.exec(message);
+      if (!match) {
+        alert('Unable to find Change-Id in footer of commit message.');
+        return;
+      }
       var revertCommitText = 'This reverts commit ' + match[1] + '.';
       // Add '> ' in front of the original commit text.
       var originalCommitText = message.replace(/^/gm, '> ');
@@ -47,7 +51,7 @@
       this.message = revertTitle + '\n\n' +
                      revertCommitText + '\n\n' +
                      'Reason for revert: <INSERT REASONING HERE>\n\n' +
-                     'Original issue\'s description:\n' + originalCommitText;
+                     'Original change\'s description:\n' + originalCommitText;
     },
 
     _handleConfirmTap: function(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
index 521aeef..8fa9a48 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -38,13 +38,21 @@
       element = fixture('basic');
     });
 
+    test('no match', function() {
+      assert.isNotOk(element.message);
+      var alertStub = sinon.stub(window, 'alert');
+      element.populateRevertMessage('not a change-id in sight');
+      assert.isTrue(alertStub.calledOnce);
+      alertStub.restore();
+    });
+
     test('single line', function() {
       assert.isNotOk(element.message);
       element.populateRevertMessage('one line commit\n\nChange-Id: abcdefg\n');
       var expected = 'Revert "one line commit"\n\n' +
                      'This reverts commit abcdefg.\n\n' +
                      'Reason for revert: <INSERT REASONING HERE>\n\n' +
-                     'Original issue\'s description:\n' +
+                     'Original change\'s description:\n' +
                      '> one line commit\n> \n' +
                      '> Change-Id: abcdefg\n> ';
       assert.equal(element.message, expected);
@@ -57,12 +65,26 @@
       var expected = 'Revert "many lines"\n\n' +
                      'This reverts commit abcdefg.\n\n' +
                      'Reason for revert: <INSERT REASONING HERE>\n\n' +
-                     'Original issue\'s description:\n' +
+                     'Original change\'s description:\n' +
                      '> many lines\n> commit\n> \n> message\n> \n' +
                      '> Change-Id: abcdefg\n> ';
       assert.equal(element.message, expected);
     });
 
+    test('issue above change id', function() {
+      assert.isNotOk(element.message);
+      element.populateRevertMessage(
+          'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n');
+      var expected = 'Revert "much lines"\n\n' +
+                     'This reverts commit abcdefg.\n\n' +
+                     'Reason for revert: <INSERT REASONING HERE>\n\n' +
+                     'Original change\'s description:\n' +
+                     '> much lines\n> very\n> \n> commit\n> \n' +
+                     '> Bug: Issue 42\n' +
+                     '> Change-Id: abcdefg\n> ';
+      assert.equal(element.message, expected);
+    });
+
     test('revert a revert', function () {
       assert.isNotOk(element.message);
       element.populateRevertMessage(
@@ -70,7 +92,7 @@
       var expected = 'Revert "Revert "one line commit""\n\n' +
                      'This reverts commit abcdefg.\n\n' +
                      'Reason for revert: <INSERT REASONING HERE>\n\n' +
-                     'Original issue\'s description:\n' +
+                     'Original change\'s description:\n' +
                      '> Revert "one line commit"\n> \n' +
                      '> Change-Id: abcdefg\n> ';
       assert.equal(element.message, expected);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index c0773d3..287e9e4 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -121,7 +121,8 @@
             <gr-comment-list
                 comments="[[comments]]"
                 change-num="[[changeNum]]"
-                patch-num="[[message._revision_number]]"></gr-comment-list>
+                patch-num="[[message._revision_number]]"
+                project-config="[[projectConfig]]"></gr-comment-list>
           </div>
           <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
             <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 99c049b..f69eddd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -14,6 +14,16 @@
 (function() {
   'use strict';
 
+  /**
+   * Possible CSS classes indicating the state of selection. Dynamically added/
+   * removed based on where the user clicks within the diff.
+   */
+  var SelectionClass = {
+    COMMENT: 'selected-comment',
+    LEFT: 'selected-left',
+    RIGHT: 'selected-right',
+  };
+
   Polymer({
     is: 'gr-diff-selection',
 
@@ -32,7 +42,7 @@
     },
 
     attached: function() {
-      this.classList.add('selected-right');
+      this.classList.add(SelectionClass.RIGHT);
     },
 
     get diffBuilder() {
@@ -48,41 +58,91 @@
       if (!lineEl) {
         return;
       }
+      var commentSelected =
+          e.target.parentNode.classList.contains('gr-diff-comment');
       var side = this.diffBuilder.getSideByLineEl(lineEl);
-      var targetClass = 'selected-' + side;
-      var alternateClass = 'selected-' + (side === 'left' ? 'right' : 'left');
+      var targetClasses = [];
+      targetClasses.push(side === 'left' ?
+          SelectionClass.LEFT :
+          SelectionClass.RIGHT);
 
-      if (this.classList.contains(alternateClass)) {
-        this.classList.remove(alternateClass);
+      if (commentSelected) {
+        targetClasses.push(SelectionClass.COMMENT);
       }
-      if (!this.classList.contains(targetClass)) {
-        this.classList.add(targetClass);
+      // Remove any selection classes that do not belong.
+      for (var key in SelectionClass) {
+        if (SelectionClass.hasOwnProperty(key)) {
+          var className = SelectionClass[key];
+          if (targetClasses.indexOf(className) === -1) {
+            this.classList.remove(SelectionClass[key]);
+          }
+        }
+      }
+      // Add new selection classes iff they are not already present.
+      for (var i = 0; i < targetClasses.length; i++) {
+        if (!this.classList.contains(targetClasses[i])) {
+          this.classList.add(targetClasses[i]);
+        }
       }
     },
 
+    /**
+     * Utility function to determine whether an element is a descendant of
+     * another element with the particular className.
+     *
+     * @param {!Element} element
+     * @param {!string} className
+     * @return {boolean}
+     */
+    _elementDescendedFromClass: function(element, className) {
+      while (!element.classList.contains(className)) {
+        if (!element.parentElement ||
+            element === this.diffBuilder.diffElement) {
+          return false;
+        }
+        element = element.parentElement;
+      }
+      return true;
+    },
+
     _handleCopy: function(e) {
-      var el = e.target;
-      while (!el.classList.contains('content')) {
-        if (!el.parentElement) {
+      var commentSelected = false;
+      if (this._elementDescendedFromClass(e.target, SelectionClass.COMMENT)) {
+        commentSelected = true;
+      } else {
+        if (!this._elementDescendedFromClass(e.target, 'content')) {
           return;
         }
-        el = el.parentElement;
       }
       var lineEl = this.diffBuilder.getLineElByChild(e.target);
       if (!lineEl) {
         return;
       }
       var side = this.diffBuilder.getSideByLineEl(lineEl);
-      var text = this._getSelectedText(side);
-      e.clipboardData.setData('Text', text);
-      e.preventDefault();
+      var text = this._getSelectedText(side, commentSelected);
+      if (text) {
+        e.clipboardData.setData('Text', text);
+        e.preventDefault();
+      }
     },
 
-    _getSelectedText: function(side) {
+    /**
+     * Get the text of the current window selection. If commentSelected is
+     * true, it returns only the text of comments within the selection.
+     * Otherwise it returns the text of the selected diff region.
+     *
+     * @param {!string} The side that is selected.
+     * @param {boolean} Whether or not a comment is selected.
+     * @return {string} The selected text.
+     */
+    _getSelectedText: function(side, commentSelected) {
       var sel = window.getSelection();
       if (sel.rangeCount != 1) {
         return; // No multi-select support yet.
       }
+      if (commentSelected) {
+        return this._getCommentLines(sel, side);
+      }
       var range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
       var startLineEl = this.diffBuilder.getLineElByChild(range.startContainer);
       var endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
@@ -93,6 +153,16 @@
           range.endOffset, side);
     },
 
+    /**
+     * Query the diff object for the selected lines.
+     *
+     * @param {int} startLineNum
+     * @param {int} startOffset
+     * @param {int} endLineNum
+     * @param {int} endOffset
+     * @param {!string} side The side that is currently selected.
+     * @return {string} The selected diff text.
+     */
     _getRangeFromDiff: function(startLineNum, startOffset, endLineNum,
         endOffset, side) {
       var lines = this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
@@ -104,11 +174,16 @@
       return lines.join('\n');
     },
 
+    /**
+     * Query the diff object for the lines from a particular side.
+     *
+     * @param {!string} side The side that is currently selected.
+     * @return {string[]} An array of strings indexed by line number.
+     */
     _getDiffLines: function(side) {
       if (this._linesCache[side]) {
         return this._linesCache[side];
       }
-
       var lines = [];
       var chunk;
       var key = side === 'left' ? 'a' : 'b';
@@ -122,9 +197,46 @@
           lines = lines.concat(chunk[key]);
         }
       }
-
       this._linesCache[side] = lines;
       return lines;
     },
+
+    /**
+     * Query the diffElement for comments and check whether they lie inside the
+     * selection range.
+     *
+     * @param {!Selection} sel The selection of the window.
+     * @param {!string} side The side that is currently selected.
+     * @return {string} The selected comment text.
+     */
+    _getCommentLines: function(sel, side) {
+      var range = sel.getRangeAt(0);
+      var content = [];
+      // Fall back to default copy behavior if the selection lies within one
+      // comment body.
+      if (this._elementDescendedFromClass(range.commonAncestorContainer,
+          'message')) {
+        return;
+      }
+      // Query the diffElement for comments.
+      var messages = this.diffBuilder.diffElement.querySelectorAll(
+          '.side-by-side [data-side="' + side +
+          '"] .message *, .unified .message *');
+
+      for (var i = 0; i < messages.length; i++) {
+        var el = messages[i];
+        // Check if the comment element exists inside the selection.
+        if (sel.containsNode(el, true)) {
+          content.push(el.textContent);
+        }
+      }
+      // Deal with offsets.
+      content[0] = content[0].substring(range.startOffset);
+      if (range.endOffset) {
+        content[content.length - 1] =
+            content[content.length - 1].substring(0, range.endOffset);
+      }
+      return content.join('\n');
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index 1ac800d..f44d349 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -27,11 +27,18 @@
 <test-fixture id="basic">
   <template>
     <gr-diff-selection>
-      <table class="side-by-side">
+      <table id="diffTable" class="side-by-side">
         <tr>
           <td class="lineNum left" data-value="1">1</td>
           <td class="content">
             <div class="contentText" data-side="left">ba ba</div>
+            <div data-side="left">
+              <div class="gr-diff-comment-thread">
+                <div class="message">
+                  <span>This is a comment</span>
+                </div>
+              </div>
+            </div>
           </td>
           <td class="lineNum right" data-value="1">1</td>
           <td class="content">
@@ -46,12 +53,26 @@
           <td class="lineNum right" data-value="2">2</td>
           <td class="content">
             <div class="contentText" data-side="right">more more more</div>
+            <div data-side="right">
+              <div class="gr-diff-comment-thread">
+                <div class="message">
+                  <span>This is a comment on the right</span>
+                </div>
+              </div>
+            </div>
           </td>
         </tr>
         <tr>
           <td class="lineNum left" data-value="3">3</td>
           <td class="content">
             <div class="contentText" data-side="left">ga ga</div>
+            <div data-side="left">
+              <div class="gr-diff-comment-thread">
+                <div class="message">
+                  <span>This is a different comment</span>
+                </div>
+              </div>
+            </div>
           </td>
           <td class="lineNum right" data-value="3">3</td>
           <td class="other">
@@ -84,6 +105,7 @@
       element._cachedDiffBuilder = {
         getLineElByChild: sinon.stub().returns({}),
         getSideByLineEl: sinon.stub(),
+        diffElement: element.querySelector('#diffTable'),
       };
       element.diff = {
         content: [
@@ -130,11 +152,11 @@
       assert.isFalse(element._getSelectedText.called);
     });
 
-    test('asks for text for right side Elements', function() {
+    test('asks for text for left side Elements', function() {
       element._cachedDiffBuilder.getSideByLineEl.returns('left');
       sinon.stub(element, '_getSelectedText');
       emulateCopyOn(element.querySelector('div.contentText'));
-      assert.deepEqual(['left'], element._getSelectedText.lastCall.args);
+      assert.deepEqual(['left', false], element._getSelectedText.lastCall.args);
     });
 
     test('reacts to copy for content Elements', function() {
@@ -145,6 +167,8 @@
 
     test('copy event is prevented for content Elements', function() {
       sinon.stub(element, '_getSelectedText');
+      element._cachedDiffBuilder.getSideByLineEl.returns('left');
+      element._getSelectedText.returns('test');
       var event = emulateCopyOn(element.querySelector('div.contentText'));
       assert.isTrue(event.preventDefault.called);
     });
@@ -175,6 +199,22 @@
           element.querySelectorAll('div.contentText')[4].firstChild, 2);
       selection.addRange(range);
       assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
+      selection.removeAllRanges();
+    });
+
+    test('copies comments', function() {
+      element.classList.add('selected-left');
+      element.classList.add('selected-comment');
+      element.classList.remove('selected-right');
+      var selection = window.getSelection();
+      var range = document.createRange();
+      range.setStart(element.querySelector('.message *').firstChild, 3);
+      range.setEnd(
+          element.querySelectorAll('.message *')[2].firstChild, 16);
+      selection.addRange(range);
+      assert.equal('s is a comment\nThis is a differ',
+          element._getSelectedText('left', true));
+      selection.removeAllRanges();
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index efe8684..6c73e08 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -174,6 +174,10 @@
           this._patchRange.patchNum, this._path, reviewed);
     },
 
+    _checkForModifiers: function(e) {
+      return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || false;
+    },
+
     _handleKey: function(e) {
       if (this.shouldSupressKeyboardShortcut(e)) { return; }
 
@@ -201,6 +205,7 @@
           this.$.cursor.moveUp();
           break;
         case 67: // 'c'
+          if (this._checkForModifiers(e)) { return; }
           if (!this.$.diff.isRangeSelected()) {
             e.preventDefault();
             var line = this.$.cursor.getTargetLineElement();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 14fd2b7..1eb3f95 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -509,5 +509,13 @@
       assert.equal(element.$.cursor.initialLineNumber, 345);
       assert.equal(element.$.cursor.side, 'left');
     });
+
+    test('_checkForModifiers', function() {
+      assert.isTrue(element._checkForModifiers({altKey: true}));
+      assert.isTrue(element._checkForModifiers({ctrlKey: true}));
+      assert.isTrue(element._checkForModifiers({metaKey: true}));
+      assert.isTrue(element._checkForModifiers({shiftKey: true}));
+      assert.isFalse(element._checkForModifiers({}));
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index d565a12..fbd1ef3 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -74,9 +74,14 @@
       return rect;
     },
 
+    _checkForModifiers: function(e) {
+      return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || false;
+    },
+
     _handleKey: function(e) {
       if (this.shouldSupressKeyboardShortcut(e)) { return; }
       if (e.keyCode === 67) { // 'c'
+        if (this._checkForModifiers(e)) { return; }
         e.preventDefault();
         this._fireCreateComment();
       }
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
index adc8532..c12966d 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -117,5 +117,13 @@
         document.createRange.restore();
       });
     });
+
+    test('_checkForModifiers', function() {
+      assert.isTrue(element._checkForModifiers({altKey: true}));
+      assert.isTrue(element._checkForModifiers({ctrlKey: true}));
+      assert.isTrue(element._checkForModifiers({metaKey: true}));
+      assert.isTrue(element._checkForModifiers({shiftKey: true}));
+      assert.isFalse(element._checkForModifiers({}));
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index 98871cb..40b7cf1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -30,8 +30,11 @@
     },
 
     _computeAccountTitle: function(account) {
-      if (!account || !account.name) { return; }
-      var result = util.escapeHTML(account.name);
+      if (!account || (!account.name && !account.email)) { return; }
+      var result = '';
+      if (account.name) {
+        result += util.escapeHTML(account.name);
+      }
       if (account.email) {
         result += ' <' + util.escapeHTML(account.email) + '>';
       }
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
index 01ae92c..bfbbd21 100644
--- a/tools/bzl/BUILD
+++ b/tools/bzl/BUILD
@@ -1,4 +1,6 @@
 
 exports_files([
   "license-map.py",
-  "test_empty.sh"])
+  "test_empty.sh",
+  "test_license.sh",
+])
diff --git a/tools/bzl/license-map.py b/tools/bzl/license-map.py
index 72c7ae8..8469f4f 100644
--- a/tools/bzl/license-map.py
+++ b/tools/bzl/license-map.py
@@ -1,25 +1,146 @@
 #!/usr/bin/env python
 
-# reads a bazel query XML file, to join target names with their licenses.
+# reads bazel query XML files, to join target names with their licenses.
 
-import sys
+from __future__ import print_function
+
+import argparse
+from collections import defaultdict
+from shutil import copyfileobj
+from sys import stdout, stderr
 import xml.etree.ElementTree as ET
 
-tree = ET.parse(sys.argv[1])
-root = tree.getroot()
+KNOWN_PROVIDED_DEPS = [
+  "//lib/bouncycastle:bcpg",
+  "//lib/bouncycastle:bcpkix",
+  "//lib/bouncycastle:bcprov",
+]
 
-entries = {}
+DO_NOT_DISTRIBUTE = "//lib:LICENSE-DO_NOT_DISTRIBUTE"
 
-for child in root:
-  rule_name = child.attrib["name"]
-  for c in child.getchildren():
-    if c.tag != "rule-input":
+LICENSE_PREFIX = "//lib:LICENSE-"
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--asciidoctor", action="store_true")
+parser.add_argument("xmls", nargs="+")
+args = parser.parse_args()
+
+entries = defaultdict(list)
+graph = defaultdict(list)
+handled_rules = []
+
+for xml in args.xmls:
+  tree = ET.parse(xml)
+  root = tree.getroot()
+
+  for child in root:
+    rule_name = child.attrib["name"]
+    if rule_name in handled_rules:
+      # already handled in other xml files
       continue
 
-    license_name = c.attrib["name"]
-    if "//lib:LICENSE" in license_name:
-      assert rule_name not in entries, (license_name, entries[rule_name])
-      entries[rule_name] = license_name
+    handled_rules.append(rule_name)
+    for c in child.getchildren():
+      if c.tag != "rule-input":
+        continue
 
-for k, v in sorted(entries.items()):
-  print k, v
+      license_name = c.attrib["name"]
+      if LICENSE_PREFIX in license_name:
+        if rule_name in KNOWN_PROVIDED_DEPS:
+          continue
+
+        entries[rule_name].append(license_name)
+        graph[license_name].append(rule_name)
+
+if len(graph[DO_NOT_DISTRIBUTE]):
+  print("DO_NOT_DISTRIBUTE license found in:", file=stderr)
+  for target in graph[DO_NOT_DISTRIBUTE]:
+    print(target, file=stderr)
+  exit(1)
+
+if args.asciidoctor:
+  print(
+# We don't want any blank line before "= Gerrit Code Review - Licenses"
+"""= Gerrit Code Review - Licenses
+
+Gerrit open source software is licensed under the <<Apache2_0,Apache
+License 2.0>>.  Executable distributions also include other software
+components that are provided under additional licenses.
+
+[[cryptography]]
+== Cryptography Notice
+
+This distribution includes cryptographic software.  The country
+in which you currently reside may have restrictions on the import,
+possession, use, and/or re-export to another country, of encryption
+software.  BEFORE using any encryption software, please check
+your country's laws, regulations and policies concerning the
+import, possession, or use, and re-export of encryption software,
+to see if this is permitted.  See the
+link:http://www.wassenaar.org/[Wassenaar Arrangement]
+for more information.
+
+The U.S. Government Department of Commerce, Bureau of Industry
+and Security (BIS), has classified this software as Export
+Commodity Control Number (ECCN) 5D002.C.1, which includes
+information security software using or performing cryptographic
+functions with asymmetric algorithms.  The form and manner of
+this distribution makes it eligible for export under the License
+Exception ENC Technology Software Unrestricted (TSU) exception
+(see the BIS Export Administration Regulations, Section 740.13)
+for both object code and source code.
+
+Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
+uploads of changes directly from `git push` command line clients.
+
+Gerrit includes an SSH client (JSch), to support authenticated
+replication of changes to remote systems, such as for automatic
+updates of mirror servers, or realtime backups.
+
+For either feature to function, Gerrit requires the
+link:http://java.sun.com/javase/technologies/security/[Java Cryptography extensions]
+and/or the
+link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
+to be installed by the end-user.
+
+== Licenses
+""")
+
+  for n in sorted(graph.keys()):
+    if len(graph[n]) == 0:
+      continue
+
+    name = n[len(LICENSE_PREFIX):]
+    safename = name.replace(".", "_")
+    print()
+    print("[[%s]]" % safename)
+    print("=== " + name)
+    print()
+    for d in sorted(graph[n]):
+      if d.startswith("//lib:") or d.startswith("//lib/"):
+        p = d[len("//lib:"):]
+      else:
+        p = d[d.index(":")+1:].lower()
+      if "__" in p:
+        p = p[:p.index("__")]
+      print("* " + p)
+    print()
+    print("[[%s_license]]" % safename)
+    print("----")
+    with open(n[2:].replace(":", "/")) as fd:
+      copyfileobj(fd, stdout)
+    print()
+    print("----")
+    print()
+
+  print(
+"""
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+""")
+
+else:
+  for k, vs in sorted(entries.items()):
+    for v in vs:
+      print(k, v)
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
index ca64438..37cc70c 100644
--- a/tools/bzl/license.bzl
+++ b/tools/bzl/license.bzl
@@ -1,47 +1,57 @@
 
-def license_map(name, target):
-    """Generate XML for all targets that depend directly on a LICENSE file"""
+def normalize_target_name(target):
+  return target.replace("//", "").replace("/", "__").replace(":", "___")
+
+def license_map(name, targets = [], opts = []):
+  """Generate XML for all targets that depend directly on a LICENSE file"""
+  xmls = []
+  tools = [ "//tools/bzl:license-map.py", "//lib:all-licenses" ]
+  for target in targets:
+    subname = name + "_" + normalize_target_name(target) + ".xml"
+    xmls.append("$(location :%s)" % subname)
+    tools.append(subname)
     native.genquery(
-        name = name + ".xml",
-        scope = [ target, ],
+      name = subname,
+      scope = [ target ],
 
-        # Find everything that depends on a license file, but remove
-        # the license files themselves from this list.
-        expression = 'rdeps(%s, filter("//lib:LICENSE.*", deps(%s)),1) - filter("//lib:LICENSE.*", deps(%s))' % (target, target, target),
+      # Find everything that depends on a license file, but remove
+      # the license files themselves from this list.
+      expression = 'rdeps(%s, filter("//lib:LICENSE.*", deps(%s)),1) - filter("//lib:LICENSE.*", deps(%s))' % (target, target, target),
 
-        # We are interested in the edges of the graph ({java_library,
-        # license-file} tuples).  'query' provides this in the XML output.
-        opts = [ "--output=xml"],
+      # We are interested in the edges of the graph ({java_library,
+      # license-file} tuples).  'query' provides this in the XML output.
+      opts = [ "--output=xml", ],
     )
 
-    # post process the XML into our favorite format.
-    native.genrule(
-        name = "gen_license_txt_" + name,
-        cmd = "python $(location //tools/bzl:license-map.py) $(location :%s.xml) > $@" % name,
-        outs = [ name + ".txt",],
-        tools = [ "//tools/bzl:license-map.py", name + ".xml"])
+  # post process the XML into our favorite format.
+  native.genrule(
+    name = "gen_license_txt_" + name,
+    cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
+    outs = [ name + ".txt" ],
+    tools = tools
+  )
 
 def license_test(name, target):
-    """Generate XML for all targets that depend directly on a LICENSE file"""
-    txt = name + "-forbidden.txt"
+  """Make sure a target doesn't depend on DO_NOT_DISTRIBUTE license"""
+  txt = name + "-forbidden.txt"
 
-    # fully qualify target name.
-    if target[0] not in ":/":
-        target = ":" + target
-    if target[0] != "/":
-        target = "//" + PACKAGE_NAME + target
+  # fully qualify target name.
+  if target[0] not in ":/":
+    target = ":" + target
+  if target[0] != "/":
+    target = "//" + PACKAGE_NAME + target
 
-    forbidden = "//lib:LICENSE-DO_NOT_DISTRIBUTE"
-    native.genquery(
-        name = txt,
-        scope = [ target, forbidden ],
-        # Find everything that depends on a license file, but remove
-        # the license files themselves from this list.
-        expression = 'rdeps(%s, "%s", 1) - rdeps(%s, "%s", 0)' % (target, forbidden, target, forbidden),
-    )
-    native.sh_test(
-        name = name,
-        srcs = [ "//tools/bzl:test_empty.sh" ],
-        args  = [ "$(location :%s)" % txt],
-        data = [ txt ],
-    )
+  forbidden = "//lib:LICENSE-DO_NOT_DISTRIBUTE"
+  native.genquery(
+    name = txt,
+    scope = [ target, forbidden ],
+    # Find everything that depends on a license file, but remove
+    # the license files themselves from this list.
+    expression = 'rdeps(%s, "%s", 1) - rdeps(%s, "%s", 0)' % (target, forbidden, target, forbidden),
+  )
+  native.sh_test(
+    name = name,
+    srcs = [ "//tools/bzl:test_license.sh" ],
+    args  = [ "$(location :%s)" % txt ],
+    data = [ txt ],
+  )
diff --git a/tools/bzl/maven.bzl b/tools/bzl/maven.bzl
index ce2f483..c255c0c 100644
--- a/tools/bzl/maven.bzl
+++ b/tools/bzl/maven.bzl
@@ -18,10 +18,7 @@
   return ('$(location //tools:merge_jars) $@ '
           + ' '.join(['$(location %s)' % j for j in jars]))
 
-def merge_maven_jars(
-    name,
-    srcs,
-    visibility = []):
+def merge_maven_jars(name, srcs, **kwargs):
   native.genrule(
     name = '%s__merged_bin' % name,
     cmd = cmd(srcs),
@@ -31,5 +28,5 @@
   native.java_import(
     name = name,
     jars = [':%s__merged_bin' % name],
-    visibility = visibility,
+    **kwargs
   )
diff --git a/tools/bzl/test_license.sh b/tools/bzl/test_license.sh
new file mode 100755
index 0000000..6ac6dab
--- /dev/null
+++ b/tools/bzl/test_license.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+filtered="$1.filtered"
+
+cat $1 \
+  | grep -v "//lib/bouncycastle:bcpg" \
+  | grep -v "//lib/bouncycastle:bcpkix" \
+  | grep -v "//lib/bouncycastle:bcprov" \
+  > $filtered
+
+if test -s $filtered
+then
+  echo "$filtered not empty:"
+  cat $filtered
+  exit 1
+fi
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 3d34588..a07cf98 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -119,6 +119,7 @@
 
   # Classpath entries are absolute for cross-cell support
   java_library = re.compile('.*/buck-out/gen/(.*)/lib__[^/]+__output/[^/]+[.]jar$')
+  srcs = re.compile('.*/(__.*__)/.*')
   for p in _query_classpath(MAIN):
     if p.endswith('-src.jar'):
       # gwt_module() depends on -src.jar for Java to JavaScript compiles.
@@ -175,9 +176,19 @@
     for j in sorted(libs):
       s = None
       if j.endswith('.jar'):
-        s = j[:-4] + '_src.jar'
+        s = j[:-4] + '-src.jar'
         if not path.exists(s):
-          s = None
+          m = srcs.match(s)
+          if m:
+            l = m.group(1)
+            if l.endswith('__jar__'):
+              s = s.replace(l, l.replace('__jar__', '_src__'))
+            else:
+              s = s.replace(l, l[:-1] + 'src__')
+            if not path.exists(s):
+              s = None
+          else:
+            s = None
       if args.plugins:
         classpathentry('lib', j, s, exported=True)
       else:
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
new file mode 100755
index 0000000..506330c
--- /dev/null
+++ b/tools/workspace-status.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+git_rev=$(git describe --always --match "v[0-9].*" --dirty)
+
+echo "STABLE_BUILD_GERRIT_LABEL ${git_rev}"