Merge "Implement inline edit REST API in PolyGerrit"
diff --git a/.bazelproject b/.bazelproject
index 41bb27f..e3a7a9c 100644
--- a/.bazelproject
+++ b/.bazelproject
@@ -18,3 +18,6 @@
 java_language_level: 8
 
 workspace_type: java
+
+build_flags:
+  --javacopt=-g
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index bb637f8..139284c 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2613,6 +2613,19 @@
 +
 Defaults to 1024.
 
+[[index.reindexAfterRefUpdate]]index.reindexAfterRefUpdate::
++
+Whether to reindex all affected open changes after a ref is updated. This
+includes reindexing all open changes to recompute the "mergeable" bit every time
+the destination branch moves, as well as reindexing changes to take into account
+new project configuration (e.g. label definitions).
++
+Leaving this enabled may result in fresher results, but may cause performance
+problems if there are lots of open changes on a project whose branches advance
+frequently.
++
+Defaults to true.
+
 ==== Lucene configuration
 
 Open and closed changes are indexed in separate indexes named
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index b6d2c53..04adcbd 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -736,6 +736,68 @@
   }
 ----
 
+[[query_attributes]]
+=== Query Attributes ===
+
+Plugins can provide additional attributes to be returned in Gerrit queries by
+implementing the ChangeAttributeFactory interface and registering it to the
+ChangeQueryProcessor.ChangeAttributeFactory class in the plugin module's
+'configure()' method. The new attribute(s) will be output under a "plugin"
+attribute in the change query output.
+
+The example below shows a plugin that adds two attributes ('exampleName' and
+'changeValue'), to the change query output.
+
+[source, java]
+----
+public class Module extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(ChangeAttributeFactory.class)
+        .annotatedWith(Exports.named("example"))
+        .to(AttributeFactory.class);
+  }
+}
+
+public class AttributeFactory implements ChangeAttributeFactory {
+
+  public class PluginAttribute extends PluginDefinedInfo {
+    public String exampleName;
+    public String changeValue;
+
+    public PluginAttribute(ChangeData c) {
+      this.exampleName = "Attribute Example";
+      this.changeValue = Integer.toString(c.getId().get());
+    }
+  }
+
+  @Override
+  public PluginDefinedInfo create(ChangeData c, ChangeQueryProcessor qp, String plugin) {
+    return new PluginAttribute(c);
+  }
+}
+----
+
+Example
+----
+
+ssh -p 29418 localhost gerrit query "change:1" --format json
+
+Output:
+
+{
+   "url" : "http://localhost:8080/1",
+   "plugins" : [
+      {
+         "name" : "myplugin-name",
+         "exampleName" : "Attribute Example",
+         "changeValue" : "1"
+      }
+   ],
+    ...
+}
+----
+
 [[simple-configuration]]
 == Simple Configuration in `gerrit.config`
 
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index b7b6f6a..1e741a8 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -228,6 +228,7 @@
     cfg.setInt("sshd", null, "commandStartThreads", 1);
     cfg.setInt("receive", null, "threadPoolSize", 1);
     cfg.setInt("index", null, "threads", 1);
+    cfg.setBoolean("index", null, "reindexAfterRefUpdate", false);
   }
 
   private static Injector createTestInjector(Daemon daemon) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index ad8140e..89585c3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -437,6 +437,17 @@
   }
 
   @Test
+  public void cannotAddNonConfirmedEmailWithoutModifyAccountPermission() throws Exception {
+    TestAccount account = accounts.create(name("user"));
+    EmailInput input = new EmailInput();
+    input.email = "test@test.com";
+    input.noConfirmation = true;
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    gApi.accounts().id(account.username).addEmail(input);
+  }
+
+  @Test
   public void deleteEmail() throws Exception {
     String email = "foo.bar@example.com";
     EmailInput input = new EmailInput();
@@ -542,12 +553,7 @@
   @Test
   @Sandboxed
   public void fetchUserBranch() throws Exception {
-    // change something in the user preferences to ensure that the user branch
-    // is created
-    setApiUser(user);
-    GeneralPreferencesInfo input = new GeneralPreferencesInfo();
-    input.changesPerPage = GeneralPreferencesInfo.defaults().changesPerPage + 10;
-    gApi.accounts().self().setPreferences(input);
+    ensureUserBranchCreated(user);
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
     String userRefName = RefNames.refsUsers(user.id);
@@ -597,11 +603,7 @@
 
   @Test
   public void pushToUserBranch() throws Exception {
-    // change something in the user preferences to ensure that the user branch
-    // is created
-    GeneralPreferencesInfo input = new GeneralPreferencesInfo();
-    input.changesPerPage = GeneralPreferencesInfo.defaults().changesPerPage + 10;
-    gApi.accounts().self().setPreferences(input);
+    ensureUserBranchCreated(admin);
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
@@ -615,11 +617,7 @@
 
   @Test
   public void pushToUserBranchForReview() throws Exception {
-    // change something in the user preferences to ensure that the user branch
-    // is created
-    GeneralPreferencesInfo input = new GeneralPreferencesInfo();
-    input.changesPerPage = GeneralPreferencesInfo.defaults().changesPerPage + 10;
-    gApi.accounts().self().setPreferences(input);
+    ensureUserBranchCreated(admin);
 
     String userRefName = RefNames.refsUsers(admin.id);
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
@@ -642,11 +640,7 @@
 
   @Test
   public void pushWatchConfigToUserBranch() throws Exception {
-    // change something in the user preferences to ensure that the user branch
-    // is created
-    GeneralPreferencesInfo input = new GeneralPreferencesInfo();
-    input.changesPerPage = GeneralPreferencesInfo.defaults().changesPerPage + 10;
-    gApi.accounts().self().setPreferences(input);
+    ensureUserBranchCreated(admin);
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
@@ -689,6 +683,8 @@
   @Test
   @Sandboxed
   public void cannotDeleteUserBranch() throws Exception {
+    ensureUserBranchCreated(admin);
+
     grant(
         Permission.DELETE,
         allUsers,
@@ -711,6 +707,8 @@
   @Test
   @Sandboxed
   public void deleteUserBranchWithAccessDatabaseCapability() throws Exception {
+    ensureUserBranchCreated(admin);
+
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     grant(
         Permission.DELETE,
@@ -1021,4 +1019,12 @@
     assertThat(accounts).hasSize(1);
     assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId());
   }
+
+  private void ensureUserBranchCreated(TestAccount account) throws Exception {
+    // Change something in the user preferences to ensure that the user branch is created.
+    setApiUser(account);
+    GeneralPreferencesInfo input = new GeneralPreferencesInfo();
+    input.changesPerPage = GeneralPreferencesInfo.defaults().changesPerPage + 10;
+    gApi.accounts().self().setPreferences(input);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 17c3488..727c8e2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -223,6 +223,50 @@
   }
 
   @Test
+  public void reviewerAndCCReceiveSameEmail() throws Exception {
+    assume().that(notesMigration.readChanges()).isTrue();
+
+    PushOneCommit.Result r = createChange();
+    for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
+      for (int i = 0; i < 10; i++) {
+        AddReviewerInput input = new AddReviewerInput();
+        input.reviewer = String.format("%s-%s@gerritcodereview.com", state, i);
+        input.state = state;
+        gApi.changes().id(r.getChangeId()).addReviewer(input);
+      }
+    }
+
+    // Also add user as a regular reviewer
+    AddReviewerInput input = new AddReviewerInput();
+    input.reviewer = user.email;
+    input.state = ReviewerState.REVIEWER;
+    gApi.changes().id(r.getChangeId()).addReviewer(input);
+
+    sender.clear();
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    // Assert that only one email was sent out to everyone
+    assertThat(sender.getMessages()).hasSize(1);
+  }
+
+  @Test
+  public void addingMultipleReviewersAndCCsAtOnceSendsOnlyOneEmail() throws Exception {
+    assume().that(notesMigration.readChanges()).isTrue();
+
+    PushOneCommit.Result r = createChange();
+    ReviewInput reviewInput = new ReviewInput();
+    for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
+      for (int i = 0; i < 10; i++) {
+        reviewInput.reviewer(String.format("%s-%s@gerritcodereview.com", state, i), state, true);
+      }
+    }
+    assertThat(reviewInput.reviewers).hasSize(20);
+
+    sender.clear();
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(reviewInput);
+    assertThat(sender.getMessages()).hasSize(1);
+  }
+
+  @Test
   public void rejectMissingEmail() throws Exception {
     assume().that(notesMigration.readChanges()).isTrue();
     PushOneCommit.Result r = createChange();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 63ea2df..f7fe4f1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -67,7 +67,6 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.ReceiveCommand;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -919,7 +918,7 @@
           new BatchUpdateOp() {
             @Override
             public void updateRepo(RepoContext ctx) throws IOException {
-              ctx.addRefUpdate(new ReceiveCommand(oldId, newId, dest));
+              ctx.addRefUpdate(oldId, newId, dest);
             }
 
             @Override
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index b4f68fa..fcbad4f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -517,7 +517,7 @@
   }
 
   @Test
-  @GerritConfig(name = "index.testReindexAfterUpdate", value = "false")
+  @GerritConfig(name = "index.testAutoReindexIfStale", value = "false")
   public void getRelatedForStaleChange() throws Exception {
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index 9d15daf..15b74bd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -120,7 +120,7 @@
     // unintentional auto-rebuilding of the change in NoteDb during the read
     // path of the reindex-if-stale check. For the purposes of this test, we
     // want precise control over when auto-rebuilding happens.
-    cfg.setBoolean("index", null, "testReindexAfterUpdate", false);
+    cfg.setBoolean("index", null, "testAutoReindexIfStale", false);
 
     return cfg;
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index 9850f2e..ada17b6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -40,7 +40,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.ReceiveCommand;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -71,7 +70,7 @@
           public void updateRepo(RepoContext ctx) throws IOException {
             ObjectId oldId = ctx.getRepoView().getRef(backup).orElse(ObjectId.zeroId());
             newId = ctx.getRepoView().getRef(master).get();
-            ctx.addRefUpdate(new ReceiveCommand(oldId, newId, backup));
+            ctx.addRefUpdate(oldId, newId, backup);
           }
 
           @Override
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index e13962d..2cb8384 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -63,4 +63,5 @@
   public Boolean _moreChanges;
 
   public List<ProblemInfo> problems;
+  public List<PluginDefinedInfo> plugins;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java
new file mode 100644
index 0000000..e6fef0f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2017 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.extensions.common;
+
+public class PluginDefinedInfo {
+  public String name;
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java
index 3eb77ea..b55cf6c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java
@@ -37,7 +37,7 @@
   private final byte[] indexSource;
 
   IndexServlet(String canonicalURL, @Nullable String cdnPath) throws URISyntaxException {
-    String resourcePath = "com/google/gerrit/httpd/raw/index.html.soy";
+    String resourcePath = "com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy";
     SoyFileSet.Builder builder = SoyFileSet.builder();
     builder.add(Resources.getResource(resourcePath));
     SoyTofu.Renderer renderer =
@@ -47,7 +47,7 @@
             .newRenderer("com.google.gerrit.httpd.raw.Index")
             .setContentKind(SanitizedContent.ContentKind.HTML)
             .setData(getTemplateData(canonicalURL, cdnPath));
-    indexSource = renderer.render().getBytes();
+    indexSource = renderer.render().getBytes(UTF_8);
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
similarity index 92%
rename from gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy
rename to gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 4d8c43b..32f91f1 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -27,7 +27,9 @@
   <meta name="description" content="Gerrit Code Review">{\n}
   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">{\n}
 
-  <script>window.CANONICAL_PATH = '{$canonicalPath}';</script>{\n}
+  {if $canonicalPath != ''}
+    <script>window.CANONICAL_PATH = '{$canonicalPath}';</script>{\n}
+  {/if}
 
   // SourceCodePro fonts are used in styles/fonts.css
   // @see https://github.com/w3c/preload/issues/32 regarding crossorigin
@@ -40,5 +42,5 @@
   <link rel="import" href="{$staticResourcePath}/elements/gr-app.html">{\n}
 
   <body unresolved>{\n}
-  <gr-app id="app" canonical-path="{$canonicalPath}"></gr-app>{\n}
+  <gr-app id="app"></gr-app>{\n}
 {/template}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
index 21daa3e..17ce24a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
@@ -48,7 +48,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.DummyIndexModule;
-import com.google.gerrit.server.index.change.ReindexAfterUpdate;
+import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
 import com.google.gerrit.server.notedb.ChangeBundleReader;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager;
 import com.google.gerrit.server.notedb.NotesMigration;
@@ -212,7 +212,7 @@
           public void configure() {
             install(dbInjector.getInstance(BatchProgramModule.class));
             DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
-                .to(ReindexAfterUpdate.class);
+                .to(ReindexAfterRefUpdate.class);
             install(new DummyIndexModule());
             factory(ChangeResource.Factory.class);
           }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index c86d5af..e625219 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -68,6 +68,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -112,6 +113,8 @@
     bind(new TypeLiteral<List<CommentLinkInfo>>() {})
         .toProvider(CommentLinkProvider.class)
         .in(SINGLETON);
+    bind(new TypeLiteral<DynamicMap<ChangeQueryProcessor.ChangeAttributeFactory>>() {})
+        .toInstance(DynamicMap.<ChangeQueryProcessor.ChangeAttributeFactory>emptyMap());
     bind(String.class)
         .annotatedWith(CanonicalWebUrl.class)
         .toProvider(CanonicalWebUrlProvider.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
index a1d21c4..74e1fda 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -225,6 +225,13 @@
       throw invalidConfig(noteId, String.format("Invalid external id: %s", externalIdKeyStr));
     }
 
+    if (!externalIdKey.sha1().getName().equals(noteId)) {
+      throw invalidConfig(
+          noteId,
+          String.format(
+              "SHA1 of external ID %s does not match note ID %s", externalIdKeyStr, noteId));
+    }
+
     String email = externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, EMAIL_KEY);
     String password =
         externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, PASSWORD_KEY);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 08473a0..8ac89ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -341,7 +341,7 @@
       return;
     }
     if (updateRefCommand == null) {
-      ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), commitId, psId.toRefName()));
+      ctx.addRefUpdate(ObjectId.zeroId(), commitId, psId.toRefName());
     } else {
       ctx.addRefUpdate(updateRefCommand);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index c86714a..4724ea1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -118,6 +118,7 @@
 import com.google.gerrit.server.query.QueryResult;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
+import com.google.gerrit.server.query.change.PluginDefinedAttributesFactory;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -213,6 +214,7 @@
   private boolean lazyLoad = true;
   private AccountLoader accountLoader;
   private FixInput fix;
+  private PluginDefinedAttributesFactory pluginDefinedAttributesFactory;
 
   @Inject
   ChangeJson(
@@ -276,6 +278,10 @@
     return this;
   }
 
+  public void setPluginDefinedAttributesFactory(PluginDefinedAttributesFactory pluginsFactory) {
+    this.pluginDefinedAttributesFactory = pluginsFactory;
+  }
+
   public ChangeInfo format(ChangeResource rsrc) throws OrmException {
     return format(changeDataFactory.create(db.get(), rsrc.getControl()));
   }
@@ -520,6 +526,8 @@
 
     out.labels = labelsFor(perm, ctl, cd, has(LABELS), has(DETAILED_LABELS));
     out.submitted = getSubmittedOn(cd);
+    out.plugins =
+        pluginDefinedAttributesFactory != null ? pluginDefinedAttributesFactory.create(cd) : null;
 
     if (out.labels != null && has(DETAILED_LABELS)) {
       // If limited to specific patch sets but not the current patch set, don't
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index f3df8e8..49d7bae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -76,7 +76,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -504,8 +503,7 @@
               new BatchUpdateOp() {
                 @Override
                 public void updateRepo(RepoContext ctx) throws IOException {
-                  ctx.addRefUpdate(
-                      new ReceiveCommand(commit, ObjectId.zeroId(), psIdToDelete.toRefName()));
+                  ctx.addRefUpdate(commit, ObjectId.zeroId(), psIdToDelete.toRefName());
                 }
               });
           if (!reuseOldPsId) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
index c7cfef9..992313d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -43,7 +43,6 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 class DeleteChangeOp implements BatchUpdateOp {
   static boolean allowDrafts(Config cfg) {
@@ -173,7 +172,7 @@
     String prefix = new PatchSet.Id(id, 1).toRefName();
     prefix = prefix.substring(0, prefix.length() - 1);
     for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(prefix).entrySet()) {
-      ctx.addRefUpdate(new ReceiveCommand(e.getValue(), ObjectId.zeroId(), prefix + e.getKey()));
+      ctx.addRefUpdate(e.getValue(), ObjectId.zeroId(), prefix + e.getKey());
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index 222230b..a4db8c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -47,7 +47,6 @@
 import java.util.Collection;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 @Singleton
 public class DeleteDraftPatchSet
@@ -133,10 +132,9 @@
         return;
       }
       ctx.addRefUpdate(
-          new ReceiveCommand(
-              ObjectId.fromString(patchSet.getRevision().get()),
-              ObjectId.zeroId(),
-              patchSet.getRefName()));
+          ObjectId.fromString(patchSet.getRevision().get()),
+          ObjectId.zeroId(),
+          patchSet.getRefName());
     }
 
     private void deleteDraftPatchSet(PatchSet patchSet, ChangeContext ctx) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 1512976..32dbcda 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -207,9 +207,7 @@
   public void updateRepo(RepoContext ctx)
       throws AuthException, ResourceConflictException, IOException, OrmException {
     validate(ctx);
-    ctx.addRefUpdate(
-        new ReceiveCommand(
-            ObjectId.zeroId(), commitId, getPatchSetId().toRefName(), ReceiveCommand.Type.CREATE));
+    ctx.addRefUpdate(ObjectId.zeroId(), commitId, getPatchSetId().toRefName());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
index 7dec0fe..42dba3f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -46,6 +46,7 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.BundleWriter;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.kohsuke.args4j.Option;
@@ -145,9 +146,9 @@
         MergeOpRepoManager orm = mergeOp.getMergeOpRepoManager();
         for (Project.NameKey p : mergeOp.getAllProjects()) {
           OpenRepo or = orm.getRepo(p);
-          BundleWriter bw = new BundleWriter(or.getRepo());
+          BundleWriter bw = new BundleWriter(or.getCodeReviewRevWalk().getObjectReader());
           bw.setObjectCountCallback(null);
-          bw.setPackConfig(null);
+          bw.setPackConfig(new PackConfig(or.getRepo()));
           Collection<ReceiveCommand> refs = or.getUpdate().getRefUpdates().values();
           for (ReceiveCommand r : refs) {
             bw.include(r.getRefName(), r.getNewId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 5f70786..b7f872b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -132,7 +132,7 @@
 import com.google.gerrit.server.git.validators.UploadValidationListener;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.group.GroupModule;
-import com.google.gerrit.server.index.change.ReindexAfterUpdate;
+import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.ListMailFilter;
 import com.google.gerrit.server.mail.MailFilter;
@@ -166,6 +166,7 @@
 import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.query.change.ConflictsCacheImpl;
 import com.google.gerrit.server.ssh.SshAddressesModule;
 import com.google.gerrit.server.tools.ToolsCatalog;
@@ -330,7 +331,7 @@
     DynamicSet.setOf(binder(), GarbageCollectorListener.class);
     DynamicSet.setOf(binder(), HeadUpdatedListener.class);
     DynamicSet.setOf(binder(), UsageDataPublishedListener.class);
-    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReindexAfterUpdate.class);
+    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReindexAfterRefUpdate.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
         .to(ProjectConfigEntry.UpdateChecker.class);
     DynamicSet.setOf(binder(), EventListener.class);
@@ -378,6 +379,8 @@
 
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class);
+    DynamicMap.mapOf(binder(), ChangeQueryProcessor.ChangeAttributeFactory.class);
+
     install(new GitwebConfig.LegacyModule(cfg));
 
     bind(AnonymousUser.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
index 1a8a788..0467c92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.data;
 
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import java.util.List;
 
@@ -43,4 +44,5 @@
   public List<DependencyAttribute> neededBy;
   public List<SubmitRecordAttribute> submitRecords;
   public List<AccountAttribute> allReviewers;
+  public List<PluginDefinedInfo> plugins;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 92333c6..1e11968 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -59,7 +59,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 /**
  * Utility functions to manipulate change edits.
@@ -223,9 +222,7 @@
             new BatchUpdateOp() {
               @Override
               public void updateRepo(RepoContext ctx) throws Exception {
-                ctx.addRefUpdate(
-                    new ReceiveCommand(
-                        edit.getEditCommit().copy(), ObjectId.zeroId(), edit.getRefName()));
+                ctx.addRefUpdate(edit.getEditCommit().copy(), ObjectId.zeroId(), edit.getRefName());
               }
             });
         bu.execute();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index 7bb8271..f504c45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -71,7 +71,6 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushCertificate;
-import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -211,7 +210,7 @@
     }
 
     if (updateRef) {
-      ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), commitId, patchSetId.toRefName()));
+      ctx.addRefUpdate(ObjectId.zeroId(), commitId, patchSetId.toRefName());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 7c236e1..93aea89 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -63,7 +63,6 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.RefSpec;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -82,7 +81,7 @@
     public void updateRepo(RepoContext ctx) throws Exception {
       CodeReviewCommit c = composeGitlinksCommit(branch);
       if (c != null) {
-        ctx.addRefUpdate(new ReceiveCommand(c.getParent(0), c, branch.get()));
+        ctx.addRefUpdate(c.getParent(0), c, branch.get());
         addBranchTip(branch, c);
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 879ca0e..49399ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -37,7 +37,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 public class CherryPick extends SubmitStrategy {
 
@@ -135,7 +134,7 @@
       args.mergeTip.moveTipTo(newCommit, newCommit);
       args.commitStatus.put(newCommit);
 
-      ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), newCommit, psId.toRefName()));
+      ctx.addRefUpdate(ObjectId.zeroId(), newCommit, psId.toRefName());
       patchSetInfo = args.patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, psId);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
index 18b4173..a156a90 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
@@ -43,7 +43,6 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 /** This strategy covers RebaseAlways and RebaseIfNecessary ones. */
 public class RebaseSubmitStrategy extends SubmitStrategy {
@@ -156,8 +155,7 @@
           toMerge.setStatusCode(SKIPPED_IDENTICAL_TREE);
           return;
         }
-        ctx.addRefUpdate(
-            new ReceiveCommand(ObjectId.zeroId(), newCommit, newPatchSetId.toRefName()));
+        ctx.addRefUpdate(ObjectId.zeroId(), newCommit, newPatchSetId.toRefName());
       } else {
         // Stale read of patch set is ok; see comments in RebaseChangeOp.
         PatchSet origPs =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index a788f8c..4edfab2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -107,7 +107,7 @@
   private final ListeningExecutorService executor;
   private final DynamicSet<ChangeIndexedListener> indexedListeners;
   private final StalenessChecker stalenessChecker;
-  private final boolean reindexAfterIndexUpdate;
+  private final boolean autoReindexIfStale;
 
   @AssistedInject
   ChangeIndexer(
@@ -131,7 +131,7 @@
     this.indexedListeners = indexedListeners;
     this.stalenessChecker = stalenessChecker;
     this.batchExecutor = batchExecutor;
-    this.reindexAfterIndexUpdate = reindexAfterIndexUpdate(cfg);
+    this.autoReindexIfStale = autoReindexIfStale(cfg);
     this.index = index;
     this.indexes = null;
   }
@@ -158,13 +158,13 @@
     this.indexedListeners = indexedListeners;
     this.stalenessChecker = stalenessChecker;
     this.batchExecutor = batchExecutor;
-    this.reindexAfterIndexUpdate = reindexAfterIndexUpdate(cfg);
+    this.autoReindexIfStale = autoReindexIfStale(cfg);
     this.index = null;
     this.indexes = indexes;
   }
 
-  private static boolean reindexAfterIndexUpdate(Config cfg) {
-    return cfg.getBoolean("index", null, "testReindexAfterUpdate", true);
+  private static boolean autoReindexIfStale(Config cfg) {
+    return cfg.getBoolean("index", null, "testAutoReindexIfStale", true);
   }
 
   /**
@@ -221,7 +221,7 @@
     // and fix the staleness. It doesn't matter which order the two
     // reindexIfStale calls actually execute in; we are guaranteed that at least
     // one of them will execute after the second index write, (4).
-    reindexAfterIndexUpdate(cd);
+    autoReindexIfStale(cd);
   }
 
   private void fireChangeIndexedEvent(int id) {
@@ -253,7 +253,7 @@
   public void index(ReviewDb db, Change change) throws IOException, OrmException {
     index(newChangeData(db, change));
     // See comment in #index(ChangeData).
-    reindexAfterIndexUpdate(change.getProject(), change.getId());
+    autoReindexIfStale(change.getProject(), change.getId());
   }
 
   /**
@@ -268,7 +268,7 @@
     ChangeData cd = newChangeData(db, project, changeId);
     index(cd);
     // See comment in #index(ChangeData).
-    reindexAfterIndexUpdate(cd);
+    autoReindexIfStale(cd);
   }
 
   /**
@@ -304,16 +304,16 @@
     return submit(new ReindexIfStaleTask(project, id), batchExecutor);
   }
 
-  private void reindexAfterIndexUpdate(ChangeData cd) throws IOException {
+  private void autoReindexIfStale(ChangeData cd) throws IOException {
     try {
-      reindexAfterIndexUpdate(cd.project(), cd.getId());
+      autoReindexIfStale(cd.project(), cd.getId());
     } catch (OrmException e) {
       throw new IOException(e);
     }
   }
 
-  private void reindexAfterIndexUpdate(Project.NameKey project, Change.Id id) {
-    if (reindexAfterIndexUpdate) {
+  private void autoReindexIfStale(Project.NameKey project, Change.Id id) {
+    if (autoReindexIfStale) {
       // Don't retry indefinitely; if this fails the change will be stale.
       @SuppressWarnings("unused")
       Future<?> possiblyIgnoredError = reindexIfStale(project, id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
similarity index 92%
rename from gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index 2f6f898..e863186 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.QueueProvider.QueueType;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -41,11 +42,12 @@
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Future;
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class ReindexAfterUpdate implements GitReferenceUpdatedListener {
-  private static final Logger log = LoggerFactory.getLogger(ReindexAfterUpdate.class);
+public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener {
+  private static final Logger log = LoggerFactory.getLogger(ReindexAfterRefUpdate.class);
 
   private final OneOffRequestContext requestContext;
   private final Provider<InternalChangeQuery> queryProvider;
@@ -53,9 +55,11 @@
   private final ChangeIndexCollection indexes;
   private final ChangeNotes.Factory notesFactory;
   private final ListeningExecutorService executor;
+  private final boolean enabled;
 
   @Inject
-  ReindexAfterUpdate(
+  ReindexAfterRefUpdate(
+      @GerritServerConfig Config cfg,
       OneOffRequestContext requestContext,
       Provider<InternalChangeQuery> queryProvider,
       ChangeIndexer.Factory indexerFactory,
@@ -68,11 +72,13 @@
     this.indexes = indexes;
     this.notesFactory = notesFactory;
     this.executor = executor;
+    this.enabled = cfg.getBoolean("index", null, "reindexAfterRefUpdate", true);
   }
 
   @Override
   public void onGitReferenceUpdated(final Event event) {
-    if (event.getRefName().startsWith(RefNames.REFS_CHANGES)
+    if (!enabled
+        || event.getRefName().startsWith(RefNames.REFS_CHANGES)
         || event.getRefName().startsWith(RefNames.REFS_DRAFT_COMMENTS)
         || event.getRefName().startsWith(RefNames.REFS_USERS)) {
       return;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index d23e856..4ecd684 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -170,7 +170,6 @@
     void flush() throws IOException {
       flushToFinalInserter();
       finalIns.flush();
-      tempIns.clear();
     }
 
     void flushToFinalInserter() throws IOException {
@@ -178,6 +177,7 @@
       for (InsertedObject obj : tempIns.getInsertedObjects()) {
         finalIns.insert(obj.type(), obj.data().toByteArray());
       }
+      tempIns.clear();
     }
 
     @Override
@@ -505,11 +505,7 @@
     } else {
       // OpenRepo buffers objects separately; caller may assume that objects are available in the
       // inserter it previously passed via setChangeRepo.
-      // TODO(dborowitz): This should be flushToFinalInserter to avoid flushing objects to the
-      // underlying repo during a dry run. However, the only user of this is PreviewSubmit, which
-      // uses BundleWriter, which only takes a Repository so it can't read unflushed objects. Fix
-      // BundleWriter, then fix this call.
-      or.flush();
+      or.flushToFinalInserter();
     }
 
     BatchRefUpdate bru = or.repo.getRefDatabase().newBatchUpdate();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 64ef091..863cffb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -758,8 +758,8 @@
     return change;
   }
 
-  public LabelTypes getLabelTypes() {
-    return changeControl.getLabelTypes();
+  public LabelTypes getLabelTypes() throws OrmException {
+    return changeControl().getLabelTypes();
   }
 
   public ChangeNotes notes() throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index 91a37d5..efe44fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -17,6 +17,8 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_LIMIT;
 
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.index.IndexConfig;
@@ -32,12 +34,26 @@
 import com.google.gerrit.server.query.QueryProcessor;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
-public class ChangeQueryProcessor extends QueryProcessor<ChangeData> {
+public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
+    implements PluginDefinedAttributesFactory {
+  /**
+   * Register a ChangeAttributeFactory in a config Module like this:
+   *
+   * <p>bind(ChangeAttributeFactory.class) .annotatedWith(Exports.named("export-name"))
+   * .to(YourClass.class);
+   */
+  public interface ChangeAttributeFactory {
+    PluginDefinedInfo create(ChangeData a, ChangeQueryProcessor qp, String plugin);
+  }
+
   private final Provider<ReviewDb> db;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangeNotes.Factory notesFactory;
+  private final DynamicMap<ChangeAttributeFactory> attributeFactories;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -55,7 +71,8 @@
       ChangeIndexRewriter rewriter,
       Provider<ReviewDb> db,
       ChangeControl.GenericFactory changeControlFactory,
-      ChangeNotes.Factory notesFactory) {
+      ChangeNotes.Factory notesFactory,
+      DynamicMap<ChangeAttributeFactory> attributeFactories) {
     super(
         userProvider,
         metrics,
@@ -67,6 +84,7 @@
     this.db = db;
     this.changeControlFactory = changeControlFactory;
     this.notesFactory = notesFactory;
+    this.attributeFactories = attributeFactories;
   }
 
   @Override
@@ -82,6 +100,30 @@
   }
 
   @Override
+  public List<PluginDefinedInfo> create(ChangeData cd) {
+    List<PluginDefinedInfo> plugins = new ArrayList<>(attributeFactories.plugins().size());
+    for (String plugin : attributeFactories.plugins()) {
+      for (Provider<ChangeAttributeFactory> provider :
+          attributeFactories.byPlugin(plugin).values()) {
+        PluginDefinedInfo pda = null;
+        try {
+          pda = provider.get().create(cd, this, plugin);
+        } catch (RuntimeException e) {
+          /* Eat runtime exceptions so that queries don't fail. */
+        }
+        if (pda != null) {
+          pda.name = plugin;
+          plugins.add(pda);
+        }
+      }
+    }
+    if (plugins.isEmpty()) {
+      plugins = null;
+    }
+    return plugins;
+  }
+
+  @Override
   protected Predicate<ChangeData> enforceVisibility(Predicate<ChangeData> pred) {
     return new AndChangeSource(
         pred,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index cd98087..0d12132 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -313,6 +313,7 @@
       eventFactory.addDependencies(rw, c, d.change(), d.currentPatchSet());
     }
 
+    c.plugins = queryProcessor.create(d);
     return c;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
new file mode 100644
index 0000000..a795025
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import java.util.List;
+
+public interface PluginDefinedAttributesFactory {
+  List<PluginDefinedInfo> create(ChangeData cd);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index 7eccf45..f0ef40d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -137,13 +137,18 @@
 
     int cnt = queries.size();
     List<QueryResult<ChangeData>> results = imp.query(qb.parse(queries));
+
     boolean requireLazyLoad =
         containsAnyOf(options, ImmutableSet.of(DETAILED_LABELS, LABELS))
             && !qb.getArgs().getSchema().hasField(ChangeField.STORED_SUBMIT_RECORD_LENIENT);
+
+    ChangeJson cjson = json.create(options);
+    cjson.setPluginDefinedAttributesFactory(this.imp);
     List<List<ChangeInfo>> res =
-        json.create(options)
+        cjson
             .lazyLoad(requireLazyLoad || containsAnyOf(options, ChangeJson.REQUIRE_LAZY_LOAD))
             .formatQueryResults(results);
+
     for (int n = 0; n < cnt; n++) {
       List<ChangeInfo> info = res.get(n);
       if (results.get(n).more()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java
index e457d9c..03a7c37 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -17,12 +17,14 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Multiset;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -129,6 +131,7 @@
         boolean dryRun)
         throws UpdateException, RestApiException {
       checkNotNull(listener);
+      checkDifferentProject(updates);
       // It's safe to downcast all members of the input collection in this case, because the only
       // way a caller could have gotten any BatchUpdates in the first place is to call the create
       // method above, which always returns instances of the type we expect. Just to be safe,
@@ -144,6 +147,15 @@
         ReviewDbBatchUpdate.execute(reviewDbUpdates, listener, requestId, dryRun);
       }
     }
+
+    private static void checkDifferentProject(Collection<BatchUpdate> updates) {
+      Multiset<Project.NameKey> projectCounts =
+          updates.stream().map(u -> u.project).collect(toImmutableMultiset());
+      checkArgument(
+          projectCounts.entrySet().size() == updates.size(),
+          "updates must all be for different projects, got: %s",
+          projectCounts);
+    }
   }
 
   static void setRequestIds(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java
index 3c1e896..9faf628 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.update;
 
 import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
@@ -31,10 +32,28 @@
   /**
    * Add a command to the pending list of commands.
    *
-   * <p>This method is the only way of updating refs in the repository from a {@link BatchUpdateOp}.
+   * <p>Adding commands to the {@code RepoContext} is the only way of updating refs in the
+   * repository from a {@link BatchUpdateOp}.
    *
    * @param cmd ref update command.
    * @throws IOException if an error occurred opening the repo.
    */
   void addRefUpdate(ReceiveCommand cmd) throws IOException;
+
+  /**
+   * Add a command to the pending list of commands.
+   *
+   * <p>Adding commands to the {@code RepoContext} is the only way of updating refs in the
+   * repository from a {@link BatchUpdateOp}.
+   *
+   * @param oldId the old object ID; must not be null. Use {@link ObjectId#zeroId()} for ref
+   *     creation.
+   * @param newId the new object ID; must not be null. Use {@link ObjectId#zeroId()} for ref
+   *     deletion.
+   * @param refName the ref name.
+   * @throws IOException if an error occurred opening the repo.
+   */
+  default void addRefUpdate(ObjectId oldId, ObjectId newId, String refName) throws IOException {
+    addRefUpdate(new ReceiveCommand(oldId, newId, refName));
+  }
 }
diff --git a/gerrit-server/src/main/java/gerrit/PRED__check_user_label_3.java b/gerrit-server/src/main/java/gerrit/PRED__check_user_label_3.java
index b2b9890..e84b3ac 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__check_user_label_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__check_user_label_3.java
@@ -21,7 +21,9 @@
 import com.google.gerrit.server.permissions.LabelPermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
 import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
+import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PInstantiationException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.exceptions.SystemException;
@@ -80,19 +82,20 @@
     }
     short val = (short) ((IntegerTerm) a3).intValue();
 
-    ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
-    LabelType type = cd.getLabelTypes().byLabel(label);
-    if (type == null) {
-      return engine.fail();
-    }
-
     try {
+      ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
+      LabelType type = cd.getLabelTypes().byLabel(label);
+      if (type == null) {
+        return engine.fail();
+      }
       StoredValues.PERMISSION_BACKEND
           .get(engine)
           .user(user)
           .change(cd)
           .check(new LabelPermission.WithValue(type, val));
       return cont;
+    } catch (OrmException err) {
+      throw new JavaException(this, 1, err);
     } catch (AuthException err) {
       return engine.fail();
     } catch (PermissionBackendException err) {
diff --git a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
index 5c61007..f7f39da 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
@@ -20,7 +20,9 @@
 import com.google.gerrit.server.permissions.LabelPermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
 import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
+import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PInstantiationException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.exceptions.SystemException;
@@ -74,15 +76,16 @@
     }
     CurrentUser user = (CurrentUser) ((JavaObjectTerm) a2).object();
 
-    ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
-    LabelType type = cd.getLabelTypes().byLabel(label);
-    if (type == null) {
-      return engine.fail();
-    }
-
     Set<LabelPermission.WithValue> can;
     try {
+      ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
+      LabelType type = cd.getLabelTypes().byLabel(label);
+      if (type == null) {
+        return engine.fail();
+      }
       can = StoredValues.PERMISSION_BACKEND.get(engine).user(user).change(cd).test(type);
+    } catch (OrmException err) {
+      throw new JavaException(this, 1, err);
     } catch (PermissionBackendException err) {
       SystemException se = new SystemException(err.getMessage());
       se.initCause(err);
diff --git a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
index 33d63c4..9bfcc61 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
@@ -17,6 +17,8 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.rules.StoredValues;
+import com.google.gwtorm.server.OrmException;
+import com.googlecode.prolog_cafe.exceptions.JavaException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.ListTerm;
@@ -51,7 +53,12 @@
   public Operation exec(Prolog engine) throws PrologException {
     engine.setB0();
     Term a1 = arg1.dereference();
-    List<LabelType> list = StoredValues.CHANGE_DATA.get(engine).getLabelTypes().getLabelTypes();
+    List<LabelType> list;
+    try {
+      list = StoredValues.CHANGE_DATA.get(engine).getLabelTypes().getLabelTypes();
+    } catch (OrmException err) {
+      throw new JavaException(this, 1, err);
+    }
     Term head = Prolog.Nil;
     for (int idx = list.size() - 1; 0 <= idx; idx--) {
       head = new ListTerm(export(list.get(idx)), head);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index 3dfcaec..40596e8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -54,7 +54,7 @@
   }
 
   @Override
-  protected void setUpEnvironment(PrologEnvironment env) {
+  protected void setUpEnvironment(PrologEnvironment env) throws Exception {
     LabelTypes labelTypes = new LabelTypes(Arrays.asList(Util.codeReview(), Util.verified()));
     ChangeData cd = EasyMock.createMock(ChangeData.class);
     expect(cd.getLabelTypes()).andStubReturn(labelTypes);
@@ -63,12 +63,12 @@
   }
 
   @Test
-  public void gerritCommon() {
+  public void gerritCommon() throws Exception {
     runPrologBasedTests();
   }
 
   @Test
-  public void reductionLimit() throws CompileException {
+  public void reductionLimit() throws Exception {
     PrologEnvironment env = envFactory.create(machine);
     setUpEnvironment(env);
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
index 6f6d189..7b2b388 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -84,7 +84,7 @@
    *
    * @param env Prolog environment.
    */
-  protected void setUpEnvironment(PrologEnvironment env) {}
+  protected void setUpEnvironment(PrologEnvironment env) throws Exception {}
 
   private PrologMachineCopy newMachine() {
     BufferingPrologControl ctl = new BufferingPrologControl();
@@ -115,7 +115,7 @@
     return env.execute(Prolog.BUILTIN, "clause", head, new VariableTerm());
   }
 
-  public void runPrologBasedTests() {
+  public void runPrologBasedTests() throws Exception {
     int errors = 0;
     long start = TimeUtil.nowMs();
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java
index 5e82836..892a9ff 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -39,7 +39,6 @@
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.transport.ReceiveCommand;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -122,9 +121,7 @@
           new RepoOnlyOp() {
             @Override
             public void updateRepo(RepoContext ctx) throws Exception {
-              ctx.addRefUpdate(
-                  new ReceiveCommand(
-                      masterCommit.getId(), branchCommit.getId(), "refs/heads/master"));
+              ctx.addRefUpdate(masterCommit.getId(), branchCommit.getId(), "refs/heads/master");
             }
           });
       bu.execute();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index 1192eb5..2e5bf71 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -24,7 +24,7 @@
 import org.kohsuke.args4j.Option;
 
 @CommandMetaData(name = "query", description = "Query the change database")
-class Query extends SshCommand {
+public class Query extends SshCommand {
   @Inject private OutputStreamQuery processor;
 
   @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 7b5acd8..38eb791 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,6 +1,6 @@
 load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "MAVEN_CENTRAL", "maven_jar")
 
-_JGIT_VERS = "4.7.0.201704051617-r.15-gc4e952109"
+_JGIT_VERS = "4.7.0.201704051617-r.37-gc80d8c590"
 
 _DOC_VERS = "4.7.0.201704051617-r" # Set to _JGIT_VERS unless using a snapshot
 
@@ -26,28 +26,28 @@
         name = "jgit_lib",
         artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "875277521153030e2bdab12bf602b740232b2b28",
-        src_sha1 = "f9adcd3ef0f77c5db16569771f95bc0142c36f46",
+        sha1 = "edb739cd1e7c72dab361a8f6011807ae7fae35e2",
+        src_sha1 = "ddf922143dd88ec8fbd2c44f48f203340e6b4d54",
         unsign = True,
     )
     maven_jar(
         name = "jgit_servlet",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "e1037f50696a6e19fb5d30f9d44cb31e3c5fe8b0",
+        sha1 = "e864cb9f7e16d77ff75805708cd82e6f82a73246",
         unsign = True,
     )
     maven_jar(
         name = "jgit_archive",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "660bc82c9ff3c33249d269860d9793e830d6c374",
+        sha1 = "cc944356eb8ca74446341729d539f5b9faccb698",
     )
     maven_jar(
         name = "jgit_junit",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "c2b28646cc2531df947a9e0f73fa9c415567b05e",
+        sha1 = "eae23cc952d8b9d332287f7a4d4200c17ae78411",
         unsign = True,
     )
 
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
index 1e277bc..0f38f36 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
@@ -23,6 +23,7 @@
 
 <link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <script>
+  /** @type {String} */
   window.CANONICAL_PATH = '/r';
 </script>
 <link rel="import" href="base-url-behavior.html">
@@ -72,4 +73,4 @@
       );
     });
   });
-</script>
\ No newline at end of file
+</script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index e4c4d11..8eb63a4 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -20,7 +20,10 @@
   var TooltipBehavior = {
 
     properties: {
-      hasTooltip: Boolean,
+      hasTooltip: {
+        type: Boolean,
+        observer: '_setupTooltipListeners',
+      },
 
       _isTouchDevice: {
         type: Boolean,
@@ -30,16 +33,10 @@
       },
       _tooltip: Element,
       _titleText: String,
-    },
-
-    attached: function() {
-      if (!this.hasTooltip) { return; }
-
-      this.addEventListener('mouseenter', this._handleShowTooltip.bind(this));
-      this.addEventListener('mouseleave', this._handleHideTooltip.bind(this));
-      this.addEventListener('tap', this._handleHideTooltip.bind(this));
-
-      this.listen(window, 'scroll', '_handleWindowScroll');
+      _hasSetupTooltipListeners: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     detached: function() {
@@ -47,6 +44,16 @@
       this.unlisten(window, 'scroll', '_handleWindowScroll');
     },
 
+    _setupTooltipListeners: function() {
+      if (this._hasSetupTooltipListeners || !this.hasTooltip) { return; }
+      this._hasSetupTooltipListeners = true;
+
+      this.addEventListener('mouseenter', this._handleShowTooltip.bind(this));
+      this.addEventListener('mouseleave', this._handleHideTooltip.bind(this));
+      this.addEventListener('tap', this._handleHideTooltip.bind(this));
+      this.listen(window, 'scroll', '_handleWindowScroll');
+    },
+
     _handleShowTooltip: function(e) {
       if (this._isTouchDevice) { return; }
 
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
index 2d7b6a2..580bb4b 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
@@ -23,6 +23,8 @@
 <link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-tooltip-behavior.html">
 
+<script>void(0);</script>
+
 <test-fixture id="basic">
   <template>
     <tooltip-behavior-element></tooltip-behavior-element>
@@ -116,5 +118,11 @@
       flushAsynchronousOperations();
       assert.isTrue(element._handleHideTooltip.called);
     });
+
+    test('sets up listeners when has-tooltip is changed', function() {
+      var addListenerStub = sandbox.stub(element, 'addEventListener');
+      element.hasTooltip = true;
+      assert.isTrue(addListenerStub.called);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
index 3d99cec..6003bc4 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
@@ -32,6 +32,13 @@
       return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
     },
 
+    isModifierPressed: function(e, modifier) {
+      e = getKeyboardEvent(e);
+      // When e is a keyboardEvent, e.event is not null.
+      if (e.event) { e = e.event; }
+      return e[modifier];
+    },
+
     shouldSuppressKeyboardShortcut: function(e) {
       e = getKeyboardEvent(e);
       if (e.path[0].tagName === 'INPUT' || e.path[0].tagName === 'TEXTAREA') {
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
index a72eb75..840998c 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -127,5 +127,26 @@
       MockInteractions.keyDownOn(element, 75, 'alt', 'k');
       assert.isTrue(spy.lastCall.returnValue);
     });
+
+    test('isModifierPressed returns accurate value', function() {
+      var spy = sandbox.spy(element, 'isModifierPressed');
+      element._handleKey = function(e) {
+        element.isModifierPressed(e, 'shiftKey');
+      };
+      MockInteractions.keyDownOn(element, 75, 'shift', 'k');
+      assert.isTrue(spy.lastCall.returnValue);
+      MockInteractions.keyDownOn(element, 75, null, 'k');
+      assert.isFalse(spy.lastCall.returnValue);
+      MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
+      assert.isFalse(spy.lastCall.returnValue);
+      MockInteractions.keyDownOn(element, 75, null, 'k');
+      assert.isFalse(spy.lastCall.returnValue);
+      MockInteractions.keyDownOn(element, 75, 'meta', 'k');
+      assert.isFalse(spy.lastCall.returnValue);
+      MockInteractions.keyDownOn(element, 75, null, 'k');
+      assert.isFalse(spy.lastCall.returnValue);
+      MockInteractions.keyDownOn(element, 75, 'alt', 'k');
+      assert.isFalse(spy.lastCall.returnValue);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
index f71fe8f..e862cba 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
@@ -100,7 +100,7 @@
     },
 
     changeBaseURL: function(changeNum, patchNum) {
-      var v =  this.getBaseUrl() + '/changes/' + changeNum;
+      var v = this.getBaseUrl() + '/changes/' + changeNum;
       if (patchNum) {
         v += '/revisions/' + patchNum;
       }
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
index 2b3e858..f3c44eb 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -21,6 +21,7 @@
 <script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../bower_components/web-component-tester/browser.js"></script>
 <script>
+  /** @type {String} */
   window.CANONICAL_PATH = '/r';
 </script>
 
@@ -74,4 +75,4 @@
       assert.deepEqual(element.changePath('1'), '/r/c/1');
     });
   });
-</script>
\ No newline at end of file
+</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index 057dd1d..afe0e38 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index b2e8051..82d85ac 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -28,7 +28,11 @@
      * @event title-change
      */
 
-    behaviors: [Gerrit.URLEncodingBehavior],
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+      Gerrit.URLEncodingBehavior,
+    ],
+
     properties: {
       /**
        * URL params passed from the router.
@@ -140,7 +144,7 @@
       offset = +(offset || 0);
       var newOffset = Math.max(0, offset + (changesPerPage * direction));
       // Double encode URI component.
-      var href = '/q/' + this.encodeURL(query, false);
+      var href = this.getBaseUrl() + '/q/' + this.encodeURL(query, false);
       if (newOffset > 0) {
         href += ',' + newOffset;
       }
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
index 0097a50..661dd2c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -89,6 +89,26 @@
           '/q/status:open,10');
     });
 
+    test('_computeNavLink with path', function() {
+      window.CANONICAL_PATH = '/r';
+      var query = 'status:open';
+      var offset = 0;
+      var direction = 1;
+      var changesPerPage = 5;
+      assert.equal(
+          element._computeNavLink(query, offset, direction, changesPerPage),
+          '/r/q/status:open,5');
+      direction = -1;
+      assert.equal(
+          element._computeNavLink(query, offset, direction, changesPerPage),
+          '/r/q/status:open');
+      offset = 5;
+      direction = 1;
+      assert.equal(
+          element._computeNavLink(query, offset, direction, changesPerPage),
+          '/r/q/status:open,10');
+    });
+
     test('_hidePrevArrow', function() {
       var offset = 0;
       assert.isTrue(element._hidePrevArrow(offset));
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index cec9487..cbe5dad 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -465,7 +465,8 @@
             project-config="[[_projectConfig]]"
             selected-index="{{viewState.selectedFileIndex}}"
             diff-view-mode="{{viewState.diffMode}}"
-            num-files-shown="{{_numFilesShown}}"></gr-file-list>
+            num-files-shown="{{_numFilesShown}}"
+            file-list-increment="{{_numFilesShown}}"></gr-file-list>
       </section>
       <gr-messages-list id="messageList"
           change-num="[[_changeNum]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 8ce7a83..e576a71 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -22,7 +22,7 @@
   var COMMENT_SAVE = 'Saving... Try again after all comments are saved.';
 
   var MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
-  var DEFAULT_NUM_FILES_SHOWN = 75;
+  var DEFAULT_NUM_FILES_SHOWN = 200;
 
   // Maximum length for patch set descriptions.
   var PATCH_DESC_MAX_LENGTH = 500;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 3534808..5b27b72 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -374,8 +374,8 @@
       };
       element.viewState.changeNum = null;
       element.viewState.diffMode = 'UNIFIED';
-      assert.equal(element.viewState.numFilesShown, 75);
-      assert.equal(element._numFilesShown, 75);
+      assert.equal(element.viewState.numFilesShown, 200);
+      assert.equal(element._numFilesShown, 200);
       element._numFilesShown = 150;
       flushAsynchronousOperations();
       assert.equal(element.viewState.diffMode, 'UNIFIED');
@@ -394,8 +394,8 @@
       flushAsynchronousOperations();
       assert.isNull(element.viewState.diffMode);
       assert.equal(element.viewState.changeNum, '2');
-      assert.equal(element.viewState.numFilesShown, 75);
-      assert.equal(element._numFilesShown, 75);
+      assert.equal(element.viewState.numFilesShown, 200);
+      assert.equal(element._numFilesShown, 200);
     });
 
     test('patch num change', function(done) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index b20c82c..f844cfd 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -27,6 +27,7 @@
 <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-select/gr-select.html">
+<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
 
 <dom-module id="gr-file-list">
   <template>
@@ -326,6 +327,7 @@
               path="[[file.__path]]"
               prefs="[[_diffPrefs]]"
               project-config="[[projectConfig]]"
+              on-line-selected="_onLineSelected"
               view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff>
         </template>
       </template>
@@ -367,13 +369,18 @@
         link on-tap="_incrementNumFilesShown">
       [[_computeIncrementText(numFilesShown, _files)]]
     </gr-button>
-    <gr-button
-        class="fileListButton"
-        id="showAllButton"
-        hidden$="[[_computeFileListButtonHidden(numFilesShown, _files)]]"
-        link on-tap="_showAllFiles">
-      [[_computeShowAllText(_files)]]
-    </gr-button>
+    <gr-tooltip-content
+        has-tooltip="[[_computeWarnShowAll(_files)]]"
+        show-icon="[[_computeWarnShowAll(_files)]]"
+        title$="[[_computeShowAllWarning(_files)]]">
+      <gr-button
+          class="fileListButton"
+          id="showAllButton"
+          hidden$="[[_computeFileListButtonHidden(numFilesShown, _files)]]"
+          link on-tap="_showAllFiles">
+        [[_computeShowAllText(_files)]]
+      </gr-button><!--
+ --></gr-tooltip-content>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
     <gr-diff-cursor id="diffCursor"></gr-diff-cursor>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 73dd969..d6eb7be 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -16,7 +16,7 @@
 
   // Maximum length for patch set descriptions.
   var PATCH_DESC_MAX_LENGTH = 500;
-
+  var WARN_SHOW_ALL_THRESHOLD = 1000;
   var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
 
   var FileStatus = {
@@ -80,11 +80,7 @@
         type: Object,
         computed: '_calculatePatchChange(_files)',
       },
-      _fileListIncrement: {
-        type: Number,
-        readOnly: true,
-        value: 75,
-      },
+      fileListIncrement: Number,
       _hideChangeTotals: {
         type: Boolean,
         computed: '_shouldHideChangeTotals(_patchChange)',
@@ -528,11 +524,13 @@
 
     _handleNKey: function(e) {
       if (this.shouldSuppressKeyboardShortcut(e) ||
-          this.modifierPressed(e)) { return; }
+          this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
+        return;
+      }
       if (!this._showInlineDiffs) { return; }
 
       e.preventDefault();
-      if (e.shiftKey) {
+      if (this.isModifierPressed(e, 'shiftKey')) {
         this.$.diffCursor.moveToNextCommentThread();
       } else {
         this.$.diffCursor.moveToNextChunk();
@@ -541,11 +539,13 @@
 
     _handlePKey: function(e) {
       if (this.shouldSuppressKeyboardShortcut(e) ||
-          this.modifierPressed(e)) { return; }
+          this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
+        return;
+      }
       if (!this._showInlineDiffs) { return; }
 
       e.preventDefault();
-      if (e.shiftKey) {
+      if (this.isModifierPressed(e, 'shiftKey')) {
         this.$.diffCursor.moveToPreviousCommentThread();
       } else {
         this.$.diffCursor.moveToPreviousChunk();
@@ -687,22 +687,23 @@
       }
     },
 
-    _filesChanged: function() {
-      this.async(function() {
-        var diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff');
+    _updateDiffCursor: function() {
+      var diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff');
 
-        // Overwrite the cursor's list of diffs:
-        this.$.diffCursor.splice.apply(this.$.diffCursor,
-            ['diffs', 0, this.$.diffCursor.diffs.length].concat(diffElements));
+      // Overwrite the cursor's list of diffs:
+      this.$.diffCursor.splice.apply(this.$.diffCursor,
+          ['diffs', 0, this.$.diffCursor.diffs.length].concat(diffElements));
+    },
 
-        var files = Polymer.dom(this.root).querySelectorAll('.file-row');
-        this.$.fileCursor.stops = files;
-        this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
-      }.bind(this), 1);
+    _filesChanged: function(files) {
+      Polymer.dom.flush();
+      var files = Polymer.dom(this.root).querySelectorAll('.file-row');
+      this.$.fileCursor.stops = files;
+      this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
     },
 
     _incrementNumFilesShown: function() {
-      this.numFilesShown += this._fileListIncrement;
+      this.numFilesShown += this.fileListIncrement;
     },
 
     _computeFileListButtonHidden: function(numFilesShown, files) {
@@ -712,7 +713,7 @@
     _computeIncrementText: function(numFilesShown, files) {
       if (!files) { return ''; }
       var text =
-          Math.min(this._fileListIncrement, files.length - numFilesShown);
+          Math.min(this.fileListIncrement, files.length - numFilesShown);
       return 'Show ' + text + ' more';
     },
 
@@ -721,6 +722,16 @@
       return 'Show all ' + files.length + ' files';
     },
 
+    _computeWarnShowAll: function(files) {
+      return files.length > WARN_SHOW_ALL_THRESHOLD;
+    },
+
+    _computeShowAllWarning: function(files) {
+      if (!this._computeWarnShowAll(files)) { return ''; }
+      return 'Warning: showing all ' + files.length +
+          ' files may take several seconds.';
+    },
+
     _showAllFiles: function() {
       this.numFilesShown = this._files.length;
     },
@@ -770,6 +781,11 @@
       return expandedFilesRecord.base.indexOf(path) !== -1;
     },
 
+    _onLineSelected: function(e, detail) {
+      this.$.diffCursor.moveToLineNumber(detail.number, detail.side,
+          detail.path);
+    },
+
     /**
      * Handle splices to the list of expanded file paths. If there are any new
      * entries in the expanded list, then render each diff corresponding in
@@ -799,6 +815,8 @@
             this.$.reporting.timeEnd(timerName);
             this.$.diffCursor.handleDiffUpdate();
           }.bind(this));
+      this._updateDiffCursor();
+      this.$.diffCursor.handleDiffUpdate();
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index c006f68..1b5a4e4 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -23,6 +23,7 @@
 <script src="../../../bower_components/page/page.js"></script>
 <script src="../../../scripts/util.js"></script>
 
+<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-file-list.html">
 
@@ -60,7 +61,7 @@
         reload: function() { return Promise.resolve(); },
       });
       element = fixture('basic');
-      element.numFilesShown = 75;
+      element.numFilesShown = 200;
       saveStub = sandbox.stub(element, '_saveReviewedState',
           function() { return Promise.resolve(); });
     });
@@ -69,6 +70,15 @@
       sandbox.restore();
     });
 
+    test('correct number of files are shown', function() {
+      element._files = _.times(500, function(i) {
+          return {__path: '/file' + i, lines_inserted: 9}; });
+      flushAsynchronousOperations();
+      assert.equal(
+          Polymer.dom(element.root).querySelectorAll('.file-row').length,
+          element.numFilesShown);
+    });
+
     test('get file list', function(done) {
       var getChangeFilesStub = sandbox.stub(element.$.restAPI, 'getChangeFiles',
           function() {
@@ -927,4 +937,236 @@
         });
     });
   });
+
+  suite('gr-file-list inline diff tests', function() {
+    var element;
+    var sandbox;
+
+    var setupDiff = function(diff) {
+      var mock = document.createElement('mock-diff-response');
+      diff._diff = mock.diffResponse;
+      diff._comments = {
+        left: [],
+        right: [],
+      };
+      diff.prefs = {
+        context: 10,
+        tab_size: 8,
+        font_size: 12,
+        line_length: 100,
+        cursor_blink_rate: 0,
+        line_wrapping: false,
+        intraline_difference: true,
+        show_line_endings: true,
+        show_tabs: true,
+        show_whitespace_errors: true,
+        syntax_highlighting: true,
+        auto_hide_diff_table_header: true,
+        theme: 'DEFAULT',
+        ignore_whitespace: 'IGNORE_NONE',
+      };
+      diff._renderDiffTable();
+    };
+
+    var renderAndGetNewDiffs = function(index) {
+      var diffs =
+          Polymer.dom(element.root).querySelectorAll('gr-diff');
+
+      for (var i = index; i < diffs.length; i++) {
+        setupDiff(diffs[i]);
+      }
+
+      element._updateDiffCursor();
+      element.$.diffCursor.handleDiffUpdate();
+      return diffs;
+    };
+
+    setup(function() {
+      sandbox = sinon.sandbox.create();
+      stub('gr-rest-api-interface', {
+        getLoggedIn: function() { return Promise.resolve(true); },
+        getPreferences: function() { return Promise.resolve({}); },
+      });
+      stub('gr-date-formatter', {
+        _loadTimeFormat: function() { return Promise.resolve(''); },
+      });
+      stub('gr-diff', {
+        reload: function() { return Promise.resolve(); },
+      });
+      element = fixture('basic');
+      element.numFilesShown = 75;
+      element.selectedIndex = 0;
+      element._files = [
+        {__path: '/COMMIT_MSG', lines_inserted: 9},
+        {
+          __path: 'file_added_in_rev2.txt',
+          lines_inserted: 1,
+          lines_deleted: 1,
+          size_delta: 10,
+          size: 100,
+        },
+        {
+          __path: 'myfile.txt',
+          lines_inserted: 1,
+          lines_deleted: 1,
+          size_delta: 10,
+          size: 100,
+        },
+      ];
+      element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+      element._loggedIn = true;
+      element.changeNum = '42';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '2',
+      };
+      sandbox.stub(window, 'fetch', function() {
+        return Promise.resolve();
+      });
+      flushAsynchronousOperations();
+    });
+
+    teardown(function() {
+      sandbox.restore();
+    });
+
+    test('cursor with individually opened files', function() {
+      MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
+      flushAsynchronousOperations();
+      var diffs = renderAndGetNewDiffs(0);
+      var diffStops = diffs[0].getCursorStops();
+
+      // 1 diff should be rendered.
+      assert.equal(diffs.length, 1);
+
+      // No line number is selected.
+      assert.isFalse(diffStops[10].classList.contains('target-row'));
+
+      // Tapping content on a line selects the line number.
+      MockInteractions.tap(Polymer.dom(
+          diffStops[10]).querySelectorAll('.contentText')[0]);
+      flushAsynchronousOperations();
+      assert.isTrue(diffStops[10].classList.contains('target-row'));
+
+      // Keyboard shortcuts are still moving the file cursor, not the diff
+      // cursor.
+      MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+      flushAsynchronousOperations();
+      assert.isTrue(diffStops[10].classList.contains('target-row'));
+      assert.isFalse(diffStops[11].classList.contains('target-row'));
+
+      // The file cusor is now at 1.
+      assert.equal(element.$.fileCursor.index, 1);
+      MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
+      flushAsynchronousOperations();
+
+      diffs = renderAndGetNewDiffs(1);
+      // Two diffs should be rendered.
+      assert.equal(diffs.length, 2);
+      var diffStopsFirst = diffs[0].getCursorStops();
+      var diffStopsSecond = diffs[1].getCursorStops();
+
+      // The line on the first diff is stil selected
+      assert.isTrue(diffStopsFirst[10].classList.contains('target-row'));
+      assert.isFalse(diffStopsSecond[10].classList.contains('target-row'));
+    });
+
+    test('cursor with toggle all files', function() {
+      MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
+      flushAsynchronousOperations();
+
+      var diffs = renderAndGetNewDiffs(0);
+      var diffStops = diffs[0].getCursorStops();
+
+      // 1 diff should be rendered.
+      assert.equal(diffs.length, 3);
+
+      // No line number is selected.
+      assert.isFalse(diffStops[10].classList.contains('target-row'));
+
+      // Tapping content on a line selects the line number.
+      MockInteractions.tap(Polymer.dom(
+          diffStops[10]).querySelectorAll('.contentText')[0]);
+      flushAsynchronousOperations();
+      assert.isTrue(diffStops[10].classList.contains('target-row'));
+
+      // Keyboard shortcuts are still moving the file cursor, not the diff
+      // cursor.
+      MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+      flushAsynchronousOperations();
+      assert.isFalse(diffStops[10].classList.contains('target-row'));
+      assert.isTrue(diffStops[11].classList.contains('target-row'));
+
+      // The file cusor is still at 0.
+      assert.equal(element.$.fileCursor.index, 0);
+    });
+
+    suite('n key presses', function() {
+      var nKeySpy;
+      var nextCommentStub;
+      var nextChunkStub;
+      var fileRows;
+      setup(function() {
+        nKeySpy = sandbox.spy(element, '_handleNKey');
+        nextCommentStub = sandbox.stub(element.$.diffCursor,
+            'moveToNextCommentThread');
+        nextChunkStub = sandbox.stub(element.$.diffCursor,
+            'moveToNextChunk');
+        fileRows =
+            Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
+      });
+      test('n key with all files expanded and no shift key', function() {
+        MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i');
+        flushAsynchronousOperations();
+
+        // Handle N key should return before calling diff cursor functions.
+        MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+        assert.isTrue(nKeySpy.called);
+        assert.isFalse(nextCommentStub.called);
+
+        // This is also called in diffCursor.moveToFirstChunk.
+        assert.equal(nextChunkStub.callCount, 1);
+        assert.isFalse(!!element._showInlineDiffs);
+      });
+
+      test('n key with all files expanded and shift key', function() {
+        MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i');
+        flushAsynchronousOperations();
+
+        MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
+        assert.isTrue(nKeySpy.called);
+        assert.isFalse(nextCommentStub.called);
+
+        // This is also called in diffCursor.moveToFirstChunk.
+        assert.equal(nextChunkStub.callCount, 1);
+        assert.isFalse(!!element._showInlineDiffs);
+      });
+
+      test('n key without all files expanded and shift key', function() {
+        MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, 'shift', 'i');
+        flushAsynchronousOperations();
+
+        MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+        assert.isTrue(nKeySpy.called);
+        assert.isFalse(nextCommentStub.called);
+
+        // This is also called in diffCursor.moveToFirstChunk.
+        assert.equal(nextChunkStub.callCount, 2);
+        assert.isTrue(element._showInlineDiffs);
+      });
+
+      test('n key without all files expanded and no shift key', function() {
+        MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, 'shift', 'i');
+        flushAsynchronousOperations();
+
+        MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
+        assert.isTrue(nKeySpy.called);
+        assert.isTrue(nextCommentStub.called);
+
+        // This is also called in diffCursor.moveToFirstChunk.
+        assert.equal(nextChunkStub.callCount, 1);
+        assert.isTrue(element._showInlineDiffs);
+      });
+    });
+  });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
index 2d7d2a9..5765411 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-alert/gr-alert.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 1e609c9..22349de 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -25,6 +25,10 @@
   Polymer({
     is: 'gr-error-manager',
 
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+    ],
+
     properties: {
       /**
        * The ID of the account that was logged in when the app was launched. If
@@ -207,7 +211,8 @@
         'left=' + left,
         'top=' + top,
       ];
-      window.open('/login/%3FcloseAfterLogin', '_blank', options.join(','));
+      window.open(this.getBaseUrl() +
+          '/login/%3FcloseAfterLogin', '_blank', options.join(','));
       this.listen(window, 'focus', '_handleWindowFocus');
     },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index 368c613..952dc67 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -17,6 +17,8 @@
   // Prevent redefinition.
   if (window.GrDiffBuilderImage) { return; }
 
+  var IMAGE_MIME_PATTERN = /^image\/(bmp|gif|jpeg|jpg|png|tiff|webp)$/;
+
   function GrDiffBuilderImage(diff, comments, prefs, outputEl, baseImage,
       revisionImage) {
     GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl, []);
@@ -53,7 +55,7 @@
   GrDiffBuilderImage.prototype._createImageCell =
       function(image, className, section) {
     var td = this._createElement('td', className);
-    if (image) {
+    if (image && IMAGE_MIME_PATTERN.test(image.type)) {
       var imageEl = this._createElement('img');
       imageEl.onload = function() {
         image._height = imageEl.naturalHeight;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index e40ccf3..abc7db3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -149,8 +149,8 @@
       this._fixSide();
     },
 
-    moveToLineNumber: function(number, side) {
-      var row = this._findRowByNumber(number, side);
+    moveToLineNumber: function(number, side, opt_path) {
+      var row = this._findRowByNumberAndFile(number, side, opt_path);
       if (row) {
         this.side = side;
         this.$.cursorManager.setCursor(row);
@@ -376,8 +376,16 @@
       }
     },
 
-    _findRowByNumber: function(targetNumber, side) {
-      var stops = this.$.cursorManager.stops;
+    _findRowByNumberAndFile: function(targetNumber, side, opt_path) {
+      var stops;
+      if (opt_path) {
+        var diff = this.diffs.filter(function(diff) {
+          return diff.path === opt_path;
+        })[0];
+        stops = diff.getCursorStops();
+      } else {
+        stops = this.$.cursorManager.stops;
+      }
       var selector;
       for (var i = 0; i < stops.length; i++) {
         selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index a77c617..16acc03 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -266,13 +266,13 @@
       assert.equal(cursorElement.getAddress(), '');
     });
 
-    test('_findRowByNumber', function() {
+    test('_findRowByNumberAndFile', function() {
       // Get the first ab row after the first chunk.
       var row = Polymer.dom(diffElement.root).querySelectorAll('tr')[8];
 
       // It should be line 8 on the right, but line 5 on the left.
-      assert.equal(cursorElement._findRowByNumber(8, 'right'), row);
-      assert.equal(cursorElement._findRowByNumber(5, 'left'), row);
+      assert.equal(cursorElement._findRowByNumberAndFile(8, 'right'), row);
+      assert.equal(cursorElement._findRowByNumberAndFile(5, 'left'), row);
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 1bf6268..159f27c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -200,6 +200,7 @@
               id="diffBuilder"
               comments="[[_comments]]"
               diff="[[_diff]]"
+              diff-path="[[path]]"
               view-mode="[[viewMode]]"
               line-wrapping="[[lineWrapping]]"
               is-image-diff="[[isImageDiff]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 155ed5a..cd1931a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -232,6 +232,7 @@
       this.fire('line-selected', {
         side: el.classList.contains('left') ? DiffSide.LEFT : DiffSide.RIGHT,
         number: el.getAttribute('data-value'),
+        path: this.path,
       });
     },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index f010add..c267eb0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -577,6 +577,43 @@
             element.reload();
           });
         });
+
+        test('does not render disallowed image type', function(done) {
+          var mockDiff = {
+            meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
+                lines: 560},
+            intraline_status: 'OK',
+            change_type: 'DELETED',
+            diff_header: [
+              'diff --git a/carrot.jpg b/carrot.jpg',
+              'index f9c2f2c..0000000 100644',
+              '--- a/carrot.jpg',
+              '+++ /dev/null',
+              'Binary files differ',
+            ],
+            content: [{skip: 66}],
+            binary: true,
+          };
+          mockFile1.type = 'image/jpeg-evil';
+
+          stubs.push(sandbox.stub(element, '_getDiff',
+              function() { return Promise.resolve(mockDiff); }));
+
+          element.addEventListener('render', function() {
+            // Recognizes that it should be an image diff.
+            assert.isTrue(element.isImageDiff);
+            assert.instanceOf(
+                element.$.diffBuilder._builder, GrDiffBuilderImage);
+            var leftImage = element.$.diffTable.querySelector('td.left img');
+            assert.isNotOk(leftImage);
+            done();
+          });
+
+          element.$.restAPI.getDiffPreferences().then(function(prefs) {
+            element.prefs = prefs;
+            element.reload();
+          });
+        });
       });
 
       test('_handleTap lineNum', function(done) {
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index acf38c7..90f641d 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -37,12 +37,6 @@
         value: function() { return document.body; },
       },
 
-      /**
-       * The path component of the canonicalWebURL. If Gerrit is running from
-       * the root of the domain, this should be empty.
-       */
-      canonicalPath: String,
-
       _account: {
         type: Object,
         observer: '_accountChanged',
@@ -80,8 +74,6 @@
     },
 
     ready: function() {
-      Gerrit.CANONICAL_PATH = this.canonicalPath;
-
       this.$.router.start();
 
       this.$.restAPI.getAccount().then(function(account) {
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 2eb835d..35b5ab1 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -27,7 +27,7 @@
 
 <test-fixture id="basic">
   <template>
-    <gr-app id="app" canonical-path="/abc/def/ghi"></gr-app>
+    <gr-app id="app"></gr-app>
   </template>
 </test-fixture>
 
@@ -72,10 +72,8 @@
       element._path = '/test/path';
       flush(function() {
         var gwtLink = element.$$('#gwtLink');
-        assert.equal(
-          gwtLink.href,
-          'http://' + location.host + element.getBaseUrl() + '/?polygerrit=0#/test/path'
-        );
+        assert.equal(gwtLink.href, 'http://' + location.host +
+            element.getBaseUrl() + '/?polygerrit=0#/test/path');
         done();
       });
     });
@@ -107,9 +105,5 @@
         done();
       });
     });
-
-    test('canonical-path', function() {
-      assert.equal(Gerrit.CANONICAL_PATH, '/abc/def/ghi');
-    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 2adfb91..d7406db 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -184,7 +184,7 @@
         }
         this._suggestions = suggestions;
         Polymer.dom.flush();
-        this._suggestionEls = this.$.suggestions.querySelectorAll('li')
+        this._suggestionEls = this.$.suggestions.querySelectorAll('li');
         this.$.cursor.moveToStart();
         if (this._index === -1) {
           this.value = null;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 1537105..95fa2a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -963,7 +963,8 @@
     },
 
     _fetchB64File: function(url) {
-      return fetch(this.getBaseUrl() + url, {credentials: 'same-origin'}).then(function(response) {
+      return fetch(this.getBaseUrl() + url, {credentials: 'same-origin'}).then(
+            function(response) {
         var type = response.headers.get('X-FYI-Content-Type');
         return response.text()
           .then(function(text) {
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
index 81e65e3..58f8e39 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
@@ -19,8 +19,8 @@
 
 <dom-module id="gr-tooltip-content">
   <template>
-    <content></content>
-    <span class="arrow" hidden$="[[!showIcon]]">&#9432;</span>
+    <content></content><!--
+ --><span class="arrow" hidden$="[[!showIcon]]">&#9432;</span>
   </template>
   <script src="gr-tooltip-content.js"></script>
 </dom-module>